From 6a24f4ac30cb626a124b207e83b41ff272a6a4c5 Mon Sep 17 00:00:00 2001 From: kevin <35275952+kaladinlight@users.noreply.github.com> Date: Sat, 17 Dec 2022 12:31:19 -0700 Subject: [PATCH] EVM Abstraction (#848) --- bchain/coins/eth/contract.go | 4 +- bchain/coins/eth/ethrpc.go | 284 ++++++++++++++++++----------------- bchain/coins/eth/evm.go | 130 ++++++++++++++++ bchain/evm_interface.go | 59 ++++++++ 4 files changed, 337 insertions(+), 140 deletions(-) create mode 100644 bchain/coins/eth/evm.go create mode 100644 bchain/evm_interface.go diff --git a/bchain/coins/eth/contract.go b/bchain/coins/eth/contract.go index c56f688d..6405c9c8 100644 --- a/bchain/coins/eth/contract.go +++ b/bchain/coins/eth/contract.go @@ -274,10 +274,10 @@ func contractGetTransfersFromTx(tx *bchain.RpcTransaction) (bchain.TokenTransfer } func (b *EthereumRPC) ethCall(data, to string) (string, error) { - ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() var r string - err := b.rpc.CallContext(ctx, &r, "eth_call", map[string]interface{}{ + err := b.RPC.CallContext(ctx, &r, "eth_call", map[string]interface{}{ "data": data, "to": to, }, "latest") diff --git a/bchain/coins/eth/ethrpc.go b/bchain/coins/eth/ethrpc.go index f241d7b1..4484a475 100644 --- a/bchain/coins/eth/ethrpc.go +++ b/bchain/coins/eth/ethrpc.go @@ -12,10 +12,10 @@ import ( "sync" "time" - ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rpc" "github.com/golang/glog" @@ -24,18 +24,18 @@ import ( "github.com/trezor/blockbook/common" ) -// EthereumNet type specifies the type of ethereum network -type EthereumNet uint32 +// Network type specifies the type of ethereum network +type Network uint32 const ( // MainNet is production network - MainNet EthereumNet = 1 + MainNet Network = 1 // TestNet is Ropsten test network - TestNet EthereumNet = 3 + TestNet Network = 3 // TestNetGoerli is Goerli test network - TestNetGoerli EthereumNet = 5 + TestNetGoerli Network = 5 // TestNetSepolia is Sepolia test network - TestNetSepolia EthereumNet = 11155111 + TestNetSepolia Network = 11155111 ) // Configuration represents json config file @@ -56,19 +56,22 @@ type Configuration struct { // EthereumRPC is an interface to JSON-RPC eth service. type EthereumRPC struct { *bchain.BaseChain - client *ethclient.Client - rpc *rpc.Client - timeout time.Duration + Client bchain.EVMClient + RPC bchain.EVMRPCClient + MainNetChainID Network + Timeout time.Duration Parser *EthereumParser + PushHandler func(bchain.NotificationType) + OpenRPC func(string) (bchain.EVMRPCClient, bchain.EVMClient, error) Mempool *bchain.MempoolEthereumType mempoolInitialized bool bestHeaderLock sync.Mutex - bestHeader *ethtypes.Header + bestHeader bchain.EVMHeader bestHeaderTime time.Time - chanNewBlock chan *ethtypes.Header - newBlockSubscription *rpc.ClientSubscription - chanNewTx chan ethcommon.Hash - newTxSubscription *rpc.ClientSubscription + NewBlock bchain.EVMNewBlockSubscriber + newBlockSubscription bchain.EVMClientSubscription + NewTx bchain.EVMNewTxSubscriber + newTxSubscription bchain.EVMClientSubscription ChainConfig *Configuration } @@ -88,15 +91,8 @@ func NewEthereumRPC(config json.RawMessage, pushHandler func(bchain.Notification c.BlockAddressesToKeep = 100 } - rc, ec, err := openRPC(c.RPCURL) - if err != nil { - return nil, err - } - s := &EthereumRPC{ BaseChain: &bchain.BaseChain{}, - client: ec, - rpc: rc, ChainConfig: &c, } @@ -104,70 +100,46 @@ func NewEthereumRPC(config json.RawMessage, pushHandler func(bchain.Notification // always create parser s.Parser = NewEthereumParser(c.BlockAddressesToKeep, c.AddressAliases) - s.timeout = time.Duration(c.RPCTimeout) * time.Second - - // new blocks notifications handling - // the subscription is done in Initialize - s.chanNewBlock = make(chan *ethtypes.Header) - go func() { - for { - h, ok := <-s.chanNewBlock - if !ok { - break - } - glog.V(2).Info("rpc: new block header ", h.Number) - // update best header to the new header - s.bestHeaderLock.Lock() - s.bestHeader = h - s.bestHeaderTime = time.Now() - s.bestHeaderLock.Unlock() - // notify blockbook - pushHandler(bchain.NotificationNewBlock) - } - }() - - // new mempool transaction notifications handling - // the subscription is done in Initialize - s.chanNewTx = make(chan ethcommon.Hash) - go func() { - for { - t, ok := <-s.chanNewTx - if !ok { - break - } - hex := t.Hex() - if glog.V(2) { - glog.Info("rpc: new tx ", hex) - } - s.Mempool.AddTransactionToMempool(hex) - pushHandler(bchain.NotificationNewTx) - } - }() + s.Timeout = time.Duration(c.RPCTimeout) * time.Second + s.PushHandler = pushHandler return s, nil } -func openRPC(url string) (*rpc.Client, *ethclient.Client, error) { - rc, err := rpc.Dial(url) - if err != nil { - return nil, nil, err - } - ec := ethclient.NewClient(rc) - return rc, ec, nil -} - // Initialize initializes ethereum rpc interface func (b *EthereumRPC) Initialize() error { - ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + b.OpenRPC = func(url string) (bchain.EVMRPCClient, bchain.EVMClient, error) { + r, err := rpc.Dial(url) + if err != nil { + return nil, nil, err + } + rc := &EthereumRPCClient{Client: r} + ec := &EthereumClient{Client: ethclient.NewClient(r)} + return rc, ec, nil + } + + rc, ec, err := b.OpenRPC(b.ChainConfig.RPCURL) + if err != nil { + return err + } + + // set chain specific + b.Client = ec + b.RPC = rc + b.MainNetChainID = MainNet + b.NewBlock = &EthereumNewBlock{channel: make(chan *types.Header)} + b.NewTx = &EthereumNewTx{channel: make(chan ethcommon.Hash)} + + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() - id, err := b.client.NetworkID(ctx) + id, err := b.Client.NetworkID(ctx) if err != nil { return err } // parameters for getInfo request - switch EthereumNet(id.Uint64()) { + switch Network(id.Uint64()) { case MainNet: b.Testnet = false b.Network = "livenet" @@ -225,13 +197,26 @@ func (b *EthereumRPC) InitializeMempool(addrDescForOutpoint bchain.AddrDescForOu } func (b *EthereumRPC) subscribeEvents() error { - // subscriptions - if err := b.subscribe(func() (*rpc.ClientSubscription, error) { + // new block notifications handling + go func() { + for { + h, ok := b.NewBlock.Read() + if !ok { + break + } + b.UpdateBestHeader(h) + // notify blockbook + b.PushHandler(bchain.NotificationNewBlock) + } + }() + + // new block subscription + if err := b.subscribe(func() (bchain.EVMClientSubscription, error) { // invalidate the previous subscription - it is either the first one or there was an error b.newBlockSubscription = nil - ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() - sub, err := b.rpc.EthSubscribe(ctx, b.chanNewBlock, "newHeads") + sub, err := b.RPC.EthSubscribe(ctx, b.NewBlock.Channel(), "newHeads") if err != nil { return nil, errors.Annotatef(err, "EthSubscribe newHeads") } @@ -242,12 +227,29 @@ func (b *EthereumRPC) subscribeEvents() error { return err } - if err := b.subscribe(func() (*rpc.ClientSubscription, error) { + // new mempool transaction notifications handling + go func() { + for { + t, ok := b.NewTx.Read() + if !ok { + break + } + hex := t.Hex() + if glog.V(2) { + glog.Info("rpc: new tx ", hex) + } + b.Mempool.AddTransactionToMempool(hex) + b.PushHandler(bchain.NotificationNewTx) + } + }() + + // new mempool transaction subscription + if err := b.subscribe(func() (bchain.EVMClientSubscription, error) { // invalidate the previous subscription - it is either the first one or there was an error b.newTxSubscription = nil - ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() - sub, err := b.rpc.EthSubscribe(ctx, b.chanNewTx, "newPendingTransactions") + sub, err := b.RPC.EthSubscribe(ctx, b.NewTx.Channel(), "newPendingTransactions") if err != nil { return nil, errors.Annotatef(err, "EthSubscribe newPendingTransactions") } @@ -262,7 +264,7 @@ func (b *EthereumRPC) subscribeEvents() error { } // subscribe subscribes notification and tries to resubscribe in case of error -func (b *EthereumRPC) subscribe(f func() (*rpc.ClientSubscription, error)) error { +func (b *EthereumRPC) subscribe(f func() (bchain.EVMClientSubscription, error)) error { s, err := f() if err != nil { return err @@ -308,27 +310,28 @@ func (b *EthereumRPC) closeRPC() { if b.newTxSubscription != nil { b.newTxSubscription.Unsubscribe() } - if b.rpc != nil { - b.rpc.Close() + if b.RPC != nil { + b.RPC.Close() } } func (b *EthereumRPC) reconnectRPC() error { glog.Info("Reconnecting RPC") b.closeRPC() - rc, ec, err := openRPC(b.ChainConfig.RPCURL) + rc, ec, err := b.OpenRPC(b.ChainConfig.RPCURL) if err != nil { return err } - b.rpc = rc - b.client = ec + b.RPC = rc + b.Client = ec return b.subscribeEvents() } // Shutdown cleans up rpc interface to ethereum func (b *EthereumRPC) Shutdown(ctx context.Context) error { b.closeRPC() - close(b.chanNewBlock) + b.NewBlock.Close() + b.NewTx.Close() glog.Info("rpc: shutdown") return nil } @@ -380,26 +383,26 @@ func (b *EthereumRPC) GetChainInfo() (*bchain.ChainInfo, error) { if err != nil { return nil, err } - ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() - id, err := b.client.NetworkID(ctx) + id, err := b.Client.NetworkID(ctx) if err != nil { return nil, err } var ver string - if err := b.rpc.CallContext(ctx, &ver, "web3_clientVersion"); err != nil { + if err := b.RPC.CallContext(ctx, &ver, "web3_clientVersion"); err != nil { return nil, err } consensusVersion := b.getConsensusVersion() rv := &bchain.ChainInfo{ - Blocks: int(h.Number.Int64()), - Bestblockhash: h.Hash().Hex(), - Difficulty: h.Difficulty.String(), + Blocks: int(h.Number().Int64()), + Bestblockhash: h.Hash(), + Difficulty: h.Difficulty().String(), Version: ver, ConsensusVersion: consensusVersion, } idi := int(id.Uint64()) - if idi == 1 { + if idi == int(b.MainNetChainID) { rv.Chain = "mainnet" } else { rv.Chain = "testnet " + strconv.Itoa(idi) @@ -407,7 +410,7 @@ func (b *EthereumRPC) GetChainInfo() (*bchain.ChainInfo, error) { return rv, nil } -func (b *EthereumRPC) getBestHeader() (*ethtypes.Header, error) { +func (b *EthereumRPC) getBestHeader() (bchain.EVMHeader, error) { b.bestHeaderLock.Lock() defer b.bestHeaderLock.Unlock() // if the best header was not updated for 15 minutes, there could be a subscription problem, reconnect RPC @@ -421,9 +424,9 @@ func (b *EthereumRPC) getBestHeader() (*ethtypes.Header, error) { } if b.bestHeader == nil { var err error - ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() - b.bestHeader, err = b.client.HeaderByNumber(ctx, nil) + b.bestHeader, err = b.Client.HeaderByNumber(ctx, nil) if err != nil { b.bestHeader = nil return nil, err @@ -433,13 +436,22 @@ func (b *EthereumRPC) getBestHeader() (*ethtypes.Header, error) { return b.bestHeader, nil } +// UpdateBestHeader keeps track of the latest block header confirmed on chain +func (b *EthereumRPC) UpdateBestHeader(h bchain.EVMHeader) { + glog.V(2).Info("rpc: new block header ", h.Number()) + b.bestHeaderLock.Lock() + b.bestHeader = h + b.bestHeaderTime = time.Now() + b.bestHeaderLock.Unlock() +} + // GetBestBlockHash returns hash of the tip of the best-block-chain func (b *EthereumRPC) GetBestBlockHash() (string, error) { h, err := b.getBestHeader() if err != nil { return "", err } - return h.Hash().Hex(), nil + return h.Hash(), nil } // GetBestBlockHeight returns height of the tip of the best-block-chain @@ -448,23 +460,23 @@ func (b *EthereumRPC) GetBestBlockHeight() (uint32, error) { if err != nil { return 0, err } - return uint32(h.Number.Uint64()), nil + return uint32(h.Number().Uint64()), nil } // GetBlockHash returns hash of block in best-block-chain at given height func (b *EthereumRPC) GetBlockHash(height uint32) (string, error) { var n big.Int n.SetUint64(uint64(height)) - ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() - h, err := b.client.HeaderByNumber(ctx, &n) + h, err := b.Client.HeaderByNumber(ctx, &n) if err != nil { if err == ethereum.NotFound { return "", bchain.ErrBlockNotFound } return "", errors.Annotatef(err, "height %v", height) } - return h.Hash().Hex(), nil + return h.Hash(), nil } func (b *EthereumRPC) ethHeaderToBlockHeader(h *rpcHeader) (*bchain.BlockHeader, error) { @@ -512,24 +524,24 @@ func (b *EthereumRPC) computeConfirmations(n uint64) (uint32, error) { if err != nil { return 0, err } - bn := bh.Number.Uint64() + bn := bh.Number().Uint64() // transaction in the best block has 1 confirmation return uint32(bn - n + 1), nil } func (b *EthereumRPC) getBlockRaw(hash string, height uint32, fullTxs bool) (json.RawMessage, error) { - ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() var raw json.RawMessage var err error if hash != "" { if hash == "pending" { - err = b.rpc.CallContext(ctx, &raw, "eth_getBlockByNumber", hash, fullTxs) + err = b.RPC.CallContext(ctx, &raw, "eth_getBlockByNumber", hash, fullTxs) } else { - err = b.rpc.CallContext(ctx, &raw, "eth_getBlockByHash", ethcommon.HexToHash(hash), fullTxs) + err = b.RPC.CallContext(ctx, &raw, "eth_getBlockByHash", ethcommon.HexToHash(hash), fullTxs) } } else { - err = b.rpc.CallContext(ctx, &raw, "eth_getBlockByNumber", fmt.Sprintf("%#x", height), fullTxs) + err = b.RPC.CallContext(ctx, &raw, "eth_getBlockByNumber", fmt.Sprintf("%#x", height), fullTxs) } if err != nil { return nil, errors.Annotatef(err, "hash %v, height %v", hash, height) @@ -540,11 +552,11 @@ func (b *EthereumRPC) getBlockRaw(hash string, height uint32, fullTxs bool) (jso } func (b *EthereumRPC) processEventsForBlock(blockNumber string) (map[string][]*bchain.RpcLog, []bchain.AddressAliasRecord, error) { - ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() var logs []rpcLogWithTxHash var ensRecords []bchain.AddressAliasRecord - err := b.rpc.CallContext(ctx, &logs, "eth_getLogs", map[string]interface{}{ + err := b.RPC.CallContext(ctx, &logs, "eth_getLogs", map[string]interface{}{ "fromBlock": blockNumber, "toBlock": blockNumber, }) @@ -629,10 +641,10 @@ func (b *EthereumRPC) getInternalDataForBlock(blockHash string, blockHeight uint data := make([]bchain.EthereumInternalData, len(transactions)) contracts := make([]bchain.ContractInfo, 0) if ProcessInternalTransactions { - ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() var trace []rpcTraceResult - err := b.rpc.CallContext(ctx, &trace, "debug_traceBlockByHash", blockHash, map[string]interface{}{"tracer": "callTracer"}) + err := b.RPC.CallContext(ctx, &trace, "debug_traceBlockByHash", blockHash, map[string]interface{}{"tracer": "callTracer"}) if err != nil { glog.Error("debug_traceBlockByHash block ", blockHash, ", error ", err) return data, contracts, err @@ -778,11 +790,11 @@ func (b *EthereumRPC) GetTransactionForMempool(txid string) (*bchain.Tx, error) // GetTransaction returns a transaction by the transaction ID. func (b *EthereumRPC) GetTransaction(txid string) (*bchain.Tx, error) { - ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() var tx *bchain.RpcTransaction hash := ethcommon.HexToHash(txid) - err := b.rpc.CallContext(ctx, &tx, "eth_getTransactionByHash", hash) + err := b.RPC.CallContext(ctx, &tx, "eth_getTransactionByHash", hash) if err != nil { return nil, err } else if tx == nil { @@ -815,7 +827,7 @@ func (b *EthereumRPC) GetTransaction(txid string) (*bchain.Tx, error) { return nil, errors.Annotatef(err, "txid %v", txid) } var receipt bchain.RpcReceipt - err = b.rpc.CallContext(ctx, &receipt, "eth_getTransactionReceipt", hash) + err = b.RPC.CallContext(ctx, &receipt, "eth_getTransactionReceipt", hash) if err != nil { return nil, errors.Annotatef(err, "txid %v", txid) } @@ -878,17 +890,19 @@ func (b *EthereumRPC) EstimateFee(blocks int) (big.Int, error) { // EstimateSmartFee returns fee estimation func (b *EthereumRPC) EstimateSmartFee(blocks int, conservative bool) (big.Int, error) { - ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() var r big.Int - gp, err := b.client.SuggestGasPrice(ctx) + gp, err := b.Client.SuggestGasPrice(ctx) if err == nil && b != nil { r = *gp } return r, err } -func getStringFromMap(p string, params map[string]interface{}) (string, bool) { +// GetStringFromMap attempts to return the value for a specific key in a map as a string if valid, +// otherwise returns an empty string with false indicating there was no key found, or the value was not a string +func GetStringFromMap(p string, params map[string]interface{}) (string, bool) { v, ok := params[p] if ok { s, ok := v.(string) @@ -899,43 +913,37 @@ func getStringFromMap(p string, params map[string]interface{}) (string, bool) { // EthereumTypeEstimateGas returns estimation of gas consumption for given transaction parameters func (b *EthereumRPC) EthereumTypeEstimateGas(params map[string]interface{}) (uint64, error) { - ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() msg := ethereum.CallMsg{} - s, ok := getStringFromMap("from", params) - if ok && len(s) > 0 { + if s, ok := GetStringFromMap("from", params); ok && len(s) > 0 { msg.From = ethcommon.HexToAddress(s) } - s, ok = getStringFromMap("to", params) - if ok && len(s) > 0 { + if s, ok := GetStringFromMap("to", params); ok && len(s) > 0 { a := ethcommon.HexToAddress(s) msg.To = &a } - s, ok = getStringFromMap("data", params) - if ok && len(s) > 0 { + if s, ok := GetStringFromMap("data", params); ok && len(s) > 0 { msg.Data = ethcommon.FromHex(s) } - s, ok = getStringFromMap("value", params) - if ok && len(s) > 0 { + if s, ok := GetStringFromMap("value", params); ok && len(s) > 0 { msg.Value, _ = hexutil.DecodeBig(s) } - s, ok = getStringFromMap("gas", params) - if ok && len(s) > 0 { + if s, ok := GetStringFromMap("gas", params); ok && len(s) > 0 { msg.Gas, _ = hexutil.DecodeUint64(s) } - s, ok = getStringFromMap("gasPrice", params) - if ok && len(s) > 0 { + if s, ok := GetStringFromMap("gasPrice", params); ok && len(s) > 0 { msg.GasPrice, _ = hexutil.DecodeBig(s) } - return b.client.EstimateGas(ctx, msg) + return b.Client.EstimateGas(ctx, msg) } // SendRawTransaction sends raw transaction func (b *EthereumRPC) SendRawTransaction(hex string) (string, error) { - ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() var raw json.RawMessage - err := b.rpc.CallContext(ctx, &raw, "eth_sendRawTransaction", hex) + err := b.RPC.CallContext(ctx, &raw, "eth_sendRawTransaction", hex) if err != nil { return "", err } else if len(raw) == 0 { @@ -953,16 +961,16 @@ func (b *EthereumRPC) SendRawTransaction(hex string) (string, error) { // EthereumTypeGetBalance returns current balance of an address func (b *EthereumRPC) EthereumTypeGetBalance(addrDesc bchain.AddressDescriptor) (*big.Int, error) { - ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() - return b.client.BalanceAt(ctx, ethcommon.BytesToAddress(addrDesc), nil) + return b.Client.BalanceAt(ctx, addrDesc, nil) } // EthereumTypeGetNonce returns current balance of an address func (b *EthereumRPC) EthereumTypeGetNonce(addrDesc bchain.AddressDescriptor) (uint64, error) { - ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() - return b.client.NonceAt(ctx, ethcommon.BytesToAddress(addrDesc), nil) + return b.Client.NonceAt(ctx, addrDesc, nil) } // GetChainParser returns ethereum BlockChainParser diff --git a/bchain/coins/eth/evm.go b/bchain/coins/eth/evm.go new file mode 100644 index 00000000..1304b243 --- /dev/null +++ b/bchain/coins/eth/evm.go @@ -0,0 +1,130 @@ +package eth + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" + "github.com/trezor/blockbook/bchain" +) + +// EthereumClient wraps a client to implement the EVMClient interface +type EthereumClient struct { + *ethclient.Client +} + +// HeaderByNumber returns a block header that implements the EVMHeader interface +func (c *EthereumClient) HeaderByNumber(ctx context.Context, number *big.Int) (bchain.EVMHeader, error) { + h, err := c.Client.HeaderByNumber(ctx, number) + if err != nil { + return nil, err + } + + return &EthereumHeader{Header: h}, nil +} + +// EstimateGas returns the current estimated gas cost for executing a transaction +func (c *EthereumClient) EstimateGas(ctx context.Context, msg interface{}) (uint64, error) { + return c.Client.EstimateGas(ctx, msg.(ethereum.CallMsg)) +} + +// BalanceAt returns the balance for the given account at a specific block, or latest known block if no block number is provided +func (c *EthereumClient) BalanceAt(ctx context.Context, addrDesc bchain.AddressDescriptor, blockNumber *big.Int) (*big.Int, error) { + return c.Client.BalanceAt(ctx, common.BytesToAddress(addrDesc), blockNumber) +} + +// NonceAt returns the nonce for the given account at a specific block, or latest known block if no block number is provided +func (c *EthereumClient) NonceAt(ctx context.Context, addrDesc bchain.AddressDescriptor, blockNumber *big.Int) (uint64, error) { + return c.Client.NonceAt(ctx, common.BytesToAddress(addrDesc), blockNumber) +} + +// EthereumRPCClient wraps an rpc client to implement the EVMRPCClient interface +type EthereumRPCClient struct { + *rpc.Client +} + +// EthSubscribe subscribes to events and returns a client subscription that implements the EVMClientSubscription interface +func (c *EthereumRPCClient) EthSubscribe(ctx context.Context, channel interface{}, args ...interface{}) (bchain.EVMClientSubscription, error) { + sub, err := c.Client.EthSubscribe(ctx, channel, args...) + if err != nil { + return nil, err + } + + return &EthereumClientSubscription{ClientSubscription: sub}, nil +} + +// EthereumHeader wraps a block header to implement the EVMHeader interface +type EthereumHeader struct { + *types.Header +} + +// Hash returns the block hash as a hex string +func (h *EthereumHeader) Hash() string { + return h.Header.Hash().Hex() +} + +// Number returns the block number +func (h *EthereumHeader) Number() *big.Int { + return h.Header.Number +} + +// Difficulty returns the block difficulty +func (h *EthereumHeader) Difficulty() *big.Int { + return h.Header.Difficulty +} + +// EthereumHash wraps a transaction hash to implement the EVMHash interface +type EthereumHash struct { + common.Hash +} + +// EthereumClientSubscription wraps a client subcription to implement the EVMClientSubscription interface +type EthereumClientSubscription struct { + *rpc.ClientSubscription +} + +// EthereumNewBlock wraps a block header channel to implement the EVMNewBlockSubscriber interface +type EthereumNewBlock struct { + channel chan *types.Header +} + +// Channel returns the underlying channel as an empty interface +func (s *EthereumNewBlock) Channel() interface{} { + return s.channel +} + +// Read from the underlying channel and return a block header that implements the EVMHeader interface +func (s *EthereumNewBlock) Read() (bchain.EVMHeader, bool) { + h, ok := <-s.channel + return &EthereumHeader{Header: h}, ok +} + +// Close the underlying channel +func (s *EthereumNewBlock) Close() { + close(s.channel) +} + +// EthereumNewTx wraps a transaction hash channel to implement the EVMNewTxSubscriber interface +type EthereumNewTx struct { + channel chan common.Hash +} + +// Channel returns the underlying channel as an empty interface +func (s *EthereumNewTx) Channel() interface{} { + return s.channel +} + +// Read from the underlying channel and return a transaction hash that implements the EVMHash interface +func (s *EthereumNewTx) Read() (bchain.EVMHash, bool) { + h, ok := <-s.channel + return &EthereumHash{Hash: h}, ok +} + +// Close the underlying channel +func (s *EthereumNewTx) Close() { + close(s.channel) +} diff --git a/bchain/evm_interface.go b/bchain/evm_interface.go new file mode 100644 index 00000000..1338b012 --- /dev/null +++ b/bchain/evm_interface.go @@ -0,0 +1,59 @@ +package bchain + +import ( + "context" + "math/big" +) + +// EVMClient provides the necessary client functionality for evm chain sync +type EVMClient interface { + NetworkID(ctx context.Context) (*big.Int, error) + HeaderByNumber(ctx context.Context, number *big.Int) (EVMHeader, error) + SuggestGasPrice(ctx context.Context) (*big.Int, error) + EstimateGas(ctx context.Context, msg interface{}) (uint64, error) + BalanceAt(ctx context.Context, addrDesc AddressDescriptor, blockNumber *big.Int) (*big.Int, error) + NonceAt(ctx context.Context, addrDesc AddressDescriptor, blockNumber *big.Int) (uint64, error) +} + +// EVMRPCClient provides the necessary rpc functionality for evm chain sync +type EVMRPCClient interface { + EthSubscribe(ctx context.Context, channel interface{}, args ...interface{}) (EVMClientSubscription, error) + CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error + Close() +} + +// EVMHeader provides access to the necessary header data for evm chain sync +type EVMHeader interface { + Hash() string + Number() *big.Int + Difficulty() *big.Int +} + +// EVMHash provides access to the necessary hash data for evm chain sync +type EVMHash interface { + Hex() string +} + +// EVMClientSubscription provides interaction with an evm client subscription +type EVMClientSubscription interface { + Err() <-chan error + Unsubscribe() +} + +// EVMSubscriber provides interaction with a subscription channel +type EVMSubscriber interface { + Channel() interface{} + Close() +} + +// EVMNewBlockSubscriber provides interaction with a new block subscription channel +type EVMNewBlockSubscriber interface { + EVMSubscriber + Read() (EVMHeader, bool) +} + +// EVMNewBlockSubscriber provides interaction with a new tx subscription channel +type EVMNewTxSubscriber interface { + EVMSubscriber + Read() (EVMHash, bool) +}