fix (braking change): gas refill params (#89)

NOTE: This needs the db to be nuked if you are running a test cluster

* updated gas refilling logic to reflect EthFaucet contract
* fully dependant on on-chain contract to refill and unlock gas
* minor fixes to nonce bootstrapper

Ideal Values:

INITIAL GIFT = 0.015
THRESHOLD    = 0.01
TIME = 12 * 60 * 60

# Sample for 30 txs
EXTREME LOW = 0.00675
LOW GAS USAGE = 0.00694605
RISING = 0.0135
HIGH = 0.027
This commit is contained in:
Mohamed Sohail 2023-05-16 15:20:01 +03:00 committed by GitHub
parent 8ef2311d8e
commit b9d3c219c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 131 additions and 92 deletions

View File

@ -60,6 +60,7 @@ func main() {
custodial, err := custodial.NewCustodial(custodial.Opts{ custodial, err := custodial.NewCustodial(custodial.Opts{
CeloProvider: celoProvider, CeloProvider: celoProvider,
LockProvider: lockProvider, LockProvider: lockProvider,
Logg: lo,
Noncestore: redisNoncestore, Noncestore: redisNoncestore,
Store: store, Store: store,
RedisClient: redisPool.Client, RedisClient: redisPool.Client,

View File

@ -13,6 +13,7 @@ import (
) )
// HandleSignTransfer godoc // HandleSignTransfer godoc
//
// @Summary Sign and dispatch transfer request. // @Summary Sign and dispatch transfer request.
// @Description Sign and dispatch a transfer request. // @Description Sign and dispatch a transfer request.
// @Tags network // @Tags network
@ -42,7 +43,7 @@ func HandleSignTransfer(cu *custodial.Custodial) func(echo.Context) error {
return err return err
} }
accountActive, gasQuota, err := cu.Store.GetAccountStatus(c.Request().Context(), req.From) accountActive, gasLock, err := cu.Store.GetAccountStatus(c.Request().Context(), req.From)
if err != nil { if err != nil {
return err return err
} }
@ -54,36 +55,15 @@ func HandleSignTransfer(cu *custodial.Custodial) func(echo.Context) error {
}) })
} }
trackingId := uuid.NewString() if gasLock {
if gasQuota < 1 {
gasRefillPayload, err := json.Marshal(task.AccountPayload{
PublicKey: req.From,
TrackingId: trackingId,
})
if err != nil {
return err
}
_, err = cu.TaskerClient.CreateTask(
c.Request().Context(),
tasker.AccountRefillGasTask,
tasker.DefaultPriority,
&tasker.Task{
Id: trackingId,
Payload: gasRefillPayload,
},
)
if err != nil {
return err
}
return c.JSON(http.StatusForbidden, ErrResp{ return c.JSON(http.StatusForbidden, ErrResp{
Ok: false, Ok: false,
Message: "Out of gas, refill pending. Try again later.", Message: "Gas lock. Gas balance unavailable. Try again later.",
}) })
} }
trackingId := uuid.NewString()
taskPayload, err := json.Marshal(task.TransferPayload{ taskPayload, err := json.Marshal(task.TransferPayload{
TrackingId: trackingId, TrackingId: trackingId,
From: req.From, From: req.From,

View File

@ -15,12 +15,14 @@ import (
"github.com/grassrootseconomics/w3-celo-patch" "github.com/grassrootseconomics/w3-celo-patch"
"github.com/labstack/gommon/log" "github.com/labstack/gommon/log"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"github.com/zerodha/logf"
) )
type ( type (
Opts struct { Opts struct {
CeloProvider *celoutils.Provider CeloProvider *celoutils.Provider
LockProvider *redislock.Client LockProvider *redislock.Client
Logg logf.Logger
Noncestore nonce.Noncestore Noncestore nonce.Noncestore
Store store.Store Store store.Store
RedisClient *redis.Client RedisClient *redis.Client
@ -54,11 +56,13 @@ func NewCustodial(o Opts) (*Custodial, error) {
return nil, err return nil, err
} }
_, err = o.Noncestore.Peek(ctx, o.SystemPublicKey) systemNonce, err := o.Noncestore.Peek(ctx, o.SystemPublicKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
o.Logg.Info("custodial: loaded_nonce", "system_nonce", systemNonce)
privateKey, err := eth_crypto.HexToECDSA(o.SystemPrivateKey) privateKey, err := eth_crypto.HexToECDSA(o.SystemPrivateKey)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -37,13 +37,17 @@ func NewRedisNoncestore(o Opts) Noncestore {
func (n *RedisNoncestore) Peek(ctx context.Context, publicKey string) (uint64, error) { func (n *RedisNoncestore) Peek(ctx context.Context, publicKey string) (uint64, error) {
nonce, err := n.redis.Client.Get(ctx, publicKey).Uint64() nonce, err := n.redis.Client.Get(ctx, publicKey).Uint64()
if err == redis.Nil { if err != nil {
nonce, err = n.bootstrap(ctx, publicKey) if err == redis.Nil {
if err != nil { nonce, err = n.bootstrap(ctx, publicKey)
if err != nil {
return 0, err
}
return nonce, nil
} else {
return 0, err return 0, err
} }
} else if err != nil {
return 0, err
} }
return nonce, nil return nonce, nil
@ -55,13 +59,15 @@ func (n *RedisNoncestore) Acquire(ctx context.Context, publicKey string) (uint64
) )
nonce, err := n.redis.Client.Get(ctx, publicKey).Uint64() nonce, err := n.redis.Client.Get(ctx, publicKey).Uint64()
if err == redis.Nil { if err != nil {
nonce, err = n.bootstrap(ctx, publicKey) if err == redis.Nil {
if err != nil { nonce, err = n.bootstrap(ctx, publicKey)
if err != nil {
return 0, err
}
} else {
return 0, err return 0, err
} }
} else if err != nil {
return 0, err
} }
err = n.redis.Client.Incr(ctx, publicKey).Err() err = n.redis.Client.Incr(ctx, publicKey).Err()

View File

@ -22,10 +22,10 @@ func (s *PgStore) ActivateAccount(
func (s *PgStore) GetAccountStatus( func (s *PgStore) GetAccountStatus(
ctx context.Context, ctx context.Context,
publicAddress string, publicAddress string,
) (bool, int, error) { ) (bool, bool, error) {
var ( var (
accountActive bool accountActive bool
gasQuota int gasLock bool
) )
if err := s.db.QueryRow( if err := s.db.QueryRow(
@ -34,21 +34,21 @@ func (s *PgStore) GetAccountStatus(
publicAddress, publicAddress,
).Scan( ).Scan(
&accountActive, &accountActive,
&gasQuota, &gasLock,
); err != nil { ); err != nil {
return false, 0, err return false, false, err
} }
return accountActive, gasQuota, nil return accountActive, gasLock, nil
} }
func (s *PgStore) DecrGasQuota( func (s *PgStore) GasLock(
ctx context.Context, ctx context.Context,
publicAddress string, publicAddress string,
) error { ) error {
if _, err := s.db.Exec( if _, err := s.db.Exec(
ctx, ctx,
s.queries.DecrGasQuota, s.queries.GasLock,
publicAddress, publicAddress,
); err != nil { ); err != nil {
return err return err
@ -57,13 +57,13 @@ func (s *PgStore) DecrGasQuota(
return nil return nil
} }
func (s *PgStore) ResetGasQuota( func (s *PgStore) GasUnlock(
ctx context.Context, ctx context.Context,
publicAddress string, publicAddress string,
) error { ) error {
if _, err := s.db.Exec( if _, err := s.db.Exec(
ctx, ctx,
s.queries.ResetGasQuota, s.queries.GasUnlock,
publicAddress, publicAddress,
); err != nil { ); err != nil {
return err return err

View File

@ -27,10 +27,10 @@ type (
UpdateDispatchStatus(context.Context, bool, string, uint64) error UpdateDispatchStatus(context.Context, bool, string, uint64) error
// Account related actions. // Account related actions.
ActivateAccount(context.Context, string) error ActivateAccount(context.Context, string) error
GetAccountStatus(context.Context, string) (bool, int, error) GetAccountStatus(context.Context, string) (bool, bool, error)
// Gas quota related actions. // Gas quota related actions.
DecrGasQuota(context.Context, string) error GasLock(context.Context, string) error
ResetGasQuota(context.Context, string) error GasUnlock(context.Context, string) error
} }
Opts struct { Opts struct {
@ -57,8 +57,8 @@ type (
// Account related queries. // Account related queries.
ActivateAccount string `query:"activate-account"` ActivateAccount string `query:"activate-account"`
GetAccountStatus string `query:"get-account-status-by-address"` GetAccountStatus string `query:"get-account-status-by-address"`
DecrGasQuota string `query:"decr-gas-quota"` GasLock string `query:"acc-gas-lock"`
ResetGasQuota string `query:"reset-gas-quota"` GasUnlock string `query:"acc-gas-unlock"`
} }
) )

View File

@ -45,11 +45,11 @@ func (s *Sub) processEventHandler(ctx context.Context, msg *nats.Msg) error {
return err return err
} }
if err := s.cu.Store.ResetGasQuota(ctx, chainEvent.To); err != nil { if err := s.cu.Store.GasUnlock(ctx, chainEvent.To); err != nil {
return err return err
} }
case "CHAIN.gas": case "CHAIN.gas":
if err := s.cu.Store.ResetGasQuota(ctx, chainEvent.To); err != nil { if err := s.cu.Store.GasUnlock(ctx, chainEvent.To); err != nil {
return err return err
} }
} }

View File

@ -35,16 +35,6 @@ func AccountRefillGasProcessor(cu *custodial.Custodial) func(context.Context, *a
return err return err
} }
_, gasQuota, err := cu.Store.GetAccountStatus(ctx, payload.PublicKey)
if err != nil {
return err
}
// The user has enough gas for atleast 5 more transactions.
if gasQuota > 5 {
return nil
}
if err := cu.CeloProvider.Client.CallCtx( if err := cu.CeloProvider.Client.CallCtx(
ctx, ctx,
eth.CallFunc( eth.CallFunc(
@ -56,8 +46,8 @@ func AccountRefillGasProcessor(cu *custodial.Custodial) func(context.Context, *a
return err return err
} }
// The user already requested funds, there is a cooldown applied. // The user recently requested funds, there is a cooldown applied.
// We can schedule an attempt after the cooldown period has passed. // We can schedule an attempt after the cooldown period has passed + 10 seconds.
if nextTime.Int64() > time.Now().Unix() { if nextTime.Int64() > time.Now().Unix() {
_, err = cu.TaskerClient.CreateTask( _, err = cu.TaskerClient.CreateTask(
ctx, ctx,
@ -66,7 +56,7 @@ func AccountRefillGasProcessor(cu *custodial.Custodial) func(context.Context, *a
&tasker.Task{ &tasker.Task{
Payload: t.Payload(), Payload: t.Payload(),
}, },
asynq.ProcessAt(time.Unix(nextTime.Int64(), 0)), asynq.ProcessAt(time.Unix(nextTime.Int64()+10, 0)),
) )
if err != nil { if err != nil {
return err return err
@ -130,6 +120,7 @@ func AccountRefillGasProcessor(cu *custodial.Custodial) func(context.Context, *a
InputData: input, InputData: input,
GasFeeCap: celoutils.SafeGasFeeCap, GasFeeCap: celoutils.SafeGasFeeCap,
GasTipCap: celoutils.SafeGasTipCap, GasTipCap: celoutils.SafeGasTipCap,
GasLimit: gasLimit,
Nonce: nonce, Nonce: nonce,
}, },
) )

View File

@ -12,6 +12,7 @@ import (
"github.com/grassrootseconomics/cic-custodial/internal/store" "github.com/grassrootseconomics/cic-custodial/internal/store"
"github.com/grassrootseconomics/cic-custodial/internal/tasker" "github.com/grassrootseconomics/cic-custodial/internal/tasker"
"github.com/grassrootseconomics/cic-custodial/pkg/enum" "github.com/grassrootseconomics/cic-custodial/pkg/enum"
"github.com/grassrootseconomics/w3-celo-patch/module/eth"
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
) )
@ -26,8 +27,9 @@ type TransferPayload struct {
func SignTransfer(cu *custodial.Custodial) func(context.Context, *asynq.Task) error { func SignTransfer(cu *custodial.Custodial) func(context.Context, *asynq.Task) error {
return func(ctx context.Context, t *asynq.Task) error { return func(ctx context.Context, t *asynq.Task) error {
var ( var (
err error err error
payload TransferPayload networkBalance big.Int
payload TransferPayload
) )
if err := json.Unmarshal(t.Payload(), &payload); err != nil { if err := json.Unmarshal(t.Payload(), &payload); err != nil {
@ -105,7 +107,10 @@ func SignTransfer(cu *custodial.Custodial) func(context.Context, *asynq.Task) er
return err return err
} }
if err := cu.Store.DecrGasQuota(ctx, payload.From); err != nil { if err := cu.CeloProvider.Client.CallCtx(
ctx,
eth.Balance(celoutils.HexToAddress(payload.From), nil).Returns(&networkBalance),
); err != nil {
return err return err
} }
@ -137,16 +142,22 @@ func SignTransfer(cu *custodial.Custodial) func(context.Context, *asynq.Task) er
return err return err
} }
_, err = cu.TaskerClient.CreateTask( if !balanceCheck(networkBalance) {
ctx, if err := cu.Store.GasLock(ctx, payload.From); err != nil {
tasker.AccountRefillGasTask, return err
tasker.DefaultPriority, }
&tasker.Task{
Payload: gasRefillPayload, _, err = cu.TaskerClient.CreateTask(
}, ctx,
) tasker.AccountRefillGasTask,
if err != nil { tasker.DefaultPriority,
return err &tasker.Task{
Payload: gasRefillPayload,
},
)
if err != nil {
return err
}
} }
return nil return nil

View File

@ -1,6 +1,7 @@
package task package task
import ( import (
"math/big"
"time" "time"
"github.com/bsm/redislock" "github.com/bsm/redislock"
@ -14,6 +15,15 @@ const (
lockTimeout = 1 * time.Second lockTimeout = 1 * time.Second
) )
var (
// 20 gwei = max gas price we are willing to pay
// 250k = max gas limit
// minGasBalanceRequired is optimistic that the immidiate next transfer request will be successful
// but the subsequent one could fail (though low probability), therefore we can trigger a gas lock.
// Therefore our system wide threshold is 0.01 CELO or 10000000000000000 gas units
minGasBalanceRequired = big.NewInt(20000000000 * 250000 * 2)
)
// lockRetry will at most try to obtain the lock 20 times within ~0.5s. // lockRetry will at most try to obtain the lock 20 times within ~0.5s.
// it is expected to prevent immidiate requeue of the task at the expense of more redis calls. // it is expected to prevent immidiate requeue of the task at the expense of more redis calls.
func lockRetry() redislock.RetryStrategy { func lockRetry() redislock.RetryStrategy {
@ -22,3 +32,8 @@ func lockRetry() redislock.RetryStrategy {
20, 20,
) )
} }
// balanceCheck compares the network balance with the system set min as threshold to execute a transfer.
func balanceCheck(networkBalance big.Int) bool {
return minGasBalanceRequired.Cmp(&networkBalance) < 0
}

View File

@ -0,0 +1,34 @@
-- Replace gas_quota with gas_lock which checks network balance threshold
DROP TRIGGER IF EXISTS update_gas_quota_timestamp ON gas_quota;
DROP TABLE IF EXISTS gas_quota_meta;
DROP TABLE IF EXISTS gas_quota;
DROP TRIGGER IF EXISTS insert_gas_quota ON keystore;
DROP FUNCTION IF EXISTS insert_gas_quota;
-- Gas lock table
-- A gas_locked account indicates gas balance is below threshold awaiting next available top up
CREATE TABLE IF NOT EXISTS gas_lock (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
key_id INT REFERENCES keystore(id) NOT NULL,
lock BOOLEAN DEFAULT true,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
create function insert_gas_lock()
returns trigger
as $$
begin
insert into gas_lock (key_id) values (new.id);
return new;
end;
$$ language plpgsql;
create trigger insert_gas_lock
after insert on keystore
for each row
execute procedure insert_gas_lock();
create trigger update_gas_lock_timestamp
before update on gas_lock
for each row
execute procedure update_timestamp();

View File

@ -73,27 +73,24 @@ UPDATE otx_dispatch SET "status" = $2, "block" = $3 WHERE otx_dispatch.id = (
UPDATE keystore SET active = true WHERE public_key=$1 UPDATE keystore SET active = true WHERE public_key=$1
--name: get-account-status-by-address --name: get-account-status-by-address
-- Gets current gas quota for an individual account by address -- Gets current gas lock and activation status for an individual account by address
-- $1: public_key -- $1: public_key
SELECT keystore.active, gas_quota.quota FROM keystore SELECT keystore.active, gas_lock.lock FROM keystore
INNER JOIN gas_quota ON keystore.id = gas_quota.key_id INNER JOIN gas_lock ON keystore.id = gas_lock.key_id
WHERE keystore.public_key=$1 WHERE keystore.public_key=$1
--name: decr-gas-quota --name: acc-gas-lock
-- Consumes a gas quota -- Locks an account for gas reasons
-- $1: public_key -- $1: public_key
UPDATE gas_quota SET quota = quota - 1 WHERE key_id = ( UPDATE gas_lock SET lock = true WHERE key_id = (
SELECT id FROM keystore SELECT id FROM keystore
WHERE public_key=$1 WHERE public_key=$1
) )
--name: reset-gas-quota --name: acc-gas-unlock
-- Resets the gas quota -- Unlocks an account for gas reasons
-- 25 is the agreed upon quota
-- $1: public_key -- $1: public_key
UPDATE gas_quota SET quota = gas_quota_meta.default_quota UPDATE gas_lock SET lock = false WHERE key_id = (
FROM gas_quota_meta
WHERE key_id = (
SELECT id FROM keystore SELECT id FROM keystore
WHERE public_key=$1 WHERE public_key=$1
) )