diff --git a/arbnode/batch_poster.go b/arbnode/batch_poster.go index d534c39c5e..0b9ee1e6b4 100644 --- a/arbnode/batch_poster.go +++ b/arbnode/batch_poster.go @@ -80,8 +80,10 @@ var ( const ( batchPosterSimpleRedisLockKey = "node.batch-poster.redis-lock.simple-lock-key" - sequencerBatchPostMethodName = "addSequencerL2BatchFromOrigin0" - sequencerBatchPostWithBlobsMethodName = "addSequencerL2BatchFromBlobs" + sequencerBatchPostMethodName = "addSequencerL2BatchFromOrigin0" + sequencerBatchPostWithBlobsMethodName = "addSequencerL2BatchFromBlobs" + sequencerBatchPostDelayProofMethodName = "addSequencerL2BatchFromOriginDelayProof" + sequencerBatchPostWithBlobsDelayProofMethodName = "addSequencerL2BatchFromBlobsDelayProof" ) type batchPosterPosition struct { @@ -312,6 +314,7 @@ func NewBatchPoster(ctx context.Context, opts *BatchPosterOpts) (*BatchPoster, e if err = opts.Config().Validate(); err != nil { return nil, err } + // TODO(delaybuffer) use new bridgegen seqInboxABI, err := bridgegen.SequencerInboxMetaData.GetAbi() if err != nil { return nil, err @@ -717,6 +720,7 @@ type buildingBatch struct { haveUsefulMessage bool use4844 bool muxBackend *simulatedMuxBackend + firstDelayedMsg *arbostypes.MessageWithMetadata firstNonDelayedMsg *arbostypes.MessageWithMetadata firstUsefulMsg *arbostypes.MessageWithMetadata } @@ -955,15 +959,25 @@ func (b *BatchPoster) encodeAddBatch( l2MessageData []byte, delayedMsg uint64, use4844 bool, + delayProof *bridgegen.DelayProof, ) ([]byte, []kzg4844.Blob, error) { - methodName := sequencerBatchPostMethodName + var methodName string if use4844 { - methodName = sequencerBatchPostWithBlobsMethodName + if delayProof != nil { + methodName = sequencerBatchPostWithBlobsDelayProofMethodName + } else { + methodName = sequencerBatchPostWithBlobsMethodName + } + } else if delayProof != nil { + methodName = sequencerBatchPostDelayProofMethodName + } else { + methodName = sequencerBatchPostMethodName } method, ok := b.seqInboxABI.Methods[methodName] if !ok { return nil, nil, errors.New("failed to find add batch method") } + var calldata []byte var kzgBlobs []kzg4844.Blob var err error @@ -972,6 +986,9 @@ func (b *BatchPoster) encodeAddBatch( if err != nil { return nil, nil, fmt.Errorf("failed to encode blobs: %w", err) } + } + switch methodName { + case sequencerBatchPostWithBlobsMethodName: // EIP4844 transactions to the sequencer inbox will not use transaction calldata for L2 info. calldata, err = method.Inputs.Pack( seqNum, @@ -980,7 +997,25 @@ func (b *BatchPoster) encodeAddBatch( new(big.Int).SetUint64(uint64(prevMsgNum)), new(big.Int).SetUint64(uint64(newMsgNum)), ) - } else { + case sequencerBatchPostWithBlobsDelayProofMethodName: + calldata, err = method.Inputs.Pack( + seqNum, + new(big.Int).SetUint64(delayedMsg), + b.config().gasRefunder, + new(big.Int).SetUint64(uint64(prevMsgNum)), + new(big.Int).SetUint64(uint64(newMsgNum)), + delayProof, + ) + case sequencerBatchPostMethodName: + calldata, err = method.Inputs.Pack( + seqNum, + l2MessageData, + new(big.Int).SetUint64(delayedMsg), + b.config().gasRefunder, + new(big.Int).SetUint64(uint64(prevMsgNum)), + new(big.Int).SetUint64(uint64(newMsgNum)), + ) + case sequencerBatchPostDelayProofMethodName: calldata, err = method.Inputs.Pack( seqNum, l2MessageData, @@ -988,7 +1023,10 @@ func (b *BatchPoster) encodeAddBatch( b.config().gasRefunder, new(big.Int).SetUint64(uint64(prevMsgNum)), new(big.Int).SetUint64(uint64(newMsgNum)), + delayProof, ) + default: + panic("impossible") } if err != nil { return nil, nil, err @@ -1015,7 +1053,17 @@ func estimateGas(client rpc.ClientInterface, ctx context.Context, params estimat return uint64(gas), err } -func (b *BatchPoster) estimateGas(ctx context.Context, sequencerMessage []byte, delayedMessages uint64, realData []byte, realBlobs []kzg4844.Blob, realNonce uint64, realAccessList types.AccessList) (uint64, error) { +func (b *BatchPoster) estimateGas( + ctx context.Context, + sequencerMessage []byte, + delayedMessages uint64, + realData []byte, + realBlobs []kzg4844.Blob, + realNonce uint64, + realAccessList types.AccessList, + delayProof *bridgegen.DelayProof, +) (uint64, error) { + config := b.config() rpcClient := b.l1Reader.Client() rawRpcClient := rpcClient.Client() @@ -1057,7 +1105,7 @@ func (b *BatchPoster) estimateGas(ctx context.Context, sequencerMessage []byte, // However, we set nextMsgNum to 1 because it is necessary for a correct estimation for the final to be non-zero. // Because we're likely estimating against older state, this might not be the actual next message, // but the gas used should be the same. - data, kzgBlobs, err := b.encodeAddBatch(abi.MaxUint256, 0, 1, sequencerMessage, delayedMessages, len(realBlobs) > 0) + data, kzgBlobs, err := b.encodeAddBatch(abi.MaxUint256, 0, 1, sequencerMessage, delayedMessages, len(realBlobs) > 0, delayProof) if err != nil { return 0, err } @@ -1309,7 +1357,11 @@ func (b *BatchPoster) maybePostSequencerBatch(ctx context.Context) (bool, error) b.building.firstUsefulMsg = msg } } - if !isDelayed && b.building.firstNonDelayedMsg == nil { + if isDelayed { + if b.building.firstDelayedMsg == nil { + b.building.firstDelayedMsg = msg + } + } else if b.building.firstNonDelayedMsg == nil { b.building.firstNonDelayedMsg = msg } b.building.msgCount++ @@ -1324,6 +1376,26 @@ func (b *BatchPoster) maybePostSequencerBatch(ctx context.Context) (bool, error) } } + delayBuffer, err := GetDelayBufferConfig(ctx, b.seqInbox) + if err != nil { + return false, err + } + if delayBuffer.Enabled && b.building.firstDelayedMsg != nil { + latestHeader, err := b.l1Reader.LastHeader(ctx) + if err != nil { + return false, err + } + latestBlock := latestHeader.Number.Uint64() + firstDelayedMsgBlock := b.building.firstDelayedMsg.Message.Header.BlockNumber + if latestBlock >= firstDelayedMsgBlock+delayBuffer.Threshold { + log.Info("force post batch because of the delay buffer", + "firstDelayedMsgBlock", firstDelayedMsgBlock, + "threshold", delayBuffer.Threshold, + "latestBlock", latestBlock) + forcePostBatch = true + } + } + if b.building.firstNonDelayedMsg != nil && hasL1Bound && config.ReorgResistanceMargin > 0 { firstMsgBlockNumber := b.building.firstNonDelayedMsg.Message.Header.BlockNumber firstMsgTimeStamp := b.building.firstNonDelayedMsg.Message.Header.Timestamp @@ -1415,7 +1487,15 @@ func (b *BatchPoster) maybePostSequencerBatch(ctx context.Context) (bool, error) prevMessageCount = 0 } - data, kzgBlobs, err := b.encodeAddBatch(new(big.Int).SetUint64(batchPosition.NextSeqNum), prevMessageCount, b.building.msgCount, sequencerMsg, b.building.segments.delayedMsg, b.building.use4844) + var delayProof *bridgegen.DelayProof + if delayBuffer.Enabled && b.building.firstDelayedMsg != nil { + delayProof, err = GenDelayProof(ctx, b.building.firstDelayedMsg, b.inbox) + if err != nil { + return false, fmt.Errorf("failed to generate delay proof: %w", err) + } + } + + data, kzgBlobs, err := b.encodeAddBatch(new(big.Int).SetUint64(batchPosition.NextSeqNum), prevMessageCount, b.building.msgCount, sequencerMsg, b.building.segments.delayedMsg, b.building.use4844, delayProof) if err != nil { return false, err } @@ -1430,7 +1510,7 @@ func (b *BatchPoster) maybePostSequencerBatch(ctx context.Context) (bool, error) // In theory, this might reduce gas usage, but only by a factor that's already // accounted for in `config.ExtraBatchGas`, as that same factor can appear if a user // posts a new delayed message that we didn't see while gas estimating. - gasLimit, err := b.estimateGas(ctx, sequencerMsg, lastPotentialMsg.DelayedMessagesRead, data, kzgBlobs, nonce, accessList) + gasLimit, err := b.estimateGas(ctx, sequencerMsg, lastPotentialMsg.DelayedMessagesRead, data, kzgBlobs, nonce, accessList, delayProof) if err != nil { return false, err } diff --git a/arbnode/delay_buffer.go b/arbnode/delay_buffer.go new file mode 100644 index 0000000000..508fec1b35 --- /dev/null +++ b/arbnode/delay_buffer.go @@ -0,0 +1,86 @@ +// Copyright 2024, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE + +// This file contains functions related to the delay buffer feature that are used mostly in the +// batch poster. + +package arbnode + +import ( + "context" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/offchainlabs/nitro/arbos/arbostypes" + "github.com/offchainlabs/nitro/solgen/go/bridgegen" + "github.com/offchainlabs/nitro/util/headerreader" +) + +// DelayBufferConfig originates from the sequencer inbox contract. +type DelayBufferConfig struct { + Enabled bool + Threshold uint64 +} + +// GetBufferConfig gets the delay buffer config from the sequencer inbox contract. +// If the contract doesn't support the delay buffer, it returns a config with Enabled set to false. +func GetDelayBufferConfig(ctx context.Context, sequencerInbox *bridgegen.SequencerInbox) ( + *DelayBufferConfig, error) { + + callOpts := bind.CallOpts{Context: ctx} + enabled, err := sequencerInbox.IsDelayBufferable(&callOpts) + if err != nil { + if headerreader.ExecutionRevertedRegexp.MatchString(err.Error()) { + return &DelayBufferConfig{Enabled: false}, nil + } + return nil, fmt.Errorf("retrieve SequencerInbox.isDelayBufferable: %w", err) + } + if !enabled { + return &DelayBufferConfig{Enabled: false}, nil + } + bufferData, err := sequencerInbox.Buffer(&callOpts) + if err != nil { + return nil, fmt.Errorf("retrieve SequencerInbox.buffer: %w", err) + } + config := &DelayBufferConfig{ + Enabled: true, + Threshold: bufferData.Threshold, + } + return config, nil +} + +// GenDelayProof generates the delay proof based on batch's first delayed message and the delayed +// accumulater from the inbox. +func GenDelayProof(ctx context.Context, message *arbostypes.MessageWithMetadata, inbox *InboxTracker) ( + *bridgegen.DelayProof, error) { + + if message.DelayedMessagesRead == 0 { + return nil, fmt.Errorf("BUG: trying to generate delay proof without delayed message") + } + seqNum := message.DelayedMessagesRead - 1 + var beforeDelayedAcc common.Hash + if seqNum > 0 { + var err error + beforeDelayedAcc, err = inbox.GetDelayedAcc(seqNum - 1) + if err != nil { + return nil, err + } + } + delayedMessage := bridgegen.MessagesMessage{ + Kind: message.Message.Header.Kind, + Sender: message.Message.Header.Poster, + BlockNumber: message.Message.Header.BlockNumber, + Timestamp: message.Message.Header.Timestamp, + InboxSeqNum: new(big.Int).SetUint64(seqNum), + BaseFeeL1: message.Message.Header.L1BaseFee, + MessageDataHash: crypto.Keccak256Hash(message.Message.L2msg), + } + delayProof := &bridgegen.DelayProof{ + BeforeDelayedAcc: beforeDelayedAcc, + DelayedMessage: delayedMessage, + } + return delayProof, nil +} diff --git a/arbnode/node.go b/arbnode/node.go index c5b3bbe071..4cf3e01e05 100644 --- a/arbnode/node.go +++ b/arbnode/node.go @@ -50,7 +50,15 @@ import ( "github.com/offchainlabs/nitro/wsbroadcastserver" ) -func GenerateRollupConfig(prod bool, wasmModuleRoot common.Hash, rollupOwner common.Address, chainConfig *params.ChainConfig, serializedChainConfig []byte, loserStakeEscrow common.Address) rollupgen.Config { +func DefaultBufferConfig() rollupgen.BufferConfig { + return rollupgen.BufferConfig{ + Threshold: 600, // 1 hour of blocks + Max: 14400, // 2 days of blocks + ReplenishRateInBasis: 500, // 5% + } +} + +func GenerateRollupConfig(prod bool, wasmModuleRoot common.Hash, rollupOwner common.Address, chainConfig *params.ChainConfig, serializedChainConfig []byte, loserStakeEscrow common.Address, bufferConfig rollupgen.BufferConfig) rollupgen.Config { var confirmPeriod uint64 if prod { confirmPeriod = 45818 @@ -74,6 +82,7 @@ func GenerateRollupConfig(prod bool, wasmModuleRoot common.Hash, rollupOwner com DelaySeconds: big.NewInt(60 * 60 * 24), FutureSeconds: big.NewInt(60 * 60), }, + BufferConfig: bufferConfig, } } diff --git a/cmd/deploy/deploy.go b/cmd/deploy/deploy.go index c70ceb1d94..6060c55586 100644 --- a/cmd/deploy/deploy.go +++ b/cmd/deploy/deploy.go @@ -17,6 +17,7 @@ import ( "github.com/offchainlabs/nitro/cmd/chaininfo" "github.com/offchainlabs/nitro/cmd/genericconf" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" + "github.com/offchainlabs/nitro/solgen/go/rollupgen" "github.com/offchainlabs/nitro/util/headerreader" "github.com/offchainlabs/nitro/validator/server_common" @@ -61,6 +62,7 @@ func main() { authorizevalidators := flag.Uint64("authorizevalidators", 0, "Number of validators to preemptively authorize") txTimeout := flag.Duration("txtimeout", 10*time.Minute, "Timeout when waiting for a transaction to be included in a block") prod := flag.Bool("prod", false, "Whether to configure the rollup for production or testing") + isDelayBufferable := flag.Bool("delayBufferable", false, "Whether the sequencer-inbox delay buffer is enabled") flag.Parse() l1ChainId := new(big.Int).SetUint64(*l1ChainIdUint) maxDataSize := new(big.Int).SetUint64(*maxDataSizeUint) @@ -170,6 +172,11 @@ func main() { panic(fmt.Errorf("failed to deserialize chain config: %w", err)) } + var bufferConfig rollupgen.BufferConfig + if *isDelayBufferable { + bufferConfig = arbnode.DefaultBufferConfig() + } + arbSys, _ := precompilesgen.NewArbSys(types.ArbSysAddress, l1client) l1Reader, err := headerreader.New(ctx, l1client, func() *headerreader.Config { return &headerReaderConfig }, arbSys) if err != nil { @@ -186,7 +193,7 @@ func main() { batchPosters, batchPosterManagerAddress, *authorizevalidators, - arbnode.GenerateRollupConfig(*prod, moduleRoot, ownerAddress, &chainConfig, chainConfigJson, loserEscrowAddress), + arbnode.GenerateRollupConfig(*prod, moduleRoot, ownerAddress, &chainConfig, chainConfigJson, loserEscrowAddress, bufferConfig), nativeToken, maxDataSize, true, diff --git a/contracts b/contracts index b140ed63ac..784f06b40e 160000 --- a/contracts +++ b/contracts @@ -1 +1 @@ -Subproject commit b140ed63acdb53cb906ffd1fa3c36fdbd474364e +Subproject commit 784f06b40e3911053f2cb2632291c4bc1c01949b diff --git a/deploy/deploy.go b/deploy/deploy.go index bb4b2e6594..858d06db89 100644 --- a/deploy/deploy.go +++ b/deploy/deploy.go @@ -49,16 +49,27 @@ func deployBridgeCreator(ctx context.Context, parentChainReader *headerreader.He return common.Address{}, fmt.Errorf("blob basefee reader deploy error: %w", err) } } - seqInboxTemplateEthBased, tx, _, err := bridgegen.DeploySequencerInbox(auth, client, maxDataSize, reader4844, false) + seqInboxTemplateEthBased, tx, _, err := bridgegen.DeploySequencerInbox(auth, client, maxDataSize, reader4844, false, false) err = andTxSucceeded(ctx, parentChainReader, tx, err) if err != nil { return common.Address{}, fmt.Errorf("sequencer inbox eth based deploy error: %w", err) } - seqInboxTemplateERC20Based, tx, _, err := bridgegen.DeploySequencerInbox(auth, client, maxDataSize, reader4844, true) + delayBufferableSeqInboxTemplateEthBased, tx, _, err := bridgegen.DeploySequencerInbox(auth, client, maxDataSize, reader4844, false, true) + err = andTxSucceeded(ctx, parentChainReader, tx, err) + if err != nil { + return common.Address{}, fmt.Errorf("delay bufferable sequencer inbox eth based deploy error: %w", err) + } + + seqInboxTemplateERC20Based, tx, _, err := bridgegen.DeploySequencerInbox(auth, client, maxDataSize, reader4844, true, false) err = andTxSucceeded(ctx, parentChainReader, tx, err) if err != nil { return common.Address{}, fmt.Errorf("sequencer inbox erc20 based deploy error: %w", err) } + delayBufferableSeqInboxTemplateERC20Based, tx, _, err := bridgegen.DeploySequencerInbox(auth, client, maxDataSize, reader4844, true, true) + err = andTxSucceeded(ctx, parentChainReader, tx, err) + if err != nil { + return common.Address{}, fmt.Errorf("delay bufferable sequencer inbox erc20 based deploy error: %w", err) + } inboxTemplate, tx, _, err := bridgegen.DeployInbox(auth, client, maxDataSize) err = andTxSucceeded(ctx, parentChainReader, tx, err) @@ -78,12 +89,13 @@ func deployBridgeCreator(ctx context.Context, parentChainReader *headerreader.He return common.Address{}, fmt.Errorf("outbox deploy error: %w", err) } - ethBasedTemplates := rollupgen.BridgeCreatorBridgeContracts{ - Bridge: bridgeTemplate, - SequencerInbox: seqInboxTemplateEthBased, - Inbox: inboxTemplate, - RollupEventInbox: rollupEventBridgeTemplate, - Outbox: outboxTemplate, + ethBasedTemplates := rollupgen.BridgeCreatorBridgeTemplates{ + Bridge: bridgeTemplate, + SequencerInbox: seqInboxTemplateEthBased, + DelayBufferableSequencerInbox: delayBufferableSeqInboxTemplateEthBased, + Inbox: inboxTemplate, + RollupEventInbox: rollupEventBridgeTemplate, + Outbox: outboxTemplate, } /// deploy ERC20 based templates @@ -111,12 +123,13 @@ func deployBridgeCreator(ctx context.Context, parentChainReader *headerreader.He return common.Address{}, fmt.Errorf("outbox deploy error: %w", err) } - erc20BasedTemplates := rollupgen.BridgeCreatorBridgeContracts{ - Bridge: erc20BridgeTemplate, - SequencerInbox: seqInboxTemplateERC20Based, - Inbox: erc20InboxTemplate, - RollupEventInbox: erc20RollupEventBridgeTemplate, - Outbox: erc20OutboxTemplate, + erc20BasedTemplates := rollupgen.BridgeCreatorBridgeTemplates{ + Bridge: erc20BridgeTemplate, + SequencerInbox: seqInboxTemplateERC20Based, + DelayBufferableSequencerInbox: delayBufferableSeqInboxTemplateERC20Based, + Inbox: erc20InboxTemplate, + RollupEventInbox: erc20RollupEventBridgeTemplate, + Outbox: erc20OutboxTemplate, } bridgeCreatorAddr, tx, _, err := rollupgen.DeployBridgeCreator(auth, client, ethBasedTemplates, erc20BasedTemplates) @@ -256,7 +269,6 @@ func DeployOnParentChain(ctx context.Context, parentChainReader *headerreader.He for i := uint64(1); i <= authorizeValidators; i++ { validatorAddrs = append(validatorAddrs, crypto.CreateAddress(validatorWalletCreator, i)) } - deployParams := rollupgen.RollupCreatorRollupDeploymentParams{ Config: config, Validators: validatorAddrs, @@ -272,6 +284,7 @@ func DeployOnParentChain(ctx context.Context, parentChainReader *headerreader.He deployAuth, deployParams, ) + if err != nil { return nil, fmt.Errorf("error submitting create rollup tx: %w", err) } diff --git a/system_tests/batch_poster_test.go b/system_tests/batch_poster_test.go index 0ec03e84c4..2ba3da328e 100644 --- a/system_tests/batch_poster_test.go +++ b/system_tests/batch_poster_test.go @@ -6,6 +6,7 @@ package arbtest import ( "context" "crypto/rand" + "errors" "fmt" "math/big" "strings" @@ -14,6 +15,7 @@ import ( "github.com/andybalholm/brotli" "github.com/ethereum/go-ethereum/accounts/abi" + "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/log" @@ -363,3 +365,113 @@ func TestAllowPostingFirstBatchWhenSequencerMessageCountMismatchEnabled(t *testi func TestAllowPostingFirstBatchWhenSequencerMessageCountMismatchDisabled(t *testing.T) { testAllowPostingFirstBatchWhenSequencerMessageCountMismatch(t, false) } + +func GetBatchCount(t *testing.T, builder *NodeBuilder) uint64 { + t.Helper() + sequenceInbox, err := bridgegen.NewSequencerInbox(builder.L1Info.GetAddress("SequencerInbox"), builder.L1.Client) + Require(t, err) + batchCount, err := sequenceInbox.BatchCount(&bind.CallOpts{Context: builder.ctx}) + Require(t, err) + return batchCount.Uint64() +} + +func CheckBatchCount(t *testing.T, builder *NodeBuilder, want uint64) { + if got := GetBatchCount(t, builder); got != want { + t.Fatalf("invalid batch count, want %v, got %v", want, got) + } +} + +func testBatchPosterDelayBuffer(t *testing.T, delayBufferEnabled bool) { + const messagesPerBatch = 3 + const numBatches = 3 + var threshold uint64 + if delayBufferEnabled { + threshold = 100 + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, true).WithDelayBuffer(threshold) + builder.L2Info.GenerateAccount("User2") + builder.nodeConfig.BatchPoster.MaxDelay = time.Hour // set high max-delay so we can test the delay buffer + cleanup := builder.Build(t) + defer cleanup() + testClientB, cleanupB := builder.Build2ndNode(t, &SecondNodeParams{}) + defer cleanupB() + + initialBatchCount := GetBatchCount(t, builder) + for batch := uint64(0); batch < numBatches; batch++ { + txs := make(types.Transactions, messagesPerBatch) + for i := range txs { + txs[i] = builder.L2Info.PrepareTx("Owner", "User2", builder.L2Info.TransferGas, common.Big1, nil) + } + SendSignedTxesInBatchViaL1(t, ctx, builder.L1Info, builder.L1.Client, builder.L2.Client, txs) + + // Check batch wasn't sent + _, err := WaitForTx(ctx, testClientB.Client, txs[0].Hash(), 100*time.Millisecond) + if err == nil || !errors.Is(err, context.DeadlineExceeded) { + Fatal(t, "expected context-deadline exceeded error, but got:", err) + } + CheckBatchCount(t, builder, initialBatchCount+batch) + + // Advance L1 to force a batch given the delay buffer threshold + AdvanceL1(t, ctx, builder.L1.Client, builder.L1Info, int(threshold)) // #nosec G115 + if !delayBufferEnabled { + // If the delay buffer is disabled, set max delay to zero to force it + CheckBatchCount(t, builder, initialBatchCount+batch) + builder.nodeConfig.BatchPoster.MaxDelay = 0 + } + for _, tx := range txs { + _, err := testClientB.EnsureTxSucceeded(tx) + Require(t, err, "tx not found on second node") + } + CheckBatchCount(t, builder, initialBatchCount+batch+1) + if !delayBufferEnabled { + builder.nodeConfig.BatchPoster.MaxDelay = time.Hour + } + } +} + +func TestBatchPosterDelayBufferEnabled(t *testing.T) { + testBatchPosterDelayBuffer(t, true) +} + +func TestBatchPosterDelayBufferDisabled(t *testing.T) { + testBatchPosterDelayBuffer(t, false) +} + +func TestBatchPosterDelayBufferDontForceNonDelayedMessages(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + const threshold = 100 + builder := NewNodeBuilder(ctx).DefaultConfig(t, true).WithDelayBuffer(threshold) + builder.L2Info.GenerateAccount("User2") + builder.nodeConfig.BatchPoster.MaxDelay = time.Hour // set high max-delay so we can test the delay buffer + cleanup := builder.Build(t) + defer cleanup() + testClientB, cleanupB := builder.Build2ndNode(t, &SecondNodeParams{}) + defer cleanupB() + + // Send non-delayed message and advance L1 + initialBatchCount := GetBatchCount(t, builder) + const numTxs = 3 + txs := make(types.Transactions, numTxs) + for i := range txs { + txs[i] = builder.L2Info.PrepareTx("Owner", "User2", builder.L2Info.TransferGas, common.Big1, nil) + } + builder.L2.SendWaitTestTransactions(t, txs) + AdvanceL1(t, ctx, builder.L1.Client, builder.L1Info, threshold) + + // Even advancing the L1, the batch won't be posted because it doesn't contain a delayed message + CheckBatchCount(t, builder, initialBatchCount) + + // Set delay to zero to force non-delayed messages + builder.nodeConfig.BatchPoster.MaxDelay = 0 + for _, tx := range txs { + _, err := testClientB.EnsureTxSucceeded(tx) + Require(t, err, "tx not found on second node") + } + CheckBatchCount(t, builder, initialBatchCount+1) +} diff --git a/system_tests/common_test.go b/system_tests/common_test.go index 027a41d875..4bbf7c473c 100644 --- a/system_tests/common_test.go +++ b/system_tests/common_test.go @@ -252,6 +252,7 @@ type NodeBuilder struct { l3InitMessage *arbostypes.ParsedInitMessage withProdConfirmPeriodBlocks bool wasmCacheTag uint32 + delayBufferThreshold uint64 // Created nodes L1 *TestClient @@ -364,6 +365,14 @@ func (b *NodeBuilder) WithStylusLongTermCache(enabled bool) *NodeBuilder { return b } +// WithDelayBuffer sets the delay-buffer threshold, which is the number of blocks the batch-poster +// is allowed to delay a batch with a delayed message. +// Setting the threshold to zero disabled the delay buffer (default behaviour). +func (b *NodeBuilder) WithDelayBuffer(threshold uint64) *NodeBuilder { + b.delayBufferThreshold = threshold + return b +} + func (b *NodeBuilder) Build(t *testing.T) func() { b.CheckConfig(t) if b.withL1 { @@ -413,6 +422,7 @@ func (b *NodeBuilder) BuildL1(t *testing.T) { locator.LatestWasmModuleRoot(), b.withProdConfirmPeriodBlocks, true, + b.delayBufferThreshold, ) b.L1.cleanup = func() { requireClose(t, b.L1.Stack) } } @@ -516,6 +526,7 @@ func (b *NodeBuilder) BuildL3OnL2(t *testing.T) func() { locator.LatestWasmModuleRoot(), b.l3Config.withProdConfirmPeriodBlocks, false, + 0, ) b.L3 = buildOnParentChain( @@ -873,6 +884,21 @@ func BridgeBalance( return tx, res } +// AdvanceL1 sends dummy transactions to L1 to create blocks. +func AdvanceL1( + t *testing.T, + ctx context.Context, + l1client *ethclient.Client, + l1info *BlockchainTestInfo, + numBlocks int, +) { + for i := 0; i < numBlocks; i++ { + SendWaitTestTransactions(t, ctx, l1client, []*types.Transaction{ + l1info.PrepareTx("Faucet", "Faucet", 30000, big.NewInt(1e12), nil), + }) + } +} + func SendSignedTxesInBatchViaL1( t *testing.T, ctx context.Context, @@ -892,12 +918,7 @@ func SendSignedTxesInBatchViaL1( _, err = EnsureTxSucceeded(ctx, l1client, l1tx) Require(t, err) - // sending l1 messages creates l1 blocks.. make enough to get that delayed inbox message in - for i := 0; i < 30; i++ { - SendWaitTestTransactions(t, ctx, l1client, []*types.Transaction{ - l1info.PrepareTx("Faucet", "Faucet", 30000, big.NewInt(1e12), nil), - }) - } + AdvanceL1(t, ctx, l1client, l1info, 30) var receipts types.Receipts for _, tx := range delayedTxes { receipt, err := EnsureTxSucceeded(ctx, l2client, tx) @@ -944,12 +965,7 @@ func SendSignedTxViaL1( _, err = EnsureTxSucceeded(ctx, l1client, l1tx) Require(t, err) - // sending l1 messages creates l1 blocks.. make enough to get that delayed inbox message in - for i := 0; i < 30; i++ { - SendWaitTestTransactions(t, ctx, l1client, []*types.Transaction{ - l1info.PrepareTx("Faucet", "Faucet", 30000, big.NewInt(1e12), nil), - }) - } + AdvanceL1(t, ctx, l1client, l1info, 30) receipt, err := EnsureTxSucceeded(ctx, l2client, delayedTx) Require(t, err) return receipt @@ -995,12 +1011,7 @@ func SendUnsignedTxViaL1( _, err = EnsureTxSucceeded(ctx, l1client, l1tx) Require(t, err) - // sending l1 messages creates l1 blocks.. make enough to get that delayed inbox message in - for i := 0; i < 30; i++ { - SendWaitTestTransactions(t, ctx, l1client, []*types.Transaction{ - l1info.PrepareTx("Faucet", "Faucet", 30000, big.NewInt(1e12), nil), - }) - } + AdvanceL1(t, ctx, l1client, l1info, 30) receipt, err := EnsureTxSucceeded(ctx, l2client, unsignedTx) Require(t, err) return receipt @@ -1259,6 +1270,7 @@ func deployOnParentChain( wasmModuleRoot common.Hash, prodConfirmPeriodBlocks bool, chainSupportsBlobs bool, + delayBufferThreshold uint64, ) (*chaininfo.RollupAddresses, *arbostypes.ParsedInitMessage) { parentChainInfo.GenerateAccount("RollupOwner") parentChainInfo.GenerateAccount("Sequencer") @@ -1281,6 +1293,9 @@ func deployOnParentChain( parentChainReader.Start(ctx) defer parentChainReader.StopAndWait() + bufferConfig := arbnode.DefaultBufferConfig() + bufferConfig.Threshold = delayBufferThreshold + nativeToken := common.Address{} maxDataSize := big.NewInt(117964) addresses, err := deploy.DeployOnParentChain( @@ -1290,7 +1305,7 @@ func deployOnParentChain( []common.Address{parentChainInfo.GetAddress("Sequencer")}, parentChainInfo.GetAddress("RollupOwner"), 0, - arbnode.GenerateRollupConfig(prodConfirmPeriodBlocks, wasmModuleRoot, parentChainInfo.GetAddress("RollupOwner"), chainConfig, serializedChainConfig, common.Address{}), + arbnode.GenerateRollupConfig(prodConfirmPeriodBlocks, wasmModuleRoot, parentChainInfo.GetAddress("RollupOwner"), chainConfig, serializedChainConfig, common.Address{}, bufferConfig), nativeToken, maxDataSize, chainSupportsBlobs, diff --git a/system_tests/full_challenge_impl_test.go b/system_tests/full_challenge_impl_test.go index bf30c928d8..af418a2e87 100644 --- a/system_tests/full_challenge_impl_test.go +++ b/system_tests/full_challenge_impl_test.go @@ -214,6 +214,7 @@ func setupSequencerInboxStub(ctx context.Context, t *testing.T, l1Info *Blockcha big.NewInt(117964), reader4844, false, + false, ) Require(t, err) _, err = EnsureTxSucceeded(ctx, l1Client, tx)