feat!: make proposals EIP-712#22531
Conversation
a8ec1a0 to
fb34548
Compare
5a02d69 to
cfe2a60
Compare
spalladino
left a comment
There was a problem hiding this comment.
Looks solid. Just two things I'd want to discuss:
1- Should we drop the SignatureDomainSeparator given EIP712 includes the typehash? Feels redundant.
2- Should we explicitly include the chainid and rollup version in the proposal and attestation p2p structs so we can easily validate them, and at the same time we don't need a coordination context on any caller of getSender? If not, then at least I'd shoot for having two different structs: the current one that gets moved throughout p2p, and a "resolved" one where the sender is the computed value, which we use internally.
| ProposePayload({archive: proposeArgs.archive, oracleInput: proposeArgs.oracleInput, headerHash: headerHash}); | ||
|
|
||
| bytes32 digest = ProposeLib.digest(proposePayload); | ||
| bytes32 digest = ProposeLib.digest(proposePayload, address(rollup)); |
There was a problem hiding this comment.
I was about to ask whether you needed both version of the digest method, with and without the verifying contract address.
| function domainSeparator(address _verifyingContract) internal view returns (bytes32) { | ||
| return keccak256(abi.encode(DOMAIN_TYPEHASH, NAME_HASH, VERSION_HASH, block.chainid, _verifyingContract)); | ||
| } |
There was a problem hiding this comment.
Can we cache this as an immutable?
There was a problem hiding this comment.
I'm pleasantly surprised the increase is so small, nice!
| address _verifyingContract | ||
| ) internal view returns (bytes32) { | ||
| return CoordinationSignatureLib.attestationsAndSignersDigest( | ||
| keccak256(abi.encode(SignatureDomainSeparator.attestationsAndSigners, _attestations, _signers)), |
There was a problem hiding this comment.
Do we still need the SignatureDomainSeparator if we have the 712 typehash?
There was a problem hiding this comment.
Atm its mainly dispatching and then because of the SignedTxs that it was leftover, but will look at also making those EIP-712 so it is just one path.
We did not use EIP712 all over, so am looking to use it more for the SignedTxs.
I'm taking a look at it now should be able to simplify nicely together with the fix in 1. |
7003b54 to
7256238
Compare
spalladino
left a comment
There was a problem hiding this comment.
Looks good! Just a few nits. And sorry I took so long to re-review.
| expectedChainId: this.signatureContext.chainId, | ||
| expectedRollupAddress: this.signatureContext.rollupAddress.toString(), | ||
| }); | ||
| return { result: 'reject', severity: PeerErrorSeverity.HighToleranceError }; |
There was a problem hiding this comment.
| return { result: 'reject', severity: PeerErrorSeverity.HighToleranceError }; | |
| return { result: 'reject', severity: PeerErrorSeverity.LowToleranceError }; |
| expectedChainId: this.signatureContext.chainId, | ||
| expectedRollupAddress: this.signatureContext.rollupAddress.toString(), | ||
| }); | ||
| return { result: 'reject', severity: PeerErrorSeverity.HighToleranceError }; |
|
|
||
| constructor( | ||
| public attestations: CommitteeAttestation[], | ||
| public readonly signatureContext: CoordinationSignatureContext = EMPTY_COORDINATION_SIGNATURE_CONTEXT, |
There was a problem hiding this comment.
Does it break too many things if we remove the default? I'm worried we forget to wire it in in a legitimate code path.
| | undefined; | ||
| readonly primaryType: CoordinationSignatureType = 'BlockProposal'; | ||
|
|
||
| private cachedSender: EthAddress | undefined | null = null; |
There was a problem hiding this comment.
Nit: I'd default to undefined for initial value, and null if no value is recoverable, or even a null-object
|
By the way, do you want to move this to |
7256238 to
41f782d
Compare
41f782d to
63e0b5c
Compare

This changes proposal-path validator signatures from
eth_sign/personal_signstyle payload signing to EIP-712 typed-data signing. Proposal and attestation signatures are now bound to both the L1chainIdand the rollup contract address, which closes cross-chain and cross-rollup replay for block proposals, checkpoint proposals, checkpoint attestations, and the proposer signature over packed attestations/signers.On the Solidity side, the rollup path now computes EIP-712 digests through
CoordinationSignatureLib.sol, reusing the existingAztec Rollupdomain name and version1, and threads the verifying contract explicitly where needed. On the TypeScript side, proposal-path signing and recovery now require a{ chainId, rollupAddress }signature context, validator duties sign typed data instead of raw messages, and the shared helper surface was renamed toCoordinationSignature*to match Solidity.Tests were updated to check digest parity between TS and Solidity, sender recovery under the correct domain, failure under the wrong
chainIdor rollup address, and the affected contract flows that verify or invalidate checkpoint-related signatures.Historically, it was possible to reason about replay from archives and chain history, but it was not always stupid-simple. With the EIP-712 domain binding, the replay boundary is explicit in the signature itself, so checking whether a signature targets the wrong chain or wrong rollup becomes straightforward and only depends on current L1 state.