Files
node/db/rpcdb/zap_server_test.go
T
Hanzo AI 8efcdff444 rpcdb: decomplect into Layer A/B/C topology
Layer A — wire framing: github.com/luxfi/api/zap (unchanged)
Layer B — service spec: github.com/luxfi/proto/rpcdb (transport-agnostic
   data carriers, was node/proto/zap/rpcdb)
Layer C — service impl + transports: node/db/rpcdb/
   - service.go      transport-neutral Service wrapping database.Database
   - zap_server.go   ZAP transport adapter (default; used by cevm)
   - grpc_server.go  gRPC transport adapter (-tags=grpc)

One Service. Many transport adapters. Adding a transport is a new
adapter file wrapping *Service; storage logic stays in service.go.

Removed (-1390 LOC of duplicate/dead code):
 - node/proto/zap/rpcdb/rpcdb.go (moved to luxfi/proto/rpcdb)
 - node/proto/rpcdb/rpcdb_zap.go (dead HandlerRegistry path, no callers)
 - node/proto/rpcdb/rpcdb_grpc.go (duplicated gRPC server, replaced by
   node/db/rpcdb/grpc_server.go)
 - node/db/rpcdb/db.go (re-export shim, replaced by Service)

Consumers updated to import the canonical adapter at node/db/rpcdb:
 - vms/rpcchainvm/zap/client.go
 - vms/rpcchainvm/zap/client_dbserver_test.go
 - vms/rpcchainvm/zap/cevm_e2e_test.go

go.mod: add `replace github.com/luxfi/proto => ../proto` for in-tree
  development of the central wire-types module.

Tests: db/rpcdb (3/3) + rpcchainvm/zap (4/4 incl. cevm cross-process
e2e — KP_META written via ZAP db channel, no fallback to local zapdb).
2026-05-15 15:47:44 -07:00

