Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lib/runtime): Implement ext_offchain_http_request_add_header_version_1 host function #1994

Merged
merged 47 commits into from
Nov 20, 2021
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
2899a29
feat: implement offchain http host functions
EclesioMeloJunior Oct 28, 2021
8f7a581
chore: decoding Result<i16, ()>
EclesioMeloJunior Oct 29, 2021
4d6167f
chore: adjust result encoding/decoding
EclesioMeloJunior Oct 31, 2021
faa5752
chore: add export comment on Get
EclesioMeloJunior Oct 31, 2021
e08300e
chore: change to map and update test wasm
EclesioMeloJunior Nov 2, 2021
70e6351
chore: use request id buffer
EclesioMeloJunior Nov 2, 2021
ad8c0bb
chore: change to NewHTTPSet
EclesioMeloJunior Nov 2, 2021
5606599
chore: add export comment
EclesioMeloJunior Nov 2, 2021
f2ca718
Merge branch 'development' into eclesio/offchain-http
EclesioMeloJunior Nov 2, 2021
81c0fb9
chore: use pkg/scale to encode Result to wasm memory
EclesioMeloJunior Nov 2, 2021
6da5ad8
Merge branch 'eclesio/offchain-http' of github.com:ChainSafe/gossamer…
EclesioMeloJunior Nov 2, 2021
fb21f11
chore: update naming and fix lint warns
EclesioMeloJunior Nov 2, 2021
881fc51
Merge branch 'development' into eclesio/offchain-http
EclesioMeloJunior Nov 2, 2021
7444590
Merge branch 'development' into eclesio/offchain-http
EclesioMeloJunior Nov 3, 2021
8896ae5
Merge branch 'development' into eclesio/offchain-http
EclesioMeloJunior Nov 3, 2021
ee372c4
chore: use buffer.put when remove http request
EclesioMeloJunior Nov 3, 2021
78bfd0b
chore: add more comments
EclesioMeloJunior Nov 3, 2021
42ff50e
chore: add unit tests
EclesioMeloJunior Nov 3, 2021
13d540a
chore: fix misspelling
EclesioMeloJunior Nov 3, 2021
60a1f11
chore: fix scale marshal to encode Result instead of Option<Result>
EclesioMeloJunior Nov 3, 2021
4cd379f
chore: ignore uneeded error
EclesioMeloJunior Nov 4, 2021
f1130ad
chore: fix unused params
EclesioMeloJunior Nov 4, 2021
5f980ab
chore: cannot remove unused params
EclesioMeloJunior Nov 4, 2021
c8913d2
chore: ignore deepsource errors
EclesioMeloJunior Nov 4, 2021
fcb5da9
chore: add parallel to wasmer tests
EclesioMeloJunior Nov 4, 2021
9d2b7c5
chore: implementing offchain http request add header
EclesioMeloJunior Nov 5, 2021
dd89472
chore: remove dereferencing
EclesioMeloJunior Nov 5, 2021
8624e09
chore: fix param compatibility
EclesioMeloJunior Nov 8, 2021
427c183
chore: embed mutex iunto httpset struct
EclesioMeloJunior Nov 8, 2021
b791f35
chore: fix request field name
EclesioMeloJunior Nov 8, 2021
750caa9
chore: update the hoost polkadot test runtime location
EclesioMeloJunior Nov 9, 2021
6cdef2b
Merge branch 'eclesio/offchain-http' into eclesio/offchain-req-add-he…
EclesioMeloJunior Nov 9, 2021
c856a87
chore: use an updated host runtime test
EclesioMeloJunior Nov 9, 2021
85c4703
chore: fix lint warns
EclesioMeloJunior Nov 9, 2021
d203860
chore: rename OffchainRequest to Request
EclesioMeloJunior Nov 9, 2021
c8463f8
Merge branch 'development' into eclesio/offchain-req-add-headers
EclesioMeloJunior Nov 9, 2021
5353927
Merge branch 'development' into eclesio/offchain-req-add-headers
EclesioMeloJunior Nov 10, 2021
281ed22
chore: update host commit hash
EclesioMeloJunior Nov 11, 2021
a7e74d3
Merge branch 'development' into eclesio/offchain-req-add-headers
EclesioMeloJunior Nov 11, 2021
18a2cc8
chore: update log
EclesioMeloJunior Nov 11, 2021
2e57583
chore: resolve conflicts
EclesioMeloJunior Nov 16, 2021
d02a434
chore: address comments
EclesioMeloJunior Nov 16, 2021
eee9136
chore: adjust the error flow
EclesioMeloJunior Nov 19, 2021
9410059
chore: fix result return
EclesioMeloJunior Nov 19, 2021
a4441a2
chore: update the host runtime link
EclesioMeloJunior Nov 19, 2021
92b9f49
chore: use request context to store bool values
EclesioMeloJunior Nov 20, 2021
75af512
chore: fix the lint issues
EclesioMeloJunior Nov 20, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions lib/runtime/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,9 @@ const (
POLKADOT_RUNTIME_URL = "https://github.com/noot/polkadot/blob/noot/v0.8.25/polkadot_runtime.wasm?raw=true"

// v0.9 test API wasm
HOST_API_TEST_RUNTIME = "hostapi_runtime"
HOST_API_TEST_RUNTIME_FP = "hostapi_runtime.compact.wasm"
// HOST_API_TEST_RUNTIME_URL = "https://github.com/ChainSafe/polkadot-spec/blob/b94d8c58ad6ea8bf827b0cae1645a999719c2bc7/test/runtimes/hostapi/hostapi_runtime.compact.wasm?raw=true"
HOST_API_TEST_RUNTIME_URL = "https://github.com/ChainSafe/polkadot-spec/blob/omar/offchain/test/runtimes/hostapi/hostapi_runtime.compact.wasm?raw=true"
HOST_API_TEST_RUNTIME = "hostapi_runtime"
HOST_API_TEST_RUNTIME_FP = "hostapi_runtime.compact.wasm"
HOST_API_TEST_RUNTIME_URL = "https://github.com/ChainSafe/polkadot-spec/blob/4d190603d21d4431888bcb1ec546c4dc03b7bf93/test/runtimes/hostapi/hostapi_runtime.compact.wasm?raw=true"

// v0.8 substrate runtime with modified name and babe C=(1, 1)
DEV_RUNTIME = "dev_runtime"
Expand Down
42 changes: 38 additions & 4 deletions lib/runtime/offchain/httpset.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ package offchain

import (
"errors"
"fmt"
"net/http"
"strings"
"sync"
)

Expand All @@ -15,6 +17,9 @@ var (
errIntBufferEmpty = errors.New("int buffer exhausted")
errIntBufferFull = errors.New("int buffer is full")
errRequestIDNotAvailable = errors.New("request id not available")
errRequestInvalid = errors.New("request is invalid")
errRequestAlreadyStarted = errors.New("request has already started")
errInvalidHeaderKey = errors.New("invalid header key")
)

// requestIDBuffer created to control the amount of available non-duplicated ids
Expand Down Expand Up @@ -48,10 +53,36 @@ func (b requestIDBuffer) put(i int16) error {
}
}

