Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,4 @@ require (
gopkg.in/yaml.v2 v2.4.0 // indirect
)

replace github.com/ethereum/go-ethereum => github.com/taikoxyz/taiko-geth v1.18.1-0.20260922032335-4e001283f488
replace github.com/ethereum/go-ethereum => github.com/taikoxyz/taiko-geth v1.18.1-0.20260924044618-8e98046bfd6a
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE=
github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
github.com/taikoxyz/taiko-geth v1.18.1-0.20260922032335-4e001283f488 h1:VIUyRvRTR8U4v8MP8FtnaChsW2WBfb8pt96ZwgrAtSE=
github.com/taikoxyz/taiko-geth v1.18.1-0.20260922032335-4e001283f488/go.mod h1:KHcRXfGOUfUmKg51IhQ0IowiqZ6PqZf08CMtk0g5K1o=
github.com/taikoxyz/taiko-geth v1.18.1-0.20260924044618-8e98046bfd6a h1:Vtn63Z+JyxXuSvL8rcUs+2HD26sEsZtPj4lMOB72F+g=
github.com/taikoxyz/taiko-geth v1.18.1-0.20260924044618-8e98046bfd6a/go.mod h1:KHcRXfGOUfUmKg51IhQ0IowiqZ6PqZf08CMtk0g5K1o=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
Expand Down
3 changes: 3 additions & 0 deletions internal/prover/manifest_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,9 @@ func decodeManifestPayload(payload []byte, offset uint64, maxBlocks int) (shasta
if err := rlp.DecodeBytes(decoded, &manifest); err != nil {
return shastaSourceManifest{}, fmt.Errorf("decode manifest rlp: %w", err)
}
if err := validateManifestTransactions(&manifest); err != nil {
return shastaSourceManifest{}, fmt.Errorf("validate manifest transactions: %w", err)
}
if len(manifest.Blocks) > maxBlocks {
return shastaSourceManifest{}, fmt.Errorf("manifest block count %d exceeds max %d", len(manifest.Blocks), maxBlocks)
}
Expand Down
225 changes: 225 additions & 0 deletions internal/prover/manifest_vectors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package prover

import (
"bytes"
"encoding/hex"
"encoding/json"
"math/big"
"os"
"path/filepath"
"runtime"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
)

type manifestVectorCorpus struct {
SchemaVersion int `json:"schema_version"`
Cases []manifestVector `json:"cases"`
}

type manifestVector struct {
Name string `json:"name"`
Family string `json:"family"`
PayloadHex string `json:"payload_hex"`
Offset uint64 `json:"offset"`
ExpectDefault bool `json:"expect_default"`
ExpectedManifest string `json:"expected_manifest_rlp_hex"`
}

func loadManifestVectors(t *testing.T) []manifestVector {
t.Helper()

_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("resolve manifest vector test path")
}
raw, err := os.ReadFile(filepath.Join(
filepath.Dir(file),
"../../testdata/derivation_vectors/manifest_cases.json",
))
if err != nil {
t.Fatalf("read manifest vectors: %v", err)
}

var corpus manifestVectorCorpus
if err := json.Unmarshal(raw, &corpus); err != nil {
t.Fatalf("decode manifest vectors: %v", err)
}
if corpus.SchemaVersion != 1 {
t.Fatalf("manifest vector schema version %d, want 1", corpus.SchemaVersion)
}
if len(corpus.Cases) != 136 {
t.Fatalf("manifest vector count %d, want 136", len(corpus.Cases))
}

familyCounts := make(map[string]int)
for _, vector := range corpus.Cases {
familyCounts[vector.Family]++
}
for family, want := range map[string]int{"f8": 17, "f9": 94, "framing": 25} {
if got := familyCounts[family]; got != want {
t.Fatalf("manifest vector family %q count %d, want %d", family, got, want)
}
delete(familyCounts, family)
}
if len(familyCounts) != 0 {
t.Fatalf("unexpected manifest vector families: %v", familyCounts)
}
return corpus.Cases
}

