diff --git a/Makefile b/Makefile index c9f2314401c..325793d3716 100644 --- a/Makefile +++ b/Makefile @@ -56,6 +56,7 @@ color_reset = "\e[0;0m" done = "%bdone!%b\n" $(color_pink) $(color_reset) replay_wasm=$(output_latest)/replay.wasm +unified_replay_wasm=$(output_latest)/unified_replay.wasm arb_brotli_files = $(wildcard crates/brotli/src/*.* crates/brotli/src/*/*.* crates/brotli/*.toml crates/brotli/*.rs) .make/cbrotli-lib .make/cbrotli-wasm @@ -182,6 +183,7 @@ build-node-deps: $(go_source) build-prover-header build-prover-lib build-jit .ma .PHONY: test-go-deps test-go-deps: \ build-replay-env \ + build-unified-replay-env \ build-validation-server \ $(stylus_test_wasms) \ $(arbitrator_stylus_lib) \ @@ -206,6 +208,12 @@ build-validation-server: $(validation_server) .PHONY: build-replay-env build-replay-env: $(prover_bin) $(arbitrator_jit) $(arbitrator_wasm_libs) $(replay_wasm) $(output_latest)/machine.v2.wavm.br +.PHONY: build-unified-replay-env +build-unified-replay-env: $(unified_replay_wasm) $(output_latest)/unified_machine.wavm.br + +.PHONY: build-unified-wasm-bin +build-unified-wasm-bin: $(unified_replay_wasm) + .PHONY: build-wasm-libs build-wasm-libs: $(arbitrator_wasm_libs) @@ -369,6 +377,11 @@ $(replay_wasm): $(DEP_PREDICATE) $(go_source) .make/solgen GOOS=wasip1 GOARCH=wasm go build -o $@ ./cmd/replay/... ./scripts/remove_reference_types.sh $@ +$(unified_replay_wasm): $(DEP_PREDICATE) $(go_source) .make/solgen + mkdir -p `dirname $(unified_replay_wasm)` + GOOS=wasip1 GOARCH=wasm go build -o $@ ./cmd/unified-replay/... + ./scripts/remove_reference_types.sh $@ + $(prover_bin): $(DEP_PREDICATE) $(rust_prover_files) mkdir -p `dirname $(prover_bin)` cargo build --release --bin prover ${CARGOFLAGS} @@ -487,6 +500,10 @@ $(output_latest)/machine.v2.wavm.br: $(DEP_PREDICATE) $(prover_bin) $(arbitrator $(prover_bin) $(replay_wasm) --generate-binaries $(output_latest) \ $(patsubst %,-l $(output_latest)/%.wasm, forward soft-float wasi_stub host_io user_host arbcompress arbcrypto program_exec) +$(output_latest)/unified_machine.wavm.br: $(DEP_PREDICATE) $(prover_bin) $(arbitrator_wasm_libs) $(unified_replay_wasm) + $(prover_bin) $(unified_replay_wasm) --generate-binaries $(output_latest) --until-hostio-bin-filename="unified-until-host-io-state.bin" --brotli-wavm-machine-filename="unified_machine.wavm.br" --module-root-filename="unified-module-root.txt" \ + $(patsubst %,-l $(output_latest)/%.wasm, forward soft-float wasi_stub host_io user_host arbcompress arbcrypto program_exec) + $(arbitrator_cases)/%.wasm: $(arbitrator_cases)/%.wat wat2wasm $< -o $@ diff --git a/changelog/rauljordan-NIT-4704.md b/changelog/rauljordan-NIT-4704.md new file mode 100644 index 00000000000..22c883b06ca --- /dev/null +++ b/changelog/rauljordan-NIT-4704.md @@ -0,0 +1,6 @@ +### Added +- Unified replay binary (`cmd/unified-replay/`) combining MEL message extraction and block production into a single WASM-compilable program +- `GetEndParentChainBlockHash` host I/O opcode for MEL proving +- `melwavmio` package providing WASM imports and native stubs for MEL +- Extended `GlobalState` to 4 bytes32 slots with backward-compatible hashing +- Makefile targets `build-unified-replay-env` and `build-unified-wasm-bin` diff --git a/cmd/replay/main.go b/cmd/replay/main.go index cac78039d71..5c8712c6a87 100644 --- a/cmd/replay/main.go +++ b/cmd/replay/main.go @@ -6,7 +6,6 @@ package main import ( "bytes" "context" - "encoding/hex" "encoding/json" "fmt" "io" @@ -255,27 +254,6 @@ func (r *DACertificatePreimageReader) RecoverPayloadAndPreimages( }) } -// To generate: -// key, _ := crypto.HexToECDSA("0000000000000000000000000000000000000000000000000000000000000001") -// sig, _ := crypto.Sign(make([]byte, 32), key) -// println(hex.EncodeToString(sig)) -const sampleSignature = "a0b37f8fba683cc68f6574cd43b39f0343a50008bf6ccea9d13231d9e7e2e1e411edc8d307254296264aebfc3dc76cd8b668373a072fd64665b50000e9fcce5201" - -// We call this early to populate the secp256k1 ecc basepoint cache in the cached early machine state. -// That means we don't need to re-compute it for every block. -func populateEcdsaCaches() { - signature, err := hex.DecodeString(sampleSignature) - if err != nil { - log.Warn("failed to decode sample signature to populate ECDSA cache", "err", err) - return - } - _, err = crypto.Ecrecover(make([]byte, 32), signature) - if err != nil { - log.Warn("failed to recover signature to populate ECDSA cache", "err", err) - return - } -} - func main() { wavmio.OnInit() gethhook.RequireHookedGeth() @@ -285,7 +263,7 @@ func main() { glogger.Verbosity(log.LevelError) log.SetDefault(log.NewLogger(glogger)) - populateEcdsaCaches() + wavmio.PopulateEcdsaCaches() raw := rawdb.NewDatabase(PreimageDb{}) db := state.NewDatabase(triedb.NewDatabase(raw, nil), nil) diff --git a/cmd/unified-replay/db.go b/cmd/unified-replay/db.go new file mode 100644 index 00000000000..ce529311520 --- /dev/null +++ b/cmd/unified-replay/db.go @@ -0,0 +1,125 @@ +// Copyright 2021-2026, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +package main + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" + + "github.com/offchainlabs/nitro/arbutil" + "github.com/offchainlabs/nitro/melwavmio" +) + +type PreimageDb struct{} + +func (db PreimageDb) Has(key []byte) (bool, error) { + if len(key) != 32 { + return false, nil + } + return false, errors.New("preimage DB doesn't support Has") +} + +func (db PreimageDb) DeleteRange(start, end []byte) error { + return errors.New("preimage DB doesn't support DeleteRange") +} + +func (db PreimageDb) Get(key []byte) ([]byte, error) { + var hash [32]byte + copy(hash[:], key) + if len(key) == 32 { + copy(hash[:], key) + } else if len(key) == len(rawdb.CodePrefix)+32 && bytes.HasPrefix(key, rawdb.CodePrefix) { + // Retrieving code + copy(hash[:], key[len(rawdb.CodePrefix):]) + } else { + return nil, fmt.Errorf("preimage DB attempted to access non-hash key %v", hex.EncodeToString(key)) + } + return melwavmio.ResolveTypedPreimage(arbutil.Keccak256PreimageType, hash) +} + +func (db PreimageDb) Put(key []byte, value []byte) error { + return errors.New("preimage DB doesn't support Put") +} + +func (db PreimageDb) Delete(key []byte) error { + return errors.New("preimage DB doesn't support Delete") +} + +func (db PreimageDb) NewBatch() ethdb.Batch { + return NopBatcher{db} +} + +func (db PreimageDb) NewBatchWithSize(size int) ethdb.Batch { + return NopBatcher{db} +} + +func (db PreimageDb) NewIterator(prefix []byte, start []byte) ethdb.Iterator { + return ErrorIterator{} +} + +func (db PreimageDb) Stat() (string, error) { + return "", errors.New("preimage DB doesn't support Stat") +} + +func (db PreimageDb) Compact(start []byte, limit []byte) error { + return nil +} + +func (db PreimageDb) Close() error { + return nil +} + +func (db PreimageDb) Release() { +} + +func (db PreimageDb) SyncAncient() error { + return nil // no-op +} + +func (db PreimageDb) SyncKeyValue() error { + return nil // no-op +} + +type NopBatcher struct { + ethdb.KeyValueStore +} + +func (b NopBatcher) ValueSize() int { + return 0 +} + +func (b NopBatcher) Write() error { + return nil +} + +func (b NopBatcher) Reset() {} + +func (b NopBatcher) Replay(w ethdb.KeyValueWriter) error { + return nil +} + +type ErrorIterator struct{} + +func (i ErrorIterator) Next() bool { + return false +} + +func (i ErrorIterator) Error() error { + return errors.New("preimage DB doesn't support iterators") +} + +func (i ErrorIterator) Key() []byte { + return []byte{} +} + +func (i ErrorIterator) Value() []byte { + return []byte{} +} + +func (i ErrorIterator) Release() {} diff --git a/cmd/unified-replay/main.go b/cmd/unified-replay/main.go new file mode 100644 index 00000000000..fb9db31e2f1 --- /dev/null +++ b/cmd/unified-replay/main.go @@ -0,0 +1,379 @@ +// Copyright 2026-2027, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "os" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus" + "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/log" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/triedb" + + "github.com/offchainlabs/nitro/arbnode/mel" + melextraction "github.com/offchainlabs/nitro/arbnode/mel/extraction" + "github.com/offchainlabs/nitro/arbos" + "github.com/offchainlabs/nitro/arbos/arbosState" + "github.com/offchainlabs/nitro/arbos/arbostypes" + "github.com/offchainlabs/nitro/arbos/burn" + "github.com/offchainlabs/nitro/arbutil" + "github.com/offchainlabs/nitro/cmd/chaininfo" + "github.com/offchainlabs/nitro/daprovider" + "github.com/offchainlabs/nitro/gethhook" + melreplay "github.com/offchainlabs/nitro/mel-replay" + "github.com/offchainlabs/nitro/melwavmio" + "github.com/offchainlabs/nitro/wavmio" +) + +func main() { + melwavmio.StubInit() + gethhook.RequireHookedGeth() + + glogger := log.NewGlogHandler( + log.NewTerminalHandler(io.Writer(os.Stderr), false)) + glogger.Verbosity(log.LevelError) + log.SetDefault(log.NewLogger(glogger)) + + wavmio.PopulateEcdsaCaches() + + melMsgHash := melwavmio.GetMELMsgHash() + startMELStateHash := melwavmio.GetStartMELRoot() + melState := readMELState(startMELStateHash) + + if melMsgHash != (common.Hash{}) { + produceBlock(melMsgHash) + melwavmio.IncreasePositionInMEL() + } else { + targetBlockHash := melwavmio.GetEndParentChainBlockHash() + chainConfig := readChainConfigFromLastBlock() + melState = extractMessagesUpTo(chainConfig, melState, targetBlockHash) + melwavmio.SetEndMELRoot(melState.Hash()) + } + + positionInMEL := melwavmio.GetPositionInMEL() + if melState.MsgCount > positionInMEL { + resolver := &wavmPreimageResolver{} + msgReader := melreplay.NewMessageReader(resolver) + nextMsg, err := msgReader.Read(context.Background(), melState, positionInMEL) + if err != nil { + panic(fmt.Errorf("error reading message idx %d: %w", positionInMEL, err)) + } + melwavmio.SetMELMsgHash(nextMsg.Hash()) + } else { + melwavmio.SetMELMsgHash(common.Hash{}) + } +} + +func produceBlock(msgHash common.Hash) { + msgBytes := readPreimage(msgHash) + message := new(arbostypes.MessageWithMetadata) + if err := rlp.DecodeBytes(msgBytes, message); err != nil { + panic(fmt.Errorf("error RLP decoding message: %w", err)) + } + raw := rawdb.NewDatabase(PreimageDb{}) + db := state.NewDatabase(triedb.NewDatabase(raw, nil), nil) + lastBlockHash := melwavmio.GetLastBlockHash() + var lastBlockHeader *types.Header + var lastBlockStateRoot common.Hash + if lastBlockHash != (common.Hash{}) { + lastBlockHeader = getHeaderByHash(lastBlockHash) + lastBlockStateRoot = lastBlockHeader.Root + } + statedb, err := state.NewDeterministic(lastBlockStateRoot, db) + if err != nil { + panic(fmt.Sprintf("Error opening state db: %v", err.Error())) + } + var newBlock *types.Block + if lastBlockStateRoot != (common.Hash{}) { + // ArbOS has already been initialized. + // Load the chain config and then produce a block normally. + initialArbosState, err := arbosState.OpenSystemArbosState(statedb, nil, true) + if err != nil { + panic(fmt.Sprintf("Error opening initial ArbOS state: %v", err.Error())) + } + chainId, err := initialArbosState.ChainId() + if err != nil { + panic(fmt.Sprintf("Error getting chain ID from initial ArbOS state: %v", err.Error())) + } + genesisBlockNum, err := initialArbosState.GenesisBlockNum() + if err != nil { + panic(fmt.Sprintf("Error getting genesis block number from initial ArbOS state: %v", err.Error())) + } + chainConfigJson, err := initialArbosState.ChainConfig() + if err != nil { + panic(fmt.Sprintf("Error getting chain config from initial ArbOS state: %v", err.Error())) + } + var chainConfig *params.ChainConfig + if len(chainConfigJson) > 0 { + chainConfig = ¶ms.ChainConfig{} + err = json.Unmarshal(chainConfigJson, chainConfig) + if err != nil { + panic(fmt.Sprintf("Error parsing chain config: %v", err.Error())) + } + if chainConfig.ChainID.Cmp(chainId) != 0 { + panic(fmt.Sprintf("Error: chain id mismatch, chainID: %v, chainConfig.ChainID: %v", chainId, chainConfig.ChainID)) + } + if chainConfig.ArbitrumChainParams.GenesisBlockNum != genesisBlockNum { + panic(fmt.Sprintf("Error: genesis block number mismatch, genesisBlockNum: %v, chainConfig.ArbitrumParams.GenesisBlockNum: %v", genesisBlockNum, chainConfig.ArbitrumChainParams.GenesisBlockNum)) + } + } else { + log.Info("Falling back to hardcoded chain config.") + chainConfig, err = chaininfo.GetChainConfig(chainId, "", genesisBlockNum, []string{}, "") + if err != nil { + panic(err) + } + } + + chainContext := WavmChainContext{chainConfig: chainConfig} + newBlock, _, _, err = arbos.ProduceBlock(message.Message, message.DelayedMessagesRead, lastBlockHeader, statedb, chainContext, false, core.NewMessageReplayContext(), false) + if err != nil { + panic(err) + } + } else { + // Initialize ArbOS with this init message and create the genesis block. + initMessage, err := message.Message.ParseInitMessage() + if err != nil { + panic(err) + } + chainConfig := initMessage.ChainConfig + if chainConfig == nil { + log.Info("No chain config in the init message. Falling back to hardcoded chain config.") + chainConfig, err = chaininfo.GetChainConfig(initMessage.ChainId, "", 0, []string{}, "") + if err != nil { + panic(err) + } + } + + _, err = arbosState.InitializeArbosState(statedb, burn.NewSystemBurner(nil, false), chainConfig, nil, initMessage) + if err != nil { + panic(fmt.Sprintf("Error initializing ArbOS: %v", err.Error())) + } + + newBlock = arbosState.MakeGenesisBlock(common.Hash{}, 0, 0, statedb.IntermediateRoot(true), chainConfig) + } + + newBlockHash := newBlock.Hash() + + log.Info("Final State", "newBlockHash", newBlockHash, "StateRoot", newBlock.Root()) + + extraInfo := types.DeserializeHeaderExtraInformation(newBlock.Header()) + if extraInfo.ArbOSFormatVersion == 0 { + panic(fmt.Sprintf("Error deserializing header extra info: %+v", newBlock.Header())) + } + melwavmio.SetLastBlockHash(newBlockHash) + melwavmio.SetSendRoot(extraInfo.SendRoot) +} + +// readChainConfigFromLastBlock reads the chain config from the L2 ArbOS state +// of the most recent block. Returns nil if no block has been produced yet. +func readChainConfigFromLastBlock() *params.ChainConfig { + lastBlockHash := melwavmio.GetLastBlockHash() + if lastBlockHash == (common.Hash{}) { + return nil + } + lastBlockHeader := getHeaderByHash(lastBlockHash) + raw := rawdb.NewDatabase(PreimageDb{}) + db := state.NewDatabase(triedb.NewDatabase(raw, nil), nil) + statedb, err := state.NewDeterministic(lastBlockHeader.Root, db) + if err != nil { + panic(fmt.Sprintf("Error opening state db: %v", err.Error())) + } + arbosState, err := arbosState.OpenSystemArbosState(statedb, nil, true) + if err != nil { + panic(fmt.Sprintf("Error opening ArbOS state: %v", err.Error())) + } + chainId, err := arbosState.ChainId() + if err != nil { + panic(fmt.Sprintf("Error getting chain ID: %v", err.Error())) + } + genesisBlockNum, err := arbosState.GenesisBlockNum() + if err != nil { + panic(fmt.Sprintf("Error getting genesis block number: %v", err.Error())) + } + chainConfigJson, err := arbosState.ChainConfig() + if err != nil { + panic(fmt.Sprintf("Error getting chain config: %v", err.Error())) + } + if len(chainConfigJson) > 0 { + chainConfig := ¶ms.ChainConfig{} + err = json.Unmarshal(chainConfigJson, chainConfig) + if err != nil { + panic(fmt.Sprintf("Error parsing chain config: %v", err.Error())) + } + if chainConfig.ChainID.Cmp(chainId) != 0 { + panic(fmt.Sprintf("Error: chain id mismatch, chainID: %v, chainConfig.ChainID: %v", chainId, chainConfig.ChainID)) + } + if chainConfig.ArbitrumChainParams.GenesisBlockNum != genesisBlockNum { + panic(fmt.Sprintf("Error: genesis block number mismatch, genesisBlockNum: %v, chainConfig.ArbitrumParams.GenesisBlockNum: %v", genesisBlockNum, chainConfig.ArbitrumChainParams.GenesisBlockNum)) + } + return chainConfig + } + log.Info("Falling back to hardcoded chain config.") + chainConfig, err := chaininfo.GetChainConfig(chainId, "", genesisBlockNum, []string{}, "") + if err != nil { + panic(err) + } + return chainConfig +} + +// Runs a replay binary of message extraction for Arbitrum chains. Given a start and end parent chain +// block hash, this program will extract all block header hashes in that range, and then run the +// message extraction algorithm over those block headers, starting from a starting MEL state and processing +// block headers one-by-one. At the end, a final MEL state is produced, and its hash is set into the +// machine using a wavmio method. +func extractMessagesUpTo( + chainConfig *params.ChainConfig, + startState *mel.State, + targetBlockHash common.Hash, +) *mel.State { + resolver := &wavmPreimageResolver{} + dapReader := daprovider.NewDAProviderRegistry() + blobReader := &BlobPreimageReader{} + if err := dapReader.SetupBlobReader(daprovider.NewReaderForBlobReader(blobReader)); err != nil { + panic(fmt.Errorf("error setting up blob reader: %w", err)) + } + + // Extract the relevant header hashes in the range from the + // block hash of the start MEL state to the end parent chain block hash. + // This is done by walking backwards from the end parent chain block hash + // until we reach the block hash of the start MEL state as blocks are + // only connected by parent linkages. + blockHeaderHashes := walkBackwards( + startState.ParentChainBlockHash, + targetBlockHash, + ) + currentState := startState + + // Loops backwards over blocks, feeding them one by one into the extract messages function. + delayedMsgDatabase := melreplay.NewDelayedMessageDatabase(resolver) + ctx := context.Background() + for i := len(blockHeaderHashes) - 1; i >= 0; i-- { + headerHash := blockHeaderHashes[i] + header := getHeaderByHash(headerHash) + log.Info("Extracting messages from block", "number", header.Number.Uint64(), "hash", header.Hash().Hex()) + txsFetcher := melreplay.NewTransactionFetcher(header, resolver) + logsFetcher := melreplay.NewLogsFetcher(header, resolver) + postState, _, _, _, err := melextraction.ExtractMessages( + ctx, + currentState, + header, + dapReader, + delayedMsgDatabase, + txsFetcher, + logsFetcher, + chainConfig, + ) + if err != nil { + panic(fmt.Errorf("error extracting messages from block %s: %w", header.Hash().Hex(), err)) + } + currentState = postState + } + return currentState +} + +// Extracts all block header hashes in the range from startHash to endHash. +func walkBackwards( + startHash, + endHash common.Hash, +) []common.Hash { + if startHash == endHash { + return nil + } + headerHashes := make([]common.Hash, 0) + curr := endHash + for { + header := getHeaderByHash(curr) + headerHashes = append(headerHashes, curr) + curr = header.ParentHash + if curr == startHash { + break + } + if curr == (common.Hash{}) { + panic(fmt.Sprintf("walkBackwards reached genesis without finding startHash %s from endHash %s", startHash.Hex(), endHash.Hex())) + } + } + return headerHashes +} + +// Gets a block header by its hash using the preimage resolver. +func getHeaderByHash(hash common.Hash) *types.Header { + enc, err := melwavmio.ResolveTypedPreimage(arbutil.Keccak256PreimageType, hash) + if err != nil { + panic(fmt.Errorf("error resolving preimage: %w", err)) + } + header := &types.Header{} + err = rlp.DecodeBytes(enc, &header) + if err != nil { + panic(fmt.Errorf("error parsing resolved block header: %w", err)) + } + return header +} + +func readMELState(hash common.Hash) *mel.State { + startStateBytes := readPreimage(hash) + state := new(mel.State) + if err := rlp.Decode(bytes.NewBuffer(startStateBytes), &state); err != nil { + panic(fmt.Errorf("error decoding MEL state: %w", err)) + } + return state +} + +func readPreimage(hash common.Hash) []byte { + preimage, err := melwavmio.ResolveTypedPreimage(arbutil.Keccak256PreimageType, hash) + if err != nil { + panic(fmt.Errorf("error resolving preimage: %w", err)) + } + return preimage +} + +type WavmChainContext struct { + chainConfig *params.ChainConfig +} + +func (c WavmChainContext) CurrentHeader() *types.Header { + return getLastBlockHeader() +} + +func (c WavmChainContext) GetHeaderByNumber(number uint64) *types.Header { + panic("GetHeaderByNumber should not be called in WavmChainContext") +} + +func (c WavmChainContext) GetHeaderByHash(hash common.Hash) *types.Header { + return getHeaderByHash(hash) +} + +func (c WavmChainContext) Config() *params.ChainConfig { + return c.chainConfig +} + +func (c WavmChainContext) Engine() consensus.Engine { + return arbos.Engine{} +} + +func (c WavmChainContext) GetHeader(hash common.Hash, num uint64) *types.Header { + header := getHeaderByHash(hash) + if !header.Number.IsUint64() || header.Number.Uint64() != num { + panic(fmt.Sprintf("Retrieved wrong block number for header hash %v -- requested %v but got %v", hash, num, header.Number.String())) + } + return header +} + +func getLastBlockHeader() *types.Header { + lastBlockHash := melwavmio.GetLastBlockHash() + if lastBlockHash == (common.Hash{}) { + return nil + } + return getHeaderByHash(lastBlockHash) +} diff --git a/cmd/unified-replay/preimage_resolver.go b/cmd/unified-replay/preimage_resolver.go new file mode 100644 index 00000000000..27b100a7664 --- /dev/null +++ b/cmd/unified-replay/preimage_resolver.go @@ -0,0 +1,50 @@ +// Copyright 2026-2027, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +package main + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto/kzg4844" + + "github.com/offchainlabs/nitro/arbutil" + "github.com/offchainlabs/nitro/melwavmio" +) + +type wavmPreimageResolver struct{} + +func (w *wavmPreimageResolver) ResolveTypedPreimage( + preimageType arbutil.PreimageType, hash common.Hash) ([]byte, error) { + return melwavmio.ResolveTypedPreimage(preimageType, hash) +} + +type BlobPreimageReader struct { +} + +func (r *BlobPreimageReader) GetBlobs( + ctx context.Context, + batchBlockHash common.Hash, + versionedHashes []common.Hash, +) ([]kzg4844.Blob, error) { + var blobs []kzg4844.Blob + for _, h := range versionedHashes { + var blob kzg4844.Blob + preimage, err := melwavmio.ResolveTypedPreimage(arbutil.EthVersionedHashPreimageType, h) + if err != nil { + return nil, err + } + if len(preimage) != len(blob) { + return nil, fmt.Errorf("for blob %v got back preimage of length %v but expected blob length %v", h, len(preimage), len(blob)) + } + copy(blob[:], preimage) + blobs = append(blobs, blob) + } + return blobs, nil +} + +func (r *BlobPreimageReader) Initialize(ctx context.Context) error { + return nil +} diff --git a/crates/prover-ffi/src/machine.rs b/crates/prover-ffi/src/machine.rs index 40ec9a8c1a1..7c9a3d74c29 100644 --- a/crates/prover-ffi/src/machine.rs +++ b/crates/prover-ffi/src/machine.rs @@ -280,6 +280,18 @@ pub unsafe extern "C" fn arbitrator_set_global_state(mach: *mut Machine, gs: Glo } } +/// Sets the ending parent chain block hash used for a machine when executing message extraction +/// algorithms. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn arbitrator_set_end_parent_chain_block_hash( + mach: *mut Machine, + block_hash: *const Bytes32, +) { + unsafe { + (*mach).set_end_parent_chain_block_hash(*block_hash); + } +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn arbitrator_set_context(mach: *mut Machine, context: u64) { unsafe { diff --git a/crates/prover/src/host.rs b/crates/prover/src/host.rs index f5c7022153d..cd5c16b598d 100644 --- a/crates/prover/src/host.rs +++ b/crates/prover/src/host.rs @@ -29,6 +29,7 @@ pub enum Hostio { WavmSetGlobalStateBytes32, WavmGetGlobalStateU64, WavmSetGlobalStateU64, + WavmGetEndParentChainBlockHash, WavmValidateCertificate, WavmReadKeccakPreimage, WavmReadSha256Preimage, @@ -78,6 +79,7 @@ impl FromStr for Hostio { ("env", "wavm_set_globalstate_bytes32") => WavmSetGlobalStateBytes32, ("env", "wavm_get_globalstate_u64") => WavmGetGlobalStateU64, ("env", "wavm_set_globalstate_u64") => WavmSetGlobalStateU64, + ("env", "wavm_get_end_parent_chain_block_hash") => WavmGetEndParentChainBlockHash, ("env", "wavm_validate_certificate") => WavmValidateCertificate, ("env", "wavm_read_keccak_256_preimage") => WavmReadKeccakPreimage, ("env", "wavm_read_sha2_256_preimage") => WavmReadSha256Preimage, @@ -141,6 +143,7 @@ impl Hostio { WavmSetGlobalStateBytes32 => func!([I32, I32]), WavmGetGlobalStateU64 => func!([I32], [I64]), WavmSetGlobalStateU64 => func!([I32, I64]), + WavmGetEndParentChainBlockHash => func!([I32]), WavmValidateCertificate => func!([I32, I32], [I32]), WavmReadKeccakPreimage => func!([I32, I32], [I32]), WavmReadSha256Preimage => func!([I32, I32], [I32]), @@ -239,6 +242,10 @@ impl Hostio { opcode!(LocalGet, 1); opcode!(SetGlobalStateU64); } + WavmGetEndParentChainBlockHash => { + opcode!(LocalGet, 0); + opcode!(GetEndParentChainBlockHash); + } WavmValidateCertificate => { opcode!(LocalGet, 0); // hash opcode!(LocalGet, 1); // preimage_ty diff --git a/crates/prover/src/machine.rs b/crates/prover/src/machine.rs index 6737eae3184..d305c6b6ebb 100644 --- a/crates/prover/src/machine.rs +++ b/crates/prover/src/machine.rs @@ -809,9 +809,11 @@ impl From for FunctionSerdeAll { // Globalstate holds: // bytes32 - last_block_hash // bytes32 - send_root +// bytes32 - mel_state_hash +// bytes32 - mel_message_hash // uint64 - inbox_position // uint64 - position_within_message -pub const GLOBAL_STATE_BYTES32_NUM: usize = 2; +pub const GLOBAL_STATE_BYTES32_NUM: usize = 4; pub const GLOBAL_STATE_U64_NUM: usize = 2; #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] @@ -836,8 +838,9 @@ impl GlobalState { fn hash(&self) -> Bytes32 { let mut h = Keccak256::new(); h.update("Global state:"); - for item in self.bytes32_vals { - h.update(item) + let end_idx = self.bytes32_last_non_zero_index(); + for i in 0..=end_idx { + h.update(self.bytes32_vals[i]); } for item in self.u64_vals { h.update(item.to_be_bytes()) @@ -847,14 +850,33 @@ impl GlobalState { fn serialize(&self) -> Vec { let mut data = Vec::new(); - for item in self.bytes32_vals { - data.extend(item) + let end_idx = self.bytes32_last_non_zero_index(); + for i in 0..=end_idx { + data.extend(self.bytes32_vals[i]); } for item in self.u64_vals { data.extend(item.to_be_bytes()) } data } + /// Finds the last non-zero index in the bytes32 values and returns + /// the max of it and 1. This is used to determine how many + /// bytes32 values to include in the hash and serialization of a global state, + /// and at least the first two values must be included for backwards compatibility. + fn bytes32_last_non_zero_index(&self) -> usize { + let last_non_zero_idx = self + .bytes32_vals + .iter() + .enumerate() + .rev() + .find(|&(_, &val)| val != Bytes32::default()) + .map(|(i, _)| i); + + match last_non_zero_idx { + Some(idx) => std::cmp::max(1, idx), + None => 1, + } + } } #[derive(Serialize)] @@ -1008,6 +1030,7 @@ pub struct Machine { inbox_contents: HashMap<(InboxIdentifier, u64), Vec>, first_too_far: u64, // Not part of machine hash preimage_resolver: PreimageResolverWrapper, + end_parent_chain_block_hash: Bytes32, // Used for MEL proving. /// Linkable Stylus modules in compressed form. Not part of the machine hash. stylus_modules: HashMap>, initial_hash: Bytes32, @@ -1582,6 +1605,7 @@ impl Machine { preimage_resolver: PreimageResolverWrapper::new(preimage_resolver), stylus_modules: HashMap::default(), initial_hash: Bytes32::default(), + end_parent_chain_block_hash: Bytes32::default(), context: 0, debug_info, }; @@ -1615,6 +1639,7 @@ impl Machine { stylus_modules: Default::default(), initial_hash: Default::default(), context: Default::default(), + end_parent_chain_block_hash: Default::default(), debug_info: Default::default(), } } @@ -1668,6 +1693,7 @@ impl Machine { preimage_resolver: PreimageResolverWrapper::new(get_empty_preimage_resolver()), stylus_modules: HashMap::default(), initial_hash: Bytes32::default(), + end_parent_chain_block_hash: Bytes32::default(), context: 0, debug_info: false, }; @@ -2490,6 +2516,15 @@ impl Machine { self.global_state.u64_vals[idx] = val } } + Opcode::GetEndParentChainBlockHash => { + let ptr = value_stack.pop().unwrap().assume_u32(); + if !module + .memory + .store_slice_aligned(ptr.into(), &*self.end_parent_chain_block_hash) + { + error!(); + } + } Opcode::ValidateCertificate => { let preimage_type = value_stack.pop().unwrap().assume_u32(); let hash_ptr = value_stack.pop().unwrap().assume_u32(); @@ -3319,6 +3354,10 @@ impl Machine { self.global_state = gs; } + pub fn set_end_parent_chain_block_hash(&mut self, hash: Bytes32) { + self.end_parent_chain_block_hash = hash; + } + pub fn set_preimage_resolver(&mut self, resolver: PreimageResolver) { self.preimage_resolver.resolver = resolver; } diff --git a/crates/prover/src/main.rs b/crates/prover/src/main.rs index a0869b5a2ad..9d78e36efa8 100644 --- a/crates/prover/src/main.rs +++ b/crates/prover/src/main.rs @@ -73,6 +73,10 @@ struct Opts { #[structopt(long)] last_send_root: Option, #[structopt(long)] + mel_state_root: Option, + #[structopt(long)] + mel_msg_hash: Option, + #[structopt(long)] inbox: Vec, #[structopt(long)] delayed_inbox: Vec, @@ -90,6 +94,13 @@ struct Opts { skip_until_host_io: bool, #[structopt(long)] max_steps: Option, + /// Options for WAVM binary generation. + #[structopt(long, default_value = "module-root.txt")] + module_root_filename: String, + #[structopt(long, default_value = "machine.v2.wavm.br")] + brotli_wavm_machine_filename: String, + #[structopt(long, default_value = "until-host-io-state.bin")] + until_hostio_bin_filename: String, // JSON inputs supercede any of the command-line inputs which could // be specified in the JSON file. #[structopt(long)] @@ -160,15 +171,15 @@ fn main() -> Result<()> { } if let Some(output_path) = opts.generate_binaries { - let mut module_root_file = File::create(output_path.join("module-root.txt"))?; + let mut module_root_file = File::create(output_path.join(opts.module_root_filename))?; writeln!(module_root_file, "{}", mach.get_modules_root())?; module_root_file.flush()?; - mach.serialize_binary(output_path.join("machine.v2.wavm.br"))?; + mach.serialize_binary(output_path.join(opts.brotli_wavm_machine_filename))?; while !mach.next_instruction_is_host_io() { mach.step_n(1)?; } - mach.serialize_state(output_path.join("until-host-io-state.bin"))?; + mach.serialize_state(output_path.join(opts.until_hostio_bin_filename))?; return Ok(()); } @@ -567,10 +578,17 @@ fn initialize_machine(opts: &Opts) -> eyre::Result { let last_block_hash = decode_hex_arg(&opts.last_block_hash, "--last-block-hash")?; let last_send_root = decode_hex_arg(&opts.last_send_root, "--last-send-root")?; + let mel_state_root = decode_hex_arg(&opts.mel_state_root, "--mel-state-root")?; + let mel_msg_hash = decode_hex_arg(&opts.mel_msg_hash, "--mel-msg-hash")?; let global_state = GlobalState { u64_vals: [opts.inbox_position, opts.position_within_message], - bytes32_vals: [last_block_hash, last_send_root], + bytes32_vals: [ + last_block_hash, + last_send_root, + mel_state_root, + mel_msg_hash, + ], }; Machine::from_paths( diff --git a/crates/prover/src/prepare.rs b/crates/prover/src/prepare.rs index 81f8fee9078..9af05357935 100644 --- a/crates/prover/src/prepare.rs +++ b/crates/prover/src/prepare.rs @@ -36,9 +36,22 @@ pub fn prepare_machine(preimages: PathBuf, machines: PathBuf) -> eyre::Result 0x8030, Opcode::PopCoThread => 0x8031, Opcode::SwitchThread => 0x8032, + Opcode::GetEndParentChainBlockHash => 0x8033, } } @@ -306,6 +309,7 @@ impl Opcode { | Opcode::ValidateCertificate | Opcode::ReadPreImage | Opcode::ReadInboxMessage + | Opcode::GetEndParentChainBlockHash ) } } diff --git a/crates/wasm-libraries/host-io/lib.rs b/crates/wasm-libraries/host-io/lib.rs index 677f05c8931..2484a8c4cd0 100644 --- a/crates/wasm-libraries/host-io/lib.rs +++ b/crates/wasm-libraries/host-io/lib.rs @@ -16,6 +16,7 @@ unsafe extern "C" { pub fn wavm_set_globalstate_bytes32(idx: u32, ptr: *const u8); pub fn wavm_get_globalstate_u64(idx: u32) -> u64; pub fn wavm_set_globalstate_u64(idx: u32, val: u64); + pub fn wavm_get_end_parent_chain_block_hash(ptr: *mut u8); pub fn wavm_read_keccak_256_preimage(ptr: *mut u8, offset: usize) -> usize; pub fn wavm_read_sha2_256_preimage(ptr: *mut u8, offset: usize) -> usize; pub fn wavm_read_eth_versioned_hash_preimage(ptr: *mut u8, offset: usize) -> usize; @@ -89,6 +90,18 @@ pub unsafe extern "C" fn wavmio__setGlobalStateU64(idx: u32, val: u64) { } } +/// Reads 32-bytes of the ending parent chain block hash for MEL. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn wavmio__getEndParentChainBlockHash(out_ptr: GuestPtr) { + let mut our_buf = MemoryLeaf([0u8; 32]); + let our_ptr = our_buf.as_mut_ptr(); + assert_eq!(our_ptr as usize % 32, 0); + unsafe { + wavm_get_end_parent_chain_block_hash(our_ptr); + } + StaticMem.write_slice(out_ptr, &our_buf[..32]); +} + /// Reads an inbox message #[unsafe(no_mangle)] pub unsafe extern "C" fn wavmio__readInboxMessage( diff --git a/melwavmio/higher.go b/melwavmio/higher.go new file mode 100644 index 00000000000..8f6d4d81d71 --- /dev/null +++ b/melwavmio/higher.go @@ -0,0 +1,110 @@ +// Copyright 2026-2027, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +//go:build wasm +// +build wasm + +package melwavmio + +import ( + "unsafe" + + "github.com/ethereum/go-ethereum/common" + + "github.com/offchainlabs/nitro/arbutil" +) + +const INITIAL_CAPACITY = 128 +const QUERY_SIZE = 32 + +// bytes32 +const IDX_LAST_BLOCKHASH = 0 +const IDX_SEND_ROOT = 1 +const IDX_MEL_ROOT = 2 +const IDX_MEL_MSG_HASH = 3 + +// u64 +const IDX_POS_IN_MEL = 1 + +func readBuffer(f func(uint32, unsafe.Pointer) uint32) []byte { + buf := make([]byte, 0, INITIAL_CAPACITY) + offset := 0 + for { + if len(buf) < offset+QUERY_SIZE { + buf = append(buf, make([]byte, offset+QUERY_SIZE-len(buf))...) + } + read := f(uint32(offset), unsafe.Pointer(&buf[offset])) + offset += int(read) + if read < QUERY_SIZE { + buf = buf[:offset] + return buf + } + } +} + +func StubInit() { +} + +func StubFinal() { +} + +func GetLastBlockHash() (hash common.Hash) { + hashUnsafe := unsafe.Pointer(&hash[0]) + getGlobalStateBytes32(IDX_LAST_BLOCKHASH, hashUnsafe) + return +} + +func GetMELMsgHash() (hash common.Hash) { + hashUnsafe := unsafe.Pointer(&hash[0]) + getGlobalStateBytes32(IDX_MEL_MSG_HASH, hashUnsafe) + return +} + +func SetMELMsgHash(hash common.Hash) { + hashUnsafe := unsafe.Pointer(&hash[0]) + setGlobalStateBytes32(IDX_MEL_MSG_HASH, hashUnsafe) +} + +func GetStartMELRoot() (hash common.Hash) { + hashUnsafe := unsafe.Pointer(&hash[0]) + getGlobalStateBytes32(IDX_MEL_ROOT, hashUnsafe) + return +} + +func GetPositionInMEL() uint64 { + return getGlobalStateU64(IDX_POS_IN_MEL) +} + +func IncreasePositionInMEL() { + pos := GetPositionInMEL() + setGlobalStateU64(IDX_POS_IN_MEL, pos+1) +} + +func GetEndParentChainBlockHash() (hash common.Hash) { + hashUnsafe := unsafe.Pointer(&hash[0]) + getEndParentChainBlockHash(hashUnsafe) + return +} + +func SetLastBlockHash(hash [32]byte) { + hashUnsafe := unsafe.Pointer(&hash[0]) + setGlobalStateBytes32(IDX_LAST_BLOCKHASH, hashUnsafe) +} + +func SetEndMELRoot(hash common.Hash) { + hashUnsafe := unsafe.Pointer(&hash[0]) + setGlobalStateBytes32(IDX_MEL_ROOT, hashUnsafe) +} + +// Note: if a GetSendRoot is ever modified, the validator will need to fill in the previous send root, which it currently does not. +func SetSendRoot(hash [32]byte) { + hashUnsafe := unsafe.Pointer(&hash[0]) + setGlobalStateBytes32(IDX_SEND_ROOT, hashUnsafe) +} + +func ResolveTypedPreimage(ty arbutil.PreimageType, hash common.Hash) ([]byte, error) { + return readBuffer(func(offset uint32, buf unsafe.Pointer) uint32 { + hashUnsafe := unsafe.Pointer(&hash[0]) + return resolveTypedPreimage(uint32(ty), hashUnsafe, offset, buf) + }), nil +} diff --git a/melwavmio/raw.go b/melwavmio/raw.go new file mode 100644 index 00000000000..d0f60cac8f3 --- /dev/null +++ b/melwavmio/raw.go @@ -0,0 +1,27 @@ +// Copyright 2026-2027, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +//go:build wasm +// +build wasm + +package melwavmio + +import "unsafe" + +//go:wasmimport wavmio resolveTypedPreimage +func resolveTypedPreimage(ty uint32, hash unsafe.Pointer, offset uint32, output unsafe.Pointer) uint32 + +//go:wasmimport wavmio getGlobalStateBytes32 +func getGlobalStateBytes32(idx uint32, output unsafe.Pointer) + +//go:wasmimport wavmio getGlobalStateU64 +func getGlobalStateU64(idx uint32) uint64 + +//go:wasmimport wavmio setGlobalStateU64 +func setGlobalStateU64(idx uint32, val uint64) + +//go:wasmimport wavmio setGlobalStateBytes32 +func setGlobalStateBytes32(idx uint32, val unsafe.Pointer) + +//go:wasmimport wavmio getEndParentChainBlockHash +func getEndParentChainBlockHash(unsafe.Pointer) diff --git a/melwavmio/stub.go b/melwavmio/stub.go new file mode 100644 index 00000000000..aa0e1bef5ea --- /dev/null +++ b/melwavmio/stub.go @@ -0,0 +1,105 @@ +// Copyright 2026-2027, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +//go:build !wasm +// +build !wasm + +package melwavmio + +import ( + "encoding/json" + "errors" + "flag" + "os" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + + "github.com/offchainlabs/nitro/arbutil" +) + +var ( + preimages = make(map[arbutil.PreimageType]map[common.Hash][]byte) + startMelStateHash = common.Hash{} + melMsgHash = common.Hash{} + endMelStateHash = common.Hash{} // This is set by the stubbed SetEndMELStateHash function + endParentChainBlockHash = common.Hash{} // This is set by the stubbed GetEndParentChainBlockHash function + lastBlockHash = common.Hash{} // This is set by the stubbed GetLastBlockHash function + positionInMEL = uint64(0) +) + +func StubInit() { + endParentChainBlockHashFlag := flag.String("end-parent-chain-block-hash", "0000000000000000000000000000000000000000000000000000000000000000", "endParentChainBlockHash") + startMelRootFlag := flag.String("start-mel-state-hash", "0000000000000000000000000000000000000000000000000000000000000000", "startMelHash") + melMsgHashFlag := flag.String("mel-msg-hash", "0000000000000000000000000000000000000000000000000000000000000000", "melMsgHash") + preimagesPath := flag.String("preimages", "", "file to load preimages from") + positionInMELFlag := flag.Uint64("position-in-mel", 0, "positionInMEL") + lastBlockHashFlag := flag.String("last-block-hash", "0000000000000000000000000000000000000000000000000000000000000000", "lastBlockHash") + flag.Parse() + endParentChainBlockHash = common.HexToHash(*endParentChainBlockHashFlag) + startMelStateHash = common.HexToHash(*startMelRootFlag) + positionInMEL = *positionInMELFlag + melMsgHash = common.HexToHash(*melMsgHashFlag) + lastBlockHash = common.HexToHash(*lastBlockHashFlag) + fileBytes, err := os.ReadFile(*preimagesPath) + if err != nil { + panic(err) + } + if err = json.Unmarshal(fileBytes, &preimages); err != nil { + panic(err) + } +} + +func StubFinal() { + log.Info("endMelStateHash", endMelStateHash.Hex()) +} + +func GetLastBlockHash() (hash common.Hash) { + return lastBlockHash +} + +func GetMELMsgHash() (hash common.Hash) { + hash = melMsgHash + return +} + +func SetMELMsgHash(hash common.Hash) { + melMsgHash = hash +} + +func GetStartMELRoot() (hash common.Hash) { + hash = startMelStateHash + return +} + +func GetEndParentChainBlockHash() (hash common.Hash) { + hash = endParentChainBlockHash + return +} + +func SetEndMELRoot(hash common.Hash) { + endMelStateHash = hash +} + +func SetLastBlockHash(hash [32]byte) { + lastBlockHash = hash +} + +func GetPositionInMEL() uint64 { + return positionInMEL +} + +func IncreasePositionInMEL() { + positionInMEL++ +} + +func SetSendRoot(hash [32]byte) { +} + +func ResolveTypedPreimage(ty arbutil.PreimageType, hash common.Hash) ([]byte, error) { + val, ok := preimages[ty][hash] + if !ok { + return []byte{}, errors.New("preimage not found") + } + return val, nil +} diff --git a/system_tests/message_extraction_layer_test.go b/system_tests/message_extraction_layer_test.go index b136250fd09..d14f37c67f2 100644 --- a/system_tests/message_extraction_layer_test.go +++ b/system_tests/message_extraction_layer_test.go @@ -626,32 +626,35 @@ func TestMessageExtractionLayer_TxStreamerHandleReorg(t *testing.T) { } CheckBatchCount(t, builder, initialBatchCount+1) - // Wait until mel can read the posted batch, send correct L2 messages to txStreamer and txStreamer is able to detect the Reorg and handle correct execution of L2 messages + // Wait until the reorg is fully handled by polling for the expected balance, + // which is the actual correctness condition. Log checks are kept as diagnostics + // but not as the gate, since TransactionStreamer rate-limits reorg log emission. + expectedBalance := new(big.Int).Add(oldBalance, txOpts.Value) { timeout := time.NewTimer(time.Minute) defer timeout.Stop() tick := time.NewTicker(100 * time.Millisecond) defer tick.Stop() for { - // Verify that both MEL and TxStreamer detected the reorg - if logHandler.WasLogged("MEL detected L1 reorg") && logHandler.WasLogged("TransactionStreamer: Reorg detected!") { + newBalance, err := builder.L2.Client.BalanceAt(ctx, txOpts.From, nil) + if err == nil && newBalance.Cmp(expectedBalance) == 0 { break } select { case <-tick.C: case <-timeout.C: - t.Fatalf("timed out waiting for MEL and TransactionStreamer to detect reorg") + latestBalance, _ := builder.L2.Client.BalanceAt(ctx, txOpts.From, nil) + t.Fatalf("timed out waiting for balance to reflect reorg handling: got %v, want %v (MEL reorg logged=%v, TxStreamer reorg logged=%v)", + latestBalance, expectedBalance, + logHandler.WasLogged("MEL detected L1 reorg"), + logHandler.WasLogged("TransactionStreamer: Reorg detected!")) } } } - // Verify that after reorg handling, resulting balance in the account is correct - newBalance, err := builder.L2.Client.BalanceAt(ctx, txOpts.From, nil) - if err != nil { - t.Fatalf("BalanceAt(%v) unexpected error: %v", txOpts.From, err) - } - if got := new(big.Int); got.Sub(newBalance, oldBalance).Cmp(txOpts.Value) != 0 { - t.Errorf("Got transferred: %v, want: %v", got, txOpts.Value) + // Verify that MEL detected the reorg (this should always be true if the balance is correct) + if !logHandler.WasLogged("MEL detected L1 reorg") { + t.Error("expected MEL to log reorg detection, but it did not") } } diff --git a/wavmio/common.go b/wavmio/common.go new file mode 100644 index 00000000000..955b3a55227 --- /dev/null +++ b/wavmio/common.go @@ -0,0 +1,30 @@ +// Copyright 2026-2027, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +package wavmio + +import ( + "encoding/hex" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" +) + +// To generate: +// key, _ := crypto.HexToECDSA("0000000000000000000000000000000000000000000000000000000000000001") +// sig, _ := crypto.Sign(make([]byte, 32), key) +// println(hex.EncodeToString(sig)) +const sampleSignature = "a0b37f8fba683cc68f6574cd43b39f0343a50008bf6ccea9d13231d9e7e2e1e411edc8d307254296264aebfc3dc76cd8b668373a072fd64665b50000e9fcce5201" + +func PopulateEcdsaCaches() { + signature, err := hex.DecodeString(sampleSignature) + if err != nil { + log.Warn("failed to decode sample signature to populate ECDSA cache", "err", err) + return + } + _, err = crypto.Ecrecover(make([]byte, 32), signature) + if err != nil { + log.Warn("failed to recover signature to populate ECDSA cache", "err", err) + return + } +}