From 341a760f027967ad980a4446c6a90add5de04302 Mon Sep 17 00:00:00 2001 From: Mohammed Sohail Date: Wed, 8 Mar 2023 06:49:09 +0000 Subject: [PATCH] feat: (wip) add account activation and gas quota lock * This is a crude lock that restricts each account to the set gas quota. --- cmd/service/custodial.go | 1 - cmd/service/main.go | 1 + cmd/service/tasker.go | 1 + config.toml | 1 - internal/api/sign.go | 20 ++++++ internal/custodial/custodial.go | 3 +- internal/queries/queries.go | 13 ++-- internal/store/account.go | 66 +++++++++++++++++++ internal/store/dispatch.go | 18 ----- internal/store/otx.go | 14 +++- internal/store/store.go | 5 ++ internal/sub/handler.go | 4 +- internal/sub/util.go | 31 +++++++++ internal/tasker/client.go | 13 ++-- internal/tasker/task/account_activate.go | 45 +++++++++++++ internal/tasker/task/account_gift_gas.go | 20 +++++- internal/tasker/task/account_gift_voucher.go | 2 +- internal/tasker/task/account_refill_gas.go | 32 ++++++--- .../tasker/task/account_register_onchain.go | 2 +- internal/tasker/task/sign_transfer.go | 5 +- internal/tasker/task/utils.go | 5 ++ internal/tasker/types.go | 1 + migrations/001_keystore.sql | 5 +- migrations/002_custodial_db.sql | 7 +- migrations/003_gas_quota.sql | 28 ++++++++ queries.sql | 43 +++++++++++- 26 files changed, 334 insertions(+), 52 deletions(-) create mode 100644 internal/store/account.go delete mode 100644 internal/store/dispatch.go create mode 100644 internal/sub/util.go create mode 100644 internal/tasker/task/account_activate.go create mode 100644 internal/tasker/task/utils.go create mode 100644 migrations/003_gas_quota.sql diff --git a/cmd/service/custodial.go b/cmd/service/custodial.go index c14ee8f..a6e3b05 100644 --- a/cmd/service/custodial.go +++ b/cmd/service/custodial.go @@ -45,7 +45,6 @@ func initSystemContainer(ctx context.Context, noncestore nonce.Noncestore) *cust GiftableGasValue: big.NewInt(ko.MustInt64("system.giftable_gas_value")), GiftableToken: w3.A(ko.MustString("system.giftable_token_address")), GiftableTokenValue: big.NewInt(ko.MustInt64("system.giftable_token_value")), - LockPrefix: ko.MustString("system.lock_prefix"), LockTimeout: 1 * time.Second, PublicKey: ko.MustString("system.public_key"), TokenDecimals: ko.MustInt("system.token_decimals"), diff --git a/cmd/service/main.go b/cmd/service/main.go index bdadbab..dfa83f2 100644 --- a/cmd/service/main.go +++ b/cmd/service/main.go @@ -67,6 +67,7 @@ func main() { Noncestore: redisNoncestore, PgStore: pgStore, Pub: jsPub, + RedisClient: redisPool.Client, SystemContainer: systemContainer, TaskerClient: taskerClient, } diff --git a/cmd/service/tasker.go b/cmd/service/tasker.go index 0e69a10..8bdc4bc 100644 --- a/cmd/service/tasker.go +++ b/cmd/service/tasker.go @@ -39,6 +39,7 @@ func initTasker(custodialContainer *custodial.Custodial, redisPool *redis.RedisP taskerServer.RegisterHandlers(tasker.AccountRegisterTask, task.AccountRegisterOnChainProcessor(custodialContainer)) taskerServer.RegisterHandlers(tasker.AccountGiftGasTask, task.AccountGiftGasProcessor(custodialContainer)) taskerServer.RegisterHandlers(tasker.AccountGiftVoucherTask, task.GiftVoucherProcessor(custodialContainer)) + taskerServer.RegisterHandlers(tasker.AccountActivateTask, task.AccountActivateProcessor(custodialContainer)) taskerServer.RegisterHandlers(tasker.AccountRefillGasTask, task.AccountRefillGasProcessor(custodialContainer)) taskerServer.RegisterHandlers(tasker.SignTransferTask, task.SignTransfer(custodialContainer)) taskerServer.RegisterHandlers(tasker.DispatchTxTask, task.DispatchTx(custodialContainer)) diff --git a/config.toml b/config.toml index 41655c6..ab7b687 100644 --- a/config.toml +++ b/config.toml @@ -17,7 +17,6 @@ devnet = false # All addresses MUST be checksumed account_index_address = "" -lock_prefix = "lock:" gas_faucet_address = "" gas_refill_threshold = 2500000000000000 gas_refill_value = 10000000000000000 diff --git a/internal/api/sign.go b/internal/api/sign.go index 7f12b0a..3361660 100644 --- a/internal/api/sign.go +++ b/internal/api/sign.go @@ -40,6 +40,21 @@ func HandleSignTransfer(c echo.Context) error { return err } + accountActive, gasQuota, err := cu.PgStore.GetAccountStatusByAddress(c.Request().Context(), req.From) + if !accountActive { + return c.JSON(http.StatusForbidden, ErrResp{ + Ok: false, + Message: "Account pending activation. Try again later.", + }) + } + + if gasQuota < 1 { + return c.JSON(http.StatusForbidden, ErrResp{ + Ok: false, + Message: "Out of gas, refill pending. Try again later.", + }) + } + trackingId := uuid.NewString() taskPayload, err := json.Marshal(task.TransferPayload{ TrackingId: trackingId, @@ -65,6 +80,11 @@ func HandleSignTransfer(c echo.Context) error { return err } + err = cu.PgStore.DecrGasQuota(c.Request().Context(), req.From) + if err != nil { + return err + } + return c.JSON(http.StatusOK, OkResp{ Ok: true, Result: H{ diff --git a/internal/custodial/custodial.go b/internal/custodial/custodial.go index 1ee9ae6..790b5b1 100644 --- a/internal/custodial/custodial.go +++ b/internal/custodial/custodial.go @@ -7,6 +7,7 @@ import ( "github.com/bsm/redislock" "github.com/celo-org/celo-blockchain/common" + "github.com/go-redis/redis/v8" "github.com/grassrootseconomics/celoutils" "github.com/grassrootseconomics/cic-custodial/internal/keystore" "github.com/grassrootseconomics/cic-custodial/internal/nonce" @@ -26,7 +27,6 @@ type ( GiftableGasValue *big.Int GiftableToken common.Address GiftableTokenValue *big.Int - LockPrefix string LockTimeout time.Duration PrivateKey *ecdsa.PrivateKey PublicKey string @@ -40,6 +40,7 @@ type ( Noncestore nonce.Noncestore PgStore store.Store Pub *pub.Pub + RedisClient *redis.Client SystemContainer *SystemContainer TaskerClient *tasker.TaskerClient } diff --git a/internal/queries/queries.go b/internal/queries/queries.go index 2a426c5..82644bc 100644 --- a/internal/queries/queries.go +++ b/internal/queries/queries.go @@ -11,10 +11,15 @@ type Queries struct { WriteKeyPair string `query:"write-key-pair"` LoadKeyPair string `query:"load-key-pair"` // Store - CreateOTX string `query:"create-otx"` - CreateDispatchStatus string `query:"create-dispatch-status"` - UpdateChainStatus string `query:"update-chain-status"` - GetTxStatusByTrackingId string `query:"get-tx-status-by-tracking-id"` + CreateOTX string `query:"create-otx"` + CreateDispatchStatus string `query:"create-dispatch-status"` + ActivateAccount string `query:"activate-account"` + UpdateChainStatus string `query:"update-chain-status"` + GetTxStatusByTrackingId string `query:"get-tx-status-by-tracking-id"` + GetAccountActivationQuorum string `query:"get-account-activation-quorum"` + GetAccountStatus string `query:"get-account-status-by-address"` + DecrGasQuota string `query:"decr-gas-quota"` + ResetGasQuota string `query:"reset-gas-quota"` } func LoadQueries(q goyesql.Queries) (*Queries, error) { diff --git a/internal/store/account.go b/internal/store/account.go new file mode 100644 index 0000000..7b95794 --- /dev/null +++ b/internal/store/account.go @@ -0,0 +1,66 @@ +package store + +import ( + "context" +) + +func (s *PostgresStore) GetAccountStatusByAddress(ctx context.Context, publicAddress string) (bool, int, error) { + var ( + accountActive bool + gasQuota int + ) + + if err := s.db.QueryRow(ctx, s.queries.GetAccountStatus, publicAddress).Scan(&accountActive, &gasQuota); err != nil { + return false, 0, err + } + + return accountActive, gasQuota, nil +} + +func (s *PostgresStore) GetAccountActivationQuorum(ctx context.Context, trackingId string) (int, error) { + var ( + quorum int + ) + + if err := s.db.QueryRow(ctx, s.queries.GetAccountActivationQuorum, trackingId).Scan(&quorum); err != nil { + return 0, err + } + + return quorum, nil +} + +func (s *PostgresStore) DecrGasQuota(ctx context.Context, publicAddress string) error { + if _, err := s.db.Exec( + ctx, + s.queries.DecrGasQuota, + publicAddress, + ); err != nil { + return err + } + + return nil +} + +func (s *PostgresStore) ResetGasQuota(ctx context.Context, publicAddress string) error { + if _, err := s.db.Exec( + ctx, + s.queries.ResetGasQuota, + publicAddress, + ); err != nil { + return err + } + + return nil +} + +func (s *PostgresStore) ActivateAccount(ctx context.Context, publicAddress string) error { + if _, err := s.db.Exec( + ctx, + s.queries.ActivateAccount, + publicAddress, + ); err != nil { + return err + } + + return nil +} diff --git a/internal/store/dispatch.go b/internal/store/dispatch.go deleted file mode 100644 index cfb21e3..0000000 --- a/internal/store/dispatch.go +++ /dev/null @@ -1,18 +0,0 @@ -package store - -import ( - "context" -) - -func (s *PostgresStore) CreateDispatchStatus(ctx context.Context, dispatch DispatchStatus) error { - if _, err := s.db.Exec( - ctx, - s.queries.CreateDispatchStatus, - dispatch.OtxId, - dispatch.Status, - ); err != nil { - return err - } - - return nil -} diff --git a/internal/store/otx.go b/internal/store/otx.go index 9d07b19..505fa2d 100644 --- a/internal/store/otx.go +++ b/internal/store/otx.go @@ -59,8 +59,20 @@ func (s *PostgresStore) GetTxStatusByTrackingId(ctx context.Context, trackingId return txs, nil } +func (s *PostgresStore) CreateDispatchStatus(ctx context.Context, dispatch DispatchStatus) error { + if _, err := s.db.Exec( + ctx, + s.queries.CreateDispatchStatus, + dispatch.OtxId, + dispatch.Status, + ); err != nil { + return err + } + + return nil +} + func (s *PostgresStore) UpdateOtxStatusFromChainEvent(ctx context.Context, chainEvent MinimalTxInfo) error { - var ( status = enum.SUCCESS ) diff --git a/internal/store/store.go b/internal/store/store.go index efba0e7..1260f16 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -40,5 +40,10 @@ type ( CreateDispatchStatus(ctx context.Context, dispatch DispatchStatus) error GetTxStatusByTrackingId(ctx context.Context, trackingId string) ([]*TxStatus, error) UpdateOtxStatusFromChainEvent(ctx context.Context, chainEvent MinimalTxInfo) error + GetAccountStatusByAddress(ctx context.Context, publicAddress string) (bool, int, error) + GetAccountActivationQuorum(ctx context.Context, trackingId string) (int, error) + DecrGasQuota(ctx context.Context, publicAddress string) error + ResetGasQuota(ctx context.Context, publicAddress string) error + ActivateAccount(ctx context.Context, publicAddress string) error } ) diff --git a/internal/sub/handler.go b/internal/sub/handler.go index 75577f8..5d656f6 100644 --- a/internal/sub/handler.go +++ b/internal/sub/handler.go @@ -23,7 +23,9 @@ func (s *Sub) handler(ctx context.Context, msg *nats.Msg) error { switch msg.Subject { case "CHAIN.gas": - // + if err := s.cu.PgStore.ResetGasQuota(ctx, checksum(chainEvent.To)); err != nil { + return err + } } return nil diff --git a/internal/sub/util.go b/internal/sub/util.go new file mode 100644 index 0000000..1b1e625 --- /dev/null +++ b/internal/sub/util.go @@ -0,0 +1,31 @@ +package sub + +import ( + "encoding/hex" + "strconv" + "strings" + + "golang.org/x/crypto/sha3" +) + +// TODO: This should probably be used project wide +func checksum(address string) string { + address = strings.ToLower(address) + address = strings.Replace(address, "0x", "", 1) + + sha := sha3.NewLegacyKeccak256() + sha.Write([]byte(address)) + hash := sha.Sum(nil) + hashstr := hex.EncodeToString(hash) + result := []string{"0x"} + for i, v := range address { + res, _ := strconv.ParseInt(string(hashstr[i]), 16, 64) + if res > 7 { + result = append(result, strings.ToUpper(string(v))) + continue + } + result = append(result, string(v)) + } + + return strings.Join(result, "") +} diff --git a/internal/tasker/client.go b/internal/tasker/client.go index 657524f..de3b825 100644 --- a/internal/tasker/client.go +++ b/internal/tasker/client.go @@ -28,18 +28,23 @@ func NewTaskerClient(o TaskerClientOpts) *TaskerClient { } } -func (c *TaskerClient) CreateTask(ctx context.Context, taskName TaskName, queueName QueueName, task *Task) (*asynq.TaskInfo, error) { +func (c *TaskerClient) CreateTask(ctx context.Context, taskName TaskName, queueName QueueName, task *Task, extraOpts ...asynq.Option) (*asynq.TaskInfo, error) { if task.Id == "" { task.Id = uuid.NewString() } - qTask := asynq.NewTask( - string(taskName), - task.Payload, + defaultOpts := []asynq.Option{ asynq.Queue(string(queueName)), asynq.TaskID(task.Id), asynq.Retention(taskRetention), asynq.Timeout(taskTimeout), + } + taskOpts := append(defaultOpts, extraOpts...) + + qTask := asynq.NewTask( + string(taskName), + task.Payload, + taskOpts... ) taskInfo, err := c.Client.EnqueueContext(ctx, qTask) diff --git a/internal/tasker/task/account_activate.go b/internal/tasker/task/account_activate.go new file mode 100644 index 0000000..456209e --- /dev/null +++ b/internal/tasker/task/account_activate.go @@ -0,0 +1,45 @@ +package task + +import ( + "context" + "encoding/json" + "errors" + + "github.com/grassrootseconomics/cic-custodial/internal/custodial" + "github.com/hibiken/asynq" +) + +const ( + requiredQuorum = 3 +) + +var ( + ErrQuorumNotReached = errors.New("Account activation quorum not reached.") +) + +func AccountActivateProcessor(cu *custodial.Custodial) func(context.Context, *asynq.Task) error { + return func(ctx context.Context, t *asynq.Task) error { + var ( + payload AccountPayload + ) + + if err := json.Unmarshal(t.Payload(), &payload); err != nil { + return err + } + + quorum, err := cu.PgStore.GetAccountActivationQuorum(ctx, payload.TrackingId) + if err != nil { + return err + } + + if quorum < requiredQuorum { + return ErrQuorumNotReached + } + + if err := cu.PgStore.ActivateAccount(ctx, payload.PublicKey); err != nil { + return err + } + + return nil + } +} diff --git a/internal/tasker/task/account_gift_gas.go b/internal/tasker/task/account_gift_gas.go index 5c58408..21ac631 100644 --- a/internal/tasker/task/account_gift_gas.go +++ b/internal/tasker/task/account_gift_gas.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "time" "github.com/celo-org/celo-blockchain/common/hexutil" "github.com/grassrootseconomics/celoutils" @@ -16,6 +17,10 @@ import ( "github.com/hibiken/asynq" ) +const ( + accountActivationCheckDelay = 5 * time.Second +) + func AccountGiftGasProcessor(cu *custodial.Custodial) func(context.Context, *asynq.Task) error { return func(ctx context.Context, t *asynq.Task) error { var ( @@ -29,7 +34,7 @@ func AccountGiftGasProcessor(cu *custodial.Custodial) func(context.Context, *asy lock, err := cu.LockProvider.Obtain( ctx, - cu.SystemContainer.LockPrefix+cu.SystemContainer.PublicKey, + lockPrefix+cu.SystemContainer.PublicKey, cu.SystemContainer.LockTimeout, nil, ) @@ -105,6 +110,19 @@ func AccountGiftGasProcessor(cu *custodial.Custodial) func(context.Context, *asy return err } + _, err = cu.TaskerClient.CreateTask( + ctx, + tasker.AccountActivateTask, + tasker.DefaultPriority, + &tasker.Task{ + Payload: t.Payload(), + }, + asynq.ProcessIn(accountActivationCheckDelay), + ) + if err != nil { + return err + } + eventPayload := &pub.EventPayload{ OtxId: id, TrackingId: payload.TrackingId, diff --git a/internal/tasker/task/account_gift_voucher.go b/internal/tasker/task/account_gift_voucher.go index a953e3f..42c6386 100644 --- a/internal/tasker/task/account_gift_voucher.go +++ b/internal/tasker/task/account_gift_voucher.go @@ -28,7 +28,7 @@ func GiftVoucherProcessor(cu *custodial.Custodial) func(context.Context, *asynq. lock, err := cu.LockProvider.Obtain( ctx, - cu.SystemContainer.LockPrefix+cu.SystemContainer.PublicKey, + lockPrefix+cu.SystemContainer.PublicKey, cu.SystemContainer.LockTimeout, nil, ) diff --git a/internal/tasker/task/account_refill_gas.go b/internal/tasker/task/account_refill_gas.go index 5d0bf52..2559bb3 100644 --- a/internal/tasker/task/account_refill_gas.go +++ b/internal/tasker/task/account_refill_gas.go @@ -3,10 +3,12 @@ package task import ( "context" "encoding/json" + "errors" "fmt" - "math/big" + "time" "github.com/celo-org/celo-blockchain/common/hexutil" + "github.com/go-redis/redis/v8" "github.com/grassrootseconomics/celoutils" "github.com/grassrootseconomics/cic-custodial/internal/custodial" "github.com/grassrootseconomics/cic-custodial/internal/pub" @@ -14,14 +16,17 @@ import ( "github.com/grassrootseconomics/cic-custodial/internal/tasker" "github.com/grassrootseconomics/cic-custodial/pkg/enum" "github.com/grassrootseconomics/w3-celo-patch" - "github.com/grassrootseconomics/w3-celo-patch/module/eth" "github.com/hibiken/asynq" ) +const ( + gasLockPrefix = "gas_lock:" + gasLockExpiry = 1 * time.Hour +) + func AccountRefillGasProcessor(cu *custodial.Custodial) func(context.Context, *asynq.Task) error { return func(ctx context.Context, t *asynq.Task) error { var ( - balance big.Int err error payload AccountPayload ) @@ -30,20 +35,25 @@ func AccountRefillGasProcessor(cu *custodial.Custodial) func(context.Context, *a return fmt.Errorf("account: failed %v: %w", err, asynq.SkipRetry) } - if err := cu.CeloProvider.Client.CallCtx( - ctx, - eth.Balance(w3.A(payload.PublicKey), nil).Returns(&balance), - ); err != nil { + // TODO: Check eth-faucet whether we can request for a topup before signing the tx. + _, gasQuota, err := cu.PgStore.GetAccountStatusByAddress(ctx, payload.PublicKey) + if err != nil { return err } - if belowThreshold := balance.Cmp(cu.SystemContainer.GasRefillThreshold); belowThreshold > 0 { + gasLock, err := cu.RedisClient.Get(ctx, gasLockPrefix+payload.PublicKey).Bool() + if !errors.Is(err, redis.Nil) { + return err + } + + if gasQuota > 0 || gasLock { return nil } + // TODO: Use eth-faucet. lock, err := cu.LockProvider.Obtain( ctx, - cu.SystemContainer.LockPrefix+cu.SystemContainer.PublicKey, + lockPrefix+cu.SystemContainer.PublicKey, cu.SystemContainer.LockTimeout, nil, ) @@ -134,6 +144,10 @@ func AccountRefillGasProcessor(cu *custodial.Custodial) func(context.Context, *a return err } + if _, err := cu.RedisClient.SetEX(ctx, gasLockPrefix+payload.PublicKey, true, gasLockExpiry).Result(); err != nil { + return err + } + return nil } } diff --git a/internal/tasker/task/account_register_onchain.go b/internal/tasker/task/account_register_onchain.go index 7a2029f..85dc315 100644 --- a/internal/tasker/task/account_register_onchain.go +++ b/internal/tasker/task/account_register_onchain.go @@ -29,7 +29,7 @@ func AccountRegisterOnChainProcessor(cu *custodial.Custodial) func(context.Conte lock, err := cu.LockProvider.Obtain( ctx, - cu.SystemContainer.LockPrefix+cu.SystemContainer.PublicKey, + lockPrefix+cu.SystemContainer.PublicKey, cu.SystemContainer.LockTimeout, nil, ) diff --git a/internal/tasker/task/sign_transfer.go b/internal/tasker/task/sign_transfer.go index 93e9dc5..a906a89 100644 --- a/internal/tasker/task/sign_transfer.go +++ b/internal/tasker/task/sign_transfer.go @@ -47,7 +47,7 @@ func SignTransfer(cu *custodial.Custodial) func(context.Context, *asynq.Task) er lock, err := cu.LockProvider.Obtain( ctx, - cu.SystemContainer.LockPrefix+payload.From, + lockPrefix+payload.From, cu.SystemContainer.LockTimeout, nil, ) @@ -136,7 +136,8 @@ func SignTransfer(cu *custodial.Custodial) func(context.Context, *asynq.Task) er } gasRefillPayload, err := json.Marshal(AccountPayload{ - PublicKey: payload.From, + PublicKey: payload.From, + TrackingId: payload.TrackingId, }) if err != nil { return err diff --git a/internal/tasker/task/utils.go b/internal/tasker/task/utils.go new file mode 100644 index 0000000..266cc28 --- /dev/null +++ b/internal/tasker/task/utils.go @@ -0,0 +1,5 @@ +package task + +const ( + lockPrefix = "lock:" +) diff --git a/internal/tasker/types.go b/internal/tasker/types.go index 99d3188..81f8c28 100644 --- a/internal/tasker/types.go +++ b/internal/tasker/types.go @@ -20,6 +20,7 @@ const ( AccountGiftGasTask TaskName = "sys:gift_gas" AccountGiftVoucherTask TaskName = "sys:gift_token" AccountRefillGasTask TaskName = "sys:refill_gas" + AccountActivateTask TaskName = "sys:quorum_check" SignTransferTask TaskName = "usr:sign_transfer" DispatchTxTask TaskName = "rpc:dispatch" ) diff --git a/migrations/001_keystore.sql b/migrations/001_keystore.sql index 4b238a5..b342898 100644 --- a/migrations/001_keystore.sql +++ b/migrations/001_keystore.sql @@ -3,6 +3,7 @@ CREATE TABLE IF NOT EXISTS keystore ( id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, public_key TEXT NOT NULL, private_key TEXT NOT NULL, - active BOOLEAN DEFAULT true, + active BOOLEAN DEFAULT false, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); \ No newline at end of file +); +CREATE INDEX IF NOT EXISTS public_key_idx ON keystore(public_key); \ No newline at end of file diff --git a/migrations/002_custodial_db.sql b/migrations/002_custodial_db.sql index d6a68de..b71a6c4 100644 --- a/migrations/002_custodial_db.sql +++ b/migrations/002_custodial_db.sql @@ -12,7 +12,7 @@ INSERT INTO otx_tx_type (value) VALUES -- Origin tx table CREATE TABLE IF NOT EXISTS otx_sign ( id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - tracking_id TEXT NOT NULL, + tracking_id uuid NOT NULL, "type" TEXT REFERENCES otx_tx_type(value) NOT NULL, raw_tx TEXT NOT NULL, tx_hash TEXT NOT NULL, @@ -24,8 +24,9 @@ CREATE TABLE IF NOT EXISTS otx_sign ( nonce int NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE INDEX IF NOT EXISTS tx_hash_idx ON otx_sign USING hash(tx_hash); -CREATE INDEX IF NOT EXISTS from_idx ON otx_sign USING hash("from"); +CREATE INDEX IF NOT EXISTS tracking_id_idx ON otx_sign (tracking_id); +CREATE INDEX IF NOT EXISTS tx_hash_idx ON otx_sign (tx_hash); +CREATE INDEX IF NOT EXISTS from_idx ON otx_sign ("from"); -- Otx dispatch status enum table -- Enforces referential integrity on the dispatch table diff --git a/migrations/003_gas_quota.sql b/migrations/003_gas_quota.sql new file mode 100644 index 0000000..746bc6c --- /dev/null +++ b/migrations/003_gas_quota.sql @@ -0,0 +1,28 @@ +-- Gas quota meta table +CREATE TABLE IF NOT EXISTS gas_quota_meta ( + default_quota INT NOT NULL +); +INSERT INTO gas_quota_meta (default_quota) VALUES (25); + +-- Gas quota table +CREATE TABLE IF NOT EXISTS gas_quota ( + id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + key_id INT REFERENCES keystore(id) NOT NULL, + quota INT NOT NULL DEFAULT 0 +); + +-- Gas quota trigger on keystore insert to default 0 quota +-- We wait for the event handler to correctly set the chain quota +create function insert_gas_quota() + returns trigger +as $$ +begin + insert into gas_quota (key_id) values (new.id); + return new; +end; +$$ language plpgsql; + +create trigger insert_gas_quota + after insert on keystore +for each row +execute procedure insert_gas_quota() \ No newline at end of file diff --git a/queries.sql b/queries.sql index 3f66cb4..4fb3065 100644 --- a/queries.sql +++ b/queries.sql @@ -9,6 +9,11 @@ INSERT INTO keystore(public_key, private_key) VALUES($1, $2) RETURNING id -- $1: public_key SELECT private_key FROM keystore WHERE public_key=$1 +--name: activate-account +-- Activate an account following successful quorum +-- $1: public_key +UPDATE keystore SET active = true WHERE public_key=$1 + --name: create-otx -- Create a new locally originating tx -- $1: tracking_id @@ -51,7 +56,7 @@ INSERT INTO otx_dispatch( UPDATE otx_dispatch SET "status" = $2, "block" = $3 WHERE otx_dispatch.id = ( SELECT otx_dispatch.id FROM otx_dispatch INNER JOIN otx_sign ON otx_dispatch.otx_id = otx_sign.id - WHERE otx_sign.tx_hash = $1 + WHERE otx_sign.tx_hash=$1 AND otx_dispatch.status = 'IN_NETWORK' ) @@ -60,6 +65,40 @@ UPDATE otx_dispatch SET "status" = $2, "block" = $3 WHERE otx_dispatch.id = ( -- $1: tracking_id SELECT otx_sign.type, otx_sign.tx_hash, otx_sign.transfer_value, otx_sign.created_at, otx_dispatch.status FROM otx_sign INNER JOIN otx_dispatch ON otx_sign.id = otx_dispatch.otx_id -WHERE otx_sign.tracking_id = $1 +WHERE otx_sign.tracking_id=$1 -- TODO: Scroll by status type with cursor pagination + +--name: get-account-activation-quorum +-- Gets quorum of required and confirmed system transactions for the account +-- $1: tracking_id +SELECT count(*) FROM otx_dispatch INNER JOIN +otx_sign ON otx_dispatch.otx_id = otx_sign.id +WHERE otx_sign.tracking_id=$1 +AND otx_dispatch.status = 'SUCCESS' + +--name: get-account-status-by-address +-- Gets current gas quota for an individual account by address +-- $1: public_key +SELECT keystore.active, gas_quota.quota FROM keystore +INNER JOIN gas_quota ON keystore.id = gas_quota.key_id +WHERE keystore.public_key=$1 + +--name: decr-gas-quota +-- Consumes a gas quota +-- $1: public_key +UPDATE gas_quota SET quota = quota - 1 WHERE key_id = ( + SELECT id FROM keystore + WHERE public_key=$1 +) + +--name: reset-gas-quota +-- Resets the gas quota +-- 25 is the agreed upon quota +-- $1: public_key +UPDATE gas_quota SET quota = gas_quota_meta.default_quota +FROM gas_quota_meta +WHERE key_id = ( + SELECT id FROM keystore + WHERE public_key=$1 +) \ No newline at end of file