func manifestVectorBytes(t *testing.T, value string) []byte {
t.Helper()
raw, err := hex.DecodeString(value)
if err != nil {
t.Fatalf("decode vector hex: %v", err)
}
return raw
}

func decodeManifestVector(t *testing.T, vector manifestVector) shastaSourceManifest {
t.Helper()
payload := manifestVectorBytes(t, vector.PayloadHex)
manifest, err := decodeBlobBackedSourceManifest(
blobSourceDataView{TxDataFromBlob: [][]byte{encodeTestKonaBlob(t, payload)}},
vector.Offset,
shastaUnzenDerivationSourceLimit,
)
if err != nil {
t.Fatalf("decode manifest vector: %v", err)
}
return manifest
}

func encodeManifestVectorResult(t *testing.T, manifest shastaSourceManifest) []byte {
t.Helper()
encoded, err := rlp.EncodeToBytes(manifest)
if err != nil {
t.Fatalf("encode decoded manifest: %v", err)
}
return encoded
}

// A lost grammar or zlib check retains a hostile source, or defaults a valid one,
// and therefore disagrees with the independently generated expected manifest RLP.
func TestManifestVectors(t *testing.T) {
vectors := loadManifestVectors(t)
for _, family := range []string{"f9", "framing", "f8"} {
t.Run(family, func(t *testing.T) {
for _, vector := range vectors {
if vector.Family != family {
continue
}
vector := vector
t.Run(vector.Name, func(t *testing.T) {
manifest := decodeManifestVector(t, vector)
if got := isDefaultSourceManifest(manifest); got != vector.ExpectDefault {
t.Fatalf("default=%t, want %t", got, vector.ExpectDefault)
}
got := encodeManifestVectorResult(t, manifest)
want := manifestVectorBytes(t, vector.ExpectedManifest)
if !bytes.Equal(got, want) {
t.Fatalf("manifest RLP %x, want %x", got, want)
}
})
}
})
}
}

func manifestVectorByName(t *testing.T, name string) manifestVector {
t.Helper()
for _, vector := range loadManifestVectors(t) {
if vector.Name == name {
return vector
}
}
t.Fatalf("manifest vector %q not found", name)
return manifestVector{}
}

func prepareManifestVector(
t *testing.T,
vector manifestVector,
forced bool,
proposalTimestamp uint64,
) shastaSourceManifest {
t.Helper()
parent := shastaManifestParentContext{
Header: &types.Header{
Number: big.NewInt(0),
Time: 99,
GasLimit: shastaMinBlockGasLimit,
},
AnchorBlockNumber: 4,
}
proposal := shastaProposalView{
Timestamp: proposalTimestamp,
Proposer: common.HexToAddress("0x9999"),
OriginBlockNumber: 5,
}
source := shastaDerivationSourceView{
IsForcedInclusion: forced,
BlobSlice: shastaBlobSliceView{
BlobHashes: []common.Hash{{1}},
Offset: vector.Offset,
},
}
payload := manifestVectorBytes(t, vector.PayloadHex)
manifest, err := prepareSourceManifest(
blobSourceDataView{TxDataFromBlob: [][]byte{encodeTestKonaBlob(t, payload)}},
source,
parent,
proposal,
params.TaikoInternalNetworkID.Uint64(),
0,
shastaUnzenDerivationSourceLimit,
)
if err != nil {
t.Fatalf("prepare manifest vector: %v", err)
}
return manifest
}

func assertInheritedDefaultManifest(t *testing.T, manifest shastaSourceManifest) {
t.Helper()
if len(manifest.Blocks) != 1 {
t.Fatalf("inherited default block count %d, want 1", len(manifest.Blocks))
}
block := manifest.Blocks[0]
if block.Timestamp != 100 ||
block.Coinbase != common.HexToAddress("0x9999") ||
block.AnchorBlockNumber != 4 ||
block.GasLimit != shastaMinBlockGasLimit ||
len(block.Transactions) != 0 {
t.Fatalf("unexpected inherited default block: %+v", block)
}
}

