mirror of
https://github.com/grassrootseconomics/cic-custodial.git
synced 2024-12-21 09:27:32 +01:00
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:
parent
8ef2311d8e
commit
b9d3c219c8
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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"`
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
34
migrations/005_remove_gas_quota.sql
Normal file
34
migrations/005_remove_gas_quota.sql
Normal 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();
|
23
queries.sql
23
queries.sql
@ -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
|
||||
)
|
||||
)
|
Loading…
Reference in New Issue
Block a user