// Request holds the request object and update the invalid and waiting status whenever
// the request starts or is waiting to be read
type Request struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use http.Request's context to store those two booleans? Just to reduce the Go API. Although it might be not worth it depending how many times we check these values.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe we can get improvements storing these values on http.Request's context the reason we had these values is to avoid operations over already ongoing or finalized HTTP calls

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not for improvements except to use *http.Request directly. Most http routers store stuff in the http.Request's context, I think it would fit nicely, so we would have one less type to manage ourselves.

Example

type contextKey string

const waitingKey contextKey = "waiting"

// ...

// to set it
request := http.NewRequest(...)
ctx := context.WithValue(request.Context(), waitingKey, false)
request = request.WithContext(ctx)

// to retrieve it
waiting := request.Context().Value(ContextUserKey).(bool)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@qdm12, done!

Request *http.Request
invalid, waiting bool
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this concurrent safe?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, when are these variables set and what are they used for?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for now, there is no concurrency (will be implemented here #2020). The invalid and waiting are set to true when we're waiting for a response, then the waiting is set to false when we got a successful or failed response however invalid remains true

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see where in the code they're set (unless it wasn't in this PR?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, right now these values aren't being set because the function that does this is not implemented yet!

}

// AddHeader adds a new HTTP header into request property, only if request is valid and has not started yet
func (r *Request) AddHeader(name, value string) error {
if r.invalid {
return errRequestInvalid
}

if r.waiting {
return errRequestAlreadyStarted
}

name = strings.TrimSpace(name)
if len(name) == 0 {
return fmt.Errorf("%w: empty header key", errInvalidHeaderKey)
}

r.Request.Header.Add(name, value)
return nil
}

// HTTPSet holds a pool of concurrent http request calls
type HTTPSet struct {
*sync.Mutex
reqs map[int16]*http.Request
reqs map[int16]*Request
idBuff requestIDBuffer
}

Expand All @@ -60,7 +91,7 @@ type HTTPSet struct {
func NewHTTPSet() *HTTPSet {
return &HTTPSet{
new(sync.Mutex),
make(map[int16]*http.Request),
make(map[int16]*Request),
newIntBuffer(maxConcurrentRequests),
}
}
Expand All @@ -85,7 +116,10 @@ func (p *HTTPSet) StartRequest(method, uri string) (int16, error) {
return 0, err
}

p.reqs[id] = req
p.reqs[id] = &Request{
Request: req,
}

return id, nil
}

