Context
SIP-94 §6, in its current revision on ssvlabs/SIPs#94, pins how the self-build envelope reaches the cluster:
- An operator is the builder operator iff the block in its own
produceBlockV4 response has the §4-decided block root (the same local test §4 uses for the Eth-Builder-Url echo). It stays correct under round change, where the deciding leader re-proposes another operator's block.
- Operators SHOULD call
produceBlockV4 with include_payload=true, so a self-build response is BlockContents and the builder operator holds the envelope, blobs, and KZG proofs from the produce call on. No beacon-node call sits between the §4 decision and the dissemination.
- The builder operator SHOULD publish
SignedExecutionPayloadEnvelopeContents (Eth-Blob-Data-Included: true) to each of its beacon nodes.
The reference runner in ssv/envelope_proposer.go (epbs-gloas-types, via #633/#643) models the other path:
executeDuty calls BeaconNode.GetBlindedExecutionPayloadEnvelope(slot, blockRoot) and treats success as the builder test ("it answers only on the beacon node that built the block").
EnvelopeCalls.SubmitExecutionPayloadEnvelope(envelope, signature) takes the blinded envelope and expects the beacon node to form the full body from its cache, i.e. the stateful publish the SIP now steers away from.
ProposedBlock carries only BlockRoot, ParentRoot, ExecutionRequestsRoot; nothing records which operator produced the decided block.
go-ssv (ssvlabs/ssv#2901) implements the SIP text: the proposer runner records ProducedLocally plus the produce-held envelope contents into the §4→§6 store at decision time, the envelope runner disseminates from that record with no beacon-node call, and publishes the Contents form to every beacon node. The wire behaviour is identical either way; the divergence is only in how the reference runner models the envelope source, which matters for mapping the spec's envelope vectors onto implementations.
Proposed adjustments
ssv.ProposedBlock gains the builder-operator facts the SIP defines: a ProducedLocally flag (the operator's own produce response has the decided root) and the produced blinded envelope for that case (nil for every other operator and for an external build). The proposer runner records them at the §4 decision, mirroring its Eth-Builder-Url owner-match.
EnvelopeProposerRunner.executeDuty disseminates and signs from ProposedBlock when ProducedLocally is set and otherwise waits for a peer's dissemination. It makes no beacon-node call.
EnvelopeCalls drops GetBlindedExecutionPayloadEnvelope. SubmitExecutionPayloadEnvelope should reflect the Contents publish: the builder operator supplies the full body (envelope plus blobs and proofs, or in the spec's mock at least "the builder holds the body"), rather than the beacon node completing it from a cache.
- Testing utilities: the Gloas proposer produce mock returns
BlockContents (block plus envelope) for a self-build, so the root_linkage and happy_flow vectors exercise the produce-held envelope; TestingBlindedExecutionPayloadEnvelope remains the disseminated fixture, and content_mismatch_no_publish keeps exercising the publish-by-content-match with a produced envelope that differs from the selected one.
- Docs: the
ProposedBlocks and EnvelopeCalls comments describe the fetch path and should follow the SIP wording ("builder operator", stateless source).
Why now
With the reference runner on the fetch path, the spec's envelope vectors assume a beacon-node call that a SIP-conformant implementation never makes, so mapping them onto go-ssv means seeding produced envelopes where the vectors expect a mock fetch. Aligning the reference with the SIP text removes that mismatch before the vectors are frozen, and keeps Anchor (sigp/anchor#1286, sigp/anchor#1288) and go-ssv reading the same model.
Context
SIP-94 §6, in its current revision on ssvlabs/SIPs#94, pins how the self-build envelope reaches the cluster:
produceBlockV4response has the §4-decided block root (the same local test §4 uses for theEth-Builder-Urlecho). It stays correct under round change, where the deciding leader re-proposes another operator's block.produceBlockV4withinclude_payload=true, so a self-build response isBlockContentsand the builder operator holds the envelope, blobs, and KZG proofs from the produce call on. No beacon-node call sits between the §4 decision and the dissemination.SignedExecutionPayloadEnvelopeContents(Eth-Blob-Data-Included: true) to each of its beacon nodes.The reference runner in
ssv/envelope_proposer.go(epbs-gloas-types, via #633/#643) models the other path:executeDutycallsBeaconNode.GetBlindedExecutionPayloadEnvelope(slot, blockRoot)and treats success as the builder test ("it answers only on the beacon node that built the block").EnvelopeCalls.SubmitExecutionPayloadEnvelope(envelope, signature)takes the blinded envelope and expects the beacon node to form the full body from its cache, i.e. the stateful publish the SIP now steers away from.ProposedBlockcarries onlyBlockRoot,ParentRoot,ExecutionRequestsRoot; nothing records which operator produced the decided block.go-ssv (ssvlabs/ssv#2901) implements the SIP text: the proposer runner records
ProducedLocallyplus the produce-held envelope contents into the §4→§6 store at decision time, the envelope runner disseminates from that record with no beacon-node call, and publishes theContentsform to every beacon node. The wire behaviour is identical either way; the divergence is only in how the reference runner models the envelope source, which matters for mapping the spec's envelope vectors onto implementations.Proposed adjustments
ssv.ProposedBlockgains the builder-operator facts the SIP defines: aProducedLocallyflag (the operator's own produce response has the decided root) and the produced blinded envelope for that case (nil for every other operator and for an external build). The proposer runner records them at the §4 decision, mirroring itsEth-Builder-Urlowner-match.EnvelopeProposerRunner.executeDutydisseminates and signs fromProposedBlockwhenProducedLocallyis set and otherwise waits for a peer's dissemination. It makes no beacon-node call.EnvelopeCallsdropsGetBlindedExecutionPayloadEnvelope.SubmitExecutionPayloadEnvelopeshould reflect theContentspublish: the builder operator supplies the full body (envelope plus blobs and proofs, or in the spec's mock at least "the builder holds the body"), rather than the beacon node completing it from a cache.BlockContents(block plus envelope) for a self-build, so theroot_linkageandhappy_flowvectors exercise the produce-held envelope;TestingBlindedExecutionPayloadEnveloperemains the disseminated fixture, andcontent_mismatch_no_publishkeeps exercising the publish-by-content-match with a produced envelope that differs from the selected one.ProposedBlocksandEnvelopeCallscomments describe the fetch path and should follow the SIP wording ("builder operator", stateless source).Why now
With the reference runner on the fetch path, the spec's envelope vectors assume a beacon-node call that a SIP-conformant implementation never makes, so mapping them onto go-ssv means seeding produced envelopes where the vectors expect a mock fetch. Aligning the reference with the SIP text removes that mismatch before the vectors are frozen, and keeps Anchor (sigp/anchor#1286, sigp/anchor#1288) and go-ssv reading the same model.