diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index e181856538..51eaa337a9 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -17,7 +17,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: ^1.20 + go-version: 1.21.1 id: go - name: Check out code into the Go module directory diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 1503c0581b..e71a162df4 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -195,6 +195,7 @@ var ( utils.SuaveConfidentialStorePebbleDbPathFlag, utils.SuaveEthBundleSigningKeyFlag, utils.SuaveEthBlockSigningKeyFlag, + utils.SuaveExternalWhitelistFlag, utils.SuaveDevModeFlag, } ) @@ -271,6 +272,16 @@ func main() { // prepare manipulates memory cache allowance and setups metric system. // This function should be called before launching devp2p stack. func prepare(ctx *cli.Context) { + if err := prepareSuaveDev(ctx); err != nil { + log.Error("failed to setup suave dev mode", "err", err) + os.Exit(1) + } + + if err := prepareSuaveNetworksRemapping(ctx); err != nil { + log.Error("failed to setup suave networks remapping", "err", err) + os.Exit(1) + } + // If we're running a known preset, log it for convenience. switch { case ctx.IsSet(utils.RinkebyFlag.Name): @@ -340,14 +351,6 @@ func geth(ctx *cli.Context) error { return fmt.Errorf("invalid command: %q", args[0]) } - if err := prepareSuaveDev(ctx); err != nil { - return fmt.Errorf("failed to setup suave development mode: %v", err) - } - - if err := prepareSuaveNetworksRemapping(ctx); err != nil { - return err - } - prepare(ctx) stack, backend := makeFullNode(ctx) defer stack.Close() @@ -531,7 +534,6 @@ func prepareSuaveDev(ctx *cli.Context) error { utils.DeveloperFlag.Name: "true", utils.DeveloperGasLimitFlag.Name: "30000000", utils.HTTPEnabledFlag.Name: "true", - utils.HTTPPortFlag.Name: "8545", utils.HTTPVirtualHostsFlag.Name: "*", utils.HTTPCORSDomainFlag.Name: "*", utils.WSEnabledFlag.Name: "true", diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 07bac21398..de6813e5ff 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -562,6 +562,13 @@ var ( Category: flags.SuaveCategory, } + SuaveExternalWhitelistFlag = &cli.StringSliceFlag{ + Name: "suave.eth.external-whitelist", + EnvVars: []string{"SUAVE_EXTERNAL_WHITELIST"}, + Usage: "List of external whitelisted addresses", + Category: flags.SuaveCategory, + } + SuaveDevModeFlag = &cli.BoolFlag{ Name: "suave.dev", Usage: "Dev mode for suave", @@ -1736,6 +1743,18 @@ func SetSuaveConfig(ctx *cli.Context, stack *node.Node, cfg *suave.Config) { if ctx.IsSet(SuaveEthBlockSigningKeyFlag.Name) { cfg.EthBlockSigningKeyHex = ctx.String(SuaveEthBlockSigningKeyFlag.Name) } + + if ctx.IsSet(SuaveEthBundleSigningKeyFlag.Name) { + cfg.EthBundleSigningKeyHex = ctx.String(SuaveEthBundleSigningKeyFlag.Name) + } + + if ctx.IsSet(SuaveExternalWhitelistFlag.Name) { + cfg.ExternalWhitelist = ctx.StringSlice(SuaveEthBundleSigningKeyFlag.Name) + if len(cfg.ExternalWhitelist) == 0 { + // As of now, default to wildcard + cfg.ExternalWhitelist = []string{"*"} + } + } } // SetEthConfig applies eth-related command line flags to the config. diff --git a/core/types/suave_structs.go b/core/types/suave_structs.go index 21349dc49f..718de03177 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: a43be97cea6fb21a9f5873c0e8e1367541d7fee460f12b6f105776d0c767e8a4 +// Hash: 80e00d3e46ad61ee6925143b317f91c0ad551d66908d0ab20e244db63b40ff40 package types import "github.com/ethereum/go-ethereum/common" @@ -29,6 +29,13 @@ type DataRecord struct { Version string } +type HttpRequest struct { + Url string + Method string + Headers []string + Body []byte +} + type Withdrawal struct { Index uint64 Validator uint64 diff --git a/core/vm/contracts_suave.go b/core/vm/contracts_suave.go index 94f5c1925e..0fb504a1b4 100644 --- a/core/vm/contracts_suave.go +++ b/core/vm/contracts_suave.go @@ -1,8 +1,13 @@ package vm import ( + "bytes" "fmt" + "io" + "net/http" + "net/url" "strings" + "time" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -123,13 +128,71 @@ func mustParseMethodAbi(data string, method string) abi.Method { return inoutAbi.Methods[method] } -func formatPeekerError(format string, args ...any) ([]byte, error) { - err := fmt.Errorf(format, args...) - return []byte(err.Error()), err -} - type suaveRuntime struct { suaveContext *SuaveContext } var _ SuaveRuntime = &suaveRuntime{} + +func (s *suaveRuntime) doHTTPRequest(request types.HttpRequest) ([]byte, error) { + if request.Method != "GET" && request.Method != "POST" { + return nil, fmt.Errorf("only GET and POST methods are supported") + } + if request.Url == "" { + return nil, fmt.Errorf("url is empty") + } + + var body io.Reader + if request.Body != nil { + body = bytes.NewReader(request.Body) + } + + // decode the url and check if the domain is allowed + parsedURL, err := url.Parse(request.Url) + if err != nil { + panic(err) + } + + var allowed bool + for _, allowedDomain := range s.suaveContext.Backend.ExternalWhitelist { + if allowedDomain == "*" || allowedDomain == parsedURL.Hostname() { + allowed = true + break + } + } + if !allowed { + return nil, fmt.Errorf("domain %s is not allowed", parsedURL.Hostname()) + } + + req, err := http.NewRequest(request.Method, request.Url, body) + if err != nil { + return nil, err + } + + for _, header := range request.Headers { + indx := strings.Index(header, ":") + if indx == -1 { + return nil, fmt.Errorf("incorrect header format '%s', no ':' present", header) + } + req.Header.Add(header[:indx], header[indx+1:]) + } + + client := &http.Client{ + Timeout: 5 * time.Second, // TODO: test + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode > 299 { + return nil, fmt.Errorf("http error: %s: %v", resp.Status, data) + } + return data, nil +} diff --git a/core/vm/contracts_suave_eth.go b/core/vm/contracts_suave_eth.go index 2641223920..bfca64caf7 100644 --- a/core/vm/contracts_suave_eth.go +++ b/core/vm/contracts_suave_eth.go @@ -1,11 +1,9 @@ package vm import ( - "bytes" "context" "encoding/json" "fmt" - "io" "math/big" "net/http" "time" @@ -280,35 +278,19 @@ func (b *suaveRuntime) buildEthBlock(blockArgs types.BuildBlockArgs, dataID type } func (b *suaveRuntime) submitEthBlockBidToRelay(relayUrl string, builderBidJson []byte) ([]byte, error) { - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(3*time.Second)) - defer cancel() - endpoint := relayUrl + "/relay/v1/builder/blocks" - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(builderBidJson)) - if err != nil { - return formatPeekerError("could not prepare request to relay: %w", err) - } - req.Header.Add("Content-Type", "application/json") - - // Execute request - resp, err := http.DefaultClient.Do(req) - if err != nil { - return formatPeekerError("could not send request to relay: %w", err) + httpReq := types.HttpRequest{ + Method: http.MethodPost, + Url: endpoint, + Body: builderBidJson, + Headers: []string{ + "Content-Type:application/json", + "Accept:application/json", + }, } - defer resp.Body.Close() - - if resp.StatusCode == http.StatusNoContent { - return nil, nil - } - - if resp.StatusCode > 299 { - bodyBytes, err := io.ReadAll(resp.Body) - if err != nil { - return formatPeekerError("could not read error response body for status code %d: %w", resp.StatusCode, err) - } - - return formatPeekerError("relay request failed with code %d: %s", resp.StatusCode, string(bodyBytes)) + if _, err := b.doHTTPRequest(httpReq); err != nil { + return nil, err } return nil, nil @@ -356,9 +338,6 @@ func executableDataToCapellaExecutionPayload(data *engine.ExecutableData) (*spec } func (c *suaveRuntime) submitBundleJsonRPC(url string, method string, params []byte) ([]byte, error) { - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(3*time.Second)) - defer cancel() - request := map[string]interface{}{ "id": json.RawMessage([]byte("1")), "jsonrpc": "2.0", @@ -379,29 +358,18 @@ func (c *suaveRuntime) submitBundleJsonRPC(url string, method string, params []b signature := crypto.PubkeyToAddress(c.suaveContext.Backend.EthBundleSigningKey.PublicKey).Hex() + ":" + hexutil.Encode(sig) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body)) - if err != nil { - return formatPeekerError("could not prepare request to relay: %w", err) - } - - req.Header.Add("Content-Type", "application/json") - req.Header.Add("Accept", "application/json") - req.Header.Add("X-Flashbots-Signature", signature) - - // Execute request - resp, err := http.DefaultClient.Do(req) - if err != nil { - return formatPeekerError("could not send request to relay: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode > 299 { - bodyBytes, err := io.ReadAll(resp.Body) - if err != nil { - return formatPeekerError("request failed with code %d", resp.StatusCode) - } - - return formatPeekerError("request failed with code %d: %s", resp.StatusCode, string(bodyBytes)) + httpReq := types.HttpRequest{ + Method: http.MethodPost, + Url: url, + Body: body, + Headers: []string{ + "Content-Type:application/json", + "Accept:application/json", + "X-Flashbots-Signature:" + signature, + }, + } + if _, err := c.doHTTPRequest(httpReq); err != nil { + return nil, err } return nil, nil diff --git a/core/vm/contracts_suave_runtime_adapter.go b/core/vm/contracts_suave_runtime_adapter.go index 9c2cf752f2..d42579bc7d 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: a43be97cea6fb21a9f5873c0e8e1367541d7fee460f12b6f105776d0c767e8a4 +// Hash: 80e00d3e46ad61ee6925143b317f91c0ad551d66908d0ab20e244db63b40ff40 package vm import ( @@ -21,6 +21,7 @@ type SuaveRuntime interface { confidentialInputs() ([]byte, error) confidentialRetrieve(dataId types.DataId, key string) ([]byte, error) confidentialStore(dataId types.DataId, key string, data1 []byte) error + doHTTPRequest(request types.HttpRequest) ([]byte, error) ethcall(contractAddr common.Address, input1 []byte) ([]byte, error) extractHint(bundleData []byte) ([]byte, error) fetchDataRecords(cond uint64, namespace string) ([]types.DataRecord, error) @@ -37,6 +38,7 @@ var ( confidentialInputsAddr = common.HexToAddress("0x0000000000000000000000000000000042010001") confidentialRetrieveAddr = common.HexToAddress("0x0000000000000000000000000000000042020001") confidentialStoreAddr = common.HexToAddress("0x0000000000000000000000000000000042020000") + doHTTPRequestAddr = common.HexToAddress("0x0000000000000000000000000000000043200002") ethcallAddr = common.HexToAddress("0x0000000000000000000000000000000042100003") extractHintAddr = common.HexToAddress("0x0000000000000000000000000000000042100037") fetchDataRecordsAddr = common.HexToAddress("0x0000000000000000000000000000000042030001") @@ -49,7 +51,7 @@ var ( ) var addrList = []common.Address{ - buildEthBlockAddr, confidentialInputsAddr, confidentialRetrieveAddr, confidentialStoreAddr, ethcallAddr, extractHintAddr, fetchDataRecordsAddr, fillMevShareBundleAddr, newDataRecordAddr, signEthTransactionAddr, simulateBundleAddr, submitBundleJsonRPCAddr, submitEthBlockBidToRelayAddr, + buildEthBlockAddr, confidentialInputsAddr, confidentialRetrieveAddr, confidentialStoreAddr, doHTTPRequestAddr, ethcallAddr, extractHintAddr, fetchDataRecordsAddr, fillMevShareBundleAddr, newDataRecordAddr, signEthTransactionAddr, simulateBundleAddr, submitBundleJsonRPCAddr, submitEthBlockBidToRelayAddr, } type SuaveRuntimeAdapter struct { @@ -70,6 +72,9 @@ func (b *SuaveRuntimeAdapter) run(addr common.Address, input []byte) ([]byte, er case confidentialStoreAddr: return b.confidentialStore(input) + case doHTTPRequestAddr: + return b.doHTTPRequest(input) + case ethcallAddr: return b.ethcall(input) @@ -262,6 +267,47 @@ func (b *SuaveRuntimeAdapter) confidentialStore(input []byte) (res []byte, err e } +func (b *SuaveRuntimeAdapter) doHTTPRequest(input []byte) (res []byte, err error) { + var ( + unpacked []interface{} + result []byte + ) + + _ = unpacked + _ = result + + unpacked, err = artifacts.SuaveAbi.Methods["doHTTPRequest"].Inputs.Unpack(input) + if err != nil { + err = errFailedToUnpackInput + return + } + + var ( + request types.HttpRequest + ) + + if err = mapstructure.Decode(unpacked[0], &request); err != nil { + err = errFailedToDecodeField + return + } + + var ( + response []byte + ) + + if response, err = b.impl.doHTTPRequest(request); err != nil { + return + } + + result, err = artifacts.SuaveAbi.Methods["doHTTPRequest"].Outputs.Pack(response) + if err != nil { + err = errFailedToPackOutput + return + } + return result, nil + +} + func (b *SuaveRuntimeAdapter) ethcall(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 010077f119..cc131f403e 100644 --- a/core/vm/contracts_suave_runtime_adapter_test.go +++ b/core/vm/contracts_suave_runtime_adapter_test.go @@ -67,6 +67,10 @@ func (m *mockRuntime) submitEthBlockBidToRelay(relayUrl string, builderBid []byt return []byte{0x1}, nil } +func (m *mockRuntime) doHTTPRequest(request types.HttpRequest) ([]byte, error) { + return []byte{0x1}, 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 ff69182a02..6bc50ca33e 100644 --- a/core/vm/contracts_suave_test.go +++ b/core/vm/contracts_suave_test.go @@ -2,6 +2,8 @@ package vm import ( "context" + "net/http" + "net/http/httptest" "testing" "github.com/ethereum/go-ethereum/beacon/engine" @@ -151,3 +153,107 @@ func TestSuave_ConfStoreWorkflow(t *testing.T) { _, err = b.confidentialRetrieve(dataRecord.Id, "key") require.Error(t, err) } + +type httpTestHandler struct{} + +func (h *httpTestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" && r.Method != "POST" { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + if val := r.Header.Get("a"); val != "" { + w.Write([]byte(val)) + return + } + if val := r.Header.Get("fail"); val != "" { + w.WriteHeader(http.StatusInternalServerError) + return + } + if r.Method == "POST" { + w.Write([]byte("ok")) + return + } + w.Write([]byte("ok1")) +} + +func TestSuave_HttpRequest_Basic(t *testing.T) { + s := &suaveRuntime{ + suaveContext: &SuaveContext{ + Backend: &SuaveExecutionBackend{ + ExternalWhitelist: []string{"127.0.0.1"}, + }, + }, + } + + srv := httptest.NewServer(&httpTestHandler{}) + defer srv.Close() + + cases := []struct { + req types.HttpRequest + err bool + resp []byte + }{ + { + // url not set + req: types.HttpRequest{}, + err: true, + }, + { + // method not supported + req: types.HttpRequest{Url: srv.URL}, + err: true, + }, + { + // url not allowed + req: types.HttpRequest{Url: "http://example.com", Method: "GET"}, + err: true, + }, + { + // incorrect header format + req: types.HttpRequest{Url: srv.URL, Method: "GET", Headers: []string{"a"}}, + err: true, + }, + { + // POST request + req: types.HttpRequest{Url: srv.URL, Method: "POST"}, + resp: []byte("ok"), + }, + { + // GET request + req: types.HttpRequest{Url: srv.URL, Method: "GET"}, + resp: []byte("ok1"), + }, + { + // GET request with headers + req: types.HttpRequest{Url: srv.URL, Method: "GET", Headers: []string{"a:b"}}, + resp: []byte("b"), + }, + { + // POST request with headers + req: types.HttpRequest{Url: srv.URL, Method: "POST", Headers: []string{"a:c"}}, + resp: []byte("c"), + }, + { + // POST request with headers with multiple : + req: types.HttpRequest{Url: srv.URL, Method: "POST", Headers: []string{"a:c:d"}}, + resp: []byte("c:d"), + }, + { + // POST with error + req: types.HttpRequest{Url: srv.URL, Method: "POST", Headers: []string{"fail:1"}}, + err: true, + }, + } + + for _, c := range cases { + t.Run("", func(t *testing.T) { + resp, err := s.doHTTPRequest(c.req) + if c.err { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, c.resp, resp) + } + }) + } +} diff --git a/core/vm/suave.go b/core/vm/suave.go index 8d476321c9..5f8ba0a7e7 100644 --- a/core/vm/suave.go +++ b/core/vm/suave.go @@ -35,6 +35,7 @@ type SuaveContext struct { type SuaveExecutionBackend struct { EthBundleSigningKey *ecdsa.PrivateKey EthBlockSigningKey *bls.SecretKey + ExternalWhitelist []string ConfidentialStore ConfidentialStore ConfidentialEthBackend suave.ConfidentialEthBackend } @@ -92,6 +93,7 @@ func (p *SuavePrecompiledContractWrapper) Run(input []byte) ([]byte, error) { ret, err := stub.run(p.addr, input) if err != nil && ret == nil { ret = []byte(err.Error()) + err = ErrExecutionReverted } return ret, err diff --git a/eth/api_backend.go b/eth/api_backend.go index dfd64d1152..51cadd7807 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -58,6 +58,7 @@ type EthAPIBackend struct { suaveEthBlockSigningKey *bls.SecretKey suaveEngine *cstore.CStoreEngine suaveEthBackend suave.ConfidentialEthBackend + suaveExternalWhitelist []string } // For testing purposes @@ -293,6 +294,7 @@ func (b *EthAPIBackend) GetMEVM(ctx context.Context, msg *core.Message, state *s suaveCtxCopy.Backend = &vm.SuaveExecutionBackend{ EthBundleSigningKey: suaveCtx.Backend.EthBundleSigningKey, EthBlockSigningKey: suaveCtx.Backend.EthBlockSigningKey, + ExternalWhitelist: suaveCtx.Backend.ExternalWhitelist, ConfidentialStore: storeTransaction, ConfidentialEthBackend: b.suaveEthBackend, } @@ -449,6 +451,7 @@ func (b *EthAPIBackend) SuaveContext(requestTx *types.Transaction, ccr *types.Co Backend: &vm.SuaveExecutionBackend{ EthBundleSigningKey: b.suaveEthBundleSigningKey, EthBlockSigningKey: b.suaveEthBlockSigningKey, + ExternalWhitelist: b.suaveExternalWhitelist, ConfidentialStore: storeTransaction, ConfidentialEthBackend: b.suaveEthBackend, }, diff --git a/eth/backend.go b/eth/backend.go index b096cb7677..3df4ac35ff 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -287,7 +287,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { confidentialStoreEngine := cstore.NewEngine(confidentialStoreBackend, confidentialStoreTransport, suaveDaSigner, types.LatestSigner(chainConfig)) - eth.APIBackend = &EthAPIBackend{stack.Config().ExtRPCEnabled(), stack.Config().AllowUnprotectedTxs, eth, nil, suaveEthBundleSigningKey, suaveEthBlockSigningKey, confidentialStoreEngine, suaveEthBackend} + eth.APIBackend = &EthAPIBackend{stack.Config().ExtRPCEnabled(), stack.Config().AllowUnprotectedTxs, eth, nil, suaveEthBundleSigningKey, suaveEthBlockSigningKey, confidentialStoreEngine, suaveEthBackend, config.Suave.ExternalWhitelist} if eth.APIBackend.allowUnprotectedTxs { log.Info("Unprotected transactions allowed") } diff --git a/suave/artifacts/SuaveLib.json b/suave/artifacts/SuaveLib.json index 764d41d8c6..0621c8e464 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":"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":"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":"submitEthBlockBidToRelay","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":"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":"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":"submitEthBlockBidToRelay","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 9d44ba2be7..64fb675800 100644 --- a/suave/artifacts/addresses.go +++ b/suave/artifacts/addresses.go @@ -1,5 +1,5 @@ // Code generated by suave/gen. DO NOT EDIT. -// Hash: a43be97cea6fb21a9f5873c0e8e1367541d7fee460f12b6f105776d0c767e8a4 +// Hash: 80e00d3e46ad61ee6925143b317f91c0ad551d66908d0ab20e244db63b40ff40 package artifacts import ( @@ -12,6 +12,7 @@ var ( confidentialInputsAddr = common.HexToAddress("0x0000000000000000000000000000000042010001") confidentialRetrieveAddr = common.HexToAddress("0x0000000000000000000000000000000042020001") confidentialStoreAddr = common.HexToAddress("0x0000000000000000000000000000000042020000") + doHTTPRequestAddr = common.HexToAddress("0x0000000000000000000000000000000043200002") ethcallAddr = common.HexToAddress("0x0000000000000000000000000000000042100003") extractHintAddr = common.HexToAddress("0x0000000000000000000000000000000042100037") fetchDataRecordsAddr = common.HexToAddress("0x0000000000000000000000000000000042030001") @@ -28,6 +29,7 @@ var SuaveMethods = map[string]common.Address{ "confidentialInputs": confidentialInputsAddr, "confidentialRetrieve": confidentialRetrieveAddr, "confidentialStore": confidentialStoreAddr, + "doHTTPRequest": doHTTPRequestAddr, "ethcall": ethcallAddr, "extractHint": extractHintAddr, "fetchDataRecords": fetchDataRecordsAddr, @@ -49,6 +51,8 @@ func PrecompileAddressToName(addr common.Address) string { return "confidentialRetrieve" case confidentialStoreAddr: return "confidentialStore" + case doHTTPRequestAddr: + return "doHTTPRequest" case ethcallAddr: return "ethcall" case extractHintAddr: diff --git a/suave/core/config.go b/suave/core/config.go index 0e75897ac6..035d26c6af 100644 --- a/suave/core/config.go +++ b/suave/core/config.go @@ -7,6 +7,7 @@ type Config struct { PebbleDbPath string EthBundleSigningKeyHex string EthBlockSigningKeyHex string + ExternalWhitelist []string } var DefaultConfig = Config{} diff --git a/suave/e2e/workflow_test.go b/suave/e2e/workflow_test.go index 13105b23f2..e293c103fc 100644 --- a/suave/e2e/workflow_test.go +++ b/suave/e2e/workflow_test.go @@ -1106,7 +1106,6 @@ func TestE2EKettleAddressEndpoint(t *testing.T) { } func TestE2EOnChainStateTransition(t *testing.T) { - // This end-to-end tests that the callx precompile gets called from a confidential request fr := newFramework(t) defer fr.Close() @@ -1120,6 +1119,63 @@ func TestE2EOnChainStateTransition(t *testing.T) { require.Error(t, err) } +func TestE2ERemoteCalls(t *testing.T) { + fr := newFramework(t, WithWhitelist([]string{"127.0.0.1"})) + defer fr.Close() + + clt := fr.NewSDKClient() + + contractAddr := common.Address{0x3} + contract := sdk.GetContract(contractAddr, exampleCallSourceContract.Abi, clt) + + t.Run("Get", func(t *testing.T) { + srvAddr := fr.testHttpRelayer(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, r.Method, "GET") + require.Equal(t, r.Header.Get("a"), "b") + w.Write([]byte{0x1, 0x2, 0x3}) + }) + + req := &types.HttpRequest{ + Method: "GET", + Url: srvAddr, + Headers: []string{"a:b"}, + } + contract.SendTransaction("remoteCall", []interface{}{req}, nil) + }) + + t.Run("Post", func(t *testing.T) { + body := []byte{0x1, 0x2, 0x3} + + srvAddr := fr.testHttpRelayer(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, r.Method, "POST") + require.Equal(t, r.Header.Get("b"), "c") + + bodyRes, _ := io.ReadAll(r.Body) + require.Equal(t, body, bodyRes) + + w.Write([]byte{0x1, 0x2, 0x3}) + }) + + req := &types.HttpRequest{ + Method: "POST", + Url: srvAddr, + Headers: []string{"b:c"}, + Body: body, + } + contract.SendTransaction("remoteCall", []interface{}{req}, nil) + }) + + t.Run("Not whitelisted", func(t *testing.T) { + req := &types.HttpRequest{ + Method: "POST", + Url: "http://example.com", + Headers: []string{"b:c"}, + } + _, err := contract.SendTransaction("remoteCall", []interface{}{req}, nil) + require.Error(t, err) + }) +} + type clientWrapper struct { t *testing.T @@ -1175,7 +1231,9 @@ type frameworkConfig struct { var defaultFrameworkConfig = frameworkConfig{ kettleAddress: false, redisStoreBackend: false, - suaveConfig: suave.Config{}, + suaveConfig: suave.Config{ + ExternalWhitelist: []string{"*"}, + }, } type frameworkOpt func(*frameworkConfig) @@ -1215,6 +1273,12 @@ func WithBlockSigningKeyOpt(t *testing.T) (frameworkOpt, *bls.PublicKey) { }, pk } +func WithWhitelist(whitelist []string) frameworkOpt { + return func(c *frameworkConfig) { + c.suaveConfig.ExternalWhitelist = whitelist + } +} + func newFramework(t *testing.T, opts ...frameworkOpt) *framework { cfg := defaultFrameworkConfig for _, opt := range opts { @@ -1248,6 +1312,24 @@ func newFramework(t *testing.T, opts ...frameworkOpt) *framework { return f } +type handlerFunc func(http.ResponseWriter, *http.Request) + +type dummyRelayer struct { + fn handlerFunc +} + +func (d *dummyRelayer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + d.fn(w, r) +} + +func (f *framework) testHttpRelayer(handler handlerFunc) string { + httpSrv := httptest.NewServer(&dummyRelayer{fn: handler}) + f.t.Cleanup(func() { + httpSrv.Close() + }) + return httpSrv.URL +} + func (f *framework) NewSDKClient() *sdk.Client { return sdk.NewClient(f.suethSrv.RPCNode(), testKey, f.KettleAddress()) } diff --git a/suave/gen/main.go b/suave/gen/main.go index 867da436b4..a366cc9588 100644 --- a/suave/gen/main.go +++ b/suave/gen/main.go @@ -122,6 +122,22 @@ func main() { return ff.Functions[i].Name < ff.Functions[j].Name }) + funcsByName := make(map[string]struct{}) + funcsByAddr := make(map[string]struct{}) + for _, f := range ff.Functions { + // validate that there are no two functions with the same name + if _, ok := funcsByName[f.Name]; ok { + panic(fmt.Sprintf("duplicate function name: %s", f.Name)) + } + funcsByName[f.Name] = struct{}{} + + // validate that there are no two functions with the same address + if _, ok := funcsByAddr[f.Address]; ok { + panic(fmt.Sprintf("duplicate function address: %s", f.Address)) + } + funcsByAddr[f.Address] = struct{}{} + } + if err := applyTemplate(structsTemplate, ff, "./core/types/suave_structs.go"); err != nil { panic(err) } diff --git a/suave/gen/main_test.go b/suave/gen/main_test.go index fd8056dcc0..486c977180 100644 --- a/suave/gen/main_test.go +++ b/suave/gen/main_test.go @@ -1,8 +1,11 @@ package main import ( + "encoding/hex" "testing" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/suave/artifacts" "github.com/stretchr/testify/require" ) @@ -43,3 +46,18 @@ func TestEncodeTypeToGolang(t *testing.T) { require.Equal(t, c.expected, actual) } } + +func TestDecodeABI_PeekerReverted(t *testing.T) { + errMsg, err := hex.DecodeString("75fff4670000000000000000000000000000000000000000000000000000000042100000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000036261640000000000000000000000000000000000000000000000000000000000") + require.NoError(t, err) + + errorEvnt := artifacts.SuaveAbi.Errors["PeekerReverted"] + vals, err := errorEvnt.Inputs.Unpack(errMsg[4:]) + require.NoError(t, err) + + addr := vals[0].(common.Address) + reason := vals[1].([]byte) + + require.Equal(t, addr.String(), "0x0000000000000000000000000000000042100000") + require.Equal(t, string(reason), "bad") +} diff --git a/suave/gen/suave_spec.yaml b/suave/gen/suave_spec.yaml index 51ddef46a1..0e8adde316 100644 --- a/suave/gen/suave_spec.yaml +++ b/suave/gen/suave_spec.yaml @@ -46,6 +46,16 @@ structs: type: Withdrawal[] - name: extra type: bytes + - name: HttpRequest + fields: + - name: url + type: string + - name: method + type: string + - name: headers + type: string[] + - name: body + type: bytes functions: - name: confidentialInputs address: "0x0000000000000000000000000000000042010001" @@ -199,3 +209,12 @@ functions: fields: - name: encodedBundle type: bytes + - name: doHTTPRequest + address: "0x0000000000000000000000000000000043200002" + input: + - name: request + type: HttpRequest + output: + fields: + - name: response + type: bytes diff --git a/suave/sol/libraries/Suave.sol b/suave/sol/libraries/Suave.sol index d79e3e0d0a..884be1626f 100644 --- a/suave/sol/libraries/Suave.sol +++ b/suave/sol/libraries/Suave.sol @@ -27,6 +27,13 @@ library Suave { string version; } + struct HttpRequest { + string url; + string method; + string[] headers; + bytes body; + } + struct Withdrawal { uint64 index; uint64 validator; @@ -46,6 +53,8 @@ library Suave { address public constant CONFIDENTIAL_STORE = 0x0000000000000000000000000000000042020000; + address public constant DO_HTTPREQUEST = 0x0000000000000000000000000000000043200002; + address public constant ETHCALL = 0x0000000000000000000000000000000042100003; address public constant EXTRACT_HINT = 0x0000000000000000000000000000000042100037; @@ -116,6 +125,15 @@ library Suave { } } + function doHTTPRequest(HttpRequest memory request) internal view returns (bytes memory) { + (bool success, bytes memory data) = DO_HTTPREQUEST.staticcall(abi.encode(request)); + if (!success) { + revert PeekerReverted(DO_HTTPREQUEST, data); + } + + return abi.decode(data, (bytes)); + } + function ethcall(address contractAddr, bytes memory input1) internal view returns (bytes memory) { (bool success, bytes memory data) = ETHCALL.staticcall(abi.encode(contractAddr, input1)); if (!success) { diff --git a/suave/sol/libraries/SuaveForge.sol b/suave/sol/libraries/SuaveForge.sol index 2cc54685be..5e0d148512 100644 --- a/suave/sol/libraries/SuaveForge.sol +++ b/suave/sol/libraries/SuaveForge.sol @@ -63,6 +63,12 @@ library SuaveForge { bytes memory data = forgeIt("0x0000000000000000000000000000000042020000", abi.encode(dataId, key, data1)); } + function doHTTPRequest(Suave.HttpRequest memory request) internal view returns (bytes memory) { + bytes memory data = forgeIt("0x0000000000000000000000000000000043200002", abi.encode(request)); + + return abi.decode(data, (bytes)); + } + function ethcall(address contractAddr, bytes memory input1) internal view returns (bytes memory) { bytes memory data = forgeIt("0x0000000000000000000000000000000042100003", abi.encode(contractAddr, input1)); diff --git a/suave/sol/standard_peekers/example.sol b/suave/sol/standard_peekers/example.sol index 3564f17623..0e03fff711 100644 --- a/suave/sol/standard_peekers/example.sol +++ b/suave/sol/standard_peekers/example.sol @@ -14,6 +14,10 @@ contract ExampleEthCallSource { function ilegalStateTransition() public payable { state++; } + + function remoteCall(Suave.HttpRequest memory request) public { + Suave.doHTTPRequest(request); + } } contract ExampleEthCallTarget {