Skip to content

Commit

Permalink
feat(erc20signersvc): add erc20 reward signer svc (#1346)
Browse files Browse the repository at this point in the history
* feat(erc20bridge): add erc20 bridge signer svc

* use internal ext client

* add bridge method to withdraw balance

* adjust removing ResultBroadcastTx

* fix lint
  • Loading branch information
Yaiba authored Feb 26, 2025
1 parent 7ba37c3 commit 3f95602
Show file tree
Hide file tree
Showing 43 changed files with 4,006 additions and 751 deletions.
11 changes: 4 additions & 7 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,6 @@ tasks:
generates:
- .build/kwild

generate:grammar:
desc: Generate the kuneiform grammar go code.
cmds:
- rm -rf node/engine/parse/gen/*
- cd node/engine/parse/grammar && ./generate.sh

generate:docs:
desc: Generate docs for CLIs
Expand All @@ -98,8 +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/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:
Expand Down
40 changes: 33 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/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"
"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"
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 @@ -148,6 +148,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,
Expand All @@ -158,6 +160,7 @@ func buildServer(ctx context.Context, d *coreDependencies) *server {
jsonRPCAdminServer: jsonRPCAdminServer,
dbCtx: db,
log: d.logger,
erc20BridgeSigner: erc20BridgeSignerMgr,
}

return s
Expand Down Expand Up @@ -507,6 +510,29 @@ func buildConsensusEngine(_ context.Context, d *coreDependencies, db *pg.DB,
return ce
}

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)

if !fileExists(stateFile) {
emptyFile, err := os.Create(stateFile)
if err != nil {
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 bridge signer state file")
}

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,
ce *consensus.ConsensusEngine, ss *snapshotter.SnapshotStore, db *pg.DB,
bp *blockprocessor.BlockProcessor, p2p *node.P2PService) *node.Node {
Expand Down
13 changes: 11 additions & 2 deletions app/node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"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"
Expand All @@ -20,11 +22,10 @@ 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"
rpcserver "github.com/kwilteam/kwil-db/node/services/jsonrpc"
"github.com/kwilteam/kwil-db/version"

"golang.org/x/sync/errgroup"
)

type server struct {
Expand All @@ -43,6 +44,7 @@ type server struct {
listeners *listeners.ListenerManager
jsonRPCServer *rpcserver.Server
jsonRPCAdminServer *rpcserver.Server
erc20BridgeSigner *signersvc.ServiceMgr
}

func runNode(ctx context.Context, rootDir string, cfg *config.Config, autogen bool, dbOwner string) (err error) {
Expand Down Expand Up @@ -259,6 +261,13 @@ func (s *server) Start(ctx context.Context) error {
})
s.log.Info("listener manager started")

// Start erc20 bridge signer svc
if s.erc20BridgeSigner != nil {
group.Go(func() error {
return s.erc20BridgeSigner.Start(groupCtx)
})
}

// TODO: node is starting the consensus engine for ease of testing
// Start the consensus engine

Expand Down
20 changes: 15 additions & 5 deletions app/node/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,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])
}
Expand All @@ -136,8 +135,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])
Expand Down
26 changes: 22 additions & 4 deletions app/node/start_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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,
},
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion cmd/kwil-cli/cmds/call-action.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
43 changes: 41 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import (
"strings"
"time"

"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"
"github.com/kwilteam/kwil-db/core/types"

"github.com/pelletier/go-toml/v2"
"github.com/kwilteam/kwil-db/node/exts/evm-sync/chains"
)

var (
Expand Down Expand Up @@ -314,6 +316,11 @@ func DefaultConfig() *Config {
Height: 0,
Hash: types.Hash{},
},
Erc20Bridge: ERC20BridgeConfig{
RPC: make(map[string]string),
BlockSyncChuckSize: make(map[string]string),
Signer: make(map[string]string),
},
}
}

Expand All @@ -339,6 +346,7 @@ type Config struct {
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"`
}

type MempoolConfig struct {
Expand Down Expand Up @@ -452,6 +460,37 @@ type Checkpoint struct {
Hash types.Hash `toml:"hash" comment:"checkpoint block hash."`
}

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: ext_alias='file_path_to_private_key'"`
}

// 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) Validate() error {
for chain, rpc := range cfg.RPC {
if err := chains.Chain(strings.ToLower(chain)).Valid(); err != nil {
return fmt.Errorf("erc20_bridge.rpc: %s", chain)
}

// enforce websocket
if !strings.HasPrefix(rpc, "wss://") && !strings.HasPrefix(rpc, "ws://") {
return fmt.Errorf("erc20_bridge.rpc: must start with wss:// or ws://")
}
}

for _, pkPath := range cfg.Signer {
if !ethCommon.FileExist(pkPath) {
return fmt.Errorf("erc20_bridge.signer: private key file %s not found", pkPath)
}
}

return nil
}

// ToTOML marshals the config to TOML. The `toml` struct field tag
// specifies the field names. For example:
//
Expand Down
8 changes: 4 additions & 4 deletions core/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions core/types/decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,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=
Expand Down
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit 3f95602

Please sign in to comment.