diff --git a/.github/workflows/registry.yml b/.github/workflows/registry.yml index 1a8e268..0a10a23 100644 --- a/.github/workflows/registry.yml +++ b/.github/workflows/registry.yml @@ -40,8 +40,6 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 - with: - version: v1.0.0 - name: Show Forge version run: | diff --git a/config/registry.json b/config/registry.json index 5363300..0460bd9 100644 --- a/config/registry.json +++ b/config/registry.json @@ -3,5 +3,7 @@ "fraudProofWindow": 7200, "unregistrationDelay": 7200, "slashWindow": 7200, - "optInDelay": 7200 + "optInDelay": 7200, + "signingDomain": "0x6d6d6f43719103511efa4f1362ff2a50996cccf329cc84cb410c5e5c7d351d03", + "chainId": "0xb08b080000000000000000000000000000000000000000000000000000000000" } \ No newline at end of file diff --git a/docs/overview.md b/docs/overview.md index ff99637..2aa4666 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -11,4 +11,53 @@ - [ ] Audit 3 ## Overview -Readers are recommended to refer to the [Constraints API documentation](https://github.com/eth-fabric/constraints-specs/blob/main/specs/proposer.md) for details on how to interact with the URC. \ No newline at end of file +Readers are recommended to refer to the [Constraints API documentation](https://github.com/eth-fabric/constraints-specs/blob/main/specs/proposer.md) for details on how to interact with the URC. + +## Signing +The URC will verify two types of BLS signatures: +- `IRegistry.SignedRegistration.signature` +- `ISlasher.SignedDelegation.signature` + +The messages are expected to be formatted as follows: +- `bytes32 messageHash = keccak256(abi.encode(IRegistry.MessageType.Registration, owner));` where `owner` is an `address` +- `bytes32 messageHash = keccak256(abi.encode(IRegistry.MessageType.Delegation, delegation));` where `delegation` is an `ISlasher.Delegation` + +The URC complies with the [Signing API defined in Commit-Boost](https://github.com/Commit-Boost/commit-boost-client/blob/2dfe96b8d45d9c2bb37f71d56130a066dec16ec8/crates/common/src/types.rs#L296). +``` +/// Structure for signatures used in Beacon chain operations +#[derive(Default, Debug, TreeHash)] +pub struct SigningData { + pub object_root: B256, + pub signing_domain: B256, +} + +/// Structure for signatures used for proposer commitments in Commit Boost. +/// The signing root of this struct must be used as the object_root of a +/// SigningData for signatures. +#[derive(Default, Debug, TreeHash)] +pub struct PropCommitSigningInfo { + pub data: B256, + pub module_signing_id: B256, + pub nonce: u64, // As per https://eips.ethereum.org/EIPS/eip-2681 + pub chain_id: U256, +} +``` + +The final signature is over the `signingRoot` which is the hash tree root of the `SigningData` struct, containing the `PropCommitSigningInfo` and `signing_domain`. + +This can be visualized as the following, where each intermediate hash uses SHA256. Specifically, `subTreeRoot` is the hash tree root of `PropCommitSigningInfo`: +``` + signingRoot + / \ + subTreeRoot signingDomain + / \ + * * + / \ / \ + messageHash signingId nonce chainId +``` + +Notes: +- `messageHash` is equivalent to `PropCommitSigningInfo.data` +- the `nonce` and `chain_id` are encoded as little-endian `bytes32`. +- the `nonce` and `signingId` aren't tracked by the URC but is mixed in with the signature according to the Commit-Boost signing spec. +- the `signingDomain` is the precalculated `compute_domain()` value from Commit-Boost for the specific chain the URC is deployed on. \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index bdd65b5..90eb9ee 100644 --- a/foundry.toml +++ b/foundry.toml @@ -8,9 +8,7 @@ auto_detect_solc = false optimizer = true optimizer_runs = 200 evm_version = "prague" # for testing bls precompiles -seed = "0x1337" solc = "0.8.25" -# via_ir = true [fmt] line_length = 120 diff --git a/script/BaseScript.s.sol b/script/BaseScript.s.sol index 92a534b..0e4e0ee 100644 --- a/script/BaseScript.s.sol +++ b/script/BaseScript.s.sol @@ -7,11 +7,9 @@ import "../src/lib/MerkleTree.sol"; import "../src/ISlasher.sol"; import { BLS } from "solady/utils/ext/ithaca/BLS.sol"; import { BLSUtils } from "../src/lib/BLSUtils.sol"; +import { ECDSAUtils } from "../src/lib/ECDSAUtils.sol"; contract BaseScript is Script { - bytes public constant REGISTRATION_DOMAIN_SEPARATOR = "0x00555243"; // "URC" in little endian - bytes public constant DELEGATION_DOMAIN_SEPARATOR = "0x0044656c"; // "Del" in little endian - function _getDefaultJson(string memory _outfile, string memory _default) internal returns (string memory jsonFile, string memory jsonObj) @@ -38,37 +36,52 @@ contract BaseScript is Script { } /// @dev NOT MEANT FOR PRODUCTION USE - function _signTestRegistration(uint256 privateKey, address _owner) - internal - view - returns (IRegistry.SignedRegistration memory signedRegistration) - { + function _signTestRegistration( + uint256 privateKey, + address _owner, + bytes32 signingDomain, + bytes32 signingId, + uint64 nonce, + bytes32 chainId + ) internal view returns (IRegistry.SignedRegistration memory signedRegistration) { BLS.G1Point memory pubkey = BLSUtils.toPublicKey(privateKey); - bytes memory message = abi.encode(_owner); - BLS.G2Point memory signature = BLSUtils.sign(message, privateKey, REGISTRATION_DOMAIN_SEPARATOR); - signedRegistration = IRegistry.SignedRegistration({ pubkey: pubkey, signature: signature }); + bytes32 messageHash = keccak256(abi.encode(IRegistry.MessageType.Registration, _owner)); + BLS.G2Point memory signature = BLSUtils.sign(privateKey, messageHash, signingDomain, signingId, nonce, chainId); + signedRegistration = IRegistry.SignedRegistration({ pubkey: pubkey, signature: signature, nonce: nonce }); } /// @dev NOT MEANT FOR PRODUCTION USE - function _nRegistrations(uint256 _n, uint256 privateKeyStart, address _owner) - internal - view - returns (IRegistry.SignedRegistration[] memory signedRegistrations) - { + function _nRegistrations( + uint256 _n, + uint256 privateKeyStart, + address _owner, + bytes32 signingDomain, + bytes32 signingId, + bytes32 chainId + ) internal view returns (IRegistry.SignedRegistration[] memory signedRegistrations) { signedRegistrations = new IRegistry.SignedRegistration[](_n); for (uint256 i = 0; i < _n; i++) { - signedRegistrations[i] = _signTestRegistration(privateKeyStart + i, _owner); + uint64 nonce = uint64(privateKeyStart + i); + signedRegistrations[i] = + _signTestRegistration(privateKeyStart + i, _owner, signingDomain, signingId, nonce, chainId); } } /// @dev NOT MEANT FOR PRODUCTION USE - function _signTestDelegation(uint256 privateKey, ISlasher.Delegation memory delegation) - internal - view - returns (ISlasher.SignedDelegation memory signedDelegation) - { - BLS.G2Point memory signature = BLSUtils.sign(abi.encode(delegation), privateKey, DELEGATION_DOMAIN_SEPARATOR); - return ISlasher.SignedDelegation({ delegation: delegation, signature: signature }); + function _signTestDelegation( + uint256 privateKey, + ISlasher.Delegation memory delegation, + bytes32 signingDomain, + bytes32 signingId, + uint64 nonce, + bytes32 chainId + ) internal view returns (ISlasher.SignedDelegation memory signedDelegation) { + bytes32 messageHash = keccak256(abi.encode(IRegistry.MessageType.Delegation, delegation)); + BLS.G2Point memory signature = BLSUtils.sign(privateKey, messageHash, signingDomain, signingId, nonce, chainId); + return + ISlasher.SignedDelegation({ + delegation: delegation, signature: signature, nonce: nonce, signingId: signingId + }); } /// @dev NOT MEANT FOR PRODUCTION USE @@ -77,11 +90,15 @@ contract BaseScript is Script { uint256 _proposerPrivateKey, uint256 _delegatePrivateKeyStart, address _committer, - uint256 _slot + uint256 _slot, + bytes32 _signingDomain, + bytes32 _signingId, + bytes32 _chainId ) internal view returns (ISlasher.SignedDelegation[] memory signedDelegations) { BLS.G1Point memory proposer = BLSUtils.toPublicKey(_proposerPrivateKey); signedDelegations = new ISlasher.SignedDelegation[](_n); for (uint256 i = 0; i < _n; i++) { + uint64 _nonce = uint64(_delegatePrivateKeyStart + i); BLS.G1Point memory delegate = BLSUtils.toPublicKey(_delegatePrivateKeyStart + i); signedDelegations[i] = _signTestDelegation( _proposerPrivateKey, // fixed proposer private key @@ -91,22 +108,57 @@ contract BaseScript is Script { committer: _committer, slot: uint64(_slot), metadata: "" - }) + }), + _signingDomain, + _signingId, + _nonce, + _chainId ); } } /// @dev NOT MEANT FOR PRODUCTION USE - function _signTestCommitment(uint256 privateKey, address slasher, uint256 commitmentType, bytes memory payload) - internal - pure - returns (ISlasher.SignedCommitment memory signedCommitment) - { - ISlasher.Commitment memory commitment = - ISlasher.Commitment({ commitmentType: uint64(commitmentType), payload: payload, slasher: slasher }); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, keccak256(abi.encode(commitment))); - bytes memory signature = abi.encodePacked(r, s, v); - return ISlasher.SignedCommitment({ commitment: commitment, signature: signature }); + function _signTestCommitment( + uint256 privateKey, + address slasher, + uint256 commitmentType, + bytes memory payload, + bytes32 signingId, + uint64 nonce, + bytes32 chainId, + bytes32 signingDomain + ) internal view returns (ISlasher.SignedCommitment memory signedCommitment) { + // Create CommitmentRequest and compute requestHash + ISlasher.CommitmentRequest memory request = + ISlasher.CommitmentRequest({ commitmentType: uint64(commitmentType), payload: payload, slasher: slasher }); + bytes32 requestHash = keccak256(abi.encode(request)); + + // Create Commitment with requestHash + ISlasher.Commitment memory commitment = ISlasher.Commitment({ + commitmentType: uint64(commitmentType), payload: payload, requestHash: requestHash, slasher: slasher + }); + + // Sign using ECDSA + bytes32 messageHash = keccak256(abi.encode(commitment)); + bytes memory signature = _signECDSA(privateKey, messageHash, signingDomain, signingId, nonce, chainId); + + return + ISlasher.SignedCommitment({ + commitment: commitment, nonce: nonce, signingId: signingId, signature: signature + }); + } + + function _signECDSA( + uint256 privateKey, + bytes32 messageHash, + bytes32 signingDomain, + bytes32 signingId, + uint64 nonce, + bytes32 chainId + ) internal view returns (bytes memory signature) { + bytes32 signingRoot = ECDSAUtils.computeSigningRoot(messageHash, signingDomain, signingId, nonce, chainId); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, signingRoot); + signature = abi.encodePacked(r, s, v); } function _writeSignedRegistrations( @@ -166,6 +218,10 @@ contract BaseScript is Script { vm.writeJson(vm.toString(abi.encode(proof.registration)), _jsonFile, ".registration"); vm.sleep(250); + // Write the signingId to the json file + vm.writeJson(vm.toString(proof.signingId), _jsonFile, ".signingId"); + vm.sleep(250); + // Write the registrationRoot to the json file vm.writeJson(vm.toString(proof.registrationRoot), _jsonFile, ".registrationRoot"); vm.sleep(250); @@ -184,6 +240,8 @@ contract BaseScript is Script { proof.registration = abi.decode(vm.parseJsonBytes(json, ".registration"), (IRegistry.SignedRegistration)); proof.registrationRoot = vm.parseJsonBytes32(json, ".registrationRoot"); + + proof.signingId = vm.parseJsonBytes32(json, ".signingId"); } function _writeDelegation(ISlasher.SignedDelegation memory delegation, string memory outfile) internal { @@ -285,7 +343,7 @@ contract BaseScript is Script { return pubkeyPretty; } - function _prettyPrintPubKey(IRegistry.SignedRegistration memory registration) internal { + function _prettyPrintPubKey(IRegistry.SignedRegistration memory registration) internal pure { // duplicate to prevent weird foundry memory issues BLS.G1Point memory pubkeyCopy = BLS.G1Point( registration.pubkey.x_a, registration.pubkey.x_b, registration.pubkey.y_a, registration.pubkey.y_b @@ -293,4 +351,11 @@ contract BaseScript is Script { bytes memory pubkeyPretty = _prettyPubKey(abi.encode(BLSUtils.compress(pubkeyCopy))); console.log("Pubkey: ", vm.toString(pubkeyPretty)); } + + function _defaultSigningParams() internal returns (bytes32 signingDomain, bytes32 chainId) { + string memory configPath = "config/registry.json"; + string memory configJson = vm.readFile(configPath); + signingDomain = vm.parseJsonBytes32(configJson, ".signingDomain"); + chainId = vm.parseJsonBytes32(configJson, ".chainId"); + } } diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index c502fb8..dc07fe5 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -19,7 +19,9 @@ contract DeployScript is Script { fraudProofWindow: uint32(vm.parseJsonUint(configJson, ".fraudProofWindow")), unregistrationDelay: uint32(vm.parseJsonUint(configJson, ".unregistrationDelay")), slashWindow: uint32(vm.parseJsonUint(configJson, ".slashWindow")), - optInDelay: uint32(vm.parseJsonUint(configJson, ".optInDelay")) + optInDelay: uint32(vm.parseJsonUint(configJson, ".optInDelay")), + signingDomain: vm.parseJsonBytes32(configJson, ".signingDomain"), + chainId: vm.parseJsonBytes32(configJson, ".chainId") }); // Deploy the Registry contract diff --git a/script/Getters.s.sol b/script/Getters.s.sol index 3390353..e3bef11 100644 --- a/script/Getters.s.sol +++ b/script/Getters.s.sol @@ -66,10 +66,12 @@ contract GettersScript is BaseScript { } // forge script script/Getters.s.sol:GettersScript --sig "getSlasherCommitment(address,bytes32,address,string)" $REGISTRY_ADDRESS $REGISTRATION_ROOT $SLASHER $SLASHER_COMMITMENT_FILE --rpc-url $RPC_URL - function getSlasherCommitment(address _registry, bytes32 _registrationRoot, address _slasher, string memory outfile) - public - returns (IRegistry.SlasherCommitment memory slasherCommitment) - { + function getSlasherCommitment( + address _registry, + bytes32 _registrationRoot, + address _slasher, + string memory outfile + ) public returns (IRegistry.SlasherCommitment memory slasherCommitment) { // Get reference to the registry IRegistry registry = IRegistry(_registry); @@ -91,10 +93,11 @@ contract GettersScript is BaseScript { console.log("SlasherCommitment written to", jsonFile); } - // forge script script/Getters.s.sol:GettersScript --sig "getRegistrationProof(address,bytes,string,string)" $REGISTRY_ADDRESS $PUBKEY $SIGNED_REGISTRATIONS_FILE $REGISTRATION_PROOF_FILE --account $FOUNDRY_WALLET --rpc-url $RPC_URL + // forge script script/Getters.s.sol:GettersScript --sig "getRegistrationProof(address,bytes,bytes32,string,string)" $REGISTRY_ADDRESS $PUBKEY $SIGNING_ID $SIGNED_REGISTRATIONS_FILE $REGISTRATION_PROOF_FILE --account $FOUNDRY_WALLET --rpc-url $RPC_URL function getRegistrationProof( address _registry, bytes memory pubkey, + bytes32 signingId, string memory signedRegistrationsFile, string memory outfile ) public returns (IRegistry.RegistrationProof memory) { @@ -131,7 +134,8 @@ contract GettersScript is BaseScript { IRegistry registry = IRegistry(_registry); // Call URC.getRegistrationProof - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(registrations, owner, leafIndex); + IRegistry.RegistrationProof memory proof = + registry.getRegistrationProof(registrations, owner, leafIndex, signingId); // Check that the registration proof is valid registry.verifyMerkleProof(proof); diff --git a/script/README.md b/script/README.md index fb39ffc..025a692 100644 --- a/script/README.md +++ b/script/README.md @@ -13,12 +13,12 @@ forge script script/Deploy.s.sol:DeployScript --sig "deploy()" --rpc-url $RPC_UR Running this script will generate `N` `SignedRegistration` messages signed by deterministic BLS private keys. The `SignedRegistrations` will be saved to `script/output/{$SIGNED_REGISTRATIONS_FILE}` following the template file [SignedRegistrations.json](./output/SignedRegistrations.json). ```bash -forge script script/Register.s.sol:RegisterScript --sig "nDummyRegistrations(uint256,address,string)" $N $OWNER $SIGNED_REGISTRATIONS_FILE +forge script script/Register.s.sol:RegisterScript --sig "nDummyRegistrations(uint256,address,bytes32,string)" $N $OWNER $SIGNING_ID $SIGNED_REGISTRATIONS_FILE ``` ### Registering to the URC Running this script will call `register()` using the supplied `SignedRegistrations` located in `script/output/{$SIGNED_REGISTRATIONS_FILE}` following the template file [SignedRegistrations.json](./output/SignedRegistrations.json). `$COLLATERAL` wei will be transferred from the caller's account to the URC. ```bash -forge script script/Register.s.sol:RegisterScript --sig "register(address,uint256,string)" $REGISTRY_ADDRESS $COLLATERAL $SIGNED_REGISTRATIONS_FILE --account $FOUNDRY_WALLET --rpc-url $RPC_URL --broadcast +forge script script/Register.s.sol:RegisterScript --sig "register(address,uint256,bytes32,string)" $REGISTRY_ADDRESS $COLLATERAL $SIGNING_ID $SIGNED_REGISTRATIONS_FILE --account $FOUNDRY_WALLET --rpc-url $RPC_URL --broadcast ``` ### Unregistering from the URC @@ -70,7 +70,7 @@ forge script script/Collateral.s.sol:CollateralScript --sig "claimSlashedCollate Running this script will generate and register an intentionally invalid BLS registration. The registration will be saved to `script/output/{$SIGNED_REGISTRATIONS_FILE}` following the template file [SignedRegistrations.json](./output/SignedRegistrations.json). ```bash -forge script script/Slashing.s.sol:SlashingScript --sig "registerBadRegistration(address,address,string)" $REGISTRY_ADDRESS $OWNER $SIGNED_REGISTRATIONS_FILE --account $FOUNDRY_WALLET --rpc-url $RPC_URL --broadcast +forge script script/Slashing.s.sol:SlashingScript --sig "registerBadRegistration(address,address,bytes32,string)" $REGISTRY_ADDRESS $OWNER $SIGNING_ID $SIGNED_REGISTRATIONS_FILE --account $FOUNDRY_WALLET --rpc-url $RPC_URL --broadcast ``` ### Slashing a registration @@ -110,7 +110,7 @@ forge script script/Getters.s.sol:GettersScript --sig "getOperatorData(address,b Given a compressed 48-byte-hex-encoded BLS `$PUBKEY` and a `SignedRegistrations` file located in `script/output/{$SIGNED_REGISTRATIONS_FILE}`, the script will write the `RegistrationProof` for the specified pubkey to `script/output/{$REGISTRATION_PROOF_FILE}` following the template file [`RegistrationProof.json`](./output/RegistrationProof.json). ```bash -forge script script/Getters.s.sol:GettersScript --sig "getRegistrationProof(address,bytes,string,string)" $REGISTRY_ADDRESS $PUBKEY $SIGNED_REGISTRATIONS_FILE $REGISTRATION_PROOF_FILE --account $FOUNDRY_WALLET --rpc-url $RPC_URL +forge script script/Getters.s.sol:GettersScript --sig "getRegistrationProof(address,bytes,bytes32,string,string)" $REGISTRY_ADDRESS $PUBKEY $SIGNING_ID $SIGNED_REGISTRATIONS_FILE $REGISTRATION_PROOF_FILE --account $FOUNDRY_WALLET --rpc-url $RPC_URL ``` ### getSlasherCommitment diff --git a/script/Register.s.sol b/script/Register.s.sol index f1855b7..5a84eb4 100644 --- a/script/Register.s.sol +++ b/script/Register.s.sol @@ -6,11 +6,13 @@ import "../src/IRegistry.sol"; import "./BaseScript.s.sol"; contract RegisterScript is BaseScript { - // forge script script/Register.s.sol:RegisterScript --sig "register(address,uint256,string)" $REGISTRY_ADDRESS $COLLATERAL $SIGNED_REGISTRATIONS_FILE --account $FOUNDRY_WALLET --rpc-url $RPC_URL --broadcast - function register(address _registry, uint256 collateralWei, string memory signedRegistrationsFile) - external - returns (bytes32 registrationRoot) - { + // forge script script/Register.s.sol:RegisterScript --sig "register(address,uint256,bytes32,string)" $REGISTRY_ADDRESS $COLLATERAL $SIGNING_ID $SIGNED_REGISTRATIONS_FILE --account $FOUNDRY_WALLET --rpc-url $RPC_URL --broadcast + function register( + address _registry, + uint256 collateralWei, + bytes32 signingId, + string memory signedRegistrationsFile + ) external returns (bytes32 registrationRoot) { // Start broadcasting transactions vm.startBroadcast(); @@ -35,7 +37,7 @@ contract RegisterScript is BaseScript { IRegistry registry = IRegistry(_registry); // Call register - registrationRoot = registry.register{ value: collateralWei }(registrations, owner); + registrationRoot = registry.register{ value: collateralWei }(registrations, owner, signingId); console.log("Success! got registrationRoot:", vm.toString(registrationRoot)); @@ -76,15 +78,19 @@ contract RegisterScript is BaseScript { /// @dev NOT MEANT FOR PRODUCTION USE /// @dev Signs N registration messages and writes them to `outfile` /// @dev Derives N dummy BLS private keys from the `owner` address - // forge script script/Register.s.sol:RegisterScript --sig "nDummyRegistrations(uint256,address,string)" $N $OWNER $SIGNED_REGISTRATIONS_FILE - function nDummyRegistrations(uint256 n, address owner, string memory outfile) public { + /// @dev Reads the signing domain and chain ID from the default "config/registry.json" file + // forge script script/Register.s.sol:RegisterScript --sig "nDummyRegistrations(uint256,address,string,bytes32)" $N $OWNER $SIGNED_REGISTRATIONS_FILE $SIGNING_ID + function nDummyRegistrations(uint256 n, address owner, bytes32 signingId, string memory outfile) public { console.log("Running nDummyRegistrations()... WARNING do not use the output in production!!!"); + (bytes32 signingDomain, bytes32 chainId) = _defaultSigningParams(); + // The n'th private key will = startPrivateKey + n uint256 startPrivateKey = uint256(keccak256(abi.encode(owner))); // Sign the registration messages - IRegistry.SignedRegistration[] memory registrations = _nRegistrations(n, startPrivateKey, owner); + IRegistry.SignedRegistration[] memory registrations = + _nRegistrations(n, startPrivateKey, owner, signingDomain, signingId, chainId); // Write them to a JSON file _writeSignedRegistrations(owner, registrations, outfile); diff --git a/script/Slashing.s.sol b/script/Slashing.s.sol index cbbdb5d..b540541 100644 --- a/script/Slashing.s.sol +++ b/script/Slashing.s.sol @@ -6,13 +6,14 @@ import "../src/IRegistry.sol"; import "./BaseScript.s.sol"; import "../src/ISlasher.sol"; import { BLSUtils } from "../src/lib/BLSUtils.sol"; -import "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; +import { ECDSAUtils } from "../src/lib/ECDSAUtils.sol"; +import { ECDSA } from "solady/utils/ECDSA.sol"; import { DummySlasher } from "../test/Slasher.t.sol"; contract SlashingScript is BaseScript { - // forge script script/Slashing.s.sol:SlashingScript --sig "registerBadRegistration(address,address,string)" $REGISTRY_ADDRESS $OWNER $SIGNED_REGISTRATIONS_FILE --account $FOUNDRY_WALLET --rpc-url $RPC_URL --broadcast - function registerBadRegistration(address _registry, address owner, string memory outfile) + // forge script script/Slashing.s.sol:SlashingScript --sig "registerBadRegistration(address,address,bytes32,string)" $REGISTRY_ADDRESS $OWNER $SIGNING_ID $SIGNED_REGISTRATIONS_FILE --account $FOUNDRY_WALLET --rpc-url $RPC_URL --broadcast + function registerBadRegistration(address _registry, address owner, bytes32 signingId, string memory outfile) external returns (bytes32 registrationRoot) { @@ -22,8 +23,12 @@ contract SlashingScript is BaseScript { // Generate an invalid BLS registration using a deterministic private key uint256 privateKey = 12345; + // Read the signing domain and chain ID from the config file + (bytes32 signingDomain, bytes32 chainId) = _defaultSigningParams(); + // different owner address for invalid registration - IRegistry.SignedRegistration[] memory registrations = _nRegistrations(1, privateKey, address(1337)); + IRegistry.SignedRegistration[] memory registrations = + _nRegistrations(1, privateKey, address(1337), signingDomain, signingId, chainId); // Get reference to the registry IRegistry registry = IRegistry(_registry); @@ -35,7 +40,7 @@ contract SlashingScript is BaseScript { _prettyPrintPubKey(registrations[0]); // Register the invalid registration - registrationRoot = registry.register{ value: collateralWei }(registrations, owner); + registrationRoot = registry.register{ value: collateralWei }(registrations, owner, signingId); console.log("Registered bad registration with root:", vm.toString(registrationRoot)); @@ -75,8 +80,14 @@ contract SlashingScript is BaseScript { // For testing we assume the proposer private key is generated from their owner address // uint256 proposerPrivateKey = uint256(keccak256(abi.encode(owner))); uint256 proposerPrivateKey = uint256(keccak256(abi.encode(owner))); + + // Read the signing domain and chain ID from the config file + (bytes32 signingDomain, bytes32 chainId) = _defaultSigningParams(); + bytes32 signingId = keccak256("test-signing-id"); + // Generate two delegations with the same proposer and slot but different delegates - ISlasher.SignedDelegation[] memory delegations = _nDelegations(2, proposerPrivateKey, 1, committer, slot); + ISlasher.SignedDelegation[] memory delegations = + _nDelegations(2, proposerPrivateKey, 1, committer, slot, signingDomain, signingId, chainId); // Write the delegations to files _writeDelegation(delegations[0], delegationOneFile); @@ -180,6 +191,34 @@ contract SlashingScript is BaseScript { vm.stopBroadcast(); } + function _testDelegation(address owner, address committer, uint256 committerPrivateKey) + internal + returns (ISlasher.SignedDelegation memory signedDelegation) + { + // Read the signing domain and chain ID from the config file + (bytes32 signingDomain, bytes32 chainId) = _defaultSigningParams(); + + // hardcoded committer + (address _committer, uint256 _committerPrivateKey) = makeAddrAndKey("committer"); + + // sign the delegation + uint256 proposerPrivateKey = uint256(keccak256(abi.encode(owner))); + signedDelegation = _signTestDelegation( + proposerPrivateKey, + ISlasher.Delegation({ + proposer: BLSUtils.toPublicKey(proposerPrivateKey), + delegate: BLSUtils.toPublicKey(0), // unused + committer: _committer, + slot: 5, + metadata: "" + }), + signingDomain, + keccak256("test-signing-id"), + uint64(0), // nonce + chainId + ); + } + /// @dev NOT MEANT FOR PRODUCTION USE /// forge script script/Slashing.s.sol:SlashingScript --sig "prepareSlashing(address,address,bytes32,string,string,bytes)" $REGISTRY_ADDRESS $OWNER $REGISTRATION_ROOT $DELEGATION_ONE_FILE $COMMITMENT_FILE $EVIDENCE --account $FOUNDRY_WALLET --rpc-url $RPC_URL --broadcast function prepareSlashing( @@ -190,62 +229,116 @@ contract SlashingScript is BaseScript { string memory commitmentFile, bytes calldata evidence ) external { - // Start broadcasting transactions vm.startBroadcast(); - // Deploy a dummy slasher contract - address dummySlasher = address(new DummySlasher()); + // Deploy dummy slasher and get committer + (address dummySlasher, address _committer, uint256 _committerPrivateKey) = _deployDummySlasherAndGetCommitter(); - // hardcoded committer - (address committer, uint256 committerPrivateKey) = makeAddrAndKey("committer"); + // Create and sign delegation + ISlasher.SignedDelegation memory signedDelegation = + _createSignedDelegation(owner, _committer, _committerPrivateKey); - // sign the delegation - uint256 proposerPrivateKey = uint256(keccak256(abi.encode(owner))); - ISlasher.SignedDelegation memory signedDelegation = _signTestDelegation( - proposerPrivateKey, - ISlasher.Delegation({ - proposer: BLSUtils.toPublicKey(proposerPrivateKey), - delegate: BLSUtils.toPublicKey(0), // unused - committer: committer, - slot: 5, - metadata: "" - }) - ); + // Create and sign commitment + ISlasher.SignedCommitment memory signedCommitment = + _createSignedCommitment(_committerPrivateKey, dummySlasher, _committer); + + // Write files and verify + _writeAndVerifyFiles(signedDelegation, signedCommitment, delegationFile, commitmentFile, _committer); + + // Opt in to slasher + _optInToSlasher(_registry, registrationRoot, dummySlasher, _committer); + + vm.stopBroadcast(); + } + + function _deployDummySlasherAndGetCommitter() + internal + returns (address dummySlasher, address _committer, uint256 _committerPrivateKey) + { + dummySlasher = address(new DummySlasher()); + (_committer, _committerPrivateKey) = makeAddrAndKey("committer"); + } + + function _createSignedDelegation(address owner, address _committer, uint256 _committerPrivateKey) + internal + returns (ISlasher.SignedDelegation memory) + { + return _testDelegation(owner, _committer, _committerPrivateKey); + } + + function _createSignedCommitment(uint256 _committerPrivateKey, address dummySlasher, address _committer) + internal + returns (ISlasher.SignedCommitment memory) + { + (bytes32 signingDomain, bytes32 chainId) = _defaultSigningParams(); + bytes32 signingId = keccak256("test-signing-id"); + uint64 nonce = 1; - // sign the commitment ISlasher.SignedCommitment memory signedCommitment = - _signTestCommitment(committerPrivateKey, dummySlasher, 0, ""); + _signTestCommitment(_committerPrivateKey, dummySlasher, 0, "", signingId, nonce, chainId, signingDomain); - // sanity check verify signature as the URC would - address committerRecovered = - ECDSA.recover(keccak256(abi.encode(signedCommitment.commitment)), signedCommitment.signature); - if (committerRecovered != committer) { + // Verify signature + _verifyCommitmentSignature(signedCommitment, _committer, signingDomain, chainId); + + return signedCommitment; + } + + function _verifyCommitmentSignature( + ISlasher.SignedCommitment memory signedCommitment, + address _committer, + bytes32 signingDomain, + bytes32 chainId + ) internal view { + bytes32 messageHash = keccak256(abi.encode(IRegistry.MessageType.Commitment, signedCommitment.commitment)); + address committerRecovered = ECDSAUtils.recover( + messageHash, + signedCommitment.signature, + signingDomain, + signedCommitment.signingId, + signedCommitment.nonce, + chainId + ); + if (committerRecovered != _committer) { revert("Recovered committer does not match"); } + } - // write the signed delegation to file + function _writeAndVerifyFiles( + ISlasher.SignedDelegation memory signedDelegation, + ISlasher.SignedCommitment memory signedCommitment, + string memory delegationFile, + string memory commitmentFile, + address _committer + ) internal { + // Write delegation to file _writeDelegation(signedDelegation, delegationFile); console.log("wrote delegation to file:", delegationFile); - // write signed commitment to file + // Write commitment to file _writeCommitment(signedCommitment, commitmentFile); console.log("wrote commitment to file:", commitmentFile); - // sanity check read commitment from file + // Verify commitment from file + _verifyCommitmentFromFile(commitmentFile, _committer); + } + + function _verifyCommitmentFromFile(string memory commitmentFile, address _committer) internal { + (bytes32 signingDomain, bytes32 chainId) = _defaultSigningParams(); + ISlasher.SignedCommitment memory s = _readCommitment(commitmentFile); - committerRecovered = ECDSA.recover(keccak256(abi.encode(s.commitment)), s.signature); - if (committerRecovered != committer) { + bytes32 messageHash = keccak256(abi.encode(IRegistry.MessageType.Commitment, s.commitment)); + address committerRecovered = + ECDSAUtils.recover(messageHash, s.signature, signingDomain, s.signingId, s.nonce, chainId); + if (committerRecovered != _committer) { revert("Recovered committer does not match"); } + } - // Get reference to the registry + function _optInToSlasher(address _registry, bytes32 registrationRoot, address dummySlasher, address _committer) + internal + { IRegistry registry = IRegistry(_registry); - - // Call optInToSlasher - registry.optInToSlasher(registrationRoot, dummySlasher, committer); - + registry.optInToSlasher(registrationRoot, dummySlasher, _committer); console.log("Opted in to dummy slasher:", vm.toString(dummySlasher)); - - vm.stopBroadcast(); } } diff --git a/script/example.env b/script/example.env index e306d08..3470bdc 100644 --- a/script/example.env +++ b/script/example.env @@ -1,13 +1,14 @@ FOUNDRY_WALLET=anvil-0 OWNER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 +SIGNING_ID=0x1111111111111111111111111111111111111111111111111111111111111111 RPC_URL=127.0.0.1:8545 REGISTRY_ADDRESS=0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 -PUBKEY=0x8c1e8d9e79e7b35cdd484f10711aedd804bb24eab3ebee66ecdec22549388fbd21e83bc6956dc9d7a986b6eccdcdf991 +PUBKEY=0x8530c1bdc4cd6b1408be0933c4a41ac3513350eef36850b804708e1f338932ce01b655a163344a4500b281c8750c461f INFILE=in.json OUTFILE=out.json N=2 COLLATERAL=1000000000000000000 -REGISTRATION_ROOT=0xc5598ecd90f70a154eadc8c309845bdf851c4db9cbffd24a9975911c7a4a9594 +REGISTRATION_ROOT=0x5a97cf75f1a84c2776235395070b1cfe32b22492cfe3729131c47eb842b858ed SLASHER=0x0000000000000000000000000000000000000001 COMMITTER=0x0000000000000000000000000000000000000002 SIGNED_REGISTRATIONS_FILE=signed_registrations.json @@ -18,4 +19,5 @@ REGISTRATION_PROOF_FILE=registration_proof.json DELEGATION_ONE_FILE=delegation_one.json DELEGATION_TWO_FILE=delegation_two.json COMMITMENT_FILE=commitment.json -EVIDENCE=0x0000 \ No newline at end of file +EVIDENCE=0x0000 +SLOT=1000 \ No newline at end of file diff --git a/script/output/RegistrationProof.json b/script/output/RegistrationProof.json index 5fe9ac9..9dd74ac 100644 --- a/script/output/RegistrationProof.json +++ b/script/output/RegistrationProof.json @@ -2,5 +2,6 @@ "registrationRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", "registration": "\u0000", "merkleProof": [], - "leafIndex": 0 + "leafIndex": 0, + "signingId": "0x0000000000000000000000000000000000000000000000000000000000000000" } \ No newline at end of file diff --git a/script/output/config.json b/script/output/config.json new file mode 100644 index 0000000..642783e --- /dev/null +++ b/script/output/config.json @@ -0,0 +1,9 @@ +{ + "minCollateralWei": 1000000000000000000, + "fraudProofWindow": 10, + "unregistrationDelay": 10, + "slashWindow": 10, + "optInDelay": 10, + "signingDomain": "0x0000000000000000000000000000000000000000000000000000000000000000", + "chainId": "0x0000000000000000000000000000000000000000000000000000000000000000" +} \ No newline at end of file diff --git a/script/output/getConfig.json b/script/output/getConfig.json index 80ed7f1..223edbc 100644 --- a/script/output/getConfig.json +++ b/script/output/getConfig.json @@ -3,5 +3,7 @@ "fraudProofWindow": 0, "unregistrationDelay": 0, "slashWindow": 0, - "optInDelay": 0 + "optInDelay": 0, + "signingDomain": "0x0000000000000000000000000000000000000000000000000000000000000000", + "chainId": "0x0000000000000000000000000000000000000000000000000000000000000000" } \ No newline at end of file diff --git a/src/IRegistry.sol b/src/IRegistry.sol index 9e8d01f..3b508dc 100644 --- a/src/IRegistry.sol +++ b/src/IRegistry.sol @@ -12,7 +12,6 @@ interface IRegistry { * * * */ - /// @notice A struct to track the configuration of the registry struct Config { /// The minimum collateral required to register @@ -25,6 +24,10 @@ interface IRegistry { uint32 slashWindow; /// The opt-in delay uint32 optInDelay; + /// The signing domain + bytes32 signingDomain; + /// The chain ID + bytes32 chainId; } /// @notice A registration of a BLS key @@ -33,6 +36,8 @@ interface IRegistry { BLS.G1Point pubkey; /// BLS signature BLS.G2Point signature; + /// Nonce + uint64 nonce; } /// @notice Data about an operator @@ -90,6 +95,14 @@ interface IRegistry { Commitment } + enum MessageType { + Reserved, + Registration, + Delegation, + Commitment, + Constraints + } + struct RegistrationProof { /// The merkle root of the registration merkle tree bytes32 registrationRoot; @@ -97,6 +110,8 @@ interface IRegistry { SignedRegistration registration; /// The merkle proof to verify the operator's key is in the registry bytes32[] merkleProof; + /// The signing ID for the module (Commit-Boost) + bytes32 signingId; } /** @@ -215,10 +230,12 @@ interface IRegistry { /// @dev - The registration root is invalid (InvalidRegistrationRoot) /// @dev - The collateral amount overflows the `collateralWei` field (CollateralOverflow) /// @dev - The owner address is 0 (InvalidOwnerAddress) + /// @dev It's assumed that the `signingId` is the same for all `SignedRegistration` structs /// @param registrations The BLS keys to register /// @param owner The authorized address to perform actions on behalf of the operator + /// @param signingId The signing ID for the module (Commit-Boost) /// @return registrationRoot The merkle root of the registration - function register(SignedRegistration[] calldata registrations, address owner) + function register(SignedRegistration[] calldata registrations, address owner, bytes32 signingId) external payable returns (bytes32 registrationRoot); @@ -434,12 +451,16 @@ interface IRegistry { /// @notice Returns a `RegistrationProof` for a given `SignedRegistration` array /// @dev This function is not intended to be called on-chain due to gas costs + /// @dev It's assumed that the `signingId` is the same for all `SignedRegistration` structs /// @param regs The array of all `SignedRegistration` structs submitted during the initial call to `register()` /// @param owner The owner address of the operator /// @param leafIndex The index of the leaf the proof is for + /// @param signingId The signing ID for the module (Commit-Boost) /// @return proof The `RegistrationProof` for the given `SignedRegistration` array - function getRegistrationProof(SignedRegistration[] calldata regs, address owner, uint256 leafIndex) - external - pure - returns (RegistrationProof memory proof); + function getRegistrationProof( + SignedRegistration[] calldata regs, + address owner, + uint256 leafIndex, + bytes32 signingId + ) external pure returns (RegistrationProof memory proof); } diff --git a/src/ISlasher.sol b/src/ISlasher.sol index 2bc5f83..5e25047 100644 --- a/src/ISlasher.sol +++ b/src/ISlasher.sol @@ -22,16 +22,32 @@ interface ISlasher { struct SignedDelegation { /// The delegation message Delegation delegation; + /// The nonce of the delegation message + uint64 nonce; + /// The signing ID of the delegation message (Commit-Boost) + bytes32 signingId; /// The signature of the delegation message BLS.G2Point signature; } + /// @notice A CommitmentRequest message binding an opaque payload to a slasher contract + struct CommitmentRequest { + /// The type of commitment + uint64 commitmentType; + /// The payload of the commitment + bytes payload; + /// The address of the slasher contract + address slasher; + } + /// @notice A Commitment message binding an opaque payload to a slasher contract struct Commitment { /// The type of commitment uint64 commitmentType; /// The payload of the commitment bytes payload; + /// The hash of the commitment request + bytes32 requestHash; /// The address of the slasher contract address slasher; } @@ -40,6 +56,10 @@ interface ISlasher { struct SignedCommitment { /// The commitment message Commitment commitment; + /// The nonce of the commitment message + uint64 nonce; + /// The signing ID of the commitment message (Commit-Boost) + bytes32 signingId; /// The signature of the commitment message bytes signature; } diff --git a/src/Registry.sol b/src/Registry.sol index 7ace8e0..70407c0 100644 --- a/src/Registry.sol +++ b/src/Registry.sol @@ -1,9 +1,8 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity >=0.8.0 <0.9.0; -import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; - import { BLSUtils } from "./lib/BLSUtils.sol"; +import { ECDSAUtils } from "./lib/ECDSAUtils.sol"; import { MerkleTree } from "./lib/MerkleTree.sol"; import { IRegistry } from "./IRegistry.sol"; import { ISlasher } from "./ISlasher.sol"; @@ -17,8 +16,6 @@ contract Registry is IRegistry { // Constants address internal constant BURNER_ADDRESS = address(0x0000000000000000000000000000000000000000); - bytes public constant REGISTRATION_DOMAIN_SEPARATOR = "0x00555243"; // "URC" in little endian - bytes public constant DELEGATION_DOMAIN_SEPARATOR = "0x0044656c"; // "Del" in little endian /// @notice The configuration for the URC Config private config; @@ -34,7 +31,7 @@ contract Registry is IRegistry { */ /// @inheritdoc IRegistry - function register(SignedRegistration[] calldata registrations, address owner) + function register(SignedRegistration[] calldata registrations, address owner, bytes32 signingId) external payable returns (bytes32 registrationRoot) @@ -51,7 +48,7 @@ contract Registry is IRegistry { if (owner == address(0)) revert InvalidOwnerAddress(); // note: owner address is mixed into the Merkle leaves to bind the registrationRoot to the owner - registrationRoot = _merkleizeSignedRegistrationsWithOwner(registrations, owner); + registrationRoot = _merkleizeSignedRegistrationsWithOwner(registrations, owner, signingId); // Revert on a bad registration root if (registrationRoot == bytes32(0)) revert InvalidRegistrationRoot(); @@ -74,9 +71,8 @@ contract Registry is IRegistry { newOperator.data.slashedAt = 0; // Store the initial collateral value in the history - newOperator.collateralHistory.push( - CollateralRecord({ timestamp: uint64(block.timestamp), collateralValue: uint80(msg.value) }) - ); + newOperator.collateralHistory + .push(CollateralRecord({ timestamp: uint64(block.timestamp), collateralValue: uint80(msg.value) })); emit OperatorRegistered(registrationRoot, msg.value, owner); } @@ -243,14 +239,18 @@ contract Registry is IRegistry { _verifyMerkleProof(proof); // Reconstruct registration message - bytes memory message = abi.encode(operator.data.owner); - - // Verify registration signature, note the domain separator mixin - if ( - BLSUtils.verify( - message, proof.registration.signature, proof.registration.pubkey, REGISTRATION_DOMAIN_SEPARATOR - ) - ) { + bytes32 messageHash = keccak256(abi.encode(MessageType.Registration, operator.data.owner)); + + // Verify registration signature + if (BLSUtils.verify( + messageHash, + proof.registration.signature, + proof.registration.pubkey, + config.signingDomain, + proof.signingId, + proof.registration.nonce, + config.chainId + )) { revert FraudProofChallengeInvalid(); } @@ -266,9 +266,10 @@ contract Registry is IRegistry { _rewardAndBurn(config.minCollateralWei / 2, msg.sender); // Push collateral changes - operator.collateralHistory.push( - CollateralRecord({ timestamp: uint64(block.timestamp), collateralValue: operator.data.collateralWei }) - ); + operator.collateralHistory + .push( + CollateralRecord({ timestamp: uint64(block.timestamp), collateralValue: operator.data.collateralWei }) + ); emit OperatorSlashed( SlashingType.Fraud, @@ -302,7 +303,14 @@ contract Registry is IRegistry { _verifyDelegation(proof, delegation); // Verify the commitment was signed by the commitment key from the Delegation - address committer = ECDSA.recover(keccak256(abi.encode(commitment.commitment)), commitment.signature); + address committer = ECDSAUtils.recover( + keccak256(abi.encode(MessageType.Commitment, commitment.commitment)), + commitment.signature, + config.signingDomain, + commitment.signingId, + commitment.nonce, + config.chainId + ); if (committer != delegation.delegation.committer) { revert UnauthorizedCommitment(); } @@ -311,9 +319,8 @@ contract Registry is IRegistry { slashedBefore[slashingDigest] = true; // Call the Slasher contract to slash the operator - slashAmountWei = ISlasher(commitment.commitment.slasher).slash( - delegation.delegation, commitment.commitment, committer, evidence, msg.sender - ); + slashAmountWei = ISlasher(commitment.commitment.slasher) + .slash(delegation.delegation, commitment.commitment, committer, evidence, msg.sender); // Handle the slashing accounting _slashCommitment(proof.registrationRoot, slashAmountWei, commitment.commitment.slasher); @@ -340,7 +347,14 @@ contract Registry is IRegistry { } // Verify the commitment was signed by the registered committer from the optInToSlasher() function - address committer = ECDSA.recover(keccak256(abi.encode(commitment.commitment)), commitment.signature); + address committer = ECDSAUtils.recover( + keccak256(abi.encode(MessageType.Commitment, commitment.commitment)), + commitment.signature, + config.signingDomain, + commitment.signingId, + commitment.nonce, + config.chainId + ); if (committer != slasherCommitment.committer) { revert UnauthorizedCommitment(); } @@ -357,9 +371,8 @@ contract Registry is IRegistry { ISlasher.Delegation memory dummyDelegation; // Call the Slasher contract to slash the operator - slashAmountWei = ISlasher(commitment.commitment.slasher).slash( - dummyDelegation, commitment.commitment, committer, evidence, msg.sender - ); + slashAmountWei = ISlasher(commitment.commitment.slasher) + .slash(dummyDelegation, commitment.commitment, committer, evidence, msg.sender); // Handle the slashing accounting _slashCommitment(registrationRoot, slashAmountWei, commitment.commitment.slasher); @@ -385,6 +398,8 @@ contract Registry is IRegistry { && keccak256(abi.encode(delegationOne.delegation.delegate)) == keccak256(abi.encode(delegationTwo.delegation.delegate)) && delegationOne.delegation.committer == delegationTwo.delegation.committer + && keccak256(abi.encode(delegationOne.delegation.metadata)) + == keccak256(abi.encode(delegationTwo.delegation.metadata)) ) { revert DelegationsAreSame(); } @@ -430,9 +445,10 @@ contract Registry is IRegistry { operator.data.collateralWei -= uint80(config.minCollateralWei); // Push collateral changes - operator.collateralHistory.push( - CollateralRecord({ timestamp: uint64(block.timestamp), collateralValue: operator.data.collateralWei }) - ); + operator.collateralHistory + .push( + CollateralRecord({ timestamp: uint64(block.timestamp), collateralValue: operator.data.collateralWei }) + ); // Burn half of the MIN_COLLATERAL amount and reward the challenger the other half _rewardAndBurn(config.minCollateralWei / 2, msg.sender); @@ -472,9 +488,10 @@ contract Registry is IRegistry { operator.data.collateralWei += uint80(msg.value); // Store the updated collateral value in the history - operator.collateralHistory.push( - CollateralRecord({ timestamp: uint64(block.timestamp), collateralValue: operator.data.collateralWei }) - ); + operator.collateralHistory + .push( + CollateralRecord({ timestamp: uint64(block.timestamp), collateralValue: operator.data.collateralWei }) + ); emit CollateralAdded(registrationRoot, operator.data.collateralWei); } @@ -641,15 +658,17 @@ contract Registry is IRegistry { } /// @inheritdoc IRegistry - function getRegistrationProof(SignedRegistration[] calldata regs, address owner, uint256 leafIndex) - external - pure - returns (RegistrationProof memory proof) - { - proof.registrationRoot = _merkleizeSignedRegistrationsWithOwner(regs, owner); + function getRegistrationProof( + SignedRegistration[] calldata regs, + address owner, + uint256 leafIndex, + bytes32 signingId + ) external pure returns (RegistrationProof memory proof) { + proof.registrationRoot = _merkleizeSignedRegistrationsWithOwner(regs, owner, signingId); proof.registration = regs[leafIndex]; + proof.signingId = signingId; - bytes32[] memory leaves = MerkleTree.hashToLeaves(regs, owner); + bytes32[] memory leaves = MerkleTree.hashToLeaves(regs, owner, signingId); proof.merkleProof = MerkleTree.generateProof(leaves, leafIndex); } @@ -686,9 +705,10 @@ contract Registry is IRegistry { operator.data.collateralWei -= uint80(slashAmountWei); // Push collateral changes - operator.collateralHistory.push( - CollateralRecord({ timestamp: uint64(block.timestamp), collateralValue: operator.data.collateralWei }) - ); + operator.collateralHistory + .push( + CollateralRecord({ timestamp: uint64(block.timestamp), collateralValue: operator.data.collateralWei }) + ); // Burn the slashed amount _burnETH(slashAmountWei); @@ -702,13 +722,13 @@ contract Registry is IRegistry { /// @dev Leaves are created by abi-encoding the `SignedRegistration` structs with the owner address, then hashing with keccak256. /// @param regs The array of `SignedRegistration` structs to merkleize /// @return registrationRoot The merkle root of the registration - function _merkleizeSignedRegistrationsWithOwner(SignedRegistration[] calldata regs, address owner) - internal - pure - returns (bytes32 registrationRoot) - { + function _merkleizeSignedRegistrationsWithOwner( + SignedRegistration[] calldata regs, + address owner, + bytes32 signingId + ) internal pure returns (bytes32 registrationRoot) { // Create leaves array with padding - bytes32[] memory leaves = MerkleTree.hashToLeaves(regs, owner); + bytes32[] memory leaves = MerkleTree.hashToLeaves(regs, owner, signingId); // Merkleize the leaves registrationRoot = MerkleTree.generateTree(leaves); @@ -721,7 +741,7 @@ contract Registry is IRegistry { /// @param proof The merkle proof to verify the operator's key is in the registry function _verifyMerkleProof(RegistrationProof calldata proof) internal view { address owner = operators[proof.registrationRoot].data.owner; - bytes32 leaf = keccak256(abi.encode(proof.registration, owner)); + bytes32 leaf = keccak256(abi.encode(proof.registration, owner, proof.signingId)); if (!MerkleTree.verifyProofCalldata(proof.registrationRoot, leaf, proof.merkleProof)) { revert InvalidProof(); } @@ -730,15 +750,17 @@ contract Registry is IRegistry { /// @notice Verifies a delegation was signed by an operator's registered BLS key /// @dev The function will return revert if either the registration proof is invalid /// @dev or the Delegation signature is invalid - /// @dev The `signedDelegation.signature` is expected to be the abi-encoded `Delegation` message mixed with the URC's `DELEGATION_DOMAIN_SEPARATOR`. /// @param proof The merkle proof to verify the operator's key is in the registry - /// @param delegation The SignedDelegation signed by the operator's BLS key - function _verifyDelegation(RegistrationProof calldata proof, ISlasher.SignedDelegation calldata delegation) + /// @param signedDelegation The SignedDelegation signed by the operator's BLS key + function _verifyDelegation(RegistrationProof calldata proof, ISlasher.SignedDelegation calldata signedDelegation) internal view { // Verify the public key in the proof is the same as the public key in the SignedDelegation - if (keccak256(abi.encode(proof.registration.pubkey)) != keccak256(abi.encode(delegation.delegation.proposer))) { + if ( + keccak256(abi.encode(proof.registration.pubkey)) + != keccak256(abi.encode(signedDelegation.delegation.proposer)) + ) { revert InvalidProof(); } @@ -746,12 +768,18 @@ contract Registry is IRegistry { _verifyMerkleProof(proof); // Reconstruct Delegation message - bytes memory message = abi.encode(delegation.delegation); + bytes32 messageHash = keccak256(abi.encode(MessageType.Delegation, signedDelegation.delegation)); // Verify it was signed by the registered BLS key - if ( - !BLSUtils.verify(message, delegation.signature, delegation.delegation.proposer, DELEGATION_DOMAIN_SEPARATOR) - ) { + if (!BLSUtils.verify( + messageHash, + signedDelegation.signature, + signedDelegation.delegation.proposer, + config.signingDomain, + signedDelegation.signingId, + signedDelegation.nonce, + config.chainId + )) { revert DelegationSignatureInvalid(); } } diff --git a/src/lib/BLS.sol b/src/lib/BLS.sol deleted file mode 100644 index 0df8725..0000000 --- a/src/lib/BLS.sol +++ /dev/null @@ -1,481 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.0 <0.9.0; -// Credit: https://github.com/paradigmxyz/forge-alphanet/blob/main/src/sign/BLS.sol - -/// @title BLS -/// @notice Wrapper functions to abstract low level details of calls to BLS precompiles -/// defined in EIP-2537, see . -/// @dev Precompile addresses come from the BLS addresses submodule in AlphaNet, see -/// -/// @notice `hashToCurve` logic is based on -/// with small modifications including: -/// - Removal of low-level assembly in _modexp to ensure compatibility with EOF which does not support low-level staticcall -/// - Usage of Fp2/G2Point structs defined here for better compatibility with existing methods -library BLS { - /// @dev A base field element (Fp) is encoded as 64 bytes by performing the - /// BigEndian encoding of the corresponding (unsigned) integer. Due to the size of p, - /// the top 16 bytes are always zeroes. - struct Fp { - uint256 a; - uint256 b; - } - - /// @dev For elements of the quadratic extension field (Fp2), encoding is byte concatenation of - /// individual encoding of the coefficients totaling in 128 bytes for a total encoding. - /// c0 + c1 * v - struct Fp2 { - Fp c0; - Fp c1; - } - - /// @dev Points of G1 and G2 are encoded as byte concatenation of the respective - /// encodings of the x and y coordinates. - struct G1Point { - Fp x; - Fp y; - } - - /// @dev Points of G1 and G2 are encoded as byte concatenation of the respective - /// encodings of the x and y coordinates. - struct G2Point { - Fp2 x; - Fp2 y; - } - - /// @dev For addition of two points on the BLS12-381 G1 curve, - address internal constant BLS12_G1ADD = 0x000000000000000000000000000000000000000b; - - /// @dev For multi-scalar multiplication (MSM) on the BLS12-381 G1 curve. - address internal constant BLS12_G1MSM = 0x000000000000000000000000000000000000000C; - - /// @dev For addition of two points on the BLS12-381 G2 curve. - address internal constant BLS12_G2ADD = 0x000000000000000000000000000000000000000d; - - /// @dev For multi-scalar multiplication (MSM) on the BLS12-381 G2 curve. - address internal constant BLS12_G2MSM = 0x000000000000000000000000000000000000000E; - - /// @dev For performing a pairing check on the BLS12-381 curve. - address internal constant BLS12_PAIRING_CHECK = 0x000000000000000000000000000000000000000F; - - /// @dev For mapping a Fp to a point on the BLS12-381 G1 curve. - address internal constant BLS12_MAP_FP_TO_G1 = 0x0000000000000000000000000000000000000010; - - /// @dev For mapping a Fp2 to a point on the BLS12-381 G2 curve. - address internal constant BLS12_MAP_FP2_TO_G2 = 0x0000000000000000000000000000000000000011; - - /// @notice G1ADD operation - /// @param a First G1 point - /// @param b Second G1 point - /// @return result Resulted G1 point - function G1Add(G1Point memory a, G1Point memory b) internal view returns (G1Point memory result) { - (bool success, bytes memory output) = address(BLS12_G1ADD).staticcall(abi.encode(a, b)); - require(success, "G1ADD failed"); - return abi.decode(output, (G1Point)); - } - - /// @notice G1MUL operation - /// @param point G1 point - /// @param scalar Scalar to multiply the point by - /// @return result Resulted G1 point - function G1Mul(G1Point memory point, uint256 scalar) internal view returns (G1Point memory result) { - (bool success, bytes memory output) = address(BLS12_G1MSM).staticcall(abi.encode(point, scalar)); - require(success, "G1MUL failed"); - return abi.decode(output, (G1Point)); - } - - /// @notice G1MSM operation - /// @param points Array of G1 points - /// @param scalars Array of scalars to multiply the points by - /// @return result Resulted G1 point - function G1MSM(G1Point[] memory points, uint256[] memory scalars) internal view returns (G1Point memory result) { - bytes memory input; - - for (uint256 i = 0; i < points.length; i++) { - input = bytes.concat(input, abi.encode(points[i], scalars[i])); - } - - (bool success, bytes memory output) = address(BLS12_G1MSM).staticcall(input); - require(success, "G1MSM failed"); - return abi.decode(output, (G1Point)); - } - - /// @notice G2ADD operation - /// @param a First G2 point - /// @param b Second G2 point - /// @return result Resulted G2 point - function G2Add(G2Point memory a, G2Point memory b) internal view returns (G2Point memory result) { - (bool success, bytes memory output) = address(BLS12_G2ADD).staticcall(abi.encode(a, b)); - require(success, "G2ADD failed"); - return abi.decode(output, (G2Point)); - } - - /// @notice G2MUL operation - /// @param point G2 point - /// @param scalar Scalar to multiply the point by - /// @return result Resulted G2 point - function G2Mul(G2Point memory point, uint256 scalar) internal view returns (G2Point memory result) { - (bool success, bytes memory output) = address(BLS12_G2MSM).staticcall(abi.encode(point, scalar)); - require(success, "G2MUL failed"); - return abi.decode(output, (G2Point)); - } - - /// @notice G2MSM operation - /// @param points Array of G2 points - /// @param scalars Array of scalars to multiply the points by - /// @return result Resulted G2 point - function G2MSM(G2Point[] memory points, uint256[] memory scalars) internal view returns (G2Point memory result) { - bytes memory input; - - for (uint256 i = 0; i < points.length; i++) { - input = bytes.concat(input, abi.encode(points[i], scalars[i])); - } - - (bool success, bytes memory output) = address(BLS12_G2MSM).staticcall(input); - require(success, "G2MSM failed"); - return abi.decode(output, (G2Point)); - } - - /// @notice PAIRING operation - /// @param g1Points Array of G1 points - /// @param g2Points Array of G2 points - /// @return result Returns whether pairing result is equal to the multiplicative identity (1). - function Pairing(G1Point[] memory g1Points, G2Point[] memory g2Points) internal view returns (bool result) { - bytes memory input; - for (uint256 i = 0; i < g1Points.length; i++) { - input = bytes.concat(input, abi.encode(g1Points[i], g2Points[i])); - } - - (bool success, bytes memory output) = address(BLS12_PAIRING_CHECK).staticcall(input); - require(success, "Pairing failed"); - return abi.decode(output, (bool)); - } - - /// @notice MAP_FP_TO_G1 operation - /// @param element Fp element - /// @return result Resulted G1 point - function MapFpToG1(Fp memory element) internal view returns (G1Point memory result) { - (bool success, bytes memory output) = address(BLS12_MAP_FP_TO_G1).staticcall(abi.encode(element)); - require(success, "MAP_FP_TO_G1 failed"); - return abi.decode(output, (G1Point)); - } - - /// @notice MAP_FP2_TO_G2 operation - /// @param element Fp2 element - /// @return result Resulted G2 point - function MapFp2ToG2(Fp2 memory element) internal view returns (G2Point memory result) { - (bool success, bytes memory output) = address(BLS12_MAP_FP2_TO_G2).staticcall(abi.encode(element)); - require(success, "MAP_FP2_TO_G2 failed"); - return abi.decode(output, (G2Point)); - } - - /// @notice Computes a point in G2 from a message - /// @dev Uses the eip-2537 precompiles - /// @param message Arbitrarylength byte string to be hashed - /// @return A point in G2 - function hashToCurveG2(bytes memory message) internal view returns (G2Point memory) { - // 1. u = hash_to_field(msg, 2) - Fp2[2] memory u = hashToFieldFp2(message, bytes("BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_")); - // 2. Q0 = map_to_curve(u[0]) - G2Point memory q0 = MapFp2ToG2(u[0]); - // 3. Q1 = map_to_curve(u[1]) - G2Point memory q1 = MapFp2ToG2(u[1]); - // 4. R = Q0 + Q1 - return G2Add(q0, q1); - } - - /// @notice Computes a field point from a message - /// @dev Follows https://datatracker.ietf.org/doc/html/rfc9380#section-5.2 - /// @param message Arbitrarylength byte string to be hashed - /// @param dst The domain separation tag - /// @return Two field points - function hashToFieldFp2(bytes memory message, bytes memory dst) private view returns (Fp2[2] memory) { - // 1. len_in_bytes = count * m * L - // so always 2 * 2 * 64 = 256 - uint16 lenInBytes = 256; - // 2. uniform_bytes = expand_message(msg, DST, len_in_bytes) - bytes32[] memory pseudoRandomBytes = expandMsgXmd(message, dst, lenInBytes); - Fp2[2] memory u; - // No loop here saves 800 gas hardcoding offset an additional 300 - // 3. for i in (0, ..., count - 1): - // 4. for j in (0, ..., m - 1): - // 5. elm_offset = L * (j + i * m) - // 6. tv = substr(uniform_bytes, elm_offset, HTF_L) - // uint8 HTF_L = 64; - // bytes memory tv = new bytes(64); - // 7. e_j = OS2IP(tv) mod p - // 8. u_i = (e_0, ..., e_(m - 1)) - // tv = bytes.concat(pseudo_random_bytes[0], pseudo_random_bytes[1]); - u[0].c0 = _modfield(pseudoRandomBytes[0], pseudoRandomBytes[1]); - u[0].c1 = _modfield(pseudoRandomBytes[2], pseudoRandomBytes[3]); - u[1].c0 = _modfield(pseudoRandomBytes[4], pseudoRandomBytes[5]); - u[1].c1 = _modfield(pseudoRandomBytes[6], pseudoRandomBytes[7]); - // 9. return (u_0, ..., u_(count - 1)) - return u; - } - - /// @notice Computes a field point from a message - /// @dev Follows https://datatracker.ietf.org/doc/html/rfc9380#section-5.3 - /// @dev bytes32[] because len_in_bytes is always a multiple of 32 in our case even 128 - /// @param message Arbitrarylength byte string to be hashed - /// @param dst The domain separation tag of at most 255 bytes - /// @param lenInBytes The length of the requested output in bytes - /// @return A field point - function expandMsgXmd(bytes memory message, bytes memory dst, uint16 lenInBytes) - private - pure - returns (bytes32[] memory) - { - // 1. ell = ceil(len_in_bytes / b_in_bytes) - // b_in_bytes seems to be 32 for sha256 - // ceil the division - uint256 ell = (lenInBytes - 1) / 32 + 1; - - // 2. ABORT if ell > 255 or len_in_bytes > 65535 or len(DST) > 255 - require(ell <= 255, "len_in_bytes too large for sha256"); - // Not really needed because of parameter type - // require(lenInBytes <= 65535, "len_in_bytes too large"); - // no length normalizing via hashing - require(dst.length <= 255, "dst too long"); - - bytes memory dstPrime = bytes.concat(dst, bytes1(uint8(dst.length))); - - // 4. Z_pad = I2OSP(0, s_in_bytes) - // this should be sha256 blocksize so 64 bytes - bytes memory zPad = new bytes(64); - - // 5. l_i_b_str = I2OSP(len_in_bytes, 2) - // length in byte string? - bytes2 libStr = bytes2(lenInBytes); - - // 6. msg_prime = Z_pad || msg || l_i_b_str || I2OSP(0, 1) || DST_prime - bytes memory msgPrime = bytes.concat(zPad, message, libStr, hex"00", dstPrime); - - // 7. b_0 = H(msg_prime) - bytes32 b_0 = sha256(msgPrime); - - bytes32[] memory b = new bytes32[](ell); - - // 8. b_1 = H(b_0 || I2OSP(1, 1) || DST_prime) - b[0] = sha256(bytes.concat(b_0, hex"01", dstPrime)); - - // 9. for i in (2, ..., ell): - for (uint8 i = 2; i <= ell; i++) { - // 10. b_i = H(strxor(b_0, b_(i - 1)) || I2OSP(i, 1) || DST_prime) - bytes memory tmp = abi.encodePacked(b_0 ^ b[i - 2], i, dstPrime); - b[i - 1] = sha256(tmp); - } - // 11. uniform_bytes = b_1 || ... || b_ell - // 12. return substr(uniform_bytes, 0, len_in_bytes) - // Here we don't need the uniform_bytes because b is already properly formed - return b; - } - - // passing two bytes32 instead of bytes memory saves approx 700 gas per call - // Computes the mod against the bls12-381 field modulus - function _modfield(bytes32 _b1, bytes32 _b2) private view returns (Fp memory r) { - (bool success, bytes memory output) = address(0x5).staticcall( - abi.encode( - // arg[0] = base.length - 0x40, - // arg[1] = exp.length - 0x20, - // arg[2] = mod.length - 0x40, - // arg[3] = base.bits - // places the first 32 bytes of _b1 and the last 32 bytes of _b2 - _b1, - _b2, - // arg[4] = exp - // exponent always 1 - 1, - // arg[5] = mod - // this field_modulus as hex 4002409555221667393417789825735904156556882819939007885332058136124031650490837864442687629129015664037894272559787 - // we add the 0 prefix so that the result will be exactly 64 bytes - // saves 300 gas per call instead of sending it along every time - // places the first 32 bytes and the last 32 bytes of the field modulus - 0x000000000000000000000000000000001a0111ea397fe69a4b1ba7b6434bacd7, - 0x64774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab - ) - ); - require(success, "MODEXP failed"); - return abi.decode(output, (Fp)); - } - - // Function to return G1 generator point - function G1_GENERATOR() internal pure returns (G1Point memory) { - return G1Point( - Fp( - 31827880280837800241567138048534752271, - 88385725958748408079899006800036250932223001591707578097800747617502997169851 - ), - Fp( - 11568204302792691131076548377920244452, - 114417265404584670498511149331300188430316142484413708742216858159411894806497 - ) - ); - } - - // Function to return negated G1 generator point - function NEGATED_G1_GENERATOR() internal pure returns (G1Point memory) { - return G1Point( - Fp( - 31827880280837800241567138048534752271, - 88385725958748408079899006800036250932223001591707578097800747617502997169851 - ), - Fp( - 22997279242622214937712647648895181298, - 46816884707101390882112958134453447585552332943769894357249934112654335001290 - ) - ); - } - - /// @dev Referenced from https://eips.ethereum.org/EIPS/eip-2537#curve-parameters - function baseFieldModulus() internal pure returns (uint256[2] memory) { - return [ - 0x000000000000000000000000000000001a0111ea397fe69a4b1ba7b6434bacd7, - 0x64774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab - ]; - } - - /** - * @notice Negates a G1 point, by reflecting it over the x-axis - * @dev Adapted from https://github.com/NethermindEth/Taiko-Preconf-AVS/blob/004d407105578a83c4815e7ec2c55ec467b9ed3f/SmartContracts/src/libraries/BLS12381.sol#L124 - * @dev Assumes that the Y coordinate is always less than the field modulus - * @param point The G1 point to negate - */ - function negate(G1Point memory point) internal pure returns (G1Point memory) { - uint256[2] memory fieldModulus = baseFieldModulus(); - uint256[2] memory yNeg; - - // Perform word-wise elementary subtraction - if (fieldModulus[1] < point.y.b) { - yNeg[1] = type(uint256).max - (point.y.b - fieldModulus[1]) + 1; - fieldModulus[0] -= 1; // borrow - } else { - yNeg[1] = fieldModulus[1] - point.y.b; - } - yNeg[0] = fieldModulus[0] - point.y.a; - - return G1Point({ x: point.x, y: Fp(yNeg[0], yNeg[1]) }); - } - - /** - * @notice Returns true if `a` is lexicographically greater than `b` - * @dev Adapted from https://github.com/NethermindEth/Taiko-Preconf-AVS/blob/004d407105578a83c4815e7ec2c55ec467b9ed3f/SmartContracts/src/libraries/BLS12381.sol#L124 - * @dev It makes the comparison bit-wise. - * This functions also assumes that the passed values are 48-byte long BLS pub keys that have - * 16 functional bytes in the first word, and 32 bytes in the second. - */ - // function _greaterThan(uint256[2] memory a, uint256[2] memory b) internal pure returns (bool) { - function _greaterThan(Fp memory a, Fp memory b) internal pure returns (bool) { - uint256 wordA; - uint256 wordB; - uint256 mask; - - // Only compare the unequal words - if (a.a == b.a) { - wordA = a.b; - wordB = b.b; - mask = 1 << 255; - } else { - wordA = a.a; - wordB = b.a; - mask = 1 << 127; // Only check for lower 16 bytes in the first word - } - - // We may safely set the control value to be less than 256 since it is guaranteed that the - // the loop returns if the first words are different. - for (uint256 i; i < 256; ++i) { - uint256 x = wordA & mask; - uint256 y = wordB & mask; - - if (x == 0 && y != 0) return false; - if (x != 0 && y == 0) return true; - - mask = mask >> 1; - } - - return false; - } - - /// @notice Converts a private key to a public key by multiplying the generator point with the private key - /// @param privateKey The private key to convert - /// @return The public key - function toPublicKey(uint256 privateKey) internal view returns (G1Point memory) { - return G1Mul(G1_GENERATOR(), privateKey); - } - - /// @notice Converts a message to a G2 point - /// @param message Arbitrarylength byte string to be hashed with the domainSeparator - /// @param domainSeparator The domain separation tag - /// @return A point in G2 - function toMessagePoint(bytes memory message, bytes memory domainSeparator) - internal - view - returns (G2Point memory) - { - return MapFp2ToG2(Fp2(Fp(0, 0), Fp(0, uint256(keccak256(abi.encodePacked(domainSeparator, message)))))); - } - - /// @notice Signs a message - /// @param message Arbitrarylength byte string to be hashed with the domainSeparator - /// @param privateKey The private key to sign with - /// @param domainSeparator The domain separation tag - /// @return A signature in G2 - function sign(bytes memory message, uint256 privateKey, bytes memory domainSeparator) - internal - view - returns (G2Point memory) - { - return G2Mul(toMessagePoint(message, domainSeparator), privateKey); - } - - /// @notice Verifies a signature - /// @param message Arbitrarylength byte string to be hashed - /// @param signature The signature to verify - /// @param publicKey The public key to verify against - /// @param domainSeparator The domain separation tag - /// @return True if the signature is valid, false otherwise - function verify( - bytes memory message, - G2Point memory signature, - G1Point memory publicKey, - bytes memory domainSeparator - ) public view returns (bool) { - // Hash the message bytes into a G2 point - BLS.G2Point memory messagePoint = toMessagePoint(message, domainSeparator); - - // Invoke the pairing check to verify the signature. - BLS.G1Point[] memory g1Points = new BLS.G1Point[](2); - g1Points[0] = NEGATED_G1_GENERATOR(); - g1Points[1] = publicKey; - - BLS.G2Point[] memory g2Points = new BLS.G2Point[](2); - g2Points[0] = signature; - g2Points[1] = messagePoint; - - return BLS.Pairing(g1Points, g2Points); - } - - /** - * @notice Returns a G1Point in the compressed form - * @dev Adapted from https://github.com/NethermindEth/Taiko-Preconf-AVS/blob/004d407105578a83c4815e7ec2c55ec467b9ed3f/SmartContracts/src/libraries/BLS12381.sol#L124 - * @dev Originally based on https://github.com/zcash/librustzcash/blob/6e0364cd42a2b3d2b958a54771ef51a8db79dd29/pairing/src/bls12_381/README.md#serialization - * @param point The G1 point to compress - */ - function compress(G1Point memory point) internal pure returns (Fp memory) { - Fp memory r = point.x; - - // Set the first MSB - r.a = r.a | (1 << 127); - - // Second MSB is left to be 0 since we are assuming that no infinity points are involved - - // Set the third MSB if point.y is lexicographically larger than the y in negated point - if (_greaterThan(point.y, negate(point).y)) { - r.a = r.a | (1 << 125); - } - - return r; - } -} diff --git a/src/lib/BLSUtils.sol b/src/lib/BLSUtils.sol index fca3dd5..6d3bec7 100644 --- a/src/lib/BLSUtils.sol +++ b/src/lib/BLSUtils.sol @@ -2,6 +2,7 @@ pragma solidity >=0.8.0 <0.9.0; import { BLS } from "solady/utils/ext/ithaca/BLS.sol"; + // Credit: https://github.com/paradigmxyz/forge-alphanet/blob/main/src/sign/BLS.sol /// @title BLS @@ -9,6 +10,27 @@ import { BLS } from "solady/utils/ext/ithaca/BLS.sol"; library BLSUtils { using BLS for *; + /// @dev For addition of two points on the BLS12-381 G1 curve, + address internal constant BLS12_G1ADD = 0x000000000000000000000000000000000000000b; + + /// @dev For multi-scalar multiplication (MSM) on the BLS12-381 G1 curve. + address internal constant BLS12_G1MSM = 0x000000000000000000000000000000000000000C; + + /// @dev For addition of two points on the BLS12-381 G2 curve. + address internal constant BLS12_G2ADD = 0x000000000000000000000000000000000000000d; + + /// @dev For multi-scalar multiplication (MSM) on the BLS12-381 G2 curve. + address internal constant BLS12_G2MSM = 0x000000000000000000000000000000000000000E; + + /// @dev For performing a pairing check on the BLS12-381 curve. + address internal constant BLS12_PAIRING_CHECK = 0x000000000000000000000000000000000000000F; + + /// @dev For mapping a Fp to a point on the BLS12-381 G1 curve. + address internal constant BLS12_MAP_FP_TO_G1 = 0x0000000000000000000000000000000000000010; + + /// @dev For mapping a Fp2 to a point on the BLS12-381 G2 curve. + address internal constant BLS12_MAP_FP2_TO_G2 = 0x0000000000000000000000000000000000000011; + /// @notice G1MUL operation /// @param point G1 point /// @param scalar Scalar to multiply the point by @@ -128,6 +150,80 @@ library BLSUtils { return false; } + /// @dev Computes a point in G2 from a message. + /// @dev Copied from Solady but changed the DST from "SWU_RO_NUL_\x2b" to "SWU_RO_POP_\x2b" + function _hashToG2(bytes memory message) internal view returns (BLS.G2Point memory result) { + assembly ("memory-safe") { + function dstPrime(o_, i_) -> _o { + mstore8(o_, i_) // 1. + mstore(add(o_, 0x01), "BLS_SIG_BLS12381G2_XMD:SHA-256_S") // 32. + mstore(add(o_, 0x21), "SWU_RO_POP_\x2b") // 12. + _o := add(0x2d, o_) + } + + function sha2(data_, n_) -> _h { + if iszero(and(eq(returndatasize(), 0x20), staticcall(gas(), 2, data_, n_, 0x00, 0x20))) { + revert(calldatasize(), 0x00) + } + _h := mload(0x00) + } + + function modfield(s_, b_) { + mcopy(add(s_, 0x60), b_, 0x40) + if iszero(and(eq(returndatasize(), 0x40), staticcall(gas(), 5, s_, 0x100, b_, 0x40))) { + revert(calldatasize(), 0x00) + } + } + + function mapToG2(s_, r_) { + if iszero( + and(eq(returndatasize(), 0x100), staticcall(gas(), BLS12_MAP_FP2_TO_G2, s_, 0x80, r_, 0x100)) + ) { + mstore(0x00, 0x89083b91) // `MapFp2ToG2Failed()`. + revert(0x1c, 0x04) + } + } + + let b := mload(0x40) + let s := add(b, 0x100) + calldatacopy(s, calldatasize(), 0x40) + mcopy(add(0x40, s), add(0x20, message), mload(message)) + let o := add(add(0x40, s), mload(message)) + mstore(o, shl(240, 256)) + let b0 := sha2(s, sub(dstPrime(add(0x02, o), 0), s)) + mstore(0x20, b0) + mstore(s, b0) + mstore(b, sha2(s, sub(dstPrime(add(0x20, s), 1), s))) + let j := b + for { let i := 2 } 1 { } { + mstore(s, xor(b0, mload(j))) + j := add(j, 0x20) + mstore(j, sha2(s, sub(dstPrime(add(0x20, s), i), s))) + i := add(i, 1) + if eq(i, 9) { break } + } + + mstore(add(s, 0x00), 0x40) + mstore(add(s, 0x20), 0x20) + mstore(add(s, 0x40), 0x40) + mstore(add(s, 0xa0), 1) + mstore(add(s, 0xc0), 0x000000000000000000000000000000001a0111ea397fe69a4b1ba7b6434bacd7) + mstore(add(s, 0xe0), 0x64774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab) + modfield(s, add(b, 0x00)) + modfield(s, add(b, 0x40)) + modfield(s, add(b, 0x80)) + modfield(s, add(b, 0xc0)) + + mapToG2(b, result) + mapToG2(add(0x80, b), add(0x100, result)) + + if iszero(and(eq(returndatasize(), 0x100), staticcall(gas(), BLS12_G2ADD, result, 0x200, result, 0x100))) { + mstore(0x00, 0xc55e5e33) // `G2AddFailed()`. + revert(0x1c, 0x04) + } + } + } + /// @notice Converts a private key to a public key by multiplying the generator point with the private key /// @param privateKey The private key to convert /// @return The public key @@ -135,47 +231,80 @@ library BLSUtils { return mul(G1_GENERATOR(), _u(privateKey)); } - /// @notice Converts a message to a G2 point - /// @param message Arbitrarylength byte string to be hashed with the domainSeparator - /// @param domainSeparator The domain separation tag - /// @return A point in G2 - function toMessagePoint(bytes memory message, bytes memory domainSeparator) - internal - view - returns (BLS.G2Point memory) - { - return BLS.toG2( - BLS.Fp2({ c0_a: 0, c0_b: 0, c1_a: 0, c1_b: keccak256(abi.encodePacked(domainSeparator, message)) }) + /// @notice Computes the signingRoot + /// + /// signingRoot + /// / \ + /// subTreeRoot signingDomain + /// / \ + /// * * + /// / \ / \ + /// messageHash signingId nonce chainId + /// + /// @param messageHash The hash of the message to sign + /// @param signingDomain The domain mixin for the signer (Commit-Boost) + /// @param signingId The signing ID for the module (Commit-Boost) + /// @param nonce The nonce for the module (Commit-Boost) + /// @param chainId The chain ID + /// @return The signing root + function computeSigningRoot( + bytes32 messageHash, + bytes32 signingDomain, + bytes32 signingId, + uint64 nonce, + bytes32 chainId + ) internal view returns (BLS.G2Point memory) { + bytes32 subTreeRoot = sha256( + abi.encodePacked( + sha256(abi.encodePacked(messageHash, signingId)), + sha256(abi.encodePacked(_toLittleEndian(nonce), chainId)) + ) ); + bytes32 signingRoot = sha256(abi.encodePacked(subTreeRoot, signingDomain)); + + // Convert the signing root hash to a G2 point + return _hashToG2(abi.encodePacked(signingRoot)); } /// @notice Signs a message - /// @param message Arbitrarylength byte string to be hashed with the domainSeparator /// @param privateKey The private key to sign with - /// @param domainSeparator The domain separation tag + /// @param messageHash The hash of the message to sign + /// @param signingDomain The domain mixin for the signer (Commit-Boost) + /// @param signingId The signing ID for the module (Commit-Boost) + /// @param nonce The nonce for the module (Commit-Boost) + /// @param chainId The chain ID /// @return A signature in G2 - function sign(bytes memory message, uint256 privateKey, bytes memory domainSeparator) - internal - view - returns (BLS.G2Point memory) - { - return mul(toMessagePoint(message, domainSeparator), _u(privateKey)); + function sign( + uint256 privateKey, + bytes32 messageHash, + bytes32 signingDomain, + bytes32 signingId, + uint64 nonce, + bytes32 chainId + ) internal view returns (BLS.G2Point memory) { + return mul(computeSigningRoot(messageHash, signingDomain, signingId, nonce, chainId), _u(privateKey)); } /// @notice Verifies a signature - /// @param message Arbitrarylength byte string to be hashed + /// @param messageHash The hash of the message to verify /// @param signature The signature to verify /// @param publicKey The public key to verify against - /// @param domainSeparator The domain separation tag + /// @param signingDomain The domain mixin for the signer (Commit-Boost) + /// @param signingId The signing ID for the module (Commit-Boost) + /// @param nonce The nonce for the module (Commit-Boost) + /// @param chainId The chain ID /// @return True if the signature is valid, false otherwise function verify( - bytes memory message, + bytes32 messageHash, BLS.G2Point memory signature, BLS.G1Point memory publicKey, - bytes memory domainSeparator + bytes32 signingDomain, + bytes32 signingId, + uint64 nonce, + bytes32 chainId ) public view returns (bool) { // Hash the message bytes into a G2 point - BLS.G2Point memory messagePoint = toMessagePoint(message, domainSeparator); + BLS.G2Point memory signingRoot = computeSigningRoot(messageHash, signingDomain, signingId, nonce, chainId); // Invoke the BLS.pairing check to verify the signature. BLS.G1Point[] memory g1Points = new BLS.G1Point[](2); @@ -184,7 +313,7 @@ library BLSUtils { BLS.G2Point[] memory g2Points = new BLS.G2Point[](2); g2Points[0] = signature; - g2Points[1] = messagePoint; + g2Points[1] = signingRoot; return BLS.pairing(g1Points, g2Points); } @@ -212,4 +341,15 @@ library BLSUtils { return r; } + + /// @notice Helper to convert a u64 to a little-endian bytes + /// @param x The u64 to convert + /// @return b The little-endian bytes + function _toLittleEndian(uint64 x) public pure returns (bytes32) { + bytes memory b = new bytes(8); + for (uint256 i = 0; i < 8; i++) { + b[i] = bytes1(uint8(x >> (8 * i))); + } + return bytes32(b); + } } diff --git a/src/lib/ECDSAUtils.sol b/src/lib/ECDSAUtils.sol new file mode 100644 index 0000000..00b8b17 --- /dev/null +++ b/src/lib/ECDSAUtils.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0 <0.9.0; + +import { ECDSA } from "solady/utils/ECDSA.sol"; + +/// @title ECDSAUtils +/// @notice Utility functions for structured ECDSA signature handling +library ECDSAUtils { + /// @notice Computes the signing root using the same Merkle tree structure as BLSUtils + /// + /// signingRoot + /// / \ + /// subTreeRoot signingDomain + /// / \ + /// * * + /// / \ / \ + /// messageHash signingId nonce chainId + /// + /// @param messageHash The hash of the message to sign + /// @param signingDomain The domain mixin for the signer (Commit-Boost) + /// @param signingId The signing ID for the module (Commit-Boost) + /// @param nonce The nonce for the module (Commit-Boost) + /// @param chainId The chain ID + /// @return The signing root + function computeSigningRoot( + bytes32 messageHash, + bytes32 signingDomain, + bytes32 signingId, + uint64 nonce, + bytes32 chainId + ) internal pure returns (bytes32) { + bytes32 subTreeRoot = sha256( + abi.encodePacked( + sha256(abi.encodePacked(messageHash, signingId)), + sha256(abi.encodePacked(_toLittleEndian(nonce), chainId)) + ) + ); + bytes32 signingRoot = sha256(abi.encodePacked(subTreeRoot, signingDomain)); + return signingRoot; + } + + /// @notice Recovers the signer address from a signature + /// @param messageHash The hash of the message to verify + /// @param signature The signature to verify + /// @param signingDomain The domain mixin for the signer (Commit-Boost) + /// @param signingId The signing ID for the module (Commit-Boost) + /// @param nonce The nonce for the module (Commit-Boost) + /// @param chainId The chain ID + /// @return The recovered signer address + function recover( + bytes32 messageHash, + bytes memory signature, + bytes32 signingDomain, + bytes32 signingId, + uint64 nonce, + bytes32 chainId + ) public view returns (address) { + bytes32 signingRoot = computeSigningRoot(messageHash, signingDomain, signingId, nonce, chainId); + return ECDSA.recover(signingRoot, signature); + } + + /// @notice Helper to convert a u64 to a little-endian bytes + /// @param x The u64 to convert + /// @return b The little-endian bytes + function _toLittleEndian(uint64 x) public pure returns (bytes32) { + bytes memory b = new bytes(8); + for (uint256 i = 0; i < 8; i++) { + b[i] = bytes1(uint8(x >> (8 * i))); + } + return bytes32(b); + } +} diff --git a/src/lib/MerkleTree.sol b/src/lib/MerkleTree.sol index e8ae1cb..763ea35 100644 --- a/src/lib/MerkleTree.sol +++ b/src/lib/MerkleTree.sol @@ -70,13 +70,13 @@ library MerkleTree { /// ``` /// /// The encoding consists of: - /// - 384 bytes for the `SignedRegistration` struct (G1 + G2 BLS points) + /// - 416 bytes for the `SignedRegistration` struct (G1 + G2 BLS points + nonce) /// - 32 bytes for the owner address (20-byte value left-padded with 12 zero bytes) - /// + /// - 32 bytes for the signing ID /// @param regs The array of `SignedRegistration` structs to hash /// @param owner The operator’s address to include in the leaf /// @return leaves The resulting array of hashed leaf nodes - function hashToLeaves(IRegistry.SignedRegistration[] calldata regs, address owner) + function hashToLeaves(IRegistry.SignedRegistration[] calldata regs, address owner, bytes32 signingId) internal pure returns (bytes32[] memory leaves) @@ -84,8 +84,8 @@ library MerkleTree { assembly { // --- Constants --- let arrayLength := regs.length // Number of SignedRegistration structs - let structSize := 0x180 // Size of a SignedRegistration (384 bytes) - let encodingSize := 0x1a0 // 384 bytes (struct) + 32 bytes (address padded) = 416 bytes + let structSize := 0x1a0 // Size of a SignedRegistration (416 bytes) + let encodingSize := 0x1e0 // 416 bytes (struct) + 32 bytes (address padded) + 32 bytes (signingId) = 480 bytes // --- Allocate memory for output bytes32[] leaves --- leaves := mload(0x40) // Load the current free memory pointer @@ -95,7 +95,7 @@ library MerkleTree { // --- Allocate scratch space for encoding one (struct, address) pair --- let tempBuffer := mload(0x40) // Load updated free memory pointer - mstore(0x40, add(tempBuffer, encodingSize)) // Reserve space for buffer (416 bytes) + mstore(0x40, add(tempBuffer, encodingSize)) // Reserve space for buffer (480 bytes) let leavesPtr := add(leaves, 0x20) // Pointer to first leaf slot (after length) @@ -107,6 +107,9 @@ library MerkleTree { // Append left-padded 20-byte address to the buffer mstore(add(tempBuffer, structSize), owner) + // Append signing ID to the buffer + mstore(add(tempBuffer, add(structSize, 0x20)), signingId) + // Hash and store mstore(add(leavesPtr, mul(i, 0x20)), keccak256(tempBuffer, encodingSize)) } diff --git a/test/BLS.t.sol b/test/BLS.t.sol index a5f412a..e0dd77d 100644 --- a/test/BLS.t.sol +++ b/test/BLS.t.sol @@ -2,33 +2,44 @@ pragma solidity >=0.8.0 <0.9.0; // Credit: https://github.com/paradigmxyz/forge-alphanet/blob/main/src/sign/BLS.sol -import { Test } from "forge-std/Test.sol"; +import { Test, console } from "forge-std/Test.sol"; import { BLSUtils } from "../src/lib/BLSUtils.sol"; import { BLS } from "solady/utils/ext/ithaca/BLS.sol"; +import { ISlasher } from "../src/ISlasher.sol"; /// @notice A simple test demonstrating BLS signature verification. contract BLSTest is Test { /// @dev Demonstrates the signing and verification of a message. - function testSignAndVerify(uint256 privateKey, bytes memory message, bytes memory domainSeparator) public view { + function testSignAndVerify( + uint256 privateKey, + bytes32 messageHash, + bytes32 signingDomain, + bytes32 signingId, + uint64 nonce, + bytes32 chainId + ) public view { BLS.G1Point memory publicKey = BLSUtils.toPublicKey(privateKey); - BLS.G2Point memory signature = BLSUtils.sign(message, privateKey, domainSeparator); - assert(BLSUtils.verify(message, signature, publicKey, domainSeparator)); + BLS.G2Point memory signature = BLSUtils.sign(privateKey, messageHash, signingDomain, signingId, nonce, chainId); + assert(BLSUtils.verify(messageHash, signature, publicKey, signingDomain, signingId, nonce, chainId)); } /// @dev Demonstrates the aggregation and verification of two signatures. function testAggregation( uint256 privateKey1, uint256 privateKey2, - bytes memory message, - bytes memory domainSeparator + bytes32 messageHash, + bytes32 signingDomain, + bytes32 signingId, + uint64 nonce, + bytes32 chainId ) public view { // public keys BLS.G1Point memory pk1 = BLSUtils.toPublicKey(privateKey1); BLS.G1Point memory pk2 = BLSUtils.toPublicKey(privateKey2); // signatures - BLS.G2Point memory sig1 = BLSUtils.sign(message, privateKey1, domainSeparator); - BLS.G2Point memory sig2 = BLSUtils.sign(message, privateKey2, domainSeparator); + BLS.G2Point memory sig1 = BLSUtils.sign(privateKey1, messageHash, signingDomain, signingId, nonce, chainId); + BLS.G2Point memory sig2 = BLSUtils.sign(privateKey2, messageHash, signingDomain, signingId, nonce, chainId); // aggregated signature BLS.G2Point memory sig = BLS.add(sig1, sig2); @@ -41,23 +52,37 @@ contract BLSTest is Test { BLS.G2Point[] memory g2Points = new BLS.G2Point[](3); g2Points[0] = sig; - g2Points[1] = BLSUtils.toMessagePoint(message, domainSeparator); - g2Points[2] = BLSUtils.toMessagePoint(message, domainSeparator); + g2Points[1] = BLSUtils.computeSigningRoot(messageHash, signingDomain, signingId, nonce, chainId); + g2Points[2] = BLSUtils.computeSigningRoot(messageHash, signingDomain, signingId, nonce, chainId); assert(BLS.pairing(g1Points, g2Points)); } - function testToMessagePoint(bytes memory message, bytes memory domainSeparator) public view { - BLS.G2Point memory messagePoint = BLSUtils.toMessagePoint(message, domainSeparator); - BLS.G2Point memory messagePointExpected = BLS.toG2( - BLS.Fp2({ c0_a: 0, c0_b: 0, c1_a: 0, c1_b: keccak256(abi.encodePacked(domainSeparator, message)) }) + function testComputeSigningRoot( + bytes32 messageHash, + bytes32 signingDomain, + bytes32 signingId, + uint64 nonce, + bytes32 chainId + ) public view { + BLS.G2Point memory signingRoot = + BLSUtils.computeSigningRoot(messageHash, signingDomain, signingId, nonce, chainId); + + // Compute expected signing root manually + bytes32 subTreeRoot = sha256( + abi.encodePacked( + sha256(abi.encodePacked(messageHash, signingId)), + sha256(abi.encodePacked(BLSUtils._toLittleEndian(nonce), chainId)) + ) ); + bytes32 expectedSigningRoot = sha256(abi.encodePacked(subTreeRoot, signingDomain)); + BLS.G2Point memory expected = BLSUtils._hashToG2(abi.encodePacked(expectedSigningRoot)); assert( - messagePoint.x_c0_a == messagePointExpected.x_c0_a && messagePoint.x_c0_b == messagePointExpected.x_c0_b - && messagePoint.x_c1_a == messagePointExpected.x_c1_a && messagePoint.x_c1_b == messagePointExpected.x_c1_b - && messagePoint.y_c0_a == messagePointExpected.y_c0_a && messagePoint.y_c0_b == messagePointExpected.y_c0_b - && messagePoint.y_c1_a == messagePointExpected.y_c1_a && messagePoint.y_c1_b == messagePointExpected.y_c1_b + signingRoot.x_c0_a == expected.x_c0_a && signingRoot.x_c0_b == expected.x_c0_b + && signingRoot.x_c1_a == expected.x_c1_a && signingRoot.x_c1_b == expected.x_c1_b + && signingRoot.y_c0_a == expected.y_c0_a && signingRoot.y_c0_b == expected.y_c0_b + && signingRoot.y_c1_a == expected.y_c1_a && signingRoot.y_c1_b == expected.y_c1_b ); } @@ -96,100 +121,394 @@ contract BLSTest is Test { assert(result.a == BLSUtils._u(247077695202111629659213208963742499723)); assert(result.b == BLSUtils._u(95407516321583900695556749922087294361570042875752728076201341488189472450913)); } + + enum MessageType { + Reserved, + Registration, + Delegation + } + + function testValidG2Point() public { + // 0x1e240 + uint256 privateKey = 123456; + + // 0xaf6e96c0eccd8d4ae868be9299af737855a1b08d57bccb565ea7e69311a30baeebe08d493c3fea97077e8337e95ac5a6 + BLS.G1Point memory publicKey = BLSUtils.toPublicKey(privateKey); + + // Owner account of the proposer (used in SignedRegistration) + address owner = address(0x1111111111111111111111111111111111111111); + + // Commit-Boost signing domain + bytes32 signingDomain = bytes32(0x00000000000000000000000000000000000000000000000000000000436f6d6d); + + // Commit-Boost signing ID + bytes32 signingId = bytes32(0x2222222222222222222222222222222222222222222222222222222222222222); + + // Commit-Boost nonce + uint64 nonce = uint64(0x0420); + + // Commit-Boost chain ID + bytes32 chainId = bytes32(0x0000000000000000000000000000000000000000000000000000000000000001); + + // 0xe0c7a9983a810c24cb2fe92669f4f7e99cdccb534b2d47678b3ca9b9c903bb11 + bytes32 messageHash = keccak256(abi.encode(MessageType.Registration, owner)); + + // 0x33f917d4c13213a7a68d6dfb920604632ea146ab4f0cde2720caadcfc0315f22 + bytes32 signingRoot = sha256( + abi.encodePacked( + sha256( + abi.encodePacked( + sha256(abi.encodePacked(messageHash, signingId)), sha256(abi.encodePacked(nonce, chainId)) + ) + ), + signingDomain + ) + ); + + // 0xa5955ed2dc4cfeffcb7a1203e60f115de2fb84e9323d3f6d7654485dcb6ccef1775295ce63f3dc69ecb1e1f5bbdbcb31196a0b74cb16950dc8aafabcf182573a4d2f146b53d6960a5a27e037c44f5f2a2181bd98641f33b8c0387e53e7eadad0 + BLS.G2Point memory signature = BLSUtils.sign(privateKey, messageHash, signingDomain, signingId, nonce, chainId); + + console.logBytes32(signature.x_c0_a); + console.logBytes32(signature.x_c0_b); + console.logBytes32(signature.x_c1_a); + console.logBytes32(signature.x_c1_b); + console.logBytes32(signature.y_c0_a); + console.logBytes32(signature.y_c0_b); + console.logBytes32(signature.y_c1_a); + console.logBytes32(signature.y_c1_b); + + // 0x00000000000000000000000000000000196a0b74cb16950dc8aafabcf182573a + // 0x4d2f146b53d6960a5a27e037c44f5f2a2181bd98641f33b8c0387e53e7eadad0 + // 0x0000000000000000000000000000000005955ed2dc4cfeffcb7a1203e60f115d + // 0xe2fb84e9323d3f6d7654485dcb6ccef1775295ce63f3dc69ecb1e1f5bbdbcb31 + // 0x00000000000000000000000000000000078009cdfda2becc35184e03b1d0eb89 + // 0xd25a89324552cd9e317e4baffd2606df7ac1f3297d7faffb44f53aa2456440bd + // 0x000000000000000000000000000000000d246a98735f7aaf9dcc7894768f8016 + // 0x22bd89742861ab1e98de83b64ba009b7243b77e9ee10c9a29a11c9366a8082c6 + + assert(BLSUtils.verify(messageHash, signature, publicKey, signingDomain, signingId, nonce, chainId)); + } + + function testValidG2PointDelegation() public { + // 0x1e240 + uint256 proposerPrivateKey = 123456; + + // 0xaf6e96c0eccd8d4ae868be9299af737855a1b08d57bccb565ea7e69311a30baeebe08d493c3fea97077e8337e95ac5a6 + BLS.G1Point memory proposerPubKey = BLSUtils.toPublicKey(proposerPrivateKey); + + // 0x1343e + uint256 delegatePrivateKey = 78910; + + // 0xaf53b192a82ec1229e8fce4f99cb60287ce33896192b6063ac332b36fbe87ba1b2936bbc849ec68a0132362ab11a7754 + BLS.G1Point memory delegatePubKey = BLSUtils.toPublicKey(delegatePrivateKey); + + ISlasher.Delegation memory delegation = ISlasher.Delegation({ + proposer: proposerPubKey, + delegate: delegatePubKey, + committer: address(0x1111111111111111111111111111111111111111), + slot: 5, + metadata: "some-metadata-here" + }); + + // Commit-Boost signing domain + bytes32 signingDomain = bytes32(0x00000000000000000000000000000000000000000000000000000000436f6d6d); + + // Commit-Boost signing ID + bytes32 signingId = bytes32(0x2222222222222222222222222222222222222222222222222222222222222222); + + // Commit-Boost nonce + uint64 nonce = uint64(0x0420); + + // Commit-Boost chain ID + bytes32 chainId = bytes32(0x0000000000000000000000000000000000000000000000000000000000000001); + + // 0xcd9aca062121f6f50df1bfd7e74e2b023a5a0d9e1387447568a2119db5022e1b + bytes32 messageHash = keccak256(abi.encode(MessageType.Delegation, delegation)); + + // 0xad9ba7af707d987c147f0379b5e68bc2da1e1b94fcaa6dc72897bb4d23b22075 + bytes32 signingRoot = sha256( + abi.encodePacked( + sha256( + abi.encodePacked( + sha256(abi.encodePacked(messageHash, signingId)), sha256(abi.encodePacked(nonce, chainId)) + ) + ), + signingDomain + ) + ); + + // 0xa4a7b68288c7e131151ddc020ebf437180f45d0440e74b45d35ae3f757aba7a1051cff0db6bf2ae8049bd864a77263c20b044bf27bf506e1f79717e36c025587bc645a28458447a2db573c8a62d0968c1f2413c449c9e2fa3e7589b6fc438bb2 + BLS.G2Point memory signature = + BLSUtils.sign(proposerPrivateKey, messageHash, signingDomain, signingId, nonce, chainId); + + console.logBytes32(signature.x_c0_a); + console.logBytes32(signature.x_c0_b); + console.logBytes32(signature.x_c1_a); + console.logBytes32(signature.x_c1_b); + console.logBytes32(signature.y_c0_a); + console.logBytes32(signature.y_c0_b); + console.logBytes32(signature.y_c1_a); + console.logBytes32(signature.y_c1_b); + + // 0x000000000000000000000000000000000b044bf27bf506e1f79717e36c025587 + // 0xbc645a28458447a2db573c8a62d0968c1f2413c449c9e2fa3e7589b6fc438bb2 + // 0x0000000000000000000000000000000004a7b68288c7e131151ddc020ebf4371 + // 0x80f45d0440e74b45d35ae3f757aba7a1051cff0db6bf2ae8049bd864a77263c2 + // 0x000000000000000000000000000000000ab58d8c14bab9130b1a7b261d2c7c9b + // 0x4503018948819890030239b32f259755fa30302c51f1f1b4fb0c18a0efd7224c + // 0x00000000000000000000000000000000110cc5b23f6b491ab0a4eb521b83acab + // 0xfa2af587c82f0d3039d576e1cd54eb12b37b03a6e9c1c053c40f7fb9534a4f9e + + assert(BLSUtils.verify(messageHash, signature, proposerPubKey, signingDomain, signingId, nonce, chainId)); + } + + function testValidateRustDelegation() public { + // decrypted + decoded from https://github.com/Commit-Boost/commit-boost-client/blob/main/tests/data/keystores/secrets/0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9 + uint256 proposerPrivateKey = 0x0501e85d5bc2e95f70efda47409710a7cf01dd02ff238e3efec679b27331d917; + + // 0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9 + BLS.G1Point memory proposerPubKey = BLSUtils.toPublicKey(proposerPrivateKey); + + // 0x1e240 + uint256 delegatePrivateKey = 123456; + + // 0xaf6e96c0eccd8d4ae868be9299af737855a1b08d57bccb565ea7e69311a30baeebe08d493c3fea97077e8337e95ac5a6 + BLS.G1Point memory delegatePubKey = BLSUtils.toPublicKey(delegatePrivateKey); + + ISlasher.Delegation memory delegation = ISlasher.Delegation({ + proposer: proposerPubKey, + delegate: delegatePubKey, + committer: address(0x1111111111111111111111111111111111111111), + slot: 0, + metadata: "" + }); + + // Commit-Boost signing domain + bytes32 signingDomain = bytes32(0x6d6d6f43719103511efa4f1362ff2a50996cccf329cc84cb410c5e5c7d351d03); + + // Commit-Boost signing ID + bytes32 signingId = bytes32(0xcb005700fab121c00ccbc94db58c04675b7847c38f9583815139d1d98bea0cb0); + + // u64::MAX - 1 as little endian + uint64 nonce = type(uint64).max - 1; + + // Hoodi is 560048, as little endian + bytes32 chainId = bytes32(0xb08b080000000000000000000000000000000000000000000000000000000000); + + // 0x9acaabc32311cf88c56b810bf9f6c789f84bca6cd7172c2056d4c7a639ffe79e + bytes32 messageHash = keccak256(abi.encode(MessageType.Delegation, delegation)); + + // 0x2df11c9d631fbe4be80eedd5caf00adb886ab9e10b1a277ec22d392c7a91d16e + bytes32 signingRoot = sha256( + abi.encodePacked( + sha256( + abi.encodePacked( + sha256(abi.encodePacked(messageHash, signingId)), + sha256(abi.encodePacked(BLSUtils._toLittleEndian(nonce), chainId)) + ) + ), + signingDomain + ) + ); + + // 0x8c6f28dfec8a79c881146d6734e84b3b784c2649cb0c3fcec292491513abffd142eb7dde6b34e36427c42ccad19c42f20ae562237453f4c2c6ce0467a17afa55c7084b5a713e113b1d44a65dd7a08e36fd4ef91aee59c03de61b3c84fab4ae43 + BLS.G2Point memory signature = + BLSUtils.sign(proposerPrivateKey, messageHash, signingDomain, signingId, nonce, chainId); + + console.logBytes32(signature.x_c0_a); + console.logBytes32(signature.x_c0_b); + console.logBytes32(signature.x_c1_a); + console.logBytes32(signature.x_c1_b); + console.logBytes32(signature.y_c0_a); + console.logBytes32(signature.y_c0_b); + console.logBytes32(signature.y_c1_a); + console.logBytes32(signature.y_c1_b); + + // 0x000000000000000000000000000000000ae562237453f4c2c6ce0467a17afa55 + // 0xc7084b5a713e113b1d44a65dd7a08e36fd4ef91aee59c03de61b3c84fab4ae43 + // 0x000000000000000000000000000000000c6f28dfec8a79c881146d6734e84b3b + // 0x784c2649cb0c3fcec292491513abffd142eb7dde6b34e36427c42ccad19c42f2 + // 0x000000000000000000000000000000000656654e89e944387f4b528f92471e3b + // 0xd511bf5c2651fbc783deb339fa4cab28bd4c8cd49e311acbdc2bb3556027c986 + // 0x0000000000000000000000000000000006148f9185fdeea718d761cde3dc10ef + // 0x9134f6df779be74d88a26aef051122b58d88288b86b4ecf8a256fcd2b0a7e715 + + assert(BLSUtils.verify(messageHash, signature, proposerPubKey, signingDomain, signingId, nonce, chainId)); + } + + function testValidateRustRegistration() public { + // decrypted + decoded from https://github.com/Commit-Boost/commit-boost-client/blob/main/tests/data/keystores/secrets/0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9 + uint256 proposerPrivateKey = 0x0501e85d5bc2e95f70efda47409710a7cf01dd02ff238e3efec679b27331d917; + + // 0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9 + BLS.G1Point memory proposerPubKey = BLSUtils.toPublicKey(proposerPrivateKey); + + address owner = address(0x1111111111111111111111111111111111111111); + + // Commit-Boost signing domain + bytes32 signingDomain = bytes32(0x6d6d6f43719103511efa4f1362ff2a50996cccf329cc84cb410c5e5c7d351d03); + + // Commit-Boost signing ID + bytes32 signingId = bytes32(0xcb005700fab121c00ccbc94db58c04675b7847c38f9583815139d1d98bea0cb0); + + // u64::MAX - 1 as little endian + uint64 nonce = type(uint64).max - 1; + + // Hoodi is 560048, as little endian + bytes32 chainId = bytes32(0xb08b080000000000000000000000000000000000000000000000000000000000); + + // 0xe0c7a9983a810c24cb2fe92669f4f7e99cdccb534b2d47678b3ca9b9c903bb11 + bytes32 messageHash = keccak256(abi.encode(MessageType.Registration, owner)); + + // 0x8f3238365553e8df2f2f439bd2ac43b4ccc02c15eed16cccd96f60d08aeae385c18d035d306d559eee0026c5560f16a11829d7181e75bb8ac3a1db28ebb2a34998486e4d7beb3ce4aa760f06e883c018bbff612271024378bb0369ed3296475c + BLS.G2Point memory signature = + BLSUtils.sign(proposerPrivateKey, messageHash, signingDomain, signingId, nonce, chainId); + + console.logBytes32(signature.x_c0_a); + console.logBytes32(signature.x_c0_b); + console.logBytes32(signature.x_c1_a); + console.logBytes32(signature.x_c1_b); + console.logBytes32(signature.y_c0_a); + console.logBytes32(signature.y_c0_b); + console.logBytes32(signature.y_c1_a); + console.logBytes32(signature.y_c1_b); + + // 0x000000000000000000000000000000001829d7181e75bb8ac3a1db28ebb2a349 + // 0x98486e4d7beb3ce4aa760f06e883c018bbff612271024378bb0369ed3296475c + // 0x000000000000000000000000000000000f3238365553e8df2f2f439bd2ac43b4 + // 0xccc02c15eed16cccd96f60d08aeae385c18d035d306d559eee0026c5560f16a1 + // 0x00000000000000000000000000000000190e7b912eb3df921e6fb627998dfba3 + // 0xe8dff7e32680cade76ac9e4b499b9b3eb7ac7b2862117e29f9beb7ef2b85d634 + // 0x00000000000000000000000000000000013e29e6f8bb23da666bc35f4d828a72 + // 0x95bac57fd2fcb56db57b114674fd72c64a4caf1fc6afaef5276b5bf03bb4f722 + + assert(BLSUtils.verify(messageHash, signature, proposerPubKey, signingDomain, signingId, nonce, chainId)); + } } contract BLSGasTest is Test { function testG1AddGas() public { + vm.pauseGasMetering(); BLS.G1Point memory a = BLSUtils.toPublicKey(1234); BLS.G1Point memory b = BLSUtils.toPublicKey(5678); - vm.resetGasMetering(); + vm.resumeGasMetering(); BLS.add(a, b); } function testG1MulGas() public { + vm.pauseGasMetering(); BLS.G1Point memory a = BLSUtils.toPublicKey(1234); - vm.resetGasMetering(); + vm.resumeGasMetering(); BLSUtils.mul(a, BLSUtils._u(1234)); } function testG1MSMGas() public { + vm.pauseGasMetering(); BLS.G1Point[] memory points = new BLS.G1Point[](2); points[0] = BLSUtils.toPublicKey(1234); points[1] = BLSUtils.toPublicKey(5678); bytes32[] memory scalars = new bytes32[](2); scalars[0] = BLSUtils._u(1234); scalars[1] = BLSUtils._u(5678); - vm.resetGasMetering(); + vm.resumeGasMetering(); BLS.msm(points, scalars); } function testG2AddGas() public { - BLS.G2Point memory g2A = BLSUtils.sign("hello", 1234, ""); + vm.pauseGasMetering(); + BLS.G2Point memory g2A = + BLSUtils.sign(1234, keccak256("hello"), bytes32(0), bytes32(0), uint64(0), bytes32(uint256(1))); - BLS.G2Point memory g2B = BLSUtils.sign("world", 5678, ""); - vm.resetGasMetering(); + BLS.G2Point memory g2B = + BLSUtils.sign(5678, keccak256("world"), bytes32(0), bytes32(0), uint64(0), bytes32(uint256(1))); + vm.resumeGasMetering(); BLS.add(g2A, g2B); } function testG2MulGas() public { - BLS.G2Point memory g2A = BLSUtils.sign("hello", 1234, ""); - vm.resetGasMetering(); + vm.pauseGasMetering(); + BLS.G2Point memory g2A = + BLSUtils.sign(1234, keccak256("hello"), bytes32(0), bytes32(0), uint64(0), bytes32(uint256(1))); + vm.resumeGasMetering(); BLSUtils.mul(g2A, BLSUtils._u(1234)); } function testG2MSMGas() public { + vm.pauseGasMetering(); BLS.G2Point[] memory points = new BLS.G2Point[](2); - points[0] = BLSUtils.sign("hello", 1234, ""); - points[1] = BLSUtils.sign("world", 5678, ""); + points[0] = BLSUtils.sign(1234, keccak256("hello"), bytes32(0), bytes32(0), uint64(0), bytes32(uint256(1))); + points[1] = BLSUtils.sign(5678, keccak256("world"), bytes32(0), bytes32(0), uint64(0), bytes32(uint256(1))); bytes32[] memory scalars = new bytes32[](2); scalars[0] = BLSUtils._u(1234); scalars[1] = BLSUtils._u(5678); - vm.resetGasMetering(); + vm.resumeGasMetering(); BLS.msm(points, scalars); } function testSinglePairingGas() public { + vm.pauseGasMetering(); BLS.G1Point[] memory g1Points = new BLS.G1Point[](2); g1Points[0] = BLSUtils.toPublicKey(1234); g1Points[1] = BLSUtils.toPublicKey(5678); BLS.G2Point[] memory g2Points = new BLS.G2Point[](2); - g2Points[0] = BLSUtils.sign("hello", 1234, ""); - g2Points[1] = BLSUtils.sign("world", 5678, ""); - vm.resetGasMetering(); + g2Points[0] = BLSUtils.sign(1234, keccak256("hello"), bytes32(0), bytes32(0), uint64(0), bytes32(uint256(1))); + g2Points[1] = BLSUtils.sign(5678, keccak256("world"), bytes32(0), bytes32(0), uint64(0), bytes32(uint256(1))); + vm.resumeGasMetering(); BLS.pairing(g1Points, g2Points); } function testMapFpToG1Gas() public { + vm.pauseGasMetering(); BLS.Fp memory fp = BLS.Fp(BLSUtils._u(1234), BLSUtils._u(5678)); - vm.resetGasMetering(); + vm.resumeGasMetering(); BLS.toG1(fp); } function testMapFp2ToG2Gas() public { + vm.pauseGasMetering(); BLS.Fp2 memory fp2 = BLS.Fp2(BLSUtils._u(1234), BLSUtils._u(5678), BLSUtils._u(91011), BLSUtils._u(121314)); - vm.resetGasMetering(); + vm.resumeGasMetering(); BLS.toG2(fp2); } function testSigningGas() public { - BLS.G2Point memory messagePoint = BLSUtils.toMessagePoint("hello", "domain"); + vm.pauseGasMetering(); + BLS.G2Point memory signingRoot = BLSUtils.computeSigningRoot( + keccak256("hello"), bytes32(uint256(keccak256("domain"))), bytes32(0), uint64(0), bytes32(uint256(1)) + ); BLS.G1Point memory publicKey = BLSUtils.toPublicKey(1234); - vm.resetGasMetering(); - BLSUtils.sign("hello", 1234, "domain"); + vm.resumeGasMetering(); + BLSUtils.sign( + 1234, keccak256("hello"), bytes32(uint256(keccak256("domain"))), bytes32(0), uint64(0), bytes32(uint256(1)) + ); } function testVerifyingSingleSignatureGas() public { - BLS.G2Point memory messagePoint = BLSUtils.toMessagePoint("hello", "domain"); + vm.pauseGasMetering(); + BLS.G2Point memory signingRoot = BLSUtils.computeSigningRoot( + keccak256("hello"), bytes32(uint256(keccak256("domain"))), bytes32(0), uint64(0), bytes32(uint256(1)) + ); BLS.G1Point memory publicKey = BLSUtils.toPublicKey(1234); - BLS.G2Point memory signature = BLSUtils.sign("hello", 1234, "domain"); + BLS.G2Point memory signature = BLSUtils.sign( + 1234, keccak256("hello"), bytes32(uint256(keccak256("domain"))), bytes32(0), uint64(0), bytes32(uint256(1)) + ); - vm.resetGasMetering(); - BLSUtils.verify("hello", signature, publicKey, "domain"); + vm.resumeGasMetering(); + BLSUtils.verify( + keccak256("hello"), + signature, + publicKey, + bytes32(uint256(keccak256("domain"))), + bytes32(0), + uint64(0), + bytes32(uint256(1)) + ); } function testG1PointCompressGas() public { + vm.pauseGasMetering(); BLS.G1Point memory point = BLSUtils.toPublicKey(123456); - vm.resetGasMetering(); + vm.resumeGasMetering(); BLSUtils.compress(point); } } diff --git a/test/ECDSA.t.sol b/test/ECDSA.t.sol new file mode 100644 index 0000000..c54c52a --- /dev/null +++ b/test/ECDSA.t.sol @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0 <0.9.0; + +import "forge-std/Test.sol"; +import "../src/lib/ECDSAUtils.sol"; +import "../src/IRegistry.sol"; +import "../src/ISlasher.sol"; + +/// @title ECDSAUtilsTest +/// @notice Comprehensive unit tests for ECDSAUtils +contract ECDSAUtilsTest is Test { + using ECDSAUtils for *; + + // Test parameters + bytes32 constant SIGNING_DOMAIN = bytes32(0x6d6d6f43719103511efa4f1362ff2a50996cccf329cc84cb410c5e5c7d351d03); + bytes32 constant SIGNING_ID = bytes32(0xcb005700fab121c00ccbc94db58c04675b7847c38f9583815139d1d98bea0cb0); + uint64 constant NONCE = type(uint64).max - 1; + bytes32 constant CHAIN_ID = bytes32(0xb08b080000000000000000000000000000000000000000000000000000000000); + + // decrypted + decoded from https://github.com/Commit-Boost/commit-boost-client/blob/main/tests/data/keystores/secrets/0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9 + uint256 constant PRIVATE_KEY = 0x0501e85d5bc2e95f70efda47409710a7cf01dd02ff238e3efec679b27331d917; + + function testComputeSigningRoot() public { + bytes32 messageHash = keccak256("test message"); + + bytes32 signingRoot = ECDSAUtils.computeSigningRoot(messageHash, SIGNING_DOMAIN, SIGNING_ID, NONCE, CHAIN_ID); + + // Verify the signing root is deterministic + bytes32 expectedSubTreeRoot = sha256( + abi.encodePacked( + sha256(abi.encodePacked(messageHash, SIGNING_ID)), + sha256(abi.encodePacked(ECDSAUtils._toLittleEndian(NONCE), CHAIN_ID)) + ) + ); + bytes32 expectedSigningRoot = sha256(abi.encodePacked(expectedSubTreeRoot, SIGNING_DOMAIN)); + + assertEq(signingRoot, expectedSigningRoot, "Signing root computation incorrect"); + } + + function testComputeSigningRootWithDifferentNonce() public { + bytes32 messageHash = keccak256("test message"); + + bytes32 signingRoot1 = ECDSAUtils.computeSigningRoot(messageHash, SIGNING_DOMAIN, SIGNING_ID, NONCE, CHAIN_ID); + + bytes32 signingRoot2 = + ECDSAUtils.computeSigningRoot(messageHash, SIGNING_DOMAIN, SIGNING_ID, NONCE + 1, CHAIN_ID); + + assertTrue(signingRoot1 != signingRoot2, "Different nonces should produce different signing roots"); + } + + function testComputeSigningRootWithDifferentSigningId() public { + bytes32 messageHash = keccak256("test message"); + + bytes32 signingRoot1 = ECDSAUtils.computeSigningRoot(messageHash, SIGNING_DOMAIN, SIGNING_ID, NONCE, CHAIN_ID); + + bytes32 signingRoot2 = ECDSAUtils.computeSigningRoot( + messageHash, SIGNING_DOMAIN, keccak256("different-signing-id"), NONCE, CHAIN_ID + ); + + assertTrue(signingRoot1 != signingRoot2, "Different signing IDs should produce different signing roots"); + } + + function testComputeSigningRootWithDifferentChainId() public { + bytes32 messageHash = keccak256("test message"); + + bytes32 signingRoot1 = ECDSAUtils.computeSigningRoot(messageHash, SIGNING_DOMAIN, SIGNING_ID, NONCE, CHAIN_ID); + + bytes32 signingRoot2 = ECDSAUtils.computeSigningRoot(messageHash, SIGNING_DOMAIN, SIGNING_ID, NONCE, "0x02"); + + assertTrue(signingRoot1 != signingRoot2, "Different chain IDs should produce different signing roots"); + } + + function testSignAndRecover() public { + bytes32 messageHash = keccak256("test message"); + + // Compute signing root + bytes32 signingRoot = ECDSAUtils.computeSigningRoot(messageHash, SIGNING_DOMAIN, SIGNING_ID, NONCE, CHAIN_ID); + + // Sign the message + (uint8 v, bytes32 r, bytes32 s) = vm.sign(PRIVATE_KEY, signingRoot); + bytes memory signature = abi.encodePacked(r, s, v); + + // Recover the signer + address recovered = ECDSAUtils.recover(messageHash, signature, SIGNING_DOMAIN, SIGNING_ID, NONCE, CHAIN_ID); + + assertEq(recovered, vm.addr(PRIVATE_KEY), "Recovered address should match expected address"); + } + + function testRecoverWithInvalidSignature() public { + bytes32 messageHash = keccak256("test message"); + + // Create an invalid signature + bytes memory invalidSignature = abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2)), uint8(27)); + + // This should return a different address or potentially revert + address recovered = + ECDSAUtils.recover(messageHash, invalidSignature, SIGNING_DOMAIN, SIGNING_ID, NONCE, CHAIN_ID); + + assertTrue(recovered != vm.addr(PRIVATE_KEY), "Invalid signature should not recover expected address"); + } + + function testRecoverWithWrongNonce() public { + bytes32 messageHash = keccak256("test message"); + + // Sign with one nonce + bytes32 signingRoot = ECDSAUtils.computeSigningRoot(messageHash, SIGNING_DOMAIN, SIGNING_ID, NONCE, CHAIN_ID); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(PRIVATE_KEY, signingRoot); + bytes memory signature = abi.encodePacked(r, s, v); + + // Try to recover with different nonce + address recovered = ECDSAUtils.recover(messageHash, signature, SIGNING_DOMAIN, SIGNING_ID, NONCE + 1, CHAIN_ID); + + assertTrue(recovered != vm.addr(PRIVATE_KEY), "Wrong nonce should not recover expected address"); + } + + function testRecoverWithWrongSigningId() public { + bytes32 messageHash = keccak256("test message"); + + // Sign with one signing ID + bytes32 signingRoot = ECDSAUtils.computeSigningRoot(messageHash, SIGNING_DOMAIN, SIGNING_ID, NONCE, CHAIN_ID); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(PRIVATE_KEY, signingRoot); + bytes memory signature = abi.encodePacked(r, s, v); + + // Try to recover with different signing ID + address recovered = + ECDSAUtils.recover(messageHash, signature, SIGNING_DOMAIN, keccak256("wrong-signing-id"), NONCE, CHAIN_ID); + + assertTrue(recovered != vm.addr(PRIVATE_KEY), "Wrong signing ID should not recover expected address"); + } + + function testRecoverWithWrongChainId() public { + bytes32 messageHash = keccak256("test message"); + + // Sign with one chain ID + bytes32 signingRoot = ECDSAUtils.computeSigningRoot(messageHash, SIGNING_DOMAIN, SIGNING_ID, NONCE, CHAIN_ID); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(PRIVATE_KEY, signingRoot); + bytes memory signature = abi.encodePacked(r, s, v); + + // Try to recover with different chain ID + address recovered = ECDSAUtils.recover(messageHash, signature, SIGNING_DOMAIN, SIGNING_ID, NONCE, "0x02"); + + assertTrue(recovered != vm.addr(PRIVATE_KEY), "Wrong chain ID should not recover expected address"); + } + + function testIntegrationWithCommitment() public { + // Create a commitment request + ISlasher.CommitmentRequest memory request = + ISlasher.CommitmentRequest({ commitmentType: 1, payload: "test payload", slasher: address(0x123) }); + + // Compute request hash + bytes32 requestHash = keccak256(abi.encode(request)); + + // Create commitment + ISlasher.Commitment memory commitment = ISlasher.Commitment({ + commitmentType: 1, payload: "test payload", requestHash: requestHash, slasher: address(0x123) + }); + + // Compute message hash + bytes32 messageHash = keccak256(abi.encode(IRegistry.MessageType.Commitment, commitment)); + + // Sign the commitment + bytes32 signingRoot = ECDSAUtils.computeSigningRoot(messageHash, SIGNING_DOMAIN, SIGNING_ID, NONCE, CHAIN_ID); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(PRIVATE_KEY, signingRoot); + bytes memory signature = abi.encodePacked(r, s, v); + + // Create signed commitment + ISlasher.SignedCommitment memory signedCommitment = ISlasher.SignedCommitment({ + commitment: commitment, nonce: NONCE, signingId: SIGNING_ID, signature: signature + }); + + // Verify the signature + address recovered = ECDSAUtils.recover(messageHash, signature, SIGNING_DOMAIN, SIGNING_ID, NONCE, CHAIN_ID); + + assertEq(recovered, vm.addr(PRIVATE_KEY), "Full commitment flow should work correctly"); + } + + function testToLittleEndian() public { + uint64 testValue = 0x123456789ABCDEF0; + bytes32 result = ECDSAUtils._toLittleEndian(testValue); + + // Verify little-endian conversion + bytes memory expected = new bytes(8); + for (uint256 i = 0; i < 8; i++) { + expected[i] = bytes1(uint8(testValue >> (8 * i))); + } + + assertEq(result, bytes32(expected), "Little-endian conversion incorrect"); + } + + function testToLittleEndianZero() public { + bytes32 result = ECDSAUtils._toLittleEndian(0); + assertEq(result, bytes32(0), "Zero should convert to zero"); + } + + function testToLittleEndianMax() public { + bytes32 result = ECDSAUtils._toLittleEndian(type(uint64).max); + assertTrue(result != bytes32(0), "Max value should not be zero"); + } + + function testValidateRustCommitment() public { + ISlasher.CommitmentRequest memory commitmentRequest = ISlasher.CommitmentRequest({ + commitmentType: 1, payload: "", slasher: address(0x1111111111111111111111111111111111111111) + }); + + ISlasher.Commitment memory commitment = ISlasher.Commitment({ + commitmentType: 1, + payload: commitmentRequest.payload, + requestHash: keccak256(abi.encode(commitmentRequest)), + slasher: commitmentRequest.slasher + }); + + // 0x310fefd12df340a4d723608aec3e7471ced2abe290a9458a5ba1832b9fc84653 + bytes32 messageHash = keccak256(abi.encode(IRegistry.MessageType.Commitment, commitment)); + + // 0x90447b3fac12f371cbb8aa2e1527f2abda7576cd618244c005cdb3c0d63e8a7b + bytes32 signingRoot = ECDSAUtils.computeSigningRoot(messageHash, SIGNING_DOMAIN, SIGNING_ID, NONCE, CHAIN_ID); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(PRIVATE_KEY, signingRoot); + bytes memory signature = abi.encodePacked(r, s, v); + + // Generated from Rust + bytes memory expected = + hex"f59f6fbee040a98021f55b8aaff8eecd4f1c1c9723ef7b5b9d09a0c0835bf94e20d90fc12a203bb9fcf81f7e838806bd3e67b82802d0bcd842163bd37c2cce581b"; + + assert(keccak256(signature) == keccak256(expected)); + + assert( + ECDSAUtils.recover(messageHash, signature, SIGNING_DOMAIN, SIGNING_ID, NONCE, CHAIN_ID) + == vm.addr(PRIVATE_KEY) + ); + } +} diff --git a/test/InclusionPreconfSlasher.t.sol b/test/InclusionPreconfSlasher.t.sol index 29d0e27..f40d2ae 100644 --- a/test/InclusionPreconfSlasher.t.sol +++ b/test/InclusionPreconfSlasher.t.sol @@ -36,9 +36,9 @@ contract InclusionPreconfSlasherTest is UnitTestHelper, PreconfStructs { uint256 collateral = 1.1 ether; uint256 committerSecretKey; address committer; + bytes32 signingId = keccak256("test-signing-id"); function setUp() public { - vm.createSelectFork(vm.rpcUrl("mainnet")); registry = new Registry(defaultConfig()); slasher = new InclusionPreconfSlasher(slashAmountWei, address(registry)); delegatePubKey = BLSUtils.toPublicKey(SECRET_KEY_2); @@ -65,7 +65,9 @@ contract InclusionPreconfSlasherTest is UnitTestHelper, PreconfStructs { committer: committer, slasher: address(slasher), metadata: metadata, - slot: slot + slot: slot, + signingId: signingId, + nonce: 1337 }); // Register operator to URC and signs delegation message @@ -97,7 +99,15 @@ contract InclusionPreconfSlasherTest is UnitTestHelper, PreconfStructs { // Delegate signs a commitment to include a TX TransactionCommitment memory txCommitment = _createInclusionCommitment(inclusionBlockNumber, id, committer, committerSecretKey); - signedCommitment = basicCommitment(committerSecretKey, address(slasher), abi.encode(txCommitment)); + signedCommitment = basicCommitment( + committerSecretKey, + address(slasher), + abi.encode(txCommitment), + signingId, + uint64(1), + defaultConfig().chainId, + defaultConfig().signingDomain + ); // Build the inclusion proof to prove failure to exclude string memory rawPreviousHeader = vm.readFile("./test/testdata/header_20785011.json"); @@ -187,8 +197,10 @@ contract InclusionPreconfSlasherTest is UnitTestHelper, PreconfStructs { committer: committer, slasher: address(slasher), metadata: metadata, - slot: 0 // already expired - }); + slot: 0, // already expired + signingId: signingId, + nonce: 1337 + }); RegisterAndDelegateResult memory result = registerAndDelegate(params); // Create commitment for expired delegation @@ -197,8 +209,15 @@ contract InclusionPreconfSlasherTest is UnitTestHelper, PreconfStructs { TransactionCommitment memory commitment = _createInclusionCommitment(inclusionBlockNumber, 1, delegate, delegatePK); - ISlasher.SignedCommitment memory signedCommitment = - basicCommitment(committerSecretKey, address(slasher), abi.encode(commitment)); + ISlasher.SignedCommitment memory signedCommitment = basicCommitment( + committerSecretKey, + address(slasher), + abi.encode(commitment), + signingId, + uint64(1), + defaultConfig().chainId, + defaultConfig().signingDomain + ); // Try to create challenge with expired delegation uint256 bond = slasher.CHALLENGE_BOND(); @@ -231,7 +250,8 @@ contract InclusionPreconfSlasherTest is UnitTestHelper, PreconfStructs { vm.warp(block.timestamp + slasher.CHALLENGE_WINDOW() + 1); // Merkle proof for URC registration - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(result.registrations, operator, 0); + IRegistry.RegistrationProof memory proof = + registry.getRegistrationProof(result.registrations, operator, 0, signingId); bytes memory evidence = abi.encode(inclusionProof); @@ -273,7 +293,8 @@ contract InclusionPreconfSlasherTest is UnitTestHelper, PreconfStructs { vm.warp(block.timestamp + slasher.CHALLENGE_WINDOW() + 1); // Merkle proof for URC registration - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(result.registrations, operator, 0); + IRegistry.RegistrationProof memory proof = + registry.getRegistrationProof(result.registrations, operator, 0, signingId); // Try to slash as different address (not the original challenger) vm.prank(operator); @@ -319,6 +340,10 @@ contract InclusionPreconfSlasherTest is UnitTestHelper, PreconfStructs { // Verify challenger's balance decreased by bond amount assertEq(challenger.balance, challengerBalanceBefore - bond); + // To save on RPC calls, we pre-fill the blockhashes with the expected values + vm.setBlockhash(inclusionProof.inclusionBlockNumber - 1, keccak256(inclusionProof.previousBlockHeaderRLP)); + vm.setBlockhash(inclusionProof.inclusionBlockNumber, keccak256(inclusionProof.inclusionBlockHeaderRLP)); + // Prove the challenge is fraudulent (transaction was actually included) vm.prank(operator); slasher.proveChallengeFraudulent(result.signedDelegation.delegation, signedCommitment, inclusionProof); diff --git a/test/MerkleTree.t.sol b/test/MerkleTree.t.sol index f036fa2..75be391 100644 --- a/test/MerkleTree.t.sol +++ b/test/MerkleTree.t.sol @@ -153,68 +153,79 @@ contract MerkleTreeGasTest is Test { } function test_gas_generateTree_1() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(1); - vm.resetGasMetering(); + vm.resumeGasMetering(); foo.generateTree(leaves); } function test_gas_generateTree_2() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(2); - vm.resetGasMetering(); + vm.resumeGasMetering(); foo.generateTree(leaves); } function test_gas_generateTree_4() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(4); - vm.resetGasMetering(); + vm.resumeGasMetering(); foo.generateTree(leaves); } function test_gas_generateTree_8() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(8); - vm.resetGasMetering(); + vm.resumeGasMetering(); foo.generateTree(leaves); } function test_gas_generateTree_16() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(16); - vm.resetGasMetering(); + vm.resumeGasMetering(); foo.generateTree(leaves); } function test_gas_generateTree_32() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(32); - vm.resetGasMetering(); + vm.resumeGasMetering(); foo.generateTree(leaves); } function test_gas_generateTree_64() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(64); - vm.resetGasMetering(); + vm.resumeGasMetering(); foo.generateTree(leaves); } function test_gas_generateTree_128() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(128); - vm.resetGasMetering(); + vm.resumeGasMetering(); foo.generateTree(leaves); } function test_gas_generateTree_256() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(256); - vm.resetGasMetering(); + vm.resumeGasMetering(); foo.generateTree(leaves); } function test_gas_generateTree_512() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(512); - vm.resetGasMetering(); + vm.resumeGasMetering(); foo.generateTree(leaves); } function test_gas_generateTree_1024() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(1024); - vm.resetGasMetering(); + vm.resumeGasMetering(); foo.generateTree(leaves); } } @@ -246,104 +257,121 @@ contract MerkleTreeBuildGasTest is Test { } function test_gas_buildTree_1() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(1); - vm.resetGasMetering(); + vm.resumeGasMetering(); bar.build(leaves); } function test_gas_buildTree_2() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(2); - vm.resetGasMetering(); + vm.resumeGasMetering(); bar.build(leaves); } function test_gas_buildTree_4() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(4); - vm.resetGasMetering(); + vm.resumeGasMetering(); bar.build(leaves); } function test_gas_buildTree_8() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(8); - vm.resetGasMetering(); + vm.resumeGasMetering(); bar.build(leaves); } function test_gas_buildTree_16() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(16); - vm.resetGasMetering(); + vm.resumeGasMetering(); bar.build(leaves); } function test_gas_buildTree_32() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(32); - vm.resetGasMetering(); + vm.resumeGasMetering(); bar.build(leaves); } function test_gas_buildTree_64() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(64); - vm.resetGasMetering(); + vm.resumeGasMetering(); bar.build(leaves); } function test_gas_buildTree_128() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(128); - vm.resetGasMetering(); + vm.resumeGasMetering(); bar.build(leaves); } function test_gas_buildTree_256() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(256); - vm.resetGasMetering(); + vm.resumeGasMetering(); bar.build(leaves); } function test_gas_buildTree_512() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(512); - vm.resetGasMetering(); + vm.resumeGasMetering(); bar.build(leaves); } function test_gas_buildTree_1024() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(1024); - vm.resetGasMetering(); + vm.resumeGasMetering(); bar.build(leaves); } function test_gas_buildTree_2048() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(2048); - vm.resetGasMetering(); + vm.resumeGasMetering(); bar.build(leaves); } function test_gas_buildTree_4096() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(4096); - vm.resetGasMetering(); + vm.resumeGasMetering(); bar.build(leaves); } function test_gas_buildTree_8192() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(8192); - vm.resetGasMetering(); + vm.resumeGasMetering(); bar.build(leaves); } function test_gas_buildTree_16384() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(16384); - vm.resetGasMetering(); + vm.resumeGasMetering(); bar.build(leaves); } function test_gas_buildTree_32768() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(32768); - vm.resetGasMetering(); + vm.resumeGasMetering(); bar.build(leaves); } function test_gas_buildTree_65536() public { + vm.pauseGasMetering(); bytes32[] memory leaves = getLeaves(65536); - vm.resetGasMetering(); + vm.resumeGasMetering(); bar.build(leaves); } } diff --git a/test/Registry.t.sol b/test/Registry.t.sol index 5e432c3..d964230 100644 --- a/test/Registry.t.sol +++ b/test/Registry.t.sol @@ -7,10 +7,14 @@ import "../src/IRegistry.sol"; import { BLS } from "solady/utils/ext/ithaca/BLS.sol"; import { BLSUtils } from "../src/lib/BLSUtils.sol"; import { - UnitTestHelper, ReentrantRegistrationContract, ReentrantSlashableRegistrationContract + UnitTestHelper, + ReentrantRegistrationContract, + ReentrantSlashableRegistrationContract } from "./UnitTestHelper.sol"; contract RegisterTester is UnitTestHelper { + bytes32 signingId = keccak256("test-signing-id"); + function setUp() public { registry = new Registry(defaultConfig()); vm.deal(operator, 100 ether); @@ -20,7 +24,8 @@ contract RegisterTester is UnitTestHelper { function test_register() public { uint256 collateral = registry.getConfig().minCollateralWei; - basicRegistration(SECRET_KEY_1, collateral, operator); + uint64 nonce = 1337; + basicRegistration(SECRET_KEY_1, collateral, operator, signingId, nonce); } function test_register_insufficientCollateral() public { @@ -28,10 +33,11 @@ contract RegisterTester is UnitTestHelper { IRegistry.SignedRegistration[] memory registrations = new IRegistry.SignedRegistration[](1); - registrations[0] = _createSignedRegistration(SECRET_KEY_1, operator); + uint64 nonce = 1337; + registrations[0] = _createSignedRegistration(SECRET_KEY_1, operator, signingId, nonce); vm.expectRevert(IRegistry.InsufficientCollateral.selector); - registry.register{ value: collateral - 1 }(registrations, operator); + registry.register{ value: collateral - 1 }(registrations, operator, signingId); } function test_register_OperatorAlreadyRegistered() public { @@ -39,15 +45,16 @@ contract RegisterTester is UnitTestHelper { IRegistry.SignedRegistration[] memory registrations = new IRegistry.SignedRegistration[](1); - registrations[0] = _createSignedRegistration(SECRET_KEY_1, operator); + uint64 nonce = 1337; + registrations[0] = _createSignedRegistration(SECRET_KEY_1, operator, signingId, nonce); - bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator); + bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator, signingId); _assertRegistration(registrationRoot, operator, uint80(collateral), uint48(block.number), type(uint48).max, 0); // Attempt duplicate registration vm.expectRevert(IRegistry.OperatorAlreadyRegistered.selector); - registry.register{ value: collateral }(registrations, operator); + registry.register{ value: collateral }(registrations, operator, signingId); } function test_verifyMerkleProofHeight1() public { @@ -55,13 +62,14 @@ contract RegisterTester is UnitTestHelper { IRegistry.SignedRegistration[] memory registrations = new IRegistry.SignedRegistration[](1); - registrations[0] = _createSignedRegistration(SECRET_KEY_1, operator); + uint64 nonce = 1337; + registrations[0] = _createSignedRegistration(SECRET_KEY_1, operator, signingId, nonce); - bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator); + bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator, signingId); _assertRegistration(registrationRoot, operator, uint80(collateral), uint48(block.number), type(uint48).max, 0); - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(registrations, operator, 0); + IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(registrations, operator, 0, signingId); // reverts if proof is invalid registry.verifyMerkleProof(proof); @@ -72,20 +80,21 @@ contract RegisterTester is UnitTestHelper { IRegistry.SignedRegistration[] memory registrations = new IRegistry.SignedRegistration[](2); - registrations[0] = _createSignedRegistration(SECRET_KEY_1, operator); + uint64 nonce = 1337; + registrations[0] = _createSignedRegistration(SECRET_KEY_1, operator, signingId, nonce); - registrations[1] = _createSignedRegistration(SECRET_KEY_2, operator); + registrations[1] = _createSignedRegistration(SECRET_KEY_2, operator, signingId, nonce); - bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator); + bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator, signingId); _assertRegistration(registrationRoot, operator, uint80(collateral), uint48(block.number), type(uint48).max, 0); // Test first proof path - leafIndex = 0 - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(registrations, operator, 0); + IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(registrations, operator, 0, signingId); registry.verifyMerkleProof(proof); // Test second proof path - leafIndex = 1 - proof = registry.getRegistrationProof(registrations, operator, 1); + proof = registry.getRegistrationProof(registrations, operator, 1, signingId); registry.verifyMerkleProof(proof); } @@ -94,19 +103,21 @@ contract RegisterTester is UnitTestHelper { IRegistry.SignedRegistration[] memory registrations = new IRegistry.SignedRegistration[](3); // will be padded to 4 - registrations[0] = _createSignedRegistration(SECRET_KEY_1, operator); + uint64 nonce = 1337; + registrations[0] = _createSignedRegistration(SECRET_KEY_1, operator, signingId, nonce); - registrations[1] = _createSignedRegistration(SECRET_KEY_1 + 1, operator); + registrations[1] = _createSignedRegistration(SECRET_KEY_1 + 1, operator, signingId, nonce); - registrations[2] = _createSignedRegistration(SECRET_KEY_1 + 2, operator); + registrations[2] = _createSignedRegistration(SECRET_KEY_1 + 2, operator, signingId, nonce); - bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator); + bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator, signingId); _assertRegistration(registrationRoot, operator, uint80(collateral), uint48(block.number), type(uint48).max, 0); // Test all proof paths for (uint256 i = 0; i < registrations.length; i++) { - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(registrations, operator, i); + IRegistry.RegistrationProof memory proof = + registry.getRegistrationProof(registrations, operator, i, signingId); registry.verifyMerkleProof(proof); } } @@ -117,21 +128,25 @@ contract RegisterTester is UnitTestHelper { IRegistry.SignedRegistration[] memory registrations = new IRegistry.SignedRegistration[](size); for (uint256 i = 0; i < size; i++) { - registrations[i] = _createSignedRegistration(SECRET_KEY_1 + i, operator); + uint64 nonce = uint64(SECRET_KEY_1 + i); + registrations[i] = _createSignedRegistration(SECRET_KEY_1 + i, operator, signingId, nonce); } // Register the keys - registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator); + registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator, signingId); // Test all proof paths for (uint256 i = 0; i < registrations.length; i++) { - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(registrations, operator, i); + IRegistry.RegistrationProof memory proof = + registry.getRegistrationProof(registrations, operator, i, signingId); registry.verifyMerkleProof(proof); } } } contract UnregisterTester is UnitTestHelper { + bytes32 signingId = keccak256("test-signing-id"); + function setUp() public { registry = new Registry(defaultConfig()); vm.deal(operator, 100 ether); @@ -142,9 +157,11 @@ contract UnregisterTester is UnitTestHelper { function test_unregister() public { uint256 collateral = registry.getConfig().minCollateralWei; - IRegistry.SignedRegistration[] memory registrations = _setupSingleRegistration(SECRET_KEY_1, operator); + uint64 nonce = 1337; + IRegistry.SignedRegistration[] memory registrations = + _setupSingleRegistration(SECRET_KEY_1, operator, signingId, nonce); - bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator); + bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator, signingId); vm.startPrank(operator); vm.expectEmit(address(registry)); @@ -159,9 +176,11 @@ contract UnregisterTester is UnitTestHelper { function test_unregister_wrongOperator() public { uint256 collateral = registry.getConfig().minCollateralWei; - IRegistry.SignedRegistration[] memory registrations = _setupSingleRegistration(SECRET_KEY_1, operator); + uint64 nonce = 1337; + IRegistry.SignedRegistration[] memory registrations = + _setupSingleRegistration(SECRET_KEY_1, operator, signingId, nonce); - bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator); + bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator, signingId); // thief tries to unregister operator's registration vm.startPrank(thief); @@ -172,9 +191,11 @@ contract UnregisterTester is UnitTestHelper { function test_unregister_alreadyUnregistered() public { uint256 collateral = registry.getConfig().minCollateralWei; - IRegistry.SignedRegistration[] memory registrations = _setupSingleRegistration(SECRET_KEY_1, operator); + uint64 nonce = 1337; + IRegistry.SignedRegistration[] memory registrations = + _setupSingleRegistration(SECRET_KEY_1, operator, signingId, nonce); - bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator); + bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator, signingId); vm.startPrank(operator); registry.unregister(registrationRoot); @@ -187,6 +208,8 @@ contract UnregisterTester is UnitTestHelper { } contract OptInAndOutTester is UnitTestHelper { + bytes32 signingId = keccak256("test-signing-id"); + function setUp() public { registry = new Registry(defaultConfig()); vm.deal(operator, 100 ether); @@ -197,9 +220,11 @@ contract OptInAndOutTester is UnitTestHelper { function test_optInAndOut() public { uint256 collateral = registry.getConfig().minCollateralWei; - IRegistry.SignedRegistration[] memory registrations = _setupSingleRegistration(SECRET_KEY_1, operator); + uint64 nonce = 1337; + IRegistry.SignedRegistration[] memory registrations = + _setupSingleRegistration(SECRET_KEY_1, operator, signingId, nonce); - bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator); + bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator, signingId); address committer = address(1234); address slasher = address(5678); @@ -223,8 +248,10 @@ contract OptInAndOutTester is UnitTestHelper { function test_optInToSlasher_wrongOperator() public { uint256 collateral = registry.getConfig().minCollateralWei; - IRegistry.SignedRegistration[] memory registrations = _setupSingleRegistration(SECRET_KEY_1, operator); - bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator); + uint64 nonce = 1337; + IRegistry.SignedRegistration[] memory registrations = + _setupSingleRegistration(SECRET_KEY_1, operator, signingId, nonce); + bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator, signingId); address slasher = address(1234); address committer = address(5678); @@ -240,8 +267,10 @@ contract OptInAndOutTester is UnitTestHelper { function test_optInToSlasher_alreadyOptedIn() public { uint256 collateral = registry.getConfig().minCollateralWei; - IRegistry.SignedRegistration[] memory registrations = _setupSingleRegistration(SECRET_KEY_1, operator); - bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator); + uint64 nonce = 1337; + IRegistry.SignedRegistration[] memory registrations = + _setupSingleRegistration(SECRET_KEY_1, operator, signingId, nonce); + bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator, signingId); address slasher = address(1234); address committer = address(5678); @@ -260,8 +289,10 @@ contract OptInAndOutTester is UnitTestHelper { function test_optOutOfSlasher_wrongOperator() public { uint256 collateral = registry.getConfig().minCollateralWei; - IRegistry.SignedRegistration[] memory registrations = _setupSingleRegistration(SECRET_KEY_1, operator); - bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator); + uint64 nonce = 1337; + IRegistry.SignedRegistration[] memory registrations = + _setupSingleRegistration(SECRET_KEY_1, operator, signingId, nonce); + bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator, signingId); address slasher = address(1234); address committer = address(5678); @@ -281,8 +312,10 @@ contract OptInAndOutTester is UnitTestHelper { function test_optOutOfSlasher_optInDelayNotMet() public { uint256 collateral = registry.getConfig().minCollateralWei; - IRegistry.SignedRegistration[] memory registrations = _setupSingleRegistration(SECRET_KEY_1, operator); - bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator); + uint64 nonce = 1337; + IRegistry.SignedRegistration[] memory registrations = + _setupSingleRegistration(SECRET_KEY_1, operator, signingId, nonce); + bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator, signingId); address slasher = address(1234); address committer = address(5678); @@ -302,6 +335,8 @@ contract OptInAndOutTester is UnitTestHelper { } contract ClaimCollateralTester is UnitTestHelper { + bytes32 signingId = keccak256("test-signing-id"); + function setUp() public { registry = new Registry(defaultConfig()); vm.deal(operator, 100 ether); @@ -312,9 +347,11 @@ contract ClaimCollateralTester is UnitTestHelper { function test_claimCollateral() public { uint256 collateral = registry.getConfig().minCollateralWei; - IRegistry.SignedRegistration[] memory registrations = _setupSingleRegistration(SECRET_KEY_1, operator); + uint64 nonce = 1337; + IRegistry.SignedRegistration[] memory registrations = + _setupSingleRegistration(SECRET_KEY_1, operator, signingId, nonce); - bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator); + bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator, signingId); vm.startPrank(operator); registry.unregister(registrationRoot); @@ -338,9 +375,11 @@ contract ClaimCollateralTester is UnitTestHelper { function test_claimCollateral_notUnregistered() public { uint256 collateral = registry.getConfig().minCollateralWei; - IRegistry.SignedRegistration[] memory registrations = _setupSingleRegistration(SECRET_KEY_1, operator); + uint64 nonce = 1337; + IRegistry.SignedRegistration[] memory registrations = + _setupSingleRegistration(SECRET_KEY_1, operator, signingId, nonce); - bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator); + bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator, signingId); // Try to claim without unregistering first vm.startPrank(operator); @@ -351,9 +390,11 @@ contract ClaimCollateralTester is UnitTestHelper { function test_claimCollateral_delayNotMet() public { uint256 collateral = registry.getConfig().minCollateralWei; - IRegistry.SignedRegistration[] memory registrations = _setupSingleRegistration(SECRET_KEY_1, operator); + uint64 nonce = 1337; + IRegistry.SignedRegistration[] memory registrations = + _setupSingleRegistration(SECRET_KEY_1, operator, signingId, nonce); - bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator); + bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator, signingId); vm.startPrank(operator); registry.unregister(registrationRoot); @@ -369,9 +410,11 @@ contract ClaimCollateralTester is UnitTestHelper { function test_claimCollateral_alreadyClaimed() public { uint256 collateral = registry.getConfig().minCollateralWei; - IRegistry.SignedRegistration[] memory registrations = _setupSingleRegistration(SECRET_KEY_1, operator); + uint64 nonce = 1337; + IRegistry.SignedRegistration[] memory registrations = + _setupSingleRegistration(SECRET_KEY_1, operator, signingId, nonce); - bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator); + bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator, signingId); vm.startPrank(operator); registry.unregister(registrationRoot); @@ -389,6 +432,8 @@ contract ClaimCollateralTester is UnitTestHelper { } contract AddCollateralTester is UnitTestHelper { + bytes32 signingId = keccak256("test-signing-id"); + function setUp() public { registry = new Registry(defaultConfig()); vm.deal(operator, 100 ether); @@ -400,9 +445,11 @@ contract AddCollateralTester is UnitTestHelper { uint256 collateral = registry.getConfig().minCollateralWei; vm.assume((addAmount + collateral) < uint256(2 ** 80)); - IRegistry.SignedRegistration[] memory registrations = _setupSingleRegistration(SECRET_KEY_1, operator); + uint64 nonce = 1337; + IRegistry.SignedRegistration[] memory registrations = + _setupSingleRegistration(SECRET_KEY_1, operator, signingId, nonce); - bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator); + bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator, signingId); uint256 expectedCollateralWei = collateral + addAmount; vm.deal(operator, addAmount); @@ -420,9 +467,11 @@ contract AddCollateralTester is UnitTestHelper { function test_addCollateral_overflow() public { uint256 collateral = registry.getConfig().minCollateralWei; - IRegistry.SignedRegistration[] memory registrations = _setupSingleRegistration(SECRET_KEY_1, operator); + uint64 nonce = 1337; + IRegistry.SignedRegistration[] memory registrations = + _setupSingleRegistration(SECRET_KEY_1, operator, signingId, nonce); - bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator); + bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator, signingId); uint256 addAmount = 2 ** 80; // overflow uint80 vm.deal(operator, addAmount); @@ -446,6 +495,8 @@ contract AddCollateralTester is UnitTestHelper { } contract SlashRegistrationTester is UnitTestHelper { + bytes32 signingId = keccak256("test-signing-id"); + function setUp() public { registry = new Registry(defaultConfig()); vm.deal(operator, 100 ether); @@ -461,11 +512,12 @@ contract SlashRegistrationTester is UnitTestHelper { BLS.G1Point memory pubkey = BLSUtils.toPublicKey(SECRET_KEY_1); // Use a different secret key to sign the registration - BLS.G2Point memory signature = _registrationSignature(SECRET_KEY_2, operator); + uint64 nonce = 1337; + BLS.G2Point memory signature = _registrationSignature(SECRET_KEY_2, operator, signingId, nonce); - registrations[0] = IRegistry.SignedRegistration({ pubkey: pubkey, signature: signature }); + registrations[0] = IRegistry.SignedRegistration({ pubkey: pubkey, signature: signature, nonce: nonce }); - bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator); + bytes32 registrationRoot = registry.register{ value: collateral }(registrations, operator, signingId); _assertRegistration(registrationRoot, operator, uint80(collateral), uint48(block.number), type(uint48).max, 0); @@ -474,7 +526,7 @@ contract SlashRegistrationTester is UnitTestHelper { uint256 urcBalanceBefore = address(registry).balance; vm.startPrank(challenger); - registry.slashRegistration(registry.getRegistrationProof(registrations, operator, 0)); + registry.slashRegistration(registry.getRegistrationProof(registrations, operator, 0, signingId)); vm.warp(block.timestamp + registry.getConfig().slashWindow); vm.startPrank(operator); @@ -500,11 +552,15 @@ contract SlashRegistrationTester is UnitTestHelper { IRegistry.SignedRegistration[] memory registrations = new IRegistry.SignedRegistration[](1); - registrations[0] = _createSignedRegistration(SECRET_KEY_1, operator); + uint64 nonce = 1337; + registrations[0] = _createSignedRegistration(SECRET_KEY_1, operator, signingId, nonce); - bytes32 registrationRoot = registry.register{ value: collateral }( + bytes32 registrationRoot = registry.register{ + value: collateral + }( registrations, - thief // thief tries to frontrun operator by setting his address as withdrawal address + thief, // thief tries to frontrun operator by setting his address as withdrawal address + signingId ); _assertRegistration( @@ -521,7 +577,7 @@ contract SlashRegistrationTester is UnitTestHelper { uint256 urcBalanceBefore = address(registry).balance; // Note that proof is created using the thief's address as the owner - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(registrations, thief, 0); + IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(registrations, thief, 0, signingId); vm.startPrank(challenger); registry.slashRegistration(proof); @@ -549,13 +605,17 @@ contract SlashRegistrationTester is UnitTestHelper { uint256 collateral = 2 * registry.getConfig().minCollateralWei; IRegistry.SignedRegistration[] memory registrations = new IRegistry.SignedRegistration[](2); - registrations[0] = _createSignedRegistration(SECRET_KEY_1, operator); + uint64 nonce = 1337; + registrations[0] = _createSignedRegistration(SECRET_KEY_1, operator, signingId, nonce); - registrations[1] = _createSignedRegistration(SECRET_KEY_2, operator); + registrations[1] = _createSignedRegistration(SECRET_KEY_2, operator, signingId, nonce); - bytes32 registrationRoot = registry.register{ value: collateral }( + bytes32 registrationRoot = registry.register{ + value: collateral + }( registrations, - thief // thief tries to frontrun operator by setting his address as withdrawal address + thief, // thief tries to frontrun operator by setting his address as withdrawal address + signingId ); // Verify initial registration state @@ -566,7 +626,7 @@ contract SlashRegistrationTester is UnitTestHelper { uint256 urcBalanceBefore = address(registry).balance; // Note that proof is created using the thief's address as the owner - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(registrations, thief, 0); + IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(registrations, thief, 0, signingId); vm.startPrank(challenger); registry.slashRegistration(proof); @@ -595,12 +655,16 @@ contract SlashRegistrationTester is UnitTestHelper { IRegistry.SignedRegistration[] memory registrations = new IRegistry.SignedRegistration[](size); for (uint256 i = 0; i < size; i++) { - registrations[i] = _createSignedRegistration(SECRET_KEY_1 + i, operator); + uint64 nonce = 1337; + registrations[i] = _createSignedRegistration(SECRET_KEY_1 + i, operator, signingId, nonce); } - bytes32 registrationRoot = registry.register{ value: collateral }( + bytes32 registrationRoot = registry.register{ + value: collateral + }( registrations, - thief // submit different withdrawal address than the one signed by validator keys + thief, // submit different withdrawal address than the one signed by validator keys + signingId ); uint256 thiefBalanceBefore = thief.balance; @@ -608,7 +672,8 @@ contract SlashRegistrationTester is UnitTestHelper { uint256 urcBalanceBefore = address(registry).balance; // Note that proof is created using the thief's address as the owner - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(registrations, thief, leafIndex); + IRegistry.RegistrationProof memory proof = + registry.getRegistrationProof(registrations, thief, leafIndex, signingId); vm.startPrank(challenger); registry.slashRegistration(proof); @@ -639,14 +704,15 @@ contract SlashRegistrationTester is UnitTestHelper { BLS.G1Point memory pubkey = BLSUtils.toPublicKey(SECRET_KEY_1); // Use a different secret key to sign the registration - BLS.G2Point memory signature = _registrationSignature(SECRET_KEY_2, operator); + uint64 nonce = 1337; + BLS.G2Point memory signature = _registrationSignature(SECRET_KEY_2, operator, signingId, nonce); - registrations[0] = IRegistry.SignedRegistration({ pubkey: pubkey, signature: signature }); + registrations[0] = IRegistry.SignedRegistration({ pubkey: pubkey, signature: signature, nonce: nonce }); - registry.register{ value: collateral }(registrations, operator); + registry.register{ value: collateral }(registrations, operator, signingId); // Get the proof - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(registrations, operator, 0); + IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(registrations, operator, 0, signingId); vm.startPrank(challenger); registry.slashRegistration(proof); @@ -658,6 +724,8 @@ contract SlashRegistrationTester is UnitTestHelper { } contract RentrancyTester is UnitTestHelper { + bytes32 signingId = keccak256("test-signing-id"); + function setUp() public { registry = new Registry(defaultConfig()); vm.deal(operator, 100 ether); @@ -673,10 +741,11 @@ contract RentrancyTester is UnitTestHelper { ReentrantRegistrationContract reentrantContract = new ReentrantRegistrationContract(address(registry)); vm.deal(address(reentrantContract), 1000 ether); + uint64 nonce = 1337; IRegistry.SignedRegistration[] memory registrations = - _setupSingleRegistration(SECRET_KEY_1, address(reentrantContract)); + _setupSingleRegistration(SECRET_KEY_1, address(reentrantContract), signingId, nonce); - reentrantContract.register(registrations); + reentrantContract.register(registrations, signingId); // pretend to unregister reentrantContract.unregister(); @@ -717,10 +786,11 @@ contract RentrancyTester is UnitTestHelper { IRegistry.SignedRegistration[] memory registrations = new IRegistry.SignedRegistration[](1); - registrations[0] = _createSignedRegistration(SECRET_KEY_1, operator); + uint64 nonce = 1337; + registrations[0] = _createSignedRegistration(SECRET_KEY_1, operator, signingId, nonce); // frontrun to set withdrawal address to reentrantContract - reentrantContract.register(registrations); + reentrantContract.register(registrations, signingId); _assertRegistration( reentrantContract.registrationRoot(), @@ -732,7 +802,7 @@ contract RentrancyTester is UnitTestHelper { ); IRegistry.RegistrationProof memory proof = - registry.getRegistrationProof(registrations, address(reentrantContract), 0); + registry.getRegistrationProof(registrations, address(reentrantContract), 0, signingId); // operator can slash the registration vm.startPrank(operator); @@ -741,93 +811,107 @@ contract RentrancyTester is UnitTestHelper { } contract RegisterGasTest is UnitTestHelper { + bytes32 signingId = keccak256("test-signing-id"); + function setUp() public { registry = new Registry(defaultConfig()); vm.deal(operator, 100 ether); } - function registrations(uint256 n) internal returns (IRegistry.SignedRegistration[] memory) { + function getRegistrations(uint256 n) internal view returns (IRegistry.SignedRegistration[] memory) { IRegistry.SignedRegistration[] memory registrations = new IRegistry.SignedRegistration[](n); for (uint256 i = 0; i < n; i++) { - registrations[i] = _createSignedRegistration(SECRET_KEY_1 + i, operator); + uint64 nonce = uint64(SECRET_KEY_1 + i); + registrations[i] = _createSignedRegistration(SECRET_KEY_1 + i, operator, signingId, nonce); } return registrations; } function test_gas_register_1() public { - IRegistry.SignedRegistration[] memory registrations = registrations(1); - vm.resetGasMetering(); + vm.pauseGasMetering(); + IRegistry.SignedRegistration[] memory registrations = getRegistrations(1); vm.startPrank(operator); - registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator); + vm.resumeGasMetering(); + registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator, signingId); } function test_gas_register_2() public { - IRegistry.SignedRegistration[] memory registrations = registrations(2); - vm.resetGasMetering(); + vm.pauseGasMetering(); + IRegistry.SignedRegistration[] memory registrations = getRegistrations(2); vm.startPrank(operator); - registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator); + vm.resumeGasMetering(); + registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator, signingId); } function test_gas_register_4() public { - IRegistry.SignedRegistration[] memory registrations = registrations(4); - vm.resetGasMetering(); + vm.pauseGasMetering(); + IRegistry.SignedRegistration[] memory registrations = getRegistrations(4); vm.startPrank(operator); - registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator); + vm.resumeGasMetering(); + registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator, signingId); } function test_gas_register_8() public { - IRegistry.SignedRegistration[] memory registrations = registrations(8); - vm.resetGasMetering(); + vm.pauseGasMetering(); + IRegistry.SignedRegistration[] memory registrations = getRegistrations(8); vm.startPrank(operator); - registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator); + vm.resumeGasMetering(); + registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator, signingId); } function test_gas_register_16() public { - IRegistry.SignedRegistration[] memory registrations = registrations(16); - vm.resetGasMetering(); + vm.pauseGasMetering(); + IRegistry.SignedRegistration[] memory registrations = getRegistrations(16); vm.startPrank(operator); - registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator); + vm.resumeGasMetering(); + registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator, signingId); } function test_gas_register_32() public { - IRegistry.SignedRegistration[] memory registrations = registrations(32); - vm.resetGasMetering(); + vm.pauseGasMetering(); + IRegistry.SignedRegistration[] memory registrations = getRegistrations(32); vm.startPrank(operator); - registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator); + vm.resumeGasMetering(); + registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator, signingId); } function test_gas_register_64() public { - IRegistry.SignedRegistration[] memory registrations = registrations(64); - vm.resetGasMetering(); + vm.pauseGasMetering(); + IRegistry.SignedRegistration[] memory registrations = getRegistrations(64); vm.startPrank(operator); - registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator); + vm.resumeGasMetering(); + registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator, signingId); } function test_gas_register_128() public { - IRegistry.SignedRegistration[] memory registrations = registrations(128); - vm.resetGasMetering(); + vm.pauseGasMetering(); + IRegistry.SignedRegistration[] memory registrations = getRegistrations(128); vm.startPrank(operator); - registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator); + vm.resumeGasMetering(); + registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator, signingId); } function test_gas_register_256() public { - IRegistry.SignedRegistration[] memory registrations = registrations(256); - vm.resetGasMetering(); + vm.pauseGasMetering(); + IRegistry.SignedRegistration[] memory registrations = getRegistrations(256); vm.startPrank(operator); - registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator); + vm.resumeGasMetering(); + registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator, signingId); } function test_gas_register_512() public { - IRegistry.SignedRegistration[] memory registrations = registrations(512); - vm.resetGasMetering(); + vm.pauseGasMetering(); + IRegistry.SignedRegistration[] memory registrations = getRegistrations(512); vm.startPrank(operator); - registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator); + vm.resumeGasMetering(); + registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator, signingId); } function test_gas_register_1024() public { - IRegistry.SignedRegistration[] memory registrations = registrations(1024); - vm.resetGasMetering(); + vm.pauseGasMetering(); + IRegistry.SignedRegistration[] memory registrations = getRegistrations(1024); vm.startPrank(operator); - registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator); + vm.resumeGasMetering(); + registry.register{ value: registry.getConfig().minCollateralWei }(registrations, operator, signingId); } } diff --git a/test/Scratch.t.sol b/test/Scratch.t.sol new file mode 100644 index 0000000..864bf89 --- /dev/null +++ b/test/Scratch.t.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0 <0.9.0; +// Credit: https://github.com/paradigmxyz/forge-alphanet/blob/main/src/sign/BLS.sol + +import { Test, console } from "forge-std/Test.sol"; +import { BLSUtils } from "../src/lib/BLSUtils.sol"; +import { BLS } from "solady/utils/ext/ithaca/BLS.sol"; +import { ISlasher } from "../src/ISlasher.sol"; + +/// @notice A simple test demonstrating BLS signature verification. +contract ScratchTest is Test { + struct CommitmentRequest { + uint64 commitmentType; + bytes payload; + address slasher; + } + + struct Commitment { + uint64 commitmentType; + bytes payload; + bytes32 requestHash; + address slasher; + } + + struct InclusionPayload { + uint64 slot; + bytes signedTx; + } + + function test_Foo() public { + Commitment memory commitment = + Commitment({ commitmentType: 1, payload: "", requestHash: bytes32(0), slasher: address(0) }); + console.logBytes(abi.encode(commitment)); + console.logBytes32(keccak256(abi.encode(commitment))); + } + + function test_Bar() public { + CommitmentRequest memory commitment = CommitmentRequest({ commitmentType: 1, payload: "", slasher: address(0) }); + console.logBytes(abi.encode(commitment)); + console.logBytes32(keccak256(abi.encode(commitment))); + } + + function test_Baz() public { + InclusionPayload memory payload = InclusionPayload({ slot: 12345, signedTx: "" }); + console.logBytes(abi.encode(payload)); + console.logBytes32(keccak256(abi.encode(payload))); + } +} diff --git a/test/Slasher.t.sol b/test/Slasher.t.sol index 36a7eeb..bad2449 100644 --- a/test/Slasher.t.sol +++ b/test/Slasher.t.sol @@ -31,6 +31,7 @@ contract SlashCommitmentTester is UnitTestHelper { uint256 collateral = 100 ether; uint256 committerSecretKey; address committer; + bytes32 signingId = keccak256("test-signing-id"); function setUp() public { registry = new Registry(defaultConfig()); @@ -51,16 +52,26 @@ contract SlashCommitmentTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: 0 + slot: 0, + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); - ISlasher.SignedCommitment memory signedCommitment = - basicCommitment(params.committerSecretKey, params.slasher, ""); + ISlasher.SignedCommitment memory signedCommitment = basicCommitment( + params.committerSecretKey, + params.slasher, + "", + params.signingId, + params.nonce, + defaultConfig().chainId, + defaultConfig().signingDomain + ); // Setup proof - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(result.registrations, operator, 0); + IRegistry.RegistrationProof memory proof = + registry.getRegistrationProof(result.registrations, operator, 0, signingId); bytes memory evidence = ""; // skip past fraud proof window @@ -112,15 +123,25 @@ contract SlashCommitmentTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: uint64(UINT256_MAX) + slot: uint64(UINT256_MAX), + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); - ISlasher.SignedCommitment memory signedCommitment = - basicCommitment(params.committerSecretKey, params.slasher, ""); + ISlasher.SignedCommitment memory signedCommitment = basicCommitment( + params.committerSecretKey, + params.slasher, + "", + params.signingId, + params.nonce, + defaultConfig().chainId, + defaultConfig().signingDomain + ); - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(result.registrations, operator, 0); + IRegistry.RegistrationProof memory proof = + registry.getRegistrationProof(result.registrations, operator, 0, signingId); bytes memory evidence = ""; // Try to slash before fraud proof window expires @@ -138,18 +159,28 @@ contract SlashCommitmentTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: uint64(UINT256_MAX) + slot: uint64(UINT256_MAX), + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); - ISlasher.SignedCommitment memory signedCommitment = - basicCommitment(params.committerSecretKey, params.slasher, ""); + ISlasher.SignedCommitment memory signedCommitment = basicCommitment( + params.committerSecretKey, + params.slasher, + "", + params.signingId, + params.nonce, + defaultConfig().chainId, + defaultConfig().signingDomain + ); // Create invalid proof, IRegistry.RegistrationProof memory proof = IRegistry.RegistrationProof({ registrationRoot: result.registrationRoot, registration: result.registrations[0], - merkleProof: new bytes32[](1) + merkleProof: new bytes32[](1), + signingId: params.signingId }); vm.warp(block.timestamp + registry.getConfig().fraudProofWindow + 1); @@ -168,19 +199,29 @@ contract SlashCommitmentTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: uint64(UINT256_MAX) + slot: uint64(UINT256_MAX), + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); - ISlasher.SignedCommitment memory signedCommitment = - basicCommitment(params.committerSecretKey, params.slasher, ""); + ISlasher.SignedCommitment memory signedCommitment = basicCommitment( + params.committerSecretKey, + params.slasher, + "", + params.signingId, + params.nonce, + defaultConfig().chainId, + defaultConfig().signingDomain + ); // Sign delegation with different secret key ISlasher.SignedDelegation memory badSignedDelegation = - signDelegation(SECRET_KEY_2, result.signedDelegation.delegation); + signDelegation(SECRET_KEY_2, result.signedDelegation.delegation, signingId, params.nonce); - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(result.registrations, operator, 0); + IRegistry.RegistrationProof memory proof = + registry.getRegistrationProof(result.registrations, operator, 0, signingId); vm.warp(block.timestamp + registry.getConfig().fraudProofWindow + 1); @@ -198,14 +239,24 @@ contract SlashCommitmentTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: uint64(UINT256_MAX) + slot: uint64(UINT256_MAX), + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); - ISlasher.SignedCommitment memory signedCommitment = - basicCommitment(params.committerSecretKey, params.slasher, ""); + ISlasher.SignedCommitment memory signedCommitment = basicCommitment( + params.committerSecretKey, + params.slasher, + "", + params.signingId, + params.nonce, + defaultConfig().chainId, + defaultConfig().signingDomain + ); - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(result.registrations, operator, 0); + IRegistry.RegistrationProof memory proof = + registry.getRegistrationProof(result.registrations, operator, 0, signingId); vm.warp(block.timestamp + registry.getConfig().fraudProofWindow + 1); @@ -224,15 +275,25 @@ contract SlashCommitmentTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: uint64(UINT256_MAX) + slot: uint64(UINT256_MAX), + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); - ISlasher.SignedCommitment memory signedCommitment = - basicCommitment(params.committerSecretKey, params.slasher, ""); + ISlasher.SignedCommitment memory signedCommitment = basicCommitment( + params.committerSecretKey, + params.slasher, + "", + params.signingId, + params.nonce, + defaultConfig().chainId, + defaultConfig().signingDomain + ); // Setup proof - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(result.registrations, operator, 0); + IRegistry.RegistrationProof memory proof = + registry.getRegistrationProof(result.registrations, operator, 0, signingId); bytes memory evidence = ""; // skip past fraud proof window @@ -257,7 +318,15 @@ contract SlashCommitmentTester is UnitTestHelper { registry.slashCommitment(proof, result.signedDelegation, signedCommitment, evidence); // attempt to slash with different SignedCommitment - signedCommitment = basicCommitment(params.committerSecretKey, params.slasher, "different payload"); + signedCommitment = basicCommitment( + params.committerSecretKey, + params.slasher, + "different payload", + params.signingId, + params.nonce, + defaultConfig().chainId, + defaultConfig().signingDomain + ); vm.expectRevert(IRegistry.SlashWindowExpired.selector); registry.slashCommitment(proof, result.signedDelegation, signedCommitment, evidence); @@ -289,15 +358,25 @@ contract SlashCommitmentTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: uint64(UINT256_MAX) + slot: uint64(UINT256_MAX), + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); - ISlasher.SignedCommitment memory signedCommitment = - basicCommitment(params.committerSecretKey, params.slasher, ""); + ISlasher.SignedCommitment memory signedCommitment = basicCommitment( + params.committerSecretKey, + params.slasher, + "", + params.signingId, + params.nonce, + defaultConfig().chainId, + defaultConfig().signingDomain + ); // Setup proof - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(result.registrations, operator, 0); + IRegistry.RegistrationProof memory proof = + registry.getRegistrationProof(result.registrations, operator, 0, signingId); bytes memory evidence = ""; // skip past fraud proof window @@ -316,7 +395,15 @@ contract SlashCommitmentTester is UnitTestHelper { registry.slashCommitment(proof, result.signedDelegation, signedCommitment, evidence); // slash again with different SignedCommitment - signedCommitment = basicCommitment(params.committerSecretKey, params.slasher, "different payload"); + signedCommitment = basicCommitment( + params.committerSecretKey, + params.slasher, + "different payload", + params.signingId, + params.nonce, + defaultConfig().chainId, + defaultConfig().signingDomain + ); vm.expectEmit(address(registry)); emit IRegistry.OperatorSlashed( IRegistry.SlashingType.Commitment, @@ -343,6 +430,7 @@ contract SlashCommitmentFromOptInTester is UnitTestHelper { uint256 collateral = 100 ether; uint256 committerSecretKey; address committer; + bytes32 signingId = keccak256("test-signing-id"); function setUp() public { registry = new Registry(defaultConfig()); @@ -363,13 +451,22 @@ contract SlashCommitmentFromOptInTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: 0 + slot: 0, + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); - ISlasher.SignedCommitment memory signedCommitment = - basicCommitment(params.committerSecretKey, params.slasher, ""); + ISlasher.SignedCommitment memory signedCommitment = basicCommitment( + params.committerSecretKey, + params.slasher, + "", + params.signingId, + params.nonce, + defaultConfig().chainId, + defaultConfig().signingDomain + ); // skip past fraud proof window vm.warp(block.timestamp + registry.getConfig().fraudProofWindow + 1); @@ -424,13 +521,22 @@ contract SlashCommitmentFromOptInTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: 0 + slot: 0, + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); - ISlasher.SignedCommitment memory signedCommitment = - basicCommitment(params.committerSecretKey, params.slasher, ""); + ISlasher.SignedCommitment memory signedCommitment = basicCommitment( + params.committerSecretKey, + params.slasher, + "", + params.signingId, + params.nonce, + defaultConfig().chainId, + defaultConfig().signingDomain + ); // skip past fraud proof window vm.warp(block.timestamp + registry.getConfig().fraudProofWindow + 1); @@ -465,13 +571,22 @@ contract SlashCommitmentFromOptInTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: 0 + slot: 0, + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); - ISlasher.SignedCommitment memory signedCommitment = - basicCommitment(params.committerSecretKey, params.slasher, ""); + ISlasher.SignedCommitment memory signedCommitment = basicCommitment( + params.committerSecretKey, + params.slasher, + "", + params.signingId, + params.nonce, + defaultConfig().chainId, + defaultConfig().signingDomain + ); // skip past fraud proof window vm.warp(block.timestamp + registry.getConfig().fraudProofWindow + 1); @@ -491,7 +606,15 @@ contract SlashCommitmentFromOptInTester is UnitTestHelper { vm.warp(block.timestamp + registry.getConfig().slashWindow + 1); // Try to slash again after window expired - signedCommitment = basicCommitment(params.committerSecretKey, params.slasher, "different payload"); + signedCommitment = basicCommitment( + params.committerSecretKey, + params.slasher, + "different payload", + params.signingId, + params.nonce, + defaultConfig().chainId, + defaultConfig().signingDomain + ); vm.expectRevert(IRegistry.SlashWindowExpired.selector); registry.slashCommitment(result.registrationRoot, signedCommitment, ""); } @@ -506,13 +629,22 @@ contract SlashCommitmentFromOptInTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: 0 + slot: 0, + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); - ISlasher.SignedCommitment memory signedCommitment = - basicCommitment(params.committerSecretKey, params.slasher, ""); + ISlasher.SignedCommitment memory signedCommitment = basicCommitment( + params.committerSecretKey, + params.slasher, + "", + params.signingId, + params.nonce, + defaultConfig().chainId, + defaultConfig().signingDomain + ); // Wait for fraud proof window vm.warp(block.timestamp + registry.getConfig().fraudProofWindow + 1); @@ -533,14 +665,24 @@ contract SlashCommitmentFromOptInTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: 0 + slot: 0, + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); // Create commitment signed by different key (address wrongCommitter, uint256 wrongCommitterKey) = makeAddrAndKey("wrongCommitter"); - ISlasher.SignedCommitment memory signedCommitment = basicCommitment(wrongCommitterKey, params.slasher, ""); + ISlasher.SignedCommitment memory signedCommitment = basicCommitment( + wrongCommitterKey, + params.slasher, + "", + params.signingId, + params.nonce, + defaultConfig().chainId, + defaultConfig().signingDomain + ); // skip past fraud proof window vm.warp(block.timestamp + registry.getConfig().fraudProofWindow + 1); @@ -568,13 +710,22 @@ contract SlashCommitmentFromOptInTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: 0 + slot: 0, + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); - ISlasher.SignedCommitment memory signedCommitment = - basicCommitment(params.committerSecretKey, params.slasher, ""); + ISlasher.SignedCommitment memory signedCommitment = basicCommitment( + params.committerSecretKey, + params.slasher, + "", + params.signingId, + params.nonce, + defaultConfig().chainId, + defaultConfig().signingDomain + ); // skip past fraud proof window vm.warp(block.timestamp + registry.getConfig().fraudProofWindow + 1); @@ -602,13 +753,22 @@ contract SlashCommitmentFromOptInTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: 0 + slot: 0, + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); - ISlasher.SignedCommitment memory signedCommitment = - basicCommitment(params.committerSecretKey, params.slasher, ""); + ISlasher.SignedCommitment memory signedCommitment = basicCommitment( + params.committerSecretKey, + params.slasher, + "", + params.signingId, + params.nonce, + defaultConfig().chainId, + defaultConfig().signingDomain + ); // skip past fraud proof window vm.warp(block.timestamp + registry.getConfig().fraudProofWindow + 1); @@ -672,13 +832,22 @@ contract SlashCommitmentFromOptInTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: 0 + slot: 0, + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); - ISlasher.SignedCommitment memory signedCommitment = - basicCommitment(params.committerSecretKey, params.slasher, ""); + ISlasher.SignedCommitment memory signedCommitment = basicCommitment( + params.committerSecretKey, + params.slasher, + "", + params.signingId, + params.nonce, + defaultConfig().chainId, + defaultConfig().signingDomain + ); // skip past fraud proof window vm.warp(block.timestamp + registry.getConfig().fraudProofWindow + 1); @@ -710,6 +879,7 @@ contract SlashEquivocationTester is UnitTestHelper { uint256 collateral = 100 ether; uint256 committerSecretKey; address committer; + bytes32 signingId = keccak256("test-signing-id"); function setUp() public { registry = new Registry(defaultConfig()); @@ -730,13 +900,16 @@ contract SlashEquivocationTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: uint64(UINT256_MAX) + slot: uint64(UINT256_MAX), + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); // Setup proof - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(result.registrations, operator, 0); + IRegistry.RegistrationProof memory proof = + registry.getRegistrationProof(result.registrations, operator, 0, signingId); // skip past fraud proof window vm.warp(block.timestamp + registry.getConfig().fraudProofWindow + 1); @@ -750,7 +923,9 @@ contract SlashEquivocationTester is UnitTestHelper { metadata: "" }); - ISlasher.SignedDelegation memory signedDelegationTwo = signDelegation(params.proposerSecretKey, delegationTwo); + uint64 nonce = uint64(1337); + ISlasher.SignedDelegation memory signedDelegationTwo = + signDelegation(params.proposerSecretKey, delegationTwo, signingId, nonce); // submit both delegations uint256 challengerBalanceBefore = challenger.balance; @@ -783,12 +958,15 @@ contract SlashEquivocationTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: uint64(UINT256_MAX) + slot: uint64(UINT256_MAX), + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(result.registrations, operator, 0); + IRegistry.RegistrationProof memory proof = + registry.getRegistrationProof(result.registrations, operator, 0, signingId); // Create second delegation ISlasher.Delegation memory delegationTwo = ISlasher.Delegation({ @@ -799,7 +977,9 @@ contract SlashEquivocationTester is UnitTestHelper { metadata: "" }); - ISlasher.SignedDelegation memory signedDelegationTwo = signDelegation(params.proposerSecretKey, delegationTwo); + uint64 nonce = uint64(1337); + ISlasher.SignedDelegation memory signedDelegationTwo = + signDelegation(params.proposerSecretKey, delegationTwo, signingId, nonce); vm.startPrank(challenger); vm.expectRevert(IRegistry.FraudProofWindowNotMet.selector); @@ -816,7 +996,9 @@ contract SlashEquivocationTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: uint64(UINT256_MAX) + slot: uint64(UINT256_MAX), + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); @@ -825,7 +1007,8 @@ contract SlashEquivocationTester is UnitTestHelper { IRegistry.RegistrationProof memory proof = IRegistry.RegistrationProof({ registrationRoot: result.registrationRoot, registration: result.registrations[0], - merkleProof: new bytes32[](1) + merkleProof: new bytes32[](1), + signingId: params.signingId }); // Create second delegation @@ -837,7 +1020,8 @@ contract SlashEquivocationTester is UnitTestHelper { metadata: "" }); - ISlasher.SignedDelegation memory signedDelegationTwo = signDelegation(params.proposerSecretKey, delegationTwo); + ISlasher.SignedDelegation memory signedDelegationTwo = + signDelegation(params.proposerSecretKey, delegationTwo, signingId, params.nonce); vm.warp(block.timestamp + registry.getConfig().fraudProofWindow + 1); @@ -856,12 +1040,15 @@ contract SlashEquivocationTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: uint64(UINT256_MAX) + slot: uint64(UINT256_MAX), + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(result.registrations, operator, 0); + IRegistry.RegistrationProof memory proof = + registry.getRegistrationProof(result.registrations, operator, 0, signingId); vm.warp(block.timestamp + registry.getConfig().fraudProofWindow + 1); @@ -884,12 +1071,15 @@ contract SlashEquivocationTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: 1000 + slot: 1000, + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(result.registrations, operator, 0); + IRegistry.RegistrationProof memory proof = + registry.getRegistrationProof(result.registrations, operator, 0, signingId); // Create second delegation with different slot ISlasher.Delegation memory delegationTwo = ISlasher.Delegation({ @@ -900,7 +1090,8 @@ contract SlashEquivocationTester is UnitTestHelper { metadata: "" }); - ISlasher.SignedDelegation memory signedDelegationTwo = signDelegation(params.proposerSecretKey, delegationTwo); + ISlasher.SignedDelegation memory signedDelegationTwo = + signDelegation(params.proposerSecretKey, delegationTwo, signingId, params.nonce); vm.warp(block.timestamp + registry.getConfig().fraudProofWindow + 1); @@ -919,12 +1110,15 @@ contract SlashEquivocationTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: uint64(UINT256_MAX) + slot: uint64(UINT256_MAX), + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(result.registrations, operator, 0); + IRegistry.RegistrationProof memory proof = + registry.getRegistrationProof(result.registrations, operator, 0, signingId); // Create second delegation ISlasher.Delegation memory delegationTwo = ISlasher.Delegation({ @@ -935,7 +1129,8 @@ contract SlashEquivocationTester is UnitTestHelper { metadata: "" }); - ISlasher.SignedDelegation memory signedDelegationTwo = signDelegation(params.proposerSecretKey, delegationTwo); + ISlasher.SignedDelegation memory signedDelegationTwo = + signDelegation(params.proposerSecretKey, delegationTwo, signingId, params.nonce); vm.warp(block.timestamp + registry.getConfig().fraudProofWindow + 1); @@ -962,12 +1157,15 @@ contract SlashEquivocationTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: uint64(UINT256_MAX) + slot: uint64(UINT256_MAX), + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(result.registrations, operator, 0); + IRegistry.RegistrationProof memory proof = + registry.getRegistrationProof(result.registrations, operator, 0, signingId); // Create second delegation ISlasher.Delegation memory delegationTwo = ISlasher.Delegation({ @@ -978,7 +1176,8 @@ contract SlashEquivocationTester is UnitTestHelper { metadata: "" }); - ISlasher.SignedDelegation memory signedDelegationTwo = signDelegation(params.proposerSecretKey, delegationTwo); + ISlasher.SignedDelegation memory signedDelegationTwo = + signDelegation(params.proposerSecretKey, delegationTwo, signingId, params.nonce); // move past the fraud proof window vm.warp(block.timestamp + registry.getConfig().fraudProofWindow + 1); @@ -1002,6 +1201,7 @@ contract SlashReentrantTester is UnitTestHelper { uint256 collateral = 100 ether; uint256 committerSecretKey; address committer; + bytes32 signingId = keccak256("test-signing-id"); function setUp() public { registry = new Registry(defaultConfig()); @@ -1026,7 +1226,9 @@ contract SlashReentrantTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: uint64(UINT256_MAX) + slot: uint64(UINT256_MAX), + signingId: signingId, + nonce: uint64(1337) }); (RegisterAndDelegateResult memory result, address reentrantContractAddress) = @@ -1034,7 +1236,7 @@ contract SlashReentrantTester is UnitTestHelper { // Setup proof IRegistry.RegistrationProof memory proof = - registry.getRegistrationProof(result.registrations, reentrantContractAddress, 0); + registry.getRegistrationProof(result.registrations, reentrantContractAddress, 0, signingId); // skip past fraud proof window vm.warp(block.timestamp + registry.getConfig().fraudProofWindow + 1); @@ -1051,7 +1253,9 @@ contract SlashReentrantTester is UnitTestHelper { committer: params.committer, slot: params.slot, metadata: "" - }) + }), + signingId, + params.nonce ); // slash from a different address @@ -1103,6 +1307,7 @@ contract SlashConditionTester is UnitTestHelper { uint256 collateral = 100 ether; uint256 committerSecretKey; address committer; + bytes32 signingId = keccak256("test-signing-id"); function setUp() public { registry = new Registry(defaultConfig()); @@ -1123,7 +1328,9 @@ contract SlashConditionTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: uint64(UINT256_MAX) + slot: uint64(UINT256_MAX), + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); @@ -1137,11 +1344,14 @@ contract SlashConditionTester is UnitTestHelper { committer: params.committer, slot: params.slot, metadata: "" - }) + }), + signingId, + params.nonce ); // Setup proof - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(result.registrations, operator, 0); + IRegistry.RegistrationProof memory proof = + registry.getRegistrationProof(result.registrations, operator, 0, signingId); // skip past fraud proof window vm.warp(block.timestamp + registry.getConfig().fraudProofWindow + 1); @@ -1171,7 +1381,9 @@ contract SlashConditionTester is UnitTestHelper { committer: committer, slasher: address(dummySlasher), metadata: "", - slot: uint64(UINT256_MAX) + slot: uint64(UINT256_MAX), + signingId: signingId, + nonce: uint64(1337) }); RegisterAndDelegateResult memory result = registerAndDelegate(params); @@ -1185,11 +1397,14 @@ contract SlashConditionTester is UnitTestHelper { committer: params.committer, slot: params.slot, metadata: "" - }) + }), + signingId, + params.nonce ); // Setup proof - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(result.registrations, operator, 0); + IRegistry.RegistrationProof memory proof = + registry.getRegistrationProof(result.registrations, operator, 0, signingId); // skip past fraud proof window vm.warp(block.timestamp + registry.getConfig().fraudProofWindow + 1); diff --git a/test/StateLockSlasher.t.sol b/test/StateLockSlasher.t.sol index ed913b9..d3b99df 100644 --- a/test/StateLockSlasher.t.sol +++ b/test/StateLockSlasher.t.sol @@ -36,9 +36,9 @@ contract StateLockSlasherTest is UnitTestHelper, PreconfStructs { uint256 collateral = 1.1 ether; uint256 committerSecretKey; address committer; + bytes32 signingId = keccak256("test-signing-id"); function setUp() public { - vm.createSelectFork(vm.rpcUrl("mainnet")); slasher = new StateLockSlasher(slashAmountWei); registry = new Registry(defaultConfig()); (committer, committerSecretKey) = makeAddrAndKey("commitmentsKey"); @@ -121,7 +121,9 @@ contract StateLockSlasherTest is UnitTestHelper, PreconfStructs { committer: committer, slasher: address(slasher), metadata: metadata, - slot: slot + slot: slot, + signingId: signingId, + nonce: 1337 }); // Register operator to URC and signs delegation message @@ -182,7 +184,15 @@ contract StateLockSlasherTest is UnitTestHelper, PreconfStructs { bytes32 inclusionTxRoot = slasher._decodeBlockHeaderRLP(inclusionProof.inclusionBlockHeaderRLP).txRoot; assertEq(inclusionTxRoot, vm.parseJsonBytes32(txProof, ".root")); - signedCommitment = basicCommitment(committerSecretKey, address(slasher), abi.encode(commitment)); + signedCommitment = basicCommitment( + committerSecretKey, + address(slasher), + abi.encode(commitment), + signingId, + uint64(1), + defaultConfig().chainId, + defaultConfig().signingDomain + ); evidence = abi.encode(inclusionProof); } @@ -202,7 +212,12 @@ contract StateLockSlasherTest is UnitTestHelper, PreconfStructs { uint256 urcBalanceBefore = address(registry).balance; IRegistry.RegistrationProof memory proof = - registry.getRegistrationProof(result.registrations, operatorData.owner, 0); + registry.getRegistrationProof(result.registrations, operatorData.owner, 0, signingId); + + // To save on RPC calls, we pre-fill the blockhashes with the expected values + PreconfStructs.InclusionProof memory inclusionProof = abi.decode(evidence, (InclusionProof)); + vm.setBlockhash(inclusionProof.inclusionBlockNumber - 1, keccak256(inclusionProof.previousBlockHeaderRLP)); + vm.setBlockhash(inclusionProof.inclusionBlockNumber, keccak256(inclusionProof.inclusionBlockHeaderRLP)); // Slash via URC vm.startPrank(challenger); diff --git a/test/UnitTestHelper.sol b/test/UnitTestHelper.sol index 4710678..6f80c7c 100644 --- a/test/UnitTestHelper.sol +++ b/test/UnitTestHelper.sol @@ -7,6 +7,7 @@ import "../src/IRegistry.sol"; import "../src/ISlasher.sol"; import { BLS } from "solady/utils/ext/ithaca/BLS.sol"; import { BLSUtils } from "../src/lib/BLSUtils.sol"; +import { ECDSAUtils } from "../src/lib/ECDSAUtils.sol"; contract UnitTestHelper is Test { Registry registry; @@ -25,26 +26,33 @@ contract UnitTestHelper is Test { fraudProofWindow: 86400, unregistrationDelay: 86400, slashWindow: 86400, - optInDelay: 86400 + optInDelay: 86400, + signingDomain: "0x436f6d6d", + chainId: "0x01" }); } /// @dev Helper to create a BLS signature for a registration - function _registrationSignature(uint256 secretKey, address owner) internal view returns (BLS.G2Point memory) { - bytes memory message = abi.encode(owner); - return BLSUtils.sign(message, secretKey, registry.REGISTRATION_DOMAIN_SEPARATOR()); + function _registrationSignature(uint256 secretKey, address owner, bytes32 signingId, uint64 nonce) + internal + view + returns (BLS.G2Point memory) + { + bytes32 messageHash = keccak256(abi.encode(IRegistry.MessageType.Registration, owner)); + IRegistry.Config memory config = defaultConfig(); + return BLSUtils.sign(secretKey, messageHash, config.signingDomain, signingId, nonce, config.chainId); } /// @dev Creates a Registration struct with a real BLS keypair - function _createSignedRegistration(uint256 secretKey, address owner) + function _createSignedRegistration(uint256 secretKey, address owner, bytes32 signingId, uint64 nonce) internal view returns (IRegistry.SignedRegistration memory) { BLS.G1Point memory pubkey = BLSUtils.toPublicKey(secretKey); - BLS.G2Point memory signature = _registrationSignature(secretKey, owner); + BLS.G2Point memory signature = _registrationSignature(secretKey, owner, signingId, nonce); - return IRegistry.SignedRegistration({ pubkey: pubkey, signature: signature }); + return IRegistry.SignedRegistration({ pubkey: pubkey, signature: signature, nonce: nonce }); } /// @dev Helper to verify operator data matches expected values @@ -55,7 +63,7 @@ contract UnitTestHelper is Test { uint48 expectedRegisteredAt, uint48 expectedUnregisteredAt, uint48 expectedSlashedAt - ) internal { + ) internal view { IRegistry.OperatorData memory operatorData = registry.getOperatorData(registrationRoot); assertEq(operatorData.owner, expectedOwner, "Wrong withdrawal address"); assertEq(operatorData.collateralWei, expectedCollateral, "Wrong collateral amount"); @@ -64,13 +72,13 @@ contract UnitTestHelper is Test { assertEq(operatorData.slashedAt, expectedSlashedAt, "Wrong slashed timestamp"); } - function _setupSingleRegistration(uint256 secretKey, address owner) + function _setupSingleRegistration(uint256 secretKey, address owner, bytes32 signingId, uint64 nonce) internal view returns (IRegistry.SignedRegistration[] memory) { IRegistry.SignedRegistration[] memory registrations = new IRegistry.SignedRegistration[](1); - registrations[0] = _createSignedRegistration(secretKey, owner); + registrations[0] = _createSignedRegistration(secretKey, owner, signingId, nonce); return registrations; } @@ -83,7 +91,7 @@ contract UnitTestHelper is Test { uint256 _challengerBalanceBefore, uint256 _operatorBalanceBefore, uint256 _urcBalanceBefore - ) internal { + ) internal view { assertEq(_challenger.balance, _challengerBalanceBefore + _rewardAmount, "challenger didn't receive reward"); assertEq( _operator.balance, @@ -99,43 +107,76 @@ contract UnitTestHelper is Test { uint256 _rewardAmount, uint256 _challengerBalanceBefore, uint256 _urcBalanceBefore - ) internal { + ) internal view { assertEq(_challenger.balance, _challengerBalanceBefore + _rewardAmount, "challenger didn't receive reward"); assertEq(address(registry).balance, _urcBalanceBefore - _slashedAmount - _rewardAmount, "urc balance incorrect"); } - function basicRegistration(uint256 secretKey, uint256 collateral, address owner) + function basicRegistration(uint256 secretKey, uint256 collateral, address owner, bytes32 signingId, uint64 nonce) public returns (bytes32 registrationRoot, IRegistry.SignedRegistration[] memory registrations) { - registrations = _setupSingleRegistration(secretKey, owner); + registrations = _setupSingleRegistration(secretKey, owner, signingId, nonce); - registrationRoot = registry.register{ value: collateral }(registrations, owner); + registrationRoot = registry.register{ value: collateral }(registrations, owner, signingId); _assertRegistration(registrationRoot, owner, uint80(collateral), uint48(block.timestamp), type(uint48).max, 0); } - function basicCommitment(uint256 secretKey, address slasher, bytes memory payload) - public - pure - returns (ISlasher.SignedCommitment memory signedCommitment) - { + function sign( + uint256 privateKey, + bytes32 messageHash, + bytes32 signingDomain, + bytes32 signingId, + uint64 nonce, + bytes32 chainId + ) internal view returns (bytes memory signature) { + bytes32 signingRoot = ECDSAUtils.computeSigningRoot(messageHash, signingDomain, signingId, nonce, chainId); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, signingRoot); + return abi.encodePacked(r, s, v); + } + + function basicCommitment( + uint256 secretKey, + address slasher, + bytes memory payload, + bytes32 signingId, + uint64 nonce, + bytes32 chainId, + bytes32 signingDomain + ) public view returns (ISlasher.SignedCommitment memory signedCommitment) { + // Create CommitmentRequest and compute requestHash + ISlasher.CommitmentRequest memory request = + ISlasher.CommitmentRequest({ commitmentType: 0, payload: payload, slasher: slasher }); + bytes32 requestHash = keccak256(abi.encode(request)); + + // Create Commitment with requestHash ISlasher.Commitment memory commitment = - ISlasher.Commitment({ commitmentType: 0, payload: payload, slasher: slasher }); + ISlasher.Commitment({ commitmentType: 0, payload: payload, requestHash: requestHash, slasher: slasher }); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(secretKey, keccak256(abi.encode(commitment))); - bytes memory signature = abi.encodePacked(r, s, v); - signedCommitment = ISlasher.SignedCommitment({ commitment: commitment, signature: signature }); + // Sign using the new structured approach + bytes32 messageHash = keccak256(abi.encode(IRegistry.MessageType.Commitment, commitment)); + bytes memory signature = sign(secretKey, messageHash, signingDomain, signingId, nonce, chainId); + + signedCommitment = ISlasher.SignedCommitment({ + commitment: commitment, nonce: nonce, signingId: signingId, signature: signature + }); } - function signDelegation(uint256 secretKey, ISlasher.Delegation memory delegation) + function signDelegation(uint256 secretKey, ISlasher.Delegation memory delegation, bytes32 signingId, uint64 nonce) public view returns (ISlasher.SignedDelegation memory) { + IRegistry.Config memory config = defaultConfig(); + + bytes32 messageHash = keccak256(abi.encode(IRegistry.MessageType.Delegation, delegation)); BLS.G2Point memory signature = - BLSUtils.sign(abi.encode(delegation), secretKey, registry.DELEGATION_DOMAIN_SEPARATOR()); - return ISlasher.SignedDelegation({ delegation: delegation, signature: signature }); + BLSUtils.sign(secretKey, messageHash, config.signingDomain, signingId, nonce, config.chainId); + return + ISlasher.SignedDelegation({ + delegation: delegation, signature: signature, nonce: nonce, signingId: signingId + }); } struct RegisterAndDelegateParams { @@ -148,6 +189,8 @@ contract UnitTestHelper is Test { address slasher; bytes metadata; uint64 slot; + bytes32 signingId; + uint64 nonce; } struct RegisterAndDelegateResult { @@ -161,8 +204,9 @@ contract UnitTestHelper is Test { returns (RegisterAndDelegateResult memory result) { // Single registration - (result.registrationRoot, result.registrations) = - basicRegistration(params.proposerSecretKey, params.collateral, params.owner); + (result.registrationRoot, result.registrations) = basicRegistration( + params.proposerSecretKey, params.collateral, params.owner, params.signingId, params.nonce + ); // Sign delegation ISlasher.Delegation memory delegation = ISlasher.Delegation({ @@ -173,7 +217,7 @@ contract UnitTestHelper is Test { metadata: params.metadata }); - result.signedDelegation = signDelegation(params.proposerSecretKey, delegation); + result.signedDelegation = signDelegation(params.proposerSecretKey, delegation, params.signingId, params.nonce); } function registerAndDelegateReentrant(RegisterAndDelegateParams memory params) @@ -182,11 +226,12 @@ contract UnitTestHelper is Test { { ReentrantSlashEquivocation reentrantContract = new ReentrantSlashEquivocation(address(registry)); - result.registrations = _setupSingleRegistration(SECRET_KEY_1, address(reentrantContract)); + result.registrations = + _setupSingleRegistration(SECRET_KEY_1, address(reentrantContract), params.signingId, params.nonce); // register via reentrant contract vm.deal(address(reentrantContract), 100 ether); - reentrantContract.register(result.registrations); + reentrantContract.register(result.registrations, params.signingId); result.registrationRoot = reentrantContract.registrationRoot(); reentrantContractAddress = address(reentrantContract); @@ -199,7 +244,7 @@ contract UnitTestHelper is Test { metadata: params.metadata }); - result.signedDelegation = signDelegation(params.proposerSecretKey, delegation); + result.signedDelegation = signDelegation(params.proposerSecretKey, delegation, params.signingId, params.nonce); // Sign a second delegation to equivocate ISlasher.Delegation memory delegationTwo = ISlasher.Delegation({ @@ -209,10 +254,18 @@ contract UnitTestHelper is Test { slot: params.slot, metadata: "different metadata" }); - ISlasher.SignedDelegation memory signedDelegationTwo = signDelegation(params.proposerSecretKey, delegationTwo); - - ISlasher.SignedCommitment memory signedCommitment = - basicCommitment(params.committerSecretKey, params.slasher, ""); + ISlasher.SignedDelegation memory signedDelegationTwo = + signDelegation(params.proposerSecretKey, delegationTwo, params.signingId, params.nonce); + + ISlasher.SignedCommitment memory signedCommitment = basicCommitment( + params.committerSecretKey, + params.slasher, + "", + params.signingId, + params.nonce, + defaultConfig().chainId, + defaultConfig().signingDomain + ); // save info for later reentrancy reentrantContract.saveResult(params, result, signedCommitment, signedDelegationTwo); @@ -256,10 +309,10 @@ contract ReentrantContract { signedDelegationTwo = _signedDelegationTwo; } - function register(IRegistry.SignedRegistration[] memory _registrations) public { + function register(IRegistry.SignedRegistration[] memory _registrations, bytes32 signingId) public { require(_registrations.length == 1, "test harness supports only 1 registration"); registrations[0] = _registrations[0]; - registrationRoot = registry.register{ value: collateral }(_registrations, address(this)); + registrationRoot = registry.register{ value: collateral }(_registrations, address(this), signingId); } function unregister() public { @@ -324,7 +377,8 @@ contract ReentrantSlashableRegistrationContract is ReentrantContract { IRegistry.SignedRegistration[] memory _registrations = new IRegistry.SignedRegistration[](1); _registrations[0] = registrations[0]; - IRegistry.RegistrationProof memory proof = registry.getRegistrationProof(_registrations, address(this), 0); + IRegistry.RegistrationProof memory proof = + registry.getRegistrationProof(_registrations, address(this), 0, params.signingId); try registry.slashRegistration(proof) { revert("should not be able to slash registration again"); } catch (bytes memory _reason) { @@ -334,7 +388,7 @@ contract ReentrantSlashableRegistrationContract is ReentrantContract { // expected re-registering to fail _registrations[0] = registrations[0]; require(_registrations.length == 1, "test harness supports only 1 registration"); - try registry.register{ value: collateral }(_registrations, address(this)) { + try registry.register{ value: collateral }(_registrations, address(this), params.signingId) { revert("should not be able to register"); } catch (bytes memory _reason) { errors += 1;