mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
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:
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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...)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user