Skip to content

Commit

Permalink
feat(erc20signersvc): add erc20 reward signer svc
Browse files Browse the repository at this point in the history
  • Loading branch information
Yaiba committed Feb 6, 2025
1 parent 7ed1d5f commit 73b23a4
Show file tree
Hide file tree
Showing 8 changed files with 642 additions and 18 deletions.
54 changes: 47 additions & 7 deletions app/node/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -155,6 +158,7 @@ func buildServer(ctx context.Context, d *coreDependencies) *server {
jsonRPCAdminServer: jsonRPCAdminServer,
dbCtx: db,
log: d.logger,
erc20RWSigner: erc20RWSignerMgr,
}

return s
Expand Down Expand Up @@ -503,6 +507,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.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 {
Expand Down
12 changes: 12 additions & 0 deletions app/node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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

Expand Down
59 changes: 48 additions & 11 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,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),
},
}
}

Expand All @@ -320,17 +327,18 @@ 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"`
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"`
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.
Expand Down Expand Up @@ -426,6 +434,35 @@ 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"`
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) {
return fmt.Errorf("private keys and targets must be configured in pairs")
}

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)
}
}

return nil
}

// ToTOML marshals the config to TOML. The `toml` struct field tag
// specifies the field names. For example:
//
Expand Down
4 changes: 4 additions & 0 deletions node/exts/erc20reward/meta/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ func GetRewardContract(ctx *kcommon.EngineContext, ee EngineExecutor, db sql.DB,
return nil, err
}

if rc == nil {
return nil, sql.ErrNoRows
}

err = ee.Execute(ctx, db, sqlListSigners,
map[string]any{"$contract_id": rc.ID},
func(row *kcommon.Row) error {
Expand Down
1 change: 1 addition & 0 deletions node/services/erc20signersvc/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
142 changes: 142 additions & 0 deletions node/services/erc20signersvc/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
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"
)

// TODO: use the type from Ext?
type Epoch struct {
ID types.UUID
StartHeight int64
EndHeight int64
TotalRewards types.Decimal
//MtreeJson string
RewardRoot []byte
SafeNonce int64
SignHash []byte // SignHash is Chain aware, it's from GnosisSafeTx
ContractID types.UUID
BlockHash []byte
CreatedAt int64
Voters []string
Finalized bool // TODO: so signerSvc can use this to see it can skip, or, in the SQL, we have option to only return not-finalized epoch
}

// 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
}

// rewardExtAPI defines the ERC20 reward extension API used by SignerSvc.
type rewardExtAPI interface {
GetTarget() string
SetTarget(ns string)
ListEpochs(ctx context.Context, afterHeight int64, limit int) ([]*Epoch, error)
FetchLatestRewards(ctx context.Context, limit int) ([]*FinalizedReward, error)
VoteEpoch(ctx context.Context, signHash []byte, signature []byte) (string, error)
}

type erc20rwExtApi struct {
clt *client.Client
target string
}

var _ rewardExtAPI = (*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) ListEpochs(ctx context.Context, startHeight int64, limit int) ([]*Epoch, error) {
procedure := "list_epochs"
input := []any{startHeight, 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
}

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) 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, signHash []byte, signature []byte) (string, error) {
procedure := "vote_epoch"
input := [][]any{{signHash, signature}}

res, err := k.clt.Execute(ctx, k.target, procedure, input, clientTypes.WithSyncBroadcast(true))
if err != nil {
return "", err
}

return res.String(), nil
}
Loading

0 comments on commit 73b23a4

Please sign in to comment.