From d1fb4c095f89f19d08551b131c1f6a844678eac3 Mon Sep 17 00:00:00 2001 From: Konstantin Zolotarev Date: Wed, 22 Jan 2025 16:55:32 +0200 Subject: [PATCH 1/4] Fast forward flashbots integration --- cmd/challenger/main.go | 31 ++++++++++++++++++++--- core/scribe_optimistic_provider.go | 40 ++++++++++++++++++++++++++---- 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/cmd/challenger/main.go b/cmd/challenger/main.go index a704786..e238d9f 100644 --- a/cmd/challenger/main.go +++ b/cmd/challenger/main.go @@ -26,13 +26,13 @@ import ( "sync" challenger "github.com/chronicleprotocol/challenger/core" - "github.com/defiweb/go-eth/txmodifier" - "github.com/defiweb/go-eth/wallet" logger "github.com/sirupsen/logrus" "github.com/defiweb/go-eth/rpc" "github.com/defiweb/go-eth/rpc/transport" + "github.com/defiweb/go-eth/txmodifier" "github.com/defiweb/go-eth/types" + "github.com/defiweb/go-eth/wallet" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/spf13/cobra" @@ -48,6 +48,7 @@ type options struct { Password string PasswordFile string RpcURL string + FlashbotRpcURL string SubscriptionURL string Address []string FromBlock uint64 @@ -144,6 +145,12 @@ func main() { logger.Fatalf("Failed to create transport: %v", err) } + // flashbot transport + flashbotTransport, err := transport.NewHTTP(transport.HTTPOptions{URL: opts.FlashbotRpcURL}) + if err != nil { + logger.Fatalf("Failed to create transport: %v", err) + } + // Key generation key, err := opts.getKey() if err != nil { @@ -194,6 +201,7 @@ func main() { logger.Fatalf("Unknown transaction type: %s. Have to be legacy, eip1559 or none", opts.TransactionType) } + // Create a JSON-RPC client to mainnet. clientOptions := []rpc.ClientOptions{ rpc.WithTransport(t), rpc.WithKeys(key), @@ -201,17 +209,31 @@ func main() { rpc.WithTXModifiers(txModifiers...), } - // Create a JSON-RPC client. client, err := rpc.NewClient(clientOptions...) if err != nil { logger.Fatalf("Failed to create RPC client: %v", err) } + // Create a JSON-RPC client to flashbot. + // TODO: tx modifiers have to be similar ? + flashbotClientOptions := []rpc.ClientOptions{ + rpc.WithTransport(flashbotTransport), + rpc.WithKeys(key), + rpc.WithDefaultAddress(key.Address()), + rpc.WithTXModifiers(txModifiers...), + } + + flashbotClient, err := rpc.NewClient(flashbotClientOptions...) + if err != nil { + logger.Fatalf("Failed to create RPC client: %v", err) + } + + // Spawning "challenger" for each address var wg sync.WaitGroup for _, address := range addresses { wg.Add(1) - p := challenger.NewScribeOptimisticRpcProvider(client) + p := challenger.NewScribeOptimisticRpcProvider(client, flashbotClient) c := challenger.NewChallenger(ctx, address, p, 0, opts.SubscriptionURL, &wg) go func(addr types.Address) { @@ -251,6 +273,7 @@ func main() { cmd.PersistentFlags().StringVar(&opts.Password, "password", "", "Key raw password as text") cmd.PersistentFlags().StringVar(&opts.PasswordFile, "password-file", "", "Path to key password file") cmd.PersistentFlags().StringVar(&opts.RpcURL, "rpc-url", "", "Node HTTP RPC_URL, normally starts with https://****") + cmd.PersistentFlags().StringVar(&opts.FlashbotRpcURL, "flashbot-rpc-url", "", "Flashbot Node HTTP RPC_URL, normally starts with https://****") cmd.PersistentFlags().StringVar(&opts.SubscriptionURL, "subscription-url", "", "[Optional] Used if you want to subscribe to events rather than poll, typically starts with wss://****") cmd.PersistentFlags().StringArrayVarP(&opts.Address, "addresses", "a", []string{}, "ScribeOptimistic contract address. Example: `0x891E368fE81cBa2aC6F6cc4b98e684c106e2EF4f`") cmd.PersistentFlags().Uint64Var(&opts.FromBlock, "from-block", 0, "Block number to start from. If not provided, binary will try to get it from given RPC") diff --git a/core/scribe_optimistic_provider.go b/core/scribe_optimistic_provider.go index cd9dc8c..d19df12 100644 --- a/core/scribe_optimistic_provider.go +++ b/core/scribe_optimistic_provider.go @@ -18,6 +18,7 @@ package core import ( "context" _ "embed" + "errors" "fmt" "math/big" @@ -27,6 +28,10 @@ import ( logger "github.com/sirupsen/logrus" ) +// MaxChallengeRetries is the maximum number of retries to send a transaction. +// NOTE: first try with flashbots, if it fails, try with mainnet client. +const MaxChallengeRetries = 6 + //go:embed ScribeOptimistic.json var scribeOptimisticContractJSON []byte @@ -35,13 +40,17 @@ var ScribeOptimisticContractABI = abi.MustParseJSON(scribeOptimisticContractJSON // ScribeOptimisticRpcProvider implements IScribeOptimisticProvider interface and provides functionality to interact with ScribeOptimistic contract. type ScribeOptimisticRpcProvider struct { - client *rpc.Client + client *rpc.Client + flashbotClient *rpc.Client } // NewScribeOptimisticRpcProvider creates a new instance of ScribeOptimisticRpcProvider. -func NewScribeOptimisticRpcProvider(client *rpc.Client) *ScribeOptimisticRpcProvider { +// Two clients are required: one for the mainnet and one for the flashbots relay. +// Logic is simple, try to send with flashbots first, if it fails, send with the mainnet client. +func NewScribeOptimisticRpcProvider(client *rpc.Client, flashbotClient *rpc.Client) *ScribeOptimisticRpcProvider { return &ScribeOptimisticRpcProvider{ - client: client, + client: client, + flashbotClient: flashbotClient, } } @@ -238,12 +247,13 @@ func (s *ScribeOptimisticRpcProvider) IsPokeSignatureValid(ctx context.Context, } // ChallengePoke challenges the given poke by sending transaction for `opChallenge` contract function. +// Makes several attempts to send a transaction, first with flashbots, then with the mainnet client. func (s *ScribeOptimisticRpcProvider) ChallengePoke(ctx context.Context, address types.Address, poke *OpPokedEvent) (*types.Hash, *types.Transaction, error) { opChallenge := ScribeOptimisticContractABI.Methods["opChallenge"] calldata, err := opChallenge.EncodeArgs(poke.Schnorr) if err != nil { - return nil, nil, fmt.Errorf("failed to encode opChallenge args: %v", err) + return nil, nil, fmt.Errorf("failed to encode opChallenge args: %w", err) } // Prepare a transaction. @@ -251,5 +261,25 @@ func (s *ScribeOptimisticRpcProvider) ChallengePoke(ctx context.Context, address SetTo(address). SetInput(calldata) - return s.client.SendTransaction(ctx, tx) + var errs []error + for i := 0; i < MaxChallengeRetries; i++ { + if i <= MaxChallengeRetries/2 { + // Try to send with flashbots first. + hash, tx, err := s.flashbotClient.SendTransaction(ctx, tx) + if err == nil { + return hash, tx, nil + } + errs = append(errs, fmt.Errorf("try: %d failed to send tx with flashbots: %w", i, err)) + } else { + // Try to send with the mainnet client. + hash, tx, err := s.client.SendTransaction(ctx, tx) + if err == nil { + return hash, tx, nil + } + errs = append(errs, fmt.Errorf("try: %d failed to send tx with mainnet: %w", i, err)) + } + i++ + } + + return nil, nil, fmt.Errorf("failed to send challenge transaction: %w", errors.Join(errs...)) } From 44bc5fbc1d788f5ea2059a949d382208b5d147d3 Mon Sep 17 00:00:00 2001 From: Konstantin Zolotarev Date: Wed, 5 Mar 2025 22:11:41 +0200 Subject: [PATCH 2/4] Added flashbots RPC support with manual gas limit --- README.md | 6 +- cmd/challenger/main.go | 72 +++++++----- core/challenger.go | 141 +++++++++++++++-------- core/client.go | 24 ++++ core/scribe_optimistic_provider.go | 174 +++++++++++++++++++++-------- core/utils.go | 56 ++++++++++ 6 files changed, 352 insertions(+), 121 deletions(-) create mode 100644 core/client.go create mode 100644 core/utils.go diff --git a/README.md b/README.md index f3abc6c..d02d9a9 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ Aliases: Flags: -a, --addresses 0x891E368fE81cBa2aC6F6cc4b98e684c106e2EF4f ScribeOptimistic contract address. Example: 0x891E368fE81cBa2aC6F6cc4b98e684c106e2EF4f --chain-id uint If no chain_id provided binary will try to get chain_id from given RPC - --from-block uint Block number to start from. If not provided, binary will try to get it from given RPC + --flashbot-rpc-url string Flashbot Node HTTP RPC_URL, normally starts with https://**** + --from-block int Block number to start from. If not provided, binary will try to get it from given RPC -h, --help help for run --keystore string Keystore file (NOT FOLDER), path to key .json file. If provided, no need to use --secret-key --password string Key raw password as text @@ -20,7 +21,8 @@ Flags: --rpc-url string Node HTTP RPC_URL, normally starts with https://**** --secret-key 0x****** Private key in format 0x****** or `*******`. If provided, no need to use --keystore --subscription-url string [Optional] Used if you want to subscribe to events rather than poll, typically starts with wss://**** - --tx-type legacy Transaction type definition, possible values are: `legacy`, `eip1559` or `none` (default "none") + --tx-type legacy Transaction type definition, possible values are: legacy, `eip1559` or `none` (default "none") + ``` Note that in *all* cases you must provide `--rpc-url`, but if you want to use event driven listening instead of polling you also need to provide `--subscription-url`. diff --git a/cmd/challenger/main.go b/cmd/challenger/main.go index e238d9f..e6c4dd3 100644 --- a/cmd/challenger/main.go +++ b/cmd/challenger/main.go @@ -51,7 +51,7 @@ type options struct { FlashbotRpcURL string SubscriptionURL string Address []string - FromBlock uint64 + FromBlock int64 ChainID uint64 TransactionType string } @@ -140,17 +140,6 @@ func main() { defer ctxCancel() } - t, err := transport.NewHTTP(transport.HTTPOptions{URL: opts.RpcURL}) - if err != nil { - logger.Fatalf("Failed to create transport: %v", err) - } - - // flashbot transport - flashbotTransport, err := transport.NewHTTP(transport.HTTPOptions{URL: opts.FlashbotRpcURL}) - if err != nil { - logger.Fatalf("Failed to create transport: %v", err) - } - // Key generation key, err := opts.getKey() if err != nil { @@ -159,10 +148,6 @@ func main() { // Basic TX modifiers txModifiers := []rpc.TXModifier{ - txmodifier.NewGasLimitEstimator(txmodifier.GasLimitEstimatorOptions{ - MaxGas: maxGasLimit, - Multiplier: defaultGasLimitMultiplier, - }), txmodifier.NewNonceProvider(txmodifier.NonceProviderOptions{ UsePendingBlock: false, Replace: false, @@ -202,11 +187,23 @@ func main() { } // Create a JSON-RPC client to mainnet. + t, err := transport.NewHTTP(transport.HTTPOptions{URL: opts.RpcURL}) + if err != nil { + logger.Fatalf("Failed to create transport: %v", err) + } + + // Set manual gas limit for flashbots, they might require more gas. + //nolint:gocritic + baseTxModifiers := append(txModifiers, txmodifier.NewGasLimitEstimator(txmodifier.GasLimitEstimatorOptions{ + MaxGas: maxGasLimit, + Multiplier: defaultGasLimitMultiplier, + })) + clientOptions := []rpc.ClientOptions{ rpc.WithTransport(t), rpc.WithKeys(key), rpc.WithDefaultAddress(key.Address()), - rpc.WithTXModifiers(txModifiers...), + rpc.WithTXModifiers(baseTxModifiers...), } client, err := rpc.NewClient(clientOptions...) @@ -215,17 +212,33 @@ func main() { } // Create a JSON-RPC client to flashbot. - // TODO: tx modifiers have to be similar ? - flashbotClientOptions := []rpc.ClientOptions{ - rpc.WithTransport(flashbotTransport), - rpc.WithKeys(key), - rpc.WithDefaultAddress(key.Address()), - rpc.WithTXModifiers(txModifiers...), - } + var flashbotClient *rpc.Client + if opts.FlashbotRpcURL != "" { + flashbotTransport, err := transport.NewHTTP(transport.HTTPOptions{URL: opts.FlashbotRpcURL}) + if err != nil { + logger.Fatalf("Failed to create transport: %v", err) + } - flashbotClient, err := rpc.NewClient(flashbotClientOptions...) - if err != nil { - logger.Fatalf("Failed to create RPC client: %v", err) + // Set manual gas limit for flashbots, they might require more gas. + //nolint:gocritic + flashbotTxModifiers := append(txModifiers, txmodifier.NewGasLimitEstimator(txmodifier.GasLimitEstimatorOptions{ + MaxGas: challenger.MaxFlashbotGasLimit, + Multiplier: defaultGasLimitMultiplier, + Replace: false, + })) + + // TODO: tx modifiers have to be similar ? + flashbotClientOptions := []rpc.ClientOptions{ + rpc.WithTransport(flashbotTransport), + rpc.WithKeys(key), + rpc.WithDefaultAddress(key.Address()), + rpc.WithTXModifiers(flashbotTxModifiers...), + } + + flashbotClient, err = rpc.NewClient(flashbotClientOptions...) + if err != nil { + logger.Fatalf("Failed to create RPC client: %v", err) + } } // Spawning "challenger" for each address @@ -234,7 +247,7 @@ func main() { wg.Add(1) p := challenger.NewScribeOptimisticRpcProvider(client, flashbotClient) - c := challenger.NewChallenger(ctx, address, p, 0, opts.SubscriptionURL, &wg) + c := challenger.NewChallenger(ctx, address, p, opts.FromBlock, opts.SubscriptionURL, &wg) go func(addr types.Address) { err := c.Run() @@ -276,7 +289,8 @@ func main() { cmd.PersistentFlags().StringVar(&opts.FlashbotRpcURL, "flashbot-rpc-url", "", "Flashbot Node HTTP RPC_URL, normally starts with https://****") cmd.PersistentFlags().StringVar(&opts.SubscriptionURL, "subscription-url", "", "[Optional] Used if you want to subscribe to events rather than poll, typically starts with wss://****") cmd.PersistentFlags().StringArrayVarP(&opts.Address, "addresses", "a", []string{}, "ScribeOptimistic contract address. Example: `0x891E368fE81cBa2aC6F6cc4b98e684c106e2EF4f`") - cmd.PersistentFlags().Uint64Var(&opts.FromBlock, "from-block", 0, "Block number to start from. If not provided, binary will try to get it from given RPC") + cmd.PersistentFlags(). + Int64Var(&opts.FromBlock, "from-block", 0, "Block number to start from. If not provided, binary will try to get it from given RPC") cmd.PersistentFlags().Uint64Var(&opts.ChainID, "chain-id", 0, "If no chain_id provided binary will try to get chain_id from given RPC") cmd.PersistentFlags().StringVar(&opts.TransactionType, "tx-type", "none", "Transaction type definition, possible values are: `legacy`, `eip1559` or `none`") diff --git a/core/challenger.go b/core/challenger.go index 1a73687..9ced4a3 100644 --- a/core/challenger.go +++ b/core/challenger.go @@ -40,7 +40,6 @@ const OpPokedEventSig = "0xb9dc937c5e394d0c8f76e0e324500b88251b4c909ddc56232df10 type Challenger struct { ctx context.Context address types.Address - fromBlock uint64 subscriptionURL string provider IScribeOptimisticProvider lastProcessedBlock *big.Int @@ -52,23 +51,27 @@ func NewChallenger( ctx context.Context, address types.Address, provider IScribeOptimisticProvider, - fromBlock uint64, + fromBlock int64, subscriptionURL string, wg *sync.WaitGroup, ) *Challenger { + var latestBlock *big.Int + if fromBlock != 0 { + latestBlock = big.NewInt(fromBlock) + } return &Challenger{ - ctx: ctx, - address: address, - provider: provider, - fromBlock: fromBlock, - wg: wg, - subscriptionURL: subscriptionURL, + ctx: ctx, + address: address, + provider: provider, + lastProcessedBlock: latestBlock, + wg: wg, + subscriptionURL: subscriptionURL, } } // Gets earliest block number we can look `OpPoked` events from. func (c *Challenger) getEarliestBlockNumber(lastBlock *big.Int, period uint16) *big.Int { - // Calculate earliest block number. + // Calculate the earliest block number. blocksPerPeriod := uint64(period) / slotPeriodInSec if lastBlock.Cmp(big.NewInt(int64(blocksPerPeriod))) == -1 { return big.NewInt(0) @@ -92,33 +95,70 @@ func (c *Challenger) getFromBlockNumber(latestBlockNumber *big.Int, period uint1 func (c *Challenger) isPokeChallengeable(poke *OpPokedEvent, challengePeriod uint16) bool { if poke == nil || poke.BlockNumber == nil { - logger.Info("OpPoked or block number is nil") + logger. + WithField("address", c.address). + Info("OpPoked or block number is nil") return false } block, err := c.provider.BlockByNumber(c.ctx, poke.BlockNumber) if err != nil { - logger.Errorf("Failed to get block by number %d with error: %v", poke.BlockNumber, err) + logger. + WithField("address", c.address). + Errorf("Failed to get block by number %d with error: %v", poke.BlockNumber, err) return false } challengeableSince := time.Now().Add(-time.Second * time.Duration(challengePeriod)) // Not challengeable by time if block.Timestamp.Before(challengeableSince) { - logger.Infof("Not challengeable by time %v", challengeableSince) + logger. + WithField("address", c.address). + Infof("Not challengeable by time %v", challengeableSince) return false } valid, err := c.provider.IsPokeSignatureValid(c.ctx, c.address, poke) if err != nil { - logger.Errorf("Failed to verify OpPoked signature with error: %v", err) + logger. + WithField("address", c.address). + Errorf("Failed to verify OpPoked signature with error: %v", err) return false } - logger.Infof("Is opPoke signature valid? %v", valid) + logger. + WithField("address", c.address). + Infof("Is opPoke signature valid? %v", valid) // Only challengeable if signature is not valid return !valid } +// SpawnChallenge spawns new goroutine and challenges the `OpPoked` event. +func (c *Challenger) SpawnChallenge(poke *OpPokedEvent) { + go func() { + logger. + WithField("address", c.address). + Warnf("Challenging OpPoked event from block %v", poke.BlockNumber) + txHash, _, err := c.provider.ChallengePoke(c.ctx, c.address, poke) + if err != nil { + logger. + WithField("address", c.address). + Errorf("failed to challenge OpPoked event from block %v with error: %v", poke.BlockNumber, err) + return + } + logger. + WithField("address", c.address). + WithField("txHash", txHash). + Infof("Challenge successful") + + // Adding metrics + ChallengeCounter.WithLabelValues( + c.address.String(), + c.provider.GetFrom(c.ctx).String(), + txHash.String(), + ).Inc() + }() +} + func (c *Challenger) executeTick() error { latestBlockNumber, err := c.provider.BlockNumber(c.ctx) if err != nil { @@ -136,7 +176,9 @@ func (c *Challenger) executeTick() error { return fmt.Errorf("failed to get blocknumber from period: %v", err) } - logger.Debugf("[%v] Block number to start with: %d", c.address, fromBlockNumber) + logger. + WithField("address", c.address). + Debugf("Block number to start with: %d", fromBlockNumber) pokeLogs, err := c.provider.GetPokes(c.ctx, c.address, fromBlockNumber, latestBlockNumber) if err != nil { @@ -151,7 +193,9 @@ func (c *Challenger) executeTick() error { LastScannedBlockGauge.WithLabelValues(c.address.String(), c.provider.GetFrom(c.ctx).String()).Set(asFloat64) if len(pokeLogs) == 0 { - logger.Debugf("No logs found") + logger. + WithField("address", c.address). + Debugf("No logs found") return nil } @@ -164,22 +208,13 @@ func (c *Challenger) executeTick() error { for _, poke := range pokes { if !c.isPokeChallengeable(poke, period) { - logger.Debugf("Event from block %v is not challengeable", poke.BlockNumber) + logger. + WithField("address", c.address). + Debugf("Event from block %v is not challengeable", poke.BlockNumber) continue } - logger.Warnf("Challenging OpPoked event from block %v", poke.BlockNumber) - txHash, _, err := c.provider.ChallengePoke(c.ctx, c.address, poke) - if err != nil { - return fmt.Errorf("failed to challenge OpPoked event from block %v with error: %v", poke.BlockNumber, err) - } - logger.Infof("Challenge transaction hash: %v", txHash.String()) - // Adding metrics - ChallengeCounter.WithLabelValues( - c.address.String(), - c.provider.GetFrom(c.ctx).String(), - txHash.String(), - ).Inc() + c.SpawnChallenge(poke) } return nil @@ -193,7 +228,9 @@ func (c *Challenger) Run() error { // Executing first tick err := c.executeTick() if err != nil { - logger.Errorf("Failed to execute tick with error: %v", err) + logger. + WithField("address", c.address). + Errorf("Failed to execute tick with error: %v", err) // Add error to metrics ErrorsCounter.WithLabelValues( @@ -203,7 +240,9 @@ func (c *Challenger) Run() error { ).Inc() } - logger.Infof("Monitoring contract %v", c.address) + logger. + WithField("address", c.address). + Infof("Started contract monitoring") if c.subscriptionURL == "" { // We poll ticker := time.NewTicker(30 * time.Second) @@ -211,15 +250,21 @@ func (c *Challenger) Run() error { for { select { case <-c.ctx.Done(): - logger.Infof("Terminate challenger") + logger. + WithField("address", c.address). + Infof("Terminate challenger") return nil case t := <-ticker.C: - logger.Debugf("Tick at: %v", t) + logger. + WithField("address", c.address). + Debugf("Tick at: %v", t) err := c.executeTick() if err != nil { - logger.Errorf("Failed to execute tick with error: %v", err) + logger. + WithField("address", c.address). + Errorf("Failed to execute tick with error: %v", err) // Add error to metrics ErrorsCounter.WithLabelValues( c.address.String(), @@ -237,7 +282,9 @@ func (c *Challenger) Run() error { // Listen listens for `OpPoked` events from WS connection and challenges them if they are challengeable. // It requires you to provide WS connection to challenger. func (c *Challenger) Listen() error { - logger.Infof("Listening for events from %v", c.address) + logger. + WithField("address", c.address). + Infof("Listening for events from %v", c.address) ethcli, err := ethclient.Dial(c.subscriptionURL) if err != nil { return err @@ -258,13 +305,17 @@ func (c *Challenger) Listen() error { for { select { case <-c.ctx.Done(): - logger.Infof("Terminate challenger") + logger. + WithField("address", c.address). + Infof("Terminate challenger") return nil case err := <-sub.Err(): return err case evlog := <-logs: if evlog.Topics[0].Hex() != OpPokedEventSig { - logger.Infof("Event occurred, but is not 'opPoked': %s", evlog.Topics[0].Hex()) + logger. + WithField("address", c.address). + Infof("Event occurred, but is not 'opPoked': %s", evlog.Topics[0].Hex()) continue } @@ -273,7 +324,9 @@ func (c *Challenger) Listen() error { if err != nil { return err } - logger.Infof("opPoked event for %v", addr) + logger. + WithField("address", c.address). + Infof("opPoked event for %v", addr) topics := make([]types.Hash, 0) for _, topic := range evlog.Topics { @@ -301,15 +354,11 @@ func (c *Challenger) Listen() error { } if c.isPokeChallengeable(poke, period) { - logger.Warnf("Challenging opPoke sent from %v", common.BytesToAddress(evlog.Topics[1].Bytes())) - txHash, _, err := c.provider.ChallengePoke(c.ctx, c.address, poke) - if err != nil { - return fmt.Errorf( - "failed to challenge OpPoked event from block %v with error: %v", poke.BlockNumber, err) - } - logger.Infof("Challenge transaction hash: %v", txHash.String()) + c.SpawnChallenge(poke) } else { - logger.Debugf("Event from block %v is not challengeable", poke.BlockNumber) + logger. + WithField("address", c.address). + Debugf("Event from block %v is not challengeable", poke.BlockNumber) } } } diff --git a/core/client.go b/core/client.go new file mode 100644 index 0000000..664a1bb --- /dev/null +++ b/core/client.go @@ -0,0 +1,24 @@ +package core + +import ( + "context" + "math/big" + + "github.com/defiweb/go-eth/types" +) + +type RpcClient interface { + Accounts(ctx context.Context) ([]types.Address, error) + + BlockNumber(ctx context.Context) (*big.Int, error) + + BlockByNumber(ctx context.Context, number types.BlockNumber, full bool) (*types.Block, error) + + SendTransaction(ctx context.Context, tx *types.Transaction) (*types.Hash, *types.Transaction, error) + + Call(ctx context.Context, call *types.Call, block types.BlockNumber) ([]byte, *types.Call, error) + + GetLogs(ctx context.Context, query *types.FilterLogsQuery) ([]types.Log, error) + + GetTransactionReceipt(ctx context.Context, hash types.Hash) (*types.TransactionReceipt, error) +} diff --git a/core/scribe_optimistic_provider.go b/core/scribe_optimistic_provider.go index d19df12..4d5e25e 100644 --- a/core/scribe_optimistic_provider.go +++ b/core/scribe_optimistic_provider.go @@ -18,9 +18,9 @@ package core import ( "context" _ "embed" - "errors" "fmt" "math/big" + "time" "github.com/defiweb/go-eth/abi" "github.com/defiweb/go-eth/rpc" @@ -28,9 +28,8 @@ import ( logger "github.com/sirupsen/logrus" ) -// MaxChallengeRetries is the maximum number of retries to send a transaction. -// NOTE: first try with flashbots, if it fails, try with mainnet client. -const MaxChallengeRetries = 6 +var MaxFlashbotGasLimit = uint64(200000) +var TxConfirmationTimeout = 5 * time.Minute //go:embed ScribeOptimistic.json var scribeOptimisticContractJSON []byte @@ -40,8 +39,8 @@ var ScribeOptimisticContractABI = abi.MustParseJSON(scribeOptimisticContractJSON // ScribeOptimisticRpcProvider implements IScribeOptimisticProvider interface and provides functionality to interact with ScribeOptimistic contract. type ScribeOptimisticRpcProvider struct { - client *rpc.Client - flashbotClient *rpc.Client + client RpcClient + flashbotClient RpcClient } // NewScribeOptimisticRpcProvider creates a new instance of ScribeOptimisticRpcProvider. @@ -125,7 +124,9 @@ func (s *ScribeOptimisticRpcProvider) GetPokes( for _, poke := range pokeLogs { decoded, err := DecodeOpPokeEvent(poke) if err != nil { - logger.Errorf("Failed to decode OpPoked event with error: %v", err) + logger. + WithField("address", address). + Errorf("Failed to decode OpPoked event with error: %v", err) continue } result = append(result, decoded) @@ -157,7 +158,9 @@ func (s *ScribeOptimisticRpcProvider) GetSuccessfulChallenges( for _, challenge := range challenges { decoded, err := DecodeOpPokeChallengedSuccessfullyEvent(challenge) if err != nil { - logger.Errorf("Failed to decode OpPokeChallengedSuccessfully event with error: %v", err) + logger. + WithField("address", address). + Errorf("Failed to decode OpPokeChallengedSuccessfully event with error: %v", err) continue } result = append(result, decoded) @@ -190,12 +193,14 @@ func (s *ScribeOptimisticRpcProvider) constructPokeMessage( if err != nil { return nil, fmt.Errorf("failed to decode constructOpPokeMessage result with error: %v", err) } - logger.Debugf( - "cast call %v 'constructPokeMessage((uint128,uint32))' '(%v,%v)'", - address, - poke.PokeData.Val, - poke.PokeData.Age, - ) + logger. + WithField("address", address). + Debugf( + "cast call %v 'constructPokeMessage((uint128,uint32))' '(%v,%v)'", + address, + poke.PokeData.Val, + poke.PokeData.Age, + ) return message, nil } @@ -225,14 +230,18 @@ func (s *ScribeOptimisticRpcProvider) isSchnorrSignatureAcceptable( if err != nil { return false, fmt.Errorf("failed to decode isAcceptableSchnorrSignatureNow result with error: %v", err) } - logger.Debugf( - "cast call %v 'isAcceptableSchnorrSignatureNow(bytes32,(bytes32,address,bytes))(bool)' %s '(%s,%v,%s)'", - address, - fmt.Sprintf("0x%x", message), - fmt.Sprintf("0x%x", poke.Schnorr.Signature), - poke.Schnorr.Commitment, - fmt.Sprintf("0x%x", poke.Schnorr.SignersBlob), - ) + + logger. + WithField("address", address). + Debugf( + "cast call %v 'isAcceptableSchnorrSignatureNow(bytes32,(bytes32,address,bytes))(bool)' %s '(%s,%v,%s)'", + address, + fmt.Sprintf("0x%x", message), + fmt.Sprintf("0x%x", poke.Schnorr.Signature), + poke.Schnorr.Commitment, + fmt.Sprintf("0x%x", poke.Schnorr.SignersBlob), + ) + return res, nil } @@ -246,9 +255,12 @@ func (s *ScribeOptimisticRpcProvider) IsPokeSignatureValid(ctx context.Context, return s.isSchnorrSignatureAcceptable(ctx, address, poke, message) } -// ChallengePoke challenges the given poke by sending transaction for `opChallenge` contract function. -// Makes several attempts to send a transaction, first with flashbots, then with the mainnet client. -func (s *ScribeOptimisticRpcProvider) ChallengePoke(ctx context.Context, address types.Address, poke *OpPokedEvent) (*types.Hash, *types.Transaction, error) { +// Sends a transaction for `opChallenge` contract function using the mainnet client. +func (s *ScribeOptimisticRpcProvider) challengePokeUsingMainnet( + ctx context.Context, + address types.Address, + poke *OpPokedEvent, +) (*types.Hash, *types.Transaction, error) { opChallenge := ScribeOptimisticContractABI.Methods["opChallenge"] calldata, err := opChallenge.EncodeArgs(poke.Schnorr) @@ -261,25 +273,99 @@ func (s *ScribeOptimisticRpcProvider) ChallengePoke(ctx context.Context, address SetTo(address). SetInput(calldata) - var errs []error - for i := 0; i < MaxChallengeRetries; i++ { - if i <= MaxChallengeRetries/2 { - // Try to send with flashbots first. - hash, tx, err := s.flashbotClient.SendTransaction(ctx, tx) - if err == nil { - return hash, tx, nil - } - errs = append(errs, fmt.Errorf("try: %d failed to send tx with flashbots: %w", i, err)) - } else { - // Try to send with the mainnet client. - hash, tx, err := s.client.SendTransaction(ctx, tx) - if err == nil { - return hash, tx, nil - } - errs = append(errs, fmt.Errorf("try: %d failed to send tx with mainnet: %w", i, err)) - } - i++ + // Try to send with the mainnet client. + hash, tx, err := s.client.SendTransaction(ctx, tx) + if err != nil { + return nil, nil, fmt.Errorf("failed to send challenge transaction: %w", err) + } + + receipt, err := WaitForTxConfirmation(ctx, s.client, hash, TxConfirmationTimeout) + if err != nil { + return nil, nil, fmt.Errorf("failed to wait for challenge transaction confirmation on mainnet: %w", err) + } + + logger. + WithField("address", address). + WithField("txHash", hash). + WithField("status", receipt.Status). + Infof("challenge transaction confirmed in block %s", receipt.BlockHash) + + return hash, tx, nil +} + +func (s *ScribeOptimisticRpcProvider) challengePokeUsingFlashbots( + ctx context.Context, + address types.Address, + poke *OpPokedEvent, +) (*types.Hash, *types.Transaction, error) { + if s.flashbotClient == nil { + return nil, nil, fmt.Errorf("flashbot client is not provided") + } + opChallenge := ScribeOptimisticContractABI.Methods["opChallenge"] + + calldata, err := opChallenge.EncodeArgs(poke.Schnorr) + if err != nil { + return nil, nil, fmt.Errorf("failed to encode opChallenge args: %w", err) + } + + // Prepare a transaction. + tx := (&types.Transaction{}). + SetTo(address). + SetInput(calldata). + // NOTE: for flashbots, we need to set the gas limit manually, and it might be more than normally. + SetGasLimit(MaxFlashbotGasLimit) + + // Try to send with the flashbots client. + // NOTE: because we have signer keys configured for provider, + // it will sign the transaction and send it using `eth_sendRawTransaction`. + hash, tx, err := s.flashbotClient.SendTransaction(ctx, tx) + if err != nil { + return nil, nil, fmt.Errorf("failed to send challenge transaction: %w", err) + } + logger. + WithField("address", address). + WithField("txHash", hash). + Debugf("flashbots challenge transaction sent, waiting for confirmation") + + receipt, err := WaitForTxConfirmation(ctx, s.flashbotClient, hash, TxConfirmationTimeout) + if err != nil { + return nil, nil, fmt.Errorf("failed to wait for challenge transaction confirmation: %w", err) + } + + logger. + WithField("address", address). + WithField("txHash", hash). + Infof("challenge transaction confirmed in block %s", receipt.BlockHash) + return hash, tx, nil +} + +// ChallengePoke challenges the given poke by sending transaction for `opChallenge` contract function. +// Makes several attempts to send a transaction, first with flashbots, then with the mainnet client. +// NOTE: Probably, it's better to run challenge in a separate goroutine and wait for the confirmation. +func (s *ScribeOptimisticRpcProvider) ChallengePoke( + ctx context.Context, + address types.Address, + poke *OpPokedEvent, +) (*types.Hash, *types.Transaction, error) { + if s.flashbotClient == nil { + logger. + WithField("address", address). + Infof("flashbot client is not provided, trying to send with the mainnet client") + return s.challengePokeUsingMainnet(ctx, address, poke) + } + + logger. + WithField("address", address). + Debugf("trying to send transaction with flashbots") + + txHash, tx, err := s.challengePokeUsingFlashbots(ctx, address, poke) + if err == nil { + return txHash, tx, nil } - return nil, nil, fmt.Errorf("failed to send challenge transaction: %w", errors.Join(errs...)) + logger. + WithField("address", address). + Warnf("failed to send transaction with flashbots, trying to send with the mainnet client, error: %v", err) + + return s.challengePokeUsingMainnet(ctx, address, poke) } diff --git a/core/utils.go b/core/utils.go new file mode 100644 index 0000000..b368d83 --- /dev/null +++ b/core/utils.go @@ -0,0 +1,56 @@ +package core + +import ( + "context" + "fmt" + "time" + + "github.com/defiweb/go-eth/types" + logger "github.com/sirupsen/logrus" +) + +// WaitForTxConfirmation waits for the transaction to be confirmed. +func WaitForTxConfirmation( + ctx context.Context, + client RpcClient, + txHash *types.Hash, + timeout time.Duration, +) (*types.TransactionReceipt, error) { + if client == nil { + return nil, fmt.Errorf("ethereum client not set") + } + if txHash == nil { + return nil, fmt.Errorf("tx hash is nil") + } + + // check +- every block + ticker := time.NewTicker(12 * time.Second) + defer ticker.Stop() + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + for { + select { + case <-ctx.Done(): + return nil, fmt.Errorf("failed to wait for transaction confirmation") + case <-ticker.C: + logger.WithField("txHash", txHash).Tracef("checking transaction confirmation") + + receipt, err := client.GetTransactionReceipt(ctx, *txHash) + if err != nil { + logger.WithField("txHash", txHash).Errorf("failed to get transaction receipt: %v", err) + continue + } + if receipt == nil { + continue + } + + if receipt.Status == nil || receipt.TransactionHash.IsZero() { + logger.WithField("txHash", txHash).Tracef("transaction is not yet confirmed") + continue + } + return receipt, nil + } + } +} From a215e94edf7c19166f047dcc92d4b7e2023de74f Mon Sep 17 00:00:00 2001 From: Konstantin Zolotarev Date: Mon, 10 Mar 2025 22:20:54 +0200 Subject: [PATCH 3/4] Added provider tests --- README.md | 6 +- cmd/challenger/main.go | 7 +- core/scribe_optimistic_provider.go | 5 +- core/scribe_optimistic_provider_test.go | 110 ++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 9 deletions(-) create mode 100644 core/scribe_optimistic_provider_test.go diff --git a/README.md b/README.md index d02d9a9..393fbc4 100644 --- a/README.md +++ b/README.md @@ -32,13 +32,13 @@ Note that in *all* cases you must provide `--rpc-url`, but if you want to use ev Starting with private key ```bash -challenger run --addresses 0x891E368fE81cBa2aC6F6cc4b98e684c106e2EF4f --rpc-url http://localhost:3334 --secret-key 0x****** +challenger run --tx-type eip1559 --addresses 0x891E368fE81cBa2aC6F6cc4b98e684c106e2EF4f --rpc-url http://localhost:3334 --secret-key 0x****** ``` Starting with key file and password ```bash -challenger run -a 0x891E368fE81cBa2aC6F6cc4b98e684c106e2EF4f --rpc-url http://localhost:3334 --keystore /path/to/key.json --password-file /path/to/file +challenger run --tx-type eip1559 -a 0x891E368fE81cBa2aC6F6cc4b98e684c106e2EF4f --rpc-url http://localhost:3334 --keystore /path/to/key.json --password-file /path/to/file ``` ## Using Docker image @@ -90,5 +90,5 @@ docker run --rm challenger-go Full example: ```bash -docker run --it --rm --name challenger-go run -a ADDRESS --rpc-url http://localhost:3334 --secret-key asdfasdfas +docker run --it --rm --name challenger-go run -a ADDRESS --tx-type eip1559 --rpc-url http://localhost:3334 --secret-key asdfasdfas ``` diff --git a/cmd/challenger/main.go b/cmd/challenger/main.go index e6c4dd3..e1c8905 100644 --- a/cmd/challenger/main.go +++ b/cmd/challenger/main.go @@ -246,7 +246,7 @@ func main() { for _, address := range addresses { wg.Add(1) - p := challenger.NewScribeOptimisticRpcProvider(client, flashbotClient) + p := challenger.NewScribeOptimisticRPCProvider(client, flashbotClient) c := challenger.NewChallenger(ctx, address, p, opts.FromBlock, opts.SubscriptionURL, &wg) go func(addr types.Address) { @@ -272,8 +272,9 @@ func main() { ) http.Handle("/metrics", promhttp.Handler()) // TODO: move `:9090` to config - logger.WithError(http.ListenAndServe(":9090", nil)). //nolint:gosec - Error("metrics server error") + logger. + WithError(http.ListenAndServe(":9090", nil)). //nolint:gosec + Error("metrics server error") <-ctx.Done() }() diff --git a/core/scribe_optimistic_provider.go b/core/scribe_optimistic_provider.go index 4d5e25e..524bb41 100644 --- a/core/scribe_optimistic_provider.go +++ b/core/scribe_optimistic_provider.go @@ -23,7 +23,6 @@ import ( "time" "github.com/defiweb/go-eth/abi" - "github.com/defiweb/go-eth/rpc" "github.com/defiweb/go-eth/types" logger "github.com/sirupsen/logrus" ) @@ -43,10 +42,10 @@ type ScribeOptimisticRpcProvider struct { flashbotClient RpcClient } -// NewScribeOptimisticRpcProvider creates a new instance of ScribeOptimisticRpcProvider. +// NewScribeOptimisticRPCProvider creates a new instance of ScribeOptimisticRpcProvider. // Two clients are required: one for the mainnet and one for the flashbots relay. // Logic is simple, try to send with flashbots first, if it fails, send with the mainnet client. -func NewScribeOptimisticRpcProvider(client *rpc.Client, flashbotClient *rpc.Client) *ScribeOptimisticRpcProvider { +func NewScribeOptimisticRPCProvider(client RpcClient, flashbotClient RpcClient) *ScribeOptimisticRpcProvider { return &ScribeOptimisticRpcProvider{ client: client, flashbotClient: flashbotClient, diff --git a/core/scribe_optimistic_provider_test.go b/core/scribe_optimistic_provider_test.go new file mode 100644 index 0000000..73475c1 --- /dev/null +++ b/core/scribe_optimistic_provider_test.go @@ -0,0 +1,110 @@ +package core + +import ( + "context" + "fmt" + "math/big" + "testing" + + "github.com/defiweb/go-eth/hexutil" + "github.com/defiweb/go-eth/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +type mockRpcClient struct { + mock.Mock +} + +func (m *mockRpcClient) Accounts(ctx context.Context) ([]types.Address, error) { + args := m.Called(ctx) + return args.Get(0).([]types.Address), args.Error(1) +} + +func (m *mockRpcClient) BlockNumber(ctx context.Context) (*big.Int, error) { + args := m.Called(ctx) + return args.Get(0).(*big.Int), args.Error(1) +} + +func (m *mockRpcClient) BlockByNumber(ctx context.Context, number types.BlockNumber, full bool) (*types.Block, error) { + args := m.Called(ctx, number, full) + return args.Get(0).(*types.Block), args.Error(1) +} + +func (m *mockRpcClient) SendTransaction(ctx context.Context, tx *types.Transaction) (*types.Hash, *types.Transaction, error) { + args := m.Called(ctx, tx) + return args.Get(0).(*types.Hash), args.Get(1).(*types.Transaction), args.Error(2) +} + +func (m *mockRpcClient) Call(ctx context.Context, call *types.Call, block types.BlockNumber) ([]byte, *types.Call, error) { + args := m.Called(ctx, call, block) + c := args.Get(1) + if c == nil { + return args.Get(0).([]byte), nil, args.Error(2) + } + return args.Get(0).([]byte), c.(*types.Call), args.Error(2) +} + +func (m *mockRpcClient) GetLogs(ctx context.Context, query *types.FilterLogsQuery) ([]types.Log, error) { + args := m.Called(ctx, query) + return args.Get(0).([]types.Log), args.Error(1) +} + +func (m *mockRpcClient) GetTransactionReceipt(ctx context.Context, hash types.Hash) (*types.TransactionReceipt, error) { + args := m.Called(ctx, hash) + return args.Get(0).(*types.TransactionReceipt), args.Error(1) +} + +func TestGetFrom(t *testing.T) { + mockRpcClient := new(mockRpcClient) + provider := NewScribeOptimisticRPCProvider(mockRpcClient, nil) + + // gets zero address if no accounts + call := mockRpcClient.On("Accounts", mock.Anything).Return([]types.Address{}, nil) + addr := provider.GetFrom(context.TODO()) + assert.Equal(t, types.ZeroAddress, addr) + mockRpcClient.AssertExpectations(t) + call.Unset() + + // zero address on error + call = mockRpcClient.On("Accounts", mock.Anything).Return([]types.Address{}, fmt.Errorf("error")) + addr = provider.GetFrom(context.TODO()) + assert.Equal(t, types.ZeroAddress, addr) + mockRpcClient.AssertExpectations(t) + call.Unset() + + // gets first account + call = mockRpcClient.On("Accounts", mock.Anything).Return([]types.Address{{0x1}}, nil) + addr = provider.GetFrom(context.TODO()) + assert.Equal(t, types.Address{0x1}, addr) + mockRpcClient.AssertExpectations(t) + call.Unset() +} + +func TestGetChallengePeriod(t *testing.T) { + mockRpcClient := new(mockRpcClient) + provider := NewScribeOptimisticRPCProvider(mockRpcClient, nil) + address := types.MustAddressFromHex("0x1F7acDa376eF37EC371235a094113dF9Cb4EfEe1") + + // gets challenge period + call := mockRpcClient.On("Call", mock.Anything, mock.Anything, types.LatestBlockNumber). + Return( + hexutil.MustHexToBytes("0x0000000000000000000000000000000000000000000000000000000000000257"), + &types.Call{}, + nil, + ) + period, err := provider.GetChallengePeriod(context.TODO(), address) + assert.NoError(t, err) + assert.Equal(t, uint16(599), period) + mockRpcClient.AssertExpectations(t) + call.Unset() + + // error on call + call = mockRpcClient.On("Call", mock.Anything, mock.Anything, mock.Anything). + Return([]byte{}, nil, fmt.Errorf("error")) + period, err = provider.GetChallengePeriod(context.TODO(), address) + assert.Error(t, err) + assert.Equal(t, uint16(0), period) + mockRpcClient.AssertExpectations(t) + call.Unset() +} From 59b38947ddab7c987028dfd2655de76551b0bd49 Mon Sep 17 00:00:00 2001 From: Konstantin Zolotarev Date: Tue, 11 Mar 2025 09:54:01 +0200 Subject: [PATCH 4/4] Fix linter --- cmd/challenger/main.go | 8 ++++---- core/client.go | 2 +- core/scribe_optimistic_provider.go | 6 +++--- core/utils.go | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cmd/challenger/main.go b/cmd/challenger/main.go index e1c8905..fe346be 100644 --- a/cmd/challenger/main.go +++ b/cmd/challenger/main.go @@ -48,7 +48,7 @@ type options struct { Password string PasswordFile string RpcURL string - FlashbotRpcURL string + FlashbotRPCURL string SubscriptionURL string Address []string FromBlock int64 @@ -213,8 +213,8 @@ func main() { // Create a JSON-RPC client to flashbot. var flashbotClient *rpc.Client - if opts.FlashbotRpcURL != "" { - flashbotTransport, err := transport.NewHTTP(transport.HTTPOptions{URL: opts.FlashbotRpcURL}) + if opts.FlashbotRPCURL != "" { + flashbotTransport, err := transport.NewHTTP(transport.HTTPOptions{URL: opts.FlashbotRPCURL}) if err != nil { logger.Fatalf("Failed to create transport: %v", err) } @@ -287,7 +287,7 @@ func main() { cmd.PersistentFlags().StringVar(&opts.Password, "password", "", "Key raw password as text") cmd.PersistentFlags().StringVar(&opts.PasswordFile, "password-file", "", "Path to key password file") cmd.PersistentFlags().StringVar(&opts.RpcURL, "rpc-url", "", "Node HTTP RPC_URL, normally starts with https://****") - cmd.PersistentFlags().StringVar(&opts.FlashbotRpcURL, "flashbot-rpc-url", "", "Flashbot Node HTTP RPC_URL, normally starts with https://****") + cmd.PersistentFlags().StringVar(&opts.FlashbotRPCURL, "flashbot-rpc-url", "", "Flashbot Node HTTP RPC_URL, normally starts with https://****") cmd.PersistentFlags().StringVar(&opts.SubscriptionURL, "subscription-url", "", "[Optional] Used if you want to subscribe to events rather than poll, typically starts with wss://****") cmd.PersistentFlags().StringArrayVarP(&opts.Address, "addresses", "a", []string{}, "ScribeOptimistic contract address. Example: `0x891E368fE81cBa2aC6F6cc4b98e684c106e2EF4f`") cmd.PersistentFlags(). diff --git a/core/client.go b/core/client.go index 664a1bb..6c1072b 100644 --- a/core/client.go +++ b/core/client.go @@ -7,7 +7,7 @@ import ( "github.com/defiweb/go-eth/types" ) -type RpcClient interface { +type RPCClient interface { Accounts(ctx context.Context) ([]types.Address, error) BlockNumber(ctx context.Context) (*big.Int, error) diff --git a/core/scribe_optimistic_provider.go b/core/scribe_optimistic_provider.go index 524bb41..dd0d6b4 100644 --- a/core/scribe_optimistic_provider.go +++ b/core/scribe_optimistic_provider.go @@ -38,14 +38,14 @@ var ScribeOptimisticContractABI = abi.MustParseJSON(scribeOptimisticContractJSON // ScribeOptimisticRpcProvider implements IScribeOptimisticProvider interface and provides functionality to interact with ScribeOptimistic contract. type ScribeOptimisticRpcProvider struct { - client RpcClient - flashbotClient RpcClient + client RPCClient + flashbotClient RPCClient } // NewScribeOptimisticRPCProvider creates a new instance of ScribeOptimisticRpcProvider. // Two clients are required: one for the mainnet and one for the flashbots relay. // Logic is simple, try to send with flashbots first, if it fails, send with the mainnet client. -func NewScribeOptimisticRPCProvider(client RpcClient, flashbotClient RpcClient) *ScribeOptimisticRpcProvider { +func NewScribeOptimisticRPCProvider(client RPCClient, flashbotClient RPCClient) *ScribeOptimisticRpcProvider { return &ScribeOptimisticRpcProvider{ client: client, flashbotClient: flashbotClient, diff --git a/core/utils.go b/core/utils.go index b368d83..7c232b4 100644 --- a/core/utils.go +++ b/core/utils.go @@ -12,7 +12,7 @@ import ( // WaitForTxConfirmation waits for the transaction to be confirmed. func WaitForTxConfirmation( ctx context.Context, - client RpcClient, + client RPCClient, txHash *types.Hash, timeout time.Duration, ) (*types.TransactionReceipt, error) {