Skip to content

Commit dc547ba

Browse files
authored
Versioned bundles support and proper identifiers for v2 (#41)
Extend (in a backwards compatible fashion) `bundle` model with `version` field. Support deterministic unique identifiers for such bundles
1 parent c1c7b4c commit dc547ba

File tree

2 files changed

+235
-16
lines changed

2 files changed

+235
-16
lines changed

rpctypes/types.go

Lines changed: 155 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"encoding/json"
99
"errors"
1010
"hash"
11+
"math/big"
1112
"sort"
1213

1314
"github.com/ethereum/go-ethereum/common"
@@ -28,13 +29,16 @@ const (
2829
BundleTxLimit = 100
2930
MevBundleTxLimit = 50
3031
MevBundleMaxDepth = 1
32+
BundleVersionV1 = "v1"
33+
BundleVersionV2 = "v2"
3134
)
3235

3336
var (
34-
ErrBundleNoTxs = errors.New("bundle with no txs")
35-
ErrBundleTooManyTxs = errors.New("too many txs in bundle")
36-
ErrMevBundleUnmatchedTx = errors.New("mev bundle with unmatched tx")
37-
ErrMevBundleTooDeep = errors.New("mev bundle too deep")
37+
ErrBundleNoTxs = errors.New("bundle with no txs")
38+
ErrBundleTooManyTxs = errors.New("too many txs in bundle")
39+
ErrMevBundleUnmatchedTx = errors.New("mev bundle with unmatched tx")
40+
ErrMevBundleTooDeep = errors.New("mev bundle too deep")
41+
ErrUnsupportedBundleVersion = errors.New("unsupported bundle version")
3842
)
3943

4044
type EthSendBundleArgs struct {
@@ -44,6 +48,7 @@ type EthSendBundleArgs struct {
4448
MaxTimestamp *uint64 `json:"maxTimestamp,omitempty"`
4549
RevertingTxHashes []common.Hash `json:"revertingTxHashes,omitempty"`
4650
ReplacementUUID *string `json:"replacementUuid,omitempty"`
51+
Version *string `json:"version,omitempty"`
4752

4853
// fields available only when receiving from the Flashbots or other builders, not users
4954
ReplacementNonce *uint64 `json:"replacementNonce,omitempty"`
@@ -186,6 +191,23 @@ func (b *EthSendBundleArgs) UniqueKey() uuid.UUID {
186191
_ = binary.Write(hash, binary.LittleEndian, *b.ReplacementNonce)
187192
}
188193

194+
sort.Slice(b.DroppingTxHashes, func(i, j int) bool {
195+
return bytes.Compare(b.DroppingTxHashes[i][:], b.DroppingTxHashes[j][:]) <= 0
196+
})
197+
for _, txHash := range b.DroppingTxHashes {
198+
_, _ = hash.Write(txHash.Bytes())
199+
}
200+
if b.RefundPercent != nil {
201+
_ = binary.Write(hash, binary.LittleEndian, *b.RefundPercent)
202+
}
203+
204+
if b.RefundRecipient != nil {
205+
_, _ = hash.Write(b.RefundRecipient.Bytes())
206+
}
207+
for _, txHash := range b.RefundTxHashes {
208+
_, _ = hash.Write([]byte(txHash))
209+
}
210+
189211
if b.SigningAddress != nil {
190212
_, _ = hash.Write(b.SigningAddress.Bytes())
191213
}
@@ -211,19 +233,136 @@ func (b *EthSendBundleArgs) Validate() (common.Hash, uuid.UUID, error) {
211233
}
212234
hashBytes := hasher.Sum(nil)
213235

214-
// then compute the uuid
215-
var buf []byte
216-
buf = binary.AppendVarint(buf, int64(blockNumber))
217-
buf = append(buf, hashBytes...)
218-
sort.Slice(b.RevertingTxHashes, func(i, j int) bool {
219-
return bytes.Compare(b.RevertingTxHashes[i][:], b.RevertingTxHashes[j][:]) <= 0
220-
})
221-
for _, txHash := range b.RevertingTxHashes {
222-
buf = append(buf, txHash[:]...)
236+
if b.Version == nil || *b.Version == BundleVersionV1 {
237+
// then compute the uuid
238+
var buf []byte
239+
buf = binary.AppendVarint(buf, int64(blockNumber))
240+
buf = append(buf, hashBytes...)
241+
sort.Slice(b.RevertingTxHashes, func(i, j int) bool {
242+
return bytes.Compare(b.RevertingTxHashes[i][:], b.RevertingTxHashes[j][:]) <= 0
243+
})
244+
for _, txHash := range b.RevertingTxHashes {
245+
buf = append(buf, txHash[:]...)
246+
}
247+
return common.BytesToHash(hashBytes),
248+
uuid.NewHash(sha256.New(), uuid.Nil, buf, 5),
249+
nil
223250
}
224-
return common.BytesToHash(hashBytes),
225-
uuid.NewHash(sha256.New(), uuid.Nil, buf, 5),
226-
nil
251+
252+
if *b.Version == BundleVersionV2 {
253+
// blockNumber, default 0
254+
blockNumber := uint64(0)
255+
if b.BlockNumber != nil {
256+
blockNumber = uint64(*b.BlockNumber)
257+
}
258+
259+
// minTimestamp, default 0
260+
minTimestamp := uint64(0)
261+
if b.MinTimestamp != nil {
262+
minTimestamp = *b.MinTimestamp
263+
}
264+
265+
// maxTimestamp, default ^uint64(0) (i.e. 0xFFFFFFFFFFFFFFFF in Rust)
266+
maxTimestamp := ^uint64(0)
267+
if b.MaxTimestamp != nil {
268+
maxTimestamp = *b.MaxTimestamp
269+
}
270+
271+
// Build up our buffer using variable-length encoding of the block
272+
// number, minTimestamp, maxTimestamp, #revertingTxHashes, #droppingTxHashes.
273+
var buf []byte
274+
buf = binary.AppendUvarint(buf, blockNumber)
275+
buf = binary.AppendUvarint(buf, minTimestamp)
276+
buf = binary.AppendUvarint(buf, maxTimestamp)
277+
buf = binary.AppendUvarint(buf, uint64(len(b.RevertingTxHashes)))
278+
buf = binary.AppendUvarint(buf, uint64(len(b.DroppingTxHashes)))
279+
280+
// Append the main txs keccak hash (already computed in hashBytes).
281+
buf = append(buf, hashBytes...)
282+
283+
// Sort revertingTxHashes and append them.
284+
sort.Slice(b.RevertingTxHashes, func(i, j int) bool {
285+
return bytes.Compare(b.RevertingTxHashes[i][:], b.RevertingTxHashes[j][:]) < 0
286+
})
287+
for _, h := range b.RevertingTxHashes {
288+
buf = append(buf, h[:]...)
289+
}
290+
291+
// Sort droppingTxHashes and append them.
292+
sort.Slice(b.DroppingTxHashes, func(i, j int) bool {
293+
return bytes.Compare(b.DroppingTxHashes[i][:], b.DroppingTxHashes[j][:]) < 0
294+
})
295+
for _, h := range b.DroppingTxHashes {
296+
buf = append(buf, h[:]...)
297+
}
298+
299+
// If a "refund" is present (analogous to the Rust code), we push:
300+
// refundPercent (1 byte)
301+
// refundRecipient (20 bytes, if an Ethereum address)
302+
// #refundTxHashes (varint)
303+
// each refundTxHash (32 bytes)
304+
// NOTE: The Rust code uses a single byte for `refund.percent`,
305+
// so we do the same here
306+
if b.RefundPercent != nil && *b.RefundPercent != 0 {
307+
if len(b.Txs) == 0 {
308+
// Bundle with not txs can't be refund-recipient
309+
return common.Hash{}, uuid.Nil, ErrBundleNoTxs
310+
}
311+
312+
// We only keep the low 8 bits of RefundPercent (mimicking Rust's `buff.push(u8)`).
313+
buf = append(buf, byte(*b.RefundPercent))
314+
315+
refundRecipient := b.RefundRecipient
316+
if refundRecipient == nil {
317+
var tx types.Transaction
318+
if err := tx.UnmarshalBinary(b.Txs[0]); err != nil {
319+
return common.Hash{}, uuid.Nil, err
320+
}
321+
from, err := types.Sender(types.LatestSignerForChainID(big.NewInt(1)), &tx)
322+
if err != nil {
323+
return common.Hash{}, uuid.Nil, err
324+
}
325+
refundRecipient = &from
326+
}
327+
bts := [20]byte(*refundRecipient)
328+
// RefundRecipient is a common.Address, which is 20 bytes in geth.
329+
buf = append(buf, bts[:]...)
330+
331+
var refundTxHashes []common.Hash
332+
for _, rth := range b.RefundTxHashes {
333+
// decode from hex
334+
refundTxHashes = append(refundTxHashes, common.HexToHash(rth))
335+
}
336+
337+
if len(refundTxHashes) == 0 {
338+
var lastTx types.Transaction
339+
if err := lastTx.UnmarshalBinary(b.Txs[len(b.Txs)-1]); err != nil {
340+
return common.Hash{}, uuid.Nil, err
341+
}
342+
refundTxHashes = []common.Hash{lastTx.Hash()}
343+
}
344+
345+
// #refundTxHashes
346+
buf = binary.AppendUvarint(buf, uint64(len(refundTxHashes)))
347+
348+
sort.Slice(refundTxHashes, func(i, j int) bool {
349+
return bytes.Compare(refundTxHashes[i][:], refundTxHashes[j][:]) < 0
350+
})
351+
for _, h := range refundTxHashes {
352+
buf = append(buf, h[:]...)
353+
}
354+
}
355+
356+
// Now produce a UUID from `buf` using SHA-256 in the same way the Rust code calls
357+
// `Self::uuid_from_buffer(buff)` (which is effectively a UUIDv5 but with SHA-256).
358+
finalUUID := uuid.NewHash(sha256.New(), uuid.Nil, buf, 5)
359+
360+
// Return the main txs keccak hash as well as the computed UUID
361+
return common.BytesToHash(hashBytes), finalUUID, nil
362+
}
363+
364+
return common.Hash{}, uuid.Nil, ErrUnsupportedBundleVersion
365+
227366
}
228367

229368
func (b *MevSendBundleArgs) UniqueKey() uuid.UUID {

rpctypes/types_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,86 @@ func TestEthSendBundleArgsValidate(t *testing.T) {
8787
ExpectedUUID: "35718fe4-5d24-51c8-93bf-9c961d7c3ea3",
8888
ExpectedUniqueKey: "3c718cb9-3f6c-5dc0-9d99-264dafc0b4e9",
8989
},
90+
{
91+
Payload: []byte(` {
92+
"version": "v2",
93+
"txs": [
94+
"0x02f86b83aa36a780800982520894f24a01ae29dec4629dfb4170647c4ed4efc392cd861ca62a4c95b880c080a07d37bb5a4da153a6fbe24cf1f346ef35748003d1d0fc59cf6c17fb22d49e42cea02c231ac233220b494b1ad501c440c8b1a34535cdb8ca633992d6f35b14428672"
95+
],
96+
"blockNumber": "0x0",
97+
"minTimestamp": 123,
98+
"maxTimestamp": 1234,
99+
"revertingTxHashes": ["0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"],
100+
"droppingTxHashes": ["0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"],
101+
"refundPercent": 1,
102+
"refundRecipient": "0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5",
103+
"refundTxHashes": ["0x75662ab9cb6d1be7334723db5587435616352c7e581a52867959ac24006ac1fe"]
104+
}`),
105+
ExpectedHash: "0xee3996920364173b0990f92cf6fbeb8a4ab832fe5549c1b728ac44aee0160f02",
106+
ExpectedUUID: "e2bdb8cd-9473-5a1b-b425-57fa7ecfe2c1",
107+
ExpectedUniqueKey: "a54c1e8f-936f-5868-bded-f5138c60b34a",
108+
},
109+
{
110+
Payload: []byte(` {
111+
"version": "v2",
112+
"txs": [
113+
"0x02f90408018303f1d4808483ab318e8304485c94a69babef1ca67a37ffaf7a485dfff3382056e78c8302be00b9014478e111f60000000000000000000000007f0f35bbf44c8343d14260372c469b331491567b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c4f4ff52950000000000000000000000000000000000000000000000000be75df44ebec5390000000000000000000000000000000000000000000036404c073ad050000000000000000000000000000000000000000000003e91fd871e8a6021ca93d911920000000000000000000000000000000000000000000000000000e91615b961030000000000000000000000000000000000000000000000000000000067eaa0b7ff8000000000000000000000000000000000000000000000000000000001229300000000000000000000000000000000000000000000000000000000f90253f9018394919fa96e88d67499339577fa202345436bcdaf79f9016ba0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513782a000000000000000000000000000000000000000000000000000000000000000a1a0bfd358e93f18da3ed276c3afdbdba00b8f0b6008a03476a6a86bd6320ee6938ba0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513785a00000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000a0a00000000000000000000000000000000000000000000000000000000000000002a0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513783a0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513784a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000004f85994c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f842a060802b93a9ac49b8c74d6ade12cf6235f4ac8c52c84fd39da757d0b2d720d76fa075245230289a9f0bf73a6c59aef6651b98b3833a62a3c0bd9ab6b0dec8ed4d8fd6947f0f35bbf44c8343d14260372c469b331491567bc0f85994d533a949740bb3306d119cc777fa900ba034cd52f842a07a7ff188ddb962db42160fb3fb573f4af0ebe1a1d6b701f1f1464b5ea43f7638a03d4653d86fe510221a71cfd2b1168b2e9af3e71339c63be5f905dabce97ee61f01a0c9d68ec80949077b6c28d45a6bf92727bc49d705d201bff8c62956201f5d3a81a036b7b953d7385d8fab8834722b7c66eea4a02a66434fc4f38ebfe8f5218a87b0"
114+
],
115+
"blockNumber": "0x0",
116+
"minTimestamp": 123,
117+
"maxTimestamp": 1234,
118+
"refundPercent": 20,
119+
"refundRecipient": "0xFF82BF5238637B7E5E345888BaB9cd99F5Ebe331",
120+
"refundTxHashes": ["0xffd9f02004350c16b312fd14ccc828f587c3c49ad3e9293391a398cc98c1a373"]
121+
}`),
122+
ExpectedHash: "0x90551b655a8a5b424064e802c0ec2daae864d8b786a788c2c6f9d7902feb42d2",
123+
ExpectedUUID: "e785c7c0-8bfa-508e-9c3f-cb24f1638de3",
124+
ExpectedUniqueKey: "fb7bff94-6f0d-5030-ab69-33adf953b742",
125+
},
126+
{
127+
Payload: []byte(` {
128+
"version": "v2",
129+
"txs": [
130+
"0x02f90408018303f1d4808483ab318e8304485c94a69babef1ca67a37ffaf7a485dfff3382056e78c8302be00b9014478e111f60000000000000000000000007f0f35bbf44c8343d14260372c469b331491567b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c4f4ff52950000000000000000000000000000000000000000000000000be75df44ebec5390000000000000000000000000000000000000000000036404c073ad050000000000000000000000000000000000000000000003e91fd871e8a6021ca93d911920000000000000000000000000000000000000000000000000000e91615b961030000000000000000000000000000000000000000000000000000000067eaa0b7ff8000000000000000000000000000000000000000000000000000000001229300000000000000000000000000000000000000000000000000000000f90253f9018394919fa96e88d67499339577fa202345436bcdaf79f9016ba0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513782a000000000000000000000000000000000000000000000000000000000000000a1a0bfd358e93f18da3ed276c3afdbdba00b8f0b6008a03476a6a86bd6320ee6938ba0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513785a00000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000a0a00000000000000000000000000000000000000000000000000000000000000002a0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513783a0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513784a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000004f85994c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f842a060802b93a9ac49b8c74d6ade12cf6235f4ac8c52c84fd39da757d0b2d720d76fa075245230289a9f0bf73a6c59aef6651b98b3833a62a3c0bd9ab6b0dec8ed4d8fd6947f0f35bbf44c8343d14260372c469b331491567bc0f85994d533a949740bb3306d119cc777fa900ba034cd52f842a07a7ff188ddb962db42160fb3fb573f4af0ebe1a1d6b701f1f1464b5ea43f7638a03d4653d86fe510221a71cfd2b1168b2e9af3e71339c63be5f905dabce97ee61f01a0c9d68ec80949077b6c28d45a6bf92727bc49d705d201bff8c62956201f5d3a81a036b7b953d7385d8fab8834722b7c66eea4a02a66434fc4f38ebfe8f5218a87b0"
131+
],
132+
"blockNumber": "0x0",
133+
"minTimestamp": 123,
134+
"maxTimestamp": 1234,
135+
"refundPercent": 20,
136+
"refundRecipient": "0xFF82BF5238637B7E5E345888BaB9cd99F5Ebe331"
137+
}`),
138+
ExpectedHash: "0x90551b655a8a5b424064e802c0ec2daae864d8b786a788c2c6f9d7902feb42d2",
139+
ExpectedUUID: "e785c7c0-8bfa-508e-9c3f-cb24f1638de3",
140+
ExpectedUniqueKey: "",
141+
},
142+
{
143+
Payload: []byte(` {
144+
"version": "v2",
145+
"txs": [
146+
"0x02f90408018303f1d4808483ab318e8304485c94a69babef1ca67a37ffaf7a485dfff3382056e78c8302be00b9014478e111f60000000000000000000000007f0f35bbf44c8343d14260372c469b331491567b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c4f4ff52950000000000000000000000000000000000000000000000000be75df44ebec5390000000000000000000000000000000000000000000036404c073ad050000000000000000000000000000000000000000000003e91fd871e8a6021ca93d911920000000000000000000000000000000000000000000000000000e91615b961030000000000000000000000000000000000000000000000000000000067eaa0b7ff8000000000000000000000000000000000000000000000000000000001229300000000000000000000000000000000000000000000000000000000f90253f9018394919fa96e88d67499339577fa202345436bcdaf79f9016ba0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513782a000000000000000000000000000000000000000000000000000000000000000a1a0bfd358e93f18da3ed276c3afdbdba00b8f0b6008a03476a6a86bd6320ee6938ba0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513785a00000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000a0a00000000000000000000000000000000000000000000000000000000000000002a0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513783a0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513784a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000004f85994c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f842a060802b93a9ac49b8c74d6ade12cf6235f4ac8c52c84fd39da757d0b2d720d76fa075245230289a9f0bf73a6c59aef6651b98b3833a62a3c0bd9ab6b0dec8ed4d8fd6947f0f35bbf44c8343d14260372c469b331491567bc0f85994d533a949740bb3306d119cc777fa900ba034cd52f842a07a7ff188ddb962db42160fb3fb573f4af0ebe1a1d6b701f1f1464b5ea43f7638a03d4653d86fe510221a71cfd2b1168b2e9af3e71339c63be5f905dabce97ee61f01a0c9d68ec80949077b6c28d45a6bf92727bc49d705d201bff8c62956201f5d3a81a036b7b953d7385d8fab8834722b7c66eea4a02a66434fc4f38ebfe8f5218a87b0"
147+
],
148+
"blockNumber": "0x0",
149+
"minTimestamp": 123,
150+
"maxTimestamp": 1234,
151+
"refundPercent": 20
152+
}`),
153+
ExpectedHash: "0x90551b655a8a5b424064e802c0ec2daae864d8b786a788c2c6f9d7902feb42d2",
154+
ExpectedUUID: "e785c7c0-8bfa-508e-9c3f-cb24f1638de3",
155+
ExpectedUniqueKey: "",
156+
},
157+
{
158+
Payload: []byte(`{
159+
"version": "v2",
160+
"txs": [
161+
"0x02f86b83aa36a780800982520894f24a01ae29dec4629dfb4170647c4ed4efc392cd861ca62a4c95b880c080a07d37bb5a4da153a6fbe24cf1f346ef35748003d1d0fc59cf6c17fb22d49e42cea02c231ac233220b494b1ad501c440c8b1a34535cdb8ca633992d6f35b14428672"
162+
],
163+
"blockNumber": "0x0",
164+
"revertingTxHashes": []
165+
}`),
166+
ExpectedHash: "0xee3996920364173b0990f92cf6fbeb8a4ab832fe5549c1b728ac44aee0160f02",
167+
ExpectedUUID: "22dc6bf0-9a12-5a76-9bbd-98ab77423415",
168+
ExpectedUniqueKey: "",
169+
},
90170
}
91171

92172
for i, input := range inputs {

0 commit comments

Comments
 (0)