diff --git a/prover/backend/invalidity/limitless/prove.go b/prover/backend/invalidity/limitless/prove.go index e4273ecb557..9d07862f49d 100644 --- a/prover/backend/invalidity/limitless/prove.go +++ b/prover/backend/invalidity/limitless/prove.go @@ -44,6 +44,8 @@ func Prove(cfg *config.Config, req *backendInvalidity.Request) (*backendInvalidi return nil, fmt.Errorf("could not decode the RlpEncodedTx: %w", err) } + backendInvalidity.SanityCheckInvalidityChainConfig(cfg, tx) + funcInput := backendInvalidity.FuncInput(req, cfg) // Build the zkEVM witness for the simulated execution diff --git a/prover/backend/invalidity/prove.go b/prover/backend/invalidity/prove.go index 68f3e582f44..9180e45f8ec 100644 --- a/prover/backend/invalidity/prove.go +++ b/prover/backend/invalidity/prove.go @@ -3,6 +3,7 @@ package invalidity import ( "encoding/hex" "fmt" + "math/big" "os" "path" "path/filepath" @@ -23,6 +24,7 @@ import ( linTypes "github.com/consensys/linea-monorepo/prover/utils/types" "github.com/consensys/linea-monorepo/prover/zkevm" "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/sirupsen/logrus" ) @@ -82,6 +84,8 @@ func Prove(cfg *config.Config, req *Request, large bool) (*Response, error) { return nil, fmt.Errorf("could not decode the RlpEncodedTx: %w", err) } + SanityCheckInvalidityChainConfig(cfg, tx) + funcInput := FuncInput(req, cfg) if cfg.Invalidity.ProverMode == config.ProverModeDev { @@ -281,3 +285,28 @@ func Prove(cfg *config.Config, req *Request, large bool) (*Response, error) { } return rsp, nil } + +// SanityCheckInvalidityChainConfig checks that the transaction's chainID matches +// the config. For non-legacy txs the chainID is embedded in the transaction; +// for legacy txs this check is skipped (chainID derived from V is unreliable). +func SanityCheckInvalidityChainConfig(cfg *config.Config, tx *ethtypes.Transaction) { + if tx.Type() == ethtypes.LegacyTxType { + return + } + cfgChainID := new(big.Int).SetUint64(uint64(cfg.Layer2.ChainID)) + txChainID := tx.ChainId() + if txChainID == nil || txChainID.Sign() == 0 { + return + } + if txChainID.Cmp(cfgChainID) != 0 { + utils.Panic( + "chain config mismatch — transaction chainID does not match config!\n\n"+ + " config chainID: %d\n"+ + " tx chainID: %s\n\n"+ + " Check your config file's [layer2] chain_id setting.\n", + cfg.Layer2.ChainID, txChainID.String(), + ) + } + logrus.Infof("Invalidity chain config check passed: tx chainID=%s matches config chainID=%d", + txChainID.String(), cfg.Layer2.ChainID) +} diff --git a/prover/circuits/invalidity/check_native.go b/prover/circuits/invalidity/check_native.go index 09983ae360d..b5d1c7e1859 100644 --- a/prover/circuits/invalidity/check_native.go +++ b/prover/circuits/invalidity/check_native.go @@ -132,8 +132,8 @@ func CheckOnlyNativeNonceBalance( switch assi.InvalidityType { case BadNonce: - if tx.Nonce() == uint64(accountNonce+1) { - return fmt.Errorf("nonce is valid (tx=%d, account+1=%d): not a bad-nonce case", tx.Nonce(), accountNonce+1) + if tx.Nonce() == uint64(accountNonce) { + return fmt.Errorf("nonce is valid (tx=%d, account=%d): not a bad-nonce case", tx.Nonce(), accountNonce) } case BadBalance: if txCost.Cmp(accountBalance) <= 0 { diff --git a/prover/circuits/invalidity/invalidity.go b/prover/circuits/invalidity/invalidity.go index 63df35c5b73..c48ed3e9827 100644 --- a/prover/circuits/invalidity/invalidity.go +++ b/prover/circuits/invalidity/invalidity.go @@ -12,6 +12,8 @@ import ( emPlonk "github.com/consensys/gnark/std/recursion/plonk" "github.com/consensys/linea-monorepo/prover/circuits" wizardk "github.com/consensys/linea-monorepo/prover/circuits/pi-interconnection/keccak/prover/protocol/wizard" + "github.com/consensys/linea-monorepo/prover/maths/field" + "github.com/consensys/linea-monorepo/prover/protocol/distributed" wizard "github.com/consensys/linea-monorepo/prover/protocol/wizard" public_input "github.com/consensys/linea-monorepo/prover/public-input" "github.com/ethereum/go-ethereum/common" @@ -226,17 +228,25 @@ func (b *builder) Compile() (constraint.ConstraintSystem, error) { } type limitlessBuilder struct { - congWIOP *wizard.CompiledIOP + congWIOP *wizard.CompiledIOP + vkMerkleRoot field.Octuplet } -func NewBuilderLimitless(congWIOP *wizard.CompiledIOP) *limitlessBuilder { - return &limitlessBuilder{congWIOP: congWIOP} +func NewBuilderLimitless(congWIOP *wizard.CompiledIOP, vkMerkleRoot field.Octuplet) *limitlessBuilder { + return &limitlessBuilder{congWIOP: congWIOP, vkMerkleRoot: vkMerkleRoot} } func (b *limitlessBuilder) Compile() (constraint.ConstraintSystem, error) { + vk0 := b.congWIOP.ExtraData[distributed.VerifyingKeyPublicInput].(field.Octuplet) + vk1 := b.congWIOP.ExtraData[distributed.VerifyingKey2PublicInput].(field.Octuplet) + circuit := &CircuitInvalidity{ SubCircuit: &BadPrecompileCircuit{ - ExecutionCtx: ExecutionCtx{LimitlessMode: true}, + ExecutionCtx: ExecutionCtx{ + LimitlessMode: true, + CongloVK: [2]field.Octuplet{vk0, vk1}, + VKMerkleRoot: b.vkMerkleRoot, + }, }, } circuit.Allocate(Config{ZkEvmComp: b.congWIOP}) diff --git a/prover/circuits/invalidity/nonce_balance.go b/prover/circuits/invalidity/nonce_balance.go index 756fd321cda..784a32fd724 100644 --- a/prover/circuits/invalidity/nonce_balance.go +++ b/prover/circuits/invalidity/nonce_balance.go @@ -69,11 +69,11 @@ func (circuit *BadNonceBalanceCircuit) Define(api frontend.API) error { // Reconstruct account nonce from limbs (4 x 16-bit, big-endian) accountNonce := combine16BitLimbs(api, toNativeSlice(account.Nonce[:])) - // Bad nonce: if invalidityType == 0 ----> tx nonce != account nonce + 1 + // Bad nonce: if invalidityType == 0 ----> tx nonce != account nonce nonceDiff := api.Add( api.Mul( api.Sub(1, circuit.InvalidityType), - api.Sub(circuit.TxNonce, api.Add(accountNonce, 1)), + api.Sub(circuit.TxNonce, accountNonce), ), circuit.InvalidityType) api.AssertIsDifferent(nonceDiff, 0) @@ -179,7 +179,7 @@ func (cir *BadNonceBalanceCircuit) Assign(assi AssigningInputs) { if assi.InvalidityType != BadNonce && assi.InvalidityType != BadBalance { utils.Panic("expected invalidity type BadNonce or BadBalance but received %v", assi.InvalidityType) } - if txNonce == uint64(acNonce+1) && assi.InvalidityType == BadNonce { + if txNonce == uint64(acNonce) && assi.InvalidityType == BadNonce { utils.Panic("tried to generate a bad-nonce proof for a possibly valid transaction") } if txCost.Cmp(balance) != 1 && assi.InvalidityType == BadBalance { diff --git a/prover/circuits/invalidity/utils_test.go b/prover/circuits/invalidity/utils_test.go index 06f53255aa2..0dd540e5623 100644 --- a/prover/circuits/invalidity/utils_test.go +++ b/prover/circuits/invalidity/utils_test.go @@ -615,7 +615,7 @@ var tcases = []TestCases{ InvalidityType: 1, }, { - // EOA + // EOA with BadNonce: tx nonce (66) != account nonce (65) Account: Account{ Nonce: 65, Balance: big.NewInt(5690), @@ -640,7 +640,7 @@ var tcases = []TestCases{ }, Tx: types.DynamicFeeTx{ ChainID: big.NewInt(59144), // Linea mainnet chain ID - Nonce: 65, // invalid nonce + Nonce: 66, // invalid nonce (account nonce is 65) Value: big.NewInt(5700), // invalid value Gas: 1, GasFeeCap: big.NewInt(1), // gas price diff --git a/prover/cmd/prover/cmd/setup.go b/prover/cmd/prover/cmd/setup.go index e9e0223017e..9aafc401f47 100644 --- a/prover/cmd/prover/cmd/setup.go +++ b/prover/cmd/prover/cmd/setup.go @@ -59,15 +59,23 @@ var AllCircuits = []circuits.CircuitID{ } // PayloadCircuits defines the ordered list of payload circuits that can be aggregated. -// This order corresponds to circuit IDs 0-5 in GlobalCircuitIDMapping. +// This order corresponds to circuit IDs 0-13 in GlobalCircuitIDMapping. // Infrastructure circuits (emulation, aggregation, pi-interconnection, emulation-dummy) are NOT included here. var PayloadCircuits = []string{ - "execution-dummy", // ID 0 - "data-availability-dummy", // ID 1 - "execution", // ID 2 - "execution-large", // ID 3 - "execution-limitless", // ID 4 - "data-availability-v2", // ID 5 + "execution-dummy", // ID 0 + "data-availability-dummy", // ID 1 + "execution", // ID 2 + "execution-large", // ID 3 + "execution-limitless", // ID 4 + "data-availability-v2", // ID 5 + "invalidity-nonce-balance-dummy", // ID 6 + "invalidity-precompile-logs-dummy", // ID 7 + "invalidity-filtered-address-dummy", // ID 8 + "invalidity-nonce-balance", // ID 9 + "invalidity-precompile-logs", // ID 10 + "invalidity-filtered-address", // ID 11 + "invalidity-precompile-logs-limitless", // ID 12 + "invalidity-precompile-logs-large", // ID 13 } // Setup orchestrates the setup process for specified circuits, ensuring assets are generated or updated as needed. @@ -146,7 +154,7 @@ func Setup(ctx context.Context, args SetupArgs) error { return fmt.Errorf("%s failed to load public input interconnection setup: %w", cmdName, err) } - // Collect verifying keys for payload circuits only (IDs 0-5) + // Collect verifying keys for payload circuits only (IDs 0-13) // The IsAllowedCircuitID bitmask in the config determines which ones are actually allowed at runtime payloadVks, err := collectPayloadVerifyingKeys(ctx, cfg, srsProvider) if err != nil { @@ -364,7 +372,12 @@ func createCircuitBuilder(c circuits.CircuitID, cfg *config.Config, args SetupAr // buf is intentionally not released: zero-copy deserialization means conglo // points into the mmap region, which must stay mapped until Compile() finishes. // For a one-shot setup process this is safe — the OS reclaims it at exit. - return invalidity.NewBuilderLimitless(conglo.RecursionCompBLS), extraFlags, nil + vkTree, err := zkevm.LoadVerificationKeyMerkleTree(cfg) + if err != nil { + return nil, nil, fmt.Errorf("failed to load VK merkle tree for invalidity limitless: %w", err) + } + vkMerkleRoot := vkTree.GetRoot() + return invalidity.NewBuilderLimitless(conglo.RecursionCompBLS, vkMerkleRoot), extraFlags, nil } logrus.Info("execution-limitless assets not found; generating them now for invalidity limitless setup") @@ -375,10 +388,11 @@ func createCircuitBuilder(c circuits.CircuitID, cfg *config.Config, args SetupAr return nil, nil, fmt.Errorf("failed to write limitless invalidity prover assets: %w", err) } compCong := asset.DistWizard.CompiledConglomeration + vkMerkleRoot := asset.DistWizard.VerificationKeyMerkleTree.GetRoot() asset = nil runtime.GC() - return invalidity.NewBuilderLimitless(compCong.RecursionCompBLS), extraFlags, nil + return invalidity.NewBuilderLimitless(compCong.RecursionCompBLS, vkMerkleRoot), extraFlags, nil case circuits.InvalidityFilteredAddressCircuitID: keccakComp := invalidity.MakeKeccakCompiledIOP(cfg.Invalidity.MaxRlpByteSize, keccak.WizardCompilationParameters()...) return invalidity.NewBuilder( @@ -398,10 +412,10 @@ func createCircuitBuilder(c circuits.CircuitID, cfg *config.Config, args SetupAr } } -// collectPayloadVerifyingKeys gathers verifying keys for payload circuits only (IDs 0-5). +// collectPayloadVerifyingKeys gathers verifying keys for payload circuits only (IDs 0-13). // These are the circuits that can be aggregated. Infrastructure circuits (emulation, // aggregation, pi-interconnection, emulation-dummy) are excluded. -// The returned slice is indexed by payload circuit ID (0-5). +// The returned slice is indexed by payload circuit ID (0-13). func collectPayloadVerifyingKeys(ctx context.Context, cfg *config.Config, srsProvider circuits.SRSProvider) ([]plonk.VerifyingKey, error) { payloadVks := make([]plonk.VerifyingKey, len(PayloadCircuits)) @@ -439,7 +453,10 @@ func collectPayloadVerifyingKeys(ctx context.Context, cfg *config.Config, srsPro // Note: emulation-dummy is NOT a payload dummy - it's an infrastructure circuit dummy. func isPayloadDummyCircuit(cID string) bool { switch circuits.CircuitID(cID) { - case circuits.ExecutionDummyCircuitID, circuits.DataAvailabilityDummyCircuitID: + case circuits.ExecutionDummyCircuitID, circuits.DataAvailabilityDummyCircuitID, + circuits.InvalidityNonceBalanceDummyCircuitID, + circuits.InvalidityPrecompileLogsDummyCircuitID, + circuits.InvalidityFilteredAddressDummyCircuitID: return true } return false diff --git a/prover/cmd/prover/cmd/setup_test.go b/prover/cmd/prover/cmd/setup_test.go index c6fed0b66d4..bdcbcb0cd27 100644 --- a/prover/cmd/prover/cmd/setup_test.go +++ b/prover/cmd/prover/cmd/setup_test.go @@ -323,6 +323,45 @@ func pow5(s []frBls.Element) []frBls.Element { return res } +// TestPayloadCircuitsMatchGlobalMapping validates that PayloadCircuits is consistent +// with GlobalCircuitIDMapping. This prevents the bug where new circuits are added to +// GlobalCircuitIDMapping but not to PayloadCircuits, causing VK digest mismatches at +// aggregation proof time. +func TestPayloadCircuitsMatchGlobalMapping(t *testing.T) { + // Determine expected payload circuit count: all circuits with ID < 14 (infrastructure threshold) + const infrastructureThreshold = 14 + expectedPayload := make(map[string]uint) + for name, id := range circuits.GlobalCircuitIDMapping { + if id < infrastructureThreshold { + expectedPayload[name] = id + } + } + + // PayloadCircuits must contain exactly the non-infrastructure circuits + assert.Equal(t, len(expectedPayload), len(PayloadCircuits), + "PayloadCircuits has %d entries but GlobalCircuitIDMapping has %d circuits with ID < %d", + len(PayloadCircuits), len(expectedPayload), infrastructureThreshold) + + // Each entry in PayloadCircuits must exist in GlobalCircuitIDMapping with matching index + for i, name := range PayloadCircuits { + id, exists := circuits.GlobalCircuitIDMapping[name] + require.True(t, exists, + "PayloadCircuits[%d] = %q is not in GlobalCircuitIDMapping", i, name) + assert.Equal(t, uint(i), id, + "PayloadCircuits[%d] = %q has GlobalCircuitIDMapping ID %d (expected %d)", i, name, id, i) + } + + // Every non-infrastructure circuit in GlobalCircuitIDMapping must be in PayloadCircuits + for name, id := range circuits.GlobalCircuitIDMapping { + if id >= infrastructureThreshold { + continue + } + if int(id) >= len(PayloadCircuits) || PayloadCircuits[id] != name { + t.Errorf("GlobalCircuitIDMapping[%q] = %d is not at PayloadCircuits[%d]", name, id, id) + } + } +} + // circuitNameToIdx returns the index of a circuit name in the allowedInputs slice func circuitNameToIdx(allowedInputs []string, name string) int { for i, n := range allowedInputs { diff --git a/prover/zkevm/full.go b/prover/zkevm/full.go index 5d8945a1e04..3782917f79d 100644 --- a/prover/zkevm/full.go +++ b/prover/zkevm/full.go @@ -414,6 +414,5 @@ func FullZKEVMWithSuite( } // Initialize the Full zkEVM arithmetization - fullZkEvm = NewZkEVM(settings) - return fullZkEvm + return NewZkEVM(settings) } diff --git a/prover/zkevm/zkevm_test.go b/prover/zkevm/zkevm_test.go index a21a50db50d..4a91cb69bd7 100644 --- a/prover/zkevm/zkevm_test.go +++ b/prover/zkevm/zkevm_test.go @@ -18,3 +18,23 @@ func TestCanGenerateFullZkEVM(t *testing.T) { _ = FullZkEvm(&cfg.TracesLimits, cfg) } + +// FullZKEVMWithSuite must not overwrite memoized globals. +func TestFullZKEVMWithSuiteNoGlobalOverwrite(t *testing.T) { + cfg, err := config.NewConfigFromFileUnchecked("../config/config-mainnet-limitless.toml") + if err != nil { + t.Fatal(err) + } + + sentinel := &ZkEvm{} + original := fullZkEvm + fullZkEvm = sentinel + + _ = FullZKEVMWithSuite(&cfg.TracesLimits, cfg, dummyCompilationSuite, nil) + + if fullZkEvm != sentinel { + t.Fatal("FullZKEVMWithSuite overwrote the fullZkEvm global") + } + + fullZkEvm = original +}