mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
codec, quantumvm: a lower-case name on the wire reaches an exported Go method
quantumvm registered node's JSON-RPC codec — the one that raises the initial of the function named after the dot. That codec is now internal/codec, so the spelling quantumvm serves, quantumvm.getBlock for Service.GetBlock, is decided in this module. The spelling is unchanged: an initial that is already upper-case is refused, and an argument that will not read reports one message. identityvm, keyvm and bridgevm register gorilla's plain codec and keep it. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package codec speaks the JSON-RPC spelling this repo's VMs serve. A client
|
||||
// names a method as class.function with a lower-case initial, and the codec
|
||||
// hands the server the exported Go method that answers it, so
|
||||
// quantumvm.getBlock reaches Service.GetBlock.
|
||||
//
|
||||
// A function whose initial is already upper-case is refused, so exactly one
|
||||
// spelling reaches a service.
|
||||
package codec
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gorilla/rpc/v2"
|
||||
"github.com/gorilla/rpc/v2/json2"
|
||||
)
|
||||
|
||||
var (
|
||||
errUppercaseMethod = errors.New("method must start with a non-uppercase letter")
|
||||
errInvalidArg = errors.New("couldn't unmarshal an argument. Ensure arguments are valid and properly formatted. See documentation for example calls")
|
||||
)
|
||||
|
||||
// New returns a JSON-RPC codec that raises the initial of the function named
|
||||
// after the dot.
|
||||
func New() rpc.Codec {
|
||||
return codec{json2.NewCodec()}
|
||||
}
|
||||
|
||||
type codec struct{ *json2.Codec }
|
||||
|
||||
func (c codec) NewRequest(r *http.Request) rpc.CodecRequest {
|
||||
return &request{c.Codec.NewRequest(r).(*json2.CodecRequest)}
|
||||
}
|
||||
|
||||
type request struct{ *json2.CodecRequest }
|
||||
|
||||
// Method names the Go method the request asks for: the class unchanged, and
|
||||
// the function with its initial raised. A name carrying no dot, or one whose
|
||||
// initial is not a rune, is passed through as it arrived.
|
||||
func (r *request) Method() (string, error) {
|
||||
method, err := r.CodecRequest.Method()
|
||||
parts := strings.SplitN(method, ".", 2)
|
||||
if len(parts) != 2 || err != nil {
|
||||
return method, err
|
||||
}
|
||||
class, function := parts[0], parts[1]
|
||||
initial, size := utf8.DecodeRuneInString(function)
|
||||
if initial == utf8.RuneError {
|
||||
return method, nil
|
||||
}
|
||||
if unicode.IsUpper(initial) {
|
||||
return method, errUppercaseMethod
|
||||
}
|
||||
raised := string(unicode.ToUpper(initial))
|
||||
return fmt.Sprintf("%s.%s%s", class, raised, function[size:]), nil
|
||||
}
|
||||
|
||||
// ReadRequest reports one message for arguments it cannot read, so a caller
|
||||
// learns its arguments are wrong and nothing further.
|
||||
func (r *request) ReadRequest(args any) error {
|
||||
if err := r.CodecRequest.ReadRequest(args); err != nil {
|
||||
return errInvalidArg
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package codec
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/rpc/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// call builds the JSON-RPC request a client would send for the named method.
|
||||
func call(t *testing.T, method, params string) *http.Request {
|
||||
t.Helper()
|
||||
body := `{"jsonrpc":"2.0","id":1,"method":"` + method + `","params":` + params + `}`
|
||||
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
return r
|
||||
}
|
||||
|
||||
// TestMethod pins the spelling a client may use. Every name here is one
|
||||
// quantumvm serves; a change to this table is a change to the wire.
|
||||
func TestMethod(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
wire, method string
|
||||
}{
|
||||
{"quantumvm.getBlock", "quantumvm.GetBlock"},
|
||||
{"quantumvm.generateCoronaKey", "quantumvm.GenerateCoronaKey"},
|
||||
{"quantumvm.verifyQuantumSignature", "quantumvm.VerifyQuantumSignature"},
|
||||
{"quantumvm.getPendingTransactions", "quantumvm.GetPendingTransactions"},
|
||||
{"quantumvm.getHealth", "quantumvm.GetHealth"},
|
||||
{"quantumvm.getConfig", "quantumvm.GetConfig"},
|
||||
} {
|
||||
t.Run(test.wire, func(t *testing.T) {
|
||||
method, err := New().NewRequest(call(t, test.wire, "[{}]")).Method()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.method, method)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMethodRaised refuses the spelling of the Go method itself, so a service
|
||||
// answers to one name and not two.
|
||||
func TestMethodRaised(t *testing.T) {
|
||||
method, err := New().NewRequest(call(t, "quantumvm.GetBlock", "[{}]")).Method()
|
||||
require.ErrorIs(t, err, errUppercaseMethod)
|
||||
require.EqualError(t, err, "method must start with a non-uppercase letter")
|
||||
require.Equal(t, "quantumvm.GetBlock", method)
|
||||
}
|
||||
|
||||
// TestMethodWhole passes a name carrying no dot through untouched — the server
|
||||
// rejects it, and the codec does not invent a class for it.
|
||||
func TestMethodWhole(t *testing.T) {
|
||||
method, err := New().NewRequest(call(t, "getBlock", "[{}]")).Method()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "getBlock", method)
|
||||
}
|
||||
|
||||
// TestReadRequest reports one message for arguments of the wrong shape.
|
||||
func TestReadRequest(t *testing.T) {
|
||||
request := New().NewRequest(call(t, "quantumvm.getBlock", `["not-an-object"]`))
|
||||
_, err := request.Method()
|
||||
require.NoError(t, err)
|
||||
require.ErrorIs(t, request.ReadRequest(&Args{}), errInvalidArg)
|
||||
}
|
||||
|
||||
type Args struct {
|
||||
Height uint64 `json:"height"`
|
||||
}
|
||||
|
||||
type Reply struct {
|
||||
Height uint64 `json:"height"`
|
||||
}
|
||||
|
||||
// service stands in for quantumvm's: an exported Go method, recording that it
|
||||
// ran.
|
||||
type service struct{ ran string }
|
||||
|
||||
func (s *service) GetBlock(_ *http.Request, a *Args, r *Reply) error {
|
||||
s.ran = "GetBlock"
|
||||
r.Height = a.Height
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestServe drives the whole path a client takes: a lower-case name on the
|
||||
// wire reaches the exported Go method and its answer comes back.
|
||||
func TestServe(t *testing.T) {
|
||||
server := rpc.NewServer()
|
||||
server.RegisterCodec(New(), "application/json")
|
||||
served := &service{}
|
||||
require.NoError(t, server.RegisterService(served, "quantumvm"))
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
server.ServeHTTP(recorder, call(t, "quantumvm.getBlock", `[{"height":7}]`))
|
||||
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
require.Equal(t, "GetBlock", served.ran)
|
||||
|
||||
var answer struct {
|
||||
Result Reply `json:"result"`
|
||||
Error *json.RawMessage `json:"error"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &answer))
|
||||
require.Nil(t, answer.Error)
|
||||
require.Equal(t, uint64(7), answer.Result.Height)
|
||||
}
|
||||
+3
-3
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/rpc/v2"
|
||||
"github.com/luxfi/chains/internal/codec"
|
||||
"github.com/luxfi/chains/quantumvm/config"
|
||||
"github.com/luxfi/chains/quantumvm/quantum"
|
||||
consensuschain "github.com/luxfi/consensus/engine/chain"
|
||||
@@ -21,7 +22,6 @@ import (
|
||||
"github.com/luxfi/database/versiondb"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/node/utils/json"
|
||||
"github.com/luxfi/node/vms/types/fee"
|
||||
"github.com/luxfi/timer/mockable"
|
||||
"github.com/luxfi/version"
|
||||
@@ -556,8 +556,8 @@ const rpcPath = "/rpc"
|
||||
func (vm *VM) initializeHTTPHandlers() error {
|
||||
vm.rpcServer = rpc.NewServer()
|
||||
|
||||
vm.rpcServer.RegisterCodec(json.NewCodec(), "application/json")
|
||||
vm.rpcServer.RegisterCodec(json.NewCodec(), "application/json;charset=UTF-8")
|
||||
vm.rpcServer.RegisterCodec(codec.New(), "application/json")
|
||||
vm.rpcServer.RegisterCodec(codec.New(), "application/json;charset=UTF-8")
|
||||
return vm.rpcServer.RegisterService(&Service{vm: vm}, "quantumvm")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user