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:
2026-08-30 14:54:14 -07:00
co-authored by Hanzo Dev
parent 95a5c2e478
commit 0ac784c33a
3 changed files with 186 additions and 3 deletions
+72
View File
@@ -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
}
+111
View File
@@ -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
View File
@@ -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")
}