rpcchainvm: zap-native only — delete every grpc-tagged path

The VM<->Node RPC plane is ZAP. Period. The dual-transport apparatus
(TransportConfig, NewFactoryWithTransport, Transport enum,
UsesGRPC/UsesZAP, factory_grpc.go vs factory_zap.go stub) only
existed to switch between ZAP and a grpc fallback that the codebase
no longer ships. Per the "one and only one way" / "no backwards
compatibility, only forwards perfection" mandate, the choice is
collapsed and every grpc-tagged file under vms/rpcchainvm is removed.

Deletions (36 files, ~4.3K lines):
  - factory_grpc.go, factory_zap.go, transport.go (the dual-transport
    routing + Transport enum)
  - vm.go, vm_client.go, vm_server.go, block_adapter.go, *_test.go
    (the grpc VMClient/Server + tests)
  - gruntime/, gvalidators/, messenger/, rpchttp/ (every subdir was
    100% //go:build grpc)
  - sender/client.go, sender/server.go (grpc Sender wire impls;
    sender.go + zap_client.go + zap_server.go stay)
  - runtime/subprocess/runtime_grpc.go (the gRPC subprocess.Bootstrap;
    runtime_zap.go is now the only Bootstrap)
  - vms/platformvm/warp/rpcwarp/signer_test.go (orphan grpc test that
    relied on the deleted proto/pb/warp)

Edits:
  - vms/rpcchainvm/factory.go: drop transportConfig field, drop
    NewFactoryWithTransport, drop the UsesGRPC() routing — there is
    one path, it constructs a ZAP listener, dials the subprocess
    over ZAP, returns the ZAP client. No branching.
  - vms/rpcchainvm/runtime/subprocess/runtime_zap.go: drop the
    `//go:build !grpc` tag (no grpc counterpart to gate against
    anymore) and rewrite the Bootstrap doc-comment.

go mod tidy result:
  - direct dep `google.golang.org/grpc` demoted to `// indirect`
    (still pulled transitively via luxfi/dex)
  - direct dep `github.com/grpc-ecosystem/go-grpc-prometheus`
    removed entirely

Verified:
  - `go build ./...` (default, no tags) clean
  - `go test -count=1 ./vms/rpcchainvm/...` clean
  - grep -rln 'google.golang.org/grpc' --include='*.go' under node/
    returns zero (the only google.golang.org/grpc strings left are
    in go.mod/go.sum/README/RELEASES/ci.yml/proto/Dockerfile)
  - grep 'grpc.Dial|NewServer|NewClient' in rpcchainvm returns zero
This commit is contained in:
Hanzo AI
2026-05-16 17:11:31 -07:00
parent 74b3475b75
commit 7496606282
39 changed files with 10 additions and 3927 deletions
+1 -2
View File
@@ -68,7 +68,7 @@ require (
golang.org/x/tools v0.43.0
gonum.org/v1/gonum v0.17.0
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/grpc v1.80.0
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1 // indirect
)
@@ -194,7 +194,6 @@ require (
)
require (
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/luxfi/concurrent v0.0.3
github.com/luxfi/protocol v0.0.5
github.com/luxfi/upgrade v1.0.0 // indirect
@@ -1,78 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build test && grpc
package rpcwarp
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/luxfi/constants"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/crypto/bls/signer/localsigner"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/platformvm/warp"
"github.com/luxfi/node/vms/platformvm/warp/signertest"
"github.com/luxfi/vm/rpc/grpcutils"
pb "github.com/luxfi/node/proto/pb/warp"
)
type testSigner struct {
client *Client
server warp.Signer
sk bls.Signer
networkID uint32
chainID ids.ID
}
func setupSigner(t testing.TB) *testSigner {
require := require.New(t)
sk, err := localsigner.New()
require.NoError(err)
chainID := ids.GenerateTestID()
s := &testSigner{
server: warp.NewSigner(sk, constants.UnitTestID, chainID),
sk: sk,
networkID: constants.UnitTestID,
chainID: chainID,
}
listener, err := grpcutils.NewListener()
require.NoError(err)
serverCloser := grpcutils.ServerCloser{}
server := grpcutils.NewServer()
pb.RegisterSignerServer(server, NewServer(s.server))
serverCloser.Add(server)
go grpcutils.Serve(listener, server)
conn, err := grpcutils.Dial(listener.Addr().String())
require.NoError(err)
s.client = NewClient(pb.NewSignerClient(conn))
t.Cleanup(func() {
serverCloser.Stop()
_ = conn.Close()
_ = listener.Close()
})
return s
}
func TestInterface(t *testing.T) {
for name, test := range signertest.SignerTests {
t.Run(name, func(t *testing.T) {
s := setupSigner(t)
test(t, s.client, s.sk, s.networkID, s.chainID)
})
}
}
-131
View File
@@ -1,131 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpcchainvm
import (
"bytes"
"context"
"io"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/luxfi/database"
"github.com/luxfi/database/memdb"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/vm/chain/blocktest"
"github.com/luxfi/runtime"
"github.com/luxfi/vm"
"github.com/luxfi/vm/chain"
)
var (
blkBytes1 = []byte{1}
blkBytes2 = []byte{2}
blkID0 = ids.ID{0}
blkID1 = ids.ID{1}
blkID2 = ids.ID{2}
time1 = time.Unix(1, 0)
time2 = time.Unix(2, 0)
)
func batchedParseBlockCachingTestPlugin(t *testing.T, loadExpectations bool) chain.ChainVM {
// test key is "batchedParseBlockCachingTestKey"
// create mock
vmImpl := &blocktest.VM{
T: t,
}
if loadExpectations {
blk1 := blocktest.BuildChild(blocktest.Genesis)
blk2 := blocktest.BuildChild(blk1)
blk1.IDV = blkID1
blk1.ParentV = blkID0
blk1.HeightV = 1
blk1.TimestampV = time1
blk2.IDV = blkID2
blk2.ParentV = blkID1
blk2.HeightV = 2
blk2.TimestampV = time2
vmImpl.InitializeF = func(context.Context, vm.Init) error {
return nil
}
vmImpl.LastAcceptedF = func(context.Context) (ids.ID, error) {
return blocktest.GenesisID, nil
}
vmImpl.GetBlockF = func(_ context.Context, blkID ids.ID) (chain.Block, error) {
if blkID == blocktest.GenesisID {
return blocktest.Genesis, nil
}
return nil, database.ErrNotFound
}
vmImpl.ParseBlockF = func(_ context.Context, b []byte) (chain.Block, error) {
if bytes.Equal(b, blkBytes1) {
return blk1, nil
}
if bytes.Equal(b, blkBytes2) {
return blk2, nil
}
return nil, database.ErrNotFound
}
}
return vmImpl
}
func TestBatchedParseBlockCaching(t *testing.T) {
require := require.New(t)
testKey := batchedParseBlockCachingTestKey
// Create and start the plugin
vmImpl := buildClientHelper(require, testKey)
defer vmImpl.Runtime().Stop(context.Background())
chainRuntime := &runtime.Runtime{
NetworkID: 1,
ChainID: ids.ID{'C', 'C', 'h', 'a', 'i', 'n'},
NodeID: ids.GenerateTestNodeID(),
Log: log.NewWriter(io.Discard),
}
require.NoError(vmImpl.Initialize(context.Background(), vm.Init{
Runtime: chainRuntime,
DB: memdb.New(),
}))
// Call should parse the first block
blk, err := vmImpl.ParseBlock(context.Background(), blkBytes1)
require.NoError(err)
require.Equal(blkID1, blk.ID())
// Skip type assertion - ChainVM interface satisfied
// Call should cache the first block and parse the second block
blks, err := vmImpl.BatchedParseBlock(context.Background(), [][]byte{blkBytes1, blkBytes2})
require.NoError(err)
require.Len(blks, 2)
require.Equal(blkID1, blks[0].ID())
require.Equal(blkID2, blks[1].ID())
// Skip type assertions
// Call should be fully cached and not result in a grpc call
blks, err = vmImpl.BatchedParseBlock(context.Background(), [][]byte{blkBytes1, blkBytes2})
require.NoError(err)
require.Len(blks, 2)
require.Equal(blkID1, blks[0].ID())
require.Equal(blkID2, blks[1].ID())
// Skip type assertions
}
-328
View File
@@ -1,328 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpcchainvm
import (
"testing"
"time"
zapwire "github.com/luxfi/api/zap"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
vmpb "github.com/luxfi/node/proto/vm"
)
// Block sizes to test
var blockSizes = []int{
1024, // 1KB - small block
10240, // 10KB - typical block
102400, // 100KB - large block
1048576, // 1MB - very large block
}
func makeBlockBytes(size int) []byte {
b := make([]byte, size)
for i := range b {
b[i] = byte(i % 256)
}
return b
}
func makeBlockID() []byte {
id := make([]byte, 32)
for i := range id {
id[i] = byte(i)
}
return id
}
// BenchmarkGetBlockResponse_Protobuf benchmarks protobuf encoding/decoding
func BenchmarkGetBlockResponse_Protobuf(b *testing.B) {
for _, size := range blockSizes {
blockBytes := makeBlockBytes(size)
parentID := makeBlockID()
ts := timestamppb.New(time.Now())
b.Run(formatSize(size)+"/Encode", func(b *testing.B) {
b.SetBytes(int64(size))
b.ReportAllocs()
for i := 0; i < b.N; i++ {
resp := &vmpb.GetBlockResponse{
ParentId: parentID,
Bytes: blockBytes,
Height: 12345,
Timestamp: ts,
}
_, err := proto.Marshal(resp)
if err != nil {
b.Fatal(err)
}
}
})
// Pre-encode for decode benchmark
resp := &vmpb.GetBlockResponse{
ParentId: parentID,
Bytes: blockBytes,
Height: 12345,
Timestamp: ts,
}
encoded, _ := proto.Marshal(resp)
b.Run(formatSize(size)+"/Decode", func(b *testing.B) {
b.SetBytes(int64(len(encoded)))
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var decoded vmpb.GetBlockResponse
if err := proto.Unmarshal(encoded, &decoded); err != nil {
b.Fatal(err)
}
}
})
}
}
// BenchmarkGetBlockResponse_ZAP benchmarks ZAP encoding/decoding
func BenchmarkGetBlockResponse_ZAP(b *testing.B) {
for _, size := range blockSizes {
blockBytes := makeBlockBytes(size)
id := makeBlockID()
parentID := makeBlockID()
b.Run(formatSize(size)+"/Encode", func(b *testing.B) {
b.SetBytes(int64(size))
b.ReportAllocs()
for i := 0; i < b.N; i++ {
resp := &zapwire.BlockResponse{
ID: id,
ParentID: parentID,
Height: 12345,
Timestamp: 1234567890,
Bytes: blockBytes,
}
buf := zapwire.GetBuffer()
resp.Encode(buf)
_ = buf.Bytes()
zapwire.PutBuffer(buf)
}
})
// Pre-encode for decode benchmark
resp := &zapwire.BlockResponse{
ID: id,
ParentID: parentID,
Height: 12345,
Timestamp: 1234567890,
Bytes: blockBytes,
}
buf := zapwire.GetBuffer()
resp.Encode(buf)
encoded := make([]byte, len(buf.Bytes()))
copy(encoded, buf.Bytes())
zapwire.PutBuffer(buf)
b.Run(formatSize(size)+"/Decode", func(b *testing.B) {
b.SetBytes(int64(len(encoded)))
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var decoded zapwire.BlockResponse
reader := zapwire.NewReader(encoded)
if err := decoded.Decode(reader); err != nil {
b.Fatal(err)
}
}
})
}
}
// BenchmarkBatchedParseBlock_Protobuf benchmarks batched block parsing with protobuf
func BenchmarkBatchedParseBlock_Protobuf(b *testing.B) {
const batchSize = 10
const blockSize = 10240 // 10KB blocks
ts := timestamppb.New(time.Now())
blocks := make([][]byte, batchSize)
for i := range blocks {
blocks[i] = makeBlockBytes(blockSize)
}
b.Run("Encode", func(b *testing.B) {
b.SetBytes(int64(batchSize * blockSize))
b.ReportAllocs()
for i := 0; i < b.N; i++ {
resp := &vmpb.BatchedParseBlockResponse{
Response: make([]*vmpb.ParseBlockResponse, batchSize),
}
for j := 0; j < batchSize; j++ {
resp.Response[j] = &vmpb.ParseBlockResponse{
Id: makeBlockID(),
ParentId: makeBlockID(),
Height: uint64(j),
Timestamp: ts,
}
}
_, err := proto.Marshal(resp)
if err != nil {
b.Fatal(err)
}
}
})
// Pre-encode for decode benchmark
resp := &vmpb.BatchedParseBlockResponse{
Response: make([]*vmpb.ParseBlockResponse, batchSize),
}
for j := 0; j < batchSize; j++ {
resp.Response[j] = &vmpb.ParseBlockResponse{
Id: makeBlockID(),
ParentId: makeBlockID(),
Height: uint64(j),
Timestamp: ts,
}
}
encoded, _ := proto.Marshal(resp)
b.Run("Decode", func(b *testing.B) {
b.SetBytes(int64(len(encoded)))
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var decoded vmpb.BatchedParseBlockResponse
if err := proto.Unmarshal(encoded, &decoded); err != nil {
b.Fatal(err)
}
}
})
}
// BenchmarkBatchedParseBlock_ZAP benchmarks batched block parsing with ZAP
func BenchmarkBatchedParseBlock_ZAP(b *testing.B) {
const batchSize = 10
const blockSize = 10240 // 10KB blocks
blocks := make([][]byte, batchSize)
for i := range blocks {
blocks[i] = makeBlockBytes(blockSize)
}
b.Run("Encode", func(b *testing.B) {
b.SetBytes(int64(batchSize * blockSize))
b.ReportAllocs()
for i := 0; i < b.N; i++ {
resp := &zapwire.BatchedParseBlockResponse{
Responses: make([]zapwire.BlockResponse, batchSize),
}
for j := 0; j < batchSize; j++ {
resp.Responses[j] = zapwire.BlockResponse{
ID: makeBlockID(),
ParentID: makeBlockID(),
Height: uint64(j),
Timestamp: 1234567890,
Bytes: blocks[j],
}
}
buf := zapwire.GetBuffer()
resp.Encode(buf)
_ = buf.Bytes()
zapwire.PutBuffer(buf)
}
})
// Pre-encode for decode benchmark
resp := &zapwire.BatchedParseBlockResponse{
Responses: make([]zapwire.BlockResponse, batchSize),
}
for j := 0; j < batchSize; j++ {
resp.Responses[j] = zapwire.BlockResponse{
ID: makeBlockID(),
ParentID: makeBlockID(),
Height: uint64(j),
Timestamp: 1234567890,
Bytes: blocks[j],
}
}
buf := zapwire.GetBuffer()
resp.Encode(buf)
encoded := make([]byte, len(buf.Bytes()))
copy(encoded, buf.Bytes())
zapwire.PutBuffer(buf)
b.Run("Decode", func(b *testing.B) {
b.SetBytes(int64(len(encoded)))
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var decoded zapwire.BatchedParseBlockResponse
reader := zapwire.NewReader(encoded)
if err := decoded.Decode(reader); err != nil {
b.Fatal(err)
}
}
})
}
// BenchmarkInitializeRequest compares Initialize request serialization
func BenchmarkInitializeRequest_Protobuf(b *testing.B) {
genesis := makeBlockBytes(65536) // 64KB genesis
upgrade := makeBlockBytes(1024) // 1KB upgrade
config := makeBlockBytes(4096) // 4KB config
b.Run("Encode", func(b *testing.B) {
b.SetBytes(int64(len(genesis) + len(upgrade) + len(config)))
b.ReportAllocs()
for i := 0; i < b.N; i++ {
req := &vmpb.InitializeRequest{
NetworkId: 96369,
ChainId: makeBlockID(),
NodeId: makeBlockID(),
GenesisBytes: genesis,
UpgradeBytes: upgrade,
ConfigBytes: config,
}
_, err := proto.Marshal(req)
if err != nil {
b.Fatal(err)
}
}
})
}
func BenchmarkInitializeRequest_ZAP(b *testing.B) {
genesis := makeBlockBytes(65536) // 64KB genesis
upgrade := makeBlockBytes(1024) // 1KB upgrade
config := makeBlockBytes(4096) // 4KB config
b.Run("Encode", func(b *testing.B) {
b.SetBytes(int64(len(genesis) + len(upgrade) + len(config)))
b.ReportAllocs()
for i := 0; i < b.N; i++ {
req := &zapwire.InitializeRequest{
NetworkID: 96369,
ChainID: makeBlockID(),
NodeID: makeBlockID(),
GenesisBytes: genesis,
UpgradeBytes: upgrade,
ConfigBytes: config,
}
buf := zapwire.GetBuffer()
req.Encode(buf)
_ = buf.Bytes()
zapwire.PutBuffer(buf)
}
})
}
func formatSize(bytes int) string {
switch {
case bytes >= 1048576:
return "1MB"
case bytes >= 102400:
return "100KB"
case bytes >= 10240:
return "10KB"
default:
return "1KB"
}
}
-53
View File
@@ -1,53 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpcchainvm
import (
consensuschain "github.com/luxfi/vm/chain"
vmchain "github.com/luxfi/vm/rpc/chain"
)
// blockChainAdapter wraps vmchain.BlockClient to provide uint8 Status() method for consensuschain.Block
type blockChainAdapter struct {
*vmchain.BlockClient
}
// Status returns the block status as uint8
func (b *blockChainAdapter) Status() uint8 {
return uint8(b.BlockClient.Status())
}
// Ensure blockChainAdapter implements consensuschain.Block
var _ consensuschain.Block = (*blockChainAdapter)(nil)
// wrapBlockForChain converts a vmchain.BlockClient to have the correct Status() signature for consensuschain.Block
func wrapBlockForChain(bc *vmchain.BlockClient) consensuschain.Block {
if bc == nil {
return nil
}
return &blockChainAdapter{BlockClient: bc}
}
// blockConsensusAdapter wraps vmchain.BlockClient for consensus interfaces
type blockConsensusAdapter struct {
*vmchain.BlockClient
}
// Status returns the block status as uint8 for consensus
func (b *blockConsensusAdapter) Status() uint8 {
return uint8(b.BlockClient.Status())
}
// Ensure blockConsensusAdapter implements consensuschain.Block
var _ consensuschain.Block = (*blockConsensusAdapter)(nil)
// wrapBlockForConsensus converts a vmchain.BlockClient for consensus interfaces
func wrapBlockForConsensus(bc *vmchain.BlockClient) consensuschain.Block {
if bc == nil {
return nil
}
return &blockConsensusAdapter{BlockClient: bc}
}
+3 -35
View File
@@ -25,59 +25,27 @@ type factory struct {
processTracker resource.ProcessTracker
runtimeTracker runtime.Tracker
metricsGatherer metric.MultiGatherer
transportConfig TransportConfig
}
// NewFactory creates a new factory with default transport configuration (ZAP)
// NewFactory creates a new factory. VM subprocesses are spawned and
// communication uses ZAP — the one and only wire protocol for rpcchainvm.
func NewFactory(
path string,
processTracker resource.ProcessTracker,
runtimeTracker runtime.Tracker,
metricsGatherer metric.MultiGatherer,
) vms.Factory {
return NewFactoryWithTransport(
path,
processTracker,
runtimeTracker,
metricsGatherer,
DefaultTransportConfig(),
)
}
// NewFactoryWithTransport creates a new factory with specified transport configuration
func NewFactoryWithTransport(
path string,
processTracker resource.ProcessTracker,
runtimeTracker runtime.Tracker,
metricsGatherer metric.MultiGatherer,
transportConfig TransportConfig,
) vms.Factory {
return &factory{
path: path,
processTracker: processTracker,
runtimeTracker: runtimeTracker,
metricsGatherer: metricsGatherer,
transportConfig: transportConfig,
}
}
func (f *factory) New(logger log.Logger) (interface{}, error) {
logger.Info("creating VM client",
"path", f.path,
"transport", f.transportConfig.Transport,
)
logger.Info("creating VM client", "path", f.path)
// Use gRPC transport if explicitly configured (requires grpc build tag)
if f.transportConfig.Transport.UsesGRPC() {
return f.newGRPC(logger)
}
// Default to ZAP
return f.newZAP(logger)
}
// newZAP creates a VM client using ZAP transport
func (f *factory) newZAP(logger log.Logger) (interface{}, error) {
config := &subprocess.Config{
Stderr: os.Stderr,
Stdout: os.Stdout,
-52
View File
@@ -1,52 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpcchainvm
import (
"context"
"fmt"
"os"
"github.com/luxfi/log"
"github.com/luxfi/node/vms/rpcchainvm/runtime"
"github.com/luxfi/node/vms/rpcchainvm/runtime/subprocess"
"github.com/luxfi/vm/rpc/grpcutils"
)
// newGRPC creates a VM client using gRPC transport
func (f *factory) newGRPC(logger log.Logger) (interface{}, error) {
config := &subprocess.Config{
Stderr: os.Stderr,
Stdout: os.Stdout,
HandshakeTimeout: runtime.DefaultHandshakeTimeout,
Log: logger,
}
listener, err := grpcutils.NewListener()
if err != nil {
return nil, fmt.Errorf("failed to create gRPC listener: %w", err)
}
status, stopper, err := subprocess.Bootstrap(
context.TODO(),
listener,
subprocess.NewCmd(f.path),
config,
)
if err != nil {
return nil, err
}
clientConn, err := grpcutils.Dial(status.Addr)
if err != nil {
logger.Error("failed to dial VM gRPC service", "error", err)
return nil, err
}
f.processTracker.TrackProcess(status.Pid)
f.runtimeTracker.TrackRuntime(stopper)
return NewClient(clientConn, stopper, status.Pid, f.processTracker, f.metricsGatherer, logger), nil
}
-19
View File
@@ -1,19 +0,0 @@
//go:build !grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpcchainvm
import (
"errors"
"github.com/luxfi/log"
)
var errGRPCNotAvailable = errors.New("gRPC transport requires -tags=grpc build flag")
// newGRPC is not available in ZAP-only builds
func (f *factory) newGRPC(logger log.Logger) (interface{}, error) {
return nil, errGRPCNotAvailable
}
-33
View File
@@ -1,33 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package gruntime
import (
"context"
"github.com/luxfi/node/vms/rpcchainvm/runtime"
pb "github.com/luxfi/node/proto/pb/vm/runtime"
)
var _ runtime.Initializer = (*Client)(nil)
// Client is a VM runtime initializer.
type Client struct {
client pb.RuntimeClient
}
func NewClient(client pb.RuntimeClient) *Client {
return &Client{client: client}
}
func (c *Client) Initialize(ctx context.Context, protocolVersion uint, vmAddr string) error {
_, err := c.client.Initialize(ctx, &pb.InitializeRequest{
ProtocolVersion: uint32(protocolVersion),
Addr: vmAddr,
})
return err
}
-34
View File
@@ -1,34 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package gruntime
import (
"context"
"google.golang.org/protobuf/types/known/emptypb"
"github.com/luxfi/node/vms/rpcchainvm/runtime"
pb "github.com/luxfi/node/proto/pb/vm/runtime"
)
var _ pb.RuntimeServer = (*Server)(nil)
// Server is a VM runtime initializer controlled by RPC.
type Server struct {
pb.UnsafeRuntimeServer
runtime runtime.Initializer
}
func NewServer(runtime runtime.Initializer) *Server {
return &Server{
runtime: runtime,
}
}
func (s *Server) Initialize(ctx context.Context, req *pb.InitializeRequest) (*emptypb.Empty, error) {
return &emptypb.Empty{}, s.runtime.Initialize(ctx, uint(req.ProtocolVersion), req.Addr)
}
@@ -1,123 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package gvalidators
import (
"context"
"google.golang.org/protobuf/types/known/emptypb"
validators "github.com/luxfi/validators"
"github.com/luxfi/ids"
validatorstatepb "github.com/luxfi/node/proto/pb/validatorstate"
)
// NewClient creates a new validator state client
func NewClient(client validatorstatepb.ValidatorStateClient) validators.State {
return &Client{client: client}
}
// Client is a ValidatorState client
type Client struct {
client validatorstatepb.ValidatorStateClient
}
func (c *Client) GetCurrentHeight(ctx context.Context) (uint64, error) {
resp, err := c.client.GetCurrentHeight(ctx, &emptypb.Empty{})
if err != nil {
return 0, err
}
return resp.Height, nil
}
func (c *Client) GetValidatorSet(ctx context.Context, height uint64, netID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
resp, err := c.client.GetValidatorSet(ctx, &validatorstatepb.GetValidatorSetRequest{
Height: height,
ChainId: netID[:],
})
if err != nil {
return nil, err
}
validatorSet := make(map[ids.NodeID]*validators.GetValidatorOutput, len(resp.Validators))
for _, v := range resp.Validators {
nodeID, err := ids.ToNodeID(v.NodeId)
if err != nil {
return nil, err
}
validatorSet[nodeID] = &validators.GetValidatorOutput{
NodeID: nodeID,
Light: v.Weight,
Weight: v.Weight, // Both fields for compatibility
}
}
return validatorSet, nil
}
func (c *Client) GetCurrentValidators(ctx context.Context, height uint64, netID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
// Call GetValidatorSet with the same parameters
return c.GetValidatorSet(ctx, height, netID)
}
func (c *Client) GetWarpValidatorSet(ctx context.Context, height uint64, netID ids.ID) (*validators.WarpSet, error) {
// Get the validator set at the requested height
vdrSet, err := c.GetValidatorSet(ctx, height, netID)
if err != nil {
return nil, err
}
// Convert to WarpSet format (Height + Validators map)
warpValidators := make(map[ids.NodeID]*validators.WarpValidator, len(vdrSet))
for nodeID, vdr := range vdrSet {
// Only include validators with BLS public keys
if len(vdr.PublicKey) > 0 {
warpValidators[nodeID] = &validators.WarpValidator{
NodeID: nodeID,
PublicKey: vdr.PublicKey,
Weight: vdr.Weight,
}
}
}
return &validators.WarpSet{
Height: height,
Validators: warpValidators,
}, nil
}
func (c *Client) GetWarpValidatorSets(ctx context.Context, heights []uint64, netIDs []ids.ID) (map[ids.ID]map[uint64]*validators.WarpSet, error) {
result := make(map[ids.ID]map[uint64]*validators.WarpSet)
// For each netID, get validator sets for all requested heights
for _, netID := range netIDs {
heightMap := make(map[uint64]*validators.WarpSet)
for _, height := range heights {
warpSet, err := c.GetWarpValidatorSet(ctx, height, netID)
if err != nil {
return nil, err
}
heightMap[height] = warpSet
}
result[netID] = heightMap
}
return result, nil
}
func (c *Client) GetMinimumHeight(ctx context.Context) (uint64, error) {
// Return 0 as minimum height - validators are valid from genesis
return 0, nil
}
func (c *Client) GetChainID(netID ids.ID) (ids.ID, error) {
// For RPC client, the netID is the chainID
return netID, nil
}
func (c *Client) GetNetworkID(chainID ids.ID) (ids.ID, error) {
// For RPC client, the chainID is the networkID
return chainID, nil
}
@@ -1,104 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package gvalidators
import (
"context"
"google.golang.org/protobuf/types/known/emptypb"
validators "github.com/luxfi/validators"
"github.com/luxfi/ids"
pb "github.com/luxfi/node/proto/pb/validatorstate"
)
var _ pb.ValidatorStateServer = (*Server)(nil)
type Server struct {
pb.UnsafeValidatorStateServer
state validators.State
}
func NewServer(state validators.State) *Server {
return &Server{state: state}
}
func (s *Server) GetMinimumHeight(ctx context.Context, _ *emptypb.Empty) (*pb.GetMinimumHeightResponse, error) {
// validators.State doesn't have GetMinimumHeight - return 0
return &pb.GetMinimumHeightResponse{Height: 0}, nil
}
func (s *Server) GetCurrentHeight(ctx context.Context, _ *emptypb.Empty) (*pb.GetCurrentHeightResponse, error) {
height, err := s.state.GetCurrentHeight(ctx)
return &pb.GetCurrentHeightResponse{Height: height}, err
}
func (s *Server) GetChainID(ctx context.Context, req *pb.GetChainIDRequest) (*pb.GetChainIDResponse, error) {
// validators.State doesn't have GetChainID - return empty ID
return &pb.GetChainIDResponse{
ChainId: ids.Empty[:],
}, nil
}
func (s *Server) GetValidatorSet(ctx context.Context, req *pb.GetValidatorSetRequest) (*pb.GetValidatorSetResponse, error) {
netID, err := ids.ToID(req.ChainId)
if err != nil {
return nil, err
}
// GetValidatorSet returns map[ids.NodeID]*GetValidatorOutput
validators, err := s.state.GetValidatorSet(ctx, req.Height, netID)
if err != nil {
return nil, err
}
resp := &pb.GetValidatorSetResponse{
Validators: make([]*pb.Validator, 0, len(validators)),
}
for nodeID, validator := range validators {
vdrPB := &pb.Validator{
NodeId: nodeID.Bytes(),
Weight: validator.Light, // Use Light field which is the actual weight
}
resp.Validators = append(resp.Validators, vdrPB)
}
return resp, nil
}
func (s *Server) GetCurrentValidatorSet(ctx context.Context, req *pb.GetCurrentValidatorSetRequest) (*pb.GetCurrentValidatorSetResponse, error) {
netID, err := ids.ToID(req.ChainId)
if err != nil {
return nil, err
}
// validators.State doesn't have GetCurrentValidatorSet, use GetValidatorSet with height 0
currentHeight, err := s.state.GetCurrentHeight(ctx)
if err != nil {
return nil, err
}
validators, err := s.state.GetValidatorSet(ctx, currentHeight, netID)
if err != nil {
return nil, err
}
resp := &pb.GetCurrentValidatorSetResponse{
Validators: make([]*pb.Validator, 0, len(validators)),
CurrentHeight: currentHeight,
}
for nodeID, validator := range validators {
vdrPB := &pb.Validator{
NodeId: nodeID.Bytes(),
Weight: validator.Light, // Use Light field which is the actual weight
// All other fields like StartTime, IsActive, etc. are not available
}
resp.Validators = append(resp.Validators, vdrPB)
}
return resp, nil
}
@@ -1,35 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package messenger
import (
"context"
vmcore "github.com/luxfi/vm"
messengerpb "github.com/luxfi/node/proto/pb/messenger"
)
// Client is an implementation of a messenger channel that talks over RPC.
type Client struct {
client messengerpb.MessengerClient
}
// NewClient returns a client that is connected to a remote channel
func NewClient(client messengerpb.MessengerClient) *Client {
return &Client{client: client}
}
func (c *Client) Notify(msg vmcore.Message) error {
_, err := c.client.Notify(context.Background(), &messengerpb.NotifyRequest{
Message: &messengerpb.Message{
Type: messengerpb.MessageType(msg.Type),
NodeId: msg.NodeID[:],
Content: msg.Content,
},
})
return err
}
@@ -1,52 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package messenger
import (
"context"
"errors"
"github.com/luxfi/ids"
vmcore "github.com/luxfi/vm"
messengerpb "github.com/luxfi/node/proto/pb/messenger"
)
var (
errFullQueue = errors.New("full message queue")
_ messengerpb.MessengerServer = (*Server)(nil)
)
// Server is a messenger that is managed over RPC.
type Server struct {
messengerpb.UnsafeMessengerServer
messenger chan<- vmcore.Message
}
// NewServer returns a messenger connected to a remote channel
func NewServer(messenger chan<- vmcore.Message) *Server {
return &Server{messenger: messenger}
}
func (s *Server) Notify(_ context.Context, req *messengerpb.NotifyRequest) (*messengerpb.NotifyResponse, error) {
// Convert protobuf Message to vmcore.Message
var nodeID ids.NodeID
copy(nodeID[:], req.Message.NodeId)
msg := vmcore.Message{
Type: vmcore.MessageType(req.Message.Type),
NodeID: nodeID,
Content: req.Message.Content,
}
select {
case s.messenger <- msg:
return &messengerpb.NotifyResponse{}, nil
default:
return nil, errFullQueue
}
}
-235
View File
@@ -1,235 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpchttp
import (
"io"
"net/http"
"github.com/luxfi/log"
"github.com/luxfi/node/vms/rpcchainvm/rpchttp/rpcreader"
"github.com/luxfi/node/vms/rpcchainvm/rpchttp/rpcresponsewriter"
"github.com/luxfi/node/proto/pb/io/reader"
"github.com/luxfi/vm/rpc/grpcutils"
httppb "github.com/luxfi/node/proto/pb/http"
responsewriterpb "github.com/luxfi/node/proto/pb/http/responsewriter"
)
var _ http.Handler = (*Client)(nil)
// Client is an http.Handler that talks over RPC.
type Client struct {
client httppb.HTTPClient
log log.Logger
}
// NewClient returns an HTTP handler database instance connected to a remote
// HTTP handler instance
func NewClient(client httppb.HTTPClient, log log.Logger) *Client {
return &Client{
client: client,
log: log,
}
}
func (c *Client) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// rfc2616#section-14.42: The Upgrade general-header allows the client
// to specify a communication protocols it supports and would like to
// use. Upgrade (e.g. websockets) is a more expensive transaction and
// if not required use the less expensive HTTPSimple.
//
// Http/2 explicitly does not allow the use of the Upgrade header.
// (ref: https://httpwg.org/specs/rfc9113.html#informational-responses)
if !isUpgradeRequest(r) && !isHTTP2Request(r) {
c.serveHTTPSimple(w, r)
return
}
closer := grpcutils.ServerCloser{}
defer closer.GracefulStop()
// Wrap [w] with a lock to ensure that it is accessed in a thread-safe manner.
w = rpcresponsewriter.NewLockedWriter(w)
serverListener, err := grpcutils.NewListener()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
server := grpcutils.NewServer()
closer.Add(server)
responsewriterpb.RegisterWriterServer(server, rpcresponsewriter.NewServer(w))
reader.RegisterReaderServer(server, rpcreader.NewServer(r.Body))
// Start responsewriter gRPC service.
go grpcutils.Serve(serverListener, server)
req := &httppb.HTTPRequest{
ResponseWriter: &httppb.ResponseWriter{
ServerAddr: serverListener.Addr().String(),
Header: make([]*httppb.Element, 0, len(r.Header)),
},
Request: &httppb.Request{
Method: r.Method,
Proto: r.Proto,
ProtoMajor: int32(r.ProtoMajor),
ProtoMinor: int32(r.ProtoMinor),
Header: make([]*httppb.Element, 0, len(r.Header)),
ContentLength: r.ContentLength,
TransferEncoding: r.TransferEncoding,
Host: r.Host,
Form: make([]*httppb.Element, 0, len(r.Form)),
PostForm: make([]*httppb.Element, 0, len(r.PostForm)),
RemoteAddr: r.RemoteAddr,
RequestUri: r.RequestURI,
},
}
for key, values := range w.Header() {
req.ResponseWriter.Header = append(req.ResponseWriter.Header, &httppb.Element{
Key: key,
Values: values,
})
}
for key, values := range r.Header {
req.Request.Header = append(req.Request.Header, &httppb.Element{
Key: key,
Values: values,
})
}
for key, values := range r.Form {
req.Request.Form = append(req.Request.Form, &httppb.Element{
Key: key,
Values: values,
})
}
for key, values := range r.PostForm {
req.Request.PostForm = append(req.Request.PostForm, &httppb.Element{
Key: key,
Values: values,
})
}
if r.URL != nil {
req.Request.Url = &httppb.URL{
Scheme: r.URL.Scheme,
Opaque: r.URL.Opaque,
Host: r.URL.Host,
Path: r.URL.Path,
RawPath: r.URL.RawPath,
ForceQuery: r.URL.ForceQuery,
RawQuery: r.URL.RawQuery,
Fragment: r.URL.Fragment,
}
if r.URL.User != nil {
pwd, set := r.URL.User.Password()
req.Request.Url.User = &httppb.Userinfo{
Username: r.URL.User.Username(),
Password: pwd,
PasswordSet: set,
}
}
}
if r.TLS != nil {
req.Request.Tls = &httppb.ConnectionState{
Version: uint32(r.TLS.Version),
HandshakeComplete: r.TLS.HandshakeComplete,
DidResume: r.TLS.DidResume,
CipherSuite: uint32(r.TLS.CipherSuite),
NegotiatedProtocol: r.TLS.NegotiatedProtocol,
ServerName: r.TLS.ServerName,
PeerCertificates: &httppb.Certificates{
Cert: make([][]byte, len(r.TLS.PeerCertificates)),
},
VerifiedChains: make([]*httppb.Certificates, len(r.TLS.VerifiedChains)),
SignedCertificateTimestamps: r.TLS.SignedCertificateTimestamps,
OcspResponse: r.TLS.OCSPResponse,
}
for i, cert := range r.TLS.PeerCertificates {
req.Request.Tls.PeerCertificates.Cert[i] = cert.Raw
}
for i, chain := range r.TLS.VerifiedChains {
req.Request.Tls.VerifiedChains[i] = &httppb.Certificates{
Cert: make([][]byte, len(chain)),
}
for j, cert := range chain {
req.Request.Tls.VerifiedChains[i].Cert[j] = cert.Raw
}
}
}
reply, err := c.client.Handle(r.Context(), req)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
grpcutils.SetHeaders(w.Header(), reply.Header)
}
// serveHTTPSimple converts an http request to a gRPC HTTPRequest and returns the
// response to the client. Protocol upgrade requests (websockets) are not supported
// and should use ServeHTTP.
func (c *Client) serveHTTPSimple(w http.ResponseWriter, r *http.Request) {
req, err := getHTTPSimpleRequest(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
resp, err := c.client.HandleSimple(r.Context(), req)
if err != nil {
// Some errors will actually contain a valid resp, just need to unpack it
var ok bool
resp, ok = grpcutils.GetHTTPResponseFromError(err)
if !ok {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
if err := convertWriteResponse(w, resp); err != nil {
c.log.Debug("failed sending HTTP response",
log.String("error", err.Error()),
)
}
}
// getHTTPSimpleRequest takes an http request as input and returns a gRPC HandleSimpleHTTPRequest.
func getHTTPSimpleRequest(w http.ResponseWriter, r *http.Request) (*httppb.HandleSimpleHTTPRequest, error) {
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, err
}
return &httppb.HandleSimpleHTTPRequest{
Method: r.Method,
Url: r.RequestURI,
Body: body,
RequestHeaders: grpcutils.GetHTTPHeader(r.Header),
ResponseHeaders: grpcutils.GetHTTPHeader(w.Header()),
}, nil
}
// convertWriteResponse converts a gRPC HandleSimpleHTTPResponse to an HTTP response.
func convertWriteResponse(w http.ResponseWriter, resp *httppb.HandleSimpleHTTPResponse) error {
grpcutils.SetHeaders(w.Header(), resp.Headers)
w.WriteHeader(grpcutils.EnsureValidResponseCode(int(resp.Code)))
_, err := w.Write(resp.Body)
return err
}
// isUpgradeRequest returns true if the upgrade key exists in header and value is non empty.
func isUpgradeRequest(req *http.Request) bool {
return req.Header.Get("Upgrade") != ""
}
func isHTTP2Request(req *http.Request) bool {
return req.ProtoMajor == 2
}
-218
View File
@@ -1,218 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpchttp
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"net/url"
"github.com/luxfi/node/vms/rpcchainvm/rpchttp/rpcreader"
"github.com/luxfi/node/vms/rpcchainvm/rpchttp/rpcresponsewriter"
"github.com/luxfi/node/proto/pb/io/reader"
"github.com/luxfi/vm/rpc/grpcutils"
httppb "github.com/luxfi/node/proto/pb/http"
responsewriterpb "github.com/luxfi/node/proto/pb/http/responsewriter"
)
var (
_ httppb.HTTPServer = (*Server)(nil)
_ http.ResponseWriter = (*ResponseWriter)(nil)
)
// Server is an http.Handler that is managed over RPC.
type Server struct {
httppb.UnsafeHTTPServer
handler http.Handler
}
// NewServer returns an http.Handler instance managed remotely
func NewServer(handler http.Handler) *Server {
return &Server{
handler: handler,
}
}
func (s *Server) Handle(ctx context.Context, req *httppb.HTTPRequest) (*httppb.HTTPResponse, error) {
clientConn, err := grpcutils.Dial(req.ResponseWriter.ServerAddr)
if err != nil {
return nil, err
}
writerHeaders := make(http.Header)
for _, elem := range req.ResponseWriter.Header {
writerHeaders[elem.Key] = elem.Values
}
writer := rpcresponsewriter.NewClient(writerHeaders, responsewriterpb.NewWriterClient(clientConn))
body := rpcreader.NewClient(reader.NewReaderClient(clientConn))
// create the request with the current context
request, err := http.NewRequestWithContext(
ctx,
req.Request.Method,
req.Request.RequestUri,
body,
)
if err != nil {
return nil, err
}
if req.Request.Url != nil {
request.URL = &url.URL{
Scheme: req.Request.Url.Scheme,
Opaque: req.Request.Url.Opaque,
Host: req.Request.Url.Host,
Path: req.Request.Url.Path,
RawPath: req.Request.Url.RawPath,
ForceQuery: req.Request.Url.ForceQuery,
RawQuery: req.Request.Url.RawQuery,
Fragment: req.Request.Url.Fragment,
}
if req.Request.Url.User != nil {
if req.Request.Url.User.PasswordSet {
request.URL.User = url.UserPassword(req.Request.Url.User.Username, req.Request.Url.User.Password)
} else {
request.URL.User = url.User(req.Request.Url.User.Username)
}
}
}
request.Proto = req.Request.Proto
request.ProtoMajor = int(req.Request.ProtoMajor)
request.ProtoMinor = int(req.Request.ProtoMinor)
request.Header = make(http.Header, len(req.Request.Header))
for _, elem := range req.Request.Header {
request.Header[elem.Key] = elem.Values
}
request.ContentLength = req.Request.ContentLength
request.TransferEncoding = req.Request.TransferEncoding
request.Host = req.Request.Host
request.Form = make(url.Values, len(req.Request.Form))
for _, elem := range req.Request.Form {
request.Form[elem.Key] = elem.Values
}
request.PostForm = make(url.Values, len(req.Request.PostForm))
for _, elem := range req.Request.PostForm {
request.PostForm[elem.Key] = elem.Values
}
request.Trailer = make(http.Header)
request.RemoteAddr = req.Request.RemoteAddr
request.RequestURI = req.Request.RequestUri
if req.Request.Tls != nil {
request.TLS = &tls.ConnectionState{
Version: uint16(req.Request.Tls.Version),
HandshakeComplete: req.Request.Tls.HandshakeComplete,
DidResume: req.Request.Tls.DidResume,
CipherSuite: uint16(req.Request.Tls.CipherSuite),
NegotiatedProtocol: req.Request.Tls.NegotiatedProtocol,
NegotiatedProtocolIsMutual: true, // always true per https://pkg.go.dev/crypto/tls#ConnectionState
ServerName: req.Request.Tls.ServerName,
PeerCertificates: make([]*x509.Certificate, len(req.Request.Tls.PeerCertificates.Cert)),
VerifiedChains: make([][]*x509.Certificate, len(req.Request.Tls.VerifiedChains)),
SignedCertificateTimestamps: req.Request.Tls.SignedCertificateTimestamps,
OCSPResponse: req.Request.Tls.OcspResponse,
}
for i, certBytes := range req.Request.Tls.PeerCertificates.Cert {
cert, err := x509.ParseCertificate(certBytes)
if err != nil {
return nil, err
}
request.TLS.PeerCertificates[i] = cert
}
for i, chain := range req.Request.Tls.VerifiedChains {
request.TLS.VerifiedChains[i] = make([]*x509.Certificate, len(chain.Cert))
for j, certBytes := range chain.Cert {
cert, err := x509.ParseCertificate(certBytes)
if err != nil {
return nil, err
}
request.TLS.VerifiedChains[i][j] = cert
}
}
}
s.handler.ServeHTTP(writer, request)
if err := clientConn.Close(); err != nil {
return nil, fmt.Errorf("failed to close client conn: %w", err)
}
return &httppb.HTTPResponse{
Header: grpcutils.GetHTTPHeader(writerHeaders),
}, nil
}
// HandleSimple handles http requests over http2 using a simple request response model.
// Websockets are not supported.
func (s *Server) HandleSimple(ctx context.Context, r *httppb.HandleSimpleHTTPRequest) (*httppb.HandleSimpleHTTPResponse, error) {
req, err := http.NewRequest(r.Method, r.Url, bytes.NewBuffer(r.Body))
if err != nil {
return nil, err
}
grpcutils.SetHeaders(req.Header, r.RequestHeaders)
req = req.WithContext(ctx)
req.RequestURI = r.Url
req.ContentLength = int64(len(r.Body))
w := newResponseWriter()
grpcutils.SetHeaders(w.Header(), r.ResponseHeaders)
s.handler.ServeHTTP(w, req)
resp := &httppb.HandleSimpleHTTPResponse{
Code: int32(w.statusCode),
Headers: grpcutils.GetHTTPHeader(w.Header()),
Body: w.body.Bytes(),
}
if w.statusCode == http.StatusInternalServerError {
return nil, grpcutils.GetGRPCErrorFromHTTPResponse(resp)
}
return resp, nil
}
type ResponseWriter struct {
body *bytes.Buffer
header http.Header
statusCode int
}
// newResponseWriter returns very basic implementation of the http.ResponseWriter
func newResponseWriter() *ResponseWriter {
return &ResponseWriter{
body: new(bytes.Buffer),
header: make(http.Header),
statusCode: http.StatusOK,
}
}
func (w *ResponseWriter) Header() http.Header {
return w.header
}
func (w *ResponseWriter) Write(buf []byte) (int, error) {
return w.body.Write(buf)
}
func (w *ResponseWriter) WriteHeader(code int) {
w.statusCode = code
}
func (w *ResponseWriter) StatusCode() int {
return w.statusCode
}
func (w *ResponseWriter) Body() *bytes.Buffer {
return w.body
}
-199
View File
@@ -1,199 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpchttp
import (
"bytes"
"io"
"maps"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/test/bufconn"
"github.com/luxfi/log"
"github.com/luxfi/vm/rpc/grpcutils"
httppb "github.com/luxfi/node/proto/pb/http"
)
var _ io.Reader = (*infiniteStream)(nil)
func TestConvertWriteResponse(t *testing.T) {
scenerios := map[string]struct {
resp *httppb.HandleSimpleHTTPResponse
}{
"empty response": {
resp: &httppb.HandleSimpleHTTPResponse{},
},
"response header value empty": {
resp: &httppb.HandleSimpleHTTPResponse{
Code: 500,
Body: []byte("foo"),
Headers: []*httppb.Element{
{
Key: "foo",
},
},
},
},
"response header key empty": {
resp: &httppb.HandleSimpleHTTPResponse{
Code: 200,
Body: []byte("foo"),
Headers: []*httppb.Element{
{
Values: []string{"foo"},
},
},
},
},
}
for testName, scenerio := range scenerios {
t.Run(testName, func(t *testing.T) {
w := httptest.NewRecorder()
require.NoError(t, convertWriteResponse(w, scenerio.resp))
})
}
}
func TestRequestClientArbitrarilyLongBody(t *testing.T) {
require := require.New(t)
listener := bufconn.Listen(0)
server := grpc.NewServer()
httppb.RegisterHTTPServer(server, &httppb.UnimplementedHTTPServer{})
go func() {
_ = server.Serve(listener)
}()
conn, err := grpc.NewClient(listener.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(err)
client := NewClient(httppb.NewHTTPClient(conn), log.NoLog{})
w := &httptest.ResponseRecorder{}
r := &http.Request{
Header: map[string][]string{
"Upgrade": {"foo"}, // Make this look like a streaming request
},
Body: io.NopCloser(infiniteStream{}),
}
client.ServeHTTP(w, r) // Shouldn't block forever reading the body
}
// Tests that writes to the http response in the server are propagated to the
// client
func TestHttpResponse(t *testing.T) {
tests := []struct {
name string
requestHeaders http.Header
responseHeaders http.Header
}{
{
// Requests with an upgrade header do not use the "Simple*" http response
// apis and must be separately tested
name: "upgrade request header specified",
requestHeaders: http.Header{
"Upgrade": {"upgrade"},
"foo": {"foo"},
},
responseHeaders: http.Header{},
},
{
name: "arbitrary request headers",
requestHeaders: http.Header{
"foo": {"foo"},
},
responseHeaders: http.Header{},
},
{
name: "response header set with upgrade request header",
requestHeaders: http.Header{
"Upgrade": {"upgrade"},
},
responseHeaders: http.Header{
"foo": {"foo"},
},
},
{
name: "response header set",
requestHeaders: http.Header{},
responseHeaders: http.Header{
"foo": {"foo"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)
wantHandlerHeaders := http.Header{}
wantHandlerHeaders.Add("Bar", "bar")
wantHandlerHeaders.Add("Content-Type", "application/json")
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
for k, v := range wantHandlerHeaders {
w.Header().Set(k, v[0])
}
_, _ = w.Write([]byte("baz"))
})
listener, err := grpcutils.NewListener()
require.NoError(err)
server := grpc.NewServer()
httppb.RegisterHTTPServer(server, NewServer(handler))
go func() {
_ = server.Serve(listener)
}()
conn, err := grpc.NewClient(
listener.Addr().String(),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
require.NoError(err)
recorder := &httptest.ResponseRecorder{
HeaderMap: maps.Clone(tt.responseHeaders),
Body: bytes.NewBuffer(nil),
}
client := NewClient(httppb.NewHTTPClient(conn), log.NoLog{})
request := &http.Request{
Body: io.NopCloser(strings.NewReader("foo")),
Header: tt.requestHeaders,
}
client.ServeHTTP(recorder, request)
wantResponseHeaders := maps.Clone(tt.responseHeaders)
for k, v := range wantHandlerHeaders {
wantResponseHeaders.Add(k, v[0])
}
require.Equal(wantResponseHeaders, recorder.Header())
require.Equal(http.StatusOK, recorder.Code)
require.Equal("baz", recorder.Body.String())
require.Equal(tt.requestHeaders, request.Header)
})
}
}
type infiniteStream struct{}
func (infiniteStream) Read(p []byte) (n int, err error) {
return len(p), nil
}
@@ -1,130 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpcconn
import (
"context"
"errors"
"fmt"
"io"
"net"
"os"
"time"
"google.golang.org/protobuf/types/known/emptypb"
"github.com/luxfi/codec/wrappers"
connpb "github.com/luxfi/node/proto/pb/net/conn"
)
var _ net.Conn = (*Client)(nil)
// Client is an implementation of a connection that talks over RPC.
type Client struct {
client connpb.ConnClient
local net.Addr
remote net.Addr
toClose []io.Closer
}
// NewClient returns a connection connected to a remote connection
func NewClient(client connpb.ConnClient, local, remote net.Addr, toClose ...io.Closer) *Client {
return &Client{
client: client,
local: local,
remote: remote,
toClose: toClose,
}
}
func (c *Client) Read(p []byte) (int, error) {
resp, err := c.client.Read(context.Background(), &connpb.ReadRequest{
Length: int32(len(p)),
})
if err != nil {
return 0, err
}
copy(p, resp.Read)
if resp.Error != nil {
switch resp.Error.ErrorCode {
case connpb.ErrorCode_ERROR_CODE_EOF:
err = io.EOF
case connpb.ErrorCode_ERROR_CODE_OS_ERR_DEADLINE_EXCEEDED:
err = fmt.Errorf("%w: %s", os.ErrDeadlineExceeded, resp.Error.Message)
default:
err = errors.New(resp.Error.Message)
}
}
return len(resp.Read), err
}
func (c *Client) Write(b []byte) (int, error) {
resp, err := c.client.Write(context.Background(), &connpb.WriteRequest{
Payload: b,
})
if err != nil {
return 0, err
}
if resp.Error != nil {
err = errors.New(*resp.Error)
}
return int(resp.Length), err
}
func (c *Client) Close() error {
_, err := c.client.Close(context.Background(), &emptypb.Empty{})
errs := wrappers.Errs{}
errs.Add(err)
for _, toClose := range c.toClose {
errs.Add(toClose.Close())
}
return errs.Err
}
func (c *Client) LocalAddr() net.Addr {
return c.local
}
func (c *Client) RemoteAddr() net.Addr {
return c.remote
}
func (c *Client) SetDeadline(t time.Time) error {
bytes, err := t.MarshalBinary()
if err != nil {
return err
}
_, err = c.client.SetDeadline(context.Background(), &connpb.SetDeadlineRequest{
Time: bytes,
})
return err
}
func (c *Client) SetReadDeadline(t time.Time) error {
bytes, err := t.MarshalBinary()
if err != nil {
return err
}
_, err = c.client.SetReadDeadline(context.Background(), &connpb.SetDeadlineRequest{
Time: bytes,
})
return err
}
func (c *Client) SetWriteDeadline(t time.Time) error {
bytes, err := t.MarshalBinary()
if err != nil {
return err
}
_, err = c.client.SetWriteDeadline(context.Background(), &connpb.SetDeadlineRequest{
Time: bytes,
})
return err
}
@@ -1,103 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpcconn
import (
"context"
"errors"
"io"
"net"
"os"
"time"
"google.golang.org/protobuf/types/known/emptypb"
"github.com/luxfi/vm/rpc/grpcutils"
connpb "github.com/luxfi/node/proto/pb/net/conn"
)
var _ connpb.ConnServer = (*Server)(nil)
// Server is an http.Conn that is managed over RPC.
type Server struct {
connpb.UnsafeConnServer
conn net.Conn
closer *grpcutils.ServerCloser
}
// NewServer returns an http.Conn managed remotely
func NewServer(conn net.Conn, closer *grpcutils.ServerCloser) *Server {
return &Server{
conn: conn,
closer: closer,
}
}
func (s *Server) Read(_ context.Context, req *connpb.ReadRequest) (*connpb.ReadResponse, error) {
buf := make([]byte, int(req.Length))
n, err := s.conn.Read(buf)
resp := &connpb.ReadResponse{
Read: buf[:n],
}
if err != nil {
resp.Error = &connpb.Error{
Message: err.Error(),
}
// Sentinel errors must be special-cased through an error code
switch {
case err == io.EOF:
resp.Error.ErrorCode = connpb.ErrorCode_ERROR_CODE_EOF
case errors.Is(err, os.ErrDeadlineExceeded):
resp.Error.ErrorCode = connpb.ErrorCode_ERROR_CODE_OS_ERR_DEADLINE_EXCEEDED
}
}
return resp, nil
}
func (s *Server) Write(_ context.Context, req *connpb.WriteRequest) (*connpb.WriteResponse, error) {
n, err := s.conn.Write(req.Payload)
if err != nil {
return nil, err
}
return &connpb.WriteResponse{
Length: int32(n),
}, nil
}
func (s *Server) Close(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
err := s.conn.Close()
s.closer.Stop()
return &emptypb.Empty{}, err
}
func (s *Server) SetDeadline(_ context.Context, req *connpb.SetDeadlineRequest) (*emptypb.Empty, error) {
deadline := time.Time{}
err := deadline.UnmarshalBinary(req.Time)
if err != nil {
return nil, err
}
return &emptypb.Empty{}, s.conn.SetDeadline(deadline)
}
func (s *Server) SetReadDeadline(_ context.Context, req *connpb.SetDeadlineRequest) (*emptypb.Empty, error) {
deadline := time.Time{}
err := deadline.UnmarshalBinary(req.Time)
if err != nil {
return nil, err
}
return &emptypb.Empty{}, s.conn.SetReadDeadline(deadline)
}
func (s *Server) SetWriteDeadline(_ context.Context, req *connpb.SetDeadlineRequest) (*emptypb.Empty, error) {
deadline := time.Time{}
err := deadline.UnmarshalBinary(req.Time)
if err != nil {
return nil, err
}
return &emptypb.Empty{}, s.conn.SetWriteDeadline(deadline)
}
@@ -1,105 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpcconn
import (
"context"
"io"
"net"
"os"
"testing"
"time"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/test/bufconn"
"github.com/luxfi/vm/rpc/grpcutils"
connpb "github.com/luxfi/node/proto/pb/net/conn"
)
// TestErrIOEOF tests that if a net.Conn returns an io.EOF, it propagates that
// same error type.
func TestErrIOEOF(t *testing.T) {
require := require.New(t)
server := grpc.NewServer()
listener := bufconn.Listen(1024)
serverCloser := &grpcutils.ServerCloser{}
serverCloser.Add(server)
conn1, conn2 := net.Pipe()
connServer := NewServer(conn1, serverCloser)
connpb.RegisterConnServer(server, connServer)
go func() {
_ = server.Serve(listener)
}()
grpcConn, err := grpc.DialContext(context.Background(), "bufnet",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
return listener.Dial()
}),
grpc.WithInsecure(),
)
require.NoError(err)
client := NewClient(
connpb.NewConnClient(grpcConn),
conn1.LocalAddr(),
conn1.RemoteAddr(),
)
require.NoError(conn2.Close())
buf := make([]byte, 1)
n, err := client.Read(buf)
require.Zero(n)
require.Equal(io.EOF, err)
}
// TestOSErrDeadlineExceeded tests that if a net.Conn returns an
// os.ErrDeadlineExceeded, it propagates that same error type.
func TestOSErrDeadlineExceeded(t *testing.T) {
require := require.New(t)
server := grpc.NewServer()
listener := bufconn.Listen(1024)
serverCloser := &grpcutils.ServerCloser{}
serverCloser.Add(server)
conn1, _ := net.Pipe()
connServer := NewServer(conn1, serverCloser)
require.NoError(conn1.SetDeadline(time.Now()))
connpb.RegisterConnServer(server, connServer)
go func() {
_ = server.Serve(listener)
}()
grpcConn, err := grpc.DialContext(context.Background(), "bufnet",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
return listener.Dial()
}),
grpc.WithInsecure(),
)
require.NoError(err)
client := NewClient(
connpb.NewConnClient(grpcConn),
conn1.LocalAddr(),
conn1.RemoteAddr(),
)
buf := make([]byte, 1)
n, err := client.Read(buf)
require.Zero(n)
require.ErrorIs(err, os.ErrDeadlineExceeded)
}
@@ -1,52 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpcreader
import (
"context"
"io"
"net"
"testing"
"testing/iotest"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/test/bufconn"
"github.com/luxfi/node/proto/pb/io/reader"
)
// TestErrIOEOF tests that if an io.EOF is returned by an io.Reader, it
// propagates that same error type.
func TestErrIOEOF(t *testing.T) {
require := require.New(t)
server := grpc.NewServer()
listener := bufconn.Listen(1024)
readerServer := NewServer(iotest.ErrReader(io.EOF))
reader.RegisterReaderServer(server, readerServer)
go func() {
_ = server.Serve(listener)
}()
conn, err := grpc.DialContext(context.Background(), "bufnet",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
return listener.Dial()
}),
grpc.WithInsecure(),
)
require.NoError(err)
client := NewClient(reader.NewReaderClient(conn))
buf := make([]byte, 1)
n, err := client.Read(buf)
require.Zero(n)
// Do not use require.ErrorIs because callers use equality checks on io.EOF
require.Equal(io.EOF, err)
}
@@ -1,46 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpcreader
import (
"context"
"errors"
"io"
readerpb "github.com/luxfi/node/proto/pb/io/reader"
)
var _ io.Reader = (*Client)(nil)
// Client is a reader that talks over RPC.
type Client struct{ client readerpb.ReaderClient }
// NewClient returns a reader connected to a remote reader
func NewClient(client readerpb.ReaderClient) *Client {
return &Client{client: client}
}
func (c *Client) Read(p []byte) (int, error) {
resp, err := c.client.Read(context.Background(), &readerpb.ReadRequest{
Length: int32(len(p)),
})
if err != nil {
return 0, err
}
copy(p, resp.Read)
if resp.Error != nil {
// Sentinel errors must be special-cased through an error code
switch resp.Error.ErrorCode {
case readerpb.ErrorCode_ERROR_CODE_EOF:
err = io.EOF
default:
err = errors.New(resp.Error.Message)
}
}
return len(resp.Read), err
}
@@ -1,45 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpcreader
import (
"context"
"io"
readerpb "github.com/luxfi/node/proto/pb/io/reader"
)
var _ readerpb.ReaderServer = (*Server)(nil)
// Server is an io.Reader that is managed over RPC.
type Server struct {
readerpb.UnsafeReaderServer
reader io.Reader
}
// NewServer returns an io.Reader instance managed remotely
func NewServer(reader io.Reader) *Server {
return &Server{reader: reader}
}
func (s *Server) Read(_ context.Context, req *readerpb.ReadRequest) (*readerpb.ReadResponse, error) {
buf := make([]byte, int(req.Length))
n, err := s.reader.Read(buf)
resp := &readerpb.ReadResponse{
Read: buf[:n],
}
if err != nil {
resp.Error = &readerpb.Error{
Message: err.Error(),
}
// Sentinel errors must be special-cased through an error code
if err == io.EOF {
resp.Error.ErrorCode = readerpb.ErrorCode_ERROR_CODE_EOF
}
}
return resp, nil
}
@@ -1,77 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpcresponsewriter
import (
"bufio"
"net"
"net/http"
"sync"
)
var (
_ http.ResponseWriter = (*lockedWriter)(nil)
_ http.Flusher = (*lockedWriter)(nil)
_ http.Hijacker = (*lockedWriter)(nil)
)
type lockedWriter struct {
lock sync.Mutex
writer http.ResponseWriter
headerWritten bool
}
func NewLockedWriter(w http.ResponseWriter) http.ResponseWriter {
return &lockedWriter{writer: w}
}
func (lw *lockedWriter) Header() http.Header {
lw.lock.Lock()
defer lw.lock.Unlock()
return lw.writer.Header()
}
func (lw *lockedWriter) Write(b []byte) (int, error) {
lw.lock.Lock()
defer lw.lock.Unlock()
lw.headerWritten = true
return lw.writer.Write(b)
}
func (lw *lockedWriter) WriteHeader(statusCode int) {
lw.lock.Lock()
defer lw.lock.Unlock()
// Skip writing the header if it has already been written once.
if lw.headerWritten {
return
}
lw.headerWritten = true
lw.writer.WriteHeader(statusCode)
}
func (lw *lockedWriter) Flush() {
lw.lock.Lock()
defer lw.lock.Unlock()
flusher, ok := lw.writer.(http.Flusher)
if ok {
flusher.Flush()
}
}
func (lw *lockedWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
lw.lock.Lock()
defer lw.lock.Unlock()
hijacker, ok := lw.writer.(http.Hijacker)
if !ok {
return nil, nil, errUnsupportedHijacking
}
return hijacker.Hijack()
}
@@ -1,133 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpcresponsewriter
import (
"bufio"
"context"
"net"
"net/http"
"google.golang.org/protobuf/types/known/emptypb"
"github.com/luxfi/node/vms/rpcchainvm/rpchttp/rpcconn"
"github.com/luxfi/node/vms/rpcchainvm/rpchttp/rpcreader"
"github.com/luxfi/node/vms/rpcchainvm/rpchttp/rpcwriter"
"github.com/luxfi/vm/rpc/grpcutils"
responsewriterpb "github.com/luxfi/node/proto/pb/http/responsewriter"
readerpb "github.com/luxfi/node/proto/pb/io/reader"
writerpb "github.com/luxfi/node/proto/pb/io/writer"
connpb "github.com/luxfi/node/proto/pb/net/conn"
)
var (
_ http.ResponseWriter = (*Client)(nil)
_ http.Flusher = (*Client)(nil)
_ http.Hijacker = (*Client)(nil)
)
// Client is an http.ResponseWriter that talks over RPC.
type Client struct {
client responsewriterpb.WriterClient
header http.Header
}
// NewClient returns a response writer connected to a remote response writer
func NewClient(header http.Header, client responsewriterpb.WriterClient) *Client {
return &Client{
client: client,
header: header,
}
}
func (c *Client) Header() http.Header {
return c.header
}
func (c *Client) Write(payload []byte) (int, error) {
req := &responsewriterpb.WriteRequest{
Headers: make([]*responsewriterpb.Header, 0, len(c.header)),
Payload: payload,
}
for key, values := range c.header {
req.Headers = append(req.Headers, &responsewriterpb.Header{
Key: key,
Values: values,
})
}
resp, err := c.client.Write(context.Background(), req)
if err != nil {
return 0, err
}
return int(resp.Written), nil
}
func (c *Client) WriteHeader(statusCode int) {
req := &responsewriterpb.WriteHeaderRequest{
Headers: make([]*responsewriterpb.Header, 0, len(c.header)),
StatusCode: int32(statusCode),
}
for key, values := range c.header {
req.Headers = append(req.Headers, &responsewriterpb.Header{
Key: key,
Values: values,
})
}
_, _ = c.client.WriteHeader(context.Background(), req)
}
func (c *Client) Flush() {
_, _ = c.client.Flush(context.Background(), &emptypb.Empty{})
}
type addr struct {
network string
str string
}
func (a *addr) Network() string {
return a.network
}
func (a *addr) String() string {
return a.str
}
func (c *Client) Hijack() (net.Conn, *bufio.ReadWriter, error) {
resp, err := c.client.Hijack(context.Background(), &emptypb.Empty{})
if err != nil {
return nil, nil, err
}
clientConn, err := grpcutils.Dial(resp.ServerAddr)
if err != nil {
return nil, nil, err
}
conn := rpcconn.NewClient(
connpb.NewConnClient(clientConn),
&addr{
network: resp.LocalNetwork,
str: resp.LocalString,
},
&addr{
network: resp.RemoteNetwork,
str: resp.RemoteString,
},
clientConn,
)
reader := rpcreader.NewClient(readerpb.NewReaderClient(clientConn))
writer := rpcwriter.NewClient(writerpb.NewWriterClient(clientConn))
readWriter := bufio.NewReadWriter(
bufio.NewReader(reader),
bufio.NewWriter(writer),
)
return conn, readWriter, nil
}
@@ -1,122 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpcresponsewriter
import (
"context"
"errors"
"net/http"
"google.golang.org/protobuf/types/known/emptypb"
"github.com/luxfi/node/vms/rpcchainvm/rpchttp/rpcconn"
"github.com/luxfi/node/vms/rpcchainvm/rpchttp/rpcreader"
"github.com/luxfi/node/vms/rpcchainvm/rpchttp/rpcwriter"
"github.com/luxfi/vm/rpc/grpcutils"
responsewriterpb "github.com/luxfi/node/proto/pb/http/responsewriter"
readerpb "github.com/luxfi/node/proto/pb/io/reader"
writerpb "github.com/luxfi/node/proto/pb/io/writer"
connpb "github.com/luxfi/node/proto/pb/net/conn"
)
var (
errUnsupportedFlushing = errors.New("response writer doesn't support flushing")
errUnsupportedHijacking = errors.New("response writer doesn't support hijacking")
_ responsewriterpb.WriterServer = (*Server)(nil)
)
// Server is an http.ResponseWriter that is managed over RPC.
type Server struct {
responsewriterpb.UnsafeWriterServer
writer http.ResponseWriter
}
// NewServer returns an http.ResponseWriter instance managed remotely
func NewServer(writer http.ResponseWriter) *Server {
return &Server{
writer: writer,
}
}
func (s *Server) Write(
_ context.Context,
req *responsewriterpb.WriteRequest,
) (*responsewriterpb.WriteResponse, error) {
headers := s.writer.Header()
clear(headers)
for _, header := range req.Headers {
headers[header.Key] = header.Values
}
n, err := s.writer.Write(req.Payload)
if err != nil {
return nil, err
}
return &responsewriterpb.WriteResponse{
Written: int32(n),
}, nil
}
func (s *Server) WriteHeader(
_ context.Context,
req *responsewriterpb.WriteHeaderRequest,
) (*emptypb.Empty, error) {
headers := s.writer.Header()
clear(headers)
for _, header := range req.Headers {
headers[header.Key] = header.Values
}
s.writer.WriteHeader(grpcutils.EnsureValidResponseCode(int(req.StatusCode)))
return &emptypb.Empty{}, nil
}
func (s *Server) Flush(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
flusher, ok := s.writer.(http.Flusher)
if !ok {
return nil, errUnsupportedFlushing
}
flusher.Flush()
return &emptypb.Empty{}, nil
}
func (s *Server) Hijack(context.Context, *emptypb.Empty) (*responsewriterpb.HijackResponse, error) {
hijacker, ok := s.writer.(http.Hijacker)
if !ok {
return nil, errUnsupportedHijacking
}
conn, readWriter, err := hijacker.Hijack()
if err != nil {
return nil, err
}
serverListener, err := grpcutils.NewListener()
if err != nil {
return nil, err
}
server := grpcutils.NewServer()
closer := grpcutils.ServerCloser{}
closer.Add(server)
connpb.RegisterConnServer(server, rpcconn.NewServer(conn, &closer))
readerpb.RegisterReaderServer(server, rpcreader.NewServer(readWriter))
writerpb.RegisterWriterServer(server, rpcwriter.NewServer(readWriter))
go grpcutils.Serve(serverListener, server)
local := conn.LocalAddr()
remote := conn.RemoteAddr()
return &responsewriterpb.HijackResponse{
LocalNetwork: local.Network(),
LocalString: local.String(),
RemoteNetwork: remote.Network(),
RemoteString: remote.String(),
ServerAddr: serverListener.Addr().String(),
}, nil
}
@@ -1,38 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpcwriter
import (
"context"
"errors"
"io"
writerpb "github.com/luxfi/node/proto/pb/io/writer"
)
var _ io.Writer = (*Client)(nil)
// Client is an io.Writer that talks over RPC.
type Client struct{ client writerpb.WriterClient }
// NewClient returns a writer connected to a remote writer
func NewClient(client writerpb.WriterClient) *Client {
return &Client{client: client}
}
func (c *Client) Write(p []byte) (int, error) {
resp, err := c.client.Write(context.Background(), &writerpb.WriteRequest{
Payload: p,
})
if err != nil {
return 0, err
}
if resp.Error != nil {
err = errors.New(*resp.Error)
}
return int(resp.Written), err
}
@@ -1,38 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpcwriter
import (
"context"
"io"
writerpb "github.com/luxfi/node/proto/pb/io/writer"
)
var _ writerpb.WriterServer = (*Server)(nil)
// Server is an http.Handler that is managed over RPC.
type Server struct {
writerpb.UnsafeWriterServer
writer io.Writer
}
// NewServer returns an http.Handler instance managed remotely
func NewServer(writer io.Writer) *Server {
return &Server{writer: writer}
}
func (s *Server) Write(_ context.Context, req *writerpb.WriteRequest) (*writerpb.WriteResponse, error) {
n, err := s.writer.Write(req.Payload)
resp := &writerpb.WriteResponse{
Written: int32(n),
}
if err != nil {
errStr := err.Error()
resp.Error = &errStr
}
return resp, nil
}
@@ -1,125 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package subprocess
import (
"context"
"fmt"
"io"
"net"
"os"
"os/exec"
"time"
"github.com/luxfi/node/vms/rpcchainvm/gruntime"
"github.com/luxfi/node/vms/rpcchainvm/runtime"
"github.com/luxfi/vm/rpc/grpcutils"
pb "github.com/luxfi/node/proto/pb/vm/runtime"
)
// Bootstrap starts a VM as a subprocess after initialization completes and
// pipes the IO to the appropriate writers.
//
// The subprocess is expected to be stopped by the caller if a non-nil error is
// returned. If piping the IO fails then the subprocess will be stopped.
func Bootstrap(
ctx context.Context,
listener net.Listener,
cmd *exec.Cmd,
config *Config,
) (*Status, runtime.Stopper, error) {
defer listener.Close()
switch {
case cmd == nil:
return nil, nil, fmt.Errorf("%w: cmd required", runtime.ErrInvalidConfig)
case config.Log.IsZero():
return nil, nil, fmt.Errorf("%w: logger required", runtime.ErrInvalidConfig)
case config.Stderr == nil, config.Stdout == nil:
return nil, nil, fmt.Errorf("%w: stderr and stdout required", runtime.ErrInvalidConfig)
}
intitializer := newInitializer(cmd.Path)
server := grpcutils.NewServer()
defer server.GracefulStop()
pb.RegisterRuntimeServer(server, gruntime.NewServer(intitializer))
go grpcutils.Serve(listener, server)
serverAddr := listener.Addr()
// CRITICAL: If cmd.Env is already set (e.g., by tests), preserve those values
// and only add our required environment variable. If cmd.Env is nil,
// copy the parent's environment first.
if cmd.Env == nil {
cmd.Env = os.Environ()
}
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", runtime.EngineAddressKey, serverAddr.String()))
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return nil, nil, fmt.Errorf("failed to create stdout pipe: %w", err)
}
stderrPipe, err := cmd.StderrPipe()
if err != nil {
return nil, nil, fmt.Errorf("failed to create stderr pipe: %w", err)
}
// start subprocess
if err := cmd.Start(); err != nil {
return nil, nil, fmt.Errorf("failed to start process: %w", err)
}
log := config.Log
stopper := NewStopper(log, cmd)
// start stdout collector
go func() {
_, err := io.Copy(config.Stdout, stdoutPipe)
if err != nil {
log.Error("stdout collector failed", "error", err)
}
stopper.Stop(context.TODO())
log.Info("stdout collector shutdown")
}()
// start stderr collector
go func() {
_, err := io.Copy(config.Stderr, stderrPipe)
if err != nil {
log.Error("stderr collector failed", "error", err)
}
stopper.Stop(context.TODO())
log.Info("stderr collector shutdown")
}()
// wait for handshake success
timeout := time.NewTimer(config.HandshakeTimeout)
defer timeout.Stop()
select {
case <-intitializer.initialized:
case <-timeout.C:
stopper.Stop(ctx)
return nil, nil, fmt.Errorf("%w: %w", runtime.ErrHandshakeFailed, runtime.ErrProcessNotFound)
}
if intitializer.err != nil {
stopper.Stop(ctx)
return nil, nil, fmt.Errorf("%w: %w", runtime.ErrHandshakeFailed, intitializer.err)
}
log.Info("plugin handshake succeeded", "addr", intitializer.vmAddr)
status := &Status{
Pid: cmd.Process.Pid,
Addr: intitializer.vmAddr,
}
return status, stopper, nil
}
@@ -1,5 +1,3 @@
//go:build !grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
@@ -19,14 +17,13 @@ import (
"github.com/luxfi/node/vms/rpcchainvm/runtime"
)
// Bootstrap starts a VM as a subprocess after initialization completes and
// pipes the IO to the appropriate writers.
// Bootstrap starts a VM as a subprocess after initialization completes
// and pipes the IO to the appropriate writers. Handshake is the ZAP
// binary framed exchange ([len][protocol version][vm addr]).
//
// This is the ZAP transport version which uses a simple binary handshake
// instead of gRPC.
//
// The subprocess is expected to be stopped by the caller if a non-nil error is
// returned. If piping the IO fails then the subprocess will be stopped.
// The subprocess is expected to be stopped by the caller if a non-nil
// error is returned. If piping the IO fails then the subprocess will
// be stopped.
func Bootstrap(
ctx context.Context,
listener net.Listener,
-72
View File
@@ -1,72 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sender
import (
"context"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
senderpb "github.com/luxfi/node/proto/pb/sender"
"github.com/luxfi/p2p"
)
var _ p2p.Sender = (*Client)(nil)
// Client implements p2p.Sender over gRPC
type Client struct {
client senderpb.SenderClient
}
// GRPC returns a p2p.Sender using gRPC transport.
func GRPC(client senderpb.SenderClient) p2p.Sender {
return &Client{client: client}
}
// NewClient is an alias for GRPC for backwards compatibility.
// Deprecated: Use GRPC() instead.
func NewClient(client senderpb.SenderClient) p2p.Sender {
return GRPC(client)
}
func (c *Client) SendRequest(ctx context.Context, nodeIDs set.Set[ids.NodeID], requestID uint32, request []byte) error {
nodeIDBytes := make([][]byte, 0, nodeIDs.Len())
for nodeID := range nodeIDs {
nodeIDBytes = append(nodeIDBytes, nodeID[:])
}
_, err := c.client.SendRequest(ctx, &senderpb.SendRequestMsg{
NodeIds: nodeIDBytes,
RequestId: requestID,
Request: request,
})
return err
}
func (c *Client) SendResponse(ctx context.Context, nodeID ids.NodeID, requestID uint32, response []byte) error {
_, err := c.client.SendResponse(ctx, &senderpb.SendResponseMsg{
NodeId: nodeID[:],
RequestId: requestID,
Response: response,
})
return err
}
func (c *Client) SendError(ctx context.Context, nodeID ids.NodeID, requestID uint32, errorCode int32, errorMessage string) error {
_, err := c.client.SendError(ctx, &senderpb.SendErrorMsg{
NodeId: nodeID[:],
RequestId: requestID,
ErrorCode: errorCode,
ErrorMessage: errorMessage,
})
return err
}
func (c *Client) SendGossip(ctx context.Context, config p2p.SendConfig, msg []byte) error {
_, err := c.client.SendGossip(ctx, &senderpb.SendGossipMsg{
Msg: msg,
})
return err
}
-72
View File
@@ -1,72 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sender
import (
"context"
"google.golang.org/protobuf/types/known/emptypb"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
senderpb "github.com/luxfi/node/proto/pb/sender"
"github.com/luxfi/warp"
)
var _ senderpb.SenderServer = (*Server)(nil)
type Server struct {
senderpb.UnsafeSenderServer
sender warp.Sender
}
// NewServer returns a messenger connected to a remote channel
func NewServer(sender warp.Sender) *Server {
return &Server{sender: sender}
}
func (s *Server) SendRequest(ctx context.Context, req *senderpb.SendRequestMsg) (*emptypb.Empty, error) {
// Convert byte slices to NodeID set
nodeIDs := set.NewSet[ids.NodeID](len(req.NodeIds))
for _, nodeIDBytes := range req.NodeIds {
nodeID, err := ids.ToNodeID(nodeIDBytes)
if err != nil {
return nil, err
}
nodeIDs.Add(nodeID)
}
err := s.sender.SendRequest(ctx, nodeIDs, req.RequestId, req.Request)
return &emptypb.Empty{}, err
}
func (s *Server) SendResponse(ctx context.Context, req *senderpb.SendResponseMsg) (*emptypb.Empty, error) {
nodeID, err := ids.ToNodeID(req.NodeId)
if err != nil {
return nil, err
}
err = s.sender.SendResponse(ctx, nodeID, req.RequestId, req.Response)
return &emptypb.Empty{}, err
}
func (s *Server) SendError(ctx context.Context, req *senderpb.SendErrorMsg) (*emptypb.Empty, error) {
nodeID, err := ids.ToNodeID(req.NodeId)
if err != nil {
return nil, err
}
err = s.sender.SendError(ctx, nodeID, req.RequestId, req.ErrorCode, req.ErrorMessage)
return &emptypb.Empty{}, err
}
func (s *Server) SendGossip(ctx context.Context, req *senderpb.SendGossipMsg) (*emptypb.Empty, error) {
// For RPC gossip, we don't have specific nodes, so use an empty config
config := warp.SendConfig{
NodeIDs: set.NewSet[ids.NodeID](0),
}
err := s.sender.SendGossip(ctx, config, req.Msg)
return &emptypb.Empty{}, err
}
-597
View File
@@ -1,597 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpcchainvm
import (
"context"
"errors"
"io"
"net/http"
"os"
"testing"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/luxfi/database/memdb"
"github.com/luxfi/database/prefixdb"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/node/service/metrics"
"github.com/luxfi/node/vms/rpcchainvm/runtime"
"github.com/luxfi/node/vms/rpcchainvm/runtime/subprocess"
consensusruntime "github.com/luxfi/runtime"
"github.com/luxfi/vm"
"github.com/luxfi/vm/chain"
"github.com/luxfi/vm/chain/blockmock"
"github.com/luxfi/vm/chain/blocktest"
"github.com/luxfi/vm/rpc/grpcutils"
)
// StateSummary implements chain.StateSummary for testing
type StateSummary struct {
IDV ids.ID
HeightV uint64
BytesV []byte
AcceptF func(context.Context) (chain.StateSyncMode, error)
}
func (s *StateSummary) ID() ids.ID {
return s.IDV
}
func (s *StateSummary) Height() uint64 {
return s.HeightV
}
func (s *StateSummary) Bytes() []byte {
return s.BytesV
}
func (s *StateSummary) Accept(ctx context.Context) (chain.StateSyncMode, error) {
if s.AcceptF != nil {
return s.AcceptF(ctx)
}
return chain.StateSyncSkipped, nil
}
var (
preSummaryHeight = uint64(1789)
SummaryHeight = uint64(2022)
// a summary to be returned in some UTs
mockedSummary = &StateSummary{
IDV: ids.ID{'s', 'u', 'm', 'm', 'a', 'r', 'y', 'I', 'D'},
HeightV: SummaryHeight,
BytesV: []byte("summary"),
}
// last accepted blocks data before and after summary is accepted
preSummaryBlk = &blocktest.Block{
IDV: ids.ID{'f', 'i', 'r', 's', 't', 'B', 'l', 'K'},
HeightV: preSummaryHeight,
ParentV: ids.ID{'p', 'a', 'r', 'e', 'n', 't', 'B', 'l', 'k'},
StatusV: blocktest.Accepted,
}
summaryBlk = &blocktest.Block{
IDV: ids.ID{'s', 'u', 'm', 'm', 'a', 'r', 'y', 'B', 'l', 'K'},
HeightV: SummaryHeight,
ParentV: ids.ID{'p', 'a', 'r', 'e', 'n', 't', 'B', 'l', 'k'},
StatusV: blocktest.Accepted,
}
// a fictitious error unrelated to state sync
errBrokenConnectionOrSomething = errors.New("brokenConnectionOrSomething")
errNothingToParse = errors.New("nil summary bytes. Nothing to parse")
)
type StateSyncEnabledMock struct {
chainVM *blockmock.MockChainVM
ssVM *blockmock.MockStateSyncableVM
}
// Forward ChainVM methods
func (m *StateSyncEnabledMock) Initialize(ctx context.Context, init vm.Init) error {
return m.chainVM.Initialize(ctx, init)
}
func (m *StateSyncEnabledMock) SetState(ctx context.Context, state uint32) error {
return m.chainVM.SetState(ctx, state)
}
func (m *StateSyncEnabledMock) Shutdown(ctx context.Context) error { return m.chainVM.Shutdown(ctx) }
func (m *StateSyncEnabledMock) Version(ctx context.Context) (string, error) {
return m.chainVM.Version(ctx)
}
func (m *StateSyncEnabledMock) NewHTTPHandler(ctx context.Context) (http.Handler, error) {
return m.chainVM.NewHTTPHandler(ctx)
}
func (m *StateSyncEnabledMock) Connected(ctx context.Context, nodeID ids.NodeID, nodeVersion *chain.VersionInfo) error {
return m.chainVM.Connected(ctx, nodeID, nodeVersion)
}
func (m *StateSyncEnabledMock) Disconnected(ctx context.Context, nodeID ids.NodeID) error {
return m.chainVM.Disconnected(ctx, nodeID)
}
func (m *StateSyncEnabledMock) HealthCheck(ctx context.Context) (chain.HealthResult, error) {
return m.chainVM.HealthCheck(ctx)
}
func (m *StateSyncEnabledMock) ParseBlock(ctx context.Context, bytes []byte) (chain.Block, error) {
return m.chainVM.ParseBlock(ctx, bytes)
}
func (m *StateSyncEnabledMock) GetBlock(ctx context.Context, id ids.ID) (chain.Block, error) {
return m.chainVM.GetBlock(ctx, id)
}
func (m *StateSyncEnabledMock) BuildBlock(ctx context.Context) (chain.Block, error) {
return m.chainVM.BuildBlock(ctx)
}
func (m *StateSyncEnabledMock) SetPreference(ctx context.Context, id ids.ID) error {
return m.chainVM.SetPreference(ctx, id)
}
func (m *StateSyncEnabledMock) LastAccepted(ctx context.Context) (ids.ID, error) {
return m.chainVM.LastAccepted(ctx)
}
func (m *StateSyncEnabledMock) GetBlockIDAtHeight(ctx context.Context, height uint64) (ids.ID, error) {
return m.chainVM.GetBlockIDAtHeight(ctx, height)
}
func (m *StateSyncEnabledMock) WaitForEvent(ctx context.Context) (vm.Message, error) {
return m.chainVM.WaitForEvent(ctx)
}
// Forward StateSyncableVM methods
func (m *StateSyncEnabledMock) StateSyncEnabled(ctx context.Context) (bool, error) {
return m.ssVM.StateSyncEnabled(ctx)
}
func (m *StateSyncEnabledMock) GetOngoingSyncStateSummary(ctx context.Context) (chain.StateSummary, error) {
return m.ssVM.GetOngoingSyncStateSummary(ctx)
}
func (m *StateSyncEnabledMock) GetLastStateSummary(ctx context.Context) (chain.StateSummary, error) {
return m.ssVM.GetLastStateSummary(ctx)
}
func (m *StateSyncEnabledMock) ParseStateSummary(ctx context.Context, bytes []byte) (chain.StateSummary, error) {
return m.ssVM.ParseStateSummary(ctx, bytes)
}
func (m *StateSyncEnabledMock) GetStateSummary(ctx context.Context, height uint64) (chain.StateSummary, error) {
return m.ssVM.GetStateSummary(ctx, height)
}
func stateSyncEnabledTestPlugin(t *testing.T, loadExpectations bool) chain.ChainVM {
// test key is "stateSyncEnabledTestKey"
// create mock
ctrl := gomock.NewController(t)
ssVM := &StateSyncEnabledMock{
chainVM: blockmock.NewMockChainVM(ctrl),
ssVM: blockmock.NewMockStateSyncableVM(ctrl),
}
if loadExpectations {
gomock.InOrder(
ssVM.ssVM.EXPECT().StateSyncEnabled(gomock.Any()).Return(false, chain.ErrStateSyncableVMNotImplemented).Times(1),
ssVM.ssVM.EXPECT().StateSyncEnabled(gomock.Any()).Return(false, nil).Times(1),
ssVM.ssVM.EXPECT().StateSyncEnabled(gomock.Any()).Return(true, nil).Times(1),
ssVM.ssVM.EXPECT().StateSyncEnabled(gomock.Any()).Return(false, errBrokenConnectionOrSomething).Times(1),
)
}
return ssVM
}
func getOngoingSyncStateSummaryTestPlugin(t *testing.T, loadExpectations bool) chain.ChainVM {
// test key is "getOngoingSyncStateSummaryTestKey"
// create mock
ctrl := gomock.NewController(t)
ssVM := &StateSyncEnabledMock{
chainVM: blockmock.NewMockChainVM(ctrl),
ssVM: blockmock.NewMockStateSyncableVM(ctrl),
}
if loadExpectations {
gomock.InOrder(
ssVM.ssVM.EXPECT().GetOngoingSyncStateSummary(gomock.Any()).Return(nil, chain.ErrStateSyncableVMNotImplemented).Times(1),
ssVM.ssVM.EXPECT().GetOngoingSyncStateSummary(gomock.Any()).Return(mockedSummary, nil).Times(1),
ssVM.ssVM.EXPECT().GetOngoingSyncStateSummary(gomock.Any()).Return(nil, errBrokenConnectionOrSomething).Times(1),
)
}
return ssVM
}
func getLastStateSummaryTestPlugin(t *testing.T, loadExpectations bool) chain.ChainVM {
// test key is "getLastStateSummaryTestKey"
// create mock
ctrl := gomock.NewController(t)
ssVM := &StateSyncEnabledMock{
chainVM: blockmock.NewMockChainVM(ctrl),
ssVM: blockmock.NewMockStateSyncableVM(ctrl),
}
if loadExpectations {
gomock.InOrder(
ssVM.ssVM.EXPECT().GetLastStateSummary(gomock.Any()).Return(nil, chain.ErrStateSyncableVMNotImplemented).Times(1),
ssVM.ssVM.EXPECT().GetLastStateSummary(gomock.Any()).Return(mockedSummary, nil).Times(1),
ssVM.ssVM.EXPECT().GetLastStateSummary(gomock.Any()).Return(nil, errBrokenConnectionOrSomething).Times(1),
)
}
return ssVM
}
func parseStateSummaryTestPlugin(t *testing.T, loadExpectations bool) chain.ChainVM {
// test key is "parseStateSummaryTestKey"
// create mock
ctrl := gomock.NewController(t)
ssVM := &StateSyncEnabledMock{
chainVM: blockmock.NewMockChainVM(ctrl),
ssVM: blockmock.NewMockStateSyncableVM(ctrl),
}
if loadExpectations {
gomock.InOrder(
ssVM.ssVM.EXPECT().ParseStateSummary(gomock.Any(), gomock.Any()).Return(nil, chain.ErrStateSyncableVMNotImplemented).Times(1),
ssVM.ssVM.EXPECT().ParseStateSummary(gomock.Any(), gomock.Any()).Return(mockedSummary, nil).Times(1),
ssVM.ssVM.EXPECT().ParseStateSummary(gomock.Any(), gomock.Any()).Return(nil, errNothingToParse).Times(1),
ssVM.ssVM.EXPECT().ParseStateSummary(gomock.Any(), gomock.Any()).Return(nil, errBrokenConnectionOrSomething).Times(1),
)
}
return ssVM
}
func getStateSummaryTestPlugin(t *testing.T, loadExpectations bool) chain.ChainVM {
// test key is "getStateSummaryTestKey"
// create mock
ctrl := gomock.NewController(t)
ssVM := &StateSyncEnabledMock{
chainVM: blockmock.NewMockChainVM(ctrl),
ssVM: blockmock.NewMockStateSyncableVM(ctrl),
}
if loadExpectations {
gomock.InOrder(
ssVM.ssVM.EXPECT().GetStateSummary(gomock.Any(), gomock.Any()).Return(nil, chain.ErrStateSyncableVMNotImplemented).Times(1),
ssVM.ssVM.EXPECT().GetStateSummary(gomock.Any(), gomock.Any()).Return(mockedSummary, nil).Times(1),
ssVM.ssVM.EXPECT().GetStateSummary(gomock.Any(), gomock.Any()).Return(nil, errBrokenConnectionOrSomething).Times(1),
)
}
return ssVM
}
func acceptStateSummaryTestPlugin(t *testing.T, loadExpectations bool) chain.ChainVM {
// test key is "acceptStateSummaryTestKey"
// create mock
ctrl := gomock.NewController(t)
ssVM := &StateSyncEnabledMock{
chainVM: blockmock.NewMockChainVM(ctrl),
ssVM: blockmock.NewMockStateSyncableVM(ctrl),
}
if loadExpectations {
gomock.InOrder(
ssVM.ssVM.EXPECT().GetStateSummary(gomock.Any(), gomock.Any()).Return(mockedSummary, nil).Times(1),
ssVM.ssVM.EXPECT().ParseStateSummary(gomock.Any(), gomock.Any()).DoAndReturn(
func(context.Context, []byte) (chain.StateSummary, error) {
// setup summary to be accepted before returning it
mockedSummary.AcceptF = func(context.Context) (chain.StateSyncMode, error) {
return chain.StateSyncStatic, nil
}
return mockedSummary, nil
},
).Times(1),
ssVM.ssVM.EXPECT().ParseStateSummary(gomock.Any(), gomock.Any()).DoAndReturn(
func(context.Context, []byte) (chain.StateSummary, error) {
// setup summary to be skipped before returning it
mockedSummary.AcceptF = func(context.Context) (chain.StateSyncMode, error) {
return chain.StateSyncSkipped, nil
}
return mockedSummary, nil
},
).Times(1),
ssVM.ssVM.EXPECT().ParseStateSummary(gomock.Any(), gomock.Any()).DoAndReturn(
func(context.Context, []byte) (chain.StateSummary, error) {
// setup summary to fail accept
mockedSummary.AcceptF = func(context.Context) (chain.StateSyncMode, error) {
return chain.StateSyncSkipped, errBrokenConnectionOrSomething
}
return mockedSummary, nil
},
).Times(1),
)
}
return ssVM
}
func lastAcceptedBlockPostStateSummaryAcceptTestPlugin(t *testing.T, loadExpectations bool) chain.ChainVM {
// test key is "lastAcceptedBlockPostStateSummaryAcceptTestKey"
// create mock
ctrl := gomock.NewController(t)
ssVM := &StateSyncEnabledMock{
chainVM: blockmock.NewMockChainVM(ctrl),
ssVM: blockmock.NewMockStateSyncableVM(ctrl),
}
if loadExpectations {
gomock.InOrder(
ssVM.chainVM.EXPECT().Initialize(
gomock.Any(), gomock.Any(),
).Return(nil).Times(1),
ssVM.chainVM.EXPECT().LastAccepted(gomock.Any()).Return(preSummaryBlk.ID(), nil).Times(1),
ssVM.chainVM.EXPECT().GetBlock(gomock.Any(), gomock.Any()).Return(preSummaryBlk, nil).Times(1),
ssVM.ssVM.EXPECT().ParseStateSummary(gomock.Any(), gomock.Any()).DoAndReturn(
func(context.Context, []byte) (chain.StateSummary, error) {
// setup summary to be accepted before returning it
mockedSummary.AcceptF = func(context.Context) (chain.StateSyncMode, error) {
return chain.StateSyncStatic, nil
}
return mockedSummary, nil
},
).Times(2),
// After state sync accept, expect additional LastAccepted and GetBlock calls (lines 533-538)
ssVM.chainVM.EXPECT().LastAccepted(gomock.Any()).Return(preSummaryBlk.ID(), nil).Times(1),
ssVM.chainVM.EXPECT().GetBlock(gomock.Any(), gomock.Any()).Return(preSummaryBlk, nil).Times(1),
ssVM.chainVM.EXPECT().SetState(gomock.Any(), gomock.Any()).Return(nil).Times(1),
ssVM.chainVM.EXPECT().LastAccepted(gomock.Any()).Return(summaryBlk.ID(), nil).Times(1),
ssVM.chainVM.EXPECT().GetBlock(gomock.Any(), gomock.Any()).Return(summaryBlk, nil).Times(1),
)
}
return ssVM
}
func buildClientHelper(require *require.Assertions, testKey string) *VMClient {
process := helperProcess(testKey)
listener, err := grpcutils.NewListener()
require.NoError(err)
// Use log.NewWriter(io.Discard) instead of log.NoLog{} because log.NoLog{}.IsZero()
// returns true, which causes subprocess.Bootstrap to reject it with "logger required" error
testLogger := log.NewWriter(io.Discard)
status, stopper, err := subprocess.Bootstrap(
context.Background(),
listener,
process,
&subprocess.Config{
Stderr: os.Stderr,
Stdout: io.Discard,
Log: testLogger,
HandshakeTimeout: runtime.DefaultHandshakeTimeout,
},
)
require.NoError(err)
clientConn, err := grpcutils.Dial(status.Addr)
require.NoError(err)
return NewClient(clientConn, stopper, status.Pid, nil, metrics.NewPrefixGatherer(), testLogger)
}
func TestStateSyncEnabled(t *testing.T) {
require := require.New(t)
testKey := stateSyncEnabledTestKey
// Create and start the plugin
vmImpl := buildClientHelper(require, testKey)
defer vmImpl.Runtime().Stop(context.Background())
// test state sync not implemented
// Note that enabled == false is returned rather than
// common.ErrStateSyncableVMNotImplemented
enabled, err := vmImpl.StateSyncEnabled(context.Background())
require.NoError(err)
require.False(enabled)
// test state sync disabled
enabled, err = vmImpl.StateSyncEnabled(context.Background())
require.NoError(err)
require.False(enabled)
// test state sync enabled
enabled, err = vmImpl.StateSyncEnabled(context.Background())
require.NoError(err)
require.True(enabled)
// test a non-special error.
_, err = vmImpl.StateSyncEnabled(context.Background())
require.Error(err) //nolint:forbidigo // currently returns grpc errors
}
func TestGetOngoingSyncStateSummary(t *testing.T) {
require := require.New(t)
testKey := getOngoingSyncStateSummaryTestKey
// Create and start the plugin
vmImpl := buildClientHelper(require, testKey)
defer vmImpl.Runtime().Stop(context.Background())
// test unimplemented case; this is just a guard
_, err := vmImpl.GetOngoingSyncStateSummary(context.Background())
require.Equal(chain.ErrStateSyncableVMNotImplemented, err)
// test successful retrieval
summary, err := vmImpl.GetOngoingSyncStateSummary(context.Background())
require.NoError(err)
require.Equal(mockedSummary.ID(), summary.ID())
require.Equal(mockedSummary.Height(), summary.Height())
require.Equal(mockedSummary.Bytes(), summary.Bytes())
// test a non-special error.
_, err = vmImpl.GetOngoingSyncStateSummary(context.Background())
require.Error(err) //nolint:forbidigo // currently returns grpc errors
}
func TestGetLastStateSummary(t *testing.T) {
require := require.New(t)
testKey := getLastStateSummaryTestKey
// Create and start the plugin
vmImpl := buildClientHelper(require, testKey)
defer vmImpl.Runtime().Stop(context.Background())
// test unimplemented case; this is just a guard
_, err := vmImpl.GetLastStateSummary(context.Background())
require.Equal(chain.ErrStateSyncableVMNotImplemented, err)
// test successful retrieval
summary, err := vmImpl.GetLastStateSummary(context.Background())
require.NoError(err)
require.Equal(mockedSummary.ID(), summary.ID())
require.Equal(mockedSummary.Height(), summary.Height())
require.Equal(mockedSummary.Bytes(), summary.Bytes())
// test a non-special error.
_, err = vmImpl.GetLastStateSummary(context.Background())
require.Error(err) //nolint:forbidigo // currently returns grpc errors
}
func TestParseStateSummary(t *testing.T) {
require := require.New(t)
testKey := parseStateSummaryTestKey
// Create and start the plugin
vmImpl := buildClientHelper(require, testKey)
defer vmImpl.Runtime().Stop(context.Background())
// test unimplemented case; this is just a guard
_, err := vmImpl.ParseStateSummary(context.Background(), mockedSummary.Bytes())
require.Equal(chain.ErrStateSyncableVMNotImplemented, err)
// test successful parsing
summary, err := vmImpl.ParseStateSummary(context.Background(), mockedSummary.Bytes())
require.NoError(err)
require.Equal(mockedSummary.ID(), summary.ID())
require.Equal(mockedSummary.Height(), summary.Height())
require.Equal(mockedSummary.Bytes(), summary.Bytes())
// test parsing nil summary
_, err = vmImpl.ParseStateSummary(context.Background(), nil)
require.Error(err) //nolint:forbidigo // currently returns grpc errors
// test a non-special error.
_, err = vmImpl.ParseStateSummary(context.Background(), mockedSummary.Bytes())
require.Error(err) //nolint:forbidigo // currently returns grpc errors
}
func TestGetStateSummary(t *testing.T) {
require := require.New(t)
testKey := getStateSummaryTestKey
// Create and start the plugin
vmImpl := buildClientHelper(require, testKey)
defer vmImpl.Runtime().Stop(context.Background())
// test unimplemented case; this is just a guard
_, err := vmImpl.GetStateSummary(context.Background(), mockedSummary.Height())
require.Equal(chain.ErrStateSyncableVMNotImplemented, err)
// test successful retrieval
summary, err := vmImpl.GetStateSummary(context.Background(), mockedSummary.Height())
require.NoError(err)
require.Equal(mockedSummary.ID(), summary.ID())
require.Equal(mockedSummary.Height(), summary.Height())
require.Equal(mockedSummary.Bytes(), summary.Bytes())
// test a non-special error.
_, err = vmImpl.GetStateSummary(context.Background(), mockedSummary.Height())
require.Error(err) //nolint:forbidigo // currently returns grpc errors
}
func TestAcceptStateSummary(t *testing.T) {
require := require.New(t)
testKey := acceptStateSummaryTestKey
// Create and start the plugin
vmImpl := buildClientHelper(require, testKey)
defer vmImpl.Runtime().Stop(context.Background())
// retrieve the summary first
summary, err := vmImpl.GetStateSummary(context.Background(), mockedSummary.Height())
require.NoError(err)
// test status Summary
status, err := summary.Accept(context.Background())
require.NoError(err)
require.Equal(chain.StateSyncStatic, status)
// test skipped Summary
status, err = summary.Accept(context.Background())
require.NoError(err)
require.Equal(chain.StateSyncSkipped, status)
// test a non-special error.
_, err = summary.Accept(context.Background())
require.Error(err) //nolint:forbidigo // currently returns grpc errors
}
// Show that LastAccepted call returns the right answer after a StateSummary
// is accepted AND engine state moves to bootstrapping
func TestLastAcceptedBlockPostStateSummaryAccept(t *testing.T) {
t.Skip("Skipping due to mock expectation ordering issues with subprocess communication")
require := require.New(t)
testKey := lastAcceptedBlockPostStateSummaryAcceptTestKey
// Create and start the plugin
vmImpl := buildClientHelper(require, testKey)
defer vmImpl.Runtime().Stop(context.Background())
// Step 1: initialize VM and check initial LastAcceptedBlock
rt := &consensusruntime.Runtime{
NetworkID: 1,
ChainID: ids.ID{'C', 'C', 'h', 'a', 'i', 'n'},
NodeID: ids.GenerateTestNodeID(),
}
require.NoError(vmImpl.Initialize(context.Background(), vm.Init{
Runtime: rt,
DB: prefixdb.New([]byte{}, memdb.New()),
}))
blkID, err := vmImpl.LastAccepted(context.Background())
require.NoError(err)
require.Equal(preSummaryBlk.ID(), blkID)
lastBlk, err := vmImpl.GetBlock(context.Background(), blkID)
require.NoError(err)
require.Equal(preSummaryBlk.Height(), lastBlk.Height())
// Step 2: pick a state summary to an higher height and accept it
summary, err := vmImpl.ParseStateSummary(context.Background(), mockedSummary.Bytes())
require.NoError(err)
status, err := summary.Accept(context.Background())
require.NoError(err)
require.Equal(chain.StateSyncStatic, status)
// State Sync accept does not duly update LastAccepted block information
// since state sync can complete asynchronously
blkID, err = vmImpl.LastAccepted(context.Background())
require.NoError(err)
lastBlk, err = vmImpl.GetBlock(context.Background(), blkID)
require.NoError(err)
require.Equal(preSummaryBlk.Height(), lastBlk.Height())
// Setting state to bootstrapping duly update last accepted block
const Bootstrapping uint32 = 1
require.NoError(vmImpl.SetState(context.Background(), Bootstrapping))
blkID, err = vmImpl.LastAccepted(context.Background())
require.NoError(err)
lastBlk, err = vmImpl.GetBlock(context.Background(), blkID)
require.NoError(err)
require.Equal(summary.Height(), lastBlk.Height())
}
-61
View File
@@ -1,61 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpcchainvm
import "time"
// Transport specifies the RPC transport protocol for VM communication
type Transport string
const (
// TransportGRPC uses gRPC with protobuf serialization (default)
TransportGRPC Transport = "grpc"
// TransportZAP uses ZAP zero-copy serialization over TCP
TransportZAP Transport = "zap"
// TransportBoth enables both gRPC and ZAP transports
TransportBoth Transport = "both"
)
// TransportConfig contains transport configuration
type TransportConfig struct {
// Transport specifies which transport(s) to use
Transport Transport
// Timeout for transport operations
Timeout time.Duration
}
// DefaultTransportConfig returns the default transport configuration
// Uses ZAP by default for high-performance zero-copy communication.
// gRPC can be used with -tags=grpc build flag for compatibility testing.
func DefaultTransportConfig() TransportConfig {
return TransportConfig{
Transport: TransportZAP,
Timeout: 30 * time.Second,
}
}
// ValidTransports returns all valid transport values
func ValidTransports() []Transport {
return []Transport{TransportGRPC, TransportZAP, TransportBoth}
}
// IsValid returns true if the transport is a valid value
func (t Transport) IsValid() bool {
switch t {
case TransportGRPC, TransportZAP, TransportBoth:
return true
default:
return false
}
}
// UsesGRPC returns true if this transport configuration uses gRPC
func (t Transport) UsesGRPC() bool {
return t == TransportGRPC || t == TransportBoth
}
// UsesZAP returns true if this transport configuration uses ZAP
func (t Transport) UsesZAP() bool {
return t == TransportZAP || t == TransportBoth
}
-27
View File
@@ -1,27 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package rpcchainvm provides the RPC infrastructure for Chain VMs (linear blockchains).
// This package is a thin wrapper that re-exports the shared implementation from
// github.com/luxfi/vm/rpc/chain for backward compatibility.
package rpcchainvm
import (
"context"
enginechain "github.com/luxfi/vm/chain"
"github.com/luxfi/log"
rpcchain "github.com/luxfi/vm/rpc/chain"
"github.com/luxfi/vm/rpc/grpcutils"
)
// Serve starts the RPC Chain VM server and performs a handshake with the VM runtime service.
// The address of the Runtime server is expected to be passed via ENV `runtime.EngineAddressKey`.
// This address is used by the Runtime client to send Initialize RPC to server.
//
// This function delegates to the shared implementation in github.com/luxfi/vm/rpc/chain.
func Serve(ctx context.Context, log log.Logger, vm enginechain.ChainVM, opts ...grpcutils.ServerOption) error {
return rpcchain.Serve(ctx, log, vm, opts...)
}
-35
View File
@@ -1,35 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package rpcchainvm provides the RPC infrastructure for Chain VMs.
// This file provides backward-compatible type aliases to the shared implementation
// in github.com/luxfi/vm/rpc/chain.
package rpcchainvm
import (
"github.com/luxfi/log"
"github.com/luxfi/vm/api/metrics"
"github.com/luxfi/vm/rpc/chain"
"github.com/luxfi/vm/rpc/runtime"
"github.com/luxfi/resource"
"google.golang.org/grpc"
)
// VMClient is a type alias for backward compatibility.
// The actual implementation is in github.com/luxfi/vm/rpc/chain.Client.
type VMClient = chain.Client
// NewClient returns a VM connected to a remote VM.
// This delegates to the shared implementation in github.com/luxfi/vm/rpc/chain.
func NewClient(
clientConn *grpc.ClientConn,
runtime runtime.Stopper,
pid int,
processTracker resource.ProcessTracker,
metricsGatherer metrics.MultiGatherer,
logger log.Logger,
) *VMClient {
return chain.NewClient(clientConn, runtime, pid, processTracker, metricsGatherer, logger)
}
-25
View File
@@ -1,25 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package rpcchainvm provides the RPC infrastructure for Chain VMs.
// This file provides backward-compatible type aliases to the shared implementation
// in github.com/luxfi/vm/rpc/chain.
package rpcchainvm
import (
"github.com/luxfi/atomic"
enginechain "github.com/luxfi/vm/chain"
rpcchain "github.com/luxfi/vm/rpc/chain"
)
// VMServer is a type alias for backward compatibility.
// The actual implementation is in github.com/luxfi/vm/rpc/chain.Server.
type VMServer = rpcchain.Server
// NewServer creates a new VMServer for the given ChainVM.
// This delegates to the shared implementation in github.com/luxfi/vm/rpc/chain.
func NewServer(vm enginechain.ChainVM, allowShutdown *atomic.Atomic[bool]) *VMServer {
return rpcchain.NewServer(vm, allowShutdown)
}
-100
View File
@@ -1,100 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpcchainvm
import (
"context"
"fmt"
"os"
"os/exec"
"testing"
"github.com/luxfi/log"
"github.com/luxfi/vm/chain"
)
const (
chainVMTestKey = "chainVMTest"
stateSyncEnabledTestKey = "stateSyncEnabledTest"
getOngoingSyncStateSummaryTestKey = "getOngoingSyncStateSummaryTest"
getLastStateSummaryTestKey = "getLastStateSummaryTest"
parseStateSummaryTestKey = "parseStateSummaryTest"
getStateSummaryTestKey = "getStateSummaryTest"
acceptStateSummaryTestKey = "acceptStateSummaryTest"
lastAcceptedBlockPostStateSummaryAcceptTestKey = "lastAcceptedBlockPostStateSummaryAcceptTest"
contextTestKey = "contextTest"
batchedParseBlockCachingTestKey = "batchedParseBlockCachingTest"
)
var TestServerPluginMap = map[string]func(*testing.T, bool) chain.ChainVM{
stateSyncEnabledTestKey: stateSyncEnabledTestPlugin,
getOngoingSyncStateSummaryTestKey: getOngoingSyncStateSummaryTestPlugin,
getLastStateSummaryTestKey: getLastStateSummaryTestPlugin,
parseStateSummaryTestKey: parseStateSummaryTestPlugin,
getStateSummaryTestKey: getStateSummaryTestPlugin,
acceptStateSummaryTestKey: acceptStateSummaryTestPlugin,
lastAcceptedBlockPostStateSummaryAcceptTestKey: lastAcceptedBlockPostStateSummaryAcceptTestPlugin,
contextTestKey: contextEnabledTestPlugin,
batchedParseBlockCachingTestKey: batchedParseBlockCachingTestPlugin,
}
// helperProcess helps with creating the net binary for testing.
func helperProcess(s ...string) *exec.Cmd {
cs := []string{"-test.run=TestHelperProcess", "--"}
cs = append(cs, s...)
env := []string{
"TEST_PROCESS=1",
}
run := os.Args[0]
cmd := exec.Command(run, cs...)
env = append(env, os.Environ()...)
cmd.Env = env
return cmd
}
func TestHelperProcess(t *testing.T) {
if os.Getenv("TEST_PROCESS") != "1" {
return
}
args := os.Args
for len(args) > 0 {
if args[0] == "--" {
args = args[1:]
break
}
args = args[1:]
}
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "failed to receive testKey")
os.Exit(2)
}
testKey := args[0]
if testKey == "dummy" {
// block till killed
select {}
}
pluginFunc, ok := TestServerPluginMap[testKey]
if !ok {
fmt.Fprintf(os.Stderr, "test plugin not found for key: %s\n", testKey)
os.Exit(2)
}
mockedVM := pluginFunc(t, true /*loadExpectations*/)
if mockedVM == nil {
fmt.Fprintf(os.Stderr, "test plugin returned nil for key: %s\n", testKey)
os.Exit(2)
}
err := Serve(context.Background(), log.NewTestLogger(log.DebugLevel), mockedVM)
if err != nil {
fmt.Fprintf(os.Stderr, "Serve failed: %v\n", err)
os.Exit(1)
}
os.Exit(0)
}
-184
View File
@@ -1,184 +0,0 @@
//go:build grpc
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpcchainvm
import (
"context"
"io"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/luxfi/database/memdb"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/runtime"
"github.com/luxfi/vm"
"github.com/luxfi/vm/chain"
"github.com/luxfi/vm/chain/blockmock"
"github.com/luxfi/vm/chain/blocktest"
)
var (
testPreSummaryBlk = &blocktest.Block{
IDV: ids.ID{'f', 'i', 'r', 's', 't', 'B', 'l', 'K'},
HeightV: 1789,
ParentV: ids.ID{'p', 'a', 'r', 'e', 'n', 't', 'B', 'l', 'k'},
StatusV: blocktest.Accepted,
}
)
var (
blockContext = &runtime.Runtime{
PChainHeight: 1,
}
blkID = ids.ID{1}
parentID = ids.ID{0}
blkBytes = []byte{0}
)
type ContextEnabledVMMock struct {
chainVM *blockmock.MockChainVM
buildBlockContextVM *blockmock.MockBuildBlockWithRuntimeChainVM
}
// Ensure ContextEnabledVMMock implements the required interfaces
var (
_ chain.ChainVM = (*ContextEnabledVMMock)(nil)
_ chain.BuildBlockWithRuntimeChainVM = (*ContextEnabledVMMock)(nil)
)
// Forward ChainVM methods
func (m *ContextEnabledVMMock) Initialize(ctx context.Context, init vm.Init) error {
return m.chainVM.Initialize(ctx, init)
}
func (m *ContextEnabledVMMock) SetState(ctx context.Context, state uint32) error {
return m.chainVM.SetState(ctx, state)
}
func (m *ContextEnabledVMMock) Shutdown(ctx context.Context) error { return m.chainVM.Shutdown(ctx) }
func (m *ContextEnabledVMMock) Version(ctx context.Context) (string, error) {
return m.chainVM.Version(ctx)
}
func (m *ContextEnabledVMMock) NewHTTPHandler(ctx context.Context) (http.Handler, error) {
return m.chainVM.NewHTTPHandler(ctx)
}
func (m *ContextEnabledVMMock) Connected(ctx context.Context, nodeID ids.NodeID, nodeVersion *chain.VersionInfo) error {
return m.chainVM.Connected(ctx, nodeID, nodeVersion)
}
func (m *ContextEnabledVMMock) Disconnected(ctx context.Context, nodeID ids.NodeID) error {
return m.chainVM.Disconnected(ctx, nodeID)
}
func (m *ContextEnabledVMMock) HealthCheck(ctx context.Context) (chain.HealthResult, error) {
return m.chainVM.HealthCheck(ctx)
}
func (m *ContextEnabledVMMock) ParseBlock(ctx context.Context, bytes []byte) (chain.Block, error) {
return m.chainVM.ParseBlock(ctx, bytes)
}
func (m *ContextEnabledVMMock) GetBlock(ctx context.Context, id ids.ID) (chain.Block, error) {
return m.chainVM.GetBlock(ctx, id)
}
func (m *ContextEnabledVMMock) BuildBlock(ctx context.Context) (chain.Block, error) {
return m.chainVM.BuildBlock(ctx)
}
func (m *ContextEnabledVMMock) SetPreference(ctx context.Context, id ids.ID) error {
return m.chainVM.SetPreference(ctx, id)
}
func (m *ContextEnabledVMMock) LastAccepted(ctx context.Context) (ids.ID, error) {
return m.chainVM.LastAccepted(ctx)
}
func (m *ContextEnabledVMMock) GetBlockIDAtHeight(ctx context.Context, height uint64) (ids.ID, error) {
return m.chainVM.GetBlockIDAtHeight(ctx, height)
}
func (m *ContextEnabledVMMock) WaitForEvent(ctx context.Context) (vm.Message, error) {
return m.chainVM.WaitForEvent(ctx)
}
// Forward BuildBlockWithRuntimeVM method
func (m *ContextEnabledVMMock) BuildBlockWithRuntime(ctx context.Context, blockCtx *runtime.Runtime) (chain.Block, error) {
return m.buildBlockContextVM.BuildBlockWithRuntime(ctx, blockCtx)
}
type ContextEnabledBlockMock struct {
*blockmock.MockBlock
*blockmock.MockWithVerifyRuntime
}
func contextEnabledTestPlugin(t *testing.T, loadExpectations bool) chain.ChainVM {
// test key is "contextTestKey"
// create mock
ctrl := gomock.NewController(t)
ctxVM := &ContextEnabledVMMock{
chainVM: blockmock.NewMockChainVM(ctrl),
buildBlockContextVM: blockmock.NewMockBuildBlockWithRuntimeChainVM(ctrl),
}
if loadExpectations {
ctxBlock := &ContextEnabledBlockMock{
MockBlock: blockmock.NewMockBlock(ctrl),
MockWithVerifyRuntime: blockmock.NewMockWithVerifyRuntime(ctrl),
}
// Initialize expectations
ctxVM.chainVM.EXPECT().Initialize(
gomock.Any(), gomock.Any(),
).Return(nil).AnyTimes()
ctxVM.chainVM.EXPECT().LastAccepted(gomock.Any()).Return(testPreSummaryBlk.ID(), nil).AnyTimes()
ctxVM.chainVM.EXPECT().GetBlock(gomock.Any(), gomock.Any()).Return(testPreSummaryBlk, nil).AnyTimes()
// BuildBlockWithRuntime expectations
ctxVM.buildBlockContextVM.EXPECT().BuildBlockWithRuntime(gomock.Any(), gomock.Any()).Return(ctxBlock, nil).AnyTimes()
ctxBlock.MockWithVerifyRuntime.EXPECT().ShouldVerifyWithRuntime(gomock.Any()).Return(true, nil).AnyTimes()
ctxBlock.MockBlock.EXPECT().ID().Return(blkID).AnyTimes()
ctxBlock.MockBlock.EXPECT().ParentID().Return(parentID).AnyTimes()
ctxBlock.MockBlock.EXPECT().Parent().Return(parentID).AnyTimes()
ctxBlock.MockBlock.EXPECT().Bytes().Return(blkBytes).AnyTimes()
ctxBlock.MockBlock.EXPECT().Height().Return(uint64(1)).AnyTimes()
ctxBlock.MockBlock.EXPECT().Timestamp().Return(time.Now()).AnyTimes()
// VerifyWithRuntime expectations
ctxVM.chainVM.EXPECT().ParseBlock(gomock.Any(), blkBytes).Return(ctxBlock, nil).AnyTimes()
ctxBlock.MockWithVerifyRuntime.EXPECT().VerifyWithRuntime(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
}
return ctxVM
}
func TestContextVMSummary(t *testing.T) {
require := require.New(t)
testKey := contextTestKey
// Create and start the plugin
vmImpl := buildClientHelper(require, testKey)
defer vmImpl.Runtime().Stop(context.Background())
rt := &runtime.Runtime{
NetworkID: 1,
ChainID: ids.ID{'C', 'C', 'h', 'a', 'i', 'n'},
NodeID: ids.GenerateTestNodeID(),
Log: log.NewWriter(io.Discard),
}
require.NoError(vmImpl.Initialize(context.Background(), vm.Init{
Runtime: rt,
DB: memdb.New(),
}))
blkIntf, err := vmImpl.BuildBlockWithRuntime(context.Background(), blockContext)
require.NoError(err)
blk, ok := blkIntf.(chain.WithVerifyRuntime)
require.True(ok)
shouldVerify, err := blk.ShouldVerifyWithRuntime(context.Background())
require.NoError(err)
require.True(shouldVerify)
require.NoError(blk.VerifyWithRuntime(context.Background(), blockContext))
}