diff --git a/common/batch/batch_header.go b/common/batch/batch_header.go index 9be5eb25a..ab5956312 100644 --- a/common/batch/batch_header.go +++ b/common/batch/batch_header.go @@ -16,6 +16,11 @@ type ( const ( expectedLengthV0 = 249 expectedLengthV1 = 257 + // V2 reuses the V1 wire format (257 bytes). The only semantic + // difference is that the 32-byte field at offset 57 stores + // keccak256(blobhash(0) || ... || blobhash(N-1)) instead of a + // single blob versioned hash. + expectedLengthV2 = 257 BatchHeaderVersion0 = 0 BatchHeaderVersion1 = 1 @@ -44,7 +49,7 @@ func (b BatchHeaderBytes) validate() error { return ErrInvalidBatchHeaderLength } case BatchHeaderVersion2: - if len(b) != expectedLengthV1 { + if len(b) != expectedLengthV2 { return ErrInvalidBatchHeaderLength } default: @@ -99,10 +104,32 @@ func (b BatchHeaderBytes) DataHash() (common.Hash, error) { return common.BytesToHash(b[25:57]), nil } +// BlobVersionedHash returns the EIP-4844 blob versioned hash recorded at +// offset [57:89]. This is only meaningful for V0/V1 batches, where the field +// holds the single blob's versioned hash. For V2 batches the same offset +// holds an aggregated hash; callers must use BlobHashesHash instead. func (b BatchHeaderBytes) BlobVersionedHash() (common.Hash, error) { if err := b.validate(); err != nil { return common.Hash{}, err } + version, _ := b.Version() + if version >= BatchHeaderVersion2 { + return common.Hash{}, errors.New("BlobVersionedHash is not available for V2+; use BlobHashesHash") + } + return common.BytesToHash(b[57:89]), nil +} + +// BlobHashesHash returns the aggregated blob hash recorded at offset [57:89] +// for V2+ batches, defined as keccak256(blobhash(0) || ... || blobhash(N-1)). +// V0/V1 batches do not aggregate and will return an error. +func (b BatchHeaderBytes) BlobHashesHash() (common.Hash, error) { + if err := b.validate(); err != nil { + return common.Hash{}, err + } + version, _ := b.Version() + if version < BatchHeaderVersion2 { + return common.Hash{}, errors.New("BlobHashesHash is only available for V2+; use BlobVersionedHash") + } return common.BytesToHash(b[57:89]), nil } diff --git a/common/batch/batch_header_test.go b/common/batch/batch_header_test.go new file mode 100644 index 000000000..b220901c0 --- /dev/null +++ b/common/batch/batch_header_test.go @@ -0,0 +1,86 @@ +package batch + +import ( + "math/big" + "testing" + + "github.com/morph-l2/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +// TestBatchHeaderV2 exercises the V2 header variant: it reuses the V1 wire +// layout (257 bytes) but the 32-byte field at offset 57 carries an aggregated +// blob hash (keccak256(blobhash(0)||...||blobhash(N-1))) rather than a single +// versioned hash. Parsing helpers must route callers accordingly. +func TestBatchHeaderV2(t *testing.T) { + aggregated := common.BigToHash(big.NewInt(0xABCDEF)) + + // Start from a V1 encoding (identical byte layout), then flip the version + // byte to V2. This matches the on-chain behavior where a V2 header is + // produced by tx-submitter with the aggregated hash stored at offset 57. + raw := BatchHeaderV1{ + BatchHeaderV0: BatchHeaderV0{ + BatchIndex: 42, + L1MessagePopped: 1, + TotalL1MessagePopped: 3, + DataHash: common.BigToHash(big.NewInt(0x11)), + BlobVersionedHash: aggregated, + PrevStateRoot: common.BigToHash(big.NewInt(0x22)), + PostStateRoot: common.BigToHash(big.NewInt(0x33)), + WithdrawalRoot: common.BigToHash(big.NewInt(0x44)), + SequencerSetVerifyHash: common.BigToHash(big.NewInt(0x55)), + ParentBatchHash: common.BigToHash(big.NewInt(0x66)), + }, + LastBlockNumber: 9_876, + }.Bytes() + raw[0] = BatchHeaderVersion2 + + version, err := raw.Version() + require.NoError(t, err) + require.EqualValues(t, BatchHeaderVersion2, version) + + batchIndex, err := raw.BatchIndex() + require.NoError(t, err) + require.EqualValues(t, 42, batchIndex) + + lastBlockNumber, err := raw.LastBlockNumber() + require.NoError(t, err) + require.EqualValues(t, 9_876, lastBlockNumber) + + // V2 headers must route callers to BlobHashesHash; the legacy accessor + // intentionally errors to prevent silent mis-use. + _, err = raw.BlobVersionedHash() + require.Error(t, err) + + aggHash, err := raw.BlobHashesHash() + require.NoError(t, err) + require.EqualValues(t, aggregated, aggHash) + + // Length check: a V2 header with the wrong length must fail validate(). + short := make(BatchHeaderBytes, expectedLengthV2-1) + short[0] = BatchHeaderVersion2 + _, err = short.BatchIndex() + require.ErrorIs(t, err, ErrInvalidBatchHeaderLength) +} + +// TestBlobHashesHashUnavailableForLegacy guards the inverse direction: V0 and +// V1 headers must reject BlobHashesHash so that callers reach for the correct +// accessor. +func TestBlobHashesHashUnavailableForLegacy(t *testing.T) { + v0 := BatchHeaderV0{ + BatchIndex: 1, + BlobVersionedHash: EmptyVersionedHash, + }.Bytes() + _, err := v0.BlobHashesHash() + require.Error(t, err) + + v1 := BatchHeaderV1{ + BatchHeaderV0: BatchHeaderV0{ + BatchIndex: 2, + BlobVersionedHash: EmptyVersionedHash, + }, + LastBlockNumber: 10, + }.Bytes() + _, err = v1.BlobHashesHash() + require.Error(t, err) +} diff --git a/common/batch/blob.go b/common/batch/blob.go index 99fcab440..088e55a6f 100644 --- a/common/batch/blob.go +++ b/common/batch/blob.go @@ -118,77 +118,117 @@ func DecodeTxsFromBytes(txsBytes []byte) (eth.Transactions, error) { txs := make(eth.Transactions, 0) for { var ( - firstByte byte - fullTxBytes []byte - innerTx eth.TxData - err error + typeByte byte + err error ) - if err = binary.Read(reader, binary.BigEndian, &firstByte); err != nil { - // if the blob byte array is completely consumed, then break the loop + if err = binary.Read(reader, binary.BigEndian, &typeByte); err != nil { if err == io.EOF { break } return nil, err } - // zero byte is found after valid tx bytes, break the loop - if firstByte == 0 { + if typeByte == 0 { break } - switch firstByte { - case eth.AccessListTxType: - if err := binary.Read(reader, binary.BigEndian, &firstByte); err != nil { + switch typeByte { + case eth.AccessListTxType, eth.DynamicFeeTxType, eth.SetCodeTxType: + tx, err := decodeTypedTx(typeByte, reader) + if err != nil { return nil, err } - innerTx = new(eth.AccessListTx) - case eth.DynamicFeeTxType: - if err := binary.Read(reader, binary.BigEndian, &firstByte); err != nil { + txs = append(txs, tx) + + case eth.MorphTxType: + tx, err := decodeMorphTx(reader) + if err != nil { return nil, err } - innerTx = new(eth.DynamicFeeTx) - case eth.SetCodeTxType: - if err := binary.Read(reader, binary.BigEndian, &firstByte); err != nil { - return nil, err + txs = append(txs, tx) + + default: + if typeByte <= 0xf7 { + return nil, fmt.Errorf("not supported tx type: %d", typeByte) } - innerTx = new(eth.SetCodeTx) - case eth.MorphTxType: - if err := binary.Read(reader, binary.BigEndian, &firstByte); err != nil { + fullTxBytes, err := extractInnerTxFullBytes(typeByte, reader) + if err != nil { return nil, err } - innerTx = new(eth.MorphTx) - default: - if firstByte <= 0xf7 { // legacy tx first byte must be greater than 0xf7(247) - return nil, fmt.Errorf("not supported tx type: %d", firstByte) + var inner eth.LegacyTx + if err = rlp.DecodeBytes(fullTxBytes, &inner); err != nil { + return nil, err } - innerTx = new(eth.LegacyTx) + txs = append(txs, eth.NewTx(&inner)) } + } + return txs, nil +} - // we support the tx types of LegacyTxType/AccessListTxType/DynamicFeeTxType - //if firstByte == eth.AccessListTxType || firstByte == eth.DynamicFeeTxType { - // // the firstByte here is used to indicate tx type, so skip it - // if err := binary.Read(reader, binary.BigEndian, &firstByte); err != nil { - // return nil, err - // } - //} else if firstByte <= 0xf7 { // legacy tx first byte must be greater than 0xf7(247) - // return nil, fmt.Errorf("not supported tx type: %d", firstByte) - //} - fullTxBytes, err = extractInnerTxFullBytes(firstByte, reader) - if err != nil { - return nil, err - } - if err = rlp.DecodeBytes(fullTxBytes, innerTx); err != nil { +// decodeTypedTx decodes a standard EIP-2718 typed tx (AccessList, DynamicFee, SetCode) +// from the reader. The type byte has already been consumed; the next byte is the RLP prefix. +func decodeTypedTx(typeByte byte, reader io.Reader) (*eth.Transaction, error) { + var rlpPrefix byte + if err := binary.Read(reader, binary.BigEndian, &rlpPrefix); err != nil { + return nil, err + } + rlpBytes, err := extractInnerTxFullBytes(rlpPrefix, reader) + if err != nil { + return nil, err + } + txBinary := make([]byte, 0, 1+len(rlpBytes)) + txBinary = append(txBinary, typeByte) + txBinary = append(txBinary, rlpBytes...) + + var tx eth.Transaction + if err := tx.UnmarshalBinary(txBinary); err != nil { + return nil, err + } + return &tx, nil +} + +// decodeMorphTx decodes a MorphTx from the reader. The type byte (0x7f) has already +// been consumed. MorphTx has two wire formats: +// - V0: type(0x7f) || RLP(fields) — next byte is RLP prefix (>= 0xC0) +// - V1: type(0x7f) || version(0x01) || RLP(fields) — next byte is version, then RLP prefix +func decodeMorphTx(reader io.Reader) (*eth.Transaction, error) { + var nextByte byte + if err := binary.Read(reader, binary.BigEndian, &nextByte); err != nil { + return nil, err + } + + var versionPrefix []byte + rlpFirstByte := nextByte + if nextByte != 0 && nextByte < 0xC0 { + // V1+: nextByte is the version byte, read the actual RLP prefix + versionPrefix = []byte{nextByte} + if err := binary.Read(reader, binary.BigEndian, &rlpFirstByte); err != nil { return nil, err } - txs = append(txs, eth.NewTx(innerTx)) } - return txs, nil + + rlpBytes, err := extractInnerTxFullBytes(rlpFirstByte, reader) + if err != nil { + return nil, err + } + + txBinary := make([]byte, 0, 1+len(versionPrefix)+len(rlpBytes)) + txBinary = append(txBinary, eth.MorphTxType) + txBinary = append(txBinary, versionPrefix...) + txBinary = append(txBinary, rlpBytes...) + + var tx eth.Transaction + if err := tx.UnmarshalBinary(txBinary); err != nil { + return nil, err + } + return &tx, nil } func extractInnerTxFullBytes(firstByte byte, reader io.Reader) ([]byte, error) { - //the occupied byte length for storing the size of the following rlp encoded bytes sizeByteLen := firstByte - 0xf7 + if sizeByteLen > 4 { + return nil, fmt.Errorf("invalid RLP size byte length: %d (firstByte=0x%x)", sizeByteLen, firstByte) + } - // the size of the following rlp encoded bytes sizeByte := make([]byte, sizeByteLen) if err := binary.Read(reader, binary.BigEndian, sizeByte); err != nil { return nil, err diff --git a/node/derivation/batch_info.go b/node/derivation/batch_info.go index 8afc95198..692633f66 100644 --- a/node/derivation/batch_info.go +++ b/node/derivation/batch_info.go @@ -11,6 +11,7 @@ import ( geth "github.com/morph-l2/go-ethereum/eth" "github.com/morph-l2/go-ethereum/eth/catalyst" + commonbatch "morph-l2/common/batch" "morph-l2/node/types" "morph-l2/node/zstd" ) @@ -81,7 +82,7 @@ func (bi *BatchInfo) ParseBatch(batch geth.RPCRollupBatch) error { if len(batch.Sidecar.Blobs) == 0 { return fmt.Errorf("blobs length can not be zero") } - parentBatchHeader := types.BatchHeaderBytes(batch.ParentBatchHeader) + parentBatchHeader := commonbatch.BatchHeaderBytes(batch.ParentBatchHeader) parentBatchIndex, err := parentBatchHeader.BatchIndex() if err != nil { return fmt.Errorf("decode batch header index error:%v", err) @@ -103,10 +104,10 @@ func (bi *BatchInfo) ParseBatch(batch geth.RPCRollupBatch) error { // must concatenate all blob bodies first and decompress once; per-blob // decompression would fail on the second blob since it is not a // standalone zstd stream. - compressed := make([]byte, 0, len(batch.Sidecar.Blobs)*types.MaxBlobBytesSize) + compressed := make([]byte, 0, len(batch.Sidecar.Blobs)*commonbatch.MaxBlobBytesSize) for i := range batch.Sidecar.Blobs { blobCopy := batch.Sidecar.Blobs[i] - blobData, err := types.RetrieveBlobBytes(&blobCopy) + blobData, err := commonbatch.RetrieveBlobBytes(&blobCopy) if err != nil { return err } @@ -166,7 +167,7 @@ func (bi *BatchInfo) ParseBatch(batch geth.RPCRollupBatch) error { txsData = batchBytes[bcLen:] } - data, err := types.DecodeTxsFromBytes(txsData) + data, err := commonbatch.DecodeTxsFromBytes(txsData) if err != nil { return err } diff --git a/node/derivation/batch_info_test.go b/node/derivation/batch_info_test.go index 3e1c19c8f..cffb57a83 100644 --- a/node/derivation/batch_info_test.go +++ b/node/derivation/batch_info_test.go @@ -11,6 +11,7 @@ import ( geth "github.com/morph-l2/go-ethereum/eth" "github.com/stretchr/testify/require" + commonbatch "morph-l2/common/batch" "morph-l2/node/types" "morph-l2/node/zstd" ) @@ -36,10 +37,10 @@ func buildBlockContexts(startBlock uint64, count int) []byte { // is one below `nextStartBlock`, so that ParseBatch can derive blockCount via // the (batch.LastBlockNumber - parent.LastBlockNumber) path. func buildV1ParentHeader(parentIndex, nextStartBlock uint64) []byte { - return types.BatchHeaderV1{ - BatchHeaderV0: types.BatchHeaderV0{ + return commonbatch.BatchHeaderV1{ + BatchHeaderV0: commonbatch.BatchHeaderV0{ BatchIndex: parentIndex, - BlobVersionedHash: types.EmptyVersionedHash, + BlobVersionedHash: commonbatch.EmptyVersionedHash, }, LastBlockNumber: nextStartBlock - 1, }.Bytes() @@ -51,12 +52,12 @@ func buildV1ParentHeader(parentIndex, nextStartBlock uint64) []byte { func splitCompressedIntoBlobs(t *testing.T, compressed []byte) []kzg4844.Blob { t.Helper() var blobs []kzg4844.Blob - for offset := 0; offset < len(compressed); offset += types.MaxBlobBytesSize { - end := offset + types.MaxBlobBytesSize + for offset := 0; offset < len(compressed); offset += commonbatch.MaxBlobBytesSize { + end := offset + commonbatch.MaxBlobBytesSize if end > len(compressed) { end = len(compressed) } - blob, err := types.MakeBlobCanonical(compressed[offset:end]) + blob, err := commonbatch.MakeBlobCanonical(compressed[offset:end]) require.NoError(t, err) blobs = append(blobs, *blob) } @@ -85,7 +86,7 @@ func TestParseBatchSingleBlob(t *testing.T) { compressed, err := zstd.CompressBatchBytes(payload) require.NoError(t, err) - require.LessOrEqual(t, len(compressed), types.MaxBlobBytesSize, + require.LessOrEqual(t, len(compressed), commonbatch.MaxBlobBytesSize, "single-blob test expects compressed payload to fit in one blob") blobs := splitCompressedIntoBlobs(t, compressed) @@ -133,7 +134,7 @@ func TestParseBatchMultiBlob(t *testing.T) { // 1 byte tx terminator + ~1.2x blob capacity of incompressible noise to // guarantee the zstd output straddles a blob boundary. - padLen := types.MaxBlobBytesSize + types.MaxBlobBytesSize/5 + padLen := commonbatch.MaxBlobBytesSize + commonbatch.MaxBlobBytesSize/5 pad := make([]byte, padLen) _, err := rand.Read(pad) require.NoError(t, err) @@ -145,7 +146,7 @@ func TestParseBatchMultiBlob(t *testing.T) { compressed, err := zstd.CompressBatchBytes(payload) require.NoError(t, err) - require.Greater(t, len(compressed), types.MaxBlobBytesSize, + require.Greater(t, len(compressed), commonbatch.MaxBlobBytesSize, "multi-blob test requires compressed payload to overflow a single blob") blobs := splitCompressedIntoBlobs(t, compressed) @@ -187,13 +188,13 @@ func TestParseBatchMultiBlob(t *testing.T) { // output. Keeping this explicit protects the invariant even if ParseBatch is // later refactored to hide the concatenation step. func TestParseBatchMultiBlobConcatDecompressInvariant(t *testing.T) { - pad := make([]byte, types.MaxBlobBytesSize+types.MaxBlobBytesSize/5) + pad := make([]byte, commonbatch.MaxBlobBytesSize+commonbatch.MaxBlobBytesSize/5) _, err := rand.Read(pad) require.NoError(t, err) compressed, err := zstd.CompressBatchBytes(pad) require.NoError(t, err) - require.Greater(t, len(compressed), types.MaxBlobBytesSize) + require.Greater(t, len(compressed), commonbatch.MaxBlobBytesSize) blobs := splitCompressedIntoBlobs(t, compressed) require.GreaterOrEqual(t, len(blobs), 2) @@ -201,7 +202,7 @@ func TestParseBatchMultiBlobConcatDecompressInvariant(t *testing.T) { // In-order concatenation round-trips. var concat []byte for i := range blobs { - body, err := types.RetrieveBlobBytes(&blobs[i]) + body, err := commonbatch.RetrieveBlobBytes(&blobs[i]) require.NoError(t, err) concat = append(concat, body...) } @@ -213,7 +214,7 @@ func TestParseBatchMultiBlobConcatDecompressInvariant(t *testing.T) { // either error or yield a different payload. var reversed []byte for i := len(blobs) - 1; i >= 0; i-- { - body, err := types.RetrieveBlobBytes(&blobs[i]) + body, err := commonbatch.RetrieveBlobBytes(&blobs[i]) require.NoError(t, err) reversed = append(reversed, body...) } diff --git a/node/types/batch_header.go b/node/types/batch_header.go index 2755e4950..c9dba67ca 100644 --- a/node/types/batch_header.go +++ b/node/types/batch_header.go @@ -1,5 +1,21 @@ package types +// DEPRECATED: this file is a duplicate of morph-l2/common/batch's +// batch_header.go and is kept alive only because tx-submitter/utils/utils.go +// still imports BatchHeaderBytes from here. node/types cannot be turned into +// a thin shim re-exporting common/batch because that would close an import +// cycle: common/batch already depends on tx-submitter/db (via BatchCache), +// which depends on tx-submitter/utils, which would then depend back on +// common/batch. +// +// Cleanup path (out of scope for this PR; should be done by the tx-submitter +// owners alongside moving BatchCache out of common/batch): +// 1. Move common/batch/batch_cache.go, batch_storage.go, batch_query.go +// down to tx-submitter/batch/, so common/batch becomes a true leaf +// (depends on nothing under tx-submitter/). +// 2. Switch tx-submitter/utils/utils.go to import morph-l2/common/batch. +// 3. Delete this file. + import ( "encoding/binary" "errors" diff --git a/node/types/batch_test.go b/node/types/batch_test.go deleted file mode 100644 index de0525626..000000000 --- a/node/types/batch_test.go +++ /dev/null @@ -1,159 +0,0 @@ -package types - -import ( - "math/big" - "morph-l2/bindings/bindings" - "testing" - - "github.com/morph-l2/go-ethereum/common" - "github.com/stretchr/testify/require" -) - -func TestBatchHeader(t *testing.T) { - expectedBatchHeaderV0 := BatchHeaderV0{ - BatchIndex: 10, - L1MessagePopped: 5, - TotalL1MessagePopped: 20, - DataHash: common.BigToHash(big.NewInt(100)), - BlobVersionedHash: EmptyVersionedHash, - PrevStateRoot: common.BigToHash(big.NewInt(101)), - PostStateRoot: common.BigToHash(big.NewInt(102)), - WithdrawalRoot: common.BigToHash(big.NewInt(103)), - SequencerSetVerifyHash: common.BigToHash(big.NewInt(104)), - ParentBatchHash: common.BigToHash(big.NewInt(200)), - } - batchHeaderBytes := expectedBatchHeaderV0.Bytes() - - version, err := batchHeaderBytes.Version() - require.NoError(t, err) - batchIndex, err := batchHeaderBytes.BatchIndex() - require.NoError(t, err) - l1MessagePopped, err := batchHeaderBytes.L1MessagePopped() - require.NoError(t, err) - totalL1MessagePopped, err := batchHeaderBytes.TotalL1MessagePopped() - require.NoError(t, err) - dataHash, err := batchHeaderBytes.DataHash() - require.NoError(t, err) - blobVersionedHash, err := batchHeaderBytes.BlobVersionedHash() - require.NoError(t, err) - prevStateRoot, err := batchHeaderBytes.PrevStateRoot() - require.NoError(t, err) - postStateRoot, err := batchHeaderBytes.PostStateRoot() - require.NoError(t, err) - withdrawalRoot, err := batchHeaderBytes.WithdrawalRoot() - require.NoError(t, err) - sequencerSetVerifyHash, err := batchHeaderBytes.SequencerSetVerifyHash() - require.NoError(t, err) - parentBatchHash, err := batchHeaderBytes.ParentBatchHash() - require.NoError(t, err) - - require.EqualValues(t, 0, version) - require.EqualValues(t, expectedBatchHeaderV0.BatchIndex, batchIndex) - require.EqualValues(t, expectedBatchHeaderV0.L1MessagePopped, l1MessagePopped) - require.EqualValues(t, expectedBatchHeaderV0.TotalL1MessagePopped, totalL1MessagePopped) - require.EqualValues(t, expectedBatchHeaderV0.DataHash, dataHash) - require.EqualValues(t, expectedBatchHeaderV0.BlobVersionedHash, blobVersionedHash) - require.EqualValues(t, expectedBatchHeaderV0.PrevStateRoot, prevStateRoot) - require.EqualValues(t, expectedBatchHeaderV0.PostStateRoot, postStateRoot) - require.EqualValues(t, expectedBatchHeaderV0.WithdrawalRoot, withdrawalRoot) - require.EqualValues(t, expectedBatchHeaderV0.SequencerSetVerifyHash, sequencerSetVerifyHash) - require.EqualValues(t, expectedBatchHeaderV0.ParentBatchHash, parentBatchHash) - - expectedBatchHeaderV1 := BatchHeaderV1{ - BatchHeaderV0: expectedBatchHeaderV0, - LastBlockNumber: 1000, - }.Bytes() - version, err = expectedBatchHeaderV1.Version() - require.NoError(t, err) - lastBlockNumber, err := expectedBatchHeaderV1.LastBlockNumber() - require.NoError(t, err) - require.EqualValues(t, 1, version) - require.EqualValues(t, 1000, lastBlockNumber) -} - -// TestBatchHeaderV2 exercises the V2 header variant: it reuses the V1 wire -// layout (257 bytes) but the 32-byte field at offset 57 carries an aggregated -// blob hash (keccak256(blobhash(0)||...||blobhash(N-1))) rather than a single -// versioned hash. Parsing helpers must route callers accordingly. -func TestBatchHeaderV2(t *testing.T) { - aggregated := common.BigToHash(big.NewInt(0xABCDEF)) - - // Start from a V1 encoding (identical byte layout), then flip the version - // byte to V2. This matches the on-chain behavior where a V2 header is - // produced by tx-submitter with the aggregated hash stored at offset 57. - raw := BatchHeaderV1{ - BatchHeaderV0: BatchHeaderV0{ - BatchIndex: 42, - L1MessagePopped: 1, - TotalL1MessagePopped: 3, - DataHash: common.BigToHash(big.NewInt(0x11)), - BlobVersionedHash: aggregated, - PrevStateRoot: common.BigToHash(big.NewInt(0x22)), - PostStateRoot: common.BigToHash(big.NewInt(0x33)), - WithdrawalRoot: common.BigToHash(big.NewInt(0x44)), - SequencerSetVerifyHash: common.BigToHash(big.NewInt(0x55)), - ParentBatchHash: common.BigToHash(big.NewInt(0x66)), - }, - LastBlockNumber: 9_876, - }.Bytes() - raw[0] = BatchHeaderVersion2 - - version, err := raw.Version() - require.NoError(t, err) - require.EqualValues(t, BatchHeaderVersion2, version) - - batchIndex, err := raw.BatchIndex() - require.NoError(t, err) - require.EqualValues(t, 42, batchIndex) - - lastBlockNumber, err := raw.LastBlockNumber() - require.NoError(t, err) - require.EqualValues(t, 9_876, lastBlockNumber) - - // V2 headers must route callers to BlobHashesHash; the legacy accessor - // intentionally errors to prevent silent mis-use. - _, err = raw.BlobVersionedHash() - require.Error(t, err) - - aggHash, err := raw.BlobHashesHash() - require.NoError(t, err) - require.EqualValues(t, aggregated, aggHash) - - // Length check: a V2 header with the wrong length must fail validate(). - short := make(BatchHeaderBytes, expectedLengthV2-1) - short[0] = BatchHeaderVersion2 - _, err = short.BatchIndex() - require.ErrorIs(t, err, ErrInvalidBatchHeaderLength) -} - -// TestBlobHashesHashUnavailableForLegacy guards the inverse direction: V0 and -// V1 headers must reject BlobHashesHash so that callers reach for the correct -// accessor. -func TestBlobHashesHashUnavailableForLegacy(t *testing.T) { - v0 := BatchHeaderV0{ - BatchIndex: 1, - BlobVersionedHash: EmptyVersionedHash, - }.Bytes() - _, err := v0.BlobHashesHash() - require.Error(t, err) - - v1 := BatchHeaderV1{ - BatchHeaderV0: BatchHeaderV0{ - BatchIndex: 2, - BlobVersionedHash: EmptyVersionedHash, - }, - LastBlockNumber: 10, - }.Bytes() - _, err = v1.BlobHashesHash() - require.Error(t, err) -} - -func TestMethodID(t *testing.T) { - beforeSkipABI, err := LegacyRollupMetaData.GetAbi() - require.NoError(t, err) - beforeMoveBlockCtxABI, err := BeforeMoveBlockCtxABI.GetAbi() - require.NoError(t, err) - currentABI, err := bindings.RollupMetaData.GetAbi() - require.NoError(t, err) - require.NotEqualValues(t, beforeSkipABI.Methods["commitBatch"].ID, beforeMoveBlockCtxABI.Methods["commitBatch"].ID, currentABI.Methods["commitBatch"].ID) -} diff --git a/node/types/blob.go b/node/types/blob.go deleted file mode 100644 index 49b158bc1..000000000 --- a/node/types/blob.go +++ /dev/null @@ -1,159 +0,0 @@ -package types - -import ( - "bytes" - "encoding/binary" - "fmt" - "io" - - eth "github.com/morph-l2/go-ethereum/core/types" - "github.com/morph-l2/go-ethereum/crypto/kzg4844" - "github.com/morph-l2/go-ethereum/rlp" -) - -const MaxBlobBytesSize = 4096 * 31 - -func RetrieveBlobBytes(blob *kzg4844.Blob) ([]byte, error) { - data := make([]byte, MaxBlobBytesSize) - for i := 0; i < 4096; i++ { - if blob[i*32] != 0 { - return nil, fmt.Errorf("invalid blob, found non-zero high order byte %x of field element %d", data[i*32], i) - } - copy(data[i*31:i*31+31], blob[i*32+1:i*32+32]) - } - return data, nil -} - -func DecodeTxsFromBytes(txsBytes []byte) (eth.Transactions, error) { - reader := bytes.NewReader(txsBytes) - txs := make(eth.Transactions, 0) - for { - var ( - typeByte byte - err error - ) - if err = binary.Read(reader, binary.BigEndian, &typeByte); err != nil { - if err == io.EOF { - break - } - return nil, err - } - if typeByte == 0 { - break - } - - switch typeByte { - case eth.AccessListTxType, eth.DynamicFeeTxType, eth.SetCodeTxType: - tx, err := decodeTypedTx(typeByte, reader) - if err != nil { - return nil, err - } - txs = append(txs, tx) - - case eth.MorphTxType: - tx, err := decodeMorphTx(reader) - if err != nil { - return nil, err - } - txs = append(txs, tx) - - default: - if typeByte <= 0xf7 { - return nil, fmt.Errorf("not supported tx type: %d", typeByte) - } - fullTxBytes, err := extractInnerTxFullBytes(typeByte, reader) - if err != nil { - return nil, err - } - var inner eth.LegacyTx - if err = rlp.DecodeBytes(fullTxBytes, &inner); err != nil { - return nil, err - } - txs = append(txs, eth.NewTx(&inner)) - } - } - return txs, nil -} - -// decodeTypedTx decodes a standard EIP-2718 typed tx (AccessList, DynamicFee, SetCode) -// from the reader. The type byte has already been consumed; the next byte is the RLP prefix. -func decodeTypedTx(typeByte byte, reader io.Reader) (*eth.Transaction, error) { - var rlpPrefix byte - if err := binary.Read(reader, binary.BigEndian, &rlpPrefix); err != nil { - return nil, err - } - rlpBytes, err := extractInnerTxFullBytes(rlpPrefix, reader) - if err != nil { - return nil, err - } - txBinary := make([]byte, 0, 1+len(rlpBytes)) - txBinary = append(txBinary, typeByte) - txBinary = append(txBinary, rlpBytes...) - - var tx eth.Transaction - if err := tx.UnmarshalBinary(txBinary); err != nil { - return nil, err - } - return &tx, nil -} - -// decodeMorphTx decodes a MorphTx from the reader. The type byte (0x7f) has already -// been consumed. MorphTx has two wire formats: -// - V0: type(0x7f) || RLP(fields) — next byte is RLP prefix (>= 0xC0) -// - V1: type(0x7f) || version(0x01) || RLP(fields) — next byte is version, then RLP prefix -func decodeMorphTx(reader io.Reader) (*eth.Transaction, error) { - var nextByte byte - if err := binary.Read(reader, binary.BigEndian, &nextByte); err != nil { - return nil, err - } - - var versionPrefix []byte - rlpFirstByte := nextByte - if nextByte != 0 && nextByte < 0xC0 { - // V1+: nextByte is the version byte, read the actual RLP prefix - versionPrefix = []byte{nextByte} - if err := binary.Read(reader, binary.BigEndian, &rlpFirstByte); err != nil { - return nil, err - } - } - - rlpBytes, err := extractInnerTxFullBytes(rlpFirstByte, reader) - if err != nil { - return nil, err - } - - txBinary := make([]byte, 0, 1+len(versionPrefix)+len(rlpBytes)) - txBinary = append(txBinary, eth.MorphTxType) - txBinary = append(txBinary, versionPrefix...) - txBinary = append(txBinary, rlpBytes...) - - var tx eth.Transaction - if err := tx.UnmarshalBinary(txBinary); err != nil { - return nil, err - } - return &tx, nil -} - -func extractInnerTxFullBytes(firstByte byte, reader io.Reader) ([]byte, error) { - sizeByteLen := firstByte - 0xf7 - if sizeByteLen > 4 { - return nil, fmt.Errorf("invalid RLP size byte length: %d (firstByte=0x%x)", sizeByteLen, firstByte) - } - - sizeByte := make([]byte, sizeByteLen) - if err := binary.Read(reader, binary.BigEndian, sizeByte); err != nil { - return nil, err - } - size := binary.BigEndian.Uint32(append(make([]byte, 4-len(sizeByte)), sizeByte...)) - - txRaw := make([]byte, size) - if err := binary.Read(reader, binary.BigEndian, txRaw); err != nil { - return nil, err - } - fullTxBytes := make([]byte, 1+uint32(sizeByteLen)+size) - copy(fullTxBytes[:1], []byte{firstByte}) - copy(fullTxBytes[1:1+sizeByteLen], sizeByte) - copy(fullTxBytes[1+sizeByteLen:], txRaw) - - return fullTxBytes, nil -} diff --git a/oracle/oracle/batch.go b/oracle/oracle/batch.go index 8e1eb9c23..df61159a6 100644 --- a/oracle/oracle/batch.go +++ b/oracle/oracle/batch.go @@ -9,8 +9,8 @@ import ( "time" "morph-l2/bindings/bindings" + commonbatch "morph-l2/common/batch" "morph-l2/node/derivation" - nodetypes "morph-l2/node/types" "morph-l2/oracle/backoff" "github.com/morph-l2/go-ethereum/accounts/abi/bind" @@ -163,7 +163,7 @@ func (o *Oracle) getBatchSubmissionByLogs(rLogs []types.Log, recordBatchSubmissi PostStateRoot: common.BytesToHash(rollupBatchData.PostStateRoot[:]), WithdrawRoot: common.BytesToHash(rollupBatchData.WithdrawalRoot[:]), } - parentBatchHeader := nodetypes.BatchHeaderBytes(batch.ParentBatchHeader) + parentBatchHeader := commonbatch.BatchHeaderBytes(batch.ParentBatchHeader) parentVersion, err := parentBatchHeader.Version() if err != nil { return fmt.Errorf("decode parent batch version error:%v", err)