Skip to content
Open
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: 2 additions & 0 deletions prover/backend/invalidity/limitless/prove.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions prover/backend/invalidity/prove.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package invalidity
import (
"encoding/hex"
"fmt"
"math/big"
"os"
"path"
"path/filepath"
Expand All @@ -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"
)

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
4 changes: 2 additions & 2 deletions prover/circuits/invalidity/check_native.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
18 changes: 14 additions & 4 deletions prover/circuits/invalidity/invalidity.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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})
Expand Down
6 changes: 3 additions & 3 deletions prover/circuits/invalidity/nonce_balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions prover/circuits/invalidity/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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
Expand Down
43 changes: 30 additions & 13 deletions prover/cmd/prover/cmd/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
Expand All @@ -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(
Expand All @@ -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))

Expand Down Expand Up @@ -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:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New dummy circuits unhandled in getDummyCircuitParams

High Severity

isPayloadDummyCircuit now returns true for the three new invalidity dummy circuits (InvalidityNonceBalanceDummyCircuitID, InvalidityPrecompileLogsDummyCircuitID, InvalidityFilteredAddressDummyCircuitID), but getDummyCircuitParams was not updated with matching cases for these circuits. When collectPayloadVerifyingKeys calls getDummyCircuitParams for any of these, it hits the default branch and returns an error, causing setup to fail.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0b7b00f. Configure here.

return true
}
return false
Expand Down
39 changes: 39 additions & 0 deletions prover/cmd/prover/cmd/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 1 addition & 2 deletions prover/zkevm/full.go
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,5 @@ func FullZKEVMWithSuite(
}

// Initialize the Full zkEVM arithmetization
fullZkEvm = NewZkEVM(settings)
return fullZkEvm
return NewZkEVM(settings)
}
20 changes: 20 additions & 0 deletions prover/zkevm/zkevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Loading