diff --git a/Dockerfile b/Dockerfile index 08eecc68b2..1306f6e998 100644 --- a/Dockerfile +++ b/Dockerfile @@ -164,6 +164,7 @@ COPY ./scripts/download-machine.sh . RUN ./download-machine.sh consensus-v10 0x6b94a7fc388fd8ef3def759297828dc311761e88d8179c7ee8d3887dc554f3c3 RUN ./download-machine.sh consensus-v10.1 0xda4e3ad5e7feacb817c21c8d0220da7650fe9051ece68a3f0b1c5d38bbb27b21 RUN ./download-machine.sh consensus-v10.2 0x0754e09320c381566cc0449904c377a52bd34a6b9404432e80afd573b67f7b17 +RUN ./download-machine.sh consensus-v10.3 0xf559b6d4fa869472dabce70fe1c15221bdda837533dfd891916836975b434dec FROM golang:1.20-bullseye as node-builder WORKDIR /workspace diff --git a/arbnode/delayed.go b/arbnode/delayed.go index f2c3d62004..51b22c58bc 100644 --- a/arbnode/delayed.go +++ b/arbnode/delayed.go @@ -184,6 +184,8 @@ func (b *DelayedBridge) logsToDeliveredMessages(ctx context.Context, logs []type } messages := make([]*DelayedInboxMessage, 0, len(logs)) + var lastParentChainBlockNumber uint64 + var lastL1BlockNumber uint64 for _, parsedLog := range parsedLogs { msgKey := common.BigToHash(parsedLog.MessageIndex) data, ok := messageData[msgKey] @@ -196,9 +198,17 @@ func (b *DelayedBridge) logsToDeliveredMessages(ctx context.Context, logs []type requestId := common.BigToHash(parsedLog.MessageIndex) parentChainBlockNumber := parsedLog.Raw.BlockNumber - l1BlockNumber, err := arbutil.CorrespondingL1BlockNumber(ctx, b.client, parentChainBlockNumber) - if err != nil { - return nil, err + var l1BlockNumber uint64 + if lastParentChainBlockNumber == parentChainBlockNumber && lastParentChainBlockNumber > 0 { + l1BlockNumber = lastL1BlockNumber + } else { + var err error + l1BlockNumber, err = arbutil.CorrespondingL1BlockNumber(ctx, b.client, parentChainBlockNumber) + if err != nil { + return nil, err + } + lastParentChainBlockNumber = parentChainBlockNumber + lastL1BlockNumber = l1BlockNumber } msg := &DelayedInboxMessage{ BlockHash: parsedLog.Raw.BlockHash, @@ -216,7 +226,7 @@ func (b *DelayedBridge) logsToDeliveredMessages(ctx context.Context, logs []type }, ParentChainBlockNumber: parsedLog.Raw.BlockNumber, } - err = msg.Message.FillInBatchGasCost(batchFetcher) + err := msg.Message.FillInBatchGasCost(batchFetcher) if err != nil { return nil, err } diff --git a/arbnode/inbox_reader.go b/arbnode/inbox_reader.go index 6ce9fd7172..9c830e3c89 100644 --- a/arbnode/inbox_reader.go +++ b/arbnode/inbox_reader.go @@ -435,11 +435,11 @@ func (r *InboxReader) run(ctx context.Context, hadError bool) error { } else if err != nil { // Unknown error (database error?) return err - } else if haveAcc == batch.BeforeInboxAcc { + } else if haveAcc == batch.AfterInboxAcc { // Skip this batch, as we already have it in the database sequencerBatches = sequencerBatches[1:] } else { - // The first batch BeforeInboxAcc matches, but this batch doesn't, + // The first batch AfterInboxAcc matches, but this batch doesn't, // so we'll successfully reorg it when we hit the addMessages break } diff --git a/arbnode/node.go b/arbnode/node.go index 55aa5585a1..32f1c1492e 100644 --- a/arbnode/node.go +++ b/arbnode/node.go @@ -18,7 +18,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" @@ -37,11 +36,8 @@ import ( "github.com/offchainlabs/nitro/execution" "github.com/offchainlabs/nitro/execution/gethexec" "github.com/offchainlabs/nitro/solgen/go/bridgegen" - "github.com/offchainlabs/nitro/solgen/go/challengegen" - "github.com/offchainlabs/nitro/solgen/go/ospgen" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" "github.com/offchainlabs/nitro/solgen/go/rollupgen" - "github.com/offchainlabs/nitro/solgen/go/upgrade_executorgen" "github.com/offchainlabs/nitro/staker" "github.com/offchainlabs/nitro/staker/validatorwallet" "github.com/offchainlabs/nitro/util/contracts" @@ -51,215 +47,6 @@ import ( "github.com/offchainlabs/nitro/wsbroadcastserver" ) -func andTxSucceeded(ctx context.Context, l1Reader *headerreader.HeaderReader, tx *types.Transaction, err error) error { - if err != nil { - return fmt.Errorf("error submitting tx: %w", err) - } - _, err = l1Reader.WaitForTxApproval(ctx, tx) - if err != nil { - return fmt.Errorf("error executing tx: %w", err) - } - return nil -} - -func deployBridgeCreator(ctx context.Context, l1Reader *headerreader.HeaderReader, auth *bind.TransactOpts, maxDataSize *big.Int) (common.Address, error) { - client := l1Reader.Client() - - /// deploy eth based templates - bridgeTemplate, tx, _, err := bridgegen.DeployBridge(auth, client) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return common.Address{}, fmt.Errorf("bridge deploy error: %w", err) - } - - seqInboxTemplate, tx, _, err := bridgegen.DeploySequencerInbox(auth, client, maxDataSize) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return common.Address{}, fmt.Errorf("sequencer inbox deploy error: %w", err) - } - - inboxTemplate, tx, _, err := bridgegen.DeployInbox(auth, client, maxDataSize) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return common.Address{}, fmt.Errorf("inbox deploy error: %w", err) - } - - rollupEventBridgeTemplate, tx, _, err := rollupgen.DeployRollupEventInbox(auth, client) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return common.Address{}, fmt.Errorf("rollup event bridge deploy error: %w", err) - } - - outboxTemplate, tx, _, err := bridgegen.DeployOutbox(auth, client) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return common.Address{}, fmt.Errorf("outbox deploy error: %w", err) - } - - ethBasedTemplates := rollupgen.BridgeCreatorBridgeContracts{ - Bridge: bridgeTemplate, - SequencerInbox: seqInboxTemplate, - Inbox: inboxTemplate, - RollupEventInbox: rollupEventBridgeTemplate, - Outbox: outboxTemplate, - } - - /// deploy ERC20 based templates - erc20BridgeTemplate, tx, _, err := bridgegen.DeployERC20Bridge(auth, client) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return common.Address{}, fmt.Errorf("bridge deploy error: %w", err) - } - - erc20InboxTemplate, tx, _, err := bridgegen.DeployERC20Inbox(auth, client, maxDataSize) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return common.Address{}, fmt.Errorf("inbox deploy error: %w", err) - } - - erc20RollupEventBridgeTemplate, tx, _, err := rollupgen.DeployERC20RollupEventInbox(auth, client) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return common.Address{}, fmt.Errorf("rollup event bridge deploy error: %w", err) - } - - erc20OutboxTemplate, tx, _, err := bridgegen.DeployERC20Outbox(auth, client) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return common.Address{}, fmt.Errorf("outbox deploy error: %w", err) - } - - erc20BasedTemplates := rollupgen.BridgeCreatorBridgeContracts{ - Bridge: erc20BridgeTemplate, - SequencerInbox: seqInboxTemplate, - Inbox: erc20InboxTemplate, - RollupEventInbox: erc20RollupEventBridgeTemplate, - Outbox: erc20OutboxTemplate, - } - - bridgeCreatorAddr, tx, _, err := rollupgen.DeployBridgeCreator(auth, client, ethBasedTemplates, erc20BasedTemplates) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return common.Address{}, fmt.Errorf("bridge creator deploy error: %w", err) - } - - return bridgeCreatorAddr, nil -} - -func deployChallengeFactory(ctx context.Context, l1Reader *headerreader.HeaderReader, auth *bind.TransactOpts) (common.Address, common.Address, error) { - client := l1Reader.Client() - osp0, tx, _, err := ospgen.DeployOneStepProver0(auth, client) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return common.Address{}, common.Address{}, fmt.Errorf("osp0 deploy error: %w", err) - } - - ospMem, _, _, err := ospgen.DeployOneStepProverMemory(auth, client) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return common.Address{}, common.Address{}, fmt.Errorf("ospMemory deploy error: %w", err) - } - - ospMath, _, _, err := ospgen.DeployOneStepProverMath(auth, client) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return common.Address{}, common.Address{}, fmt.Errorf("ospMath deploy error: %w", err) - } - - ospHostIo, _, _, err := ospgen.DeployOneStepProverHostIo(auth, client) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return common.Address{}, common.Address{}, fmt.Errorf("ospHostIo deploy error: %w", err) - } - - ospEntryAddr, tx, _, err := ospgen.DeployOneStepProofEntry(auth, client, osp0, ospMem, ospMath, ospHostIo) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return common.Address{}, common.Address{}, fmt.Errorf("ospEntry deploy error: %w", err) - } - - challengeManagerAddr, tx, _, err := challengegen.DeployChallengeManager(auth, client) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return common.Address{}, common.Address{}, fmt.Errorf("ospEntry deploy error: %w", err) - } - - return ospEntryAddr, challengeManagerAddr, nil -} - -func deployRollupCreator(ctx context.Context, l1Reader *headerreader.HeaderReader, auth *bind.TransactOpts, maxDataSize *big.Int) (*rollupgen.RollupCreator, common.Address, common.Address, common.Address, error) { - bridgeCreator, err := deployBridgeCreator(ctx, l1Reader, auth, maxDataSize) - if err != nil { - return nil, common.Address{}, common.Address{}, common.Address{}, fmt.Errorf("bridge creator deploy error: %w", err) - } - - ospEntryAddr, challengeManagerAddr, err := deployChallengeFactory(ctx, l1Reader, auth) - if err != nil { - return nil, common.Address{}, common.Address{}, common.Address{}, err - } - - rollupAdminLogic, tx, _, err := rollupgen.DeployRollupAdminLogic(auth, l1Reader.Client()) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return nil, common.Address{}, common.Address{}, common.Address{}, fmt.Errorf("rollup admin logic deploy error: %w", err) - } - - rollupUserLogic, tx, _, err := rollupgen.DeployRollupUserLogic(auth, l1Reader.Client()) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return nil, common.Address{}, common.Address{}, common.Address{}, fmt.Errorf("rollup user logic deploy error: %w", err) - } - - rollupCreatorAddress, tx, rollupCreator, err := rollupgen.DeployRollupCreator(auth, l1Reader.Client()) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return nil, common.Address{}, common.Address{}, common.Address{}, fmt.Errorf("rollup creator deploy error: %w", err) - } - - upgradeExecutor, tx, _, err := upgrade_executorgen.DeployUpgradeExecutor(auth, l1Reader.Client()) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return nil, common.Address{}, common.Address{}, common.Address{}, fmt.Errorf("upgrade executor deploy error: %w", err) - } - - validatorUtils, tx, _, err := rollupgen.DeployValidatorUtils(auth, l1Reader.Client()) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return nil, common.Address{}, common.Address{}, common.Address{}, fmt.Errorf("validator utils deploy error: %w", err) - } - - validatorWalletCreator, tx, _, err := rollupgen.DeployValidatorWalletCreator(auth, l1Reader.Client()) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return nil, common.Address{}, common.Address{}, common.Address{}, fmt.Errorf("validator wallet creator deploy error: %w", err) - } - - l2FactoriesDeployHelper, tx, _, err := rollupgen.DeployDeployHelper(auth, l1Reader.Client()) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return nil, common.Address{}, common.Address{}, common.Address{}, fmt.Errorf("deploy helper creator deploy error: %w", err) - } - - tx, err = rollupCreator.SetTemplates( - auth, - bridgeCreator, - ospEntryAddr, - challengeManagerAddr, - rollupAdminLogic, - rollupUserLogic, - upgradeExecutor, - validatorUtils, - validatorWalletCreator, - l2FactoriesDeployHelper, - ) - err = andTxSucceeded(ctx, l1Reader, tx, err) - if err != nil { - return nil, common.Address{}, common.Address{}, common.Address{}, fmt.Errorf("rollup set template error: %w", err) - } - - return rollupCreator, rollupCreatorAddress, validatorUtils, validatorWalletCreator, nil -} - func GenerateRollupConfig(prod bool, wasmModuleRoot common.Hash, rollupOwner common.Address, chainConfig *params.ChainConfig, serializedChainConfig []byte, loserStakeEscrow common.Address) rollupgen.Config { var confirmPeriod uint64 if prod { @@ -287,60 +74,6 @@ func GenerateRollupConfig(prod bool, wasmModuleRoot common.Hash, rollupOwner com } } -func DeployOnL1(ctx context.Context, parentChainReader *headerreader.HeaderReader, deployAuth *bind.TransactOpts, batchPoster common.Address, authorizeValidators uint64, config rollupgen.Config, nativeToken common.Address, maxDataSize *big.Int) (*chaininfo.RollupAddresses, error) { - if config.WasmModuleRoot == (common.Hash{}) { - return nil, errors.New("no machine specified") - } - - rollupCreator, _, validatorUtils, validatorWalletCreator, err := deployRollupCreator(ctx, parentChainReader, deployAuth, maxDataSize) - if err != nil { - return nil, fmt.Errorf("error deploying rollup creator: %w", err) - } - - var validatorAddrs []common.Address - for i := uint64(1); i <= authorizeValidators; i++ { - validatorAddrs = append(validatorAddrs, crypto.CreateAddress(validatorWalletCreator, i)) - } - - deployParams := rollupgen.RollupCreatorRollupDeploymentParams{ - Config: config, - BatchPoster: batchPoster, - Validators: validatorAddrs, - MaxDataSize: maxDataSize, - NativeToken: nativeToken, - DeployFactoriesToL2: false, - MaxFeePerGasForRetryables: big.NewInt(0), // needed when utility factories are deployed - } - - tx, err := rollupCreator.CreateRollup( - deployAuth, - deployParams, - ) - if err != nil { - return nil, fmt.Errorf("error submitting create rollup tx: %w", err) - } - receipt, err := parentChainReader.WaitForTxApproval(ctx, tx) - if err != nil { - return nil, fmt.Errorf("error executing create rollup tx: %w", err) - } - info, err := rollupCreator.ParseRollupCreated(*receipt.Logs[len(receipt.Logs)-1]) - if err != nil { - return nil, fmt.Errorf("error parsing rollup created log: %w", err) - } - - return &chaininfo.RollupAddresses{ - Bridge: info.Bridge, - Inbox: info.InboxAddress, - SequencerInbox: info.SequencerInbox, - DeployedAt: receipt.BlockNumber.Uint64(), - Rollup: info.RollupAddress, - NativeToken: nativeToken, - UpgradeExecutor: info.UpgradeExecutor, - ValidatorUtils: validatorUtils, - ValidatorWalletCreator: validatorWalletCreator, - }, nil -} - type Config struct { Sequencer bool `koanf:"sequencer"` ParentChainReader headerreader.Config `koanf:"parent-chain-reader" reload:"hot"` @@ -1053,8 +786,23 @@ func (n *Node) Start(ctx context.Context) error { return fmt.Errorf("error starting inbox reader: %w", err) } } + // must init broadcast server before trying to sequence anything + if n.BroadcastServer != nil { + err = n.BroadcastServer.Start(ctx) + if err != nil { + return fmt.Errorf("error starting feed broadcast server: %w", err) + } + } if n.SeqCoordinator != nil { n.SeqCoordinator.Start(ctx) + } else { + if n.DelayedSequencer != nil { + err := n.DelayedSequencer.ForceSequenceDelayed(ctx) + if err != nil { + return fmt.Errorf("error initially sequencing delayed instructions: %w", err) + } + } + n.Execution.Activate() } if n.MaintenanceRunner != nil { n.MaintenanceRunner.Start(ctx) @@ -1101,12 +849,6 @@ func (n *Node) Start(ctx context.Context) error { if n.L1Reader != nil { n.L1Reader.Start(ctx) } - if n.BroadcastServer != nil { - err = n.BroadcastServer.Start(ctx) - if err != nil { - return fmt.Errorf("error starting feed broadcast server: %w", err) - } - } if n.BroadcastClients != nil { go func() { if n.InboxReader != nil { diff --git a/arbnode/seq_coordinator.go b/arbnode/seq_coordinator.go index 1e8405e01a..cb6f4fe502 100644 --- a/arbnode/seq_coordinator.go +++ b/arbnode/seq_coordinator.go @@ -156,9 +156,6 @@ func NewSeqCoordinator( config: config, signer: signer, } - if sequencer != nil { - sequencer.Pause() - } streamer.SetSeqCoordinator(coordinator) return coordinator, nil } diff --git a/arbnode/transaction_streamer.go b/arbnode/transaction_streamer.go index 2ee1526ee9..31e7751e56 100644 --- a/arbnode/transaction_streamer.go +++ b/arbnode/transaction_streamer.go @@ -531,7 +531,9 @@ func (s *TransactionStreamer) AddFakeInitMessage() error { if err != nil { return fmt.Errorf("failed to serialize chain config: %w", err) } - msg := append(append(math.U256Bytes(s.chainConfig.ChainID), 0), chainConfigJson...) + // TODO: once we have a safe U256Bytes that does a copy internally, use that instead of doing an explicit copy here + chainIdBytes := math.U256Bytes(new(big.Int).Set(s.chainConfig.ChainID)) + msg := append(append(chainIdBytes, 0), chainConfigJson...) return s.AddMessages(0, false, []arbostypes.MessageWithMetadata{{ Message: &arbostypes.L1IncomingMessage{ Header: &arbostypes.L1IncomingMessageHeader{ diff --git a/arbos/arbostypes/incomingmessage.go b/arbos/arbostypes/incomingmessage.go index 04ce8ebe2e..1dc75c3e36 100644 --- a/arbos/arbostypes/incomingmessage.go +++ b/arbos/arbostypes/incomingmessage.go @@ -34,6 +34,8 @@ const ( const MaxL2MessageSize = 256 * 1024 +const ArbosVersion_FixRedeemGas = uint64(11) + type L1IncomingMessageHeader struct { Kind uint8 `json:"kind"` Poster common.Address `json:"sender"` diff --git a/arbos/block_processor.go b/arbos/block_processor.go index 6f87864b61..4864a31255 100644 --- a/arbos/block_processor.go +++ b/arbos/block_processor.go @@ -269,6 +269,11 @@ func ProduceBlockAdvanced( return nil, nil, err } + // Additional pre-transaction validity check + if err = extraPreTxFilter(chainConfig, header, statedb, state, tx, options, sender, l1Info); err != nil { + return nil, nil, err + } + if basefee.Sign() > 0 { dataGas = math.MaxUint64 brotliCompressionLevel, err := state.BrotliCompressionLevel() @@ -327,6 +332,12 @@ func ProduceBlockAdvanced( return nil, nil, err } + // Additional post-transaction validity check + if err = extraPostTxFilter(chainConfig, header, statedb, state, tx, options, sender, l1Info, result); err != nil { + statedb.RevertToSnapshot(snap) + return nil, nil, err + } + return receipt, result, nil })() @@ -366,6 +377,19 @@ func ProduceBlockAdvanced( } txGasUsed := header.GasUsed - preTxHeaderGasUsed + arbosVer := types.DeserializeHeaderExtraInformation(header).ArbOSFormatVersion + if arbosVer >= arbostypes.ArbosVersion_FixRedeemGas { + // subtract gas burned for future use + for _, scheduledTx := range result.ScheduledTxes { + switch inner := scheduledTx.GetInner().(type) { + case *types.ArbitrumRetryTx: + txGasUsed = arbmath.SaturatingUSub(txGasUsed, inner.Gas) + default: + log.Warn("Unexpected type of scheduled tx", "type", scheduledTx.Type()) + } + } + } + // Update expectedTotalBalanceDelta (also done in logs loop) switch txInner := tx.GetInner().(type) { case *types.ArbitrumDepositTx: diff --git a/arbos/extra_transaction_checks.go b/arbos/extra_transaction_checks.go new file mode 100644 index 0000000000..0f970c9925 --- /dev/null +++ b/arbos/extra_transaction_checks.go @@ -0,0 +1,42 @@ +package arbos + +import ( + "github.com/ethereum/go-ethereum/arbitrum_types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/arbosState" +) + +// extraPreTxFilter should be modified by chain operators to enforce additional pre-transaction validity rules +func extraPreTxFilter( + chainConfig *params.ChainConfig, + currentBlockHeader *types.Header, + statedb *state.StateDB, + state *arbosState.ArbosState, + tx *types.Transaction, + options *arbitrum_types.ConditionalOptions, + sender common.Address, + l1Info *L1Info, +) error { + // TODO: implement additional pre-transaction checks + return nil +} + +// extraPostTxFilter should be modified by chain operators to enforce additional post-transaction validity rules +func extraPostTxFilter( + chainConfig *params.ChainConfig, + currentBlockHeader *types.Header, + statedb *state.StateDB, + state *arbosState.ArbosState, + tx *types.Transaction, + options *arbitrum_types.ConditionalOptions, + sender common.Address, + l1Info *L1Info, + result *core.ExecutionResult, +) error { + // TODO: implement additional post-transaction checks + return nil +} diff --git a/broadcastclients/broadcastclients.go b/broadcastclients/broadcastclients.go index 551dcdb462..b2824221ea 100644 --- a/broadcastclients/broadcastclients.go +++ b/broadcastclients/broadcastclients.go @@ -166,7 +166,9 @@ func (bcs *BroadcastClients) Start(ctx context.Context) { continue } lastConfirmed = cs - bcs.primaryRouter.forwardConfirmationChan <- cs + if bcs.primaryRouter.forwardConfirmationChan != nil { + bcs.primaryRouter.forwardConfirmationChan <- cs + } // Secondary Feeds case msg := <-bcs.secondaryRouter.messageChan: @@ -187,7 +189,9 @@ func (bcs *BroadcastClients) Start(ctx context.Context) { continue } lastConfirmed = cs - bcs.secondaryRouter.forwardConfirmationChan <- cs + if bcs.secondaryRouter.forwardConfirmationChan != nil { + bcs.secondaryRouter.forwardConfirmationChan <- cs + } // Cycle buckets to get rid of old entries case <-recentFeedItemsCleanup.C: diff --git a/cmd/chaininfo/arbitrum_chain_info.json b/cmd/chaininfo/arbitrum_chain_info.json index 051ccd03c5..421abea20a 100644 --- a/cmd/chaininfo/arbitrum_chain_info.json +++ b/cmd/chaininfo/arbitrum_chain_info.json @@ -5,9 +5,9 @@ "parent-chain-is-arbitrum": false, "sequencer-url": "https://arb1-sequencer.arbitrum.io/rpc", "feed-url": "wss://arb1-feed.arbitrum.io/feed", + "secondary-feed-url": "wss://arb1-delayed-feed.arbitrum.io/feed,wss://arb1-feed-fallback-1.arbitrum.io/feed,wss://arb1-feed-fallback-2.arbitrum.io/feed,wss://arb1-feed-fallback-3.arbitrum.io/feed,wss://arb1-feed-fallback-4.arbitrum.io/feed,wss://arb1-feed-fallback-5.arbitrum.io/feed", "has-genesis-state": true, - "chain-config": - { + "chain-config": { "chainId": 42161, "homesteadBlock": 0, "daoForkBlock": null, @@ -23,13 +23,11 @@ "muirGlacierBlock": 0, "berlinBlock": 0, "londonBlock": 0, - "clique": - { + "clique": { "period": 0, "epoch": 0 }, - "arbitrum": - { + "arbitrum": { "EnableArbOS": true, "AllowDebugPrecompiles": false, "DataAvailabilityCommittee": false, @@ -38,8 +36,7 @@ "GenesisBlockNum": 0 } }, - "rollup": - { + "rollup": { "bridge": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a", "inbox": "0x4dbd4fc535ac27206064b68ffcf827b0a60bab3f", "rollup": "0x5ef0d09d1e6204141b4d37530808ed19f60fba35", @@ -56,8 +53,7 @@ "sequencer-url": "https://nova.arbitrum.io/rpc", "feed-url": "wss://nova-feed.arbitrum.io/feed", "das-index-url": "https://nova.arbitrum.io/das-servers", - "chain-config": - { + "chain-config": { "chainId": 42170, "homesteadBlock": 0, "daoForkBlock": null, @@ -73,13 +69,11 @@ "muirGlacierBlock": 0, "berlinBlock": 0, "londonBlock": 0, - "clique": - { + "clique": { "period": 0, "epoch": 0 }, - "arbitrum": - { + "arbitrum": { "EnableArbOS": true, "AllowDebugPrecompiles": false, "DataAvailabilityCommittee": true, @@ -88,8 +82,7 @@ "GenesisBlockNum": 0 } }, - "rollup": - { + "rollup": { "bridge": "0xc1ebd02f738644983b6c4b2d440b8e77dde276bd", "inbox": "0xc4448b71118c9071bcb9734a0eac55d18a153949", "rollup": "0xfb209827c58283535b744575e11953dcc4bead88", @@ -105,8 +98,7 @@ "parent-chain-is-arbitrum": false, "sequencer-url": "https://goerli-rollup.arbitrum.io/rpc", "feed-url": "wss://goerli-rollup.arbitrum.io/feed", - "chain-config": - { + "chain-config": { "chainId": 421613, "homesteadBlock": 0, "daoForkBlock": null, @@ -122,13 +114,11 @@ "muirGlacierBlock": 0, "berlinBlock": 0, "londonBlock": 0, - "clique": - { + "clique": { "period": 0, "epoch": 0 }, - "arbitrum": - { + "arbitrum": { "EnableArbOS": true, "AllowDebugPrecompiles": false, "DataAvailabilityCommittee": false, @@ -137,8 +127,7 @@ "GenesisBlockNum": 0 } }, - "rollup": - { + "rollup": { "bridge": "0xaf4159a80b6cc41ed517db1c453d1ef5c2e4db72", "inbox": "0x6bebc4925716945d46f0ec336d5c2564f419682c", "rollup": "0x45e5caea8768f42b385a366d3551ad1e0cbfab17", @@ -150,8 +139,7 @@ }, { "chain-name": "arb-dev-test", - "chain-config": - { + "chain-config": { "chainId": 412346, "homesteadBlock": 0, "daoForkBlock": null, @@ -167,13 +155,11 @@ "muirGlacierBlock": 0, "berlinBlock": 0, "londonBlock": 0, - "clique": - { + "clique": { "period": 0, "epoch": 0 }, - "arbitrum": - { + "arbitrum": { "EnableArbOS": true, "AllowDebugPrecompiles": true, "DataAvailabilityCommittee": false, @@ -185,8 +171,7 @@ }, { "chain-name": "anytrust-dev-test", - "chain-config": - { + "chain-config": { "chainId": 412347, "homesteadBlock": 0, "daoForkBlock": null, @@ -202,13 +187,11 @@ "muirGlacierBlock": 0, "berlinBlock": 0, "londonBlock": 0, - "clique": - { + "clique": { "period": 0, "epoch": 0 }, - "arbitrum": - { + "arbitrum": { "EnableArbOS": true, "AllowDebugPrecompiles": true, "DataAvailabilityCommittee": true, @@ -225,8 +208,7 @@ "chain-name": "sepolia-rollup", "sequencer-url": "https://sepolia-rollup-sequencer.arbitrum.io/rpc", "feed-url": "wss://sepolia-rollup.arbitrum.io/feed", - "chain-config": - { + "chain-config": { "chainId": 421614, "homesteadBlock": 0, "daoForkBlock": null, @@ -242,13 +224,11 @@ "muirGlacierBlock": 0, "berlinBlock": 0, "londonBlock": 0, - "clique": - { + "clique": { "period": 0, "epoch": 0 }, - "arbitrum": - { + "arbitrum": { "EnableArbOS": true, "AllowDebugPrecompiles": false, "DataAvailabilityCommittee": false, @@ -257,8 +237,7 @@ "GenesisBlockNum": 0 } }, - "rollup": - { + "rollup": { "bridge": "0x38f918D0E9F1b721EDaA41302E399fa1B79333a9", "inbox": "0xaAe29B0366299461418F5324a79Afc425BE5ae21", "sequencer-inbox": "0x6c97864CE4bEf387dE0b3310A44230f7E3F1be0D", @@ -268,4 +247,4 @@ "deployed-at": 4139226 } } -] +] \ No newline at end of file diff --git a/cmd/chaininfo/chain_info.go b/cmd/chaininfo/chain_info.go index cc13321513..fe499442d2 100644 --- a/cmd/chaininfo/chain_info.go +++ b/cmd/chaininfo/chain_info.go @@ -22,12 +22,13 @@ type ChainInfo struct { ParentChainId uint64 `json:"parent-chain-id"` ParentChainIsArbitrum *bool `json:"parent-chain-is-arbitrum"` // This is the forwarding target to submit transactions to, called the sequencer URL for clarity - SequencerUrl string `json:"sequencer-url"` - FeedUrl string `json:"feed-url"` - DasIndexUrl string `json:"das-index-url"` - HasGenesisState bool `json:"has-genesis-state"` - ChainConfig *params.ChainConfig `json:"chain-config"` - RollupAddresses *RollupAddresses `json:"rollup"` + SequencerUrl string `json:"sequencer-url"` + FeedUrl string `json:"feed-url"` + SecondaryFeedUrl string `json:"secondary-feed-url"` + DasIndexUrl string `json:"das-index-url"` + HasGenesisState bool `json:"has-genesis-state"` + ChainConfig *params.ChainConfig `json:"chain-config"` + RollupAddresses *RollupAddresses `json:"rollup"` } func GetChainConfig(chainId *big.Int, chainName string, genesisBlockNum uint64, l2ChainInfoFiles []string, l2ChainInfoJson string) (*params.ChainConfig, error) { diff --git a/cmd/datool/datool.go b/cmd/datool/datool.go index d20a5b52cd..d78d975fd5 100644 --- a/cmd/datool/datool.go +++ b/cmd/datool/datool.go @@ -102,7 +102,6 @@ func parseClientStoreConfig(args []string) (*ClientStoreConfig, error) { f.String("signing-wallet", "", "wallet containing ecdsa key to sign the message with") f.String("signing-wallet-password", genericconf.PASSWORD_NOT_SET, "password to unlock the wallet, if not specified the user is prompted for the password") f.Duration("das-retention-period", 24*time.Hour, "The period which DASes are requested to retain the stored batches.") - genericconf.ConfConfigAddOptions("conf", f) k, err := confighelpers.BeginCommonParse(f, args) if err != nil { @@ -204,8 +203,6 @@ func parseRESTClientGetByHashConfig(args []string) (*RESTClientGetByHashConfig, f.String("url", "http://localhost:9877", "URL of DAS server to connect to.") f.String("data-hash", "", "hash of the message to retrieve, if starts with '0x' it's treated as hex encoded, otherwise base64 encoded") - genericconf.ConfConfigAddOptions("conf", f) - k, err := confighelpers.BeginCommonParse(f, args) if err != nil { return nil, err @@ -267,7 +264,6 @@ func parseKeyGenConfig(args []string) (*KeyGenConfig, error) { f.String("dir", "", "the directory to generate the keys in") f.Bool("ecdsa", false, "generate an ECDSA keypair instead of BLS") f.Bool("wallet", false, "generate the ECDSA keypair in a wallet file") - genericconf.ConfConfigAddOptions("conf", f) k, err := confighelpers.BeginCommonParse(f, args) if err != nil { diff --git a/cmd/deploy/deploy.go b/cmd/deploy/deploy.go index 0b72038908..afbcddec62 100644 --- a/cmd/deploy/deploy.go +++ b/cmd/deploy/deploy.go @@ -25,6 +25,7 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/offchainlabs/nitro/arbnode" "github.com/offchainlabs/nitro/cmd/util" + deploycode "github.com/offchainlabs/nitro/deploy" ) func main() { @@ -141,7 +142,7 @@ func main() { defer l1Reader.StopAndWait() nativeToken := common.HexToAddress(*nativeTokenAddressString) - deployedAddresses, err := arbnode.DeployOnL1( + deployedAddresses, err := deploycode.DeployOnL1( ctx, l1Reader, l1TransactionOpts, diff --git a/cmd/nitro/nitro.go b/cmd/nitro/nitro.go index 42ba54c642..7e089d946c 100644 --- a/cmd/nitro/nitro.go +++ b/cmd/nitro/nitro.go @@ -815,7 +815,10 @@ func applyChainParameters(ctx context.Context, k *koanf.Koanf, chainId uint64, c chainDefaults["execution.forwarding-target"] = chainInfo.SequencerUrl } if chainInfo.FeedUrl != "" { - chainDefaults["node.feed.input.url"] = chainInfo.FeedUrl + chainDefaults["node.feed.input.url"] = strings.Split(chainInfo.FeedUrl, ",") + } + if chainInfo.SecondaryFeedUrl != "" { + chainDefaults["node.feed.input.secondary-url"] = strings.Split(chainInfo.SecondaryFeedUrl, ",") } if chainInfo.DasIndexUrl != "" { chainDefaults["node.data-availability.enable"] = true diff --git a/deploy/deploy.go b/deploy/deploy.go new file mode 100644 index 0000000000..bd2f2ec329 --- /dev/null +++ b/deploy/deploy.go @@ -0,0 +1,283 @@ +package deploy + +import ( + "context" + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/offchainlabs/nitro/cmd/chaininfo" + "github.com/offchainlabs/nitro/solgen/go/bridgegen" + "github.com/offchainlabs/nitro/solgen/go/challengegen" + "github.com/offchainlabs/nitro/solgen/go/ospgen" + "github.com/offchainlabs/nitro/solgen/go/rollupgen" + "github.com/offchainlabs/nitro/solgen/go/upgrade_executorgen" + "github.com/offchainlabs/nitro/util/headerreader" +) + +func andTxSucceeded(ctx context.Context, l1Reader *headerreader.HeaderReader, tx *types.Transaction, err error) error { + if err != nil { + return fmt.Errorf("error submitting tx: %w", err) + } + _, err = l1Reader.WaitForTxApproval(ctx, tx) + if err != nil { + return fmt.Errorf("error executing tx: %w", err) + } + return nil +} + +func deployBridgeCreator(ctx context.Context, l1Reader *headerreader.HeaderReader, auth *bind.TransactOpts, maxDataSize *big.Int) (common.Address, error) { + client := l1Reader.Client() + + /// deploy eth based templates + bridgeTemplate, tx, _, err := bridgegen.DeployBridge(auth, client) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return common.Address{}, fmt.Errorf("bridge deploy error: %w", err) + } + + seqInboxTemplate, tx, _, err := bridgegen.DeploySequencerInbox(auth, client, maxDataSize) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return common.Address{}, fmt.Errorf("sequencer inbox deploy error: %w", err) + } + + inboxTemplate, tx, _, err := bridgegen.DeployInbox(auth, client, maxDataSize) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return common.Address{}, fmt.Errorf("inbox deploy error: %w", err) + } + + rollupEventBridgeTemplate, tx, _, err := rollupgen.DeployRollupEventInbox(auth, client) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return common.Address{}, fmt.Errorf("rollup event bridge deploy error: %w", err) + } + + outboxTemplate, tx, _, err := bridgegen.DeployOutbox(auth, client) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return common.Address{}, fmt.Errorf("outbox deploy error: %w", err) + } + + ethBasedTemplates := rollupgen.BridgeCreatorBridgeContracts{ + Bridge: bridgeTemplate, + SequencerInbox: seqInboxTemplate, + Inbox: inboxTemplate, + RollupEventInbox: rollupEventBridgeTemplate, + Outbox: outboxTemplate, + } + + /// deploy ERC20 based templates + erc20BridgeTemplate, tx, _, err := bridgegen.DeployERC20Bridge(auth, client) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return common.Address{}, fmt.Errorf("bridge deploy error: %w", err) + } + + erc20InboxTemplate, tx, _, err := bridgegen.DeployERC20Inbox(auth, client, maxDataSize) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return common.Address{}, fmt.Errorf("inbox deploy error: %w", err) + } + + erc20RollupEventBridgeTemplate, tx, _, err := rollupgen.DeployERC20RollupEventInbox(auth, client) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return common.Address{}, fmt.Errorf("rollup event bridge deploy error: %w", err) + } + + erc20OutboxTemplate, tx, _, err := bridgegen.DeployERC20Outbox(auth, client) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return common.Address{}, fmt.Errorf("outbox deploy error: %w", err) + } + + erc20BasedTemplates := rollupgen.BridgeCreatorBridgeContracts{ + Bridge: erc20BridgeTemplate, + SequencerInbox: seqInboxTemplate, + Inbox: erc20InboxTemplate, + RollupEventInbox: erc20RollupEventBridgeTemplate, + Outbox: erc20OutboxTemplate, + } + + bridgeCreatorAddr, tx, _, err := rollupgen.DeployBridgeCreator(auth, client, ethBasedTemplates, erc20BasedTemplates) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return common.Address{}, fmt.Errorf("bridge creator deploy error: %w", err) + } + + return bridgeCreatorAddr, nil +} + +func deployChallengeFactory(ctx context.Context, l1Reader *headerreader.HeaderReader, auth *bind.TransactOpts) (common.Address, common.Address, error) { + client := l1Reader.Client() + osp0, tx, _, err := ospgen.DeployOneStepProver0(auth, client) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return common.Address{}, common.Address{}, fmt.Errorf("osp0 deploy error: %w", err) + } + + ospMem, tx, _, err := ospgen.DeployOneStepProverMemory(auth, client) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return common.Address{}, common.Address{}, fmt.Errorf("ospMemory deploy error: %w", err) + } + + ospMath, tx, _, err := ospgen.DeployOneStepProverMath(auth, client) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return common.Address{}, common.Address{}, fmt.Errorf("ospMath deploy error: %w", err) + } + + ospHostIo, tx, _, err := ospgen.DeployOneStepProverHostIo(auth, client) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return common.Address{}, common.Address{}, fmt.Errorf("ospHostIo deploy error: %w", err) + } + + challengeManagerAddr, tx, _, err := challengegen.DeployChallengeManager(auth, client) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return common.Address{}, common.Address{}, fmt.Errorf("challenge manager deploy error: %w", err) + } + + ospEntryAddr, tx, _, err := ospgen.DeployOneStepProofEntry(auth, client, osp0, ospMem, ospMath, ospHostIo) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return common.Address{}, common.Address{}, fmt.Errorf("ospEntry deploy error: %w", err) + } + + return ospEntryAddr, challengeManagerAddr, nil +} + +func deployRollupCreator(ctx context.Context, l1Reader *headerreader.HeaderReader, auth *bind.TransactOpts, maxDataSize *big.Int) (*rollupgen.RollupCreator, common.Address, common.Address, common.Address, error) { + bridgeCreator, err := deployBridgeCreator(ctx, l1Reader, auth, maxDataSize) + if err != nil { + return nil, common.Address{}, common.Address{}, common.Address{}, fmt.Errorf("bridge creator deploy error: %w", err) + } + + ospEntryAddr, challengeManagerAddr, err := deployChallengeFactory(ctx, l1Reader, auth) + if err != nil { + return nil, common.Address{}, common.Address{}, common.Address{}, err + } + + rollupAdminLogic, tx, _, err := rollupgen.DeployRollupAdminLogic(auth, l1Reader.Client()) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return nil, common.Address{}, common.Address{}, common.Address{}, fmt.Errorf("rollup admin logic deploy error: %w", err) + } + + rollupUserLogic, tx, _, err := rollupgen.DeployRollupUserLogic(auth, l1Reader.Client()) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return nil, common.Address{}, common.Address{}, common.Address{}, fmt.Errorf("rollup user logic deploy error: %w", err) + } + + rollupCreatorAddress, tx, rollupCreator, err := rollupgen.DeployRollupCreator(auth, l1Reader.Client()) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return nil, common.Address{}, common.Address{}, common.Address{}, fmt.Errorf("rollup creator deploy error: %w", err) + } + + upgradeExecutor, tx, _, err := upgrade_executorgen.DeployUpgradeExecutor(auth, l1Reader.Client()) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return nil, common.Address{}, common.Address{}, common.Address{}, fmt.Errorf("upgrade executor deploy error: %w", err) + } + + validatorUtils, tx, _, err := rollupgen.DeployValidatorUtils(auth, l1Reader.Client()) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return nil, common.Address{}, common.Address{}, common.Address{}, fmt.Errorf("validator utils deploy error: %w", err) + } + + validatorWalletCreator, tx, _, err := rollupgen.DeployValidatorWalletCreator(auth, l1Reader.Client()) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return nil, common.Address{}, common.Address{}, common.Address{}, fmt.Errorf("validator wallet creator deploy error: %w", err) + } + + l2FactoriesDeployHelper, tx, _, err := rollupgen.DeployDeployHelper(auth, l1Reader.Client()) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return nil, common.Address{}, common.Address{}, common.Address{}, fmt.Errorf("deploy helper creator deploy error: %w", err) + } + + tx, err = rollupCreator.SetTemplates( + auth, + bridgeCreator, + ospEntryAddr, + challengeManagerAddr, + rollupAdminLogic, + rollupUserLogic, + upgradeExecutor, + validatorUtils, + validatorWalletCreator, + l2FactoriesDeployHelper, + ) + err = andTxSucceeded(ctx, l1Reader, tx, err) + if err != nil { + return nil, common.Address{}, common.Address{}, common.Address{}, fmt.Errorf("rollup set template error: %w", err) + } + + return rollupCreator, rollupCreatorAddress, validatorUtils, validatorWalletCreator, nil +} + +func DeployOnL1(ctx context.Context, parentChainReader *headerreader.HeaderReader, deployAuth *bind.TransactOpts, batchPoster common.Address, authorizeValidators uint64, config rollupgen.Config, nativeToken common.Address, maxDataSize *big.Int) (*chaininfo.RollupAddresses, error) { + if config.WasmModuleRoot == (common.Hash{}) { + return nil, errors.New("no machine specified") + } + + rollupCreator, _, validatorUtils, validatorWalletCreator, err := deployRollupCreator(ctx, parentChainReader, deployAuth, maxDataSize) + if err != nil { + return nil, fmt.Errorf("error deploying rollup creator: %w", err) + } + + var validatorAddrs []common.Address + for i := uint64(1); i <= authorizeValidators; i++ { + validatorAddrs = append(validatorAddrs, crypto.CreateAddress(validatorWalletCreator, i)) + } + + deployParams := rollupgen.RollupCreatorRollupDeploymentParams{ + Config: config, + BatchPoster: batchPoster, + Validators: validatorAddrs, + MaxDataSize: maxDataSize, + NativeToken: nativeToken, + DeployFactoriesToL2: false, + MaxFeePerGasForRetryables: big.NewInt(0), // needed when utility factories are deployed + } + + tx, err := rollupCreator.CreateRollup( + deployAuth, + deployParams, + ) + if err != nil { + return nil, fmt.Errorf("error submitting create rollup tx: %w", err) + } + receipt, err := parentChainReader.WaitForTxApproval(ctx, tx) + if err != nil { + return nil, fmt.Errorf("error executing create rollup tx: %w", err) + } + info, err := rollupCreator.ParseRollupCreated(*receipt.Logs[len(receipt.Logs)-1]) + if err != nil { + return nil, fmt.Errorf("error parsing rollup created log: %w", err) + } + + return &chaininfo.RollupAddresses{ + Bridge: info.Bridge, + Inbox: info.InboxAddress, + SequencerInbox: info.SequencerInbox, + DeployedAt: receipt.BlockNumber.Uint64(), + Rollup: info.RollupAddress, + NativeToken: nativeToken, + UpgradeExecutor: info.UpgradeExecutor, + ValidatorUtils: validatorUtils, + ValidatorWalletCreator: validatorWalletCreator, + }, nil +} diff --git a/execution/gethexec/forwarder.go b/execution/gethexec/forwarder.go index 7dea24b417..dc3420f8de 100644 --- a/execution/gethexec/forwarder.go +++ b/execution/gethexec/forwarder.go @@ -9,6 +9,7 @@ import ( "fmt" "net" "net/http" + "regexp" "sync" "sync/atomic" "time" @@ -79,19 +80,23 @@ func AddOptionsForForwarderConfigImpl(prefix string, defaultConfig *ForwarderCon } type TxForwarder struct { + ctx context.Context + enabled atomic.Bool - target string timeout time.Duration transport *http.Transport - rpcClient *rpc.Client - ethClient *ethclient.Client healthMutex sync.Mutex healthErr error healthChecked time.Time + + targets []string + rpcClients []*rpc.Client + ethClients []*ethclient.Client + tryNewForwarderErrors *regexp.Regexp } -func NewForwarder(target string, config *ForwarderConfig) *TxForwarder { +func NewForwarder(targets []string, config *ForwarderConfig) *TxForwarder { dialer := net.Dialer{ Timeout: 5 * time.Second, KeepAlive: 2 * time.Second, @@ -116,34 +121,45 @@ func NewForwarder(target string, config *ForwarderConfig) *TxForwarder { ExpectContinueTimeout: 1 * time.Second, } return &TxForwarder{ - target: target, - timeout: config.ConnectionTimeout, - transport: transport, + targets: targets, + timeout: config.ConnectionTimeout, + transport: transport, + tryNewForwarderErrors: regexp.MustCompile(`(?i)(^http:|^json:|^i/0|timeout exceeded|no such host)`), } } -func (f *TxForwarder) ctxWithTimeout(inctx context.Context) (context.Context, context.CancelFunc) { +func (f *TxForwarder) ctxWithTimeout() (context.Context, context.CancelFunc) { if f.timeout == time.Duration(0) { - return context.WithCancel(inctx) + return context.WithCancel(f.ctx) } - return context.WithTimeout(inctx, f.timeout) + return context.WithTimeout(f.ctx, f.timeout) } func (f *TxForwarder) PublishTransaction(inctx context.Context, tx *types.Transaction, options *arbitrum_types.ConditionalOptions) error { if !f.enabled.Load() { return ErrNoSequencer } - ctx, cancelFunc := f.ctxWithTimeout(inctx) + ctx, cancelFunc := f.ctxWithTimeout() defer cancelFunc() - if options == nil { - return f.ethClient.SendTransaction(ctx, tx) + for pos, rpcClient := range f.rpcClients { + var err error + if options == nil { + err = f.ethClients[pos].SendTransaction(ctx, tx) + } else { + err = arbitrum.SendConditionalTransactionRPC(ctx, rpcClient, tx, options) + } + if err == nil || !f.tryNewForwarderErrors.MatchString(err.Error()) { + return err + } + log.Info("error forwarding transaction to a backup target", "target", f.targets[pos], "err", err) } - return arbitrum.SendConditionalTransactionRPC(ctx, f.rpcClient, tx, options) + return errors.New("failed to publish transaction to any of the forwarding targets") } const cacheUpstreamHealth = 2 * time.Second const maxHealthTimeout = 10 * time.Second +// CheckHealth returns health of the highest priority forwarding target func (f *TxForwarder) CheckHealth(inctx context.Context) error { if !f.enabled.Load() { return ErrNoSequencer @@ -157,28 +173,41 @@ func (f *TxForwarder) CheckHealth(inctx context.Context) error { } ctx, cancelFunc := context.WithTimeout(context.Background(), timeout) defer cancelFunc() - f.healthErr = f.rpcClient.CallContext(ctx, nil, "arb_checkPublisherHealth") + f.healthErr = f.rpcClients[0].CallContext(ctx, nil, "arb_checkPublisherHealth") f.healthChecked = time.Now() } return f.healthErr } func (f *TxForwarder) Initialize(inctx context.Context) error { - if f.target == "" { - f.rpcClient = nil - f.ethClient = nil - f.enabled.Store(false) - return nil + if f.ctx == nil { + f.ctx = inctx } - ctx, cancelFunc := f.ctxWithTimeout(inctx) + ctx, cancelFunc := f.ctxWithTimeout() defer cancelFunc() - rpcClient, err := rpc.DialTransport(ctx, f.target, f.transport) - if err != nil { - return err + var targets []string + var lastError error + for _, target := range f.targets { + if target == "" { + continue + } + rpcClient, err := rpc.DialTransport(ctx, target, f.transport) + if err != nil { + log.Warn("error initializing a forwarding client in txForwarder", "forwarding url", target, "err", err) + lastError = err + continue + } + targets = append(targets, target) + ethClient := ethclient.NewClient(rpcClient) + f.rpcClients = append(f.rpcClients, rpcClient) + f.ethClients = append(f.ethClients, ethClient) + } + f.targets = targets + if len(f.rpcClients) > 0 { + f.enabled.Store(true) + } else { + return lastError } - f.rpcClient = rpcClient - f.ethClient = ethclient.NewClient(rpcClient) - f.enabled.Store(true) return nil } @@ -192,8 +221,8 @@ func (f *TxForwarder) Start(ctx context.Context) error { } func (f *TxForwarder) StopAndWait() { - if f.ethClient != nil { - f.ethClient.Close() // internally closes also the rpc client + for _, ethClient := range f.ethClients { + ethClient.Close() // internally closes also the rpc client } } @@ -346,7 +375,7 @@ func (f *RedisTxForwarder) update(ctx context.Context) time.Duration { } var newForwarder *TxForwarder for { - newForwarder = NewForwarder(newSequencerUrl, f.config) + newForwarder = NewForwarder([]string{newSequencerUrl}, f.config) err := newForwarder.Initialize(ctx) if err == nil { break diff --git a/execution/gethexec/node.go b/execution/gethexec/node.go index 5a99d59c5a..7dd6e301fe 100644 --- a/execution/gethexec/node.go +++ b/execution/gethexec/node.go @@ -39,16 +39,17 @@ func DangerousConfigAddOptions(prefix string, f *flag.FlagSet) { } type Config struct { - ParentChainReader headerreader.Config `koanf:"parent-chain-reader" reload:"hot"` - Sequencer SequencerConfig `koanf:"sequencer" reload:"hot"` - RecordingDatabase arbitrum.RecordingDatabaseConfig `koanf:"recording-database"` - TxPreChecker TxPreCheckerConfig `koanf:"tx-pre-checker" reload:"hot"` - Forwarder ForwarderConfig `koanf:"forwarder"` - ForwardingTarget string `koanf:"forwarding-target"` - Caching CachingConfig `koanf:"caching"` - RPC arbitrum.Config `koanf:"rpc"` - TxLookupLimit uint64 `koanf:"tx-lookup-limit"` - Dangerous DangerousConfig `koanf:"dangerous"` + ParentChainReader headerreader.Config `koanf:"parent-chain-reader" reload:"hot"` + Sequencer SequencerConfig `koanf:"sequencer" reload:"hot"` + RecordingDatabase arbitrum.RecordingDatabaseConfig `koanf:"recording-database"` + TxPreChecker TxPreCheckerConfig `koanf:"tx-pre-checker" reload:"hot"` + Forwarder ForwarderConfig `koanf:"forwarder"` + ForwardingTarget string `koanf:"forwarding-target"` + SecondaryForwardingTarget []string `koanf:"secondary-forwarding-target"` + Caching CachingConfig `koanf:"caching"` + RPC arbitrum.Config `koanf:"rpc"` + TxLookupLimit uint64 `koanf:"tx-lookup-limit"` + Dangerous DangerousConfig `koanf:"dangerous"` forwardingTarget string } @@ -77,6 +78,7 @@ func ConfigAddOptions(prefix string, f *flag.FlagSet) { headerreader.AddOptions(prefix+".parent-chain-reader", f) arbitrum.RecordingDatabaseConfigAddOptions(prefix+".recording-database", f) f.String(prefix+".forwarding-target", ConfigDefault.ForwardingTarget, "transaction forwarding target URL, or \"null\" to disable forwarding (iff not sequencer)") + f.StringSlice(prefix+".secondary-forwarding-target", ConfigDefault.SecondaryForwardingTarget, "secondary transaction forwarding target URL") AddOptionsForNodeForwarderConfig(prefix+".forwarder", f) TxPreCheckerConfigAddOptions(prefix+".tx-pre-checker", f) CachingConfigAddOptions(prefix+".caching", f) @@ -85,21 +87,22 @@ func ConfigAddOptions(prefix string, f *flag.FlagSet) { } var ConfigDefault = Config{ - RPC: arbitrum.DefaultConfig, - Sequencer: DefaultSequencerConfig, - ParentChainReader: headerreader.DefaultConfig, - RecordingDatabase: arbitrum.DefaultRecordingDatabaseConfig, - ForwardingTarget: "", - TxPreChecker: DefaultTxPreCheckerConfig, - TxLookupLimit: 126_230_400, // 1 year at 4 blocks per second - Caching: DefaultCachingConfig, - Dangerous: DefaultDangerousConfig, - Forwarder: DefaultNodeForwarderConfig, + RPC: arbitrum.DefaultConfig, + Sequencer: DefaultSequencerConfig, + ParentChainReader: headerreader.DefaultConfig, + RecordingDatabase: arbitrum.DefaultRecordingDatabaseConfig, + ForwardingTarget: "", + SecondaryForwardingTarget: []string{}, + TxPreChecker: DefaultTxPreCheckerConfig, + TxLookupLimit: 126_230_400, // 1 year at 4 blocks per second + Caching: DefaultCachingConfig, + Dangerous: DefaultDangerousConfig, + Forwarder: DefaultNodeForwarderConfig, } func ConfigDefaultNonSequencerTest() *Config { config := ConfigDefault - config.ParentChainReader = headerreader.Config{} + config.ParentChainReader = headerreader.TestConfig config.Sequencer.Enable = false config.Forwarder = DefaultTestForwarderConfig config.ForwardingTarget = "null" @@ -111,7 +114,6 @@ func ConfigDefaultNonSequencerTest() *Config { func ConfigDefaultTest() *Config { config := ConfigDefault - config.ParentChainReader = headerreader.Config{} config.Sequencer = TestSequencerConfig config.ForwardingTarget = "null" config.ParentChainReader = headerreader.TestConfig @@ -176,7 +178,8 @@ func CreateExecutionNode( } else if config.forwardingTarget == "" { txPublisher = NewTxDropper() } else { - txPublisher = NewForwarder(config.forwardingTarget, &config.Forwarder) + targets := append([]string{config.forwardingTarget}, config.SecondaryForwardingTarget...) + txPublisher = NewForwarder(targets, &config.Forwarder) } } diff --git a/execution/gethexec/sequencer.go b/execution/gethexec/sequencer.go index e1b55b6ce2..1c60702c85 100644 --- a/execution/gethexec/sequencer.go +++ b/execution/gethexec/sequencer.go @@ -322,6 +322,7 @@ func NewSequencer(execEngine *ExecutionEngine, l1Reader *headerreader.HeaderRead containers.NewLruCacheWithOnEvict(config.NonceCacheSize, s.onNonceFailureEvict), func() time.Duration { return configFetcher().NonceFailureCacheExpiry }, } + s.Pause() execEngine.EnableReorgSequencing() return s, nil } @@ -489,20 +490,20 @@ func (s *Sequencer) ForwardTarget() string { if s.forwarder == nil { return "" } - return s.forwarder.target + return s.forwarder.targets[0] } func (s *Sequencer) ForwardTo(url string) error { s.activeMutex.Lock() defer s.activeMutex.Unlock() if s.forwarder != nil { - if s.forwarder.target == url { + if s.forwarder.targets[0] == url { log.Warn("attempted to update sequencer forward target with existing target", "url", url) return nil } s.forwarder.Disable() } - s.forwarder = NewForwarder(url, &s.config().Forwarder) + s.forwarder = NewForwarder([]string{url}, &s.config().Forwarder) err := s.forwarder.Initialize(s.GetContext()) if err != nil { log.Error("failed to set forward agent", "err", err) diff --git a/go-ethereum b/go-ethereum index be1fd29a26..f4eec6e6f9 160000 --- a/go-ethereum +++ b/go-ethereum @@ -1 +1 @@ -Subproject commit be1fd29a26173eae9008c33280b2942a86a4b0fc +Subproject commit f4eec6e6f9dcc2c2b32b5af049001ca1629bb5f2 diff --git a/go.mod b/go.mod index 1815528c65..a637ec39c1 100644 --- a/go.mod +++ b/go.mod @@ -102,7 +102,7 @@ require ( github.com/dustin/go-humanize v1.0.0 // indirect github.com/elastic/gosigar v0.14.2 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/ethereum/c-kzg-4844 v0.3.0 // indirect + github.com/ethereum/c-kzg-4844 v0.3.1 // indirect github.com/facebookgo/atomicfile v0.0.0-20151019160806-2de1f203e7d5 // indirect github.com/flynn/noise v1.0.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect @@ -252,7 +252,7 @@ require ( github.com/samber/lo v1.36.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/stretchr/testify v1.8.4 // indirect - github.com/supranational/blst v0.3.11-0.20230406105308-e9dfc5ee724b // indirect + github.com/supranational/blst v0.3.11 // indirect github.com/urfave/cli/v2 v2.24.1 // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect @@ -282,8 +282,8 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.24.0 // indirect go4.org v0.0.0-20200411211856-f5505b9728dd // indirect - golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect - golang.org/x/mod v0.10.0 // indirect + golang.org/x/exp v0.0.0-20230810033253-352e893a4cad // indirect + golang.org/x/mod v0.11.0 // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/sync v0.3.0 // indirect golang.org/x/text v0.13.0 // indirect diff --git a/go.sum b/go.sum index 7ecde76314..fc1da5bfcb 100644 --- a/go.sum +++ b/go.sum @@ -332,8 +332,8 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go. github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= -github.com/ethereum/c-kzg-4844 v0.3.0 h1:3Y3hD6l5i0dEYsBL50C+Om644kve3pNqoAcvE26o9zI= -github.com/ethereum/c-kzg-4844 v0.3.0/go.mod h1:WI2Nd82DMZAAZI1wV2neKGost9EKjvbpQR9OqE5Qqa8= +github.com/ethereum/c-kzg-4844 v0.3.1 h1:sR65+68+WdnMKxseNWxSJuAv2tsUrihTpVBTfM/U5Zg= +github.com/ethereum/c-kzg-4844 v0.3.1/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= github.com/facebookgo/atomicfile v0.0.0-20151019160806-2de1f203e7d5 h1:BBso6MBKW8ncyZLv37o+KNyy0HrrHgfnOaGQC2qvN+A= github.com/facebookgo/atomicfile v0.0.0-20151019160806-2de1f203e7d5/go.mod h1:JpoxHjuQauoxiFMl1ie8Xc/7TfLuMZ5eOCONd1sUBHg= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= @@ -1589,8 +1589,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/supranational/blst v0.3.11-0.20230406105308-e9dfc5ee724b h1:u49mjRnygnB34h8OKbnNJFVUtWSKIKb1KukdV8bILUM= -github.com/supranational/blst v0.3.11-0.20230406105308-e9dfc5ee724b/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= +github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= @@ -1800,8 +1800,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= -golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/exp v0.0.0-20230810033253-352e893a4cad h1:g0bG7Z4uG+OgH2QDODnjp6ggkk1bJDsINcuWmJN1iJU= +golang.org/x/exp v0.0.0-20230810033253-352e893a4cad/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1826,8 +1826,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= -golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180406214816-61147c48b25b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/staker/challenge-cache/cache.go b/staker/challenge-cache/cache.go index 923dbd26ce..b2690b7182 100644 --- a/staker/challenge-cache/cache.go +++ b/staker/challenge-cache/cache.go @@ -36,6 +36,7 @@ import ( "io" "os" "path/filepath" + "time" protocol "github.com/OffchainLabs/bold/chain-abstraction" l2stateprovider "github.com/OffchainLabs/bold/layer2-state-provider" @@ -66,11 +67,44 @@ type Cache struct { baseDir string } +func isOlderThanFourteenDays(t time.Time) bool { + return time.Since(t) > 14*24*time.Hour +} + +func deleteFilesOlderThanFourteenDays(dir string) error { + files, err := os.ReadDir(dir) + if err != nil { + return err + } + for _, file := range files { + fileInfo, err := file.Info() + if err != nil { + return err + } + if fileInfo.IsDir() { + if err := deleteFilesOlderThanFourteenDays(filepath.Join(dir, fileInfo.Name())); err != nil { + return err + } + } else { + if isOlderThanFourteenDays(fileInfo.ModTime()) { + if err := os.Remove(filepath.Join(dir, fileInfo.Name())); err != nil { + return err + } + } + } + } + return nil +} + // New cache from a base directory path. -func New(baseDir string) *Cache { +func New(baseDir string) (*Cache, error) { + err := deleteFilesOlderThanFourteenDays(baseDir) + if err != nil { + return nil, err + } return &Cache{ baseDir: baseDir, - } + }, nil } // Key for cache lookups includes the wavm module root of a challenge, as well diff --git a/staker/challenge-cache/cache_test.go b/staker/challenge-cache/cache_test.go index 53b8bf85c8..68491ebaa2 100644 --- a/staker/challenge-cache/cache_test.go +++ b/staker/challenge-cache/cache_test.go @@ -27,7 +27,10 @@ func TestCache(t *testing.T) { t.Fatal(err) } }) - cache := New(basePath) + cache, err := New(basePath) + if err != nil { + t.Fatal(err) + } key := &Key{ WavmModuleRoot: common.BytesToHash([]byte("foo")), MessageHeight: 0, @@ -49,7 +52,7 @@ func TestCache(t *testing.T) { common.BytesToHash([]byte("bar")), common.BytesToHash([]byte("baz")), } - err := cache.Put(key, want) + err = cache.Put(key, want) if err != nil { t.Fatal(err) } @@ -290,7 +293,10 @@ func BenchmarkCache_Read_32Mb(b *testing.B) { b.Fatal(err) } }) - cache := New(basePath) + cache, err := New(basePath) + if err != nil { + b.Fatal(err) + } key := &Key{ WavmModuleRoot: common.BytesToHash([]byte("foo")), MessageHeight: 0, diff --git a/staker/staker.go b/staker/staker.go index 2d831f0186..bb8773437d 100644 --- a/staker/staker.go +++ b/staker/staker.go @@ -28,6 +28,7 @@ import ( "github.com/offchainlabs/nitro/solgen/go/rollupgen" "github.com/offchainlabs/nitro/staker/txbuilder" "github.com/offchainlabs/nitro/util/arbmath" + "github.com/offchainlabs/nitro/util/headerreader" "github.com/offchainlabs/nitro/util/stopwaiter" "github.com/offchainlabs/nitro/validator" ) @@ -245,7 +246,7 @@ type LatestConfirmedNotifier interface { type Staker struct { *L1Validator stopwaiter.StopWaiter - l1Reader L1ReaderInterface + l1Reader *headerreader.HeaderReader stakedNotifiers []LatestStakedNotifier confirmedNotifiers []LatestConfirmedNotifier activeChallenge *ChallengeManager @@ -283,7 +284,7 @@ type ValidatorWalletInterface interface { } func NewStaker( - l1Reader L1ReaderInterface, + l1Reader *headerreader.HeaderReader, wallet ValidatorWalletInterface, callOpts bind.CallOpts, config L1ValidatorConfig, diff --git a/staker/state_provider.go b/staker/state_provider.go index c4fe00feb1..98ac353866 100644 --- a/staker/state_provider.go +++ b/staker/state_provider.go @@ -90,7 +90,10 @@ func NewStateManager( challengeLeafHeights []l2stateprovider.Height, validatorName string, ) (*StateManager, error) { - historyCache := challengecache.New(cacheBaseDir) + historyCache, err := challengecache.New(cacheBaseDir) + if err != nil { + return nil, err + } sm := &StateManager{ validator: val, historyCache: historyCache, diff --git a/staker/stateless_block_validator.go b/staker/stateless_block_validator.go index c4968ca9e4..acd86f8627 100644 --- a/staker/stateless_block_validator.go +++ b/staker/stateless_block_validator.go @@ -19,7 +19,6 @@ import ( "github.com/offchainlabs/nitro/validator" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" @@ -71,13 +70,6 @@ type InboxReaderInterface interface { GetSequencerMessageBytes(ctx context.Context, seqNum uint64) ([]byte, error) } -type L1ReaderInterface interface { - Client() arbutil.L1Interface - Subscribe(bool) (<-chan *types.Header, func()) - WaitForTxApproval(ctx context.Context, tx *types.Transaction) (*types.Receipt, error) - UseFinalityData() bool -} - type GlobalStatePosition struct { BatchNumber uint64 PosInBatch uint64 diff --git a/staker/validatorwallet/contract.go b/staker/validatorwallet/contract.go index 302e4fb439..774e9ab407 100644 --- a/staker/validatorwallet/contract.go +++ b/staker/validatorwallet/contract.go @@ -400,19 +400,12 @@ func (b *Contract) DataPoster() *dataposter.DataPoster { return b.dataPoster } -type L1ReaderInterface interface { - Client() arbutil.L1Interface - Subscribe(bool) (<-chan *types.Header, func()) - WaitForTxApproval(ctx context.Context, tx *types.Transaction) (*types.Receipt, error) - UseFinalityData() bool -} - func GetValidatorWalletContract( ctx context.Context, validatorWalletFactoryAddr common.Address, fromBlock int64, transactAuth *bind.TransactOpts, - l1Reader L1ReaderInterface, + l1Reader *headerreader.HeaderReader, createIfMissing bool, ) (*common.Address, error) { client := l1Reader.Client() diff --git a/system_tests/common_test.go b/system_tests/common_test.go index 327ce94e06..7752fbd34e 100644 --- a/system_tests/common_test.go +++ b/system_tests/common_test.go @@ -23,6 +23,7 @@ import ( "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/cmd/genericconf" "github.com/offchainlabs/nitro/das" + "github.com/offchainlabs/nitro/deploy" "github.com/offchainlabs/nitro/execution/gethexec" "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/headerreader" @@ -188,7 +189,7 @@ func (b *NodeBuilder) Build(t *testing.T) func() { } else { l2 := NewTestClient(b.ctx) b.L2Info, l2.ConsensusNode, l2.Client = - createTestNode(t, b.ctx, b.L2Info, b.nodeConfig, b.execConfig, b.takeOwnership) + createTestNode(t, b.ctx, b.L2Info, b.nodeConfig, b.execConfig, b.chainConfig, b.takeOwnership) b.L2 = l2 } b.L2.ExecNode = getExecNode(t, b.L2.ConsensusNode) @@ -656,7 +657,7 @@ func DeployOnTestL1( nativeToken := common.Address{} maxDataSize := big.NewInt(117964) - addresses, err := arbnode.DeployOnL1( + addresses, err := deploy.DeployOnL1( ctx, l1Reader, &l1TransactionOpts, @@ -800,7 +801,7 @@ func createTestNodeWithL1( // L2 -Only. Enough for tests that needs no interface to L1 // Requires precompiles.AllowDebugPrecompiles = true func createTestNode( - t *testing.T, ctx context.Context, l2Info *BlockchainTestInfo, nodeConfig *arbnode.Config, execConfig *gethexec.Config, takeOwnership bool, + t *testing.T, ctx context.Context, l2Info *BlockchainTestInfo, nodeConfig *arbnode.Config, execConfig *gethexec.Config, chainConfig *params.ChainConfig, takeOwnership bool, ) (*BlockchainTestInfo, *arbnode.Node, *ethclient.Client) { if nodeConfig == nil { nodeConfig = arbnode.ConfigDefaultL2Test() @@ -813,7 +814,7 @@ func createTestNode( AddDefaultValNode(t, ctx, nodeConfig, true) - l2info, stack, chainDb, arbDb, blockchain := createL2BlockChain(t, l2Info, "", params.ArbitrumDevTestChainConfig(), &execConfig.Caching) + l2info, stack, chainDb, arbDb, blockchain := createL2BlockChain(t, l2Info, "", chainConfig, &execConfig.Caching) Require(t, execConfig.Validate()) execConfigFetcher := func() *gethexec.Config { return execConfig } diff --git a/system_tests/deployment_test.go b/system_tests/deployment_test.go new file mode 100644 index 0000000000..a689abaa1a --- /dev/null +++ b/system_tests/deployment_test.go @@ -0,0 +1,143 @@ +// Copyright 2021-2022, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE + +package arbtest + +import ( + "bytes" + "context" + "math/big" + "strings" + "testing" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/params" +) + +func testContractDeployment(t *testing.T, ctx context.Context, client *ethclient.Client, contractCode []byte, accountInfo *AccountInfo, expectedEstimateGasError error) { + // First, we need to make the "deploy code" which returns the contractCode to be deployed + deployCode := []byte{ + 0x7F, // PUSH32 + } + // len(contractCode) + deployCode = append(deployCode, math.U256Bytes(big.NewInt(int64(len(contractCode))))...) + var codeOffset byte = 42 + deployCode = append(deployCode, []byte{ + 0x80, // DUP + 0x60, codeOffset, // PUSH1 [codeOffset] + 0x60, 0x00, // PUSH1 0 + 0x39, // CODECOPY + 0x60, 0x00, // PUSH1 0 + 0xF3, // RETURN + }...) + if len(deployCode) != int(codeOffset) { + Fatal(t, "computed codeOffset", codeOffset, "incorrectly, should be", len(deployCode)) + } + deployCode = append(deployCode, contractCode...) + + deploymentGas, err := client.EstimateGas(ctx, ethereum.CallMsg{ + Data: deployCode, + }) + if expectedEstimateGasError != nil { + if err == nil { + Fatal(t, "missing expected contract deployment error", expectedEstimateGasError) + } else if strings.Contains(err.Error(), expectedEstimateGasError.Error()) { + // success + return + } + // else, fall through to Require, as this error is unexpected + } + Require(t, err) + + chainId, err := client.ChainID(ctx) + Require(t, err) + latestHeader, err := client.HeaderByNumber(ctx, nil) + Require(t, err) + nonce, err := client.PendingNonceAt(ctx, accountInfo.Address) + Require(t, err) + + tx := types.NewTx(&types.DynamicFeeTx{ + ChainID: chainId, + Nonce: nonce, + GasTipCap: common.Big0, + GasFeeCap: latestHeader.BaseFee, + Gas: deploymentGas, + To: nil, + Value: common.Big0, + Data: deployCode, + }) + tx, err = types.SignTx(tx, types.LatestSignerForChainID(chainId), accountInfo.PrivateKey) + Require(t, err) + + err = client.SendTransaction(ctx, tx) + Require(t, err) + receipt, err := EnsureTxSucceeded(ctx, client, tx) + Require(t, err) + + deployedCode, err := client.CodeAt(ctx, receipt.ContractAddress, receipt.BlockNumber) + Require(t, err) + if !bytes.Equal(deployedCode, contractCode) { + Fatal(t, "expected to deploy code of length", len(contractCode), "but got code of length", len(deployedCode)) + } + + callResult, err := client.CallContract(ctx, ethereum.CallMsg{To: &receipt.ContractAddress}, nil) + Require(t, err) + if len(callResult) > 0 { + Fatal(t, "somehow got a non-empty result from contract of", callResult) + } +} + +// Makes a contract which does nothing but takes up a given length +func makeContractOfLength(length int) []byte { + ret := make([]byte, length) + for i := 0; i < length; i++ { + if i%2 == 0 { + ret[i] = 0x58 // PC + } else { + ret[i] = 0x50 // POP + } + } + return ret +} + +func TestContractDeployment(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + account := builder.L2Info.GetInfoWithPrivKey("Faucet") + for _, size := range []int{0, 1, 1000, 20000, params.MaxCodeSize} { + testContractDeployment(t, ctx, builder.L2.Client, makeContractOfLength(size), account, nil) + } + + testContractDeployment(t, ctx, builder.L2.Client, makeContractOfLength(40000), account, vm.ErrMaxCodeSizeExceeded) + testContractDeployment(t, ctx, builder.L2.Client, makeContractOfLength(60000), account, core.ErrMaxInitCodeSizeExceeded) +} + +func TestExtendedContractDeployment(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + builder.chainConfig.ArbitrumChainParams.MaxCodeSize = params.MaxCodeSize * 3 + builder.chainConfig.ArbitrumChainParams.MaxInitCodeSize = params.MaxInitCodeSize * 3 + cleanup := builder.Build(t) + defer cleanup() + + account := builder.L2Info.GetInfoWithPrivKey("Faucet") + for _, size := range []int{0, 1, 1000, 20000, 30000, 40000, 60000, params.MaxCodeSize * 3} { + testContractDeployment(t, ctx, builder.L2.Client, makeContractOfLength(size), account, nil) + } + + testContractDeployment(t, ctx, builder.L2.Client, makeContractOfLength(100000), account, vm.ErrMaxCodeSizeExceeded) + testContractDeployment(t, ctx, builder.L2.Client, makeContractOfLength(200000), account, core.ErrMaxInitCodeSizeExceeded) +} diff --git a/system_tests/full_challenge_impl_test.go b/system_tests/full_challenge_impl_test.go index 98e776487a..43ba6a056c 100644 --- a/system_tests/full_challenge_impl_test.go +++ b/system_tests/full_challenge_impl_test.go @@ -42,30 +42,31 @@ import ( ) func DeployOneStepProofEntry(t *testing.T, ctx context.Context, auth *bind.TransactOpts, client *ethclient.Client) common.Address { - osp0, _, _, err := ospgen.DeployOneStepProver0(auth, client) - if err != nil { - Fatal(t, err) - } - ospMem, _, _, err := ospgen.DeployOneStepProverMemory(auth, client) - if err != nil { - Fatal(t, err) - } - ospMath, _, _, err := ospgen.DeployOneStepProverMath(auth, client) - if err != nil { - Fatal(t, err) - } - ospHostIo, _, _, err := ospgen.DeployOneStepProverHostIo(auth, client) - if err != nil { - Fatal(t, err) - } + osp0, tx, _, err := ospgen.DeployOneStepProver0(auth, client) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, client, tx) + Require(t, err) + + ospMem, tx, _, err := ospgen.DeployOneStepProverMemory(auth, client) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, client, tx) + Require(t, err) + + ospMath, tx, _, err := ospgen.DeployOneStepProverMath(auth, client) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, client, tx) + Require(t, err) + + ospHostIo, tx, _, err := ospgen.DeployOneStepProverHostIo(auth, client) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, client, tx) + Require(t, err) + ospEntry, tx, _, err := ospgen.DeployOneStepProofEntry(auth, client, osp0, ospMem, ospMath, ospHostIo) - if err != nil { - Fatal(t, err) - } + Require(t, err) _, err = EnsureTxSucceeded(ctx, client, tx) - if err != nil { - Fatal(t, err) - } + Require(t, err) + return ospEntry } diff --git a/system_tests/retryable_test.go b/system_tests/retryable_test.go index 3400af335d..4619671700 100644 --- a/system_tests/retryable_test.go +++ b/system_tests/retryable_test.go @@ -191,11 +191,92 @@ func TestSubmitRetryableImmediateSuccess(t *testing.T) { l2balance, err := builder.L2.Client.BalanceAt(ctx, builder.L2Info.GetAddress("User2"), nil) Require(t, err) - if !arbmath.BigEquals(l2balance, big.NewInt(1e6)) { + if !arbmath.BigEquals(l2balance, callValue) { Fatal(t, "Unexpected balance:", l2balance) } } +func TestSubmitRetryableEmptyEscrow(t *testing.T) { + t.Parallel() + builder, delayedInbox, lookupL2Tx, ctx, teardown := retryableSetup(t) + defer teardown() + + user2Address := builder.L2Info.GetAddress("User2") + beneficiaryAddress := builder.L2Info.GetAddress("Beneficiary") + + deposit := arbmath.BigMul(big.NewInt(1e12), big.NewInt(1e12)) + callValue := common.Big0 + + nodeInterface, err := node_interfacegen.NewNodeInterface(types.NodeInterfaceAddress, builder.L2.Client) + Require(t, err, "failed to deploy NodeInterface") + + // estimate the gas needed to auto redeem the retryable + usertxoptsL2 := builder.L2Info.GetDefaultTransactOpts("Faucet", ctx) + usertxoptsL2.NoSend = true + usertxoptsL2.GasMargin = 0 + tx, err := nodeInterface.EstimateRetryableTicket( + &usertxoptsL2, + usertxoptsL2.From, + deposit, + user2Address, + callValue, + beneficiaryAddress, + beneficiaryAddress, + []byte{0x32, 0x42, 0x32, 0x88}, // increase the cost to beyond that of params.TxGas + ) + Require(t, err, "failed to estimate retryable submission") + estimate := tx.Gas() + colors.PrintBlue("estimate: ", estimate) + + // submit & auto redeem the retryable using the gas estimate + usertxoptsL1 := builder.L1Info.GetDefaultTransactOpts("Faucet", ctx) + usertxoptsL1.Value = deposit + l1tx, err := delayedInbox.CreateRetryableTicket( + &usertxoptsL1, + user2Address, + callValue, + big.NewInt(1e16), + beneficiaryAddress, + beneficiaryAddress, + arbmath.UintToBig(estimate), + big.NewInt(l2pricing.InitialBaseFeeWei*2), + []byte{0x32, 0x42, 0x32, 0x88}, + ) + Require(t, err) + + l1Receipt, err := builder.L1.EnsureTxSucceeded(l1tx) + Require(t, err) + if l1Receipt.Status != types.ReceiptStatusSuccessful { + Fatal(t, "l1Receipt indicated failure") + } + + waitForL1DelayBlocks(t, ctx, builder) + + l2Tx := lookupL2Tx(l1Receipt) + receipt, err := builder.L2.EnsureTxSucceeded(l2Tx) + Require(t, err) + if receipt.Status != types.ReceiptStatusSuccessful { + Fatal(t) + } + + l2balance, err := builder.L2.Client.BalanceAt(ctx, builder.L2Info.GetAddress("User2"), nil) + Require(t, err) + + if !arbmath.BigEquals(l2balance, callValue) { + Fatal(t, "Unexpected balance:", l2balance) + } + + escrowAccount := retryables.RetryableEscrowAddress(l2Tx.Hash()) + state, err := builder.L2.ExecNode.ArbInterface.BlockChain().State() + Require(t, err) + escrowCodeHash := state.GetCodeHash(escrowAccount) + if escrowCodeHash == (common.Hash{}) { + Fatal(t, "Escrow account deleted (or not created)") + } else if escrowCodeHash != types.EmptyCodeHash { + Fatal(t, "Escrow account has unexpected code hash", escrowCodeHash) + } +} + func TestSubmitRetryableFailThenRetry(t *testing.T) { t.Parallel() builder, delayedInbox, lookupL2Tx, ctx, teardown := retryableSetup(t) diff --git a/system_tests/wrap_transaction_test.go b/system_tests/wrap_transaction_test.go index e4ce6a4bb8..85cf015211 100644 --- a/system_tests/wrap_transaction_test.go +++ b/system_tests/wrap_transaction_test.go @@ -15,8 +15,10 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rpc" "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" + "github.com/offchainlabs/nitro/util/headerreader" ) func GetPendingBlockNumber(ctx context.Context, client arbutil.L1Interface) (*big.Int, error) { @@ -77,11 +79,27 @@ func EnsureTxSucceeded(ctx context.Context, client arbutil.L1Interface, tx *type } func EnsureTxSucceededWithTimeout(ctx context.Context, client arbutil.L1Interface, tx *types.Transaction, timeout time.Duration) (*types.Receipt, error) { - txRes, err := WaitForTx(ctx, client, tx.Hash(), timeout) + receipt, err := WaitForTx(ctx, client, tx.Hash(), timeout) if err != nil { return nil, fmt.Errorf("waitFoxTx (tx=%s) got: %w", tx.Hash().Hex(), err) } - return txRes, arbutil.DetailTxError(ctx, client, tx, txRes) + if receipt.Status == types.ReceiptStatusSuccessful && tx.ChainId().Cmp(simulatedChainID) == 0 { + for { + safeBlock, err := client.HeaderByNumber(ctx, big.NewInt(int64(rpc.SafeBlockNumber))) + if err != nil { + return receipt, err + } + if safeBlock.Number.Cmp(receipt.BlockNumber) >= 0 { + break + } + select { + case <-time.After(headerreader.TestConfig.Dangerous.WaitForTxApprovalSafePoll): + case <-ctx.Done(): + return receipt, ctx.Err() + } + } + } + return receipt, arbutil.DetailTxError(ctx, client, tx, receipt) } func headerSubscribeMainLoop(chanOut chan<- *types.Header, ctx context.Context, client ethereum.ChainReader) { diff --git a/util/headerreader/header_reader.go b/util/headerreader/header_reader.go index 04b9cf2660..94fb857abb 100644 --- a/util/headerreader/header_reader.go +++ b/util/headerreader/header_reader.go @@ -55,13 +55,18 @@ type cachedHeader struct { } type Config struct { - Enable bool `koanf:"enable"` - PollOnly bool `koanf:"poll-only" reload:"hot"` - PollInterval time.Duration `koanf:"poll-interval" reload:"hot"` - SubscribeErrInterval time.Duration `koanf:"subscribe-err-interval" reload:"hot"` - TxTimeout time.Duration `koanf:"tx-timeout" reload:"hot"` - OldHeaderTimeout time.Duration `koanf:"old-header-timeout" reload:"hot"` - UseFinalityData bool `koanf:"use-finality-data" reload:"hot"` + Enable bool `koanf:"enable"` + PollOnly bool `koanf:"poll-only" reload:"hot"` + PollInterval time.Duration `koanf:"poll-interval" reload:"hot"` + SubscribeErrInterval time.Duration `koanf:"subscribe-err-interval" reload:"hot"` + TxTimeout time.Duration `koanf:"tx-timeout" reload:"hot"` + OldHeaderTimeout time.Duration `koanf:"old-header-timeout" reload:"hot"` + UseFinalityData bool `koanf:"use-finality-data" reload:"hot"` + Dangerous DangerousConfig `koanf:"dangerous"` +} + +type DangerousConfig struct { + WaitForTxApprovalSafePoll time.Duration `koanf:"wait-for-tx-approval-safe-poll"` } type ConfigFetcher func() *Config @@ -74,6 +79,9 @@ var DefaultConfig = Config{ TxTimeout: 5 * time.Minute, OldHeaderTimeout: 5 * time.Minute, UseFinalityData: true, + Dangerous: DangerousConfig{ + WaitForTxApprovalSafePoll: 0, + }, } func AddOptions(prefix string, f *flag.FlagSet) { @@ -84,6 +92,11 @@ func AddOptions(prefix string, f *flag.FlagSet) { f.Duration(prefix+".subscribe-err-interval", DefaultConfig.SubscribeErrInterval, "interval for subscribe error") f.Duration(prefix+".tx-timeout", DefaultConfig.TxTimeout, "timeout when waiting for a transaction") f.Duration(prefix+".old-header-timeout", DefaultConfig.OldHeaderTimeout, "warns if the latest l1 block is at least this old") + AddDangerousOptions(prefix+".dangerous", f) +} + +func AddDangerousOptions(prefix string, f *flag.FlagSet) { + f.Duration(prefix+".wait-for-tx-approval-safe-poll", DefaultConfig.Dangerous.WaitForTxApprovalSafePoll, "Dangerous! only meant to be used by system tests") } var TestConfig = Config{ @@ -93,6 +106,9 @@ var TestConfig = Config{ TxTimeout: time.Second * 5, OldHeaderTimeout: 5 * time.Minute, UseFinalityData: false, + Dangerous: DangerousConfig{ + WaitForTxApprovalSafePoll: time.Millisecond * 100, + }, } func New(ctx context.Context, client arbutil.L1Interface, config ConfigFetcher, arbSysPrecompile ArbSysInterface) (*HeaderReader, error) { @@ -120,6 +136,8 @@ func New(ctx context.Context, client arbutil.L1Interface, config ConfigFetcher, }, nil } +func (s *HeaderReader) Config() *Config { return s.config() } + // Subscribe to block header updates. // Subscribers are notified when there is a change. // Channel could be missing headers and have duplicates. @@ -329,22 +347,52 @@ func (s *HeaderReader) WaitForTxApproval(ctxIn context.Context, tx *types.Transa ctx, cancel := context.WithTimeout(ctxIn, s.config().TxTimeout) defer cancel() txHash := tx.Hash() + waitForBlock := false + waitForSafePoll := s.config().Dangerous.WaitForTxApprovalSafePoll for { - receipt, err := s.client.TransactionReceipt(ctx, txHash) - if err == nil && receipt.BlockNumber.IsUint64() { - receiptBlockNr := receipt.BlockNumber.Uint64() - callBlockNr := s.LastPendingCallBlockNr() - if callBlockNr > receiptBlockNr { - return receipt, arbutil.DetailTxError(ctx, s.client, tx, receipt) + if waitForBlock { + select { + case _, ok := <-headerchan: + if !ok { + return nil, fmt.Errorf("waiting for %v: channel closed", txHash) + } + case <-ctx.Done(): + return nil, ctx.Err() } } - select { - case _, ok := <-headerchan: - if !ok { - return nil, fmt.Errorf("waiting for %v: channel closed", txHash) + waitForBlock = true + receipt, err := s.client.TransactionReceipt(ctx, txHash) + if err != nil || receipt == nil { + continue + } + if !receipt.BlockNumber.IsUint64() { + continue + } + receiptBlockNr := receipt.BlockNumber.Uint64() + callBlockNr := s.LastPendingCallBlockNr() + if callBlockNr <= receiptBlockNr { + continue + } + if waitForSafePoll != 0 { + safeBlock, err := s.client.BlockByNumber(ctx, big.NewInt(int64(rpc.SafeBlockNumber))) + if err != nil || safeBlock == nil { + log.Warn("parent chain: failed getting safeblock", "err", err) + continue } - case <-ctx.Done(): - return nil, ctx.Err() + if safeBlock.NumberU64() < receiptBlockNr { + log.Info("parent chain: waiting for safe block (see wait-for-tx-approval-safe-poll)", "waiting", receiptBlockNr, "safe", safeBlock.NumberU64()) + select { + case <-time.After(time.Millisecond * 100): + case <-ctx.Done(): + return nil, ctx.Err() + } + waitForBlock = false + continue + } + } + block, err := s.client.BlockByHash(ctx, receipt.BlockHash) + if block != nil && err == nil { + return receipt, arbutil.DetailTxError(ctx, s.client, tx, receipt) } } }