Ethereum bridge milestone 2: implement execute_signed()#506
Conversation
Deploying kolme with
|
| Latest commit: |
5dbea27
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://3d75ae84.kolme.pages.dev |
| Branch Preview URL: | https://ethereum-bridge-2-execute.kolme.pages.dev |
b399144 to
83ae599
Compare
It will be used to verify processor's and approvers' signatures
To be used in conjunction with _recoverSigner() for signature verification
...and run them in CI
83ae599 to
9e07ba5
Compare
contracts/ethereum/src/Bridge.sol
Outdated
| ValidatorSet internal validatorSet; | ||
| uint64 internal nextEventId; | ||
| uint64 internal nextActionId; | ||
| address internal processorSigner; |
There was a problem hiding this comment.
Adding these fields duplicates storage and additionally introduces potential bugs when rotating signers. I'd rather instead that we compute these as needed. (I can't speak to Ethereum APIs and whether such a conversion is strictly necessary.)
There was a problem hiding this comment.
Fixed - moved those calculations into the execute_signed(). This will increase gas usage somewhat (not significantly, as far as I can tell by looking at a test_ExecuteSignedIncrementsIdsAndEmitsEvent() gas usage (116k vs 101k). Do you think its worth to investigate gas usage more precisely here?
There was a problem hiding this comment.
No, let's leave it alone for the moment, we can worry about optimizations like that later.
contracts/ethereum/src/Bridge.sol
Outdated
| uint64 internal nextEventId; | ||
| uint64 internal nextActionId; | ||
| address internal processorSigner; | ||
| mapping(address => bool) internal approverSigners; |
There was a problem hiding this comment.
Is there a reason that here you used a mapping but in ValidatorSet the approvers are represented as an array?
There was a problem hiding this comment.
I got to know about mapping existence a bit later, and thought about going there, but in this particular case mapping is only useful if we put EVM addresses in it, and if we convert validator keys into those, there's no reverse conversion - so we'll lose original key values.
There was a problem hiding this comment.
Pull request overview
Implements milestone 2 of the Ethereum bridge by adding signature verification helpers and introducing the execute_signed(payload, processorSig, approverSigs) entry point, plus CI/e2e smoke coverage to validate the on-chain flow.
Changes:
- Add
execute_signed()to the bridge contract with action-id ordering + processor/approver signature verification and a newSignedevent. - Derive and store processor/approver EVM signer addresses at construction time to simplify runtime signature checks.
- Expand Solidity + Rust integration tests and add a Dockerized Anvil CLI smoke test step in CI.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
contracts/ethereum/src/Bridge.sol |
Adds execute_signed, signature recovery, pubkey→address derivation, new errors, and signer state. |
contracts/ethereum/test/Bridge.t.sol |
Adds harness + tests for signature helpers and execute_signed happy/negative paths. |
contracts/ethereum/script/Bridge.s.sol |
Updates default deployment keys to distinct processor/approver keys. |
packages/integration-tests/tests/ethereum-bridge.rs |
Updates expected config keys to match deterministic e2e deployment values. |
contracts/ethereum/e2e/ethereum-anvil-cli-smoke.sh |
New CLI smoke script exercising get_config + execute_signed and replay rejection. |
contracts/ethereum/e2e/compose.yaml |
Mounts the new smoke script into the Anvil container. |
contracts/ethereum/e2e/bridge/bootstrap-state.sh |
Updates deterministic deployment keys and improves bridge address extraction. |
contracts/ethereum/e2e/bridge/Dockerfile |
Pins Foundry install version and includes tooling needed for smoke tests (e.g., jq). |
justfile |
Adds a just ethereum-anvil-cli-smoke recipe in the e2e working directory. |
.github/workflows/ci.yml |
Adds the Anvil CLI smoke step to CI. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
contracts/ethereum/test/Bridge.t.sol
Outdated
| bytes[] memory approverSigs = new bytes[](1); | ||
| approverSigs[0] = _signPayload(APPROVER_PRIVATE_KEY, payload); | ||
|
|
||
| vm.expectEmit(true, false, false, true, address(bridge)); |
There was a problem hiding this comment.
expectEmit(true, false, false, true, ...) does not verify the indexed sender topic for Signed. Since this test already controls the caller with vm.prank, it can safely enable checking topic2 as well to make the assertion stricter and catch accidental changes to emitted sender.
| vm.expectEmit(true, false, false, true, address(bridge)); | |
| vm.expectEmit(true, true, false, true, address(bridge)); |
contracts/ethereum/src/Bridge.sol
Outdated
| processorSigner = _validatorAddress(processor); | ||
| for (uint256 i = 0; i < approvers.length; i++) { | ||
| approverSigners[_validatorAddress(approvers[i])] = true; | ||
| } |
There was a problem hiding this comment.
The constructor derives EVM addresses (and therefore validates curve points) for the processor and approvers, but it does not do the same for listeners. This leaves the contract able to deploy with listener keys that are valid-length/prefix but invalid secp256k1 points, which will likely break later milestones once listener signature verification is added. Consider validating all validator keys’ curve points during construction (or explicitly document why listeners are exempt for now).
| uint256 approverCount = approvers.length; | ||
| address[] memory seen = new address[](approverCount); | ||
| uint256 uniqueApprovers = 0; | ||
| for (uint256 i = 0; i < approverCount; i++) { | ||
| address signer = _recoverSigner(payloadHash, approvers[i]); | ||
| if (!approverSigners[signer]) { | ||
| revert InvalidApproverSignature(signer); | ||
| } | ||
| for (uint256 j = 0; j < uniqueApprovers; j++) { | ||
| if (seen[j] == signer) { | ||
| revert DuplicateApproverSignature(signer); | ||
| } | ||
| } | ||
| seen[uniqueApprovers] = signer; | ||
| uniqueApprovers += 1; | ||
| } |
There was a problem hiding this comment.
execute_signed performs an O(n^2) duplicate check across all provided approver signatures, and it will iterate even if the caller supplies far more signatures than the configured approver set. Add a cheap bound/early-exit (e.g., revert if approvers.length exceeds the configured total, and/or stop once neededApprovers unique valid signatures are collected) to keep gas costs predictable.
There was a problem hiding this comment.
Partially addressed - added early exit when the quorum is reached
| bool success; | ||
| assembly { | ||
| success := staticcall( | ||
| gas(), | ||
| 0x05, | ||
| add(input, 0x20), | ||
| mload(input), | ||
| add(output, 0x20), | ||
| 0x20 | ||
| ) | ||
| } | ||
| assert(success); | ||
| return abi.decode(output, (uint256)); |
There was a problem hiding this comment.
_modExp uses assert(success). If the precompile call ever fails, this triggers a Panic and consumes all remaining gas, which is undesirable for a public contract. Prefer a normal revert with a dedicated error (and optionally include the inputs) so failures are debuggable and don’t burn all gas.
| RUN curl -L https://foundry.paradigm.xyz | bash && \ | ||
| /root/.foundry/bin/foundryup | ||
| /root/.foundry/bin/foundryup --install "${FOUNDRY_VERSION}" |
There was a problem hiding this comment.
curl -L https://foundry.paradigm.xyz | bash downloads and executes remote code during the image build without any integrity verification, so a compromise or MITM of that endpoint could run arbitrary commands in your build/CI environment and tamper with artifacts or exfiltrate secrets. To mitigate this, avoid piping curl directly to bash and instead download a specific, integrity-checked artifact (pinned by checksum, signature, or content hash) before executing it.
There was a problem hiding this comment.
I'd leave it as is for now
| } | ||
|
|
||
| uint256 approverCount = approvers.length; | ||
| address[] memory seen = new address[](approverCount); |
There was a problem hiding this comment.
Can this be a mapping to avoid the extra for loop below to check if something has been seen?
There was a problem hiding this comment.
mapping can be used only in storage, if I understand it correctly
There was a problem hiding this comment.
Depends on #505
execute_signed(payload, processor, approvers)entry point in Ethereum bridge contract(action_id, action_data)action_datais ignored, will be implemented in next milestoneaction_idordering againstnextActionIdsha256(payload)Signed(event_id, sender, action_id)and incrementsnextEventId/nextActionIdexecute_signed()testing