Understand why some public values must be constrained in the verifier, not just committed by the proof.
A valid SP1 proof commits a full claim payload, including chain_id, but the guest does not know which chain this proof is for because the guest is not a chain, it's offchain code.
Identify the bug in the contract and exploit it with a proof that was generated for a different chain.
starter/: vulnerable implementation.- Host: starter/rust/host/src/main.rs - generates two Groth16 fixtures for the same guest program: one for the honest chain and one for a foreign chain.
- Guest: starter/rust/guest/src/main.rs - SP1 program that commits the full
PublicValuesV1, but only constrains the canonical payload except forchain_id. - Verifier: starter/foundry/src/UnconstrainedVerifierStarter.sol - vulnerable onchain consumer that verifies proofs and prevents exact replay, but forgets to check that the committed
chain_idmatchesblock.chainid.
solution/: recommended patch.- Verifier: solution/foundry/src/UnconstrainedVerifierSolution.sol fixed implementation (no peeking!).
Use this file to implement the exploit:
It contains an exercise scaffold with TODOs. The function exerciseForeignChainProofIsAccepted() is not executed by default; rename it to start with test when you are ready to run the exercise.
Optional: build the guest ELF and regenerate the Solidity fixture library. Fixture values are written into both starter and solution at test/fixtures/UnconstrainedVerifierFixture.sol.
cd ctfs/sp1/c03-unconstrained-public-values/starter/rust
cargo prove build
cargo run -p host --releaseRun starter baseline tests:
cd ctfs/sp1/c03-unconstrained-public-values/starter/foundry
forge testRun the exercise after filling in the TODO and renaming the function to start with test:
cd ctfs/sp1/c03-unconstrained-public-values/starter/foundry
forge test --match-test testExerciseForeignChainProofIsAcceptedRun solution tests:
cd ctfs/sp1/c03-unconstrained-public-values/solution/foundry
forge testThe guest accepts exactly one canonical PublicValuesV1 envelope for every field except chain_id:
chain_id = caller suppliedcontext_hash = 0x2222...2222(32 bytes)recipient = 0x3333...3333amount = 100nonce = 1version = 1
The host emits two valid proofs under the same program key:
HONEST_CHAIN_ID = 1FOREIGN_CHAIN_ID = 10
- Distinguish proof validity from application-level chain binding.
- Observe that committing
chain_idis not enough if the verifier never compares it toblock.chainid. - Reproduce an exploit where a proof intended for chain
10is accepted on chain1. - Constrain verifier-visible public values before applying any state effect.