From 8f1a7da2bd1b82acbd0294fb49c2f96074371e07 Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Tue, 4 Feb 2025 18:27:45 -0600 Subject: [PATCH 01/30] feat(erc20signersvc): add erc20 reward signer svc --- Taskfile.yml | 8 +- app/node/build.go | 54 +- app/node/node.go | 12 + config/config.go | 66 +- go.mod | 5 + go.sum | 4 + node/exts/erc20reward/meta_extension.go | 104 ++- node/exts/erc20reward/meta_schema.sql | 14 +- node/exts/erc20reward/meta_sql.go | 82 ++- node/exts/erc20reward/named_extension.go | 10 +- node/exts/erc20reward/reward/crypto.go | 20 +- node/services/erc20signersvc/.gitignore | 1 + .../erc20signersvc/abigen/multicall3.go | 644 ++++++++++++++++++ .../erc20signersvc/abigen/multicall3_abi.json | 440 ++++++++++++ node/services/erc20signersvc/abigen/safe.go | 274 ++++++++ .../erc20signersvc/abigen/safe_abi.json | 41 ++ node/services/erc20signersvc/eth.go | 226 ++++++ node/services/erc20signersvc/eth_test.go | 34 + node/services/erc20signersvc/kwil.go | 204 ++++++ node/services/erc20signersvc/multicall.go | 92 +++ node/services/erc20signersvc/signer.go | 362 ++++++++++ node/services/erc20signersvc/state.go | 120 ++++ test/go.mod | 1 + test/go.sum | 2 + 24 files changed, 2745 insertions(+), 75 deletions(-) create mode 100644 node/services/erc20signersvc/.gitignore create mode 100644 node/services/erc20signersvc/abigen/multicall3.go create mode 100644 node/services/erc20signersvc/abigen/multicall3_abi.json create mode 100644 node/services/erc20signersvc/abigen/safe.go create mode 100644 node/services/erc20signersvc/abigen/safe_abi.json create mode 100644 node/services/erc20signersvc/eth.go create mode 100644 node/services/erc20signersvc/eth_test.go create mode 100644 node/services/erc20signersvc/kwil.go create mode 100644 node/services/erc20signersvc/multicall.go create mode 100644 node/services/erc20signersvc/signer.go create mode 100644 node/services/erc20signersvc/state.go diff --git a/Taskfile.yml b/Taskfile.yml index e7e8bd945..55646e7ce 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -73,11 +73,11 @@ tasks: generates: - .build/kwild - generate:grammar: - desc: Generate the kuneiform grammar go code. + gen:abi: # TODO: merge with brennan's + desc: Generate abis cmds: - - rm -rf node/engine/parse/gen/* - - cd node/engine/parse/grammar && ./generate.sh + - abigen --abi=./node/services/erc20signersvc/abigen/safe_abi.json --pkg abigen --out=./node/services/erc20signersvc/abigen/safe.go --type Safe + - abigen --abi=./node/services/erc20signersvc/abigen/multicall3_abi.json --pkg abigen --out=./node/services/erc20signersvc/abigen/multicall3.go --type Multicall3 generate:docs: desc: Generate docs for CLIs diff --git a/app/node/build.go b/app/node/build.go index e1c37cf1e..6179a5ecc 100644 --- a/app/node/build.go +++ b/app/node/build.go @@ -26,23 +26,23 @@ import ( "github.com/kwilteam/kwil-db/node/consensus" "github.com/kwilteam/kwil-db/node/engine" "github.com/kwilteam/kwil-db/node/engine/interpreter" + _ "github.com/kwilteam/kwil-db/node/exts/erc20reward" "github.com/kwilteam/kwil-db/node/listeners" "github.com/kwilteam/kwil-db/node/mempool" "github.com/kwilteam/kwil-db/node/meta" "github.com/kwilteam/kwil-db/node/migrations" "github.com/kwilteam/kwil-db/node/pg" - "github.com/kwilteam/kwil-db/node/snapshotter" - "github.com/kwilteam/kwil-db/node/store" - "github.com/kwilteam/kwil-db/node/txapp" - "github.com/kwilteam/kwil-db/node/types/sql" - "github.com/kwilteam/kwil-db/node/voting" - - _ "github.com/kwilteam/kwil-db/node/exts/erc20reward" + signersvc "github.com/kwilteam/kwil-db/node/services/erc20signersvc" rpcserver "github.com/kwilteam/kwil-db/node/services/jsonrpc" "github.com/kwilteam/kwil-db/node/services/jsonrpc/adminsvc" "github.com/kwilteam/kwil-db/node/services/jsonrpc/chainsvc" "github.com/kwilteam/kwil-db/node/services/jsonrpc/funcsvc" "github.com/kwilteam/kwil-db/node/services/jsonrpc/usersvc" + "github.com/kwilteam/kwil-db/node/snapshotter" + "github.com/kwilteam/kwil-db/node/store" + "github.com/kwilteam/kwil-db/node/txapp" + "github.com/kwilteam/kwil-db/node/types/sql" + "github.com/kwilteam/kwil-db/node/voting" ) func buildServer(ctx context.Context, d *coreDependencies) *server { @@ -96,6 +96,9 @@ func buildServer(ctx context.Context, d *coreDependencies) *server { // Consensus ce := buildConsensusEngine(ctx, d, db, mp, bs, bp) + // Erc20 reward signer service + erc20RWSignerMgr := buildErc20RWignerMgr(d) + // Node node := buildNode(d, mp, bs, ce, snapshotStore, db, bp, p2pSvc) @@ -155,6 +158,7 @@ func buildServer(ctx context.Context, d *coreDependencies) *server { jsonRPCAdminServer: jsonRPCAdminServer, dbCtx: db, log: d.logger, + erc20RWSigner: erc20RWSignerMgr, } return s @@ -504,6 +508,42 @@ func buildConsensusEngine(_ context.Context, d *coreDependencies, db *pg.DB, return ce } +func buildErc20RWignerMgr(d *coreDependencies) *signersvc.ServiceMgr { + cfg := d.cfg.Erc20RWSigner + if !cfg.Enable { + return nil + } + + if err := cfg.Validate(); err != nil { + failBuild(err, "invalid erc20 reward signer config") + } + + // create shared state + stateFile := signersvc.StateFilePath(d.rootDir) + + if !fileExists(stateFile) { + emptyFile, err := os.Create(stateFile) + if err != nil { + failBuild(err, "Failed to create erc20 reward signer state file") + } + _ = emptyFile.Close() + } + + state, err := signersvc.LoadStateFromFile(stateFile) + if err != nil { + failBuild(err, "Failed to load erc20 reward signer state file") + } + + rpcUrl := "http://" + d.cfg.RPC.ListenAddress + + mgr, err := signersvc.NewServiceMgr(rpcUrl, cfg.Targets, cfg.EthRpcs, cfg.PrivateKeys, time.Duration(cfg.SyncEvery), state, d.logger.New("EVMRW")) + if err != nil { + failBuild(err, "Failed to create erc20 reward signer service manager") + } + + return mgr +} + func buildNode(d *coreDependencies, mp *mempool.Mempool, bs *store.BlockStore, ce *consensus.ConsensusEngine, ss *snapshotter.SnapshotStore, db *pg.DB, bp *blockprocessor.BlockProcessor, p2p *node.P2PService) *node.Node { diff --git a/app/node/node.go b/app/node/node.go index af2f9b6a1..e7825ca89 100644 --- a/app/node/node.go +++ b/app/node/node.go @@ -5,11 +5,13 @@ import ( "crypto/tls" "errors" "fmt" + signersvc "github.com/kwilteam/kwil-db/node/services/erc20signersvc" "io" "os" "path/filepath" "runtime" "slices" + "time" "github.com/kwilteam/kwil-db/app/key" "github.com/kwilteam/kwil-db/config" @@ -43,6 +45,7 @@ type server struct { listeners *listeners.ListenerManager jsonRPCServer *rpcserver.Server jsonRPCAdminServer *rpcserver.Server + erc20RWSigner *signersvc.ServiceMgr } func runNode(ctx context.Context, rootDir string, cfg *config.Config, autogen bool, dbOwner string) (err error) { @@ -259,6 +262,15 @@ func (s *server) Start(ctx context.Context) error { }) s.log.Info("listener manager started") + // Start erc20 reward signer svc + if s.erc20RWSigner != nil { + // a naive way to wait for the RPC service is running, should be fine + time.Sleep(time.Second * 3) + group.Go(func() error { + return s.erc20RWSigner.Start(groupCtx) + }) + } + // TODO: node is starting the consensus engine for ease of testing // Start the consensus engine diff --git a/config/config.go b/config/config.go index ea7b1c5ad..c7155383a 100644 --- a/config/config.go +++ b/config/config.go @@ -311,6 +311,13 @@ func DefaultConfig() *Config { Height: 0, Hash: types.Hash{}, }, + Erc20RWSigner: Erc20RewardSignerConfig{ + Enable: false, + PrivateKeys: nil, + Targets: nil, + // the reasonable value is the block time + SyncEvery: types.Duration(1 * time.Minute), + }, } } @@ -323,18 +330,19 @@ type Config struct { ProfileMode string `toml:"profile_mode,commented" comment:"profile mode (http, cpu, mem, mutex, or block)"` ProfileFile string `toml:"profile_file,commented" comment:"profile output file path (e.g. cpu.pprof)"` - P2P PeerConfig `toml:"p2p" comment:"P2P related configuration"` - Consensus ConsensusConfig `toml:"consensus" comment:"Consensus related configuration"` - DB DBConfig `toml:"db" comment:"DB (PostgreSQL) related configuration"` - Store StoreConfig `toml:"store" comment:"Block store configuration"` - RPC RPCConfig `toml:"rpc" comment:"User RPC service configuration"` - Admin AdminConfig `toml:"admin" comment:"Admin RPC service configuration"` - Snapshots SnapshotConfig `toml:"snapshots" comment:"Snapshot creation and provider configuration"` - StateSync StateSyncConfig `toml:"state_sync" comment:"Statesync configuration (vs block sync)"` - Extensions map[string]map[string]string `toml:"extensions" comment:"extension configuration"` - GenesisState string `toml:"genesis_state" comment:"path to the genesis state file, relative to the root directory"` - Migrations MigrationConfig `toml:"migrations" comment:"zero downtime migration configuration"` - Checkpoint Checkpoint `toml:"checkpoint" comment:"checkpoint info for the leader to sync to before proposing a new block"` + P2P PeerConfig `toml:"p2p" comment:"P2P related configuration"` + Consensus ConsensusConfig `toml:"consensus" comment:"Consensus related configuration"` + DB DBConfig `toml:"db" comment:"DB (PostgreSQL) related configuration"` + Store StoreConfig `toml:"store" comment:"Block store configuration"` + RPC RPCConfig `toml:"rpc" comment:"User RPC service configuration"` + Admin AdminConfig `toml:"admin" comment:"Admin RPC service configuration"` + Snapshots SnapshotConfig `toml:"snapshots" comment:"Snapshot creation and provider configuration"` + StateSync StateSyncConfig `toml:"state_sync" comment:"Statesync configuration (vs block sync)"` + Extensions map[string]map[string]string `toml:"extensions" comment:"extension configuration"` + GenesisState string `toml:"genesis_state" comment:"path to the genesis state file, relative to the root directory"` + Migrations MigrationConfig `toml:"migrations" comment:"zero downtime migration configuration"` + Checkpoint Checkpoint `toml:"checkpoint" comment:"checkpoint info for the leader to sync to before proposing a new block"` + Erc20RWSigner Erc20RewardSignerConfig `toml:"erc20_reward_signer" comment:"ERC20 reward signer service configuration"` } // PeerConfig corresponds to the [p2p] section of the config. @@ -443,6 +451,40 @@ type Checkpoint struct { Hash types.Hash `toml:"hash" comment:"checkpoint block hash."` } +type Erc20RewardSignerConfig struct { + Enable bool `toml:"enable" comment:"enable the ERC20 reward signer service"` + Targets []string `json:"targets" comment:"target reward ext alias for the ERC20 reward"` + PrivateKeys []string `json:"private_keys" comment:"private key for the ERC20 reward target"` + EthRpcs []string `json:"eth_rpcs" comment:"eth rpc address for the ERC20 reward target"` + SyncEvery types.Duration `json:"sync_every" comment:"sync interval; a recommend value is same as the block time"` +} + +func (cfg Erc20RewardSignerConfig) Validate() error { + if (len(cfg.PrivateKeys) != len(cfg.Targets)) && (len(cfg.EthRpcs) != len(cfg.Targets)) { + return fmt.Errorf("private keys and targets and eth_rpcs must be configured in triples") + } + + if len(cfg.Targets) == 0 { + return fmt.Errorf("no target configured") + } + + for i, target := range cfg.Targets { + if target == "" { + return fmt.Errorf("target %dth is empty", i) + } + + if cfg.PrivateKeys[i] == "" { + return fmt.Errorf("private key %dth is empty", i) + } + + if cfg.EthRpcs[i] == "" { + return fmt.Errorf("eth rpc %dth is empty", i) + } + } + + return nil +} + // ToTOML marshals the config to TOML. The `toml` struct field tag // specifies the field names. For example: // diff --git a/go.mod b/go.mod index 2832fbf5a..e3b044843 100644 --- a/go.mod +++ b/go.mod @@ -217,3 +217,8 @@ require ( github.com/pion/transport/v3 v3.0.7 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect ) + +require ( + github.com/joho/godotenv v1.5.1 + github.com/samber/lo v1.47.0 +) diff --git a/go.sum b/go.sum index 5c97769db..c68c02547 100644 --- a/go.sum +++ b/go.sum @@ -294,6 +294,8 @@ github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPw github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jrick/logrotate v1.1.2 h1:6ePk462NCX7TfKtNp5JJ7MbA2YIslkpfgP03TlTYMN0= @@ -566,6 +568,8 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= diff --git a/node/exts/erc20reward/meta_extension.go b/node/exts/erc20reward/meta_extension.go index 81fce5d1b..52af3e407 100644 --- a/node/exts/erc20reward/meta_extension.go +++ b/node/exts/erc20reward/meta_extension.go @@ -505,7 +505,7 @@ func init() { }, Returns: &precompiles.MethodReturn{ Fields: []precompiles.PrecompileValue{ - {Name: "chain", Type: types.TextType}, + {Name: "chain_id", Type: types.TextType}, {Name: "escrow", Type: types.TextType}, {Name: "epoch_period", Type: types.TextType}, {Name: "erc20", Type: types.TextType, Nullable: true}, @@ -820,11 +820,15 @@ func init() { }, }, { - // lists epochs that have not been confirmed yet, but have been ended. - // It lists them in ascending order (e.g. oldest first). - Name: "list_unconfirmed_epochs", + // lists epochs after(non-include) given height, in ASC order. + // If confirmedOnly is true, only returns confirmed epochs. + // NOTE: only unfirmed epoch will return voters and vote_nonces + Name: "list_epochs", Parameters: []precompiles.PrecompileValue{ {Name: "id", Type: types.UUIDType}, + {Name: "after", Type: types.IntType}, + {Name: "limit", Type: types.IntType}, + {Name: "confirmed_only", Type: types.BoolType}, }, Returns: &precompiles.MethodReturn{ IsTable: true, @@ -835,13 +839,18 @@ func init() { {Name: "end_height", Type: types.IntType}, {Name: "reward_root", Type: types.ByteaType}, {Name: "end_block_hash", Type: types.ByteaType}, + {Name: "voters", Type: types.TextArrayType}, + {Name: "vote_nonces", Type: types.IntType}, }, }, AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { id := inputs[0].(*types.UUID) + after := inputs[1].(int64) + limit := inputs[2].(int64) + confirmedOnly := inputs[3].(bool) - return getUnconfirmedEpochs(ctx.TxContext.Ctx, app, id, func(e *Epoch) error { + return getEpochs(ctx.TxContext.Ctx, app, id, after, limit, confirmedOnly, func(e *Epoch) error { return resultFn([]any{e.ID, e.StartHeight, e.StartTime.Unix(), *e.EndHeight, e.Root, e.BlockHash}) }) }, @@ -989,22 +998,63 @@ func init() { // return errors.New("propose_epoch can only be called by the Kwil network") // } - // panic("finish me") - // }, - // }, - // { - // // Supposed to be called by a multisig signer - // Name: "vote_epoch", - // Parameters: []precompiles.PrecompileValue{ - // {Name: "id", Type: types.UUIDType}, - // {Name: "sign_hash", Type: types.ByteaType}, - // {Name: "signatures", Type: types.ByteaType}, - // }, - // AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC}, - // Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { - // panic("finish me") - // }, - // }, + { + // Supposed to be called by the SignerService, to verify the reward root. + Name: "get_epoch_rewards", + Parameters: []precompiles.PrecompileValue{ + {Name: "id", Type: types.UUIDType}, + {Name: "epoch_id", Type: types.UUIDType}, + }, + Returns: &precompiles.MethodReturn{ + IsTable: true, + Fields: []precompiles.PrecompileValue{ + {Name: "recipient", Type: types.TextType}, + {Name: "amount", Type: types.TextType}, + }, + }, + Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { + //id := inputs[0].(*types.UUID) + epochID := inputs[1].(*types.UUID) + return getRewardsForEpoch(ctx.TxContext.Ctx, app, epochID, func(reward *EpochReward) error { + return resultFn([]any{reward.Recipient.String(), reward.Amount.String()}) + }) + }, + }, + { + // Supposed to be called by a multisig signer + Name: "vote_epoch", + Parameters: []precompiles.PrecompileValue{ + {Name: "id", Type: types.UUIDType}, + {Name: "epoch_id", Type: types.UUIDType}, + {Name: "signature", Type: types.ByteaType}, + }, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC}, + Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { + //id := inputs[0].(*types.UUID) + epochID := inputs[1].(*types.UUID) + signature := inputs[2].([]byte) + + if len(signature) != reward.GnosisSafeSigLength { + return fmt.Errorf("signature is not 65 bytes") + } + + from, err := ethAddressFromHex(ctx.TxContext.Caller) + if err != nil { + return err + } + + confirmed, err := epochConfirmed(ctx.TxContext.Ctx, app, epochID) + if err != nil { + return fmt.Errorf("check epoch is confirmed: %w", err) + } + + if confirmed { + return fmt.Errorf("epoch is already confirmed") + } + + return voteEpoch(ctx.TxContext.Ctx, app, epochID, from, signature) + }, + }, // { // // Lists all epochs that have received enough votes // Name: "list_finalized", @@ -1097,7 +1147,11 @@ func init() { return nil } - rewards, err := getRewardsForEpoch(ctx, app, info.currentEpoch.ID) + var rewards []*EpochReward + err := getRewardsForEpoch(ctx, app, info.currentEpoch.ID, func(reward *EpochReward) error { + rewards = append(rewards, reward) + return nil + }) if err != nil { return err } @@ -1279,12 +1333,18 @@ func (p *PendingEpoch) copy() *PendingEpoch { } } +type EpochVoteInfo struct { + Voters []ethcommon.Address + VoteNonces []int64 +} + // Epoch is a period in which rewards are distributed. type Epoch struct { PendingEpoch EndHeight *int64 // nil if not finalized BlockHash []byte // hash of the block that finalized the epoch, nil if not finalized Root []byte // merkle root of all rewards, nil if not finalized + EpochVoteInfo } type extensionInfo struct { diff --git a/node/exts/erc20reward/meta_schema.sql b/node/exts/erc20reward/meta_schema.sql index d837d14c5..c1f20f949 100644 --- a/node/exts/erc20reward/meta_schema.sql +++ b/node/exts/erc20reward/meta_schema.sql @@ -63,6 +63,18 @@ CREATE TABLE epoch_rewards ( CREATE INDEX idx_epoch_rewards_epoch_id ON epoch_rewards(epoch_id); -CREATE TABLE meta ( +CREATE TABLE meta +( version INT8 PRIMARY KEY ); + +-- epoch_votes holds the votes from signer +-- A signer can vote multiple times with different safe_nonce +-- After an epoch is confirmed, we can delete all related votes. +CREATE TABLE epoch_votes ( + epoch_id UUID NOT NULL REFERENCES epochs(id) ON UPDATE RESTRICT ON DELETE RESTRICT, + voter TEXT NOT NULL, + signature BYTEA NOT NULL, + nonce INT8 NOT NULL, -- safe nonce; technically we don't need this, but this helps to identify why a signer is not valid + PRIMARY KEY (epoch_id, voter) +); \ No newline at end of file diff --git a/node/exts/erc20reward/meta_sql.go b/node/exts/erc20reward/meta_sql.go index c5dce2d83..18521ee1f 100644 --- a/node/exts/erc20reward/meta_sql.go +++ b/node/exts/erc20reward/meta_sql.go @@ -193,7 +193,7 @@ func bytesToEthAddress(bts []byte) (ethcommon.Address, error) { // creditBalance credits a balance to a user. // The rewardId is the ID of the reward instance. -// It if is negative, it will subtract. +// If it is negative, it will subtract. func creditBalance(ctx context.Context, app *common.App, rewardId *types.UUID, user ethcommon.Address, amount *types.Decimal) error { balanceId := userBalanceID(rewardId, user) return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` @@ -328,9 +328,8 @@ func balanceOf(ctx context.Context, app *common.App, rewardID *types.UUID, user } // getRewardsForEpoch gets all rewards for an epoch. -func getRewardsForEpoch(ctx context.Context, app *common.App, epochID *types.UUID) ([]*EpochReward, error) { - var rewards []*EpochReward - err := app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` +func getRewardsForEpoch(ctx context.Context, app *common.App, epochID *types.UUID, fn func(reward *EpochReward) error) error { + return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` {kwil_erc20_meta}SELECT recipient, amount FROM epoch_rewards WHERE epoch_id = $epoch_id @@ -346,27 +345,29 @@ func getRewardsForEpoch(ctx context.Context, app *common.App, epochID *types.UUI return err } - rewards = append(rewards, &EpochReward{ + return fn(&EpochReward{ Recipient: recipient, Amount: row.Values[1].(*types.Decimal), }) - return nil }) - if err != nil { - return nil, err - } - return rewards, nil } -// getUnconfirmedEpochs gets all unconfirmed epochs. -func getUnconfirmedEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, fn func(*Epoch) error) error { - return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` - SELECT id, created_at_block, created_at_unix, ended_at, block_hash, reward_root - FROM epochs - WHERE instance_id = $instance_id AND confirmed IS FALSE - ORDER BY ended_at ASC - `, map[string]any{ +// getEpochs gets epochs by given conditions. +func getEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, after int64, limit int64, confirmedOnly bool, fn func(*Epoch) error) error { + query := ` + SELECT e.id, e.created_at_block, e.created_at_unix, e.ended_at, e.block_hash, e.reward_root, array_agg(v.voter) as voters, array_agg(v.nonce) as signatures + FROM epochs e + JOIN epoch_votes as v ON v.epoch_id = epochs.id + WHERE instance_id = $instance_id AND ended_at_block > $after` + if confirmedOnly { + query += ` AND confirmed IS $confirmed` + } + query += ` ORDER BY ended_at ASC LIMIT $limit` + return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, query, map[string]any{ "instance_id": instanceID, + "after": after, + "limit": limit, + "confirmed": confirmedOnly, }, func(r *common.Row) error { if len(r.Values) != 6 { return fmt.Errorf("expected 6 values, got %d", len(r.Values)) @@ -435,3 +436,48 @@ func setVersionToCurrent(ctx context.Context, app *common.App) error { "version": currentVersion, }, nil) } + +func epochConfirmed(ctx context.Context, app *common.App, epochID *types.UUID) (bool, error) { + var confirmed bool + err := app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` + {kwil_erc20_meta}SELECT confirmed + FROM epochs WHERE id = $id; + `, map[string]any{ + "id": epochID, + }, func(row *common.Row) error { + if len(row.Values) != 1 { + return fmt.Errorf("expected 1 value, got %d", len(row.Values)) + } + + confirmed = row.Values[0].(bool) + return nil + }) + + if err != nil { + return false, err + } + + return confirmed, nil +} + +// voteEpoch vote an epoch by submitting signature. +func voteEpoch(ctx context.Context, app *common.App, epochID *types.UUID, voter ethcommon.Address, signature []byte) error { + return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` + {kwil_erc20_meta}INSERT into epoch_votes(epoch_id, voter, signature) + VALUES ($epoch_id, $voter, $signature); + `, map[string]any{ + "epoch_id": epochID, + "voter": voter.Bytes(), + "signature": signature, + }, nil) +} + +// removeEpochVotes removes all votes associated with an epoch. +func removeEpochVotes(ctx context.Context, app *common.App, epochID *types.UUID) error { + return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` + {kwil_erc20_meta}DELETE FROM epoch_votes + WHERE epoch_id = $epoch_id; + `, map[string]any{ + "epoch_id": epochID, + }, nil) +} diff --git a/node/exts/erc20reward/named_extension.go b/node/exts/erc20reward/named_extension.go index e4630114f..50dbbcf9a 100644 --- a/node/exts/erc20reward/named_extension.go +++ b/node/exts/erc20reward/named_extension.go @@ -41,7 +41,7 @@ func init() { var distributionPeriod string distributionPeriodAny, ok := metadata["distribution_period"] if !ok { - distributionPeriod = "24h" + distributionPeriod = "24h" // 'h' is the highest units supported } else { distributionPeriod, ok = distributionPeriodAny.(string) if !ok { @@ -171,7 +171,13 @@ func init() { Handler: makeMetaHandler("balance"), }, { - Name: "list_unconfirmed_epochs", + Name: "list_epochs", + Parameters: []precompiles.PrecompileValue{ + {Name: "id", Type: types.UUIDType}, + {Name: "after", Type: types.IntType}, + {Name: "limit", Type: types.IntType}, + {Name: "confirmed_only", Type: types.BoolType}, + }, Returns: &precompiles.MethodReturn{ IsTable: true, Fields: []precompiles.PrecompileValue{ diff --git a/node/exts/erc20reward/reward/crypto.go b/node/exts/erc20reward/reward/crypto.go index a1b87e497..f1bdd1ec1 100644 --- a/node/exts/erc20reward/reward/crypto.go +++ b/node/exts/erc20reward/reward/crypto.go @@ -23,6 +23,8 @@ const ContractABI = `[{"name":"postReward","type":"function","inputs":[{"name":" {"name":"updatePosterFee","type":"function","inputs":[{"name":"newFee","type":"uint256"}],"outputs":[]}, {"name":"rewardPoster","type":"function","inputs":[{"name":"root","type":"bytes32"}],"outputs":[{"name":"","type":"address"}]}]` +const GnosisSafeSigLength = ethCrypto.SignatureLength + func GenPostRewardTxData(root []byte, amount *big.Int) ([]byte, error) { // Parse the ABI parsedABI, err := ethAbi.JSON(strings.NewReader(ContractABI)) @@ -45,16 +47,16 @@ func GenPostRewardTxData(root []byte, amount *big.Int) ([]byte, error) { // GenGnosisSafeTx returns a safe tx, and the tx hash to be used to generate signature. // More info: https://docs.safe.global/sdk/protocol-kit/guides/signatures/transactions -// Since Gnosis 1.3.0, ChainId is a part of the EIP-712 domain. -func GenGnosisSafeTx(to, safe string, value int64, data hexutil.Bytes, chainID int64, - nonce int64) (*core.GnosisSafeTx, []byte, error) { +// Since Gnosis 1.3.0, ChainID is a part of the EIP-712 domain. +func GenGnosisSafeTx(to, safe string, value int64, data hexutil.Bytes, chainID math.HexOrDecimal256, + nonce big.Int) (*core.GnosisSafeTx, []byte, error) { gnosisSafeTx := core.GnosisSafeTx{ To: ethCommon.NewMixedcaseAddress(ethCommon.HexToAddress(to)), Value: *math.NewDecimal256(value), Data: &data, Operation: 0, // Call - ChainId: math.NewHexOrDecimal256(chainID), + ChainId: &chainID, Safe: ethCommon.NewMixedcaseAddress(ethCommon.HexToAddress(safe)), // NOTE: we ignore all those parameters since we're generating off-chain @@ -66,7 +68,7 @@ func GenGnosisSafeTx(to, safe string, value int64, data hexutil.Bytes, chainID i //SafeTxGas: big.Int{}, //SafeTxGas: *big.NewInt(*safeTxGas), - Nonce: *big.NewInt(nonce), + Nonce: nonce, // not sure what's the purpose of this field InputExpHash: ethCommon.Hash{}, @@ -136,9 +138,9 @@ func EthGnosisSignDigest(digest []byte, key *ecdsa.PrivateKey) ([]byte, error) { func EthGnosisVerifyDigest(sig []byte, digest []byte, address []byte) error { // signature is 65 bytes, [R || S || V] format - if len(sig) != ethCrypto.SignatureLength { + if len(sig) != GnosisSafeSigLength { return fmt.Errorf("invalid signature length: expected %d, received %d", - ethCrypto.SignatureLength, len(sig)) + GnosisSafeSigLength, len(sig)) } if sig[ethCrypto.RecoveryIDOffset] != 31 && sig[ethCrypto.RecoveryIDOffset] != 32 { @@ -164,9 +166,9 @@ func EthGnosisVerifyDigest(sig []byte, digest []byte, address []byte) error { func EthGnosisRecoverSigner(sig []byte, digest []byte) (*ethCommon.Address, error) { // signature is 65 bytes, [R || S || V] format - if len(sig) != ethCrypto.SignatureLength { + if len(sig) != GnosisSafeSigLength { return nil, fmt.Errorf("invalid signature length: expected %d, received %d", - ethCrypto.SignatureLength, len(sig)) + GnosisSafeSigLength, len(sig)) } if sig[ethCrypto.RecoveryIDOffset] != 31 && sig[ethCrypto.RecoveryIDOffset] != 32 { diff --git a/node/services/erc20signersvc/.gitignore b/node/services/erc20signersvc/.gitignore new file mode 100644 index 000000000..2eea525d8 --- /dev/null +++ b/node/services/erc20signersvc/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/node/services/erc20signersvc/abigen/multicall3.go b/node/services/erc20signersvc/abigen/multicall3.go new file mode 100644 index 000000000..41bfe516c --- /dev/null +++ b/node/services/erc20signersvc/abigen/multicall3.go @@ -0,0 +1,644 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package abigen + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// Multicall3Call is an auto generated low-level Go binding around an user-defined struct. +type Multicall3Call struct { + Target common.Address + CallData []byte +} + +// Multicall3Call3 is an auto generated low-level Go binding around an user-defined struct. +type Multicall3Call3 struct { + Target common.Address + AllowFailure bool + CallData []byte +} + +// Multicall3Call3Value is an auto generated low-level Go binding around an user-defined struct. +type Multicall3Call3Value struct { + Target common.Address + AllowFailure bool + Value *big.Int + CallData []byte +} + +// Multicall3Result is an auto generated low-level Go binding around an user-defined struct. +type Multicall3Result struct { + Success bool + ReturnData []byte +} + +// Multicall3MetaData contains all meta data concerning the Multicall3 contract. +var Multicall3MetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"structMulticall3.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"aggregate\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"bytes[]\",\"name\":\"returnData\",\"type\":\"bytes[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"allowFailure\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"structMulticall3.Call3[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"aggregate3\",\"outputs\":[{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"returnData\",\"type\":\"bytes\"}],\"internalType\":\"structMulticall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"allowFailure\",\"type\":\"bool\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"structMulticall3.Call3Value[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"aggregate3Value\",\"outputs\":[{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"returnData\",\"type\":\"bytes\"}],\"internalType\":\"structMulticall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"structMulticall3.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"blockAndAggregate\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"returnData\",\"type\":\"bytes\"}],\"internalType\":\"structMulticall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getBasefee\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"basefee\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"}],\"name\":\"getBlockHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getBlockNumber\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getChainId\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"chainid\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockCoinbase\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"coinbase\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockDifficulty\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"difficulty\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockGasLimit\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"gaslimit\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockTimestamp\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"}],\"name\":\"getEthBalance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"balance\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getLastBlockHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bool\",\"name\":\"requireSuccess\",\"type\":\"bool\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"structMulticall3.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"tryAggregate\",\"outputs\":[{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"returnData\",\"type\":\"bytes\"}],\"internalType\":\"structMulticall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bool\",\"name\":\"requireSuccess\",\"type\":\"bool\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"structMulticall3.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"tryBlockAndAggregate\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"returnData\",\"type\":\"bytes\"}],\"internalType\":\"structMulticall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"}]", +} + +// Multicall3ABI is the input ABI used to generate the binding from. +// Deprecated: Use Multicall3MetaData.ABI instead. +var Multicall3ABI = Multicall3MetaData.ABI + +// Multicall3 is an auto generated Go binding around an Ethereum contract. +type Multicall3 struct { + Multicall3Caller // Read-only binding to the contract + Multicall3Transactor // Write-only binding to the contract + Multicall3Filterer // Log filterer for contract events +} + +// Multicall3Caller is an auto generated read-only Go binding around an Ethereum contract. +type Multicall3Caller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// Multicall3Transactor is an auto generated write-only Go binding around an Ethereum contract. +type Multicall3Transactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// Multicall3Filterer is an auto generated log filtering Go binding around an Ethereum contract events. +type Multicall3Filterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// Multicall3Session is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type Multicall3Session struct { + Contract *Multicall3 // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// Multicall3CallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type Multicall3CallerSession struct { + Contract *Multicall3Caller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// Multicall3TransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type Multicall3TransactorSession struct { + Contract *Multicall3Transactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// Multicall3Raw is an auto generated low-level Go binding around an Ethereum contract. +type Multicall3Raw struct { + Contract *Multicall3 // Generic contract binding to access the raw methods on +} + +// Multicall3CallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type Multicall3CallerRaw struct { + Contract *Multicall3Caller // Generic read-only contract binding to access the raw methods on +} + +// Multicall3TransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type Multicall3TransactorRaw struct { + Contract *Multicall3Transactor // Generic write-only contract binding to access the raw methods on +} + +// NewMulticall3 creates a new instance of Multicall3, bound to a specific deployed contract. +func NewMulticall3(address common.Address, backend bind.ContractBackend) (*Multicall3, error) { + contract, err := bindMulticall3(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Multicall3{Multicall3Caller: Multicall3Caller{contract: contract}, Multicall3Transactor: Multicall3Transactor{contract: contract}, Multicall3Filterer: Multicall3Filterer{contract: contract}}, nil +} + +// NewMulticall3Caller creates a new read-only instance of Multicall3, bound to a specific deployed contract. +func NewMulticall3Caller(address common.Address, caller bind.ContractCaller) (*Multicall3Caller, error) { + contract, err := bindMulticall3(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &Multicall3Caller{contract: contract}, nil +} + +// NewMulticall3Transactor creates a new write-only instance of Multicall3, bound to a specific deployed contract. +func NewMulticall3Transactor(address common.Address, transactor bind.ContractTransactor) (*Multicall3Transactor, error) { + contract, err := bindMulticall3(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &Multicall3Transactor{contract: contract}, nil +} + +// NewMulticall3Filterer creates a new log filterer instance of Multicall3, bound to a specific deployed contract. +func NewMulticall3Filterer(address common.Address, filterer bind.ContractFilterer) (*Multicall3Filterer, error) { + contract, err := bindMulticall3(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &Multicall3Filterer{contract: contract}, nil +} + +// bindMulticall3 binds a generic wrapper to an already deployed contract. +func bindMulticall3(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := Multicall3MetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Multicall3 *Multicall3Raw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Multicall3.Contract.Multicall3Caller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Multicall3 *Multicall3Raw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Multicall3.Contract.Multicall3Transactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Multicall3 *Multicall3Raw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Multicall3.Contract.Multicall3Transactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Multicall3 *Multicall3CallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Multicall3.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Multicall3 *Multicall3TransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Multicall3.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Multicall3 *Multicall3TransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Multicall3.Contract.contract.Transact(opts, method, params...) +} + +// GetBasefee is a free data retrieval call binding the contract method 0x3e64a696. +// +// Solidity: function getBasefee() view returns(uint256 basefee) +func (_Multicall3 *Multicall3Caller) GetBasefee(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Multicall3.contract.Call(opts, &out, "getBasefee") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetBasefee is a free data retrieval call binding the contract method 0x3e64a696. +// +// Solidity: function getBasefee() view returns(uint256 basefee) +func (_Multicall3 *Multicall3Session) GetBasefee() (*big.Int, error) { + return _Multicall3.Contract.GetBasefee(&_Multicall3.CallOpts) +} + +// GetBasefee is a free data retrieval call binding the contract method 0x3e64a696. +// +// Solidity: function getBasefee() view returns(uint256 basefee) +func (_Multicall3 *Multicall3CallerSession) GetBasefee() (*big.Int, error) { + return _Multicall3.Contract.GetBasefee(&_Multicall3.CallOpts) +} + +// GetBlockHash is a free data retrieval call binding the contract method 0xee82ac5e. +// +// Solidity: function getBlockHash(uint256 blockNumber) view returns(bytes32 blockHash) +func (_Multicall3 *Multicall3Caller) GetBlockHash(opts *bind.CallOpts, blockNumber *big.Int) ([32]byte, error) { + var out []interface{} + err := _Multicall3.contract.Call(opts, &out, "getBlockHash", blockNumber) + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// GetBlockHash is a free data retrieval call binding the contract method 0xee82ac5e. +// +// Solidity: function getBlockHash(uint256 blockNumber) view returns(bytes32 blockHash) +func (_Multicall3 *Multicall3Session) GetBlockHash(blockNumber *big.Int) ([32]byte, error) { + return _Multicall3.Contract.GetBlockHash(&_Multicall3.CallOpts, blockNumber) +} + +// GetBlockHash is a free data retrieval call binding the contract method 0xee82ac5e. +// +// Solidity: function getBlockHash(uint256 blockNumber) view returns(bytes32 blockHash) +func (_Multicall3 *Multicall3CallerSession) GetBlockHash(blockNumber *big.Int) ([32]byte, error) { + return _Multicall3.Contract.GetBlockHash(&_Multicall3.CallOpts, blockNumber) +} + +// GetBlockNumber is a free data retrieval call binding the contract method 0x42cbb15c. +// +// Solidity: function getBlockNumber() view returns(uint256 blockNumber) +func (_Multicall3 *Multicall3Caller) GetBlockNumber(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Multicall3.contract.Call(opts, &out, "getBlockNumber") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetBlockNumber is a free data retrieval call binding the contract method 0x42cbb15c. +// +// Solidity: function getBlockNumber() view returns(uint256 blockNumber) +func (_Multicall3 *Multicall3Session) GetBlockNumber() (*big.Int, error) { + return _Multicall3.Contract.GetBlockNumber(&_Multicall3.CallOpts) +} + +// GetBlockNumber is a free data retrieval call binding the contract method 0x42cbb15c. +// +// Solidity: function getBlockNumber() view returns(uint256 blockNumber) +func (_Multicall3 *Multicall3CallerSession) GetBlockNumber() (*big.Int, error) { + return _Multicall3.Contract.GetBlockNumber(&_Multicall3.CallOpts) +} + +// GetChainId is a free data retrieval call binding the contract method 0x3408e470. +// +// Solidity: function getChainId() view returns(uint256 chainid) +func (_Multicall3 *Multicall3Caller) GetChainId(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Multicall3.contract.Call(opts, &out, "getChainId") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetChainId is a free data retrieval call binding the contract method 0x3408e470. +// +// Solidity: function getChainId() view returns(uint256 chainid) +func (_Multicall3 *Multicall3Session) GetChainId() (*big.Int, error) { + return _Multicall3.Contract.GetChainId(&_Multicall3.CallOpts) +} + +// GetChainId is a free data retrieval call binding the contract method 0x3408e470. +// +// Solidity: function getChainId() view returns(uint256 chainid) +func (_Multicall3 *Multicall3CallerSession) GetChainId() (*big.Int, error) { + return _Multicall3.Contract.GetChainId(&_Multicall3.CallOpts) +} + +// GetCurrentBlockCoinbase is a free data retrieval call binding the contract method 0xa8b0574e. +// +// Solidity: function getCurrentBlockCoinbase() view returns(address coinbase) +func (_Multicall3 *Multicall3Caller) GetCurrentBlockCoinbase(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _Multicall3.contract.Call(opts, &out, "getCurrentBlockCoinbase") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// GetCurrentBlockCoinbase is a free data retrieval call binding the contract method 0xa8b0574e. +// +// Solidity: function getCurrentBlockCoinbase() view returns(address coinbase) +func (_Multicall3 *Multicall3Session) GetCurrentBlockCoinbase() (common.Address, error) { + return _Multicall3.Contract.GetCurrentBlockCoinbase(&_Multicall3.CallOpts) +} + +// GetCurrentBlockCoinbase is a free data retrieval call binding the contract method 0xa8b0574e. +// +// Solidity: function getCurrentBlockCoinbase() view returns(address coinbase) +func (_Multicall3 *Multicall3CallerSession) GetCurrentBlockCoinbase() (common.Address, error) { + return _Multicall3.Contract.GetCurrentBlockCoinbase(&_Multicall3.CallOpts) +} + +// GetCurrentBlockDifficulty is a free data retrieval call binding the contract method 0x72425d9d. +// +// Solidity: function getCurrentBlockDifficulty() view returns(uint256 difficulty) +func (_Multicall3 *Multicall3Caller) GetCurrentBlockDifficulty(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Multicall3.contract.Call(opts, &out, "getCurrentBlockDifficulty") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetCurrentBlockDifficulty is a free data retrieval call binding the contract method 0x72425d9d. +// +// Solidity: function getCurrentBlockDifficulty() view returns(uint256 difficulty) +func (_Multicall3 *Multicall3Session) GetCurrentBlockDifficulty() (*big.Int, error) { + return _Multicall3.Contract.GetCurrentBlockDifficulty(&_Multicall3.CallOpts) +} + +// GetCurrentBlockDifficulty is a free data retrieval call binding the contract method 0x72425d9d. +// +// Solidity: function getCurrentBlockDifficulty() view returns(uint256 difficulty) +func (_Multicall3 *Multicall3CallerSession) GetCurrentBlockDifficulty() (*big.Int, error) { + return _Multicall3.Contract.GetCurrentBlockDifficulty(&_Multicall3.CallOpts) +} + +// GetCurrentBlockGasLimit is a free data retrieval call binding the contract method 0x86d516e8. +// +// Solidity: function getCurrentBlockGasLimit() view returns(uint256 gaslimit) +func (_Multicall3 *Multicall3Caller) GetCurrentBlockGasLimit(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Multicall3.contract.Call(opts, &out, "getCurrentBlockGasLimit") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetCurrentBlockGasLimit is a free data retrieval call binding the contract method 0x86d516e8. +// +// Solidity: function getCurrentBlockGasLimit() view returns(uint256 gaslimit) +func (_Multicall3 *Multicall3Session) GetCurrentBlockGasLimit() (*big.Int, error) { + return _Multicall3.Contract.GetCurrentBlockGasLimit(&_Multicall3.CallOpts) +} + +// GetCurrentBlockGasLimit is a free data retrieval call binding the contract method 0x86d516e8. +// +// Solidity: function getCurrentBlockGasLimit() view returns(uint256 gaslimit) +func (_Multicall3 *Multicall3CallerSession) GetCurrentBlockGasLimit() (*big.Int, error) { + return _Multicall3.Contract.GetCurrentBlockGasLimit(&_Multicall3.CallOpts) +} + +// GetCurrentBlockTimestamp is a free data retrieval call binding the contract method 0x0f28c97d. +// +// Solidity: function getCurrentBlockTimestamp() view returns(uint256 timestamp) +func (_Multicall3 *Multicall3Caller) GetCurrentBlockTimestamp(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Multicall3.contract.Call(opts, &out, "getCurrentBlockTimestamp") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetCurrentBlockTimestamp is a free data retrieval call binding the contract method 0x0f28c97d. +// +// Solidity: function getCurrentBlockTimestamp() view returns(uint256 timestamp) +func (_Multicall3 *Multicall3Session) GetCurrentBlockTimestamp() (*big.Int, error) { + return _Multicall3.Contract.GetCurrentBlockTimestamp(&_Multicall3.CallOpts) +} + +// GetCurrentBlockTimestamp is a free data retrieval call binding the contract method 0x0f28c97d. +// +// Solidity: function getCurrentBlockTimestamp() view returns(uint256 timestamp) +func (_Multicall3 *Multicall3CallerSession) GetCurrentBlockTimestamp() (*big.Int, error) { + return _Multicall3.Contract.GetCurrentBlockTimestamp(&_Multicall3.CallOpts) +} + +// GetEthBalance is a free data retrieval call binding the contract method 0x4d2301cc. +// +// Solidity: function getEthBalance(address addr) view returns(uint256 balance) +func (_Multicall3 *Multicall3Caller) GetEthBalance(opts *bind.CallOpts, addr common.Address) (*big.Int, error) { + var out []interface{} + err := _Multicall3.contract.Call(opts, &out, "getEthBalance", addr) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetEthBalance is a free data retrieval call binding the contract method 0x4d2301cc. +// +// Solidity: function getEthBalance(address addr) view returns(uint256 balance) +func (_Multicall3 *Multicall3Session) GetEthBalance(addr common.Address) (*big.Int, error) { + return _Multicall3.Contract.GetEthBalance(&_Multicall3.CallOpts, addr) +} + +// GetEthBalance is a free data retrieval call binding the contract method 0x4d2301cc. +// +// Solidity: function getEthBalance(address addr) view returns(uint256 balance) +func (_Multicall3 *Multicall3CallerSession) GetEthBalance(addr common.Address) (*big.Int, error) { + return _Multicall3.Contract.GetEthBalance(&_Multicall3.CallOpts, addr) +} + +// GetLastBlockHash is a free data retrieval call binding the contract method 0x27e86d6e. +// +// Solidity: function getLastBlockHash() view returns(bytes32 blockHash) +func (_Multicall3 *Multicall3Caller) GetLastBlockHash(opts *bind.CallOpts) ([32]byte, error) { + var out []interface{} + err := _Multicall3.contract.Call(opts, &out, "getLastBlockHash") + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// GetLastBlockHash is a free data retrieval call binding the contract method 0x27e86d6e. +// +// Solidity: function getLastBlockHash() view returns(bytes32 blockHash) +func (_Multicall3 *Multicall3Session) GetLastBlockHash() ([32]byte, error) { + return _Multicall3.Contract.GetLastBlockHash(&_Multicall3.CallOpts) +} + +// GetLastBlockHash is a free data retrieval call binding the contract method 0x27e86d6e. +// +// Solidity: function getLastBlockHash() view returns(bytes32 blockHash) +func (_Multicall3 *Multicall3CallerSession) GetLastBlockHash() ([32]byte, error) { + return _Multicall3.Contract.GetLastBlockHash(&_Multicall3.CallOpts) +} + +// Aggregate is a paid mutator transaction binding the contract method 0x252dba42. +// +// Solidity: function aggregate((address,bytes)[] calls) payable returns(uint256 blockNumber, bytes[] returnData) +func (_Multicall3 *Multicall3Transactor) Aggregate(opts *bind.TransactOpts, calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.contract.Transact(opts, "aggregate", calls) +} + +// Aggregate is a paid mutator transaction binding the contract method 0x252dba42. +// +// Solidity: function aggregate((address,bytes)[] calls) payable returns(uint256 blockNumber, bytes[] returnData) +func (_Multicall3 *Multicall3Session) Aggregate(calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.Contract.Aggregate(&_Multicall3.TransactOpts, calls) +} + +// Aggregate is a paid mutator transaction binding the contract method 0x252dba42. +// +// Solidity: function aggregate((address,bytes)[] calls) payable returns(uint256 blockNumber, bytes[] returnData) +func (_Multicall3 *Multicall3TransactorSession) Aggregate(calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.Contract.Aggregate(&_Multicall3.TransactOpts, calls) +} + +// Aggregate3 is a paid mutator transaction binding the contract method 0x82ad56cb. +// +// Solidity: function aggregate3((address,bool,bytes)[] calls) payable returns((bool,bytes)[] returnData) +func (_Multicall3 *Multicall3Transactor) Aggregate3(opts *bind.TransactOpts, calls []Multicall3Call3) (*types.Transaction, error) { + return _Multicall3.contract.Transact(opts, "aggregate3", calls) +} + +// Aggregate3 is a paid mutator transaction binding the contract method 0x82ad56cb. +// +// Solidity: function aggregate3((address,bool,bytes)[] calls) payable returns((bool,bytes)[] returnData) +func (_Multicall3 *Multicall3Session) Aggregate3(calls []Multicall3Call3) (*types.Transaction, error) { + return _Multicall3.Contract.Aggregate3(&_Multicall3.TransactOpts, calls) +} + +// Aggregate3 is a paid mutator transaction binding the contract method 0x82ad56cb. +// +// Solidity: function aggregate3((address,bool,bytes)[] calls) payable returns((bool,bytes)[] returnData) +func (_Multicall3 *Multicall3TransactorSession) Aggregate3(calls []Multicall3Call3) (*types.Transaction, error) { + return _Multicall3.Contract.Aggregate3(&_Multicall3.TransactOpts, calls) +} + +// Aggregate3Value is a paid mutator transaction binding the contract method 0x174dea71. +// +// Solidity: function aggregate3Value((address,bool,uint256,bytes)[] calls) payable returns((bool,bytes)[] returnData) +func (_Multicall3 *Multicall3Transactor) Aggregate3Value(opts *bind.TransactOpts, calls []Multicall3Call3Value) (*types.Transaction, error) { + return _Multicall3.contract.Transact(opts, "aggregate3Value", calls) +} + +// Aggregate3Value is a paid mutator transaction binding the contract method 0x174dea71. +// +// Solidity: function aggregate3Value((address,bool,uint256,bytes)[] calls) payable returns((bool,bytes)[] returnData) +func (_Multicall3 *Multicall3Session) Aggregate3Value(calls []Multicall3Call3Value) (*types.Transaction, error) { + return _Multicall3.Contract.Aggregate3Value(&_Multicall3.TransactOpts, calls) +} + +// Aggregate3Value is a paid mutator transaction binding the contract method 0x174dea71. +// +// Solidity: function aggregate3Value((address,bool,uint256,bytes)[] calls) payable returns((bool,bytes)[] returnData) +func (_Multicall3 *Multicall3TransactorSession) Aggregate3Value(calls []Multicall3Call3Value) (*types.Transaction, error) { + return _Multicall3.Contract.Aggregate3Value(&_Multicall3.TransactOpts, calls) +} + +// BlockAndAggregate is a paid mutator transaction binding the contract method 0xc3077fa9. +// +// Solidity: function blockAndAggregate((address,bytes)[] calls) payable returns(uint256 blockNumber, bytes32 blockHash, (bool,bytes)[] returnData) +func (_Multicall3 *Multicall3Transactor) BlockAndAggregate(opts *bind.TransactOpts, calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.contract.Transact(opts, "blockAndAggregate", calls) +} + +// BlockAndAggregate is a paid mutator transaction binding the contract method 0xc3077fa9. +// +// Solidity: function blockAndAggregate((address,bytes)[] calls) payable returns(uint256 blockNumber, bytes32 blockHash, (bool,bytes)[] returnData) +func (_Multicall3 *Multicall3Session) BlockAndAggregate(calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.Contract.BlockAndAggregate(&_Multicall3.TransactOpts, calls) +} + +// BlockAndAggregate is a paid mutator transaction binding the contract method 0xc3077fa9. +// +// Solidity: function blockAndAggregate((address,bytes)[] calls) payable returns(uint256 blockNumber, bytes32 blockHash, (bool,bytes)[] returnData) +func (_Multicall3 *Multicall3TransactorSession) BlockAndAggregate(calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.Contract.BlockAndAggregate(&_Multicall3.TransactOpts, calls) +} + +// TryAggregate is a paid mutator transaction binding the contract method 0xbce38bd7. +// +// Solidity: function tryAggregate(bool requireSuccess, (address,bytes)[] calls) payable returns((bool,bytes)[] returnData) +func (_Multicall3 *Multicall3Transactor) TryAggregate(opts *bind.TransactOpts, requireSuccess bool, calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.contract.Transact(opts, "tryAggregate", requireSuccess, calls) +} + +// TryAggregate is a paid mutator transaction binding the contract method 0xbce38bd7. +// +// Solidity: function tryAggregate(bool requireSuccess, (address,bytes)[] calls) payable returns((bool,bytes)[] returnData) +func (_Multicall3 *Multicall3Session) TryAggregate(requireSuccess bool, calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.Contract.TryAggregate(&_Multicall3.TransactOpts, requireSuccess, calls) +} + +// TryAggregate is a paid mutator transaction binding the contract method 0xbce38bd7. +// +// Solidity: function tryAggregate(bool requireSuccess, (address,bytes)[] calls) payable returns((bool,bytes)[] returnData) +func (_Multicall3 *Multicall3TransactorSession) TryAggregate(requireSuccess bool, calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.Contract.TryAggregate(&_Multicall3.TransactOpts, requireSuccess, calls) +} + +// TryBlockAndAggregate is a paid mutator transaction binding the contract method 0x399542e9. +// +// Solidity: function tryBlockAndAggregate(bool requireSuccess, (address,bytes)[] calls) payable returns(uint256 blockNumber, bytes32 blockHash, (bool,bytes)[] returnData) +func (_Multicall3 *Multicall3Transactor) TryBlockAndAggregate(opts *bind.TransactOpts, requireSuccess bool, calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.contract.Transact(opts, "tryBlockAndAggregate", requireSuccess, calls) +} + +// TryBlockAndAggregate is a paid mutator transaction binding the contract method 0x399542e9. +// +// Solidity: function tryBlockAndAggregate(bool requireSuccess, (address,bytes)[] calls) payable returns(uint256 blockNumber, bytes32 blockHash, (bool,bytes)[] returnData) +func (_Multicall3 *Multicall3Session) TryBlockAndAggregate(requireSuccess bool, calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.Contract.TryBlockAndAggregate(&_Multicall3.TransactOpts, requireSuccess, calls) +} + +// TryBlockAndAggregate is a paid mutator transaction binding the contract method 0x399542e9. +// +// Solidity: function tryBlockAndAggregate(bool requireSuccess, (address,bytes)[] calls) payable returns(uint256 blockNumber, bytes32 blockHash, (bool,bytes)[] returnData) +func (_Multicall3 *Multicall3TransactorSession) TryBlockAndAggregate(requireSuccess bool, calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.Contract.TryBlockAndAggregate(&_Multicall3.TransactOpts, requireSuccess, calls) +} diff --git a/node/services/erc20signersvc/abigen/multicall3_abi.json b/node/services/erc20signersvc/abigen/multicall3_abi.json new file mode 100644 index 000000000..d9c5855e7 --- /dev/null +++ b/node/services/erc20signersvc/abigen/multicall3_abi.json @@ -0,0 +1,440 @@ +[ + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "aggregate", + "outputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "internalType": "bytes[]", + "name": "returnData", + "type": "bytes[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bool", + "name": "allowFailure", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Call3[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "aggregate3", + "outputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Result[]", + "name": "returnData", + "type": "tuple[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bool", + "name": "allowFailure", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Call3Value[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "aggregate3Value", + "outputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Result[]", + "name": "returnData", + "type": "tuple[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "blockAndAggregate", + "outputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "blockHash", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Result[]", + "name": "returnData", + "type": "tuple[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "getBasefee", + "outputs": [ + { + "internalType": "uint256", + "name": "basefee", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + } + ], + "name": "getBlockHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "blockHash", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getBlockNumber", + "outputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getChainId", + "outputs": [ + { + "internalType": "uint256", + "name": "chainid", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentBlockCoinbase", + "outputs": [ + { + "internalType": "address", + "name": "coinbase", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentBlockDifficulty", + "outputs": [ + { + "internalType": "uint256", + "name": "difficulty", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentBlockGasLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "gaslimit", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentBlockTimestamp", + "outputs": [ + { + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "name": "getEthBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLastBlockHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "blockHash", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "requireSuccess", + "type": "bool" + }, + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "tryAggregate", + "outputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Result[]", + "name": "returnData", + "type": "tuple[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "requireSuccess", + "type": "bool" + }, + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "tryBlockAndAggregate", + "outputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "blockHash", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Result[]", + "name": "returnData", + "type": "tuple[]" + } + ], + "stateMutability": "payable", + "type": "function" + } +] \ No newline at end of file diff --git a/node/services/erc20signersvc/abigen/safe.go b/node/services/erc20signersvc/abigen/safe.go new file mode 100644 index 000000000..16a29e58b --- /dev/null +++ b/node/services/erc20signersvc/abigen/safe.go @@ -0,0 +1,274 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package abigen + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// SafeMetaData contains all meta data concerning the Safe contract. +var SafeMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[],\"name\":\"nonce\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getThreshold\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getOwners\",\"outputs\":[{\"internalType\":\"address[]\",\"name\":\"\",\"type\":\"address[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", +} + +// SafeABI is the input ABI used to generate the binding from. +// Deprecated: Use SafeMetaData.ABI instead. +var SafeABI = SafeMetaData.ABI + +// Safe is an auto generated Go binding around an Ethereum contract. +type Safe struct { + SafeCaller // Read-only binding to the contract + SafeTransactor // Write-only binding to the contract + SafeFilterer // Log filterer for contract events +} + +// SafeCaller is an auto generated read-only Go binding around an Ethereum contract. +type SafeCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// SafeTransactor is an auto generated write-only Go binding around an Ethereum contract. +type SafeTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// SafeFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type SafeFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// SafeSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type SafeSession struct { + Contract *Safe // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// SafeCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type SafeCallerSession struct { + Contract *SafeCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// SafeTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type SafeTransactorSession struct { + Contract *SafeTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// SafeRaw is an auto generated low-level Go binding around an Ethereum contract. +type SafeRaw struct { + Contract *Safe // Generic contract binding to access the raw methods on +} + +// SafeCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type SafeCallerRaw struct { + Contract *SafeCaller // Generic read-only contract binding to access the raw methods on +} + +// SafeTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type SafeTransactorRaw struct { + Contract *SafeTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewSafe creates a new instance of Safe, bound to a specific deployed contract. +func NewSafe(address common.Address, backend bind.ContractBackend) (*Safe, error) { + contract, err := bindSafe(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Safe{SafeCaller: SafeCaller{contract: contract}, SafeTransactor: SafeTransactor{contract: contract}, SafeFilterer: SafeFilterer{contract: contract}}, nil +} + +// NewSafeCaller creates a new read-only instance of Safe, bound to a specific deployed contract. +func NewSafeCaller(address common.Address, caller bind.ContractCaller) (*SafeCaller, error) { + contract, err := bindSafe(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &SafeCaller{contract: contract}, nil +} + +// NewSafeTransactor creates a new write-only instance of Safe, bound to a specific deployed contract. +func NewSafeTransactor(address common.Address, transactor bind.ContractTransactor) (*SafeTransactor, error) { + contract, err := bindSafe(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &SafeTransactor{contract: contract}, nil +} + +// NewSafeFilterer creates a new log filterer instance of Safe, bound to a specific deployed contract. +func NewSafeFilterer(address common.Address, filterer bind.ContractFilterer) (*SafeFilterer, error) { + contract, err := bindSafe(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &SafeFilterer{contract: contract}, nil +} + +// bindSafe binds a generic wrapper to an already deployed contract. +func bindSafe(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := SafeMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Safe *SafeRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Safe.Contract.SafeCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Safe *SafeRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Safe.Contract.SafeTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Safe *SafeRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Safe.Contract.SafeTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Safe *SafeCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Safe.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Safe *SafeTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Safe.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Safe *SafeTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Safe.Contract.contract.Transact(opts, method, params...) +} + +// GetOwners is a free data retrieval call binding the contract method 0xa0e67e2b. +// +// Solidity: function getOwners() view returns(address[]) +func (_Safe *SafeCaller) GetOwners(opts *bind.CallOpts) ([]common.Address, error) { + var out []interface{} + err := _Safe.contract.Call(opts, &out, "getOwners") + + if err != nil { + return *new([]common.Address), err + } + + out0 := *abi.ConvertType(out[0], new([]common.Address)).(*[]common.Address) + + return out0, err + +} + +// GetOwners is a free data retrieval call binding the contract method 0xa0e67e2b. +// +// Solidity: function getOwners() view returns(address[]) +func (_Safe *SafeSession) GetOwners() ([]common.Address, error) { + return _Safe.Contract.GetOwners(&_Safe.CallOpts) +} + +// GetOwners is a free data retrieval call binding the contract method 0xa0e67e2b. +// +// Solidity: function getOwners() view returns(address[]) +func (_Safe *SafeCallerSession) GetOwners() ([]common.Address, error) { + return _Safe.Contract.GetOwners(&_Safe.CallOpts) +} + +// GetThreshold is a free data retrieval call binding the contract method 0xe75235b8. +// +// Solidity: function getThreshold() view returns(uint256) +func (_Safe *SafeCaller) GetThreshold(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Safe.contract.Call(opts, &out, "getThreshold") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetThreshold is a free data retrieval call binding the contract method 0xe75235b8. +// +// Solidity: function getThreshold() view returns(uint256) +func (_Safe *SafeSession) GetThreshold() (*big.Int, error) { + return _Safe.Contract.GetThreshold(&_Safe.CallOpts) +} + +// GetThreshold is a free data retrieval call binding the contract method 0xe75235b8. +// +// Solidity: function getThreshold() view returns(uint256) +func (_Safe *SafeCallerSession) GetThreshold() (*big.Int, error) { + return _Safe.Contract.GetThreshold(&_Safe.CallOpts) +} + +// Nonce is a free data retrieval call binding the contract method 0xaffed0e0. +// +// Solidity: function nonce() view returns(uint256) +func (_Safe *SafeCaller) Nonce(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Safe.contract.Call(opts, &out, "nonce") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// Nonce is a free data retrieval call binding the contract method 0xaffed0e0. +// +// Solidity: function nonce() view returns(uint256) +func (_Safe *SafeSession) Nonce() (*big.Int, error) { + return _Safe.Contract.Nonce(&_Safe.CallOpts) +} + +// Nonce is a free data retrieval call binding the contract method 0xaffed0e0. +// +// Solidity: function nonce() view returns(uint256) +func (_Safe *SafeCallerSession) Nonce() (*big.Int, error) { + return _Safe.Contract.Nonce(&_Safe.CallOpts) +} diff --git a/node/services/erc20signersvc/abigen/safe_abi.json b/node/services/erc20signersvc/abigen/safe_abi.json new file mode 100644 index 000000000..34f158781 --- /dev/null +++ b/node/services/erc20signersvc/abigen/safe_abi.json @@ -0,0 +1,41 @@ +[ + { + "inputs": [], + "name": "nonce", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getThreshold", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getOwners", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/node/services/erc20signersvc/eth.go b/node/services/erc20signersvc/eth.go new file mode 100644 index 000000000..e5249f92a --- /dev/null +++ b/node/services/erc20signersvc/eth.go @@ -0,0 +1,226 @@ +package signersvc + +import ( + "context" + "fmt" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/samber/lo" + + extabigen "github.com/kwilteam/kwil-db/node/exts/erc20reward/abigen" + "github.com/kwilteam/kwil-db/node/services/erc20signersvc/abigen" +) + +// +//func NewEthClient(rpc string) (*ethclient.Client, error) { +// return ethclient.Dial(rpc) +//} + +var ( + safeABI = lo.Must(abi.JSON(strings.NewReader(abigen.SafeMetaData.ABI))) + + nonceCallData = lo.Must(safeABI.Pack("nonce")) // nonce() + thresholdCallData = lo.Must(safeABI.Pack("getThreshold")) // getThreshold() + ownersCallData = lo.Must(safeABI.Pack("getOwners")) // getOwners() +) + +type safeMetadata struct { + threshold *big.Int + owners []common.Address + nonce *big.Int +} + +type Safe struct { + chainID *big.Int + addr common.Address + + safe *abigen.Safe + safeABI *abi.ABI + eth *ethclient.Client +} + +func NewSafeFromEscrow(rpc string, escrowAddr string) (*Safe, error) { + client, err := ethclient.Dial(rpc) + if err != nil { + return nil, fmt.Errorf("create eth cliet: %w", err) + } + + chainID, err := client.ChainID(context.Background()) + if err != nil { + return nil, fmt.Errorf("create eth chainID: %w", err) + } + + rd, err := extabigen.NewRewardDistributor(common.HexToAddress(escrowAddr), client) + if err != nil { + return nil, fmt.Errorf("create reward distributor: %w", err) + } + + safeAddr, err := rd.Safe(nil) + if err != nil { + return nil, fmt.Errorf("get safe address: %w", err) + } + + safe, err := abigen.NewSafe(safeAddr, client) + if err != nil { + return nil, fmt.Errorf("create safe: %w", err) + } + + return &Safe{ + chainID: chainID, + addr: safeAddr, + safe: safe, + safeABI: &safeABI, + eth: client, + }, nil +} + +func NewSafe(rpc string, addr string) (*Safe, error) { + client, err := ethclient.Dial(rpc) + if err != nil { + return nil, fmt.Errorf("create eth cliet: %w", err) + } + + chainID, err := client.ChainID(context.Background()) + if err != nil { + return nil, fmt.Errorf("create eth chainID: %w", err) + } + + safe, err := abigen.NewSafe(common.HexToAddress(addr), client) + if err != nil { + return nil, fmt.Errorf("create safe: %w", err) + } + + return &Safe{ + chainID: chainID, + addr: common.HexToAddress(addr), + safe: safe, + safeABI: &safeABI, + eth: client, + }, nil +} + +// height retrieves current block height. +func (s *Safe) height(ctx context.Context) (uint64, error) { + return s.eth.BlockNumber(ctx) +} + +// nonce retrieves the nonce of the Safe contract at a specified block number. +func (s *Safe) nonce(ctx context.Context, blockNumber *big.Int) (*big.Int, error) { + callOpts := &bind.CallOpts{ + Pending: false, + BlockNumber: blockNumber, + Context: ctx, + } + return s.safe.Nonce(callOpts) +} + +// threshold retrieves the threshold value of the Safe contract at a specified block number. +func (s *Safe) threshold(ctx context.Context, blockNumber *big.Int) (*big.Int, error) { + callOpts := &bind.CallOpts{ + Pending: false, + BlockNumber: blockNumber, + Context: ctx, + } + return s.safe.GetThreshold(callOpts) +} + +// owners retrieves the list of owner addresses of the Safe contract at a specified block number. +func (s *Safe) owners(ctx context.Context, blockNumber *big.Int) ([]common.Address, error) { + callOpts := &bind.CallOpts{ + Pending: false, + BlockNumber: blockNumber, + Context: ctx, + } + return s.safe.GetOwners(callOpts) +} + +func (s *Safe) latestMetadata(ctx context.Context) (*safeMetadata, error) { + height, err := s.height(ctx) + if err != nil { + return nil, err + } + + return s.metadata(ctx, new(big.Int).SetUint64(height)) +} + +func (s *Safe) metadata(ctx context.Context, blockNumber *big.Int) (*safeMetadata, error) { + if IsMulticall3Deployed(s.chainID.Uint64(), blockNumber) { + return s.getSafeMetadata3(ctx, blockNumber) + } + + return s.getSafeMetadataSeq(ctx, blockNumber) +} + +// getSafeMetadataSeq retrieves safe wallet metadata in sequence +func (s *Safe) getSafeMetadataSeq(ctx context.Context, blockNumber *big.Int) (*safeMetadata, error) { + nonce, err := s.nonce(ctx, blockNumber) + if err != nil { + return nil, err + } + + threshold, err := s.threshold(ctx, blockNumber) + if err != nil { + return nil, err + } + + owners, err := s.owners(ctx, blockNumber) + if err != nil { + return nil, err + } + + return &safeMetadata{ + threshold: threshold, + owners: owners, + nonce: nonce, + }, nil +} + +// getSafeMetadata3 retrieves safe wallet metadata in one go, using multicall3 +func (s *Safe) getSafeMetadata3(ctx context.Context, blockNumber *big.Int) (*safeMetadata, error) { + res, err := Aggregate3(ctx, s.chainID.Uint64(), []abigen.Multicall3Call3{ + { + Target: s.addr, + AllowFailure: false, + CallData: nonceCallData, + }, + { + Target: s.addr, + AllowFailure: false, + CallData: thresholdCallData, + }, + { + Target: s.addr, + AllowFailure: false, + CallData: ownersCallData, + }, + }, blockNumber, s.eth) + if err != nil { + return nil, err + } + + nonce, err := safeABI.Unpack("nonce", res[0].ReturnData) + if err != nil { + return nil, err + } + + threshold, err := safeABI.Unpack("getThreshold", res[1].ReturnData) + if err != nil { + return nil, err + } + + owners, err := safeABI.Unpack("getOwners", res[2].ReturnData) + if err != nil { + return nil, err + } + + return &safeMetadata{ + nonce: nonce[0].(*big.Int), + threshold: threshold[0].(*big.Int), + owners: owners[0].([]common.Address), + }, nil +} diff --git a/node/services/erc20signersvc/eth_test.go b/node/services/erc20signersvc/eth_test.go new file mode 100644 index 000000000..67813f226 --- /dev/null +++ b/node/services/erc20signersvc/eth_test.go @@ -0,0 +1,34 @@ +package signersvc + +import ( + "context" + "flag" + "math/big" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +var ethRpc = flag.String("eth-rpc", os.Getenv("ETH_RPC"), "eth provider rpc") + +func TestSafe_metadata(t *testing.T) { + if *ethRpc == "" { + t.Skip("no eth rpc configured") + } + + blockNumber := new(big.Int).SetUint64(7660784) + + s, err := NewSafe("11155111", *ethRpc, "0x56D510E4782cDed87F8B93D260282776adEd3f4B") + require.NoError(t, err) + + ctx := context.Background() + + got, err := s.getSafeMetadata3(ctx, blockNumber) + require.NoError(t, err) + + got2, err := s.getSafeMetadata(ctx, blockNumber) + require.NoError(t, err) + + require.EqualValues(t, got, got2) +} diff --git a/node/services/erc20signersvc/kwil.go b/node/services/erc20signersvc/kwil.go new file mode 100644 index 000000000..e985f9e01 --- /dev/null +++ b/node/services/erc20signersvc/kwil.go @@ -0,0 +1,204 @@ +package signersvc + +import ( + "context" + + "github.com/kwilteam/kwil-db/core/client" + clientTypes "github.com/kwilteam/kwil-db/core/client/types" + "github.com/kwilteam/kwil-db/core/types" +) + +type RewardInstanceInfo struct { + ChainID string + Escrow string + EpochPeriod int64 + Erc20 string + Decimals int64 + Balance string + Synced bool + SyncedAt int64 + Enabled bool +} + +// TODO: use the type from Ext? +type Epoch struct { + ID types.UUID + StartHeight int64 + StartTime int64 + EndHeight int64 + RewardRoot []byte + BlockHash []byte +} + +// TODO: use the type from Ext? +type FinalizedReward struct { + ID types.UUID + Voters []string + Signatures [][]byte + EpochID types.UUID + CreatedAt int64 + // + StartHeight int64 + EndHeight int64 + TotalRewards types.Decimal + RewardRoot []byte + SafeNonce int64 + SignHash []byte + ContractID types.UUID + BlockHash []byte +} + +type EpochReward struct { + Recipient string + Amount string +} + +// erc20ExtAPI defines the ERC20 reward extension API used by SignerSvc. +type erc20ExtAPI interface { + GetTarget() string + SetTarget(ns string) + InstanceInfo(tx context.Context) (*RewardInstanceInfo, error) + ListUnconfirmedEpochs(ctx context.Context, afterHeight int64, limit int) ([]*Epoch, error) + GetEpochRewards(ctx context.Context, epochID types.UUID) ([]*EpochReward, error) + VoteEpoch(ctx context.Context, rewardRoot []byte, signature []byte) (string, error) +} + +type erc20rwExtApi struct { + clt *client.Client + target string + instanceID string +} + +var _ erc20ExtAPI = (*erc20rwExtApi)(nil) + +func newERC20RWExtAPI(clt *client.Client, ns string) *erc20rwExtApi { + return &erc20rwExtApi{ + clt: clt, + target: ns, + } +} + +func (k *erc20rwExtApi) GetTarget() string { + return k.target +} + +func (k *erc20rwExtApi) SetTarget(ns string) { + k.target = ns +} + +func (k *erc20rwExtApi) InstanceInfo(ctx context.Context) (*RewardInstanceInfo, error) { + procedure := "info" + input := []any{} + + res, err := k.clt.Call(ctx, k.target, procedure, input) + if err != nil { + return nil, err + } + + if len(res.QueryResult.Values) == 0 { + return nil, nil + } + + er := &RewardInstanceInfo{} + err = types.ScanTo(res.QueryResult.Values[1], + &er.ChainID, &er.Escrow, &er.EpochPeriod, &er.Erc20, &er.Decimals, &er.Balance, &er.Synced, &er.SyncedAt, &er.Enabled) + if err != nil { + return nil, err + } + + return er, nil +} + +func (k *erc20rwExtApi) ListUnconfirmedEpochs(ctx context.Context, afterHeight int64, limit int) ([]*Epoch, error) { + procedure := "list_epochs" + + input := []any{afterHeight, limit, true} + + res, err := k.clt.Call(ctx, k.target, procedure, input) + if err != nil { + return nil, err + } + + if len(res.QueryResult.Values) == 0 { + return nil, nil + } + + ers := make([]*Epoch, len(res.QueryResult.Values)) + for i, v := range res.QueryResult.Values { + er := &Epoch{} + err = types.ScanTo(v, &er.ID, &er.StartHeight, &er.EndHeight, &er.TotalRewards, + &er.RewardRoot, &er.SafeNonce, &er.SignHash, &er.ContractID, &er.BlockHash, &er.CreatedAt, &er.Voters) + if err != nil { + return nil, err + } + ers[i] = er + } + + return ers, nil +} + +func (k *erc20rwExtApi) GetEpochRewards(ctx context.Context, epochID types.UUID) ([]*EpochReward, error) { + procedure := "get_epoch_rewards" + input := []any{epochID} + + res, err := k.clt.Call(ctx, k.target, procedure, input) + if err != nil { + return nil, err + } + + if len(res.QueryResult.Values) == 0 { + return nil, nil + } + + ers := make([]*EpochReward, len(res.QueryResult.Values)) + for i, v := range res.QueryResult.Values { + er := &EpochReward{} + err = types.ScanTo(v, &er.Recipient, &er.Amount) + if err != nil { + return nil, err + } + ers[i] = er + } + + return ers, nil +} + +//func (k *erc20rwExtApi) FetchLatestRewards(ctx context.Context, limit int) ([]*FinalizedReward, error) { +// procedure := "latest_finalized" +// input := []any{limit} +// +// res, err := k.clt.Call(ctx, k.target, procedure, input) +// if err != nil { +// return nil, err +// } +// +// if len(res.QueryResult.Values) == 0 { +// return nil, nil +// } +// +// frs := make([]*FinalizedReward, len(res.QueryResult.Values)) +// for i, v := range res.QueryResult.Values { +// fr := &FinalizedReward{} +// err = types.ScanTo(v, &fr.ID, &fr.Voters, &fr.Signatures, &fr.EpochID, +// &fr.CreatedAt, &fr.StartHeight, &fr.EndHeight, &fr.TotalRewards, +// &fr.RewardRoot, &fr.SafeNonce, &fr.SignHash, &fr.ContractID, &fr.BlockHash) +// if err != nil { +// return nil, err +// } +// frs[i] = fr +// } +// +// return frs, nil +//} + +func (k *erc20rwExtApi) VoteEpoch(ctx context.Context, rewardRoot []byte, signature []byte) (string, error) { + procedure := "vote_epoch" + input := [][]any{{rewardRoot, signature}} + + res, err := k.clt.Execute(ctx, k.target, procedure, input, clientTypes.WithSyncBroadcast(true)) + if err != nil { + return "", err + } + + return res.String(), nil +} diff --git a/node/services/erc20signersvc/multicall.go b/node/services/erc20signersvc/multicall.go new file mode 100644 index 000000000..6096eb0f1 --- /dev/null +++ b/node/services/erc20signersvc/multicall.go @@ -0,0 +1,92 @@ +package signersvc + +import ( + "context" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/kwilteam/kwil-db/node/services/erc20signersvc/abigen" +) + +// Multicall https://github.com/mds1/multicall +// https://etherscan.io/address/0xcA11bde05977b3631167028862bE2a173976CA11 + +const ( + EthNetworkMainnet = 1 + EthNetworkSepolia = 11155111 +) + +var ( + AddressMulticall3 = common.HexToAddress("0xcA11bde05977b3631167028862bE2a173976CA11") +) + +var deployedAtMap = map[uint64]uint64{ + EthNetworkMainnet: 14353601, // https://etherscan.io/tx/0x00d9fcb7848f6f6b0aae4fb709c133d69262b902156c85a473ef23faa60760bd + EthNetworkSepolia: 751532, // https://sepolia.etherscan.io/tx/0x6313b2cee1ddd9a77a8a1edf93495a9eb3c51a4d85479f4f8fec0090ad82596b +} + +func IsMulticall3Deployed(chainID uint64, blockNumber *big.Int) bool { + deployedAt, exists := deployedAtMap[chainID] + if !exists { + return false + } + + if blockNumber == nil { + return true + } + + return deployedAt < blockNumber.Uint64() +} + +// Aggregate3 aggregates multicall result. +// based on https://github.com/RSS3-Network/Node/blob/947b387f11857144c48250dd95804b5069731153/provider/ethereum/contract/multicall3/contract.go +func Aggregate3(ctx context.Context, chainID uint64, calls []abigen.Multicall3Call3, + blockNumber *big.Int, contractBackend bind.ContractCaller) ([]*abigen.Multicall3Result, error) { + if !IsMulticall3Deployed(chainID, blockNumber) { + return nil, fmt.Errorf("multicall3 is not deployed on chainID %d yet", chainID) + } + + abi, err := abigen.Multicall3MetaData.GetAbi() + if err != nil { + return nil, fmt.Errorf("load abi: %w", err) + } + + callData, err := abi.Pack("aggregate3", calls) + if err != nil { + return nil, fmt.Errorf("pack data: %w", err) + } + + message := ethereum.CallMsg{ + To: &AddressMulticall3, + Data: callData, + } + + results := make([]abigen.Multicall3Result, 0, len(calls)) + + data, err := contractBackend.CallContract(ctx, message, blockNumber) + if err != nil { + return nil, fmt.Errorf("call contract: %w", err) + } + + if len(data) == 0 { + return nil, fmt.Errorf("data in empty") + } + + if err := abi.UnpackIntoInterface(&results, "aggregate3", data); err != nil { + return nil, fmt.Errorf("unpack result: %w", err) + } + + return ToSlicePtr(results), nil +} + +func ToSlicePtr[T any](collection []T) []*T { + result := make([]*T, len(collection)) + + for i := range collection { + result[i] = &collection[i] + } + return result +} diff --git a/node/services/erc20signersvc/signer.go b/node/services/erc20signersvc/signer.go new file mode 100644 index 000000000..16cff26a9 --- /dev/null +++ b/node/services/erc20signersvc/signer.go @@ -0,0 +1,362 @@ +// Package signersvc implements the SignerSvc of the Kwil reward system. +// It simply fetches the new Epoch from Kwil network and verify&sign it, then +// upload the signature back to the Kwil network. Each rewardSigner targets one registered +// erc20 Reward instance. +package signersvc + +import ( + "context" + "crypto/ecdsa" + "encoding/hex" + "fmt" + "math/big" + "path/filepath" + "slices" + "sync" + "time" + + ethAccounts "github.com/ethereum/go-ethereum/accounts" + ethCommon "github.com/ethereum/go-ethereum/common" + ethMath "github.com/ethereum/go-ethereum/common/math" + ethCrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/kwilteam/kwil-db/core/client" + clientType "github.com/kwilteam/kwil-db/core/client/types" + "github.com/kwilteam/kwil-db/core/crypto" + "github.com/kwilteam/kwil-db/core/crypto/auth" + "github.com/kwilteam/kwil-db/core/log" + "github.com/kwilteam/kwil-db/node/exts/erc20reward/reward" +) + +// StateFilePath returns the state file. +func StateFilePath(dir string) string { + return filepath.Join(dir, "erc20reward_signer_state.json") +} + +// rewardSigner handles one registered erc20 reward instance. +type rewardSigner struct { + kwilRpc string + target string + kwil erc20ExtAPI + lastVoteBlock int64 + escrowAddr ethCommon.Address + + ethRpc string + signerPkStr string + signerPk *ecdsa.PrivateKey + signerAddr ethCommon.Address + safe *Safe + + logger log.Logger + every time.Duration + state *State +} + +// newRewardSigner returns a new rewardSigner. +func newRewardSigner(kwilRpc string, target string, ethRpc string, pkStr string, + every time.Duration, state *State, logger log.Logger) (*rewardSigner, error) { + if logger == nil { + logger = log.DiscardLogger + } + + privateKey, err := ethCrypto.HexToECDSA(pkStr) + if err != nil { + return nil, err + } + + // Get the public key + publicKey := privateKey.Public().(*ecdsa.PublicKey) + + // Get the Ethereum address from the public key + address := ethCrypto.PubkeyToAddress(*publicKey) + + return &rewardSigner{ + kwilRpc: kwilRpc, + ethRpc: ethRpc, + signerPkStr: pkStr, + signerPk: privateKey, + signerAddr: address, + state: state, + logger: logger, + every: every, + target: target, + }, nil +} + +func (s *rewardSigner) init() error { + ctx := context.Background() + + pkBytes, err := hex.DecodeString(s.signerPkStr) + if err != nil { + return fmt.Errorf("decode erc20 reward signer private key failed: %w", err) + } + + key, err := crypto.UnmarshalSecp256k1PrivateKey(pkBytes) + if err != nil { + return fmt.Errorf("parse erc20 reward signer private key failed: %w", err) + } + + opts := &clientType.Options{Signer: &auth.EthPersonalSigner{Key: *key}} + + clt, err := client.NewClient(ctx, s.kwilRpc, opts) + if err != nil { + return fmt.Errorf("create erc20 reward signer api client failed: %w", err) + } + + s.kwil = newERC20RWExtAPI(clt, s.target) + + info, err := s.kwil.InstanceInfo(ctx) + if err != nil { + return fmt.Errorf("get reward metadata failed: %w", err) + } + + s.safe, err = NewSafeFromEscrow(s.ethRpc, info.Escrow) + if err != nil { + return fmt.Errorf("create safe failed: %w", err) + } + + if s.safe.chainID.String() != info.ChainID { + return fmt.Errorf("chainID mismatch: %s != %s", s.safe.chainID.String(), info.ChainID) + } + + s.escrowAddr = ethCommon.HexToAddress(info.Escrow) + + // overwrite configured lastVoteBlock with the value from state if exist + lastVote := s.state.LastVote(s.target) + if lastVote != nil { + s.lastVoteBlock = lastVote.BlockHeight + } + + s.logger.Info("will sync after last vote epoch", "height", s.lastVoteBlock) + + return nil +} + +// canSkip returns true if the epoch: +// a) is not the owner b) is finalized c) is not finalized already voted from this signer; +func (s *rewardSigner) canSkip(epoch *Epoch, safeMeta *safeMetadata) bool { + // TODO: if no rewards in epoch, skip + + if !slices.Contains(safeMeta.owners, s.signerAddr) { + s.logger.Warn("signer is not safe owner", "signer", s.signerAddr.String(), "owners", safeMeta.owners) + return true + } + + for _, voter := range epoch.Voters { + if voter == s.signerAddr.String() { + return true + } + } + + return false +} + +// verify verifies if the reward root is correct, and return the total amount. +func (s *rewardSigner) verify(ctx context.Context, epoch *Epoch, safeAddr string) (*big.Int, error) { + rewards, err := s.kwil.GetEpochRewards(ctx, epoch.ID) + if err != nil { + return nil, err + } + + recipients := make([]string, len(rewards)) + amounts := make([]*big.Int, len(rewards)) + + var ok bool + total := big.NewInt(0) + for i, r := range rewards { + recipients[i] = r.Recipient + + amounts[i], ok = new(big.Int).SetString(r.Amount, 10) + if !ok { + return nil, fmt.Errorf("parse reward amount %s failed", r.Amount) + } + + total = total.Add(total, amounts[i]) + } + + var b32 [32]byte + copy(b32[:], epoch.BlockHash) + + _, root, err := reward.GenRewardMerkleTree(recipients, amounts, safeAddr, b32) + if err != nil { + return nil, err + } + + if !slices.Equal(root, epoch.RewardRoot) { + return nil, fmt.Errorf("reward root mismatch: %s != %s", hex.EncodeToString(root), hex.EncodeToString(epoch.RewardRoot)) + } + + s.logger.Info("verified epoch", "id", epoch.ID.String(), "rewardRoot", hex.EncodeToString(epoch.RewardRoot)) + return total, nil +} + +// vote votes an epoch reward, and updates the state. +// It will first fetch metadata from ETH, then generate the safeTx, then vote. +func (s *rewardSigner) vote(ctx context.Context, epoch *Epoch, safeMeta *safeMetadata, total *big.Int) error { + safeTxData, err := reward.GenPostRewardTxData(epoch.RewardRoot, total) + if err != nil { + return err + } + + // safeTxHash is the data that all signers will be signing(using personal_sign) + _, safeTxHash, err := reward.GenGnosisSafeTx(s.escrowAddr.String(), s.safe.addr.String(), + 0, safeTxData, ethMath.HexOrDecimal256(*s.safe.chainID), *safeMeta.nonce) + if err != nil { + return err + } + + signHash := ethAccounts.TextHash(safeTxHash) + sig, err := reward.EthGnosisSignDigest(signHash, s.signerPk) + if err != nil { + return err + } + + h, err := s.kwil.VoteEpoch(ctx, epoch.RewardRoot, sig) + if err != nil { + return err + } + + // NOTE: it's fine if s.kwil.VoteEpoch succeed, but s.state.UpdateLastVote failed, + // as the epoch will be fetched again and skipped + err = s.state.UpdateLastVote(s.target, &voteRecord{ + RewardRoot: epoch.RewardRoot, + BlockHeight: epoch.EndHeight, + BlockHash: hex.EncodeToString(epoch.BlockHash), + SafeNonce: safeMeta.nonce.Uint64(), + }) + if err != nil { + return err + } + + s.logger.Info("vote epoch", "tx", h, "id", epoch.ID.String(), + "signHash", hex.EncodeToString(signHash)) + + return nil +} + +// watch polls on newer epochs and try to vote/sign them. +// Since there could be the case that the target(namespace/or id) not exist for whatever reason, +// this function won't return Error, and also won't log at Error level. +func (s *rewardSigner) watch(ctx context.Context) { + s.logger.Info("start watching erc20 reward epoches") + + tick := time.NewTicker(s.every) + + for { + s.logger.Debug("polling epochs", "lastVoteBlock", s.lastVoteBlock) + // fetch next batch rewards to be voted, and vote them. + // NOTE: we use ListUnconfirmedEpochs (not FetchLatestRewards) so we don't accidently SKIP epoch. + epochs, err := s.kwil.ListUnconfirmedEpochs(ctx, s.lastVoteBlock, 10) + if err != nil { + s.logger.Warn("fetch epoch", "error", err.Error()) + continue + } + + if len(epochs) == 0 { + s.logger.Debug("no epoch found") + continue + } + + safeMeta, err := s.safe.latestMetadata(ctx) + if err != nil { + s.logger.Warn("fetch safe metadata", "error", err.Error()) + continue + } + + for _, epoch := range epochs { + voteRecord := s.state.LastVote(s.target) + if voteRecord != nil && voteRecord.SafeNonce == safeMeta.nonce.Uint64() { + continue + } + + if s.canSkip(epoch, safeMeta) { + s.logger.Debug("skip epoch", "id", epoch.ID.String(), "height", epoch.EndHeight) + s.lastVoteBlock = epoch.EndHeight // update since we can skip it + continue + } + + total, err := s.verify(ctx, epoch, s.safe.addr.String()) + if err != nil { + s.logger.Warn("verify epoch", "id", epoch.ID.String(), "height", epoch.EndHeight, "error", err.Error()) + break + } + + err = s.vote(ctx, epoch, safeMeta, total) + if err != nil { + s.logger.Warn("vote epoch", "id", epoch.ID.String(), "height", epoch.EndHeight, "error", err.Error()) + break + } + + s.lastVoteBlock = epoch.EndHeight // update after all operations succeed + } + + select { + case <-ctx.Done(): + s.logger.Info("stop watching erc20 reward epoches") + return + case <-tick.C: + continue + } + } +} + +// ServiceMgr manages multiple rewardSigner instances running in parallel. +type ServiceMgr struct { + signers []*rewardSigner + logger log.Logger +} + +func NewServiceMgr( + kwilRpc string, + targets []string, + ethRpcs []string, + pkStrs []string, + syncEvery time.Duration, + state *State, + logger log.Logger) (*ServiceMgr, error) { + + signers := make([]*rewardSigner, len(targets)) + for i, target := range targets { + pk := pkStrs[i] + svc, err := newRewardSigner(kwilRpc, target, ethRpcs[i], pk, + syncEvery, state, logger.New("EVMRW."+target)) + if err != nil { + return nil, fmt.Errorf("create erc20 reward signer service failed: %w", err) + } + + signers[i] = svc + } + + return &ServiceMgr{ + signers: signers, + logger: logger, + }, nil +} + +// Start runs all rewardSigners. It returns error if there are issues initializing the rewardSigner; +// no errors are returned after the rewardSigner is running. +func (s *ServiceMgr) Start(ctx context.Context) error { + // since we need to wait on RPC running, we move the initialization logic into `init` + for _, s := range s.signers { + err := s.init() + if err != nil { + return err + } + } + + wg := &sync.WaitGroup{} + + for _, s := range s.signers { + wg.Add(1) + go func() { + defer wg.Done() + s.watch(ctx) + }() + } + + <-ctx.Done() + wg.Wait() + + s.logger.Infof("Erc20 reward signer service shutting down...") + + return nil +} diff --git a/node/services/erc20signersvc/state.go b/node/services/erc20signersvc/state.go new file mode 100644 index 000000000..e11e3adef --- /dev/null +++ b/node/services/erc20signersvc/state.go @@ -0,0 +1,120 @@ +// This file implements a naive KV persistent solution using a file, changes made +// through the exposed functions will be written to files on every invoking. +// For signer svc, this is good enough. + +package signersvc + +import ( + "encoding/json" + "fmt" + "os" + "sync" +) + +type voteRecord struct { + RewardRoot []byte `json:"reward_root"` + BlockHeight int64 `json:"block_height"` + BlockHash string `json:"block_hash"` + SafeNonce uint64 `json:"safe_nonce"` +} + +// State is a naive kv impl used by singer rewardSigner. +type State struct { + path string + + mu sync.Mutex + + data map[string]*voteRecord // target => latest vote record +} + +// _sync will write State on to disk if it's loaded from disk. +func (s *State) _sync() error { + if s.path == "" { + return nil + } + + tmpPath := s.path + ".tmp" + + err := os.RemoveAll(tmpPath) + if err != nil { + return fmt.Errorf("ensure no tmp file: %w", err) + } + + tmpFile, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + return fmt.Errorf("open file: %w", err) + } + defer tmpFile.Close() + + err = json.NewEncoder(tmpFile).Encode(s.data) + if err != nil { + return fmt.Errorf("write state to file: %w", err) + } + + err = tmpFile.Sync() + if err != nil { + return fmt.Errorf("file sync: %w", err) + } + + err = os.Rename(tmpPath, s.path) + if err != nil { + return fmt.Errorf("") + } + + return err +} + +// UpdateLastVote updates the latest vote record, and syncs the changes to disk. +func (s *State) UpdateLastVote(target string, newVote *voteRecord) error { + s.mu.Lock() + defer s.mu.Unlock() + + s.data[target] = newVote + + return s._sync() +} + +func (s *State) LastVote(target string) *voteRecord { + s.mu.Lock() + defer s.mu.Unlock() + + if record, ok := s.data[target]; ok { + return record + } + + return nil +} + +// LoadStateFromFile load the state from a file. +func LoadStateFromFile(stateFile string) (*State, error) { + s := &State{ + path: stateFile, + data: make(map[string]*voteRecord), + } + + data, err := os.ReadFile(stateFile) + if err != nil { + return nil, err + } + + if len(data) == 0 { + return s, nil + } + + err = json.Unmarshal(data, &s.data) + if err != nil { + return nil, err + } + + return s, nil +} + +func NewMemState() *State { + return &State{} +} + +func NewTmpState() *State { + return &State{ + path: "/tmp/erc20rw-signer-state.json", + } +} diff --git a/test/go.mod b/test/go.mod index e7d29ed84..870243383 100644 --- a/test/go.mod +++ b/test/go.mod @@ -283,6 +283,7 @@ require ( github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc // indirect github.com/raulk/go-watchdog v1.3.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.47.0 // indirect github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect diff --git a/test/go.sum b/test/go.sum index cd1fb0d54..7428003eb 100644 --- a/test/go.sum +++ b/test/go.sum @@ -870,6 +870,8 @@ github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3V github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/secure-systems-lab/go-securesystemslib v0.4.0 h1:b23VGrQhTA8cN2CbBw7/FulN9fTtqYUdS5+Oxzt+DUE= github.com/secure-systems-lab/go-securesystemslib v0.4.0/go.mod h1:FGBZgq2tXWICsxWQW1msNf49F0Pf2Op5Htayx335Qbs= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= From 3089f835d67ae1a4e6685b55cbf466889a6e482f Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Tue, 11 Feb 2025 17:02:41 -0600 Subject: [PATCH 02/30] return votes --- cmd/kwil-cli/cmds/call-action.go | 4 +- core/rpc/client/jsonrpc.go | 2 + node/engine/interpreter/interpreter.go | 1 + node/exts/erc20reward/meta_extension.go | 85 ++++++++++++++---------- node/exts/erc20reward/meta_schema.sql | 2 +- node/exts/erc20reward/meta_sql.go | 77 +++++++++++++++++---- node/exts/erc20reward/named_extension.go | 13 ++-- node/services/erc20signersvc/kwil.go | 4 +- node/services/erc20signersvc/signer.go | 10 +-- 9 files changed, 136 insertions(+), 62 deletions(-) diff --git a/cmd/kwil-cli/cmds/call-action.go b/cmd/kwil-cli/cmds/call-action.go index 216cea7ee..502e17feb 100644 --- a/cmd/kwil-cli/cmds/call-action.go +++ b/cmd/kwil-cli/cmds/call-action.go @@ -17,7 +17,7 @@ import ( var ( callActionLong = `Call a view action. - + This command calls a view action against the database, and formats the results in a table. It can only be used to call view actions, not write actions. @@ -113,6 +113,8 @@ func callActionCmd() *cobra.Command { return display.PrintErr(cmd, err) } + fmt.Printf("--------Result: %+v\n", res.QueryResult) + return display.PrintCmd(cmd, &respCall{Data: res, PrintLogs: logs, cmd: cmd}) }) }, diff --git a/core/rpc/client/jsonrpc.go b/core/rpc/client/jsonrpc.go index e1753a4f6..84fc54dc7 100644 --- a/core/rpc/client/jsonrpc.go +++ b/core/rpc/client/jsonrpc.go @@ -202,6 +202,8 @@ func (cl *JSONRPCClient) CallMethod(ctx context.Context, method string, cmd, res // fmt.Printf("got id %v, expected %v\n", resp.ID, id) // } // who cares, this is http post + fmt.Println("====+++++++++resp.Result: ", string(resp.Result)) + if err = json.Unmarshal(resp.Result, res); err != nil { return fmt.Errorf("failed to decode result as response: %w", errors.Join(err, httpErr)) } diff --git a/node/engine/interpreter/interpreter.go b/node/engine/interpreter/interpreter.go index 33e4c4640..dd9f5bea3 100644 --- a/node/engine/interpreter/interpreter.go +++ b/node/engine/interpreter/interpreter.go @@ -730,6 +730,7 @@ func rowToCommonRow(row *row) *common.Row { convertedResults := make([]any, len(row.Values)) dataTypes := make([]*types.DataType, len(row.Values)) for i, result := range row.Values { + fmt.Printf("------+++------%v, %v\n", result.Type(), result.RawValue()) convertedResults[i] = result.RawValue() dataTypes[i] = result.Type() } diff --git a/node/exts/erc20reward/meta_extension.go b/node/exts/erc20reward/meta_extension.go index 52af3e407..5202c2f68 100644 --- a/node/exts/erc20reward/meta_extension.go +++ b/node/exts/erc20reward/meta_extension.go @@ -819,42 +819,6 @@ func init() { return resultFn([]any{bal}) }, }, - { - // lists epochs after(non-include) given height, in ASC order. - // If confirmedOnly is true, only returns confirmed epochs. - // NOTE: only unfirmed epoch will return voters and vote_nonces - Name: "list_epochs", - Parameters: []precompiles.PrecompileValue{ - {Name: "id", Type: types.UUIDType}, - {Name: "after", Type: types.IntType}, - {Name: "limit", Type: types.IntType}, - {Name: "confirmed_only", Type: types.BoolType}, - }, - Returns: &precompiles.MethodReturn{ - IsTable: true, - Fields: []precompiles.PrecompileValue{ - {Name: "epoch_id", Type: types.UUIDType}, - {Name: "start_height", Type: types.IntType}, - {Name: "start_timestamp", Type: types.IntType}, - {Name: "end_height", Type: types.IntType}, - {Name: "reward_root", Type: types.ByteaType}, - {Name: "end_block_hash", Type: types.ByteaType}, - {Name: "voters", Type: types.TextArrayType}, - {Name: "vote_nonces", Type: types.IntType}, - }, - }, - AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, - Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { - id := inputs[0].(*types.UUID) - after := inputs[1].(int64) - limit := inputs[2].(int64) - confirmedOnly := inputs[3].(bool) - - return getEpochs(ctx.TxContext.Ctx, app, id, after, limit, confirmedOnly, func(e *Epoch) error { - return resultFn([]any{e.ID, e.StartHeight, e.StartTime.Unix(), *e.EndHeight, e.Root, e.BlockHash}) - }) - }, - }, { Name: "decimals", Parameters: []precompiles.PrecompileValue{ @@ -997,7 +961,56 @@ func init() { // if !calledByExtension(ctx) { // return errors.New("propose_epoch can only be called by the Kwil network") // } + { + // lists epochs after(non-include) given height, in ASC order. + // If confirmedOnly is true, only returns confirmed epochs. + // NOTE: only unfirmed epoch will return voters and vote_nonces + Name: "list_epochs", + Parameters: []precompiles.PrecompileValue{ + {Name: "id", Type: types.UUIDType}, + {Name: "after", Type: types.IntType}, + {Name: "limit", Type: types.IntType}, + {Name: "confirmed_only", Type: types.BoolType}, + }, + Returns: &precompiles.MethodReturn{ + IsTable: true, + Fields: []precompiles.PrecompileValue{ + {Name: "epoch_id", Type: types.UUIDType}, + {Name: "start_height", Type: types.IntType}, + {Name: "start_timestamp", Type: types.IntType}, + {Name: "end_height", Type: types.IntType, Nullable: true}, + {Name: "reward_root", Type: types.ByteaType, Nullable: true}, + {Name: "end_block_hash", Type: types.ByteaType, Nullable: true}, + {Name: "voters", Type: types.TextArrayType, Nullable: true}, + {Name: "vote_nonces", Type: types.IntArrayType, Nullable: true}, + }, + }, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, + Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { + id := inputs[0].(*types.UUID) + after := inputs[1].(int64) + limit := inputs[2].(int64) + confirmedOnly := inputs[3].(bool) + return getEpochs(ctx.TxContext.Ctx, app, id, after, limit, confirmedOnly, func(e *Epoch) error { + fmt.Printf("-=-=-=-=%+v, %+v, %v, %v\n", e.Voters, e.VoteNonces, e.Voters, e.VoteNonces) + + var voters []string + if len(e.Voters) > 0 { + for _, item := range e.Voters { + voters = append(voters, item.String()) + } + } + fmt.Printf("voters: %v\n", voters) + fmt.Printf("voterNonces: %v\n", e.VoteNonces) + + return resultFn([]any{e.ID, e.StartHeight, e.StartTime.Unix(), *e.EndHeight, e.Root, e.BlockHash, + voters, + e.VoteNonces, + }) + }) + }, + }, { // Supposed to be called by the SignerService, to verify the reward root. Name: "get_epoch_rewards", diff --git a/node/exts/erc20reward/meta_schema.sql b/node/exts/erc20reward/meta_schema.sql index c1f20f949..3f4f30763 100644 --- a/node/exts/erc20reward/meta_schema.sql +++ b/node/exts/erc20reward/meta_schema.sql @@ -73,7 +73,7 @@ CREATE TABLE meta -- After an epoch is confirmed, we can delete all related votes. CREATE TABLE epoch_votes ( epoch_id UUID NOT NULL REFERENCES epochs(id) ON UPDATE RESTRICT ON DELETE RESTRICT, - voter TEXT NOT NULL, + voter BYTEA NOT NULL, signature BYTEA NOT NULL, nonce INT8 NOT NULL, -- safe nonce; technically we don't need this, but this helps to identify why a signer is not valid PRIMARY KEY (epoch_id, voter) diff --git a/node/exts/erc20reward/meta_sql.go b/node/exts/erc20reward/meta_sql.go index 18521ee1f..18fe63a6d 100644 --- a/node/exts/erc20reward/meta_sql.go +++ b/node/exts/erc20reward/meta_sql.go @@ -355,30 +355,79 @@ func getRewardsForEpoch(ctx context.Context, app *common.App, epochID *types.UUI // getEpochs gets epochs by given conditions. func getEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, after int64, limit int64, confirmedOnly bool, fn func(*Epoch) error) error { query := ` - SELECT e.id, e.created_at_block, e.created_at_unix, e.ended_at, e.block_hash, e.reward_root, array_agg(v.voter) as voters, array_agg(v.nonce) as signatures - FROM epochs e - JOIN epoch_votes as v ON v.epoch_id = epochs.id - WHERE instance_id = $instance_id AND ended_at_block > $after` + {kwil_erc20_meta}SELECT e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.ended_at, e.block_hash, array_agg(v.voter) as voters, array_agg(v.nonce) as nonces + FROM epochs AS e + LEFT JOIN epoch_votes AS v ON v.epoch_id = e.id + WHERE e.instance_id = $instance_id AND e.created_at_block > $after` if confirmedOnly { - query += ` AND confirmed IS $confirmed` + query += ` AND confirmed IS true` } - query += ` ORDER BY ended_at ASC LIMIT $limit` + query += ` + GROUP BY e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.ended_at, e.block_hash + ORDER BY ended_at ASC LIMIT $limit` + fmt.Println("=====_+_+_+_++__++", query) return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, query, map[string]any{ "instance_id": instanceID, "after": after, "limit": limit, - "confirmed": confirmedOnly, }, func(r *common.Row) error { - if len(r.Values) != 6 { - return fmt.Errorf("expected 6 values, got %d", len(r.Values)) + if len(r.Values) != 8 { + return fmt.Errorf("expected 8 values, got %d", len(r.Values)) } id := r.Values[0].(*types.UUID) createdAtBlock := r.Values[1].(int64) createdAtUnix := r.Values[2].(int64) - endedAt := r.Values[3].(int64) - blockHash := r.Values[4].([]byte) - rewardRoot := r.Values[5].([]byte) + + var rewardRoot []byte + if r.Values[3] != nil { + rewardRoot = r.Values[3].([]byte) + } + + var endedAt int64 + if r.Values[4] != nil { + endedAt = r.Values[4].(int64) + } + + var blockHash []byte + if r.Values[5] != nil { + blockHash = r.Values[5].([]byte) + } + + var voters []ethcommon.Address + fmt.Printf("+++++++ %+v, %+v\n", voters, r.Values[6]) + if r.Values[6] != nil { + rawVoters := r.Values[6].([][]byte) + // empty value is [[]], cannot use make() + for _, rawVoter := range rawVoters { + fmt.Println("+++++++", rawVoter) + if len(rawVoter) == 0 { + continue + } + voter, err := bytesToEthAddress(rawVoter) + if err != nil { + return err + } + voters = append(voters, voter) + } + } + + fmt.Printf("+++++++ %+v\n", voters) + + var voteNonces []int64 + fmt.Printf("+++-----++++ %+v\n", voteNonces, r.Values[7]) + if r.Values[7] != nil { + rawNonces := r.Values[7].([]*int64) + for _, rawNonce := range rawNonces { + fmt.Println("+++-----++++", rawNonce) + if rawNonce == nil { + continue + } + voteNonces = append(voteNonces, *rawNonce) + } + } + + fmt.Printf("+++-----++++ %+v\n", voteNonces) return fn(&Epoch{ PendingEpoch: PendingEpoch{ @@ -389,6 +438,10 @@ func getEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, aft EndHeight: &endedAt, BlockHash: blockHash, Root: rewardRoot, + EpochVoteInfo: EpochVoteInfo{ + Voters: voters, + VoteNonces: voteNonces, + }, }) }) } diff --git a/node/exts/erc20reward/named_extension.go b/node/exts/erc20reward/named_extension.go index 50dbbcf9a..642f33ea9 100644 --- a/node/exts/erc20reward/named_extension.go +++ b/node/exts/erc20reward/named_extension.go @@ -3,6 +3,7 @@ package erc20reward import ( "context" "errors" + "fmt" "github.com/kwilteam/kwil-db/common" "github.com/kwilteam/kwil-db/core/types" @@ -56,6 +57,7 @@ func init() { makeMetaHandler := func(method string) precompiles.HandlerFunc { return func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { _, err2 := app.Engine.Call(ctx, app.DB, RewardMetaExtensionName, method, append([]any{&id}, inputs...), func(r *common.Row) error { + fmt.Printf("meta handler result: %+v\n", r.Values) return resultFn(r.Values) }) return err2 @@ -173,7 +175,6 @@ func init() { { Name: "list_epochs", Parameters: []precompiles.PrecompileValue{ - {Name: "id", Type: types.UUIDType}, {Name: "after", Type: types.IntType}, {Name: "limit", Type: types.IntType}, {Name: "confirmed_only", Type: types.BoolType}, @@ -184,13 +185,15 @@ func init() { {Name: "epoch_id", Type: types.UUIDType}, {Name: "start_height", Type: types.IntType}, {Name: "start_timestamp", Type: types.IntType}, - {Name: "end_height", Type: types.IntType}, - {Name: "reward_root", Type: types.ByteaType}, - {Name: "end_block_hash", Type: types.ByteaType}, + {Name: "end_height", Type: types.IntType, Nullable: true}, + {Name: "reward_root", Type: types.ByteaType, Nullable: true}, + {Name: "end_block_hash", Type: types.ByteaType, Nullable: true}, + {Name: "voters", Type: types.TextArrayType, Nullable: true}, + {Name: "vote_nonces", Type: types.IntArrayType, Nullable: true}, }, }, AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, - Handler: makeMetaHandler("list_unconfirmed_epochs"), + Handler: makeMetaHandler("list_epochs"), }, { Name: "decimals", diff --git a/node/services/erc20signersvc/kwil.go b/node/services/erc20signersvc/kwil.go index e985f9e01..c2f92ca9d 100644 --- a/node/services/erc20signersvc/kwil.go +++ b/node/services/erc20signersvc/kwil.go @@ -126,8 +126,8 @@ func (k *erc20rwExtApi) ListUnconfirmedEpochs(ctx context.Context, afterHeight i ers := make([]*Epoch, len(res.QueryResult.Values)) for i, v := range res.QueryResult.Values { er := &Epoch{} - err = types.ScanTo(v, &er.ID, &er.StartHeight, &er.EndHeight, &er.TotalRewards, - &er.RewardRoot, &er.SafeNonce, &er.SignHash, &er.ContractID, &er.BlockHash, &er.CreatedAt, &er.Voters) + err = types.ScanTo(v, &er.ID, &er.StartHeight, &er.StartTime, &er.EndHeight, + &er.RewardRoot, &er.BlockHash) if err != nil { return nil, err } diff --git a/node/services/erc20signersvc/signer.go b/node/services/erc20signersvc/signer.go index 16cff26a9..6bf5f89fd 100644 --- a/node/services/erc20signersvc/signer.go +++ b/node/services/erc20signersvc/signer.go @@ -141,11 +141,11 @@ func (s *rewardSigner) canSkip(epoch *Epoch, safeMeta *safeMetadata) bool { return true } - for _, voter := range epoch.Voters { - if voter == s.signerAddr.String() { - return true - } - } + //for _, voter := range epoch.Voters { + // if voter == s.signerAddr.String() { + // return true + // } + //} return false } From 6bdc42182f56e2b8764765f11d2c5ea7e8581725 Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Wed, 12 Feb 2025 15:47:34 -0600 Subject: [PATCH 03/30] fix event sync wrong topic --- .../erc20reward/abigen/reward_distributor.go | 86 ++- .../abigen/reward_distributor_abi.json | 570 +++++++++--------- node/exts/erc20reward/meta_extension.go | 1 + 3 files changed, 316 insertions(+), 341 deletions(-) diff --git a/node/exts/erc20reward/abigen/reward_distributor.go b/node/exts/erc20reward/abigen/reward_distributor.go index bcb550c07..69634710f 100644 --- a/node/exts/erc20reward/abigen/reward_distributor.go +++ b/node/exts/erc20reward/abigen/reward_distributor.go @@ -31,7 +31,7 @@ var ( // RewardDistributorMetaData contains all meta data concerning the RewardDistributor contract. var RewardDistributorMetaData = &bind.MetaData{ - ABI: "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_safe\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_posterFee\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_rewardToken\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"ReentrancyGuardReentrantCall\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"newFee\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"}],\"name\":\"PosterFeeUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"claimer\",\"type\":\"address\"}],\"name\":\"RewardClaimed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"poster\",\"type\":\"address\"}],\"name\":\"RewardPosted\",\"type\":\"event\"},{\"stateMutability\":\"payable\",\"type\":\"fallback\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"kwilBlockHash\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"rewardRoot\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32[]\",\"name\":\"proofs\",\"type\":\"bytes32[]\"}],\"name\":\"claimReward\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"isRewardClaimed\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"nonce\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"postReward\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"postedRewards\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"posterFee\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"rewardPoster\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"rewardToken\",\"outputs\":[{\"internalType\":\"contractIERC20\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"safe\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"unpostedRewards\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"newFee\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"_nonce\",\"type\":\"uint256\"}],\"name\":\"updatePosterFee\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"stateMutability\":\"payable\",\"type\":\"receive\"}]", + ABI: "[{\"inputs\":[],\"name\":\"ReentrancyGuardReentrantCall\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"oldFee\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"newFee\",\"type\":\"uint256\"}],\"name\":\"PosterFeeUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"claimer\",\"type\":\"address\"}],\"name\":\"RewardClaimed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"poster\",\"type\":\"address\"}],\"name\":\"RewardPosted\",\"type\":\"event\"},{\"stateMutability\":\"payable\",\"type\":\"fallback\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"kwilBlockHash\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"rewardRoot\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32[]\",\"name\":\"proofs\",\"type\":\"bytes32[]\"}],\"name\":\"claimReward\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"isRewardClaimed\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"postReward\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"postedRewards\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"posterFee\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"rewardPoster\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"rewardToken\",\"outputs\":[{\"internalType\":\"contractIERC20\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"safe\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_safe\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_posterFee\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_rewardToken\",\"type\":\"address\"}],\"name\":\"setup\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"unpostedRewards\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"newFee\",\"type\":\"uint256\"}],\"name\":\"updatePosterFee\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"stateMutability\":\"payable\",\"type\":\"receive\"}]", } // RewardDistributorABI is the input ABI used to generate the binding from. @@ -211,37 +211,6 @@ func (_RewardDistributor *RewardDistributorCallerSession) IsRewardClaimed(arg0 [ return _RewardDistributor.Contract.IsRewardClaimed(&_RewardDistributor.CallOpts, arg0, arg1) } -// Nonce is a free data retrieval call binding the contract method 0xaffed0e0. -// -// Solidity: function nonce() view returns(uint256) -func (_RewardDistributor *RewardDistributorCaller) Nonce(opts *bind.CallOpts) (*big.Int, error) { - var out []interface{} - err := _RewardDistributor.contract.Call(opts, &out, "nonce") - - if err != nil { - return *new(*big.Int), err - } - - out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) - - return out0, err - -} - -// Nonce is a free data retrieval call binding the contract method 0xaffed0e0. -// -// Solidity: function nonce() view returns(uint256) -func (_RewardDistributor *RewardDistributorSession) Nonce() (*big.Int, error) { - return _RewardDistributor.Contract.Nonce(&_RewardDistributor.CallOpts) -} - -// Nonce is a free data retrieval call binding the contract method 0xaffed0e0. -// -// Solidity: function nonce() view returns(uint256) -func (_RewardDistributor *RewardDistributorCallerSession) Nonce() (*big.Int, error) { - return _RewardDistributor.Contract.Nonce(&_RewardDistributor.CallOpts) -} - // PostedRewards is a free data retrieval call binding the contract method 0x122d52de. // // Solidity: function postedRewards() view returns(uint256) @@ -470,25 +439,46 @@ func (_RewardDistributor *RewardDistributorTransactorSession) PostReward(root [3 return _RewardDistributor.Contract.PostReward(&_RewardDistributor.TransactOpts, root, amount) } -// UpdatePosterFee is a paid mutator transaction binding the contract method 0xe08b22fa. +// Setup is a paid mutator transaction binding the contract method 0xf00e5686. +// +// Solidity: function setup(address _safe, uint256 _posterFee, address _rewardToken) returns() +func (_RewardDistributor *RewardDistributorTransactor) Setup(opts *bind.TransactOpts, _safe common.Address, _posterFee *big.Int, _rewardToken common.Address) (*types.Transaction, error) { + return _RewardDistributor.contract.Transact(opts, "setup", _safe, _posterFee, _rewardToken) +} + +// Setup is a paid mutator transaction binding the contract method 0xf00e5686. +// +// Solidity: function setup(address _safe, uint256 _posterFee, address _rewardToken) returns() +func (_RewardDistributor *RewardDistributorSession) Setup(_safe common.Address, _posterFee *big.Int, _rewardToken common.Address) (*types.Transaction, error) { + return _RewardDistributor.Contract.Setup(&_RewardDistributor.TransactOpts, _safe, _posterFee, _rewardToken) +} + +// Setup is a paid mutator transaction binding the contract method 0xf00e5686. +// +// Solidity: function setup(address _safe, uint256 _posterFee, address _rewardToken) returns() +func (_RewardDistributor *RewardDistributorTransactorSession) Setup(_safe common.Address, _posterFee *big.Int, _rewardToken common.Address) (*types.Transaction, error) { + return _RewardDistributor.Contract.Setup(&_RewardDistributor.TransactOpts, _safe, _posterFee, _rewardToken) +} + +// UpdatePosterFee is a paid mutator transaction binding the contract method 0xb19050bd. // -// Solidity: function updatePosterFee(uint256 newFee, uint256 _nonce) returns() -func (_RewardDistributor *RewardDistributorTransactor) UpdatePosterFee(opts *bind.TransactOpts, newFee *big.Int, _nonce *big.Int) (*types.Transaction, error) { - return _RewardDistributor.contract.Transact(opts, "updatePosterFee", newFee, _nonce) +// Solidity: function updatePosterFee(uint256 newFee) returns() +func (_RewardDistributor *RewardDistributorTransactor) UpdatePosterFee(opts *bind.TransactOpts, newFee *big.Int) (*types.Transaction, error) { + return _RewardDistributor.contract.Transact(opts, "updatePosterFee", newFee) } -// UpdatePosterFee is a paid mutator transaction binding the contract method 0xe08b22fa. +// UpdatePosterFee is a paid mutator transaction binding the contract method 0xb19050bd. // -// Solidity: function updatePosterFee(uint256 newFee, uint256 _nonce) returns() -func (_RewardDistributor *RewardDistributorSession) UpdatePosterFee(newFee *big.Int, _nonce *big.Int) (*types.Transaction, error) { - return _RewardDistributor.Contract.UpdatePosterFee(&_RewardDistributor.TransactOpts, newFee, _nonce) +// Solidity: function updatePosterFee(uint256 newFee) returns() +func (_RewardDistributor *RewardDistributorSession) UpdatePosterFee(newFee *big.Int) (*types.Transaction, error) { + return _RewardDistributor.Contract.UpdatePosterFee(&_RewardDistributor.TransactOpts, newFee) } -// UpdatePosterFee is a paid mutator transaction binding the contract method 0xe08b22fa. +// UpdatePosterFee is a paid mutator transaction binding the contract method 0xb19050bd. // -// Solidity: function updatePosterFee(uint256 newFee, uint256 _nonce) returns() -func (_RewardDistributor *RewardDistributorTransactorSession) UpdatePosterFee(newFee *big.Int, _nonce *big.Int) (*types.Transaction, error) { - return _RewardDistributor.Contract.UpdatePosterFee(&_RewardDistributor.TransactOpts, newFee, _nonce) +// Solidity: function updatePosterFee(uint256 newFee) returns() +func (_RewardDistributor *RewardDistributorTransactorSession) UpdatePosterFee(newFee *big.Int) (*types.Transaction, error) { + return _RewardDistributor.Contract.UpdatePosterFee(&_RewardDistributor.TransactOpts, newFee) } // Fallback is a paid mutator transaction binding the contract fallback function. @@ -602,14 +592,14 @@ func (it *RewardDistributorPosterFeeUpdatedIterator) Close() error { // RewardDistributorPosterFeeUpdated represents a PosterFeeUpdated event raised by the RewardDistributor contract. type RewardDistributorPosterFeeUpdated struct { + OldFee *big.Int NewFee *big.Int - Nonce *big.Int Raw types.Log // Blockchain specific contextual infos } // FilterPosterFeeUpdated is a free log retrieval operation binding the contract event 0x7c7423dff6eff60ac491456a649034ee92866801bb236290a4b9190e370e8952. // -// Solidity: event PosterFeeUpdated(uint256 newFee, uint256 nonce) +// Solidity: event PosterFeeUpdated(uint256 oldFee, uint256 newFee) func (_RewardDistributor *RewardDistributorFilterer) FilterPosterFeeUpdated(opts *bind.FilterOpts) (*RewardDistributorPosterFeeUpdatedIterator, error) { logs, sub, err := _RewardDistributor.contract.FilterLogs(opts, "PosterFeeUpdated") @@ -621,7 +611,7 @@ func (_RewardDistributor *RewardDistributorFilterer) FilterPosterFeeUpdated(opts // WatchPosterFeeUpdated is a free log subscription operation binding the contract event 0x7c7423dff6eff60ac491456a649034ee92866801bb236290a4b9190e370e8952. // -// Solidity: event PosterFeeUpdated(uint256 newFee, uint256 nonce) +// Solidity: event PosterFeeUpdated(uint256 oldFee, uint256 newFee) func (_RewardDistributor *RewardDistributorFilterer) WatchPosterFeeUpdated(opts *bind.WatchOpts, sink chan<- *RewardDistributorPosterFeeUpdated) (event.Subscription, error) { logs, sub, err := _RewardDistributor.contract.WatchLogs(opts, "PosterFeeUpdated") @@ -658,7 +648,7 @@ func (_RewardDistributor *RewardDistributorFilterer) WatchPosterFeeUpdated(opts // ParsePosterFeeUpdated is a log parse operation binding the contract event 0x7c7423dff6eff60ac491456a649034ee92866801bb236290a4b9190e370e8952. // -// Solidity: event PosterFeeUpdated(uint256 newFee, uint256 nonce) +// Solidity: event PosterFeeUpdated(uint256 oldFee, uint256 newFee) func (_RewardDistributor *RewardDistributorFilterer) ParsePosterFeeUpdated(log types.Log) (*RewardDistributorPosterFeeUpdated, error) { event := new(RewardDistributorPosterFeeUpdated) if err := _RewardDistributor.contract.UnpackLog(event, "PosterFeeUpdated", log); err != nil { diff --git a/node/exts/erc20reward/abigen/reward_distributor_abi.json b/node/exts/erc20reward/abigen/reward_distributor_abi.json index db4ec9acf..446957efc 100644 --- a/node/exts/erc20reward/abigen/reward_distributor_abi.json +++ b/node/exts/erc20reward/abigen/reward_distributor_abi.json @@ -1,295 +1,279 @@ [ - { - "inputs": [ - { - "internalType": "address", - "name": "_safe", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_posterFee", - "type": "uint256" - }, - { - "internalType": "address", - "name": "_rewardToken", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "inputs": [], - "name": "ReentrancyGuardReentrantCall", - "type": "error" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "newFee", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "nonce", - "type": "uint256" - } - ], - "name": "PosterFeeUpdated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address", - "name": "claimer", - "type": "address" - } - ], - "name": "RewardClaimed", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "bytes32", - "name": "root", - "type": "bytes32" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address", - "name": "poster", - "type": "address" - } - ], - "name": "RewardPosted", - "type": "event" - }, - { - "stateMutability": "payable", - "type": "fallback" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "kwilBlockHash", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "rewardRoot", - "type": "bytes32" - }, - { - "internalType": "bytes32[]", - "name": "proofs", - "type": "bytes32[]" - } - ], - "name": "claimReward", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "name": "isRewardClaimed", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "nonce", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "root", - "type": "bytes32" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "postReward", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "postedRewards", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "posterFee", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "name": "rewardPoster", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "rewardToken", - "outputs": [ - { - "internalType": "contract IERC20", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "safe", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "unpostedRewards", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "newFee", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_nonce", - "type": "uint256" - } - ], - "name": "updatePosterFee", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "stateMutability": "payable", - "type": "receive" - } + { + "inputs": [ ], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldFee", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newFee", + "type": "uint256" + } + ], + "name": "PosterFeeUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "claimer", + "type": "address" + } + ], + "name": "RewardClaimed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "root", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "poster", + "type": "address" + } + ], + "name": "RewardPosted", + "type": "event" + }, + { + "stateMutability": "payable", + "type": "fallback" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "kwilBlockHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "rewardRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "proofs", + "type": "bytes32[]" + } + ], + "name": "claimReward", + "outputs": [ ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "isRewardClaimed", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "root", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "postReward", + "outputs": [ ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ ], + "name": "postedRewards", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ ], + "name": "posterFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "rewardPoster", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ ], + "name": "rewardToken", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ ], + "name": "safe", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_safe", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_posterFee", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_rewardToken", + "type": "address" + } + ], + "name": "setup", + "outputs": [ ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ ], + "name": "unpostedRewards", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newFee", + "type": "uint256" + } + ], + "name": "updatePosterFee", + "outputs": [ ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } ] \ No newline at end of file diff --git a/node/exts/erc20reward/meta_extension.go b/node/exts/erc20reward/meta_extension.go index 5202c2f68..7e0fcd378 100644 --- a/node/exts/erc20reward/meta_extension.go +++ b/node/exts/erc20reward/meta_extension.go @@ -1025,6 +1025,7 @@ func init() { {Name: "amount", Type: types.TextType}, }, }, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { //id := inputs[0].(*types.UUID) epochID := inputs[1].(*types.UUID) From 2def39f68842c87c42c18ac1c30ed522ffc04f79 Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Thu, 13 Feb 2025 10:57:42 -0600 Subject: [PATCH 04/30] fix wrong sql --- node/engine/interpreter/interpreter.go | 1 - node/exts/erc20reward/meta_extension.go | 12 +++++++----- node/exts/erc20reward/meta_schema.sql | 3 ++- node/exts/erc20reward/meta_sql.go | 25 +++++++++--------------- node/exts/erc20reward/named_extension.go | 14 ++++++------- node/services/erc20signersvc/kwil.go | 2 ++ node/services/erc20signersvc/signer.go | 22 ++++++++++++++------- 7 files changed, 42 insertions(+), 37 deletions(-) diff --git a/node/engine/interpreter/interpreter.go b/node/engine/interpreter/interpreter.go index dd9f5bea3..33e4c4640 100644 --- a/node/engine/interpreter/interpreter.go +++ b/node/engine/interpreter/interpreter.go @@ -730,7 +730,6 @@ func rowToCommonRow(row *row) *common.Row { convertedResults := make([]any, len(row.Values)) dataTypes := make([]*types.DataType, len(row.Values)) for i, result := range row.Values { - fmt.Printf("------+++------%v, %v\n", result.Type(), result.RawValue()) convertedResults[i] = result.RawValue() dataTypes[i] = result.Type() } diff --git a/node/exts/erc20reward/meta_extension.go b/node/exts/erc20reward/meta_extension.go index 7e0fcd378..6c9c013b5 100644 --- a/node/exts/erc20reward/meta_extension.go +++ b/node/exts/erc20reward/meta_extension.go @@ -298,6 +298,10 @@ func init() { // we dont need to worry about locking the instances yet // because we just read them from the db for _, instance := range instances { + if _, ok := SINGLETON.instances.Get(*instance.ID); ok { + return fmt.Errorf("internal bug: duplicate instance id %s", instance.ID) + } + // if instance is active, we should start one of its // two listeners. If it is synced, we should start the // transfer listener. Otherwise, we should start the state poller @@ -640,7 +644,7 @@ func init() { return err } - err = issueReward(ctx.TxContext.Ctx, app, info.currentEpoch.ID, addr, amount) + err = issueReward(ctx.TxContext.Ctx, app, id, info.currentEpoch.ID, addr, amount) if err != nil { info.mu.RUnlock() return err @@ -993,16 +997,12 @@ func init() { confirmedOnly := inputs[3].(bool) return getEpochs(ctx.TxContext.Ctx, app, id, after, limit, confirmedOnly, func(e *Epoch) error { - fmt.Printf("-=-=-=-=%+v, %+v, %v, %v\n", e.Voters, e.VoteNonces, e.Voters, e.VoteNonces) - var voters []string if len(e.Voters) > 0 { for _, item := range e.Voters { voters = append(voters, item.String()) } } - fmt.Printf("voters: %v\n", voters) - fmt.Printf("voterNonces: %v\n", e.VoteNonces) return resultFn([]any{e.ID, e.StartHeight, e.StartTime.Unix(), *e.EndHeight, e.Root, e.BlockHash, voters, @@ -1161,6 +1161,8 @@ func init() { return nil } + // TODO: if last epoch(if exists) not confirmed, we do nothing. + var rewards []*EpochReward err := getRewardsForEpoch(ctx, app, info.currentEpoch.ID, func(reward *EpochReward) error { rewards = append(rewards, reward) diff --git a/node/exts/erc20reward/meta_schema.sql b/node/exts/erc20reward/meta_schema.sql index 3f4f30763..81c93a79a 100644 --- a/node/exts/erc20reward/meta_schema.sql +++ b/node/exts/erc20reward/meta_schema.sql @@ -19,7 +19,8 @@ CREATE TABLE reward_instances ( erc20_address BYTEA, erc20_decimals INT8, synced_at INT8, -- the unix timestamp (in seconds) when the reward was synced - balance NUMERIC(78, 0) NOT NULL DEFAULT 0 CHECK(balance >= 0) -- the total balance owned by the database that can be distributed + balance NUMERIC(78, 0) NOT NULL DEFAULT 0 CHECK(balance >= 0), -- the total balance owned by the database that can be distributed + UNIQUE (chain_id, escrow_address) -- unique per chain and escrow ); -- balances tracks the balance of each user in a given reward instance. diff --git a/node/exts/erc20reward/meta_sql.go b/node/exts/erc20reward/meta_sql.go index 18fe63a6d..0991dde17 100644 --- a/node/exts/erc20reward/meta_sql.go +++ b/node/exts/erc20reward/meta_sql.go @@ -233,18 +233,20 @@ func createSchema(ctx context.Context, app *common.App) error { } // issueReward issues a reward to a user. -func issueReward(ctx context.Context, app *common.App, epochID *types.UUID, user ethcommon.Address, amount *types.Decimal) error { +func issueReward(ctx context.Context, app *common.App, instanceId *types.UUID, epochID *types.UUID, user ethcommon.Address, amount *types.Decimal) error { return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` {kwil_erc20_meta}UPDATE reward_instances - SET balance = balance - $amount; + SET balance = balance - $amount + WHERE id = $instance_id; {kwil_erc20_meta}INSERT INTO epoch_rewards(epoch_id, recipient, amount) - VALUES ($id, $reward_id, $user, $amount) - ON CONFLICT (id, recipient) DO UPDATE SET amount = epoch_rewards.amount + $amount; + VALUES ($epoch_id, $user, $amount) + ON CONFLICT (epoch_id, recipient) DO UPDATE SET amount = epoch_rewards.amount + $amount; `, map[string]any{ - "id": epochID, - "user": user.Bytes(), - "amount": amount, + "instance_id": instanceId, + "epoch_id": epochID, + "user": user.Bytes(), + "amount": amount, }, nil) } @@ -365,7 +367,6 @@ func getEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, aft query += ` GROUP BY e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.ended_at, e.block_hash ORDER BY ended_at ASC LIMIT $limit` - fmt.Println("=====_+_+_+_++__++", query) return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, query, map[string]any{ "instance_id": instanceID, "after": after, @@ -395,12 +396,10 @@ func getEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, aft } var voters []ethcommon.Address - fmt.Printf("+++++++ %+v, %+v\n", voters, r.Values[6]) if r.Values[6] != nil { rawVoters := r.Values[6].([][]byte) // empty value is [[]], cannot use make() for _, rawVoter := range rawVoters { - fmt.Println("+++++++", rawVoter) if len(rawVoter) == 0 { continue } @@ -412,14 +411,10 @@ func getEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, aft } } - fmt.Printf("+++++++ %+v\n", voters) - var voteNonces []int64 - fmt.Printf("+++-----++++ %+v\n", voteNonces, r.Values[7]) if r.Values[7] != nil { rawNonces := r.Values[7].([]*int64) for _, rawNonce := range rawNonces { - fmt.Println("+++-----++++", rawNonce) if rawNonce == nil { continue } @@ -427,8 +422,6 @@ func getEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, aft } } - fmt.Printf("+++-----++++ %+v\n", voteNonces) - return fn(&Epoch{ PendingEpoch: PendingEpoch{ ID: id, diff --git a/node/exts/erc20reward/named_extension.go b/node/exts/erc20reward/named_extension.go index 642f33ea9..1db2c4e68 100644 --- a/node/exts/erc20reward/named_extension.go +++ b/node/exts/erc20reward/named_extension.go @@ -116,7 +116,7 @@ func init() { Name: "issue", Parameters: []precompiles.PrecompileValue{ {Name: "user", Type: types.TextType}, - {Name: "amount", Type: types.TextType}, + {Name: "amount", Type: uint256Numeric}, }, AccessModifiers: []precompiles.Modifier{precompiles.SYSTEM}, Handler: makeMetaHandler("issue"), @@ -125,7 +125,7 @@ func init() { Name: "transfer", Parameters: []precompiles.PrecompileValue{ {Name: "to", Type: types.TextType}, - {Name: "amount", Type: types.TextType}, + {Name: "amount", Type: uint256Numeric}, }, // anybody can call this as long as they have the tokens. // There is no security risk if somebody calls this directly @@ -135,7 +135,7 @@ func init() { { Name: "lock", Parameters: []precompiles.PrecompileValue{ - {Name: "amount", Type: types.TextType}, + {Name: "amount", Type: uint256Numeric}, }, AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC}, Handler: makeMetaHandler("lock"), @@ -144,7 +144,7 @@ func init() { Name: "lock_admin", Parameters: []precompiles.PrecompileValue{ {Name: "user", Type: types.TextType}, - {Name: "amount", Type: types.TextType}, + {Name: "amount", Type: uint256Numeric}, }, AccessModifiers: []precompiles.Modifier{precompiles.SYSTEM}, Handler: makeMetaHandler("lock_admin"), @@ -153,7 +153,7 @@ func init() { Name: "unlock", Parameters: []precompiles.PrecompileValue{ {Name: "user", Type: types.TextType}, - {Name: "amount", Type: types.TextType}, + {Name: "amount", Type: uint256Numeric}, }, AccessModifiers: []precompiles.Modifier{precompiles.SYSTEM}, Handler: makeMetaHandler("unlock"), @@ -166,7 +166,7 @@ func init() { }, Returns: &precompiles.MethodReturn{ Fields: []precompiles.PrecompileValue{ - {Name: "balance", Type: types.TextType}, + {Name: "balance", Type: uint256Numeric}, }, }, AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, @@ -208,7 +208,7 @@ func init() { { Name: "scale_down", Parameters: []precompiles.PrecompileValue{ - {Name: "amount", Type: uint256Numeric}, + {Name: "amount", Type: types.TextType}, }, Returns: &precompiles.MethodReturn{ Fields: []precompiles.PrecompileValue{ diff --git a/node/services/erc20signersvc/kwil.go b/node/services/erc20signersvc/kwil.go index c2f92ca9d..cc495dbd8 100644 --- a/node/services/erc20signersvc/kwil.go +++ b/node/services/erc20signersvc/kwil.go @@ -28,6 +28,8 @@ type Epoch struct { EndHeight int64 RewardRoot []byte BlockHash []byte + Voters []string + VoteNonce []int64 } // TODO: use the type from Ext? diff --git a/node/services/erc20signersvc/signer.go b/node/services/erc20signersvc/signer.go index 6bf5f89fd..b575aaa50 100644 --- a/node/services/erc20signersvc/signer.go +++ b/node/services/erc20signersvc/signer.go @@ -131,8 +131,10 @@ func (s *rewardSigner) init() error { return nil } -// canSkip returns true if the epoch: -// a) is not the owner b) is finalized c) is not finalized already voted from this signer; +// canSkip returns true if: +// - signer is not one of the safe owners +// - signer has voted this epoch +// - is not finalized already voted from this signer; func (s *rewardSigner) canSkip(epoch *Epoch, safeMeta *safeMetadata) bool { // TODO: if no rewards in epoch, skip @@ -141,11 +143,17 @@ func (s *rewardSigner) canSkip(epoch *Epoch, safeMeta *safeMetadata) bool { return true } - //for _, voter := range epoch.Voters { - // if voter == s.signerAddr.String() { - // return true - // } - //} + if epoch.Voters == nil { + return false + } + + // if has vote + for i, voter := range epoch.Voters { + if voter == s.signerAddr.String() && + safeMeta.nonce.Cmp(big.NewInt(epoch.VoteNonce[i])) <= 0 { + return true + } + } return false } From b8c58051e45b12f39d70e437ad4909691767bf07 Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Thu, 13 Feb 2025 15:38:02 -0600 Subject: [PATCH 05/30] fix epoch finaliz/confirm --- node/exts/erc20reward/meta_extension.go | 126 ++++++++++++++++------- node/exts/erc20reward/meta_schema.sql | 5 +- node/exts/erc20reward/meta_sql.go | 43 ++++++-- node/exts/erc20reward/named_extension.go | 16 +++ 4 files changed, 144 insertions(+), 46 deletions(-) diff --git a/node/exts/erc20reward/meta_extension.go b/node/exts/erc20reward/meta_extension.go index 6c9c013b5..81e5b33a1 100644 --- a/node/exts/erc20reward/meta_extension.go +++ b/node/exts/erc20reward/meta_extension.go @@ -298,9 +298,10 @@ func init() { // we dont need to worry about locking the instances yet // because we just read them from the db for _, instance := range instances { - if _, ok := SINGLETON.instances.Get(*instance.ID); ok { - return fmt.Errorf("internal bug: duplicate instance id %s", instance.ID) - } + fmt.Println("yaiba =======instance", instance.ID.String()) + //if _, ok := SINGLETON.instances.Get(*instance.ID); ok { + // return fmt.Errorf("internal bug: duplicate instance id %s", instance.ID) + //} // if instance is active, we should start one of its // two listeners. If it is synced, we should start the @@ -1004,7 +1005,7 @@ func init() { } } - return resultFn([]any{e.ID, e.StartHeight, e.StartTime.Unix(), *e.EndHeight, e.Root, e.BlockHash, + return resultFn([]any{e.ID, e.StartHeight, e.StartTime, *e.EndHeight, e.Root, e.BlockHash, voters, e.VoteNonces, }) @@ -1155,50 +1156,96 @@ func init() { err := SINGLETON.ForEachInstance(true, func(id *types.UUID, info *rewardExtensionInfo) error { info.mu.RLock() defer info.mu.RUnlock() - // if the block is greater than or equal to the start time + distribution period, - // we should propose a new epoch. Otherwise, we should do nothing. - if block.Timestamp < info.currentEpoch.StartTime.Add(time.Duration(info.userProvidedData.DistributionPeriod)*time.Second).Unix() { + // If the block is greater than or equal to the start time + distribution period: + // First if current epoch is not finalized, finalized; if so, jump to next stop + // Then if current epoch is finalized but not confirmed, skip; if so, create new epoch + // Otherwise, we should do nothing. + + // Lifecycle of an epoch + // - created, all issurance will assicoated with this epoch + // - finalized, end of block hook will finalize it when epoch period is reach + // - confirmed, offchain PosterSvc will post finalized epoch on EVM chain, then oracle sync back as confirmed + + fmt.Println("yaiba ===== epoch", info.currentEpoch.ID) + fmt.Println("yaiba ==== block timestamp: ", block.Timestamp) + fmt.Println("yaiba ==== epoch start. ", info.currentEpoch.StartTime) + fmt.Println("yaiba ==== epoch period(s) ", info.userProvidedData.DistributionPeriod) + if block.Timestamp-info.currentEpoch.StartTime < info.userProvidedData.DistributionPeriod { return nil } - - // TODO: if last epoch(if exists) not confirmed, we do nothing. - - var rewards []*EpochReward - err := getRewardsForEpoch(ctx, app, info.currentEpoch.ID, func(reward *EpochReward) error { - rewards = append(rewards, reward) - return nil - }) + // + //if block.Timestamp < info.currentEpoch.StartTime.Add(time.Duration(info.userProvidedData.DistributionPeriod)*time.Second).Unix() { + // return nil + //} + + // 1st round + // Create epochA + // Reward will be issued to epochA + // tick: Finalize epochA & create new epochB; Reward will be issued to epochB + // + // 2rd round + // Reward will be issued to epochB + // tick: If A is confirmed, finalize epochB & create new epochC; Reward will be issued to epochC + // If not, skip; + // + // ... new round is same as 2rd round + + // NOTE: last epoch endHeight = curren epoch startHeight + preExists, preConfirmed, err := previousEpochConfirmed(ctx, app, id, info.currentEpoch.StartHeight) + fmt.Println("yaiba =====preConfirmed", preExists, preConfirmed, err) if err != nil { return err } - users := make([]string, len(rewards)) - amounts := make([]*big.Int, len(rewards)) + if !preExists || // first epoch should always be finalized + (preExists && preConfirmed) { // previous exists and is confirmed + var rewards []*EpochReward + err = getRewardsForEpoch(ctx, app, info.currentEpoch.ID, func(reward *EpochReward) error { + rewards = append(rewards, reward) + return nil + }) + if err != nil { + return err + } - for i, reward := range rewards { - users[i] = reward.Recipient.Hex() - amounts[i] = reward.Amount.BigInt() - } + fmt.Println("yaiba =====rewards", len(rewards)) + if len(rewards) == 0 { // no rewards, delay finalize current epoch + fmt.Println("TODO: log no rewards, delay finalize current epoch") + return nil + } - _, root, err := reward.GenRewardMerkleTree(users, amounts, info.EscrowAddress.Hex(), block.Hash) - if err != nil { - return err - } + users := make([]string, len(rewards)) + amounts := make([]*big.Int, len(rewards)) - err = finalizeEpoch(ctx, app, info.currentEpoch.ID, block.Height, block.Hash[:], root) - if err != nil { - return err - } + for i, reward := range rewards { + users[i] = reward.Recipient.Hex() + amounts[i] = reward.Amount.BigInt() + fmt.Println("yaiba ===== reward", reward.Recipient.Hex(), reward.Amount.BigInt()) + } - // create a new epoch - newEpoch := newPendingEpoch(id, block) - err = createEpoch(ctx, app, newEpoch, id) - if err != nil { - return err - } + _, root, err := reward.GenRewardMerkleTree(users, amounts, info.EscrowAddress.Hex(), block.Hash) + if err != nil { + return err + } + + err = finalizeEpoch(ctx, app, info.currentEpoch.ID, block.Height, block.Hash[:], root) + if err != nil { + return err + } - newEpochs[*id] = newEpoch + // create a new epoch + newEpoch := newPendingEpoch(id, block) + err = createEpoch(ctx, app, newEpoch, id) + if err != nil { + return err + } + + newEpochs[*id] = newEpoch + return nil + } + // if previous epoch exists and not confirmed, we do nothing. + fmt.Println("TODO: log previous epoch is not confirmed yet, skip finalize current epoch") return nil }) if err != nil { @@ -1324,7 +1371,7 @@ func newPendingEpoch(rewardID *types.UUID, block *common.BlockContext) *PendingE return &PendingEpoch{ ID: generateEpochID(rewardID, block.Height), StartHeight: block.Height, - StartTime: time.Unix(block.Timestamp, 0), + StartTime: block.Timestamp, } } @@ -1332,7 +1379,7 @@ func newPendingEpoch(rewardID *types.UUID, block *common.BlockContext) *PendingE type PendingEpoch struct { ID *types.UUID StartHeight int64 - StartTime time.Time + StartTime int64 } // EpochReward is a reward given to a user within an epoch @@ -1346,6 +1393,7 @@ func (p *PendingEpoch) copy() *PendingEpoch { return &PendingEpoch{ ID: &id, StartHeight: p.StartHeight, + StartTime: p.StartTime, } } @@ -1411,7 +1459,7 @@ func (e *extensionInfo) ForEachInstance(readOnly bool, fn func(id *types.UUID, i } for _, kv := range order.OrderMap(orderableMap) { - err := fn(kv.Value.userProvidedData.ID, kv.Value) + err = fn(kv.Value.userProvidedData.ID, kv.Value) if err != nil { return } diff --git a/node/exts/erc20reward/meta_schema.sql b/node/exts/erc20reward/meta_schema.sql index 81c93a79a..a7a547a36 100644 --- a/node/exts/erc20reward/meta_schema.sql +++ b/node/exts/erc20reward/meta_schema.sql @@ -40,6 +40,9 @@ CREATE TABLE balances ( -- 3. Confirmed: the epoch has been confirmed on chain -- Ideally, Kwil would have a unique indexes on this table where "confirmed" is null (to enforce only one active epoch at a time), -- but this requires partial indexes which are not yet supported in Kwil +-- Because we need an epoch to issue reward to, but you should not issue reward to a finalized reward, +-- thus we'll always have two epochs at the same time(except the very first epoch), +-- one is finalized and waiting to be confirmed, the other is collecting new rewards. CREATE TABLE epochs ( id UUID PRIMARY KEY, created_at_block INT8 NOT NULL, -- kwil block height @@ -48,7 +51,7 @@ CREATE TABLE epochs ( reward_root BYTEA UNIQUE, -- the root of the merkle tree of rewards, it's unique per contract ended_at INT8, -- kwil block height block_hash BYTEA, -- the hash of the block that is used in merkle tree leaf, which is the last block of the epoch - confirmed BOOLEAN -- whether the epoch has been confirmed on chain + confirmed BOOLEAN NOT NULL DEFAULT FALSE -- whether the epoch has been confirmed on chain ); -- index helps us query for unconfirmed epochs, which is very common diff --git a/node/exts/erc20reward/meta_sql.go b/node/exts/erc20reward/meta_sql.go index 0991dde17..5e21d9f67 100644 --- a/node/exts/erc20reward/meta_sql.go +++ b/node/exts/erc20reward/meta_sql.go @@ -5,7 +5,6 @@ import ( _ "embed" "errors" "fmt" - "time" ethcommon "github.com/ethereum/go-ethereum/common" @@ -54,7 +53,7 @@ func createEpoch(ctx context.Context, app *common.App, epoch *PendingEpoch, inst )`, map[string]any{ "id": epoch.ID, "created_at_block": epoch.StartHeight, - "created_at_unix": epoch.StartTime.Unix(), + "created_at_unix": epoch.StartTime, "instance_id": instanceID, }, nil) } @@ -104,7 +103,8 @@ func setRewardSynced(ctx context.Context, app *common.App, id *types.UUID, synce }, nil) } -// getStoredRewardInstances gets all stored reward instances. +// getStoredRewardInstances gets all stored reward instances. Also returns the +// current epoch(not finalized) that is being used. func getStoredRewardInstances(ctx context.Context, app *common.App) ([]*rewardExtensionInfo, error) { var rewards []*rewardExtensionInfo err := app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` @@ -112,7 +112,7 @@ func getStoredRewardInstances(ctx context.Context, app *common.App) ([]*rewardEx r.erc20_address, r.erc20_decimals, r.synced_at, r.balance, e.id AS epoch_id, e.created_at_block AS epoch_created_at_block, e.created_at_unix AS epoch_created_at_seconds FROM reward_instances r - LEFT JOIN epochs e on r.id = e.instance_id AND e.confirmed IS NULL + LEFT JOIN epochs e on r.id = e.instance_id AND e.confirmed IS NOT TRUE AND e.ended_at IS NULL `, nil, func(row *common.Row) error { if len(row.Values) != 13 { return fmt.Errorf("expected 13 values, got %d", len(row.Values)) @@ -143,6 +143,7 @@ func getStoredRewardInstances(ctx context.Context, app *common.App) ([]*rewardEx active: row.Values[5].(bool), } + fmt.Printf("yaiba ======", row.Values[10]) if row.Values[10] == nil { return fmt.Errorf("internal bug: instance %s has no epoch", reward.ID) } @@ -154,7 +155,7 @@ func getStoredRewardInstances(ctx context.Context, app *common.App) ([]*rewardEx reward.currentEpoch = &PendingEpoch{ ID: epochID, StartHeight: epochCreatedAtBlock, - StartTime: time.Unix(epochCreatedAtUnix, 0), + StartTime: epochCreatedAtUnix, } if !reward.synced { @@ -354,6 +355,36 @@ func getRewardsForEpoch(ctx context.Context, app *common.App, epochID *types.UUI }) } +// previousEpochConfirmed return whether previous exists and confirmed. +func previousEpochConfirmed(ctx context.Context, app *common.App, instanceID *types.UUID, endBlock int64) (bool, bool, error) { + // if no previous epoch + exist := false + confirmed := false + + err := app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` + {kwil_erc20_meta}SELECT confirmed from epochs + WHERE instance_id = $instance_id AND ended_at = $end_block + `, map[string]any{ + "instance_id": instanceID, + "end_block": endBlock, + }, func(r *common.Row) error { + // might be not necessary + if exist { + return fmt.Errorf("internal bug: expected single record") + } + exist = true + + if len(r.Values) != 1 { + return fmt.Errorf("expected 1 values, got %d", len(r.Values)) + } + + confirmed = r.Values[0].(bool) + return nil + }) + + return exist, confirmed, err +} + // getEpochs gets epochs by given conditions. func getEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, after int64, limit int64, confirmedOnly bool, fn func(*Epoch) error) error { query := ` @@ -426,7 +457,7 @@ func getEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, aft PendingEpoch: PendingEpoch{ ID: id, StartHeight: createdAtBlock, - StartTime: time.Unix(createdAtUnix, 0), + StartTime: createdAtUnix, }, EndHeight: &endedAt, BlockHash: blockHash, diff --git a/node/exts/erc20reward/named_extension.go b/node/exts/erc20reward/named_extension.go index 1db2c4e68..21ebf7a87 100644 --- a/node/exts/erc20reward/named_extension.go +++ b/node/exts/erc20reward/named_extension.go @@ -195,6 +195,22 @@ func init() { AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, Handler: makeMetaHandler("list_epochs"), }, + { + // Supposed to be called by the SignerService, to verify the reward root. + Name: "get_epoch_rewards", + Parameters: []precompiles.PrecompileValue{ + {Name: "epoch_id", Type: types.UUIDType}, + }, + Returns: &precompiles.MethodReturn{ + IsTable: true, + Fields: []precompiles.PrecompileValue{ + {Name: "recipient", Type: types.TextType}, + {Name: "amount", Type: types.TextType}, + }, + }, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, + Handler: makeMetaHandler("get_epoch_rewards"), + }, { Name: "decimals", Returns: &precompiles.MethodReturn{ From f931a7a534a1a88ff00eb43701ad052b41fb157c Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Fri, 14 Feb 2025 14:22:08 -0600 Subject: [PATCH 06/30] list wallet rewards --- cmd/kwil-cli/cmds/call-action.go | 2 - core/rpc/client/jsonrpc.go | 2 - .../erc20reward/abigen/reward_distributor.go | 1 - node/exts/erc20reward/meta_extension.go | 416 +++++++++++------- node/exts/erc20reward/meta_schema.sql | 6 +- node/exts/erc20reward/meta_sql.go | 273 ++++++++---- node/exts/erc20reward/meta_sql_test.go | 6 +- node/exts/erc20reward/named_extension.go | 112 +++-- node/exts/erc20reward/reward/crypto.go | 8 +- node/exts/erc20reward/reward/mtree.go | 14 +- node/exts/erc20reward/reward/mtree_test.go | 6 +- node/exts/evm-sync/chains/chains.go | 4 + node/exts/evm-sync/listener.go | 1 + node/services/erc20signersvc/eth_test.go | 4 +- node/services/erc20signersvc/kwil.go | 116 ++--- node/services/erc20signersvc/multicall.go | 1 + node/services/erc20signersvc/signer.go | 160 ++++--- 17 files changed, 715 insertions(+), 417 deletions(-) diff --git a/cmd/kwil-cli/cmds/call-action.go b/cmd/kwil-cli/cmds/call-action.go index 502e17feb..6f3f6354c 100644 --- a/cmd/kwil-cli/cmds/call-action.go +++ b/cmd/kwil-cli/cmds/call-action.go @@ -113,8 +113,6 @@ func callActionCmd() *cobra.Command { return display.PrintErr(cmd, err) } - fmt.Printf("--------Result: %+v\n", res.QueryResult) - return display.PrintCmd(cmd, &respCall{Data: res, PrintLogs: logs, cmd: cmd}) }) }, diff --git a/core/rpc/client/jsonrpc.go b/core/rpc/client/jsonrpc.go index 84fc54dc7..e1753a4f6 100644 --- a/core/rpc/client/jsonrpc.go +++ b/core/rpc/client/jsonrpc.go @@ -202,8 +202,6 @@ func (cl *JSONRPCClient) CallMethod(ctx context.Context, method string, cmd, res // fmt.Printf("got id %v, expected %v\n", resp.ID, id) // } // who cares, this is http post - fmt.Println("====+++++++++resp.Result: ", string(resp.Result)) - if err = json.Unmarshal(resp.Result, res); err != nil { return fmt.Errorf("failed to decode result as response: %w", errors.Join(err, httpErr)) } diff --git a/node/exts/erc20reward/abigen/reward_distributor.go b/node/exts/erc20reward/abigen/reward_distributor.go index 69634710f..9433b2ca6 100644 --- a/node/exts/erc20reward/abigen/reward_distributor.go +++ b/node/exts/erc20reward/abigen/reward_distributor.go @@ -873,7 +873,6 @@ type RewardDistributorRewardPosted struct { // // Solidity: event RewardPosted(bytes32 root, uint256 amount, address poster) func (_RewardDistributor *RewardDistributorFilterer) FilterRewardPosted(opts *bind.FilterOpts) (*RewardDistributorRewardPostedIterator, error) { - logs, sub, err := _RewardDistributor.contract.FilterLogs(opts, "RewardPosted") if err != nil { return nil, err diff --git a/node/exts/erc20reward/meta_extension.go b/node/exts/erc20reward/meta_extension.go index 81e5b33a1..78f2983c1 100644 --- a/node/exts/erc20reward/meta_extension.go +++ b/node/exts/erc20reward/meta_extension.go @@ -23,6 +23,10 @@ import ( "sync" "time" + "github.com/decred/dcrd/container/lru" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/samber/lo" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" ethcommon "github.com/ethereum/go-ethereum/common" @@ -48,6 +52,8 @@ import ( const ( RewardMetaExtensionName = "kwil_erc20_meta" uint256Precision = 78 + + rewardMerkleTreeLRUSize = 1000 ) var ( @@ -68,6 +74,8 @@ var ( // so that we know how to decode them logTypeTransfer = []byte("e20trsnfr") logTypeConfirmedEpoch = []byte("cnfepch") + + mtLRUCache = lru.NewMap[[32]byte, []byte](rewardMerkleTreeLRUSize) // tree root => tree body ) // generates a deterministic UUID for the chain and escrow @@ -298,11 +306,6 @@ func init() { // we dont need to worry about locking the instances yet // because we just read them from the db for _, instance := range instances { - fmt.Println("yaiba =======instance", instance.ID.String()) - //if _, ok := SINGLETON.instances.Get(*instance.ID); ok { - // return fmt.Errorf("internal bug: duplicate instance id %s", instance.ID) - //} - // if instance is active, we should start one of its // two listeners. If it is synced, we should start the // transfer listener. Otherwise, we should start the state poller @@ -821,6 +824,10 @@ func init() { return err } + if bal == nil { + bal, _ = erc20ValueFromBigInt(big.NewInt(0)) + } + return resultFn([]any{bal}) }, }, @@ -920,74 +927,87 @@ func init() { return resultFn([]any{scaled}) }, }, - // { - // // Supposed to be called by Signer service - // // Returns epoch rewards after(non-include) after_height, in ASC order. - // Name: "list_epochs", - // Parameters: []precompiles.PrecompileValue{ - // {Name: "id", Type: types.UUIDType}, - // {Name: "after_height", Type: types.IntType}, - // {Name: "limit", Type: types.IntType}, - // }, - // Returns: &precompiles.MethodReturn{ - // IsTable: true, - // Fields: (&Epoch{}).UnpackTypes(), // TODO: I might need to update this depending on what happens with decimal types - // }, - // AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, - // Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { - // panic("finish me") - // }, - // }, - // { - // // Supposed to be called by the SignerService, to verify the reward root. - // // Could be merged into 'list_epochs' - // // Returns pending rewards from(include) start_height to(include) end_height, in ASC order. - // // NOTE: Rewards of same address will be aggregated. - // Name: "search_rewards", - // Parameters: []precompiles.PrecompileValue{ - // {Name: "id", Type: types.UUIDType}, - // {Name: "start_height", Type: types.IntType}, - // {Name: "end_height", Type: types.IntType}, - // }, - // Returns: &precompiles.MethodReturn{ - // IsTable: true, - // Fields: (&Reward{}).UnpackTypes(), // TODO: I might need to update this depending on what happens with decimal types - // }, - // AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, - // Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { - // panic("finish me") - // }, - // }, - // { - // // Supposed to be called by Kwil network in an end block hook. - // Name: "propose_epoch", - // AccessModifiers: []precompiles.Modifier{precompiles.SYSTEM}, - // Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { - // if !calledByExtension(ctx) { - // return errors.New("propose_epoch can only be called by the Kwil network") - // } + { + // lists only active epochs: one collects reward; one waits to be confirmed + Name: "get_active_epochs", + Parameters: []precompiles.PrecompileValue{ + {Name: "id", Type: types.UUIDType}, + }, + Returns: &precompiles.MethodReturn{ + IsTable: true, + Fields: []precompiles.PrecompileValue{ + {Name: "id", Type: types.UUIDType}, + {Name: "start_height", Type: types.IntType}, + {Name: "start_timestamp", Type: types.IntType}, + {Name: "end_height", Type: types.IntType, Nullable: true}, + {Name: "reward_root", Type: types.ByteaType, Nullable: true}, + {Name: "reward_amount", Type: types.TextType, Nullable: true}, + {Name: "end_block_hash", Type: types.ByteaType, Nullable: true}, + {Name: "confirmed", Type: types.BoolType}, + {Name: "voters", Type: types.TextArrayType, Nullable: true}, + {Name: "vote_amounts", Type: types.TextArrayType, Nullable: true}, + {Name: "vote_nonces", Type: types.IntArrayType, Nullable: true}, + {Name: "voter_signatures", Type: types.ByteaArrayType, Nullable: true}, + }, + }, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, + Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { + id := inputs[0].(*types.UUID) + + return getActiveEpochs(ctx.TxContext.Ctx, app, id, func(e *Epoch) error { + var voters []string + if len(e.Voters) > 0 { + for _, item := range e.Voters { + voters = append(voters, item.String()) + } + } + + var voteAmts []string + if len(e.VoteAmounts) > 0 { + for _, item := range e.VoteAmounts { + voteAmts = append(voteAmts, item.String()) + } + } + + total := "0" + if e.Total != nil { + total = e.Total.String() + } + + return resultFn([]any{e.ID, e.StartHeight, e.StartTime, *e.EndHeight, e.Root, total, e.BlockHash, e.Confirmed, + voters, + voteAmts, + e.VoteNonces, + e.VoteSigs, + }) + }) + }}, { // lists epochs after(non-include) given height, in ASC order. - // If confirmedOnly is true, only returns confirmed epochs. - // NOTE: only unfirmed epoch will return voters and vote_nonces + // If finalized_only is true, only returns finalized yet not confirmed epochs. + // NOTE: only un-confirmed epoch will return voters and vote_nonces Name: "list_epochs", Parameters: []precompiles.PrecompileValue{ {Name: "id", Type: types.UUIDType}, {Name: "after", Type: types.IntType}, {Name: "limit", Type: types.IntType}, - {Name: "confirmed_only", Type: types.BoolType}, + {Name: "finalized_only", Type: types.BoolType}, }, Returns: &precompiles.MethodReturn{ IsTable: true, Fields: []precompiles.PrecompileValue{ - {Name: "epoch_id", Type: types.UUIDType}, + {Name: "id", Type: types.UUIDType}, {Name: "start_height", Type: types.IntType}, {Name: "start_timestamp", Type: types.IntType}, {Name: "end_height", Type: types.IntType, Nullable: true}, {Name: "reward_root", Type: types.ByteaType, Nullable: true}, + {Name: "reward_amount", Type: types.TextType, Nullable: true}, {Name: "end_block_hash", Type: types.ByteaType, Nullable: true}, + {Name: "confirmed", Type: types.BoolType}, {Name: "voters", Type: types.TextArrayType, Nullable: true}, + {Name: "vote_amounts", Type: types.TextArrayType, Nullable: true}, {Name: "vote_nonces", Type: types.IntArrayType, Nullable: true}, + {Name: "voter_signatures", Type: types.ByteaArrayType, Nullable: true}, }, }, AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, @@ -995,9 +1015,9 @@ func init() { id := inputs[0].(*types.UUID) after := inputs[1].(int64) limit := inputs[2].(int64) - confirmedOnly := inputs[3].(bool) + finalizedOnly := inputs[3].(bool) - return getEpochs(ctx.TxContext.Ctx, app, id, after, limit, confirmedOnly, func(e *Epoch) error { + return getEpochs(ctx.TxContext.Ctx, app, id, after, limit, finalizedOnly, func(e *Epoch) error { var voters []string if len(e.Voters) > 0 { for _, item := range e.Voters { @@ -1005,15 +1025,29 @@ func init() { } } - return resultFn([]any{e.ID, e.StartHeight, e.StartTime, *e.EndHeight, e.Root, e.BlockHash, + var voteAmts []string + if len(e.VoteAmounts) > 0 { + for _, item := range e.VoteAmounts { + voteAmts = append(voteAmts, item.String()) + } + } + + total := "0" + if e.Total != nil { + total = e.Total.String() + } + + return resultFn([]any{e.ID, e.StartHeight, e.StartTime, *e.EndHeight, e.Root, total, e.BlockHash, e.Confirmed, voters, + voteAmts, e.VoteNonces, + e.VoteSigs, }) }) }, }, { - // Supposed to be called by the SignerService, to verify the reward root. + // get all rewards associated with given epoch_id Name: "get_epoch_rewards", Parameters: []precompiles.PrecompileValue{ {Name: "id", Type: types.UUIDType}, @@ -1036,18 +1070,25 @@ func init() { }, }, { - // Supposed to be called by a multisig signer Name: "vote_epoch", Parameters: []precompiles.PrecompileValue{ {Name: "id", Type: types.UUIDType}, {Name: "epoch_id", Type: types.UUIDType}, + {Name: "amount", Type: uint256Numeric}, + {Name: "nonce", Type: types.IntType}, {Name: "signature", Type: types.ByteaType}, }, AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC}, Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { //id := inputs[0].(*types.UUID) epochID := inputs[1].(*types.UUID) - signature := inputs[2].([]byte) + amount := inputs[2].(*types.Decimal) + nonce := inputs[3].(int64) + signature := inputs[4].([]byte) + + if amount.IsNegative() { + return fmt.Errorf("amount cannot be negative") + } if len(signature) != reward.GnosisSafeSigLength { return fmt.Errorf("signature is not 65 bytes") @@ -1067,47 +1108,107 @@ func init() { return fmt.Errorf("epoch is already confirmed") } - return voteEpoch(ctx.TxContext.Ctx, app, epochID, from, signature) + return voteEpoch(ctx.TxContext.Ctx, app, epochID, from, amount, nonce, signature) + }, + }, + { + // list all the rewards of the given wallet; + // if pending=true, the results will include all finalized(not necessary confirmed) rewards + Name: "list_wallet_rewards", + Parameters: []precompiles.PrecompileValue{ + {Name: "id", Type: types.UUIDType}, + {Name: "wallet", Type: types.TextType}, + {Name: "pending", Type: types.BoolType}, + }, + Returns: &precompiles.MethodReturn{ + IsTable: true, + Fields: []precompiles.PrecompileValue{ + {Name: "chain", Type: types.TextType}, + {Name: "chain_id", Type: types.TextType}, + {Name: "contract", Type: types.TextType}, + {Name: "etherscan", Type: types.TextType}, + {Name: "created_at", Type: types.IntType}, + {Name: "params", Type: types.TextArrayType}, // recipient,amount,block_hash,root,proofs + }, + }, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, + Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { + id := inputs[0].(*types.UUID) + wallet := inputs[1].(string) + walletAddr, err := ethAddressFromHex(wallet) + if err != nil { + return err + } + + pending := inputs[2].(bool) + + info, err := SINGLETON.getUsableInstance(id) + if err != nil { + return err + } + + info.mu.RLock() + defer info.mu.RUnlock() + + var epochs []*Epoch + err = getWalletEpochs(ctx.TxContext.Ctx, app, id, walletAddr, pending, func(e *Epoch) error { + epochs = append(epochs, e) + return nil + }) + if err != nil { + return fmt.Errorf("get wallet epochs :%w", err) + } + + var jsonTree, root []byte + var ok bool + for _, epoch := range epochs { + var b32Root [32]byte + copy(b32Root[:], epoch.Root) + + jsonTree, ok = mtLRUCache.Get(b32Root) + if !ok { + var b32Hash [32]byte + copy(b32Hash[:], epoch.BlockHash) + _, jsonTree, root, _, err = genMerkleTreeForEpoch(ctx.TxContext.Ctx, app, epoch.ID, info.EscrowAddress.Hex(), b32Hash) + if err != nil { + return err + } + + if !bytes.Equal(root, epoch.Root) { + return fmt.Errorf("internal bug: epoch root mismatch") + } + + mtLRUCache.Put(b32Root, jsonTree) + } + + _, proofs, _, bh, uint256AmtStr, err := reward.GetMTreeProof(jsonTree, walletAddr.String()) + if err != nil { + return err + } + + err = resultFn([]any{info.ChainInfo.Name.String(), + info.ChainInfo.ID, + info.EscrowAddress.String(), + info.ChainInfo.Etherscan + info.EscrowAddress.String() + "#writeContract", + epoch.EndHeight, + []string{ + walletAddr.String(), + uint256AmtStr, + hexutil.Encode(bh), + hexutil.Encode(epoch.Root), + strings.Join(lo.Map(proofs, func(item []byte, _ int) string { + return hexutil.Encode(item) + }), ","), // comma separated byte32str + }, + }) + if err != nil { + return err + } + } + + return nil }, }, - // { - // // Lists all epochs that have received enough votes - // Name: "list_finalized", - // Parameters: []precompiles.PrecompileValue{ - // {Name: "id", Type: types.UUIDType}, - // {Name: "after_height", Type: types.IntType}, - // {Name: "limit", Type: types.IntType}, - // }, - // Returns: &precompiles.MethodReturn{ - // IsTable: true, - // Fields: (&FinalizedReward{}).UnpackTypes(), // TODO: I might need to update this depending on what happens with decimal types - // }, - // AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, - // Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { - // panic("finish me") - // }, - // }, - // { - // // Called by app / user - // Name: "claim_info", - // Parameters: []precompiles.PrecompileValue{ - // {Name: "id", Type: types.UUIDType}, - // {Name: "sign_hash", Type: types.ByteaType, Nullable: false}, - // {Name: "wallet_address", Type: types.TextType, Nullable: false}, - // }, - // Returns: &precompiles.MethodReturn{ - // Fields: []precompiles.PrecompileValue{ - // {Name: "amount", Type: types.TextType}, - // {Name: "block_hash", Type: types.TextType}, - // {Name: "root", Type: types.TextType}, - // {Name: "proofs", Type: types.TextArrayType}, - // }, - // }, - // AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, - // Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { - // panic("finish me") - // }, - // }, }, }, nil }) @@ -1156,83 +1257,51 @@ func init() { err := SINGLETON.ForEachInstance(true, func(id *types.UUID, info *rewardExtensionInfo) error { info.mu.RLock() defer info.mu.RUnlock() - // If the block is greater than or equal to the start time + distribution period: - // First if current epoch is not finalized, finalized; if so, jump to next stop - // Then if current epoch is finalized but not confirmed, skip; if so, create new epoch - // Otherwise, we should do nothing. - - // Lifecycle of an epoch - // - created, all issurance will assicoated with this epoch - // - finalized, end of block hook will finalize it when epoch period is reach - // - confirmed, offchain PosterSvc will post finalized epoch on EVM chain, then oracle sync back as confirmed - - fmt.Println("yaiba ===== epoch", info.currentEpoch.ID) - fmt.Println("yaiba ==== block timestamp: ", block.Timestamp) - fmt.Println("yaiba ==== epoch start. ", info.currentEpoch.StartTime) - fmt.Println("yaiba ==== epoch period(s) ", info.userProvidedData.DistributionPeriod) + // If the block is greater than or equal to the start time + distribution period: Otherwise, we should do nothing. if block.Timestamp-info.currentEpoch.StartTime < info.userProvidedData.DistributionPeriod { return nil } - // - //if block.Timestamp < info.currentEpoch.StartTime.Add(time.Duration(info.userProvidedData.DistributionPeriod)*time.Second).Unix() { - // return nil - //} - - // 1st round - // Create epochA - // Reward will be issued to epochA - // tick: Finalize epochA & create new epochB; Reward will be issued to epochB - // - // 2rd round - // Reward will be issued to epochB - // tick: If A is confirmed, finalize epochB & create new epochC; Reward will be issued to epochC - // If not, skip; - // - // ... new round is same as 2rd round + + // There will be always 2 epochs(except the very first epoch): + // - finalized epoch: finalized but not confirmed, wait to be confimed + // - current epoch: collect all new rewards, wait to be finalized + // Thus: + // - The first epoch should always be finalized + // - All other epochs wait for their previous epoch to be confirmed before finalizing and creating a new one. // NOTE: last epoch endHeight = curren epoch startHeight preExists, preConfirmed, err := previousEpochConfirmed(ctx, app, id, info.currentEpoch.StartHeight) - fmt.Println("yaiba =====preConfirmed", preExists, preConfirmed, err) if err != nil { return err } if !preExists || // first epoch should always be finalized - (preExists && preConfirmed) { // previous exists and is confirmed - var rewards []*EpochReward - err = getRewardsForEpoch(ctx, app, info.currentEpoch.ID, func(reward *EpochReward) error { - rewards = append(rewards, reward) - return nil - }) + (preExists && preConfirmed) { // previous epoch exists and is confirmed + leafNum, jsonBody, root, total, err := genMerkleTreeForEpoch(ctx, app, info.currentEpoch.ID, info.EscrowAddress.Hex(), block.Hash) if err != nil { return err } - fmt.Println("yaiba =====rewards", len(rewards)) - if len(rewards) == 0 { // no rewards, delay finalize current epoch - fmt.Println("TODO: log no rewards, delay finalize current epoch") + if leafNum == 0 { + app.Service.Logger.Info("no rewards to distribute, deplay finalized current epoch") return nil } - users := make([]string, len(rewards)) - amounts := make([]*big.Int, len(rewards)) - - for i, reward := range rewards { - users[i] = reward.Recipient.Hex() - amounts[i] = reward.Amount.BigInt() - fmt.Println("yaiba ===== reward", reward.Recipient.Hex(), reward.Amount.BigInt()) - } - - _, root, err := reward.GenRewardMerkleTree(users, amounts, info.EscrowAddress.Hex(), block.Hash) + erc20Total, err := erc20ValueFromBigInt(total) if err != nil { return err } - err = finalizeEpoch(ctx, app, info.currentEpoch.ID, block.Height, block.Hash[:], root) + err = finalizeEpoch(ctx, app, info.currentEpoch.ID, block.Height, block.Hash[:], root, erc20Total) if err != nil { return err } + // put in cache + var b32Root [32]byte + copy(b32Root[:], root) + mtLRUCache.Put(b32Root, jsonBody) + // create a new epoch newEpoch := newPendingEpoch(id, block) err = createEpoch(ctx, app, newEpoch, id) @@ -1245,7 +1314,7 @@ func init() { } // if previous epoch exists and not confirmed, we do nothing. - fmt.Println("TODO: log previous epoch is not confirmed yet, skip finalize current epoch") + app.Service.Logger.Info("log previous epoch is not confirmed yet, skip finalize current epoch") return nil }) if err != nil { @@ -1268,6 +1337,39 @@ func init() { } } +func genMerkleTreeForEpoch(ctx context.Context, app *common.App, epochID *types.UUID, + escrowAddr string, blockHash [32]byte) (leafNum int, jsonTree []byte, root []byte, total *big.Int, err error) { + var rewards []*EpochReward + err = getRewardsForEpoch(ctx, app, epochID, func(reward *EpochReward) error { + rewards = append(rewards, reward) + return nil + }) + if err != nil { + return 0, nil, nil, nil, err + } + + if len(rewards) == 0 { // no rewards, delay finalize current epoch + return 0, nil, nil, nil, nil // should skip + } + + users := make([]string, len(rewards)) + amounts := make([]*big.Int, len(rewards)) + total = big.NewInt(0) + + for i, r := range rewards { + users[i] = r.Recipient.Hex() + amounts[i] = r.Amount.BigInt() + total.Add(total, amounts[i]) + } + + jsonTree, root, err = reward.GenRewardMerkleTree(users, amounts, escrowAddr, blockHash) + if err != nil { + return 0, nil, nil, nil, err + } + + return len(rewards), jsonTree, root, total, nil +} + func genesisExec(ctx context.Context, app *common.App) error { // we will create the schema at genesis err := app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, "USE kwil_erc20_meta AS kwil_erc20_meta", nil, nil) @@ -1398,16 +1500,20 @@ func (p *PendingEpoch) copy() *PendingEpoch { } type EpochVoteInfo struct { - Voters []ethcommon.Address - VoteNonces []int64 + Voters []ethcommon.Address + VoteAmounts []*types.Decimal + VoteSigs [][]byte + VoteNonces []int64 } // Epoch is a period in which rewards are distributed. type Epoch struct { PendingEpoch EndHeight *int64 // nil if not finalized - BlockHash []byte // hash of the block that finalized the epoch, nil if not finalized Root []byte // merkle root of all rewards, nil if not finalized + Total *types.Decimal + BlockHash []byte // hash of the block that finalized the epoch, nil if not finalized + Confirmed bool EpochVoteInfo } diff --git a/node/exts/erc20reward/meta_schema.sql b/node/exts/erc20reward/meta_schema.sql index a7a547a36..513cd1593 100644 --- a/node/exts/erc20reward/meta_schema.sql +++ b/node/exts/erc20reward/meta_schema.sql @@ -49,6 +49,7 @@ CREATE TABLE epochs ( created_at_unix INT8 NOT NULL, -- unix timestamp (in seconds) instance_id UUID NOT NULL REFERENCES reward_instances(id) ON UPDATE RESTRICT ON DELETE RESTRICT, reward_root BYTEA UNIQUE, -- the root of the merkle tree of rewards, it's unique per contract + reward_amount NUMERIC(78, 0), -- the total amount in current epoch ended_at INT8, -- kwil block height block_hash BYTEA, -- the hash of the block that is used in merkle tree leaf, which is the last block of the epoch confirmed BOOLEAN NOT NULL DEFAULT FALSE -- whether the epoch has been confirmed on chain @@ -78,7 +79,8 @@ CREATE TABLE meta CREATE TABLE epoch_votes ( epoch_id UUID NOT NULL REFERENCES epochs(id) ON UPDATE RESTRICT ON DELETE RESTRICT, voter BYTEA NOT NULL, + amount NUMERIC(78, 0) NOT NULL, -- so posterSvc won't need to calculate again + nonce INT8 NOT NULL, -- safe nonce; this helps to skip unnecessary dup votes signature BYTEA NOT NULL, - nonce INT8 NOT NULL, -- safe nonce; technically we don't need this, but this helps to identify why a signer is not valid - PRIMARY KEY (epoch_id, voter) + PRIMARY KEY (epoch_id, voter, nonce) ); \ No newline at end of file diff --git a/node/exts/erc20reward/meta_sql.go b/node/exts/erc20reward/meta_sql.go index 5e21d9f67..6cc39ef14 100644 --- a/node/exts/erc20reward/meta_sql.go +++ b/node/exts/erc20reward/meta_sql.go @@ -60,27 +60,32 @@ func createEpoch(ctx context.Context, app *common.App, epoch *PendingEpoch, inst // finalizeEpoch finalizes an epoch. // It sets the end height, block hash, and reward root -func finalizeEpoch(ctx context.Context, app *common.App, epochID *types.UUID, endHeight int64, blockHash []byte, root []byte) error { +func finalizeEpoch(ctx context.Context, app *common.App, epochID *types.UUID, endHeight int64, blockHash []byte, root []byte, total *types.Decimal) error { return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` {kwil_erc20_meta}UPDATE epochs SET ended_at = $ended_at, block_hash = $block_hash, - reward_root = $reward_root + reward_root = $reward_root, + reward_amount = $reward_amount WHERE id = $id `, map[string]any{ - "id": epochID, - "ended_at": endHeight, - "block_hash": blockHash, - "reward_root": root, + "id": epochID, + "ended_at": endHeight, + "block_hash": blockHash, + "reward_root": root, + "reward_amount": total, }, nil) } -// confirmEpoch confirms an epoch was received on-chain +// confirmEpoch confirms an epoch was received on-chain, also delete all the votes +// associated with the epoch. func confirmEpoch(ctx context.Context, app *common.App, root []byte) error { return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` {kwil_erc20_meta}UPDATE epochs SET confirmed = true - WHERE reward_root = $root + WHERE reward_root = $root; + + {kwil_erc20_meta}DELETE FROM epoch_votes where epoch_id=(SELECT id FROM epochs WHERE reward_root = $root); `, map[string]any{ "root": root, }, nil) @@ -143,7 +148,6 @@ func getStoredRewardInstances(ctx context.Context, app *common.App) ([]*rewardEx active: row.Values[5].(bool), } - fmt.Printf("yaiba ======", row.Values[10]) if row.Values[10] == nil { return fmt.Errorf("internal bug: instance %s has no epoch", reward.ID) } @@ -385,88 +389,155 @@ func previousEpochConfirmed(ctx context.Context, app *common.App, instanceID *ty return exist, confirmed, err } -// getEpochs gets epochs by given conditions. -func getEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, after int64, limit int64, confirmedOnly bool, fn func(*Epoch) error) error { - query := ` - {kwil_erc20_meta}SELECT e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.ended_at, e.block_hash, array_agg(v.voter) as voters, array_agg(v.nonce) as nonces - FROM epochs AS e - LEFT JOIN epoch_votes AS v ON v.epoch_id = e.id - WHERE e.instance_id = $instance_id AND e.created_at_block > $after` - if confirmedOnly { - query += ` AND confirmed IS true` +func rowToEpoch(r *common.Row) (*Epoch, error) { + if len(r.Values) != 12 { + return nil, fmt.Errorf("expected 12 values, got %d", len(r.Values)) } - query += ` - GROUP BY e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.ended_at, e.block_hash - ORDER BY ended_at ASC LIMIT $limit` - return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, query, map[string]any{ - "instance_id": instanceID, - "after": after, - "limit": limit, - }, func(r *common.Row) error { - if len(r.Values) != 8 { - return fmt.Errorf("expected 8 values, got %d", len(r.Values)) - } - id := r.Values[0].(*types.UUID) - createdAtBlock := r.Values[1].(int64) - createdAtUnix := r.Values[2].(int64) + id := r.Values[0].(*types.UUID) + createdAtBlock := r.Values[1].(int64) + createdAtUnix := r.Values[2].(int64) - var rewardRoot []byte - if r.Values[3] != nil { - rewardRoot = r.Values[3].([]byte) - } + var rewardRoot []byte + if r.Values[3] != nil { + rewardRoot = r.Values[3].([]byte) + } - var endedAt int64 - if r.Values[4] != nil { - endedAt = r.Values[4].(int64) - } + var rewardAmount *types.Decimal + if r.Values[4] != nil { + rewardAmount = r.Values[4].(*types.Decimal) + } + + var endedAt int64 + if r.Values[5] != nil { + endedAt = r.Values[5].(int64) + } - var blockHash []byte - if r.Values[5] != nil { - blockHash = r.Values[5].([]byte) + var blockHash []byte + if r.Values[6] != nil { + blockHash = r.Values[6].([]byte) + } + + confirmed := r.Values[7].(bool) + + // NOTE: empty value is [[]] + // NOTE: should not use append, we could accidently skip some 'nil' element and messup the index + // But we can be sure values[7]-values[10] will all be empty if any, from the SQL; + var voters []ethcommon.Address + if r.Values[8] != nil { + rawVoters := r.Values[8].([][]byte) + // empty value is [[]], cannot use make(), otherwise we'll have a empty `ethcommon.Address` + for _, rawVoter := range rawVoters { + if len(rawVoter) == 0 { + continue + } + voter, err := bytesToEthAddress(rawVoter) + if err != nil { + return nil, err + } + voters = append(voters, voter) } + } - var voters []ethcommon.Address - if r.Values[6] != nil { - rawVoters := r.Values[6].([][]byte) - // empty value is [[]], cannot use make() - for _, rawVoter := range rawVoters { - if len(rawVoter) == 0 { - continue - } - voter, err := bytesToEthAddress(rawVoter) - if err != nil { - return err - } - voters = append(voters, voter) + // NOTE: empty value is [] + var amounts []*types.Decimal + if r.Values[9] != nil { + for _, rawAmount := range r.Values[9].([]*types.Decimal) { + if rawAmount != nil { + amounts = append(amounts, rawAmount) } } + } - var voteNonces []int64 - if r.Values[7] != nil { - rawNonces := r.Values[7].([]*int64) - for _, rawNonce := range rawNonces { - if rawNonce == nil { - continue - } + // NOTE: empty value is [] + var voteNonces []int64 + if r.Values[10] != nil { + rawNonces := r.Values[10].([]*int64) + for _, rawNonce := range rawNonces { + if rawNonce != nil { + // NOTE: this is probably problematic, since we can messup the index + // If we don't skip, return -1 ? voteNonces = append(voteNonces, *rawNonce) } } + } - return fn(&Epoch{ - PendingEpoch: PendingEpoch{ - ID: id, - StartHeight: createdAtBlock, - StartTime: createdAtUnix, - }, - EndHeight: &endedAt, - BlockHash: blockHash, - Root: rewardRoot, - EpochVoteInfo: EpochVoteInfo{ - Voters: voters, - VoteNonces: voteNonces, - }, - }) + // NOTE: empty value is [[]] + var signatures [][]byte + if r.Values[11] != nil { + // we skip the empty value, otherwise after conversion, [] will be returned + for _, rawSig := range r.Values[11].([][]byte) { + if len(rawSig) != 0 { + signatures = append(signatures, rawSig) + } + } + } + + return &Epoch{ + PendingEpoch: PendingEpoch{ + ID: id, + StartHeight: createdAtBlock, + StartTime: createdAtUnix, + }, + EndHeight: &endedAt, + BlockHash: blockHash, + Root: rewardRoot, + Total: rewardAmount, + Confirmed: confirmed, + EpochVoteInfo: EpochVoteInfo{ + Voters: voters, + VoteAmounts: amounts, + VoteSigs: signatures, + VoteNonces: voteNonces, + }, + }, nil +} + +// getActiveEpochs get current active epochs, at most two: +// one collects all new rewards, and one waits to be confirmed. +func getActiveEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, fn func(*Epoch) error) error { + query := ` + {kwil_erc20_meta}SELECT e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.reward_amount, e.ended_at, e.block_hash, e.confirmed, array_agg(v.voter) as voters, array_agg(v.amount) as amounts, array_agg(v.nonce) as nonces, array_agg(v.signature) as signatures + FROM epochs AS e + LEFT JOIN epoch_votes AS v ON v.epoch_id = e.id + WHERE e.instance_id = $instance_id AND e.confirmed IS NOT true + GROUP BY e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.reward_amount, e.ended_at, e.block_hash, e.confirmed + ORDER BY e.created_at_block ASC ` + return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, query, map[string]any{ + "instance_id": instanceID, + }, func(r *common.Row) error { + epoch, err := rowToEpoch(r) + if err != nil { + return err + } + return fn(epoch) + }) +} + +// getEpochs gets epochs by given conditions. +func getEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, after int64, limit int64, finalizedOnly bool, fn func(*Epoch) error) error { + query := ` + {kwil_erc20_meta}SELECT e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.reward_amount, e.ended_at, e.block_hash, e.confirmed, array_agg(v.voter) as voters, array_agg(v.amount) as amounts, array_agg(v.nonce) as nonces, array_agg(v.signature) as signatures + FROM epochs AS e + LEFT JOIN epoch_votes AS v ON v.epoch_id = e.id + WHERE e.instance_id = $instance_id AND e.created_at_block > $after` + if finalizedOnly { + query += ` AND e.ended_at IS NOT NULL AND e.confirmed IS NOT true` // finalized but not confirmed + } + query += ` + GROUP BY e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.reward_amount, e.ended_at, e.block_hash, e.confirmed + ORDER BY e.ended_at ASC LIMIT $limit` + + return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, query, map[string]any{ + "instance_id": instanceID, + "after": after, + "limit": limit, + }, func(r *common.Row) error { + epoch, err := rowToEpoch(r) + if err != nil { + return err + } + return fn(epoch) }) } @@ -538,14 +609,17 @@ func epochConfirmed(ctx context.Context, app *common.App, epochID *types.UUID) ( } // voteEpoch vote an epoch by submitting signature. -func voteEpoch(ctx context.Context, app *common.App, epochID *types.UUID, voter ethcommon.Address, signature []byte) error { +func voteEpoch(ctx context.Context, app *common.App, epochID *types.UUID, + voter ethcommon.Address, amount *types.Decimal, nonce int64, signature []byte) error { return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` - {kwil_erc20_meta}INSERT into epoch_votes(epoch_id, voter, signature) - VALUES ($epoch_id, $voter, $signature); + {kwil_erc20_meta}INSERT into epoch_votes(epoch_id, voter, amount, nonce, signature) + VALUES ($epoch_id, $voter, $amount, $nonce, $signature); `, map[string]any{ "epoch_id": epochID, "voter": voter.Bytes(), + "amount": amount, "signature": signature, + "nonce": nonce, }, nil) } @@ -558,3 +632,42 @@ func removeEpochVotes(ctx context.Context, app *common.App, epochID *types.UUID) "epoch_id": epochID, }, nil) } + +// getWalletEpochs returns all confirmed epochs that the given wallet has reward in. +// If pending=true, return all finalized epochs(no necessary confirmed). +func getWalletEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, + wallet ethcommon.Address, pending bool, fn func(*Epoch) error) error { + + query := ` + {kwil_erc20_meta}SELECT e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.reward_amount, e.ended_at, e.block_hash, e.confirmed, array_agg(v.voter) as voters, array_agg(v.amount) as amounts, array_agg(v.nonce) as nonces, array_agg(v.signature) as signatures + FROM epoch_rewards AS r + JOIN epochs AS e ON r.epoch_id = e.id + LEFT JOIN epoch_votes AS v ON v.epoch_id = e.id + WHERE recipient = $wallet AND e.instance_id = $instance_id AND e.ended_at IS NOT NULL` // at least finalized + + // TODO: use this after SQL issue is fixed + //query := ` + //{kwil_erc20_meta}SELECT e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.ended_at, e.block_hash, e.confirmed, ARRAY[]::bytea[], ARRAY[]::int8[] + //FROM epoch_rewards AS r + //JOIN epochs AS e ON r.epoch_id = e.id + //WHERE recipient = $wallet AND e.instance_id = $instance_id AND e.ended_at IS NOT NULL` // at least finalized + if !pending { + query += ` AND e.confirmed IS true` + } + + // TODO: remove + query += ` GROUP BY e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.reward_amount, e.ended_at, e.block_hash, e.confirmed` + + query += ";" + return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, query, + map[string]any{ + "wallet": wallet.Bytes(), + "instance_id": instanceID, + }, func(r *common.Row) error { + epoch, err := rowToEpoch(r) + if err != nil { + return err + } + return fn(epoch) + }) +} diff --git a/node/exts/erc20reward/meta_sql_test.go b/node/exts/erc20reward/meta_sql_test.go index c247f511d..c31deb8b9 100644 --- a/node/exts/erc20reward/meta_sql_test.go +++ b/node/exts/erc20reward/meta_sql_test.go @@ -2,11 +2,9 @@ package erc20reward import ( "context" - "testing" - "time" - ethcommon "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" + "testing" "github.com/kwilteam/kwil-db/common" "github.com/kwilteam/kwil-db/core/log" @@ -98,7 +96,7 @@ func TestCreateNewRewardInstance(t *testing.T) { pending := &PendingEpoch{ ID: newUUID(), StartHeight: 10, - StartTime: time.Unix(100, 0), + StartTime: 100, } err = createEpoch(ctx, app, pending, id) require.NoError(t, err) diff --git a/node/exts/erc20reward/named_extension.go b/node/exts/erc20reward/named_extension.go index 21ebf7a87..d9431d97f 100644 --- a/node/exts/erc20reward/named_extension.go +++ b/node/exts/erc20reward/named_extension.go @@ -3,8 +3,6 @@ package erc20reward import ( "context" "errors" - "fmt" - "github.com/kwilteam/kwil-db/common" "github.com/kwilteam/kwil-db/core/types" "github.com/kwilteam/kwil-db/extensions/precompiles" @@ -57,7 +55,6 @@ func init() { makeMetaHandler := func(method string) precompiles.HandlerFunc { return func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { _, err2 := app.Engine.Call(ctx, app.DB, RewardMetaExtensionName, method, append([]any{&id}, inputs...), func(r *common.Row) error { - fmt.Printf("meta handler result: %+v\n", r.Values) return resultFn(r.Values) }) return err2 @@ -172,31 +169,93 @@ func init() { AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, Handler: makeMetaHandler("balance"), }, + { + Name: "decimals", + Returns: &precompiles.MethodReturn{ + Fields: []precompiles.PrecompileValue{ + {Name: "decimals", Type: types.IntType}, + }, + }, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, + Handler: makeMetaHandler("decimals"), + }, + { + Name: "scale_down", + Parameters: []precompiles.PrecompileValue{ + {Name: "amount", Type: types.TextType}, + }, + Returns: &precompiles.MethodReturn{ + Fields: []precompiles.PrecompileValue{ + {Name: "scaled", Type: types.TextType}, + }, + }, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, + Handler: makeMetaHandler("scale_down"), + }, + { + Name: "scale_up", + Parameters: []precompiles.PrecompileValue{ + {Name: "amount", Type: types.TextType}, + }, + Returns: &precompiles.MethodReturn{ + Fields: []precompiles.PrecompileValue{ + {Name: "scaled", Type: uint256Numeric}, + }, + }, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, + Handler: makeMetaHandler("scale_up"), + }, + { + Name: "get_active_epochs", + Parameters: []precompiles.PrecompileValue{}, + Returns: &precompiles.MethodReturn{ + IsTable: true, + Fields: []precompiles.PrecompileValue{ + {Name: "id", Type: types.UUIDType}, + {Name: "start_height", Type: types.IntType}, + {Name: "start_timestamp", Type: types.IntType}, + {Name: "end_height", Type: types.IntType, Nullable: true}, + {Name: "reward_root", Type: types.ByteaType, Nullable: true}, + {Name: "reward_amount", Type: types.TextType, Nullable: true}, + {Name: "end_block_hash", Type: types.ByteaType, Nullable: true}, + {Name: "confirmed", Type: types.BoolType}, + {Name: "voters", Type: types.TextArrayType, Nullable: true}, + {Name: "vote_amounts", Type: types.TextArrayType, Nullable: true}, + {Name: "vote_nonces", Type: types.IntArrayType, Nullable: true}, + {Name: "voter_signatures", Type: types.ByteaArrayType, Nullable: true}, + }, + }, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, + Handler: makeMetaHandler("get_active_epochs"), + }, { Name: "list_epochs", Parameters: []precompiles.PrecompileValue{ {Name: "after", Type: types.IntType}, {Name: "limit", Type: types.IntType}, - {Name: "confirmed_only", Type: types.BoolType}, + {Name: "finalized_only", Type: types.BoolType}, }, Returns: &precompiles.MethodReturn{ IsTable: true, Fields: []precompiles.PrecompileValue{ - {Name: "epoch_id", Type: types.UUIDType}, + {Name: "id", Type: types.UUIDType}, {Name: "start_height", Type: types.IntType}, {Name: "start_timestamp", Type: types.IntType}, {Name: "end_height", Type: types.IntType, Nullable: true}, {Name: "reward_root", Type: types.ByteaType, Nullable: true}, + {Name: "reward_amount", Type: types.TextType, Nullable: true}, {Name: "end_block_hash", Type: types.ByteaType, Nullable: true}, + {Name: "confirmed", Type: types.BoolType}, {Name: "voters", Type: types.TextArrayType, Nullable: true}, + {Name: "vote_amounts", Type: types.TextArrayType, Nullable: true}, {Name: "vote_nonces", Type: types.IntArrayType, Nullable: true}, + {Name: "voter_signatures", Type: types.ByteaArrayType, Nullable: true}, }, }, AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, Handler: makeMetaHandler("list_epochs"), }, { - // Supposed to be called by the SignerService, to verify the reward root. Name: "get_epoch_rewards", Parameters: []precompiles.PrecompileValue{ {Name: "epoch_id", Type: types.UUIDType}, @@ -212,40 +271,35 @@ func init() { Handler: makeMetaHandler("get_epoch_rewards"), }, { - Name: "decimals", - Returns: &precompiles.MethodReturn{ - Fields: []precompiles.PrecompileValue{ - {Name: "decimals", Type: types.IntType}, - }, - }, - AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, - Handler: makeMetaHandler("decimals"), - }, - { - Name: "scale_down", + Name: "vote_epoch", Parameters: []precompiles.PrecompileValue{ - {Name: "amount", Type: types.TextType}, - }, - Returns: &precompiles.MethodReturn{ - Fields: []precompiles.PrecompileValue{ - {Name: "scaled", Type: types.TextType}, - }, + {Name: "epoch_id", Type: types.UUIDType}, + {Name: "amount", Type: uint256Numeric}, + {Name: "nonce", Type: types.IntType}, + {Name: "signature", Type: types.ByteaType}, }, - AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, - Handler: makeMetaHandler("scale_down"), + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC}, + Handler: makeMetaHandler("vote_epoch"), }, { - Name: "scale_up", + Name: "list_wallet_rewards", Parameters: []precompiles.PrecompileValue{ - {Name: "amount", Type: types.TextType}, + {Name: "wallet", Type: types.TextType}, // wallet address + {Name: "with_pending", Type: types.BoolType}, }, Returns: &precompiles.MethodReturn{ + IsTable: true, Fields: []precompiles.PrecompileValue{ - {Name: "scaled", Type: uint256Numeric}, + {Name: "chain", Type: types.TextType}, + {Name: "chain_id", Type: types.TextType}, + {Name: "contract", Type: types.TextType}, + {Name: "etherscan", Type: types.TextType}, + {Name: "created_at", Type: types.IntType}, + {Name: "params", Type: types.TextArrayType}, // recipient,amount,block_hash,root,proofs }, }, AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, - Handler: makeMetaHandler("scale_up"), + Handler: makeMetaHandler("list_wallet_rewards"), }, }, }, nil diff --git a/node/exts/erc20reward/reward/crypto.go b/node/exts/erc20reward/reward/crypto.go index f1bdd1ec1..8b84257b0 100644 --- a/node/exts/erc20reward/reward/crypto.go +++ b/node/exts/erc20reward/reward/crypto.go @@ -48,15 +48,15 @@ func GenPostRewardTxData(root []byte, amount *big.Int) ([]byte, error) { // GenGnosisSafeTx returns a safe tx, and the tx hash to be used to generate signature. // More info: https://docs.safe.global/sdk/protocol-kit/guides/signatures/transactions // Since Gnosis 1.3.0, ChainID is a part of the EIP-712 domain. -func GenGnosisSafeTx(to, safe string, value int64, data hexutil.Bytes, chainID math.HexOrDecimal256, - nonce big.Int) (*core.GnosisSafeTx, []byte, error) { +func GenGnosisSafeTx(to, safe string, value int64, data hexutil.Bytes, chainID int64, + nonce int64) (*core.GnosisSafeTx, []byte, error) { gnosisSafeTx := core.GnosisSafeTx{ To: ethCommon.NewMixedcaseAddress(ethCommon.HexToAddress(to)), Value: *math.NewDecimal256(value), Data: &data, Operation: 0, // Call - ChainId: &chainID, + ChainId: math.NewHexOrDecimal256(chainID), Safe: ethCommon.NewMixedcaseAddress(ethCommon.HexToAddress(safe)), // NOTE: we ignore all those parameters since we're generating off-chain @@ -68,7 +68,7 @@ func GenGnosisSafeTx(to, safe string, value int64, data hexutil.Bytes, chainID m //SafeTxGas: big.Int{}, //SafeTxGas: *big.NewInt(*safeTxGas), - Nonce: nonce, + Nonce: *big.NewInt(nonce), // not sure what's the purpose of this field InputExpHash: ethCommon.Hash{}, diff --git a/node/exts/erc20reward/reward/mtree.go b/node/exts/erc20reward/reward/mtree.go index 7cf4322e7..093198dae 100644 --- a/node/exts/erc20reward/reward/mtree.go +++ b/node/exts/erc20reward/reward/mtree.go @@ -15,9 +15,9 @@ var ( MerkleLeafEncoding = []string{smt.SOL_ADDRESS, smt.SOL_UINT256, smt.SOL_ADDRESS, smt.SOL_BYTES32} ) -func GenRewardMerkleTree(users []string, amounts []*big.Int, contractAddress string, kwilBlockHash [32]byte) (string, []byte, error) { +func GenRewardMerkleTree(users []string, amounts []*big.Int, contractAddress string, kwilBlockHash [32]byte) ([]byte, []byte, error) { if len(users) != len(amounts) { - return "", nil, fmt.Errorf("users and amounts length not equal") + return nil, nil, fmt.Errorf("users and amounts length not equal") } values := [][]interface{}{} @@ -33,20 +33,20 @@ func GenRewardMerkleTree(users []string, amounts []*big.Int, contractAddress str rewardTree, err := smt.Of(values, MerkleLeafEncoding) if err != nil { - return "", nil, fmt.Errorf("create reward tree error: %w", err) + return nil, nil, fmt.Errorf("create reward tree error: %w", err) } dump, err := rewardTree.TreeMarshal() if err != nil { - return "", nil, fmt.Errorf("reward tree marshal error: %w", err) + return nil, nil, fmt.Errorf("reward tree marshal error: %w", err) } - return string(dump), rewardTree.GetRoot(), nil + return dump, rewardTree.GetRoot(), nil } // GetMTreeProof returns the leaf proof along with the leaf hash, amount. -func GetMTreeProof(mtreeJson string, addr string) (root []byte, proof [][]byte, leafHash []byte, blockHash []byte, amount string, err error) { - t, err := smt.Load([]byte(mtreeJson)) +func GetMTreeProof(mtreeJson []byte, addr string) (root []byte, proof [][]byte, leafHash []byte, blockHash []byte, amount string, err error) { + t, err := smt.Load(mtreeJson) if err != nil { return nil, nil, nil, nil, "", fmt.Errorf("load mtree error: %w", err) } diff --git a/node/exts/erc20reward/reward/mtree_test.go b/node/exts/erc20reward/reward/mtree_test.go index 94a37f2d5..312a32321 100644 --- a/node/exts/erc20reward/reward/mtree_test.go +++ b/node/exts/erc20reward/reward/mtree_test.go @@ -43,7 +43,7 @@ func TestMerkleTree(t *testing.T) { Map([]string{"100", "200", "100"}, smt.SolNumber), contract, b32Hash) require.NoError(t, err) require.Equal(t, expectRoot, hex.EncodeToString(root)) - assert.JSONEq(t, treeLeafs3, mt) + assert.JSONEq(t, treeLeafs3, string(mt)) mtRoot, mtProof, mtLeaf, bh, amt, err := GetMTreeProof(mt, user2) require.NoError(t, err) require.Equal(t, expectRoot, hex.EncodeToString(mtRoot)) @@ -61,7 +61,7 @@ func TestMerkleTree(t *testing.T) { Map([]string{"100", "200", "100", "200"}, smt.SolNumber), contract, b32Hash) require.NoError(t, err) require.Equal(t, expectRoot, hex.EncodeToString(root)) - assert.JSONEq(t, treeLeafs4, mt) + assert.JSONEq(t, treeLeafs4, string(mt)) mtRoot, mtProof, mtLeaf, bh, amt, err := GetMTreeProof(mt, user2) require.NoError(t, err) require.Equal(t, expectRoot, hex.EncodeToString(mtRoot)) @@ -79,7 +79,7 @@ func TestMerkleTree(t *testing.T) { Map([]string{"100", "200", "100", "200", "100"}, smt.SolNumber), contract, b32Hash) require.NoError(t, err) require.Equal(t, expectRoot, hex.EncodeToString(root)) - assert.JSONEq(t, treeLeafs5, mt) + assert.JSONEq(t, treeLeafs5, string(mt)) mtRoot, mtProof, mtLeaf, bh, amt, err := GetMTreeProof(mt, user2) require.NoError(t, err) require.Equal(t, expectRoot, hex.EncodeToString(mtRoot)) diff --git a/node/exts/evm-sync/chains/chains.go b/node/exts/evm-sync/chains/chains.go index 7a81fa391..f07f3cec2 100644 --- a/node/exts/evm-sync/chains/chains.go +++ b/node/exts/evm-sync/chains/chains.go @@ -16,6 +16,8 @@ type ChainInfo struct { // RequiredConfirmations is the number of confirmations required before an event is considered final. // For example, Ethereum mainnet requires 12 confirmations. RequiredConfirmations int64 + // Etherscan address for this chain. + Etherscan string } func init() { @@ -24,11 +26,13 @@ func init() { Name: "ethereum", ID: "1", RequiredConfirmations: 12, + Etherscan: "https://etherscan.io/address/", }, ChainInfo{ Name: "sepolia", ID: "11155111", RequiredConfirmations: 12, + Etherscan: "https://sepolia.etherscan.io/address/", }, ) if err != nil { diff --git a/node/exts/evm-sync/listener.go b/node/exts/evm-sync/listener.go index 5a238bf16..c453d96ec 100644 --- a/node/exts/evm-sync/listener.go +++ b/node/exts/evm-sync/listener.go @@ -260,6 +260,7 @@ func (i *individualListener) listen(ctx context.Context, eventstore listeners.Ev func (i *individualListener) processEvents(ctx context.Context, from, to int64, eventStore listeners.EventStore, logger log.Logger) error { logs, err := i.getLogsFunc(ctx, i.client.client, uint64(from), uint64(to), logger) + /// THIS IS WHERE I LEFT if err != nil { return err } diff --git a/node/services/erc20signersvc/eth_test.go b/node/services/erc20signersvc/eth_test.go index 67813f226..d9c2e7f28 100644 --- a/node/services/erc20signersvc/eth_test.go +++ b/node/services/erc20signersvc/eth_test.go @@ -19,7 +19,7 @@ func TestSafe_metadata(t *testing.T) { blockNumber := new(big.Int).SetUint64(7660784) - s, err := NewSafe("11155111", *ethRpc, "0x56D510E4782cDed87F8B93D260282776adEd3f4B") + s, err := NewSafe(*ethRpc, "0x56D510E4782cDed87F8B93D260282776adEd3f4B") require.NoError(t, err) ctx := context.Background() @@ -27,7 +27,7 @@ func TestSafe_metadata(t *testing.T) { got, err := s.getSafeMetadata3(ctx, blockNumber) require.NoError(t, err) - got2, err := s.getSafeMetadata(ctx, blockNumber) + got2, err := s.getSafeMetadataSeq(ctx, blockNumber) require.NoError(t, err) require.EqualValues(t, got, got2) diff --git a/node/services/erc20signersvc/kwil.go b/node/services/erc20signersvc/kwil.go index cc495dbd8..d90a1ac54 100644 --- a/node/services/erc20signersvc/kwil.go +++ b/node/services/erc20signersvc/kwil.go @@ -9,9 +9,9 @@ import ( ) type RewardInstanceInfo struct { - ChainID string + Chain string Escrow string - EpochPeriod int64 + EpochPeriod string Erc20 string Decimals int64 Balance string @@ -20,19 +20,21 @@ type RewardInstanceInfo struct { Enabled bool } -// TODO: use the type from Ext? type Epoch struct { - ID types.UUID - StartHeight int64 - StartTime int64 - EndHeight int64 - RewardRoot []byte - BlockHash []byte - Voters []string - VoteNonce []int64 + ID types.UUID + StartHeight int64 + StartTimestamp int64 + EndHeight int64 + RewardRoot []byte + RewardAmount string + EndBlockHash []byte + Confirmed bool + Voters []string + VoteAmounts []string + VoteNonces []int64 + VoteSignatures [][]byte } -// TODO: use the type from Ext? type FinalizedReward struct { ID types.UUID Voters []string @@ -60,14 +62,15 @@ type erc20ExtAPI interface { GetTarget() string SetTarget(ns string) InstanceInfo(tx context.Context) (*RewardInstanceInfo, error) + GetActiveEpochs(ctx context.Context) ([]*Epoch, error) ListUnconfirmedEpochs(ctx context.Context, afterHeight int64, limit int) ([]*Epoch, error) GetEpochRewards(ctx context.Context, epochID types.UUID) ([]*EpochReward, error) - VoteEpoch(ctx context.Context, rewardRoot []byte, signature []byte) (string, error) + VoteEpoch(ctx context.Context, epochID types.UUID, amount *types.Decimal, safeNonce int64, signature []byte) (string, error) } type erc20rwExtApi struct { clt *client.Client - target string + namespace string instanceID string } @@ -75,24 +78,24 @@ var _ erc20ExtAPI = (*erc20rwExtApi)(nil) func newERC20RWExtAPI(clt *client.Client, ns string) *erc20rwExtApi { return &erc20rwExtApi{ - clt: clt, - target: ns, + clt: clt, + namespace: ns, } } func (k *erc20rwExtApi) GetTarget() string { - return k.target + return k.namespace } func (k *erc20rwExtApi) SetTarget(ns string) { - k.target = ns + k.namespace = ns } func (k *erc20rwExtApi) InstanceInfo(ctx context.Context) (*RewardInstanceInfo, error) { procedure := "info" input := []any{} - res, err := k.clt.Call(ctx, k.target, procedure, input) + res, err := k.clt.Call(ctx, k.namespace, procedure, input) if err != nil { return nil, err } @@ -102,8 +105,8 @@ func (k *erc20rwExtApi) InstanceInfo(ctx context.Context) (*RewardInstanceInfo, } er := &RewardInstanceInfo{} - err = types.ScanTo(res.QueryResult.Values[1], - &er.ChainID, &er.Escrow, &er.EpochPeriod, &er.Erc20, &er.Decimals, &er.Balance, &er.Synced, &er.SyncedAt, &er.Enabled) + err = types.ScanTo(res.QueryResult.Values[0], + &er.Chain, &er.Escrow, &er.EpochPeriod, &er.Erc20, &er.Decimals, &er.Balance, &er.Synced, &er.SyncedAt, &er.Enabled) if err != nil { return nil, err } @@ -111,12 +114,39 @@ func (k *erc20rwExtApi) InstanceInfo(ctx context.Context) (*RewardInstanceInfo, return er, nil } +func (k *erc20rwExtApi) GetActiveEpochs(ctx context.Context) ([]*Epoch, error) { + procedure := "get_active_epochs" + input := []any{} + + res, err := k.clt.Call(ctx, k.namespace, procedure, input) + if err != nil { + return nil, err + } + + if len(res.QueryResult.Values) == 0 { + return nil, nil + } + + ers := make([]*Epoch, len(res.QueryResult.Values)) + for i, v := range res.QueryResult.Values { + er := &Epoch{} + err = types.ScanTo(v, &er.ID, &er.StartHeight, &er.StartTimestamp, &er.EndHeight, + &er.RewardRoot, &er.RewardAmount, &er.EndBlockHash, &er.Confirmed, &er.Voters, &er.VoteAmounts, &er.VoteNonces, &er.VoteSignatures) + if err != nil { + return nil, err + } + ers[i] = er + } + + return ers, nil +} + func (k *erc20rwExtApi) ListUnconfirmedEpochs(ctx context.Context, afterHeight int64, limit int) ([]*Epoch, error) { procedure := "list_epochs" - input := []any{afterHeight, limit, true} + input := []any{afterHeight, limit, false} - res, err := k.clt.Call(ctx, k.target, procedure, input) + res, err := k.clt.Call(ctx, k.namespace, procedure, input) if err != nil { return nil, err } @@ -128,8 +158,8 @@ func (k *erc20rwExtApi) ListUnconfirmedEpochs(ctx context.Context, afterHeight i ers := make([]*Epoch, len(res.QueryResult.Values)) for i, v := range res.QueryResult.Values { er := &Epoch{} - err = types.ScanTo(v, &er.ID, &er.StartHeight, &er.StartTime, &er.EndHeight, - &er.RewardRoot, &er.BlockHash) + err = types.ScanTo(v, &er.ID, &er.StartHeight, &er.StartTimestamp, &er.EndHeight, + &er.RewardRoot, &er.RewardAmount, &er.EndBlockHash, &er.Confirmed, &er.Voters, &er.VoteAmounts, &er.VoteNonces, &er.VoteSignatures) if err != nil { return nil, err } @@ -143,7 +173,7 @@ func (k *erc20rwExtApi) GetEpochRewards(ctx context.Context, epochID types.UUID) procedure := "get_epoch_rewards" input := []any{epochID} - res, err := k.clt.Call(ctx, k.target, procedure, input) + res, err := k.clt.Call(ctx, k.namespace, procedure, input) if err != nil { return nil, err } @@ -165,39 +195,11 @@ func (k *erc20rwExtApi) GetEpochRewards(ctx context.Context, epochID types.UUID) return ers, nil } -//func (k *erc20rwExtApi) FetchLatestRewards(ctx context.Context, limit int) ([]*FinalizedReward, error) { -// procedure := "latest_finalized" -// input := []any{limit} -// -// res, err := k.clt.Call(ctx, k.target, procedure, input) -// if err != nil { -// return nil, err -// } -// -// if len(res.QueryResult.Values) == 0 { -// return nil, nil -// } -// -// frs := make([]*FinalizedReward, len(res.QueryResult.Values)) -// for i, v := range res.QueryResult.Values { -// fr := &FinalizedReward{} -// err = types.ScanTo(v, &fr.ID, &fr.Voters, &fr.Signatures, &fr.EpochID, -// &fr.CreatedAt, &fr.StartHeight, &fr.EndHeight, &fr.TotalRewards, -// &fr.RewardRoot, &fr.SafeNonce, &fr.SignHash, &fr.ContractID, &fr.BlockHash) -// if err != nil { -// return nil, err -// } -// frs[i] = fr -// } -// -// return frs, nil -//} - -func (k *erc20rwExtApi) VoteEpoch(ctx context.Context, rewardRoot []byte, signature []byte) (string, error) { +func (k *erc20rwExtApi) VoteEpoch(ctx context.Context, epochID types.UUID, amount *types.Decimal, safeNonce int64, signature []byte) (string, error) { procedure := "vote_epoch" - input := [][]any{{rewardRoot, signature}} + input := [][]any{{epochID, amount, safeNonce, signature}} - res, err := k.clt.Execute(ctx, k.target, procedure, input, clientTypes.WithSyncBroadcast(true)) + res, err := k.clt.Execute(ctx, k.namespace, procedure, input, clientTypes.WithSyncBroadcast(true)) if err != nil { return "", err } diff --git a/node/services/erc20signersvc/multicall.go b/node/services/erc20signersvc/multicall.go index 6096eb0f1..6363a58e6 100644 --- a/node/services/erc20signersvc/multicall.go +++ b/node/services/erc20signersvc/multicall.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/kwilteam/kwil-db/node/services/erc20signersvc/abigen" ) diff --git a/node/services/erc20signersvc/signer.go b/node/services/erc20signersvc/signer.go index b575aaa50..307c0fc18 100644 --- a/node/services/erc20signersvc/signer.go +++ b/node/services/erc20signersvc/signer.go @@ -17,14 +17,16 @@ import ( ethAccounts "github.com/ethereum/go-ethereum/accounts" ethCommon "github.com/ethereum/go-ethereum/common" - ethMath "github.com/ethereum/go-ethereum/common/math" ethCrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/kwilteam/kwil-db/core/client" clientType "github.com/kwilteam/kwil-db/core/client/types" "github.com/kwilteam/kwil-db/core/crypto" "github.com/kwilteam/kwil-db/core/crypto/auth" "github.com/kwilteam/kwil-db/core/log" + "github.com/kwilteam/kwil-db/core/types" "github.com/kwilteam/kwil-db/node/exts/erc20reward/reward" + "github.com/kwilteam/kwil-db/node/exts/evm-sync/chains" ) // StateFilePath returns the state file. @@ -114,8 +116,13 @@ func (s *rewardSigner) init() error { return fmt.Errorf("create safe failed: %w", err) } - if s.safe.chainID.String() != info.ChainID { - return fmt.Errorf("chainID mismatch: %s != %s", s.safe.chainID.String(), info.ChainID) + chainInfo, ok := chains.GetChainInfo(chains.Chain(info.Chain)) + if !ok { + return fmt.Errorf("chainID %s not supported", s.safe.chainID.String()) + } + + if s.safe.chainID.String() != chainInfo.ID { + return fmt.Errorf("chainID mismatch: configured %s != target %s", s.safe.chainID.String(), chainInfo.ID) } s.escrowAddr = ethCommon.HexToAddress(info.Escrow) @@ -133,11 +140,8 @@ func (s *rewardSigner) init() error { // canSkip returns true if: // - signer is not one of the safe owners -// - signer has voted this epoch -// - is not finalized already voted from this signer; +// - signer has voted this epoch with the same nonce as current safe nonce func (s *rewardSigner) canSkip(epoch *Epoch, safeMeta *safeMetadata) bool { - // TODO: if no rewards in epoch, skip - if !slices.Contains(safeMeta.owners, s.signerAddr) { s.logger.Warn("signer is not safe owner", "signer", s.signerAddr.String(), "owners", safeMeta.owners) return true @@ -147,10 +151,9 @@ func (s *rewardSigner) canSkip(epoch *Epoch, safeMeta *safeMetadata) bool { return false } - // if has vote for i, voter := range epoch.Voters { if voter == s.signerAddr.String() && - safeMeta.nonce.Cmp(big.NewInt(epoch.VoteNonce[i])) <= 0 { + safeMeta.nonce.Cmp(big.NewInt(epoch.VoteNonces[i])) == 0 { return true } } @@ -159,7 +162,7 @@ func (s *rewardSigner) canSkip(epoch *Epoch, safeMeta *safeMetadata) bool { } // verify verifies if the reward root is correct, and return the total amount. -func (s *rewardSigner) verify(ctx context.Context, epoch *Epoch, safeAddr string) (*big.Int, error) { +func (s *rewardSigner) verify(ctx context.Context, epoch *Epoch, escrowAddr string) (*big.Int, error) { rewards, err := s.kwil.GetEpochRewards(ctx, epoch.ID) if err != nil { return nil, err @@ -182,9 +185,9 @@ func (s *rewardSigner) verify(ctx context.Context, epoch *Epoch, safeAddr string } var b32 [32]byte - copy(b32[:], epoch.BlockHash) + copy(b32[:], epoch.EndBlockHash) - _, root, err := reward.GenRewardMerkleTree(recipients, amounts, safeAddr, b32) + _, root, err := reward.GenRewardMerkleTree(recipients, amounts, escrowAddr, b32) if err != nil { return nil, err } @@ -197,6 +200,17 @@ func (s *rewardSigner) verify(ctx context.Context, epoch *Epoch, safeAddr string return total, nil } +// erc20ValueFromBigInt converts a big.Int to a decimal.Decimal(78,0) +// NOTE: this is copied from meta ext +func erc20ValueFromBigInt(b *big.Int) (*types.Decimal, error) { + dec, err := types.NewDecimalFromBigInt(b, 0) + if err != nil { + return nil, fmt.Errorf("failed to convert big.Int to decimal.Decimal: %w", err) + } + err = dec.SetPrecisionAndScale(78, 0) + return dec, err +} + // vote votes an epoch reward, and updates the state. // It will first fetch metadata from ETH, then generate the safeTx, then vote. func (s *rewardSigner) vote(ctx context.Context, epoch *Epoch, safeMeta *safeMetadata, total *big.Int) error { @@ -207,7 +221,7 @@ func (s *rewardSigner) vote(ctx context.Context, epoch *Epoch, safeMeta *safeMet // safeTxHash is the data that all signers will be signing(using personal_sign) _, safeTxHash, err := reward.GenGnosisSafeTx(s.escrowAddr.String(), s.safe.addr.String(), - 0, safeTxData, ethMath.HexOrDecimal256(*s.safe.chainID), *safeMeta.nonce) + 0, safeTxData, s.safe.chainID.Int64(), safeMeta.nonce.Int64()) if err != nil { return err } @@ -218,7 +232,12 @@ func (s *rewardSigner) vote(ctx context.Context, epoch *Epoch, safeMeta *safeMet return err } - h, err := s.kwil.VoteEpoch(ctx, epoch.RewardRoot, sig) + uint256Amount, err := erc20ValueFromBigInt(total) + if err != nil { + return err + } + + h, err := s.kwil.VoteEpoch(ctx, epoch.ID, uint256Amount, safeMeta.nonce.Int64(), sig) if err != nil { return err } @@ -228,7 +247,7 @@ func (s *rewardSigner) vote(ctx context.Context, epoch *Epoch, safeMeta *safeMet err = s.state.UpdateLastVote(s.target, &voteRecord{ RewardRoot: epoch.RewardRoot, BlockHeight: epoch.EndHeight, - BlockHash: hex.EncodeToString(epoch.BlockHash), + BlockHash: hex.EncodeToString(epoch.EndBlockHash), SafeNonce: safeMeta.nonce.Uint64(), }) if err != nil { @@ -236,75 +255,65 @@ func (s *rewardSigner) vote(ctx context.Context, epoch *Epoch, safeMeta *safeMet } s.logger.Info("vote epoch", "tx", h, "id", epoch.ID.String(), - "signHash", hex.EncodeToString(signHash)) + "nonce", safeMeta.nonce.Int64()) return nil } -// watch polls on newer epochs and try to vote/sign them. +// sync polls on newer epochs and try to vote/sign them. // Since there could be the case that the target(namespace/or id) not exist for whatever reason, // this function won't return Error, and also won't log at Error level. -func (s *rewardSigner) watch(ctx context.Context) { - s.logger.Info("start watching erc20 reward epoches") - - tick := time.NewTicker(s.every) +func (s *rewardSigner) sync(ctx context.Context) { + s.logger.Debug("polling epochs", "lastVoteBlock", s.lastVoteBlock) - for { - s.logger.Debug("polling epochs", "lastVoteBlock", s.lastVoteBlock) - // fetch next batch rewards to be voted, and vote them. - // NOTE: we use ListUnconfirmedEpochs (not FetchLatestRewards) so we don't accidently SKIP epoch. - epochs, err := s.kwil.ListUnconfirmedEpochs(ctx, s.lastVoteBlock, 10) - if err != nil { - s.logger.Warn("fetch epoch", "error", err.Error()) - continue - } + epochs, err := s.kwil.GetActiveEpochs(ctx) + if err != nil { + s.logger.Warn("fetch epoch", "error", err.Error()) + return + } - if len(epochs) == 0 { - s.logger.Debug("no epoch found") - continue - } + if len(epochs) == 0 { + s.logger.Error("no epoch found") + return + } - safeMeta, err := s.safe.latestMetadata(ctx) - if err != nil { - s.logger.Warn("fetch safe metadata", "error", err.Error()) - continue - } + if len(epochs) == 1 { + // the very first round of epoch, we wait until there are 2 active epochs + return + } - for _, epoch := range epochs { - voteRecord := s.state.LastVote(s.target) - if voteRecord != nil && voteRecord.SafeNonce == safeMeta.nonce.Uint64() { - continue - } + if len(epochs) != 2 { + s.logger.Error("unexpected number of epochs", "count", len(epochs)) + return + } - if s.canSkip(epoch, safeMeta) { - s.logger.Debug("skip epoch", "id", epoch.ID.String(), "height", epoch.EndHeight) - s.lastVoteBlock = epoch.EndHeight // update since we can skip it - continue - } + finalizedEpoch := epochs[0] - total, err := s.verify(ctx, epoch, s.safe.addr.String()) - if err != nil { - s.logger.Warn("verify epoch", "id", epoch.ID.String(), "height", epoch.EndHeight, "error", err.Error()) - break - } + safeMeta, err := s.safe.latestMetadata(ctx) + if err != nil { + s.logger.Warn("fetch safe metadata", "error", err.Error()) + return + } - err = s.vote(ctx, epoch, safeMeta, total) - if err != nil { - s.logger.Warn("vote epoch", "id", epoch.ID.String(), "height", epoch.EndHeight, "error", err.Error()) - break - } + if s.canSkip(finalizedEpoch, safeMeta) { + s.logger.Info("skip epoch", "id", finalizedEpoch.ID.String(), "height", finalizedEpoch.EndHeight) + s.lastVoteBlock = finalizedEpoch.EndHeight // update since we can skip it + return + } - s.lastVoteBlock = epoch.EndHeight // update after all operations succeed - } + total, err := s.verify(ctx, finalizedEpoch, s.escrowAddr.String()) + if err != nil { + s.logger.Warn("verify epoch", "id", finalizedEpoch.ID.String(), "height", finalizedEpoch.EndHeight, "error", err.Error()) + return + } - select { - case <-ctx.Done(): - s.logger.Info("stop watching erc20 reward epoches") - return - case <-tick.C: - continue - } + err = s.vote(ctx, finalizedEpoch, safeMeta, total) + if err != nil { + s.logger.Warn("vote epoch", "id", finalizedEpoch.ID.String(), "height", finalizedEpoch.EndHeight, "error", err.Error()) + return } + + s.lastVoteBlock = finalizedEpoch.EndHeight // update after all operations succeed } // ServiceMgr manages multiple rewardSigner instances running in parallel. @@ -357,7 +366,20 @@ func (s *ServiceMgr) Start(ctx context.Context) error { wg.Add(1) go func() { defer wg.Done() - s.watch(ctx) + + s.logger.Info("start watching erc20 reward epoches") + tick := time.NewTicker(s.every) + + for { + s.sync(ctx) + + select { + case <-ctx.Done(): + s.logger.Info("stop watching erc20 reward epoches") + return + case <-tick.C: + } + } }() } From c94ea661c900901ae622c4e3109cc7c1ca3095f5 Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Tue, 18 Feb 2025 16:13:04 -0600 Subject: [PATCH 07/30] fix erc20 signer config --- config/config.go | 8 ++++---- go.mod | 5 +---- go.sum | 2 -- node/services/erc20signersvc/signer.go | 25 ++++++++++++++++++++----- 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/config/config.go b/config/config.go index c7155383a..3984855f1 100644 --- a/config/config.go +++ b/config/config.go @@ -453,10 +453,10 @@ type Checkpoint struct { type Erc20RewardSignerConfig struct { Enable bool `toml:"enable" comment:"enable the ERC20 reward signer service"` - Targets []string `json:"targets" comment:"target reward ext alias for the ERC20 reward"` - PrivateKeys []string `json:"private_keys" comment:"private key for the ERC20 reward target"` - EthRpcs []string `json:"eth_rpcs" comment:"eth rpc address for the ERC20 reward target"` - SyncEvery types.Duration `json:"sync_every" comment:"sync interval; a recommend value is same as the block time"` + Targets []string `toml:"targets" comment:"target reward ext alias for the ERC20 reward"` + PrivateKeys []string `toml:"private_keys" comment:"private key for the ERC20 reward target"` + EthRpcs []string `toml:"eth_rpcs" comment:"eth rpc address for the ERC20 reward target"` + SyncEvery types.Duration `toml:"sync_every" comment:"sync interval; a recommend value is same as the block time"` } func (cfg Erc20RewardSignerConfig) Validate() error { diff --git a/go.mod b/go.mod index e3b044843..bd61c5c33 100644 --- a/go.mod +++ b/go.mod @@ -218,7 +218,4 @@ require ( go.opentelemetry.io/auto/sdk v1.1.0 // indirect ) -require ( - github.com/joho/godotenv v1.5.1 - github.com/samber/lo v1.47.0 -) +require github.com/samber/lo v1.47.0 diff --git a/go.sum b/go.sum index c68c02547..42d7c2728 100644 --- a/go.sum +++ b/go.sum @@ -294,8 +294,6 @@ github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPw github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jrick/logrotate v1.1.2 h1:6ePk462NCX7TfKtNp5JJ7MbA2YIslkpfgP03TlTYMN0= diff --git a/node/services/erc20signersvc/signer.go b/node/services/erc20signersvc/signer.go index 307c0fc18..e3c5af782 100644 --- a/node/services/erc20signersvc/signer.go +++ b/node/services/erc20signersvc/signer.go @@ -31,7 +31,7 @@ import ( // StateFilePath returns the state file. func StateFilePath(dir string) string { - return filepath.Join(dir, "erc20reward_signer_state.json") + return filepath.Join(dir, "erc20_signer_state.json") } // rewardSigner handles one registered erc20 reward instance. @@ -353,11 +353,26 @@ func NewServiceMgr( // no errors are returned after the rewardSigner is running. func (s *ServiceMgr) Start(ctx context.Context) error { // since we need to wait on RPC running, we move the initialization logic into `init` - for _, s := range s.signers { - err := s.init() - if err != nil { - return err + + // To be able to run with docker, we need to apply a retry logic, since a new + // docker instance has no erc20 instance configured, but we need to config the + // erc20 instance target. + for { // naive way to keep trying the init + var err error + for _, s := range s.signers { + err = s.init() + if err != nil { + break + } } + + if err == nil { + break + } + + // if any error happens in init, we try again + time.Sleep(time.Second * 5) + s.logger.Warn("failed to initialize erc20 reward signer, will retry", "error", err.Error()) } wg := &sync.WaitGroup{} From 5115e349a469ae5a2d387d16ab6ad4a4ea3a659b Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Tue, 18 Feb 2025 17:28:43 -0600 Subject: [PATCH 08/30] fix extension flags parsing --- app/node/start.go | 20 +++++++++++++++----- app/node/start_test.go | 26 ++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/app/node/start.go b/app/node/start.go index 60ad6d1f0..f365ad6f3 100644 --- a/app/node/start.go +++ b/app/node/start.go @@ -104,13 +104,12 @@ func StartCmd() *cobra.Command { func parseExtensionFlags(args []string) (map[string]map[string]string, error) { exts := make(map[string]map[string]string) for i := 0; i < len(args); i++ { - if !strings.HasPrefix(args[i], "--extension.") { + if !strings.HasPrefix(args[i], "--extensions.") { return nil, fmt.Errorf("expected extension flag, got %q", args[i]) } - // split the flag into the extension name and the flag name - // we intentionally do not use SplitN because we want to verify - // there are exactly 3 parts. - parts := strings.Split(args[i], ".") + // split the flag into the extension name and the flag name; + // last part can have values like URL, will verify only 3 segements in the flag below + parts := strings.SplitN(args[i], ".", 3) if len(parts) != 3 { return nil, fmt.Errorf("invalid extension flag %q", args[i]) } @@ -131,8 +130,19 @@ func parseExtensionFlags(args []string) (map[string]map[string]string, error) { if strings.Contains(parts[2], "=") { // flag value is in the same argument val := strings.SplitN(parts[2], "=", 2) + + // flag can only have 3 segements + if strings.Contains(val[0], ".") { + return nil, fmt.Errorf("invalid extension flag %q", args[i]) + } + ext[val[0]] = val[1] } else { + // flag can only have 3 segements + if strings.Contains(parts[2], ".") { + return nil, fmt.Errorf("invalid extension flag %q", args[i]) + } + // flag value is in the next argument if i+1 >= len(args) { return nil, fmt.Errorf("missing value for extension flag %q", args[i]) diff --git a/app/node/start_test.go b/app/node/start_test.go index e4f2b5b06..b1ac80ebf 100644 --- a/app/node/start_test.go +++ b/app/node/start_test.go @@ -22,7 +22,7 @@ func Test_ExtensionFlags(t *testing.T) { }, { name: "single flag", - flagset: []string{"--extension.extname.flagname", "value"}, + flagset: []string{"--extensions.extname.flagname", "value"}, want: map[string]map[string]string{ "extname": { "flagname": "value", @@ -31,7 +31,7 @@ func Test_ExtensionFlags(t *testing.T) { }, { name: "multiple flags", - flagset: []string{"--extension.extname.flagname", "value", "--extension.extname2.flagname2=value2"}, + flagset: []string{"--extensions.extname.flagname", "value", "--extensions.extname2.flagname2=value2"}, want: map[string]map[string]string{ "extname": { "flagname": "value", @@ -41,17 +41,34 @@ func Test_ExtensionFlags(t *testing.T) { }, }, }, + { + name: "multiple flags with dot values", + flagset: []string{"--extensions.extname.flagname", "value.a.b", "--extensions.extname2.flagname2=value2.a.b"}, + want: map[string]map[string]string{ + "extname": { + "flagname": "value.a.b", + }, + "extname2": { + "flagname2": "value2.a.b", + }, + }, + }, + { + name: "more than 3 fields", + flagset: []string{"--extensions.extname.flagname.another", "value", "--extensions.extname.flagname.another=value"}, + wantErr: true, + }, { name: "missing value", flagset: []string{ - "--extension.extname.flagname", + "--extensions.extname.flagname", }, wantErr: true, }, { name: "pass flag as a value errors", flagset: []string{ - "--extension.extname.flagname", "--extension.extname2.flagname2=value2", + "--extensions.extname.flagname", "--extensions.extname2.flagname2=value2", }, wantErr: true, }, @@ -62,6 +79,7 @@ func Test_ExtensionFlags(t *testing.T) { got, err := parseExtensionFlags(tt.flagset) if tt.wantErr { require.Error(t, err) + t.Log(err) return } require.NoError(t, err) From b1a40c6fe738c652174c0987403f149da020e1d3 Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Tue, 18 Feb 2025 21:37:08 -0600 Subject: [PATCH 09/30] udpate api --- node/exts/erc20reward/meta_extension.go | 92 +++++++++++----------- node/exts/erc20reward/meta_sql.go | 10 +-- node/exts/erc20reward/named_extension.go | 19 ++--- node/exts/erc20reward/reward/crypto.go | 22 ++---- node/exts/erc20reward/reward/mtree.go | 15 ++-- node/exts/erc20reward/reward/mtree_test.go | 3 +- node/exts/evm-sync/chains/chains.go | 4 - 7 files changed, 77 insertions(+), 88 deletions(-) diff --git a/node/exts/erc20reward/meta_extension.go b/node/exts/erc20reward/meta_extension.go index 78f2983c1..eb5471aa1 100644 --- a/node/exts/erc20reward/meta_extension.go +++ b/node/exts/erc20reward/meta_extension.go @@ -24,9 +24,6 @@ import ( "time" "github.com/decred/dcrd/container/lru" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/samber/lo" - "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" ethcommon "github.com/ethereum/go-ethereum/common" @@ -518,7 +515,7 @@ func init() { {Name: "epoch_period", Type: types.TextType}, {Name: "erc20", Type: types.TextType, Nullable: true}, {Name: "decimals", Type: types.IntType, Nullable: true}, - {Name: "balance", Type: types.TextType, Nullable: true}, // total unspent balance + {Name: "balance", Type: uint256Numeric, Nullable: true}, // total unspent balance {Name: "synced", Type: types.BoolType}, {Name: "synced_at", Type: types.IntType, Nullable: true}, {Name: "enabled", Type: types.BoolType}, @@ -537,7 +534,8 @@ func init() { defer info.mu.RUnlock() // these values can be null if the extension is not synced - var erc20Address, ownedBalance *string + var erc20Address *string + var ownedBalance *types.Decimal var decimals, syncedAt *int64 dur := time.Duration(info.userProvidedData.DistributionPeriod) * time.Second @@ -546,8 +544,8 @@ func init() { erc20Addr := info.syncedRewardData.Erc20Address.Hex() erc20Address = &erc20Addr decimals = &info.syncedRewardData.Erc20Decimals - owbalStr := info.ownedBalance.String() - ownedBalance = &owbalStr + //owbalStr := info.ownedBalance.String() + ownedBalance = info.ownedBalance syncedAt = &info.syncedAt } @@ -928,7 +926,7 @@ func init() { }, }, { - // lists only active epochs: one collects reward; one waits to be confirmed + // get only active epochs: finalized epoch and collecting epoch Name: "get_active_epochs", Parameters: []precompiles.PrecompileValue{ {Name: "id", Type: types.UUIDType}, @@ -941,11 +939,11 @@ func init() { {Name: "start_timestamp", Type: types.IntType}, {Name: "end_height", Type: types.IntType, Nullable: true}, {Name: "reward_root", Type: types.ByteaType, Nullable: true}, - {Name: "reward_amount", Type: types.TextType, Nullable: true}, + {Name: "reward_amount", Type: uint256Numeric, Nullable: true}, {Name: "end_block_hash", Type: types.ByteaType, Nullable: true}, {Name: "confirmed", Type: types.BoolType}, {Name: "voters", Type: types.TextArrayType, Nullable: true}, - {Name: "vote_amounts", Type: types.TextArrayType, Nullable: true}, + {Name: "vote_amounts", Type: types.TextArrayType, Nullable: true}, // cannot successful return uint256NumericArray, as empty value {Name: "vote_nonces", Type: types.IntArrayType, Nullable: true}, {Name: "voter_signatures", Type: types.ByteaArrayType, Nullable: true}, }, @@ -962,16 +960,16 @@ func init() { } } - var voteAmts []string - if len(e.VoteAmounts) > 0 { - for _, item := range e.VoteAmounts { - voteAmts = append(voteAmts, item.String()) - } + total := e.Total + if total == nil { + total, _ = erc20ValueFromBigInt(big.NewInt(0)) } - total := "0" - if e.Total != nil { - total = e.Total.String() + var voteAmts []string + for _, amt := range e.VoteAmounts { + if amt != nil { + voteAmts = append(voteAmts, amt.String()) + } } return resultFn([]any{e.ID, e.StartHeight, e.StartTime, *e.EndHeight, e.Root, total, e.BlockHash, e.Confirmed, @@ -984,14 +982,11 @@ func init() { }}, { // lists epochs after(non-include) given height, in ASC order. - // If finalized_only is true, only returns finalized yet not confirmed epochs. - // NOTE: only un-confirmed epoch will return voters and vote_nonces Name: "list_epochs", Parameters: []precompiles.PrecompileValue{ {Name: "id", Type: types.UUIDType}, {Name: "after", Type: types.IntType}, {Name: "limit", Type: types.IntType}, - {Name: "finalized_only", Type: types.BoolType}, }, Returns: &precompiles.MethodReturn{ IsTable: true, @@ -1001,11 +996,11 @@ func init() { {Name: "start_timestamp", Type: types.IntType}, {Name: "end_height", Type: types.IntType, Nullable: true}, {Name: "reward_root", Type: types.ByteaType, Nullable: true}, - {Name: "reward_amount", Type: types.TextType, Nullable: true}, + {Name: "reward_amount", Type: uint256Numeric, Nullable: true}, {Name: "end_block_hash", Type: types.ByteaType, Nullable: true}, {Name: "confirmed", Type: types.BoolType}, {Name: "voters", Type: types.TextArrayType, Nullable: true}, - {Name: "vote_amounts", Type: types.TextArrayType, Nullable: true}, + {Name: "vote_amounts", Type: types.TextArrayType, Nullable: true}, // cannot successful return uint256NumericArray, as empty value {Name: "vote_nonces", Type: types.IntArrayType, Nullable: true}, {Name: "voter_signatures", Type: types.ByteaArrayType, Nullable: true}, }, @@ -1015,9 +1010,8 @@ func init() { id := inputs[0].(*types.UUID) after := inputs[1].(int64) limit := inputs[2].(int64) - finalizedOnly := inputs[3].(bool) - return getEpochs(ctx.TxContext.Ctx, app, id, after, limit, finalizedOnly, func(e *Epoch) error { + return getEpochs(ctx.TxContext.Ctx, app, id, after, limit, func(e *Epoch) error { var voters []string if len(e.Voters) > 0 { for _, item := range e.Voters { @@ -1025,16 +1019,19 @@ func init() { } } - var voteAmts []string - if len(e.VoteAmounts) > 0 { - for _, item := range e.VoteAmounts { - voteAmts = append(voteAmts, item.String()) - } + total := e.Total + if total == nil { + total, _ = erc20ValueFromBigInt(big.NewInt(0)) } - total := "0" - if e.Total != nil { - total = e.Total.String() + // uint256NumericArray = types.ArrayType(uint256Numeric) + // NOTE: how to return a nil uint256NumericArray, I cannot set precision/scale on nil type + + var voteAmts []string + for _, amt := range e.VoteAmounts { + if amt != nil { + voteAmts = append(voteAmts, amt.String()) + } } return resultFn([]any{e.ID, e.StartHeight, e.StartTime, *e.EndHeight, e.Root, total, e.BlockHash, e.Confirmed, @@ -1126,9 +1123,12 @@ func init() { {Name: "chain", Type: types.TextType}, {Name: "chain_id", Type: types.TextType}, {Name: "contract", Type: types.TextType}, - {Name: "etherscan", Type: types.TextType}, {Name: "created_at", Type: types.IntType}, - {Name: "params", Type: types.TextArrayType}, // recipient,amount,block_hash,root,proofs + {Name: "param_recipient", Type: types.TextType}, + {Name: "param_amount", Type: uint256Numeric}, + {Name: "param_block_hash", Type: types.ByteaType}, + {Name: "param_root", Type: types.ByteaType}, + {Name: "param_proofs", Type: types.ByteaArrayType}, }, }, AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, @@ -1181,7 +1181,12 @@ func init() { mtLRUCache.Put(b32Root, jsonTree) } - _, proofs, _, bh, uint256AmtStr, err := reward.GetMTreeProof(jsonTree, walletAddr.String()) + _, proofs, _, bh, amtBig, err := reward.GetMTreeProof(jsonTree, walletAddr.String()) + if err != nil { + return err + } + + uint256Amt, err := erc20ValueFromBigInt(amtBig) if err != nil { return err } @@ -1189,17 +1194,12 @@ func init() { err = resultFn([]any{info.ChainInfo.Name.String(), info.ChainInfo.ID, info.EscrowAddress.String(), - info.ChainInfo.Etherscan + info.EscrowAddress.String() + "#writeContract", epoch.EndHeight, - []string{ - walletAddr.String(), - uint256AmtStr, - hexutil.Encode(bh), - hexutil.Encode(epoch.Root), - strings.Join(lo.Map(proofs, func(item []byte, _ int) string { - return hexutil.Encode(item) - }), ","), // comma separated byte32str - }, + walletAddr.String(), + uint256Amt, + bh, + epoch.Root, + proofs, }) if err != nil { return err diff --git a/node/exts/erc20reward/meta_sql.go b/node/exts/erc20reward/meta_sql.go index 6cc39ef14..f6daa3b8d 100644 --- a/node/exts/erc20reward/meta_sql.go +++ b/node/exts/erc20reward/meta_sql.go @@ -514,17 +514,13 @@ func getActiveEpochs(ctx context.Context, app *common.App, instanceID *types.UUI }) } -// getEpochs gets epochs by given conditions. -func getEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, after int64, limit int64, finalizedOnly bool, fn func(*Epoch) error) error { +// getEpochs gets epochs. +func getEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, after int64, limit int64, fn func(*Epoch) error) error { query := ` {kwil_erc20_meta}SELECT e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.reward_amount, e.ended_at, e.block_hash, e.confirmed, array_agg(v.voter) as voters, array_agg(v.amount) as amounts, array_agg(v.nonce) as nonces, array_agg(v.signature) as signatures FROM epochs AS e LEFT JOIN epoch_votes AS v ON v.epoch_id = e.id - WHERE e.instance_id = $instance_id AND e.created_at_block > $after` - if finalizedOnly { - query += ` AND e.ended_at IS NOT NULL AND e.confirmed IS NOT true` // finalized but not confirmed - } - query += ` + WHERE e.instance_id = $instance_id AND e.created_at_block > $after GROUP BY e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.reward_amount, e.ended_at, e.block_hash, e.confirmed ORDER BY e.ended_at ASC LIMIT $limit` diff --git a/node/exts/erc20reward/named_extension.go b/node/exts/erc20reward/named_extension.go index d9431d97f..fc4cec031 100644 --- a/node/exts/erc20reward/named_extension.go +++ b/node/exts/erc20reward/named_extension.go @@ -88,7 +88,7 @@ func init() { {Name: "epoch_period", Type: types.TextType}, {Name: "erc20", Type: types.TextType, Nullable: true}, {Name: "decimals", Type: types.IntType, Nullable: true}, - {Name: "balance", Type: types.TextType, Nullable: true}, // total unspent balance + {Name: "balance", Type: uint256Numeric, Nullable: true}, // total unspent balance {Name: "synced", Type: types.BoolType}, {Name: "synced_at", Type: types.IntType, Nullable: true}, {Name: "enabled", Type: types.BoolType}, @@ -216,11 +216,11 @@ func init() { {Name: "start_timestamp", Type: types.IntType}, {Name: "end_height", Type: types.IntType, Nullable: true}, {Name: "reward_root", Type: types.ByteaType, Nullable: true}, - {Name: "reward_amount", Type: types.TextType, Nullable: true}, + {Name: "reward_amount", Type: uint256Numeric, Nullable: true}, {Name: "end_block_hash", Type: types.ByteaType, Nullable: true}, {Name: "confirmed", Type: types.BoolType}, {Name: "voters", Type: types.TextArrayType, Nullable: true}, - {Name: "vote_amounts", Type: types.TextArrayType, Nullable: true}, + {Name: "vote_amounts", Type: types.TextArrayType, Nullable: true}, // cannot successful return uint256NumericArray, as empty value {Name: "vote_nonces", Type: types.IntArrayType, Nullable: true}, {Name: "voter_signatures", Type: types.ByteaArrayType, Nullable: true}, }, @@ -233,7 +233,6 @@ func init() { Parameters: []precompiles.PrecompileValue{ {Name: "after", Type: types.IntType}, {Name: "limit", Type: types.IntType}, - {Name: "finalized_only", Type: types.BoolType}, }, Returns: &precompiles.MethodReturn{ IsTable: true, @@ -243,11 +242,11 @@ func init() { {Name: "start_timestamp", Type: types.IntType}, {Name: "end_height", Type: types.IntType, Nullable: true}, {Name: "reward_root", Type: types.ByteaType, Nullable: true}, - {Name: "reward_amount", Type: types.TextType, Nullable: true}, + {Name: "reward_amount", Type: uint256Numeric, Nullable: true}, {Name: "end_block_hash", Type: types.ByteaType, Nullable: true}, {Name: "confirmed", Type: types.BoolType}, {Name: "voters", Type: types.TextArrayType, Nullable: true}, - {Name: "vote_amounts", Type: types.TextArrayType, Nullable: true}, + {Name: "vote_amounts", Type: types.TextArrayType, Nullable: true}, // cannot successful return uint256NumericArray, as empty value {Name: "vote_nonces", Type: types.IntArrayType, Nullable: true}, {Name: "voter_signatures", Type: types.ByteaArrayType, Nullable: true}, }, @@ -293,10 +292,12 @@ func init() { {Name: "chain", Type: types.TextType}, {Name: "chain_id", Type: types.TextType}, {Name: "contract", Type: types.TextType}, - {Name: "etherscan", Type: types.TextType}, {Name: "created_at", Type: types.IntType}, - {Name: "params", Type: types.TextArrayType}, // recipient,amount,block_hash,root,proofs - }, + {Name: "param_recipient", Type: types.TextType}, + {Name: "param_amount", Type: uint256Numeric}, + {Name: "param_block_hash", Type: types.ByteaType}, + {Name: "param_root", Type: types.ByteaType}, + {Name: "param_proofs", Type: types.ByteaArrayType}}, }, AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, Handler: makeMetaHandler("list_wallet_rewards"), diff --git a/node/exts/erc20reward/reward/crypto.go b/node/exts/erc20reward/reward/crypto.go index 8b84257b0..c4f90b3f4 100644 --- a/node/exts/erc20reward/reward/crypto.go +++ b/node/exts/erc20reward/reward/crypto.go @@ -6,38 +6,32 @@ import ( "fmt" "math/big" "slices" - "strings" ethAccounts "github.com/ethereum/go-ethereum/accounts" - ethAbi "github.com/ethereum/go-ethereum/accounts/abi" ethCommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/math" ethCrypto "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/signer/core" "github.com/ethereum/go-ethereum/signer/core/apitypes" -) -// ContractABI defines the ABI for the reward contract. -const ContractABI = `[{"name":"postReward","type":"function","inputs":[{"name":"rewardRoot","type":"bytes32"},{"name":"rewardAmount","type":"uint256"}],"outputs":[]}, -{"name":"updatePosterFee","type":"function","inputs":[{"name":"newFee","type":"uint256"}],"outputs":[]}, -{"name":"rewardPoster","type":"function","inputs":[{"name":"root","type":"bytes32"}],"outputs":[{"name":"","type":"address"}]}]` + "github.com/kwilteam/kwil-db/node/exts/erc20reward/abigen" +) const GnosisSafeSigLength = ethCrypto.SignatureLength func GenPostRewardTxData(root []byte, amount *big.Int) ([]byte, error) { - // Parse the ABI - parsedABI, err := ethAbi.JSON(strings.NewReader(ContractABI)) - if err != nil { - return nil, fmt.Errorf("failed to parse ABI: %v", err) - } - // Convert the root to a common.Hash type (because it's bytes32 in Ethereum) //rootHash := ethCommon.HexToHash(root) rootHash := ethCommon.BytesToHash(root) + rdABI, err := abigen.RewardDistributorMetaData.GetAbi() + if err != nil { + return nil, fmt.Errorf("failed to get ABI: %v", err) + } + // Encode the "postReward" function call with the given parameters - data, err := parsedABI.Pack("postReward", rootHash, amount) + data, err := rdABI.Pack("postReward", rootHash, amount) if err != nil { return nil, fmt.Errorf("failed to encode function call: %v", err) } diff --git a/node/exts/erc20reward/reward/mtree.go b/node/exts/erc20reward/reward/mtree.go index 093198dae..2f601a7af 100644 --- a/node/exts/erc20reward/reward/mtree.go +++ b/node/exts/erc20reward/reward/mtree.go @@ -5,6 +5,7 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" + smt "github.com/kwilteam/openzeppelin-merkle-tree-go/standard_merkle_tree" ) @@ -45,10 +46,10 @@ func GenRewardMerkleTree(users []string, amounts []*big.Int, contractAddress str } // GetMTreeProof returns the leaf proof along with the leaf hash, amount. -func GetMTreeProof(mtreeJson []byte, addr string) (root []byte, proof [][]byte, leafHash []byte, blockHash []byte, amount string, err error) { +func GetMTreeProof(mtreeJson []byte, addr string) (root []byte, proof [][]byte, leafHash []byte, blockHash []byte, amount *big.Int, err error) { t, err := smt.Load(mtreeJson) if err != nil { - return nil, nil, nil, nil, "", fmt.Errorf("load mtree error: %w", err) + return nil, nil, nil, nil, nil, fmt.Errorf("load mtree error: %w", err) } entries := t.Entries() @@ -56,24 +57,24 @@ func GetMTreeProof(mtreeJson []byte, addr string) (root []byte, proof [][]byte, if v.Value[0] == smt.SolAddress(addr) { proof, err := t.GetProofWithIndex(i) if err != nil { - return nil, nil, nil, nil, "", fmt.Errorf("get proof error: %w", err) + return nil, nil, nil, nil, nil, fmt.Errorf("get proof error: %w", err) } amt, ok := v.Value[1].(*big.Int) if !ok { - return nil, nil, nil, nil, "", fmt.Errorf("internal bug: get leaf amount error: %w", err) + return nil, nil, nil, nil, nil, fmt.Errorf("internal bug: get leaf amount error: %w", err) } blockHash, ok := v.Value[3].([32]byte) if !ok { - return nil, nil, nil, nil, "", fmt.Errorf("internal bug: get leaf block hash error: %w", err) + return nil, nil, nil, nil, nil, fmt.Errorf("internal bug: get leaf block hash error: %w", err) } - return t.GetRoot(), proof, v.Hash, blockHash[:], amt.String(), nil + return t.GetRoot(), proof, v.Hash, blockHash[:], amt, nil } } - return nil, nil, nil, nil, "", fmt.Errorf("get proof error: %w", err) + return nil, nil, nil, nil, nil, fmt.Errorf("get proof error: %w", err) } func GetLeafAddresses(mtreeJson string) ([]string, error) { diff --git a/node/exts/erc20reward/reward/mtree_test.go b/node/exts/erc20reward/reward/mtree_test.go index 312a32321..72069c153 100644 --- a/node/exts/erc20reward/reward/mtree_test.go +++ b/node/exts/erc20reward/reward/mtree_test.go @@ -6,9 +6,10 @@ import ( "testing" "github.com/ethereum/go-ethereum/common/hexutil" - smt "github.com/kwilteam/openzeppelin-merkle-tree-go/standard_merkle_tree" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + smt "github.com/kwilteam/openzeppelin-merkle-tree-go/standard_merkle_tree" ) func Map[T1, T2 any](s []T1, f func(T1) T2) []T2 { diff --git a/node/exts/evm-sync/chains/chains.go b/node/exts/evm-sync/chains/chains.go index f07f3cec2..7a81fa391 100644 --- a/node/exts/evm-sync/chains/chains.go +++ b/node/exts/evm-sync/chains/chains.go @@ -16,8 +16,6 @@ type ChainInfo struct { // RequiredConfirmations is the number of confirmations required before an event is considered final. // For example, Ethereum mainnet requires 12 confirmations. RequiredConfirmations int64 - // Etherscan address for this chain. - Etherscan string } func init() { @@ -26,13 +24,11 @@ func init() { Name: "ethereum", ID: "1", RequiredConfirmations: 12, - Etherscan: "https://etherscan.io/address/", }, ChainInfo{ Name: "sepolia", ID: "11155111", RequiredConfirmations: 12, - Etherscan: "https://sepolia.etherscan.io/address/", }, ) if err != nil { From 6d5f18f3c997c1b58dd3abafd03635492cb893dc Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Tue, 18 Feb 2025 21:54:28 -0600 Subject: [PATCH 10/30] update taskfile --- Taskfile.yml | 7 ++----- node/exts/erc20reward/abigen/reward_distributor.go | 1 + 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index 55646e7ce..2b087a4d9 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -73,11 +73,6 @@ tasks: generates: - .build/kwild - gen:abi: # TODO: merge with brennan's - desc: Generate abis - cmds: - - abigen --abi=./node/services/erc20signersvc/abigen/safe_abi.json --pkg abigen --out=./node/services/erc20signersvc/abigen/safe.go --type Safe - - abigen --abi=./node/services/erc20signersvc/abigen/multicall3_abi.json --pkg abigen --out=./node/services/erc20signersvc/abigen/multicall3.go --type Multicall3 generate:docs: desc: Generate docs for CLIs @@ -100,6 +95,8 @@ tasks: cmds: - abigen --abi=./node/exts/erc20reward/abigen/reward_distributor_abi.json --pkg abigen --out=./node/exts/erc20reward/abigen/reward_distributor.go --type RewardDistributor - abigen --abi=./node/exts/erc20reward/abigen/erc20_abi.json --pkg abigen --out=./node/exts/erc20reward/abigen/erc20.go --type Erc20 + - abigen --abi=./node/services/erc20signersvc/abigen/safe_abi.json --pkg abigen --out=./node/services/erc20signersvc/abigen/safe.go --type Safe + - abigen --abi=./node/services/erc20signersvc/abigen/multicall3_abi.json --pkg abigen --out=./node/services/erc20signersvc/abigen/multicall3.go --type Multicall3 # ************ docker ************ vendor: diff --git a/node/exts/erc20reward/abigen/reward_distributor.go b/node/exts/erc20reward/abigen/reward_distributor.go index 9433b2ca6..69634710f 100644 --- a/node/exts/erc20reward/abigen/reward_distributor.go +++ b/node/exts/erc20reward/abigen/reward_distributor.go @@ -873,6 +873,7 @@ type RewardDistributorRewardPosted struct { // // Solidity: event RewardPosted(bytes32 root, uint256 amount, address poster) func (_RewardDistributor *RewardDistributorFilterer) FilterRewardPosted(opts *bind.FilterOpts) (*RewardDistributorRewardPostedIterator, error) { + logs, sub, err := _RewardDistributor.contract.FilterLogs(opts, "RewardPosted") if err != nil { return nil, err From f60ca9d19d21296dcad0cd1035efc978b8eb3b66 Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Tue, 18 Feb 2025 22:09:30 -0600 Subject: [PATCH 11/30] remove sleep before start signerSvc --- app/node/node.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/node/node.go b/app/node/node.go index e7825ca89..44156a0fe 100644 --- a/app/node/node.go +++ b/app/node/node.go @@ -5,13 +5,12 @@ import ( "crypto/tls" "errors" "fmt" - signersvc "github.com/kwilteam/kwil-db/node/services/erc20signersvc" + "golang.org/x/sync/errgroup" "io" "os" "path/filepath" "runtime" "slices" - "time" "github.com/kwilteam/kwil-db/app/key" "github.com/kwilteam/kwil-db/config" @@ -23,10 +22,9 @@ import ( "github.com/kwilteam/kwil-db/node" "github.com/kwilteam/kwil-db/node/consensus" "github.com/kwilteam/kwil-db/node/listeners" + signersvc "github.com/kwilteam/kwil-db/node/services/erc20signersvc" rpcserver "github.com/kwilteam/kwil-db/node/services/jsonrpc" "github.com/kwilteam/kwil-db/version" - - "golang.org/x/sync/errgroup" ) type server struct { @@ -264,8 +262,6 @@ func (s *server) Start(ctx context.Context) error { // Start erc20 reward signer svc if s.erc20RWSigner != nil { - // a naive way to wait for the RPC service is running, should be fine - time.Sleep(time.Second * 3) group.Go(func() error { return s.erc20RWSigner.Start(groupCtx) }) From f644782389ddbb17427b019edcaadf6b9bc05e61 Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Tue, 18 Feb 2025 22:25:10 -0600 Subject: [PATCH 12/30] cleanup --- go.mod | 3 +-- node/exts/erc20reward/meta_schema.sql | 3 +-- node/services/erc20signersvc/eth.go | 5 ----- node/services/erc20signersvc/kwil.go | 30 --------------------------- node/services/erc20signersvc/state.go | 2 +- 5 files changed, 3 insertions(+), 40 deletions(-) diff --git a/go.mod b/go.mod index bd61c5c33..6c592b6b2 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/olekukonko/tablewriter v0.0.5 github.com/pelletier/go-toml/v2 v2.2.3 github.com/prometheus/client_golang v1.20.5 + github.com/samber/lo v1.47.0 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.6 github.com/stretchr/testify v1.10.0 @@ -217,5 +218,3 @@ require ( github.com/pion/transport/v3 v3.0.7 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect ) - -require github.com/samber/lo v1.47.0 diff --git a/node/exts/erc20reward/meta_schema.sql b/node/exts/erc20reward/meta_schema.sql index 513cd1593..f02464759 100644 --- a/node/exts/erc20reward/meta_schema.sql +++ b/node/exts/erc20reward/meta_schema.sql @@ -40,8 +40,7 @@ CREATE TABLE balances ( -- 3. Confirmed: the epoch has been confirmed on chain -- Ideally, Kwil would have a unique indexes on this table where "confirmed" is null (to enforce only one active epoch at a time), -- but this requires partial indexes which are not yet supported in Kwil --- Because we need an epoch to issue reward to, but you should not issue reward to a finalized reward, --- thus we'll always have two epochs at the same time(except the very first epoch), +-- We'll always have two active epochs at the same time(except the very first epoch), -- one is finalized and waiting to be confirmed, the other is collecting new rewards. CREATE TABLE epochs ( id UUID PRIMARY KEY, diff --git a/node/services/erc20signersvc/eth.go b/node/services/erc20signersvc/eth.go index e5249f92a..15341a876 100644 --- a/node/services/erc20signersvc/eth.go +++ b/node/services/erc20signersvc/eth.go @@ -16,11 +16,6 @@ import ( "github.com/kwilteam/kwil-db/node/services/erc20signersvc/abigen" ) -// -//func NewEthClient(rpc string) (*ethclient.Client, error) { -// return ethclient.Dial(rpc) -//} - var ( safeABI = lo.Must(abi.JSON(strings.NewReader(abigen.SafeMetaData.ABI))) diff --git a/node/services/erc20signersvc/kwil.go b/node/services/erc20signersvc/kwil.go index d90a1ac54..39068edae 100644 --- a/node/services/erc20signersvc/kwil.go +++ b/node/services/erc20signersvc/kwil.go @@ -63,7 +63,6 @@ type erc20ExtAPI interface { SetTarget(ns string) InstanceInfo(tx context.Context) (*RewardInstanceInfo, error) GetActiveEpochs(ctx context.Context) ([]*Epoch, error) - ListUnconfirmedEpochs(ctx context.Context, afterHeight int64, limit int) ([]*Epoch, error) GetEpochRewards(ctx context.Context, epochID types.UUID) ([]*EpochReward, error) VoteEpoch(ctx context.Context, epochID types.UUID, amount *types.Decimal, safeNonce int64, signature []byte) (string, error) } @@ -140,35 +139,6 @@ func (k *erc20rwExtApi) GetActiveEpochs(ctx context.Context) ([]*Epoch, error) { return ers, nil } - -func (k *erc20rwExtApi) ListUnconfirmedEpochs(ctx context.Context, afterHeight int64, limit int) ([]*Epoch, error) { - procedure := "list_epochs" - - input := []any{afterHeight, limit, false} - - res, err := k.clt.Call(ctx, k.namespace, procedure, input) - if err != nil { - return nil, err - } - - if len(res.QueryResult.Values) == 0 { - return nil, nil - } - - ers := make([]*Epoch, len(res.QueryResult.Values)) - for i, v := range res.QueryResult.Values { - er := &Epoch{} - err = types.ScanTo(v, &er.ID, &er.StartHeight, &er.StartTimestamp, &er.EndHeight, - &er.RewardRoot, &er.RewardAmount, &er.EndBlockHash, &er.Confirmed, &er.Voters, &er.VoteAmounts, &er.VoteNonces, &er.VoteSignatures) - if err != nil { - return nil, err - } - ers[i] = er - } - - return ers, nil -} - func (k *erc20rwExtApi) GetEpochRewards(ctx context.Context, epochID types.UUID) ([]*EpochReward, error) { procedure := "get_epoch_rewards" input := []any{epochID} diff --git a/node/services/erc20signersvc/state.go b/node/services/erc20signersvc/state.go index e11e3adef..9730939b2 100644 --- a/node/services/erc20signersvc/state.go +++ b/node/services/erc20signersvc/state.go @@ -1,6 +1,6 @@ // This file implements a naive KV persistent solution using a file, changes made // through the exposed functions will be written to files on every invoking. -// For signer svc, this is good enough. +// This state won't grow, for signer svc, this is good enough. package signersvc From 887ce2c45d76a0e5aed081b943f09368ea83c40e Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Tue, 18 Feb 2025 22:42:43 -0600 Subject: [PATCH 13/30] lint --- app/node/node.go | 3 ++- node/exts/erc20reward/meta_extension.go | 2 +- node/exts/erc20reward/meta_sql.go | 13 +------------ node/exts/erc20reward/meta_sql_test.go | 7 +++++-- node/exts/erc20reward/named_extension.go | 1 + node/exts/erc20reward/reward/mtree_test.go | 9 +++++---- node/services/erc20signersvc/kwil.go | 5 ++--- 7 files changed, 17 insertions(+), 23 deletions(-) diff --git a/app/node/node.go b/app/node/node.go index 44156a0fe..d960328a4 100644 --- a/app/node/node.go +++ b/app/node/node.go @@ -5,13 +5,14 @@ import ( "crypto/tls" "errors" "fmt" - "golang.org/x/sync/errgroup" "io" "os" "path/filepath" "runtime" "slices" + "golang.org/x/sync/errgroup" + "github.com/kwilteam/kwil-db/app/key" "github.com/kwilteam/kwil-db/config" "github.com/kwilteam/kwil-db/core/crypto" diff --git a/node/exts/erc20reward/meta_extension.go b/node/exts/erc20reward/meta_extension.go index eb5471aa1..e56812e19 100644 --- a/node/exts/erc20reward/meta_extension.go +++ b/node/exts/erc20reward/meta_extension.go @@ -1,4 +1,4 @@ -// package meta implements a meta extension that manages all rewards on a Kwil network. +// package erc20reward implements a meta extension that manages all rewards on a Kwil network. // It is used to create other extensions with which users can distribute erc20 tokens // to users. // It works by exposing an action to the DB owner which allows creation of new extensions diff --git a/node/exts/erc20reward/meta_sql.go b/node/exts/erc20reward/meta_sql.go index f6daa3b8d..bd4ae8e8c 100644 --- a/node/exts/erc20reward/meta_sql.go +++ b/node/exts/erc20reward/meta_sql.go @@ -421,8 +421,7 @@ func rowToEpoch(r *common.Row) (*Epoch, error) { confirmed := r.Values[7].(bool) // NOTE: empty value is [[]] - // NOTE: should not use append, we could accidently skip some 'nil' element and messup the index - // But we can be sure values[7]-values[10] will all be empty if any, from the SQL; + // values[7]-values[10] will all be empty if any, from the SQL; var voters []ethcommon.Address if r.Values[8] != nil { rawVoters := r.Values[8].([][]byte) @@ -619,16 +618,6 @@ func voteEpoch(ctx context.Context, app *common.App, epochID *types.UUID, }, nil) } -// removeEpochVotes removes all votes associated with an epoch. -func removeEpochVotes(ctx context.Context, app *common.App, epochID *types.UUID) error { - return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` - {kwil_erc20_meta}DELETE FROM epoch_votes - WHERE epoch_id = $epoch_id; - `, map[string]any{ - "epoch_id": epochID, - }, nil) -} - // getWalletEpochs returns all confirmed epochs that the given wallet has reward in. // If pending=true, return all finalized epochs(no necessary confirmed). func getWalletEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, diff --git a/node/exts/erc20reward/meta_sql_test.go b/node/exts/erc20reward/meta_sql_test.go index c31deb8b9..f5da82d1a 100644 --- a/node/exts/erc20reward/meta_sql_test.go +++ b/node/exts/erc20reward/meta_sql_test.go @@ -2,9 +2,11 @@ package erc20reward import ( "context" + "math/big" + "testing" + ethcommon "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" - "testing" "github.com/kwilteam/kwil-db/common" "github.com/kwilteam/kwil-db/core/log" @@ -134,8 +136,9 @@ func TestCreateNewRewardInstance(t *testing.T) { require.Equal(t, int64(18), rewards[0].syncedRewardData.Erc20Decimals) root := []byte{0x03, 0x04} + amt, _ := erc20ValueFromBigInt(big.NewInt(100)) // finalize the epoch - err = finalizeEpoch(ctx, app, pending.ID, 20, []byte{0x01, 0x02}, root) + err = finalizeEpoch(ctx, app, pending.ID, 20, []byte{0x01, 0x02}, root, amt) require.NoError(t, err) // confirm the epoch diff --git a/node/exts/erc20reward/named_extension.go b/node/exts/erc20reward/named_extension.go index fc4cec031..c52c1a8fe 100644 --- a/node/exts/erc20reward/named_extension.go +++ b/node/exts/erc20reward/named_extension.go @@ -3,6 +3,7 @@ package erc20reward import ( "context" "errors" + "github.com/kwilteam/kwil-db/common" "github.com/kwilteam/kwil-db/core/types" "github.com/kwilteam/kwil-db/extensions/precompiles" diff --git a/node/exts/erc20reward/reward/mtree_test.go b/node/exts/erc20reward/reward/mtree_test.go index 72069c153..d47d86da4 100644 --- a/node/exts/erc20reward/reward/mtree_test.go +++ b/node/exts/erc20reward/reward/mtree_test.go @@ -2,6 +2,7 @@ package reward import ( "encoding/hex" + "math/big" "strings" "testing" @@ -48,7 +49,7 @@ func TestMerkleTree(t *testing.T) { mtRoot, mtProof, mtLeaf, bh, amt, err := GetMTreeProof(mt, user2) require.NoError(t, err) require.Equal(t, expectRoot, hex.EncodeToString(mtRoot)) - require.Equal(t, "200", amt) + require.Equal(t, big.NewInt(200), amt) require.EqualValues(t, kwilBlockHashBytes, bh) require.Len(t, mtProof, 2) assert.Equal(t, "0x644f999664d65d1d2a3feefade54d643dc2b9696971e9070c36f0ec788e55f5b", hexutil.Encode(mtProof[0])) @@ -66,7 +67,7 @@ func TestMerkleTree(t *testing.T) { mtRoot, mtProof, mtLeaf, bh, amt, err := GetMTreeProof(mt, user2) require.NoError(t, err) require.Equal(t, expectRoot, hex.EncodeToString(mtRoot)) - require.Equal(t, "200", amt) + require.Equal(t, big.NewInt(200), amt) require.EqualValues(t, kwilBlockHashBytes, bh) require.Len(t, mtProof, 2) assert.Equal(t, "0x644f999664d65d1d2a3feefade54d643dc2b9696971e9070c36f0ec788e55f5b", hexutil.Encode(mtProof[0])) @@ -84,7 +85,7 @@ func TestMerkleTree(t *testing.T) { mtRoot, mtProof, mtLeaf, bh, amt, err := GetMTreeProof(mt, user2) require.NoError(t, err) require.Equal(t, expectRoot, hex.EncodeToString(mtRoot)) - require.Equal(t, "200", amt) + require.Equal(t, big.NewInt(200), amt) require.EqualValues(t, kwilBlockHashBytes, bh) require.Len(t, mtProof, 3) assert.Equal(t, "0x644f999664d65d1d2a3feefade54d643dc2b9696971e9070c36f0ec788e55f5b", hexutil.Encode(mtProof[0])) @@ -101,7 +102,7 @@ func TestMerkleTree(t *testing.T) { mtRoot, mtProof, mtLeaf, bh, amt, err := GetMTreeProof(mt, user1) require.NoError(t, err) require.Equal(t, root, mtRoot) - require.Equal(t, "100", amt) + require.Equal(t, big.NewInt(100), amt) require.EqualValues(t, kwilBlockHashBytes, bh) require.Len(t, mtProof, 0) // no proofs assert.Equal(t, root, mtLeaf) // the leaf is the root diff --git a/node/services/erc20signersvc/kwil.go b/node/services/erc20signersvc/kwil.go index 39068edae..77fdfa262 100644 --- a/node/services/erc20signersvc/kwil.go +++ b/node/services/erc20signersvc/kwil.go @@ -68,9 +68,8 @@ type erc20ExtAPI interface { } type erc20rwExtApi struct { - clt *client.Client - namespace string - instanceID string + clt *client.Client + namespace string } var _ erc20ExtAPI = (*erc20rwExtApi)(nil) From 55bcc0f08961b99b0429925ebc424c3b787b778e Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Wed, 19 Feb 2025 09:29:01 -0600 Subject: [PATCH 14/30] update erc20 signer config name --- app/node/build.go | 2 +- config/config.go | 34 +++++++++++++++++----------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/node/build.go b/app/node/build.go index 6179a5ecc..168dd670e 100644 --- a/app/node/build.go +++ b/app/node/build.go @@ -509,7 +509,7 @@ func buildConsensusEngine(_ context.Context, d *coreDependencies, db *pg.DB, } func buildErc20RWignerMgr(d *coreDependencies) *signersvc.ServiceMgr { - cfg := d.cfg.Erc20RWSigner + cfg := d.cfg.Erc20BridgeSigner if !cfg.Enable { return nil } diff --git a/config/config.go b/config/config.go index 3984855f1..9f173b6aa 100644 --- a/config/config.go +++ b/config/config.go @@ -311,7 +311,7 @@ func DefaultConfig() *Config { Height: 0, Hash: types.Hash{}, }, - Erc20RWSigner: Erc20RewardSignerConfig{ + Erc20BridgeSigner: ERC20BridgeSignerConfig{ Enable: false, PrivateKeys: nil, Targets: nil, @@ -330,19 +330,19 @@ type Config struct { ProfileMode string `toml:"profile_mode,commented" comment:"profile mode (http, cpu, mem, mutex, or block)"` ProfileFile string `toml:"profile_file,commented" comment:"profile output file path (e.g. cpu.pprof)"` - P2P PeerConfig `toml:"p2p" comment:"P2P related configuration"` - Consensus ConsensusConfig `toml:"consensus" comment:"Consensus related configuration"` - DB DBConfig `toml:"db" comment:"DB (PostgreSQL) related configuration"` - Store StoreConfig `toml:"store" comment:"Block store configuration"` - RPC RPCConfig `toml:"rpc" comment:"User RPC service configuration"` - Admin AdminConfig `toml:"admin" comment:"Admin RPC service configuration"` - Snapshots SnapshotConfig `toml:"snapshots" comment:"Snapshot creation and provider configuration"` - StateSync StateSyncConfig `toml:"state_sync" comment:"Statesync configuration (vs block sync)"` - Extensions map[string]map[string]string `toml:"extensions" comment:"extension configuration"` - GenesisState string `toml:"genesis_state" comment:"path to the genesis state file, relative to the root directory"` - Migrations MigrationConfig `toml:"migrations" comment:"zero downtime migration configuration"` - Checkpoint Checkpoint `toml:"checkpoint" comment:"checkpoint info for the leader to sync to before proposing a new block"` - Erc20RWSigner Erc20RewardSignerConfig `toml:"erc20_reward_signer" comment:"ERC20 reward signer service configuration"` + P2P PeerConfig `toml:"p2p" comment:"P2P related configuration"` + Consensus ConsensusConfig `toml:"consensus" comment:"Consensus related configuration"` + DB DBConfig `toml:"db" comment:"DB (PostgreSQL) related configuration"` + Store StoreConfig `toml:"store" comment:"Block store configuration"` + RPC RPCConfig `toml:"rpc" comment:"User RPC service configuration"` + Admin AdminConfig `toml:"admin" comment:"Admin RPC service configuration"` + Snapshots SnapshotConfig `toml:"snapshots" comment:"Snapshot creation and provider configuration"` + StateSync StateSyncConfig `toml:"state_sync" comment:"Statesync configuration (vs block sync)"` + Extensions map[string]map[string]string `toml:"extensions" comment:"extension configuration"` + GenesisState string `toml:"genesis_state" comment:"path to the genesis state file, relative to the root directory"` + Migrations MigrationConfig `toml:"migrations" comment:"zero downtime migration configuration"` + Checkpoint Checkpoint `toml:"checkpoint" comment:"checkpoint info for the leader to sync to before proposing a new block"` + Erc20BridgeSigner ERC20BridgeSignerConfig `toml:"erc20_bridge_signer" comment:"ERC20 bridge signer service configuration"` } // PeerConfig corresponds to the [p2p] section of the config. @@ -451,15 +451,15 @@ type Checkpoint struct { Hash types.Hash `toml:"hash" comment:"checkpoint block hash."` } -type Erc20RewardSignerConfig struct { - Enable bool `toml:"enable" comment:"enable the ERC20 reward signer service"` +type ERC20BridgeSignerConfig struct { + Enable bool `toml:"enable" comment:"enable the ERC20 bridge signer service"` Targets []string `toml:"targets" comment:"target reward ext alias for the ERC20 reward"` PrivateKeys []string `toml:"private_keys" comment:"private key for the ERC20 reward target"` EthRpcs []string `toml:"eth_rpcs" comment:"eth rpc address for the ERC20 reward target"` SyncEvery types.Duration `toml:"sync_every" comment:"sync interval; a recommend value is same as the block time"` } -func (cfg Erc20RewardSignerConfig) Validate() error { +func (cfg ERC20BridgeSignerConfig) Validate() error { if (len(cfg.PrivateKeys) != len(cfg.Targets)) && (len(cfg.EthRpcs) != len(cfg.Targets)) { return fmt.Errorf("private keys and targets and eth_rpcs must be configured in triples") } From 0fd31957ca1d22f93af590d1975edd16f48b8eb9 Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Wed, 19 Feb 2025 10:27:48 -0600 Subject: [PATCH 15/30] use empty array instead of JOIN in list_wallet_epoch --- node/exts/erc20reward/meta_sql.go | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/node/exts/erc20reward/meta_sql.go b/node/exts/erc20reward/meta_sql.go index bd4ae8e8c..0e822cc55 100644 --- a/node/exts/erc20reward/meta_sql.go +++ b/node/exts/erc20reward/meta_sql.go @@ -623,26 +623,16 @@ func voteEpoch(ctx context.Context, app *common.App, epochID *types.UUID, func getWalletEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, wallet ethcommon.Address, pending bool, fn func(*Epoch) error) error { + // WE don't need vote info, we just return empty arrays instead of JOIN query := ` - {kwil_erc20_meta}SELECT e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.reward_amount, e.ended_at, e.block_hash, e.confirmed, array_agg(v.voter) as voters, array_agg(v.amount) as amounts, array_agg(v.nonce) as nonces, array_agg(v.signature) as signatures + {kwil_erc20_meta}SELECT e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.reward_amount, e.ended_at, e.block_hash, e.confirmed, ARRAY[]::BYTEA[] as voters, ARRAY[]::NUMERIC(78, 0)[] as amounts, ARRAY[]::INT8[] as nonces, ARRAY[]::BYTEA[] as signatures FROM epoch_rewards AS r JOIN epochs AS e ON r.epoch_id = e.id - LEFT JOIN epoch_votes AS v ON v.epoch_id = e.id WHERE recipient = $wallet AND e.instance_id = $instance_id AND e.ended_at IS NOT NULL` // at least finalized - - // TODO: use this after SQL issue is fixed - //query := ` - //{kwil_erc20_meta}SELECT e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.ended_at, e.block_hash, e.confirmed, ARRAY[]::bytea[], ARRAY[]::int8[] - //FROM epoch_rewards AS r - //JOIN epochs AS e ON r.epoch_id = e.id - //WHERE recipient = $wallet AND e.instance_id = $instance_id AND e.ended_at IS NOT NULL` // at least finalized if !pending { query += ` AND e.confirmed IS true` } - // TODO: remove - query += ` GROUP BY e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.reward_amount, e.ended_at, e.block_hash, e.confirmed` - query += ";" return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, query, map[string]any{ From 9806648bce4f4d99245ccc094b0db3939cb95a97 Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Wed, 19 Feb 2025 11:18:09 -0600 Subject: [PATCH 16/30] re-org erc20 and signersvc pkg --- app/node/build.go | 4 ++-- app/node/node.go | 2 +- .../{erc20reward => erc20-bridge}/abigen/erc20.go | 0 .../abigen/erc20_abi.json | 0 .../erc20-bridge}/abigen/multicall3.go | 0 .../erc20-bridge}/abigen/multicall3_abi.json | 0 .../abigen/reward_distributor.go | 0 .../abigen/reward_distributor_abi.json | 0 .../erc20-bridge}/abigen/safe.go | 0 .../erc20-bridge}/abigen/safe_abi.json | 0 .../{erc20reward => erc20-bridge/erc20}/.gitignore | 0 .../erc20}/meta_extension.go | 13 +++++++------ .../erc20}/meta_extension_test.go | 2 +- .../erc20}/meta_schema.sql | 0 .../erc20}/meta_sql.go | 2 +- .../erc20}/meta_sql_test.go | 2 +- .../erc20}/named_extension.go | 2 +- .../erc20-bridge/signersvc}/.gitignore | 0 .../erc20-bridge/signersvc}/eth.go | 6 ++---- .../erc20-bridge/signersvc}/eth_test.go | 0 .../erc20-bridge/signersvc}/kwil.go | 0 .../erc20-bridge/signersvc}/multicall.go | 4 ++-- .../erc20-bridge/signersvc}/signer.go | 10 +++++----- .../erc20-bridge/signersvc}/state.go | 0 .../reward => erc20-bridge/utils}/crypto.go | 4 ++-- .../reward => erc20-bridge/utils}/crypto_test.go | 14 +++++++------- .../reward => erc20-bridge/utils}/mtree.go | 2 +- .../reward => erc20-bridge/utils}/mtree_test.go | 2 +- 28 files changed, 34 insertions(+), 35 deletions(-) rename node/exts/{erc20reward => erc20-bridge}/abigen/erc20.go (100%) rename node/exts/{erc20reward => erc20-bridge}/abigen/erc20_abi.json (100%) rename node/{services/erc20signersvc => exts/erc20-bridge}/abigen/multicall3.go (100%) rename node/{services/erc20signersvc => exts/erc20-bridge}/abigen/multicall3_abi.json (100%) rename node/exts/{erc20reward => erc20-bridge}/abigen/reward_distributor.go (100%) rename node/exts/{erc20reward => erc20-bridge}/abigen/reward_distributor_abi.json (100%) rename node/{services/erc20signersvc => exts/erc20-bridge}/abigen/safe.go (100%) rename node/{services/erc20signersvc => exts/erc20-bridge}/abigen/safe_abi.json (100%) rename node/exts/{erc20reward => erc20-bridge/erc20}/.gitignore (100%) rename node/exts/{erc20reward => erc20-bridge/erc20}/meta_extension.go (99%) rename node/exts/{erc20reward => erc20-bridge/erc20}/meta_extension_test.go (99%) rename node/exts/{erc20reward => erc20-bridge/erc20}/meta_schema.sql (100%) rename node/exts/{erc20reward => erc20-bridge/erc20}/meta_sql.go (99%) rename node/exts/{erc20reward => erc20-bridge/erc20}/meta_sql_test.go (99%) rename node/exts/{erc20reward => erc20-bridge/erc20}/named_extension.go (99%) rename node/{services/erc20signersvc => exts/erc20-bridge/signersvc}/.gitignore (100%) rename node/{services/erc20signersvc => exts/erc20-bridge/signersvc}/eth.go (95%) rename node/{services/erc20signersvc => exts/erc20-bridge/signersvc}/eth_test.go (100%) rename node/{services/erc20signersvc => exts/erc20-bridge/signersvc}/kwil.go (100%) rename node/{services/erc20signersvc => exts/erc20-bridge/signersvc}/multicall.go (97%) rename node/{services/erc20signersvc => exts/erc20-bridge/signersvc}/signer.go (96%) rename node/{services/erc20signersvc => exts/erc20-bridge/signersvc}/state.go (100%) rename node/exts/{erc20reward/reward => erc20-bridge/utils}/crypto.go (98%) rename node/exts/{erc20reward/reward => erc20-bridge/utils}/crypto_test.go (86%) rename node/exts/{erc20reward/reward => erc20-bridge/utils}/mtree.go (99%) rename node/exts/{erc20reward/reward => erc20-bridge/utils}/mtree_test.go (99%) diff --git a/app/node/build.go b/app/node/build.go index 168dd670e..fbec88ae7 100644 --- a/app/node/build.go +++ b/app/node/build.go @@ -26,13 +26,13 @@ import ( "github.com/kwilteam/kwil-db/node/consensus" "github.com/kwilteam/kwil-db/node/engine" "github.com/kwilteam/kwil-db/node/engine/interpreter" - _ "github.com/kwilteam/kwil-db/node/exts/erc20reward" + _ "github.com/kwilteam/kwil-db/node/exts/erc20-bridge/erc20" + "github.com/kwilteam/kwil-db/node/exts/erc20-bridge/signersvc" "github.com/kwilteam/kwil-db/node/listeners" "github.com/kwilteam/kwil-db/node/mempool" "github.com/kwilteam/kwil-db/node/meta" "github.com/kwilteam/kwil-db/node/migrations" "github.com/kwilteam/kwil-db/node/pg" - signersvc "github.com/kwilteam/kwil-db/node/services/erc20signersvc" rpcserver "github.com/kwilteam/kwil-db/node/services/jsonrpc" "github.com/kwilteam/kwil-db/node/services/jsonrpc/adminsvc" "github.com/kwilteam/kwil-db/node/services/jsonrpc/chainsvc" diff --git a/app/node/node.go b/app/node/node.go index d960328a4..0acad46e0 100644 --- a/app/node/node.go +++ b/app/node/node.go @@ -22,8 +22,8 @@ import ( authExt "github.com/kwilteam/kwil-db/extensions/auth" "github.com/kwilteam/kwil-db/node" "github.com/kwilteam/kwil-db/node/consensus" + "github.com/kwilteam/kwil-db/node/exts/erc20-bridge/signersvc" "github.com/kwilteam/kwil-db/node/listeners" - signersvc "github.com/kwilteam/kwil-db/node/services/erc20signersvc" rpcserver "github.com/kwilteam/kwil-db/node/services/jsonrpc" "github.com/kwilteam/kwil-db/version" ) diff --git a/node/exts/erc20reward/abigen/erc20.go b/node/exts/erc20-bridge/abigen/erc20.go similarity index 100% rename from node/exts/erc20reward/abigen/erc20.go rename to node/exts/erc20-bridge/abigen/erc20.go diff --git a/node/exts/erc20reward/abigen/erc20_abi.json b/node/exts/erc20-bridge/abigen/erc20_abi.json similarity index 100% rename from node/exts/erc20reward/abigen/erc20_abi.json rename to node/exts/erc20-bridge/abigen/erc20_abi.json diff --git a/node/services/erc20signersvc/abigen/multicall3.go b/node/exts/erc20-bridge/abigen/multicall3.go similarity index 100% rename from node/services/erc20signersvc/abigen/multicall3.go rename to node/exts/erc20-bridge/abigen/multicall3.go diff --git a/node/services/erc20signersvc/abigen/multicall3_abi.json b/node/exts/erc20-bridge/abigen/multicall3_abi.json similarity index 100% rename from node/services/erc20signersvc/abigen/multicall3_abi.json rename to node/exts/erc20-bridge/abigen/multicall3_abi.json diff --git a/node/exts/erc20reward/abigen/reward_distributor.go b/node/exts/erc20-bridge/abigen/reward_distributor.go similarity index 100% rename from node/exts/erc20reward/abigen/reward_distributor.go rename to node/exts/erc20-bridge/abigen/reward_distributor.go diff --git a/node/exts/erc20reward/abigen/reward_distributor_abi.json b/node/exts/erc20-bridge/abigen/reward_distributor_abi.json similarity index 100% rename from node/exts/erc20reward/abigen/reward_distributor_abi.json rename to node/exts/erc20-bridge/abigen/reward_distributor_abi.json diff --git a/node/services/erc20signersvc/abigen/safe.go b/node/exts/erc20-bridge/abigen/safe.go similarity index 100% rename from node/services/erc20signersvc/abigen/safe.go rename to node/exts/erc20-bridge/abigen/safe.go diff --git a/node/services/erc20signersvc/abigen/safe_abi.json b/node/exts/erc20-bridge/abigen/safe_abi.json similarity index 100% rename from node/services/erc20signersvc/abigen/safe_abi.json rename to node/exts/erc20-bridge/abigen/safe_abi.json diff --git a/node/exts/erc20reward/.gitignore b/node/exts/erc20-bridge/erc20/.gitignore similarity index 100% rename from node/exts/erc20reward/.gitignore rename to node/exts/erc20-bridge/erc20/.gitignore diff --git a/node/exts/erc20reward/meta_extension.go b/node/exts/erc20-bridge/erc20/meta_extension.go similarity index 99% rename from node/exts/erc20reward/meta_extension.go rename to node/exts/erc20-bridge/erc20/meta_extension.go index e56812e19..201b7aa7e 100644 --- a/node/exts/erc20reward/meta_extension.go +++ b/node/exts/erc20-bridge/erc20/meta_extension.go @@ -10,7 +10,7 @@ // Internally, the node will start another event listener which is responsible for tracking // the erc20's Transfer event. When a transfer event is detected, the node will update the // reward balance of the recipient. -package erc20reward +package erc20 import ( "bytes" @@ -29,6 +29,7 @@ import ( ethcommon "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" + "github.com/kwilteam/kwil-db/common" "github.com/kwilteam/kwil-db/core/log" "github.com/kwilteam/kwil-db/core/types" @@ -38,8 +39,8 @@ import ( "github.com/kwilteam/kwil-db/extensions/precompiles" "github.com/kwilteam/kwil-db/extensions/resolutions" "github.com/kwilteam/kwil-db/node/engine" - "github.com/kwilteam/kwil-db/node/exts/erc20reward/abigen" - "github.com/kwilteam/kwil-db/node/exts/erc20reward/reward" + "github.com/kwilteam/kwil-db/node/exts/erc20-bridge/abigen" + "github.com/kwilteam/kwil-db/node/exts/erc20-bridge/utils" evmsync "github.com/kwilteam/kwil-db/node/exts/evm-sync" "github.com/kwilteam/kwil-db/node/exts/evm-sync/chains" "github.com/kwilteam/kwil-db/node/types/sql" @@ -1087,7 +1088,7 @@ func init() { return fmt.Errorf("amount cannot be negative") } - if len(signature) != reward.GnosisSafeSigLength { + if len(signature) != utils.GnosisSafeSigLength { return fmt.Errorf("signature is not 65 bytes") } @@ -1181,7 +1182,7 @@ func init() { mtLRUCache.Put(b32Root, jsonTree) } - _, proofs, _, bh, amtBig, err := reward.GetMTreeProof(jsonTree, walletAddr.String()) + _, proofs, _, bh, amtBig, err := utils.GetMTreeProof(jsonTree, walletAddr.String()) if err != nil { return err } @@ -1362,7 +1363,7 @@ func genMerkleTreeForEpoch(ctx context.Context, app *common.App, epochID *types. total.Add(total, amounts[i]) } - jsonTree, root, err = reward.GenRewardMerkleTree(users, amounts, escrowAddr, blockHash) + jsonTree, root, err = utils.GenRewardMerkleTree(users, amounts, escrowAddr, blockHash) if err != nil { return 0, nil, nil, nil, err } diff --git a/node/exts/erc20reward/meta_extension_test.go b/node/exts/erc20-bridge/erc20/meta_extension_test.go similarity index 99% rename from node/exts/erc20reward/meta_extension_test.go rename to node/exts/erc20-bridge/erc20/meta_extension_test.go index a7a95a09a..4be7bd7e8 100644 --- a/node/exts/erc20reward/meta_extension_test.go +++ b/node/exts/erc20-bridge/erc20/meta_extension_test.go @@ -1,4 +1,4 @@ -package erc20reward +package erc20 import ( "bytes" diff --git a/node/exts/erc20reward/meta_schema.sql b/node/exts/erc20-bridge/erc20/meta_schema.sql similarity index 100% rename from node/exts/erc20reward/meta_schema.sql rename to node/exts/erc20-bridge/erc20/meta_schema.sql diff --git a/node/exts/erc20reward/meta_sql.go b/node/exts/erc20-bridge/erc20/meta_sql.go similarity index 99% rename from node/exts/erc20reward/meta_sql.go rename to node/exts/erc20-bridge/erc20/meta_sql.go index 0e822cc55..404f8a292 100644 --- a/node/exts/erc20reward/meta_sql.go +++ b/node/exts/erc20-bridge/erc20/meta_sql.go @@ -1,4 +1,4 @@ -package erc20reward +package erc20 import ( "context" diff --git a/node/exts/erc20reward/meta_sql_test.go b/node/exts/erc20-bridge/erc20/meta_sql_test.go similarity index 99% rename from node/exts/erc20reward/meta_sql_test.go rename to node/exts/erc20-bridge/erc20/meta_sql_test.go index f5da82d1a..287e14281 100644 --- a/node/exts/erc20reward/meta_sql_test.go +++ b/node/exts/erc20-bridge/erc20/meta_sql_test.go @@ -1,4 +1,4 @@ -package erc20reward +package erc20 import ( "context" diff --git a/node/exts/erc20reward/named_extension.go b/node/exts/erc20-bridge/erc20/named_extension.go similarity index 99% rename from node/exts/erc20reward/named_extension.go rename to node/exts/erc20-bridge/erc20/named_extension.go index c52c1a8fe..18cef2cad 100644 --- a/node/exts/erc20reward/named_extension.go +++ b/node/exts/erc20-bridge/erc20/named_extension.go @@ -1,4 +1,4 @@ -package erc20reward +package erc20 import ( "context" diff --git a/node/services/erc20signersvc/.gitignore b/node/exts/erc20-bridge/signersvc/.gitignore similarity index 100% rename from node/services/erc20signersvc/.gitignore rename to node/exts/erc20-bridge/signersvc/.gitignore diff --git a/node/services/erc20signersvc/eth.go b/node/exts/erc20-bridge/signersvc/eth.go similarity index 95% rename from node/services/erc20signersvc/eth.go rename to node/exts/erc20-bridge/signersvc/eth.go index 15341a876..136188914 100644 --- a/node/services/erc20signersvc/eth.go +++ b/node/exts/erc20-bridge/signersvc/eth.go @@ -10,10 +10,8 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" + "github.com/kwilteam/kwil-db/node/exts/erc20-bridge/abigen" "github.com/samber/lo" - - extabigen "github.com/kwilteam/kwil-db/node/exts/erc20reward/abigen" - "github.com/kwilteam/kwil-db/node/services/erc20signersvc/abigen" ) var ( @@ -50,7 +48,7 @@ func NewSafeFromEscrow(rpc string, escrowAddr string) (*Safe, error) { return nil, fmt.Errorf("create eth chainID: %w", err) } - rd, err := extabigen.NewRewardDistributor(common.HexToAddress(escrowAddr), client) + rd, err := abigen.NewRewardDistributor(common.HexToAddress(escrowAddr), client) if err != nil { return nil, fmt.Errorf("create reward distributor: %w", err) } diff --git a/node/services/erc20signersvc/eth_test.go b/node/exts/erc20-bridge/signersvc/eth_test.go similarity index 100% rename from node/services/erc20signersvc/eth_test.go rename to node/exts/erc20-bridge/signersvc/eth_test.go diff --git a/node/services/erc20signersvc/kwil.go b/node/exts/erc20-bridge/signersvc/kwil.go similarity index 100% rename from node/services/erc20signersvc/kwil.go rename to node/exts/erc20-bridge/signersvc/kwil.go diff --git a/node/services/erc20signersvc/multicall.go b/node/exts/erc20-bridge/signersvc/multicall.go similarity index 97% rename from node/services/erc20signersvc/multicall.go rename to node/exts/erc20-bridge/signersvc/multicall.go index 6363a58e6..ea3d5d816 100644 --- a/node/services/erc20signersvc/multicall.go +++ b/node/exts/erc20-bridge/signersvc/multicall.go @@ -8,8 +8,8 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" - - "github.com/kwilteam/kwil-db/node/services/erc20signersvc/abigen" + + "github.com/kwilteam/kwil-db/node/exts/erc20-bridge/abigen" ) // Multicall https://github.com/mds1/multicall diff --git a/node/services/erc20signersvc/signer.go b/node/exts/erc20-bridge/signersvc/signer.go similarity index 96% rename from node/services/erc20signersvc/signer.go rename to node/exts/erc20-bridge/signersvc/signer.go index e3c5af782..c6b8c42be 100644 --- a/node/services/erc20signersvc/signer.go +++ b/node/exts/erc20-bridge/signersvc/signer.go @@ -25,7 +25,7 @@ import ( "github.com/kwilteam/kwil-db/core/crypto/auth" "github.com/kwilteam/kwil-db/core/log" "github.com/kwilteam/kwil-db/core/types" - "github.com/kwilteam/kwil-db/node/exts/erc20reward/reward" + "github.com/kwilteam/kwil-db/node/exts/erc20-bridge/utils" "github.com/kwilteam/kwil-db/node/exts/evm-sync/chains" ) @@ -187,7 +187,7 @@ func (s *rewardSigner) verify(ctx context.Context, epoch *Epoch, escrowAddr stri var b32 [32]byte copy(b32[:], epoch.EndBlockHash) - _, root, err := reward.GenRewardMerkleTree(recipients, amounts, escrowAddr, b32) + _, root, err := utils.GenRewardMerkleTree(recipients, amounts, escrowAddr, b32) if err != nil { return nil, err } @@ -214,20 +214,20 @@ func erc20ValueFromBigInt(b *big.Int) (*types.Decimal, error) { // vote votes an epoch reward, and updates the state. // It will first fetch metadata from ETH, then generate the safeTx, then vote. func (s *rewardSigner) vote(ctx context.Context, epoch *Epoch, safeMeta *safeMetadata, total *big.Int) error { - safeTxData, err := reward.GenPostRewardTxData(epoch.RewardRoot, total) + safeTxData, err := utils.GenPostRewardTxData(epoch.RewardRoot, total) if err != nil { return err } // safeTxHash is the data that all signers will be signing(using personal_sign) - _, safeTxHash, err := reward.GenGnosisSafeTx(s.escrowAddr.String(), s.safe.addr.String(), + _, safeTxHash, err := utils.GenGnosisSafeTx(s.escrowAddr.String(), s.safe.addr.String(), 0, safeTxData, s.safe.chainID.Int64(), safeMeta.nonce.Int64()) if err != nil { return err } signHash := ethAccounts.TextHash(safeTxHash) - sig, err := reward.EthGnosisSignDigest(signHash, s.signerPk) + sig, err := utils.EthGnosisSignDigest(signHash, s.signerPk) if err != nil { return err } diff --git a/node/services/erc20signersvc/state.go b/node/exts/erc20-bridge/signersvc/state.go similarity index 100% rename from node/services/erc20signersvc/state.go rename to node/exts/erc20-bridge/signersvc/state.go diff --git a/node/exts/erc20reward/reward/crypto.go b/node/exts/erc20-bridge/utils/crypto.go similarity index 98% rename from node/exts/erc20reward/reward/crypto.go rename to node/exts/erc20-bridge/utils/crypto.go index c4f90b3f4..f9062af05 100644 --- a/node/exts/erc20reward/reward/crypto.go +++ b/node/exts/erc20-bridge/utils/crypto.go @@ -1,4 +1,4 @@ -package reward +package utils import ( "bytes" @@ -15,7 +15,7 @@ import ( "github.com/ethereum/go-ethereum/signer/core" "github.com/ethereum/go-ethereum/signer/core/apitypes" - "github.com/kwilteam/kwil-db/node/exts/erc20reward/abigen" + "github.com/kwilteam/kwil-db/node/exts/erc20-bridge/abigen" ) const GnosisSafeSigLength = ethCrypto.SignatureLength diff --git a/node/exts/erc20reward/reward/crypto_test.go b/node/exts/erc20-bridge/utils/crypto_test.go similarity index 86% rename from node/exts/erc20reward/reward/crypto_test.go rename to node/exts/erc20-bridge/utils/crypto_test.go index 4fe535e74..49a7b0020 100644 --- a/node/exts/erc20reward/reward/crypto_test.go +++ b/node/exts/erc20-bridge/utils/crypto_test.go @@ -1,4 +1,4 @@ -package reward_test +package utils_test import ( "crypto/ecdsa" @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/kwilteam/kwil-db/node/exts/erc20reward/reward" + "github.com/kwilteam/kwil-db/node/exts/erc20-bridge/utils" ) func getSigner(t *testing.T) (*ecdsa.PrivateKey, *ecdsa.PublicKey) { @@ -48,7 +48,7 @@ func TestSignHash(t *testing.T) { // THIS is what we want, with V adjusted gnosisAdjustedExpected := "8e7091f38dff5127c08580adaa07ab0b3ab5326beaca194f8703da1a31efdf735a4bddb505ec92ee52714a5591db71c9af57c5144458c5cc56098054e26ad44f1f" - gnosisAdjustedSig, err := reward.EthGnosisSign(safeTxHash, priKey) + gnosisAdjustedSig, err := utils.EthGnosisSign(safeTxHash, priKey) require.NoError(t, err) assert.Equal(t, gnosisAdjustedExpected, hex.EncodeToString(gnosisAdjustedSig)) } @@ -68,21 +68,21 @@ func TestGenSafeTx(t *testing.T) { root, err := hex.DecodeString(rootHex) require.NoError(t, err) - data, err := reward.GenPostRewardTxData(root, big.NewInt(21)) + data, err := utils.GenPostRewardTxData(root, big.NewInt(21)) require.NoError(t, err) fmt.Println("tx data:", hex.EncodeToString(data)) priKey, pubKey := getSigner(t) sender := crypto.PubkeyToAddress(*pubKey) - _, safeTxHash, err := reward.GenGnosisSafeTx(rewardAddress, safeAddress, value, data, chainID, nonce) + _, safeTxHash, err := utils.GenGnosisSafeTx(rewardAddress, safeAddress, value, data, chainID, nonce) require.NoError(t, err) assert.Equal(t, expectedSafeTxHash, hex.EncodeToString(safeTxHash)) - sig, err := reward.EthGnosisSign(safeTxHash, priKey) + sig, err := utils.EthGnosisSign(safeTxHash, priKey) require.NoError(t, err) assert.Equal(t, expectedSig, hex.EncodeToString(sig)) - err = reward.EthGnosisVerify(sig, safeTxHash, sender.Bytes()) + err = utils.EthGnosisVerify(sig, safeTxHash, sender.Bytes()) require.NoError(t, err) } diff --git a/node/exts/erc20reward/reward/mtree.go b/node/exts/erc20-bridge/utils/mtree.go similarity index 99% rename from node/exts/erc20reward/reward/mtree.go rename to node/exts/erc20-bridge/utils/mtree.go index 2f601a7af..09fe8dbec 100644 --- a/node/exts/erc20reward/reward/mtree.go +++ b/node/exts/erc20-bridge/utils/mtree.go @@ -1,4 +1,4 @@ -package reward +package utils import ( "fmt" diff --git a/node/exts/erc20reward/reward/mtree_test.go b/node/exts/erc20-bridge/utils/mtree_test.go similarity index 99% rename from node/exts/erc20reward/reward/mtree_test.go rename to node/exts/erc20-bridge/utils/mtree_test.go index d47d86da4..13f2b3bd5 100644 --- a/node/exts/erc20reward/reward/mtree_test.go +++ b/node/exts/erc20-bridge/utils/mtree_test.go @@ -1,4 +1,4 @@ -package reward +package utils import ( "encoding/hex" From 6bbe7948c948a1dd0f89a3ba2722a0a5e5db87ae Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Wed, 19 Feb 2025 14:04:58 -0600 Subject: [PATCH 17/30] return uint256NumericArray instead of TextArray --- .../exts/erc20-bridge/erc20/meta_extension.go | 40 ++++--------------- .../erc20-bridge/erc20/named_extension.go | 4 +- 2 files changed, 9 insertions(+), 35 deletions(-) diff --git a/node/exts/erc20-bridge/erc20/meta_extension.go b/node/exts/erc20-bridge/erc20/meta_extension.go index 201b7aa7e..02c471663 100644 --- a/node/exts/erc20-bridge/erc20/meta_extension.go +++ b/node/exts/erc20-bridge/erc20/meta_extension.go @@ -67,6 +67,7 @@ var ( return dt }() + uint256NumericArray = types.ArrayType(uint256Numeric) // the below are used to identify different types of logs from ethereum // so that we know how to decode them @@ -944,7 +945,7 @@ func init() { {Name: "end_block_hash", Type: types.ByteaType, Nullable: true}, {Name: "confirmed", Type: types.BoolType}, {Name: "voters", Type: types.TextArrayType, Nullable: true}, - {Name: "vote_amounts", Type: types.TextArrayType, Nullable: true}, // cannot successful return uint256NumericArray, as empty value + {Name: "vote_amounts", Type: uint256NumericArray, Nullable: true}, {Name: "vote_nonces", Type: types.IntArrayType, Nullable: true}, {Name: "voter_signatures", Type: types.ByteaArrayType, Nullable: true}, }, @@ -961,21 +962,9 @@ func init() { } } - total := e.Total - if total == nil { - total, _ = erc20ValueFromBigInt(big.NewInt(0)) - } - - var voteAmts []string - for _, amt := range e.VoteAmounts { - if amt != nil { - voteAmts = append(voteAmts, amt.String()) - } - } - - return resultFn([]any{e.ID, e.StartHeight, e.StartTime, *e.EndHeight, e.Root, total, e.BlockHash, e.Confirmed, + return resultFn([]any{e.ID, e.StartHeight, e.StartTime, *e.EndHeight, e.Root, e.Total, e.BlockHash, e.Confirmed, voters, - voteAmts, + e.VoteAmounts, e.VoteNonces, e.VoteSigs, }) @@ -1001,7 +990,7 @@ func init() { {Name: "end_block_hash", Type: types.ByteaType, Nullable: true}, {Name: "confirmed", Type: types.BoolType}, {Name: "voters", Type: types.TextArrayType, Nullable: true}, - {Name: "vote_amounts", Type: types.TextArrayType, Nullable: true}, // cannot successful return uint256NumericArray, as empty value + {Name: "vote_amounts", Type: uint256NumericArray, Nullable: true}, {Name: "vote_nonces", Type: types.IntArrayType, Nullable: true}, {Name: "voter_signatures", Type: types.ByteaArrayType, Nullable: true}, }, @@ -1020,24 +1009,9 @@ func init() { } } - total := e.Total - if total == nil { - total, _ = erc20ValueFromBigInt(big.NewInt(0)) - } - - // uint256NumericArray = types.ArrayType(uint256Numeric) - // NOTE: how to return a nil uint256NumericArray, I cannot set precision/scale on nil type - - var voteAmts []string - for _, amt := range e.VoteAmounts { - if amt != nil { - voteAmts = append(voteAmts, amt.String()) - } - } - - return resultFn([]any{e.ID, e.StartHeight, e.StartTime, *e.EndHeight, e.Root, total, e.BlockHash, e.Confirmed, + return resultFn([]any{e.ID, e.StartHeight, e.StartTime, *e.EndHeight, e.Root, e.Total, e.BlockHash, e.Confirmed, voters, - voteAmts, + e.VoteAmounts, e.VoteNonces, e.VoteSigs, }) diff --git a/node/exts/erc20-bridge/erc20/named_extension.go b/node/exts/erc20-bridge/erc20/named_extension.go index 18cef2cad..7373d0076 100644 --- a/node/exts/erc20-bridge/erc20/named_extension.go +++ b/node/exts/erc20-bridge/erc20/named_extension.go @@ -221,7 +221,7 @@ func init() { {Name: "end_block_hash", Type: types.ByteaType, Nullable: true}, {Name: "confirmed", Type: types.BoolType}, {Name: "voters", Type: types.TextArrayType, Nullable: true}, - {Name: "vote_amounts", Type: types.TextArrayType, Nullable: true}, // cannot successful return uint256NumericArray, as empty value + {Name: "vote_amounts", Type: uint256NumericArray, Nullable: true}, {Name: "vote_nonces", Type: types.IntArrayType, Nullable: true}, {Name: "voter_signatures", Type: types.ByteaArrayType, Nullable: true}, }, @@ -247,7 +247,7 @@ func init() { {Name: "end_block_hash", Type: types.ByteaType, Nullable: true}, {Name: "confirmed", Type: types.BoolType}, {Name: "voters", Type: types.TextArrayType, Nullable: true}, - {Name: "vote_amounts", Type: types.TextArrayType, Nullable: true}, // cannot successful return uint256NumericArray, as empty value + {Name: "vote_amounts", Type: uint256NumericArray, Nullable: true}, {Name: "vote_nonces", Type: types.IntArrayType, Nullable: true}, {Name: "voter_signatures", Type: types.ByteaArrayType, Nullable: true}, }, From da362535e6232f0c395a7f538194cf8d4ba1999a Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Wed, 19 Feb 2025 14:56:04 -0600 Subject: [PATCH 18/30] fix epochConfirmed, use canVoteEpoch instead --- .../exts/erc20-bridge/erc20/meta_extension.go | 8 +++---- node/exts/erc20-bridge/erc20/meta_sql.go | 21 ++++++++----------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/node/exts/erc20-bridge/erc20/meta_extension.go b/node/exts/erc20-bridge/erc20/meta_extension.go index 02c471663..e82f43bf2 100644 --- a/node/exts/erc20-bridge/erc20/meta_extension.go +++ b/node/exts/erc20-bridge/erc20/meta_extension.go @@ -1071,13 +1071,13 @@ func init() { return err } - confirmed, err := epochConfirmed(ctx.TxContext.Ctx, app, epochID) + ok, err := canVoteEpoch(ctx.TxContext.Ctx, app, epochID) if err != nil { - return fmt.Errorf("check epoch is confirmed: %w", err) + return fmt.Errorf("check epoch can vote: %w", err) } - if confirmed { - return fmt.Errorf("epoch is already confirmed") + if !ok { + return fmt.Errorf("epoch cannot be voted") } return voteEpoch(ctx.TxContext.Ctx, app, epochID, from, amount, nonce, signature) diff --git a/node/exts/erc20-bridge/erc20/meta_sql.go b/node/exts/erc20-bridge/erc20/meta_sql.go index 404f8a292..c5e4d02ee 100644 --- a/node/exts/erc20-bridge/erc20/meta_sql.go +++ b/node/exts/erc20-bridge/erc20/meta_sql.go @@ -360,12 +360,8 @@ func getRewardsForEpoch(ctx context.Context, app *common.App, epochID *types.UUI } // previousEpochConfirmed return whether previous exists and confirmed. -func previousEpochConfirmed(ctx context.Context, app *common.App, instanceID *types.UUID, endBlock int64) (bool, bool, error) { - // if no previous epoch - exist := false - confirmed := false - - err := app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` +func previousEpochConfirmed(ctx context.Context, app *common.App, instanceID *types.UUID, endBlock int64) (exist bool, confirmed bool, err error) { + err = app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` {kwil_erc20_meta}SELECT confirmed from epochs WHERE instance_id = $instance_id AND ended_at = $end_block `, map[string]any{ @@ -580,11 +576,12 @@ func setVersionToCurrent(ctx context.Context, app *common.App) error { }, nil) } -func epochConfirmed(ctx context.Context, app *common.App, epochID *types.UUID) (bool, error) { - var confirmed bool - err := app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` +// canVoteEpoch returns a bool indicate whether an epoch can be voted. +func canVoteEpoch(ctx context.Context, app *common.App, epochID *types.UUID) (ok bool, err error) { + // get epoch that is finalized, but not confirmed. + err = app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` {kwil_erc20_meta}SELECT confirmed - FROM epochs WHERE id = $id; + FROM epochs WHERE id = $id AND ended_at IS NOT NULL AND confirmed IS NOT true; `, map[string]any{ "id": epochID, }, func(row *common.Row) error { @@ -592,7 +589,7 @@ func epochConfirmed(ctx context.Context, app *common.App, epochID *types.UUID) ( return fmt.Errorf("expected 1 value, got %d", len(row.Values)) } - confirmed = row.Values[0].(bool) + ok = true return nil }) @@ -600,7 +597,7 @@ func epochConfirmed(ctx context.Context, app *common.App, epochID *types.UUID) ( return false, err } - return confirmed, nil + return ok, nil } // voteEpoch vote an epoch by submitting signature. From fba0ec09b3473f6e2700774ffb13ad546d03eaf9 Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Wed, 19 Feb 2025 18:06:57 -0600 Subject: [PATCH 19/30] change erc20-bridge confiog --- app/node/build.go | 17 ++--- app/node/node.go | 2 +- config/config.go | 70 +++++++----------- node/exts/erc20-bridge/signersvc/signer.go | 86 +++++++++++++++------- node/exts/evm-sync/evm_state.go | 2 +- node/exts/evm-sync/instance.go | 2 +- node/exts/evm-sync/listener.go | 64 +++++++--------- 7 files changed, 122 insertions(+), 121 deletions(-) diff --git a/app/node/build.go b/app/node/build.go index fbec88ae7..2cb9977c5 100644 --- a/app/node/build.go +++ b/app/node/build.go @@ -96,7 +96,7 @@ func buildServer(ctx context.Context, d *coreDependencies) *server { // Consensus ce := buildConsensusEngine(ctx, d, db, mp, bs, bp) - // Erc20 reward signer service + // Erc20 bridge signer service erc20RWSignerMgr := buildErc20RWignerMgr(d) // Node @@ -509,13 +509,10 @@ func buildConsensusEngine(_ context.Context, d *coreDependencies, db *pg.DB, } func buildErc20RWignerMgr(d *coreDependencies) *signersvc.ServiceMgr { - cfg := d.cfg.Erc20BridgeSigner - if !cfg.Enable { - return nil - } + cfg := d.cfg.Erc20Bridge if err := cfg.Validate(); err != nil { - failBuild(err, "invalid erc20 reward signer config") + failBuild(err, "invalid erc20 bridge config") } // create shared state @@ -524,21 +521,21 @@ func buildErc20RWignerMgr(d *coreDependencies) *signersvc.ServiceMgr { if !fileExists(stateFile) { emptyFile, err := os.Create(stateFile) if err != nil { - failBuild(err, "Failed to create erc20 reward signer state file") + failBuild(err, "Failed to create erc20 bridge signer state file") } _ = emptyFile.Close() } state, err := signersvc.LoadStateFromFile(stateFile) if err != nil { - failBuild(err, "Failed to load erc20 reward signer state file") + failBuild(err, "Failed to load erc20 bridge signer state file") } rpcUrl := "http://" + d.cfg.RPC.ListenAddress - mgr, err := signersvc.NewServiceMgr(rpcUrl, cfg.Targets, cfg.EthRpcs, cfg.PrivateKeys, time.Duration(cfg.SyncEvery), state, d.logger.New("EVMRW")) + mgr, err := signersvc.NewServiceMgr(rpcUrl, cfg, state, d.logger.New("EVMRW")) if err != nil { - failBuild(err, "Failed to create erc20 reward signer service manager") + failBuild(err, "Failed to create erc20 bridge signer service manager") } return mgr diff --git a/app/node/node.go b/app/node/node.go index 0acad46e0..a46fe56fd 100644 --- a/app/node/node.go +++ b/app/node/node.go @@ -261,7 +261,7 @@ func (s *server) Start(ctx context.Context) error { }) s.log.Info("listener manager started") - // Start erc20 reward signer svc + // Start erc20 bridge signer svc if s.erc20RWSigner != nil { group.Go(func() error { return s.erc20RWSigner.Start(groupCtx) diff --git a/config/config.go b/config/config.go index 9f173b6aa..ced75f21a 100644 --- a/config/config.go +++ b/config/config.go @@ -311,12 +311,10 @@ func DefaultConfig() *Config { Height: 0, Hash: types.Hash{}, }, - Erc20BridgeSigner: ERC20BridgeSignerConfig{ - Enable: false, - PrivateKeys: nil, - Targets: nil, - // the reasonable value is the block time - SyncEvery: types.Duration(1 * time.Minute), + Erc20Bridge: ERC20BridgeConfig{ + RPC: make(map[string]string), + BlockSyncChuckSize: make(map[string]string), + Signer: make(map[string]string), }, } } @@ -330,19 +328,19 @@ type Config struct { ProfileMode string `toml:"profile_mode,commented" comment:"profile mode (http, cpu, mem, mutex, or block)"` ProfileFile string `toml:"profile_file,commented" comment:"profile output file path (e.g. cpu.pprof)"` - P2P PeerConfig `toml:"p2p" comment:"P2P related configuration"` - Consensus ConsensusConfig `toml:"consensus" comment:"Consensus related configuration"` - DB DBConfig `toml:"db" comment:"DB (PostgreSQL) related configuration"` - Store StoreConfig `toml:"store" comment:"Block store configuration"` - RPC RPCConfig `toml:"rpc" comment:"User RPC service configuration"` - Admin AdminConfig `toml:"admin" comment:"Admin RPC service configuration"` - Snapshots SnapshotConfig `toml:"snapshots" comment:"Snapshot creation and provider configuration"` - StateSync StateSyncConfig `toml:"state_sync" comment:"Statesync configuration (vs block sync)"` - Extensions map[string]map[string]string `toml:"extensions" comment:"extension configuration"` - GenesisState string `toml:"genesis_state" comment:"path to the genesis state file, relative to the root directory"` - Migrations MigrationConfig `toml:"migrations" comment:"zero downtime migration configuration"` - Checkpoint Checkpoint `toml:"checkpoint" comment:"checkpoint info for the leader to sync to before proposing a new block"` - Erc20BridgeSigner ERC20BridgeSignerConfig `toml:"erc20_bridge_signer" comment:"ERC20 bridge signer service configuration"` + P2P PeerConfig `toml:"p2p" comment:"P2P related configuration"` + Consensus ConsensusConfig `toml:"consensus" comment:"Consensus related configuration"` + DB DBConfig `toml:"db" comment:"DB (PostgreSQL) related configuration"` + Store StoreConfig `toml:"store" comment:"Block store configuration"` + RPC RPCConfig `toml:"rpc" comment:"User RPC service configuration"` + Admin AdminConfig `toml:"admin" comment:"Admin RPC service configuration"` + Snapshots SnapshotConfig `toml:"snapshots" comment:"Snapshot creation and provider configuration"` + StateSync StateSyncConfig `toml:"state_sync" comment:"Statesync configuration (vs block sync)"` + Extensions map[string]map[string]string `toml:"extensions" comment:"extension configuration"` + GenesisState string `toml:"genesis_state" comment:"path to the genesis state file, relative to the root directory"` + Migrations MigrationConfig `toml:"migrations" comment:"zero downtime migration configuration"` + Checkpoint Checkpoint `toml:"checkpoint" comment:"checkpoint info for the leader to sync to before proposing a new block"` + Erc20Bridge ERC20BridgeConfig `toml:"erc20_bridge" comment:"ERC20 bridge configuration"` } // PeerConfig corresponds to the [p2p] section of the config. @@ -451,34 +449,16 @@ type Checkpoint struct { Hash types.Hash `toml:"hash" comment:"checkpoint block hash."` } -type ERC20BridgeSignerConfig struct { - Enable bool `toml:"enable" comment:"enable the ERC20 bridge signer service"` - Targets []string `toml:"targets" comment:"target reward ext alias for the ERC20 reward"` - PrivateKeys []string `toml:"private_keys" comment:"private key for the ERC20 reward target"` - EthRpcs []string `toml:"eth_rpcs" comment:"eth rpc address for the ERC20 reward target"` - SyncEvery types.Duration `toml:"sync_every" comment:"sync interval; a recommend value is same as the block time"` +type ERC20BridgeConfig struct { + RPC map[string]string `toml:"rpc" comment:"evm RPC; format: chain_name='rpc_url'"` + BlockSyncChuckSize map[string]string `toml:"block_sync_chuck_size" comment:"block sync chunk size; format: chain_name='chunk_size'"` + Signer map[string]string `toml:"signer" comment:"signer service configuration; format: chain_name='target:file_path_to_private_key'"` } -func (cfg ERC20BridgeSignerConfig) Validate() error { - if (len(cfg.PrivateKeys) != len(cfg.Targets)) && (len(cfg.EthRpcs) != len(cfg.Targets)) { - return fmt.Errorf("private keys and targets and eth_rpcs must be configured in triples") - } - - if len(cfg.Targets) == 0 { - return fmt.Errorf("no target configured") - } - - for i, target := range cfg.Targets { - if target == "" { - return fmt.Errorf("target %dth is empty", i) - } - - if cfg.PrivateKeys[i] == "" { - return fmt.Errorf("private key %dth is empty", i) - } - - if cfg.EthRpcs[i] == "" { - return fmt.Errorf("eth rpc %dth is empty", i) +func (cfg ERC20BridgeConfig) Validate() error { + for chain, _ := range cfg.Signer { + if _, ok := cfg.RPC[chain]; !ok { + return fmt.Errorf("signer service: chain '%s' is not in rpc", chain) } } diff --git a/node/exts/erc20-bridge/signersvc/signer.go b/node/exts/erc20-bridge/signersvc/signer.go index c6b8c42be..be4d9a7d9 100644 --- a/node/exts/erc20-bridge/signersvc/signer.go +++ b/node/exts/erc20-bridge/signersvc/signer.go @@ -10,8 +10,10 @@ import ( "encoding/hex" "fmt" "math/big" + "os" "path/filepath" "slices" + "strings" "sync" "time" @@ -19,6 +21,7 @@ import ( ethCommon "github.com/ethereum/go-ethereum/common" ethCrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/kwilteam/kwil-db/config" "github.com/kwilteam/kwil-db/core/client" clientType "github.com/kwilteam/kwil-db/core/client/types" "github.com/kwilteam/kwil-db/core/crypto" @@ -49,13 +52,12 @@ type rewardSigner struct { safe *Safe logger log.Logger - every time.Duration state *State } // newRewardSigner returns a new rewardSigner. func newRewardSigner(kwilRpc string, target string, ethRpc string, pkStr string, - every time.Duration, state *State, logger log.Logger) (*rewardSigner, error) { + state *State, logger log.Logger) (*rewardSigner, error) { if logger == nil { logger = log.DiscardLogger } @@ -79,7 +81,6 @@ func newRewardSigner(kwilRpc string, target string, ethRpc string, pkStr string, signerAddr: address, state: state, logger: logger, - every: every, target: target, }, nil } @@ -89,19 +90,19 @@ func (s *rewardSigner) init() error { pkBytes, err := hex.DecodeString(s.signerPkStr) if err != nil { - return fmt.Errorf("decode erc20 reward signer private key failed: %w", err) + return fmt.Errorf("decode erc20 bridge signer private key failed: %w", err) } key, err := crypto.UnmarshalSecp256k1PrivateKey(pkBytes) if err != nil { - return fmt.Errorf("parse erc20 reward signer private key failed: %w", err) + return fmt.Errorf("parse erc20 bridge signer private key failed: %w", err) } opts := &clientType.Options{Signer: &auth.EthPersonalSigner{Key: *key}} clt, err := client.NewClient(ctx, s.kwilRpc, opts) if err != nil { - return fmt.Errorf("create erc20 reward signer api client failed: %w", err) + return fmt.Errorf("create erc20 bridge signer api client failed: %w", err) } s.kwil = newERC20RWExtAPI(clt, s.target) @@ -318,40 +319,71 @@ func (s *rewardSigner) sync(ctx context.Context) { // ServiceMgr manages multiple rewardSigner instances running in parallel. type ServiceMgr struct { - signers []*rewardSigner - logger log.Logger + syncInterval time.Duration + signers []*rewardSigner + logger log.Logger } func NewServiceMgr( kwilRpc string, - targets []string, - ethRpcs []string, - pkStrs []string, - syncEvery time.Duration, + cfg config.ERC20BridgeConfig, state *State, logger log.Logger) (*ServiceMgr, error) { - signers := make([]*rewardSigner, len(targets)) - for i, target := range targets { - pk := pkStrs[i] - svc, err := newRewardSigner(kwilRpc, target, ethRpcs[i], pk, - syncEvery, state, logger.New("EVMRW."+target)) + signerCfgDelimiter := ":" + + var signers []*rewardSigner + for chain, value := range cfg.Signer { + chainRpc, ok := cfg.RPC[chain] + if !ok { + return nil, fmt.Errorf("chain %s not found in rpc config", chain) + } + + // we need http endpoint + if strings.HasPrefix(chainRpc, "wss://") { + chainRpc = strings.Replace(chainRpc, "wss://", "https://", 1) + } + if strings.HasPrefix(chainRpc, "ws") { + chainRpc = strings.Replace(chainRpc, "ws://", "http://", 1) + } + + if !strings.Contains(value, signerCfgDelimiter) { + return nil, fmt.Errorf("invalid signer config: %s", value) + } + + segs := strings.SplitN(value, signerCfgDelimiter, 2) + + target := segs[0] + pkPath := segs[1] + + if !ethCommon.FileExist(pkPath) { + return nil, fmt.Errorf("private key file %s not found", pkPath) + } + + pkBytes, err := os.ReadFile(pkPath) + if err != nil { + return nil, fmt.Errorf("read private key file %s failed: %w", pkPath, err) + } + + svc, err := newRewardSigner(kwilRpc, target, chainRpc, strings.TrimSpace(string(pkBytes)), + state, logger.New("EVMRW."+target)) if err != nil { - return nil, fmt.Errorf("create erc20 reward signer service failed: %w", err) + return nil, fmt.Errorf("create erc20 bridge signer service failed: %w", err) } - signers[i] = svc + signers = append(signers, svc) } return &ServiceMgr{ - signers: signers, - logger: logger, + signers: signers, + logger: logger, + syncInterval: time.Minute, // default to 1m }, nil } // Start runs all rewardSigners. It returns error if there are issues initializing the rewardSigner; // no errors are returned after the rewardSigner is running. -func (s *ServiceMgr) Start(ctx context.Context) error { +func (m *ServiceMgr) Start(ctx context.Context) error { // since we need to wait on RPC running, we move the initialization logic into `init` // To be able to run with docker, we need to apply a retry logic, since a new @@ -359,7 +391,7 @@ func (s *ServiceMgr) Start(ctx context.Context) error { // erc20 instance target. for { // naive way to keep trying the init var err error - for _, s := range s.signers { + for _, s := range m.signers { err = s.init() if err != nil { break @@ -372,18 +404,18 @@ func (s *ServiceMgr) Start(ctx context.Context) error { // if any error happens in init, we try again time.Sleep(time.Second * 5) - s.logger.Warn("failed to initialize erc20 reward signer, will retry", "error", err.Error()) + m.logger.Warn("failed to initialize erc20 bridge signer, will retry", "error", err.Error()) } wg := &sync.WaitGroup{} - for _, s := range s.signers { + for _, s := range m.signers { wg.Add(1) go func() { defer wg.Done() s.logger.Info("start watching erc20 reward epoches") - tick := time.NewTicker(s.every) + tick := time.NewTicker(m.syncInterval) for { s.sync(ctx) @@ -401,7 +433,7 @@ func (s *ServiceMgr) Start(ctx context.Context) error { <-ctx.Done() wg.Wait() - s.logger.Infof("Erc20 reward signer service shutting down...") + m.logger.Infof("Erc20 bridge signer service shutting down...") return nil } diff --git a/node/exts/evm-sync/evm_state.go b/node/exts/evm-sync/evm_state.go index 125b2affa..8235a7a2d 100644 --- a/node/exts/evm-sync/evm_state.go +++ b/node/exts/evm-sync/evm_state.go @@ -211,7 +211,7 @@ func (s *statePoller) runPollFuncs(ctx context.Context, service *common.Service, } func makeNewClient(ctx context.Context, service *common.Service, chain chains.Chain) (*ethclient.Client, error) { - chainConf, err := getChainConf(service.LocalConfig.Extensions, chain) + chainConf, err := getChainConf(service.LocalConfig.Erc20Bridge, chain) if err != nil { return nil, fmt.Errorf("failed to get chain config for %s: %v", chain, err) } diff --git a/node/exts/evm-sync/instance.go b/node/exts/evm-sync/instance.go index d92c9de82..c92c89b85 100644 --- a/node/exts/evm-sync/instance.go +++ b/node/exts/evm-sync/instance.go @@ -205,7 +205,7 @@ type listenerInfo struct { func (l *listenerInfo) listen(ctx context.Context, service *common.Service, eventstore listeners.EventStore, syncConf *syncConfig) { logger := service.Logger.New(l.uniqueName + "." + string(l.chain.Name)) - chainConf, err := getChainConf(service.LocalConfig.Extensions, l.chain.Name) + chainConf, err := getChainConf(service.LocalConfig.Erc20Bridge, l.chain.Name) if err != nil { logger.Error("failed to get chain config", "err", err) return diff --git a/node/exts/evm-sync/listener.go b/node/exts/evm-sync/listener.go index c453d96ec..d28a3f2eb 100644 --- a/node/exts/evm-sync/listener.go +++ b/node/exts/evm-sync/listener.go @@ -6,9 +6,11 @@ import ( "encoding/binary" "errors" "fmt" + "github.com/kwilteam/kwil-db/config" "io" "sort" "strconv" + "strings" "time" ethcommon "github.com/ethereum/go-ethereum/common" @@ -104,31 +106,48 @@ func (c *syncConfig) load(m map[string]string) error { } // getChainConf gets the chain config from the node's local configuration. -func getChainConf(m map[string]map[string]string, chain chains.Chain) (*chainConfig, error) { - var m2 map[string]string +func getChainConf(cfg config.ERC20BridgeConfig, chain chains.Chain) (*chainConfig, error) { var ok bool + var provider string + var syncChunk string switch chain { - case chains.Ethereum: - m2, ok = m["ethereum_sync"] + case chains.Ethereum, chains.Sepolia: + provider, ok = cfg.RPC[chain.String()] if !ok { return nil, errors.New("local configuration does not have an ethereum_sync config") } - case chains.Sepolia: - m2, ok = m["sepolia_sync"] + + // we need websocket endpoint + if strings.HasPrefix(provider, "https://") { + provider = strings.Replace(provider, "https://", "wss://", 1) + } + if strings.HasPrefix(provider, "http://") { + provider = strings.Replace(provider, "http://", "ws://", 1) + } + + syncChunk, ok = cfg.BlockSyncChuckSize[chains.Ethereum.String()] if !ok { - return nil, errors.New("local configuration does not have a sepolia_sync config") + syncChunk = "1000000" } default: // suggests an internal bug where we have not added a case for a new chain return nil, fmt.Errorf("unknown chain %s", chain) } - conf := &chainConfig{} - err := conf.load(m2) + blockSyncChunkSize, err := strconv.ParseInt(syncChunk, 10, 64) if err != nil { return nil, err } + if blockSyncChunkSize <= 0 { + return nil, errors.New("block_sync_chunk_size must be greater than 0") + } + + conf := &chainConfig{ + BlockSyncChunkSize: blockSyncChunkSize, + Provider: provider, + } + return conf, nil } @@ -143,33 +162,6 @@ type chainConfig struct { Provider string } -// load loads the config into the struct from the node's local configuration -func (c *chainConfig) load(m map[string]string) error { - if v, ok := m["block_sync_chunk_size"]; ok { - i, err := strconv.ParseInt(v, 10, 64) - if err != nil { - return err - } - - if i <= 0 { - return errors.New("block_sync_chunk_size must be greater than 0") - } - - c.BlockSyncChunkSize = i - } else { - c.BlockSyncChunkSize = 1000000 - } - - v, ok := m["provider"] - if !ok { - return errors.New("provider is required") - } - - c.Provider = v - - return nil -} - // individualListener is a singler configured client that is responsible for listening to a single set of contracts. // Many individual listeners can exist for a single chain. type individualListener struct { From b0ee0506ffce914d5e24885ef15052b0ed3fdf54 Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Wed, 19 Feb 2025 18:13:26 -0600 Subject: [PATCH 20/30] fix lint --- config/config.go | 2 +- node/exts/erc20-bridge/signersvc/multicall.go | 2 +- node/exts/evm-sync/listener.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/config.go b/config/config.go index ced75f21a..028d3c011 100644 --- a/config/config.go +++ b/config/config.go @@ -456,7 +456,7 @@ type ERC20BridgeConfig struct { } func (cfg ERC20BridgeConfig) Validate() error { - for chain, _ := range cfg.Signer { + for chain := range cfg.Signer { if _, ok := cfg.RPC[chain]; !ok { return fmt.Errorf("signer service: chain '%s' is not in rpc", chain) } diff --git a/node/exts/erc20-bridge/signersvc/multicall.go b/node/exts/erc20-bridge/signersvc/multicall.go index ea3d5d816..a4ebe2365 100644 --- a/node/exts/erc20-bridge/signersvc/multicall.go +++ b/node/exts/erc20-bridge/signersvc/multicall.go @@ -8,7 +8,7 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" - + "github.com/kwilteam/kwil-db/node/exts/erc20-bridge/abigen" ) diff --git a/node/exts/evm-sync/listener.go b/node/exts/evm-sync/listener.go index d28a3f2eb..99a6aae6f 100644 --- a/node/exts/evm-sync/listener.go +++ b/node/exts/evm-sync/listener.go @@ -6,7 +6,6 @@ import ( "encoding/binary" "errors" "fmt" - "github.com/kwilteam/kwil-db/config" "io" "sort" "strconv" @@ -18,6 +17,7 @@ import ( "github.com/ethereum/go-ethereum/ethclient" "github.com/kwilteam/kwil-db/common" + "github.com/kwilteam/kwil-db/config" "github.com/kwilteam/kwil-db/core/log" "github.com/kwilteam/kwil-db/extensions/listeners" "github.com/kwilteam/kwil-db/node/exts/evm-sync/chains" From 7a97e74314fdcf27213bb6a766e288eef4417699 Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Wed, 19 Feb 2025 18:25:17 -0600 Subject: [PATCH 21/30] force using websocket rpc provider --- node/exts/erc20-bridge/signersvc/signer.go | 9 +++------ node/exts/evm-sync/listener.go | 7 ++----- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/node/exts/erc20-bridge/signersvc/signer.go b/node/exts/erc20-bridge/signersvc/signer.go index be4d9a7d9..763d987cd 100644 --- a/node/exts/erc20-bridge/signersvc/signer.go +++ b/node/exts/erc20-bridge/signersvc/signer.go @@ -339,12 +339,9 @@ func NewServiceMgr( return nil, fmt.Errorf("chain %s not found in rpc config", chain) } - // we need http endpoint - if strings.HasPrefix(chainRpc, "wss://") { - chainRpc = strings.Replace(chainRpc, "wss://", "https://", 1) - } - if strings.HasPrefix(chainRpc, "ws") { - chainRpc = strings.Replace(chainRpc, "ws://", "http://", 1) + // we need websocket endpoint + if !strings.HasPrefix(chainRpc, "wss://") && !strings.HasPrefix(chainRpc, "ws://") { + return nil, fmt.Errorf("chain %s rpc must start with wss:// or ws://", chain) } if !strings.Contains(value, signerCfgDelimiter) { diff --git a/node/exts/evm-sync/listener.go b/node/exts/evm-sync/listener.go index 99a6aae6f..cace115c6 100644 --- a/node/exts/evm-sync/listener.go +++ b/node/exts/evm-sync/listener.go @@ -118,11 +118,8 @@ func getChainConf(cfg config.ERC20BridgeConfig, chain chains.Chain) (*chainConfi } // we need websocket endpoint - if strings.HasPrefix(provider, "https://") { - provider = strings.Replace(provider, "https://", "wss://", 1) - } - if strings.HasPrefix(provider, "http://") { - provider = strings.Replace(provider, "http://", "ws://", 1) + if !strings.HasPrefix(provider, "wss://") && !strings.HasPrefix(provider, "ws://") { + return nil, fmt.Errorf("chain %s rpc must start with wss:// or ws://", chain) } syncChunk, ok = cfg.BlockSyncChuckSize[chains.Ethereum.String()] From eb9af226b1b3a5bb1ad5e4a95fbfa225430bc147 Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Wed, 19 Feb 2025 21:58:17 -0600 Subject: [PATCH 22/30] fix erc20_bridge config --- app/node/build.go | 8 +-- config/config.go | 30 ++++++--- node/exts/erc20-bridge/signersvc/signer.go | 77 ++++++++++++++-------- node/exts/evm-sync/listener.go | 13 ++-- 4 files changed, 77 insertions(+), 51 deletions(-) diff --git a/app/node/build.go b/app/node/build.go index 2cb9977c5..eb049a854 100644 --- a/app/node/build.go +++ b/app/node/build.go @@ -509,12 +509,6 @@ func buildConsensusEngine(_ context.Context, d *coreDependencies, db *pg.DB, } func buildErc20RWignerMgr(d *coreDependencies) *signersvc.ServiceMgr { - cfg := d.cfg.Erc20Bridge - - if err := cfg.Validate(); err != nil { - failBuild(err, "invalid erc20 bridge config") - } - // create shared state stateFile := signersvc.StateFilePath(d.rootDir) @@ -533,7 +527,7 @@ func buildErc20RWignerMgr(d *coreDependencies) *signersvc.ServiceMgr { rpcUrl := "http://" + d.cfg.RPC.ListenAddress - mgr, err := signersvc.NewServiceMgr(rpcUrl, cfg, state, d.logger.New("EVMRW")) + mgr, err := signersvc.NewServiceMgr(rpcUrl, d.cfg.Erc20Bridge, state, d.logger.New("EVMRW")) if err != nil { failBuild(err, "Failed to create erc20 bridge signer service manager") } diff --git a/config/config.go b/config/config.go index 028d3c011..d77fd06fc 100644 --- a/config/config.go +++ b/config/config.go @@ -13,12 +13,13 @@ import ( "strings" "time" + "github.com/pelletier/go-toml/v2" + "github.com/kwilteam/kwil-db/core/crypto" "github.com/kwilteam/kwil-db/core/crypto/auth" "github.com/kwilteam/kwil-db/core/log" "github.com/kwilteam/kwil-db/core/types" - - "github.com/pelletier/go-toml/v2" + "github.com/kwilteam/kwil-db/node/exts/evm-sync/chains" ) var ( @@ -450,15 +451,24 @@ type Checkpoint struct { } type ERC20BridgeConfig struct { - RPC map[string]string `toml:"rpc" comment:"evm RPC; format: chain_name='rpc_url'"` - BlockSyncChuckSize map[string]string `toml:"block_sync_chuck_size" comment:"block sync chunk size; format: chain_name='chunk_size'"` - Signer map[string]string `toml:"signer" comment:"signer service configuration; format: chain_name='target:file_path_to_private_key'"` -} + RPC map[string]string `toml:"rpc" comment:"evm websocket RPC; format: chain_name='rpc_url'"` + BlockSyncChuckSize map[string]string `toml:"block_sync_chuck_size" comment:"rpc option block sync chunk size; format: chain_name='chunk_size'"` + Signer map[string]string `toml:"signer" comment:"signer service configuration; format: target='chain_name:file_path_to_private_key'"` +} + +// ValidateRpc validates the bridge rpc config, other validations will be performed +// when correspond components derive config from it. +// BlockSyncChuckSize config will be validated by evm-sync listener. +// Signer config will be validated by erc20 signerSvc. +func (cfg ERC20BridgeConfig) ValidateRpc() error { + for chain, rpc := range cfg.RPC { + if err := chains.Chain(chain).Valid(); err != nil { + return fmt.Errorf("erc20_bridge.rpc: %s", chain) + } -func (cfg ERC20BridgeConfig) Validate() error { - for chain := range cfg.Signer { - if _, ok := cfg.RPC[chain]; !ok { - return fmt.Errorf("signer service: chain '%s' is not in rpc", chain) + // enforce websocket + if !strings.HasPrefix(rpc, "wss://") && !strings.HasPrefix(rpc, "ws://") { + return fmt.Errorf("erc20_bridge.rpc: must start with wss:// or ws://") } } diff --git a/node/exts/erc20-bridge/signersvc/signer.go b/node/exts/erc20-bridge/signersvc/signer.go index 763d987cd..b1ee8410f 100644 --- a/node/exts/erc20-bridge/signersvc/signer.go +++ b/node/exts/erc20-bridge/signersvc/signer.go @@ -317,53 +317,76 @@ func (s *rewardSigner) sync(ctx context.Context) { s.lastVoteBlock = finalizedEpoch.EndHeight // update after all operations succeed } -// ServiceMgr manages multiple rewardSigner instances running in parallel. -type ServiceMgr struct { - syncInterval time.Duration - signers []*rewardSigner - logger log.Logger +type signerConfig struct { + target string // erc20 bridge target name + chainRPC string + privateKeyPath string // file path to private key } -func NewServiceMgr( - kwilRpc string, - cfg config.ERC20BridgeConfig, - state *State, - logger log.Logger) (*ServiceMgr, error) { +// GetSignerCfgs verifies config and returns a list of config for erc20 signerSvc. +func getSignerCfgs(cfg config.ERC20BridgeConfig) ([]signerConfig, error) { + if err := cfg.ValidateRpc(); err != nil { + return nil, err + } signerCfgDelimiter := ":" - var signers []*rewardSigner - for chain, value := range cfg.Signer { - chainRpc, ok := cfg.RPC[chain] - if !ok { - return nil, fmt.Errorf("chain %s not found in rpc config", chain) - } - - // we need websocket endpoint - if !strings.HasPrefix(chainRpc, "wss://") && !strings.HasPrefix(chainRpc, "ws://") { - return nil, fmt.Errorf("chain %s rpc must start with wss:// or ws://", chain) - } + var signerCfg []signerConfig + for target, value := range cfg.Signer { if !strings.Contains(value, signerCfgDelimiter) { return nil, fmt.Errorf("invalid signer config: %s", value) } segs := strings.SplitN(value, signerCfgDelimiter, 2) - - target := segs[0] + chain := segs[0] pkPath := segs[1] + chainRpc, ok := cfg.RPC[chain] + if !ok { + return nil, fmt.Errorf("chain '%s' not found in erc20_bridge.rpc config", chain) + } + if !ethCommon.FileExist(pkPath) { return nil, fmt.Errorf("private key file %s not found", pkPath) } - pkBytes, err := os.ReadFile(pkPath) + signerCfg = append(signerCfg, signerConfig{ + target: target, + chainRPC: chainRpc, + privateKeyPath: pkPath, + }) + } + + return signerCfg, nil +} + +// ServiceMgr manages multiple rewardSigner instances running in parallel. +type ServiceMgr struct { + syncInterval time.Duration + signers []*rewardSigner + logger log.Logger +} + +func NewServiceMgr( + kwilRpc string, + cfg config.ERC20BridgeConfig, + state *State, + logger log.Logger) (*ServiceMgr, error) { + signerCfgs, err := getSignerCfgs(cfg) + if err != nil { + return nil, fmt.Errorf("get erc20 bridge signer config failed: %w", err) + } + + var signers []*rewardSigner + for _, cfg := range signerCfgs { + pkBytes, err := os.ReadFile(cfg.privateKeyPath) if err != nil { - return nil, fmt.Errorf("read private key file %s failed: %w", pkPath, err) + return nil, fmt.Errorf("read private key file %s failed: %w", cfg.privateKeyPath, err) } - svc, err := newRewardSigner(kwilRpc, target, chainRpc, strings.TrimSpace(string(pkBytes)), - state, logger.New("EVMRW."+target)) + svc, err := newRewardSigner(kwilRpc, cfg.target, cfg.chainRPC, strings.TrimSpace(string(pkBytes)), + state, logger.New("EVMRW."+cfg.target)) if err != nil { return nil, fmt.Errorf("create erc20 bridge signer service failed: %w", err) } diff --git a/node/exts/evm-sync/listener.go b/node/exts/evm-sync/listener.go index cace115c6..4e6e4b9ef 100644 --- a/node/exts/evm-sync/listener.go +++ b/node/exts/evm-sync/listener.go @@ -9,7 +9,6 @@ import ( "io" "sort" "strconv" - "strings" "time" ethcommon "github.com/ethereum/go-ethereum/common" @@ -107,6 +106,11 @@ func (c *syncConfig) load(m map[string]string) error { // getChainConf gets the chain config from the node's local configuration. func getChainConf(cfg config.ERC20BridgeConfig, chain chains.Chain) (*chainConfig, error) { + err := cfg.ValidateRpc() + if err != nil { + return nil, err + } + var ok bool var provider string var syncChunk string @@ -114,12 +118,7 @@ func getChainConf(cfg config.ERC20BridgeConfig, chain chains.Chain) (*chainConfi case chains.Ethereum, chains.Sepolia: provider, ok = cfg.RPC[chain.String()] if !ok { - return nil, errors.New("local configuration does not have an ethereum_sync config") - } - - // we need websocket endpoint - if !strings.HasPrefix(provider, "wss://") && !strings.HasPrefix(provider, "ws://") { - return nil, fmt.Errorf("chain %s rpc must start with wss:// or ws://", chain) + return nil, fmt.Errorf("local configuration does not have an '%s' config", chain.String()) } syncChunk, ok = cfg.BlockSyncChuckSize[chains.Ethereum.String()] From cd91dc416aca10ac84543507f2c3a09ca4ed4f91 Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Thu, 20 Feb 2025 12:20:25 -0600 Subject: [PATCH 23/30] fix erc20 bridge signer config --- config/config.go | 15 +- node/exts/erc20-bridge/signersvc/signer.go | 253 +++++++++------------ node/exts/evm-sync/listener.go | 2 +- 3 files changed, 124 insertions(+), 146 deletions(-) diff --git a/config/config.go b/config/config.go index d77fd06fc..dafd78de8 100644 --- a/config/config.go +++ b/config/config.go @@ -15,6 +15,7 @@ import ( "github.com/pelletier/go-toml/v2" + ethCommon "github.com/ethereum/go-ethereum/common" "github.com/kwilteam/kwil-db/core/crypto" "github.com/kwilteam/kwil-db/core/crypto/auth" "github.com/kwilteam/kwil-db/core/log" @@ -453,16 +454,16 @@ type Checkpoint struct { type ERC20BridgeConfig struct { RPC map[string]string `toml:"rpc" comment:"evm websocket RPC; format: chain_name='rpc_url'"` BlockSyncChuckSize map[string]string `toml:"block_sync_chuck_size" comment:"rpc option block sync chunk size; format: chain_name='chunk_size'"` - Signer map[string]string `toml:"signer" comment:"signer service configuration; format: target='chain_name:file_path_to_private_key'"` + Signer map[string]string `toml:"signer" comment:"signer service configuration; format: ext_alias='file_path_to_private_key'"` } -// ValidateRpc validates the bridge rpc config, other validations will be performed +// Validate validates the bridge general config, other validations will be performed // when correspond components derive config from it. // BlockSyncChuckSize config will be validated by evm-sync listener. // Signer config will be validated by erc20 signerSvc. -func (cfg ERC20BridgeConfig) ValidateRpc() error { +func (cfg ERC20BridgeConfig) Validate() error { for chain, rpc := range cfg.RPC { - if err := chains.Chain(chain).Valid(); err != nil { + if err := chains.Chain(strings.ToLower(chain)).Valid(); err != nil { return fmt.Errorf("erc20_bridge.rpc: %s", chain) } @@ -472,6 +473,12 @@ func (cfg ERC20BridgeConfig) ValidateRpc() error { } } + for _, pkPath := range cfg.Signer { + if !ethCommon.FileExist(pkPath) { + return fmt.Errorf("erc20_bridge.signer: private key file %s not found", pkPath) + } + } + return nil } diff --git a/node/exts/erc20-bridge/signersvc/signer.go b/node/exts/erc20-bridge/signersvc/signer.go index b1ee8410f..871ae4158 100644 --- a/node/exts/erc20-bridge/signersvc/signer.go +++ b/node/exts/erc20-bridge/signersvc/signer.go @@ -39,104 +39,45 @@ func StateFilePath(dir string) string { // rewardSigner handles one registered erc20 reward instance. type rewardSigner struct { - kwilRpc string target string kwil erc20ExtAPI lastVoteBlock int64 escrowAddr ethCommon.Address - ethRpc string - signerPkStr string - signerPk *ecdsa.PrivateKey - signerAddr ethCommon.Address - safe *Safe + signerPk *ecdsa.PrivateKey + signerAddr ethCommon.Address + safe *Safe logger log.Logger state *State } // newRewardSigner returns a new rewardSigner. -func newRewardSigner(kwilRpc string, target string, ethRpc string, pkStr string, - state *State, logger log.Logger) (*rewardSigner, error) { +func newRewardSigner(kwil erc20ExtAPI, safe *Safe, target string, signerPk *ecdsa.PrivateKey, signerAddr ethCommon.Address, escrowAddr ethCommon.Address, state *State, logger log.Logger) (*rewardSigner, error) { if logger == nil { logger = log.DiscardLogger } - privateKey, err := ethCrypto.HexToECDSA(pkStr) - if err != nil { - return nil, err - } - - // Get the public key - publicKey := privateKey.Public().(*ecdsa.PublicKey) - - // Get the Ethereum address from the public key - address := ethCrypto.PubkeyToAddress(*publicKey) - - return &rewardSigner{ - kwilRpc: kwilRpc, - ethRpc: ethRpc, - signerPkStr: pkStr, - signerPk: privateKey, - signerAddr: address, - state: state, - logger: logger, - target: target, - }, nil -} - -func (s *rewardSigner) init() error { - ctx := context.Background() - - pkBytes, err := hex.DecodeString(s.signerPkStr) - if err != nil { - return fmt.Errorf("decode erc20 bridge signer private key failed: %w", err) - } - - key, err := crypto.UnmarshalSecp256k1PrivateKey(pkBytes) - if err != nil { - return fmt.Errorf("parse erc20 bridge signer private key failed: %w", err) - } - - opts := &clientType.Options{Signer: &auth.EthPersonalSigner{Key: *key}} - - clt, err := client.NewClient(ctx, s.kwilRpc, opts) - if err != nil { - return fmt.Errorf("create erc20 bridge signer api client failed: %w", err) - } - - s.kwil = newERC20RWExtAPI(clt, s.target) - - info, err := s.kwil.InstanceInfo(ctx) - if err != nil { - return fmt.Errorf("get reward metadata failed: %w", err) - } - - s.safe, err = NewSafeFromEscrow(s.ethRpc, info.Escrow) - if err != nil { - return fmt.Errorf("create safe failed: %w", err) - } - - chainInfo, ok := chains.GetChainInfo(chains.Chain(info.Chain)) - if !ok { - return fmt.Errorf("chainID %s not supported", s.safe.chainID.String()) - } - - if s.safe.chainID.String() != chainInfo.ID { - return fmt.Errorf("chainID mismatch: configured %s != target %s", s.safe.chainID.String(), chainInfo.ID) - } - - s.escrowAddr = ethCommon.HexToAddress(info.Escrow) - // overwrite configured lastVoteBlock with the value from state if exist - lastVote := s.state.LastVote(s.target) + lastVoteBlock := int64(0) + lastVote := state.LastVote(target) if lastVote != nil { - s.lastVoteBlock = lastVote.BlockHeight + lastVoteBlock = lastVote.BlockHeight } - s.logger.Info("will sync after last vote epoch", "height", s.lastVoteBlock) + logger.Info("will sync after last vote epoch", "height", lastVoteBlock) - return nil + return &rewardSigner{ + kwil: kwil, + signerPk: signerPk, + signerAddr: signerAddr, + state: state, + logger: logger, + target: target, + safe: safe, + escrowAddr: escrowAddr, + lastVoteBlock: lastVoteBlock, + }, nil } // canSkip returns true if: @@ -317,54 +258,102 @@ func (s *rewardSigner) sync(ctx context.Context) { s.lastVoteBlock = finalizedEpoch.EndHeight // update after all operations succeed } -type signerConfig struct { - target string // erc20 bridge target name - chainRPC string - privateKeyPath string // file path to private key -} - -// GetSignerCfgs verifies config and returns a list of config for erc20 signerSvc. -func getSignerCfgs(cfg config.ERC20BridgeConfig) ([]signerConfig, error) { - if err := cfg.ValidateRpc(); err != nil { +// getSigners verifies config and returns a list of signerSvc. +func getSigners(kwilRpc string, cfg config.ERC20BridgeConfig, state *State, logger log.Logger) ([]*rewardSigner, error) { + if err := cfg.Validate(); err != nil { return nil, err } - signerCfgDelimiter := ":" + ctx := context.Background() - var signerCfg []signerConfig + clt, err := client.NewClient(ctx, kwilRpc, nil) + if err != nil { + return nil, fmt.Errorf("create erc20 bridge signer api client failed: %w", err) + } + + signers := make([]*rewardSigner, 0, len(cfg.Signer)) + for target, pkPath := range cfg.Signer { + // pkPath is validated already - for target, value := range cfg.Signer { - if !strings.Contains(value, signerCfgDelimiter) { - return nil, fmt.Errorf("invalid signer config: %s", value) + readOnlyAPi := newERC20RWExtAPI(clt, target) + instanceInfo, err := readOnlyAPi.InstanceInfo(ctx) + if err != nil { + return nil, fmt.Errorf("get reward metadata failed: %w", err) } - segs := strings.SplitN(value, signerCfgDelimiter, 2) - chain := segs[0] - pkPath := segs[1] + rawPkBytes, err := os.ReadFile(pkPath) + if err != nil { + return nil, fmt.Errorf("read private key file %s failed: %w", pkPath, err) + } + + pkStr := strings.TrimSpace(string(rawPkBytes)) + pkBytes, err := hex.DecodeString(pkStr) + if err != nil { + return nil, fmt.Errorf("parse erc20 bridge signer private key failed: %w", err) + } + + signerPk, err := ethCrypto.ToECDSA(pkBytes) + if err != nil { + return nil, fmt.Errorf("parse erc20 bridge signer private key failed: %w", err) + } + + // Get the public key + signerPubKey := signerPk.Public().(*ecdsa.PublicKey) + + // Get the Ethereum address from the public key + signerAddr := ethCrypto.PubkeyToAddress(*signerPubKey) + + key, err := crypto.UnmarshalSecp256k1PrivateKey(pkBytes) + if err != nil { + return nil, fmt.Errorf("parse erc20 bridge signer private key failed: %w", err) + } + + opts := &clientType.Options{Signer: &auth.EthPersonalSigner{Key: *key}} + + clt, err := client.NewClient(ctx, kwilRpc, opts) + if err != nil { + return nil, fmt.Errorf("create erc20 bridge signer api client failed: %w", err) + } + + kwil := newERC20RWExtAPI(clt, target) + + chainRpc, ok := cfg.RPC[strings.ToLower(instanceInfo.Chain)] + if !ok { + return nil, fmt.Errorf("target '%s' chain '%s' not found in erc20_bridge.rpc config", target, instanceInfo.Chain) + } + + safe, err := NewSafeFromEscrow(chainRpc, instanceInfo.Escrow) + if err != nil { + return nil, fmt.Errorf("create safe failed: %w", err) + } - chainRpc, ok := cfg.RPC[chain] + chainInfo, ok := chains.GetChainInfo(chains.Chain(instanceInfo.Chain)) if !ok { - return nil, fmt.Errorf("chain '%s' not found in erc20_bridge.rpc config", chain) + return nil, fmt.Errorf("chainID %s not supported", safe.chainID.String()) + } + + if safe.chainID.String() != chainInfo.ID { + return nil, fmt.Errorf("chainID mismatch: configured %s != target %s", safe.chainID.String(), chainInfo.ID) } - if !ethCommon.FileExist(pkPath) { - return nil, fmt.Errorf("private key file %s not found", pkPath) + // wilRpc, target, chainRpc, strings.TrimSpace(string(pkBytes)) + svc, err := newRewardSigner(kwil, safe, target, signerPk, signerAddr, ethCommon.HexToAddress(instanceInfo.Escrow), state, logger.New("EVMRW."+target)) + if err != nil { + return nil, fmt.Errorf("create erc20 bridge signer service failed: %w", err) } - signerCfg = append(signerCfg, signerConfig{ - target: target, - chainRPC: chainRpc, - privateKeyPath: pkPath, - }) + signers = append(signers, svc) } - return signerCfg, nil + return signers, nil } // ServiceMgr manages multiple rewardSigner instances running in parallel. type ServiceMgr struct { + kwilRpc string + state *State + bridgeCfg config.ERC20BridgeConfig syncInterval time.Duration - signers []*rewardSigner logger log.Logger } @@ -373,29 +362,10 @@ func NewServiceMgr( cfg config.ERC20BridgeConfig, state *State, logger log.Logger) (*ServiceMgr, error) { - signerCfgs, err := getSignerCfgs(cfg) - if err != nil { - return nil, fmt.Errorf("get erc20 bridge signer config failed: %w", err) - } - - var signers []*rewardSigner - for _, cfg := range signerCfgs { - pkBytes, err := os.ReadFile(cfg.privateKeyPath) - if err != nil { - return nil, fmt.Errorf("read private key file %s failed: %w", cfg.privateKeyPath, err) - } - - svc, err := newRewardSigner(kwilRpc, cfg.target, cfg.chainRPC, strings.TrimSpace(string(pkBytes)), - state, logger.New("EVMRW."+cfg.target)) - if err != nil { - return nil, fmt.Errorf("create erc20 bridge signer service failed: %w", err) - } - - signers = append(signers, svc) - } - return &ServiceMgr{ - signers: signers, + kwilRpc: kwilRpc, + state: state, + bridgeCfg: cfg, logger: logger, syncInterval: time.Minute, // default to 1m }, nil @@ -406,30 +376,31 @@ func NewServiceMgr( func (m *ServiceMgr) Start(ctx context.Context) error { // since we need to wait on RPC running, we move the initialization logic into `init` - // To be able to run with docker, we need to apply a retry logic, since a new - // docker instance has no erc20 instance configured, but we need to config the - // erc20 instance target. - for { // naive way to keep trying the init - var err error - for _, s := range m.signers { - err = s.init() - if err != nil { - break - } + var err error + var signers []*rewardSigner + // To be able to run with docker, we need to apply a retry logic, because kwild + // won't have erc20 instance when boot + for { // naive way to keep retrying the init + select { + case <-ctx.Done(): + m.logger.Info("stop initializing erc20 bridge signer") + return nil + default: } + signers, err = getSigners(m.kwilRpc, m.bridgeCfg, m.state, m.logger) if err == nil { break } - // if any error happens in init, we try again - time.Sleep(time.Second * 5) m.logger.Warn("failed to initialize erc20 bridge signer, will retry", "error", err.Error()) + // any error, we try again + time.Sleep(time.Second * 3) } wg := &sync.WaitGroup{} - for _, s := range m.signers { + for _, s := range signers { wg.Add(1) go func() { defer wg.Done() diff --git a/node/exts/evm-sync/listener.go b/node/exts/evm-sync/listener.go index 4e6e4b9ef..56d41bc8b 100644 --- a/node/exts/evm-sync/listener.go +++ b/node/exts/evm-sync/listener.go @@ -106,7 +106,7 @@ func (c *syncConfig) load(m map[string]string) error { // getChainConf gets the chain config from the node's local configuration. func getChainConf(cfg config.ERC20BridgeConfig, chain chains.Chain) (*chainConfig, error) { - err := cfg.ValidateRpc() + err := cfg.Validate() if err != nil { return nil, err } From 08c8eeb66da8c03cb0014c36e276136c20dd8a5f Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Thu, 20 Feb 2025 12:40:48 -0600 Subject: [PATCH 24/30] remove amount in vote table --- .../exts/erc20-bridge/erc20/meta_extension.go | 27 ++++-------- node/exts/erc20-bridge/erc20/meta_schema.sql | 1 - node/exts/erc20-bridge/erc20/meta_sql.go | 44 +++++++------------ .../erc20-bridge/erc20/named_extension.go | 3 -- node/exts/erc20-bridge/signersvc/kwil.go | 26 ++--------- node/exts/erc20-bridge/signersvc/signer.go | 19 +------- 6 files changed, 30 insertions(+), 90 deletions(-) diff --git a/node/exts/erc20-bridge/erc20/meta_extension.go b/node/exts/erc20-bridge/erc20/meta_extension.go index e82f43bf2..53662bfc9 100644 --- a/node/exts/erc20-bridge/erc20/meta_extension.go +++ b/node/exts/erc20-bridge/erc20/meta_extension.go @@ -67,7 +67,6 @@ var ( return dt }() - uint256NumericArray = types.ArrayType(uint256Numeric) // the below are used to identify different types of logs from ethereum // so that we know how to decode them @@ -945,7 +944,6 @@ func init() { {Name: "end_block_hash", Type: types.ByteaType, Nullable: true}, {Name: "confirmed", Type: types.BoolType}, {Name: "voters", Type: types.TextArrayType, Nullable: true}, - {Name: "vote_amounts", Type: uint256NumericArray, Nullable: true}, {Name: "vote_nonces", Type: types.IntArrayType, Nullable: true}, {Name: "voter_signatures", Type: types.ByteaArrayType, Nullable: true}, }, @@ -964,7 +962,6 @@ func init() { return resultFn([]any{e.ID, e.StartHeight, e.StartTime, *e.EndHeight, e.Root, e.Total, e.BlockHash, e.Confirmed, voters, - e.VoteAmounts, e.VoteNonces, e.VoteSigs, }) @@ -990,7 +987,6 @@ func init() { {Name: "end_block_hash", Type: types.ByteaType, Nullable: true}, {Name: "confirmed", Type: types.BoolType}, {Name: "voters", Type: types.TextArrayType, Nullable: true}, - {Name: "vote_amounts", Type: uint256NumericArray, Nullable: true}, {Name: "vote_nonces", Type: types.IntArrayType, Nullable: true}, {Name: "voter_signatures", Type: types.ByteaArrayType, Nullable: true}, }, @@ -1011,7 +1007,6 @@ func init() { return resultFn([]any{e.ID, e.StartHeight, e.StartTime, *e.EndHeight, e.Root, e.Total, e.BlockHash, e.Confirmed, voters, - e.VoteAmounts, e.VoteNonces, e.VoteSigs, }) @@ -1046,7 +1041,6 @@ func init() { Parameters: []precompiles.PrecompileValue{ {Name: "id", Type: types.UUIDType}, {Name: "epoch_id", Type: types.UUIDType}, - {Name: "amount", Type: uint256Numeric}, {Name: "nonce", Type: types.IntType}, {Name: "signature", Type: types.ByteaType}, }, @@ -1054,13 +1048,8 @@ func init() { Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { //id := inputs[0].(*types.UUID) epochID := inputs[1].(*types.UUID) - amount := inputs[2].(*types.Decimal) - nonce := inputs[3].(int64) - signature := inputs[4].([]byte) - - if amount.IsNegative() { - return fmt.Errorf("amount cannot be negative") - } + nonce := inputs[2].(int64) + signature := inputs[3].([]byte) if len(signature) != utils.GnosisSafeSigLength { return fmt.Errorf("signature is not 65 bytes") @@ -1071,6 +1060,9 @@ func init() { return err } + // NOTE: if we have safe address and safe nonce, we can verify the signature + // But if we only have safe address, and safeNonce from the input, then it's no point + ok, err := canVoteEpoch(ctx.TxContext.Ctx, app, epochID) if err != nil { return fmt.Errorf("check epoch can vote: %w", err) @@ -1080,7 +1072,7 @@ func init() { return fmt.Errorf("epoch cannot be voted") } - return voteEpoch(ctx.TxContext.Ctx, app, epochID, from, amount, nonce, signature) + return voteEpoch(ctx.TxContext.Ctx, app, epochID, from, nonce, signature) }, }, { @@ -1475,10 +1467,9 @@ func (p *PendingEpoch) copy() *PendingEpoch { } type EpochVoteInfo struct { - Voters []ethcommon.Address - VoteAmounts []*types.Decimal - VoteSigs [][]byte - VoteNonces []int64 + Voters []ethcommon.Address + VoteSigs [][]byte + VoteNonces []int64 } // Epoch is a period in which rewards are distributed. diff --git a/node/exts/erc20-bridge/erc20/meta_schema.sql b/node/exts/erc20-bridge/erc20/meta_schema.sql index f02464759..728d84b22 100644 --- a/node/exts/erc20-bridge/erc20/meta_schema.sql +++ b/node/exts/erc20-bridge/erc20/meta_schema.sql @@ -78,7 +78,6 @@ CREATE TABLE meta CREATE TABLE epoch_votes ( epoch_id UUID NOT NULL REFERENCES epochs(id) ON UPDATE RESTRICT ON DELETE RESTRICT, voter BYTEA NOT NULL, - amount NUMERIC(78, 0) NOT NULL, -- so posterSvc won't need to calculate again nonce INT8 NOT NULL, -- safe nonce; this helps to skip unnecessary dup votes signature BYTEA NOT NULL, PRIMARY KEY (epoch_id, voter, nonce) diff --git a/node/exts/erc20-bridge/erc20/meta_sql.go b/node/exts/erc20-bridge/erc20/meta_sql.go index c5e4d02ee..1fea7dcae 100644 --- a/node/exts/erc20-bridge/erc20/meta_sql.go +++ b/node/exts/erc20-bridge/erc20/meta_sql.go @@ -386,8 +386,8 @@ func previousEpochConfirmed(ctx context.Context, app *common.App, instanceID *ty } func rowToEpoch(r *common.Row) (*Epoch, error) { - if len(r.Values) != 12 { - return nil, fmt.Errorf("expected 12 values, got %d", len(r.Values)) + if len(r.Values) != 11 { + return nil, fmt.Errorf("expected 11 values, got %d", len(r.Values)) } id := r.Values[0].(*types.UUID) @@ -417,7 +417,7 @@ func rowToEpoch(r *common.Row) (*Epoch, error) { confirmed := r.Values[7].(bool) // NOTE: empty value is [[]] - // values[7]-values[10] will all be empty if any, from the SQL; + // values[8]-values[10] will all be empty if any, from the SQL; var voters []ethcommon.Address if r.Values[8] != nil { rawVoters := r.Values[8].([][]byte) @@ -434,20 +434,10 @@ func rowToEpoch(r *common.Row) (*Epoch, error) { } } - // NOTE: empty value is [] - var amounts []*types.Decimal - if r.Values[9] != nil { - for _, rawAmount := range r.Values[9].([]*types.Decimal) { - if rawAmount != nil { - amounts = append(amounts, rawAmount) - } - } - } - // NOTE: empty value is [] var voteNonces []int64 - if r.Values[10] != nil { - rawNonces := r.Values[10].([]*int64) + if r.Values[9] != nil { + rawNonces := r.Values[9].([]*int64) for _, rawNonce := range rawNonces { if rawNonce != nil { // NOTE: this is probably problematic, since we can messup the index @@ -459,9 +449,9 @@ func rowToEpoch(r *common.Row) (*Epoch, error) { // NOTE: empty value is [[]] var signatures [][]byte - if r.Values[11] != nil { + if r.Values[10] != nil { // we skip the empty value, otherwise after conversion, [] will be returned - for _, rawSig := range r.Values[11].([][]byte) { + for _, rawSig := range r.Values[10].([][]byte) { if len(rawSig) != 0 { signatures = append(signatures, rawSig) } @@ -480,10 +470,9 @@ func rowToEpoch(r *common.Row) (*Epoch, error) { Total: rewardAmount, Confirmed: confirmed, EpochVoteInfo: EpochVoteInfo{ - Voters: voters, - VoteAmounts: amounts, - VoteSigs: signatures, - VoteNonces: voteNonces, + Voters: voters, + VoteSigs: signatures, + VoteNonces: voteNonces, }, }, nil } @@ -492,7 +481,7 @@ func rowToEpoch(r *common.Row) (*Epoch, error) { // one collects all new rewards, and one waits to be confirmed. func getActiveEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, fn func(*Epoch) error) error { query := ` - {kwil_erc20_meta}SELECT e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.reward_amount, e.ended_at, e.block_hash, e.confirmed, array_agg(v.voter) as voters, array_agg(v.amount) as amounts, array_agg(v.nonce) as nonces, array_agg(v.signature) as signatures + {kwil_erc20_meta}SELECT e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.reward_amount, e.ended_at, e.block_hash, e.confirmed, array_agg(v.voter) as voters, array_agg(v.nonce) as nonces, array_agg(v.signature) as signatures FROM epochs AS e LEFT JOIN epoch_votes AS v ON v.epoch_id = e.id WHERE e.instance_id = $instance_id AND e.confirmed IS NOT true @@ -512,7 +501,7 @@ func getActiveEpochs(ctx context.Context, app *common.App, instanceID *types.UUI // getEpochs gets epochs. func getEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, after int64, limit int64, fn func(*Epoch) error) error { query := ` - {kwil_erc20_meta}SELECT e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.reward_amount, e.ended_at, e.block_hash, e.confirmed, array_agg(v.voter) as voters, array_agg(v.amount) as amounts, array_agg(v.nonce) as nonces, array_agg(v.signature) as signatures + {kwil_erc20_meta}SELECT e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.reward_amount, e.ended_at, e.block_hash, e.confirmed, array_agg(v.voter) as voters, array_agg(v.nonce) as nonces, array_agg(v.signature) as signatures FROM epochs AS e LEFT JOIN epoch_votes AS v ON v.epoch_id = e.id WHERE e.instance_id = $instance_id AND e.created_at_block > $after @@ -602,14 +591,13 @@ func canVoteEpoch(ctx context.Context, app *common.App, epochID *types.UUID) (ok // voteEpoch vote an epoch by submitting signature. func voteEpoch(ctx context.Context, app *common.App, epochID *types.UUID, - voter ethcommon.Address, amount *types.Decimal, nonce int64, signature []byte) error { + voter ethcommon.Address, nonce int64, signature []byte) error { return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` - {kwil_erc20_meta}INSERT into epoch_votes(epoch_id, voter, amount, nonce, signature) - VALUES ($epoch_id, $voter, $amount, $nonce, $signature); + {kwil_erc20_meta}INSERT into epoch_votes(epoch_id, voter, nonce, signature) + VALUES ($epoch_id, $voter, $nonce, $signature); `, map[string]any{ "epoch_id": epochID, "voter": voter.Bytes(), - "amount": amount, "signature": signature, "nonce": nonce, }, nil) @@ -622,7 +610,7 @@ func getWalletEpochs(ctx context.Context, app *common.App, instanceID *types.UUI // WE don't need vote info, we just return empty arrays instead of JOIN query := ` - {kwil_erc20_meta}SELECT e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.reward_amount, e.ended_at, e.block_hash, e.confirmed, ARRAY[]::BYTEA[] as voters, ARRAY[]::NUMERIC(78, 0)[] as amounts, ARRAY[]::INT8[] as nonces, ARRAY[]::BYTEA[] as signatures + {kwil_erc20_meta}SELECT e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.reward_amount, e.ended_at, e.block_hash, e.confirmed, ARRAY[]::BYTEA[] as voters, ARRAY[]::INT8[] as nonces, ARRAY[]::BYTEA[] as signatures FROM epoch_rewards AS r JOIN epochs AS e ON r.epoch_id = e.id WHERE recipient = $wallet AND e.instance_id = $instance_id AND e.ended_at IS NOT NULL` // at least finalized diff --git a/node/exts/erc20-bridge/erc20/named_extension.go b/node/exts/erc20-bridge/erc20/named_extension.go index 7373d0076..a0c78fd3c 100644 --- a/node/exts/erc20-bridge/erc20/named_extension.go +++ b/node/exts/erc20-bridge/erc20/named_extension.go @@ -221,7 +221,6 @@ func init() { {Name: "end_block_hash", Type: types.ByteaType, Nullable: true}, {Name: "confirmed", Type: types.BoolType}, {Name: "voters", Type: types.TextArrayType, Nullable: true}, - {Name: "vote_amounts", Type: uint256NumericArray, Nullable: true}, {Name: "vote_nonces", Type: types.IntArrayType, Nullable: true}, {Name: "voter_signatures", Type: types.ByteaArrayType, Nullable: true}, }, @@ -247,7 +246,6 @@ func init() { {Name: "end_block_hash", Type: types.ByteaType, Nullable: true}, {Name: "confirmed", Type: types.BoolType}, {Name: "voters", Type: types.TextArrayType, Nullable: true}, - {Name: "vote_amounts", Type: uint256NumericArray, Nullable: true}, {Name: "vote_nonces", Type: types.IntArrayType, Nullable: true}, {Name: "voter_signatures", Type: types.ByteaArrayType, Nullable: true}, }, @@ -274,7 +272,6 @@ func init() { Name: "vote_epoch", Parameters: []precompiles.PrecompileValue{ {Name: "epoch_id", Type: types.UUIDType}, - {Name: "amount", Type: uint256Numeric}, {Name: "nonce", Type: types.IntType}, {Name: "signature", Type: types.ByteaType}, }, diff --git a/node/exts/erc20-bridge/signersvc/kwil.go b/node/exts/erc20-bridge/signersvc/kwil.go index 77fdfa262..97b916d68 100644 --- a/node/exts/erc20-bridge/signersvc/kwil.go +++ b/node/exts/erc20-bridge/signersvc/kwil.go @@ -30,28 +30,10 @@ type Epoch struct { EndBlockHash []byte Confirmed bool Voters []string - VoteAmounts []string VoteNonces []int64 VoteSignatures [][]byte } -type FinalizedReward struct { - ID types.UUID - Voters []string - Signatures [][]byte - EpochID types.UUID - CreatedAt int64 - // - StartHeight int64 - EndHeight int64 - TotalRewards types.Decimal - RewardRoot []byte - SafeNonce int64 - SignHash []byte - ContractID types.UUID - BlockHash []byte -} - type EpochReward struct { Recipient string Amount string @@ -64,7 +46,7 @@ type erc20ExtAPI interface { InstanceInfo(tx context.Context) (*RewardInstanceInfo, error) GetActiveEpochs(ctx context.Context) ([]*Epoch, error) GetEpochRewards(ctx context.Context, epochID types.UUID) ([]*EpochReward, error) - VoteEpoch(ctx context.Context, epochID types.UUID, amount *types.Decimal, safeNonce int64, signature []byte) (string, error) + VoteEpoch(ctx context.Context, epochID types.UUID, safeNonce int64, signature []byte) (string, error) } type erc20rwExtApi struct { @@ -129,7 +111,7 @@ func (k *erc20rwExtApi) GetActiveEpochs(ctx context.Context) ([]*Epoch, error) { for i, v := range res.QueryResult.Values { er := &Epoch{} err = types.ScanTo(v, &er.ID, &er.StartHeight, &er.StartTimestamp, &er.EndHeight, - &er.RewardRoot, &er.RewardAmount, &er.EndBlockHash, &er.Confirmed, &er.Voters, &er.VoteAmounts, &er.VoteNonces, &er.VoteSignatures) + &er.RewardRoot, &er.RewardAmount, &er.EndBlockHash, &er.Confirmed, &er.Voters, &er.VoteNonces, &er.VoteSignatures) if err != nil { return nil, err } @@ -164,9 +146,9 @@ func (k *erc20rwExtApi) GetEpochRewards(ctx context.Context, epochID types.UUID) return ers, nil } -func (k *erc20rwExtApi) VoteEpoch(ctx context.Context, epochID types.UUID, amount *types.Decimal, safeNonce int64, signature []byte) (string, error) { +func (k *erc20rwExtApi) VoteEpoch(ctx context.Context, epochID types.UUID, safeNonce int64, signature []byte) (string, error) { procedure := "vote_epoch" - input := [][]any{{epochID, amount, safeNonce, signature}} + input := [][]any{{epochID, safeNonce, signature}} res, err := k.clt.Execute(ctx, k.namespace, procedure, input, clientTypes.WithSyncBroadcast(true)) if err != nil { diff --git a/node/exts/erc20-bridge/signersvc/signer.go b/node/exts/erc20-bridge/signersvc/signer.go index 871ae4158..66992be6e 100644 --- a/node/exts/erc20-bridge/signersvc/signer.go +++ b/node/exts/erc20-bridge/signersvc/signer.go @@ -27,7 +27,6 @@ import ( "github.com/kwilteam/kwil-db/core/crypto" "github.com/kwilteam/kwil-db/core/crypto/auth" "github.com/kwilteam/kwil-db/core/log" - "github.com/kwilteam/kwil-db/core/types" "github.com/kwilteam/kwil-db/node/exts/erc20-bridge/utils" "github.com/kwilteam/kwil-db/node/exts/evm-sync/chains" ) @@ -142,17 +141,6 @@ func (s *rewardSigner) verify(ctx context.Context, epoch *Epoch, escrowAddr stri return total, nil } -// erc20ValueFromBigInt converts a big.Int to a decimal.Decimal(78,0) -// NOTE: this is copied from meta ext -func erc20ValueFromBigInt(b *big.Int) (*types.Decimal, error) { - dec, err := types.NewDecimalFromBigInt(b, 0) - if err != nil { - return nil, fmt.Errorf("failed to convert big.Int to decimal.Decimal: %w", err) - } - err = dec.SetPrecisionAndScale(78, 0) - return dec, err -} - // vote votes an epoch reward, and updates the state. // It will first fetch metadata from ETH, then generate the safeTx, then vote. func (s *rewardSigner) vote(ctx context.Context, epoch *Epoch, safeMeta *safeMetadata, total *big.Int) error { @@ -174,12 +162,7 @@ func (s *rewardSigner) vote(ctx context.Context, epoch *Epoch, safeMeta *safeMet return err } - uint256Amount, err := erc20ValueFromBigInt(total) - if err != nil { - return err - } - - h, err := s.kwil.VoteEpoch(ctx, epoch.ID, uint256Amount, safeMeta.nonce.Int64(), sig) + h, err := s.kwil.VoteEpoch(ctx, epoch.ID, safeMeta.nonce.Int64(), sig) if err != nil { return err } From b63ab7bad653f9225ca88f1291e0d8561c5abc96 Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Thu, 20 Feb 2025 14:41:43 -0600 Subject: [PATCH 25/30] fix taskfile --- Taskfile.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index 2b087a4d9..fc24d33b4 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -93,10 +93,10 @@ tasks: generate:abi: desc: Generate the ABI for the smart contracts cmds: - - abigen --abi=./node/exts/erc20reward/abigen/reward_distributor_abi.json --pkg abigen --out=./node/exts/erc20reward/abigen/reward_distributor.go --type RewardDistributor - - abigen --abi=./node/exts/erc20reward/abigen/erc20_abi.json --pkg abigen --out=./node/exts/erc20reward/abigen/erc20.go --type Erc20 - - abigen --abi=./node/services/erc20signersvc/abigen/safe_abi.json --pkg abigen --out=./node/services/erc20signersvc/abigen/safe.go --type Safe - - abigen --abi=./node/services/erc20signersvc/abigen/multicall3_abi.json --pkg abigen --out=./node/services/erc20signersvc/abigen/multicall3.go --type Multicall3 + - abigen --abi=./node/exts/erc20-bridge/abigen/reward_distributor_abi.json --pkg abigen --out=./node/exts/erc20-bridge/abigen/reward_distributor.go --type RewardDistributor + - abigen --abi=./node/exts/erc20-bridge/abigen/erc20_abi.json --pkg abigen --out=./node/exts/erc20-bridge/abigen/erc20.go --type Erc20 + - abigen --abi=./node/exts/erc20-bridge/abigen/safe_abi.json --pkg abigen --out=./node/exts/erc20-bridge/abigen/safe.go --type Safe + - abigen --abi=./node/exts/erc20-bridge/abigen/multicall3_abi.json --pkg abigen --out=./node/exts/erc20-bridge/abigen/multicall3.go --type Multicall3 # ************ docker ************ vendor: From 154900e509966c1bdc2ee1eabdb4bcd1d0ed8f62 Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Thu, 20 Feb 2025 17:00:05 -0600 Subject: [PATCH 26/30] fix typo --- app/node/build.go | 6 +++--- app/node/node.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/node/build.go b/app/node/build.go index eb049a854..b49aaddd0 100644 --- a/app/node/build.go +++ b/app/node/build.go @@ -97,7 +97,7 @@ func buildServer(ctx context.Context, d *coreDependencies) *server { ce := buildConsensusEngine(ctx, d, db, mp, bs, bp) // Erc20 bridge signer service - erc20RWSignerMgr := buildErc20RWignerMgr(d) + erc20BridgeSignerMgr := buildErc20BridgeSignerMgr(d) // Node node := buildNode(d, mp, bs, ce, snapshotStore, db, bp, p2pSvc) @@ -158,7 +158,7 @@ func buildServer(ctx context.Context, d *coreDependencies) *server { jsonRPCAdminServer: jsonRPCAdminServer, dbCtx: db, log: d.logger, - erc20RWSigner: erc20RWSignerMgr, + erc20BridgeSigner: erc20BridgeSignerMgr, } return s @@ -508,7 +508,7 @@ func buildConsensusEngine(_ context.Context, d *coreDependencies, db *pg.DB, return ce } -func buildErc20RWignerMgr(d *coreDependencies) *signersvc.ServiceMgr { +func buildErc20BridgeSignerMgr(d *coreDependencies) *signersvc.ServiceMgr { // create shared state stateFile := signersvc.StateFilePath(d.rootDir) diff --git a/app/node/node.go b/app/node/node.go index a46fe56fd..688d7a3e4 100644 --- a/app/node/node.go +++ b/app/node/node.go @@ -44,7 +44,7 @@ type server struct { listeners *listeners.ListenerManager jsonRPCServer *rpcserver.Server jsonRPCAdminServer *rpcserver.Server - erc20RWSigner *signersvc.ServiceMgr + erc20BridgeSigner *signersvc.ServiceMgr } func runNode(ctx context.Context, rootDir string, cfg *config.Config, autogen bool, dbOwner string) (err error) { @@ -262,9 +262,9 @@ func (s *server) Start(ctx context.Context) error { s.log.Info("listener manager started") // Start erc20 bridge signer svc - if s.erc20RWSigner != nil { + if s.erc20BridgeSigner != nil { group.Go(func() error { - return s.erc20RWSigner.Start(groupCtx) + return s.erc20BridgeSigner.Start(groupCtx) }) } From bfbca162e6584d100ef7f09364002ae663d0765b Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Fri, 21 Feb 2025 12:01:24 -0600 Subject: [PATCH 27/30] use internal ext client --- app/node/build.go | 19 +- core/client/client.go | 8 +- .../exts/erc20-bridge/erc20/meta_extension.go | 3 +- node/exts/erc20-bridge/signersvc/kwil.go | 378 ++++++++++++++---- node/exts/erc20-bridge/signersvc/signer.go | 91 ++--- node/exts/erc20-bridge/signersvc/state.go | 2 +- 6 files changed, 354 insertions(+), 147 deletions(-) diff --git a/app/node/build.go b/app/node/build.go index b49aaddd0..97f2bc629 100644 --- a/app/node/build.go +++ b/app/node/build.go @@ -96,9 +96,6 @@ func buildServer(ctx context.Context, d *coreDependencies) *server { // Consensus ce := buildConsensusEngine(ctx, d, db, mp, bs, bp) - // Erc20 bridge signer service - erc20BridgeSignerMgr := buildErc20BridgeSignerMgr(d) - // Node node := buildNode(d, mp, bs, ce, snapshotStore, db, bp, p2pSvc) @@ -148,6 +145,8 @@ func buildServer(ctx context.Context, d *coreDependencies) *server { jsonRPCAdminServer.RegisterSvc(jsonChainSvc) } + erc20BridgeSignerMgr := buildErc20BridgeSignerMgr(d, db, e, node, bp) + s := &server{ cfg: d.cfg, closers: closers, @@ -508,7 +507,9 @@ func buildConsensusEngine(_ context.Context, d *coreDependencies, db *pg.DB, return ce } -func buildErc20BridgeSignerMgr(d *coreDependencies) *signersvc.ServiceMgr { +func buildErc20BridgeSignerMgr(d *coreDependencies, db *pg.DB, + engine *interpreter.ThreadSafeInterpreter, node *node.Node, + bp *blockprocessor.BlockProcessor) *signersvc.ServiceMgr { // create shared state stateFile := signersvc.StateFilePath(d.rootDir) @@ -525,14 +526,8 @@ func buildErc20BridgeSignerMgr(d *coreDependencies) *signersvc.ServiceMgr { failBuild(err, "Failed to load erc20 bridge signer state file") } - rpcUrl := "http://" + d.cfg.RPC.ListenAddress - - mgr, err := signersvc.NewServiceMgr(rpcUrl, d.cfg.Erc20Bridge, state, d.logger.New("EVMRW")) - if err != nil { - failBuild(err, "Failed to create erc20 bridge signer service manager") - } - - return mgr + return signersvc.NewServiceMgr(d.genesisCfg.ChainID, db, engine, node, bp, + d.cfg.Erc20Bridge, state, d.logger.New("EVMRW")) } func buildNode(d *coreDependencies, mp *mempool.Mempool, bs *store.BlockStore, diff --git a/core/client/client.go b/core/client/client.go index 2ea782671..752a93a68 100644 --- a/core/client/client.go +++ b/core/client/client.go @@ -241,7 +241,7 @@ func (c *Client) ChainInfo(ctx context.Context) (*types.ChainInfo, error) { func (c *Client) Execute(ctx context.Context, namespace string, action string, tuples [][]any, opts ...clientType.TxOpt) (types.Hash, error) { encodedTuples := make([][]*types.EncodedValue, len(tuples)) for i, tuple := range tuples { - encoded, err := encodeTuple(tuple) + encoded, err := EncodeInputs(tuple) if err != nil { return types.Hash{}, err } @@ -303,7 +303,7 @@ func (c *Client) ExecuteSQL(ctx context.Context, stmt string, params map[string] // Call calls an action. It returns the result records. func (c *Client) Call(ctx context.Context, namespace string, action string, inputs []any) (*types.CallResult, error) { - encoded, err := encodeTuple(inputs) + encoded, err := EncodeInputs(inputs) if err != nil { return nil, err } @@ -374,8 +374,8 @@ func (c *Client) GetAccount(ctx context.Context, acctID *types.AccountID, status return c.txClient.GetAccount(ctx, acctID, status) } -// encodeTuple encodes a tuple for usage in a transaction. -func encodeTuple(tup []any) ([]*types.EncodedValue, error) { +// EncodeInputs encodes input(a tuple) for usage in a transaction. +func EncodeInputs(tup []any) ([]*types.EncodedValue, error) { encoded := make([]*types.EncodedValue, 0, len(tup)) for _, val := range tup { ev, err := types.EncodeValue(val) diff --git a/node/exts/erc20-bridge/erc20/meta_extension.go b/node/exts/erc20-bridge/erc20/meta_extension.go index 53662bfc9..ec72abd49 100644 --- a/node/exts/erc20-bridge/erc20/meta_extension.go +++ b/node/exts/erc20-bridge/erc20/meta_extension.go @@ -545,7 +545,6 @@ func init() { erc20Addr := info.syncedRewardData.Erc20Address.Hex() erc20Address = &erc20Addr decimals = &info.syncedRewardData.Erc20Decimals - //owbalStr := info.ownedBalance.String() ownedBalance = info.ownedBalance syncedAt = &info.syncedAt } @@ -1250,7 +1249,7 @@ func init() { } if leafNum == 0 { - app.Service.Logger.Info("no rewards to distribute, deplay finalized current epoch") + app.Service.Logger.Info("no rewards to distribute, delay finalized current epoch") return nil } diff --git a/node/exts/erc20-bridge/signersvc/kwil.go b/node/exts/erc20-bridge/signersvc/kwil.go index 97b916d68..eac43f306 100644 --- a/node/exts/erc20-bridge/signersvc/kwil.go +++ b/node/exts/erc20-bridge/signersvc/kwil.go @@ -2,10 +2,17 @@ package signersvc import ( "context" + "fmt" + "math/big" + "github.com/samber/lo" + + "github.com/kwilteam/kwil-db/common" "github.com/kwilteam/kwil-db/core/client" - clientTypes "github.com/kwilteam/kwil-db/core/client/types" + "github.com/kwilteam/kwil-db/core/crypto/auth" + rpcclient "github.com/kwilteam/kwil-db/core/rpc/client" "github.com/kwilteam/kwil-db/core/types" + "github.com/kwilteam/kwil-db/node/types/sql" ) type RewardInstanceInfo struct { @@ -14,19 +21,19 @@ type RewardInstanceInfo struct { EpochPeriod string Erc20 string Decimals int64 - Balance string + Balance *types.Decimal Synced bool SyncedAt int64 Enabled bool } type Epoch struct { - ID types.UUID + ID *types.UUID StartHeight int64 StartTimestamp int64 EndHeight int64 RewardRoot []byte - RewardAmount string + RewardAmount *types.Decimal EndBlockHash []byte Confirmed bool Voters []string @@ -39,121 +46,336 @@ type EpochReward struct { Amount string } -// erc20ExtAPI defines the ERC20 reward extension API used by SignerSvc. -type erc20ExtAPI interface { - GetTarget() string - SetTarget(ns string) - InstanceInfo(tx context.Context) (*RewardInstanceInfo, error) - GetActiveEpochs(ctx context.Context) ([]*Epoch, error) - GetEpochRewards(ctx context.Context, epochID types.UUID) ([]*EpochReward, error) - VoteEpoch(ctx context.Context, epochID types.UUID, safeNonce int64, signature []byte) (string, error) +// bridgeSignerClient defines the ERC20 bridge extension client for signer service. +type bridgeSignerClient interface { + InstanceInfo(tx context.Context, namespace string) (*RewardInstanceInfo, error) + GetActiveEpochs(ctx context.Context, namespace string) ([]*Epoch, error) + GetEpochRewards(ctx context.Context, namespace string, epochID *types.UUID) ([]*EpochReward, error) + VoteEpoch(ctx context.Context, namespace string, txSigner auth.Signer, epochID *types.UUID, safeNonce int64, signature []byte) (types.Hash, error) +} + +type txBcast interface { + BroadcastTx(ctx context.Context, tx *types.Transaction, sync uint8) (*types.ResultBroadcastTx, error) +} + +type engineCall interface { + CallWithoutEngineCtx(ctx context.Context, db sql.DB, namespace, action string, args []any, resultFn func(*common.Row) error) (*common.CallResult, error) +} + +type nodeApp interface { + AccountInfo(ctx context.Context, db sql.DB, account *types.AccountID, pending bool) (balance *big.Int, nonce int64, err error) + Price(ctx context.Context, dbTx sql.DB, tx *types.Transaction) (*big.Int, error) } -type erc20rwExtApi struct { - clt *client.Client - namespace string +type DB interface { + sql.ReadTxMaker + sql.DelayedReadTxMaker } -var _ erc20ExtAPI = (*erc20rwExtApi)(nil) +type signerClient struct { + chainID string + db DB + call engineCall + bcast txBcast + kwilNode nodeApp +} -func newERC20RWExtAPI(clt *client.Client, ns string) *erc20rwExtApi { - return &erc20rwExtApi{ - clt: clt, - namespace: ns, +func NewSignerClient(chainID string, db DB, call engineCall, bcast txBcast, nodeApp nodeApp) *signerClient { + return &signerClient{ + chainID: chainID, + db: db, + call: call, + bcast: bcast, + kwilNode: nodeApp, } } -func (k *erc20rwExtApi) GetTarget() string { - return k.namespace +func (k *signerClient) InstanceInfo(ctx context.Context, namespace string) (*RewardInstanceInfo, error) { + info := &RewardInstanceInfo{} + + readTx := k.db.BeginDelayedReadTx() + defer readTx.Rollback(ctx) + + _, err := k.call.CallWithoutEngineCtx(ctx, readTx, namespace, "info", []any{}, func(row *common.Row) error { + var ok bool + + info.Chain, ok = row.Values[0].(string) + if !ok { + return fmt.Errorf("failed to get chain") + } + + info.Escrow, ok = row.Values[1].(string) + if !ok { + return fmt.Errorf("failed to get escrow") + } + + info.EpochPeriod, ok = row.Values[2].(string) + if !ok { + return fmt.Errorf("failed to get epoch period") + } + + info.Erc20, ok = row.Values[3].(string) + if !ok { + return fmt.Errorf("failed to get erc20") + } + + info.Decimals, ok = row.Values[4].(int64) + if !ok { + return fmt.Errorf("failed to get decimals") + } + + info.Balance, ok = row.Values[5].(*types.Decimal) + if !ok { + return fmt.Errorf("failed to get balance") + } + + info.Synced, ok = row.Values[6].(bool) + if !ok { + return fmt.Errorf("failed to get synced") + } + + info.SyncedAt, ok = row.Values[7].(int64) + if !ok { + return fmt.Errorf("failed to get synced at") + } + + info.Enabled, ok = row.Values[8].(bool) + if !ok { + return fmt.Errorf("failed to get enabled") + } + + return nil + }) + + return info, err } -func (k *erc20rwExtApi) SetTarget(ns string) { - k.namespace = ns +func (k *signerClient) GetActiveEpochs(ctx context.Context, namespace string) ([]*Epoch, error) { + var epochs []*Epoch + + readTx := k.db.BeginDelayedReadTx() + defer readTx.Rollback(ctx) + + _, err := k.call.CallWithoutEngineCtx(ctx, readTx, namespace, "get_active_epochs", []any{}, func(row *common.Row) error { + var ok bool + e := &Epoch{} + + e.ID, ok = row.Values[0].(*types.UUID) + if !ok { + return fmt.Errorf("failed to get id") + } + e.StartHeight, ok = row.Values[1].(int64) + if !ok { + return fmt.Errorf("failed to get start height") + } + e.StartTimestamp, ok = row.Values[2].(int64) + if !ok { + return fmt.Errorf("failed to get start timestamp") + } + + if row.Values[3] != nil { + e.EndHeight, ok = row.Values[3].(int64) + if !ok { + return fmt.Errorf("failed to get end height") + } + } + + if row.Values[4] != nil { + e.RewardRoot, ok = row.Values[4].([]byte) + if !ok { + return fmt.Errorf("failed to get reward root") + } + } + + if row.Values[5] != nil { + e.RewardAmount, ok = row.Values[5].(*types.Decimal) + if !ok { + return fmt.Errorf("failed to get reward amount") + } + } + + if row.Values[6] != nil { + e.EndBlockHash, ok = row.Values[6].([]byte) + if !ok { + return fmt.Errorf("failed to get end block hash") + } + } + + e.Confirmed, ok = row.Values[7].(bool) + if !ok { + return fmt.Errorf("failed to get confirmed") + } + + if row.Values[8] != nil { + voters, ok := row.Values[8].([]*string) + if !ok { + return fmt.Errorf("failed to get voters") + } + + e.Voters = lo.Map(voters, func(v *string, i int) string { + if v == nil { + return "" + } + return *v + + }) + } + + if row.Values[9] != nil { + voteNonces, ok := row.Values[9].([]*int64) + if !ok { + return fmt.Errorf("failed to get vote nonces") + } + + e.VoteNonces = lo.Map(voteNonces, func(v *int64, i int) int64 { + if v == nil { + return 0 + } + return *v + }) + + } + + if row.Values[10] != nil { + e.VoteSignatures, ok = row.Values[10].([][]byte) + if !ok { + return fmt.Errorf("failed to get vote signatures") + } + } + + epochs = append(epochs, e) + return nil + }) + + return epochs, err } -func (k *erc20rwExtApi) InstanceInfo(ctx context.Context) (*RewardInstanceInfo, error) { - procedure := "info" - input := []any{} +func (k *signerClient) GetEpochRewards(ctx context.Context, namespace string, epochID *types.UUID) ([]*EpochReward, error) { + var rewards []*EpochReward - res, err := k.clt.Call(ctx, k.namespace, procedure, input) - if err != nil { - return nil, err - } + readTx := k.db.BeginDelayedReadTx() + defer readTx.Rollback(ctx) - if len(res.QueryResult.Values) == 0 { - return nil, nil - } + _, err := k.call.CallWithoutEngineCtx(ctx, readTx, namespace, "get_epoch_rewards", []any{epochID}, func(row *common.Row) error { + var ok bool + e := &EpochReward{} + + e.Recipient, ok = row.Values[0].(string) + if !ok { + return fmt.Errorf("failed to get recipient") + } + + e.Amount, ok = row.Values[1].(string) + if !ok { + return fmt.Errorf("failed to get amount") + } + + rewards = append(rewards, e) + + return nil + }) + + return rewards, err +} - er := &RewardInstanceInfo{} - err = types.ScanTo(res.QueryResult.Values[0], - &er.Chain, &er.Escrow, &er.EpochPeriod, &er.Erc20, &er.Decimals, &er.Balance, &er.Synced, &er.SyncedAt, &er.Enabled) +func (k *signerClient) VoteEpoch(ctx context.Context, namespace string, txSigner auth.Signer, epochID *types.UUID, safeNonce int64, signature []byte) (types.Hash, error) { + inputs := [][]any{{epochID, safeNonce, signature}} + res, err := k.execute(ctx, namespace, txSigner, "vote_epoch", inputs) if err != nil { - return nil, err + return types.Hash{}, err } - return er, nil + return res, nil } -func (k *erc20rwExtApi) GetActiveEpochs(ctx context.Context) ([]*Epoch, error) { - procedure := "get_active_epochs" - input := []any{} +func (k *signerClient) estimatePrice(ctx context.Context, tx *types.Transaction) (*big.Int, error) { + readTx := k.db.BeginDelayedReadTx() + defer readTx.Rollback(ctx) - res, err := k.clt.Call(ctx, k.namespace, procedure, input) + return k.kwilNode.Price(ctx, readTx, tx) + +} + +func (k *signerClient) accountNonce(ctx context.Context, acc *types.AccountID) (uint64, error) { + readTx := k.db.BeginDelayedReadTx() + defer readTx.Rollback(ctx) + + _, nonce, err := k.kwilNode.AccountInfo(ctx, readTx, acc, true) if err != nil { - return nil, err + return 0, fmt.Errorf("failed to get account info: %w", err) } - if len(res.QueryResult.Values) == 0 { - return nil, nil - } + return uint64(nonce), nil +} - ers := make([]*Epoch, len(res.QueryResult.Values)) - for i, v := range res.QueryResult.Values { - er := &Epoch{} - err = types.ScanTo(v, &er.ID, &er.StartHeight, &er.StartTimestamp, &er.EndHeight, - &er.RewardRoot, &er.RewardAmount, &er.EndBlockHash, &er.Confirmed, &er.Voters, &er.VoteNonces, &er.VoteSignatures) +// execute mimics client.Client.Execute, without client options. +func (k *signerClient) execute(ctx context.Context, namespace string, txSigner auth.Signer, action string, tuples [][]any) (types.Hash, error) { + encodedTuples := make([][]*types.EncodedValue, len(tuples)) + for i, tuple := range tuples { + encoded, err := client.EncodeInputs(tuple) if err != nil { - return nil, err + return types.Hash{}, err } - ers[i] = er + encodedTuples[i] = encoded } - return ers, nil + executionBody := &types.ActionExecution{ + Action: action, + Namespace: namespace, + Arguments: encodedTuples, + } + + tx, err := k.newTx(ctx, executionBody, txSigner) + if err != nil { + return types.Hash{}, fmt.Errorf("failed to create tx: %w", err) + } + + res, err := k.bcast.BroadcastTx(ctx, tx, uint8(rpcclient.BroadcastWaitCommit)) + if err != nil { + return types.Hash{}, fmt.Errorf("failed to broadcast tx: %w", err) + } + + return res.Hash, nil } -func (k *erc20rwExtApi) GetEpochRewards(ctx context.Context, epochID types.UUID) ([]*EpochReward, error) { - procedure := "get_epoch_rewards" - input := []any{epochID} - res, err := k.clt.Call(ctx, k.namespace, procedure, input) +// newTx mimics client.Client.newTx to create a new tx, without tx options. +func (k *signerClient) newTx(ctx context.Context, data types.Payload, txSigner auth.Signer) (*types.Transaction, error) { + // Get the latest nonce for the account, if it exists. + ident, err := types.GetSignerAccount(txSigner) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get signer account: %w", err) } - if len(res.QueryResult.Values) == 0 { - return nil, nil + nonce, err := k.accountNonce(ctx, ident) + if err != nil { + return nil, fmt.Errorf("failed to get account nonce: %w", err) } - ers := make([]*EpochReward, len(res.QueryResult.Values)) - for i, v := range res.QueryResult.Values { - er := &EpochReward{} - err = types.ScanTo(v, &er.Recipient, &er.Amount) - if err != nil { - return nil, err - } - ers[i] = er + // whether account gets balance or not + nonce += 1 + + // build transaction + tx, err := types.CreateTransaction(data, k.chainID, nonce) + if err != nil { + return nil, fmt.Errorf("failed to create transaction: %w", err) } - return ers, nil -} + // estimate price + price, err := k.estimatePrice(ctx, tx) + if err != nil { + return nil, fmt.Errorf("failed to estimate price: %w", err) + } -func (k *erc20rwExtApi) VoteEpoch(ctx context.Context, epochID types.UUID, safeNonce int64, signature []byte) (string, error) { - procedure := "vote_epoch" - input := [][]any{{epochID, safeNonce, signature}} + // set fee + tx.Body.Fee = price - res, err := k.clt.Execute(ctx, k.namespace, procedure, input, clientTypes.WithSyncBroadcast(true)) + // sign transaction + err = tx.Sign(txSigner) if err != nil { - return "", err + return nil, fmt.Errorf("failed to sign transaction: %w", err) } - return res.String(), nil + return tx, nil } + +var _ bridgeSignerClient = (*signerClient)(nil) diff --git a/node/exts/erc20-bridge/signersvc/signer.go b/node/exts/erc20-bridge/signersvc/signer.go index 66992be6e..0e0bf2fda 100644 --- a/node/exts/erc20-bridge/signersvc/signer.go +++ b/node/exts/erc20-bridge/signersvc/signer.go @@ -1,6 +1,6 @@ // Package signersvc implements the SignerSvc of the Kwil reward system. // It simply fetches the new Epoch from Kwil network and verify&sign it, then -// upload the signature back to the Kwil network. Each rewardSigner targets one registered +// upload the signature back to the Kwil network. Each bridgeSigner targets one registered // erc20 Reward instance. package signersvc @@ -22,8 +22,6 @@ import ( ethCrypto "github.com/ethereum/go-ethereum/crypto" "github.com/kwilteam/kwil-db/config" - "github.com/kwilteam/kwil-db/core/client" - clientType "github.com/kwilteam/kwil-db/core/client/types" "github.com/kwilteam/kwil-db/core/crypto" "github.com/kwilteam/kwil-db/core/crypto/auth" "github.com/kwilteam/kwil-db/core/log" @@ -36,13 +34,14 @@ func StateFilePath(dir string) string { return filepath.Join(dir, "erc20_signer_state.json") } -// rewardSigner handles one registered erc20 reward instance. -type rewardSigner struct { +// bridgeSigner handles the voting on one registered erc20 reward instance. +type bridgeSigner struct { target string - kwil erc20ExtAPI lastVoteBlock int64 escrowAddr ethCommon.Address + kwil bridgeSignerClient + txSigner auth.Signer signerPk *ecdsa.PrivateKey signerAddr ethCommon.Address safe *Safe @@ -51,8 +50,9 @@ type rewardSigner struct { state *State } -// newRewardSigner returns a new rewardSigner. -func newRewardSigner(kwil erc20ExtAPI, safe *Safe, target string, signerPk *ecdsa.PrivateKey, signerAddr ethCommon.Address, escrowAddr ethCommon.Address, state *State, logger log.Logger) (*rewardSigner, error) { +func newBridgeSigner(kwil bridgeSignerClient, safe *Safe, target string, txSigner auth.Signer, + signerPk *ecdsa.PrivateKey, signerAddr ethCommon.Address, escrowAddr ethCommon.Address, + state *State, logger log.Logger) (*bridgeSigner, error) { if logger == nil { logger = log.DiscardLogger } @@ -66,8 +66,9 @@ func newRewardSigner(kwil erc20ExtAPI, safe *Safe, target string, signerPk *ecds logger.Info("will sync after last vote epoch", "height", lastVoteBlock) - return &rewardSigner{ + return &bridgeSigner{ kwil: kwil, + txSigner: txSigner, signerPk: signerPk, signerAddr: signerAddr, state: state, @@ -82,7 +83,7 @@ func newRewardSigner(kwil erc20ExtAPI, safe *Safe, target string, signerPk *ecds // canSkip returns true if: // - signer is not one of the safe owners // - signer has voted this epoch with the same nonce as current safe nonce -func (s *rewardSigner) canSkip(epoch *Epoch, safeMeta *safeMetadata) bool { +func (s *bridgeSigner) canSkip(epoch *Epoch, safeMeta *safeMetadata) bool { if !slices.Contains(safeMeta.owners, s.signerAddr) { s.logger.Warn("signer is not safe owner", "signer", s.signerAddr.String(), "owners", safeMeta.owners) return true @@ -103,8 +104,8 @@ func (s *rewardSigner) canSkip(epoch *Epoch, safeMeta *safeMetadata) bool { } // verify verifies if the reward root is correct, and return the total amount. -func (s *rewardSigner) verify(ctx context.Context, epoch *Epoch, escrowAddr string) (*big.Int, error) { - rewards, err := s.kwil.GetEpochRewards(ctx, epoch.ID) +func (s *bridgeSigner) verify(ctx context.Context, epoch *Epoch, escrowAddr string) (*big.Int, error) { + rewards, err := s.kwil.GetEpochRewards(ctx, s.target, epoch.ID) if err != nil { return nil, err } @@ -143,7 +144,7 @@ func (s *rewardSigner) verify(ctx context.Context, epoch *Epoch, escrowAddr stri // vote votes an epoch reward, and updates the state. // It will first fetch metadata from ETH, then generate the safeTx, then vote. -func (s *rewardSigner) vote(ctx context.Context, epoch *Epoch, safeMeta *safeMetadata, total *big.Int) error { +func (s *bridgeSigner) vote(ctx context.Context, epoch *Epoch, safeMeta *safeMetadata, total *big.Int) error { safeTxData, err := utils.GenPostRewardTxData(epoch.RewardRoot, total) if err != nil { return err @@ -162,7 +163,7 @@ func (s *rewardSigner) vote(ctx context.Context, epoch *Epoch, safeMeta *safeMet return err } - h, err := s.kwil.VoteEpoch(ctx, epoch.ID, safeMeta.nonce.Int64(), sig) + h, err := s.kwil.VoteEpoch(ctx, s.target, s.txSigner, epoch.ID, safeMeta.nonce.Int64(), sig) if err != nil { return err } @@ -188,10 +189,10 @@ func (s *rewardSigner) vote(ctx context.Context, epoch *Epoch, safeMeta *safeMet // sync polls on newer epochs and try to vote/sign them. // Since there could be the case that the target(namespace/or id) not exist for whatever reason, // this function won't return Error, and also won't log at Error level. -func (s *rewardSigner) sync(ctx context.Context) { +func (s *bridgeSigner) sync(ctx context.Context) { s.logger.Debug("polling epochs", "lastVoteBlock", s.lastVoteBlock) - epochs, err := s.kwil.GetActiveEpochs(ctx) + epochs, err := s.kwil.GetActiveEpochs(ctx, s.target) if err != nil { s.logger.Warn("fetch epoch", "error", err.Error()) return @@ -242,28 +243,18 @@ func (s *rewardSigner) sync(ctx context.Context) { } // getSigners verifies config and returns a list of signerSvc. -func getSigners(kwilRpc string, cfg config.ERC20BridgeConfig, state *State, logger log.Logger) ([]*rewardSigner, error) { +func getSigners(cfg config.ERC20BridgeConfig, kwil bridgeSignerClient, state *State, logger log.Logger) ([]*bridgeSigner, error) { if err := cfg.Validate(); err != nil { return nil, err } ctx := context.Background() - clt, err := client.NewClient(ctx, kwilRpc, nil) - if err != nil { - return nil, fmt.Errorf("create erc20 bridge signer api client failed: %w", err) - } - - signers := make([]*rewardSigner, 0, len(cfg.Signer)) + signers := make([]*bridgeSigner, 0, len(cfg.Signer)) for target, pkPath := range cfg.Signer { // pkPath is validated already - readOnlyAPi := newERC20RWExtAPI(clt, target) - instanceInfo, err := readOnlyAPi.InstanceInfo(ctx) - if err != nil { - return nil, fmt.Errorf("get reward metadata failed: %w", err) - } - + // parse signer private key rawPkBytes, err := os.ReadFile(pkPath) if err != nil { return nil, fmt.Errorf("read private key file %s failed: %w", pkPath, err) @@ -280,26 +271,23 @@ func getSigners(kwilRpc string, cfg config.ERC20BridgeConfig, state *State, logg return nil, fmt.Errorf("parse erc20 bridge signer private key failed: %w", err) } - // Get the public key signerPubKey := signerPk.Public().(*ecdsa.PublicKey) - - // Get the Ethereum address from the public key signerAddr := ethCrypto.PubkeyToAddress(*signerPubKey) + // derive tx signer key, err := crypto.UnmarshalSecp256k1PrivateKey(pkBytes) if err != nil { return nil, fmt.Errorf("parse erc20 bridge signer private key failed: %w", err) } - opts := &clientType.Options{Signer: &auth.EthPersonalSigner{Key: *key}} + txSigner := &auth.EthPersonalSigner{Key: *key} - clt, err := client.NewClient(ctx, kwilRpc, opts) + // use instance info to create safe + instanceInfo, err := kwil.InstanceInfo(ctx, target) if err != nil { - return nil, fmt.Errorf("create erc20 bridge signer api client failed: %w", err) + return nil, fmt.Errorf("get reward metadata failed: %w", err) } - kwil := newERC20RWExtAPI(clt, target) - chainRpc, ok := cfg.RPC[strings.ToLower(instanceInfo.Chain)] if !ok { return nil, fmt.Errorf("target '%s' chain '%s' not found in erc20_bridge.rpc config", target, instanceInfo.Chain) @@ -320,7 +308,7 @@ func getSigners(kwilRpc string, cfg config.ERC20BridgeConfig, state *State, logg } // wilRpc, target, chainRpc, strings.TrimSpace(string(pkBytes)) - svc, err := newRewardSigner(kwil, safe, target, signerPk, signerAddr, ethCommon.HexToAddress(instanceInfo.Escrow), state, logger.New("EVMRW."+target)) + svc, err := newBridgeSigner(kwil, safe, target, txSigner, signerPk, signerAddr, ethCommon.HexToAddress(instanceInfo.Escrow), state, logger.New("EVMRW."+target)) if err != nil { return nil, fmt.Errorf("create erc20 bridge signer service failed: %w", err) } @@ -331,9 +319,9 @@ func getSigners(kwilRpc string, cfg config.ERC20BridgeConfig, state *State, logg return signers, nil } -// ServiceMgr manages multiple rewardSigner instances running in parallel. +// ServiceMgr manages multiple bridgeSigner instances running in parallel. type ServiceMgr struct { - kwilRpc string + kwil bridgeSignerClient // will be shared among all signers state *State bridgeCfg config.ERC20BridgeConfig syncInterval time.Duration @@ -341,29 +329,33 @@ type ServiceMgr struct { } func NewServiceMgr( - kwilRpc string, + chainID string, + db DB, + call engineCall, + bcast txBcast, + nodeApp nodeApp, cfg config.ERC20BridgeConfig, state *State, - logger log.Logger) (*ServiceMgr, error) { + logger log.Logger) *ServiceMgr { return &ServiceMgr{ - kwilRpc: kwilRpc, + kwil: NewSignerClient(chainID, db, call, bcast, nodeApp), state: state, bridgeCfg: cfg, logger: logger, syncInterval: time.Minute, // default to 1m - }, nil + } } -// Start runs all rewardSigners. It returns error if there are issues initializing the rewardSigner; -// no errors are returned after the rewardSigner is running. +// Start runs all rewardSigners. It returns error if there are issues initializing the bridgeSigner; +// no errors are returned after the bridgeSigner is running. func (m *ServiceMgr) Start(ctx context.Context) error { // since we need to wait on RPC running, we move the initialization logic into `init` var err error - var signers []*rewardSigner + var signers []*bridgeSigner // To be able to run with docker, we need to apply a retry logic, because kwild // won't have erc20 instance when boot - for { // naive way to keep retrying the init + for { // naive way to keep retrying the init, on any error select { case <-ctx.Done(): m.logger.Info("stop initializing erc20 bridge signer") @@ -371,13 +363,12 @@ func (m *ServiceMgr) Start(ctx context.Context) error { default: } - signers, err = getSigners(m.kwilRpc, m.bridgeCfg, m.state, m.logger) + signers, err = getSigners(m.bridgeCfg, m.kwil, m.state, m.logger) if err == nil { break } m.logger.Warn("failed to initialize erc20 bridge signer, will retry", "error", err.Error()) - // any error, we try again time.Sleep(time.Second * 3) } diff --git a/node/exts/erc20-bridge/signersvc/state.go b/node/exts/erc20-bridge/signersvc/state.go index 9730939b2..a17a169f8 100644 --- a/node/exts/erc20-bridge/signersvc/state.go +++ b/node/exts/erc20-bridge/signersvc/state.go @@ -18,7 +18,7 @@ type voteRecord struct { SafeNonce uint64 `json:"safe_nonce"` } -// State is a naive kv impl used by singer rewardSigner. +// State is a naive kv impl used by bridgeSigner. type State struct { path string From 137b2812e33b8e4b6febfbaba1c67c607d623752 Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Tue, 25 Feb 2025 09:05:12 -0600 Subject: [PATCH 28/30] fix typos --- node/exts/erc20-bridge/erc20/meta_extension.go | 2 +- node/exts/erc20-bridge/signersvc/signer.go | 4 +--- node/exts/erc20-bridge/utils/crypto.go | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/node/exts/erc20-bridge/erc20/meta_extension.go b/node/exts/erc20-bridge/erc20/meta_extension.go index ec72abd49..d6e3c4244 100644 --- a/node/exts/erc20-bridge/erc20/meta_extension.go +++ b/node/exts/erc20-bridge/erc20/meta_extension.go @@ -511,7 +511,7 @@ func init() { }, Returns: &precompiles.MethodReturn{ Fields: []precompiles.PrecompileValue{ - {Name: "chain_id", Type: types.TextType}, + {Name: "chain", Type: types.TextType}, {Name: "escrow", Type: types.TextType}, {Name: "epoch_period", Type: types.TextType}, {Name: "erc20", Type: types.TextType, Nullable: true}, diff --git a/node/exts/erc20-bridge/signersvc/signer.go b/node/exts/erc20-bridge/signersvc/signer.go index 0e0bf2fda..527b69c0e 100644 --- a/node/exts/erc20-bridge/signersvc/signer.go +++ b/node/exts/erc20-bridge/signersvc/signer.go @@ -17,7 +17,6 @@ import ( "sync" "time" - ethAccounts "github.com/ethereum/go-ethereum/accounts" ethCommon "github.com/ethereum/go-ethereum/common" ethCrypto "github.com/ethereum/go-ethereum/crypto" @@ -157,8 +156,7 @@ func (s *bridgeSigner) vote(ctx context.Context, epoch *Epoch, safeMeta *safeMet return err } - signHash := ethAccounts.TextHash(safeTxHash) - sig, err := utils.EthGnosisSignDigest(signHash, s.signerPk) + sig, err := utils.EthGnosisSign(safeTxHash, s.signerPk) if err != nil { return err } diff --git a/node/exts/erc20-bridge/utils/crypto.go b/node/exts/erc20-bridge/utils/crypto.go index f9062af05..072baed45 100644 --- a/node/exts/erc20-bridge/utils/crypto.go +++ b/node/exts/erc20-bridge/utils/crypto.go @@ -107,7 +107,7 @@ func EthZeppelinSign(msg []byte, key *ecdsa.PrivateKey) ([]byte, error) { // https://docs.safe.global/advanced/smart-account-signatures // SDK: safe-core-sdk/packages/protocol-kit/src/utils/signatures/utils.ts `adjustVInSignature` // -// Since we use EIP-191, the V should be 31(0x1df) or 32(0x20). +// Since we use EIP-191, the V should be 31(0x1f) or 32(0x20). func EthGnosisSign(msg []byte, key *ecdsa.PrivateKey) ([]byte, error) { return EthGnosisSignDigest(ethAccounts.TextHash(msg), key) } From 7d03ec7120b5b15b858a251545c3d41e2cad6c58 Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Tue, 25 Feb 2025 12:13:33 -0600 Subject: [PATCH 29/30] add bridge method to withdraw balance --- core/types/decimal.go | 10 ++ .../exts/erc20-bridge/erc20/meta_extension.go | 135 ++++++++++++------ .../erc20-bridge/erc20/named_extension.go | 8 ++ node/exts/erc20-bridge/signersvc/signer.go | 2 +- 4 files changed, 110 insertions(+), 45 deletions(-) diff --git a/core/types/decimal.go b/core/types/decimal.go index 787193c74..8b45fcd9e 100644 --- a/core/types/decimal.go +++ b/core/types/decimal.go @@ -199,6 +199,16 @@ func (d *Decimal) IsNegative() bool { return d.dec.Negative } +// IsZero returns true if the decimal is zero. +func (d *Decimal) IsZero() bool { + return d.dec.IsZero() +} + +// IsPositive returns true if the decimal is positive. +func (d *Decimal) IsPositive() bool { + return !d.IsNegative() && !d.IsZero() +} + // String returns the string representation of the decimal. func (d *Decimal) String() string { return d.dec.String() diff --git a/node/exts/erc20-bridge/erc20/meta_extension.go b/node/exts/erc20-bridge/erc20/meta_extension.go index d6e3c4244..a70ee0734 100644 --- a/node/exts/erc20-bridge/erc20/meta_extension.go +++ b/node/exts/erc20-bridge/erc20/meta_extension.go @@ -616,48 +616,7 @@ func init() { user := inputs[1].(string) amount := inputs[2].(*types.Decimal) - if amount.IsNegative() { - return fmt.Errorf("amount cannot be negative") - } - - info, err := SINGLETON.getUsableInstance(id) - if err != nil { - return err - } - info.mu.RLock() - // we cannot defer an RUnlock here because we need to unlock - // the read lock before we can acquire the write lock, which - // we do at the end of this - - newBal, err := types.DecimalSub(info.ownedBalance, amount) - if err != nil { - info.mu.RUnlock() - return err - } - - if newBal.IsNegative() { - info.mu.RUnlock() - return fmt.Errorf("network does not enough balance to issue %s to %s", amount, user) - } - - addr, err := ethAddressFromHex(user) - if err != nil { - info.mu.RUnlock() - return err - } - - err = issueReward(ctx.TxContext.Ctx, app, id, info.currentEpoch.ID, addr, amount) - if err != nil { - info.mu.RUnlock() - return err - } - - info.mu.RUnlock() - info.mu.Lock() - info.ownedBalance = newBal - info.mu.Unlock() - - return nil + return SINGLETON.issueTokens(ctx.TxContext.Ctx, app, id, user, amount) }, }, { @@ -829,6 +788,44 @@ func init() { return resultFn([]any{bal}) }, }, + { + // bridge will 'issue' token to the caller, from its own balance + Name: "bridge", + Parameters: []precompiles.PrecompileValue{ + {Name: "id", Type: types.UUIDType}, + {Name: "amount", Type: uint256Numeric, Nullable: true}, + }, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC}, + Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { + id := inputs[0].(*types.UUID) + + var err error + var amount *types.Decimal + // if 'amount' is omited, withdraw all balance + if inputs[1] == nil { + callerAddr, err := ethAddressFromHex(ctx.TxContext.Caller) + if err != nil { + return err + } + + amount, err = balanceOf(ctx.TxContext.Ctx, app, id, callerAddr) + if err != nil { + return err + } + } else { + amount = inputs[1].(*types.Decimal) + } + + // first, lock required 'amount' from caller to the network + err = SINGLETON.lockTokens(ctx.TxContext.Ctx, app, id, ctx.TxContext.Caller, amount) + if err != nil { + return err + } + + // then issue to caller itself + return SINGLETON.issueTokens(ctx.TxContext.Ctx, app, id, ctx.TxContext.Caller, amount) + }, + }, { Name: "decimals", Parameters: []precompiles.PrecompileValue{ @@ -1377,8 +1374,8 @@ func (e *extensionInfo) lockTokens(ctx context.Context, app *common.App, id *typ return err } - if amount.IsNegative() { - return fmt.Errorf("amount cannot be negative") + if !amount.IsPositive() { + return fmt.Errorf("amount needs to be positive") } // we call getUsableInstance before transfer to ensure that the extension is active and synced. @@ -1406,6 +1403,56 @@ func (e *extensionInfo) lockTokens(ctx context.Context, app *common.App, id *typ return nil } +// issueTokens issues tokens from network's balance. +func (e *extensionInfo) issueTokens(ctx context.Context, app *common.App, id *types.UUID, to string, amount *types.Decimal) error { + if !amount.IsPositive() { + return fmt.Errorf("amount needs to be positive") + } + + // then issue to caller itself + // because this is in one tx, we can be sure that the instance has enough balance to issue. + info, err := e.getUsableInstance(id) + if err != nil { + return err + } + + info.mu.RLock() + // we cannot defer an RUnlock here because we need to unlock + // the read lock before we can acquire the write lock, which + // we do at the end of this + + newBal, err := types.DecimalSub(info.ownedBalance, amount) + if err != nil { + info.mu.RUnlock() + return err + } + + if newBal.IsNegative() { + info.mu.RUnlock() + return fmt.Errorf("network does not enough balance to issue %s to %s", amount, to) + } + + addr, err := ethAddressFromHex(to) + if err != nil { + info.mu.RUnlock() + return err + } + + err = issueReward(ctx, app, id, info.currentEpoch.ID, addr, amount) + if err != nil { + info.mu.RUnlock() + return err + } + + info.mu.RUnlock() + + info.mu.Lock() + info.ownedBalance = newBal + info.mu.Unlock() + + return nil +} + // getUsableInstance gets an instance and ensures it is active and synced. func (e *extensionInfo) getUsableInstance(id *types.UUID) (*rewardExtensionInfo, error) { info, ok := e.instances.Get(*id) diff --git a/node/exts/erc20-bridge/erc20/named_extension.go b/node/exts/erc20-bridge/erc20/named_extension.go index a0c78fd3c..936feb59c 100644 --- a/node/exts/erc20-bridge/erc20/named_extension.go +++ b/node/exts/erc20-bridge/erc20/named_extension.go @@ -170,6 +170,14 @@ func init() { AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, Handler: makeMetaHandler("balance"), }, + { + Name: "bridge", + Parameters: []precompiles.PrecompileValue{ + {Name: "amount", Type: uint256Numeric}, + }, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC}, + Handler: makeMetaHandler("bridge"), + }, { Name: "decimals", Returns: &precompiles.MethodReturn{ diff --git a/node/exts/erc20-bridge/signersvc/signer.go b/node/exts/erc20-bridge/signersvc/signer.go index 527b69c0e..5d56dddd2 100644 --- a/node/exts/erc20-bridge/signersvc/signer.go +++ b/node/exts/erc20-bridge/signersvc/signer.go @@ -30,7 +30,7 @@ import ( // StateFilePath returns the state file. func StateFilePath(dir string) string { - return filepath.Join(dir, "erc20_signer_state.json") + return filepath.Join(dir, "erc20_bridge_vote.json") } // bridgeSigner handles the voting on one registered erc20 reward instance. From c5d58a672a1ef9fdc24d3805304ebb9a0a5dd164 Mon Sep 17 00:00:00 2001 From: Yaiba <4yaiba@gmail.com> Date: Tue, 25 Feb 2025 12:23:09 -0600 Subject: [PATCH 30/30] remove trivials --- node/exts/erc20-bridge/signersvc/signer.go | 46 ++++++++-------------- node/exts/erc20-bridge/signersvc/state.go | 9 +++-- 2 files changed, 21 insertions(+), 34 deletions(-) diff --git a/node/exts/erc20-bridge/signersvc/signer.go b/node/exts/erc20-bridge/signersvc/signer.go index 5d56dddd2..7fe88d173 100644 --- a/node/exts/erc20-bridge/signersvc/signer.go +++ b/node/exts/erc20-bridge/signersvc/signer.go @@ -35,9 +35,8 @@ func StateFilePath(dir string) string { // bridgeSigner handles the voting on one registered erc20 reward instance. type bridgeSigner struct { - target string - lastVoteBlock int64 - escrowAddr ethCommon.Address + target string + escrowAddr ethCommon.Address kwil bridgeSignerClient txSigner auth.Signer @@ -56,26 +55,16 @@ func newBridgeSigner(kwil bridgeSignerClient, safe *Safe, target string, txSigne logger = log.DiscardLogger } - // overwrite configured lastVoteBlock with the value from state if exist - lastVoteBlock := int64(0) - lastVote := state.LastVote(target) - if lastVote != nil { - lastVoteBlock = lastVote.BlockHeight - } - - logger.Info("will sync after last vote epoch", "height", lastVoteBlock) - return &bridgeSigner{ - kwil: kwil, - txSigner: txSigner, - signerPk: signerPk, - signerAddr: signerAddr, - state: state, - logger: logger, - target: target, - safe: safe, - escrowAddr: escrowAddr, - lastVoteBlock: lastVoteBlock, + kwil: kwil, + txSigner: txSigner, + signerPk: signerPk, + signerAddr: signerAddr, + state: state, + logger: logger, + target: target, + safe: safe, + escrowAddr: escrowAddr, }, nil } @@ -169,10 +158,10 @@ func (s *bridgeSigner) vote(ctx context.Context, epoch *Epoch, safeMeta *safeMet // NOTE: it's fine if s.kwil.VoteEpoch succeed, but s.state.UpdateLastVote failed, // as the epoch will be fetched again and skipped err = s.state.UpdateLastVote(s.target, &voteRecord{ - RewardRoot: epoch.RewardRoot, - BlockHeight: epoch.EndHeight, - BlockHash: hex.EncodeToString(epoch.EndBlockHash), - SafeNonce: safeMeta.nonce.Uint64(), + Epoch: epoch.ID.String(), + RewardRoot: epoch.RewardRoot, + TxHash: h.String(), + SafeNonce: safeMeta.nonce.Uint64(), }) if err != nil { return err @@ -188,7 +177,7 @@ func (s *bridgeSigner) vote(ctx context.Context, epoch *Epoch, safeMeta *safeMet // Since there could be the case that the target(namespace/or id) not exist for whatever reason, // this function won't return Error, and also won't log at Error level. func (s *bridgeSigner) sync(ctx context.Context) { - s.logger.Debug("polling epochs", "lastVoteBlock", s.lastVoteBlock) + s.logger.Debug("polling epochs") epochs, err := s.kwil.GetActiveEpochs(ctx, s.target) if err != nil { @@ -221,7 +210,6 @@ func (s *bridgeSigner) sync(ctx context.Context) { if s.canSkip(finalizedEpoch, safeMeta) { s.logger.Info("skip epoch", "id", finalizedEpoch.ID.String(), "height", finalizedEpoch.EndHeight) - s.lastVoteBlock = finalizedEpoch.EndHeight // update since we can skip it return } @@ -236,8 +224,6 @@ func (s *bridgeSigner) sync(ctx context.Context) { s.logger.Warn("vote epoch", "id", finalizedEpoch.ID.String(), "height", finalizedEpoch.EndHeight, "error", err.Error()) return } - - s.lastVoteBlock = finalizedEpoch.EndHeight // update after all operations succeed } // getSigners verifies config and returns a list of signerSvc. diff --git a/node/exts/erc20-bridge/signersvc/state.go b/node/exts/erc20-bridge/signersvc/state.go index a17a169f8..06b2725cf 100644 --- a/node/exts/erc20-bridge/signersvc/state.go +++ b/node/exts/erc20-bridge/signersvc/state.go @@ -11,11 +11,12 @@ import ( "sync" ) +// voteRecord holds the vote info of the epoch. type voteRecord struct { - RewardRoot []byte `json:"reward_root"` - BlockHeight int64 `json:"block_height"` - BlockHash string `json:"block_hash"` - SafeNonce uint64 `json:"safe_nonce"` + Epoch string `json:"epoch"` + RewardRoot []byte `json:"reward_root"` + TxHash string `json:"tx_hash"` + SafeNonce uint64 `json:"safe_nonce"` } // State is a naive kv impl used by bridgeSigner.