func TestManifestVectorsDefaultWholeSourceAndPreserveNeighbor(t *testing.T) {
invalid := manifestVectorByName(t, "type2_parity_2")
gotInvalid := prepareManifestVector(t, invalid, false, 100)
assertInheritedDefaultManifest(t, gotInvalid)

control := manifestVectorByName(t, "type2_control")
gotControl := prepareManifestVector(t, control, false, 100)
wantControl := manifestVectorBytes(t, control.ExpectedManifest)
if got := encodeManifestVectorResult(t, gotControl); !bytes.Equal(got, wantControl) {
t.Fatalf("valid neighboring source RLP %x, want %x", got, wantControl)
}

lateInvalid := decodeManifestVector(t, manifestVectorByName(t, "late_bad_transaction"))
if !isDefaultSourceManifest(lateInvalid) {
t.Fatalf("late bad transaction retained part of its source: %+v", lateInvalid)
}
}

func TestManifestVectorsForcedMultiBlockDefaultsAndInherits(t *testing.T) {
vector := manifestVectorByName(t, "two_valid_blocks")
manifest := prepareManifestVector(t, vector, true, 101)
assertInheritedDefaultManifest(t, manifest)
}
82 changes: 82 additions & 0 deletions internal/prover/transaction_encoding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package prover

import (
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/core/types"
)

// validateManifestTransactions checks execution-compatible transaction encodings before
// retaining a source. One incompatible transaction invalidates the whole source, preserving
// the normal default-source path that inserts an anchor before execution.
func validateManifestTransactions(manifest *shastaSourceManifest) error {
for blockIndex, block := range manifest.Blocks {
for txIndex, tx := range block.Transactions {
if err := validateManifestTransactionEncoding(tx); err != nil {
return fmt.Errorf("block %d transaction %d: %w", blockIndex, txIndex, err)
}
}
}
return nil
}

// validateManifestTransactionEncoding checks the grammar shared by the manifest decoder and
// the execution transaction decoder. Signer recovery and execution validity remain the
// execution engine's responsibility.
func validateManifestTransactionEncoding(tx *types.Transaction) error {
switch tx.Type() {
case types.LegacyTxType, types.AccessListTxType, types.DynamicFeeTxType,
types.BlobTxType, types.SetCodeTxType:
default:
return fmt.Errorf("unsupported transaction type %d", tx.Type())
}

for _, field := range []struct {
name string
value *big.Int
bits int
}{
{"maxFeePerGas", tx.GasFeeCap(), 128},
{"maxPriorityFeePerGas", tx.GasTipCap(), 128},
{"value", tx.Value(), 256},
} {
if field.value.Sign() < 0 || field.value.BitLen() > field.bits {
return fmt.Errorf("%s exceeds uint%d", field.name, field.bits)
}
}

v, r, s := tx.RawSignatureValues()
if r.Sign() < 0 || r.BitLen() > 256 || s.Sign() < 0 || s.BitLen() > 256 {
return fmt.Errorf("signature scalar exceeds uint256")
}
if tx.Type() == types.LegacyTxType {
if v.Cmp(big.NewInt(27)) == 0 || v.Cmp(big.NewInt(28)) == 0 {
return nil
}
if v.Cmp(big.NewInt(35)) < 0 {
return fmt.Errorf("invalid legacy signature v")
}
chainID := new(big.Int).Rsh(new(big.Int).Sub(v, big.NewInt(35)), 1)
if !chainID.IsUint64() {
return fmt.Errorf("legacy chain ID exceeds uint64")
}
return nil
}

if !tx.ChainId().IsUint64() {
return fmt.Errorf("chain ID exceeds uint64")
}
if v.Sign() < 0 || v.BitLen() > 1 {
return fmt.Errorf("invalid typed transaction parity")
}
if tx.Type() == types.BlobTxType {
if fee := tx.BlobGasFeeCap(); fee.Sign() < 0 || fee.BitLen() > 128 {
return fmt.Errorf("maxFeePerBlobGas exceeds uint128")
}
if tx.BlobTxSidecar() != nil {
return fmt.Errorf("blob sidecar wrapper is not an execution transaction")
}
}
return nil
}
Loading
Loading