fix (braking change): gas refill params

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 12:19:04 +00:00
parent 8ef2311d8e
commit 7cc1a3d924
Signed by: kamikazechaser
GPG Key ID: 7DD45520C01CD85D
12 changed files with 131 additions and 92 deletions

View File

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

View File

@ -13,6 +13,7 @@ import (
)
// HandleSignTransfer godoc
//
// @Summary Sign and dispatch transfer request.
// @Description Sign and dispatch a transfer request.
// @Tags network
@ -42,7 +43,7 @@ func HandleSignTransfer(cu *custodial.Custodial) func(echo.Context) error {
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 {
return err
}
@ -54,36 +55,15 @@ func HandleSignTransfer(cu *custodial.Custodial) func(echo.Context) error {
})
}
trackingId := uuid.NewString()
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
}
if gasLock {
return c.JSON(http.StatusForbidden, ErrResp{
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{
TrackingId: trackingId,
From: req.From,

View File

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

View File

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

View File

@ -27,10 +27,10 @@ type (
UpdateDispatchStatus(context.Context, bool, string, uint64) error
// Account related actions.
ActivateAccount(context.Context, string) error
GetAccountStatus(context.Context, string) (bool, int, error)
GetAccountStatus(context.Context, string) (bool, bool, error)
// Gas quota related actions.
DecrGasQuota(context.Context, string) error
ResetGasQuota(context.Context, string) error
GasLock(context.Context, string) error
GasUnlock(context.Context, string) error
}
Opts struct {
@ -57,8 +57,8 @@ type (
// Account related queries.
ActivateAccount string `query:"activate-account"`
GetAccountStatus string `query:"get-account-status-by-address"`
DecrGasQuota string `query:"decr-gas-quota"`
ResetGasQuota string `query:"reset-gas-quota"`
GasLock string `query:"acc-gas-lock"`
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
}
if err := s.cu.Store.ResetGasQuota(ctx, chainEvent.To); err != nil {
if err := s.cu.Store.GasUnlock(ctx, chainEvent.To); err != nil {
return err
}
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
}
}

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package task
import (
"math/big"
"time"
"github.com/bsm/redislock"
@ -14,6 +15,15 @@ const (
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.
// it is expected to prevent immidiate requeue of the task at the expense of more redis calls.
func lockRetry() redislock.RetryStrategy {
@ -22,3 +32,8 @@ func lockRetry() redislock.RetryStrategy {
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
--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
SELECT keystore.active, gas_quota.quota FROM keystore
INNER JOIN gas_quota ON keystore.id = gas_quota.key_id
SELECT keystore.active, gas_lock.lock FROM keystore
INNER JOIN gas_lock ON keystore.id = gas_lock.key_id
WHERE keystore.public_key=$1
--name: decr-gas-quota
-- Consumes a gas quota
--name: acc-gas-lock
-- Locks an account for gas reasons
-- $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
WHERE public_key=$1
)
--name: reset-gas-quota
-- Resets the gas quota
-- 25 is the agreed upon quota
--name: acc-gas-unlock
-- Unlocks an account for gas reasons
-- $1: public_key
UPDATE gas_quota SET quota = gas_quota_meta.default_quota
FROM gas_quota_meta
WHERE key_id = (
UPDATE gas_lock SET lock = false WHERE key_id = (
SELECT id FROM keystore
WHERE public_key=$1
)