Expand All @@ -100,7 +134,7 @@ func (p *HTTPSet) Remove(id int16) error {
}

// Get returns a request or nil if request not found
func (p *HTTPSet) Get(id int16) *http.Request {
func (p *HTTPSet) Get(id int16) *Request {
p.Lock()
defer p.Unlock()

Expand Down
58 changes: 55 additions & 3 deletions lib/runtime/offchain/httpset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package offchain

import (
"fmt"
"net/http"
"testing"

Expand All @@ -28,7 +29,7 @@ func TestHTTPSet_StartRequest_NotAvailableID(t *testing.T) {
t.Parallel()

set := NewHTTPSet()
set.reqs[1] = &http.Request{}
set.reqs[1] = &Request{}

_, err := set.StartRequest(http.MethodGet, defaultTestURI)
require.ErrorIs(t, errRequestIDNotAvailable, err)
Expand All @@ -45,6 +46,57 @@ func TestHTTPSetGet(t *testing.T) {
req := set.Get(id)
require.NotNil(t, req)

require.Equal(t, http.MethodGet, req.Method)
require.Equal(t, defaultTestURI, req.URL.String())
require.Equal(t, http.MethodGet, req.Request.Method)
require.Equal(t, defaultTestURI, req.Request.URL.String())
}

func TestOffchainRequest_AddHeader(t *testing.T) {
t.Parallel()

cases := map[string]struct {
offReq Request
err error
headerK, headerV string
}{
"should return invalid request": {
offReq: Request{invalid: true},
err: errRequestInvalid,
},
"should return request already started": {
offReq: Request{waiting: true},
err: errRequestAlreadyStarted,
},
"should add header": {
offReq: Request{Request: &http.Request{Header: make(http.Header)}},
headerK: "key",
headerV: "value",
},
"should return invalid empty header": {
offReq: Request{Request: &http.Request{Header: make(http.Header)}},
headerK: "",
headerV: "value",
err: fmt.Errorf("%w: %s", errInvalidHeaderKey, "empty header key"),
},
}

for name, tc := range cases {
qdm12 marked this conversation as resolved.
Show resolved Hide resolved
tc := tc

t.Run(name, func(t *testing.T) {
t.Parallel()

err := tc.offReq.AddHeader(tc.headerK, tc.headerV)

if tc.err != nil {
require.Error(t, err)
require.Equal(t, tc.err.Error(), err.Error())
return
}

qdm12 marked this conversation as resolved.
Show resolved Hide resolved
require.NoError(t, err)

got := tc.offReq.Request.Header.Get(tc.headerK)
require.Equal(t, tc.headerV, got)
})
}
}
73 changes: 67 additions & 6 deletions lib/runtime/wasmer/imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ package wasmer
// extern int64_t ext_offchain_timestamp_version_1(void *context);
// extern void ext_offchain_sleep_until_version_1(void *context, int64_t a);
// extern int64_t ext_offchain_http_request_start_version_1(void *context, int64_t a, int64_t b, int64_t c);
// extern int64_t ext_offchain_http_request_add_header_version_1(void *context, int32_t a, int64_t k, int64_t v);
//
// extern void ext_storage_append_version_1(void *context, int64_t a, int64_t b);
// extern int64_t ext_storage_changes_root_version_1(void *context, int64_t a);
Expand Down Expand Up @@ -1722,24 +1723,80 @@ func ext_offchain_http_request_start_version_1(context unsafe.Pointer, methodSpa
logger.Debug("executing...")

instanceContext := wasm.IntoInstanceContext(context)
runtimeCtx := instanceContext.Data().(*runtime.Context)

httpMethod := asMemorySlice(instanceContext, methodSpan)
uri := asMemorySlice(instanceContext, uriSpan)

result := scale.NewResult(int16(0), nil)

runtimeCtx := instanceContext.Data().(*runtime.Context)
reqID, err := runtimeCtx.OffchainHTTPSet.StartRequest(string(httpMethod), string(uri))

if err != nil {
// StartRequest error already was logged
logger.Errorf("failed to start request: %s", err)
_ = result.Set(scale.Err, nil)
err = result.Set(scale.Err, nil)
} else {
_ = result.Set(scale.OK, reqID)
err = result.Set(scale.OK, reqID)
}

// note: just check if an error occurs while setting the result data
if err != nil {
logger.Errorf("failed to set the result data: %s", err)
return C.int64_t(0)
}

enc, err := scale.Marshal(result)
if err != nil {
logger.Errorf("failed to scale marshal the result: %s", err)
return C.int64_t(0)
}

enc, _ := scale.Marshal(result)
ptr, _ := toWasmMemory(instanceContext, enc)
ptr, err := toWasmMemory(instanceContext, enc)
if err != nil {
logger.Errorf("failed to allocate result on memory: %s", err)
return C.int64_t(0)
}

return C.int64_t(ptr)
}

//export ext_offchain_http_request_add_header_version_1
func ext_offchain_http_request_add_header_version_1(context unsafe.Pointer, reqID C.int32_t, nameSpan, valueSpan C.int64_t) C.int64_t {
logger.Debug("executing...")
kishansagathiya marked this conversation as resolved.
Show resolved Hide resolved
instanceContext := wasm.IntoInstanceContext(context)

name := asMemorySlice(instanceContext, nameSpan)
value := asMemorySlice(instanceContext, valueSpan)

runtimeCtx := instanceContext.Data().(*runtime.Context)
offchainReq := runtimeCtx.OffchainHTTPSet.Get(int16(reqID))

result := scale.NewResult(nil, nil)
resultMode := scale.OK

err := offchainReq.AddHeader(string(name), string(value))
if err != nil {
logger.Errorf("failed to add request header: %s", err)
resultMode = scale.Err
}

err = result.Set(resultMode, nil)
if err != nil {
logger.Errorf("failed to set the result data: %s", err)
return C.int64_t(0)
}

enc, err := scale.Marshal(result)
if err != nil {
logger.Errorf("failed to scale marshal the result: %s", err)
return C.int64_t(0)
}

ptr, err := toWasmMemory(instanceContext, enc)
if err != nil {
logger.Errorf("failed to allocate result on memory: %s", err)
return C.int64_t(0)
}

return C.int64_t(ptr)
}
Expand Down Expand Up @@ -2416,6 +2473,10 @@ func ImportsNodeRuntime() (*wasm.Imports, error) { //nolint
if err != nil {
return nil, err
}
_, err = imports.Append("ext_offchain_http_request_add_header_version_1", ext_offchain_http_request_add_header_version_1, C.ext_offchain_http_request_add_header_version_1)
if err != nil {
return nil, err
}
_, err = imports.Append("ext_sandbox_instance_teardown_version_1", ext_sandbox_instance_teardown_version_1, C.ext_sandbox_instance_teardown_version_1)
if err != nil {
return nil, err
Expand Down
66 changes: 66 additions & 0 deletions lib/runtime/wasmer/imports_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package wasmer
import (
"bytes"
"encoding/binary"
"net/http"
"os"
"sort"
"testing"
Expand Down Expand Up @@ -312,6 +313,71 @@ func Test_ext_offchain_http_request_start_version_1(t *testing.T) {
require.Equal(t, int16(3), requestNumber)
}

func Test_ext_offchain_http_request_add_header(t *testing.T) {
t.Parallel()

inst := NewTestInstance(t, runtime.HOST_API_TEST_RUNTIME)

cases := map[string]struct {
key, value string
expectedErr bool
}{
"should add headers without problems": {
key: "SOME_HEADER_KEY",
value: "SOME_HEADER_VALUE",
expectedErr: false,
},

"should return a result error": {
key: "",
value: "",
expectedErr: true,
},
}

for tname, tcase := range cases {
t.Run(tname, func(t *testing.T) {
t.Parallel()

reqID, err := inst.ctx.OffchainHTTPSet.StartRequest(http.MethodGet, "http://uri.example")
require.NoError(t, err)

encID, err := scale.Marshal(uint32(reqID))
require.NoError(t, err)

encHeaderKey, err := scale.Marshal(tcase.key)
require.NoError(t, err)

encHeaderValue, err := scale.Marshal(tcase.value)
require.NoError(t, err)

params := append([]byte{}, encID...)
params = append(params, encHeaderKey...)
params = append(params, encHeaderValue...)

ret, err := inst.Exec("rtm_ext_offchain_http_request_add_header_version_1", params)
require.NoError(t, err)

gotResult := scale.NewResult(nil, nil)
err = scale.Unmarshal(ret, &gotResult)
require.NoError(t, err)

ok, err := gotResult.Unwrap()
if tcase.expectedErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}

offchainReq := inst.ctx.OffchainHTTPSet.Get(reqID)
gotValue := offchainReq.Request.Header.Get(tcase.key)
require.Equal(t, tcase.value, gotValue)

require.Nil(t, ok)
kishansagathiya marked this conversation as resolved.
Show resolved Hide resolved
})
}
}

func Test_ext_storage_clear_prefix_version_1_hostAPI(t *testing.T) {
t.Parallel()
inst := NewTestInstance(t, runtime.HOST_API_TEST_RUNTIME)
Expand Down