diff --git a/arbnode/api.go b/arbnode/api.go index 55b6b1bf20..105fcb9f65 100644 --- a/arbnode/api.go +++ b/arbnode/api.go @@ -63,7 +63,6 @@ type PricingModelHistory struct { GasBacklog []uint64 `json:"gasBacklog"` GasUsed []uint64 `json:"gasUsed"` L1BaseFeeEstimate []*big.Int `json:"l1BaseFeeEstimate"` - L1BaseFeeUpdateTime []uint64 `json:"l1BaseFeeUpdateTime"` MinBaseFee *big.Int `json:"minBaseFee"` SpeedLimit uint64 `json:"speedLimit"` MaxPerBlockGasLimit uint64 `json:"maxPerBlockGasLimit"` @@ -82,26 +81,12 @@ func (api *ArbDebugAPI) PricingModel(ctx context.Context, start, end rpc.BlockNu } history := PricingModelHistory{ - First: uint64(start), - Timestamp: make([]uint64, blocks), - BaseFee: make([]*big.Int, blocks), - GasBacklog: make([]uint64, blocks), - GasUsed: make([]uint64, blocks), - L1BaseFeeEstimate: make([]*big.Int, blocks), - L1BaseFeeUpdateTime: make([]uint64, blocks+1), - } - - genesisBlock := api.blockchain.Config().ArbitrumChainParams.GenesisBlockNum - if start > rpc.BlockNumber(genesisBlock) { - state, _, err := stateAndHeader(api.blockchain, uint64(start)-1) - if err != nil { - return history, err - } - l1BaseFeeUpdateTime, err := state.L1PricingState().LastL1BaseFeeUpdateTime() - if err != nil { - return history, err - } - history.L1BaseFeeUpdateTime[0] = l1BaseFeeUpdateTime + First: uint64(start), + Timestamp: make([]uint64, blocks), + BaseFee: make([]*big.Int, blocks), + GasBacklog: make([]uint64, blocks), + GasUsed: make([]uint64, blocks), + L1BaseFeeEstimate: make([]*big.Int, blocks), } for i := uint64(0); i < uint64(blocks); i++ { @@ -116,21 +101,16 @@ func (api *ArbDebugAPI) PricingModel(ctx context.Context, start, end rpc.BlockNu history.BaseFee[i] = header.BaseFee gasBacklog, _ := l2Pricing.GasBacklog() - l1BaseFeeEstimate, _ := l1Pricing.L1BaseFeeEstimateWei() - l1BaseFeeUpdateTime, err := l1Pricing.LastL1BaseFeeUpdateTime() - if err != nil { - return history, err - } + l1BaseFeeEstimate, _ := l1Pricing.PricePerUnit() history.GasBacklog[i] = gasBacklog history.GasUsed[i] = header.GasUsed history.L1BaseFeeEstimate[i] = l1BaseFeeEstimate - history.L1BaseFeeUpdateTime[i+1] = l1BaseFeeUpdateTime if i == uint64(blocks)-1 { speedLimit, _ := l2Pricing.SpeedLimitPerSecond() maxPerBlockGasLimit, _ := l2Pricing.PerBlockGasLimit() - l1BaseFeeEstimateInertia, err := l1Pricing.L1BaseFeeEstimateInertia() + l1BaseFeeEstimateInertia, err := l1Pricing.Inertia() minBaseFee, _ := l2Pricing.MinBaseFeeWei() pricingInertia, _ := l2Pricing.PricingInertia() backlogTolerance, _ := l2Pricing.BacklogTolerance() diff --git a/arbnode/batch_poster.go b/arbnode/batch_poster.go index 0d0690ad0d..4638b3f6b7 100644 --- a/arbnode/batch_poster.go +++ b/arbnode/batch_poster.go @@ -10,6 +10,7 @@ import ( "math/big" "time" + "github.com/offchainlabs/nitro/arbos" "github.com/offchainlabs/nitro/util/headerreader" "github.com/andybalholm/brotli" @@ -353,30 +354,39 @@ func (b *BatchPoster) maybePostSequencerBatch(ctx context.Context, batchSeqNum u if err != nil { return nil, err } + forcePostBatch := timeSinceNextMessage >= b.config.MaxBatchPostInterval + haveUsefulMessage := false + for b.building.msgCount < msgCount { msg, err := b.streamer.GetMessage(b.building.msgCount) if err != nil { log.Error("error getting message from streamer", "error", err) break } + if msg.Message.Header.Kind != arbos.L1MessageType_BatchPostingReport { + haveUsefulMessage = true + } success, err := b.building.segments.AddMessage(&msg) if err != nil { log.Error("error adding message to batch", "error", err) break } if !success { - forcePostBatch = true // this batch is full + // this batch is full + forcePostBatch = true + haveUsefulMessage = true break } b.building.msgCount++ } + if b.building.segments.IsEmpty() { // we don't need to post a batch for the time being b.pendingMsgTimestamp = time.Now() return nil, nil } - if !forcePostBatch { + if !forcePostBatch || !haveUsefulMessage { // the batch isn't full yet and we've posted a batch recently // don't post anything for now return nil, nil diff --git a/arbnode/node.go b/arbnode/node.go index 97a1b26958..fd5bcc716b 100644 --- a/arbnode/node.go +++ b/arbnode/node.go @@ -682,6 +682,7 @@ func createNodeImpl( if err != nil { return nil, err } + txStreamer.SetInboxReader(inboxReader) nitroMachineConfig := validator.DefaultNitroMachineConfig if config.Wasm.RootPath != "" { diff --git a/arbnode/sequencer.go b/arbnode/sequencer.go index 2efd5932b0..dd42231e7d 100644 --- a/arbnode/sequencer.go +++ b/arbnode/sequencer.go @@ -86,13 +86,6 @@ func (s *Sequencer) PublishTransaction(ctx context.Context, tx *types.Transactio } func (s *Sequencer) preTxFilter(state *arbosState.ArbosState, tx *types.Transaction, sender common.Address) error { - agg, err := state.L1PricingState().ReimbursableAggregatorForSender(sender) - if err != nil { - return err - } - if agg == nil || *agg != l1pricing.SequencerAddress { - return errors.New("transaction sender's preferred aggregator is not the sequencer") - } return nil } @@ -217,7 +210,7 @@ func (s *Sequencer) sequenceTransactions(ctx context.Context) { header := &arbos.L1IncomingMessageHeader{ Kind: arbos.L1MessageType_L2Message, - Poster: l1pricing.SequencerAddress, + Poster: l1pricing.BatchPosterAddress, BlockNumber: l1Block, Timestamp: uint64(timestamp), RequestId: nil, diff --git a/arbnode/transaction_streamer.go b/arbnode/transaction_streamer.go index 64a45171a3..7b22996d4b 100644 --- a/arbnode/transaction_streamer.go +++ b/arbnode/transaction_streamer.go @@ -54,6 +54,7 @@ type TransactionStreamer struct { coordinator *SeqCoordinator broadcastServer *broadcaster.Broadcaster validator *validator.BlockValidator + inboxReader *InboxReader } func NewTransactionStreamer(db ethdb.Database, bc *core.BlockChain, broadcastServer *broadcaster.Broadcaster) (*TransactionStreamer, error) { @@ -96,6 +97,16 @@ func (s *TransactionStreamer) SetSeqCoordinator(coordinator *SeqCoordinator) { s.coordinator = coordinator } +func (s *TransactionStreamer) SetInboxReader(inboxReader *InboxReader) { + if s.Started() { + panic("trying to set inbox reader after start") + } + if s.inboxReader != nil { + panic("trying to set inbox reader when already set") + } + s.inboxReader = inboxReader +} + func (s *TransactionStreamer) cleanupInconsistentState() error { // If it doesn't exist yet, set the message count to 0 hasMessageCount, err := s.db.Has(messageCountKey) @@ -695,6 +706,10 @@ func (s *TransactionStreamer) createBlocks(ctx context.Context) error { } }() + batchFetcher := func(batchNum uint64) ([]byte, error) { + return s.inboxReader.GetSequencerMessageBytes(ctx, batchNum) + } + for pos < msgCount { statedb, err = s.bc.StateAt(lastBlockHeader.Root) @@ -719,14 +734,18 @@ func (s *TransactionStreamer) createBlocks(ctx context.Context) error { return err } - block, receipts := arbos.ProduceBlock( + block, receipts, err := arbos.ProduceBlock( msg.Message, msg.DelayedMessagesRead, lastBlockHeader, statedb, s.bc, s.bc.Config(), + batchFetcher, ) + if err != nil { + return err + } // ProduceBlock advances one message pos++ diff --git a/arbos/addressSet/addressSet.go b/arbos/addressSet/addressSet.go index d47f08fc5e..3903e34944 100644 --- a/arbos/addressSet/addressSet.go +++ b/arbos/addressSet/addressSet.go @@ -44,9 +44,9 @@ func (aset *AddressSet) GetAnyMember() (*common.Address, error) { if err != nil || size == 0 { return nil, err } - addrAsHash, err := aset.backingStorage.GetByUint64(1) - addr := common.BytesToAddress(addrAsHash.Bytes()) - return &addr, err + sba := aset.backingStorage.OpenStorageBackedAddressOrNil(1) + addr, err := sba.Get() + return addr, err } func (aset *AddressSet) Clear() error { @@ -65,18 +65,21 @@ func (aset *AddressSet) Clear() error { return aset.size.Clear() } -func (aset *AddressSet) AllMembers() ([]common.Address, error) { +func (aset *AddressSet) AllMembers(maxNumToReturn uint64) ([]common.Address, error) { size, err := aset.size.Get() if err != nil { return nil, err } + if size > maxNumToReturn { + size = maxNumToReturn + } ret := make([]common.Address, size) for i := range ret { - bytes, err := aset.backingStorage.GetByUint64(uint64(i + 1)) + sba := aset.backingStorage.OpenStorageBackedAddress(uint64(i + 1)) + ret[i], err = sba.Get() if err != nil { return nil, err } - ret[i] = common.BytesToAddress(bytes.Bytes()) } return ret, nil } @@ -90,13 +93,15 @@ func (aset *AddressSet) Add(addr common.Address) error { if err != nil { return err } + sba := aset.backingStorage.OpenStorageBackedAddress(1 + size) slot := util.UintToHash(1 + size) addrAsHash := common.BytesToHash(addr.Bytes()) err = aset.byAddress.Set(addrAsHash, slot) if err != nil { return err } - err = aset.backingStorage.Set(slot, addrAsHash) + sba = aset.backingStorage.OpenStorageBackedAddress(1 + size) + err = sba.Set(addr) if err != nil { return err } diff --git a/arbos/addressSet/addressSet_test.go b/arbos/addressSet/addressSet_test.go index 7a70184a27..8485f85cc6 100644 --- a/arbos/addressSet/addressSet_test.go +++ b/arbos/addressSet/addressSet_test.go @@ -4,6 +4,7 @@ package addressSet import ( + "github.com/ethereum/go-ethereum/common/math" "testing" "github.com/ethereum/go-ethereum/common" @@ -92,7 +93,7 @@ func TestAddressSet(t *testing.T) { } Require(t, aset.Add(addr1)) - all, err := aset.AllMembers() + all, err := aset.AllMembers(math.MaxUint64) Require(t, err) if len(all) != 2 { Fail(t) diff --git a/arbos/arbosState/initialization_test.go b/arbos/arbosState/initialization_test.go index 675c87e972..0d4804b736 100644 --- a/arbos/arbosState/initialization_test.go +++ b/arbos/arbosState/initialization_test.go @@ -142,6 +142,7 @@ func checkRetryables(arbState *ArbosState, expected []statetransfer.Initializati func checkAccounts(db *state.StateDB, arbState *ArbosState, accts []statetransfer.AccountInitializationInfo, t *testing.T) { l1p := arbState.L1PricingState() + posterTable := l1p.BatchPosterTable() for _, acct := range accts { addr := acct.Addr if db.GetNonce(addr) != acct.Nonce { @@ -168,20 +169,17 @@ func checkAccounts(db *state.StateDB, arbState *ArbosState, accts []statetransfe t.Fatal(err) } } - if acct.AggregatorInfo != nil { - fc, err := l1p.AggregatorFeeCollector(addr) + isPoster, err := posterTable.ContainsPoster(addr) + Require(t, err) + if acct.AggregatorInfo != nil && isPoster { + posterInfo, err := posterTable.OpenPoster(addr, false) + Require(t, err) + fc, err := posterInfo.PayTo() Require(t, err) if fc != acct.AggregatorInfo.FeeCollector { t.Fatal() } } - if acct.AggregatorToPay != nil { - aggregator, err := l1p.ReimbursableAggregatorForSender(addr) - Require(t, err) - if aggregator == nil || *aggregator != *acct.AggregatorToPay { - Fail(t) - } - } } _ = l1p } diff --git a/arbos/arbosState/initialize.go b/arbos/arbosState/initialize.go index 425c213e21..be957f2c71 100644 --- a/arbos/arbosState/initialize.go +++ b/arbos/arbosState/initialize.go @@ -154,16 +154,22 @@ func initializeRetryables(rs *retryables.RetryableState, initData statetransfer. func initializeArbosAccount(statedb *state.StateDB, arbosState *ArbosState, account statetransfer.AccountInitializationInfo) error { l1pState := arbosState.L1PricingState() + posterTable := l1pState.BatchPosterTable() if account.AggregatorInfo != nil { - err := l1pState.SetAggregatorFeeCollector(account.Addr, account.AggregatorInfo.FeeCollector) + isPoster, err := posterTable.ContainsPoster(account.Addr) if err != nil { return err } - } - if account.AggregatorToPay != nil { - err := l1pState.SetUserSpecifiedAggregator(account.Addr, account.AggregatorToPay) - if err != nil { - return err + if isPoster { + // poster is already authorized, just set its fee collector + poster, err := posterTable.OpenPoster(account.Addr, false) + if err != nil { + return err + } + err = poster.SetPayTo(account.AggregatorInfo.FeeCollector) + if err != nil { + return err + } } } diff --git a/arbos/block_processor.go b/arbos/block_processor.go index 3949a60963..37ccbc2290 100644 --- a/arbos/block_processor.go +++ b/arbos/block_processor.go @@ -29,6 +29,8 @@ import ( // set by the precompile module, to avoid a package dependence cycle var ArbRetryableTxAddress common.Address var ArbSysAddress common.Address +var InternalTxStartBlockMethodID [4]byte +var InternalTxBatchPostingReportMethodID [4]byte var RedeemScheduledEventID common.Hash var L2ToL1TransactionEventID common.Hash var L2ToL1TxEventID common.Hash @@ -95,6 +97,8 @@ func noopSequencingHooks() *SequencingHooks { } } +type FallibleBatchFetcher func(batchNum uint64) ([]byte, error) + func ProduceBlock( message *L1IncomingMessage, delayedMessagesRead uint64, @@ -102,17 +106,30 @@ func ProduceBlock( statedb *state.StateDB, chainContext core.ChainContext, chainConfig *params.ChainConfig, -) (*types.Block, types.Receipts) { - txes, err := message.ParseL2Transactions(chainConfig.ChainID) + batchFetcher FallibleBatchFetcher, +) (*types.Block, types.Receipts, error) { + var batchFetchErr error + txes, err := message.ParseL2Transactions(chainConfig.ChainID, func(batchNum uint64) []byte { + data, err := batchFetcher(batchNum) + if err != nil { + batchFetchErr = err + return nil + } + return data + }) + if batchFetchErr != nil { + return nil, nil, batchFetchErr + } if err != nil { log.Warn("error parsing incoming message", "err", err) txes = types.Transactions{} } hooks := noopSequencingHooks() - return ProduceBlockAdvanced( + block, receipts := ProduceBlockAdvanced( message.Header, txes, delayedMessagesRead, lastBlockHeader, statedb, chainContext, chainConfig, hooks, ) + return block, receipts, nil } // A bit more flexible than ProduceBlock for use in the sequencer. @@ -186,10 +203,7 @@ func ProduceBlockAdvanced( } else { tx = txes[0] txes = txes[1:] - switch tx := tx.GetInner().(type) { - case *types.ArbitrumInternalTx: - tx.TxIndex = uint64(len(receipts)) - default: + if tx.Type() == types.ArbitrumInternalTxType { hooks = sequencingHooks // the sequencer has the ability to drop this tx isUserTx = true } @@ -210,7 +224,7 @@ func ProduceBlockAdvanced( if gasPrice.Sign() > 0 { dataGas = math.MaxUint64 - state.L1PricingState().AddPosterInfo(tx, sender, poster) + state.L1PricingState().AddPosterInfo(tx, poster) posterCostInL2Gas := arbmath.BigDiv(tx.PosterCost, gasPrice) if posterCostInL2Gas.IsUint64() { diff --git a/arbos/incomingmessage.go b/arbos/incomingmessage.go index 640d983127..c0e42f6fe0 100644 --- a/arbos/incomingmessage.go +++ b/arbos/incomingmessage.go @@ -16,6 +16,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" "github.com/offchainlabs/nitro/arbos/util" ) @@ -29,6 +30,7 @@ const ( L1MessageType_BatchForGasEstimation = 10 // probably won't use this in practice L1MessageType_Initialize = 11 L1MessageType_EthDeposit = 12 + L1MessageType_BatchPostingReport = 13 L1MessageType_Invalid = 0xFF ) @@ -169,7 +171,9 @@ func ParseIncomingL1Message(rd io.Reader) (*L1IncomingMessage, error) { }, nil } -func (msg *L1IncomingMessage) ParseL2Transactions(chainId *big.Int) (types.Transactions, error) { +type InfallibleBatchFetcher func(batchNum uint64) []byte + +func (msg *L1IncomingMessage) ParseL2Transactions(chainId *big.Int, batchFetcher InfallibleBatchFetcher) (types.Transactions, error) { if len(msg.L2msg) > MaxL2MessageSize { // ignore the message if l2msg is too large return nil, errors.New("message too large") @@ -220,6 +224,12 @@ func (msg *L1IncomingMessage) ParseL2Transactions(chainId *big.Int) (types.Trans case L1MessageType_RollupEvent: log.Debug("ignoring rollup event message") return types.Transactions{}, nil + case L1MessageType_BatchPostingReport: + tx, err := parseBatchPostingReportMessage(bytes.NewReader(msg.L2msg), chainId, batchFetcher) + if err != nil { + return nil, err + } + return types.Transactions{tx}, nil case L1MessageType_Invalid: // intentionally invalid message return nil, errors.New("invalid message") @@ -496,3 +506,48 @@ func parseSubmitRetryableMessage(rd io.Reader, header *L1IncomingMessageHeader, } return types.NewTx(tx), err } + +func parseBatchPostingReportMessage(rd io.Reader, chainId *big.Int, batchFetcher InfallibleBatchFetcher) (*types.Transaction, error) { + batchTimestamp, err := util.HashFromReader(rd) + if err != nil { + return nil, err + } + batchPosterAddr, err := util.AddressFromReader(rd) + if err != nil { + return nil, err + } + _, err = util.HashFromReader(rd) // unused: data hash + if err != nil { + return nil, err + } + batchNumHash, err := util.HashFromReader(rd) + if err != nil { + return nil, err + } + batchNum := batchNumHash.Big().Uint64() + + l1BaseFee, err := util.HashFromReader(rd) + if err != nil { + return nil, err + } + batchData := batchFetcher(batchNum) + var batchDataGas uint64 + for _, b := range batchData { + if b == 0 { + batchDataGas += params.TxDataZeroGas + } else { + batchDataGas += params.TxDataNonZeroGasEIP2028 + } + } + data, err := util.PackInternalTxDataBatchPostingReport( + batchTimestamp.Big(), batchPosterAddr, batchNum, batchDataGas, l1BaseFee.Big(), + ) + if err != nil { + return nil, err + } + return types.NewTx(&types.ArbitrumInternalTx{ + ChainId: chainId, + Data: data, + // don't need to fill in the other fields, since they exist only to ensure uniqueness, and batchNum is already unique + }), nil +} diff --git a/arbos/incomingmessage_test.go b/arbos/incomingmessage_test.go index f5ac4b2dd6..4aa3c86c41 100644 --- a/arbos/incomingmessage_test.go +++ b/arbos/incomingmessage_test.go @@ -34,7 +34,7 @@ func TestSerializeAndParseL1Message(t *testing.T) { if err != nil { t.Error(err) } - txes, err := newMsg.ParseL2Transactions(chainId) + txes, err := newMsg.ParseL2Transactions(chainId, nil) if err != nil { t.Error(err) } diff --git a/arbos/internal_tx.go b/arbos/internal_tx.go index 4e840cffc7..11e94e66bf 100644 --- a/arbos/internal_tx.go +++ b/arbos/internal_tx.go @@ -7,6 +7,10 @@ import ( "fmt" "math/big" + "github.com/offchainlabs/nitro/util/arbmath" + + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" @@ -14,12 +18,6 @@ import ( "github.com/offchainlabs/nitro/arbos/util" ) -// Types of ArbitrumInternalTx, distinguished by the first data byte -const ( - // Contains 8 bytes indicating the big endian L1 block number to set - arbInternalTxStartBlock uint8 = 0 -) - func InternalTxStartBlock( chainId, l1BaseFee *big.Int, @@ -34,49 +32,64 @@ func InternalTxStartBlock( if l1BaseFee == nil { l1BaseFee = big.NewInt(0) } - data, err := util.PackInternalTxDataStartBlock(l1BaseFee, l1BlockNum, timePassed) + data, err := util.PackInternalTxDataStartBlock(l1BaseFee, l1BlockNum, l2BlockNum, timePassed) if err != nil { panic(fmt.Sprintf("Failed to pack internal tx %v", err)) } return &types.ArbitrumInternalTx{ - ChainId: chainId, - SubType: arbInternalTxStartBlock, - Data: data, - L2BlockNumber: l2BlockNum, + ChainId: chainId, + Data: data, } } func ApplyInternalTxUpdate(tx *types.ArbitrumInternalTx, state *arbosState.ArbosState, evm *vm.EVM) { - inputs, err := util.UnpackInternalTxDataStartBlock(tx.Data) - if err != nil { - panic(err) - } - l1BaseFee, _ := inputs[0].(*big.Int) // current block's - l1BlockNumber, _ := inputs[1].(uint64) // current block's - timePassed, _ := inputs[2].(uint64) // since last block + switch *(*[4]byte)(tx.Data[:4]) { + case InternalTxStartBlockMethodID: + inputs, err := util.UnpackInternalTxDataStartBlock(tx.Data) + if err != nil { + panic(err) + } + l1BlockNumber, _ := inputs[1].(uint64) // current block's + timePassed, _ := inputs[2].(uint64) // since last block - nextL1BlockNumber, err := state.Blockhashes().NextBlockNumber() - state.Restrict(err) + nextL1BlockNumber, err := state.Blockhashes().NextBlockNumber() + state.Restrict(err) - l2BaseFee, err := state.L2PricingState().BaseFeeWei() - state.Restrict(err) + l2BaseFee, err := state.L2PricingState().BaseFeeWei() + state.Restrict(err) - if l1BlockNumber >= nextL1BlockNumber { - var prevHash common.Hash - if evm.Context.BlockNumber.Sign() > 0 { - prevHash = evm.Context.GetHash(evm.Context.BlockNumber.Uint64() - 1) + if l1BlockNumber >= nextL1BlockNumber { + var prevHash common.Hash + if evm.Context.BlockNumber.Sign() > 0 { + prevHash = evm.Context.GetHash(evm.Context.BlockNumber.Uint64() - 1) + } + state.Restrict(state.Blockhashes().RecordNewL1Block(l1BlockNumber, prevHash)) } - state.Restrict(state.Blockhashes().RecordNewL1Block(l1BlockNumber, prevHash)) - } - currentTime := evm.Context.Time.Uint64() + currentTime := evm.Context.Time.Uint64() + + // Try to reap 2 retryables + _ = state.RetryableState().TryToReapOneRetryable(currentTime, evm, util.TracingDuringEVM) + _ = state.RetryableState().TryToReapOneRetryable(currentTime, evm, util.TracingDuringEVM) - // Try to reap 2 retryables - _ = state.RetryableState().TryToReapOneRetryable(currentTime, evm, util.TracingDuringEVM) - _ = state.RetryableState().TryToReapOneRetryable(currentTime, evm, util.TracingDuringEVM) + state.L2PricingState().UpdatePricingModel(l2BaseFee, timePassed, false) - state.L2PricingState().UpdatePricingModel(l2BaseFee, timePassed, false) - state.L1PricingState().UpdatePricingModel(l1BaseFee, currentTime) + state.UpgradeArbosVersionIfNecessary(currentTime, evm.ChainConfig()) + case InternalTxBatchPostingReportMethodID: + inputs, err := util.UnpackInternalTxDataBatchPostingReport(tx.Data) + if err != nil { + panic(err) + } + batchTimestamp, _ := inputs[0].(*big.Int) + batchPosterAddress, _ := inputs[1].(common.Address) + // ignore input[2], batchNumber, which exist because we might need them in the future + batchDataGas, _ := inputs[3].(uint64) + l1BaseFeeWei, _ := inputs[4].(*big.Int) - state.UpgradeArbosVersionIfNecessary(currentTime, evm.ChainConfig()) + weiSpent := arbmath.BigMulByUint(l1BaseFeeWei, batchDataGas) + err = state.L1PricingState().UpdateForBatchPosterSpending(evm.StateDB, evm, batchTimestamp.Uint64(), evm.Context.Time.Uint64(), batchPosterAddress, weiSpent) + if err != nil { + log.Warn("L1Pricing UpdateForSequencerSpending failed", "err", err) + } + } } diff --git a/arbos/l1pricing/batchPoster.go b/arbos/l1pricing/batchPoster.go new file mode 100644 index 0000000000..6d332706ea --- /dev/null +++ b/arbos/l1pricing/batchPoster.go @@ -0,0 +1,171 @@ +// Copyright 2022, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE + +package l1pricing + +import ( + "errors" + "math" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/offchainlabs/nitro/arbos/addressSet" + "github.com/offchainlabs/nitro/arbos/storage" + "github.com/offchainlabs/nitro/util/arbmath" +) + +const totalFundsDueOffset = 0 + +var ( + PosterAddrsKey = []byte{0} + PosterInfoKey = []byte{1} + + ErrAlreadyExists = errors.New("tried to add a batch poster that already exists") + ErrNotExist = errors.New("tried to open a batch poster that does not exist") +) + +// layout of storage in the table +type BatchPostersTable struct { + posterAddrs *addressSet.AddressSet + posterInfo *storage.Storage + totalFundsDue storage.StorageBackedBigInt +} + +type BatchPosterState struct { + fundsDue storage.StorageBackedBigInt + payTo storage.StorageBackedAddress + postersTable *BatchPostersTable +} + +func InitializeBatchPostersTable(storage *storage.Storage) error { + totalFundsDue := storage.OpenStorageBackedBigInt(totalFundsDueOffset) + if err := totalFundsDue.Set(common.Big0); err != nil { + return err + } + return addressSet.Initialize(storage.OpenSubStorage(PosterAddrsKey)) +} + +func OpenBatchPostersTable(storage *storage.Storage) *BatchPostersTable { + return &BatchPostersTable{ + posterAddrs: addressSet.OpenAddressSet(storage.OpenSubStorage(PosterAddrsKey)), + posterInfo: storage.OpenSubStorage(PosterInfoKey), + totalFundsDue: storage.OpenStorageBackedBigInt(totalFundsDueOffset), + } +} + +func (bpt *BatchPostersTable) OpenPoster(poster common.Address, createIfNotExist bool) (*BatchPosterState, error) { + isBatchPoster, err := bpt.posterAddrs.IsMember(poster) + if err != nil { + return nil, err + } + if !isBatchPoster { + if !createIfNotExist { + return nil, ErrNotExist + } + return bpt.AddPoster(poster, poster) + } + return bpt.internalOpen(poster), nil +} + +func (bpt *BatchPostersTable) internalOpen(poster common.Address) *BatchPosterState { + bpStorage := bpt.posterInfo.OpenSubStorage(poster.Bytes()) + return &BatchPosterState{ + fundsDue: bpStorage.OpenStorageBackedBigInt(0), + payTo: bpStorage.OpenStorageBackedAddress(1), + postersTable: bpt, + } +} + +func (bpt *BatchPostersTable) ContainsPoster(poster common.Address) (bool, error) { + return bpt.posterAddrs.IsMember(poster) +} + +func (bpt *BatchPostersTable) AddPoster(posterAddress common.Address, payTo common.Address) (*BatchPosterState, error) { + isBatchPoster, err := bpt.posterAddrs.IsMember(posterAddress) + if err != nil { + return nil, err + } + if isBatchPoster { + return nil, ErrAlreadyExists + } + bpState := bpt.internalOpen(posterAddress) + if err := bpState.fundsDue.Set(common.Big0); err != nil { + return nil, err + } + if err := bpState.payTo.Set(payTo); err != nil { + return nil, err + } + + if err := bpt.posterAddrs.Add(posterAddress); err != nil { + return nil, err + } + + return bpState, nil +} + +func (bpt *BatchPostersTable) AllPosters(maxNumToGet uint64) ([]common.Address, error) { + return bpt.posterAddrs.AllMembers(maxNumToGet) +} + +func (bpt *BatchPostersTable) TotalFundsDue() (*big.Int, error) { + return bpt.totalFundsDue.Get() +} + +func (bps *BatchPosterState) FundsDue() (*big.Int, error) { + return bps.fundsDue.Get() +} + +func (bps *BatchPosterState) SetFundsDue(val *big.Int) error { + fundsDue := bps.fundsDue + totalFundsDue := bps.postersTable.totalFundsDue + prev, err := fundsDue.Get() + if err != nil { + return err + } + prevTotal, err := totalFundsDue.Get() + if err != nil { + return err + } + if err := totalFundsDue.Set(arbmath.BigSub(arbmath.BigAdd(prevTotal, val), prev)); err != nil { + return err + } + return bps.fundsDue.Set(val) +} + +func (bps *BatchPosterState) PayTo() (common.Address, error) { + return bps.payTo.Get() +} + +func (bps *BatchPosterState) SetPayTo(addr common.Address) error { + return bps.payTo.Set(addr) +} + +type FundsDueItem struct { + dueTo common.Address + balance *big.Int +} + +func (bpt *BatchPostersTable) GetFundsDueList() ([]FundsDueItem, error) { + ret := []FundsDueItem{} + allPosters, err := bpt.AllPosters(math.MaxUint64) + if err != nil { + return nil, err + } + for _, posterAddr := range allPosters { + poster, err := bpt.OpenPoster(posterAddr, false) + if err != nil { + return nil, err + } + due, err := poster.FundsDue() + if err != nil { + return nil, err + } + if due.Sign() > 0 { + ret = append(ret, FundsDueItem{ + dueTo: posterAddr, + balance: due, + }) + } + } + return ret, nil +} diff --git a/arbos/l1pricing/batchPoster_test.go b/arbos/l1pricing/batchPoster_test.go new file mode 100644 index 0000000000..5b47534b23 --- /dev/null +++ b/arbos/l1pricing/batchPoster_test.go @@ -0,0 +1,114 @@ +// Copyright 2022, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE + +package l1pricing + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" + "github.com/offchainlabs/nitro/arbos/burn" + "github.com/offchainlabs/nitro/arbos/storage" + "math/big" + "testing" +) + +func TestBatchPosterTable(t *testing.T) { + sto := storage.NewMemoryBacked(burn.NewSystemBurner(nil, false)) + err := InitializeBatchPostersTable(sto) + Require(t, err) + + bpTable := OpenBatchPostersTable(sto) + + addr1 := common.Address{1, 2, 3} + pay1 := common.Address{4, 5, 6, 7} + addr2 := common.Address{2, 4, 6} + pay2 := common.Address{8, 10, 12, 14} + + // test creation and counting of bps + allPosters, err := bpTable.AllPosters(math.MaxUint64) + Require(t, err) + if len(allPosters) != 0 { + t.Fatal() + } + exists, err := bpTable.ContainsPoster(addr1) + Require(t, err) + if exists { + t.Fatal() + } + + bp1, err := bpTable.AddPoster(addr1, pay1) + Require(t, err) + getPay1, err := bp1.PayTo() + Require(t, err) + if getPay1 != pay1 { + t.Fatal() + } + getDue1, err := bp1.FundsDue() + Require(t, err) + if getDue1.Sign() != 0 { + t.Fatal() + } + exists, err = bpTable.ContainsPoster(addr1) + Require(t, err) + if !exists { + t.Fatal() + } + + bp2, err := bpTable.AddPoster(addr2, pay2) + Require(t, err) + _ = bp2 + getPay2, err := bp2.PayTo() + Require(t, err) + if getPay2 != pay2 { + t.Fatal() + } + getDue2, err := bp2.FundsDue() + Require(t, err) + if getDue2.Sign() != 0 { + t.Fatal() + } + exists, err = bpTable.ContainsPoster(addr2) + Require(t, err) + if !exists { + t.Fatal() + } + + allPosters, err = bpTable.AllPosters(math.MaxUint64) + Require(t, err) + if len(allPosters) != 2 { + t.Fatal() + } + + // test get/set of BP fields + bp1, err = bpTable.OpenPoster(addr1, false) + Require(t, err) + err = bp1.SetPayTo(addr2) + Require(t, err) + getPay1, err = bp1.PayTo() + Require(t, err) + if getPay1 != addr2 { + t.Fatal() + } + err = bp1.SetFundsDue(big.NewInt(13)) + Require(t, err) + getDue1, err = bp1.FundsDue() + Require(t, err) + if getDue1.Uint64() != 13 { + t.Fatal() + } + + // test adding up the fundsDue + err = bp2.SetFundsDue(big.NewInt(42)) + Require(t, err) + getDue2, err = bp2.FundsDue() + Require(t, err) + if getDue2.Uint64() != 42 { + t.Fatal() + } + + totalDue, err := bpTable.TotalFundsDue() + Require(t, err) + if totalDue.Uint64() != 13+42 { + t.Fatal() + } +} diff --git a/arbos/l1pricing/l1pricing.go b/arbos/l1pricing/l1pricing.go index 93932b26b7..b413266071 100644 --- a/arbos/l1pricing/l1pricing.go +++ b/arbos/l1pricing/l1pricing.go @@ -5,241 +5,368 @@ package l1pricing import ( "errors" + "github.com/ethereum/go-ethereum/common/math" "math/big" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbcompress" - "github.com/offchainlabs/nitro/arbos/addressSet" - "github.com/offchainlabs/nitro/util/arbmath" + am "github.com/offchainlabs/nitro/util/arbmath" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/params" "github.com/offchainlabs/nitro/arbos/storage" "github.com/offchainlabs/nitro/arbos/util" ) type L1PricingState struct { - storage *storage.Storage - defaultAggregator storage.StorageBackedAddress - l1BaseFeeEstimate storage.StorageBackedBigInt - l1BaseFeeEstimateInertia storage.StorageBackedUint64 - lastL1BaseFeeUpdateTime storage.StorageBackedUint64 - userSpecifiedAggregators *storage.Storage - refuseDefaultAggregator *storage.Storage - aggregatorFeeCollectors *storage.Storage - aggregatorCompressionRatios *storage.Storage + storage *storage.Storage + + // parameters + batchPosterTable *BatchPostersTable + payRewardsTo storage.StorageBackedAddress + equilibrationUnits storage.StorageBackedBigInt + inertia storage.StorageBackedUint64 + perUnitReward storage.StorageBackedUint64 + // variables + lastUpdateTime storage.StorageBackedUint64 // timestamp of the last update from L1 that we processed + fundsDueForRewards storage.StorageBackedBigInt + // funds collected since update are recorded as the balance in account L1PricerFundsPoolAddress + unitsSinceUpdate storage.StorageBackedUint64 // calldata units collected for since last update + pricePerUnit storage.StorageBackedBigInt // current price per calldata unit } var ( - SequencerAddress = common.HexToAddress("0xA4B000000000000000000073657175656e636572") + BatchPosterTableKey = []byte{0} + BatchPosterAddress = common.HexToAddress("0xA4B000000000000000000073657175656e636572") + BatchPosterPayToAddress = BatchPosterAddress + L1PricerFundsPoolAddress = common.HexToAddress("0xA4B00000000000000000000000000000000000f6") - userSpecifiedAggregatorKey = []byte{0} - refuseDefaultAggregatorKey = []byte{1} - aggregatorFeeCollectorKey = []byte{2} - aggregatorCompressionRatioKey = []byte{3} + ErrInvalidTime = errors.New("invalid timestamp") ) const ( - defaultAggregatorAddressOffset uint64 = 0 - l1BaseFeeEstimateOffset uint64 = 1 - l1BaseFeeEstimateInertiaOffset uint64 = 2 - lastL1BaseFeeUpdateTimeOffset uint64 = 3 + payRewardsToOffset uint64 = iota + equilibrationUnitsOffset + inertiaOffset + perUnitRewardOffset + lastUpdateTimeOffset + fundsDueForRewardsOffset + unitsSinceOffset + pricePerUnitOffset ) -const InitialL1BaseFeeEstimate = 50 * params.GWei -const InitialL1BaseFeeEstimateInertia = 24 +const ( + InitialEquilibrationUnits uint64 = 60 * params.TxDataNonZeroGasEIP2028 * 100000 // one minute at 100000 bytes / sec + InitialInertia = 10 + InitialPerUnitReward = 10 + InitialPricePerUnitWei = 50 * params.GWei +) func InitializeL1PricingState(sto *storage.Storage) error { - err := sto.SetByUint64(defaultAggregatorAddressOffset, common.BytesToHash(SequencerAddress.Bytes())) - if err != nil { + bptStorage := sto.OpenSubStorage(BatchPosterTableKey) + if err := InitializeBatchPostersTable(bptStorage); err != nil { + return err + } + bpTable := OpenBatchPostersTable(bptStorage) + if _, err := bpTable.AddPoster(BatchPosterAddress, BatchPosterPayToAddress); err != nil { + return err + } + if err := sto.SetByUint64(payRewardsToOffset, util.AddressToHash(BatchPosterAddress)); err != nil { + return err + } + equilibrationUnits := sto.OpenStorageBackedBigInt(equilibrationUnitsOffset) + if err := equilibrationUnits.Set(am.UintToBig(InitialEquilibrationUnits)); err != nil { return err } - if err := sto.SetUint64ByUint64(l1BaseFeeEstimateInertiaOffset, InitialL1BaseFeeEstimateInertia); err != nil { + if err := sto.SetUint64ByUint64(inertiaOffset, InitialInertia); err != nil { return err } - if err := sto.SetUint64ByUint64(l1BaseFeeEstimateOffset, InitialL1BaseFeeEstimate); err != nil { + fundsDueForRewards := sto.OpenStorageBackedBigInt(fundsDueForRewardsOffset) + if err := fundsDueForRewards.Set(common.Big0); err != nil { return err } - return sto.SetUint64ByUint64(lastL1BaseFeeUpdateTimeOffset, 0) + if err := sto.SetUint64ByUint64(perUnitRewardOffset, InitialPerUnitReward); err != nil { + return err + } + pricePerUnit := sto.OpenStorageBackedBigInt(pricePerUnitOffset) + return pricePerUnit.SetByUint(InitialPricePerUnitWei) } func OpenL1PricingState(sto *storage.Storage) *L1PricingState { return &L1PricingState{ sto, - sto.OpenStorageBackedAddress(defaultAggregatorAddressOffset), - sto.OpenStorageBackedBigInt(l1BaseFeeEstimateOffset), - sto.OpenStorageBackedUint64(l1BaseFeeEstimateInertiaOffset), - sto.OpenStorageBackedUint64(lastL1BaseFeeUpdateTimeOffset), - sto.OpenSubStorage(userSpecifiedAggregatorKey), - sto.OpenSubStorage(refuseDefaultAggregatorKey), - sto.OpenSubStorage(aggregatorFeeCollectorKey), - sto.OpenSubStorage(aggregatorCompressionRatioKey), + OpenBatchPostersTable(sto.OpenSubStorage(BatchPosterTableKey)), + sto.OpenStorageBackedAddress(payRewardsToOffset), + sto.OpenStorageBackedBigInt(equilibrationUnitsOffset), + sto.OpenStorageBackedUint64(inertiaOffset), + sto.OpenStorageBackedUint64(perUnitRewardOffset), + sto.OpenStorageBackedUint64(lastUpdateTimeOffset), + sto.OpenStorageBackedBigInt(fundsDueForRewardsOffset), + sto.OpenStorageBackedUint64(unitsSinceOffset), + sto.OpenStorageBackedBigInt(pricePerUnitOffset), } } -func (ps *L1PricingState) DefaultAggregator() (common.Address, error) { - return ps.defaultAggregator.Get() +func (ps *L1PricingState) BatchPosterTable() *BatchPostersTable { + return ps.batchPosterTable } -func (ps *L1PricingState) SetDefaultAggregator(val common.Address) error { - return ps.defaultAggregator.Set(val) +func (ps *L1PricingState) PayRewardsTo() (common.Address, error) { + return ps.payRewardsTo.Get() } -func (ps *L1PricingState) L1BaseFeeEstimateWei() (*big.Int, error) { - return ps.l1BaseFeeEstimate.Get() +func (ps *L1PricingState) SetPayRewardsTo(addr common.Address) error { + return ps.payRewardsTo.Set(addr) } -func (ps *L1PricingState) SetL1BaseFeeEstimateWei(val *big.Int) error { - return ps.l1BaseFeeEstimate.Set(val) +func (ps *L1PricingState) EquilibrationUnits() (*big.Int, error) { + return ps.equilibrationUnits.Get() } -func (ps *L1PricingState) LastL1BaseFeeUpdateTime() (uint64, error) { - return ps.lastL1BaseFeeUpdateTime.Get() +func (ps *L1PricingState) SetEquilibrationUnits(equilUnits *big.Int) error { + return ps.equilibrationUnits.Set(equilUnits) } -func (ps *L1PricingState) SetLastL1BaseFeeUpdateTime(t uint64) error { - return ps.lastL1BaseFeeUpdateTime.Set(t) +func (ps *L1PricingState) Inertia() (uint64, error) { + return ps.inertia.Get() } -// Update the pricing model with info from the start of a block -func (ps *L1PricingState) UpdatePricingModel(baseFeeSample *big.Int, currentTime uint64) { +func (ps *L1PricingState) SetInertia(inertia uint64) error { + return ps.inertia.Set(inertia) +} - if baseFeeSample.Sign() == 0 { - // The sequencer's normal messages do not include the l1 basefee, so ignore them - return - } +func (ps *L1PricingState) PerUnitReward() (uint64, error) { + return ps.perUnitReward.Get() +} - // update the l1 basefee estimate, which is the weighted average of the past and present - // basefee' = weighted average of the historical rate and the current, discounting time passed - // basefee' = (memory * basefee + sqrt(passed) * sample) / (memory + sqrt(passed)) - // - baseFee, _ := ps.L1BaseFeeEstimateWei() - inertia, _ := ps.L1BaseFeeEstimateInertia() - lastTime, _ := ps.LastL1BaseFeeUpdateTime() - if currentTime <= lastTime { - return - } - passedSqrt := arbmath.ApproxSquareRoot(currentTime - lastTime) - newBaseFee := arbmath.BigDivByUint( - arbmath.BigAdd(arbmath.BigMulByUint(baseFee, inertia), arbmath.BigMulByUint(baseFeeSample, passedSqrt)), - inertia+passedSqrt, - ) +func (ps *L1PricingState) SetPerUnitReward(weiPerUnit uint64) error { + return ps.perUnitReward.Set(weiPerUnit) +} + +func (ps *L1PricingState) LastUpdateTime() (uint64, error) { + return ps.lastUpdateTime.Get() +} - _ = ps.SetL1BaseFeeEstimateWei(newBaseFee) - _ = ps.SetLastL1BaseFeeUpdateTime(currentTime) +func (ps *L1PricingState) SetLastUpdateTime(t uint64) error { + return ps.lastUpdateTime.Set(t) } -// Get how slowly ArbOS updates its estimate of the L1 basefee -func (ps *L1PricingState) L1BaseFeeEstimateInertia() (uint64, error) { - return ps.l1BaseFeeEstimateInertia.Get() +func (ps *L1PricingState) FundsDueForRewards() (*big.Int, error) { + return ps.fundsDueForRewards.Get() } -// Set how slowly ArbOS updates its estimate of the L1 basefee -func (ps *L1PricingState) SetL1BaseFeeEstimateInertia(inertia uint64) error { - return ps.l1BaseFeeEstimateInertia.Set(inertia) +func (ps *L1PricingState) SetFundsDueForRewards(amt *big.Int) error { + return ps.fundsDueForRewards.Set(amt) } -func (ps *L1PricingState) userSpecifiedAggregatorsForAddress(sender common.Address) *addressSet.AddressSet { - return addressSet.OpenAddressSet(ps.userSpecifiedAggregators.OpenSubStorage(sender.Bytes())) +func (ps *L1PricingState) UnitsSinceUpdate() (uint64, error) { + return ps.unitsSinceUpdate.Get() } -// Get sender's user-specified aggregator, or nil if there is none. This does NOT fall back to the default aggregator -// if there is no user-specified aggregator. If that is what you want, call ReimbursableAggregatorForSender instead. -func (ps *L1PricingState) UserSpecifiedAggregator(sender common.Address) (*common.Address, error) { - return ps.userSpecifiedAggregatorsForAddress(sender).GetAnyMember() +func (ps *L1PricingState) SetUnitsSinceUpdate(units uint64) error { + return ps.unitsSinceUpdate.Set(units) } -func (ps *L1PricingState) SetUserSpecifiedAggregator(sender common.Address, maybeAggregator *common.Address) error { - paSet := ps.userSpecifiedAggregatorsForAddress(sender) - if err := paSet.Clear(); err != nil { +func (ps *L1PricingState) AddToUnitsSinceUpdate(units uint64) error { + oldUnits, err := ps.unitsSinceUpdate.Get() + if err != nil { return err } - if maybeAggregator == nil { - return nil - } - return paSet.Add(*maybeAggregator) + return ps.unitsSinceUpdate.Set(oldUnits + units) +} + +func (ps *L1PricingState) PricePerUnit() (*big.Int, error) { + return ps.pricePerUnit.Get() +} + +func (ps *L1PricingState) SetPricePerUnit(price *big.Int) error { + return ps.pricePerUnit.Set(price) } -func (ps *L1PricingState) RefusesDefaultAggregator(addr common.Address) (bool, error) { - val, err := ps.refuseDefaultAggregator.Get(common.BytesToHash(addr.Bytes())) +// Update the pricing model based on a payment by a batch poster +func (ps *L1PricingState) UpdateForBatchPosterSpending(statedb vm.StateDB, evm *vm.EVM, updateTime uint64, currentTime uint64, batchPoster common.Address, weiSpent *big.Int) error { + batchPosterTable := ps.BatchPosterTable() + posterState, err := batchPosterTable.OpenPoster(batchPoster, true) if err != nil { - return false, err + return err } - return val != (common.Hash{}), nil -} -func (ps *L1PricingState) SetRefusesDefaultAggregator(addr common.Address, refuses bool) error { - val := uint64(0) - if refuses { - val = 1 + // compute previous shortfall + totalFundsDue, err := batchPosterTable.TotalFundsDue() + if err != nil { + return err } - return ps.refuseDefaultAggregator.Set(common.BytesToHash(addr.Bytes()), common.BigToHash(arbmath.UintToBig(val))) -} + fundsDueForRewards, err := ps.FundsDueForRewards() + if err != nil { + return err + } + oldSurplus := am.BigSub(statedb.GetBalance(L1PricerFundsPoolAddress), am.BigAdd(totalFundsDue, fundsDueForRewards)) -// Get the aggregator who is eligible to be reimbursed for L1 costs of txs from sender, or nil if there is none. -func (ps *L1PricingState) ReimbursableAggregatorForSender(sender common.Address) (*common.Address, error) { - fromTable, err := ps.UserSpecifiedAggregator(sender) + // compute allocation fraction -- will allocate updateTimeDelta/timeDelta fraction of units and funds to this update + lastUpdateTime, err := ps.LastUpdateTime() if err != nil { - return nil, err + return err + } + if lastUpdateTime == 0 && currentTime > 0 { // it's the first update, so there isn't a last update time + lastUpdateTime = updateTime - 1 } - if fromTable != nil { - return fromTable, nil + if updateTime >= currentTime || updateTime < lastUpdateTime { + return ErrInvalidTime } + updateTimeDelta := updateTime - lastUpdateTime + timeDelta := currentTime - lastUpdateTime - refuses, err := ps.RefusesDefaultAggregator(sender) - if err != nil || refuses { - return nil, err + // allocate units to this update + unitsSinceUpdate, err := ps.UnitsSinceUpdate() + if err != nil { + return err + } + unitsAllocated := unitsSinceUpdate * updateTimeDelta / timeDelta + unitsSinceUpdate -= unitsAllocated + if err := ps.SetUnitsSinceUpdate(unitsSinceUpdate); err != nil { + return err } - aggregator, err := ps.DefaultAggregator() + + dueToPoster, err := posterState.FundsDue() if err != nil { - return nil, err + return err } - if aggregator == (common.Address{}) { - return nil, nil + err = posterState.SetFundsDue(am.BigAdd(dueToPoster, weiSpent)) + if err != nil { + return err + } + perUnitReward, err := ps.PerUnitReward() + if err != nil { + return err + } + fundsDueForRewards = am.BigAdd(fundsDueForRewards, am.BigMulByUint(am.UintToBig(unitsAllocated), perUnitReward)) + if err := ps.SetFundsDueForRewards(fundsDueForRewards); err != nil { + return err } - return &aggregator, nil -} + // allocate funds to this update + collectedSinceUpdate := statedb.GetBalance(L1PricerFundsPoolAddress) + availableFunds := am.BigDivByUint(am.BigMulByUint(collectedSinceUpdate, updateTimeDelta), timeDelta) -func (ps *L1PricingState) SetAggregatorFeeCollector(aggregator common.Address, addr common.Address) error { - return ps.aggregatorFeeCollectors.Set(common.BytesToHash(aggregator.Bytes()), common.BytesToHash(addr.Bytes())) -} + // pay rewards, as much as possible + paymentForRewards := am.BigMulByUint(am.UintToBig(perUnitReward), unitsAllocated) + if am.BigLessThan(availableFunds, paymentForRewards) { + paymentForRewards = availableFunds + } + fundsDueForRewards = am.BigSub(fundsDueForRewards, paymentForRewards) + if err := ps.SetFundsDueForRewards(fundsDueForRewards); err != nil { + return err + } + payRewardsTo, err := ps.PayRewardsTo() + if err != nil { + return err + } + err = util.TransferBalance(&L1PricerFundsPoolAddress, &payRewardsTo, paymentForRewards, evm, util.TracingBeforeEVM, "batchPosterReward") + if err != nil { + return err + } + availableFunds = am.BigSub(availableFunds, paymentForRewards) -func (ps *L1PricingState) AggregatorFeeCollector(aggregator common.Address) (common.Address, error) { - raw, err := ps.aggregatorFeeCollectors.Get(common.BytesToHash(aggregator.Bytes())) - if raw == (common.Hash{}) { - return aggregator, err - } else { - return common.BytesToAddress(raw.Bytes()), err + // settle up our batch poster payments owed, as much as possible + allPosterAddrs, err := batchPosterTable.AllPosters(math.MaxUint64) + if err != nil { + return err + } + for _, posterAddr := range allPosterAddrs { + poster, err := batchPosterTable.OpenPoster(posterAddr, false) + if err != nil { + return err + } + balanceDueToPoster, err := poster.FundsDue() + if err != nil { + return err + } + balanceToTransfer := balanceDueToPoster + if am.BigLessThan(availableFunds, balanceToTransfer) { + balanceToTransfer = availableFunds + } + if balanceToTransfer.Sign() > 0 { + addrToPay, err := poster.PayTo() + if err != nil { + return err + } + err = util.TransferBalance(&L1PricerFundsPoolAddress, &addrToPay, balanceToTransfer, evm, util.TracingBeforeEVM, "batchPosterRefund") + if err != nil { + return err + } + availableFunds = am.BigSub(availableFunds, balanceToTransfer) + balanceDueToPoster = am.BigSub(balanceDueToPoster, balanceToTransfer) + err = poster.SetFundsDue(balanceDueToPoster) + if err != nil { + return err + } + } } -} -func (ps *L1PricingState) AggregatorCompressionRatio(aggregator common.Address) (arbmath.Bips, error) { - raw, err := ps.aggregatorCompressionRatios.Get(common.BytesToHash(aggregator.Bytes())) - if raw == (common.Hash{}) { - return arbmath.OneInBips, err - } else { - return arbmath.BigToBips(raw.Big()), err + // update time + if err := ps.SetLastUpdateTime(updateTime); err != nil { + return err } -} -func (ps *L1PricingState) SetAggregatorCompressionRatio(aggregator common.Address, ratio arbmath.Bips) error { - if ratio > arbmath.PercentToBips(200) { - return errors.New("compression ratio out of bounds") + // adjust the price + if unitsAllocated > 0 { + totalFundsDue, err = batchPosterTable.TotalFundsDue() + if err != nil { + return err + } + fundsDueForRewards, err = ps.FundsDueForRewards() + if err != nil { + return err + } + surplus := am.BigSub(statedb.GetBalance(L1PricerFundsPoolAddress), am.BigAdd(totalFundsDue, fundsDueForRewards)) + + inertia, err := ps.Inertia() + if err != nil { + return err + } + equilUnits, err := ps.EquilibrationUnits() + if err != nil { + return err + } + inertiaUnits := am.BigDivByUint(equilUnits, inertia) + price, err := ps.PricePerUnit() + if err != nil { + return err + } + + allocPlusInert := am.BigAddByUint(inertiaUnits, unitsAllocated) + priceChange := am.BigDiv( + am.BigSub( + am.BigMul(surplus, am.BigSub(equilUnits, common.Big1)), + am.BigMul(oldSurplus, equilUnits), + ), + am.BigMul(equilUnits, allocPlusInert), + ) + + newPrice := am.BigAdd(price, priceChange) + if newPrice.Sign() < 0 { + newPrice = common.Big0 + } + if err := ps.SetPricePerUnit(newPrice); err != nil { + return err + } } - return ps.aggregatorCompressionRatios.Set(util.AddressToHash(aggregator), util.UintToHash(uint64(ratio))) + return nil } -func (ps *L1PricingState) AddPosterInfo(tx *types.Transaction, sender, poster common.Address) { - - tx.PosterCost = big.NewInt(0) +func (ps *L1PricingState) AddPosterInfo(tx *types.Transaction, posterAddr common.Address) { + tx.PosterCost = common.Big0 - aggregator, perr := ps.ReimbursableAggregatorForSender(sender) + if posterAddr != BatchPosterAddress { + return + } txBytes, merr := tx.MarshalBinary() txType := tx.Type() - if !util.TxTypeHasPosterCosts(txType) || perr != nil || merr != nil || aggregator == nil || poster != *aggregator { + if !util.TxTypeHasPosterCosts(txType) || merr != nil { return } @@ -250,54 +377,38 @@ func (ps *L1PricingState) AddPosterInfo(tx *types.Transaction, sender, poster co } // Approximate the l1 fee charged for posting this tx's calldata - l1GasPrice, _ := ps.L1BaseFeeEstimateWei() - l1BytePrice := arbmath.BigMulByUint(l1GasPrice, params.TxDataNonZeroGasEIP2028) - l1Fee := arbmath.BigMulByUint(l1BytePrice, uint64(l1Bytes)) - - // Adjust the price paid by the aggregator's reported improvements due to batching - ratio, _ := ps.AggregatorCompressionRatio(poster) - adjustedL1Fee := arbmath.BigMulByBips(l1Fee, ratio) - - tx.PosterCost = adjustedL1Fee + pricePerUnit, _ := ps.PricePerUnit() + numUnits := l1Bytes * params.TxDataNonZeroGasEIP2028 + tx.PosterCost = am.BigMulByUint(pricePerUnit, numUnits) + tx.CalldataUnits = numUnits } const TxFixedCost = 140 // assumed maximum size in bytes of a typical RLP-encoded tx, not including its calldata -func (ps *L1PricingState) PosterDataCost(message core.Message, sender, poster common.Address) *big.Int { - +func (ps *L1PricingState) PosterDataCost(message core.Message, poster common.Address) (*big.Int, uint64) { if tx := message.UnderlyingTransaction(); tx != nil { if tx.PosterCost == nil { - ps.AddPosterInfo(tx, sender, poster) + ps.AddPosterInfo(tx, poster) } - return tx.PosterCost + return tx.PosterCost, tx.CalldataUnits } - if message.RunMode() == types.MessageGasEstimationMode { - // assume for the purposes of gas estimation that the poster will be the user's preferred aggregator - aggregator, _ := ps.ReimbursableAggregatorForSender(sender) - if aggregator != nil { - poster = *aggregator - } else { - // assume the user will use the delayed inbox since there's no reimbursable party - return big.NewInt(0) - } + if poster != BatchPosterAddress { + return common.Big0, 0 } byteCount, err := byteCountAfterBrotli0(message.Data()) if err != nil { log.Error("failed to compress tx", "err", err) - return big.NewInt(0) + return common.Big0, 0 } // Approximate the l1 fee charged for posting this tx's calldata l1Bytes := byteCount + TxFixedCost - l1GasPrice, _ := ps.L1BaseFeeEstimateWei() - l1BytePrice := arbmath.BigMulByUint(l1GasPrice, params.TxDataNonZeroGasEIP2028) - l1Fee := arbmath.BigMulByUint(l1BytePrice, uint64(l1Bytes)) + pricePerUnit, _ := ps.PricePerUnit() - // Adjust the price paid by the aggregator's reported improvements due to batching - ratio, _ := ps.AggregatorCompressionRatio(poster) - return arbmath.BigMulByBips(l1Fee, ratio) + units := l1Bytes * params.TxDataNonZeroGasEIP2028 + return am.BigMulByUint(pricePerUnit, units), units } func byteCountAfterBrotli0(input []byte) (uint64, error) { diff --git a/arbos/l1pricing/l1pricing_test.go b/arbos/l1pricing/l1pricing_test.go index d93720228a..7c83373987 100644 --- a/arbos/l1pricing/l1pricing_test.go +++ b/arbos/l1pricing/l1pricing_test.go @@ -4,17 +4,16 @@ package l1pricing import ( + am "github.com/offchainlabs/nitro/util/arbmath" "math" "math/big" "testing" - "github.com/offchainlabs/nitro/arbos/burn" - "github.com/offchainlabs/nitro/arbos/storage" - "github.com/offchainlabs/nitro/util/arbmath" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/burn" + "github.com/offchainlabs/nitro/arbos/storage" ) func TestTxFixedCost(t *testing.T) { @@ -57,33 +56,16 @@ func TestL1PriceUpdate(t *testing.T) { Require(t, err) ps := OpenL1PricingState(sto) - tyme, err := ps.LastL1BaseFeeUpdateTime() + tyme, err := ps.LastUpdateTime() Require(t, err) if tyme != 0 { Fail(t) } - priceEstimate, err := ps.L1BaseFeeEstimateWei() - Require(t, err) - if priceEstimate.Cmp(big.NewInt(InitialL1BaseFeeEstimate)) != 0 { - Fail(t) - } - - newPrice := big.NewInt(20 * params.GWei) - ps.UpdatePricingModel(newPrice, 2) - priceEstimate, err = ps.L1BaseFeeEstimateWei() + initialPriceEstimate := am.UintToBig(InitialPricePerUnitWei) + priceEstimate, err := ps.PricePerUnit() Require(t, err) - - if priceEstimate.Cmp(newPrice) <= 0 || priceEstimate.Cmp(big.NewInt(InitialL1BaseFeeEstimate)) >= 0 { + if priceEstimate.Cmp(initialPriceEstimate) != 0 { Fail(t) } - - ps.UpdatePricingModel(newPrice, uint64(1)<<63) - priceEstimate, err = ps.L1BaseFeeEstimateWei() - Require(t, err) - - priceLimit := arbmath.BigAdd(newPrice, big.NewInt(300)) - if arbmath.BigGreaterThan(priceEstimate, priceLimit) || arbmath.BigLessThan(priceEstimate, newPrice) { - Fail(t, priceEstimate) - } } diff --git a/arbos/l1pricing_test.go b/arbos/l1pricing_test.go new file mode 100644 index 0000000000..fb0f1779c6 --- /dev/null +++ b/arbos/l1pricing_test.go @@ -0,0 +1,172 @@ +// Copyright 2021-2022, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE + +package arbos + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/offchainlabs/nitro/arbos/arbosState" + "github.com/offchainlabs/nitro/arbos/l1pricing" + "github.com/offchainlabs/nitro/util/arbmath" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/burn" +) + +type l1PricingTest struct { + unitReward uint64 + unitsPerSecond uint64 + fundsCollectedPerSecond uint64 + fundsSpent uint64 +} + +type l1TestExpectedResults struct { + rewardRecipientBalance *big.Int + unitsRemaining uint64 + fundsReceived *big.Int + fundsStillHeld *big.Int +} + +func TestL1Pricing(t *testing.T) { + inputs := []*l1PricingTest{ + { + unitReward: 10, + unitsPerSecond: 78, + fundsCollectedPerSecond: 7800, + fundsSpent: 3000, + }, + { + unitReward: 10, + unitsPerSecond: 78, + fundsCollectedPerSecond: 1313, + fundsSpent: 3000, + }, + { + unitReward: 10, + unitsPerSecond: 78, + fundsCollectedPerSecond: 31, + fundsSpent: 3000, + }, + } + for _, input := range inputs { + expectedResult := expectedResultsForL1Test(input) + _testL1PricingFundsDue(t, input, expectedResult) + } +} + +func expectedResultsForL1Test(input *l1PricingTest) *l1TestExpectedResults { + ret := &l1TestExpectedResults{} + availableFunds := arbmath.UintToBig(input.fundsCollectedPerSecond) + fundsWantedForRewards := big.NewInt(int64(input.unitReward * input.unitsPerSecond)) + unitsAllocated := arbmath.UintToBig(input.unitsPerSecond) + if arbmath.BigLessThan(availableFunds, fundsWantedForRewards) { + ret.rewardRecipientBalance = availableFunds + } else { + ret.rewardRecipientBalance = fundsWantedForRewards + } + availableFunds = arbmath.BigSub(availableFunds, ret.rewardRecipientBalance) + ret.unitsRemaining = (3 * input.unitsPerSecond) - unitsAllocated.Uint64() + + maxCollectable := big.NewInt(int64(input.fundsSpent)) + if arbmath.BigLessThan(availableFunds, maxCollectable) { + maxCollectable = availableFunds + } + ret.fundsReceived = maxCollectable + availableFunds = arbmath.BigSub(availableFunds, maxCollectable) + ret.fundsStillHeld = arbmath.BigAdd(arbmath.UintToBig(2*input.fundsCollectedPerSecond), availableFunds) + + return ret +} + +func _testL1PricingFundsDue(t *testing.T, testParams *l1PricingTest, expectedResults *l1TestExpectedResults) { + evm := newMockEVMForTesting() + burner := burn.NewSystemBurner(nil, false) + arbosSt, err := arbosState.OpenArbosState(evm.StateDB, burner) + Require(t, err) + + l1p := arbosSt.L1PricingState() + err = l1p.SetPerUnitReward(testParams.unitReward) + Require(t, err) + rewardAddress := common.Address{137} + err = l1p.SetPayRewardsTo(rewardAddress) + Require(t, err) + + posterTable := l1p.BatchPosterTable() + + // check initial funds state + rewardsDue, err := l1p.FundsDueForRewards() + Require(t, err) + if rewardsDue.Sign() != 0 { + t.Fatal() + } + if evm.StateDB.GetBalance(rewardAddress).Sign() != 0 { + t.Fatal() + } + posterAddrs, err := posterTable.AllPosters(math.MaxUint64) + Require(t, err) + if len(posterAddrs) != 1 { + t.Fatal() + } + firstPoster := posterAddrs[0] + firstPayTo := common.Address{1, 2} + poster, err := posterTable.OpenPoster(firstPoster, true) + Require(t, err) + due, err := poster.FundsDue() + Require(t, err) + if due.Sign() != 0 { + t.Fatal() + } + err = poster.SetPayTo(firstPayTo) + Require(t, err) + + // add another poster + secondPoster := common.Address{3, 4, 5} + secondPayTo := common.Address{6, 7} + _, err = posterTable.AddPoster(secondPoster, secondPayTo) + Require(t, err) + + // create some fake collection + balanceAdded := big.NewInt(int64(testParams.fundsCollectedPerSecond * 3)) + unitsAdded := uint64(testParams.unitsPerSecond * 3) + evm.StateDB.AddBalance(l1pricing.L1PricerFundsPoolAddress, balanceAdded) + err = l1p.SetUnitsSinceUpdate(unitsAdded) + Require(t, err) + + // submit a fake spending update, then check that balances are correct + err = l1p.UpdateForBatchPosterSpending(evm.StateDB, evm, 1, 3, firstPoster, new(big.Int).SetUint64(testParams.fundsSpent)) + Require(t, err) + rewardRecipientBalance := evm.StateDB.GetBalance(rewardAddress) + if !arbmath.BigEquals(rewardRecipientBalance, expectedResults.rewardRecipientBalance) { + t.Fatal(rewardRecipientBalance, expectedResults.rewardRecipientBalance) + } + unitsRemaining, err := l1p.UnitsSinceUpdate() + Require(t, err) + if unitsRemaining != expectedResults.unitsRemaining { + t.Fatal(unitsRemaining, expectedResults.unitsRemaining) + } + fundsReceived := evm.StateDB.GetBalance(firstPayTo) + if !arbmath.BigEquals(fundsReceived, expectedResults.fundsReceived) { + t.Fatal(fundsReceived, expectedResults.fundsReceived) + } + fundsStillHeld := evm.StateDB.GetBalance(l1pricing.L1PricerFundsPoolAddress) + if !arbmath.BigEquals(fundsStillHeld, expectedResults.fundsStillHeld) { + t.Fatal() + } +} + +func newMockEVMForTesting() *vm.EVM { + chainConfig := params.ArbitrumDevTestChainConfig() + _, statedb := arbosState.NewArbosMemoryBackedArbOSState() + context := vm.BlockContext{ + BlockNumber: big.NewInt(0), + GasLimit: ^uint64(0), + Time: big.NewInt(0), + } + evm := vm.NewEVM(context, vm.TxContext{}, statedb, chainConfig, vm.Config{}) + evm.ProcessingHook = &TxProcessor{} + return evm +} diff --git a/arbos/storage/storage.go b/arbos/storage/storage.go index 6dfd1c4d83..df9f726a5b 100644 --- a/arbos/storage/storage.go +++ b/arbos/storage/storage.go @@ -450,6 +450,10 @@ func (sbbi *StorageBackedBigInt) Set(val *big.Int) error { return sbbi.StorageSlot.Set(common.BigToHash(val)) } +func (sbbi *StorageBackedBigInt) SetByUint(val uint64) error { + return sbbi.StorageSlot.Set(util.UintToHash(val)) +} + type StorageBackedAddress struct { StorageSlot } diff --git a/arbos/tx_processor.go b/arbos/tx_processor.go index 792bb2a817..6cb98fe804 100644 --- a/arbos/tx_processor.go +++ b/arbos/tx_processor.go @@ -9,6 +9,8 @@ import ( "math/big" "time" + "github.com/offchainlabs/nitro/arbos/l1pricing" + "github.com/offchainlabs/nitro/arbos/util" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" "github.com/offchainlabs/nitro/util/arbmath" @@ -252,8 +254,19 @@ func (p *TxProcessor) GasChargingHook(gasRemaining *uint64) error { var gasNeededToStartEVM uint64 gasPrice := p.evm.Context.BaseFee - coinbase := p.evm.Context.Coinbase - posterCost := p.state.L1PricingState().PosterDataCost(p.msg, p.msg.From(), coinbase) + + var poster common.Address + if p.msg.RunMode() != types.MessageCommitMode { + poster = l1pricing.BatchPosterAddress + } else { + poster = p.evm.Context.Coinbase + } + posterCost, calldataUnits := p.state.L1PricingState().PosterDataCost(p.msg, poster) + if calldataUnits > 0 { + if err := p.state.L1PricingState().AddToUnitsSinceUpdate(calldataUnits); err != nil { + return err + } + } if p.msg.RunMode() == types.MessageGasEstimationMode { // Suggest the amount of gas needed for a given amount of ETH is higher in case of congestion. @@ -271,6 +284,7 @@ func (p *TxProcessor) GasChargingHook(gasRemaining *uint64) error { // Pad the L1 cost by 10% in case the L1 gas price rises posterCost = arbmath.BigMulByFrac(posterCost, 110, 100) } + if gasPrice.Sign() > 0 { posterCostInL2Gas := arbmath.BigDiv(posterCost, gasPrice) // the cost as if it were an amount of gas if !posterCostInL2Gas.IsUint64() { diff --git a/arbos/util/util.go b/arbos/util/util.go index 55c10b0b93..ca97fa0c6b 100644 --- a/arbos/util/util.go +++ b/arbos/util/util.go @@ -15,6 +15,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" + "github.com/offchainlabs/nitro/util/arbmath" ) var AddressAliasOffset *big.Int @@ -24,6 +25,8 @@ var ParseL2ToL1TransactionLog func(interface{}, *types.Log) error var ParseL2ToL1TxLog func(interface{}, *types.Log) error var PackInternalTxDataStartBlock func(...interface{}) ([]byte, error) var UnpackInternalTxDataStartBlock func([]byte) ([]interface{}, error) +var PackInternalTxDataBatchPostingReport func(...interface{}) ([]byte, error) +var UnpackInternalTxDataBatchPostingReport func([]byte) ([]interface{}, error) var PackArbRetryableTxRedeem func(...interface{}) ([]byte, error) func init() { @@ -32,7 +35,7 @@ func init() { panic("Error initializing AddressAliasOffset") } AddressAliasOffset = offset - InverseAddressAliasOffset = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 160), AddressAliasOffset) + InverseAddressAliasOffset = arbmath.BigSub(new(big.Int).Lsh(big.NewInt(1), 160), AddressAliasOffset) // Create a mechanism for parsing event logs logParser := func(source string, name string) func(interface{}, *types.Log) error { @@ -88,6 +91,7 @@ func init() { acts := precompilesgen.ArbosActsABI PackInternalTxDataStartBlock, UnpackInternalTxDataStartBlock = callParser(acts, "startBlock") + PackInternalTxDataBatchPostingReport, UnpackInternalTxDataBatchPostingReport = callParser(acts, "batchPostingReport") PackArbRetryableTxRedeem, _ = callParser(precompilesgen.ArbRetryableTxABI, "redeem") } diff --git a/arbstate/geth_test.go b/arbstate/geth_test.go index 33b11d5235..74b71b7e06 100644 --- a/arbstate/geth_test.go +++ b/arbstate/geth_test.go @@ -116,7 +116,7 @@ func RunMessagesThroughAPI(t *testing.T, msgs [][]byte, statedb *state.StateDB) if err != nil { t.Error(err) } - txes, err := msg.ParseL2Transactions(chainId) + txes, err := msg.ParseL2Transactions(chainId, nil) if err != nil { t.Error(err) } diff --git a/arbstate/inbox.go b/arbstate/inbox.go index b7f270655a..5f0c3be13e 100644 --- a/arbstate/inbox.go +++ b/arbstate/inbox.go @@ -367,7 +367,7 @@ func (r *inboxMultiplexer) getNextMsg() (*MessageWithMetadata, error) { Message: &arbos.L1IncomingMessage{ Header: &arbos.L1IncomingMessageHeader{ Kind: arbos.L1MessageType_L2Message, - Poster: l1pricing.SequencerAddress, + Poster: l1pricing.BatchPosterAddress, BlockNumber: blockNumber, Timestamp: timestamp, RequestId: nil, diff --git a/cmd/replay/main.go b/cmd/replay/main.go index 444e56945c..be8558c1ee 100644 --- a/cmd/replay/main.go +++ b/cmd/replay/main.go @@ -164,7 +164,13 @@ func main() { message := readMessage(chainConfig.ArbitrumChainParams.DataAvailabilityCommittee) chainContext := WavmChainContext{} - newBlock, _ = arbos.ProduceBlock(message.Message, message.DelayedMessagesRead, lastBlockHeader, statedb, chainContext, chainConfig) + batchFetcher := func(batchNum uint64) ([]byte, error) { + return wavmio.ReadInboxMessage(batchNum), nil + } + newBlock, _, err = arbos.ProduceBlock(message.Message, message.DelayedMessagesRead, lastBlockHeader, statedb, chainContext, chainConfig, batchFetcher) + if err != nil { + panic(err) + } } else { // Initialize ArbOS with this init message and create the genesis block. diff --git a/contracts/src/bridge/IInbox.sol b/contracts/src/bridge/IInbox.sol index 3ecf47dd7d..1063ff3e3b 100644 --- a/contracts/src/bridge/IInbox.sol +++ b/contracts/src/bridge/IInbox.sol @@ -76,6 +76,12 @@ interface IInbox is IMessageProvider { bytes calldata data ) external payable returns (uint256); + function submitBatchSpendingReportTransaction( + address batchPosterAddr, + bytes32 dataHash, + uint256 batchNumber + ) external returns (uint256); + /// @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error function createRetryableTicket( address to, diff --git a/contracts/src/bridge/Inbox.sol b/contracts/src/bridge/Inbox.sol index f216cc6181..3afde1b732 100644 --- a/contracts/src/bridge/Inbox.sol +++ b/contracts/src/bridge/Inbox.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.4; import "./IInbox.sol"; +import "./ISequencerInbox.sol"; import "./IBridge.sol"; import "./Messages.sol"; @@ -15,6 +16,7 @@ import { L1MessageType_L2FundedByL1, L1MessageType_submitRetryableTx, L1MessageType_ethDeposit, + L1MessageType_batchPostingReport, L2MessageType_unsignedEOATx, L2MessageType_unsignedContractTx } from "../libraries/MessageTypes.sol"; @@ -31,6 +33,7 @@ import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; */ contract Inbox is DelegateCallAware, PausableUpgradeable, IInbox { IBridge public override bridge; + ISequencerInbox public sequencerInbox; /// ------------------------------------ allow list start ------------------------------------ /// @@ -83,9 +86,14 @@ contract Inbox is DelegateCallAware, PausableUpgradeable, IInbox { _unpause(); } - function initialize(IBridge _bridge) external initializer onlyDelegated { + function initialize(IBridge _bridge, ISequencerInbox _sequencerInbox) + external + initializer + onlyDelegated + { if (address(bridge) != address(0)) revert AlreadyInit(); bridge = _bridge; + sequencerInbox = _sequencerInbox; allowListEnabled = false; __Pausable_init(); } @@ -228,6 +236,26 @@ contract Inbox is DelegateCallAware, PausableUpgradeable, IInbox { ); } + function submitBatchSpendingReportTransaction( + address batchPosterAddr, + bytes32 dataHash, + uint256 batchNumber + ) external virtual override whenNotPaused returns (uint256) { + require(ISequencerInbox(msg.sender) == sequencerInbox, "unauthorized"); + return + _deliverMessage( + L1MessageType_batchPostingReport, + batchPosterAddr, + abi.encodePacked( + block.timestamp, + batchPosterAddr, + dataHash, + batchNumber, + block.basefee + ) + ); + } + /** * @notice Get the L1 fee for submitting a retryable * @dev This fee can be paid by funds already in the L2 aliased address or by the current message value diff --git a/contracts/src/bridge/SequencerInbox.sol b/contracts/src/bridge/SequencerInbox.sol index 1c1e022886..7128a7c379 100644 --- a/contracts/src/bridge/SequencerInbox.sol +++ b/contracts/src/bridge/SequencerInbox.sol @@ -5,10 +5,12 @@ pragma solidity ^0.8.0; import "./IBridge.sol"; +import "./IInbox.sol"; import "./ISequencerInbox.sol"; import "../rollup/IRollupLogic.sol"; import "./Messages.sol"; +import {L1MessageType_batchPostingReport} from "../libraries/MessageTypes.sol"; import {GasRefundEnabled, IGasRefunder} from "../libraries/IGasRefunder.sol"; import "../libraries/DelegateCallAware.sol"; import {MAX_DATA_SIZE} from "../libraries/Constants.sol"; @@ -25,6 +27,7 @@ contract SequencerInbox is DelegateCallAware, GasRefundEnabled, ISequencerInbox uint256 public totalDelayedMessagesRead; IBridge public delayedBridge; + IInbox public delayedInbox; /// @dev The size of the batch header uint256 public constant HEADER_LENGTH = 40; @@ -46,12 +49,14 @@ contract SequencerInbox is DelegateCallAware, GasRefundEnabled, ISequencerInbox function initialize( IBridge delayedBridge_, + IInbox delayedInbox_, address rollup_, ISequencerInbox.MaxTimeVariation calldata maxTimeVariation_ ) external onlyDelegated { if (delayedBridge != IBridge(address(0))) revert AlreadyInit(); if (delayedBridge_ == IBridge(address(0))) revert HadZeroInit(); delayedBridge = delayedBridge_; + delayedInbox = delayedInbox_; rollup = rollup_; maxTimeVariation = maxTimeVariation_; } @@ -150,6 +155,9 @@ contract SequencerInbox is DelegateCallAware, GasRefundEnabled, ISequencerInbox dataHash, afterDelayedMessagesRead ); + if (data.length > 0) { + _reportBatchSpending(dataHash, sequenceNumber); + } emit SequencerBatchDelivered( inboxAccs.length - 1, beforeAcc, @@ -190,6 +198,10 @@ contract SequencerInbox is DelegateCallAware, GasRefundEnabled, ISequencerInbox emit SequencerBatchData(sequenceNumber, data); } + function _reportBatchSpending(bytes32 dataHash, uint256 sequenceNumber) internal { + delayedInbox.submitBatchSpendingReportTransaction(msg.sender, dataHash, sequenceNumber); + } + function dasKeysetHashFromBatchData(bytes memory data) internal pure returns (bytes32) { if (data.length < 33 || data[0] & 0x80 == 0) { return bytes32(0); diff --git a/contracts/src/libraries/MessageTypes.sol b/contracts/src/libraries/MessageTypes.sol index 770a8135ea..093cb332a2 100644 --- a/contracts/src/libraries/MessageTypes.sol +++ b/contracts/src/libraries/MessageTypes.sol @@ -8,6 +8,7 @@ uint8 constant L2_MSG = 3; uint8 constant L1MessageType_L2FundedByL1 = 7; uint8 constant L1MessageType_submitRetryableTx = 9; uint8 constant L1MessageType_ethDeposit = 12; +uint8 constant L1MessageType_batchPostingReport = 13; uint8 constant L2MessageType_unsignedEOATx = 0; uint8 constant L2MessageType_unsignedContractTx = 1; diff --git a/contracts/src/mocks/InboxStub.sol b/contracts/src/mocks/InboxStub.sol index c9677855c9..de32569523 100644 --- a/contracts/src/mocks/InboxStub.sol +++ b/contracts/src/mocks/InboxStub.sol @@ -99,6 +99,14 @@ contract InboxStub is IInbox { revert("NOT_IMPLEMENTED"); } + function submitBatchSpendingReportTransaction( + address, + bytes32, + uint256 + ) external pure override returns (uint256) { + revert("NOT_IMPLEMENTED"); + } + function createRetryableTicket( address, uint256, diff --git a/contracts/src/precompiles/ArbAggregator.sol b/contracts/src/precompiles/ArbAggregator.sol index 53958a6be2..4c01f00b6e 100644 --- a/contracts/src/precompiles/ArbAggregator.sol +++ b/contracts/src/precompiles/ArbAggregator.sol @@ -7,45 +7,35 @@ pragma solidity >=0.4.21 <0.9.0; /// @title Provides aggregators and their users methods for configuring how they participate in L1 aggregation. /// @notice Precompiled contract that exists in every Arbitrum chain at 0x000000000000000000000000000000000000006d interface ArbAggregator { - /// @notice Get the preferred aggregator for an address. - /// @param addr The address to fetch aggregator for - /// @return (preferredAggregatorAddress, isDefault) - /// isDefault is true if addr is set to prefer the default aggregator + /// @notice Deprecated, customization of preferred aggregator is no longer supported + /// @notice Get the address of an arbitrarily chosen batch poster. + /// @param addr ignored + /// @return (batchPosterAddress, true) function getPreferredAggregator(address addr) external view returns (address, bool); - /// @notice Set the caller's preferred aggregator. - /// @param prefAgg If prefAgg is zero, this sets the caller to prefer the default aggregator - function setPreferredAggregator(address prefAgg) external; - + /// @notice Deprecated, there is no longer a single preferred aggregator, use getBatchPosters instead /// @notice Get default aggregator. function getDefaultAggregator() external view returns (address); - /// @notice Set the preferred aggregator. - /// This reverts unless called by the aggregator, its fee collector, or a chain owner - /// @param newDefault New default aggregator - function setDefaultAggregator(address newDefault) external; - - /// @notice Get the aggregator's compression ratio - /// @param aggregator The aggregator to fetch the compression ratio for - /// @return The compression ratio, measured in basis points - function getCompressionRatio(address aggregator) external view returns (uint64); - - /// @notice Set the aggregator's compression ratio - /// This reverts unless called by the aggregator, its fee collector, or a chain owner - /// @param aggregator The aggregator to set the compression ratio for - /// @param ratio The compression ratio, measured in basis points - function setCompressionRatio(address aggregator, uint64 ratio) external; - - /// @notice Get the address where fees to aggregator are sent. - /// @param aggregator The aggregator to get the fee collector for - /// @return The fee collectors address. This will often but not always be the same as the aggregator's address. - function getFeeCollector(address aggregator) external view returns (address); - - /// @notice Set the address where fees to aggregator are sent. - /// This reverts unless called by the aggregator, its fee collector, or a chain owner - /// @param aggregator The aggregator to set the fee collector for + /// @notice Get a list of all current batch posters + /// @return Batch poster addresses + function getBatchPosters() external view returns (address[] memory); + + /// @notice Adds newBatchPoster as a batch poster + /// This reverts unless called by a chain owner + /// @param newBatchPoster New batch poster + function addBatchPoster(address newBatchPoster) external; + + /// @notice Get the address where fees to batchPoster are sent. + /// @param batchPoster The batch poster to get the fee collector for + /// @return The fee collectors address. This will sometimes but not always be the same as the batch poster's address. + function getFeeCollector(address batchPoster) external view returns (address); + + /// @notice Set the address where fees to batchPoster are sent. + /// This reverts unless called by the batch poster, its fee collector, or a chain owner + /// @param batchPoster The batch poster to set the fee collector for /// @param newFeeCollector The new fee collector to set - function setFeeCollector(address aggregator, address newFeeCollector) external; + function setFeeCollector(address batchPoster, address newFeeCollector) external; /// @notice Deprecated, always returns zero /// @notice Get the tx base fee (in approximate L1 gas) for aggregator diff --git a/contracts/src/precompiles/ArbOwner.sol b/contracts/src/precompiles/ArbOwner.sol index f469b6ff53..03a4325c1d 100644 --- a/contracts/src/precompiles/ArbOwner.sol +++ b/contracts/src/precompiles/ArbOwner.sol @@ -24,9 +24,6 @@ interface ArbOwner { /// @notice Retrieves the list of chain owners function getAllChainOwners() external view returns (address[] memory); - /// @notice Set the L1 basefee estimate directly, bypassing the autoregression - function setL1BaseFeeEstimate(uint256 priceInWei) external; - /// @notice Set how slowly ArbOS updates its estimate of the L1 basefee function setL1BaseFeeEstimateInertia(uint64 inertia) external; @@ -57,6 +54,18 @@ interface ArbOwner { /// @notice Upgrades ArbOS to the requested version at the requested timestamp function scheduleArbOSUpgrade(uint64 newVersion, uint64 timestamp) external; + /// @notice Sets equilibration units parameter for L1 price adjustment algorithm + function setL1PricingEquilibrationUnits(uint256 equilibrationUnits) external; + + /// @notice Sets inertia parameter for L1 price adjustment algorithm + function setL1PricingInertia(uint64 inertia) external; + + /// @notice Sets reward recipient address for L1 price adjustment algorithm + function setL1PricingRewardRecipient(address recipient) external; + + /// @notice Sets reward amount for L1 price adjustment algorithm, in wei per unit + function setL1PricingRewardRate(uint64 weiPerUnit) external; + // Emitted when a successful call is made to this precompile event OwnerActs(bytes4 indexed method, address indexed owner, bytes data); } diff --git a/contracts/src/precompiles/ArbosActs.sol b/contracts/src/precompiles/ArbosActs.sol index 61b349c09c..7ca71b6518 100644 --- a/contracts/src/precompiles/ArbosActs.sol +++ b/contracts/src/precompiles/ArbosActs.sol @@ -32,8 +32,17 @@ interface ArbosActs { function startBlock( uint256 l1BaseFee, uint64 l1BlockNumber, + uint64 l2BlockNumber, uint64 timePassed ) external; + function batchPostingReport( + uint256 batchTimestamp, + address batchPosterAddress, + uint64 batchNumber, + uint64 batchDataGas, + uint256 l1BaseFeeWei + ) external; + error CallerNotArbOS(); } diff --git a/contracts/src/rollup/BridgeCreator.sol b/contracts/src/rollup/BridgeCreator.sol index e684320280..50c34d947e 100644 --- a/contracts/src/rollup/BridgeCreator.sol +++ b/contracts/src/rollup/BridgeCreator.sol @@ -101,8 +101,13 @@ contract BridgeCreator is Ownable { } frame.delayedBridge.initialize(); - frame.sequencerInbox.initialize(IBridge(frame.delayedBridge), rollup, maxTimeVariation); - frame.inbox.initialize(IBridge(frame.delayedBridge)); + frame.sequencerInbox.initialize( + IBridge(frame.delayedBridge), + IInbox(frame.inbox), + rollup, + maxTimeVariation + ); + frame.inbox.initialize(IBridge(frame.delayedBridge), ISequencerInbox(frame.sequencerInbox)); frame.rollupEventBridge.initialize(address(frame.delayedBridge), rollup); frame.outbox.initialize(rollup, IBridge(frame.delayedBridge)); diff --git a/contracts/test/contract/sequencerInboxForceInclude.spec.ts b/contracts/test/contract/sequencerInboxForceInclude.spec.ts index cc74f294dd..2de836f007 100644 --- a/contracts/test/contract/sequencerInboxForceInclude.spec.ts +++ b/contracts/test/contract/sequencerInboxForceInclude.spec.ts @@ -257,6 +257,7 @@ describe('SequencerInboxForceInclude', async () => { await sequencerInbox.initialize( bridgeProxy.address, + inbox.address, await dummyRollup.getAddress(), { delayBlocks: maxDelayBlocks, @@ -265,7 +266,7 @@ describe('SequencerInboxForceInclude', async () => { futureSeconds: 3000, }, ) - await inbox.initialize(bridgeProxy.address) + await inbox.initialize(bridgeProxy.address, sequencerInbox.address) await bridge.setInbox(inbox.address, true) diff --git a/contracts/test/prover/one-step-proof.ts b/contracts/test/prover/one-step-proof.ts index 91cf3065d7..c87011c59f 100644 --- a/contracts/test/prover/one-step-proof.ts +++ b/contracts/test/prover/one-step-proof.ts @@ -15,7 +15,8 @@ async function sendTestMessages() { const path = msgRoot + "msg" + String(msgNum) + ".bin"; const buf = fs.readFileSync(path); await inbox.sendL2MessageFromOrigin(buf, gasOpts); - await seqInbox.addSequencerL2BatchFromOrigin(msgNum, buf, 0, ethers.constants.AddressZero, gasOpts); + // Don't use the FromOrigin variant as the stub will fail to create a batch posting report + await seqInbox.addSequencerL2Batch(msgNum, buf, 0, ethers.constants.AddressZero, gasOpts); } } diff --git a/go-ethereum b/go-ethereum index 7e332b7049..4a00cd8cc4 160000 --- a/go-ethereum +++ b/go-ethereum @@ -1 +1 @@ -Subproject commit 7e332b704961e33a5dd7edcc419e49d8de244bb5 +Subproject commit 4a00cd8cc4b2b0025c8ba9e98259714ed69e49cb diff --git a/nodeInterface/NodeInterface.go b/nodeInterface/NodeInterface.go index b507ad4418..6dc9f965fd 100644 --- a/nodeInterface/NodeInterface.go +++ b/nodeInterface/NodeInterface.go @@ -124,7 +124,7 @@ func (n NodeInterface) EstimateRetryableTicket( pRetryTo = &to } - l1BaseFee, _ := c.State.L1PricingState().L1BaseFeeEstimateWei() + l1BaseFee, _ := c.State.L1PricingState().PricePerUnit() maxSubmissionFee := retryables.RetryableSubmissionFee(len(data), l1BaseFee) submitTx := &types.ArbitrumSubmitRetryableTx{ diff --git a/nodeInterface/virtual-contracts.go b/nodeInterface/virtual-contracts.go index 4c97fdd9ea..5d9c8bc86d 100644 --- a/nodeInterface/virtual-contracts.go +++ b/nodeInterface/virtual-contracts.go @@ -125,12 +125,12 @@ func init() { log.Error("failed to open ArbOS state", "err", err) return } - poster, _ := state.L1PricingState().ReimbursableAggregatorForSender(msg.From()) - if poster == nil || header.BaseFee.Sign() == 0 { + if header.BaseFee.Sign() == 0 { // if gas is free or there's no reimbursable poster, the user won't pay for L1 data costs return } - posterCost := state.L1PricingState().PosterDataCost(msg, msg.From(), *poster) + + posterCost, _ := state.L1PricingState().PosterDataCost(msg, header.Coinbase) posterCostInL2Gas := arbmath.BigToUintSaturating(arbmath.BigDiv(posterCost, header.BaseFee)) *gascap = arbmath.SaturatingUAdd(*gascap, posterCostInL2Gas) } diff --git a/precompiles/ArbAggregator.go b/precompiles/ArbAggregator.go index f13fd84e19..224a104bc7 100644 --- a/precompiles/ArbAggregator.go +++ b/precompiles/ArbAggregator.go @@ -5,12 +5,8 @@ package precompiles import ( "errors" + "github.com/offchainlabs/nitro/arbos/l1pricing" "math/big" - - "github.com/ethereum/go-ethereum/common" - - "github.com/offchainlabs/nitro/arbos/arbosState" - "github.com/offchainlabs/nitro/util/arbmath" ) // Provides aggregators and their users methods for configuring how they participate in L1 aggregation. @@ -20,87 +16,74 @@ type ArbAggregator struct { Address addr // 0x6d } -// Gets an account's preferred aggregator +var ErrNotOwner = errors.New("must be called by chain owner") + +// [Deprecated] func (con ArbAggregator) GetPreferredAggregator(c ctx, evm mech, address addr) (prefAgg addr, isDefault bool, err error) { - l1p := c.State.L1PricingState() - maybePrefAgg, err := l1p.UserSpecifiedAggregator(address) - if err != nil { - return common.Address{}, false, err - } - if maybePrefAgg != nil { - return *maybePrefAgg, false, nil - } - maybeReimbursableAgg, err := l1p.ReimbursableAggregatorForSender(address) - if err != nil || maybeReimbursableAgg == nil { - return common.Address{}, false, err - } - return *maybeReimbursableAgg, true, nil + return l1pricing.BatchPosterAddress, true, err } -// Sets the caller's preferred aggregator to that provided -func (con ArbAggregator) SetPreferredAggregator(c ctx, evm mech, prefAgg addr) error { - var maybePrefAgg *common.Address - if prefAgg != (common.Address{}) { - maybePrefAgg = &prefAgg - } - return c.State.L1PricingState().SetUserSpecifiedAggregator(c.caller, maybePrefAgg) +// [Deprecated] +func (con ArbAggregator) GetDefaultAggregator(c ctx, evm mech) (addr, error) { + return l1pricing.BatchPosterAddress, nil } -// Gets the chain's default aggregator -func (con ArbAggregator) GetDefaultAggregator(c ctx, evm mech) (addr, error) { - return c.State.L1PricingState().DefaultAggregator() +// Get the addresses of all current batch posters +func (con ArbAggregator) GetBatchPosters(c ctx, evm mech) ([]addr, error) { + return c.State.L1PricingState().BatchPosterTable().AllPosters(65536) } -// Sets the chain's default aggregator (caller must be the current default aggregator, its fee collector, or an owner) -func (con ArbAggregator) SetDefaultAggregator(c ctx, evm mech, newDefault addr) error { - l1State := c.State.L1PricingState() - defaultAgg, err := l1State.DefaultAggregator() +func (con ArbAggregator) AddBatchPoster(c ctx, evm mech, newBatchPoster addr) error { + isOwner, err := c.State.ChainOwners().IsMember(c.caller) if err != nil { return err } - allowed, err := accountIsAggregatorOrCollectorOrOwner(c.caller, defaultAgg, c.State) + if !isOwner { + return ErrNotOwner + } + batchPosterTable := c.State.L1PricingState().BatchPosterTable() + isBatchPoster, err := batchPosterTable.ContainsPoster(newBatchPoster) if err != nil { return err } - if !allowed { - return errors.New("Only the current default (or its fee collector / chain owner) may change the default") + if !isBatchPoster { + _, err = batchPosterTable.AddPoster(newBatchPoster, newBatchPoster) + if err != nil { + return err + } } - return l1State.SetDefaultAggregator(newDefault) + return nil } -// Get the aggregator's compression ratio, measured in basis points -func (con ArbAggregator) GetCompressionRatio(c ctx, evm mech, aggregator addr) (uint64, error) { - ratio, err := c.State.L1PricingState().AggregatorCompressionRatio(aggregator) - return uint64(ratio), err +// Gets a batch poster's fee collector +func (con ArbAggregator) GetFeeCollector(c ctx, evm mech, batchPoster addr) (addr, error) { + posterInfo, err := c.State.L1PricingState().BatchPosterTable().OpenPoster(batchPoster, false) + if err != nil { + return addr{}, err + } + return posterInfo.PayTo() } -// Set the aggregator's compression ratio, measured in basis points -func (con ArbAggregator) SetCompressionRatio(c ctx, evm mech, aggregator addr, newRatio uint64) error { - allowed, err := accountIsAggregatorOrCollectorOrOwner(c.caller, aggregator, c.State) +// Sets a batch poster's fee collector (caller must be the batch poster, its fee collector, or an owner) +func (con ArbAggregator) SetFeeCollector(c ctx, evm mech, batchPoster addr, newFeeCollector addr) error { + posterInfo, err := c.State.L1PricingState().BatchPosterTable().OpenPoster(batchPoster, false) if err != nil { return err } - if !allowed { - return errors.New("Only an aggregator (or its fee collector / chain owner) may change its compression ratio") - } - return c.State.L1PricingState().SetAggregatorCompressionRatio(aggregator, arbmath.Bips(newRatio)) -} - -// Gets an aggregator's fee collector -func (con ArbAggregator) GetFeeCollector(c ctx, evm mech, aggregator addr) (addr, error) { - return c.State.L1PricingState().AggregatorFeeCollector(aggregator) -} - -// Sets an aggregator's fee collector (caller must be the aggregator, its fee collector, or an owner) -func (con ArbAggregator) SetFeeCollector(c ctx, evm mech, aggregator addr, newFeeCollector addr) error { - allowed, err := accountIsAggregatorOrCollectorOrOwner(c.caller, aggregator, c.State) + oldFeeCollector, err := posterInfo.PayTo() if err != nil { return err } - if !allowed { - return errors.New("Only an aggregator (or its fee collector / chain owner) may change its fee collector") + if c.caller != batchPoster && c.caller != oldFeeCollector { + isOwner, err := c.State.ChainOwners().IsMember(c.caller) + if err != nil { + return err + } + if !isOwner { + return errors.New("Only a batch poster (or its fee collector / chain owner) may change its fee collector") + } } - return c.State.L1PricingState().SetAggregatorFeeCollector(aggregator, newFeeCollector) + return posterInfo.SetPayTo(newFeeCollector) } // Gets an aggregator's current fixed fee to submit a tx @@ -114,15 +97,3 @@ func (con ArbAggregator) SetTxBaseFee(c ctx, evm mech, aggregator addr, feeInL1G // This is deprecated and is now a no-op. return nil } - -func accountIsAggregatorOrCollectorOrOwner(account, aggregator addr, state *arbosState.ArbosState) (bool, error) { - if account == aggregator { - return true, nil - } - l1State := state.L1PricingState() - collector, err := l1State.AggregatorFeeCollector(aggregator) - if account == collector || err != nil { - return true, err - } - return state.ChainOwners().IsMember(account) -} diff --git a/precompiles/ArbAggregator_test.go b/precompiles/ArbAggregator_test.go index 156131a0cb..ce1cebde5d 100644 --- a/precompiles/ArbAggregator_test.go +++ b/precompiles/ArbAggregator_test.go @@ -12,76 +12,30 @@ import ( "github.com/offchainlabs/nitro/arbos/l1pricing" ) -func TestDefaultAggregator(t *testing.T) { +func TestArbAggregatorBatchPosters(t *testing.T) { evm := newMockEVMForTesting() context := testContext(common.Address{}, evm) addr := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) - // initial default aggregator should be zero address - def, err := ArbAggregator{}.GetDefaultAggregator(context, evm) + // initially should have one batch poster + bps, err := ArbAggregator{}.GetBatchPosters(context, evm) Require(t, err) - if def != (l1pricing.SequencerAddress) { + if len(bps) != 1 { Fail(t) } - // set default aggregator to addr + // add addr as a batch poster Require(t, ArbDebug{}.BecomeChainOwner(context, evm)) - Require(t, ArbAggregator{}.SetDefaultAggregator(context, evm, addr)) + Require(t, ArbAggregator{}.AddBatchPoster(context, evm, addr)) - // default aggregator should now be addr - res, err := ArbAggregator{}.GetDefaultAggregator(context, evm) + // there should now be two batch posters, and addr should be one of them + bps, err = ArbAggregator{}.GetBatchPosters(context, evm) Require(t, err) - if res != addr { + if len(bps) != 2 { Fail(t) } -} - -func TestPreferredAggregator(t *testing.T) { - evm := newMockEVMForTesting() - agg := ArbAggregator{} - - userAddr := common.BytesToAddress(crypto.Keccak256([]byte{0})[:20]) - defaultAggAddr := common.BytesToAddress(crypto.Keccak256([]byte{1})[:20]) - prefAggAddr := common.BytesToAddress(crypto.Keccak256([]byte{2})[:20]) - - callerCtx := testContext(common.Address{}, evm) - userCtx := testContext(userAddr, evm) - - // initial preferred aggregator should be the default of zero address - res, isDefault, err := ArbAggregator{}.GetPreferredAggregator(callerCtx, evm, userAddr) - Require(t, err) - if !isDefault { - Fail(t) - } - if res != (l1pricing.SequencerAddress) { - Fail(t) - } - - // set default aggregator - Require(t, ArbDebug{}.BecomeChainOwner(callerCtx, evm)) - Require(t, agg.SetDefaultAggregator(callerCtx, evm, defaultAggAddr)) - - // preferred aggregator should be the new default address - res, isDefault, err = agg.GetPreferredAggregator(callerCtx, evm, userAddr) - Require(t, err) - if !isDefault { - Fail(t) - } - if res != defaultAggAddr { - Fail(t) - } - - // set preferred aggregator - Require(t, agg.SetPreferredAggregator(userCtx, evm, prefAggAddr)) - - // preferred aggregator should now be prefAggAddr - res, isDefault, err = agg.GetPreferredAggregator(callerCtx, evm, userAddr) - Require(t, err) - if isDefault { - Fail(t) - } - if res != prefAggAddr { + if bps[0] != addr && bps[1] != addr { Fail(t) } } @@ -90,7 +44,7 @@ func TestFeeCollector(t *testing.T) { evm := newMockEVMForTesting() agg := ArbAggregator{} - aggAddr := common.BytesToAddress(crypto.Keccak256([]byte{0})[:20]) + aggAddr := l1pricing.BatchPosterAddress collectorAddr := common.BytesToAddress(crypto.Keccak256([]byte{1})[:20]) impostorAddr := common.BytesToAddress(crypto.Keccak256([]byte{2})[:20]) diff --git a/precompiles/ArbGasInfo.go b/precompiles/ArbGasInfo.go index 21578c2670..3401ed9f2e 100644 --- a/precompiles/ArbGasInfo.go +++ b/precompiles/ArbGasInfo.go @@ -6,8 +6,6 @@ package precompiles import ( "math/big" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/params" "github.com/offchainlabs/nitro/arbos/l1pricing" "github.com/offchainlabs/nitro/arbos/storage" @@ -28,22 +26,17 @@ func (con ArbGasInfo) GetPricesInWeiWithAggregator( evm mech, aggregator addr, ) (huge, huge, huge, huge, huge, huge, error) { - l1GasPrice, err := c.State.L1PricingState().L1BaseFeeEstimateWei() + l1GasPrice, err := c.State.L1PricingState().PricePerUnit() if err != nil { return nil, nil, nil, nil, nil, nil, err } l2GasPrice := evm.Context.BaseFee - ratio, err := c.State.L1PricingState().AggregatorCompressionRatio(aggregator) - if err != nil { - return nil, nil, nil, nil, nil, nil, err - } // aggregators compress calldata, so we must estimate accordingly weiForL1Calldata := arbmath.BigMulByUint(l1GasPrice, params.TxDataNonZeroGasEIP2028) - perL1CalldataUnit := arbmath.BigDivByUint(arbmath.BigMulByBips(weiForL1Calldata, ratio), 16) // the cost of a simple tx without calldata - perL2Tx := arbmath.BigMulByUint(perL1CalldataUnit, 16*l1pricing.TxFixedCost) + perL2Tx := arbmath.BigMulByUint(weiForL1Calldata, l1pricing.TxFixedCost) // nitro's compute-centric l2 gas pricing has no special compute component that rises independently perArbGasBase := l2GasPrice @@ -52,37 +45,25 @@ func (con ArbGasInfo) GetPricesInWeiWithAggregator( weiForL2Storage := arbmath.BigMul(l2GasPrice, storageArbGas) - return perL2Tx, perL1CalldataUnit, weiForL2Storage, perArbGasBase, perArbGasCongestion, perArbGasTotal, nil + return perL2Tx, weiForL1Calldata, weiForL2Storage, perArbGasBase, perArbGasCongestion, perArbGasTotal, nil } // Get prices in wei when using the caller's preferred aggregator func (con ArbGasInfo) GetPricesInWei(c ctx, evm mech) (huge, huge, huge, huge, huge, huge, error) { - maybeAggregator, err := c.State.L1PricingState().ReimbursableAggregatorForSender(c.caller) - if err != nil { - return nil, nil, nil, nil, nil, nil, err - } - if maybeAggregator == nil { - return con.GetPricesInWeiWithAggregator(c, evm, common.Address{}) - } - return con.GetPricesInWeiWithAggregator(c, evm, *maybeAggregator) + return con.GetPricesInWeiWithAggregator(c, evm, addr{}) } // Get prices in ArbGas when using the provided aggregator func (con ArbGasInfo) GetPricesInArbGasWithAggregator(c ctx, evm mech, aggregator addr) (huge, huge, huge, error) { - l1GasPrice, err := c.State.L1PricingState().L1BaseFeeEstimateWei() + l1GasPrice, err := c.State.L1PricingState().PricePerUnit() if err != nil { return nil, nil, nil, err } l2GasPrice := evm.Context.BaseFee - ratio, err := c.State.L1PricingState().AggregatorCompressionRatio(aggregator) - if err != nil { - return nil, nil, nil, err - } // aggregators compress calldata, so we must estimate accordingly weiForL1Calldata := arbmath.BigMulByUint(l1GasPrice, params.TxDataNonZeroGasEIP2028) - compressedCharge := arbmath.BigMulByBips(weiForL1Calldata, ratio) - gasForL1Calldata := arbmath.BigDiv(compressedCharge, l2GasPrice) + gasForL1Calldata := arbmath.BigDiv(weiForL1Calldata, l2GasPrice) perL2Tx := big.NewInt(l1pricing.TxFixedCost) return perL2Tx, gasForL1Calldata, storageArbGas, nil @@ -90,14 +71,7 @@ func (con ArbGasInfo) GetPricesInArbGasWithAggregator(c ctx, evm mech, aggregato // Get prices in ArbGas when using the caller's preferred aggregator func (con ArbGasInfo) GetPricesInArbGas(c ctx, evm mech) (huge, huge, huge, error) { - maybeAggregator, err := c.State.L1PricingState().ReimbursableAggregatorForSender(c.caller) - if err != nil { - return nil, nil, nil, err - } - if maybeAggregator == nil { - return con.GetPricesInArbGasWithAggregator(c, evm, common.Address{}) - } - return con.GetPricesInArbGasWithAggregator(c, evm, *maybeAggregator) + return con.GetPricesInArbGasWithAggregator(c, evm, addr{}) } // Get the rollup's speed limit, pool size, and tx gas limit @@ -115,17 +89,21 @@ func (con ArbGasInfo) GetMinimumGasPrice(c ctx, evm mech) (huge, error) { // Get the current estimate of the L1 basefee func (con ArbGasInfo) GetL1BaseFeeEstimate(c ctx, evm mech) (huge, error) { - return c.State.L1PricingState().L1BaseFeeEstimateWei() + return c.State.L1PricingState().PricePerUnit() } // Get how slowly ArbOS updates its estimate of the L1 basefee func (con ArbGasInfo) GetL1BaseFeeEstimateInertia(c ctx, evm mech) (uint64, error) { - return c.State.L1PricingState().L1BaseFeeEstimateInertia() + return c.State.L1PricingState().Inertia() } -// Deprecated -- Same as getL1BaseFeeEstimate() +// Get the current estimate of the L1 basefee func (con ArbGasInfo) GetL1GasPriceEstimate(c ctx, evm mech) (huge, error) { - return con.GetL1BaseFeeEstimate(c, evm) + ppu, err := c.State.L1PricingState().PricePerUnit() + if err != nil { + return nil, err + } + return arbmath.BigMulByUint(ppu, params.TxDataNonZeroGasEIP2028), nil } // Get the fee paid to the aggregator for posting this tx diff --git a/precompiles/ArbOwner.go b/precompiles/ArbOwner.go index 74fe94b69c..230130520a 100644 --- a/precompiles/ArbOwner.go +++ b/precompiles/ArbOwner.go @@ -19,6 +19,10 @@ type ArbOwner struct { OwnerActsGasCost func(bytes4, addr, []byte) (uint64, error) } +var ( + ErrOutOfBounds = errors.New("value out of bounds") +) + // Add account as a chain owner func (con ArbOwner) AddChainOwner(c ctx, evm mech, newOwner addr) error { return c.State.ChainOwners().Add(newOwner) @@ -40,17 +44,12 @@ func (con ArbOwner) IsChainOwner(c ctx, evm mech, addr addr) (bool, error) { // Retrieves the list of chain owners func (con ArbOwner) GetAllChainOwners(c ctx, evm mech) ([]common.Address, error) { - return c.State.ChainOwners().AllMembers() -} - -// Sets the L1 basefee estimate directly, bypassing the autoregression -func (con ArbOwner) SetL1BaseFeeEstimate(c ctx, evm mech, priceInWei huge) error { - return c.State.L1PricingState().SetL1BaseFeeEstimateWei(priceInWei) + return c.State.ChainOwners().AllMembers(65536) } // Set how slowly ArbOS updates its estimate of the L1 basefee func (con ArbOwner) SetL1BaseFeeEstimateInertia(c ctx, evm mech, inertia uint64) error { - return c.State.L1PricingState().SetL1BaseFeeEstimateInertia(inertia) + return c.State.L1PricingState().SetInertia(inertia) } // Sets the L2 gas price directly, bypassing the pool calculus @@ -97,3 +96,19 @@ func (con ArbOwner) SetNetworkFeeAccount(c ctx, evm mech, newNetworkFeeAccount a func (con ArbOwner) ScheduleArbOSUpgrade(c ctx, evm mech, newVersion uint64, timestamp uint64) error { return c.State.ScheduleArbOSUpgrade(newVersion, timestamp) } + +func (con ArbOwner) SetL1PricingEquilibrationUnits(c ctx, evm mech, equilibrationUnits huge) error { + return c.State.L1PricingState().SetEquilibrationUnits(equilibrationUnits) +} + +func (con ArbOwner) SetL1PricingInertia(c ctx, evm mech, inertia uint64) error { + return c.State.L1PricingState().SetInertia(inertia) +} + +func (con ArbOwner) SetL1PricingRewardRecipient(c ctx, evm mech, recipient addr) error { + return c.State.L1PricingState().SetPayRewardsTo(recipient) +} + +func (con ArbOwner) SetL1PricingRewardRate(c ctx, evm mech, weiPerUnit uint64) error { + return c.State.L1PricingState().SetPerUnitReward(weiPerUnit) +} diff --git a/precompiles/ArbOwnerPublic.go b/precompiles/ArbOwnerPublic.go index 6d1a4fa62a..a11e07c4de 100644 --- a/precompiles/ArbOwnerPublic.go +++ b/precompiles/ArbOwnerPublic.go @@ -16,7 +16,7 @@ type ArbOwnerPublic struct { // Retrieves the list of chain owners func (con ArbOwnerPublic) GetAllChainOwners(c ctx, evm mech) ([]common.Address, error) { - return c.State.ChainOwners().AllMembers() + return c.State.ChainOwners().AllMembers(65536) } // See if the user is a chain owner diff --git a/precompiles/ArbosActs.go b/precompiles/ArbosActs.go index edb33496a2..4cd5baf7f9 100644 --- a/precompiles/ArbosActs.go +++ b/precompiles/ArbosActs.go @@ -11,6 +11,10 @@ type ArbosActs struct { CallerNotArbOSError func() error } -func (con ArbosActs) StartBlock(c ctx, evm mech, l1BaseFee huge, l1BlockNumber, timeLastBlock uint64) error { +func (con ArbosActs) StartBlock(c ctx, evm mech, l1BaseFee huge, l1BlockNumber, l2BlockNumber, timeLastBlock uint64) error { + return con.CallerNotArbOSError() +} + +func (con ArbosActs) BatchPostingReport(c ctx, evm mech, batchTimestamp huge, batchPosterAddress addr, batchNumber uint64, batchDataGas uint64, l1BaseFeeWei huge) error { return con.CallerNotArbOSError() } diff --git a/precompiles/precompile.go b/precompiles/precompile.go index 55855466d5..c13a3fc821 100644 --- a/precompiles/precompile.go +++ b/precompiles/precompile.go @@ -58,12 +58,14 @@ const ( ) type Precompile struct { - methods map[[4]byte]PrecompileMethod - events map[string]PrecompileEvent - errors map[string]PrecompileError - implementer reflect.Value - address common.Address - arbosVersion uint64 + methods map[[4]byte]PrecompileMethod + methodsByName map[string]PrecompileMethod + events map[string]PrecompileEvent + errors map[string]PrecompileError + name string + implementer reflect.Value + address common.Address + arbosVersion uint64 } type PrecompileMethod struct { @@ -154,6 +156,7 @@ func MakePrecompile(metadata *bind.MetaData, implementer interface{}) (addr, Pre } methods := make(map[[4]byte]PrecompileMethod) + methodsByName := make(map[string]PrecompileMethod) events := make(map[string]PrecompileEvent) errors := make(map[string]PrecompileError) @@ -218,13 +221,15 @@ func MakePrecompile(metadata *bind.MetaData, implementer interface{}) (addr, Pre ) } - methods[id] = PrecompileMethod{ + method := PrecompileMethod{ name, method, purity, handler, 0, } + methods[id] = method + methodsByName[name] = method } // provide the implementer mechanisms to emit logs for the solidity events @@ -487,8 +492,10 @@ func MakePrecompile(metadata *bind.MetaData, implementer interface{}) (addr, Pre return address, Precompile{ methods, + methodsByName, events, errors, + contract, reflect.ValueOf(implementer), address, 0, @@ -518,7 +525,6 @@ func Precompiles() map[addr]ArbosPrecompile { insert(MakePrecompile(templates.ArbGasInfoMetaData, &ArbGasInfo{Address: hex("6c")})) insert(MakePrecompile(templates.ArbAggregatorMetaData, &ArbAggregator{Address: hex("6d")})) insert(MakePrecompile(templates.ArbStatisticsMetaData, &ArbStatistics{Address: hex("6f")})) - insert(MakePrecompile(templates.ArbosActsMetaData, &ArbosActs{Address: types.ArbosAddress})) eventCtx := func(gasLimit uint64, err error) *Context { if err != nil { @@ -559,6 +565,10 @@ func Precompiles() map[addr]ArbosPrecompile { insert(ownerOnly(ArbOwnerImpl.Address, ArbOwner, emitOwnerActs)) insert(debugOnly(MakePrecompile(templates.ArbDebugMetaData, &ArbDebug{Address: hex("ff")}))) + ArbosActs := insert(MakePrecompile(templates.ArbosActsMetaData, &ArbosActs{Address: types.ArbosAddress})) + arbos.InternalTxStartBlockMethodID = ArbosActs.GetMethodID("StartBlock") + arbos.InternalTxBatchPostingReportMethodID = ArbosActs.GetMethodID("BatchPostingReport") + return contracts } @@ -567,6 +577,14 @@ func (p Precompile) SwapImpl(impl interface{}) Precompile { return p } +func (p Precompile) GetMethodID(name string) bytes4 { + method, ok := p.methodsByName[name] + if !ok { + panic(fmt.Sprintf("Precompile %v does not have a method with the name %v", p.name, name)) + } + return *(*bytes4)(method.template.ID) +} + // call a precompile in typed form, deserializing its inputs and serializing its outputs func (p Precompile) Call( input []byte, diff --git a/system_tests/block_validator_test.go b/system_tests/block_validator_test.go index 1ea14ae286..b535f58d3a 100644 --- a/system_tests/block_validator_test.go +++ b/system_tests/block_validator_test.go @@ -92,11 +92,25 @@ func testBlockValidatorSimple(t *testing.T, dasModeString string, expensiveTx bo Fail(t, "Unexpected balance:", l2balance) } - lastBlockHeader, err := l2clientB.HeaderByNumber(ctx, nil) + lastBlock, err := l2clientB.BlockByNumber(ctx, nil) Require(t, err) + for { + usefulBlock := false + for _, tx := range lastBlock.Transactions() { + if tx.Type() != types.ArbitrumInternalTxType { + usefulBlock = true + break + } + } + if usefulBlock { + break + } + lastBlock, err = l2clientB.BlockByHash(ctx, lastBlock.ParentHash()) + Require(t, err) + } testDeadLine, _ := t.Deadline() nodeA.StopAndWait() - if !nodeB.BlockValidator.WaitForBlock(lastBlockHeader.Number.Uint64(), time.Until(testDeadLine)-time.Second*10) { + if !nodeB.BlockValidator.WaitForBlock(lastBlock.NumberU64(), time.Until(testDeadLine)-time.Second*10) { Fail(t, "did not validate all blocks") } nodeB.StopAndWait() diff --git a/system_tests/common_test.go b/system_tests/common_test.go index 1ec7c14076..c2a360052d 100644 --- a/system_tests/common_test.go +++ b/system_tests/common_test.go @@ -212,6 +212,8 @@ func CreateTestNodeOnL1WithConfig(t *testing.T, ctx context.Context, isSequencer if !isSequencer { nodeConfig.BatchPoster.Enable = false + nodeConfig.Sequencer.Enable = false + nodeConfig.DelayedSequencer.Enable = false } node, err := arbnode.CreateNode(ctx, l2stack, l2chainDb, nodeConfig, l2blockchain, l1client, addresses, sequencerTxOptsPtr, nil) diff --git a/system_tests/fees_test.go b/system_tests/fees_test.go index e292ecf989..a410e298c8 100644 --- a/system_tests/fees_test.go +++ b/system_tests/fees_test.go @@ -7,151 +7,164 @@ import ( "context" "math/big" "testing" + "time" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" "github.com/offchainlabs/nitro/arbcompress" + "github.com/offchainlabs/nitro/arbnode" "github.com/offchainlabs/nitro/arbos/l1pricing" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/colors" - "github.com/offchainlabs/nitro/util/testhelpers" ) -func TestTips(t *testing.T) { +func TestSequencerFeePaid(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - l2info, _, l2client, l1info, _, l1client, stack := CreateTestNodeOnL1(t, ctx, true) + l2info, _, l2client, _, _, _, stack := CreateTestNodeOnL1(t, ctx, true) defer stack.Close() - auth := l2info.GetDefaultTransactOpts("Owner", ctx) callOpts := l2info.GetDefaultCallOpts("Owner", ctx) - aggregator := testhelpers.RandomAddress() // get the network fee account arbOwnerPublic, err := precompilesgen.NewArbOwnerPublic(common.HexToAddress("0x6b"), l2client) Require(t, err, "could not deploy ArbOwner contract") + arbGasInfo, err := precompilesgen.NewArbGasInfo(common.HexToAddress("0x6c"), l2client) + Require(t, err, "could not deploy ArbOwner contract") networkFeeAccount, err := arbOwnerPublic.GetNetworkFeeAccount(callOpts) Require(t, err, "could not get the network fee account") - // set a preferred aggregator who won't be the one to post the tx - arbAggregator, err := precompilesgen.NewArbAggregator(common.HexToAddress("0x6d"), l2client) - Require(t, err, "could not deploy ArbAggregator contract") - tx, err := arbAggregator.SetPreferredAggregator(&auth, aggregator) - Require(t, err, "could not set L2 gas price") - _, err = EnsureTxSucceeded(ctx, l2client, tx) + l1Estimate, err := arbGasInfo.GetL1BaseFeeEstimate(callOpts) Require(t, err) - - basefee := GetBaseFee(t, l2client, ctx) - auth.GasFeeCap = arbmath.BigMulByUfrac(basefee, 5, 4) // add room for a 20% tip - auth.GasTipCap = arbmath.BigMulByUfrac(basefee, 1, 4) // add a 20% tip - networkBefore := GetBalance(t, ctx, l2client, networkFeeAccount) - // use L1 to post a message since the sequencer won't do it - nosend := auth - nosend.NoSend = true - tx, err = arbAggregator.SetPreferredAggregator(&nosend, aggregator) - Require(t, err) - receipt := SendSignedTxViaL1(t, ctx, l1info, l1client, l2client, tx) - if receipt.Status != types.ReceiptStatusSuccessful { - Fail(t, "failed to prefer the sequencer") - } + l2info.GasPrice = GetBaseFee(t, l2client, ctx) + tx, receipt := TransferBalance(t, "Faucet", "Faucet", big.NewInt(0), l2info, l2client, ctx) + txSize := compressedTxSize(t, tx) networkAfter := GetBalance(t, ctx, l2client, networkFeeAccount) - colors.PrintMint("network: ", networkFeeAccount, networkBefore, networkAfter) - colors.PrintBlue("pricing: ", l2info.GasPrice, auth.GasFeeCap, auth.GasTipCap) - colors.PrintBlue("payment: ", tx.GasPrice(), tx.GasFeeCap(), tx.GasTipCap()) - - if !arbmath.BigEquals(tx.GasPrice(), auth.GasFeeCap) { - Fail(t, "user did not pay the tip") - } + l1Charge := arbmath.BigMulByUint(l2info.GasPrice, receipt.GasUsedForL1) - tip := arbmath.BigMulByUint(arbmath.BigSub(tx.GasPrice(), basefee), receipt.GasUsed) - full := arbmath.BigMulByUint(basefee, receipt.GasUsed) // was gasprice before upgrade networkRevenue := arbmath.BigSub(networkAfter, networkBefore) - colors.PrintMint("price: ", tip, full, networkRevenue) - colors.PrintRed("used: ", receipt.GasUsed, basefee) - - if !arbmath.BigEquals(full, networkRevenue) { - Fail(t, "the network didn't receive the funds") + gasUsedForL2 := receipt.GasUsed - receipt.GasUsedForL1 + if !arbmath.BigEquals(networkRevenue, arbmath.BigMulByUint(tx.GasPrice(), gasUsedForL2)) { + Fail(t, "network didn't receive expected payment") } -} - -// Test that the sequencer won't subvert a user's aggregation preferences -func TestSequencerWontPostWhenNotPreferred(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - l2info, _, client := CreateTestL2(t, ctx) - auth := l2info.GetDefaultTransactOpts("Owner", ctx) + l1GasBought := arbmath.BigDiv(l1Charge, l1Estimate).Uint64() + l1GasActual := txSize * params.TxDataNonZeroGasEIP2028 - // prefer a 3rd party aggregator - arbAggregator, err := precompilesgen.NewArbAggregator(common.HexToAddress("0x6d"), client) - Require(t, err, "could not deploy ArbAggregator contract") - tx, err := arbAggregator.SetPreferredAggregator(&auth, testhelpers.RandomAddress()) - Require(t, err, "could not set L2 gas price") - _, err = EnsureTxSucceeded(ctx, client, tx) - Require(t, err) + colors.PrintBlue("bytes ", l1GasBought/params.TxDataNonZeroGasEIP2028, txSize) - // get the network fee account - _, err = arbAggregator.SetPreferredAggregator(&auth, testhelpers.RandomAddress()) - colors.PrintBlue("expecting error: ", err) - if err == nil { - Fail(t, "the sequencer should have rejected this tx") + if l1GasBought != l1GasActual { + Fail(t, "the sequencer's future revenue does not match its costs", l1GasBought, l1GasActual) } } -func TestSequencerFeePaid(t *testing.T) { +func TestSequencerPriceAdjusts(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - l2info, _, l2client, _, _, _, stack := CreateTestNodeOnL1(t, ctx, true) - defer stack.Close() - callOpts := l2info.GetDefaultCallOpts("Owner", ctx) + chainConfig := params.ArbitrumDevTestChainConfig() + conf := arbnode.ConfigDefaultL1Test() + conf.DelayedSequencer.FinalizeDistance = 1 - // get the network fee account - arbOwnerPublic, err := precompilesgen.NewArbOwnerPublic(common.HexToAddress("0x6b"), l2client) - Require(t, err, "could not deploy ArbOwner contract") - arbGasInfo, err := precompilesgen.NewArbGasInfo(common.HexToAddress("0x6c"), l2client) - Require(t, err, "could not deploy ArbOwner contract") - networkFeeAccount, err := arbOwnerPublic.GetNetworkFeeAccount(callOpts) - Require(t, err, "could not get the network fee account") + l2info, node, l2client, _, _, l1client, stack := CreateTestNodeOnL1WithConfig(t, ctx, true, conf, chainConfig) + defer stack.Close() - l1Estimate, err := arbGasInfo.GetL1GasPriceEstimate(callOpts) + arbGasInfo, err := precompilesgen.NewArbGasInfo(common.HexToAddress("0x6c"), l2client) + Require(t, err) + lastEstimate, err := arbGasInfo.GetL1BaseFeeEstimate(&bind.CallOpts{Context: ctx}) + Require(t, err) + lastBatchCount, err := node.InboxTracker.GetBatchCount() + Require(t, err) + l1Header, err := l1client.HeaderByNumber(ctx, nil) Require(t, err) - networkBefore := GetBalance(t, ctx, l2client, networkFeeAccount) - seqBefore := GetBalance(t, ctx, l2client, l1pricing.SequencerAddress) - - l2info.GasPrice = GetBaseFee(t, l2client, ctx) - tx, receipt := TransferBalance(t, "Faucet", "Faucet", big.NewInt(0), l2info, l2client, ctx) - - networkAfter := GetBalance(t, ctx, l2client, networkFeeAccount) - seqAfter := GetBalance(t, ctx, l2client, l1pricing.SequencerAddress) - networkRevenue := arbmath.BigSub(networkAfter, networkBefore) - seqRevenue := arbmath.BigSub(seqAfter, seqBefore) + sequencerBalanceBefore := GetBalance(t, ctx, l2client, l1pricing.BatchPosterAddress) + timesPriceAdjusted := 0 + + colors.PrintBlue("Initial values") + colors.PrintBlue(" L1 base fee ", l1Header.BaseFee) + colors.PrintBlue(" L1 estimate ", lastEstimate) + + for i := 0; i < 128; i++ { + tx, receipt := TransferBalance(t, "Owner", "Owner", common.Big1, l2info, l2client, ctx) + header, err := l2client.HeaderByHash(ctx, receipt.BlockHash) + Require(t, err) + + units := compressedTxSize(t, tx) * params.TxDataNonZeroGasEIP2028 + estimatedL1FeePerUnit := arbmath.BigDivByUint(arbmath.BigMulByUint(header.BaseFee, receipt.GasUsedForL1), units) + + if !arbmath.BigEquals(lastEstimate, estimatedL1FeePerUnit) { + l1Header, err = l1client.HeaderByNumber(ctx, nil) + Require(t, err) + + callOpts := &bind.CallOpts{Context: ctx, BlockNumber: receipt.BlockNumber} + actualL1FeePerUnit, err := arbGasInfo.GetL1BaseFeeEstimate(callOpts) + Require(t, err) + + colors.PrintGrey("ArbOS updated its L1 estimate") + colors.PrintGrey(" L1 base fee ", l1Header.BaseFee) + colors.PrintGrey(" L1 estimate ", lastEstimate, " ➤ ", estimatedL1FeePerUnit, " = ", actualL1FeePerUnit) + + oldDiff := arbmath.BigAbs(arbmath.BigSub(lastEstimate, l1Header.BaseFee)) + newDiff := arbmath.BigAbs(arbmath.BigSub(actualL1FeePerUnit, l1Header.BaseFee)) + + if arbmath.BigGreaterThan(newDiff, oldDiff) { + Fail(t, "L1 gas price estimate should tend toward the basefee") + } + diff := arbmath.BigAbs(arbmath.BigSub(actualL1FeePerUnit, estimatedL1FeePerUnit)) + maxDiffToAllow := arbmath.BigDivByUint(actualL1FeePerUnit, 100) + if arbmath.BigLessThan(maxDiffToAllow, diff) { // verify that estimates is within 1% of actual + Fail(t, "New L1 estimate differs too much from receipt") + } + if arbmath.BigEquals(actualL1FeePerUnit, common.Big0) { + Fail(t, "Estimate is zero", i) + } + lastEstimate = actualL1FeePerUnit + timesPriceAdjusted++ + } + + if i%16 == 0 { + // see that the inbox advances + + for j := 16; j > 0; j-- { + newBatchCount, err := node.InboxTracker.GetBatchCount() + Require(t, err) + if newBatchCount > lastBatchCount { + colors.PrintGrey("posted new batch ", newBatchCount) + lastBatchCount = newBatchCount + break + } + if j == 1 { + Fail(t, "batch count didn't update in time") + } + time.Sleep(time.Millisecond * 100) + } + } + } - gasUsedForL2 := receipt.GasUsed - receipt.GasUsedForL1 + sequencerBalanceAfter := GetBalance(t, ctx, l2client, l1pricing.BatchPosterAddress) + colors.PrintMint("sequencer balance ", sequencerBalanceBefore, " ➤ ", sequencerBalanceAfter) + colors.PrintMint("price changes ", timesPriceAdjusted) - if !arbmath.BigEquals(seqRevenue, arbmath.BigMulByUint(tx.GasPrice(), receipt.GasUsedForL1)) { - Fail(t, "sequencer didn't receive expected payment") + if timesPriceAdjusted == 0 { + Fail(t, "L1 gas price estimate never adjusted") } - if !arbmath.BigEquals(networkRevenue, arbmath.BigMulByUint(tx.GasPrice(), gasUsedForL2)) { - Fail(t, "network didn't receive expected payment") + if !arbmath.BigGreaterThan(sequencerBalanceAfter, sequencerBalanceBefore) { + Fail(t, "sequencer didn't get paid") } +} - paidBytes := arbmath.BigDiv(seqRevenue, l1Estimate).Uint64() / params.TxDataNonZeroGasEIP2028 - +func compressedTxSize(t *testing.T, tx *types.Transaction) uint64 { txBin, err := tx.MarshalBinary() Require(t, err) compressed, err := arbcompress.CompressFast(txBin) Require(t, err) - - if uint64(len(compressed)) != paidBytes { - t.Fatal("unexpected number of bytes paid for") - } - + return uint64(len(compressed)) } diff --git a/system_tests/replay_fuzz/replay_fuzz.go b/system_tests/replay_fuzz/replay_fuzz.go index 1dc21bae1c..fa867c09f6 100644 --- a/system_tests/replay_fuzz/replay_fuzz.go +++ b/system_tests/replay_fuzz/replay_fuzz.go @@ -29,6 +29,7 @@ func BuildBlock( chainContext core.ChainContext, chainConfig *params.ChainConfig, inbox arbstate.InboxBackend, + seqBatch []byte, ) (*types.Block, error) { var delayedMessagesRead uint64 if lastBlockHeader != nil { @@ -45,10 +46,13 @@ func BuildBlock( delayedMessagesRead = inboxMultiplexer.DelayedMessagesRead() l1Message := message.Message - block, _ := arbos.ProduceBlock( - l1Message, delayedMessagesRead, lastBlockHeader, statedb, chainContext, chainConfig, + batchFetcher := func(uint64) ([]byte, error) { + return seqBatch, nil + } + block, _, err := arbos.ProduceBlock( + l1Message, delayedMessagesRead, lastBlockHeader, statedb, chainContext, chainConfig, batchFetcher, ) - return block, nil + return block, err } // A simple mock inbox multiplexer backend @@ -142,7 +146,7 @@ func Fuzz(input []byte) int { positionWithinMessage: 0, delayedMessages: delayedMessages, } - _, err = BuildBlock(statedb, genesis, noopChainContext{}, params.ArbitrumOneChainConfig(), inbox) + _, err = BuildBlock(statedb, genesis, noopChainContext{}, params.ArbitrumOneChainConfig(), inbox, seqBatch) if err != nil { // With the fixed header it shouldn't be possible to read a delayed message, // and no other type of error should be possible. diff --git a/system_tests/retryable_test.go b/system_tests/retryable_test.go index f15180cf03..f5d430368b 100644 --- a/system_tests/retryable_test.go +++ b/system_tests/retryable_test.go @@ -53,7 +53,7 @@ func retryableSetup(t *testing.T) ( if len(messages) != 1 { Fail(t, "expected 1 message from retryable submission, found", len(messages)) } - txs, err := messages[0].Message.ParseL2Transactions(params.ArbitrumDevTestChainConfig().ChainID) + txs, err := messages[0].Message.ParseL2Transactions(params.ArbitrumDevTestChainConfig().ChainID, nil) Require(t, err) if len(txs) != 1 { Fail(t, "expected 1 tx from retryable submission, found", len(txs)) diff --git a/system_tests/seqcompensation_test.go b/system_tests/seqcompensation_test.go index 56b70a9cb1..51833c1ef7 100644 --- a/system_tests/seqcompensation_test.go +++ b/system_tests/seqcompensation_test.go @@ -13,7 +13,7 @@ import ( "github.com/offchainlabs/nitro/arbos/l1pricing" ) -// Sequencer address gets something for posting batches +// L1 Pricer pool address gets something when the sequencer posts batches func TestSequencerCompensation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -50,15 +50,9 @@ func TestSequencerCompensation(t *testing.T) { Fail(t, "Unexpected balance:", l2balance) } - initialSeqBalance, err := l2clientB.BalanceAt(ctx, l1pricing.SequencerAddress, big.NewInt(0)) + initialSeqBalance, err := l2clientB.BalanceAt(ctx, l1pricing.BatchPosterAddress, big.NewInt(0)) Require(t, err) if initialSeqBalance.Sign() != 0 { Fail(t, "Unexpected initial sequencer balance:", initialSeqBalance) } - finalSeqBalance, err := l2clientB.BalanceAt(ctx, l1pricing.SequencerAddress, nil) - Require(t, err) - if finalSeqBalance.Sign() <= 0 { - Fail(t, "Unexpected final sequencer balance:", finalSeqBalance) - } - } diff --git a/util/arbmath/math.go b/util/arbmath/math.go index ed1a2e1cdd..e54c3aa64f 100644 --- a/util/arbmath/math.go +++ b/util/arbmath/math.go @@ -28,6 +28,14 @@ func MinInt(value, ceiling int64) int64 { return value } +// the minimum of two uints +func MinUint(value, ceiling uint64) uint64 { + if value > ceiling { + return ceiling + } + return value +} + // the maximum of two ints func MaxInt(value, floor int64) int64 { if value < floor { @@ -110,6 +118,16 @@ func BigDiv(dividend *big.Int, divisor *big.Int) *big.Int { return new(big.Int).Div(dividend, divisor) } +// absolute value of a huge +func BigAbs(value *big.Int) *big.Int { + return new(big.Int).Abs(value) +} + +// add a uint to a huge +func BigAddByUint(augend *big.Int, addend uint64) *big.Int { + return new(big.Int).Add(augend, UintToBig(addend)) +} + // multiply a huge by a rational func BigMulByFrac(value *big.Int, numerator, denominator int64) *big.Int { value = new(big.Int).Set(value) @@ -233,6 +251,16 @@ func SaturatingUCast(value int64) uint64 { return uint64(value) } +func SaturatingCastToUint(value *big.Int) uint64 { + if value.Sign() < 0 { + return 0 + } + if !value.IsUint64() { + return math.MaxUint64 + } + return value.Uint64() +} + // the number of eth-words needed to store n bytes func WordsForBytes(nbytes uint64) uint64 { return (nbytes + 31) / 32 diff --git a/validator/block_validator.go b/validator/block_validator.go index fc12261c75..74cd7b381b 100644 --- a/validator/block_validator.go +++ b/validator/block_validator.go @@ -204,13 +204,13 @@ func (v *BlockValidator) readLastBlockValidatedDbInfo() error { return nil } -func (v *BlockValidator) prepareBlock(header *types.Header, prevHeader *types.Header, msg arbstate.MessageWithMetadata, validationStatus *validationStatus) { - preimages, hasDelayedMessage, delayedMsgToRead, err := BlockDataForValidation(v.blockchain, header, prevHeader, msg, v.config.StorePreimages) +func (v *BlockValidator) prepareBlock(ctx context.Context, header *types.Header, prevHeader *types.Header, msg arbstate.MessageWithMetadata, validationStatus *validationStatus) { + preimages, readBatchInfo, hasDelayedMessage, delayedMsgToRead, err := BlockDataForValidation(ctx, v.blockchain, v.inboxReader, header, prevHeader, msg, v.config.StorePreimages) if err != nil { log.Error("failed to set up validation", "err", err, "header", header, "prevHeader", prevHeader) return } - validationEntry, err := newValidationEntry(prevHeader, header, hasDelayedMessage, delayedMsgToRead, preimages) + validationEntry, err := newValidationEntry(prevHeader, header, hasDelayedMessage, delayedMsgToRead, preimages, readBatchInfo) if err != nil { log.Error("failed to create validation entry", "err", err, "header", header, "prevHeader", prevHeader) return @@ -255,7 +255,7 @@ func (v *BlockValidator) NewBlock(block *types.Block, prevHeader *types.Header, if v.nextValidationEntryBlock <= blockNum { v.nextValidationEntryBlock = blockNum + 1 } - v.LaunchUntrackedThread(func() { v.prepareBlock(block.Header(), prevHeader, msg, status) }) + v.LaunchUntrackedThread(func() { v.prepareBlock(context.Background(), block.Header(), prevHeader, msg, status) }) } var launchTime = time.Now().Format("2006_01_02__15_04") @@ -398,10 +398,14 @@ func (v *BlockValidator) validate(ctx context.Context, validationStatus *validat default: } }() + entry.BatchInfo = append(entry.BatchInfo, BatchInfo{ + Number: entry.StartPosition.BatchNumber, + Data: seqMsg, + }) log.Info("starting validation for block", "blockNr", entry.BlockNumber) for _, moduleRoot := range validationStatus.ModuleRoots { before := time.Now() - gsEnd, delayedMsg, err := v.executeBlock(ctx, entry, seqMsg, moduleRoot) + gsEnd, delayedMsg, err := v.executeBlock(ctx, entry, moduleRoot) duration := time.Since(before) if err != nil { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { diff --git a/validator/challenge_manager.go b/validator/challenge_manager.go index a43c67ec17..02fa735b2f 100644 --- a/validator/challenge_manager.go +++ b/validator/challenge_manager.go @@ -7,9 +7,10 @@ import ( "context" "encoding/binary" "fmt" - "github.com/offchainlabs/nitro/arbstate" "math/big" + "github.com/offchainlabs/nitro/arbstate" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" @@ -416,12 +417,14 @@ func (m *ChallengeManager) createInitialMachine(ctx context.Context, blockNum in if err != nil { return err } + var batchInfo []BatchInfo if tooFar { // Just record the part of block creation before the message is read - _, preimages, err := RecordBlockCreation(m.blockchain, blockHeader, nil) + _, preimages, readBatchInfo, err := RecordBlockCreation(ctx, m.blockchain, m.inboxReader, blockHeader, nil, true) if err != nil { return err } + batchInfo = readBatchInfo err = SetMachinePreimageResolver(ctx, machine, preimages, nil, m.blockchain, m.das) if err != nil { return err @@ -440,7 +443,7 @@ func (m *ChallengeManager) createInitialMachine(ctx context.Context, blockNum in if nextHeader == nil { return fmt.Errorf("next block header %v after challenge point unknown", blockNum+1) } - preimages, hasDelayedMsg, delayedMsgNr, err := BlockDataForValidation(m.blockchain, nextHeader, blockHeader, message, false) + preimages, readBatchInfo, hasDelayedMsg, delayedMsgNr, err := BlockDataForValidation(ctx, m.blockchain, m.inboxReader, nextHeader, blockHeader, message, false) if err != nil { return err } @@ -448,7 +451,12 @@ func (m *ChallengeManager) createInitialMachine(ctx context.Context, blockNum in if err != nil { return err } - err = SetMachinePreimageResolver(ctx, machine, preimages, batchBytes, m.blockchain, m.das) + readBatchInfo = append(readBatchInfo, BatchInfo{ + Number: startGlobalState.Batch, + Data: batchBytes, + }) + batchInfo = readBatchInfo + err = SetMachinePreimageResolver(ctx, machine, preimages, batchInfo, m.blockchain, m.das) if err != nil { return err } @@ -462,7 +470,9 @@ func (m *ChallengeManager) createInitialMachine(ctx context.Context, blockNum in return err } } - err = machine.AddSequencerInboxMessage(startGlobalState.Batch, batchBytes) + } + for _, batch := range batchInfo { + err = machine.AddSequencerInboxMessage(batch.Number, batch.Data) if err != nil { return err } diff --git a/validator/stateless_block_validator.go b/validator/stateless_block_validator.go index c7346df44a..08a8a59546 100644 --- a/validator/stateless_block_validator.go +++ b/validator/stateless_block_validator.go @@ -6,12 +6,14 @@ package validator import ( "context" "fmt" + "github.com/offchainlabs/nitro/arbutil" "github.com/ethereum/go-ethereum/arbitrum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" @@ -136,6 +138,7 @@ type validationEntry struct { StartPosition GlobalStatePosition EndPosition GlobalStatePosition Preimages map[common.Hash][]byte + BatchInfo []BatchInfo } func (v *validationEntry) start() GoGlobalState { @@ -164,6 +167,7 @@ func newValidationEntry( hasDelayed bool, delayedMsgNr uint64, preimages map[common.Hash][]byte, + batchInfo []BatchInfo, ) (*validationEntry, error) { extraInfo, err := types.DeserializeHeaderExtraInformation(header) if err != nil { @@ -183,6 +187,7 @@ func newValidationEntry( HasDelayedMsg: hasDelayed, DelayedMsgNr: delayedMsgNr, Preimages: preimages, + BatchInfo: batchInfo, }, nil } @@ -212,11 +217,32 @@ func NewStatelessBlockValidator( return validator, nil } +type BatchInfo struct { + Number uint64 + Data []byte +} + // If msg is nil, this will record block creation up to the point where message would be accessed (for a "too far" proof) -func RecordBlockCreation(blockchain *core.BlockChain, prevHeader *types.Header, msg *arbstate.MessageWithMetadata) (common.Hash, map[common.Hash][]byte, error) { - recordingdb, chaincontext, recordingKV, err := arbitrum.PrepareRecording(blockchain, prevHeader) - if err != nil { - return common.Hash{}, nil, err +func RecordBlockCreation(ctx context.Context, blockchain *core.BlockChain, inboxReader InboxReaderInterface, prevHeader *types.Header, msg *arbstate.MessageWithMetadata, producePreimages bool) (common.Hash, map[common.Hash][]byte, []BatchInfo, error) { + var recordingdb *state.StateDB + var chaincontext core.ChainContext + var recordingKV *arbitrum.RecordingKV + var err error + if producePreimages { + recordingdb, chaincontext, recordingKV, err = arbitrum.PrepareRecording(blockchain, prevHeader) + if err != nil { + return common.Hash{}, nil, nil, err + } + } else { + var prevRoot common.Hash + if prevHeader != nil { + prevRoot = prevHeader.Root + } + recordingdb, err = blockchain.StateAt(prevRoot) + if err != nil { + return common.Hash{}, nil, nil, err + } + chaincontext = blockchain } chainConfig := blockchain.Config() @@ -226,36 +252,57 @@ func RecordBlockCreation(blockchain *core.BlockChain, prevHeader *types.Header, if prevHeader != nil { initialArbosState, err := arbosState.OpenSystemArbosState(recordingdb, nil, true) if err != nil { - return common.Hash{}, nil, fmt.Errorf("error opening initial ArbOS state: %w", err) + return common.Hash{}, nil, nil, fmt.Errorf("error opening initial ArbOS state: %w", err) } chainId, err := initialArbosState.ChainId() if err != nil { - return common.Hash{}, nil, fmt.Errorf("error getting chain ID from initial ArbOS state: %w", err) + return common.Hash{}, nil, nil, fmt.Errorf("error getting chain ID from initial ArbOS state: %w", err) } if chainId.Cmp(chainConfig.ChainID) != 0 { - return common.Hash{}, nil, fmt.Errorf("unexpected chain ID %v in ArbOS state, expected %v", chainId, chainConfig.ChainID) + return common.Hash{}, nil, nil, fmt.Errorf("unexpected chain ID %v in ArbOS state, expected %v", chainId, chainConfig.ChainID) } } var blockHash common.Hash + var readBatchInfo []BatchInfo if msg != nil { - block, _ := arbos.ProduceBlock( + batchFetcher := func(batchNum uint64) ([]byte, error) { + data, err := inboxReader.GetSequencerMessageBytes(ctx, batchNum) + if err != nil { + return nil, err + } + readBatchInfo = append(readBatchInfo, BatchInfo{ + Number: batchNum, + Data: data, + }) + return data, nil + } + block, _, err := arbos.ProduceBlock( msg.Message, msg.DelayedMessagesRead, prevHeader, recordingdb, chaincontext, chainConfig, + batchFetcher, ) + if err != nil { + return common.Hash{}, nil, nil, err + } blockHash = block.Hash() } - preimages, err := arbitrum.PreimagesFromRecording(chaincontext, recordingKV) - - return blockHash, preimages, err + var preimages map[common.Hash][]byte + if recordingKV != nil { + preimages, err = arbitrum.PreimagesFromRecording(chaincontext, recordingKV) + if err != nil { + return common.Hash{}, nil, nil, err + } + } + return blockHash, preimages, readBatchInfo, err } -func BlockDataForValidation(blockchain *core.BlockChain, header, prevHeader *types.Header, msg arbstate.MessageWithMetadata, producePreimages bool) (preimages map[common.Hash][]byte, hasDelayedMessage bool, delayedMsgNr uint64, err error) { +func BlockDataForValidation(ctx context.Context, blockchain *core.BlockChain, inboxReader InboxReaderInterface, header, prevHeader *types.Header, msg arbstate.MessageWithMetadata, producePreimages bool) (preimages map[common.Hash][]byte, readBatchInfo []BatchInfo, hasDelayedMessage bool, delayedMsgNr uint64, err error) { var prevHash common.Hash if prevHeader != nil { prevHash = prevHeader.Hash() @@ -265,9 +312,9 @@ func BlockDataForValidation(blockchain *core.BlockChain, header, prevHeader *typ return } - if prevHeader != nil && producePreimages { + if prevHeader != nil { var blockhash common.Hash - blockhash, preimages, err = RecordBlockCreation(blockchain, prevHeader, &msg) + blockhash, preimages, readBatchInfo, err = RecordBlockCreation(ctx, blockchain, inboxReader, prevHeader, &msg, producePreimages) if err != nil { return } @@ -287,23 +334,25 @@ func BlockDataForValidation(blockchain *core.BlockChain, header, prevHeader *typ return } -func SetMachinePreimageResolver(ctx context.Context, mach *ArbitratorMachine, preimages map[common.Hash][]byte, seqMsg []byte, bc *core.BlockChain, das arbstate.DataAvailabilityReader) error { +func SetMachinePreimageResolver(ctx context.Context, mach *ArbitratorMachine, preimages map[common.Hash][]byte, batchInfo []BatchInfo, bc *core.BlockChain, das arbstate.DataAvailabilityReader) error { recordNewPreimages := true if preimages == nil { preimages = make(map[common.Hash][]byte) recordNewPreimages = false } - if arbstate.IsDASMessageHeaderByte(seqMsg[40]) { - if das == nil { - log.Error("No DAS configured, but sequencer message found with DAS header") - if bc.Config().ArbitrumChainParams.DataAvailabilityCommittee { - return errors.New("processing data availability chain without DAS configured") - } - } else { - _, err := arbstate.RecoverPayloadFromDasBatch(ctx, seqMsg, das, preimages) - if err != nil { - return err + for _, batch := range batchInfo { + if len(batch.Data) >= 41 && arbstate.IsDASMessageHeaderByte(batch.Data[40]) { + if das == nil { + log.Error("No DAS configured, but sequencer message found with DAS header") + if bc.Config().ArbitrumChainParams.DataAvailabilityCommittee { + return errors.New("processing data availability chain without DAS configured") + } + } else { + _, err := arbstate.RecoverPayloadFromDasBatch(ctx, batch.Data, das, preimages) + if err != nil { + return err + } } } } @@ -336,7 +385,7 @@ func SetMachinePreimageResolver(ctx context.Context, mach *ArbitratorMachine, pr }) } -func (v *StatelessBlockValidator) executeBlock(ctx context.Context, entry *validationEntry, seqMsg []byte, moduleRoot common.Hash) (GoGlobalState, []byte, error) { +func (v *StatelessBlockValidator) executeBlock(ctx context.Context, entry *validationEntry, moduleRoot common.Hash) (GoGlobalState, []byte, error) { start := entry.StartPosition gsStart := entry.start() @@ -345,7 +394,7 @@ func (v *StatelessBlockValidator) executeBlock(ctx context.Context, entry *valid return GoGlobalState{}, nil, fmt.Errorf("unabled to get WASM machine: %w", err) } mach := basemachine.Clone() - err = SetMachinePreimageResolver(ctx, mach, entry.Preimages, seqMsg, v.blockchain, v.daService) + err = SetMachinePreimageResolver(ctx, mach, entry.Preimages, entry.BatchInfo, v.blockchain, v.daService) if err != nil { return GoGlobalState{}, nil, err } @@ -354,10 +403,12 @@ func (v *StatelessBlockValidator) executeBlock(ctx context.Context, entry *valid log.Error("error while setting global state for proving", "err", err, "gsStart", gsStart) return GoGlobalState{}, nil, errors.New("error while setting global state for proving") } - err = mach.AddSequencerInboxMessage(start.BatchNumber, seqMsg) - if err != nil { - log.Error("error while trying to add sequencer msg for proving", "err", err, "seq", start.BatchNumber, "blockNr", entry.BlockNumber) - return GoGlobalState{}, nil, errors.New("error while trying to add sequencer msg for proving") + for _, batch := range entry.BatchInfo { + err = mach.AddSequencerInboxMessage(batch.Number, batch.Data) + if err != nil { + log.Error("error while trying to add sequencer msg for proving", "err", err, "seq", start.BatchNumber, "blockNr", entry.BlockNumber) + return GoGlobalState{}, nil, errors.New("error while trying to add sequencer msg for proving") + } } var delayedMsg []byte if entry.HasDelayedMsg { @@ -406,7 +457,7 @@ func (v *StatelessBlockValidator) ValidateBlock(ctx context.Context, header *typ if err != nil { return false, err } - preimages, hasDelayedMessage, delayedMsgToRead, err := BlockDataForValidation(v.blockchain, header, prevHeader, msg, false) + preimages, readBatchInfo, hasDelayedMessage, delayedMsgToRead, err := BlockDataForValidation(ctx, v.blockchain, v.inboxReader, header, prevHeader, msg, false) if err != nil { return false, fmt.Errorf("failed to get block data to validate: %w", err) } @@ -425,7 +476,7 @@ func (v *StatelessBlockValidator) ValidateBlock(ctx context.Context, header *typ return false, fmt.Errorf("failed calculating position for validation: %w", err) } - entry, err := newValidationEntry(prevHeader, header, hasDelayedMessage, delayedMsgToRead, preimages) + entry, err := newValidationEntry(prevHeader, header, hasDelayedMessage, delayedMsgToRead, preimages, readBatchInfo) if err != nil { return false, fmt.Errorf("failed to create validation entry %w", err) } @@ -436,8 +487,12 @@ func (v *StatelessBlockValidator) ValidateBlock(ctx context.Context, header *typ if err != nil { return false, err } + entry.BatchInfo = append(entry.BatchInfo, BatchInfo{ + Number: startPos.BatchNumber, + Data: seqMsg, + }) - gsEnd, _, err := v.executeBlock(ctx, entry, seqMsg, moduleRoot) + gsEnd, _, err := v.executeBlock(ctx, entry, moduleRoot) if err != nil { return false, err }