diff --git a/bolt-sidecar/src/builder/template.rs b/bolt-sidecar/src/builder/template.rs index b9d8df82..6d3b5735 100644 --- a/bolt-sidecar/src/builder/template.rs +++ b/bolt-sidecar/src/builder/template.rs @@ -42,7 +42,7 @@ impl BlockTemplate { /// Returns the cloned list of transactions from the constraints. #[inline] pub fn transactions(&self) -> Vec { - self.signed_constraints_list.iter().flat_map(|sc| sc.message.constraints.clone()).collect() + self.signed_constraints_list.iter().flat_map(|sc| sc.message.transactions.clone()).collect() } /// Converts the list of signed constraints into a list of signed transactions. Use this when @@ -52,7 +52,7 @@ impl BlockTemplate { self.signed_constraints_list .iter() .flat_map(|sc| { - sc.message.constraints.iter().map(|c| c.clone().into_inner().into_transaction()) + sc.message.transactions.iter().map(|c| c.clone().into_inner().into_transaction()) }) .collect() } @@ -64,7 +64,7 @@ impl BlockTemplate { let (commitments, proofs, blobs) = self.signed_constraints_list .iter() - .flat_map(|sc| sc.message.constraints.iter()) + .flat_map(|sc| sc.message.transactions.iter()) .filter_map(|c| c.blob_sidecar()) .fold( (Vec::new(), Vec::new(), Vec::new()), @@ -90,14 +90,14 @@ impl BlockTemplate { /// Returns the length of the transactions in the block template. #[inline] pub fn transactions_len(&self) -> usize { - self.signed_constraints_list.iter().fold(0, |acc, sc| acc + sc.message.constraints.len()) + self.signed_constraints_list.iter().fold(0, |acc, sc| acc + sc.message.transactions.len()) } /// Returns the committed gas in the block template. #[inline] pub fn committed_gas(&self) -> u64 { self.signed_constraints_list.iter().fold(0, |acc, sc| { - acc + sc.message.constraints.iter().fold(0, |acc, c| acc + c.gas_limit()) + acc + sc.message.transactions.iter().fold(0, |acc, c| acc + c.gas_limit()) }) } @@ -105,7 +105,7 @@ impl BlockTemplate { #[inline] pub fn blob_count(&self) -> usize { self.signed_constraints_list.iter().fold(0, |mut acc, sc| { - acc += sc.message.constraints.iter().fold(0, |acc, c| { + acc += sc.message.transactions.iter().fold(0, |acc, c| { acc + c.as_eip4844().map(|tx| tx.blob_versioned_hashes.len()).unwrap_or(0) }); @@ -115,7 +115,7 @@ impl BlockTemplate { /// Adds a list of constraints to the block template and updates the state diff. pub fn add_constraints(&mut self, constraints: SignedConstraints) { - for constraint in constraints.message.constraints.iter() { + for constraint in constraints.message.transactions.iter() { let max_cost = max_transaction_cost(constraint); self.state_diff .diffs @@ -134,7 +134,7 @@ impl BlockTemplate { fn remove_constraints_at_index(&mut self, index: usize) { let constraints = self.signed_constraints_list.remove(index); - for constraint in constraints.message.constraints.iter() { + for constraint in constraints.message.transactions.iter() { self.state_diff .diffs .entry(*constraint.sender().expect("recovered sender")) @@ -155,7 +155,7 @@ impl BlockTemplate { .signed_constraints_list .iter() .enumerate() - .map(|(idx, c)| (idx, &c.message.constraints)) + .map(|(idx, c)| (idx, &c.message.transactions)) .filter(|(_idx, c)| c.iter().any(|c| c.sender().expect("recovered sender") == &address)) .map(|(idx, c)| { ( diff --git a/bolt-sidecar/src/driver.rs b/bolt-sidecar/src/driver.rs index 6cd3520a..99a37912 100644 --- a/bolt-sidecar/src/driver.rs +++ b/bolt-sidecar/src/driver.rs @@ -231,7 +231,7 @@ impl SidecarDriver, + pub transactions: Vec, } impl ConstraintsMessage { /// Builds a constraints message from an inclusion request and metadata pub fn build(pubkey: BlsPublicKey, request: InclusionRequest) -> Self { - let constraints = request.txs; + let transactions = request.txs; - Self { pubkey, slot: request.slot, top: false, constraints } + Self { pubkey, slot: request.slot, top: false, transactions } } } @@ -79,8 +79,8 @@ impl SignableBLS for ConstraintsMessage { hasher.update(self.slot.to_le_bytes()); hasher.update((self.top as u8).to_le_bytes()); - for constraint in &self.constraints { - hasher.update(constraint.hash()); + for tx in &self.transactions { + hasher.update(tx.hash()); } hasher.finalize().into() @@ -114,10 +114,10 @@ mod tests { let pubkey = BlsPublicKey::default(); let slot = 0; let top = false; - let constraints = random_constraints(1); // Generate 'n' random constraints + let transactions = random_constraints(1); // Generate 'n' random constraints // Create a random `ConstraintsMessage` - let message = ConstraintsMessage { pubkey, slot, top, constraints }; + let message = ConstraintsMessage { pubkey, slot, top, transactions }; // Compute tree hash root let digest = SignableBLS::digest(&message); @@ -134,10 +134,10 @@ mod tests { let pubkey = BlsPublicKey::default(); let slot = random_u64(&mut rng); let top = false; - let constraints = random_constraints(2); // Generate 'n' random constraints + let transactions = random_constraints(2); // Generate 'n' random constraints // Create a random `ConstraintsMessage` - let message = ConstraintsMessage { pubkey, slot, top, constraints }; + let message = ConstraintsMessage { pubkey, slot, top, transactions }; // Serialize the `ConstraintsMessage` to JSON let json = serde_json::to_string(&message).unwrap(); diff --git a/bolt-sidecar/src/primitives/mod.rs b/bolt-sidecar/src/primitives/mod.rs index e705e698..d9cfbec7 100644 --- a/bolt-sidecar/src/primitives/mod.rs +++ b/bolt-sidecar/src/primitives/mod.rs @@ -6,7 +6,10 @@ use std::{ sync::{atomic::AtomicU64, Arc}, }; -use alloy::primitives::{Address, U256}; +use alloy::{ + primitives::{Address, U256}, + signers::k256::sha2::{Digest, Sha256}, +}; use ethereum_consensus::{ crypto::KzgCommitment, deneb::{ @@ -36,6 +39,8 @@ pub mod constraint; pub use constraint::{BatchedSignedConstraints, ConstraintsMessage, SignedConstraints}; use tracing::{error, info}; +use crate::crypto::SignableBLS; + /// An alias for a Beacon Chain slot number pub type Slot = u64; @@ -430,6 +435,16 @@ pub struct DelegationMessage { pub delegatee_pubkey: BlsPublicKey, } +impl SignableBLS for DelegationMessage { + fn digest(&self) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(&self.validator_pubkey.to_vec()); + hasher.update(&self.delegatee_pubkey.to_vec()); + + hasher.finalize().into() + } +} + #[derive(Debug, Clone, Serialize)] pub struct SignedRevocation { pub message: RevocationMessage, @@ -441,3 +456,13 @@ pub struct RevocationMessage { pub validator_pubkey: BlsPublicKey, pub delegatee_pubkey: BlsPublicKey, } + +impl SignableBLS for RevocationMessage { + fn digest(&self) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(&self.validator_pubkey.to_vec()); + hasher.update(&self.delegatee_pubkey.to_vec()); + + hasher.finalize().into() + } +} diff --git a/bolt-sidecar/src/test_util.rs b/bolt-sidecar/src/test_util.rs index c03bad87..ad599b12 100644 --- a/bolt-sidecar/src/test_util.rs +++ b/bolt-sidecar/src/test_util.rs @@ -11,13 +11,18 @@ use alloy::{ }; use alloy_node_bindings::{Anvil, AnvilInstance}; use blst::min_pk::SecretKey; +use ethereum_consensus::crypto::{PublicKey, Signature}; +use rand::Rng; use reth_primitives::PooledTransactionsElement; use secp256k1::Message; use tracing::warn; use crate::{ - crypto::{ecdsa::SignableECDSA, SignableBLS}, - primitives::{CommitmentRequest, FullTransaction, InclusionRequest}, + crypto::{bls::Signer as BlsSigner, ecdsa::SignableECDSA, SignableBLS, SignerBLS}, + primitives::{ + CommitmentRequest, ConstraintsMessage, DelegationMessage, FullTransaction, + InclusionRequest, RevocationMessage, SignedConstraints, SignedDelegation, SignedRevocation, + }, Config, }; @@ -170,3 +175,103 @@ pub(crate) async fn create_signed_commitment_request( Ok(CommitmentRequest::Inclusion(request)) } + +fn random_constraints(count: usize) -> Vec { + // Random inclusion request + let json_req = r#"{ + "slot": 10, + "txs": [ + "0x02f86c870c72dd9d5e883e4d0183408f2382520894d2e2adf7177b7a8afddbc12d1634cf23ea1a71020180c001a08556dcfea479b34675db3fe08e29486fe719c2b22f6b0c1741ecbbdce4575cc6a01cd48009ccafd6b9f1290bbe2ceea268f94101d1d322c787018423ebcbc87ab4", + "0x02f9017b8501a2140cff8303dec685012a05f2008512a05f2000830249f094843669e5220036eddbaca89d8c8b5b82268a0fc580b901040cc7326300000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000022006292538e66f0000000000000000000000005ba38f2c245618e39f6fa067bf1dec304e73ff3c00000000000000000000000092f0ee29e6e1bf0f7c668317ada78f5774a6cb7f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000003fac6482aee49bf58515be2d3fb58378a8497cc9000000000000000000000000c6cc140787b02ae479a10e41169607000c0d44f6c080a00cf74c45dbe9ee1fb923118ec5ce9db8f88cd651196ed3f9d4f8f2a65827e611a04a6bc1d49a7e18b7c92e8f3614cae116b1832ceb311c81d54b2c87de1545f68f", + "0x02f8708501a2140cff82012f800782520894b6c402298fcb88039bbfde70f5ace791f18cfac88707131d70870dc880c080a03aab1b17ecf28f85de43c7733611759b87d25ba885babacb6b4c625d715415eea03fb52cb7744ccb885906e42f6b9cf82e74b47a4b4b4072af2aa52a8dc472236e" + ] + }"#; + + let req: InclusionRequest = serde_json::from_str(json_req).unwrap(); + + req.txs.iter().cloned().take(count).collect() +} + +#[tokio::test] +async fn generate_test_data() { + let sk = test_bls_secret_key(); + let pk = sk.sk_to_pk(); + let signer = BlsSigner::new(sk); + + println!("Validator Public Key: {}", hex::encode(pk.to_bytes())); + + // Generate a delegatee's BLS secret key and public key + let delegatee_ikm: [u8; 32] = rand::thread_rng().gen(); + let delegatee_sk = + SecretKey::key_gen(&delegatee_ikm, &[]).expect("Failed to generate delegatee secret key"); + let delegatee_pk = delegatee_sk.sk_to_pk(); + + // Prepare a Delegation message + let delegation_msg = DelegationMessage { + validator_pubkey: PublicKey::try_from(pk.to_bytes().as_slice()) + .expect("Failed to convert validator public key"), + delegatee_pubkey: PublicKey::try_from(delegatee_pk.to_bytes().as_slice()) + .expect("Failed to convert delegatee public key"), + }; + + let digest = SignableBLS::digest(&delegation_msg); + + // Sign the Delegation message + let delegation_signature = SignerBLS::sign(&signer, &digest).await.unwrap(); + + // Create SignedDelegation + let signed_delegation = SignedDelegation { + message: delegation_msg, + signature: Signature::try_from(delegation_signature.as_ref()) + .expect("Failed to convert delegation signature"), + }; + + // Output SignedDelegation + println!("{}", serde_json::to_string_pretty(&signed_delegation).unwrap()); + + // Prepare a revocation message + let revocation_msg = RevocationMessage { + validator_pubkey: PublicKey::try_from(pk.to_bytes().as_slice()) + .expect("Failed to convert validator public key"), + delegatee_pubkey: PublicKey::try_from(delegatee_pk.to_bytes().as_slice()) + .expect("Failed to convert delegatee public key"), + }; + + let digest = SignableBLS::digest(&revocation_msg); + + // Sign the Revocation message + let revocation_signature = SignerBLS::sign(&signer, &digest).await.unwrap(); + + // Create SignedRevocation + let signed_revocation = SignedRevocation { + message: revocation_msg, + signature: Signature::try_from(revocation_signature.as_ref()) + .expect("Failed to convert revocation signature"), + }; + + // Output SignedRevocation + println!("{}", serde_json::to_string_pretty(&signed_revocation).unwrap()); + + let transactions = random_constraints(1); + + // Prepare a ConstraintsMessage + let constraints_msg = ConstraintsMessage { + pubkey: PublicKey::try_from(pk.to_bytes().as_slice()) + .expect("Failed to convert validator public key"), + slot: 32, + top: true, + transactions, + }; + + let digest = SignableBLS::digest(&constraints_msg); + + // Sign the ConstraintsMessage + let constraints_signature = SignerBLS::sign(&signer, &digest).await.unwrap(); + + // Create SignedConstraints + let signed_constraints = + SignedConstraints { message: constraints_msg, signature: constraints_signature }; + + // Output SignedConstraints + println!("{}", serde_json::to_string_pretty(&signed_constraints).unwrap()); +} diff --git a/builder/builder/builder.go b/builder/builder/builder.go index 1068c454..e6e8ad73 100644 --- a/builder/builder/builder.go +++ b/builder/builder/builder.go @@ -53,7 +53,8 @@ const ( ) const ( - SubscribeConstraintsPath = "/relay/v1/builder/constraints" + GetConstraintsPath = "/relay/v1/builder/constraints" + SubscribeConstraintsPath = "/relay/v1/builder/constraints_stream" ) type PubkeyHex string @@ -66,7 +67,8 @@ type ValidatorData struct { type IRelay interface { SubmitBlock(msg *builderSpec.VersionedSubmitBlockRequest, vd ValidatorData) error - SubmitBlockWithProofs(msg *common.VersionedSubmitBlockRequestWithProofs, vd ValidatorData) error + SubmitBlockWithProofs(msg *types.VersionedSubmitBlockRequestWithProofs, vd ValidatorData) error + GetDelegationsForSlot(nextSlot uint64) (types.SignedDelegations, error) GetValidatorForSlot(nextSlot uint64) (ValidatorData, error) Config() RelayConfig Start() error @@ -100,10 +102,11 @@ type Builder struct { limiter *rate.Limiter submissionOffsetFromEndOfSlot time.Duration - slotMu sync.Mutex - slotAttrs types.BuilderPayloadAttributes - slotCtx context.Context - slotCtxCancel context.CancelFunc + slotMu sync.Mutex + slotConstraintsPubkeys []phase0.BLSPubKey // The pubkey of the authorized constraints signer + slotAttrs types.BuilderPayloadAttributes + slotCtx context.Context + slotCtxCancel context.CancelFunc stop chan struct{} } @@ -347,7 +350,7 @@ func (b *Builder) subscribeToRelayForConstraints(relayBaseEndpoint string) error // We assume the data is the JSON representation of the constraints log.Info(fmt.Sprintf("Received new constraint: %s", data)) - constraintsSigned := make(common.SignedConstraintsList, 0, 8) + constraintsSigned := make(types.SignedConstraintsList, 0, 8) if err := json.Unmarshal([]byte(data), &constraintsSigned); err != nil { log.Warn(fmt.Sprintf("Failed to unmarshal constraints: %v", err)) continue @@ -359,14 +362,29 @@ func (b *Builder) subscribeToRelayForConstraints(relayBaseEndpoint string) error } for _, constraint := range constraintsSigned { + oneValidSignature := false + // Check if the signature is valid against any of the authorized pubkeys + for _, pubkey := range b.slotConstraintsPubkeys { + valid, err := constraint.VerifySignature(pubkey, b.GetConstraintsDomain()) + if err != nil || !valid { + log.Error("Failed to verify constraint signature", "err", err) + continue + } + + oneValidSignature = true + } + + // If there is no valid signature, continue with the next constraint + if !oneValidSignature { + continue + } + decodedConstraints, err := DecodeConstraints(constraint) if err != nil { log.Error("Failed to decode constraint: ", err) continue } - EmitBoltDemoEvent(fmt.Sprintf("Received constraint from relay for slot %d, stored in cache (path: %s)", constraint.Message.Slot, SubscribeConstraintsPath)) - // For every constraint, we need to check if it has already been seen for the associated slot slotConstraints, _ := b.constraintsCache.Get(constraint.Message.Slot) if len(slotConstraints) == 0 { @@ -382,9 +400,7 @@ func (b *Builder) subscribeToRelayForConstraints(relayBaseEndpoint string) error // Update the slot constraints in the cache b.constraintsCache.Put(constraint.Message.Slot, slotConstraints) - } - } return nil @@ -395,7 +411,20 @@ func (b *Builder) Stop() error { return nil } -// BOLT: modify to calculate merkle inclusion proofs for preconfirmed transactions +// GetConstraintsDomain returns the constraints domain used to sign constraints-API related messages. +// +// The builder signing domain is built as follows: +// - We build a ForkData ssz container with the fork version and the genesis validators root. In the builder domain, this is an empty root. +// - We take the hash tree root of this container and replace the first 4 bytes with the builder domain mask. That gives us the signing domain. +// +// To get the constraints domain, we take the builder domain and replace the first 4 bytes with the constraints domain type. +func (b *Builder) GetConstraintsDomain() phase0.Domain { + domain := b.builderSigningDomain + copy(domain[:4], types.ConstraintsDomainType[:]) + return domain +} + +// BOLT: modify to calculate merkle inclusion proofs for committed transactions func (b *Builder) onSealedBlock(opts SubmitBlockOpts) error { executableData := engine.BlockToExecutableData(opts.Block, opts.BlockValue, opts.BlobSidecars) var dataVersion spec.DataVersion @@ -432,7 +461,7 @@ func (b *Builder) onSealedBlock(opts SubmitBlockOpts) error { return err } - var versionedBlockRequestWithPreconfsProofs *common.VersionedSubmitBlockRequestWithProofs + var versionedBlockRequestWithConstraintProofs *types.VersionedSubmitBlockRequestWithProofs // BOLT: fetch constraints from the cache, which is automatically updated by the SSE subscription constraints, _ := b.constraintsCache.Get(opts.PayloadAttributes.Slot) @@ -441,23 +470,17 @@ func (b *Builder) onSealedBlock(opts SubmitBlockOpts) error { if len(constraints) > 0 { message := fmt.Sprintf("sealing block %d with %d constraints", opts.Block.Number(), len(constraints)) log.Info(message) - EmitBoltDemoEvent(message) - timeStart := time.Now() inclusionProof, _, err := CalculateMerkleMultiProofs(opts.Block.Transactions(), constraints) - timeForProofs := time.Since(timeStart) if err != nil { log.Error("[BOLT]: could not calculate merkle multiproofs", "err", err) return err } - // BOLT: send event to web demo - EmitBoltDemoEvent(fmt.Sprintf("created merkle multiproof of %d constraint(s) for block %d in %v", len(constraints), opts.Block.Number(), timeForProofs)) - - versionedBlockRequestWithPreconfsProofs = &common.VersionedSubmitBlockRequestWithProofs{ - Inner: versionedBlockRequest, - Proofs: inclusionProof, + versionedBlockRequestWithConstraintProofs = &types.VersionedSubmitBlockRequestWithProofs{ + VersionedSubmitBlockRequest: versionedBlockRequest, + Proofs: inclusionProof, } } @@ -474,13 +497,13 @@ func (b *Builder) onSealedBlock(opts SubmitBlockOpts) error { log.Error("could not validate block", "version", dataVersion.String(), "err", err) } } else { - // NOTE: we can ignore preconfs for `processBuiltBlock` + // NOTE: we can ignore constraints for `processBuiltBlock` go b.processBuiltBlock(opts.Block, opts.BlockValue, opts.OrdersClosedAt, opts.SealedAt, opts.CommitedBundles, opts.AllBundles, opts.UsedSbundles, &blockBidMsg) - if versionedBlockRequestWithPreconfsProofs != nil { - log.Info(fmt.Sprintf("[BOLT]: Sending sealed block to relay %s", versionedBlockRequestWithPreconfsProofs)) - err = b.relay.SubmitBlockWithProofs(versionedBlockRequestWithPreconfsProofs, opts.ValidatorData) + if versionedBlockRequestWithConstraintProofs != nil { + log.Info(fmt.Sprintf("[BOLT]: Sending sealed block to relay %s", versionedBlockRequestWithConstraintProofs)) + err = b.relay.SubmitBlockWithProofs(versionedBlockRequestWithConstraintProofs, opts.ValidatorData) } else if len(constraints) == 0 { - // If versionedBlockRequestWithPreconfsProofs is nil and no constraints, then we don't have proofs to send + // If versionedBlockRequestWithConstraintsProofs is nil and no constraints, then we don't have proofs to send err = b.relay.SubmitBlock(versionedBlockRequest, opts.ValidatorData) } else { log.Warn(fmt.Sprintf("[BOLT]: Could not send sealed block this time because we have %d constraints but no proofs", len(constraints))) @@ -570,6 +593,40 @@ func (b *Builder) OnPayloadAttribute(attrs *types.BuilderPayloadAttributes) erro return fmt.Errorf("could not get validator while submitting block for slot %d - %w", attrs.Slot, err) } + proposerPubkey, err := utils.HexToPubkey(string(vd.Pubkey)) + if err != nil { + return fmt.Errorf("could not parse pubkey (%s) - %w", vd.Pubkey, err) + } + + // BOLT: by default, the proposer key is the constraint signer key + pubkey, err := utils.HexToPubkey(string(vd.Pubkey)) + if err != nil { + log.Error("could not parse pubkey", "pubkey", vd.Pubkey, "err", err) + } + constraintsPubkeys := []phase0.BLSPubKey{pubkey} + + // BOLT: get delegations for the slot + delegations, err := b.relay.GetDelegationsForSlot(attrs.Slot) + if err != nil { + log.Error("could not get delegations for slot, using default validator key", "slot", attrs.Slot, "err", err) + } + + if len(delegations) > 0 { + // If there are delegations, reset the constraintsPubkeys (i.e. remove the validator key as authorized) + constraintsPubkeys = make([]phase0.BLSPubKey, 0, len(delegations)) + for _, delegation := range delegations { + // Verify signature against the public key + valid, err := delegation.VerifySignature(pubkey, b.GetConstraintsDomain()) + if err != nil || !valid { + log.Error("could not verify signature", "err", err) + continue + } + + // If signature is valid, add the pubkey to the list of authorized constraint pubkeys + constraintsPubkeys = append(constraintsPubkeys, delegation.Message.DelegateePubkey) + } + } + parentBlock := b.eth.GetBlockByHash(attrs.HeadHash) if parentBlock == nil { return fmt.Errorf("parent block hash not found in block tree given head block hash %s", attrs.HeadHash) @@ -578,11 +635,6 @@ func (b *Builder) OnPayloadAttribute(attrs *types.BuilderPayloadAttributes) erro attrs.SuggestedFeeRecipient = [20]byte(vd.FeeRecipient) attrs.GasLimit = core.CalcGasLimit(parentBlock.GasLimit(), vd.GasLimit) - proposerPubkey, err := utils.HexToPubkey(string(vd.Pubkey)) - if err != nil { - return fmt.Errorf("could not parse pubkey (%s) - %w", vd.Pubkey, err) - } - if !b.eth.Synced() { return errors.New("backend not Synced") } @@ -601,6 +653,8 @@ func (b *Builder) OnPayloadAttribute(attrs *types.BuilderPayloadAttributes) erro slotCtx, slotCtxCancel := context.WithTimeout(context.Background(), 12*time.Second) b.slotAttrs = *attrs + // BOLT: save the authorized pubkeys for the upcoming slot + b.slotConstraintsPubkeys = constraintsPubkeys b.slotCtx = slotCtx b.slotCtxCancel = slotCtxCancel diff --git a/builder/builder/builder_test.go b/builder/builder/builder_test.go index 2badb919..c4aa3586 100644 --- a/builder/builder/builder_test.go +++ b/builder/builder/builder_test.go @@ -6,7 +6,6 @@ import ( "fmt" "math/big" "net/http" - "strings" "testing" "time" @@ -25,7 +24,6 @@ import ( "github.com/flashbots/go-boost-utils/utils" "github.com/gorilla/handlers" "github.com/holiman/uint256" - "github.com/pkg/errors" "github.com/stretchr/testify/require" ) @@ -179,7 +177,7 @@ func TestOnPayloadAttributes(t *testing.T) { require.NotNil(t, testRelay.submittedMsg) } -func TestBlockWithPreconfs(t *testing.T) { +func TestBlockWithConstraints(t *testing.T) { const ( validatorDesiredGasLimit = 30_000_000 payloadAttributeGasLimit = 30_000_000 // Was zero in the other test @@ -215,16 +213,16 @@ func TestBlockWithPreconfs(t *testing.T) { // https://etherscan.io/tx/0x9d48b4a021898a605b7ae49bf93ad88fa6bd7050e9448f12dde064c10f22fe9c // 0x02f87601836384348477359400850517683ba883019a28943678fce4028b6745eb04fa010d9c8e4b36d6288c872b0f1366ad800080c080a0b6b7aba1954160d081b2c8612e039518b9c46cd7df838b405a03f927ad196158a071d2fb6813e5b5184def6bd90fb5f29e0c52671dea433a7decb289560a58416e - preconfTxByte, _ := hex.DecodeString("02f87601836384348477359400850517683ba883019a28943678fce4028b6745eb04fa010d9c8e4b36d6288c872b0f1366ad800080c080a0b6b7aba1954160d081b2c8612e039518b9c46cd7df838b405a03f927ad196158a071d2fb6813e5b5184def6bd90fb5f29e0c52671dea433a7decb289560a58416e") - preconfTx := new(types.Transaction) - err = preconfTx.UnmarshalBinary(preconfTxByte) + constraintTxByte, _ := hex.DecodeString("02f87601836384348477359400850517683ba883019a28943678fce4028b6745eb04fa010d9c8e4b36d6288c872b0f1366ad800080c080a0b6b7aba1954160d081b2c8612e039518b9c46cd7df838b405a03f927ad196158a071d2fb6813e5b5184def6bd90fb5f29e0c52671dea433a7decb289560a58416e") + constraintTx := new(types.Transaction) + err = constraintTx.UnmarshalBinary(constraintTxByte) require.NoError(t, err) // https://etherscan.io/tx/0x15bd881daa1408b33f67fa4bdeb8acfb0a2289d9b4c6f81eef9bb2bb2e52e780 - Blob Tx // 0x03f9029c01830299f184b2d05e008507aef40a00832dc6c09468d30f47f19c07bccef4ac7fae2dc12fca3e0dc980b90204ef16e845000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000633b68f5d8d3a86593ebb815b4663bcbe0302e31382e302d64657600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004109de8da2a97e37f2e6dc9f7d50a408f9344d7aa1a925ae53daf7fbef43491a571960d76c0cb926190a9da10df7209fb1ba93cd98b1565a3a2368749d505f90c81c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0843b9aca00e1a00141e3a338e30c49ed0501e315bcc45e4edefebed43ab1368a1505461d9cf64901a01e8511e06b17683d89eb57b9869b96b8b611f969f7f56cbc0adc2df7c88a2a07a00910deacf91bba0d74e368d285d311dc5884e7cfe219d85aea5741b2b6e3a2fe - preconfTxWithBlobByte, _ := hex.DecodeString("03f9029c01830299f184b2d05e008507aef40a00832dc6c09468d30f47f19c07bccef4ac7fae2dc12fca3e0dc980b90204ef16e845000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000633b68f5d8d3a86593ebb815b4663bcbe0302e31382e302d64657600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004109de8da2a97e37f2e6dc9f7d50a408f9344d7aa1a925ae53daf7fbef43491a571960d76c0cb926190a9da10df7209fb1ba93cd98b1565a3a2368749d505f90c81c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0843b9aca00e1a00141e3a338e30c49ed0501e315bcc45e4edefebed43ab1368a1505461d9cf64901a01e8511e06b17683d89eb57b9869b96b8b611f969f7f56cbc0adc2df7c88a2a07a00910deacf91bba0d74e368d285d311dc5884e7cfe219d85aea5741b2b6e3a2fe") - preconfTxWithBlob := new(types.Transaction) - err = preconfTxWithBlob.UnmarshalBinary(preconfTxWithBlobByte) + constraintTxWithBlobByte, _ := hex.DecodeString("03f9029c01830299f184b2d05e008507aef40a00832dc6c09468d30f47f19c07bccef4ac7fae2dc12fca3e0dc980b90204ef16e845000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000633b68f5d8d3a86593ebb815b4663bcbe0302e31382e302d64657600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004109de8da2a97e37f2e6dc9f7d50a408f9344d7aa1a925ae53daf7fbef43491a571960d76c0cb926190a9da10df7209fb1ba93cd98b1565a3a2368749d505f90c81c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0843b9aca00e1a00141e3a338e30c49ed0501e315bcc45e4edefebed43ab1368a1505461d9cf64901a01e8511e06b17683d89eb57b9869b96b8b611f969f7f56cbc0adc2df7c88a2a07a00910deacf91bba0d74e368d285d311dc5884e7cfe219d85aea5741b2b6e3a2fe") + constraintTxWithBlob := new(types.Transaction) + err = constraintTxWithBlob.UnmarshalBinary(constraintTxWithBlobByte) require.NoError(t, err) testExecutableData := &engine.ExecutableData{ @@ -242,10 +240,10 @@ func TestBlockWithPreconfs(t *testing.T) { BaseFeePerGas: big.NewInt(16), BlockHash: common.HexToHash("3cce5d0f5c9a7e188e79c35168256e91bec2d98a1140f6701da6ed3c98ea9d04"), - Transactions: [][]byte{preconfTxByte, preconfTxWithBlobByte}, + Transactions: [][]byte{constraintTxByte, constraintTxWithBlobByte}, } - testBlock, err := engine.ExecutableDataToBlock(*testExecutableData, preconfTxWithBlob.BlobHashes(), nil) + testBlock, err := engine.ExecutableDataToBlock(*testExecutableData, constraintTxWithBlob.BlobHashes(), nil) require.NoError(t, err) testPayloadAttributes := &types.BuilderPayloadAttributes{ @@ -277,20 +275,16 @@ func TestBlockWithPreconfs(t *testing.T) { defer builder.Stop() // Add the transaction to the cache directly - builder.constraintsCache.Put(25, map[common.Hash]*types.ConstraintDecoded{ - preconfTx.Hash(): { - Tx: preconfTx, - }, - preconfTxWithBlob.Hash(): { - Tx: preconfTxWithBlob, - }, + builder.constraintsCache.Put(25, map[common.Hash]*types.Transaction{ + constraintTx.Hash(): constraintTx, + constraintTxWithBlob.Hash(): constraintTxWithBlob, }) err = builder.OnPayloadAttribute(testPayloadAttributes) require.NoError(t, err) time.Sleep(time.Second * 3) - require.NotNil(t, testRelay.submittedMsgWithPreconf) + require.NotNil(t, testRelay.submittedMsgWithProofs) expectedProposerPubkey, err := utils.HexToPubkey(testBeacon.validator.Pk.String()) require.NoError(t, err) @@ -306,8 +300,9 @@ func TestBlockWithPreconfs(t *testing.T) { Value: &uint256.Int{0x0a}, } copy(expectedMessage.BlockHash[:], hexutil.MustDecode("0x3cce5d0f5c9a7e188e79c35168256e91bec2d98a1140f6701da6ed3c98ea9d04")[:]) - require.NotNil(t, testRelay.submittedMsgWithPreconf.Inner.Bellatrix) - require.Equal(t, expectedMessage, *testRelay.submittedMsgWithPreconf.Inner.Bellatrix.Message) + require.NotNil(t, testRelay.submittedMsgWithProofs.Bellatrix) + + require.Equal(t, expectedMessage, *testRelay.submittedMsgWithProofs.Bellatrix.Message) expectedExecutionPayload := bellatrix.ExecutionPayload{ ParentHash: [32]byte(testExecutableData.ParentHash), @@ -323,35 +318,35 @@ func TestBlockWithPreconfs(t *testing.T) { ExtraData: hexutil.MustDecode("0x0042fafc"), BaseFeePerGas: [32]byte{0x10}, BlockHash: expectedMessage.BlockHash, - Transactions: []bellatrix.Transaction{preconfTxByte, preconfTxWithBlobByte}, + Transactions: []bellatrix.Transaction{constraintTxByte, constraintTxWithBlobByte}, } - require.Equal(t, expectedExecutionPayload, *testRelay.submittedMsgWithPreconf.Inner.Bellatrix.ExecutionPayload) + require.Equal(t, expectedExecutionPayload, *testRelay.submittedMsgWithProofs.Bellatrix.ExecutionPayload) expectedSignature, err := utils.HexToSignature("0x97db0496dcfd04ed444b87b6fc1c9e3339a0d35f7c01825ac353812601a72e7e35ef94899a9b03f4d23102214701255805efd0f6552073791ea1c3e10003ae435952f8305f6b89e58d4442ced149d3c33a486f5a390b4b8047e6ea4176059755") require.NoError(t, err) - require.Equal(t, expectedSignature, testRelay.submittedMsgWithPreconf.Inner.Bellatrix.Signature) + require.Equal(t, expectedSignature, testRelay.submittedMsgWithProofs.Bellatrix.Signature) require.Equal(t, uint64(25), testRelay.requestedSlot) // Clear the submitted message and check that the job will be ran again and but a new message will not be submitted since the hash is the same testEthService.testBlockValue = big.NewInt(10) - testRelay.submittedMsgWithPreconf = nil + testRelay.submittedMsgWithProofs = nil time.Sleep(2200 * time.Millisecond) - require.Nil(t, testRelay.submittedMsgWithPreconf) + require.Nil(t, testRelay.submittedMsgWithProofs) // Change the hash, expect to get the block testExecutableData.ExtraData = hexutil.MustDecode("0x0042fafd") testExecutableData.BlockHash = common.HexToHash("0x38456f6f1f5e76cf83c89ebb8606ff2b700bf02a86a165316c6d7a0c4e6a8614") - testBlock, err = engine.ExecutableDataToBlock(*testExecutableData, preconfTxWithBlob.BlobHashes(), nil) + testBlock, err = engine.ExecutableDataToBlock(*testExecutableData, constraintTxWithBlob.BlobHashes(), nil) testEthService.testBlockValue = big.NewInt(10) require.NoError(t, err) testEthService.testBlock = testBlock time.Sleep(2200 * time.Millisecond) - require.NotNil(t, testRelay.submittedMsgWithPreconf) + require.NotNil(t, testRelay.submittedMsgWithProofs) } func TestSubscribeProposerConstraints(t *testing.T) { @@ -461,7 +456,7 @@ func TestSubscribeProposerConstraints(t *testing.T) { for key := range cachedConstraints { _, ok := decodedConstraint[key] require.True(t, ok, fmt.Sprintf("Key %s found in cachedConstraints but not in decodedConstraint", key.String())) - require.Equal(t, cachedConstraints[key].Tx.Data(), decodedConstraint[key].Tx.Data(), "The decodedConstraint Tx should be equal to the cachedConstraints Tx") + require.Equal(t, cachedConstraints[key].Data(), decodedConstraint[key].Data(), "The decodedConstraint Tx should be equal to the cachedConstraints Tx") } for key := range decodedConstraint { _, ok := cachedConstraints[key] @@ -482,13 +477,6 @@ func sseConstraintsHandler(w http.ResponseWriter, r *http.Request) { return } - auth := r.Header.Get("Authorization") - _, err := validateConstraintSubscriptionAuth(auth, 0) - if err != nil { - http.Error(w, err.Error(), http.StatusUnauthorized) - return - } - for i := 0; i < 256; i++ { // Generate some duplicated constraints slot := uint64(i) % 32 @@ -504,63 +492,18 @@ func sseConstraintsHandler(w http.ResponseWriter, r *http.Request) { } // generateMockConstraintsForSlot generates a list of constraints for a given slot -func generateMockConstraintsForSlot(slot uint64) common.SignedConstraintsList { - rawTx := new(common.HexBytes) - err := rawTx.UnmarshalJSON([]byte("\"0x02f876018305da308401312d0085041f1196d2825208940c598786c88883ff5e4f461750fad64d3fae54268804b7ec32d7a2000080c080a0086f02eacec72820be3b117e1edd5bd7ed8956964b28b2d903d2cba53dd13560a06d61ec9ccce6acb31bf21878b9a844e7fdac860c5b7d684f7eb5f38a5945357c\"")) +func generateMockConstraintsForSlot(slot uint64) types.SignedConstraintsList { + rawTx := new(types.Transaction) + err := rawTx.UnmarshalBinary(common.Hex2Bytes("0x02f876018305da308401312d0085041f1196d2825208940c598786c88883ff5e4f461750fad64d3fae54268804b7ec32d7a2000080c080a0086f02eacec72820be3b117e1edd5bd7ed8956964b28b2d903d2cba53dd13560a06d61ec9ccce6acb31bf21878b9a844e7fdac860c5b7d684f7eb5f38a5945357c")) if err != nil { fmt.Println("Failed to unmarshal rawTx: ", err) } - return common.SignedConstraintsList{ - &common.SignedConstraints{ - Message: common.ConstraintMessage{ - Constraints: []*common.Constraint{{Tx: *rawTx}}, ValidatorIndex: 0, Slot: slot, + return types.SignedConstraintsList{ + &types.SignedConstraints{ + Message: types.ConstraintsMessage{ + Transactions: []*types.Transaction{rawTx}, Pubkey: phase0.BLSPubKey{}, Slot: slot, }, Signature: phase0.BLSSignature{}, }, } } - -// validateConstraintSubscriptionAuth checks the authentication string data from the Builder, -// and returns its BLS public key if the authentication is valid. -func validateConstraintSubscriptionAuth(auth string, headSlot uint64) (phase0.BLSPubKey, error) { - zeroKey := phase0.BLSPubKey{} - if auth == "" { - return zeroKey, errors.New("authorization header missing") - } - // Authorization: - parts := strings.Split(auth, " ") - if len(parts) != 2 { - return zeroKey, errors.New("ill-formed authorization header") - } - if parts[0] != "BOLT" { - return zeroKey, errors.New("not BOLT authentication scheme") - } - // , - parts = strings.SplitN(parts[1], ",", 2) - if len(parts) != 2 { - return zeroKey, errors.New("ill-formed authorization header") - } - - signature := new(phase0.BLSSignature) - if err := signature.UnmarshalJSON([]byte(parts[0])); err != nil { - fmt.Println("Failed to unmarshal authData: ", err) - return zeroKey, errors.New("ill-formed authorization header") - } - - authDataRaw := []byte(parts[1]) - authData := new(common.ConstraintSubscriptionAuth) - if err := json.Unmarshal(authDataRaw, authData); err != nil { - fmt.Println("Failed to unmarshal authData: ", err) - return zeroKey, errors.New("ill-formed authorization header") - } - - if headSlot != authData.Slot { - return zeroKey, errors.New("invalid head slot") - } - - ok, err := bls.VerifySignatureBytes(authDataRaw, signature[:], authData.PublicKey[:]) - if err != nil || !ok { - return zeroKey, errors.New("invalid signature") - } - return authData.PublicKey, nil -} diff --git a/builder/builder/eth_service.go b/builder/builder/eth_service.go index 4d692b02..03624cd7 100644 --- a/builder/builder/eth_service.go +++ b/builder/builder/eth_service.go @@ -31,7 +31,7 @@ type testEthereumService struct { testBundlesMerged []types.SimulatedBundle testAllBundles []types.SimulatedBundle testUsedSbundles []types.UsedSBundle - testPreconfs []*types.Transaction + testConstraints []*types.Transaction } func (t *testEthereumService) BuildBlock(attrs *types.BuilderPayloadAttributes, sealedBlockCallback miner.BlockHookFn, constraintsCache *shardmap.FIFOMap[uint64, types.HashToConstraintDecoded]) error { diff --git a/builder/builder/local_relay.go b/builder/builder/local_relay.go index ea4c8c46..94a07eb1 100644 --- a/builder/builder/local_relay.go +++ b/builder/builder/local_relay.go @@ -21,8 +21,8 @@ import ( "github.com/attestantio/go-eth2-client/spec/bellatrix" "github.com/attestantio/go-eth2-client/spec/phase0" eth2UtilBellatrix "github.com/attestantio/go-eth2-client/util/bellatrix" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" "github.com/flashbots/go-boost-utils/bls" "github.com/flashbots/go-boost-utils/ssz" @@ -117,7 +117,7 @@ func (r *LocalRelay) SubmitBlock(msg *builderSpec.VersionedSubmitBlockRequest, _ return r.submitBlock(msg.Bellatrix) } -func (r *LocalRelay) SubmitBlockWithProofs(msg *common.VersionedSubmitBlockRequestWithProofs, _ ValidatorData) error { +func (r *LocalRelay) SubmitBlockWithProofs(msg *types.VersionedSubmitBlockRequestWithProofs, _ ValidatorData) error { panic("Not implemented!") } @@ -235,6 +235,10 @@ func (r *LocalRelay) GetValidatorForSlot(nextSlot uint64) (ValidatorData, error) return ValidatorData{}, errors.New("missing validator") } +func (r *LocalRelay) GetDelegationsForSlot(nextSlot uint64) (types.SignedDelegations, error) { + return types.SignedDelegations{}, nil +} + func (r *LocalRelay) handleGetHeader(w http.ResponseWriter, req *http.Request) { vars := mux.Vars(req) slot, err := strconv.Atoi(vars["slot"]) diff --git a/builder/builder/relay.go b/builder/builder/relay.go index ef002f6b..865d3359 100644 --- a/builder/builder/relay.go +++ b/builder/builder/relay.go @@ -11,7 +11,7 @@ import ( builderSpec "github.com/attestantio/go-builder-client/spec" "github.com/attestantio/go-eth2-client/spec" - "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" "github.com/flashbots/go-boost-utils/utils" ) @@ -129,6 +129,32 @@ func (r *RemoteRelay) GetValidatorForSlot(nextSlot uint64) (ValidatorData, error return ValidatorData{}, ErrValidatorNotFound } +func (r *RemoteRelay) GetDelegationsForSlot(nextSlot uint64) (types.SignedDelegations, error) { + log.Info("Getting delegations for slot", "slot", nextSlot, "endpoint", r.config.Endpoint) + endpoint := r.config.Endpoint + fmt.Sprintf("/relay/v1/builder/delegations?slot=%d", nextSlot) + + if r.config.SszEnabled { + panic("ssz not supported") + } + + // BOLT: Add 2s timeout to request + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + var dst types.SignedDelegations + code, err := SendHTTPRequest(ctx, *http.DefaultClient, http.MethodGet, endpoint, nil, &dst) + if err != nil { + return nil, fmt.Errorf("error getting delegations from relay %s. err: %w", r.config.Endpoint, err) + } + + if code > 299 { + return nil, fmt.Errorf("non-ok response code %d from relay", code) + } + + return dst, nil + +} + func (r *RemoteRelay) Start() error { return nil } @@ -184,8 +210,8 @@ func (r *RemoteRelay) SubmitBlock(msg *builderSpec.VersionedSubmitBlockRequest, return nil } -func (r *RemoteRelay) SubmitBlockWithProofs(msg *common.VersionedSubmitBlockRequestWithProofs, _ ValidatorData) error { - log.Info("submitting block with proofs to remote relay", "endpoint", r.config.Endpoint) +func (r *RemoteRelay) SubmitBlockWithProofs(msg *types.VersionedSubmitBlockRequestWithProofs, _ ValidatorData) error { + log.Info("submitting block with constraint inclusion proofs to remote relay", "endpoint", r.config.Endpoint) endpoint := r.config.Endpoint + "/relay/v1/builder/blocks_with_proofs" if r.cancellationsEnabled { endpoint = endpoint + "?cancellations=1" @@ -194,18 +220,15 @@ func (r *RemoteRelay) SubmitBlockWithProofs(msg *common.VersionedSubmitBlockRequ var code int var err error if r.config.SszEnabled { - panic("ssz not supported for preconfs proofs yet") + panic("ssz not supported for constraint proofs yet") } else { - - // BOLT: send event to web demo if len(msg.Proofs.TransactionHashes) > 0 { - number, _ := msg.Inner.BlockNumber() + number, _ := msg.BlockNumber() message := fmt.Sprintf("sending block %d with proofs to relay (path: %s)", number, "/relay/v1/builder/blocks_with_proofs") log.Info(message) - EmitBoltDemoEvent(message) } - switch msg.Inner.Version { + switch msg.Version { case spec.DataVersionBellatrix: code, err = SendHTTPRequest(context.TODO(), *http.DefaultClient, http.MethodPost, endpoint, msg, nil) case spec.DataVersionCapella: @@ -213,7 +236,7 @@ func (r *RemoteRelay) SubmitBlockWithProofs(msg *common.VersionedSubmitBlockRequ case spec.DataVersionDeneb: code, err = SendHTTPRequest(context.TODO(), *http.DefaultClient, http.MethodPost, endpoint, msg, nil) default: - return fmt.Errorf("unknown data version %d", msg.Inner.Version) + return fmt.Errorf("unknown data version %d", msg.Version) } } diff --git a/builder/builder/relay_aggregator.go b/builder/builder/relay_aggregator.go index 4655ebe1..4ceb4dc9 100644 --- a/builder/builder/relay_aggregator.go +++ b/builder/builder/relay_aggregator.go @@ -6,7 +6,7 @@ import ( "sync" builderSpec "github.com/attestantio/go-builder-client/spec" - "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" ) @@ -61,7 +61,7 @@ func (r *RemoteRelayAggregator) SubmitBlock(msg *builderSpec.VersionedSubmitBloc return nil } -func (r *RemoteRelayAggregator) SubmitBlockWithProofs(msg *common.VersionedSubmitBlockRequestWithProofs, registration ValidatorData) error { +func (r *RemoteRelayAggregator) SubmitBlockWithProofs(msg *types.VersionedSubmitBlockRequestWithProofs, registration ValidatorData) error { r.registrationsCacheLock.RLock() defer r.registrationsCacheLock.RUnlock() @@ -81,6 +81,56 @@ func (r *RemoteRelayAggregator) SubmitBlockWithProofs(msg *common.VersionedSubmi return nil } +func (r *RemoteRelayAggregator) GetDelegationsForSlot(nextSlot uint64) (types.SignedDelegations, error) { + delegationsCh := make(chan *types.SignedDelegations, len(r.relays)) + + for i, relay := range r.relays { + go func(relay IRelay, relayI int) { + delegations, err := relay.GetDelegationsForSlot(nextSlot) + if err != nil { + // Send nil to channel to indicate error + log.Error("could not get delegations", "err", err, "relay", relay.Config().Endpoint) + delegationsCh <- nil + } + + delegationsCh <- &delegations + }(relay, i) + } + + err := errors.New("could not get delegations from any relay") + + aggregated := types.SignedDelegations{} + for i := 0; i < len(r.relays); i++ { + d := <-delegationsCh + + if d != nil { + err = nil + + // Check if the delegations array already contains the delegations, if not add them + for _, delegation := range *d { + found := false + for _, existing := range aggregated { + if existing == delegation { + found = true + break + } + } + + if !found { + aggregated = append(aggregated, delegation) + } + } + } + } + + // If we still have an error, return error to caller + if err != nil { + return nil, err + } + + return aggregated, nil +} + type RelayValidatorRegistration struct { vd ValidatorData relayI int // index into relays array to preserve relative order diff --git a/builder/builder/relay_aggregator_test.go b/builder/builder/relay_aggregator_test.go index d6eebed9..c5a7a1e5 100644 --- a/builder/builder/relay_aggregator_test.go +++ b/builder/builder/relay_aggregator_test.go @@ -8,7 +8,7 @@ import ( builderApiBellatrix "github.com/attestantio/go-builder-client/api/bellatrix" builderSpec "github.com/attestantio/go-builder-client/spec" "github.com/attestantio/go-eth2-client/spec" - "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/stretchr/testify/require" ) @@ -23,11 +23,11 @@ type testRelay struct { gvsVd ValidatorData gvsErr error - requestedSlot uint64 - submittedMsg *builderSpec.VersionedSubmitBlockRequest - submittedMsgWithPreconf *common.VersionedSubmitBlockRequestWithProofs - submittedMsgCh chan *builderSpec.VersionedSubmitBlockRequest - submittedMsgWithPreconfCh chan *common.VersionedSubmitBlockRequestWithProofs + requestedSlot uint64 + submittedMsg *builderSpec.VersionedSubmitBlockRequest + submittedMsgWithProofs *types.VersionedSubmitBlockRequestWithProofs + submittedMsgCh chan *builderSpec.VersionedSubmitBlockRequest + submittedMsgWithProofsCh chan *types.VersionedSubmitBlockRequestWithProofs } type testRelayAggBackend struct { @@ -59,17 +59,21 @@ func (r *testRelay) SubmitBlock(msg *builderSpec.VersionedSubmitBlockRequest, re return r.sbError } -func (r *testRelay) SubmitBlockWithProofs(msg *common.VersionedSubmitBlockRequestWithProofs, vd ValidatorData) error { - if r.submittedMsgWithPreconfCh != nil { +func (r *testRelay) SubmitBlockWithProofs(msg *types.VersionedSubmitBlockRequestWithProofs, vd ValidatorData) error { + if r.submittedMsgWithProofsCh != nil { select { - case r.submittedMsgWithPreconfCh <- msg: + case r.submittedMsgWithProofsCh <- msg: default: } } - r.submittedMsgWithPreconf = msg + r.submittedMsgWithProofs = msg return r.sbError } +func (r *testRelay) GetDelegationsForSlot(nextSlot uint64) (types.SignedDelegations, error) { + return types.SignedDelegations{}, nil +} + func (r *testRelay) GetValidatorForSlot(nextSlot uint64) (ValidatorData, error) { r.requestedSlot = nextSlot return r.gvsVd, r.gvsErr diff --git a/builder/builder/utils.go b/builder/builder/utils.go index 59bd040d..f1d4672e 100644 --- a/builder/builder/utils.go +++ b/builder/builder/utils.go @@ -11,7 +11,6 @@ import ( "math" "net/http" "slices" - "strings" "time" "github.com/attestantio/go-eth2-client/spec/bellatrix" @@ -24,14 +23,10 @@ import ( var errHTTPErrorResponse = errors.New("HTTP error response") -func DecodeConstraints(constraints *common.SignedConstraints) (types.HashToConstraintDecoded, error) { +func DecodeConstraints(constraints *types.SignedConstraints) (types.HashToConstraintDecoded, error) { decodedConstraints := make(types.HashToConstraintDecoded) - for _, tx := range constraints.Message.Constraints { - decoded := new(types.Transaction) - if err := decoded.UnmarshalBinary(tx.Tx); err != nil { - return nil, err - } - decodedConstraints[decoded.Hash()] = &types.ConstraintDecoded{Index: tx.Index, Tx: decoded} + for _, tx := range constraints.Message.Transactions { + decodedConstraints[tx.Hash()] = tx } return decodedConstraints, nil } @@ -141,25 +136,11 @@ func SendHTTPRequest(ctx context.Context, client http.Client, method, url string return resp.StatusCode, nil } -// EmitBoltDemoEvent sends a message to the web demo backend to log an event. -// This is only used for demo purposes and should be removed in production. -func EmitBoltDemoEvent(message string) { - event := strings.NewReader(fmt.Sprintf("{ \"message\": \"BOLT-BUILDER: %s\"}", message)) - eventRes, err := http.Post("http://host.docker.internal:3001/events", "application/json", event) - if err != nil { - log.Error("Failed to send web demo event: ", err) - } - if eventRes != nil { - defer eventRes.Body.Close() - } -} - func CalculateMerkleMultiProofs( payloadTransactions types.Transactions, HashToConstraintDecoded types.HashToConstraintDecoded, -) (inclusionProof *common.InclusionProof, rootNode *ssz.Node, err error) { - constraintsOrderedByIndex, constraintsWithoutIndex, _, _ := types.ParseConstraintsDecoded(HashToConstraintDecoded) - constraints := slices.Concat(constraintsOrderedByIndex, constraintsWithoutIndex) +) (inclusionProof *types.InclusionProof, rootNode *ssz.Node, err error) { + constraints, _, _ := types.ParseConstraintsDecoded(HashToConstraintDecoded) // BOLT: generate merkle tree from payload transactions (we need raw RLP bytes for this) rawTxs := make([]bellatrix.Transaction, len(payloadTransactions)) @@ -184,39 +165,38 @@ func CalculateMerkleMultiProofs( // to output the leaf correctly. This is also never documented in fastssz. -__- rootNode.Hash() - // using our gen index formula: 2 * 2^21 + preconfIndex + // using our gen index formula: 2 * 2^21 + constraintIndex baseGeneralizedIndex := int(math.Pow(float64(2), float64(21))) generalizedIndexes := make([]int, len(constraints)) transactionHashes := make([]common.Hash, len(constraints)) for i, constraint := range constraints { - tx := constraint.Tx - // get the index of the preconfirmed transaction in the block - preconfIndex := slices.IndexFunc(payloadTransactions, func(payloadTx *types.Transaction) bool { return payloadTx.Hash() == tx.Hash() }) - if preconfIndex == -1 { - log.Error(fmt.Sprintf("Preconfirmed transaction %s not found in block", tx.Hash())) + tx := constraint + // get the index of the committed transaction in the block + committedIndex := slices.IndexFunc(payloadTransactions, func(payloadTx *types.Transaction) bool { return payloadTx.Hash() == tx.Hash() }) + if committedIndex == -1 { + log.Error(fmt.Sprintf("Committed transaction %s not found in block", tx.Hash())) log.Error(fmt.Sprintf("block has %v transactions", len(payloadTransactions))) continue } - generalizedIndex := baseGeneralizedIndex + preconfIndex + generalizedIndex := baseGeneralizedIndex + committedIndex generalizedIndexes[i] = generalizedIndex transactionHashes[i] = tx.Hash() } - log.Info(fmt.Sprintf("[BOLT]: Calculating merkle multiproof for %d preconfirmed transaction", - len(constraints))) + log.Info(fmt.Sprintf("[BOLT]: Calculating merkle multiproof for %d committed transactions", len(constraints))) timeStart := time.Now() multiProof, err := rootNode.ProveMulti(generalizedIndexes) if err != nil { - return nil, nil, fmt.Errorf("could not calculate merkle multiproof for %d preconf: %w", len(constraints), err) + return nil, nil, fmt.Errorf("could not calculate merkle multiproof for %d transactions: %w", len(constraints), err) } timeForProofs := time.Since(timeStart) - log.Info(fmt.Sprintf("[BOLT]: Calculated merkle multiproof for %d preconf in %s", len(constraints), timeForProofs)) + log.Info(fmt.Sprintf("[BOLT]: Calculated merkle multiproof for %d transactions in %s", len(constraints), timeForProofs)) - inclusionProof = common.InclusionProofFromMultiProof(multiProof) + inclusionProof = types.InclusionProofFromMultiProof(multiProof) inclusionProof.TransactionHashes = transactionHashes return diff --git a/builder/builder/utils_test.go b/builder/builder/utils_test.go index 76e6cfdf..f4af130d 100644 --- a/builder/builder/utils_test.go +++ b/builder/builder/utils_test.go @@ -57,7 +57,7 @@ func TestGenerateMerkleMultiProofs(t *testing.T) { constraints := make(types.HashToConstraintDecoded) for _, tx := range chosenConstraintTransactions { - constraints[tx.Hash()] = &types.ConstraintDecoded{Tx: tx} + constraints[tx.Hash()] = tx } inclusionProof, root, err := CalculateMerkleMultiProofs(payloadTransactions, constraints) @@ -67,15 +67,15 @@ func TestGenerateMerkleMultiProofs(t *testing.T) { leaves := make([][]byte, len(constraints)) i := 0 - for _, constraint := range constraints { - if constraint == nil || constraint.Tx == nil { - t.Logf("nil constraint or transaction!") + for _, tx := range constraints { + if tx == nil { + t.Logf("nil constraint transaction!") } - // Compute the hash tree root for the raw preconfirmed transaction + // Compute the hash tree root for the raw committed transaction // and use it as "Leaf" in the proof to be verified against - withoutBlob, err := constraint.Tx.WithoutBlobTxSidecar().MarshalBinary() + withoutBlob, err := tx.WithoutBlobTxSidecar().MarshalBinary() if err != nil { t.Logf("error marshalling transaction without blob tx sidecar: %v", err) } diff --git a/builder/common/types.go b/builder/common/types.go index 21dff977..b4dc1872 100644 --- a/builder/common/types.go +++ b/builder/common/types.go @@ -31,14 +31,6 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "golang.org/x/crypto/sha3" - - "github.com/attestantio/go-builder-client/api/bellatrix" - "github.com/attestantio/go-builder-client/api/capella" - "github.com/attestantio/go-builder-client/api/deneb" - builderSpec "github.com/attestantio/go-builder-client/spec" - consensusSpec "github.com/attestantio/go-eth2-client/spec" - "github.com/attestantio/go-eth2-client/spec/phase0" - fastSsz "github.com/ferranbt/fastssz" ) // Lengths of hashes and addresses in bytes. @@ -514,123 +506,3 @@ func (s *HexBytes) UnmarshalJSON(input []byte) error { return nil } - -// InclusionProof is a Merkle Multiproof of inclusion of a set of TransactionHashes -type InclusionProof struct { - TransactionHashes []Hash `json:"transaction_hashes"` - GeneralizedIndexes []uint64 `json:"generalized_indexes"` - MerkleHashes []*HexBytes `json:"merkle_hashes"` -} - -// InclusionProofFromMultiProof converts a fastssz.Multiproof into an InclusionProof, without -// filling the TransactionHashes -func InclusionProofFromMultiProof(mp *fastSsz.Multiproof) *InclusionProof { - merkleHashes := make([]*HexBytes, len(mp.Hashes)) - for i, h := range mp.Hashes { - merkleHashes[i] = new(HexBytes) - *(merkleHashes[i]) = h - } - - leaves := make([]*HexBytes, len(mp.Leaves)) - for i, h := range mp.Leaves { - leaves[i] = new(HexBytes) - *(leaves[i]) = h - } - generalIndexes := make([]uint64, len(mp.Indices)) - for i, idx := range mp.Indices { - generalIndexes[i] = uint64(idx) - } - return &InclusionProof{ - MerkleHashes: merkleHashes, - GeneralizedIndexes: generalIndexes, - } -} - -func (p *InclusionProof) String() string { - return JSONStringify(p) -} - -// A wrapper struct over `builderSpec.VersionedSubmitBlockRequest` -// to include preconfirmation proofs -type VersionedSubmitBlockRequestWithProofs struct { - Inner *builderSpec.VersionedSubmitBlockRequest `json:"inner"` - Proofs *InclusionProof `json:"proofs"` -} - -// this is necessary, because the mev-boost-relay deserialization doesn't expect a "Version" and "Data" wrapper object -// for deserialization. Instead, it tries to decode the object into the "Deneb" version first and if that fails, it tries -// the "Capella" version. This is a workaround to make the deserialization work. -func (v *VersionedSubmitBlockRequestWithProofs) MarshalJSON() ([]byte, error) { - switch v.Inner.Version { - case consensusSpec.DataVersionBellatrix: - return json.Marshal(struct { - Inner *bellatrix.SubmitBlockRequest `json:"inner"` - Proofs *InclusionProof `json:"proofs"` - }{ - Inner: v.Inner.Bellatrix, - Proofs: v.Proofs, - }) - case consensusSpec.DataVersionCapella: - return json.Marshal(struct { - Inner *capella.SubmitBlockRequest `json:"inner"` - Proofs *InclusionProof `json:"proofs"` - }{ - Inner: v.Inner.Capella, - Proofs: v.Proofs, - }) - case consensusSpec.DataVersionDeneb: - return json.Marshal(struct { - Inner *deneb.SubmitBlockRequest `json:"inner"` - Proofs *InclusionProof `json:"proofs"` - }{ - Inner: v.Inner.Deneb, - Proofs: v.Proofs, - }) - } - - return nil, fmt.Errorf("unknown data version %d", v.Inner.Version) -} - -func (v *VersionedSubmitBlockRequestWithProofs) String() string { - return JSONStringify(v) -} - -// SignedConstraintsList are a list of proposer constraints that a builder must satisfy -// in order to produce a valid bid. This is not defined on the -// [spec](https://chainbound.github.io/bolt-docs/api/builder-api) -// but it's useful as an helper type -type SignedConstraintsList = []*SignedConstraints - -// Reference: https://chainbound.github.io/bolt-docs/api/builder-api -type SignedConstraints struct { - Message ConstraintMessage `json:"message"` - Signature phase0.BLSSignature `json:"signature"` -} - -// Reference: https://chainbound.github.io/bolt-docs/api/builder-api -type ConstraintMessage struct { - Constraints []*Constraint `json:"constraints"` - ValidatorIndex uint64 `json:"validator_index"` - Slot uint64 `json:"slot"` -} - -// Reference: https://chainbound.github.io/bolt-docs/api/builder-api -type Constraint struct { - Index *uint64 `json:"index"` - Tx HexBytes `json:"tx"` -} - -// ConstraintSubscriptionAuth is the struct the builder signs over to authenticate -// when subscribing to SSE constraint events from the relay -type ConstraintSubscriptionAuth struct { - PublicKey phase0.BLSPubKey `json:"publicKey"` - Slot uint64 `json:"slot"` -} - -func (c *ConstraintSubscriptionAuth) String() string { - buf, err := json.Marshal(c) - if err != nil { - return fmt.Sprintf("failed to marshal ConstraintSubscriptionAuth: %v", err) - } - return string(buf) -} diff --git a/builder/common/utils.go b/builder/common/utils.go index 50ee385e..2a1258aa 100644 --- a/builder/common/utils.go +++ b/builder/common/utils.go @@ -31,6 +31,13 @@ func Filter[T any](slice *[]*T, predicate func(el *T) bool) { } } +func Last[T any](slice []*T) *T { + if len(slice) == 0 { + return nil + } + return slice[len(slice)-1] +} + func Pop[T any](slice *[]*T) *T { if slice == nil || len(*slice) == 0 { return nil diff --git a/builder/core/types/constraints.go b/builder/core/types/constraints.go index e587b475..39c7caec 100644 --- a/builder/core/types/constraints.go +++ b/builder/core/types/constraints.go @@ -1,62 +1,302 @@ package types import ( + "crypto/sha256" + "encoding/binary" + "encoding/json" + "fmt" "sort" + builderSpec "github.com/attestantio/go-builder-client/spec" + consensusSpec "github.com/attestantio/go-eth2-client/spec" + bellatrixSpec "github.com/attestantio/go-eth2-client/spec/bellatrix" + capellaSpec "github.com/attestantio/go-eth2-client/spec/capella" + denebSpec "github.com/attestantio/go-eth2-client/spec/deneb" + + v1 "github.com/attestantio/go-builder-client/api/v1" + "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/ethereum/go-ethereum/common" + ssz "github.com/ferranbt/fastssz" + "github.com/flashbots/go-boost-utils/bls" +) + +var ( + // ConstraintsDomainType is the expected signing domain mask for constraints-API related messages + ConstraintsDomainType = phase0.DomainType([4]byte{109, 109, 111, 67}) ) -// NOTE: not the greatest place for this type but given that it uses -// `common.Hash`, `Transaction` and it's used in both the builder -// package and the miner package, here it's a good place for now +// NOTE: given that it uses `common.Hash`, `Transaction` and it's used in both +// the builder package and the miner package, here it's a good place for now type ( - HashToConstraintDecoded = map[common.Hash]*ConstraintDecoded - ConstraintDecoded struct { - Index *uint64 - Tx *Transaction + HashToConstraintDecoded = map[common.Hash]*Transaction + TransactionEcRecovered = struct { + Transaction *Transaction + Sender common.Address } ) // ParseConstraintsDecoded receives a map of constraints and returns -// - a slice of constraints sorted by index -// - a slice of constraints without index sorted by nonce and hash +// - a slice of constraints sorted by nonce descending and hash descending // - the total gas required by the constraints // - the total blob gas required by the constraints -func ParseConstraintsDecoded(constraints HashToConstraintDecoded) ([]*ConstraintDecoded, []*ConstraintDecoded, uint64, uint64) { +func ParseConstraintsDecoded(constraints HashToConstraintDecoded) ([]*Transaction, uint64, uint64) { // Here we initialize and track the constraints left to be executed along // with their gas requirements - constraintsOrderedByIndex := make([]*ConstraintDecoded, 0, len(constraints)) - constraintsWithoutIndex := make([]*ConstraintDecoded, 0, len(constraints)) + constraintsOrdered := make([]*Transaction, 0, len(constraints)) constraintsTotalGasLeft := uint64(0) constraintsTotalBlobGasLeft := uint64(0) for _, constraint := range constraints { - if constraint.Index == nil { - constraintsWithoutIndex = append(constraintsWithoutIndex, constraint) - } else { - constraintsOrderedByIndex = append(constraintsOrderedByIndex, constraint) - } - constraintsTotalGasLeft += constraint.Tx.Gas() - constraintsTotalBlobGasLeft += constraint.Tx.BlobGas() + constraintsOrdered = append(constraintsOrdered, constraint) + constraintsTotalGasLeft += constraint.Gas() + constraintsTotalBlobGasLeft += constraint.BlobGas() } - // Sorts the constraints by index ascending - sort.Slice(constraintsOrderedByIndex, func(i, j int) bool { - // By assumption, all constraints here have a non-nil index - return *constraintsOrderedByIndex[i].Index < *constraintsOrderedByIndex[j].Index - }) - // Sorts the unindexed constraints by nonce ascending and by hash - sort.Slice(constraintsWithoutIndex, func(i, j int) bool { - iNonce := constraintsWithoutIndex[i].Tx.Nonce() - jNonce := constraintsWithoutIndex[j].Tx.Nonce() + sort.Slice(constraintsOrdered, func(i, j int) bool { + iNonce := constraintsOrdered[i].Nonce() + jNonce := constraintsOrdered[j].Nonce() // Sort by hash if iNonce == jNonce { - return constraintsWithoutIndex[i].Tx.Hash().Cmp(constraintsWithoutIndex[j].Tx.Hash()) < 0 + return constraintsOrdered[i].Hash().Cmp(constraintsOrdered[j].Hash()) > 0 // descending } - return iNonce < jNonce + return iNonce > jNonce // descending }) - return constraintsOrderedByIndex, constraintsWithoutIndex, constraintsTotalGasLeft, constraintsTotalBlobGasLeft + return constraintsOrdered, constraintsTotalGasLeft, constraintsTotalBlobGasLeft +} + +// InclusionProof is a Merkle Multiproof of inclusion of a set of TransactionHashes +type InclusionProof struct { + TransactionHashes []common.Hash `json:"transaction_hashes"` + GeneralizedIndexes []uint64 `json:"generalized_indexes"` + MerkleHashes []*common.HexBytes `json:"merkle_hashes"` +} + +// InclusionProofFromMultiProof converts a fastssz.Multiproof into an InclusionProof, without +// filling the TransactionHashes +func InclusionProofFromMultiProof(mp *ssz.Multiproof) *InclusionProof { + merkleHashes := make([]*common.HexBytes, len(mp.Hashes)) + for i, h := range mp.Hashes { + merkleHashes[i] = new(common.HexBytes) + *(merkleHashes[i]) = h + } + + leaves := make([]*common.HexBytes, len(mp.Leaves)) + for i, h := range mp.Leaves { + leaves[i] = new(common.HexBytes) + *(leaves[i]) = h + } + generalIndexes := make([]uint64, len(mp.Indices)) + for i, idx := range mp.Indices { + generalIndexes[i] = uint64(idx) + } + return &InclusionProof{ + MerkleHashes: merkleHashes, + GeneralizedIndexes: generalIndexes, + } +} + +func (p *InclusionProof) String() string { + return common.JSONStringify(p) +} + +// A wrapper struct over `builderSpec.VersionedSubmitBlockRequest` +// to include constraint inclusion proofs +type VersionedSubmitBlockRequestWithProofs struct { + Proofs *InclusionProof + *builderSpec.VersionedSubmitBlockRequest +} + +// this is necessary, because the mev-boost-relay deserialization doesn't expect a "Version" and "Data" wrapper object +// for deserialization. Instead, it tries to decode the object into the "Deneb" version first and if that fails, it tries +// the "Capella" version. This is a workaround to make the deserialization work. +// +// NOTE(bolt): struct embedding of the VersionedSubmitBlockRequest is not possible for some reason because it causes the json +// encoding to omit the `proofs` field. Embedding all of the fields directly does the job. +func (v *VersionedSubmitBlockRequestWithProofs) MarshalJSON() ([]byte, error) { + switch v.Version { + case consensusSpec.DataVersionBellatrix: + return json.Marshal(struct { + Message *v1.BidTrace `json:"message"` + ExecutionPayload *bellatrixSpec.ExecutionPayload `json:"execution_payload"` + Signature phase0.BLSSignature `json:"signature"` + Proofs *InclusionProof `json:"proofs"` + }{ + Message: v.Bellatrix.Message, + ExecutionPayload: v.Bellatrix.ExecutionPayload, + Signature: v.Bellatrix.Signature, + Proofs: v.Proofs, + }) + case consensusSpec.DataVersionCapella: + return json.Marshal(struct { + Message *v1.BidTrace `json:"message"` + ExecutionPayload *capellaSpec.ExecutionPayload `json:"execution_payload"` + Signature phase0.BLSSignature `json:"signature"` + Proofs *InclusionProof `json:"proofs"` + }{ + Message: v.Capella.Message, + ExecutionPayload: v.Capella.ExecutionPayload, + Signature: v.Capella.Signature, + Proofs: v.Proofs, + }) + case consensusSpec.DataVersionDeneb: + return json.Marshal(struct { + Message *v1.BidTrace `json:"message"` + ExecutionPayload *denebSpec.ExecutionPayload `json:"execution_payload"` + Signature phase0.BLSSignature `json:"signature"` + Proofs *InclusionProof `json:"proofs"` + }{ + Message: v.Deneb.Message, + ExecutionPayload: v.Deneb.ExecutionPayload, + Signature: v.Deneb.Signature, + Proofs: v.Proofs, + }) + } + + return nil, fmt.Errorf("unknown data version %d", v.Version) +} + +func (v *VersionedSubmitBlockRequestWithProofs) String() string { + return common.JSONStringify(v) +} + +// SignedConstraintsList are a list of proposer constraints that a builder must satisfy +// in order to produce a valid bid. This is not defined on the +// [spec](https://chainbound.github.io/bolt-docs/api/builder) +// but it's useful as an helper type +type SignedConstraintsList = []*SignedConstraints + +// Reference: https://chainbound.github.io/bolt-docs/api/builder +type SignedConstraints struct { + Message ConstraintsMessage `json:"message"` + Signature phase0.BLSSignature `json:"signature"` +} + +// Reference: https://chainbound.github.io/bolt-docs/api/builder +type ConstraintsMessage struct { + Pubkey phase0.BLSPubKey `json:"pubkey"` + Slot uint64 `json:"slot"` + Top bool `json:"top"` + Transactions []*Transaction // Custom marshal and unmarshal implemented below +} + +func (c *ConstraintsMessage) MarshalJSON() ([]byte, error) { + transactionBytes := make([]common.HexBytes, len(c.Transactions)) + for i, tx := range c.Transactions { + bytes, err := tx.MarshalBinary() + if err != nil { + return nil, err + } + + transactionBytes[i] = bytes + } + + type Alias ConstraintsMessage + return json.Marshal(&struct { + *Alias + Transactions []common.HexBytes `json:"transactions"` + }{ + Alias: (*Alias)(c), + Transactions: transactionBytes, + }) +} + +func (c *ConstraintsMessage) UnmarshalJSON(data []byte) error { + type Alias ConstraintsMessage + aux := &struct { + Transactions []common.HexBytes `json:"transactions"` + *Alias + }{ + Alias: (*Alias)(c), + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + c.Transactions = make([]*Transaction, len(aux.Transactions)) + for i, txBytes := range aux.Transactions { + tx := new(Transaction) + if err := tx.UnmarshalBinary(txBytes); err != nil { + return err + } + + c.Transactions[i] = tx + } + + return nil +} + +// Digest returns the sha256 digest of the constraints message. This is what needs to be signed. +func (c *SignedConstraints) Digest() []byte { + hasher := sha256.New() + // NOTE: ignoring errors here + slotBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(slotBytes, c.Message.Slot) + + var top byte + if c.Message.Top { + top = 1 + } else { + top = 0 + } + + hasher.Write(c.Message.Pubkey[:]) + hasher.Write(slotBytes) + hasher.Write([]byte{top}) + + for _, tx := range c.Message.Transactions { + hasher.Write(tx.Hash().Bytes()) + + } + + return hasher.Sum(nil) +} + +// VerifySignature verifies the signature of a signed constraints message. IMPORTANT: it uses the Bolt signing domain to +// verify the signature. +func (c *SignedConstraints) VerifySignature(pubkey phase0.BLSPubKey, domain phase0.Domain) (bool, error) { + signingData := phase0.SigningData{ObjectRoot: phase0.Root(c.Digest()), Domain: domain} + root, err := signingData.HashTreeRoot() + if err != nil { + return false, err + } + + return bls.VerifySignatureBytes(root[:], c.Signature[:], pubkey[:]) +} + +// List of signed delegations +type SignedDelegations = []*SignedDelegation + +type SignedDelegation struct { + Message Delegation `json:"message"` + Signature phase0.BLSSignature `json:"signature"` +} + +type Delegation struct { + ValidatorPubkey phase0.BLSPubKey `json:"validator_pubkey"` + DelegateePubkey phase0.BLSPubKey `json:"delegatee_pubkey"` +} + +// Digest returns the sha256 digest of the delegation. This is what needs to be signed. +func (d *SignedDelegation) Digest() []byte { + hasher := sha256.New() + // NOTE: ignoring errors here + hasher.Write(d.Message.ValidatorPubkey[:]) + hasher.Write(d.Message.DelegateePubkey[:]) + return hasher.Sum(nil) +} + +// VerifySignature verifies the signature of a signed delegation. IMPORTANT: it uses the Bolt signing domain to +// verify the signature. +func (d *SignedDelegation) VerifySignature(pubkey phase0.BLSPubKey, domain phase0.Domain) (bool, error) { + signingData := phase0.SigningData{ObjectRoot: phase0.Root(d.Digest()), Domain: domain} + root, err := signingData.HashTreeRoot() + if err != nil { + return false, err + } + + return bls.VerifySignatureBytes(root[:], d.Signature[:], pubkey[:]) } diff --git a/builder/miner/worker.go b/builder/miner/worker.go index c845edbd..da4bee78 100644 --- a/builder/miner/worker.go +++ b/builder/miner/worker.go @@ -1033,11 +1033,20 @@ func (w *worker) commitTransactions(env *environment, plainTxs, blobTxs *transac // Here we initialize and track the constraints left to be executed along // with their gas requirements - constraintsOrderedByIndex, - constraintsWithoutIndex, + constraintsOrderedByNonceAndHashDesc, constraintsTotalGasLeft, constraintsTotalBlobGasLeft := types.ParseConstraintsDecoded(constraints) + constraintsRecoveredOrderedByNonceAndHashDesc := make([]*types.TransactionEcRecovered, 0, len(constraintsOrderedByNonceAndHashDesc)) + for _, tx := range constraintsOrderedByNonceAndHashDesc { + // Error may be ignored here, see assumption + from, _ := types.Sender(env.signer, tx) + constraintsRecoveredOrderedByNonceAndHashDesc = append(constraintsRecoveredOrderedByNonceAndHashDesc, &types.TransactionEcRecovered{ + Transaction: tx, + Sender: from, + }) + } + for { // `env.tcount` starts from 0 so it's correct to use it as the current index currentTxIndex := uint64(env.tcount) @@ -1106,68 +1115,77 @@ func (w *worker) commitTransactions(env *environment, plainTxs, blobTxs *transac isConstraint bool } - var constraintTx *types.ConstraintDecoded - if len(constraintsOrderedByIndex) > 0 { - constraintTx = constraintsOrderedByIndex[0] - } - isSomePoolTxLeft := lazyTx != nil + var from common.Address + + if isSomePoolTxLeft { + // Check if there enough gas left for this tx + if constraintsTotalGasLeft+lazyTx.Gas > env.gasPool.Gas() || constraintsTotalBlobGasLeft+lazyTx.BlobGas > blobGasLeft { + // Skip this tx and try to fit one with less gas. + // Drop all consecutive transactions from the same sender because of `nonce-too-high` clause. + log.Debug("Could not find transactions gas with the remaining constraints, account skipped", "hash", lazyTx.Hash) + txs.Pop() + // Edge case: + // + // Assumption: suppose sender A sends tx T_1 with nonce 1, and T_2 with nonce 2, and T_2 is a constraint. + // + // + // When running the block building algorithm I first have to make sure to reserve enough gas for the constraints. + // This implies that when a pooled tx comes I have to check if there is enough gas for it while taking into account + // the rest of the remaining constraint gas to allocate. + // Suppose there is no gas for the pooled tx T_1, then I have to drop it and consequently drop every tx from the same + // sender with higher nonce due to "nonce-too-high" issues, including T_2. + // But then, I have dropped a constraint which means my bid is invalid. + // + // NOTE: this actually cannot happen because the sidecar accept constraints considering the previous block + // state and not pending transactions. So this setting would be rejected by the sidecar with `NonceTooHigh` + // error. A scenario like T_1 is a constraint and T_2 is not is possible instead and correctly handled (see below). + + // Repeat the loop to try to find another pool transaction + continue + } - isThereConstraintWithThisIndex := constraintTx != nil && constraintTx.Index != nil && *constraintTx.Index == currentTxIndex - if isThereConstraintWithThisIndex { - // we retrieve the candidate constraint by shifting it from the list - candidate = candidateTx{tx: common.Shift(&constraintsOrderedByIndex).Tx, isConstraint: true} - } else { - if isSomePoolTxLeft { - // Check if there enough gas left for this tx - if constraintsTotalGasLeft+lazyTx.Gas > env.gasPool.Gas() || constraintsTotalBlobGasLeft+lazyTx.BlobGas > blobGasLeft { - // Skip this tx and try to fit one with less gas. - // Drop all consecutive transactions from the same sender because of `nonce-too-high` clause. - log.Debug("Could not find transactions gas with the remaining constraints, account skipped", "hash", lazyTx.Hash) - txs.Pop() - // Edge case: - // - // Assumption: suppose sender A sends tx T_1 with nonce 1, and T_2 with nonce 2, and T_2 is a constraint. - // - // - // When running the block building algorithm I first have to make sure to reserve enough gas for the constraints. - // This implies that when a pooled tx comes I have to check if there is enough gas for it while taking into account - // the rest of the remaining constraint gas to allocate. - // Suppose there is no gas for the pooled tx T_1, then I have to drop it and consequently drop every tx from the same - // sender with higher nonce due to "nonce-too-high" issues, including T_2. - // But then, I have dropped a constraint which means my bid is invalid. - // - // FIXME: for the PoC we're not handling this - - // Repeat the loop to try to find another pool transaction - continue - } - // We can safely consider the pool tx as the candidate, - // since by assumption it is not nonce-conflicting - tx := lazyTx.Resolve() - if tx == nil { - log.Trace("Ignoring evicted transaction", "hash", candidate.tx.Hash()) - txs.Pop() - continue - } - candidate = candidateTx{tx: tx, isConstraint: false} + // We can safely consider the pool tx as the candidate, + // since by assumption it is not nonce-conflicting. + tx := lazyTx.Resolve() + if tx == nil { + log.Trace("Ignoring evicted transaction", "hash", candidate.tx.Hash()) + txs.Pop() + continue + } + + // Error may be ignored here, see assumption + from, _ = types.Sender(env.signer, tx) + + // We cannot choose this pooled tx yet, we need to make sure that there is not a constraint with lower nonce. + // That is, a scenario where T_1 is a constraint and T_2 is pooled. + constraintsBySender := append(constraintsRecoveredOrderedByNonceAndHashDesc, []*types.TransactionEcRecovered{}...) + common.Filter(&constraintsBySender, func(txRecovered *types.TransactionEcRecovered) bool { + return txRecovered.Sender == from + }) + + lowestNonceConstraintBySender := common.Last(constraintsBySender) + if lowestNonceConstraintBySender.Transaction.Nonce() < tx.Nonce() { + // This means that the constraint with the lowest nonce from this sender + // has lower nonce than the pooled tx, so we cannot execute the pooled tx yet. + // We need to execute the constraint first. + candidate = candidateTx{tx: lowestNonceConstraintBySender.Transaction, isConstraint: true} } else { - // No more pool tx left, we can add the unindexed ones if available - if len(constraintsWithoutIndex) == 0 { - // To recap, this means: - // 1. there are no more pool tx left - // 2. there are no more constraints without an index - // 3. the remaining indexes inside `constraintsOrderedByIndex`, if any, cannot be satisfied - // As such, we can safely exist - break - } - candidate = candidateTx{tx: common.Shift(&constraintsWithoutIndex).Tx, isConstraint: true} + candidate = candidateTx{tx: tx, isConstraint: false} + } + } else { + // No more pool tx left, we can add the unindexed ones if available + if len(constraintsOrderedByNonceAndHashDesc) == 0 { + // To recap, this means: + // 1. there are no more pool tx left + // 2. there are no more constraints + // As such, we can safely exist + break } + from = common.Last(constraintsRecoveredOrderedByNonceAndHashDesc).Sender + candidate = candidateTx{tx: common.Pop(&constraintsRecoveredOrderedByNonceAndHashDesc).Transaction, isConstraint: true} } - // Error may be ignored here, see assumption - from, _ := types.Sender(env.signer, candidate.tx) - // Check whether the tx is replay protected. If we're not in the EIP155 hf // phase, start ignoring the sender until we do. if candidate.tx.Protected() && !w.chainConfig.IsEIP155(env.header.Number) { @@ -1246,7 +1264,7 @@ type generateParams struct { noTxs bool // Flag whether an empty block without any transaction is expected onBlock BlockHookFn // Callback to call for each produced block slot uint64 // The slot in which the block is being produced - constraintsCache *shardmap.FIFOMap[uint64, types.HashToConstraintDecoded] // The preconfirmations to include in the block + constraintsCache *shardmap.FIFOMap[uint64, types.HashToConstraintDecoded] // The constraints to include in the block } func doPrepareHeader(genParams *generateParams, chain *core.BlockChain, config *Config, chainConfig *params.ChainConfig, extra []byte, engine consensus.Engine) (*types.Header, *types.Header, error) { @@ -1422,9 +1440,9 @@ func (w *worker) fillTransactions(interrupt *atomic.Int32, env *environment, con // Drop all transactions that conflict with the constraints (sender, nonce) signerAndNonceOfConstraints := make(map[common.Address]uint64) - for _, constraint := range constraints { - from, err := types.Sender(env.signer, constraint.Tx) - log.Info(fmt.Sprintf("Inside fillTransactions, constraint %s from %s", constraint.Tx.Hash().String(), from.String())) + for _, tx := range constraints { + from, err := types.Sender(env.signer, tx) + log.Info(fmt.Sprintf("Inside fillTransactions, constraint %s from %s", tx.Hash().String(), from.String())) if err != nil { // NOTE: is this the right behaviour? If this happens the builder is not able to // produce a valid bid @@ -1432,7 +1450,7 @@ func (w *worker) fillTransactions(interrupt *atomic.Int32, env *environment, con continue } - signerAndNonceOfConstraints[from] = constraint.Tx.Nonce() + signerAndNonceOfConstraints[from] = tx.Nonce() } for sender, lazyTxs := range pendingPlainTxs { common.Filter(&lazyTxs, func(lazyTx *txpool.LazyTransaction) bool { diff --git a/builder/miner/worker_test.go b/builder/miner/worker_test.go index 745a4761..bf534487 100644 --- a/builder/miner/worker_test.go +++ b/builder/miner/worker_test.go @@ -122,8 +122,8 @@ func init() { } else { idx = nil } - constraints := make(map[common.Hash]*types.ConstraintDecoded) - constraints[tx1.Hash()] = &types.ConstraintDecoded{Index: idx, Tx: tx1} + constraints := make(map[common.Hash]*types.Transaction) + constraints[tx1.Hash()] = tx1 // FIXME: slot 0 is probably not correct for these tests testConstraintsCache.Put(0, constraints) }