Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(mevboost, relay): ordering logic for multiproofs #166

Merged
merged 4 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 20 additions & 12 deletions mev-boost-relay/services/api/proofs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ import (
"time"

"github.com/attestantio/go-eth2-client/spec/phase0"
gethCommon "github.com/ethereum/go-ethereum/common"
fastSsz "github.com/ferranbt/fastssz"
"github.com/flashbots/mev-boost-relay/common"
"github.com/sirupsen/logrus"
)

var (
ErrNilConstraint = errors.New("nil constraint")
ErrNilProof = errors.New("nil proof")
ErrInvalidProofs = errors.New("proof verification failed")
ErrInvalidRoot = errors.New("failed getting tx root from bid")
ErrNilConstraint = errors.New("nil constraint")
ErrNilProof = errors.New("nil proof")
ErrInvalidProofs = errors.New("proof verification failed")
ErrInvalidRoot = errors.New("failed getting tx root from bid")
ErrHashesIndexesMismatch = errors.New("proof transaction hashes and indexes length mismatch")
ErrHashesConstraintsMismatch = errors.New("proof transaction hashes and constraints length mismatch")
)

// verifyInclusionProof verifies the proofs against the constraints, and returns an error if the proofs are invalid.
Expand All @@ -26,12 +29,20 @@ func verifyInclusionProof(log *logrus.Entry, transactionsRoot phase0.Root, proof
return ErrNilProof
}

constraints := ParseConstraintsDecoded(hashToConstraints)
if len(proof.TransactionHashes) != len(proof.GeneralizedIndexes) {
return ErrHashesIndexesMismatch
}

if len(proof.TransactionHashes) != len(hashToConstraints) {
return ErrHashesIndexesMismatch
}

leaves := make([][]byte, len(constraints))
leaves := make([][]byte, len(hashToConstraints))
indexes := make([]int, len(proof.GeneralizedIndexes))

for i, constraint := range constraints {
if constraint == nil {
for i, hash := range proof.TransactionHashes {
constraint, ok := hashToConstraints[gethCommon.Hash(hash)]
if constraint == nil || !ok {
return ErrNilConstraint
}

Expand All @@ -50,17 +61,14 @@ func verifyInclusionProof(log *logrus.Entry, transactionsRoot phase0.Root, proof
}

leaves[i] = txHashTreeRoot[:]
indexes[i] = int(proof.GeneralizedIndexes[i])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be careful with converting from uin64 to int64 here?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is safe because max i64 is 9,223,372,036,854,775,807 and all generalized indexes for transactions are capped at 2^22 -1 = 4,194,303 by consensus specs

Copy link
Contributor Author

@thedevbirb thedevbirb Jul 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the multiproof verification it is required that indexes is a []int. Given that generalized indexes will reasonably be small numbers (in the order of $2^{21}$ in our use case), we should not worry too much about it

i++
}

hashes := make([][]byte, len(proof.MerkleHashes))
for i, hash := range proof.MerkleHashes {
hashes[i] = []byte(*hash)
}
indexes := make([]int, len(proof.GeneralizedIndexes))
for i, index := range proof.GeneralizedIndexes {
indexes[i] = int(index)
}

currentTime := time.Now()
ok, err := fastSsz.VerifyMultiproof(transactionsRoot[:], hashes, leaves, indexes)
Expand Down
42 changes: 0 additions & 42 deletions mev-boost-relay/services/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import (
"encoding/json"
"errors"
"fmt"
"slices"
"sort"

"github.com/attestantio/go-eth2-client/spec/phase0"
gethCommon "github.com/ethereum/go-ethereum/common"
Expand Down Expand Up @@ -51,43 +49,3 @@ type (
Tx *types.Transaction
}
)

// ParseConstraintsDecoded receives a map of constraints and
// - creates a slice of constraints sorted by index
// - creates a slice of constraints without index sorted by nonce and hash
// Returns the concatenation of the slices
func ParseConstraintsDecoded(constraints HashToConstraintDecoded) []*ConstraintDecoded {
// 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))

for _, constraint := range constraints {
if constraint.Index == nil {
constraintsWithoutIndex = append(constraintsWithoutIndex, constraint)
} else {
constraintsOrderedByIndex = append(constraintsOrderedByIndex, constraint)
}
}

// 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 by hash
if iNonce == jNonce {
return constraintsWithoutIndex[i].Tx.Hash().Cmp(constraintsWithoutIndex[j].Tx.Hash()) < 0
}
return iNonce < jNonce
})

constraintsConcat := slices.Concat(constraintsOrderedByIndex, constraintsWithoutIndex)

return constraintsConcat
}
43 changes: 0 additions & 43 deletions mev-boost/server/constraints.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package server

