From d6b806626a6805ccdf81e7990966f73124a7ee10 Mon Sep 17 00:00:00 2001 From: Alan Date: Wed, 12 Mar 2025 09:17:26 +0000 Subject: [PATCH 1/3] preconfirmation SIP --- sips/preconfimration.md | 171 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 sips/preconfimration.md diff --git a/sips/preconfimration.md b/sips/preconfimration.md new file mode 100644 index 0000000..ea336ad --- /dev/null +++ b/sips/preconfimration.md @@ -0,0 +1,171 @@ +| Author | Title | Category | Status | Date | +|-------|----------------|--------------------------------------|---------|--------------------| +| Alan Li | Preconfirmation Support | Core | Open-for-discussion | 2025-03-07 | + +## Summary +This SIP describes new preconfirmation duty for SSV operators, requiring them to sign, aggregate, and submit preconfirmation requests to a preconfirmation protocol sidecar. This enhancement operates under the commit-boost framework and aims to support different preconfirmation protocol implementations. + +## Rationale +This proposal aligns with Ethereum’s roadmap toward better proposer-builder separation (PBS) while maintaining decentralization. Instead of relying on centralized services for preconfirmation, SSV operators can collectively perform this duty in a trust-minimized manner. + +For validators on SSV network, supporting preconfirmation also potentially increase validators' profits. + +## Background +There are two types of preconfirmation protocol design: +1. A sidecar forwards preconf requests to the validator, the validator signs the requests and send back to the sidecar, which handles communication with relays for building blocks with the signed constraints. ETHGas is an example protocol for this type. +2. The validator delegates preconf request signing to relays, the relays handle preconf signing and block building, finally return blocks that satisfy constraints through the preconf sidecar to the validators for submission. Primev is an example protocol for this type. + +For design type 2, SSV network doesn't have to change anything because everything is delegated to the relays. This proposal aims to support protocols with design type 1. + +## Assumptions +1. Validators only register to preconf services that they trust, spam requests are not in the scope of this SIP. +2. When a valid preconf request submits to a validator, all of its operators' commit boost instances will receive the request. +3. The preconf sidecar handles request validation. Only valid preconf requests creates `PreconfCommitment` duty. For example, the request slot is valid, and we have not made too many commitments, etc. +4. For now we assume all operators have the same commit boost module installed (i.e. run the same preconf protocol). +5. The relays handles block building. It returns valid block based on the signed preconf requests. Every operator talks to the same relays. +6. We trust the blinded blocks proposed by the leader of the proposer duty. + +## Specification +### Commit-Boost API +The commit-boost API allows validators to sign and submit preconf request root, and get constrained blocks from relays. + +```go +type CommitBoostAPI interface { + GetNewRequest() (requestRoot phase0.Root, error) + SubmitCommitment(requestRoot phase0.Root, signature phase0.BLSSignature) error + // GetBlockHeader is for proposer duty + GetBlockHeader() (ssz.Marshaler, spec.DataVersion, error) +} +``` + +`GetNewRequest()` is expected to return a new **valid** preconf request root each call, it returns an error if there is no new preconf request. + +`SubmitCommitment()` sends a signed preconf request root under the [commit boost domain](https://github.com/Commit-Boost/commit-boost-client/blob/c5a16eec53b7e6ce0ee5c18295565f1a0aa6e389/crates/common/src/constants.rs#L3) to the preconf protocol sidecar, which is responsible for communicating with relays and users. + +(signing root and request root are different). + +`GetBlockHeader()` allows the proposer to get a block header that satisfies the signed preconf requests the from relays. This function assumes to only return a valid block header as the preconf protocol sidecar can verify the block using Merkle proofs of transaction inclusion given the block header and the proof provided by the relays. + +### Preconfirmation Duty Runner +This duty handles preconfirmation requests sent to validators. The preconf runner has access to the preconf sidecar (commit boost module). + +```go +type PreconfRunner struct { + BaseRunner *BaseRunner + + beacon BeaconNode + preconf PreconfSidecar + network Network + signer types.BeaconSigner + operatorSigner *types.OperatorSigner + valCheck qbft.ProposedValueCheckF + + requestRoot phase0.Root +} +``` + +The duty starts when a preconf request is received at the operators' local commit boost module. All operators are expected to receive the same requests from the commit boost module at the same time. + +```go +func (r *PreconfRunner) executeDuty(duty types.Duty) error { + request, err := r.GetPreconfSidecar().GetNewRequest() + if err != nil { + return errors.Wrap(err, "failed to get preconf request") + } + + r.requestRoot = request.Root + + msg, err := r.BaseRunner.signBeaconObject(r, r.BaseRunner.State.StartingDuty.(*types.ValidatorDuty), &request, + duty.DutySlot(), + types.DomainCommitBoost) + if err != nil { + return errors.Wrap(err, "failed signing attestation data") + } + ... +} +``` + +The request root is stored by the runner as the commit boost API request the root when submitting the signed request. + +The signing root (request root + commit boost domain) is broadcasted to other operators. + +When receiving other partial signatures, the runner submits the request root and the signature (over the signing root) to the commit boost API. + +```go +func (r *PreconfRunner) ProcessPreConsensus(signedMsg *types.PartialSignatureMessages) error { + quorum, roots, err := r.BaseRunner.basePreConsensusMsgProcessing(r, signedMsg) + if err != nil { + return errors.Wrap(err, "failed processing preconfirmation message") + } + + if !quorum { + return nil + } + + root := roots[0] + fullSig, err := r.GetState().ReconstructBeaconSig(r.GetState().PreConsensusContainer, root, r.GetShare().ValidatorPubKey[:], r.GetShare().ValidatorIndex) + if err != nil { + // If the reconstructed signature verification failed, fall back to verifying each partial signature + r.BaseRunner.FallBackAndVerifyEachSignature(r.GetState().PreConsensusContainer, root, r.GetShare().Committee, + r.GetShare().ValidatorIndex) + return errors.Wrap(err, "got pre-consensus quorum but it has invalid signatures") + } + specSig := phase0.BLSSignature{} + copy(specSig[:], fullSig) + + if err := r.GetPreconfSidecar().SubmitCommitment(r.requestRoot, specSig); err != nil { + return errors.Wrap(err, "could not submit to commitment to sidecar") + } + + r.GetState().Finished = true + r.requestRoot = phase0.Root{} + return nil +} +``` + +### Validator Commit Boost: Manager for Preconfirmation Duties +Since the current `Validator` duty manager is designed to have only one runner per duty, it is not aligned with the multiple duties requirement for preconf. Therefore, a new `ValidatorCommitBoost` duty manager is created to manage preconfirmation duty runners. + +```go +type PreconfRunners map[phase0.Root]Runner + +type ValidatorCommitBoost struct { + PreconfRunners PreconfRunners + Network Network + CommitteeMember *types.CommitteeMember + Share *types.Share + Signer types.BeaconSigner + OperatorSigner *types.OperatorSigner +} +``` + +Unlike the `Validator` duty manager that routes duty to the an existing runner, the `ValidatorCommitBoost` duty manager handles preconf runner creation on receiving new preconf request. + +```go +type PreconfDuty struct { + RequestRoot phase0.Root `ssz-size:"32"` + Slot phase0.Slot `ssz-size:"8"` +} + +func (v *ValidatorCommitBoost) StartDuty(duty types.PreconfDuty) error { + _, exist := v.PreconfRunners[duty.RequestRoot] + if exist { + return errors.Errorf("duty runner for request %s already exists", duty.RequestRoot.String()) + } + shareMap := make(map[phase0.ValidatorIndex]*types.Share) + shareMap[v.Share.ValidatorIndex] = v.Share + dutyRunner, err := NewPreconfRunner(v.BeaconNetwork, shareMap, v.Beacon, v.PreconfSidecar, v.Network, v.Signer, v.OperatorSigner) + if err != nil { + return errors.Wrap(err, "failed to create new preconf runner") + } + v.PreconfRunners[duty.RequestRoot] = dutyRunner + return dutyRunner.StartNewDuty(duty, v.CommitteeMember.GetQuorum()) +} + +``` + +## Open Questions +- Should we enforce some rate limit on the preconf requests? +- How to enforce and check that all operators receive the same requests? +- Do we need to make sure/how to make sure they use the same relays? +- Can we get proofs for block headers from commit boost API? Scenario: a proposer leader proposer a bad block (e.g. because it uses a different relay) \ No newline at end of file From f35e9c5de1aefd49866d4d10ab6a54e10fb003af Mon Sep 17 00:00:00 2001 From: Alan Date: Thu, 20 Mar 2025 10:24:54 +0000 Subject: [PATCH 2/3] preconf -> CB signing --- sips/commit_boost_api.md | 192 +++++++++++++++++++++++++++++++++++++++ sips/preconfimration.md | 171 ---------------------------------- 2 files changed, 192 insertions(+), 171 deletions(-) create mode 100644 sips/commit_boost_api.md delete mode 100644 sips/preconfimration.md diff --git a/sips/commit_boost_api.md b/sips/commit_boost_api.md new file mode 100644 index 0000000..0fb5ca1 --- /dev/null +++ b/sips/commit_boost_api.md @@ -0,0 +1,192 @@ +| Author | Title | Category | Status | Date | +|-------|----------------|--------------------------------------|---------|--------------------| +| Alan Li | Commit-Boost Signing Support | Core | Open-for-discussion | 2025-03-07 | + +## Summary +This SIP describes the new commit-boost signing duty for SSV operators, requiring them to implement the commit-boost API, accepting requests to sign on some signing roots. This enhancement operates under the commit-boost framework and aims to support new features such as preconfirmation. + +## Rationale +Commit-boost is becoming a standardized framework for Ethereum validators to interact with off-chain protocols such as preconfirmation protocols to increase profits for validators. + +By enabling commit-Boost API, this proposal also aligns with Ethereum’s roadmap toward better proposer-builder separation (PBS) while maintaining decentralization. + +## Security Assumptions +1. When a signing request submits to a validator, all of its operators will receive the request. That is, they have the same commit boost module installed and the module forwards requests to all operators in a cluster. +2. Request sent to operators are assumed to be validated by external protocols. For example, for a preconfirmation request, the preconfirmation protocol makes sure that the request slot is valid, and we have not made too many commitments, etc. + +## Commit-Boost API +The [commit-boost API](https://commit-boost.github.io/commit-boost-client/api) is required to be implemented for validators to return the validator public key, receive signing request, and generate proxy key. + +```go +type CommitBoostAPI interface { + HandleGetPubkeys() ([]byte, error) + HandleRequestSignature(keyType string, pubkey phase0.BLSPubKey, objectRoot phase0.Root) (phase0.BLSSignature, error) + // GetBlockHeader is for proposer duty + HandleGenerateProxyKey(pubkey phase0.BLSPubKey, scheme string) ([]byte, error) +} +``` + +`HandleGetPubkeys()` returns the validator's public key. + +`HandleRequestSignature()` returns the signed object root under the [commit boost domain](https://github.com/Commit-Boost/commit-boost-client/blob/c5a16eec53b7e6ce0ee5c18295565f1a0aa6e389/crates/common/src/constants.rs#L3). + +`HandleGenerateProxyKey()` returns a proxy key for a given proxy pubkey. + +This SIP considers only `HandleRequestSignature()`, which introduces a new commit boost signing duty. + +## Specification +### Commit-Boost Signing Duty Runner +This duty handles commit boost signing requests sent from the commit-boost API. + +```go +type CBSigningRunner struct { + BaseRunner *BaseRunner + + beacon BeaconNode + network Network + signer types.BeaconSigner + operatorSigner *types.OperatorSigner + + requestRoot phase0.Root +} +``` + +The duty starts when a signing request is received at the operators' local commit boost module. All operators are expected to receive the same requests from the commit boost module at the same time. + +```go +func (r *PreconfRunner) executeDuty(duty types.CBSingingDuty) error { + r.requestRoot = duty.request.Root + + msg, err := r.BaseRunner.signBeaconObject(r, r.BaseRunner.State.StartingDuty.(*types.ValidatorDuty), &r.requestRoot, + duty.DutySlot(), + types.DomainCommitBoost) + if err != nil { + return errors.Wrap(err, "failed signing object root") + } + + preConsensusMsg := &types.PartialSignatureMessages{ + Type: types.PreconfPartialSig, + Slot: duty.DutySlot(), + Messages: []*types.PartialSignatureMessage{msg}, + } + + CBPreConsensusMsg := &types.CBPartialSignature{ + RequestRoot: r.requestRoot, + PartialSig: *preConsensusMsg, + } + + msgID := types.NewMsgID(r.GetShare().DomainType, r.GetShare().ValidatorPubKey[:], r.BaseRunner.RunnerRoleType) + + encodedMsg, err := CBPreConsensusMsg.Encode() + if err != nil { + return err + } + + ssvMsg := &types.SSVMessage{ + MsgType: types.SSVPartialSignatureMsgType, + MsgID: msgID, + Data: encodedMsg, + } + + sig, err := r.operatorSigner.SignSSVMessage(ssvMsg) + if err != nil { + return errors.Wrap(err, "could not sign SSVMessage") + } + + msgToBroadcast := &types.SignedSSVMessage{ + Signatures: [][]byte{sig}, + OperatorIDs: []types.OperatorID{r.operatorSigner.GetOperatorID()}, + SSVMessage: ssvMsg, + } + + if err := r.GetNetwork().Broadcast(msgToBroadcast.SSVMessage.GetID(), msgToBroadcast); err != nil { + return errors.Wrap(err, "can't broadcast partial post consensus sig") + } + return nil +} +``` + +The request root is stored by the runner as the commit boost API request the root when submitting the signed request. + +As shown above, when the signing root (request root + commit boost domain) is broadcasted to other operators, the request root is also included in the message, in order to route the message to the correct runner. + +The runner uses the pre consensus phase to aggregate and return signed request. +```go +func (r *PreconfRunner) ProcessPreConsensus(signedMsg *types.PartialSignatureMessages) error { + quorum, roots, err := r.BaseRunner.basePreConsensusMsgProcessing(r, signedMsg) + if err != nil { + return errors.Wrap(err, "failed processing preconfirmation message") + } + + if !quorum { + return nil + } + + root := roots[0] + fullSig, err := r.GetState().ReconstructBeaconSig(r.GetState().PreConsensusContainer, root, r.GetShare().ValidatorPubKey[:], r.GetShare().ValidatorIndex) + if err != nil { + // If the reconstructed signature verification failed, fall back to verifying each partial signature + r.BaseRunner.FallBackAndVerifyEachSignature(r.GetState().PreConsensusContainer, root, r.GetShare().Committee, + r.GetShare().ValidatorIndex) + return errors.Wrap(err, "got pre-consensus quorum but it has invalid signatures") + } + specSig := phase0.BLSSignature{} + copy(specSig[:], fullSig) + + if err := r.ReturnCommitment(specSig); err != nil { + return errors.Wrap(err, "could not answer signing request") + } + + r.GetState().Finished = true + r.requestRoot = phase0.Root{} + return nil +} +``` + +### Validator Commit Boost: Manager for Preconfirmation Duties +Since the current `Validator` duty manager is designed to have only one runner per duty, it is not aligned with the multiple duties requirement for commit-boost signing. Therefore, a new `ValidatorCommitBoost` duty manager is created to manage commit-boost signing duty runners. + +```go +type CBSigningRunners map[phase0.Root]Runner + +type ValidatorCommitBoost struct { + CBSigningRunners CBSigningRunners + Network Network + CommitteeMember *types.CommitteeMember + Share *types.Share + Signer types.BeaconSigner + OperatorSigner *types.OperatorSigner +} +``` + +Unlike the `Validator` duty manager that routes duty to the an existing runner, the `ValidatorCommitBoost` duty manager creates a new commit-boost signing runner on receiving new signing request. + +```go +type PreconfDuty struct { + RequestRoot phase0.Root `ssz-size:"32"` + Slot phase0.Slot `ssz-size:"8"` +} + +func (v *ValidatorCommitBoost) StartDuty(duty types.PreconfDuty) error { + _, exist := v.PreconfRunners[duty.RequestRoot] + if exist { + return errors.Errorf("duty runner for request %s already exists", duty.RequestRoot.String()) + } + shareMap := make(map[phase0.ValidatorIndex]*types.Share) + shareMap[v.Share.ValidatorIndex] = v.Share + dutyRunner, err := NewCBSigningRunner(v.BeaconNetwork, shareMap, v.Beacon, v.Network, v.Signer, v.OperatorSigner) + if err != nil { + return errors.Wrap(err, "failed to create new commit-boost signing runner") + } + v.CBSigningRunners[duty.RequestRoot] = dutyRunner + return dutyRunner.StartNewDuty(duty, v.CommitteeMember.GetQuorum()) +} +``` + +### Hard Rate Limit Constraint for Signature Requests +As a protection against spam requests, the number of commitments a validator can accept is capped at 1715 per slot. + +This is due to the consideration that signature requests are most likely preconfirmation request for transaction inclusion, where one signing request corresponds to at least one transaction to include in the proposer duty. Given 36M gas limit per block and 21K minimum gas per transaction, 1715 is the upper bound number of signature requests a validator should receive. Note that in reality a validator should receive much less requests than this limit. + +## Open Questions +- Should we enforce some rate limit on the signing requests? \ No newline at end of file diff --git a/sips/preconfimration.md b/sips/preconfimration.md deleted file mode 100644 index ea336ad..0000000 --- a/sips/preconfimration.md +++ /dev/null @@ -1,171 +0,0 @@ -| Author | Title | Category | Status | Date | -|-------|----------------|--------------------------------------|---------|--------------------| -| Alan Li | Preconfirmation Support | Core | Open-for-discussion | 2025-03-07 | - -## Summary -This SIP describes new preconfirmation duty for SSV operators, requiring them to sign, aggregate, and submit preconfirmation requests to a preconfirmation protocol sidecar. This enhancement operates under the commit-boost framework and aims to support different preconfirmation protocol implementations. - -## Rationale -This proposal aligns with Ethereum’s roadmap toward better proposer-builder separation (PBS) while maintaining decentralization. Instead of relying on centralized services for preconfirmation, SSV operators can collectively perform this duty in a trust-minimized manner. - -For validators on SSV network, supporting preconfirmation also potentially increase validators' profits. - -## Background -There are two types of preconfirmation protocol design: -1. A sidecar forwards preconf requests to the validator, the validator signs the requests and send back to the sidecar, which handles communication with relays for building blocks with the signed constraints. ETHGas is an example protocol for this type. -2. The validator delegates preconf request signing to relays, the relays handle preconf signing and block building, finally return blocks that satisfy constraints through the preconf sidecar to the validators for submission. Primev is an example protocol for this type. - -For design type 2, SSV network doesn't have to change anything because everything is delegated to the relays. This proposal aims to support protocols with design type 1. - -## Assumptions -1. Validators only register to preconf services that they trust, spam requests are not in the scope of this SIP. -2. When a valid preconf request submits to a validator, all of its operators' commit boost instances will receive the request. -3. The preconf sidecar handles request validation. Only valid preconf requests creates `PreconfCommitment` duty. For example, the request slot is valid, and we have not made too many commitments, etc. -4. For now we assume all operators have the same commit boost module installed (i.e. run the same preconf protocol). -5. The relays handles block building. It returns valid block based on the signed preconf requests. Every operator talks to the same relays. -6. We trust the blinded blocks proposed by the leader of the proposer duty. - -## Specification -### Commit-Boost API -The commit-boost API allows validators to sign and submit preconf request root, and get constrained blocks from relays. - -```go -type CommitBoostAPI interface { - GetNewRequest() (requestRoot phase0.Root, error) - SubmitCommitment(requestRoot phase0.Root, signature phase0.BLSSignature) error - // GetBlockHeader is for proposer duty - GetBlockHeader() (ssz.Marshaler, spec.DataVersion, error) -} -``` - -`GetNewRequest()` is expected to return a new **valid** preconf request root each call, it returns an error if there is no new preconf request. - -`SubmitCommitment()` sends a signed preconf request root under the [commit boost domain](https://github.com/Commit-Boost/commit-boost-client/blob/c5a16eec53b7e6ce0ee5c18295565f1a0aa6e389/crates/common/src/constants.rs#L3) to the preconf protocol sidecar, which is responsible for communicating with relays and users. - -(signing root and request root are different). - -`GetBlockHeader()` allows the proposer to get a block header that satisfies the signed preconf requests the from relays. This function assumes to only return a valid block header as the preconf protocol sidecar can verify the block using Merkle proofs of transaction inclusion given the block header and the proof provided by the relays. - -### Preconfirmation Duty Runner -This duty handles preconfirmation requests sent to validators. The preconf runner has access to the preconf sidecar (commit boost module). - -```go -type PreconfRunner struct { - BaseRunner *BaseRunner - - beacon BeaconNode - preconf PreconfSidecar - network Network - signer types.BeaconSigner - operatorSigner *types.OperatorSigner - valCheck qbft.ProposedValueCheckF - - requestRoot phase0.Root -} -``` - -The duty starts when a preconf request is received at the operators' local commit boost module. All operators are expected to receive the same requests from the commit boost module at the same time. - -```go -func (r *PreconfRunner) executeDuty(duty types.Duty) error { - request, err := r.GetPreconfSidecar().GetNewRequest() - if err != nil { - return errors.Wrap(err, "failed to get preconf request") - } - - r.requestRoot = request.Root - - msg, err := r.BaseRunner.signBeaconObject(r, r.BaseRunner.State.StartingDuty.(*types.ValidatorDuty), &request, - duty.DutySlot(), - types.DomainCommitBoost) - if err != nil { - return errors.Wrap(err, "failed signing attestation data") - } - ... -} -``` - -The request root is stored by the runner as the commit boost API request the root when submitting the signed request. - -The signing root (request root + commit boost domain) is broadcasted to other operators. - -When receiving other partial signatures, the runner submits the request root and the signature (over the signing root) to the commit boost API. - -```go -func (r *PreconfRunner) ProcessPreConsensus(signedMsg *types.PartialSignatureMessages) error { - quorum, roots, err := r.BaseRunner.basePreConsensusMsgProcessing(r, signedMsg) - if err != nil { - return errors.Wrap(err, "failed processing preconfirmation message") - } - - if !quorum { - return nil - } - - root := roots[0] - fullSig, err := r.GetState().ReconstructBeaconSig(r.GetState().PreConsensusContainer, root, r.GetShare().ValidatorPubKey[:], r.GetShare().ValidatorIndex) - if err != nil { - // If the reconstructed signature verification failed, fall back to verifying each partial signature - r.BaseRunner.FallBackAndVerifyEachSignature(r.GetState().PreConsensusContainer, root, r.GetShare().Committee, - r.GetShare().ValidatorIndex) - return errors.Wrap(err, "got pre-consensus quorum but it has invalid signatures") - } - specSig := phase0.BLSSignature{} - copy(specSig[:], fullSig) - - if err := r.GetPreconfSidecar().SubmitCommitment(r.requestRoot, specSig); err != nil { - return errors.Wrap(err, "could not submit to commitment to sidecar") - } - - r.GetState().Finished = true - r.requestRoot = phase0.Root{} - return nil -} -``` - -### Validator Commit Boost: Manager for Preconfirmation Duties -Since the current `Validator` duty manager is designed to have only one runner per duty, it is not aligned with the multiple duties requirement for preconf. Therefore, a new `ValidatorCommitBoost` duty manager is created to manage preconfirmation duty runners. - -```go -type PreconfRunners map[phase0.Root]Runner - -type ValidatorCommitBoost struct { - PreconfRunners PreconfRunners - Network Network - CommitteeMember *types.CommitteeMember - Share *types.Share - Signer types.BeaconSigner - OperatorSigner *types.OperatorSigner -} -``` - -Unlike the `Validator` duty manager that routes duty to the an existing runner, the `ValidatorCommitBoost` duty manager handles preconf runner creation on receiving new preconf request. - -```go -type PreconfDuty struct { - RequestRoot phase0.Root `ssz-size:"32"` - Slot phase0.Slot `ssz-size:"8"` -} - -func (v *ValidatorCommitBoost) StartDuty(duty types.PreconfDuty) error { - _, exist := v.PreconfRunners[duty.RequestRoot] - if exist { - return errors.Errorf("duty runner for request %s already exists", duty.RequestRoot.String()) - } - shareMap := make(map[phase0.ValidatorIndex]*types.Share) - shareMap[v.Share.ValidatorIndex] = v.Share - dutyRunner, err := NewPreconfRunner(v.BeaconNetwork, shareMap, v.Beacon, v.PreconfSidecar, v.Network, v.Signer, v.OperatorSigner) - if err != nil { - return errors.Wrap(err, "failed to create new preconf runner") - } - v.PreconfRunners[duty.RequestRoot] = dutyRunner - return dutyRunner.StartNewDuty(duty, v.CommitteeMember.GetQuorum()) -} - -``` - -## Open Questions -- Should we enforce some rate limit on the preconf requests? -- How to enforce and check that all operators receive the same requests? -- Do we need to make sure/how to make sure they use the same relays? -- Can we get proofs for block headers from commit boost API? Scenario: a proposer leader proposer a bad block (e.g. because it uses a different relay) \ No newline at end of file From 22f365a32760e49cc99a9ce7d82756f9b9bf638d Mon Sep 17 00:00:00 2001 From: Alan Date: Wed, 26 Mar 2025 11:04:37 +0000 Subject: [PATCH 3/3] add new message type --- sips/commit_boost_api.md | 107 +++++++++++++++++++++++++++++---------- 1 file changed, 80 insertions(+), 27 deletions(-) diff --git a/sips/commit_boost_api.md b/sips/commit_boost_api.md index 0fb5ca1..57b8ab5 100644 --- a/sips/commit_boost_api.md +++ b/sips/commit_boost_api.md @@ -21,7 +21,6 @@ The [commit-boost API](https://commit-boost.github.io/commit-boost-client/api) i type CommitBoostAPI interface { HandleGetPubkeys() ([]byte, error) HandleRequestSignature(keyType string, pubkey phase0.BLSPubKey, objectRoot phase0.Root) (phase0.BLSSignature, error) - // GetBlockHeader is for proposer duty HandleGenerateProxyKey(pubkey phase0.BLSPubKey, scheme string) ([]byte, error) } ``` @@ -32,9 +31,23 @@ type CommitBoostAPI interface { `HandleGenerateProxyKey()` returns a proxy key for a given proxy pubkey. -This SIP considers only `HandleRequestSignature()`, which introduces a new commit boost signing duty. +This SIP considers only `HandleRequestSignature()`, which introduces a new commit boost signing duty. `HandleGetPubkeys()` simply returns the public key of the validator, and `HandleGenerateProxyKey()` will be rejected. ## Specification +### CommitBoostPartialSignatureMsgType MsgType +The existing `msgID` used for route messages for other runners does not work for commit-boost signing runner, because the `msgID` assumes there is only one duty per slot per validator, which is not the case for commit-boost signing requests. + +Due to this reason, a new message type `CommitBoostPartialSignatureMsgType` is introduced on the wire to identify `CBPartialSignatures` for commit-boost signing duty runners. +```go +type CBPartialSignatures struct { + RequestRoot phase0.Root `ssz-size:"32"` + PartialSig PartialSignatureMessages +} +``` + +The message contains a `PartialSignatureMessages` as other runners use, in addition, it contains the request root routing the message to the correct commit-boost signing duty runner. + + ### Commit-Boost Signing Duty Runner This duty handles commit boost signing requests sent from the commit-boost API. @@ -46,31 +59,43 @@ type CBSigningRunner struct { network Network signer types.BeaconSigner operatorSigner *types.OperatorSigner + valCheck qbft.ProposedValueCheckF requestRoot phase0.Root + requestSig chan phase0.BLSSignature } ``` -The duty starts when a signing request is received at the operators' local commit boost module. All operators are expected to receive the same requests from the commit boost module at the same time. +When a runner is initialized, the `requestRoot` is set. One runner is only created to process one signing request. Once the runner finishes the duty and has the complete signature, `requestSig` will be filled. ```go -func (r *PreconfRunner) executeDuty(duty types.CBSingingDuty) error { - r.requestRoot = duty.request.Root +func (r *CBSigningRunner) executeDuty(duty types.Duty) error { + cbSigningDuty := types.CBSigningDuty{} + if cb, ok := duty.(*types.CBSigningDuty); ok { + cbSigningDuty = *cb + } else if cb, ok := duty.(types.CBSigningDuty); ok { + cbSigningDuty = cb + } else { + return errors.New("duty is not a CBSigningDuty") + } + request := cbSigningDuty.Request - msg, err := r.BaseRunner.signBeaconObject(r, r.BaseRunner.State.StartingDuty.(*types.ValidatorDuty), &r.requestRoot, + r.requestRoot = request.Root + + msg, err := r.BaseRunner.signBeaconObject(r, &cbSigningDuty.Duty, &request, duty.DutySlot(), types.DomainCommitBoost) if err != nil { - return errors.Wrap(err, "failed signing object root") + return errors.Wrap(err, "failed signing attestation data") } - + preConsensusMsg := &types.PartialSignatureMessages{ - Type: types.PreconfPartialSig, + Type: types.CBSigningPartialSig, Slot: duty.DutySlot(), Messages: []*types.PartialSignatureMessage{msg}, } - CBPreConsensusMsg := &types.CBPartialSignature{ + CBPreConsensusMsg := &types.CBPartialSignatures{ RequestRoot: r.requestRoot, PartialSig: *preConsensusMsg, } @@ -83,7 +108,7 @@ func (r *PreconfRunner) executeDuty(duty types.CBSingingDuty) error { } ssvMsg := &types.SSVMessage{ - MsgType: types.SSVPartialSignatureMsgType, + MsgType: types.CommitBoostPartialSignatureMsgType, MsgID: msgID, Data: encodedMsg, } @@ -106,16 +131,14 @@ func (r *PreconfRunner) executeDuty(duty types.CBSingingDuty) error { } ``` -The request root is stored by the runner as the commit boost API request the root when submitting the signed request. - As shown above, when the signing root (request root + commit boost domain) is broadcasted to other operators, the request root is also included in the message, in order to route the message to the correct runner. The runner uses the pre consensus phase to aggregate and return signed request. ```go -func (r *PreconfRunner) ProcessPreConsensus(signedMsg *types.PartialSignatureMessages) error { +func (r *CBSigningRunner) ProcessPreConsensus(signedMsg *types.PartialSignatureMessages) error { quorum, roots, err := r.BaseRunner.basePreConsensusMsgProcessing(r, signedMsg) if err != nil { - return errors.Wrap(err, "failed processing preconfirmation message") + return errors.Wrap(err, "failed processing commit-boost signing message") } if !quorum { @@ -133,12 +156,9 @@ func (r *PreconfRunner) ProcessPreConsensus(signedMsg *types.PartialSignatureMes specSig := phase0.BLSSignature{} copy(specSig[:], fullSig) - if err := r.ReturnCommitment(specSig); err != nil { - return errors.Wrap(err, "could not answer signing request") - } + r.requestSig <- specSig r.GetState().Finished = true - r.requestRoot = phase0.Root{} return nil } ``` @@ -159,18 +179,51 @@ type ValidatorCommitBoost struct { } ``` -Unlike the `Validator` duty manager that routes duty to the an existing runner, the `ValidatorCommitBoost` duty manager creates a new commit-boost signing runner on receiving new signing request. +The duty starts when a signing request is received at the operators' local commit boost module. All operators are expected to receive the same requests from the commit boost module at the same time. ```go -type PreconfDuty struct { - RequestRoot phase0.Root `ssz-size:"32"` - Slot phase0.Slot `ssz-size:"8"` +func (v *ValidatorCommitBoost) HandleRequestSignature(keyType string, pubkey types.ValidatorPK, objectRoot phase0.Root) (phase0.BLSSignature, error) { + // Proxy key is not supported currently + if keyType != "consensus" { + return phase0.BLSSignature{}, errors.New("invalid key type") + } + + if pubkey != v.Share.ValidatorPubKey { + return phase0.BLSSignature{}, errors.New("invalid pubkey") + } + + var signingDuty = types.CBSigningDuty{ + Request: types.CBSigningRequest{ + Root: objectRoot, + }, + Duty: types.ValidatorDuty{ + Slot: v.BeaconNetwork.EstimatedCurrentSlot(), + ValidatorIndex: v.Share.ValidatorIndex, + }, + } + + err := v.StartDuty(signingDuty) + if err != nil { + return phase0.BLSSignature{}, errors.Wrap(err, "failed to start duty") + } + + dutyRunner, exist := v.CBSigningRunners[objectRoot] + if !exist { + return phase0.BLSSignature{}, errors.Errorf("could not get duty runner for request %s", objectRoot.String()) + } + sig := dutyRunner.GetSignature() + + return sig, nil } +``` + +Unlike the `Validator` duty manager that routes duty to the an existing runner, the `ValidatorCommitBoost` duty manager creates a new commit-boost signing runner on receiving new signing request. -func (v *ValidatorCommitBoost) StartDuty(duty types.PreconfDuty) error { - _, exist := v.PreconfRunners[duty.RequestRoot] +```go +func (v *ValidatorCommitBoost) StartDuty(duty types.CBSigningDuty) error { + _, exist := v.CBSigningRunners[duty.Request.Root] if exist { - return errors.Errorf("duty runner for request %s already exists", duty.RequestRoot.String()) + return errors.Errorf("duty runner for request %s already exists", duty.Request.Root.String()) } shareMap := make(map[phase0.ValidatorIndex]*types.Share) shareMap[v.Share.ValidatorIndex] = v.Share @@ -178,7 +231,7 @@ func (v *ValidatorCommitBoost) StartDuty(duty types.PreconfDuty) error { if err != nil { return errors.Wrap(err, "failed to create new commit-boost signing runner") } - v.CBSigningRunners[duty.RequestRoot] = dutyRunner + v.CBSigningRunners[duty.Request.Root] = dutyRunner return dutyRunner.StartNewDuty(duty, v.CommitteeMember.GetQuorum()) } ```