Skip to content

Commit

Permalink
Support share bundle cancellations
Browse files Browse the repository at this point in the history
  • Loading branch information
dvush committed Nov 4, 2024
1 parent 1ee492f commit f01ef25
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 6 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.22
require (
github.com/VictoriaMetrics/metrics v1.35.1
github.com/ethereum/go-ethereum v1.14.10
github.com/flashbots/go-utils v0.8.1-0.20241030134403-501d395be6a9
github.com/flashbots/go-utils v0.8.1-0.20241104120502-7337c4b9d7b6
github.com/google/uuid v1.6.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/stretchr/testify v1.9.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ github.com/ethereum/go-ethereum v1.14.10 h1:kC24WjYeRjDy86LVo6MfF5Xs7nnUu+XG4Aja
github.com/ethereum/go-ethereum v1.14.10/go.mod h1:+l/fr42Mma+xBnhefL/+z11/hcmJ2egl+ScIVPjhc7E=
github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 h1:8NfxH2iXvJ60YRB8ChToFTUzl8awsc3cJ8CbLjGIl/A=
github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
github.com/flashbots/go-utils v0.8.1-0.20241030134403-501d395be6a9 h1:n0Xl5xg08GubAZRy/6aF7rBiyiSYpGlHoRiz957yWPg=
github.com/flashbots/go-utils v0.8.1-0.20241030134403-501d395be6a9/go.mod h1:Lo/nrlC+q8ANgT3e6MKALIJCU+V9qTSgNtoLk/q1uIw=
github.com/flashbots/go-utils v0.8.1-0.20241104120502-7337c4b9d7b6 h1:CYI4xzd3ho4p9tjm4j9vOKHOqes2zz0mSsgUXZVwUJ8=
github.com/flashbots/go-utils v0.8.1-0.20241104120502-7337c4b9d7b6/go.mod h1:Lo/nrlC+q8ANgT3e6MKALIJCU+V9qTSgNtoLk/q1uIw=
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
Expand Down
28 changes: 27 additions & 1 deletion proxy/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ var (
errSubsidyWrongEndpoint = errors.New("subsidy can only be called on public method")
errSubsidyWrongCaller = errors.New("subsidy can only be called by Flashbots")

errUUIDParse = errors.New("failed to parse UUID")

apiNow = time.Now
)

Expand Down Expand Up @@ -146,7 +148,6 @@ func (prx *ReceiverProxy) MevSendBundle(ctx context.Context, mevSendBundle rpcty
return err
}

// TODO: make sure that cancellations are handled by the builder properly
err = ValidateMevSendBundle(&mevSendBundle, publicEndpoint)
if err != nil {
return err
Expand All @@ -156,8 +157,33 @@ func (prx *ReceiverProxy) MevSendBundle(ctx context.Context, mevSendBundle rpcty
mevSendBundle.Metadata = &rpctypes.MevBundleMetadata{
Signer: &parsedRequest.signer,
}
if mevSendBundle.ReplacementUUID != "" {
replUUID, err := uuid.Parse(mevSendBundle.ReplacementUUID)
if err != nil {
return errors.Join(errUUIDParse, err)
}
replacementKey := replacementNonceKey{
uuid: replUUID,
signer: parsedRequest.signer,
}
// this is not atomic but the normal user will not send multiple replacements in parallel
nonce, ok := prx.replacementNonceRLU.Peek(replacementKey)
if ok {
nonce += 1
} else {
nonce = 0
}
prx.replacementNonceRLU.Add(replacementKey, nonce)
mevSendBundle.Metadata.ReplacementNonce = &nonce

if len(mevSendBundle.Body) == 0 {
cancelled := true
mevSendBundle.Metadata.Cancelled = &cancelled
}
}
}

// @note: unique key filterst same requests and it can interact with cancellations (you can't cancel multiple times per block)
uniqueKey := mevSendBundle.UniqueKey()
parsedRequest.requestArgUniqueKey = &uniqueKey

Expand Down
16 changes: 14 additions & 2 deletions proxy/api_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ var (
errRefundPercent = errors.New("refund percent field should not be set")
errRefundRecipient = errors.New("refund recipient field should not be set")
errRefundTxHashes = errors.New("refund tx hashes field should not be set")

errLocalEndpointSbundleMetadata = errors.New("mev share bundle should not containt metadata when sent to local endpoint")
)

func ValidateEthSendBundle(args *rpctypes.EthSendBundleArgs, publicEndpoint bool) error {
Expand Down Expand Up @@ -54,8 +56,18 @@ func ValidateEthCancelBundle(args *rpctypes.EthCancelBundleArgs, publicEndpoint
return nil
}

func ValidateMevSendBundle(args *rpctypes.MevSendBundleArgs, _ bool) error {
func ValidateMevSendBundle(args *rpctypes.MevSendBundleArgs, publicEndpoint bool) error {
// @perf it calculates hash
_, err := args.Validate()
return err
if err != nil {
return err
}

if !publicEndpoint {
if args.Metadata != nil {
return errLocalEndpointSbundleMetadata
}
}

return nil
}
11 changes: 11 additions & 0 deletions proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,16 @@ var (
requestsRLUTTL = time.Second * 12

peerUpdateTime = time.Minute * 5

replacementNonceSize = 4096
replacementNonceTTL = time.Second * 5 * 12
)

type replacementNonceKey struct {
uuid uuid.UUID
signer common.Address
}

type ReceiverProxy struct {
ReceiverProxyConstantConfig

Expand All @@ -48,6 +56,8 @@ type ReceiverProxy struct {

requestUniqueKeysRLU *expirable.LRU[uuid.UUID, struct{}]

replacementNonceRLU *expirable.LRU[replacementNonceKey, int]

peerUpdaterClose chan struct{}
}

Expand Down Expand Up @@ -98,6 +108,7 @@ func NewReceiverProxy(config ReceiverProxyConfig) (*ReceiverProxy, error) {
Certificate: certificate,
localBuilder: localBuilder,
requestUniqueKeysRLU: expirable.NewLRU[uuid.UUID, struct{}](requestsRLUSize, nil, requestsRLUTTL),
replacementNonceRLU: expirable.NewLRU[replacementNonceKey, int](replacementNonceSize, nil, replacementNonceTTL),
}
maxRequestBodySizeBytes := DefaultMaxRequestBodySizeBytes
if config.MaxRequestBodySizeBytes != 0 {
Expand Down
115 changes: 115 additions & 0 deletions proxy/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@ import (
"fmt"
"io"
"log/slog"
"math/big"
"net"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"

"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/crypto"
"github.com/flashbots/go-utils/rpctypes"
"github.com/flashbots/go-utils/signature"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -357,3 +362,113 @@ func TestProxySendToArchive(t *testing.T) {
expectedArchiveRequest := `{"method":"flashbots_newOrderEvents","params":[{"orderEvents":[{"eth_sendBundle":{"params":{"txs":null,"blockNumber":"0x7b","signingAddress":"0x9349365494be4f6205e5d44bdc7ec7dcd134becf"},"metadata":{"receivedAt":1730000000000}}},{"eth_sendBundle":{"params":{"txs":null,"blockNumber":"0x1c8","signingAddress":"0x9349365494be4f6205e5d44bdc7ec7dcd134becf"},"metadata":{"receivedAt":1730000000000}}}]}],"id":0,"jsonrpc":"2.0"}`
require.Equal(t, expectedArchiveRequest, archiveRequest.body)
}

func createTestTx(i int) *hexutil.Bytes {
privateKey, err := crypto.HexToECDSA("c7589782d55a642c8ced7794ddcb24b62d4ebefbb81001034cb46545ff80e39e")
if err != nil {
panic(err)
}

chainID := big.NewInt(1)
txData := &types.DynamicFeeTx{
ChainID: chainID,
Nonce: uint64(i),
GasTipCap: big.NewInt(1),
GasFeeCap: big.NewInt(1),
Gas: 21000,
To: &common.Address{},
Value: big.NewInt(0),
Data: nil,
AccessList: nil,
}
tx, err := types.SignNewTx(privateKey, types.LatestSignerForChainID(big.NewInt(1)), txData)
if err != nil {
panic(err)
}
binary, err := tx.MarshalBinary()
if err != nil {
panic(err)
}
hexBytes := hexutil.Bytes(binary)
return &hexBytes
}

func TestProxyShareBundleReplacementUUIDAndCancellation(t *testing.T) {
defer func() {
proxiesFlushQueue()
for {
select {
case <-time.After(time.Millisecond * 100):
expectNoRequest(t, archiveServerRequests)
return
case <-archiveServerRequests:
}
}
}()

signer, err := signature.NewSignerFromHexPrivateKey("0xd63b3c447fdea415a05e4c0b859474d14105a88178efdf350bc9f7b05be3cc58")
require.NoError(t, err)
client, err := RPCClientWithCertAndSigner(proxies[0].localServerEndpoint, proxies[0].proxy.PublicCertPEM, signer)
require.NoError(t, err)

// we start with no peers
builderHubPeers = nil
err = proxies[0].proxy.RegisterSecrets(context.Background())
require.NoError(t, err)
proxiesUpdatePeers(t)

// first call
resp, err := client.Call(context.Background(), MevSendBundleMethod, &rpctypes.MevSendBundleArgs{
Version: "v0.1",
ReplacementUUID: "550e8400-e29b-41d4-a716-446655440000",
Inclusion: rpctypes.MevBundleInclusion{
BlockNumber: 10,
},
Body: []rpctypes.MevBundleBody{
{
Tx: createTestTx(0),
},
},
})
require.NoError(t, err)
require.Nil(t, resp.Error)

expectedRequest := `{"method":"mev_sendBundle","params":[{"version":"v0.1","replacementUuid":"550e8400-e29b-41d4-a716-446655440000","inclusion":{"block":"0xa","maxBlock":"0x0"},"body":[{"tx":"0x02f862018001018252089400000000000000000000000000000000000000008080c001a05900a5ea3e4e07980b0d6276e2764b734be64d64c20f3eb87746c7ed1d72aa26a073f3a0877bd80098bd720afd1ba2c6d2d9c76d87cbc23f098d7be74902d9bfd4"}],"validity":{},"metadata":{"signer":"0x9349365494be4f6205e5d44bdc7ec7dcd134becf","replacementNonce":0}}],"id":0,"jsonrpc":"2.0"}`

builderRequest := expectRequest(t, proxies[0].localBuilderRequests)
require.Equal(t, expectedRequest, builderRequest.body)

// second call
resp, err = client.Call(context.Background(), MevSendBundleMethod, &rpctypes.MevSendBundleArgs{
Version: "v0.1",
ReplacementUUID: "550e8400-e29b-41d4-a716-446655440000",
Inclusion: rpctypes.MevBundleInclusion{
BlockNumber: 10,
},
Body: []rpctypes.MevBundleBody{
{
Tx: createTestTx(1),
},
},
})
require.NoError(t, err)
require.Nil(t, resp.Error)

expectedRequest = `{"method":"mev_sendBundle","params":[{"version":"v0.1","replacementUuid":"550e8400-e29b-41d4-a716-446655440000","inclusion":{"block":"0xa","maxBlock":"0x0"},"body":[{"tx":"0x02f862010101018252089400000000000000000000000000000000000000008080c001a03b5edc6a7fe16f7c7bf25c56281b86107e742a922f900ac94293225b380fd5bea00f2ea6392842711064ca5c0fe12d812a60e8936ec8dc13ca95ecde8b262fd1fe"}],"validity":{},"metadata":{"signer":"0x9349365494be4f6205e5d44bdc7ec7dcd134becf","replacementNonce":1}}],"id":0,"jsonrpc":"2.0"}`

builderRequest = expectRequest(t, proxies[0].localBuilderRequests)
require.Equal(t, expectedRequest, builderRequest.body)

// cancell
resp, err = client.Call(context.Background(), MevSendBundleMethod, &rpctypes.MevSendBundleArgs{
Version: "v0.1",
ReplacementUUID: "550e8400-e29b-41d4-a716-446655440000",
})
require.NoError(t, err)
require.Nil(t, resp.Error)

expectedRequest = `{"method":"mev_sendBundle","params":[{"version":"v0.1","replacementUuid":"550e8400-e29b-41d4-a716-446655440000","inclusion":{"block":"0x0","maxBlock":"0x0"},"body":null,"validity":{},"metadata":{"signer":"0x9349365494be4f6205e5d44bdc7ec7dcd134becf","replacementNonce":2,"cancelled":true}}],"id":0,"jsonrpc":"2.0"}`

builderRequest = expectRequest(t, proxies[0].localBuilderRequests)
require.Equal(t, expectedRequest, builderRequest.body)
}

0 comments on commit f01ef25

Please sign in to comment.