diff --git a/constraints-oapi.yml b/constraints-oapi.yml index 4a2b349..d9e42a8 100644 --- a/constraints-oapi.yml +++ b/constraints-oapi.yml @@ -133,7 +133,7 @@ A proposer can delegate constraint submission rights by signing a Delegate messa tags: - Constraints API summary: "Endpoint to retrieve the signed constraints for a given slot" - description: "For a given slot, the Relay should only return signed constraints that were signed by the L1 proposer or a gateway that was delegated to by the proposer. The request requires authorization via the `X-Receiver-Signature` header which is a BLS signature over the requested `slot` number and the `X-Receiver-PublicKey` header which is the corresponding BLS public key. If there are restrictions on accessing constraints, the Relay will check the signature against the BLS public keys in `ConstraintsMessage.Receivers[]`. + description: "For a given slot, the Relay should only return signed constraints that were signed by the L1 proposer or a gateway that was delegated to by the proposer. The request requires authorization via the `X-Receiver-Signature` header which is a BLS signature over the requested `slot` number and the `X-Receiver-PublicKey` header which is the corresponding BLS public key. The `X-Receiver-Nonce` header is the nonce used for the signature and the `X-Receiver-SigningId` header is the signing ID used for the signature as part of the [Commit-Boost Signing Spec](https://commit-boost.github.io/commit-boost-client/api/). If there are restrictions on accessing constraints, the Relay will check the signature against the BLS public keys in `ConstraintsMessage.Receivers[]`. " parameters: - name: slot @@ -151,6 +151,16 @@ A proposer can delegate constraint submission rights by signing a Delegate messa required: true schema: $ref: "./beacon-apis/types/primitive.yaml#/Pubkey" + - name: X-Receiver-Nonce + in: header + required: true + schema: + $ref: "./beacon-apis/types/primitive.yaml#/Uint64" + - name: X-Receiver-SigningId + in: header + required: true + schema: + $ref: "./beacon-apis/types/primitive.yaml#/Bytes32" responses: "200": description: "OK" @@ -283,10 +293,16 @@ components: properties: message: $ref: '#/components/schemas/Delegation' + nonce: + $ref: './beacon-apis/types/primitive.yaml#/Uint64' + signing_id: + $ref: './beacon-apis/types/primitive.yaml#/Bytes32' signature: $ref: './beacon-apis/types/primitive.yaml#/Signature' required: - message + - nonce + - signing_id - signature Delegation: type: object @@ -314,6 +330,10 @@ components: properties: message: $ref: '#/components/schemas/ConstraintsMessage' + nonce: + $ref: './beacon-apis/types/primitive.yaml#/Uint64' + signing_id: + $ref: './beacon-apis/types/primitive.yaml#/Bytes32' signature: $ref: "./beacon-apis/types/primitive.yaml#/Signature" required: diff --git a/specs/builder.md b/specs/builder.md index 2e530f5..5f8f0dd 100644 --- a/specs/builder.md +++ b/specs/builder.md @@ -56,8 +56,7 @@ Some nuances: | Name | Value | | - | - | | `DOMAIN_APPLICATION_BUILDER` | `DomainType('0x00000001')` | -| `DOMAIN_APPLICATION_GATEWAY` | TBD | -| `DELEGATION_DOMAIN_SEPARATOR` | `DomainType('0x0044656c')` | +| `SIGNING_DOMAIN` | `Bytes32('0x6d6d6f43719103511efa4f1362ff2a50996cccf329cc84cb410c5e5c7d351d03')` | #### Constraints parameters @@ -91,6 +90,8 @@ class ConstraintsMessage(Container): ```python class SignedConstraints(Container): message: ConstraintsMessage + nonce: uint64 + signing_id: Bytes32 signature: BLSSignature ``` @@ -165,10 +166,29 @@ def verify_builder_slot_signature(slot: uint64, builder_pubkey: BLSPubkey, signa ### `verify_constraint_signature` ```python -def verify_constraint_signature(signed_constraints: SignedConstraints) -> bool: - domain = compute_domain(DOMAIN_APPLICATION_GATEWAY) - signing_root = compute_signing_root(signed_constraints.message, domain) - return bls.Verify(signed_constraints.message.delegate, signing_root, signed_constraints.signature) + class PropCommitSigningInfo(Container): + data: Bytes32 + pub module_signing_id: Bytes32 + pub nonce: uint64 + pub chain_id: uint64 + + class SigningData(Container): + object_root: Bytes32 + signing_domain: Bytes32 + + def verify_constraint_signature( + signed_constraints: SignedConstraints, + signing_id: bytes32, + nonce: uint64, + chainid: uint64 + ) -> bool: + # note the object root is abi-encoded, not SSZ + object_root = keccak256(abi.encode(signed_constraints.message)) + + # the rest is SSZ-encoded + comm_info = PropCommitSigningInfo(object_root, signing_id, to_little_endian_bytes32(nonce), to_little_endian_bytes32(chain_id)) + signing_data = SigningData(comm_info.hash_tree_root(), SIGNING_DOMAIN) + return BLS.verify(signed_constraints.message.delegate, signing_data.hash_tree_root(), signed_constraints.signature) ``` ### `process_constraints` @@ -177,7 +197,8 @@ A `SignedConstraints` is considered valid if the following function completes wi ```python def process_constraints(state: BeaconState, signed_constraints: SignedConstraints, - delegations: Dict[BLSPubkey, Delegation]) -> bool: + delegations: Dict[BLSPubkey, Delegation], + chainid: uint64) -> bool: constraints = signed_constraints.message # Verify delegate has authority from proposer @@ -189,7 +210,7 @@ def process_constraints(state: BeaconState, assert len(constraints.constraints) <= MAX_CONSTRAINTS_PER_SLOT # Verify constraint signature - assert verify_constraint_signature(signed_constraints) + assert verify_constraint_signature(signed_constraints, signed_constraints.signing_id, signed_constraints.nonce, chainid) # Verify slot matches delegation assert constraints.slot == delegation.slot diff --git a/specs/constraints-api.md b/specs/constraints-api.md index c905203..1583dae 100644 --- a/specs/constraints-api.md +++ b/specs/constraints-api.md @@ -74,6 +74,8 @@ Endpoint for submitting a batch of constraints to the relay. The constraints are # A signed "bundle" of constraints. class SignedConstraints(Container): message: ConstraintsMessage + nonce: uint64 + signing_id: Bytes32 signature: BLSSignature # A "bundle" of constraints for a specific slot. @@ -108,8 +110,8 @@ Endpoint for submitting a batch of constraints to the relay. The constraints are Additional requirements: - - To ensure deterministic behavior for stateful constraints it is required for the ConstraintsMessage.Constraints[] to be processed in the order received. - - The ConstraintsMessage.Receivers[] field contains a list of public keys that are authorized to access these constraints. If this list is empty, the constraints are publicly accessible to anyone. + - To ensure deterministic behavior for stateful constraints it is required for the `ConstraintsMessage.Constraints[]` to be processed in the order received. + - The `ConstraintsMessage.Receivers[]` field contains a list of public keys that are authorized to access these constraints. If this list is empty, the constraints are publicly accessible to anyone. - **Example** @@ -141,6 +143,8 @@ Endpoint for a Proposer to delegate constraint submission rights to a Gateway. T # A signed delegation class SignedDelegation(Container): message: Delegation + nonce: uint64 + signing_id: Bytes32 signature: BLSSignature # A delegation from a proposer to a BLS public key @@ -216,6 +220,8 @@ Return the active delegations for the proposer of this slot, if they exist. "slot": "12345", "metadata": "0xe9587369b2301d0790347320302cc069b2301d0790347320302cc0943d5a1884560367e8208d920f2e9587369b2301de9587369b2301d0790347320302cc0" }, + "nonce": 1337, + "signing_id": "0x3078313233340000000000000000000000000000000000000000000000000000", "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" } ] @@ -230,7 +236,7 @@ Return the active delegations for the proposer of this slot, if they exist. ### Endpoint: `/constraints/v0/relay/constraints/{slot}` -Returns all signed constraints for a given slot, if they exist. The request requires authorization via the `X-Receiver-Signature` header which is a BLS signature over the requested `slot` number. If there are restrictions on accessing constraints, the Relay will check the signature against the BLS public keys in `ConstraintsMessage.Receivers[]`. +Returns all signed constraints for a given slot, if they exist. The request requires authorization via the `X-Receiver-Signature` header which is a BLS signature over the requested `slot` number. If there are restrictions on accessing constraints, the Relay will check the signature against the BLS public keys in `ConstraintsMessage.Receivers[]`. The `X-Receiver-Signature` will adhere to the [Commit-Boost Signing Spec](https://commit-boost.github.io/commit-boost-client/api/). - **Method:** `GET` - **Response:** `SignedConstraints[]` @@ -241,6 +247,8 @@ Returns all signed constraints for a given slot, if they exist. The request requ - `Content-Type: application/json` - `X-Receiver-Signature: ` - `X-Receiver-PublicKey: ` + - `X-Receiver-Nonce: ` + - `X-Receiver-SigningId: ` - **Example Response** ```json @@ -265,6 +273,8 @@ Returns all signed constraints for a given slot, if they exist. The request requ "0x84e47f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74b" ] }, + "nonce": 1337, + "signing_id": "0x3078313233340000000000000000000000000000000000000000000000000000", "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" } ] diff --git a/specs/fault-attribution.md b/specs/fault-attribution.md index 7e08e95..45d616f 100644 --- a/specs/fault-attribution.md +++ b/specs/fault-attribution.md @@ -70,14 +70,14 @@ Fault attribution is very dependent on the *completeness* of `SignedConstraints` The following modes serve as implementation guidance rather than prescriptive rules. From the perspective of the spec, these modes are indistinguishable—each simply represents a different strategy for leveraging the `ConstraintProofs` field. This is similar to how “optimistic relaying” emerged as an optimization within PBS: adopted in practice but not enshrined in the spec. ### Optimistic relaying -This mode favors optimizes for performance over safety. By omitting heavy proof generation and verification, more time is available to build the block, potentially increasing the value of the block. However, this introduces the possibility that an invalid block is relayed to the Proposer, in which case the Builder should be penalized. The Proposer should decide whether to enable optimistic relaying, i.e., in their `Delegation.metadata` field. +This mode optimizes for performance over safety. By omitting heavy proof generation and verification, more time is available to build the block, potentially increasing the value of the block. However, this introduces the possibility that an invalid block is relayed to the Proposer, in which case the Builder should be penalized. The Proposer should decide whether to enable optimistic relaying, i.e., in their `Delegation.metadata` field. 1. Gateway posts `SignedConstraints` to the Relay. 2. Builder constructs a block that satisfies all the constraints they received. In the `VersionedSubmitBlockRequestWithProofs.proofs` field, the Builder includes their signature over the `SignedContraints` object, effectively attesting that the block complies with the constraints they were given. 3. Relay includes the block in their auction if the Builder's signature is valid over the `SignedConstraints`. 4. Proposer receives the signed header and proposes the block. ### Pessimistic relaying -Conversely, this mode favors optimizes for safety over performance. Builders are required to generate proofs of constraint satisfaction which comes with the associated compute costs. It is advised that this is the default setting that needs to be overriden by Proposers, i.e., in their `Delegation.metadata` field. +Conversely, this mode optimizes for safety over performance. Builders are required to generate proofs of constraint satisfaction which comes with the associated compute costs. It is advised that this is the default setting that needs to be overriden by Proposers, i.e., in their `Delegation.metadata` field. 1. Gateway posts `SignedConstraints` to the Relay. 2. Builder constructs a block that satisfies all the constraints they received. The Builder includes proofs of constraint satisfaction in the `VersionedSubmitBlockRequestWithProofs.proofs` field. 3. Relay includes the block in their auction if the Builder's proofs are valid over the `SignedConstraints`. diff --git a/specs/gateway.md b/specs/gateway.md index eb3aae3..fe8c147 100644 --- a/specs/gateway.md +++ b/specs/gateway.md @@ -67,8 +67,7 @@ Some nuances: | Name | Value | | - | - | | `DOMAIN_APPLICATION_BUILDER` | `DomainType('0x00000001')` | -| `DOMAIN_APPLICATION_GATEWAY` | TBD | -| `DELEGATION_DOMAIN_SEPARATOR` | `DomainType('0x0044656c')` | +| `SIGNING_DOMAIN` | `Bytes32('0x6d6d6f43719103511efa4f1362ff2a50996cccf329cc84cb410c5e5c7d351d03')` | #### URC parameters @@ -99,6 +98,8 @@ class Delegation(Container): ```python class SignedDelegation(Container): message: Delegation + nonce: uint64 + signing_id: Bytes32 signature: BLS.G2Point ``` @@ -123,6 +124,8 @@ class ConstraintsMessage(Container): ```python class SignedConstraints(Container): message: ConstraintsMessage + nonce: uint64 + signing_id: Bytes32 signature: BLSSignature ``` @@ -167,12 +170,31 @@ def is_eligible_for_delegation(state: BeaconState, validator: Validator) -> bool #### verify_delegation_signature ```python -def verify_delegation_signature(signed_delegation: SignedDelegation) -> bool: - pubkey = signed_delegation.message.proposer - # note abi-encoded, not SSZ - message = abi.encode(signed_delegation.message) - signing_root = keccak256(abi.encode_packed(DELEGATION_DOMAIN_SEPARATOR, message)) - return bls.Verify(pubkey, signing_root, signed_delegation.signature) + class PropCommitSigningInfo(Container): + data: Bytes32 + pub module_signing_id: Bytes32 + pub nonce: uint64 + pub chainid: uint64 + + class SigningData(Container): + object_root: Bytes32 + signing_domain: Bytes32 + + def verify_delegation_signature( + signed_delegation: SignedDelegation, + signing_id: Bytes32, + nonce: uint64, + chainid: uint64 + ) -> bool: + pubkey = signed_delegation.message.proposer + + # note the object root is abi-encoded, not SSZ + object_root = keccak256(abi.encode(IRegistry.MessageType.Delegation, signed_delegation.message)) + + # the rest is SSZ-encoded + comm_info = PropCommitSigningInfo(object_root, signing_id, to_little_endian_Bytes32(nonce), to_little_endian_Bytes32(chainid)) + signing_data = SigningData(comm_info.hash_tree_root(), SIGNING_DOMAIN) + return BLS.verify(pubkey, signing_data.hash_tree_root(), signed_delegation.signature) ``` #### process_delegation @@ -182,7 +204,8 @@ A `delegation` is considered valid if the following function completes without r def process_delegation(state: BeaconState, signed_delegation: SignedDelegation, delegations: Dict[BLSPubkey, Delegation], - current_timestamp: uint64): + current_timestamp: uint64, + chainid: uint64): signature = signed_delegation.signature delegation = signed_delegation.message @@ -197,7 +220,7 @@ def process_delegation(state: BeaconState, assert is_eligible_for_delegation(state, validator) # Verify delegation signature - assert verify_delegation_signature(signed_delegation) + assert verify_delegation_signature(signed_delegation, signed_delegation.signing_id, signed_delegation.nonce, chainid) ``` ## Issuing commitments @@ -251,7 +274,10 @@ class get_signed_constraints( proposer: BLSPubkey, slot: int, receivers: List[BLSPubkey], - privkey: int) -> SignedConstraints: + privkey: int, + signing_id: Bytes32 + nonce: uint64. + chainid: uint64) -> SignedConstraints: message = ConstraintsMessage( proposer: proposer, @@ -260,12 +286,15 @@ class get_signed_constraints( constraints: constraints, receivers: receivers) - # note abi-encoded, not SSZ - message = abi.encode(message) - signing_root = keccak256(abi.encode_packed(DOMAIN_APPLICATION_GATEWAY, message)) - signature = bls.Sign(privkey, signing_root) + # note the object root is abi-encoded, not SSZ + object_root = keccak256(abi.encode(message)) + + # the rest is SSZ-encoded + comm_info = PropCommitSigningInfo(object_root, signing_id, to_little_endian_Bytes32(nonce), to_little_endian_Bytes32(chainid)) + signing_data = SigningData(comm_info.hash_tree_root(), SIGNING_DOMAIN) + signature = BLS.sign(privkey, signing_data.hash_tree_root()) - return SignedConstraints(message: message, signature: signature) + return SignedConstraints(message: message, nonce: nonce, signing_id: signing_id, signature: signature) ``` ### Disseminating constraints diff --git a/specs/proposer.md b/specs/proposer.md index 593dfe7..0ebc9c4 100644 --- a/specs/proposer.md +++ b/specs/proposer.md @@ -82,8 +82,7 @@ Note the following constants are subject to change prior to the launch of the UR | Name | Value | | - | - | -| `DELEGATION_DOMAIN_SEPARATOR` | `DomainType('0x0044656c')` | -| `REGISTRATION_DOMAIN_SEPARATOR` | `DomainType('0x00435255')` | +| `SIGNING_DOMAIN` | `Bytes32('0x6d6d6f43719103511efa4f1362ff2a50996cccf329cc84cb410c5e5c7d351d03')` | ### URC parameters @@ -107,13 +106,30 @@ The proposer will register to the URC by submitting `Registration` messages for 2. The proposer generates a signature with the BLS key they wish to register. ```python + class PropCommitSigningInfo(Container): + data: Bytes32 + pub module_signing_id: Bytes32 + pub nonce: uint64 + pub chainid: uint64 + + class SigningData(Container): + object_root: Bytes32 + signing_domain: Bytes32 + def get_urc_registration_signature( owner: Address, - privkey: int + privkey: int, + signing_id: Bytes32, + nonce: uint64, + chainid: uint64 ) -> BLSSignature: - # note: abi-encoded, not SSZ - message = abi.encode(owner) - return BLS.sign(message, privkey, REGISTRATION_DOMAIN_SEPARATOR) + # note the object root is abi-encoded, not SSZ + object_root = keccak256(abi.encode(IRegistry.MessageType.Registration, owner)) + + # the rest is SSZ-encoded + comm_info = PropCommitSigningInfo(object_root, signing_id, to_little_endian_Bytes32(nonce), to_little_endian_Bytes32(chainid)) + signing_data = SigningData(comm_info.hash_tree_root(), SIGNING_DOMAIN) + return BLS.sign(privkey, signing_data.hash_tree_root()) ``` 3. The `signature` is placed in a `SignedRegistration` object with the BLS public key. @@ -122,6 +138,7 @@ The proposer will register to the URC by submitting `Registration` messages for class SignedRegistration(Container): pubkey: BLS.G1Point # note the encoding matches URC not beacon specs signature: BLS.G2Point # note the encoding matches URC not beacon specs + nonce: uint64 ``` #### **Signing and submitting a registration** @@ -130,19 +147,20 @@ The proposer will repeat steps 2-3 for each BLS key they wish to register. Once ```python def get_signed_registration( sigs: List[BLSSignature], - pubkeys: List[BLSPubkey] + pubkeys: List[BLSPubkey], + nonces: List[uint64] ) -> List[SignedRegistration]: # encode to BLS.G1Point and BLS.G2Point return [ - SignedRegistration(to_g1_point(pk), to_g2_point(sig)) - for pk, sig in zip(sigs, pubkeys) + SignedRegistration(to_g1_point(pk), to_g2_point(sig), nonce) + for pk, sig, nonce in zip(pubkeys, sigs, nonces) ] ``` They will then submit them to the URC via the `register()` function. ```Solidity -function register(SignedRegistration[] registrations, address owner) - external payable returns (bytes32 registrationRoot) +function register(SignedRegistration[] registrations, address owner, Bytes32 signingId) + external payable returns (Bytes32 registrationRoot) ``` The proposer is required to send at least `MIN_COLLATERAL` Ether (as defined in the URC) when calling `register()`. @@ -187,20 +205,27 @@ It is not required but is assumed that the `delegate` and `committer` private ke ```Python def get_delegation_signature( delegation: Delegation, - privkey: int + privkey: int, + signing_id: Bytes32, + nonce: uint64, + chainid: uint64 ) -> SignedDelegation: - # note: abi-encoded, not SSZ - message = abi.encode(delegation) - signature = BLS.sign(message, privkey, DELEGATION_DOMAIN_SEPARATOR) - return SignedDelegation(message=delegation, signature=signature) + # note the object root is abi-encoded, not SSZ + object_root = keccak256(abi.encode(IRegistry.MessageType.Delegation, delegation)) + + # the rest is SSZ-encoded + comm_info = PropCommitSigningInfo(object_root, signing_id, to_little_endian_Bytes32(nonce), to_little_endian_Bytes32(chain_id)) + signing_data = SigningData(comm_info.hash_tree_root(), SIGNING_DOMAIN) + signature = BLS.sign(privkey, signing_data.hash_tree_root()) + return SignedDelegation(message=delegation, nonce=nonce, signing_id=signing_id, signature=signature) ``` - Note RLP encoding is used instead of SSZ for simpler on-chain verification. - 2. The `signature` is placed in a `SignedDelegation` object with the BLS public key. ```Python class SignedDelegation(Container): message: Delegation + nonce: uint64 + signing_id: Bytes32 signature: BLS.G2Point ``` @@ -228,7 +253,7 @@ The URC optionally allows an on-chain way for proposers to opt in to a proposer #### **Updating the URC** The proposer will call the `optInToSlasher()` function in the URC with the `Slasher` contract address, `committer` address, and the `RegistrationRoot` from the URC registration step. ```Solidity -function optInToSlasher(bytes32 registrationRoot, address slasher, address committer) external +function optInToSlasher(Bytes32 registrationRoot, address slasher, address committer) external ``` This function can only be called after the proposer has registered to the URC and the `FRAUD_PROOF_WINDOW` has elapsed. @@ -251,14 +276,14 @@ Unregistering from the URC is a two-step process: The proposer's `owner` address in the URC can call `unregister()` to initiate the deregistration process, saving the block timestamp that it was called. ```Solidity -function unregister(bytes32 registrationRoot) external; +function unregister(Bytes32 registrationRoot) external; ``` #### Calling `claimCollateral()` The `owner` address can call `claimCollateral()` to retrieve their collateral after `UNREGISTRATION_DELAY` seconds have elapsed. ```Solidity -function claimCollateral(bytes32 registrationRoot) external; +function claimCollateral(Bytes32 registrationRoot) external; ``` The proposer's collateral is transferred to their `owner` address. @@ -268,7 +293,7 @@ The proposer's collateral is transferred to their `owner` address. If the proposer was slashed, the `owner` address can call `claimSlashedCollateral()` after `SLASH_WINDOW` seconds have elapsed to retrieve their remaining collateral. ```Solidity -function claimSlashedCollateral(bytes32 registrationRoot) external; +function claimSlashedCollateral(Bytes32 registrationRoot) external; ``` The proposer's remaining collateral is transferred to the `owner` address. @@ -277,7 +302,7 @@ The proposer's remaining collateral is transferred to the `owner` address. If a proposer previously opted in to a slasher contract [as described above](#opting-in-to-slasher-contracts-on-chain-optional), they can opt out by calling `optOutOfSlasher()` after `OPT_IN_DELAY` seconds have elapsed. ```Solidity -function optOutOfSlasher(bytes32 registrationRoot, address slasher) external +function optOutOfSlasher(Bytes32 registrationRoot, address slasher) external ``` ## Slashing