diff --git a/core/types/suave_structs.go b/core/types/suave_structs.go index 132f5ef81c..b6b6f2b8dd 100755 --- a/core/types/suave_structs.go +++ b/core/types/suave_structs.go @@ -1,5 +1,5 @@ // Code generated by suave/gen. DO NOT EDIT. -// Hash: 1e4312debc072d2aa68e9aa2ddf7ef264f705d23d86a1aea0966c3751b36d318 +// Hash: 4ccd7b1f4af28d241d36e113a93b55aaf1db59d9e3b16ef9e46458b7bb062660 package types import "github.com/ethereum/go-ethereum/common" @@ -36,6 +36,19 @@ type HttpRequest struct { Body []byte } +type SimulateTransactionResult struct { + Egp uint64 + Logs []*SimulatedLog + Success bool + Error string +} + +type SimulatedLog struct { + Data []byte + Addr common.Address + Topics []common.Hash +} + type Withdrawal struct { Index uint64 Validator uint64 diff --git a/core/vm/contracts_suave.go b/core/vm/contracts_suave.go index d8a9a956e2..8c15d6e3d6 100644 --- a/core/vm/contracts_suave.go +++ b/core/vm/contracts_suave.go @@ -2,6 +2,7 @@ package vm import ( "bytes" + "context" "fmt" "io" "net/http" @@ -224,3 +225,20 @@ func (s *suaveRuntime) doHTTPRequest(request types.HttpRequest) ([]byte, error) } return data, nil } + +func (s *suaveRuntime) newBuilder() (string, error) { + return s.suaveContext.Backend.ConfidentialEthBackend.NewSession(context.Background()) +} + +func (s *suaveRuntime) simulateTransaction(session string, txnBytes []byte) (types.SimulateTransactionResult, error) { + txn := new(types.Transaction) + if err := txn.UnmarshalBinary(txnBytes); err != nil { + return types.SimulateTransactionResult{}, err + } + + result, err := s.suaveContext.Backend.ConfidentialEthBackend.AddTransaction(context.Background(), session, txn) + if err != nil { + return types.SimulateTransactionResult{}, err + } + return *result, nil +} diff --git a/core/vm/contracts_suave_runtime_adapter.go b/core/vm/contracts_suave_runtime_adapter.go index 5410a7f7a3..3e7cf8d79f 100644 --- a/core/vm/contracts_suave_runtime_adapter.go +++ b/core/vm/contracts_suave_runtime_adapter.go @@ -1,5 +1,5 @@ // Code generated by suave/gen. DO NOT EDIT. -// Hash: 1e4312debc072d2aa68e9aa2ddf7ef264f705d23d86a1aea0966c3751b36d318 +// Hash: 4ccd7b1f4af28d241d36e113a93b55aaf1db59d9e3b16ef9e46458b7bb062660 package vm import ( @@ -26,10 +26,12 @@ type SuaveRuntime interface { extractHint(bundleData []byte) ([]byte, error) fetchDataRecords(cond uint64, namespace string) ([]types.DataRecord, error) fillMevShareBundle(dataId types.DataId) ([]byte, error) + newBuilder() (string, error) newDataRecord(decryptionCondition uint64, allowedPeekers []common.Address, allowedStores []common.Address, dataType string) (types.DataRecord, error) signEthTransaction(txn []byte, chainId string, signingKey string) ([]byte, error) signMessage(digest []byte, signingKey string) ([]byte, error) simulateBundle(bundleData []byte) (uint64, error) + simulateTransaction(session string, txn []byte) (types.SimulateTransactionResult, error) submitBundleJsonRPC(url string, method string, params []byte) ([]byte, error) submitEthBlockToRelay(relayUrl string, builderBid []byte) ([]byte, error) } @@ -44,16 +46,18 @@ var ( extractHintAddr = common.HexToAddress("0x0000000000000000000000000000000042100037") fetchDataRecordsAddr = common.HexToAddress("0x0000000000000000000000000000000042030001") fillMevShareBundleAddr = common.HexToAddress("0x0000000000000000000000000000000043200001") + newBuilderAddr = common.HexToAddress("0x0000000000000000000000000000000053200001") newDataRecordAddr = common.HexToAddress("0x0000000000000000000000000000000042030000") signEthTransactionAddr = common.HexToAddress("0x0000000000000000000000000000000040100001") signMessageAddr = common.HexToAddress("0x0000000000000000000000000000000040100003") simulateBundleAddr = common.HexToAddress("0x0000000000000000000000000000000042100000") + simulateTransactionAddr = common.HexToAddress("0x0000000000000000000000000000000053200002") submitBundleJsonRPCAddr = common.HexToAddress("0x0000000000000000000000000000000043000001") submitEthBlockToRelayAddr = common.HexToAddress("0x0000000000000000000000000000000042100002") ) var addrList = []common.Address{ - buildEthBlockAddr, confidentialInputsAddr, confidentialRetrieveAddr, confidentialStoreAddr, doHTTPRequestAddr, ethcallAddr, extractHintAddr, fetchDataRecordsAddr, fillMevShareBundleAddr, newDataRecordAddr, signEthTransactionAddr, signMessageAddr, simulateBundleAddr, submitBundleJsonRPCAddr, submitEthBlockToRelayAddr, + buildEthBlockAddr, confidentialInputsAddr, confidentialRetrieveAddr, confidentialStoreAddr, doHTTPRequestAddr, ethcallAddr, extractHintAddr, fetchDataRecordsAddr, fillMevShareBundleAddr, newBuilderAddr, newDataRecordAddr, signEthTransactionAddr, signMessageAddr, simulateBundleAddr, simulateTransactionAddr, submitBundleJsonRPCAddr, submitEthBlockToRelayAddr, } type SuaveRuntimeAdapter struct { @@ -89,6 +93,9 @@ func (b *SuaveRuntimeAdapter) run(addr common.Address, input []byte) ([]byte, er case fillMevShareBundleAddr: return b.fillMevShareBundle(input) + case newBuilderAddr: + return b.newBuilder(input) + case newDataRecordAddr: return b.newDataRecord(input) @@ -101,6 +108,9 @@ func (b *SuaveRuntimeAdapter) run(addr common.Address, input []byte) ([]byte, er case simulateBundleAddr: return b.simulateBundle(input) + case simulateTransactionAddr: + return b.simulateTransaction(input) + case submitBundleJsonRPCAddr: return b.submitBundleJsonRPC(input) @@ -464,6 +474,40 @@ func (b *SuaveRuntimeAdapter) fillMevShareBundle(input []byte) (res []byte, err } +func (b *SuaveRuntimeAdapter) newBuilder(input []byte) (res []byte, err error) { + var ( + unpacked []interface{} + result []byte + ) + + _ = unpacked + _ = result + + unpacked, err = artifacts.SuaveAbi.Methods["newBuilder"].Inputs.Unpack(input) + if err != nil { + err = errFailedToUnpackInput + return + } + + var () + + var ( + id string + ) + + if id, err = b.impl.newBuilder(); err != nil { + return + } + + result, err = artifacts.SuaveAbi.Methods["newBuilder"].Outputs.Pack(id) + if err != nil { + err = errFailedToPackOutput + return + } + return result, nil + +} + func (b *SuaveRuntimeAdapter) newDataRecord(input []byte) (res []byte, err error) { var ( unpacked []interface{} @@ -628,6 +672,46 @@ func (b *SuaveRuntimeAdapter) simulateBundle(input []byte) (res []byte, err erro } +func (b *SuaveRuntimeAdapter) simulateTransaction(input []byte) (res []byte, err error) { + var ( + unpacked []interface{} + result []byte + ) + + _ = unpacked + _ = result + + unpacked, err = artifacts.SuaveAbi.Methods["simulateTransaction"].Inputs.Unpack(input) + if err != nil { + err = errFailedToUnpackInput + return + } + + var ( + session string + txn []byte + ) + + session = unpacked[0].(string) + txn = unpacked[1].([]byte) + + var ( + output1 types.SimulateTransactionResult + ) + + if output1, err = b.impl.simulateTransaction(session, txn); err != nil { + return + } + + result, err = artifacts.SuaveAbi.Methods["simulateTransaction"].Outputs.Pack(output1) + if err != nil { + err = errFailedToPackOutput + return + } + return result, nil + +} + func (b *SuaveRuntimeAdapter) submitBundleJsonRPC(input []byte) (res []byte, err error) { var ( unpacked []interface{} diff --git a/core/vm/contracts_suave_runtime_adapter_test.go b/core/vm/contracts_suave_runtime_adapter_test.go index a166e176c2..9ed29c8807 100644 --- a/core/vm/contracts_suave_runtime_adapter_test.go +++ b/core/vm/contracts_suave_runtime_adapter_test.go @@ -75,6 +75,14 @@ func (m *mockRuntime) doHTTPRequest(request types.HttpRequest) ([]byte, error) { return []byte{0x1}, nil } +func (m *mockRuntime) newBuilder() (string, error) { + return "", nil +} + +func (m *mockRuntime) simulateTransaction(session string, txn []byte) (types.SimulateTransactionResult, error) { + return types.SimulateTransactionResult{}, nil +} + func TestRuntimeAdapter(t *testing.T) { adapter := &SuaveRuntimeAdapter{ impl: &mockRuntime{}, diff --git a/core/vm/contracts_suave_test.go b/core/vm/contracts_suave_test.go index eb02ccb0d9..a70ab55ee7 100644 --- a/core/vm/contracts_suave_test.go +++ b/core/vm/contracts_suave_test.go @@ -20,6 +20,14 @@ type mockSuaveBackend struct { func (m *mockSuaveBackend) Start() error { return nil } func (m *mockSuaveBackend) Stop() error { return nil } +func (m *mockSuaveBackend) NewSession(ctx context.Context) (string, error) { + return "", nil +} + +func (m *mockSuaveBackend) AddTransaction(ctx context.Context, sessionId string, tx *types.Transaction) (*types.SimulateTransactionResult, error) { + return &types.SimulateTransactionResult{}, nil +} + func (m *mockSuaveBackend) InitializeBid(record suave.DataRecord) error { return nil } diff --git a/eth/backend.go b/eth/backend.go index 3df4ac35ff..882bc61a8d 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -59,6 +59,8 @@ import ( "github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/suave/backends" suave_backends "github.com/ethereum/go-ethereum/suave/backends" + suave_builder "github.com/ethereum/go-ethereum/suave/builder" + suave_builder_api "github.com/ethereum/go-ethereum/suave/builder/api" suave "github.com/ethereum/go-ethereum/suave/core" "github.com/ethereum/go-ethereum/suave/cstore" "github.com/flashbots/go-boost-utils/bls" @@ -351,6 +353,13 @@ func (s *Ethereum) APIs() []rpc.API { Service: backends.NewEthBackendServer(s.APIBackend), }) + sessionManager := suave_builder.NewSessionManager(s.blockchain, &suave_builder.Config{}) + + apis = append(apis, rpc.API{ + Namespace: "suavex", + Service: suave_builder_api.NewServer(sessionManager), + }) + // Append any APIs exposed explicitly by the consensus engine apis = append(apis, s.engine.APIs(s.BlockChain())...) diff --git a/suave/artifacts/SuaveLib.json b/suave/artifacts/SuaveLib.json index 853935c329..2588f6f93c 100644 --- a/suave/artifacts/SuaveLib.json +++ b/suave/artifacts/SuaveLib.json @@ -1 +1 @@ -[{"type":"error","name":"PeekerReverted","inputs":[{"name":"addr","type":"address"},{"name":"err","type":"bytes"}]},{"type":"function","name":"buildEthBlock","inputs":[{"name":"blockArgs","type":"tuple","internalType":"struct Suave.BuildBlockArgs","components":[{"name":"slot","type":"uint64","internalType":"uint64"},{"name":"proposerPubkey","type":"bytes","internalType":"bytes"},{"name":"parent","type":"bytes32","internalType":"bytes32"},{"name":"timestamp","type":"uint64","internalType":"uint64"},{"name":"feeRecipient","type":"address","internalType":"address"},{"name":"gasLimit","type":"uint64","internalType":"uint64"},{"name":"random","type":"bytes32","internalType":"bytes32"},{"name":"withdrawals","type":"tuple[]","internalType":"struct Suave.Withdrawal[]","components":[{"name":"index","type":"uint64","internalType":"uint64"},{"name":"validator","type":"uint64","internalType":"uint64"},{"name":"Address","type":"address","internalType":"address"},{"name":"amount","type":"uint64","internalType":"uint64"}]},{"name":"extra","type":"bytes","internalType":"bytes"}]},{"name":"dataId","type":"bytes16","internalType":"struct Suave.DataId"},{"name":"namespace","type":"string","internalType":"string"}],"outputs":[{"name":"output1","type":"bytes","internalType":"bytes"},{"name":"output2","type":"bytes","internalType":"bytes"}]},{"type":"function","name":"confidentialInputs","outputs":[{"name":"output1","type":"bytes","internalType":"bytes"}]},{"type":"function","name":"confidentialRetrieve","inputs":[{"name":"dataId","type":"bytes16","internalType":"struct Suave.DataId"},{"name":"key","type":"string","internalType":"string"}],"outputs":[{"name":"output1","type":"bytes","internalType":"bytes"}]},{"type":"function","name":"confidentialStore","inputs":[{"name":"dataId","type":"bytes16","internalType":"struct Suave.DataId"},{"name":"key","type":"string","internalType":"string"},{"name":"data1","type":"bytes","internalType":"bytes"}]},{"type":"function","name":"doHTTPRequest","inputs":[{"name":"request","type":"tuple","internalType":"struct Suave.HttpRequest","components":[{"name":"url","type":"string","internalType":"string"},{"name":"method","type":"string","internalType":"string"},{"name":"headers","type":"string[]","internalType":"string[]"},{"name":"body","type":"bytes","internalType":"bytes"}]}],"outputs":[{"name":"response","type":"bytes","internalType":"bytes"}]},{"type":"function","name":"ethcall","inputs":[{"name":"contractAddr","type":"address","internalType":"address"},{"name":"input1","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"output1","type":"bytes","internalType":"bytes"}]},{"type":"function","name":"extractHint","inputs":[{"name":"bundleData","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"output1","type":"bytes","internalType":"bytes"}]},{"type":"function","name":"fetchDataRecords","inputs":[{"name":"cond","type":"uint64","internalType":"uint64"},{"name":"namespace","type":"string","internalType":"string"}],"outputs":[{"name":"dataRecords","type":"tuple[]","internalType":"struct Suave.DataRecord[]","components":[{"name":"id","type":"bytes16","internalType":"struct Suave.DataId"},{"name":"salt","type":"bytes16","internalType":"struct Suave.DataId"},{"name":"decryptionCondition","type":"uint64","internalType":"uint64"},{"name":"allowedPeekers","type":"address[]","internalType":"address[]"},{"name":"allowedStores","type":"address[]","internalType":"address[]"},{"name":"version","type":"string","internalType":"string"}]}]},{"type":"function","name":"fillMevShareBundle","inputs":[{"name":"dataId","type":"bytes16","internalType":"struct Suave.DataId"}],"outputs":[{"name":"encodedBundle","type":"bytes","internalType":"bytes"}]},{"type":"function","name":"newDataRecord","inputs":[{"name":"decryptionCondition","type":"uint64","internalType":"uint64"},{"name":"allowedPeekers","type":"address[]","internalType":"address[]"},{"name":"allowedStores","type":"address[]","internalType":"address[]"},{"name":"dataType","type":"string","internalType":"string"}],"outputs":[{"name":"dataRecord","type":"tuple","internalType":"struct Suave.DataRecord","components":[{"name":"id","type":"bytes16","internalType":"struct Suave.DataId"},{"name":"salt","type":"bytes16","internalType":"struct Suave.DataId"},{"name":"decryptionCondition","type":"uint64","internalType":"uint64"},{"name":"allowedPeekers","type":"address[]","internalType":"address[]"},{"name":"allowedStores","type":"address[]","internalType":"address[]"},{"name":"version","type":"string","internalType":"string"}]}]},{"type":"function","name":"signEthTransaction","inputs":[{"name":"txn","type":"bytes","internalType":"bytes"},{"name":"chainId","type":"string","internalType":"string"},{"name":"signingKey","type":"string","internalType":"string"}],"outputs":[{"name":"output1","type":"bytes","internalType":"bytes"}]},{"type":"function","name":"signMessage","inputs":[{"name":"digest","type":"bytes","internalType":"bytes"},{"name":"signingKey","type":"string","internalType":"string"}],"outputs":[{"name":"output1","type":"bytes","internalType":"bytes"}]},{"type":"function","name":"simulateBundle","inputs":[{"name":"bundleData","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"output1","type":"uint64","internalType":"uint64"}]},{"type":"function","name":"submitBundleJsonRPC","inputs":[{"name":"url","type":"string","internalType":"string"},{"name":"method","type":"string","internalType":"string"},{"name":"params","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"output1","type":"bytes","internalType":"bytes"}]},{"type":"function","name":"submitEthBlockToRelay","inputs":[{"name":"relayUrl","type":"string","internalType":"string"},{"name":"builderBid","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"output1","type":"bytes","internalType":"bytes"}]}] \ No newline at end of file +[{"type":"error","name":"PeekerReverted","inputs":[{"name":"addr","type":"address"},{"name":"err","type":"bytes"}]},{"type":"function","name":"buildEthBlock","inputs":[{"name":"blockArgs","type":"tuple","internalType":"struct Suave.BuildBlockArgs","components":[{"name":"slot","type":"uint64","internalType":"uint64"},{"name":"proposerPubkey","type":"bytes","internalType":"bytes"},{"name":"parent","type":"bytes32","internalType":"bytes32"},{"name":"timestamp","type":"uint64","internalType":"uint64"},{"name":"feeRecipient","type":"address","internalType":"address"},{"name":"gasLimit","type":"uint64","internalType":"uint64"},{"name":"random","type":"bytes32","internalType":"bytes32"},{"name":"withdrawals","type":"tuple[]","internalType":"struct Suave.Withdrawal[]","components":[{"name":"index","type":"uint64","internalType":"uint64"},{"name":"validator","type":"uint64","internalType":"uint64"},{"name":"Address","type":"address","internalType":"address"},{"name":"amount","type":"uint64","internalType":"uint64"}]},{"name":"extra","type":"bytes","internalType":"bytes"}]},{"name":"dataId","type":"bytes16","internalType":"struct Suave.DataId"},{"name":"namespace","type":"string","internalType":"string"}],"outputs":[{"name":"output1","type":"bytes","internalType":"bytes"},{"name":"output2","type":"bytes","internalType":"bytes"}]},{"type":"function","name":"confidentialInputs","outputs":[{"name":"output1","type":"bytes","internalType":"bytes"}]},{"type":"function","name":"confidentialRetrieve","inputs":[{"name":"dataId","type":"bytes16","internalType":"struct Suave.DataId"},{"name":"key","type":"string","internalType":"string"}],"outputs":[{"name":"output1","type":"bytes","internalType":"bytes"}]},{"type":"function","name":"confidentialStore","inputs":[{"name":"dataId","type":"bytes16","internalType":"struct Suave.DataId"},{"name":"key","type":"string","internalType":"string"},{"name":"data1","type":"bytes","internalType":"bytes"}]},{"type":"function","name":"doHTTPRequest","inputs":[{"name":"request","type":"tuple","internalType":"struct Suave.HttpRequest","components":[{"name":"url","type":"string","internalType":"string"},{"name":"method","type":"string","internalType":"string"},{"name":"headers","type":"string[]","internalType":"string[]"},{"name":"body","type":"bytes","internalType":"bytes"}]}],"outputs":[{"name":"response","type":"bytes","internalType":"bytes"}]},{"type":"function","name":"ethcall","inputs":[{"name":"contractAddr","type":"address","internalType":"address"},{"name":"input1","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"output1","type":"bytes","internalType":"bytes"}]},{"type":"function","name":"extractHint","inputs":[{"name":"bundleData","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"output1","type":"bytes","internalType":"bytes"}]},{"type":"function","name":"fetchDataRecords","inputs":[{"name":"cond","type":"uint64","internalType":"uint64"},{"name":"namespace","type":"string","internalType":"string"}],"outputs":[{"name":"dataRecords","type":"tuple[]","internalType":"struct Suave.DataRecord[]","components":[{"name":"id","type":"bytes16","internalType":"struct Suave.DataId"},{"name":"salt","type":"bytes16","internalType":"struct Suave.DataId"},{"name":"decryptionCondition","type":"uint64","internalType":"uint64"},{"name":"allowedPeekers","type":"address[]","internalType":"address[]"},{"name":"allowedStores","type":"address[]","internalType":"address[]"},{"name":"version","type":"string","internalType":"string"}]}]},{"type":"function","name":"fillMevShareBundle","inputs":[{"name":"dataId","type":"bytes16","internalType":"struct Suave.DataId"}],"outputs":[{"name":"encodedBundle","type":"bytes","internalType":"bytes"}]},{"type":"function","name":"newBuilder","outputs":[{"name":"id","type":"string","internalType":"string"}]},{"type":"function","name":"newDataRecord","inputs":[{"name":"decryptionCondition","type":"uint64","internalType":"uint64"},{"name":"allowedPeekers","type":"address[]","internalType":"address[]"},{"name":"allowedStores","type":"address[]","internalType":"address[]"},{"name":"dataType","type":"string","internalType":"string"}],"outputs":[{"name":"dataRecord","type":"tuple","internalType":"struct Suave.DataRecord","components":[{"name":"id","type":"bytes16","internalType":"struct Suave.DataId"},{"name":"salt","type":"bytes16","internalType":"struct Suave.DataId"},{"name":"decryptionCondition","type":"uint64","internalType":"uint64"},{"name":"allowedPeekers","type":"address[]","internalType":"address[]"},{"name":"allowedStores","type":"address[]","internalType":"address[]"},{"name":"version","type":"string","internalType":"string"}]}]},{"type":"function","name":"signEthTransaction","inputs":[{"name":"txn","type":"bytes","internalType":"bytes"},{"name":"chainId","type":"string","internalType":"string"},{"name":"signingKey","type":"string","internalType":"string"}],"outputs":[{"name":"output1","type":"bytes","internalType":"bytes"}]},{"type":"function","name":"signMessage","inputs":[{"name":"digest","type":"bytes","internalType":"bytes"},{"name":"signingKey","type":"string","internalType":"string"}],"outputs":[{"name":"output1","type":"bytes","internalType":"bytes"}]},{"type":"function","name":"simulateBundle","inputs":[{"name":"bundleData","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"output1","type":"uint64","internalType":"uint64"}]},{"type":"function","name":"simulateTransaction","inputs":[{"name":"session","type":"string","internalType":"string"},{"name":"txn","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"output1","type":"tuple","internalType":"struct Suave.SimulateTransactionResult","components":[{"name":"egp","type":"uint64","internalType":"uint64"},{"name":"logs","type":"tuple[]","internalType":"struct Suave.SimulatedLog[]","components":[{"name":"data","type":"bytes","internalType":"bytes"},{"name":"addr","type":"address","internalType":"address"},{"name":"topics","type":"bytes32[]","internalType":"bytes32[]"}]},{"name":"success","type":"bool","internalType":"bool"},{"name":"error","type":"string","internalType":"string"}]}]},{"type":"function","name":"submitBundleJsonRPC","inputs":[{"name":"url","type":"string","internalType":"string"},{"name":"method","type":"string","internalType":"string"},{"name":"params","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"output1","type":"bytes","internalType":"bytes"}]},{"type":"function","name":"submitEthBlockToRelay","inputs":[{"name":"relayUrl","type":"string","internalType":"string"},{"name":"builderBid","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"output1","type":"bytes","internalType":"bytes"}]}] \ No newline at end of file diff --git a/suave/artifacts/addresses.go b/suave/artifacts/addresses.go index 4091707443..c8e54c7cd1 100644 --- a/suave/artifacts/addresses.go +++ b/suave/artifacts/addresses.go @@ -1,5 +1,5 @@ // Code generated by suave/gen. DO NOT EDIT. -// Hash: 1e4312debc072d2aa68e9aa2ddf7ef264f705d23d86a1aea0966c3751b36d318 +// Hash: 4ccd7b1f4af28d241d36e113a93b55aaf1db59d9e3b16ef9e46458b7bb062660 package artifacts import ( @@ -17,10 +17,12 @@ var ( extractHintAddr = common.HexToAddress("0x0000000000000000000000000000000042100037") fetchDataRecordsAddr = common.HexToAddress("0x0000000000000000000000000000000042030001") fillMevShareBundleAddr = common.HexToAddress("0x0000000000000000000000000000000043200001") + newBuilderAddr = common.HexToAddress("0x0000000000000000000000000000000053200001") newDataRecordAddr = common.HexToAddress("0x0000000000000000000000000000000042030000") signEthTransactionAddr = common.HexToAddress("0x0000000000000000000000000000000040100001") signMessageAddr = common.HexToAddress("0x0000000000000000000000000000000040100003") simulateBundleAddr = common.HexToAddress("0x0000000000000000000000000000000042100000") + simulateTransactionAddr = common.HexToAddress("0x0000000000000000000000000000000053200002") submitBundleJsonRPCAddr = common.HexToAddress("0x0000000000000000000000000000000043000001") submitEthBlockToRelayAddr = common.HexToAddress("0x0000000000000000000000000000000042100002") ) @@ -35,10 +37,12 @@ var SuaveMethods = map[string]common.Address{ "extractHint": extractHintAddr, "fetchDataRecords": fetchDataRecordsAddr, "fillMevShareBundle": fillMevShareBundleAddr, + "newBuilder": newBuilderAddr, "newDataRecord": newDataRecordAddr, "signEthTransaction": signEthTransactionAddr, "signMessage": signMessageAddr, "simulateBundle": simulateBundleAddr, + "simulateTransaction": simulateTransactionAddr, "submitBundleJsonRPC": submitBundleJsonRPCAddr, "submitEthBlockToRelay": submitEthBlockToRelayAddr, } @@ -63,6 +67,8 @@ func PrecompileAddressToName(addr common.Address) string { return "fetchDataRecords" case fillMevShareBundleAddr: return "fillMevShareBundle" + case newBuilderAddr: + return "newBuilder" case newDataRecordAddr: return "newDataRecord" case signEthTransactionAddr: @@ -71,6 +77,8 @@ func PrecompileAddressToName(addr common.Address) string { return "signMessage" case simulateBundleAddr: return "simulateBundle" + case simulateTransactionAddr: + return "simulateTransaction" case submitBundleJsonRPCAddr: return "submitBundleJsonRPC" case submitEthBlockToRelayAddr: diff --git a/suave/backends/eth_backends.go b/suave/backends/eth_backends.go index 98940e595d..f76552a7af 100644 --- a/suave/backends/eth_backends.go +++ b/suave/backends/eth_backends.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rpc" + builder "github.com/ethereum/go-ethereum/suave/builder/api" suave "github.com/ethereum/go-ethereum/suave/core" "github.com/ethereum/go-ethereum/trie" ) @@ -17,7 +18,9 @@ var ( _ EthBackend = &RemoteEthBackend{} ) -type EthMock struct{} +type EthMock struct { + *builder.MockServer +} func (e *EthMock) BuildEthBlock(ctx context.Context, args *suave.BuildBlockArgs, txs types.Transactions) (*engine.ExecutionPayloadEnvelope, error) { block := types.NewBlock(&types.Header{GasUsed: 1000}, txs, nil, nil, trie.NewStackTrie(nil)) @@ -40,15 +43,20 @@ func (e *EthMock) Call(ctx context.Context, contractAddr common.Address, input [ type RemoteEthBackend struct { endpoint string client *rpc.Client + + *builder.APIClient } func NewRemoteEthBackend(endpoint string) *RemoteEthBackend { - return &RemoteEthBackend{ + r := &RemoteEthBackend{ endpoint: endpoint, } + + r.APIClient = builder.NewClientFromRPC(r) + return r } -func (e *RemoteEthBackend) call(ctx context.Context, result interface{}, method string, args ...interface{}) error { +func (e *RemoteEthBackend) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { if e.client == nil { // should lock var err error @@ -72,21 +80,21 @@ func (e *RemoteEthBackend) call(ctx context.Context, result interface{}, method func (e *RemoteEthBackend) BuildEthBlock(ctx context.Context, args *suave.BuildBlockArgs, txs types.Transactions) (*engine.ExecutionPayloadEnvelope, error) { var result engine.ExecutionPayloadEnvelope - err := e.call(ctx, &result, "suavex_buildEthBlock", args, txs) + err := e.CallContext(ctx, &result, "suavex_buildEthBlock", args, txs) return &result, err } func (e *RemoteEthBackend) BuildEthBlockFromBundles(ctx context.Context, args *suave.BuildBlockArgs, bundles []types.SBundle) (*engine.ExecutionPayloadEnvelope, error) { var result engine.ExecutionPayloadEnvelope - err := e.call(ctx, &result, "suavex_buildEthBlockFromBundles", args, bundles) + err := e.CallContext(ctx, &result, "suavex_buildEthBlockFromBundles", args, bundles) return &result, err } func (e *RemoteEthBackend) Call(ctx context.Context, contractAddr common.Address, input []byte) ([]byte, error) { var result []byte - err := e.call(ctx, &result, "suavex_call", contractAddr, input) + err := e.CallContext(ctx, &result, "suavex_call", contractAddr, input) return result, err } diff --git a/suave/builder/api/api.go b/suave/builder/api/api.go new file mode 100644 index 0000000000..b11d237ba2 --- /dev/null +++ b/suave/builder/api/api.go @@ -0,0 +1,12 @@ +package api + +import ( + "context" + + "github.com/ethereum/go-ethereum/core/types" +) + +type API interface { + NewSession(ctx context.Context) (string, error) + AddTransaction(ctx context.Context, sessionId string, tx *types.Transaction) (*types.SimulateTransactionResult, error) +} diff --git a/suave/builder/api/api_client.go b/suave/builder/api/api_client.go new file mode 100644 index 0000000000..3baefde531 --- /dev/null +++ b/suave/builder/api/api_client.go @@ -0,0 +1,42 @@ +package api + +import ( + "context" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rpc" +) + +var _ API = (*APIClient)(nil) + +type APIClient struct { + rpc rpcClient +} + +func NewClient(endpoint string) (*APIClient, error) { + clt, err := rpc.Dial(endpoint) + if err != nil { + return nil, err + } + return NewClientFromRPC(clt), nil +} + +type rpcClient interface { + CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error +} + +func NewClientFromRPC(rpc rpcClient) *APIClient { + return &APIClient{rpc: rpc} +} + +func (a *APIClient) NewSession(ctx context.Context) (string, error) { + var id string + err := a.rpc.CallContext(ctx, &id, "suavex_newSession") + return id, err +} + +func (a *APIClient) AddTransaction(ctx context.Context, sessionId string, tx *types.Transaction) (*types.SimulateTransactionResult, error) { + var receipt *types.SimulateTransactionResult + err := a.rpc.CallContext(ctx, &receipt, "suavex_addTransaction", sessionId, tx) + return receipt, err +} diff --git a/suave/builder/api/api_server.go b/suave/builder/api/api_server.go new file mode 100644 index 0000000000..7eb6daf795 --- /dev/null +++ b/suave/builder/api/api_server.go @@ -0,0 +1,43 @@ +package api + +import ( + "context" + + "github.com/ethereum/go-ethereum/core/types" +) + +// sessionManager is the backend that manages the session state of the builder API. +type sessionManager interface { + NewSession() (string, error) + AddTransaction(sessionId string, tx *types.Transaction) (*types.SimulateTransactionResult, error) +} + +func NewServer(s sessionManager) *Server { + api := &Server{ + sessionMngr: s, + } + return api +} + +type Server struct { + sessionMngr sessionManager +} + +func (s *Server) NewSession(ctx context.Context) (string, error) { + return s.sessionMngr.NewSession() +} + +func (s *Server) AddTransaction(ctx context.Context, sessionId string, tx *types.Transaction) (*types.SimulateTransactionResult, error) { + return s.sessionMngr.AddTransaction(sessionId, tx) +} + +type MockServer struct { +} + +func (s *MockServer) NewSession(ctx context.Context) (string, error) { + return "", nil +} + +func (s *MockServer) AddTransaction(ctx context.Context, sessionId string, tx *types.Transaction) (*types.SimulateTransactionResult, error) { + return &types.SimulateTransactionResult{}, nil +} diff --git a/suave/builder/api/api_test.go b/suave/builder/api/api_test.go new file mode 100644 index 0000000000..51122eef02 --- /dev/null +++ b/suave/builder/api/api_test.go @@ -0,0 +1,39 @@ +package api + +import ( + "context" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rpc" + "github.com/stretchr/testify/require" +) + +func TestAPI(t *testing.T) { + srv := rpc.NewServer() + + builderAPI := NewServer(&nullSessionManager{}) + srv.RegisterName("suavex", builderAPI) + + c := NewClientFromRPC(rpc.DialInProc(srv)) + + res0, err := c.NewSession(context.Background()) + require.NoError(t, err) + require.Equal(t, res0, "1") + + txn := types.NewTransaction(0, common.Address{}, big.NewInt(1), 1, big.NewInt(1), []byte{}) + _, err = c.AddTransaction(context.Background(), "1", txn) + require.NoError(t, err) +} + +type nullSessionManager struct{} + +func (n *nullSessionManager) NewSession() (string, error) { + return "1", nil +} + +func (n *nullSessionManager) AddTransaction(sessionId string, tx *types.Transaction) (*types.SimulateTransactionResult, error) { + return &types.SimulateTransactionResult{Logs: []*types.SimulatedLog{}}, nil +} diff --git a/suave/builder/builder.go b/suave/builder/builder.go new file mode 100644 index 0000000000..0d5c30ce9e --- /dev/null +++ b/suave/builder/builder.go @@ -0,0 +1,77 @@ +package builder + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" +) + +type builder struct { + config *builderConfig + txns []*types.Transaction + receipts []*types.Receipt + state *state.StateDB + gasPool *core.GasPool + gasUsed *uint64 +} + +type builderConfig struct { + preState *state.StateDB + header *types.Header + config *params.ChainConfig + context core.ChainContext +} + +func newBuilder(config *builderConfig) *builder { + gp := core.GasPool(config.header.GasLimit) + var gasUsed uint64 + + return &builder{ + config: config, + state: config.preState.Copy(), + gasPool: &gp, + gasUsed: &gasUsed, + } +} + +func (b *builder) AddTransaction(txn *types.Transaction) (*types.SimulateTransactionResult, error) { + dummyAuthor := common.Address{} + + vmConfig := vm.Config{ + NoBaseFee: true, + } + + snap := b.state.Snapshot() + + b.state.SetTxContext(txn.Hash(), len(b.txns)) + receipt, err := core.ApplyTransaction(b.config.config, b.config.context, &dummyAuthor, b.gasPool, b.state, b.config.header, txn, b.gasUsed, vmConfig) + if err != nil { + b.state.RevertToSnapshot(snap) + + result := &types.SimulateTransactionResult{ + Success: false, + Error: err.Error(), + } + return result, nil + } + + b.txns = append(b.txns, txn) + b.receipts = append(b.receipts, receipt) + + result := &types.SimulateTransactionResult{ + Success: true, + Logs: []*types.SimulatedLog{}, + } + for _, log := range receipt.Logs { + result.Logs = append(result.Logs, &types.SimulatedLog{ + Addr: log.Address, + Topics: log.Topics, + Data: log.Data, + }) + } + + return result, nil +} diff --git a/suave/builder/builder_test.go b/suave/builder/builder_test.go new file mode 100644 index 0000000000..0ceae209ae --- /dev/null +++ b/suave/builder/builder_test.go @@ -0,0 +1,122 @@ +package builder + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" +) + +func TestBuilder_AddTxn_Simple(t *testing.T) { + to := common.Address{0x01, 0x10, 0xab} + + mock := newMockBuilder(t) + txn := mock.state.newTransfer(t, to, big.NewInt(1)) + + _, err := mock.builder.AddTransaction(txn) + require.NoError(t, err) + + mock.expect(t, expectedResult{ + txns: []*types.Transaction{ + txn, + }, + balances: map[common.Address]*big.Int{ + to: big.NewInt(1), + }, + }) +} + +func newMockBuilder(t *testing.T) *mockBuilder { + // create a dummy header at 0 + header := &types.Header{ + Number: big.NewInt(0), + GasLimit: 1000000000000, + Time: 1000, + Difficulty: big.NewInt(1), + } + + mState := newMockState(t) + + m := &mockBuilder{ + state: mState, + } + + stateRef, err := mState.stateAt(mState.stateRoot) + require.NoError(t, err) + + config := &builderConfig{ + header: header, + preState: stateRef, + config: mState.chainConfig, + context: m, // m implements ChainContext with panics + } + m.builder = newBuilder(config) + + return m +} + +type mockBuilder struct { + builder *builder + state *mockState +} + +func (m *mockBuilder) Engine() consensus.Engine { + panic("TODO") +} + +func (m *mockBuilder) GetHeader(common.Hash, uint64) *types.Header { + panic("TODO") +} + +type expectedResult struct { + txns []*types.Transaction + balances map[common.Address]*big.Int +} + +func (m *mockBuilder) expect(t *testing.T, res expectedResult) { + // validate txns + if len(res.txns) != len(m.builder.txns) { + t.Fatalf("expected %d txns, got %d", len(res.txns), len(m.builder.txns)) + } + for indx, txn := range res.txns { + if txn.Hash() != m.builder.txns[indx].Hash() { + t.Fatalf("expected txn %d to be %s, got %s", indx, txn.Hash(), m.builder.txns[indx].Hash()) + } + } + + // The receipts must be the same as the txns + if len(res.txns) != len(m.builder.receipts) { + t.Fatalf("expected %d receipts, got %d", len(res.txns), len(m.builder.receipts)) + } + for indx, txn := range res.txns { + if txn.Hash() != m.builder.receipts[indx].TxHash { + t.Fatalf("expected receipt %d to be %s, got %s", indx, txn.Hash(), m.builder.receipts[indx].TxHash) + } + } + + // The gas left in the pool must be the header gas limit minus + // the total gas consumed by all the transactions in the block. + totalGasConsumed := uint64(0) + for _, receipt := range m.builder.receipts { + totalGasConsumed += receipt.GasUsed + } + if m.builder.gasPool.Gas() != m.builder.config.header.GasLimit-totalGasConsumed { + t.Fatalf("expected gas pool to be %d, got %d", m.builder.config.header.GasLimit-totalGasConsumed, m.builder.gasPool.Gas()) + } + + // The 'gasUsed' must match the total gas consumed by all the transactions + if *m.builder.gasUsed != totalGasConsumed { + t.Fatalf("expected gas used to be %d, got %d", totalGasConsumed, m.builder.gasUsed) + } + + // The state must match the expected balances + for addr, expectedBalance := range res.balances { + balance := m.builder.state.GetBalance(addr) + if balance.Cmp(expectedBalance) != 0 { + t.Fatalf("expected balance of %s to be %d, got %d", addr, expectedBalance, balance) + } + } +} diff --git a/suave/builder/session_manager.go b/suave/builder/session_manager.go new file mode 100644 index 0000000000..b3e740cbd4 --- /dev/null +++ b/suave/builder/session_manager.go @@ -0,0 +1,138 @@ +package builder + +import ( + "fmt" + "math/big" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/misc" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + "github.com/google/uuid" +) + +// blockchain is the minimum interface to the blockchain +// required to build a block +type blockchain interface { + core.ChainContext + + // Header returns the current tip of the chain + CurrentHeader() *types.Header + + // StateAt returns the state at the given root + StateAt(root common.Hash) (*state.StateDB, error) + + // Config returns the chain config + Config() *params.ChainConfig +} + +type Config struct { + GasCeil uint64 + SessionIdleTimeout time.Duration +} + +type SessionManager struct { + sessions map[string]*builder + sessionTimers map[string]*time.Timer + sessionsLock sync.RWMutex + blockchain blockchain + config *Config +} + +func NewSessionManager(blockchain blockchain, config *Config) *SessionManager { + if config.GasCeil == 0 { + config.GasCeil = 1000000000000000000 + } + if config.SessionIdleTimeout == 0 { + config.SessionIdleTimeout = 5 * time.Second + } + + s := &SessionManager{ + sessions: make(map[string]*builder), + sessionTimers: make(map[string]*time.Timer), + blockchain: blockchain, + config: config, + } + return s +} + +// NewSession creates a new builder session and returns the session id +func (s *SessionManager) NewSession() (string, error) { + s.sessionsLock.Lock() + defer s.sessionsLock.Unlock() + + parent := s.blockchain.CurrentHeader() + + chainConfig := s.blockchain.Config() + + header := &types.Header{ + ParentHash: parent.Hash(), + Number: new(big.Int).Add(parent.Number, common.Big1), + GasLimit: core.CalcGasLimit(parent.GasLimit, s.config.GasCeil), + Time: 1000, // TODO: fix this + Coinbase: common.Address{}, // TODO: fix this + Difficulty: big.NewInt(1), + } + + // Set baseFee and GasLimit if we are on an EIP-1559 chain + if chainConfig.IsLondon(header.Number) { + header.BaseFee = misc.CalcBaseFee(chainConfig, parent) + if !chainConfig.IsLondon(parent.Number) { + parentGasLimit := parent.GasLimit * chainConfig.ElasticityMultiplier() + header.GasLimit = core.CalcGasLimit(parentGasLimit, s.config.GasCeil) + } + } + + stateRef, err := s.blockchain.StateAt(parent.Root) + if err != nil { + return "", err + } + + cfg := &builderConfig{ + preState: stateRef, + header: header, + config: s.blockchain.Config(), + context: s.blockchain, + } + + id := uuid.New().String()[:7] + s.sessions[id] = newBuilder(cfg) + + // start session timer + s.sessionTimers[id] = time.AfterFunc(s.config.SessionIdleTimeout, func() { + s.sessionsLock.Lock() + defer s.sessionsLock.Unlock() + + delete(s.sessions, id) + delete(s.sessionTimers, id) + }) + + return id, nil +} + +func (s *SessionManager) getSession(sessionId string) (*builder, error) { + s.sessionsLock.RLock() + defer s.sessionsLock.RUnlock() + + session, ok := s.sessions[sessionId] + if !ok { + return nil, fmt.Errorf("session %s not found", sessionId) + } + + // reset session timer + s.sessionTimers[sessionId].Reset(s.config.SessionIdleTimeout) + + return session, nil +} + +func (s *SessionManager) AddTransaction(sessionId string, tx *types.Transaction) (*types.SimulateTransactionResult, error) { + builder, err := s.getSession(sessionId) + if err != nil { + return nil, err + } + return builder.AddTransaction(tx) +} diff --git a/suave/builder/session_manager_test.go b/suave/builder/session_manager_test.go new file mode 100644 index 0000000000..8d9dbaef4f --- /dev/null +++ b/suave/builder/session_manager_test.go @@ -0,0 +1,183 @@ +package builder + +import ( + "crypto/ecdsa" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" +) + +func TestSessionManager_SessionTimeout(t *testing.T) { + mngr, _ := newSessionManager(t, &Config{ + SessionIdleTimeout: 500 * time.Millisecond, + }) + + id, err := mngr.NewSession() + require.NoError(t, err) + + time.Sleep(1 * time.Second) + + _, err = mngr.getSession(id) + require.Error(t, err) +} + +func TestSessionManager_SessionRefresh(t *testing.T) { + mngr, _ := newSessionManager(t, &Config{ + SessionIdleTimeout: 500 * time.Millisecond, + }) + + id, err := mngr.NewSession() + require.NoError(t, err) + + // if we query the session under the idle timeout, + // we should be able to refresh it + for i := 0; i < 5; i++ { + time.Sleep(250 * time.Millisecond) + + _, err = mngr.getSession(id) + require.NoError(t, err) + } + + // if we query the session after the idle timeout, + // we should get an error + + time.Sleep(1 * time.Second) + + _, err = mngr.getSession(id) + require.Error(t, err) +} + +func TestSessionManager_StartSession(t *testing.T) { + // test that the session starts and it can simulate transactions + mngr, bMock := newSessionManager(t, &Config{}) + + id, err := mngr.NewSession() + require.NoError(t, err) + + txn := bMock.state.newTransfer(t, common.Address{}, big.NewInt(1)) + receipt, err := mngr.AddTransaction(id, txn) + require.NoError(t, err) + require.NotNil(t, receipt) +} + +func newSessionManager(t *testing.T, cfg *Config) (*SessionManager, *blockchainMock) { + if cfg == nil { + cfg = &Config{} + } + + state := newMockState(t) + + bMock := &blockchainMock{ + state: state, + } + return NewSessionManager(bMock, cfg), bMock +} + +type blockchainMock struct { + state *mockState +} + +func (b *blockchainMock) Engine() consensus.Engine { + panic("TODO") +} + +func (b *blockchainMock) GetHeader(common.Hash, uint64) *types.Header { + panic("TODO") +} + +func (b *blockchainMock) Config() *params.ChainConfig { + return b.state.chainConfig +} + +func (b *blockchainMock) CurrentHeader() *types.Header { + return &types.Header{ + Number: big.NewInt(1), + Difficulty: big.NewInt(1), + Root: b.state.stateRoot, + } +} + +func (b *blockchainMock) StateAt(root common.Hash) (*state.StateDB, error) { + return b.state.stateAt(root) +} + +type mockState struct { + stateRoot common.Hash + statedb state.Database + + premineKey *ecdsa.PrivateKey + premineKeyAdd common.Address + + nextNonce uint64 // figure out a better way + signer types.Signer + + chainConfig *params.ChainConfig +} + +func newMockState(t *testing.T) *mockState { + premineKey, _ := crypto.GenerateKey() // TODO: it would be nice to have it deterministic + premineKeyAddr := crypto.PubkeyToAddress(premineKey.PublicKey) + + // create a state reference with at least one premined account + // In order to test the statedb in isolation, we are going + // to commit this pre-state to a memory database + db := state.NewDatabase(rawdb.NewMemoryDatabase()) + preState, err := state.New(types.EmptyRootHash, db, nil) + require.NoError(t, err) + + preState.AddBalance(premineKeyAddr, big.NewInt(1000000000000000000)) + + root, err := preState.Commit(true) + require.NoError(t, err) + + // for the sake of this test, we only need all the forks enabled + chainConfig := params.SuaveChainConfig + + // Disable london so that we do not check gasFeeCap (TODO: Fix) + chainConfig.LondonBlock = big.NewInt(100) + + return &mockState{ + statedb: db, + stateRoot: root, + premineKey: premineKey, + premineKeyAdd: premineKeyAddr, + signer: types.NewEIP155Signer(chainConfig.ChainID), + chainConfig: chainConfig, + } +} + +func (m *mockState) stateAt(root common.Hash) (*state.StateDB, error) { + return state.New(root, m.statedb, nil) +} + +func (m *mockState) getNonce() uint64 { + next := m.nextNonce + m.nextNonce++ + return next +} + +func (m *mockState) newTransfer(t *testing.T, to common.Address, amount *big.Int) *types.Transaction { + tx := types.NewTransaction(m.getNonce(), to, amount, 1000000, big.NewInt(1), nil) + return m.newTxn(t, tx) +} + +func (m *mockState) newTxn(t *testing.T, tx *types.Transaction) *types.Transaction { + // sign the transaction + signature, err := crypto.Sign(m.signer.Hash(tx).Bytes(), m.premineKey) + require.NoError(t, err) + + // include the signature in the transaction + tx, err = tx.WithSignature(m.signer, signature) + require.NoError(t, err) + + return tx +} diff --git a/suave/core/types.go b/suave/core/types.go index c7de9cd998..98c8c602af 100644 --- a/suave/core/types.go +++ b/suave/core/types.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/node" + builder "github.com/ethereum/go-ethereum/suave/builder/api" ) var AllowedPeekerAny = common.HexToAddress("0xC8df3686b4Afb2BB53e60EAe97EF043FE03Fb829") // "*" @@ -64,4 +65,6 @@ type ConfidentialEthBackend interface { BuildEthBlock(ctx context.Context, args *BuildBlockArgs, txs types.Transactions) (*engine.ExecutionPayloadEnvelope, error) BuildEthBlockFromBundles(ctx context.Context, args *BuildBlockArgs, bundles []types.SBundle) (*engine.ExecutionPayloadEnvelope, error) Call(ctx context.Context, contractAddr common.Address, input []byte) ([]byte, error) + + builder.API } diff --git a/suave/e2e/workflow_test.go b/suave/e2e/workflow_test.go index 1548b48305..1f2b8c9633 100644 --- a/suave/e2e/workflow_test.go +++ b/suave/e2e/workflow_test.go @@ -1248,6 +1248,37 @@ func TestE2ERemoteCalls(t *testing.T) { }) } +func TestE2EPrecompile_Builder(t *testing.T) { + fr := newFramework(t, WithKettleAddress()) + defer fr.Close() + + clt := fr.NewSDKClient() + + // TODO: We do this all the time, unify in a single function? + contractAddr := common.Address{0x3} + sourceContract := sdk.GetContract(contractAddr, exampleCallSourceContract.Abi, clt) + + // build a txn that calls the contract 'func1' in 'ExampleEthCallTarget' + var subTxns []*types.Transaction + for i := 0; i < 2; i++ { + subTxn, _ := types.SignTx(types.NewTx(&types.LegacyTx{ + To: &testAddr3, + Gas: 220000, + GasPrice: big.NewInt(13), + Nonce: uint64(i), + Data: exampleCallTargetContract.Abi.Methods["func1"].ID, + }), signer, testKey) + + subTxns = append(subTxns, subTxn) + } + + subTxnBytes1, _ := subTxns[0].MarshalBinary() + subTxnBytes2, _ := subTxns[1].MarshalBinary() + + _, err := sourceContract.SendTransaction("sessionE2ETest", []interface{}{subTxnBytes1, subTxnBytes2}, nil) + require.NoError(t, err) +} + type clientWrapper struct { t *testing.T diff --git a/suave/gen/suave_spec.yaml b/suave/gen/suave_spec.yaml index 623abdd85f..d0394f796c 100644 --- a/suave/gen/suave_spec.yaml +++ b/suave/gen/suave_spec.yaml @@ -56,6 +56,24 @@ structs: type: string[] - name: body type: bytes + - name: SimulateTransactionResult + fields: + - name: egp + type: uint64 + - name: logs + type: SimulatedLog[] + - name: success + type: bool + - name: error + type: string + - name: SimulatedLog + fields: + - name: data + type: bytes + - name: addr + type: address + - name: topics + type: bytes32[] functions: - name: confidentialInputs address: "0x0000000000000000000000000000000042010001" @@ -230,3 +248,20 @@ functions: fields: - name: response type: bytes + - name: newBuilder + address: "0x0000000000000000000000000000000053200001" + output: + fields: + - name: id + type: string + - name: simulateTransaction + address: "0x0000000000000000000000000000000053200002" + input: + - name: session + type: string + - name: txn + type: bytes + output: + fields: + - name: output1 + type: SimulateTransactionResult \ No newline at end of file diff --git a/suave/sol/libraries/Suave.sol b/suave/sol/libraries/Suave.sol index f155bbc88e..0a0235a76f 100644 --- a/suave/sol/libraries/Suave.sol +++ b/suave/sol/libraries/Suave.sol @@ -34,6 +34,19 @@ library Suave { bytes body; } + struct SimulateTransactionResult { + uint64 egp; + SimulatedLog[] logs; + bool success; + string error; + } + + struct SimulatedLog { + bytes data; + address addr; + bytes32[] topics; + } + struct Withdrawal { uint64 index; uint64 validator; @@ -63,6 +76,8 @@ library Suave { address public constant FILL_MEV_SHARE_BUNDLE = 0x0000000000000000000000000000000043200001; + address public constant NEW_BUILDER = 0x0000000000000000000000000000000053200001; + address public constant NEW_DATA_RECORD = 0x0000000000000000000000000000000042030000; address public constant SIGN_ETH_TRANSACTION = 0x0000000000000000000000000000000040100001; @@ -71,6 +86,8 @@ library Suave { address public constant SIMULATE_BUNDLE = 0x0000000000000000000000000000000042100000; + address public constant SIMULATE_TRANSACTION = 0x0000000000000000000000000000000053200002; + address public constant SUBMIT_BUNDLE_JSON_RPC = 0x0000000000000000000000000000000043000001; address public constant SUBMIT_ETH_BLOCK_TO_RELAY = 0x0000000000000000000000000000000042100002; @@ -174,6 +191,15 @@ library Suave { return data; } + function newBuilder() internal view returns (string memory) { + (bool success, bytes memory data) = NEW_BUILDER.staticcall(abi.encode()); + if (!success) { + revert PeekerReverted(NEW_BUILDER, data); + } + + return abi.decode(data, (string)); + } + function newDataRecord( uint64 decryptionCondition, address[] memory allowedPeekers, @@ -221,6 +247,19 @@ library Suave { return abi.decode(data, (uint64)); } + function simulateTransaction(string memory session, bytes memory txn) + internal + view + returns (SimulateTransactionResult memory) + { + (bool success, bytes memory data) = SIMULATE_TRANSACTION.staticcall(abi.encode(session, txn)); + if (!success) { + revert PeekerReverted(SIMULATE_TRANSACTION, data); + } + + return abi.decode(data, (SimulateTransactionResult)); + } + function submitBundleJsonRPC(string memory url, string memory method, bytes memory params) internal view diff --git a/suave/sol/libraries/SuaveForge.sol b/suave/sol/libraries/SuaveForge.sol index 4f86026499..e42d6c8684 100644 --- a/suave/sol/libraries/SuaveForge.sol +++ b/suave/sol/libraries/SuaveForge.sol @@ -93,6 +93,12 @@ library SuaveForge { return data; } + function newBuilder() internal view returns (string memory) { + bytes memory data = forgeIt("0x0000000000000000000000000000000053200001", abi.encode()); + + return abi.decode(data, (string)); + } + function newDataRecord( uint64 decryptionCondition, address[] memory allowedPeekers, @@ -129,6 +135,16 @@ library SuaveForge { return abi.decode(data, (uint64)); } + function simulateTransaction(string memory session, bytes memory txn) + internal + view + returns (Suave.SimulateTransactionResult memory) + { + bytes memory data = forgeIt("0x0000000000000000000000000000000053200002", abi.encode(session, txn)); + + return abi.decode(data, (Suave.SimulateTransactionResult)); + } + function submitBundleJsonRPC(string memory url, string memory method, bytes memory params) internal view diff --git a/suave/sol/standard_peekers/example.sol b/suave/sol/standard_peekers/example.sol index 2a105bf7b0..6c8bf5ae97 100644 --- a/suave/sol/standard_peekers/example.sol +++ b/suave/sol/standard_peekers/example.sol @@ -23,10 +23,43 @@ contract ExampleEthCallSource { function remoteCall(Suave.HttpRequest memory request) public { Suave.doHTTPRequest(request); } + + function emptyCallback() public payable {} + + function sessionE2ETest(bytes memory subTxn, bytes memory subTxn2) public payable returns (bytes memory) { + string memory id = Suave.newBuilder(); + + Suave.SimulateTransactionResult memory sim1 = Suave.simulateTransaction(id, subTxn); + require(sim1.success == true); + require(sim1.logs.length == 1); + + // simulate the same transaction again should fail because the nonce is the same + Suave.SimulateTransactionResult memory sim2 = Suave.simulateTransaction(id, subTxn); + require(sim2.success == false); + + // now, simulate the transaction with the correct nonce + Suave.SimulateTransactionResult memory sim3 = Suave.simulateTransaction(id, subTxn2); + require(sim3.success == true); + require(sim3.logs.length == 2); + + return abi.encodeWithSelector(this.emptyCallback.selector); + } } contract ExampleEthCallTarget { + uint256 stateCount; + function get() public view returns (uint256) { return 101; } + + event Example(uint256 num); + + function func1() public payable { + stateCount++; + + for (uint256 i = 0; i < stateCount; i++) { + emit Example(1); + } + } }