276 lines
6.9 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpcdb
import (
"context"
"net"
"testing"
"time"
"github.com/stretchr/testify/require"
zapwire "github.com/luxfi/api/zap"
"github.com/luxfi/database"
"github.com/luxfi/database/memdb"
)
// startZAPServer spawns a ZAPServer over an in-memory listener bound to
// 127.0.0.1:0, returns the addr + the server (for shutdown).
func startZAPServer(t *testing.T, db database.Database) (string, *ZAPServer, context.CancelFunc) {
t.Helper()
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
s := NewZAPServer(db)
s.ListenOn(listener)
ctx, cancel := context.WithCancel(context.Background())
go func() {
_ = s.Serve(ctx)
}()
// Wait for Serve to fully install accept loop.
time.Sleep(50 * time.Millisecond)
return listener.Addr().String(), s, cancel
}
// TestZAPServer_HasGetPutDelete exercises the four primitive ops.
func TestZAPServer_HasGetPutDelete(t *testing.T) {
require := require.New(t)
mem := memdb.New()
addr, srv, cancel := startZAPServer(t, mem)
defer cancel()
defer srv.Close()
conn, err := zapwire.Dial(context.Background(), addr, nil)
require.NoError(err)
defer conn.Close()
ctx := context.Background()
// Has (key absent) → has=false, err=NotFound
{
buf := zapwire.GetBuffer()
buf.WriteBytes([]byte("foo"))
_, resp, err := conn.Call(ctx, MsgDBHas, buf.Bytes())
zapwire.PutBuffer(buf)
require.NoError(err)
r := zapwire.NewReader(resp)
hasB, _ := r.ReadUint8()
errB, _ := r.ReadUint8()
require.Equal(uint8(0), hasB)
require.Equal(dbErrUnspecified, errB) // memdb returns no err on Has-miss
}
// Put
{
buf := zapwire.GetBuffer()
buf.WriteBytes([]byte("foo"))
buf.WriteBytes([]byte("bar"))
_, resp, err := conn.Call(ctx, MsgDBPut, buf.Bytes())
zapwire.PutBuffer(buf)
require.NoError(err)
r := zapwire.NewReader(resp)
errB, _ := r.ReadUint8()
require.Equal(dbErrUnspecified, errB)
}
// Has (key present) → has=true
{
buf := zapwire.GetBuffer()
buf.WriteBytes([]byte("foo"))
_, resp, err := conn.Call(ctx, MsgDBHas, buf.Bytes())
zapwire.PutBuffer(buf)
require.NoError(err)
r := zapwire.NewReader(resp)
hasB, _ := r.ReadUint8()
errB, _ := r.ReadUint8()
require.Equal(uint8(1), hasB)
require.Equal(dbErrUnspecified, errB)
}
// Get
{
buf := zapwire.GetBuffer()
buf.WriteBytes([]byte("foo"))
_, resp, err := conn.Call(ctx, MsgDBGet, buf.Bytes())
zapwire.PutBuffer(buf)
require.NoError(err)
r := zapwire.NewReader(resp)
val, _ := r.ReadBytes()
errB, _ := r.ReadUint8()
require.Equal([]byte("bar"), val)
require.Equal(dbErrUnspecified, errB)
}
// Get (missing) → empty value, err=NotFound
{
buf := zapwire.GetBuffer()
buf.WriteBytes([]byte("absent"))
_, resp, err := conn.Call(ctx, MsgDBGet, buf.Bytes())
zapwire.PutBuffer(buf)
require.NoError(err)
r := zapwire.NewReader(resp)
val, _ := r.ReadBytes()
errB, _ := r.ReadUint8()
require.Empty(val)
require.Equal(dbErrNotFound, errB)
}
// Delete
{
buf := zapwire.GetBuffer()
buf.WriteBytes([]byte("foo"))
_, resp, err := conn.Call(ctx, MsgDBDelete, buf.Bytes())
zapwire.PutBuffer(buf)
require.NoError(err)
r := zapwire.NewReader(resp)
errB, _ := r.ReadUint8()
require.Equal(dbErrUnspecified, errB)
}
// Get post-delete → NotFound
{
buf := zapwire.GetBuffer()
buf.WriteBytes([]byte("foo"))
_, resp, err := conn.Call(ctx, MsgDBGet, buf.Bytes())
zapwire.PutBuffer(buf)
require.NoError(err)
r := zapwire.NewReader(resp)
_, _ = r.ReadBytes()
errB, _ := r.ReadUint8()
require.Equal(dbErrNotFound, errB)
}
}
// TestZAPServer_WriteBatch_AndIterate covers batched writes plus the
// iterator path used by cevm's load_persisted snapshot_with_prefix.
func TestZAPServer_WriteBatch_AndIterate(t *testing.T) {
require := require.New(t)
mem := memdb.New()
addr, srv, cancel := startZAPServer(t, mem)
defer cancel()
defer srv.Close()
conn, err := zapwire.Dial(context.Background(), addr, nil)
require.NoError(err)
defer conn.Close()
ctx := context.Background()
// WriteBatch: 3 puts (prefix p:), 1 delete (no-op since absent).
{
buf := zapwire.GetBuffer()
buf.WriteUint32(3)
buf.WriteBytes([]byte("p:1"))
buf.WriteBytes([]byte("v1"))
buf.WriteBytes([]byte("p:2"))
buf.WriteBytes([]byte("v2"))
buf.WriteBytes([]byte("q:3"))
buf.WriteBytes([]byte("v3"))
buf.WriteUint32(1)
buf.WriteBytes([]byte("absent"))
_, resp, err := conn.Call(ctx, MsgDBWriteBatch, buf.Bytes())
zapwire.PutBuffer(buf)
require.NoError(err)
r := zapwire.NewReader(resp)
errB, _ := r.ReadUint8()
require.Equal(dbErrUnspecified, errB)
}
// IteratorNew with prefix "p:"
var iterID uint64
{
buf := zapwire.GetBuffer()
buf.WriteBytes(nil) // start
buf.WriteBytes([]byte("p:")) // prefix
_, resp, err := conn.Call(ctx, MsgDBIteratorNew, buf.Bytes())
zapwire.PutBuffer(buf)
require.NoError(err)
r := zapwire.NewReader(resp)
iterID, _ = r.ReadUint64()
}
// IteratorNext — collect.
collected := map[string]string{}
for {
buf := zapwire.GetBuffer()
buf.WriteUint64(iterID)
_, resp, err := conn.Call(ctx, MsgDBIteratorNext, buf.Bytes())
zapwire.PutBuffer(buf)
require.NoError(err)
r := zapwire.NewReader(resp)
n, _ := r.ReadUint32()
if n == 0 {
break
}
for i := uint32(0); i < n; i++ {
k, _ := r.ReadBytes()
v, _ := r.ReadBytes()
collected[string(k)] = string(v)
}
}
// IteratorRelease
{
buf := zapwire.GetBuffer()
buf.WriteUint64(iterID)
_, resp, err := conn.Call(ctx, MsgDBIteratorRelease, buf.Bytes())
zapwire.PutBuffer(buf)
require.NoError(err)
r := zapwire.NewReader(resp)
errB, _ := r.ReadUint8()
require.Equal(dbErrUnspecified, errB)
}
require.Equal(map[string]string{"p:1": "v1", "p:2": "v2"}, collected)
}
// TestZAPServer_ParityWithMemDB writes/reads via wire and via direct
// memdb Get to confirm both paths see the same state — proves the ZAP
// round-trip preserves bytes exactly.
func TestZAPServer_ParityWithMemDB(t *testing.T) {
require := require.New(t)
mem := memdb.New()
addr, srv, cancel := startZAPServer(t, mem)
defer cancel()
defer srv.Close()
conn, err := zapwire.Dial(context.Background(), addr, nil)
require.NoError(err)
defer conn.Close()
ctx := context.Background()
key := []byte{0x42, 0x00, 0x01, 0xff}
value := make([]byte, 256)
for i := range value {
value[i] = byte(i)
}
// Put via wire.
buf := zapwire.GetBuffer()
buf.WriteBytes(key)
buf.WriteBytes(value)
_, _, err = conn.Call(ctx, MsgDBPut, buf.Bytes())
zapwire.PutBuffer(buf)
require.NoError(err)
// Read directly.
got, dErr := mem.Get(key)
require.NoError(dErr)
require.Equal(value, got)
// Read via wire.
buf2 := zapwire.GetBuffer()
buf2.WriteBytes(key)
_, resp, err := conn.Call(ctx, MsgDBGet, buf2.Bytes())
zapwire.PutBuffer(buf2)
require.NoError(err)
r := zapwire.NewReader(resp)
wireVal, _ := r.ReadBytes()
require.Equal(value, wireVal)
}