Skip to content

Commit

Permalink
Devnet contract utils (#7928)
Browse files Browse the repository at this point in the history
This request is extending the devnet functionality to more fully handle
contract processing by adding support for the following calls:
 * trace_call,
* trace_transaction
* debug_accountAt,
* eth_getCode
* eth_estimateGas
* eth_gasPrice

It also contains an initial rationalization of the devnet subscription
code to use the erigon client code directly rather than using its own
intermediate subscription management.

This is used to implement a general purpose block waiter - which can be
used in any scenario step - rather than being specific to transaction
processing.

This pull also contains an end to end tested sync processor for bor and
associated support services:
* Heimdall (supports sync event transfer)
* Faucet - allows the creation and funding of arbitary test specific
accounts (cross chain)

Notes and Caveats:
 
* Code generation for contracts requires `--evm-version paris`. For
chains which don't support push0 for solc over 0.8.19
* The bor log processing post the application of sync events causes a
panic - this will be the subject of a seperate smaller push as it is not
devnet specific
* The bor code seems to make repeated calls for the same sync events and
also reverts requests - this needs further investigation. This is the
behaviour of the current implementation and may be required - although
it does seem to generate repeat processing - which could be avoided.
  • Loading branch information
mh0lt authored Jul 28, 2023
1 parent 7d35c6b commit cc74e74
Show file tree
Hide file tree
Showing 43 changed files with 1,929 additions and 451 deletions.
4 changes: 4 additions & 0 deletions cmd/devnet/accounts/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ func NewAccount(name string) *Account {
return account
}

func (a *Account) SigKey() *ecdsa.PrivateKey {
return a.sigKey
}

func GetAccount(account string) *Account {
if account, ok := accountsByName[account]; ok {
return account
Expand Down
101 changes: 91 additions & 10 deletions cmd/devnet/accounts/steps/steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@ package accounts_steps

import (
"context"
"encoding/json"
"fmt"
"math/big"

libcommon "github.com/ledgerwatch/erigon-lib/common"

"github.com/ledgerwatch/erigon/accounts/abi/bind"
"github.com/ledgerwatch/erigon/cmd/devnet/accounts"
"github.com/ledgerwatch/erigon/cmd/devnet/devnet"
"github.com/ledgerwatch/erigon/cmd/devnet/requests"
"github.com/ledgerwatch/erigon/cmd/devnet/scenarios"
"github.com/ledgerwatch/erigon/cmd/devnet/services"
"github.com/ledgerwatch/erigon/cmd/devnet/transactions"
"github.com/ledgerwatch/erigon/turbo/adapter/ethapi"
)

func init() {
Expand Down Expand Up @@ -56,31 +60,108 @@ func SendFunds(ctx context.Context, chainName string, name string, ethAmount flo
return 0, fmt.Errorf("Unknown account: %s", name)
}

//if fbal, err := faucet.Balance(chainCtx); err == nil {
// fmt.Println(faucet.Address(), fbal)
//}
facuetStartingBalance, _ := faucet.Balance(chainCtx)

_, hash, err := faucet.Send(chainCtx, account.Address, ethAmount)
sent, hash, err := faucet.Send(chainCtx, account, ethAmount)

if err != nil {
return 0, err
}

if _, err = transactions.AwaitTransactions(chainCtx, hash); err != nil {
blockMap, err := transactions.AwaitTransactions(chainCtx, hash)

if err != nil {
return 0, fmt.Errorf("Failed to get transfer tx: %w", err)
}

//if fbal, err := faucet.Balance(chainCtx); err == nil {
// fmt.Println(faucet.Address(), fbal)
//}
blockNum, _ := blockMap[hash]

logs, err := faucet.Contract().FilterSent(&bind.FilterOpts{
Start: blockNum,
End: &blockNum,
})

if err != nil {
return 0, fmt.Errorf("Failed to get post transfer logs: %w", err)
}

sendConfirmed := false

for logs.Next() {
if account.Address != logs.Event.Destination {
return 0, fmt.Errorf("Unexpected send destination: %s", logs.Event.Destination)
}

if sent.Cmp(logs.Event.Amount) != 0 {
return 0, fmt.Errorf("Unexpected send amount: %s", logs.Event.Amount)
}

sendConfirmed = true
}

node := devnet.SelectBlockProducer(chainCtx)
balance, err := node.GetBalance(account.Address, requests.BlockNumbers.Latest)
//fmt.Println(account.Address, balance)

if !sendConfirmed {
logger := devnet.Logger(chainCtx)

traceResults, err := node.TraceTransaction(hash)

if err != nil {
return 0, fmt.Errorf("Send transaction failure: transaction trace failed: %w", err)
}

for _, traceResult := range traceResults {
accountResult, err := node.DebugAccountAt(traceResult.BlockHash, traceResult.TransactionPosition, faucet.Address())

if err != nil {
return 0, fmt.Errorf("Send transaction failure: account debug failed: %w", err)
}

logger.Info("Faucet account details", "address", faucet.Address(), "account", accountResult)

accountCode, err := node.GetCode(faucet.Address(), requests.BlockNumber(traceResult.BlockHash.Hex()))

if err != nil {
return 0, fmt.Errorf("Send transaction failure: get account code failed: %w", err)
}

logger.Info("Faucet account code", "address", faucet.Address(), "code", accountCode)

callResults, err := node.TraceCall(fmt.Sprintf("0x%x", blockNum), ethapi.CallArgs{
From: &traceResult.Action.From,
To: &traceResult.Action.To,
Data: &traceResult.Action.Input,
}, requests.TraceOpts.StateDiff, requests.TraceOpts.Trace, requests.TraceOpts.VmTrace)

if err != nil {
return 0, fmt.Errorf("Send transaction failure: trace call failed: %w", err)
}

results, _ := json.MarshalIndent(callResults, " ", " ")
logger.Debug("Send transaction call trace", "hash", hash, "trace", string(results))
}
}

balance, err := faucet.Balance(chainCtx)

if err != nil {
return 0, fmt.Errorf("Failed to get post transfer faucet balance: %w", err)
}

if balance.Cmp((&big.Int{}).Sub(facuetStartingBalance, sent)) != 0 {
return 0, fmt.Errorf("Unexpected post transfer faucet balance got: %s:, expected: %s", balance, (&big.Int{}).Sub(facuetStartingBalance, sent))
}

balance, err = node.GetBalance(account.Address, requests.BlockNumbers.Latest)

if err != nil {
return 0, fmt.Errorf("Failed to get post transfer balance: %w", err)
}

if balance.Cmp(sent) != 0 {
return 0, fmt.Errorf("Unexpected post transfer balance got: %s:, expected: %s", balance, sent)
}

return balance.Uint64(), nil
}

Expand Down
2 changes: 2 additions & 0 deletions cmd/devnet/args/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type Node struct {
PrivateApiAddr string `arg:"--private.api.addr" default:"localhost:9090" json:"private.api.addr"`
HttpPort int `arg:"--http.port" default:"8545" json:"http.port"`
HttpVHosts string `arg:"--http.vhosts" json:"http.vhosts"`
HttpCorsDomain string `arg:"--http.corsdomain" json:"http.corsdomain"`
AuthRpcPort int `arg:"--authrpc.port" default:"8551" json:"authrpc.port"`
AuthRpcVHosts string `arg:"--authrpc.vhosts" json:"authrpc.vhosts"`
WSPort int `arg:"-" default:"8546" json:"-"` // flag not defined
Expand All @@ -43,6 +44,7 @@ type Node struct {
StaticPeers string `arg:"--staticpeers" json:"staticpeers,omitempty"`
WithoutHeimdall bool `arg:"--bor.withoutheimdall" flag:"" default:"false" json:"bor.withoutheimdall,omitempty"`
HeimdallGRpc string `arg:"--bor.heimdallgRPC" json:"bor.heimdallgRPC,omitempty"`
VMDebug bool `arg:"--vmdebug" flag:"" default:"false" json:"dmdebug"`
}

func (node *Node) configure(base Node, nodeNumber int) error {
Expand Down
32 changes: 32 additions & 0 deletions cmd/devnet/blocks/checks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package blocks

import (
"context"
"fmt"

libcommon "github.com/ledgerwatch/erigon-lib/common"
"github.com/ledgerwatch/erigon/cmd/devnet/devnet"
"github.com/ledgerwatch/erigon/cmd/devnet/requests"
)

var CompletionChecker = BlockHandlerFunc(
func(ctx context.Context, node devnet.Node, block *requests.BlockResult, transaction *requests.Transaction) error {
transactionHash := libcommon.HexToHash(transaction.Hash)
traceResults, err := node.TraceTransaction(transactionHash)

if err != nil {
return fmt.Errorf("Failed to trace transaction: %s: %w", transaction.Hash, err)
}

for _, traceResult := range traceResults {
if traceResult.TransactionHash == transactionHash {
if len(traceResult.Error) != 0 {
return fmt.Errorf("Transaction error: %s", traceResult.Error)
}

break
}
}

return nil
})
25 changes: 25 additions & 0 deletions cmd/devnet/blocks/fees.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package blocks

import (
"context"
"fmt"

"github.com/ledgerwatch/erigon/cmd/devnet/devnet"
"github.com/ledgerwatch/erigon/cmd/devnet/devnetutils"
)

func BaseFeeFromBlock(ctx context.Context) (uint64, error) {
var val uint64
res, err := devnet.SelectNode(ctx).GetBlockDetailsByNumber("latest", false)
if err != nil {
return 0, fmt.Errorf("failed to get base fee from block: %v\n", err)
}

if v, ok := res["baseFeePerGas"]; !ok {
return val, fmt.Errorf("baseFeePerGas field missing from response")
} else {
val = devnetutils.HexToInt(v.(string))
}

return val, err
}
171 changes: 171 additions & 0 deletions cmd/devnet/blocks/waiter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package blocks

import (
"context"

ethereum "github.com/ledgerwatch/erigon"
libcommon "github.com/ledgerwatch/erigon-lib/common"
"github.com/ledgerwatch/erigon/cmd/devnet/devnet"
"github.com/ledgerwatch/erigon/cmd/devnet/requests"
"github.com/ledgerwatch/erigon/core/types"
"github.com/ledgerwatch/log/v3"
)

type BlockHandler interface {
Handle(ctx context.Context, node devnet.Node, block *requests.BlockResult, transaction *requests.Transaction) error
}

type BlockHandlerFunc func(ctx context.Context, node devnet.Node, block *requests.BlockResult, transaction *requests.Transaction) error

func (f BlockHandlerFunc) Handle(ctx context.Context, node devnet.Node, block *requests.BlockResult, transaction *requests.Transaction) error {
return f(ctx, node, block, transaction)
}

type BlockMap map[libcommon.Hash]*requests.BlockResult

type waitResult struct {
err error
blockMap BlockMap
}

type blockWaiter struct {
result chan waitResult
hashes chan map[libcommon.Hash]struct{}
waitHashes map[libcommon.Hash]struct{}
headersSub ethereum.Subscription
handler BlockHandler
logger log.Logger
}

type Waiter interface {
Await(libcommon.Hash) (*requests.BlockResult, error)
AwaitMany(...libcommon.Hash) (BlockMap, error)
}

type waitError struct {
err error
}

func (w waitError) Await(libcommon.Hash) (*requests.BlockResult, error) {
return nil, w.err
}

func (w waitError) AwaitMany(...libcommon.Hash) (BlockMap, error) {
return nil, w.err
}

type wait struct {
waiter *blockWaiter
}

func (w wait) Await(hash libcommon.Hash) (*requests.BlockResult, error) {
w.waiter.hashes <- map[libcommon.Hash]struct{}{hash: {}}
res := <-w.waiter.result

if len(res.blockMap) > 0 {
for _, block := range res.blockMap {
return block, res.err
}
}

return nil, res.err
}

func (w wait) AwaitMany(hashes ...libcommon.Hash) (BlockMap, error) {
if len(hashes) == 0 {
return nil, nil
}

hashMap := map[libcommon.Hash]struct{}{}

for _, hash := range hashes {
hashMap[hash] = struct{}{}
}

w.waiter.hashes <- hashMap

res := <-w.waiter.result
return res.blockMap, res.err
}

func BlockWaiter(ctx context.Context, handler BlockHandler) (Waiter, context.CancelFunc) {
ctx, cancel := context.WithCancel(ctx)

node := devnet.SelectBlockProducer(ctx)

waiter := &blockWaiter{
result: make(chan waitResult, 1),
hashes: make(chan map[libcommon.Hash]struct{}, 1),
handler: handler,
logger: devnet.Logger(ctx),
}

var err error

headers := make(chan types.Header)
waiter.headersSub, err = node.Subscribe(ctx, requests.Methods.ETHNewHeads, headers)

if err != nil {
defer close(waiter.result)
return waitError{err}, cancel
}

go waiter.receive(ctx, node, headers)

return wait{waiter}, cancel
}

func (c *blockWaiter) receive(ctx context.Context, node devnet.Node, headers chan types.Header) {
blockMap := map[libcommon.Hash]*requests.BlockResult{}

defer close(c.result)

for header := range headers {
select {
case <-ctx.Done():
c.headersSub.Unsubscribe()
c.result <- waitResult{blockMap: blockMap, err: ctx.Err()}
return
default:
}

blockNum := header.Number

block, err := node.GetBlockByNumber(blockNum.Uint64(), true)

if err != nil {
c.logger.Error("Block waiter failed to get block", "err", err)
continue
}

if len(block.Transactions) > 0 && c.waitHashes == nil {
c.waitHashes = <-c.hashes
}

for i := range block.Transactions {
tx := &block.Transactions[i] // avoid implicit memory aliasing

txHash := libcommon.HexToHash(tx.Hash)

if _, ok := c.waitHashes[txHash]; ok {
c.logger.Info("Tx included into block", "txHash", txHash, "blockNum", block.BlockNumber)
blockMap[txHash] = block
delete(c.waitHashes, txHash)

if len(c.waitHashes) == 0 {
c.headersSub.Unsubscribe()
res := waitResult{
err: c.handler.Handle(ctx, node, block, tx),
}

if res.err == nil {
res.blockMap = blockMap
}

c.result <- res
return
}
}
}
}
}
Loading

0 comments on commit cc74e74

Please sign in to comment.