Skip to content

Commit

Permalink
Merge pull request #8 from chainbound/lore/fix/proof-checks
Browse files Browse the repository at this point in the history
fix: enforce checking proofs when constraints are provided for a slot, solve TODOs
  • Loading branch information
thedevbirb authored Nov 6, 2024
2 parents f170038 + 53a4e3f commit 0e08eeb
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 363 deletions.
75 changes: 40 additions & 35 deletions server/constraints.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package server

import (
"errors"

"github.com/attestantio/go-eth2-client/spec/phase0"
gethCommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
lru "github.com/hashicorp/golang-lru/v2"
)

var errNilTransaction = errors.New("cannot add nil transaction")

type (
BatchedSignedConstraints = []*SignedConstraints
HashToTransactionDecoded = map[gethCommon.Hash]*types.Transaction
Expand All @@ -22,10 +26,10 @@ type SignedConstraints struct {
// ConstraintsMessage represents the constraints message.
// Reference: https://docs.boltprotocol.xyz/api/builder
type ConstraintsMessage struct {
Pubkey phase0.BLSPubKey `json:"pubkey"`
Slot uint64 `json:"slot"`
Top bool `json:"top"`
Transactions []Transaction `json:"transactions"`
Pubkey phase0.BLSPubKey `json:"pubkey"`
Slot uint64 `json:"slot"`
Top bool `json:"top"`
Transactions []*HexTransaction `json:"transactions"`
}

func (s *SignedConstraints) String() string {
Expand All @@ -36,67 +40,68 @@ func (m *ConstraintsMessage) String() string {
return JSONStringify(m)
}

// TransactionHashMap is a map of transaction hashes to transactions that have
// been marshalled without the blob sidecar.
type TransactionHashMap = map[gethCommon.Hash]*HexTransaction

// ConstraintsCache is a cache for constraints.
type ConstraintsCache struct {
// map of slots to all constraints for that slot
constraints *lru.Cache[uint64, map[gethCommon.Hash]*Transaction]
constraints *lru.Cache[uint64, TransactionHashMap]
}

// NewConstraintsCache creates a new constraint cache.
// cap is the maximum number of slots to store constraints for.
func NewConstraintsCache(cap int) *ConstraintsCache {
constraints, _ := lru.New[uint64, map[gethCommon.Hash]*Transaction](cap)
// _cap is the maximum number of slots to store constraints for.
func NewConstraintsCache(_cap int) *ConstraintsCache {
constraints, _ := lru.New[uint64, TransactionHashMap](_cap)
return &ConstraintsCache{
constraints: constraints,
}
}

// AddInclusionConstraint adds an inclusion constraint to the cache at the given slot for the given transaction.
func (c *ConstraintsCache) AddInclusionConstraint(slot uint64, tx Transaction, index *uint64) error {
if _, exists := c.constraints.Get(slot); !exists {
c.constraints.Add(slot, make(map[gethCommon.Hash]*Transaction))
// AddInclusionConstraints adds multiple inclusion constraints to the cache at the given slot
func (c *ConstraintsCache) AddInclusionConstraints(slot uint64, transactions []*HexTransaction) error {
if len(transactions) == 0 {
return nil
}

// parse transaction to get its hash and store it in the cache
// for constant time lookup later
parsedTx := new(types.Transaction)
err := parsedTx.UnmarshalBinary(tx)
if err != nil {
return err
m, exists := c.constraints.Get(slot)
if !exists {
c.constraints.Add(slot, make(TransactionHashMap))
m, _ = c.constraints.Get(slot)
}

m, _ := c.constraints.Get(slot)
m[parsedTx.Hash()] = &tx

return nil
}
for _, txRaw := range transactions {
if txRaw == nil {
return errNilTransaction
}

// AddInclusionConstraints adds multiple inclusion constraints to the cache at the given slot
func (c *ConstraintsCache) AddInclusionConstraints(slot uint64, transactions []Transaction) error {
if _, exists := c.constraints.Get(slot); !exists {
c.constraints.Add(slot, make(map[gethCommon.Hash]*Transaction))
}
txDecoded := new(types.Transaction)
err := txDecoded.UnmarshalBinary(*txRaw)
if err != nil {
return err
}

m, _ := c.constraints.Get(slot)
for _, tx := range transactions {
parsedTx := new(types.Transaction)
err := parsedTx.UnmarshalBinary(tx)
txDecoded = txDecoded.WithoutBlobTxSidecar()
txWithoutblobSidecarRaw, err := txDecoded.MarshalBinary()
if err != nil {
return err
}
m[parsedTx.Hash()] = &tx
hex := HexTransaction(txWithoutblobSidecarRaw)

m[txDecoded.Hash()] = &hex
}

return nil
}

// Get gets the constraints at the given slot.
func (c *ConstraintsCache) Get(slot uint64) (map[gethCommon.Hash]*Transaction, bool) {
func (c *ConstraintsCache) Get(slot uint64) (TransactionHashMap, bool) {
return c.constraints.Get(slot)
}

// FindTransactionByHash finds the constraint for the given transaction hash and returns it.
func (c *ConstraintsCache) FindTransactionByHash(txHash gethCommon.Hash) (*Transaction, bool) {
func (c *ConstraintsCache) FindTransactionByHash(txHash gethCommon.Hash) (*HexTransaction, bool) {
for _, hashToTx := range c.constraints.Values() {
if tx, exists := hashToTx[txHash]; exists {
return tx, true
Expand Down
2 changes: 1 addition & 1 deletion server/mock_relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ func (m *mockRelay) defaultHandleSubmitConstraint(w http.ResponseWriter, req *ht
}

func (m *mockRelay) MakeGetHeaderWithConstraintsResponse(value uint64, blockHash, parentHash, publicKey string, version spec.DataVersion, constraints []struct {
tx Transaction
tx HexTransaction
hash phase0.Hash32
},
) *VersionedSignedBuilderBidWithProofs {
Expand Down
97 changes: 25 additions & 72 deletions server/proofs.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import (

fastSsz "github.com/ferranbt/fastssz"

"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"
Expand All @@ -26,34 +24,10 @@ type VersionedSignedBuilderBidWithProofs struct {
*builderSpec.VersionedSignedBuilderBid
}

// 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 VersionedSignedBuilderBid 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.
// Custom MarshalJSON implementation according to Constraints-API.
// Reference: https://docs.boltprotocol.xyz/technical-docs/api/builder#get_header_with_proofs
func (v *VersionedSignedBuilderBidWithProofs) MarshalJSON() ([]byte, error) {
switch v.Version {
case consensusSpec.DataVersionBellatrix:
return json.Marshal(struct {
Message *bellatrix.BuilderBid `json:"message"`
Signature phase0.BLSSignature `json:"signature"`
Proofs *InclusionProof `json:"proofs"`
}{
Message: v.Bellatrix.Message,
Signature: v.Bellatrix.Signature,
Proofs: v.Proofs,
})
case consensusSpec.DataVersionCapella:
return json.Marshal(struct {
Message *capella.BuilderBid `json:"message"`
Signature phase0.BLSSignature `json:"signature"`
Proofs *InclusionProof `json:"proofs"`
}{
Message: v.Capella.Message,
Signature: v.Capella.Signature,
Proofs: v.Proofs,
})
case consensusSpec.DataVersionDeneb:
return json.Marshal(struct {
Message *deneb.BuilderBid `json:"message"`
Expand All @@ -64,78 +38,57 @@ func (v *VersionedSignedBuilderBidWithProofs) MarshalJSON() ([]byte, error) {
Signature: v.Deneb.Signature,
Proofs: v.Proofs,
})
default:
return nil, fmt.Errorf("unknown or unsupported data version %d", v.Version)
}

return nil, fmt.Errorf("unknown data version %d", v.Version)
}

// Custom UnmarshalJSON implementation according to Constraints-API. This is
// needed in order to be spec compliant and without re-implementing the
// underlying consensus types from scratch. Reference:
// https://docs.boltprotocol.xyz/technical-docs/api/builder#get_header_with_proofs
func (v *VersionedSignedBuilderBidWithProofs) UnmarshalJSON(data []byte) error {
var err error

var partialBid struct {
Version consensusSpec.DataVersion `json:"version"`
Proofs *InclusionProof `json:"proofs"`
// No `Data` field yet, because we need a workaround to add the `Proofs` field in it
}

err = json.Unmarshal(data, &partialBid)
if err != nil {
return err
}

v.VersionedSignedBuilderBid = &builderSpec.VersionedSignedBuilderBid{}

if partialBid.Version == consensusSpec.DataVersionDeneb {
var dataBid struct {
Message *deneb.SignedBuilderBid `json:"data"`
}

err = json.Unmarshal(data, &dataBid)
if err != nil {
return err
}

v.Proofs = partialBid.Proofs
v.Version = partialBid.Version
v.Deneb = dataBid.Message

return nil
}

if partialBid.Version == consensusSpec.DataVersionCapella {
switch partialBid.Version {
case consensusSpec.DataVersionDeneb:
var dataBid struct {
Message *capella.SignedBuilderBid `json:"data"`
Data struct {
Message *deneb.BuilderBid `json:"message"`
Signature phase0.BLSSignature `json:"signature"`
Proofs *InclusionProof `json:"proofs"`
} `json:"data"`
}

err = json.Unmarshal(data, &dataBid)
if err != nil {
return err
}

v.Proofs = partialBid.Proofs
v.Version = partialBid.Version
v.Capella = dataBid.Message

return nil
}

if partialBid.Version == consensusSpec.DataVersionBellatrix {
var dataBid struct {
Message *bellatrix.SignedBuilderBid `json:"data"`
}

err = json.Unmarshal(data, &dataBid)
if err != nil {
return err
v.VersionedSignedBuilderBid = &builderSpec.VersionedSignedBuilderBid{
Version: partialBid.Version,
Deneb: &deneb.SignedBuilderBid{Message: dataBid.Data.Message, Signature: dataBid.Data.Signature},
}

v.Proofs = partialBid.Proofs
v.Version = partialBid.Version
v.Bellatrix = dataBid.Message
v.Proofs = dataBid.Data.Proofs

return nil
default:
return fmt.Errorf(
"failed to unmarshal VersionedSignedBuilderBidWithProofs: unknown or unsupported data version %s",
partialBid.Version,
)
}

return fmt.Errorf("failed to unmarshal VersionedSignedBuilderBidWithProofs: %v", err)
}

func (v *VersionedSignedBuilderBidWithProofs) String() string {
Expand Down
74 changes: 74 additions & 0 deletions server/proofs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package server

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/require"
)

func TestUnmarshalVersionedSignedBuilderBidWithProofs(t *testing.T) {
rawJSON := []byte(`{
"data": {
"message": {
"blob_kzg_commitments": [],
"header": {
"base_fee_per_gas": "95",
"blob_gas_used": "0",
"block_hash": "0x6abb9cb08060c12751d7e6b2f5212a811c2f085ff9034aabc236685fa5e0263c",
"block_number": "125",
"excess_blob_gas": "0",
"extra_data": "0x496c6c756d696e61746520446d6f63726174697a6520447374726962757465",
"fee_recipient": "0x8943545177806ed17b9f23f0a21ee5948ecaa776",
"gas_limit": "28133906",
"gas_used": "252500",
"logs_bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"parent_hash": "0xd0b4b72a3a46a4ec79d78aab562aaea974ff0ec417ce296c71be29e93298d199",
"prev_randao": "0x7e37495f5e8f9ca3912a63f9271b33647b88c088e553172512c18d6cefef78ee",
"receipts_root": "0x81d411e7dd497ca729468319eb151b990f920b50c5e9ca362b1b3c5132f5c0ce",
"state_root": "0x84af6fd15fca9c36ef1d6ac92fc6461943bd862845fc9a8f35cf25682bb6612b",
"timestamp": "1730905679",
"transactions_root": "0x4e47f8c8f9699093a85f23717a9d9b7d8d7c2eb815e0bcc4e80d6c540a6cca0e",
"withdrawals_root": "0x792930bbd5baac43bcc798ee49aa8185ef76bb3b44ba62b91d86ae569e4bb535"
},
"pubkey": "0xa55c1285d84ba83a5ad26420cd5ad3091e49c55a813eee651cd467db38a8c8e63192f47955e9376f6b42f6d190571cb5",
"value": "258884085311000"
},
"proofs": {
"generalized_indexes": [2097162],
"merkle_hashes": [
"0x1951581ccbd7afa8b00ebc5f213b646724db1faa9e03ee5906d4cc61a9e135cd",
"0x5c8189822f8cfc5f0e1ff35118bf8e03dea057ad1d838fbb66752fc88024700e",
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71",
"0xe721214932dc0eda5c208e3d23e367cf3d75f753f8756adb78f5044f7cc3e145",
"0x536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123c",
"0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30",
"0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1",
"0x87eb0ddba57e35f6d286673802a4af5975e22506c7cf4c64bb6be5ee11527f2c",
"0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193",
"0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1",
"0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b",
"0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220",
"0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f",
"0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e",
"0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784",
"0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb",
"0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb",
"0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab",
"0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4",
"0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f",
"0x0c00000000000000000000000000000000000000000000000000000000000000"
],
"transaction_hashes": [
"0xbf94242519c86644375032ad692bb88441ff0813f1264fa012059bf47691efed"
]
},
"signature": "0x970fc0ce631ecd37d1199df8e24ebd9ce800641deff117bb780f8d7cacc4ddb914ae0aa342ad67dd7d61ce961b1ef14d08a5c666978c1263f9637db1db0d6c9e7d3ac6d5ad759744650d4c8b3c82a2c78e89abf45ff8a3dd6fe08561243964c6"
},
"version": "deneb"
}`)

bidWithProofs := &VersionedSignedBuilderBidWithProofs{}
err := json.Unmarshal(rawJSON, bidWithProofs)
require.NoError(t, err)
}
Loading

0 comments on commit 0e08eeb

Please sign in to comment.