From 7d86ae6bf04c14cd2da4917d3720d8be0b647f0f Mon Sep 17 00:00:00 2001 From: Ryan Schneider Date: Wed, 23 Oct 2024 05:54:16 -0700 Subject: [PATCH] feat: Add Bundle Validate methods (#4) * feat: Add Bundle Validate methods * fixup: Fix generated uuids/hashes, add unit tests * fixup: note source of mev bundle payload --- go.mod | 6 ++- go.sum | 3 ++ proxy/types.go | 83 +++++++++++++++++++++++++++++- proxy/types_test.go | 121 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 proxy/types_test.go diff --git a/go.mod b/go.mod index dfc46e6..f6c2e5f 100644 --- a/go.mod +++ b/go.mod @@ -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 ( @@ -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 @@ -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 @@ -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 ) diff --git a/go.sum b/go.sum index 11bcd18..6d65e6a 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/proxy/types.go b/proxy/types.go index 7fc91bb..14bcc82 100644 --- a/proxy/types.go +++ b/proxy/types.go @@ -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: @@ -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 @@ -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() @@ -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) @@ -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) diff --git a/proxy/types_test.go b/proxy/types_test.go new file mode 100644 index 0000000..335ae81 --- /dev/null +++ b/proxy/types_test.go @@ -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()) +}