Skip to content

Commit

Permalink
feat: Add Bundle Validate methods (#4)
Browse files Browse the repository at this point in the history
* feat: Add Bundle Validate methods

* fixup: Fix generated uuids/hashes, add unit tests

* fixup: note source of mev bundle payload
  • Loading branch information
ryanschneider authored Oct 23, 2024
1 parent 5888545 commit 7d86ae6
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 2 deletions.
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ require (
github.com/ethereum/go-ethereum v1.14.10
github.com/flashbots/go-utils v0.8.0
github.com/google/uuid v1.6.0
github.com/stretchr/testify v1.9.0
github.com/urfave/cli/v2 v2.27.2
golang.org/x/crypto v0.22.0
)

require (
Expand All @@ -20,6 +22,7 @@ require (
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c // indirect
github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deckarep/golang-set/v2 v2.6.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
Expand All @@ -28,6 +31,7 @@ require (
github.com/gorilla/websocket v1.4.2 // indirect
github.com/holiman/uint256 v1.3.1 // indirect
github.com/mmcloughlin/addchain v0.4.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
Expand All @@ -37,9 +41,9 @@ require (
github.com/valyala/fastrand v1.1.0 // indirect
github.com/valyala/histogram v1.2.0 // indirect
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
)
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
83 changes: 82 additions & 1 deletion proxy/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import (
"crypto/sha256"
"encoding/binary"
"encoding/json"
"errors"
"hash"
"sort"

"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/rpc"
"github.com/google/uuid"
"golang.org/x/crypto/sha3"
)

// Note on optional Signer field:
Expand All @@ -21,6 +24,19 @@ import (

// eth_SendBundle

const (
BundleTxLimit = 100
MevBundleTxLimit = 50
MevBundleMaxDepth = 1
)

var (
ErrBundleNoTxs = errors.New("bundle with no txs")
ErrBundleTooManyTxs = errors.New("too many txs in bundle")
ErrMevBundleUnmatchedTx = errors.New("mev bundle with unmatched tx")
ErrMevBundleTooDeep = errors.New("mev bundle too deep")
)

type EthSendBundleArgs struct {
Txs []hexutil.Bytes `json:"txs"` // empty txs for cancellations are not supported
BlockNumber rpc.BlockNumber `json:"blockNumber"` // 0 block number is not supported
Expand Down Expand Up @@ -112,7 +128,7 @@ type EthCancelBundleArgs struct {
type BidSubsisideBlockArgs uint64

/// unique key
/// unique key is used to deduplicate requests, its will give different resuts then bundle uuid
/// unique key is used to deduplicate requests, its will give different results then bundle uuid

func newHash() hash.Hash {
return sha256.New()
Expand Down Expand Up @@ -144,6 +160,39 @@ func (b *EthSendBundleArgs) UniqueKey() uuid.UUID {
return uuidFromHash(hash)
}

func (b *EthSendBundleArgs) Validate() (common.Hash, uuid.UUID, error) {
if len(b.Txs) == 0 {
return common.Hash{}, uuid.Nil, ErrBundleNoTxs
}
if len(b.Txs) > BundleTxLimit {
return common.Hash{}, uuid.Nil, ErrBundleTooManyTxs
}
// first compute keccak hash over the txs
hasher := sha3.NewLegacyKeccak256()
for _, rawTx := range b.Txs {
var tx types.Transaction
if err := tx.UnmarshalBinary(rawTx); err != nil {
return common.Hash{}, uuid.Nil, err
}
hasher.Write(tx.Hash().Bytes())
}
hashBytes := hasher.Sum(nil)

// then compute the uuid
var buf []byte
buf = binary.AppendVarint(buf, b.BlockNumber.Int64())
buf = append(buf, hashBytes...)
sort.Slice(b.RevertingTxHashes, func(i, j int) bool {
return bytes.Compare(b.RevertingTxHashes[i][:], b.RevertingTxHashes[j][:]) <= 0
})
for _, txHash := range b.RevertingTxHashes {
buf = append(buf, txHash[:]...)
}
return common.BytesToHash(hashBytes),
uuid.NewHash(sha256.New(), uuid.Nil, buf, 5),
nil
}

func (b *MevSendBundleArgs) UniqueKey() uuid.UUID {
hash := newHash()
uniqueKeyMevSendBundle(b, hash)
Expand Down Expand Up @@ -171,6 +220,38 @@ func uniqueKeyMevSendBundle(b *MevSendBundleArgs, hash hash.Hash) {
_, _ = hash.Write(b.Metadata.Signer.Bytes())
}

func (b *MevSendBundleArgs) Validate() (common.Hash, error) {
if len(b.Body) == 0 {
return common.Hash{}, ErrBundleNoTxs
}
return hashMevSendBundle(0, b)
}

func hashMevSendBundle(level int, b *MevSendBundleArgs) (common.Hash, error) {
if level > MevBundleMaxDepth {
return common.Hash{}, ErrMevBundleTooDeep
}
hasher := sha3.NewLegacyKeccak256()
for _, body := range b.Body {
if body.Hash != nil {
return common.Hash{}, ErrMevBundleUnmatchedTx
} else if body.Bundle != nil {
innerHash, err := hashMevSendBundle(level+1, body.Bundle)
if err != nil {
return common.Hash{}, err
}
hasher.Write(innerHash.Bytes())
} else if body.Tx != nil {
tx := new(types.Transaction)
if err := tx.UnmarshalBinary(*body.Tx); err != nil {
return common.Hash{}, err
}
hasher.Write(tx.Hash().Bytes())
}
}
return common.BytesToHash(hasher.Sum(nil)), nil
}

func (b *EthSendRawTransactionArgs) UniqueKey() uuid.UUID {
hash := newHash()
_, _ = hash.Write(*b)
Expand Down
121 changes: 121 additions & 0 deletions proxy/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package proxy_test

import (
"encoding/json"
"fmt"
"github.com/flashbots/tdx-orderflow-proxy/proxy"
"github.com/stretchr/testify/require"
"testing"
)

func TestEthSendBundleArgs_Validate(t *testing.T) {
// from https://github.com/flashbots/rbuilder/blob/develop/crates/rbuilder/src/primitives/serialize.rs#L607
inputs := []struct {
Payload json.RawMessage
ExpectedHash string
ExpectedUUID string
}{
{
Payload: []byte(`{
"blockNumber": "0x1136F1F",
"txs": ["0x02f9037b018203cd8405f5e1008503692da370830388ba943fc91a3afd70395cd496c647d5a6cc9d4b2b7fad8780e531581b77c4b903043593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000064f390d300000000000000000000000000000000000000000000000000000000000000030b090c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000080e531581b77c400000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000009184e72a0000000000000000000000000000000000000000000000000000080e531581b77c400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000b5ea574dd8f2b735424dfc8c4e16760fc44a931b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000c001a0a9ea84ad107d335afd5e5d2ddcc576f183be37386a9ac6c9d4469d0329c22e87a06a51ea5a0809f43bf72d0156f1db956da3a9f3da24b590b7eed01128ff84a2c1"],
"revertingTxHashes": ["0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]
}`),
ExpectedHash: "0xcf3c567aede099e5455207ed81c4884f72a4c0c24ddca331163a335525cd22cc",
ExpectedUUID: "d9a3ae52-79a2-5ce9-a687-e2aa4183d5c6",
},
{
Payload: []byte(`{
"blockNumber": "0x1136F1F",
"txs": ["0x02f9037b018203cd8405f5e1008503692da370830388ba943fc91a3afd70395cd496c647d5a6cc9d4b2b7fad8780e531581b77c4b903043593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000064f390d300000000000000000000000000000000000000000000000000000000000000030b090c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000080e531581b77c400000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000009184e72a0000000000000000000000000000000000000000000000000000080e531581b77c400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000b5ea574dd8f2b735424dfc8c4e16760fc44a931b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000c001a0a9ea84ad107d335afd5e5d2ddcc576f183be37386a9ac6c9d4469d0329c22e87a06a51ea5a0809f43bf72d0156f1db956da3a9f3da24b590b7eed01128ff84a2c1"],
"revertingTxHashes": ["0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"]
}`),
ExpectedHash: "0xcf3c567aede099e5455207ed81c4884f72a4c0c24ddca331163a335525cd22cc",
ExpectedUUID: "d9a3ae52-79a2-5ce9-a687-e2aa4183d5c6",
},
{
Payload: []byte(`{
"blockNumber": "0xA136F1F",
"txs": ["0x02f9037b018203cd8405f5e1008503692da370830388ba943fc91a3afd70395cd496c647d5a6cc9d4b2b7fad8780e531581b77c4b903043593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000064f390d300000000000000000000000000000000000000000000000000000000000000030b090c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000080e531581b77c400000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000009184e72a0000000000000000000000000000000000000000000000000000080e531581b77c400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000b5ea574dd8f2b735424dfc8c4e16760fc44a931b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000c001a0a9ea84ad107d335afd5e5d2ddcc576f183be37386a9ac6c9d4469d0329c22e87a06a51ea5a0809f43bf72d0156f1db956da3a9f3da24b590b7eed01128ff84a2c1"],
"revertingTxHashes": []
}`),
ExpectedHash: "0xcf3c567aede099e5455207ed81c4884f72a4c0c24ddca331163a335525cd22cc",
ExpectedUUID: "5d5bf52c-ac3f-57eb-a3e9-fc01b18ca516",
},
}

for i, input := range inputs {
t.Run(fmt.Sprintf("inout-%d", i), func(t *testing.T) {
bundle := &proxy.EthSendBundleArgs{}
require.NoError(t, json.Unmarshal(input.Payload, bundle))
hash, uuid, err := bundle.Validate()
require.NoError(t, err)
require.Equal(t, input.ExpectedHash, hash.Hex())
require.Equal(t, input.ExpectedUUID, uuid.String())
})
}
}

func TestMevSendBundleArgs_Validate(t *testing.T) {
// From: https://github.com/flashbots/rbuilder/blob/91f7a2c22eaeaf6c44e28c0bda98a2a0d566a6cb/crates/rbuilder/src/primitives/serialize.rs#L700
// NOTE: I had to dump the hash in a debugger to get the expected hash since the test above uses a computed hash
raw := []byte(`{
"version": "v0.1",
"inclusion": {
"block": "0x1"
},
"body": [
{
"bundle": {
"version": "v0.1",
"inclusion": {
"block": "0x1"
},
"body": [
{
"tx": "0x02f86b0180843b9aca00852ecc889a0082520894c87037874aed04e51c29f582394217a0a2b89d808080c080a0a463985c616dd8ee17d7ef9112af4e6e06a27b071525b42182fe7b0b5c8b4925a00af5ca177ffef2ff28449292505d41be578bebb77110dfc09361d2fb56998260",
"canRevert": true
},
{
"tx": "0x02f8730180843b9aca00852ecc889a008288b894c10000000000000000000000000000000000000088016345785d8a000080c001a07c8890151fed9a826f241d5a37c84062ebc55ca7f5caef4683dcda6ac99dbffba069108de72e4051a764f69c51a6b718afeff4299107963a5d84d5207b2d6932a4"
}
],
"validity": {
"refund": [
{
"bodyIdx": 0,
"percent": 90
}
],
"refundConfig": [
{
"address": "0x3e7dfb3e26a16e3dbf6dfeeff8a5ae7a04f73aad",
"percent": 100
}
]
}
}
},
{
"tx": "0x02f8730101843b9aca00852ecc889a008288b894c10000000000000000000000000000000000000088016345785d8a000080c001a0650c394d77981e46be3d8cf766ecc435ec3706375baed06eb9bef21f9da2828da064965fdf88b91575cd74f20301649c9d011b234cefb6c1761cc5dd579e4750b1"
}
],
"validity": {
"refund": [
{
"bodyIdx": 0,
"percent": 80
}
]
},
"metadata": {
"signer": "0x4696595f68034b47BbEc82dB62852B49a8EE7105"
}
}`)

bundle := &proxy.MevSendBundleArgs{}
require.NoError(t, json.Unmarshal(raw, bundle))
hash, err := bundle.Validate()
require.NoError(t, err)
require.Equal(t, "0x3b1994ad123d089f978074cfa197811b644e43b2b44b4c4710614f3a30ee0744", hash.Hex())
}

0 comments on commit 7d86ae6

Please sign in to comment.