import (
"slices"
"sort"

"github.com/attestantio/go-eth2-client/spec/phase0"
gethCommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
Expand Down Expand Up @@ -119,43 +116,3 @@ type (
Tx *types.Transaction
}
)

// ParseConstraintsDecoded receives a map of constraints and
// - creates a slice of constraints sorted by index
// - creates a slice of constraints without index sorted by nonce and hash
// Returns the concatenation of the slices
func ParseConstraintsDecoded(constraints HashToConstraintDecoded) []*ConstraintDecoded {
// 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))

for _, constraint := range constraints {
if constraint.Index == nil {
constraintsWithoutIndex = append(constraintsWithoutIndex, constraint)
} else {
constraintsOrderedByIndex = append(constraintsOrderedByIndex, constraint)
}
}

// 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 by hash
if iNonce == jNonce {
return constraintsWithoutIndex[i].Tx.Hash().Cmp(constraintsWithoutIndex[j].Tx.Hash()) < 0
}
return iNonce < jNonce
})

constraintsConcat := slices.Concat(constraintsOrderedByIndex, constraintsWithoutIndex)

return constraintsConcat
}
52 changes: 0 additions & 52 deletions mev-boost/server/constraints_test.go

This file was deleted.

62 changes: 33 additions & 29 deletions mev-boost/server/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
eth2ApiV1Capella "github.com/attestantio/go-eth2-client/api/v1/capella"
eth2ApiV1Deneb "github.com/attestantio/go-eth2-client/api/v1/deneb"
"github.com/attestantio/go-eth2-client/spec/phase0"
gethCommon "github.com/ethereum/go-ethereum/common"
gethTypes "github.com/ethereum/go-ethereum/core/types"
fastSsz "github.com/ferranbt/fastssz"
"github.com/flashbots/go-boost-utils/ssz"
Expand All @@ -46,11 +47,14 @@ var (

// Bolt errors
var (
errNilProof = errors.New("nil proof")
errMissingConstraint = errors.New("missing constraint")
errMismatchProofSize = errors.New("proof size mismatch")
errInvalidProofs = errors.New("proof verification failed")
errInvalidRoot = errors.New("failed getting tx root from bid")
errNilProof = errors.New("nil proof")
errMissingConstraint = errors.New("missing constraint")
errMismatchProofSize = errors.New("proof size mismatch")
errInvalidProofs = errors.New("proof verification failed")
errInvalidRoot = errors.New("failed getting tx root from bid")
errNilConstraint = errors.New("nil constraint")
errHashesIndexesMismatch = errors.New("proof transaction hashes and indexes length mismatch")
errHashesConstraintsMismatch = errors.New("proof transaction hashes and constraints length mismatch")
)

var (
Expand Down Expand Up @@ -338,7 +342,7 @@ func (m *BoostService) handleRegisterValidator(w http.ResponseWriter, req *http.
}

// verifyInclusionProof verifies the proofs against the constraints, and returns an error if the proofs are invalid.
func (m *BoostService) verifyInclusionProof(responsePayload *BidWithInclusionProofs, slot uint64) error {
func (m *BoostService) verifyInclusionProof(transactionsRoot phase0.Root, proof *InclusionProof, slot uint64) error {
log := m.log.WithFields(logrus.Fields{})

// BOLT: get constraints for the slot
Expand All @@ -349,21 +353,19 @@ func (m *BoostService) verifyInclusionProof(responsePayload *BidWithInclusionPro
return errMissingConstraint
}

if responsePayload.Proofs == nil {
if proof == nil {
return errNilProof
}

if len(responsePayload.Proofs.TransactionHashes) != len(inclusionConstraints) {
if len(proof.TransactionHashes) != len(inclusionConstraints) {
return errMismatchProofSize
}

log.Infof("[BOLT]: Verifying merkle multiproofs for %d transactions", len(responsePayload.Proofs.TransactionHashes))

transactionsRoot, err := responsePayload.Bid.TransactionsRoot()
if err != nil {
return errInvalidRoot
if len(proof.TransactionHashes) != len(proof.GeneralizedIndexes) {
return errHashesIndexesMismatch
}

log.Infof("[BOLT]: Verifying merkle multiproofs for %d transactions", len(proof.TransactionHashes))

// Decode the constraints, and sort them according to the utility function used
// TODO: this should be done before verification ideally
hashToConstraint := make(HashToConstraintDecoded)
Expand All @@ -379,18 +381,18 @@ func (m *BoostService) verifyInclusionProof(responsePayload *BidWithInclusionPro
Index: constraint.Index,
}
}
constraints := ParseConstraintsDecoded(hashToConstraint)
leaves := make([][]byte, len(inclusionConstraints))
indexes := make([]int, len(proof.GeneralizedIndexes))

leaves := make([][]byte, len(constraints))
for i, hash := range proof.TransactionHashes {
constraint, ok := hashToConstraint[gethCommon.Hash(hash)]
if constraint == nil || !ok {
return errNilConstraint
}

for i, constraint := range constraints {
// Compute the hash tree root for the raw preconfirmed transaction
// and use it as "Leaf" in the proof to be verified against

// TODO: this is pretty inefficient, we should work with the transaction already
// parsed without the blob here to avoid unmarshalling and marshalling again
transaction := constraint.Tx
encoded, err := transaction.MarshalBinary()
encoded, err := constraint.Tx.MarshalBinary()
if err != nil {
log.WithError(err).Error("error marshalling transaction without blob tx sidecar")
return err
Expand All @@ -403,17 +405,14 @@ func (m *BoostService) verifyInclusionProof(responsePayload *BidWithInclusionPro
}

leaves[i] = txHashTreeRoot[:]
indexes[i] = int(proof.GeneralizedIndexes[i])
i++
}

hashes := make([][]byte, len(responsePayload.Proofs.MerkleHashes))
for i, hash := range responsePayload.Proofs.MerkleHashes {
hashes := make([][]byte, len(proof.MerkleHashes))
for i, hash := range proof.MerkleHashes {
hashes[i] = []byte(*hash)
}
indexes := make([]int, len(responsePayload.Proofs.GeneralizedIndexes))
for i, index := range responsePayload.Proofs.GeneralizedIndexes {
indexes[i] = int(index)
}

currentTime := time.Now()
ok, err := fastSsz.VerifyMultiproof(transactionsRoot[:], hashes, leaves, indexes)
Expand Down Expand Up @@ -881,7 +880,12 @@ func (m *BoostService) handleGetHeaderWithProofs(w http.ResponseWriter, req *htt
// BOLT: verify preconfirmation inclusion proofs. If they don't match, we don't consider the bid to be valid.
if responsePayload.Proofs != nil {
// BOLT: verify the proofs against the constraints. If they don't match, we don't consider the bid to be valid.
if err := m.verifyInclusionProof(responsePayload, slotUint); err != nil {
transactionsRoot, err := responsePayload.Bid.TransactionsRoot()
if err != nil {
log.WithError(err).Error("[BOLT]: error getting transaction root")
return
}
if err := m.verifyInclusionProof(transactionsRoot, responsePayload.Proofs, slotUint); err != nil {
log.Warnf("[BOLT]: Proof verification failed for relay %s: %s", relay.URL, err)
return
}
Expand Down
Loading