Skip to content

Commit

Permalink
feat(relay): SSZ Merkle Multiproof verification
Browse files Browse the repository at this point in the history
  • Loading branch information
thedevbirb committed Jun 12, 2024
1 parent 8441feb commit 472f117
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 127 deletions.
60 changes: 8 additions & 52 deletions mev-boost-relay/common/preconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"fmt"
"strings"

fastSsz "github.com/ferranbt/fastssz"
"github.com/sirupsen/logrus"

builderSpec "github.com/attestantio/go-builder-client/spec"
Expand All @@ -19,9 +18,8 @@ import (
// over `builderSpec.VersionedSubmitBlockRequest`
// to include preconfirmation proofs
type VersionedSubmitBlockRequestWithProofs struct {
Inner *VersionedSubmitBlockRequest `json:"inner"`
// FIXME: this is not spec-aligned yet https://github.com/chainbound/bolt/issues/55
Proofs []*PreconfirmationWithProof `json:"proofs"`
Inner *VersionedSubmitBlockRequest `json:"inner"`
Proofs *InclusionProof `json:"proofs"`
}

func (v *VersionedSubmitBlockRequestWithProofs) String() string {
Expand All @@ -36,7 +34,7 @@ type BidWithPreconfirmationsProofs struct {
// The block bid
Bid *builderSpec.VersionedSignedBuilderBid `json:"bid"`
// The preconfirmations with proofs
Proofs []*PreconfirmationWithProof `json:"proofs"`
Proofs *InclusionProof `json:"proofs"`
}

func (b *BidWithPreconfirmationsProofs) String() string {
Expand All @@ -47,14 +45,6 @@ func (b *BidWithPreconfirmationsProofs) String() string {
return string(out)
}

func (p *PreconfirmationWithProof) String() string {
proofs, err := json.Marshal(p)
if err != nil {
return err.Error()
}
return string(proofs)
}

type HexBytes []byte

// MarshalJSON implements json.Marshaler.
Expand Down Expand Up @@ -90,45 +80,11 @@ func (h HexBytes) String() string {
return JSONStringify(h)
}

// SerializedMerkleProof contains a serialized Merkle proof of transaction inclusion.
// - `Index“ is the generalized index of the included transaction from the SSZ tree
// created from the list of transactions.
// - `Hashes` are the other branch hashes needed to reconstruct the Merkle proof.
//
// For reference, see https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md
type SerializedMerkleProof struct {
Index int `json:"index"`
Hashes []HexBytes `ssz-size:"dynamic" json:"hashes"`
}

func (s *SerializedMerkleProof) FromFastSszProof(p *fastSsz.Proof) {
s.Index = p.Index
s.Hashes = make([]HexBytes, len(p.Hashes))
for i, h := range p.Hashes {
s.Hashes[i] = h
}
}

// ToFastSszProof converts a SerializedMerkleProof to a fastssz.Proof.
func (s *SerializedMerkleProof) ToFastSszProof(leaf []byte) *fastSsz.Proof {
p := &fastSsz.Proof{
Index: s.Index,
Leaf: leaf,
Hashes: make([][]byte, len(s.Hashes)),
}
for i, h := range s.Hashes {
p.Hashes[i] = h
}
return p
}

// PreconfirmationWithProof is a preconfirmed transaction in the block with
// proof of inclusion, using Merkle Trees.
type PreconfirmationWithProof struct {
// The transaction hash of the preconfirmation
TxHash phase0.Hash32 `ssz-size:"32" json:"txHash"`
// The Merkle proof of the preconfirmation
MerkleProof *SerializedMerkleProof `json:"merkleProof"`
// InclusionProof is a Merkle Multiproof of inclusion of a set of TransactionHashes
type InclusionProof struct {
TransactionHashes []phase0.Hash32 `json:"transaction_hashes"`
GeneralizedIndexes []uint64 `json:"generalized_indexes"`
MerkleHashes []*HexBytes `json:"merkle_hashes"`
}

func NewBoltLogger(service string) *logrus.Entry {
Expand Down
7 changes: 4 additions & 3 deletions mev-boost-relay/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type IDatabaseService interface {
saveExecPayload bool,
profile common.Profile,
optimisticSubmission bool,
preconfirmations []*common.PreconfirmationWithProof) (entry *BuilderBlockSubmissionEntry, err error)
inclusionProof *common.InclusionProof) (entry *BuilderBlockSubmissionEntry, err error)
GetBlockSubmissionEntry(slot uint64, proposerPubkey, blockHash string) (entry *BuilderBlockSubmissionEntry, err error)
GetBuilderSubmissions(filters GetBuilderSubmissionsFilters) ([]*BuilderBlockSubmissionEntry, error)
GetBuilderSubmissionsBySlots(slotFrom, slotTo uint64) (entries []*BuilderBlockSubmissionEntry, err error)
Expand Down Expand Up @@ -194,7 +194,8 @@ func (s *DatabaseService) SaveBuilderBlockSubmission(
saveExecPayload bool,
profile common.Profile,
optimisticSubmission bool,
preconfirmations []*common.PreconfirmationWithProof) (entry *BuilderBlockSubmissionEntry, err error) {
inclusionProof *common.InclusionProof,
) (entry *BuilderBlockSubmissionEntry, err error) {
// Save execution_payload: insert, or if already exists update to be able to return the id ('on conflict do nothing' doesn't return an id)
execPayloadEntry, err := PayloadToExecPayloadEntry(payload)
if err != nil {
Expand Down Expand Up @@ -224,7 +225,7 @@ func (s *DatabaseService) SaveBuilderBlockSubmission(
return nil, err
}

jsonPreconfirmations, err := json.Marshal(preconfirmations)
jsonPreconfirmations, err := json.Marshal(inclusionProof)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion mev-boost-relay/database/mockdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func (db MockDB) GetLatestValidatorRegistrations(timestampOnly bool) ([]*Validat
return nil, nil
}

func (db MockDB) SaveBuilderBlockSubmission(payload *common.VersionedSubmitBlockRequest, requestError, validationError error, receivedAt, eligibleAt time.Time, wasSimulated, saveExecPayload bool, profile common.Profile, optimisticSubmission bool, proofs []*common.PreconfirmationWithProof) (entry *BuilderBlockSubmissionEntry, err error) {
func (db MockDB) SaveBuilderBlockSubmission(payload *common.VersionedSubmitBlockRequest, requestError, validationError error, receivedAt, eligibleAt time.Time, wasSimulated, saveExecPayload bool, profile common.Profile, optimisticSubmission bool, proof *common.InclusionProof) (entry *BuilderBlockSubmissionEntry, err error) {
return nil, nil
}

Expand Down
48 changes: 24 additions & 24 deletions mev-boost-relay/datastore/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ import (
builderSpec "github.com/attestantio/go-builder-client/spec"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/capella"
"github.com/thedevbirb/flashbots-go-utils/cli"
"github.com/flashbots/mev-boost-relay/common"
"github.com/go-redis/redis/v9"
"github.com/sirupsen/logrus"
"github.com/thedevbirb/flashbots-go-utils/cli"
)

var (
Expand Down Expand Up @@ -87,7 +87,7 @@ type RedisCache struct {
prefixGetHeaderResponse string
prefixExecPayloadCapella string
prefixPayloadContentsDeneb string
prefixPreconfirmationsProofs string
prefixInclusionProof string
prefixBidTrace string
prefixBlockBuilderLatestBids string // latest bid for a given slot
prefixBlockBuilderLatestBidsValue string // value of latest bid for a given slot
Expand Down Expand Up @@ -126,11 +126,11 @@ func NewRedisCache(prefix, redisURI, readonlyURI string) (*RedisCache, error) {
readonlyClient: roClient,
boltLog: common.NewBoltLogger("REDIS"),

prefixGetHeaderResponse: fmt.Sprintf("%s/%s:cache-gethead-response", redisPrefix, prefix),
prefixExecPayloadCapella: fmt.Sprintf("%s/%s:cache-execpayload-capella", redisPrefix, prefix),
prefixPayloadContentsDeneb: fmt.Sprintf("%s/%s:cache-payloadcontents-deneb", redisPrefix, prefix),
prefixPreconfirmationsProofs: fmt.Sprintf("%s/%s:cache-preconfirmations-proofs", redisPrefix, prefix),
prefixBidTrace: fmt.Sprintf("%s/%s:cache-bid-trace", redisPrefix, prefix),
prefixGetHeaderResponse: fmt.Sprintf("%s/%s:cache-gethead-response", redisPrefix, prefix),
prefixExecPayloadCapella: fmt.Sprintf("%s/%s:cache-execpayload-capella", redisPrefix, prefix),
prefixPayloadContentsDeneb: fmt.Sprintf("%s/%s:cache-payloadcontents-deneb", redisPrefix, prefix),
prefixInclusionProof: fmt.Sprintf("%s/%s:cache-preconfirmations-proofs", redisPrefix, prefix),
prefixBidTrace: fmt.Sprintf("%s/%s:cache-bid-trace", redisPrefix, prefix),

prefixBlockBuilderLatestBids: fmt.Sprintf("%s/%s:block-builder-latest-bid", redisPrefix, prefix), // hashmap for slot+parentHash+proposerPubkey with builderPubkey as field
prefixBlockBuilderLatestBidsValue: fmt.Sprintf("%s/%s:block-builder-latest-bid-value", redisPrefix, prefix), // hashmap for slot+parentHash+proposerPubkey with builderPubkey as field
Expand Down Expand Up @@ -160,12 +160,10 @@ func (r *RedisCache) keyExecPayloadCapella(slot uint64, proposerPubkey, blockHas

func (r *RedisCache) keyPayloadContentsDeneb(slot uint64, proposerPubkey, blockHash string) string {
return fmt.Sprintf("%s:%d_%s_%s", r.prefixPayloadContentsDeneb, slot, proposerPubkey, blockHash)

}

func (r *RedisCache) keyPreconfirmationsProofs(slot uint64, proposerPubkey string, blockHash string) string {
return fmt.Sprintf("%s:%d_%s_%s", r.prefixPreconfirmationsProofs, slot, proposerPubkey, blockHash)

func (r *RedisCache) keyInclusionProof(slot uint64, proposerPubkey string, blockHash string) string {
return fmt.Sprintf("%s:%d_%s_%s", r.prefixInclusionProof, slot, proposerPubkey, blockHash)
}

func (r *RedisCache) keyCacheBidTrace(slot uint64, proposerPubkey, blockHash string) string {
Expand Down Expand Up @@ -375,10 +373,10 @@ func (r *RedisCache) GetBestBid(slot uint64, parentHash, proposerPubkey string)
return resp, err
}

func (r *RedisCache) GetPreconfirmationsProofs(slot uint64, proposerPubkey string, bidBlockHash string) ([]*common.PreconfirmationWithProof, error) {
key := r.keyPreconfirmationsProofs(slot, proposerPubkey, bidBlockHash)
func (r *RedisCache) GetInclusionProof(slot uint64, proposerPubkey string, bidBlockHash string) (*common.InclusionProof, error) {
key := r.keyInclusionProof(slot, proposerPubkey, bidBlockHash)
r.boltLog.Infof("Getting preconfirmations proofs from Redis with key %s", key)
resp := make([]*common.PreconfirmationWithProof, 0, 10)
resp := new(common.InclusionProof)
err := r.GetObj(key, &resp)
if errors.Is(err, redis.Nil) {
return nil, nil
Expand Down Expand Up @@ -439,11 +437,12 @@ func (r *RedisCache) SaveExecutionPayloadCapella(ctx context.Context, pipeliner

// SavePreconfirmationsProofs saves the preconfirmation proofs in the Redis cache with JSON encoding
// TODO: maybe ssz encoding?
func (r *RedisCache) SavePreconfirmationsProofs(ctx context.Context, pipeliner redis.Pipeliner,
slot uint64, proposerPubkey string, bidBlockHash string, proofs []*common.PreconfirmationWithProof) (err error) {
key := r.keyPreconfirmationsProofs(slot, proposerPubkey, bidBlockHash)
r.boltLog.Infof("Saving %d preconfirmations proofs with key %s", len(proofs), key)
b, err := json.Marshal(proofs)
func (r *RedisCache) SaveInclusionProof(ctx context.Context, pipeliner redis.Pipeliner,
slot uint64, proposerPubkey string, bidBlockHash string, proof *common.InclusionProof,
) (err error) {
key := r.keyInclusionProof(slot, proposerPubkey, bidBlockHash)
r.boltLog.Infof("Saving %d inclusion proofs with key %s", len(proof.TransactionHashes), key)
b, err := json.Marshal(proof)
if err != nil {
r.boltLog.WithError(err).Errorf("Failed to marshal preconfirmations proofs for slot %d", slot)
return err
Expand Down Expand Up @@ -557,8 +556,8 @@ func (r *RedisCache) SaveBidAndUpdateTopBid(
reqReceivedAt time.Time,
isCancellationEnabled bool,
floorValue *big.Int,
proofs []*common.PreconfirmationWithProof) (state SaveBidAndUpdateTopBidResponse, err error) {

proof *common.InclusionProof,
) (state SaveBidAndUpdateTopBidResponse, err error) {
var prevTime, nextTime time.Time
prevTime = time.Now()

Expand Down Expand Up @@ -619,8 +618,8 @@ func (r *RedisCache) SaveBidAndUpdateTopBid(
}

// BOLT: If preconfirmations proofs are available, save them
if len(proofs) > 0 {
err = r.SavePreconfirmationsProofs(ctx, pipeliner, submission.BidTrace.Slot, submission.BidTrace.ProposerPubkey.String(), submission.BidTrace.BlockHash.String(), proofs)
if proof != nil {
err = r.SaveInclusionProof(ctx, pipeliner, submission.BidTrace.Slot, submission.BidTrace.ProposerPubkey.String(), submission.BidTrace.BlockHash.String(), proof)
if err != nil {
r.boltLog.WithError(err).Errorf("Failed to save preconfirmations proofs to redis for slot %d", submission.BidTrace.Slot)
return state, err
Expand Down Expand Up @@ -724,7 +723,8 @@ func (r *RedisCache) _updateTopBid(
parentHash,
proposerPubkey string,
floorValue *big.Int) (
resp SaveBidAndUpdateTopBidResponse, err error) {
resp SaveBidAndUpdateTopBidResponse, err error,
) {
r.boltLog.Info("Updating top bid")

if builderBids == nil {
Expand Down
59 changes: 28 additions & 31 deletions mev-boost-relay/services/api/proofs.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@ var (
ErrInvalidRoot = errors.New("failed getting tx root from bid")
)

// verifyConstraintProofs verifies the proofs against the constraints, and returns an error if the proofs are invalid.
func verifyConstraintProofs(log *logrus.Entry, transactionsRoot phase0.Root, proofs []*common.PreconfirmationWithProof, constraints map[phase0.Hash32]*Constraint) error {
if proofs == nil {
return errors.New("proofs are nil")
// verifyInclusionProof verifies the proofs against the constraints, and returns an error if the proofs are invalid.
func verifyInclusionProof(log *logrus.Entry, transactionsRoot phase0.Root, proof *common.InclusionProof, constraints map[phase0.Hash32]*Constraint) error {
if proof == nil {
return ErrNilProof
}

log.WithField("len", len(proofs)).Info("[BOLT]: Verifying constraint proofs")
leaves := make([][]byte, len(constraints))

i := 0
for hash, constraint := range constraints {
if constraint == nil {
log.Warn("[BOLT]: nil constraint!")
return ErrNilConstraint
}

Expand All @@ -42,37 +42,34 @@ func verifyConstraintProofs(log *logrus.Entry, transactionsRoot phase0.Root, pro
tx := Transaction(constraint.Tx)
txHashTreeRoot, err := tx.HashTreeRoot()
if err != nil {
log.WithError(err).Error("[BOLT]: error getting tx hash tree root")
return ErrInvalidRoot
}

proof := Find(proofs, func(proof *common.PreconfirmationWithProof) bool {
return proof.TxHash == hash
})

if proof == nil {
log.Warnf("[BOLT]: No proof found for tx hash %s", hash)
return ErrNilProof
}

// Verify the proof
sszProof := proof.MerkleProof.ToFastSszProof(txHashTreeRoot[:])
leaves[i] = txHashTreeRoot[:]
i++
}

currentTime := time.Now()
ok, err := fastSsz.VerifyProof(transactionsRoot[:], sszProof)
elapsed := time.Since(currentTime)
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)
}

if err != nil {
log.WithError(err).Error("error verifying merkle proof")
return err
}
currentTime := time.Now()
ok, err := fastSsz.VerifyMultiproof(transactionsRoot[:], hashes, leaves, indexes)
elapsed := time.Since(currentTime)
if err != nil {
log.WithError(err).Error("error verifying merkle proof")
return err
}

if !ok {
log.Error("[BOLT]: constraint proof verification failed: 'not ok' for tx hash: ", proof.TxHash.String())
return ErrInvalidProofs
} else {
log.Info(fmt.Sprintf("[BOLT]: constraint proof verified for tx hash %s in %s", proof.TxHash.String(), elapsed))
}
if !ok {
return ErrInvalidProofs
} else {
log.Info(fmt.Sprintf("[BOLT]: inclusion proof verified in %s", elapsed))
}

return nil
Expand Down
Loading

0 comments on commit 472f117

Please sign in to comment.