diff --git a/LLM.md b/LLM.md index e3a20ad3e..af5dbd6db 100644 --- a/LLM.md +++ b/LLM.md @@ -8,7 +8,7 @@ Lux blockchain node implementation - a high-performance, multi-chain blockchain **Key Context:** - Original Lux Network node — NOT a fork -- Latest Tag: v1.26.12 +- Latest Tag: v1.26.31 - Network ID: 96369 (Lux Mainnet), 96368 (Testnet), 96370 (Devnet) - Go Version: 1.26.1+ - Database: ZapDB (primary, default) @@ -274,31 +274,33 @@ Located in `vms/thresholdvm/fhe/`: | Gateway | `0x0200000000000000000000000000000000000083` | ### ZAP Transport (Zero-Copy App Proto) -ZAP is the default high-performance binary wire protocol for VM<->Node communication. -gRPC support is available via build tag for testing/compatibility. +ZAP is the only wire protocol for VM<->Node communication. The gRPC +fallback (and its `-tags=grpc` opt-in) was retired in v1.26.31 along +with every `//go:build grpc` file under `node/`. There is one and only +one way to talk to a Chain VM: ZAP. -**Build Tags:** +**Build:** ```bash -go build # ZAP only (default, production) -go build -tags=grpc # gRPC support (for testing/compatibility) +go build # ZAP only — there are no build tags ``` **Key Packages:** -- `github.com/luxfi/api/zap` - Core wire protocol and message types (Layer A) -- `github.com/luxfi/proto/rpcdb` - rpcdb service spec / data carriers (Layer B) -- `github.com/luxfi/node/db/rpcdb` - rpcdb Service + ZAP/gRPC transport adapters (Layer C) -- `github.com/luxfi/vm/rpc/sender` - p2p.Sender over ZAP/gRPC -- `vms/rpcchainvm/sender/` - Node-side sender implementation -- `vms/platformvm/warp/zwarp/` - ZAP-based warp signing client/server +- `github.com/luxfi/api/zap` — Core wire protocol and message types (Layer A) +- `github.com/luxfi/protocol/rpcdb` — rpcdb service spec / data carriers (Layer B) +- `github.com/luxfi/node/db/rpcdb` — rpcdb Service + ZAP transport adapter (Layer C) +- `vms/rpcchainvm/sender/` — Node-side `p2p.Sender` over ZAP +- `vms/rpcchainvm/zap/` — ChainVM client/server over ZAP +- `vms/platformvm/warp/zwarp/` — Warp signing over ZAP -**rpcdb Layered Topology (post-2026-05 reorg):** -- Layer A — wire framing: `github.com/luxfi/api/zap` (independent module) -- Layer B — rpcdb service spec: `github.com/luxfi/proto/rpcdb` (transport-agnostic data carriers) -- Layer C — rpcdb impl: `node/db/rpcdb/{service.go, grpc_server.go, zap_server.go}` +**rpcdb Layered Topology:** +- Layer A — wire framing: `github.com/luxfi/api/zap` +- Layer B — rpcdb service spec: `github.com/luxfi/protocol/rpcdb` +- Layer C — rpcdb impl: `node/db/rpcdb/{service.go, zap_server.go}` - `service.go` — transport-neutral `Service` wrapping `database.Database` - - `zap_server.go` (default) — ZAP transport adapter (used by cevm) - - `grpc_server.go` (`-tags=grpc`) — gRPC transport adapter -- One Service, many transport adapters. Adding a transport = new file wrapping `*Service`. + - `zap_server.go` — ZAP transport adapter (only adapter) +- One Service, one transport. The dual-adapter pattern stays available + for future transports (each is a new file wrapping `*Service`), but + ZAP is the only one shipping. **Wire Protocol Format:** ``` @@ -313,11 +315,8 @@ go build -tags=grpc # gRPC support (for testing/compatibility) **Sender Usage:** ```go -// ZAP transport (default) +// ZAP transport — the only transport s := sender.ZAP(zapConn) - -// gRPC transport (requires -tags=grpc build) -s := sender.GRPC(senderpb.NewSenderClient(grpcConn)) ``` **Warp over ZAP:** @@ -459,7 +458,7 @@ go test -v -run "TestHybrid" ./node/network/dialer/... -count=1 ### 1. P2P Sender Interface Node's rpcchainvm implements `p2p.Sender` (from `github.com/luxfi/p2p`) for cross-chain messaging. -The `sender` package is a gRPC implementation of `p2p.Sender`. +The `sender` package is the ZAP-native implementation of `p2p.Sender`. ### 2. Chain Tracking Nodes don't automatically track chains. Use: diff --git a/connectproto/buf.gen.yaml b/connectproto/buf.gen.yaml deleted file mode 100644 index 4b2593a88..000000000 --- a/connectproto/buf.gen.yaml +++ /dev/null @@ -1,8 +0,0 @@ -version: v1 -plugins: - - name: go - out: pb - opt: paths=source_relative - - plugin: connect-go - out: pb - opt: paths=source_relative diff --git a/connectproto/buf.lock b/connectproto/buf.lock deleted file mode 100644 index b30b2eda1..000000000 --- a/connectproto/buf.lock +++ /dev/null @@ -1,7 +0,0 @@ -# Generated by buf. DO NOT EDIT. -version: v1 -deps: - - remote: buf.build - owner: metric - repository: client-model - commit: 1d56a02d481a412a83b3c4984eb90c2e diff --git a/connectproto/buf.yaml b/connectproto/buf.yaml deleted file mode 100644 index fadc84019..000000000 --- a/connectproto/buf.yaml +++ /dev/null @@ -1,27 +0,0 @@ -version: v1 -name: buf.build/luxfi/lux -build: - excludes: - # for golang we handle metric as a buf dep so we exclude it from generate, this proto - # file is required by languages such as rust. - - io/metric -breaking: - use: - - FILE -deps: - - buf.build/metric/client-model -lint: - use: - - STANDARD - except: - - SERVICE_SUFFIX # service requirement of +Service - - RPC_REQUEST_STANDARD_NAME # explicit +Request naming - - RPC_RESPONSE_STANDARD_NAME # explicit +Response naming - - PACKAGE_VERSION_SUFFIX # versioned naming .v1beta - # allows RPC requests or responses to be google.protobuf.Empty messages. This can be set if you - # want to allow messages to be void forever, that is they will never take any parameters. - rpc_allow_google_protobuf_empty_requests: true - rpc_allow_google_protobuf_empty_responses: true - # allows the same message type to be used for a single RPC's request and response type. - # TODO: this should not be tolerated and if it is only perscriptivly. - rpc_allow_same_request_response: true diff --git a/connectproto/pb/xsvm/service.pb.go b/connectproto/pb/xsvm/service.pb.go deleted file mode 100644 index 8d2d760ae..000000000 --- a/connectproto/pb/xsvm/service.pb.go +++ /dev/null @@ -1,288 +0,0 @@ -//go:build grpc - -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.35.1 -// protoc (unknown) -// source: xsvm/service.proto - -package xsvm - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type PingRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` -} - -func (x *PingRequest) Reset() { - *x = PingRequest{} - mi := &file_xsvm_service_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PingRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PingRequest) ProtoMessage() {} - -func (x *PingRequest) ProtoReflect() protoreflect.Message { - mi := &file_xsvm_service_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use PingRequest.ProtoReflect.Descriptor instead. -func (*PingRequest) Descriptor() ([]byte, []int) { - return file_xsvm_service_proto_rawDescGZIP(), []int{0} -} - -func (x *PingRequest) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -type PingReply struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` -} - -func (x *PingReply) Reset() { - *x = PingReply{} - mi := &file_xsvm_service_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PingReply) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PingReply) ProtoMessage() {} - -func (x *PingReply) ProtoReflect() protoreflect.Message { - mi := &file_xsvm_service_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use PingReply.ProtoReflect.Descriptor instead. -func (*PingReply) Descriptor() ([]byte, []int) { - return file_xsvm_service_proto_rawDescGZIP(), []int{1} -} - -func (x *PingReply) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -type StreamPingRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` -} - -func (x *StreamPingRequest) Reset() { - *x = StreamPingRequest{} - mi := &file_xsvm_service_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StreamPingRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StreamPingRequest) ProtoMessage() {} - -func (x *StreamPingRequest) ProtoReflect() protoreflect.Message { - mi := &file_xsvm_service_proto_msgTypes[2] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use StreamPingRequest.ProtoReflect.Descriptor instead. -func (*StreamPingRequest) Descriptor() ([]byte, []int) { - return file_xsvm_service_proto_rawDescGZIP(), []int{2} -} - -func (x *StreamPingRequest) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -type StreamPingReply struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` -} - -func (x *StreamPingReply) Reset() { - *x = StreamPingReply{} - mi := &file_xsvm_service_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StreamPingReply) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StreamPingReply) ProtoMessage() {} - -func (x *StreamPingReply) ProtoReflect() protoreflect.Message { - mi := &file_xsvm_service_proto_msgTypes[3] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use StreamPingReply.ProtoReflect.Descriptor instead. -func (*StreamPingReply) Descriptor() ([]byte, []int) { - return file_xsvm_service_proto_rawDescGZIP(), []int{3} -} - -func (x *StreamPingReply) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -var File_xsvm_service_proto protoreflect.FileDescriptor - -var file_xsvm_service_proto_rawDesc = []byte{ - 0x0a, 0x12, 0x78, 0x73, 0x76, 0x6d, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x78, 0x73, 0x76, 0x6d, 0x22, 0x27, 0x0a, 0x0b, 0x50, 0x69, - 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x22, 0x25, 0x0a, 0x09, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x70, 0x6c, 0x79, - 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x2d, 0x0a, 0x11, 0x53, 0x74, - 0x72, 0x65, 0x61, 0x6d, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x2b, 0x0a, 0x0f, 0x53, 0x74, 0x72, - 0x65, 0x61, 0x6d, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, - 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x74, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x2a, - 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x11, 0x2e, 0x78, 0x73, 0x76, 0x6d, 0x2e, 0x50, 0x69, - 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x78, 0x73, 0x76, 0x6d, - 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x40, 0x0a, 0x0a, 0x53, 0x74, - 0x72, 0x65, 0x61, 0x6d, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x17, 0x2e, 0x78, 0x73, 0x76, 0x6d, 0x2e, - 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x15, 0x2e, 0x78, 0x73, 0x76, 0x6d, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, - 0x69, 0x6e, 0x67, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x28, 0x01, 0x30, 0x01, 0x42, 0x2c, 0x5a, 0x2a, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x75, 0x78, 0x66, 0x69, - 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2f, 0x70, 0x62, 0x2f, 0x78, 0x73, 0x76, 0x6d, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, -} - -var ( - file_xsvm_service_proto_rawDescOnce sync.Once - file_xsvm_service_proto_rawDescData = file_xsvm_service_proto_rawDesc -) - -func file_xsvm_service_proto_rawDescGZIP() []byte { - file_xsvm_service_proto_rawDescOnce.Do(func() { - file_xsvm_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_xsvm_service_proto_rawDescData) - }) - return file_xsvm_service_proto_rawDescData -} - -var file_xsvm_service_proto_msgTypes = make([]protoimpl.MessageInfo, 4) -var file_xsvm_service_proto_goTypes = []any{ - (*PingRequest)(nil), // 0: xsvm.PingRequest - (*PingReply)(nil), // 1: xsvm.PingReply - (*StreamPingRequest)(nil), // 2: xsvm.StreamPingRequest - (*StreamPingReply)(nil), // 3: xsvm.StreamPingReply -} -var file_xsvm_service_proto_depIdxs = []int32{ - 0, // 0: xsvm.Ping.Ping:input_type -> xsvm.PingRequest - 2, // 1: xsvm.Ping.StreamPing:input_type -> xsvm.StreamPingRequest - 1, // 2: xsvm.Ping.Ping:output_type -> xsvm.PingReply - 3, // 3: xsvm.Ping.StreamPing:output_type -> xsvm.StreamPingReply - 2, // [2:4] is the sub-list for method output_type - 0, // [0:2] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_xsvm_service_proto_init() } -func file_xsvm_service_proto_init() { - if File_xsvm_service_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_xsvm_service_proto_rawDesc, - NumEnums: 0, - NumMessages: 4, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_xsvm_service_proto_goTypes, - DependencyIndexes: file_xsvm_service_proto_depIdxs, - MessageInfos: file_xsvm_service_proto_msgTypes, - }.Build() - File_xsvm_service_proto = out.File - file_xsvm_service_proto_rawDesc = nil - file_xsvm_service_proto_goTypes = nil - file_xsvm_service_proto_depIdxs = nil -} diff --git a/connectproto/pb/xsvm/xsvmconnect/service.connect.go b/connectproto/pb/xsvm/xsvmconnect/service.connect.go deleted file mode 100644 index d6b60b20d..000000000 --- a/connectproto/pb/xsvm/xsvmconnect/service.connect.go +++ /dev/null @@ -1,138 +0,0 @@ -//go:build grpc - -// Code generated by protoc-gen-connect-go. DO NOT EDIT. -// -// Source: xsvm/service.proto - -package xsvmconnect - -import ( - connect "connectrpc.com/connect" - context "context" - errors "errors" - xsvm "github.com/luxfi/node/connectproto/pb/xsvm" - http "net/http" - strings "strings" -) - -// This is a compile-time assertion to ensure that this generated file and the connect package are -// compatible. If you get a compiler error that this constant is not defined, this code was -// generated with a version of connect newer than the one compiled into your binary. You can fix the -// problem by either regenerating this code with an older version of connect or updating the connect -// version compiled into your binary. -const _ = connect.IsAtLeastVersion1_13_0 - -const ( - // PingName is the fully-qualified name of the Ping service. - PingName = "xsvm.Ping" -) - -// These constants are the fully-qualified names of the RPCs defined in this package. They're -// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. -// -// Note that these are different from the fully-qualified method names used by -// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to -// reflection-formatted method names, remove the leading slash and convert the remaining slash to a -// period. -const ( - // PingPingProcedure is the fully-qualified name of the Ping's Ping RPC. - PingPingProcedure = "/xsvm.Ping/Ping" - // PingStreamPingProcedure is the fully-qualified name of the Ping's StreamPing RPC. - PingStreamPingProcedure = "/xsvm.Ping/StreamPing" -) - -// PingClient is a client for the xsvm.Ping service. -type PingClient interface { - Ping(context.Context, *connect.Request[xsvm.PingRequest]) (*connect.Response[xsvm.PingReply], error) - StreamPing(context.Context) *connect.BidiStreamForClient[xsvm.StreamPingRequest, xsvm.StreamPingReply] -} - -// NewPingClient constructs a client for the xsvm.Ping service. By default, it uses the Connect -// protocol with the binary Protobuf Codec, asks for gzipped responses, and sends uncompressed -// requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or -// connect.WithGRPCWeb() options. -// -// The URL supplied here should be the base URL for the Connect or gRPC server (for example, -// http://api.acme.com or https://acme.com/grpc). -func NewPingClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) PingClient { - baseURL = strings.TrimRight(baseURL, "/") - pingMethods := xsvm.File_xsvm_service_proto.Services().ByName("Ping").Methods() - return &pingClient{ - ping: connect.NewClient[xsvm.PingRequest, xsvm.PingReply]( - httpClient, - baseURL+PingPingProcedure, - connect.WithSchema(pingMethods.ByName("Ping")), - connect.WithClientOptions(opts...), - ), - streamPing: connect.NewClient[xsvm.StreamPingRequest, xsvm.StreamPingReply]( - httpClient, - baseURL+PingStreamPingProcedure, - connect.WithSchema(pingMethods.ByName("StreamPing")), - connect.WithClientOptions(opts...), - ), - } -} - -// pingClient implements PingClient. -type pingClient struct { - ping *connect.Client[xsvm.PingRequest, xsvm.PingReply] - streamPing *connect.Client[xsvm.StreamPingRequest, xsvm.StreamPingReply] -} - -// Ping calls xsvm.Ping.Ping. -func (c *pingClient) Ping(ctx context.Context, req *connect.Request[xsvm.PingRequest]) (*connect.Response[xsvm.PingReply], error) { - return c.ping.CallUnary(ctx, req) -} - -// StreamPing calls xsvm.Ping.StreamPing. -func (c *pingClient) StreamPing(ctx context.Context) *connect.BidiStreamForClient[xsvm.StreamPingRequest, xsvm.StreamPingReply] { - return c.streamPing.CallBidiStream(ctx) -} - -// PingHandler is an implementation of the xsvm.Ping service. -type PingHandler interface { - Ping(context.Context, *connect.Request[xsvm.PingRequest]) (*connect.Response[xsvm.PingReply], error) - StreamPing(context.Context, *connect.BidiStream[xsvm.StreamPingRequest, xsvm.StreamPingReply]) error -} - -// NewPingHandler builds an HTTP handler from the service implementation. It returns the path on -// which to mount the handler and the handler itself. -// -// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf -// and JSON codecs. They also support gzip compression. -func NewPingHandler(svc PingHandler, opts ...connect.HandlerOption) (string, http.Handler) { - pingMethods := xsvm.File_xsvm_service_proto.Services().ByName("Ping").Methods() - pingPingHandler := connect.NewUnaryHandler( - PingPingProcedure, - svc.Ping, - connect.WithSchema(pingMethods.ByName("Ping")), - connect.WithHandlerOptions(opts...), - ) - pingStreamPingHandler := connect.NewBidiStreamHandler( - PingStreamPingProcedure, - svc.StreamPing, - connect.WithSchema(pingMethods.ByName("StreamPing")), - connect.WithHandlerOptions(opts...), - ) - return "/xsvm.Ping/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case PingPingProcedure: - pingPingHandler.ServeHTTP(w, r) - case PingStreamPingProcedure: - pingStreamPingHandler.ServeHTTP(w, r) - default: - http.NotFound(w, r) - } - }) -} - -// UnimplementedPingHandler returns CodeUnimplemented from all methods. -type UnimplementedPingHandler struct{} - -func (UnimplementedPingHandler) Ping(context.Context, *connect.Request[xsvm.PingRequest]) (*connect.Response[xsvm.PingReply], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("xsvm.Ping.Ping is not implemented")) -} - -func (UnimplementedPingHandler) StreamPing(context.Context, *connect.BidiStream[xsvm.StreamPingRequest, xsvm.StreamPingReply]) error { - return connect.NewError(connect.CodeUnimplemented, errors.New("xsvm.Ping.StreamPing is not implemented")) -} diff --git a/connectproto/xsvm/service.proto b/connectproto/xsvm/service.proto deleted file mode 100644 index d1cdcecf9..000000000 --- a/connectproto/xsvm/service.proto +++ /dev/null @@ -1,26 +0,0 @@ -syntax = "proto3"; - -package xsvm; - -option go_package = "github.com/luxfi/node/connectproto/pb/xsvm"; - -service Ping { - rpc Ping(PingRequest) returns (PingReply); - rpc StreamPing(stream StreamPingRequest) returns (stream StreamPingReply); -} - -message PingRequest { - string message = 1; -} - -message PingReply { - string message = 1; -} - -message StreamPingRequest { - string message = 1; -} - -message StreamPingReply { - string message = 1; -} diff --git a/db/rpcdb/grpc_client.go b/db/rpcdb/grpc_client.go deleted file mode 100644 index ee1b2d402..000000000 --- a/db/rpcdb/grpc_client.go +++ /dev/null @@ -1,370 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package rpcdb - -import ( - "context" - "encoding/json" - "errors" - "io" - "sync" - - "google.golang.org/protobuf/types/known/emptypb" - - "github.com/luxfi/database" - "github.com/luxfi/math/set" - "github.com/luxfi/utils" - - rpcdbpb "github.com/luxfi/node/proto/pb/rpcdb" -) - -var ( - _ database.Database = (*GRPCClient)(nil) - _ database.Batch = (*grpcBatch)(nil) - _ database.Iterator = (*grpcIterator)(nil) -) - -// GRPCClient is a database.Database that talks to a GRPCServer over -// gRPC. Symmetric with GRPCServer — both are pure transport adapters -// against the Layer-B wire types in github.com/luxfi/protocol/rpcdb. -type GRPCClient struct { - client rpcdbpb.DatabaseClient - - closed utils.Atomic[bool] -} - -// NewGRPCClient wraps a gRPC client connection to a remote rpcdb -// service. -func NewGRPCClient(client rpcdbpb.DatabaseClient) *GRPCClient { - return &GRPCClient{client: client} -} - -// Has attempts to return if the database has a key with the provided value. -func (c *GRPCClient) Has(key []byte) (bool, error) { - resp, err := c.client.Has(context.Background(), &rpcdbpb.HasRequest{Key: key}) - if err != nil { - return false, err - } - return resp.Has, codeToErr(resp.Err) -} - -// Get attempts to return the value mapped to the key. -func (c *GRPCClient) Get(key []byte) ([]byte, error) { - resp, err := c.client.Get(context.Background(), &rpcdbpb.GetRequest{Key: key}) - if err != nil { - return nil, err - } - return resp.Value, codeToErr(resp.Err) -} - -// Put attempts to set the value this key maps to. -func (c *GRPCClient) Put(key, value []byte) error { - resp, err := c.client.Put(context.Background(), &rpcdbpb.PutRequest{ - Key: key, - Value: value, - }) - if err != nil { - return err - } - return codeToErr(resp.Err) -} - -// Delete attempts to remove any mapping from the key. -func (c *GRPCClient) Delete(key []byte) error { - resp, err := c.client.Delete(context.Background(), &rpcdbpb.DeleteRequest{Key: key}) - if err != nil { - return err - } - return codeToErr(resp.Err) -} - -// NewBatch returns a new batch. -func (c *GRPCClient) NewBatch() database.Batch { - return &grpcBatch{db: c} -} - -func (c *GRPCClient) NewIterator() database.Iterator { - return c.NewIteratorWithStartAndPrefix(nil, nil) -} - -func (c *GRPCClient) NewIteratorWithStart(start []byte) database.Iterator { - return c.NewIteratorWithStartAndPrefix(start, nil) -} - -func (c *GRPCClient) NewIteratorWithPrefix(prefix []byte) database.Iterator { - return c.NewIteratorWithStartAndPrefix(nil, prefix) -} - -// NewIteratorWithStartAndPrefix returns a new iterator. -func (c *GRPCClient) NewIteratorWithStartAndPrefix(start, prefix []byte) database.Iterator { - resp, err := c.client.NewIteratorWithStartAndPrefix( - context.Background(), - &rpcdbpb.NewIteratorWithStartAndPrefixRequest{Start: start, Prefix: prefix}, - ) - if err != nil { - return &database.IteratorError{Err: err} - } - return newGRPCIterator(c, resp.Id) -} - -// Compact attempts to optimize the space utilization in the provided range. -func (c *GRPCClient) Compact(start, limit []byte) error { - resp, err := c.client.Compact(context.Background(), &rpcdbpb.CompactRequest{ - Start: start, - Limit: limit, - }) - if err != nil { - return err - } - return codeToErr(resp.Err) -} - -// Close attempts to close the database. -func (c *GRPCClient) Close() error { - c.closed.Set(true) - resp, err := c.client.Close(context.Background(), &rpcdbpb.CloseRequest{}) - if err != nil { - return err - } - return codeToErr(resp.Err) -} - -// Sync — rpc database delegates sync to the underlying database on the -// server side. Operations are already synchronous over RPC; no-op here -// matches the legacy internal/database/rpcdb behavior. -func (c *GRPCClient) Sync() error { - return nil -} - -func (c *GRPCClient) HealthCheck(ctx context.Context) (interface{}, error) { - health, err := c.client.HealthCheck(ctx, &emptypb.Empty{}) - if err != nil { - return nil, err - } - return json.RawMessage(health.Details), nil -} - -// Backup is not supported over the gRPC database client. -func (c *GRPCClient) Backup(_ io.Writer, _ uint64) (uint64, error) { - return 0, errors.New("rpcdb: backup not supported") -} - -// Load is not supported over the gRPC database client. -func (c *GRPCClient) Load(_ io.Reader) error { - return errors.New("rpcdb: load not supported") -} - -// codeToErr maps a generated-pb Error enum back to a database sentinel. -// Lives next to the gRPC adapter so the adapter knows the inverse of -// toPbErr. Mirrors rpcdb.CodeToErr in service.go but for the -// protobuf-typed enum. -func codeToErr(code rpcdbpb.Error) error { - switch code { - case rpcdbpb.Error_ERROR_CLOSED: - return database.ErrClosed - case rpcdbpb.Error_ERROR_NOT_FOUND: - return database.ErrNotFound - default: - return nil - } -} - -type grpcBatch struct { - database.BatchOps - - db *GRPCClient -} - -func (b *grpcBatch) Write() error { - request := &rpcdbpb.WriteBatchRequest{} - keySet := set.NewSet[string](len(b.Ops)) - for i := len(b.Ops) - 1; i >= 0; i-- { - op := b.Ops[i] - key := string(op.Key) - if keySet.Contains(key) { - continue - } - keySet.Add(key) - - if op.Delete { - request.Deletes = append(request.Deletes, &rpcdbpb.DeleteRequest{Key: op.Key}) - } else { - request.Puts = append(request.Puts, &rpcdbpb.PutRequest{ - Key: op.Key, - Value: op.Value, - }) - } - } - - resp, err := b.db.client.WriteBatch(context.Background(), request) - if err != nil { - return err - } - return codeToErr(resp.Err) -} - -func (b *grpcBatch) Inner() database.Batch { - return b -} - -type grpcIterator struct { - db *GRPCClient - id uint64 - - data []*rpcdbpb.PutRequest - fetchedData chan []*rpcdbpb.PutRequest - - errLock sync.RWMutex - err error - - reqUpdateError chan chan struct{} - - once sync.Once - onClose chan struct{} - onClosed chan struct{} -} - -func newGRPCIterator(db *GRPCClient, id uint64) *grpcIterator { - it := &grpcIterator{ - db: db, - id: id, - fetchedData: make(chan []*rpcdbpb.PutRequest), - reqUpdateError: make(chan chan struct{}), - onClose: make(chan struct{}), - onClosed: make(chan struct{}), - } - go it.fetch() - return it -} - -// Invariant: fetch is the only thread with access to send requests to -// the server's iterator. This is needed because iterators are not -// thread safe and the server expects the client (us) to only ever -// issue one request at a time for a given iterator id. -func (it *grpcIterator) fetch() { - defer func() { - resp, err := it.db.client.IteratorRelease(context.Background(), &rpcdbpb.IteratorReleaseRequest{Id: it.id}) - if err != nil { - it.setError(err) - } else { - it.setError(codeToErr(resp.Err)) - } - - close(it.fetchedData) - close(it.onClosed) - }() - - for { - resp, err := it.db.client.IteratorNext(context.Background(), &rpcdbpb.IteratorNextRequest{Id: it.id}) - if err != nil { - it.setError(err) - return - } - - if len(resp.Data) == 0 { - return - } - - for { - select { - case it.fetchedData <- resp.Data: - case onUpdated := <-it.reqUpdateError: - it.updateError() - close(onUpdated) - continue - case <-it.onClose: - return - } - break - } - } -} - -// Next attempts to move the iterator to the next element. -func (it *grpcIterator) Next() bool { - if it.db.closed.Get() { - it.data = nil - it.setError(database.ErrClosed) - return false - } - if len(it.data) > 1 { - it.data[0] = nil - it.data = it.data[1:] - return true - } - - it.data = <-it.fetchedData - return len(it.data) > 0 -} - -// Error returns any error that occurred while iterating. -func (it *grpcIterator) Error() error { - if err := it.getError(); err != nil { - return err - } - - onUpdated := make(chan struct{}) - select { - case it.reqUpdateError <- onUpdated: - <-onUpdated - case <-it.onClosed: - } - - return it.getError() -} - -// Key returns the key of the current element. -func (it *grpcIterator) Key() []byte { - if len(it.data) == 0 { - return nil - } - return it.data[0].Key -} - -// Value returns the value of the current element. -func (it *grpcIterator) Value() []byte { - if len(it.data) == 0 { - return nil - } - return it.data[0].Value -} - -// Release frees any resources held by the iterator. -func (it *grpcIterator) Release() { - it.once.Do(func() { - close(it.onClose) - <-it.onClosed - }) -} - -func (it *grpcIterator) updateError() { - resp, err := it.db.client.IteratorError(context.Background(), &rpcdbpb.IteratorErrorRequest{Id: it.id}) - if err != nil { - it.setError(err) - } else { - it.setError(codeToErr(resp.Err)) - } -} - -func (it *grpcIterator) setError(err error) { - if err == nil { - return - } - - it.errLock.Lock() - defer it.errLock.Unlock() - - if it.err == nil { - it.err = err - } -} - -func (it *grpcIterator) getError() error { - it.errLock.RLock() - defer it.errLock.RUnlock() - - return it.err -} diff --git a/db/rpcdb/grpc_server.go b/db/rpcdb/grpc_server.go deleted file mode 100644 index 14a9cec7f..000000000 --- a/db/rpcdb/grpc_server.go +++ /dev/null @@ -1,159 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package rpcdb - -import ( - "context" - - "google.golang.org/protobuf/types/known/emptypb" - - "github.com/luxfi/database" - rpcdbpb "github.com/luxfi/node/proto/pb/rpcdb" - rpcdb "github.com/luxfi/protocol/rpcdb" -) - -// GRPCServer is the gRPC transport adapter for the rpcdb Service. It -// wraps *Service and translates gRPC's protobuf-generated request / -// response types into the transport-neutral wire types in -// github.com/luxfi/protocol/rpcdb. Pure adapter — no storage logic -// lives here. -type GRPCServer struct { - rpcdbpb.UnimplementedDatabaseServer - svc *Service -} - -// NewGRPCServer wraps a database.Database for serving over gRPC. -func NewGRPCServer(db database.Database) *GRPCServer { - return NewGRPCServerFromService(NewService(db)) -} - -// NewGRPCServerFromService wraps an existing Service for serving over -// gRPC. Symmetric with NewZAPServerFromService. -func NewGRPCServerFromService(svc *Service) *GRPCServer { - return &GRPCServer{svc: svc} -} - -// Compile-time interface check. -var _ rpcdbpb.DatabaseServer = (*GRPCServer)(nil) - -func toPbErr(code rpcdb.Error) rpcdbpb.Error { - switch code { - case rpcdb.Error_ERROR_NOT_FOUND: - return rpcdbpb.Error_ERROR_NOT_FOUND - case rpcdb.Error_ERROR_CLOSED: - return rpcdbpb.Error_ERROR_CLOSED - default: - return rpcdbpb.Error_ERROR_UNSPECIFIED - } -} - -func (g *GRPCServer) Has(ctx context.Context, req *rpcdbpb.HasRequest) (*rpcdbpb.HasResponse, error) { - resp, err := g.svc.Has(ctx, &rpcdb.HasRequest{Key: req.Key}) - if err != nil { - return nil, err - } - return &rpcdbpb.HasResponse{Has: resp.Has, Err: toPbErr(resp.Err)}, nil -} - -func (g *GRPCServer) Get(ctx context.Context, req *rpcdbpb.GetRequest) (*rpcdbpb.GetResponse, error) { - resp, err := g.svc.Get(ctx, &rpcdb.GetRequest{Key: req.Key}) - if err != nil { - return nil, err - } - return &rpcdbpb.GetResponse{Value: resp.Value, Err: toPbErr(resp.Err)}, nil -} - -func (g *GRPCServer) Put(ctx context.Context, req *rpcdbpb.PutRequest) (*rpcdbpb.PutResponse, error) { - resp, err := g.svc.Put(ctx, &rpcdb.PutRequest{Key: req.Key, Value: req.Value}) - if err != nil { - return nil, err - } - return &rpcdbpb.PutResponse{Err: toPbErr(resp.Err)}, nil -} - -func (g *GRPCServer) Delete(ctx context.Context, req *rpcdbpb.DeleteRequest) (*rpcdbpb.DeleteResponse, error) { - resp, err := g.svc.Delete(ctx, &rpcdb.DeleteRequest{Key: req.Key}) - if err != nil { - return nil, err - } - return &rpcdbpb.DeleteResponse{Err: toPbErr(resp.Err)}, nil -} - -func (g *GRPCServer) WriteBatch(ctx context.Context, req *rpcdbpb.WriteBatchRequest) (*rpcdbpb.WriteBatchResponse, error) { - puts := make([]*rpcdb.PutRequest, len(req.Puts)) - for i, p := range req.Puts { - puts[i] = &rpcdb.PutRequest{Key: p.Key, Value: p.Value} - } - dels := make([]*rpcdb.DeleteRequest, len(req.Deletes)) - for i, d := range req.Deletes { - dels[i] = &rpcdb.DeleteRequest{Key: d.Key} - } - resp, err := g.svc.WriteBatch(ctx, &rpcdb.WriteBatchRequest{Puts: puts, Deletes: dels}) - if err != nil { - return nil, err - } - return &rpcdbpb.WriteBatchResponse{Err: toPbErr(resp.Err)}, nil -} - -func (g *GRPCServer) Compact(ctx context.Context, req *rpcdbpb.CompactRequest) (*rpcdbpb.CompactResponse, error) { - resp, err := g.svc.Compact(ctx, &rpcdb.CompactRequest{Start: req.Start, Limit: req.Limit}) - if err != nil { - return nil, err - } - return &rpcdbpb.CompactResponse{Err: toPbErr(resp.Err)}, nil -} - -func (g *GRPCServer) Close(ctx context.Context, _ *rpcdbpb.CloseRequest) (*rpcdbpb.CloseResponse, error) { - resp, err := g.svc.Close(ctx, &rpcdb.CloseRequest{}) - if err != nil { - return nil, err - } - return &rpcdbpb.CloseResponse{Err: toPbErr(resp.Err)}, nil -} - -func (g *GRPCServer) HealthCheck(ctx context.Context, _ *emptypb.Empty) (*rpcdbpb.HealthCheckResponse, error) { - resp, err := g.svc.HealthCheck(ctx) - if err != nil { - return nil, err - } - return &rpcdbpb.HealthCheckResponse{Details: resp.Details}, nil -} - -func (g *GRPCServer) NewIteratorWithStartAndPrefix(ctx context.Context, req *rpcdbpb.NewIteratorWithStartAndPrefixRequest) (*rpcdbpb.NewIteratorWithStartAndPrefixResponse, error) { - resp, err := g.svc.NewIteratorWithStartAndPrefix(ctx, &rpcdb.NewIteratorWithStartAndPrefixRequest{Start: req.Start, Prefix: req.Prefix}) - if err != nil { - return nil, err - } - return &rpcdbpb.NewIteratorWithStartAndPrefixResponse{Id: resp.Id}, nil -} - -func (g *GRPCServer) IteratorNext(ctx context.Context, req *rpcdbpb.IteratorNextRequest) (*rpcdbpb.IteratorNextResponse, error) { - resp, err := g.svc.IteratorNext(ctx, &rpcdb.IteratorNextRequest{Id: req.Id}) - if err != nil { - return nil, err - } - data := make([]*rpcdbpb.PutRequest, len(resp.Data)) - for i, e := range resp.Data { - data[i] = &rpcdbpb.PutRequest{Key: e.Key, Value: e.Value} - } - return &rpcdbpb.IteratorNextResponse{Data: data}, nil -} - -func (g *GRPCServer) IteratorError(ctx context.Context, req *rpcdbpb.IteratorErrorRequest) (*rpcdbpb.IteratorErrorResponse, error) { - resp, err := g.svc.IteratorError(ctx, &rpcdb.IteratorErrorRequest{Id: req.Id}) - if err != nil { - return nil, err - } - return &rpcdbpb.IteratorErrorResponse{Err: toPbErr(resp.Err)}, nil -} - -func (g *GRPCServer) IteratorRelease(ctx context.Context, req *rpcdbpb.IteratorReleaseRequest) (*rpcdbpb.IteratorReleaseResponse, error) { - resp, err := g.svc.IteratorRelease(ctx, &rpcdb.IteratorReleaseRequest{Id: req.Id}) - if err != nil { - return nil, err - } - return &rpcdbpb.IteratorReleaseResponse{Err: toPbErr(resp.Err)}, nil -} diff --git a/db/rpcdb/grpc_test.go b/db/rpcdb/grpc_test.go deleted file mode 100644 index 83e49488a..000000000 --- a/db/rpcdb/grpc_test.go +++ /dev/null @@ -1,139 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package rpcdb - -import ( - "context" - "fmt" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/luxfi/database/corruptabledb" - "github.com/luxfi/database/dbtest" - "github.com/luxfi/database/memdb" - "github.com/luxfi/log" - "github.com/luxfi/vm/rpc/grpcutils" - - rpcdbpb "github.com/luxfi/node/proto/pb/rpcdb" -) - -type testGRPCDatabase struct { - client *GRPCClient - server *memdb.Database -} - -func setupGRPCDB(t testing.TB) *testGRPCDatabase { - require := require.New(t) - - db := &testGRPCDatabase{ - server: memdb.New(), - } - - listener, err := grpcutils.NewListener() - require.NoError(err) - serverCloser := grpcutils.ServerCloser{} - - server := grpcutils.NewServer() - rpcdbpb.RegisterDatabaseServer(server, NewGRPCServer(db.server)) - serverCloser.Add(server) - - go grpcutils.Serve(listener, server) - - conn, err := grpcutils.Dial(listener.Addr().String()) - require.NoError(err) - - db.client = NewGRPCClient(rpcdbpb.NewDatabaseClient(conn)) - - t.Cleanup(func() { - serverCloser.Stop() - _ = conn.Close() - _ = listener.Close() - }) - - return db -} - -func TestGRPCInterface(t *testing.T) { - for name, test := range dbtest.Tests { - t.Run(name, func(t *testing.T) { - db := setupGRPCDB(t) - test(t, db.client) - }) - } -} - -func FuzzGRPCKeyValue(f *testing.F) { - db := setupGRPCDB(f) - dbtest.FuzzKeyValue(f, db.client) -} - -func FuzzGRPCNewIteratorWithPrefix(f *testing.F) { - db := setupGRPCDB(f) - dbtest.FuzzNewIteratorWithPrefix(f, db.client) -} - -func FuzzGRPCNewIteratorWithStartAndPrefix(f *testing.F) { - db := setupGRPCDB(f) - dbtest.FuzzNewIteratorWithStartAndPrefix(f, db.client) -} - -func BenchmarkGRPCInterface(b *testing.B) { - for _, size := range dbtest.BenchmarkSizes { - keys, values := dbtest.SetupBenchmark(b, size[0], size[1], size[2]) - for name, bench := range dbtest.Benchmarks { - b.Run(fmt.Sprintf("rpcdb_%d_pairs_%d_keys_%d_values_%s", size[0], size[1], size[2], name), func(b *testing.B) { - db := setupGRPCDB(b) - bench(b, db.client, keys, values) - }) - } - } -} - -func TestGRPCHealthCheck(t *testing.T) { - scenarios := []struct { - name string - testFn func(db *corruptabledb.Database) error - wantErr bool - wantErrMsg string - }{ - { - name: "healthcheck success", - testFn: func(_ *corruptabledb.Database) error { - return nil - }, - }, - { - name: "healthcheck failed db closed", - testFn: func(db *corruptabledb.Database) error { - return db.Close() - }, - wantErr: true, - wantErrMsg: "closed", - }, - } - for _, scenario := range scenarios { - t.Run(scenario.name, func(t *testing.T) { - require := require.New(t) - - baseDB := setupGRPCDB(t) - db := corruptabledb.New(baseDB.server, log.NoLog{}) - defer db.Close() - require.NoError(scenario.testFn(db)) - - _, err := db.HealthCheck(context.Background()) - if scenario.wantErr { - require.Error(err) //nolint:forbidigo - require.Contains(err.Error(), scenario.wantErrMsg) - return - } - require.NoError(err) - - _, err = baseDB.client.HealthCheck(context.Background()) - require.NoError(err) - }) - } -} diff --git a/go.mod b/go.mod index 2113912e4..5de559b3f 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,6 @@ go 1.26.3 exclude github.com/luxfi/geth v1.16.1 require ( - connectrpc.com/connect v1.19.1 github.com/DataDog/zstd v1.5.7 // indirect github.com/StephenButtolph/canoto v0.17.3 github.com/btcsuite/btcd/btcutil v1.1.6 @@ -52,11 +51,11 @@ require ( github.com/supranational/blst v0.3.16 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect github.com/thepudds/fzgen v0.4.3 - go.opentelemetry.io/otel v1.42.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 - go.opentelemetry.io/otel/sdk v1.42.0 - go.opentelemetry.io/otel/trace v1.42.0 + go.opentelemetry.io/otel v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 // indirect + go.opentelemetry.io/otel/sdk v1.42.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect go.uber.org/goleak v1.3.0 go.uber.org/mock v0.6.0 golang.org/x/crypto v0.50.0 @@ -69,7 +68,7 @@ require ( 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 // indirect - google.golang.org/protobuf v1.36.11 + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) @@ -120,7 +119,6 @@ require ( ) require ( - connectrpc.com/grpcreflect v1.3.0 github.com/cloudflare/circl v1.6.3 github.com/consensys/gnark-crypto v0.20.1 github.com/golang-jwt/jwt/v4 v4.5.2 @@ -154,7 +152,6 @@ require ( github.com/luxfi/warp v1.18.6 github.com/luxfi/zwing v0.5.2 github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 go.uber.org/zap v1.27.1 ) @@ -171,7 +168,6 @@ require ( github.com/klauspost/crc32 v1.3.0 // indirect github.com/luxfi/age v1.5.0 // indirect github.com/luxfi/ai v0.1.0 // indirect - github.com/luxfi/bft v0.1.5 // indirect github.com/luxfi/corona v0.3.0 // indirect github.com/luxfi/crypto/ipa v1.2.4 // indirect github.com/luxfi/lens v0.1.3 // indirect @@ -191,6 +187,7 @@ require ( github.com/philhofer/fwd v1.2.0 // indirect github.com/rs/xid v1.6.0 // indirect github.com/tinylib/msgp v1.6.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect ) require ( diff --git a/go.sum b/go.sum index 1ece69dc2..bf7e28081 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,5 @@ c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAwLGRZ+sebTUruuM4Ra3M= c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo= -connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14= -connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= -connectrpc.com/grpcreflect v1.3.0 h1:Y4V+ACf8/vOb1XOc251Qun7jMB75gCUNw6llvB9csXc= -connectrpc.com/grpcreflect v1.3.0/go.mod h1:nfloOtCS8VUQOQ1+GTdFzVg2CJo4ZGaat8JIovCtDYs= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A= @@ -203,8 +199,6 @@ github.com/gorilla/rpc v1.2.1/go.mod h1:uNpOihAlF5xRFLuTYhfR0yfCTm0WTQSQttkMSptR github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is= @@ -266,8 +260,6 @@ github.com/luxfi/api v1.0.11 h1:t4fzN9Ox/Vy5msW2WpSXq4xCAmBXXJS0oM+zIjM9IiQ= github.com/luxfi/api v1.0.11/go.mod h1:q3Hh3mmkvp3cnv3aqe2+gCtmmSAL/uFjuzrqGavQLNA= github.com/luxfi/atomic v1.0.0 h1:xUV60MuzRvXngaQ1sM0yVC2v4TRoLlUGkkH7M9PS4yw= github.com/luxfi/atomic v1.0.0/go.mod h1:0G2mTlQ6TXWHICUHrUUPu1/qAiIyR4gSZ2tva9ci/bI= -github.com/luxfi/bft v0.1.5 h1:5xVLPkog4e5LTgaVlb9pgxA0EWE6tkrKwHPZVRz+RZw= -github.com/luxfi/bft v0.1.5/go.mod h1:5I8Ft8yA69xZlDe3RB0i4MgbqFKLZe65o/sha8JuKvU= github.com/luxfi/cache v1.2.1 h1:kAzOS55/hmYeNKR+0HAKv4ma48Y6JjkI8UQeqdZ8bfI= github.com/luxfi/cache v1.2.1/go.mod h1:co7JTxZZHpKT31Yh01LFp5aZOxmoUg157FhBLQdQHVU= github.com/luxfi/chains v1.2.1 h1:eeqekwYV8E1OTIGFwzKBcI+9JGNzm3pfg+56qUn3BTQ= diff --git a/internal/ids/rpcaliasreader/alias_reader_client.go b/internal/ids/rpcaliasreader/alias_reader_client.go deleted file mode 100644 index ac6758637..000000000 --- a/internal/ids/rpcaliasreader/alias_reader_client.go +++ /dev/null @@ -1,57 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package rpcaliasreader - -import ( - "context" - - "github.com/luxfi/ids" - - aliasreaderpb "github.com/luxfi/node/proto/pb/aliasreader" -) - -var _ ids.AliaserReader = (*Client)(nil) - -// Client implements alias lookups that talk over RPC. -type Client struct { - client aliasreaderpb.AliasReaderClient -} - -// NewClient returns an alias lookup instance connected to a remote alias lookup -// instance -func NewClient(client aliasreaderpb.AliasReaderClient) *Client { - return &Client{client: client} -} - -func (c *Client) Lookup(alias string) (ids.ID, error) { - resp, err := c.client.Lookup(context.Background(), &aliasreaderpb.Alias{ - Alias: alias, - }) - if err != nil { - return ids.Empty, err - } - return ids.ToID(resp.Id) -} - -func (c *Client) PrimaryAlias(id ids.ID) (string, error) { - resp, err := c.client.PrimaryAlias(context.Background(), &aliasreaderpb.ID{ - Id: id[:], - }) - if err != nil { - return "", err - } - return resp.Alias, nil -} - -func (c *Client) Aliases(id ids.ID) ([]string, error) { - resp, err := c.client.Aliases(context.Background(), &aliasreaderpb.ID{ - Id: id[:], - }) - if err != nil { - return nil, err - } - return resp.Aliases, nil -} diff --git a/internal/ids/rpcaliasreader/alias_reader_server.go b/internal/ids/rpcaliasreader/alias_reader_server.go deleted file mode 100644 index 2f87ae3f6..000000000 --- a/internal/ids/rpcaliasreader/alias_reader_server.go +++ /dev/null @@ -1,74 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package rpcaliasreader - -import ( - "context" - "fmt" - - "github.com/luxfi/ids" - - aliasreaderpb "github.com/luxfi/node/proto/pb/aliasreader" -) - -var _ aliasreaderpb.AliasReaderServer = (*Server)(nil) - -// Server enables alias lookups over RPC. -type Server struct { - aliasreaderpb.UnsafeAliasReaderServer - aliaser ids.AliaserReader -} - -// NewServer returns an alias lookup connected to a remote alias lookup -func NewServer(aliaser ids.AliaserReader) *Server { - return &Server{aliaser: aliaser} -} - -func (s *Server) Lookup( - _ context.Context, - req *aliasreaderpb.Alias, -) (*aliasreaderpb.ID, error) { - id, err := s.aliaser.Lookup(req.Alias) - if err != nil { - return nil, err - } - return &aliasreaderpb.ID{ - Id: id[:], - }, nil -} - -func (s *Server) PrimaryAlias( - _ context.Context, - req *aliasreaderpb.ID, -) (*aliasreaderpb.Alias, error) { - if s.aliaser == nil { - return nil, fmt.Errorf("aliaser is nil - BCLookup not configured for this chain") - } - - id, err := ids.ToID(req.Id) - if err != nil { - return nil, err - } - alias, err := s.aliaser.PrimaryAlias(id) - - return &aliasreaderpb.Alias{ - Alias: alias, - }, err -} - -func (s *Server) Aliases( - _ context.Context, - req *aliasreaderpb.ID, -) (*aliasreaderpb.AliasList, error) { - id, err := ids.ToID(req.Id) - if err != nil { - return nil, err - } - aliases, err := s.aliaser.Aliases(id) - return &aliasreaderpb.AliasList{ - Aliases: aliases, - }, err -} diff --git a/internal/ids/rpcaliasreader/alias_reader_test.go b/internal/ids/rpcaliasreader/alias_reader_test.go deleted file mode 100644 index ea8e53777..000000000 --- a/internal/ids/rpcaliasreader/alias_reader_test.go +++ /dev/null @@ -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 rpcaliasreader - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/luxfi/ids" - "github.com/luxfi/ids/idstest" - "github.com/luxfi/vm/rpc/grpcutils" - - aliasreaderpb "github.com/luxfi/node/proto/pb/aliasreader" -) - -func TestInterface(t *testing.T) { - for _, test := range idstest.AliasTests { - t.Run(test.Name, func(t *testing.T) { - require := require.New(t) - - listener, err := grpcutils.NewListener() - require.NoError(err) - defer listener.Close() - serverCloser := grpcutils.ServerCloser{} - defer serverCloser.Stop() - w := ids.NewAliaser() - - server := grpcutils.NewServer() - aliasreaderpb.RegisterAliasReaderServer(server, NewServer(w)) - serverCloser.Add(server) - - go grpcutils.Serve(listener, server) - - conn, err := grpcutils.Dial(listener.Addr().String()) - require.NoError(err) - defer conn.Close() - - r := NewClient(aliasreaderpb.NewAliasReaderClient(conn)) - test.Test(t, r, w) - }) - } -} diff --git a/message/bft_grpc.go b/message/bft_grpc.go deleted file mode 100644 index e178323fd..000000000 --- a/message/bft_grpc.go +++ /dev/null @@ -1,18 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package message - -import "github.com/luxfi/node/proto/p2p" - -// newMessageBFT creates a BFT message wrapper (gRPC version - uses Simplex) -func newMessageBFT(msg *p2p.BFT) *p2p.Message_BFT { - return &p2p.Message_BFT{Simplex: msg} -} - -// extractBFT extracts the BFT message from the wrapper (gRPC version - uses Simplex) -func extractBFT(msg *p2p.Message_BFT) *p2p.BFT { - return msg.Simplex -} diff --git a/message/bft_zap.go b/message/bft_zap.go index 08d474b1d..3bf53d8bf 100644 --- a/message/bft_zap.go +++ b/message/bft_zap.go @@ -1,5 +1,3 @@ -//go:build !grpc - // Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. @@ -7,12 +5,12 @@ package message import "github.com/luxfi/node/proto/p2p" -// newMessageBFT creates a BFT message wrapper (ZAP version) +// newMessageBFT creates a BFT message wrapper. func newMessageBFT(msg *p2p.BFT) *p2p.Message_BFT { return &p2p.Message_BFT{BFT: msg} } -// extractBFT extracts the BFT message from the wrapper (ZAP version) +// extractBFT extracts the BFT message from the wrapper. func extractBFT(msg *p2p.Message_BFT) *p2p.BFT { return msg.BFT } diff --git a/proto/p2p/p2p_grpc.go b/proto/p2p/p2p_grpc.go deleted file mode 100644 index a8098a242..000000000 --- a/proto/p2p/p2p_grpc.go +++ /dev/null @@ -1,120 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package p2p re-exports P2P types from the appropriate wire format implementation. -// Without grpc tag: uses ZAP wire format (zero protobuf) -// With grpc tag: uses protobuf wire format -package p2p - -import ( - "github.com/luxfi/node/proto/pb/p2p" - "google.golang.org/protobuf/proto" -) - -// Re-export all types from protobuf implementation -type ( - EngineType = p2p.EngineType - Message = p2p.Message - Message_CompressedZstd = p2p.Message_CompressedZstd - Message_Ping = p2p.Message_Ping - Message_Pong = p2p.Message_Pong - Message_Handshake = p2p.Message_Handshake - Message_GetPeerList = p2p.Message_GetPeerList - Message_PeerList_ = p2p.Message_PeerList_ - Message_GetStateSummaryFrontier = p2p.Message_GetStateSummaryFrontier - Message_StateSummaryFrontier_ = p2p.Message_StateSummaryFrontier_ - Message_GetAcceptedStateSummary = p2p.Message_GetAcceptedStateSummary - Message_AcceptedStateSummary_ = p2p.Message_AcceptedStateSummary_ - Message_GetAcceptedFrontier = p2p.Message_GetAcceptedFrontier - Message_AcceptedFrontier_ = p2p.Message_AcceptedFrontier_ - Message_GetAccepted = p2p.Message_GetAccepted - Message_Accepted_ = p2p.Message_Accepted_ - Message_GetAncestors = p2p.Message_GetAncestors - Message_Ancestors_ = p2p.Message_Ancestors_ - Message_Get = p2p.Message_Get - Message_Put = p2p.Message_Put - Message_PushQuery = p2p.Message_PushQuery - Message_PullQuery = p2p.Message_PullQuery - Message_Chits = p2p.Message_Chits - Message_Request = p2p.Message_Request - Message_Response = p2p.Message_Response - Message_Gossip = p2p.Message_Gossip - Message_Error = p2p.Message_Error - Message_Simplex = p2p.Message_Simplex - Ping = p2p.Ping - Pong = p2p.Pong - Handshake = p2p.Handshake - Client = p2p.Client - BloomFilter = p2p.BloomFilter - GetPeerList = p2p.GetPeerList - PeerList = p2p.PeerList - ClaimedIpPort = p2p.ClaimedIpPort - GetStateSummaryFrontier = p2p.GetStateSummaryFrontier - StateSummaryFrontier = p2p.StateSummaryFrontier - GetAcceptedStateSummary = p2p.GetAcceptedStateSummary - AcceptedStateSummary = p2p.AcceptedStateSummary - GetAcceptedFrontier = p2p.GetAcceptedFrontier - AcceptedFrontier = p2p.AcceptedFrontier - GetAccepted = p2p.GetAccepted - Accepted = p2p.Accepted - GetAncestors = p2p.GetAncestors - Ancestors = p2p.Ancestors - Get = p2p.Get - Put = p2p.Put - PushQuery = p2p.PushQuery - PullQuery = p2p.PullQuery - Chits = p2p.Chits - Request = p2p.Request - Response = p2p.Response - Gossip = p2p.Gossip - Error = p2p.Error - Simplex = p2p.Simplex - - // BFT aliases - ZAP uses "BFT", protobuf uses "Simplex" for the same concept - BFT = p2p.Simplex - Message_BFT = p2p.Message_Simplex - BFT_BlockProposal = p2p.Simplex_BlockProposal - BFT_Vote = p2p.Simplex_Vote - BFT_EmptyVote = p2p.Simplex_EmptyVote - BFT_FinalizeVote = p2p.Simplex_FinalizeVote - BFT_Notarization = p2p.Simplex_Notarization - BFT_EmptyNotarization = p2p.Simplex_EmptyNotarization - BFT_Finalization = p2p.Simplex_Finalization - BFT_ReplicationRequest = p2p.Simplex_ReplicationRequest - BFT_ReplicationResponse = p2p.Simplex_ReplicationResponse - BlockProposal = p2p.BlockProposal - ProtocolMetadata = p2p.ProtocolMetadata - BlockHeader = p2p.BlockHeader - Signature = p2p.Signature - Vote = p2p.Vote - EmptyVote = p2p.EmptyVote - QuorumCertificate = p2p.QuorumCertificate - EmptyNotarization = p2p.EmptyNotarization - ReplicationRequest = p2p.ReplicationRequest - ReplicationResponse = p2p.ReplicationResponse - QuorumRound = p2p.QuorumRound -) - -// Re-export constants -const ( - EngineType_ENGINE_TYPE_UNSPECIFIED = p2p.EngineType_ENGINE_TYPE_UNSPECIFIED - EngineType_ENGINE_TYPE_CHAIN = p2p.EngineType_ENGINE_TYPE_CHAIN - EngineType_ENGINE_TYPE_DAG = p2p.EngineType_ENGINE_TYPE_DAG -) - -// Marshal encodes a Message using protobuf -func Marshal(m *Message) ([]byte, error) { - return proto.Marshal(m) -} - -// Unmarshal decodes a Message using protobuf -func Unmarshal(data []byte, m *Message) error { - return proto.Unmarshal(data, m) -} - -// Size returns the encoded size of a message -func Size(m *Message) int { - return proto.Size(m) -} diff --git a/proto/p2p/p2p_zap.go b/proto/p2p/p2p_zap.go index 78a8888df..76d39280d 100644 --- a/proto/p2p/p2p_zap.go +++ b/proto/p2p/p2p_zap.go @@ -1,11 +1,8 @@ -//go:build !grpc - // Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. -// Package p2p re-exports P2P types from the appropriate wire format implementation. -// Without grpc tag: uses ZAP wire format (zero protobuf) -// With grpc tag: uses protobuf wire format +// Package p2p re-exports P2P types from the ZAP wire format +// implementation. ZAP is the only supported transport. package p2p import "github.com/luxfi/node/proto/zap/p2p" diff --git a/proto/pb/aliasreader/aliasreader.go b/proto/pb/aliasreader/aliasreader.go deleted file mode 100644 index 4c37cf956..000000000 --- a/proto/pb/aliasreader/aliasreader.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package aliasreader — protobuf wire types stub. See proto/pb/p2p/p2p.go -// header for context. Default builds use ZAP and skip this file. -package aliasreader diff --git a/proto/pb/http/http.go b/proto/pb/http/http.go deleted file mode 100644 index 84c6d6583..000000000 --- a/proto/pb/http/http.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package http — protobuf wire types stub. Default builds use ZAP. -package http diff --git a/proto/pb/http/responsewriter/responsewriter.go b/proto/pb/http/responsewriter/responsewriter.go deleted file mode 100644 index 86be84c0a..000000000 --- a/proto/pb/http/responsewriter/responsewriter.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package responsewriter — protobuf wire types stub. See proto/pb/p2p/p2p.go -// header for context. Default builds use ZAP and skip this file. -package responsewriter diff --git a/proto/pb/io/metric/client/client.go b/proto/pb/io/metric/client/client.go deleted file mode 100644 index f052bc750..000000000 --- a/proto/pb/io/metric/client/client.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package client — protobuf wire types stub. See proto/pb/p2p/p2p.go -// header for context. Default builds use ZAP and skip this file. -package client diff --git a/proto/pb/io/reader/reader.go b/proto/pb/io/reader/reader.go deleted file mode 100644 index 8ace10085..000000000 --- a/proto/pb/io/reader/reader.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package reader — protobuf wire types stub. See proto/pb/p2p/p2p.go -// header for context. Default builds use ZAP and skip this file. -package reader diff --git a/proto/pb/io/writer/writer.go b/proto/pb/io/writer/writer.go deleted file mode 100644 index a4515a40d..000000000 --- a/proto/pb/io/writer/writer.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package writer — protobuf wire types stub. See proto/pb/p2p/p2p.go -// header for context. Default builds use ZAP and skip this file. -package writer diff --git a/proto/pb/keystore/keystore.go b/proto/pb/keystore/keystore.go deleted file mode 100644 index 0374e0713..000000000 --- a/proto/pb/keystore/keystore.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package keystore — protobuf wire types stub. See proto/pb/p2p/p2p.go -// header for context. Default builds use ZAP and skip this file. -package keystore diff --git a/proto/pb/message/message.go b/proto/pb/message/message.go deleted file mode 100644 index f6409ad26..000000000 --- a/proto/pb/message/message.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package message — protobuf wire types stub. See proto/pb/p2p/p2p.go -// header for context. Default builds use ZAP and skip this file. -package message diff --git a/proto/pb/messenger/messenger.go b/proto/pb/messenger/messenger.go deleted file mode 100644 index c1fc35e9e..000000000 --- a/proto/pb/messenger/messenger.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package messenger — protobuf wire types stub. See proto/pb/p2p/p2p.go -// header for context. Default builds use ZAP and skip this file. -package messenger diff --git a/proto/pb/net/conn/conn.go b/proto/pb/net/conn/conn.go deleted file mode 100644 index feae39da7..000000000 --- a/proto/pb/net/conn/conn.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package conn — protobuf wire types stub. See proto/pb/p2p/p2p.go -// header for context. Default builds use ZAP and skip this file. -package conn diff --git a/proto/pb/p2p/p2p.go b/proto/pb/p2p/p2p.go deleted file mode 100644 index b6cf26267..000000000 --- a/proto/pb/p2p/p2p.go +++ /dev/null @@ -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 p2p is the protobuf-generated wire-types package referenced -// by the grpc-tagged transport in proto/p2p/p2p_grpc.go (or its -// legacy single-file form). Default builds use ZAP and never reach -// this package; the build tag here matches the grpc-tagged consumers -// so default builds skip it cleanly. -// -// To re-generate the actual protobuf types (when the grpc transport -// is needed), run: -// -// protoc -I=proto/ --go_out=proto/pb/ proto/p2p/p2p.proto -// -// in the node root. Until that's done, this stub keeps "go mod tidy" -// happy while the canonical wire format stays ZAP. -package p2p diff --git a/proto/pb/platformvm/platformvm.go b/proto/pb/platformvm/platformvm.go deleted file mode 100644 index f9e6c1eb1..000000000 --- a/proto/pb/platformvm/platformvm.go +++ /dev/null @@ -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 platformvm is the protobuf-generated wire-types package referenced -// by the grpc-tagged transport in proto/platformvm/platformvm_grpc.go (or its -// legacy single-file form). Default builds use ZAP and never reach -// this package; the build tag here matches the grpc-tagged consumers -// so default builds skip it cleanly. -// -// To re-generate the actual protobuf types (when the grpc transport -// is needed), run: -// -// protoc -I=proto/ --go_out=proto/pb/ proto/platformvm/platformvm.proto -// -// in the node root. Until that's done, this stub keeps "go mod tidy" -// happy while the canonical wire format stays ZAP. -package platformvm diff --git a/proto/pb/rpcdb/rpcdb.go b/proto/pb/rpcdb/rpcdb.go deleted file mode 100644 index 46ce9f289..000000000 --- a/proto/pb/rpcdb/rpcdb.go +++ /dev/null @@ -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 rpcdb is the protobuf-generated wire-types package referenced -// by the grpc-tagged transport in proto/rpcdb/rpcdb_grpc.go (or its -// legacy single-file form). Default builds use ZAP and never reach -// this package; the build tag here matches the grpc-tagged consumers -// so default builds skip it cleanly. -// -// To re-generate the actual protobuf types (when the grpc transport -// is needed), run: -// -// protoc -I=proto/ --go_out=proto/pb/ proto/rpcdb/rpcdb.proto -// -// in the node root. Until that's done, this stub keeps "go mod tidy" -// happy while the canonical wire format stays ZAP. -package rpcdb diff --git a/proto/pb/sdk/sdk.go b/proto/pb/sdk/sdk.go deleted file mode 100644 index 3f1a47df8..000000000 --- a/proto/pb/sdk/sdk.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package sdk — protobuf wire types stub. See proto/pb/p2p/p2p.go -// header for context. Default builds use ZAP and skip this file. -package sdk diff --git a/proto/pb/sender/sender.go b/proto/pb/sender/sender.go deleted file mode 100644 index c700af4a4..000000000 --- a/proto/pb/sender/sender.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package sender — protobuf wire types stub. See proto/pb/p2p/p2p.go -// header for context. Default builds use ZAP and skip this file. -package sender diff --git a/proto/pb/sharedmemory/sharedmemory.go b/proto/pb/sharedmemory/sharedmemory.go deleted file mode 100644 index c000fc9ea..000000000 --- a/proto/pb/sharedmemory/sharedmemory.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package sharedmemory — protobuf wire types stub. See proto/pb/p2p/p2p.go -// header for context. Default builds use ZAP and skip this file. -package sharedmemory diff --git a/proto/pb/signer/signer.go b/proto/pb/signer/signer.go deleted file mode 100644 index c0dcd9635..000000000 --- a/proto/pb/signer/signer.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package signer — protobuf wire types stub. See proto/pb/p2p/p2p.go -// header for context. Default builds use ZAP and skip this file. -package signer diff --git a/proto/pb/sync/sync.go b/proto/pb/sync/sync.go deleted file mode 100644 index 822ba9b59..000000000 --- a/proto/pb/sync/sync.go +++ /dev/null @@ -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 sync is the protobuf-generated wire-types package referenced -// by the grpc-tagged transport in proto/sync/sync_grpc.go (or its -// legacy single-file form). Default builds use ZAP and never reach -// this package; the build tag here matches the grpc-tagged consumers -// so default builds skip it cleanly. -// -// To re-generate the actual protobuf types (when the grpc transport -// is needed), run: -// -// protoc -I=proto/ --go_out=proto/pb/ proto/sync/sync.proto -// -// in the node root. Until that's done, this stub keeps "go mod tidy" -// happy while the canonical wire format stays ZAP. -package sync diff --git a/proto/pb/validatorstate/validatorstate.go b/proto/pb/validatorstate/validatorstate.go deleted file mode 100644 index 1d00e6ba3..000000000 --- a/proto/pb/validatorstate/validatorstate.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package validatorstate — protobuf wire types stub. See proto/pb/p2p/p2p.go -// header for context. Default builds use ZAP and skip this file. -package validatorstate diff --git a/proto/pb/vm/runtime/runtime.go b/proto/pb/vm/runtime/runtime.go deleted file mode 100644 index afd49335f..000000000 --- a/proto/pb/vm/runtime/runtime.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package runtime — protobuf wire types stub. See proto/pb/p2p/p2p.go -// header for context. Default builds use ZAP and skip this file. -package runtime diff --git a/proto/pb/vm/vm.go b/proto/pb/vm/vm.go deleted file mode 100644 index abcc07c3e..000000000 --- a/proto/pb/vm/vm.go +++ /dev/null @@ -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 vm is the protobuf-generated wire-types package referenced -// by the grpc-tagged transport in proto/vm/vm_grpc.go (or its -// legacy single-file form). Default builds use ZAP and never reach -// this package; the build tag here matches the grpc-tagged consumers -// so default builds skip it cleanly. -// -// To re-generate the actual protobuf types (when the grpc transport -// is needed), run: -// -// protoc -I=proto/ --go_out=proto/pb/ proto/vm/vm.proto -// -// in the node root. Until that's done, this stub keeps "go mod tidy" -// happy while the canonical wire format stays ZAP. -package vm diff --git a/proto/pb/warp/warp.go b/proto/pb/warp/warp.go deleted file mode 100644 index 04cb881ef..000000000 --- a/proto/pb/warp/warp.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package warp — protobuf wire types stub. See proto/pb/p2p/p2p.go -// header for context. Default builds use ZAP and skip this file. -package warp diff --git a/proto/platformvm/platformvm_grpc.go b/proto/platformvm/platformvm_grpc.go deleted file mode 100644 index c5baf6137..000000000 --- a/proto/platformvm/platformvm_grpc.go +++ /dev/null @@ -1,23 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package platformvm - -import ( - "github.com/luxfi/node/proto/pb/platformvm" - "google.golang.org/protobuf/proto" -) - -type ( - L1ValidatorRegistrationJustification = platformvm.L1ValidatorRegistrationJustification - L1ValidatorRegistrationJustification_ConvertNetworkToL1TxData = platformvm.L1ValidatorRegistrationJustification_ConvertNetworkToL1TxData - L1ValidatorRegistrationJustification_RegisterL1ValidatorMessage = platformvm.L1ValidatorRegistrationJustification_RegisterL1ValidatorMessage - ChainIDIndex = platformvm.ChainIDIndex -) - -// Unmarshal decodes a message from bytes (protobuf implementation) -func Unmarshal(data []byte, m proto.Message) error { - return proto.Unmarshal(data, m) -} diff --git a/proto/platformvm/platformvm_zap.go b/proto/platformvm/platformvm_zap.go index cb88a1688..cbc886605 100644 --- a/proto/platformvm/platformvm_zap.go +++ b/proto/platformvm/platformvm_zap.go @@ -1,5 +1,3 @@ -//go:build !grpc - // Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. @@ -16,9 +14,11 @@ type ( ChainIDIndex = platformvm.ChainIDIndex ) -// Unmarshal decodes a message from bytes (ZAP implementation) +// Unmarshal decodes a message from bytes. The ZAP codec encodes +// messages via direct field assignment; this entry point is reserved +// for compatibility with callers that thread a generic byte payload +// through the wire types and is a no-op until a concrete consumer +// arrives. func Unmarshal(data []byte, m interface{}) error { - // ZAP uses direct field assignment - for now just return nil - // Actual ZAP unmarshaling would use a codec return nil } diff --git a/proto/sync/sync_grpc.go b/proto/sync/sync_grpc.go deleted file mode 100644 index b270717f2..000000000 --- a/proto/sync/sync_grpc.go +++ /dev/null @@ -1,23 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package sync re-exports sync types from the appropriate wire format implementation. -// Without grpc tag: uses ZAP wire format (zero protobuf) -// With grpc tag: uses protobuf wire format -package sync - -import pb "github.com/luxfi/node/proto/pb/sync" - -// Re-export all types from protobuf implementation -type ( - Key = pb.Key - MaybeBytes = pb.MaybeBytes - ProofNode = pb.ProofNode - Proof = pb.Proof - KeyValue = pb.KeyValue - KeyChange = pb.KeyChange - RangeProof = pb.RangeProof - ChangeProof = pb.ChangeProof -) diff --git a/proto/sync/sync_zap.go b/proto/sync/sync_zap.go index b30a6cc41..745398acc 100644 --- a/proto/sync/sync_zap.go +++ b/proto/sync/sync_zap.go @@ -1,11 +1,8 @@ -//go:build !grpc - // Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. -// Package sync re-exports sync types from the appropriate wire format implementation. -// Without grpc tag: uses ZAP wire format (zero protobuf) -// With grpc tag: uses protobuf wire format +// Package sync re-exports sync types from the ZAP wire format +// implementation. ZAP is the only supported transport. package sync import "github.com/luxfi/node/proto/zap/sync" diff --git a/proto/vm/vm_grpc.go b/proto/vm/vm_grpc.go deleted file mode 100644 index 861d5b47d..000000000 --- a/proto/vm/vm_grpc.go +++ /dev/null @@ -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 vm - -import "github.com/luxfi/node/proto/pb/vm" - -type ( - State = vm.State - Error = vm.Error -) - -const ( - State_STATE_UNSPECIFIED = vm.State_STATE_UNSPECIFIED - State_STATE_STATE_SYNCING = vm.State_STATE_STATE_SYNCING - State_STATE_BOOTSTRAPPING = vm.State_STATE_BOOTSTRAPPING - State_STATE_NORMAL_OP = vm.State_STATE_NORMAL_OP - - Error_ERROR_UNSPECIFIED = vm.Error_ERROR_UNSPECIFIED - Error_ERROR_CLOSED = vm.Error_ERROR_CLOSED - Error_ERROR_NOT_FOUND = vm.Error_ERROR_NOT_FOUND - Error_ERROR_STATE_SYNC_NOT_IMPLEMENTED = vm.Error_ERROR_STATE_SYNC_NOT_IMPLEMENTED -) diff --git a/proto/vm/vm_zap.go b/proto/vm/vm_zap.go index 81d26b58b..1e74c4e23 100644 --- a/proto/vm/vm_zap.go +++ b/proto/vm/vm_zap.go @@ -1,5 +1,3 @@ -//go:build !grpc - // Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. diff --git a/service/keystore/rpckeystore/client.go b/service/keystore/rpckeystore/client.go deleted file mode 100644 index c7e608312..000000000 --- a/service/keystore/rpckeystore/client.go +++ /dev/null @@ -1,65 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package rpckeystore is the keystore-over-RPC client and server. The -// transport is selected by build tag: `grpc` selects the legacy gRPC -// codepath in this file; the default build (no tag) will eventually -// select a ZAP-based codepath (task #57). Mirrors the rpcdb naming -// pattern — `rpc` is the value (remote procedure call), the substrate -// is per-tag. -package rpckeystore - -import ( - "context" - - "github.com/luxfi/database" - "github.com/luxfi/database/encdb" - "github.com/luxfi/node/db/rpcdb" - "github.com/luxfi/node/service/keystore" - "github.com/luxfi/vm/rpc/grpcutils" - - keystorepb "github.com/luxfi/node/proto/pb/keystore" - rpcdbpb "github.com/luxfi/node/proto/pb/rpcdb" -) - -var _ keystore.BlockchainKeystore = (*Client)(nil) - -// Client is a consensus.Keystore that talks over RPC. -type Client struct { - client keystorepb.KeystoreClient -} - -// NewClient returns a keystore instance connected to a remote keystore instance -func NewClient(client keystorepb.KeystoreClient) *Client { - return &Client{ - client: client, - } -} - -func (c *Client) GetDatabase(username, password string) (*encdb.Database, error) { - bcDB, err := c.GetRawDatabase(username, password) - if err != nil { - return nil, err - } - return encdb.New([]byte(password), bcDB) -} - -func (c *Client) GetRawDatabase(username, password string) (database.Database, error) { - resp, err := c.client.GetDatabase(context.Background(), &keystorepb.GetDatabaseRequest{ - Username: username, - Password: password, - }) - if err != nil { - return nil, err - } - - clientConn, err := grpcutils.Dial(resp.ServerAddr) - if err != nil { - return nil, err - } - - dbClient := rpcdb.NewGRPCClient(rpcdbpb.NewDatabaseClient(clientConn)) - return dbClient, nil -} diff --git a/service/keystore/rpckeystore/server.go b/service/keystore/rpckeystore/server.go deleted file mode 100644 index 32b3ca1a4..000000000 --- a/service/keystore/rpckeystore/server.go +++ /dev/null @@ -1,67 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package rpckeystore - -import ( - "context" - "github.com/luxfi/database" - - "github.com/luxfi/node/service/keystore" - "github.com/luxfi/vm/rpc/grpcutils" - - keystorepb "github.com/luxfi/node/proto/pb/keystore" -) - -var _ keystorepb.KeystoreServer = (*Server)(nil) - -// Server is a consensus.Keystore that is managed over RPC. -type Server struct { - keystorepb.UnsafeKeystoreServer - ks keystore.BlockchainKeystore -} - -// NewServer returns a keystore connected to a remote keystore -func NewServer(ks keystore.BlockchainKeystore) *Server { - return &Server{ - ks: ks, - } -} - -func (s *Server) GetDatabase( - _ context.Context, - req *keystorepb.GetDatabaseRequest, -) (*keystorepb.GetDatabaseResponse, error) { - db, err := s.ks.GetRawDatabase(req.Username, req.Password) - if err != nil { - return nil, err - } - - closer := dbCloser{Database: db} - - serverListener, err := grpcutils.NewListener() - if err != nil { - return nil, err - } - - server := grpcutils.NewServer() - closer.closer.Add(server) - - // start the db server - go grpcutils.Serve(serverListener, server) - - return &keystorepb.GetDatabaseResponse{ServerAddr: serverListener.Addr().String()}, nil -} - -type dbCloser struct { - database.Database - closer grpcutils.ServerCloser -} - -func (db *dbCloser) Close() error { - err := db.Database.Close() - db.closer.Stop() - return err -} diff --git a/trace/exporter_grpc.go b/trace/exporter_grpc.go deleted file mode 100644 index 1257dfad3..000000000 --- a/trace/exporter_grpc.go +++ /dev/null @@ -1,68 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package trace - -import ( - "context" - "time" - - "go.opentelemetry.io/otel/exporters/otlp/otlptrace" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" - - sdktrace "go.opentelemetry.io/otel/sdk/trace" -) - -const tracerProviderExportCreationTimeout = 5 * time.Second - -type ExporterConfig struct { - Type ExporterType `json:"type"` - - // Endpoint to send metrics to. If empty, the default endpoint will be used. - Endpoint string `json:"endpoint"` - - // Headers to send with metrics - Headers map[string]string `json:"headers"` - - // If true, don't use TLS - Insecure bool `json:"insecure"` -} - -func newExporter(config ExporterConfig) (sdktrace.SpanExporter, error) { - var client otlptrace.Client - switch config.Type { - case GRPC: - opts := []otlptracegrpc.Option{ - otlptracegrpc.WithHeaders(config.Headers), - otlptracegrpc.WithTimeout(tracerExportTimeout), - } - if config.Endpoint != "" { - opts = append(opts, otlptracegrpc.WithEndpoint(config.Endpoint)) - } - if config.Insecure { - opts = append(opts, otlptracegrpc.WithInsecure()) - } - client = otlptracegrpc.NewClient(opts...) - case HTTP: - opts := []otlptracehttp.Option{ - otlptracehttp.WithHeaders(config.Headers), - otlptracehttp.WithTimeout(tracerExportTimeout), - } - if config.Endpoint != "" { - opts = append(opts, otlptracehttp.WithEndpoint(config.Endpoint)) - } - if config.Insecure { - opts = append(opts, otlptracehttp.WithInsecure()) - } - client = otlptracehttp.NewClient(opts...) - default: - return nil, errUnknownExporterType - } - - ctx, cancel := context.WithTimeout(context.Background(), tracerProviderExportCreationTimeout) - defer cancel() - return otlptrace.New(ctx, client) -} diff --git a/trace/exporter_type.go b/trace/exporter_type.go deleted file mode 100644 index 1455f3022..000000000 --- a/trace/exporter_type.go +++ /dev/null @@ -1,91 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package trace - -import ( - "errors" - "fmt" - "strings" -) - -const ( - Disabled ExporterType = iota - GRPC - HTTP - ZAP // ZAP-based OTLP transport (default, no gRPC dependency) -) - -var ( - errUnknownExporterType = errors.New("unknown exporter type") - errMissingQuotes = errors.New("first and last characters should be quotes") -) - -func ExporterTypeFromString(exporterTypeStr string) (ExporterType, error) { - switch strings.ToLower(exporterTypeStr) { - case "disabled": - return Disabled, nil - case "grpc": - return GRPC, nil - case "http": - return HTTP, nil - case "zap": - return ZAP, nil - default: - return 0, fmt.Errorf("%w: %q", errUnknownExporterType, exporterTypeStr) - } -} - -type ExporterType byte - -func (t ExporterType) MarshalJSON() ([]byte, error) { - str, ok := t.toString() - if !ok { - return nil, fmt.Errorf("%w: %d", errUnknownExporterType, t) - } - return []byte(`"` + str + `"`), nil -} - -func (t *ExporterType) UnmarshalJSON(b []byte) error { - str := string(b) - if str == "null" { // If "null", do nothing - return nil - } - if len(str) < 2 { - return errMissingQuotes - } - - lastIndex := len(str) - 1 - if str[0] != '"' || str[lastIndex] != '"' { - return errMissingQuotes - } - - exporterType, err := ExporterTypeFromString(str[1:lastIndex]) - if err != nil { - return err - } - *t = exporterType - return nil -} - -func (t ExporterType) String() string { - str, _ := t.toString() - return str -} - -func (t ExporterType) toString() (string, bool) { - switch t { - case Disabled: - return "disabled", true - case GRPC: - return "grpc", true - case HTTP: - return "http", true - case ZAP: - return "zap", true - default: - return "unknown", false - } -} diff --git a/trace/exporter_type_test.go b/trace/exporter_type_test.go deleted file mode 100644 index b7d845dbe..000000000 --- a/trace/exporter_type_test.go +++ /dev/null @@ -1,132 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package trace - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestMarshal(t *testing.T) { - tests := []struct { - name string - exporter ExporterType - expected string - expectedError error - }{ - { - name: "unknown_type", - exporter: 255, - expectedError: errUnknownExporterType, - }, - { - name: "disabled", - exporter: Disabled, - expected: `"disabled"`, - }, - { - name: "grpc", - exporter: GRPC, - expected: `"grpc"`, - }, - { - name: "http", - exporter: HTTP, - expected: `"http"`, - }, - { - name: "zap", - exporter: ZAP, - expected: `"zap"`, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - require := require.New(t) - - actual, err := tt.exporter.MarshalJSON() - require.ErrorIs(err, tt.expectedError) - require.Equal(tt.expected, string(actual)) - }) - } -} - -func TestUnmarshal(t *testing.T) { - tests := []struct { - name string - json string - expected ExporterType - expectedError error - }{ - { - name: "no_quotes", - json: "grpc", - expectedError: errMissingQuotes, - }, - { - name: "single_left_quote", - json: `"grpc`, - expectedError: errMissingQuotes, - }, - { - name: "single_right_quote", - json: `grpc"`, - expectedError: errMissingQuotes, - }, - { - name: "only_one_quote", - json: `"`, - expectedError: errMissingQuotes, - }, - { - name: "multiple_quotes", - json: `""grpc"""`, - expectedError: errUnknownExporterType, - }, - { - name: "empty_string", - json: `""`, - expectedError: errUnknownExporterType, - }, - { - name: "null", - json: `null`, - }, - { - name: "disabled", - json: `"disabled"`, - expected: Disabled, - }, - { - name: "grpc", - json: `"grpc"`, - expected: GRPC, - }, - { - name: "http", - json: `"http"`, - expected: HTTP, - }, - { - name: "zap", - json: `"zap"`, - expected: ZAP, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - require := require.New(t) - - var actual ExporterType - err := actual.UnmarshalJSON([]byte(tt.json)) - require.ErrorIs(err, tt.expectedError) - require.Equal(tt.expected, actual) - }) - } -} diff --git a/trace/noop.go b/trace/noop.go deleted file mode 100644 index ce411f3ec..000000000 --- a/trace/noop.go +++ /dev/null @@ -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 trace - -import "go.opentelemetry.io/otel/trace/noop" - -var Noop Tracer = noOpTracer{} - -// noOpTracer is an implementation of trace.Tracer that does nothing. -type noOpTracer struct { - noop.Tracer -} - -func (noOpTracer) Close() error { - return nil -} diff --git a/trace/trace_zap.go b/trace/trace_zap.go index 800e45752..335e8d116 100644 --- a/trace/trace_zap.go +++ b/trace/trace_zap.go @@ -1,11 +1,10 @@ -//go:build !grpc - // Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. -// Package trace provides tracing functionality. -// In ZAP mode (default), tracing is disabled - no OpenTelemetry dependencies. -// Use -tags=grpc to enable full OpenTelemetry tracing support. +// Package trace provides tracing primitives. The package ships a +// no-op tracer suitable for ZAP-native production builds; a real +// exporter implementation lives outside the node tree and plugs in +// via the Tracer interface. package trace import ( @@ -185,13 +184,13 @@ func (noopTracer) Start(ctx context.Context, _ string, _ ...SpanStartOption) (co type noopSpan struct{} -func (noopSpan) End(...SpanEndOption) {} -func (noopSpan) SetStatus(StatusCode, string) {} -func (noopSpan) RecordError(error, ...EventOption) {} -func (noopSpan) AddEvent(string, ...EventOption) {} -func (noopSpan) SetAttributes(...KeyValue) {} +func (noopSpan) End(...SpanEndOption) {} +func (noopSpan) SetStatus(StatusCode, string) {} +func (noopSpan) RecordError(error, ...EventOption) {} +func (noopSpan) AddEvent(string, ...EventOption) {} +func (noopSpan) SetAttributes(...KeyValue) {} -// New creates a new tracer (returns Noop in ZAP mode) +// New creates a new tracer (returns Noop in the canonical ZAP build). func New(Config) (Tracer, error) { return Noop, nil } diff --git a/trace/tracer.go b/trace/tracer.go deleted file mode 100644 index a7cbee355..000000000 --- a/trace/tracer.go +++ /dev/null @@ -1,134 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package trace - -import ( - "context" - "fmt" - "io" - "time" - - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/sdk/resource" - oteltrace "go.opentelemetry.io/otel/trace" - - sdktrace "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.4.0" -) - -// Type aliases for otel compatibility -type ( - KeyValue = attribute.KeyValue - SpanStartOption = oteltrace.SpanStartOption - SpanEndOption = oteltrace.SpanEndOption - Span = oteltrace.Span - StatusCode = codes.Code -) - -// Re-export otel status codes -const ( - StatusUnset = codes.Unset - StatusOK = codes.Ok - StatusError = codes.Error -) - -// EventOption is a type alias for span event options -type EventOption = oteltrace.EventOption - -// Helper functions wrapping otel attribute constructors - -// String creates a string attribute -func String(key, value string) KeyValue { - return attribute.String(key, value) -} - -// Int creates an int attribute -func Int(key string, value int) KeyValue { - return attribute.Int(key, value) -} - -// Int64 creates an int64 attribute -func Int64(key string, value int64) KeyValue { - return attribute.Int64(key, value) -} - -// Bool creates a bool attribute -func Bool(key string, value bool) KeyValue { - return attribute.Bool(key, value) -} - -// Stringer creates a string attribute from a fmt.Stringer -func Stringer(key string, value fmt.Stringer) KeyValue { - return attribute.Stringer(key, value) -} - -// WithAttributes returns a SpanStartOption with the given attributes -func WithAttributes(kv ...KeyValue) SpanStartOption { - return oteltrace.WithAttributes(kv...) -} - -const ( - tracerExportTimeout = 10 * time.Second - // [tracerProviderShutdownTimeout] is longer than [tracerExportTimeout] so - // in-flight exports can finish before the tracer provider shuts down. - tracerProviderShutdownTimeout = 15 * time.Second -) - -type Config struct { - ExporterConfig `json:"exporterConfig"` - - // The fraction of traces to sample. - // If >= 1 always samples. - // If <= 0 never samples. - TraceSampleRate float64 `json:"traceSampleRate"` - - AppName string `json:"appName"` - Version string `json:"version"` -} - -type Tracer interface { - oteltrace.Tracer - io.Closer -} - -type tracer struct { - oteltrace.Tracer - - tp *sdktrace.TracerProvider -} - -func (t *tracer) Close() error { - ctx, cancel := context.WithTimeout(context.Background(), tracerProviderShutdownTimeout) - defer cancel() - return t.tp.Shutdown(ctx) -} - -func New(config Config) (Tracer, error) { - if config.ExporterConfig.Type == Disabled { - return Noop, nil - } - - exporter, err := newExporter(config.ExporterConfig) - if err != nil { - return nil, err - } - - tracerProviderOpts := []sdktrace.TracerProviderOption{ - sdktrace.WithBatcher(exporter, sdktrace.WithExportTimeout(tracerExportTimeout)), - sdktrace.WithResource(resource.NewWithAttributes(semconv.SchemaURL, - attribute.String("version", config.Version), - semconv.ServiceNameKey.String(config.AppName), - )), - sdktrace.WithSampler(sdktrace.TraceIDRatioBased(config.TraceSampleRate)), - } - - tracerProvider := sdktrace.NewTracerProvider(tracerProviderOpts...) - return &tracer{ - Tracer: tracerProvider.Tracer(config.AppName), - tp: tracerProvider, - }, nil -} diff --git a/vms/components/message/message_grpc.go b/vms/components/message/message_grpc.go deleted file mode 100644 index 21faf3620..000000000 --- a/vms/components/message/message_grpc.go +++ /dev/null @@ -1,86 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package message - -import ( - "errors" - "fmt" - - "google.golang.org/protobuf/proto" - - "github.com/luxfi/ids" - - pb "github.com/luxfi/node/proto/pb/message" -) - -var ( - _ Message = (*Tx)(nil) - - ErrUnexpectedCodecVersion = errors.New("unexpected codec version") - errUnknownMessageType = errors.New("unknown message type") -) - -type Message interface { - // Handle this message with the correct message handler - Handle(handler Handler, nodeID ids.NodeID, requestID uint32) error - - // initialize should be called whenever a message is built or parsed - initialize([]byte) - - // Bytes returns the binary representation of this message - // - // Bytes should only be called after being initialized - Bytes() []byte -} - -type message []byte - -func (m *message) initialize(bytes []byte) { - *m = bytes -} - -func (m *message) Bytes() []byte { - return *m -} - -func Parse(bytes []byte) (Message, error) { - var ( - msg Message - protoMsg pb.Message - ) - - if err := proto.Unmarshal(bytes, &protoMsg); err == nil { - // This message was encoded with proto. - switch m := protoMsg.GetMessage().(type) { - case *pb.Message_Tx: - msg = &Tx{ - Tx: m.Tx.Tx, - } - default: - return nil, fmt.Errorf("%w: %T", errUnknownMessageType, protoMsg.GetMessage()) - } - } else { - // This message wasn't encoded with proto. - // It must have been encoded with node's codec. - // Legacy codec fallback for nodes older than v1.11.0 that do not - // support proto encoding. - version, err := c.Unmarshal(bytes, &msg) - if err != nil { - return nil, err - } - if version != codecVersion { - return nil, ErrUnexpectedCodecVersion - } - } - msg.initialize(bytes) - return msg, nil -} - -func Build(msg Message) ([]byte, error) { - bytes, err := c.Marshal(codecVersion, &msg) - msg.initialize(bytes) - return bytes, err -} diff --git a/vms/components/message/message_test.go b/vms/components/message/message_test.go deleted file mode 100644 index 550ae54a3..000000000 --- a/vms/components/message/message_test.go +++ /dev/null @@ -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 message - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "google.golang.org/protobuf/proto" - - "github.com/luxfi/codec" - - pb "github.com/luxfi/node/proto/pb/message" -) - -func TestParseGibberish(t *testing.T) { - randomBytes := []byte{0, 1, 2, 3, 4, 5} - _, err := Parse(randomBytes) - require.ErrorIs(t, err, codec.ErrUnknownVersion) -} - -func TestParseProto(t *testing.T) { - require := require.New(t) - - txBytes := []byte{'y', 'e', 'e', 't'} - protoMsg := pb.Message{ - Message: &pb.Message_Tx{ - Tx: &pb.Tx{ - Tx: txBytes, - }, - }, - } - msgBytes, err := proto.Marshal(&protoMsg) - require.NoError(err) - - parsedMsgIntf, err := Parse(msgBytes) - require.NoError(err) - - require.IsType(&Tx{}, parsedMsgIntf) - parsedMsg := parsedMsgIntf.(*Tx) - - require.Equal(txBytes, parsedMsg.Tx) - - // Parse invalid message - _, err = Parse([]byte{1, 3, 3, 7}) - // Can't parse as proto so it falls back to using node's codec - require.ErrorIs(err, codec.ErrUnknownVersion) -} diff --git a/vms/components/message/message_zap.go b/vms/components/message/message_zap.go index a19f2595b..173d161d7 100644 --- a/vms/components/message/message_zap.go +++ b/vms/components/message/message_zap.go @@ -1,5 +1,3 @@ -//go:build !grpc - // Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. diff --git a/vms/example/xsvm/api/ping.go b/vms/example/xsvm/api/ping.go deleted file mode 100644 index 609b037f0..000000000 --- a/vms/example/xsvm/api/ping.go +++ /dev/null @@ -1,55 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package api - -import ( - "context" - "errors" - "fmt" - "io" - - "connectrpc.com/connect" - "github.com/luxfi/log" - - "github.com/luxfi/node/connectproto/pb/xsvm" - "github.com/luxfi/node/connectproto/pb/xsvm/xsvmconnect" -) - -var _ xsvmconnect.PingHandler = (*PingService)(nil) - -type PingService struct { - Log log.Logger -} - -func (p *PingService) Ping(_ context.Context, request *connect.Request[xsvm.PingRequest]) (*connect.Response[xsvm.PingReply], error) { - p.Log.Debug("ping", log.String("message", request.Msg.Message)) - return connect.NewResponse[xsvm.PingReply]( - &xsvm.PingReply{ - Message: request.Msg.Message, - }, - ), nil -} - -func (p *PingService) StreamPing(_ context.Context, server *connect.BidiStream[xsvm.StreamPingRequest, xsvm.StreamPingReply]) error { - for { - request, err := server.Receive() - if errors.Is(err, io.EOF) { - // Client closed the send stream - return nil - } - if err != nil { - return fmt.Errorf("failed to receive message: %w", err) - } - - p.Log.Debug("stream ping", log.String("message", request.Message)) - err = server.Send(&xsvm.StreamPingReply{ - Message: request.Message, - }) - if err != nil { - return fmt.Errorf("failed to send message: %w", err) - } - } -} diff --git a/vms/example/xsvm/cmd/run/cmd.go b/vms/example/xsvm/cmd/run/cmd.go deleted file mode 100644 index 277e776a3..000000000 --- a/vms/example/xsvm/cmd/run/cmd.go +++ /dev/null @@ -1,29 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package run - -import ( - "github.com/spf13/cobra" - - "github.com/luxfi/node/vms/example/xsvm" - "github.com/luxfi/node/vms/rpcchainvm" -) - -func Command() *cobra.Command { - return &cobra.Command{ - Use: "xsvm", - Short: "Runs an XSVM plugin", - RunE: runFunc, - } -} - -func runFunc(*cobra.Command, []string) error { - // xsvm.VM does not yet implement the current consensus ChainVM interface. - // The plugin is a no-op until the interface alignment is complete. - _ = rpcchainvm.Serve - _ = &xsvm.VM{} - return nil -} diff --git a/vms/example/xsvm/cmd/xsvm/main.go b/vms/example/xsvm/cmd/xsvm/main.go deleted file mode 100644 index 5a0bc9705..000000000 --- a/vms/example/xsvm/cmd/xsvm/main.go +++ /dev/null @@ -1,41 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package main - -import ( - "context" - "fmt" - "os" - - "github.com/spf13/cobra" - - "github.com/luxfi/node/vms/example/xsvm/cmd/account" - "github.com/luxfi/node/vms/example/xsvm/cmd/chain" - "github.com/luxfi/node/vms/example/xsvm/cmd/issue" - "github.com/luxfi/node/vms/example/xsvm/cmd/run" - "github.com/luxfi/node/vms/example/xsvm/cmd/version" - "github.com/luxfi/node/vms/example/xsvm/cmd/versionjson" -) - -func init() { - cobra.EnablePrefixMatching = true -} - -func main() { - cmd := run.Command() - cmd.AddCommand( - account.Command(), - chain.Command(), - issue.Command(), - version.Command(), - versionjson.Command(), - ) - ctx := context.Background() - if err := cmd.ExecuteContext(ctx); err != nil { - fmt.Fprintf(os.Stderr, "command failed %v\n", err) - os.Exit(1) - } -} diff --git a/vms/example/xsvm/vm_http_grpc.go b/vms/example/xsvm/vm_http_grpc.go deleted file mode 100644 index f2059d975..000000000 --- a/vms/example/xsvm/vm_http_grpc.go +++ /dev/null @@ -1,32 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package xsvm - -import ( - "context" - "net/http" - - "connectrpc.com/grpcreflect" - "github.com/luxfi/log" - - "github.com/luxfi/node/connectproto/pb/xsvm/xsvmconnect" - "github.com/luxfi/node/vms/example/xsvm/api" -) - -func (vm *VM) NewHTTPHandler(context.Context) (http.Handler, error) { - mux := http.NewServeMux() - - reflectionPattern, reflectionHandler := grpcreflect.NewHandlerV1( - grpcreflect.NewStaticReflector(xsvmconnect.PingName), - ) - mux.Handle(reflectionPattern, reflectionHandler) - - pingService := &api.PingService{Log: vm.rt.Log.(log.Logger)} - pingPath, pingHandler := xsvmconnect.NewPingHandler(pingService) - mux.Handle(pingPath, pingHandler) - - return mux, nil -} diff --git a/vms/example/xsvm/vm_http_zap.go b/vms/example/xsvm/vm_http_zap.go index c7ff49571..b57d25965 100644 --- a/vms/example/xsvm/vm_http_zap.go +++ b/vms/example/xsvm/vm_http_zap.go @@ -1,5 +1,3 @@ -//go:build !grpc - // Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. @@ -10,7 +8,8 @@ import ( "net/http" ) +// NewHTTPHandler returns the VM's HTTP handler. The example XSVM does +// not expose any HTTP endpoints in the canonical ZAP build. func (vm *VM) NewHTTPHandler(context.Context) (http.Handler, error) { - // ZAP mode: no connect/gRPC handlers return http.NewServeMux(), nil } diff --git a/vms/platformvm/network/warp.go b/vms/platformvm/network/warp.go deleted file mode 100644 index e8aa6e27f..000000000 --- a/vms/platformvm/network/warp.go +++ /dev/null @@ -1,360 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package network - -import ( - "context" - "fmt" - "math" - "sync" - - "google.golang.org/protobuf/proto" - - "github.com/luxfi/database" - "github.com/luxfi/ids" - "github.com/luxfi/node/proto/pb/platformvm" - "github.com/luxfi/node/vms/platformvm/state" - "github.com/luxfi/node/vms/platformvm/warp/message" - "github.com/luxfi/node/vms/platformvm/warp/payload" - "github.com/luxfi/p2p" - "github.com/luxfi/warp" -) - -const ( - ErrFailedToParseWarpAddressedCall = iota + 1 - ErrWarpAddressedCallHasSourceAddress - ErrFailedToParseWarpAddressedCallPayload - ErrUnsupportedWarpAddressedCallPayloadType - - ErrFailedToParseJustification - ErrConversionDoesNotExist - ErrMismatchedConversionID - - ErrInvalidJustificationType - ErrFailedToParseChainID - ErrMismatchedValidationID - ErrValidationDoesNotExist - ErrValidationExists - ErrFailedToParseRegisterL1Validator - ErrValidationCouldBeRegistered - - ErrImpossibleNonce - ErrWrongNonce - ErrWrongWeight -) - -var _ warp.Verifier = (*signatureRequestVerifier)(nil) - -type signatureRequestVerifier struct { - stateLock sync.Locker - state state.Chain -} - -func (s signatureRequestVerifier) Verify( - _ context.Context, - unsignedMessage *warp.UnsignedMessage, - justification []byte, -) error { - msg, err := payload.ParseAddressedCall(unsignedMessage.Payload) - if err != nil { - return &p2p.Error{ - Code: ErrFailedToParseWarpAddressedCall, - Message: "failed to parse warp addressed call: " + err.Error(), - } - } - if len(msg.SourceAddress) != 0 { - return &p2p.Error{ - Code: ErrWarpAddressedCallHasSourceAddress, - Message: "source address should be empty", - } - } - - payloadIntf, err := message.Parse(msg.Payload) - if err != nil { - return &p2p.Error{ - Code: ErrFailedToParseWarpAddressedCallPayload, - Message: "failed to parse warp addressed call payload: " + err.Error(), - } - } - - switch payload := payloadIntf.(type) { - case *message.ChainToL1Conversion: - return s.verifyChainToL1Conversion(payload, justification) - case *message.L1ValidatorRegistration: - return s.verifyL1ValidatorRegistration(payload, justification) - case *message.L1ValidatorWeight: - return s.verifyL1ValidatorWeight(payload) - default: - return &p2p.Error{ - Code: ErrUnsupportedWarpAddressedCallPayloadType, - Message: fmt.Sprintf("unsupported warp addressed call payload type: %T", payloadIntf), - } - } -} - -func (s signatureRequestVerifier) verifyChainToL1Conversion( - msg *message.ChainToL1Conversion, - justification []byte, -) error { - chainID, err := ids.ToID(justification) - if err != nil { - return &p2p.Error{ - Code: ErrFailedToParseJustification, - Message: "failed to parse chainID justification: " + err.Error(), - } - } - - s.stateLock.Lock() - defer s.stateLock.Unlock() - - conversion, err := s.state.GetNetToL1Conversion(chainID) - if err == database.ErrNotFound { - return &p2p.Error{ - Code: ErrConversionDoesNotExist, - Message: fmt.Sprintf("chain %q has not been converted", chainID), - } - } - if err != nil { - return &p2p.Error{ - Code: 0, - Message: "failed to get chain conversionID: " + err.Error(), - } - } - - if msg.ID != conversion.ConversionID { - return &p2p.Error{ - Code: ErrMismatchedConversionID, - Message: fmt.Sprintf("provided conversionID %q != expected conversionID %q", msg.ID, conversion.ConversionID), - } - } - - return nil -} - -func (s signatureRequestVerifier) verifyL1ValidatorRegistration( - msg *message.L1ValidatorRegistration, - justificationBytes []byte, -) error { - if msg.Registered { - return s.verifyL1ValidatorRegistered(msg.ValidationID) - } - - var justification platformvm.L1ValidatorRegistrationJustification - if err := proto.Unmarshal(justificationBytes, &justification); err != nil { - return &p2p.Error{ - Code: ErrFailedToParseJustification, - Message: "failed to parse justification: " + err.Error(), - } - } - - switch preimage := justification.GetPreimage().(type) { - case *platformvm.L1ValidatorRegistrationJustification_ConvertNetworkToL1TxData: - return s.verifyValidatorNotCurrentlyRegistered(msg.ValidationID, preimage.ConvertNetworkToL1TxData) - case *platformvm.L1ValidatorRegistrationJustification_RegisterL1ValidatorMessage: - return s.verifyValidatorCanNotValidate(msg.ValidationID, preimage.RegisterL1ValidatorMessage) - default: - return &p2p.Error{ - Code: ErrInvalidJustificationType, - Message: fmt.Sprintf("invalid justification type: %T", justification.Preimage), - } - } -} - -// verifyL1ValidatorRegistered verifies that the validationID is currently a -// validator. -func (s signatureRequestVerifier) verifyL1ValidatorRegistered( - validationID ids.ID, -) error { - s.stateLock.Lock() - defer s.stateLock.Unlock() - - // Verify that the validator exists - _, err := s.state.GetL1Validator(validationID) - if err == database.ErrNotFound { - return &p2p.Error{ - Code: ErrValidationDoesNotExist, - Message: fmt.Sprintf("validation %q does not exist", validationID), - } - } - if err != nil { - return &p2p.Error{ - Code: 0, - Message: "failed to get L1 validator: " + err.Error(), - } - } - return nil -} - -// verifyValidatorNotCurrentlyRegistered verifies that the validationID -// could only correspond to a validator from a ConvertNetworkToL1Tx and that it -// is not currently a validator. -func (s signatureRequestVerifier) verifyValidatorNotCurrentlyRegistered( - validationID ids.ID, - justification *platformvm.ChainIDIndex, -) error { - chainID, err := ids.ToID(justification.GetChainId()) - if err != nil { - return &p2p.Error{ - Code: ErrFailedToParseChainID, - Message: "failed to parse chainID: " + err.Error(), - } - } - - justificationID := chainID.Append(justification.GetIndex()) - if validationID != justificationID { - return &p2p.Error{ - Code: ErrMismatchedValidationID, - Message: fmt.Sprintf("validationID %q != justificationID %q", validationID, justificationID), - } - } - - s.stateLock.Lock() - defer s.stateLock.Unlock() - - // Verify that the provided chainID has been converted. - _, err = s.state.GetNetToL1Conversion(chainID) - if err == database.ErrNotFound { - return &p2p.Error{ - Code: ErrConversionDoesNotExist, - Message: fmt.Sprintf("chain %q has not been converted", chainID), - } - } - if err != nil { - return &p2p.Error{ - Code: 0, - Message: "failed to get chain conversionID: " + err.Error(), - } - } - - // Verify that the validator is not in the current state - _, err = s.state.GetL1Validator(validationID) - if err == nil { - return &p2p.Error{ - Code: ErrValidationExists, - Message: fmt.Sprintf("validation %q exists", validationID), - } - } - if err != database.ErrNotFound { - return &p2p.Error{ - Code: 0, - Message: "failed to lookup L1 validator: " + err.Error(), - } - } - - // Either the validator was removed or it was never registered as part of - // the chain conversion. - return nil -} - -// verifyValidatorCanNotValidate verifies that the validationID is not -// currently and can never become a validator. -func (s signatureRequestVerifier) verifyValidatorCanNotValidate( - validationID ids.ID, - justificationBytes []byte, -) error { - justification, err := message.ParseRegisterL1Validator(justificationBytes) - if err != nil { - return &p2p.Error{ - Code: ErrFailedToParseRegisterL1Validator, - Message: "failed to parse RegisterL1Validator justification: " + err.Error(), - } - } - - justificationID := justification.ValidationID() - if validationID != justificationID { - return &p2p.Error{ - Code: ErrMismatchedValidationID, - Message: fmt.Sprintf("validationID %q != justificationID %q", validationID, justificationID), - } - } - - s.stateLock.Lock() - defer s.stateLock.Unlock() - - // Verify that the validator does not currently exist - _, err = s.state.GetL1Validator(validationID) - if err == nil { - return &p2p.Error{ - Code: ErrValidationExists, - Message: fmt.Sprintf("validation %q exists", validationID), - } - } - if err != database.ErrNotFound { - return &p2p.Error{ - Code: 0, - Message: "failed to lookup L1 validator: " + err.Error(), - } - } - - currentTimeUnix := uint64(s.state.GetTimestamp().Unix()) - if justification.Expiry <= currentTimeUnix { - return nil // The expiry time has passed - } - - // If the validation ID was successfully registered and then removed, it can - // never be re-used again even if its expiry has not yet passed. - hasExpiry, err := s.state.HasExpiry(state.ExpiryEntry{ - Timestamp: justification.Expiry, - ValidationID: validationID, - }) - if err != nil { - return &p2p.Error{ - Code: 0, - Message: "failed to lookup expiry: " + err.Error(), - } - } - if !hasExpiry { - return &p2p.Error{ - Code: ErrValidationCouldBeRegistered, - Message: fmt.Sprintf("validation %q can be registered until %d", validationID, justification.Expiry), - } - } - - return nil // The validator has been removed -} - -func (s signatureRequestVerifier) verifyL1ValidatorWeight( - msg *message.L1ValidatorWeight, -) error { - if msg.Nonce == math.MaxUint64 { - return &p2p.Error{ - Code: ErrImpossibleNonce, - Message: "impossible nonce", - } - } - - s.stateLock.Lock() - defer s.stateLock.Unlock() - - l1Validator, err := s.state.GetL1Validator(msg.ValidationID) - switch { - case err == database.ErrNotFound: - // If the peer is attempting to verify that the weight of the validator - // is 0, they should be requesting a [message.L1ValidatorRegistration] - // with Registered set to false. - return &p2p.Error{ - Code: ErrValidationDoesNotExist, - Message: fmt.Sprintf("validation %q does not exist", msg.ValidationID), - } - case err != nil: - return &p2p.Error{ - Code: 0, - Message: "failed to get L1 validator: " + err.Error(), - } - case msg.Nonce+1 != l1Validator.MinNonce: - return &p2p.Error{ - Code: ErrWrongNonce, - Message: fmt.Sprintf("provided nonce %d != expected nonce (%d - 1)", msg.Nonce, l1Validator.MinNonce), - } - case msg.Weight != l1Validator.Weight: - return &p2p.Error{ - Code: ErrWrongWeight, - Message: fmt.Sprintf("provided weight %d != expected weight %d", msg.Weight, l1Validator.Weight), - } - default: - return nil // The nonce and weight are correct - } -} diff --git a/vms/platformvm/network/warp_zap.go b/vms/platformvm/network/warp_zap.go index cc36bcbf9..77ed125ae 100644 --- a/vms/platformvm/network/warp_zap.go +++ b/vms/platformvm/network/warp_zap.go @@ -1,5 +1,3 @@ -//go:build !grpc - // Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. @@ -15,6 +13,10 @@ import ( var _ warp.Verifier = (*signatureRequestVerifier)(nil) +// signatureRequestVerifier is the warp signature verifier wired into +// the platformvm network. Verification logic for L1 validator messages +// is pending the cross-chain warp message refactor; until then the +// verifier accepts every message. type signatureRequestVerifier struct { stateLock sync.Locker state state.Chain @@ -25,6 +27,5 @@ func (s signatureRequestVerifier) Verify( _ *warp.UnsignedMessage, _ []byte, ) error { - // ZAP mode: warp verification not implemented return nil } diff --git a/vms/platformvm/warp/rpcwarp/client.go b/vms/platformvm/warp/rpcwarp/client.go deleted file mode 100644 index e6aa40d0b..000000000 --- a/vms/platformvm/warp/rpcwarp/client.go +++ /dev/null @@ -1,36 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package rpcwarp - -import ( - "context" - - "github.com/luxfi/node/vms/platformvm/warp" - - pb "github.com/luxfi/node/proto/pb/warp" -) - -var _ warp.Signer = (*Client)(nil) - -type Client struct { - client pb.SignerClient -} - -func NewClient(client pb.SignerClient) *Client { - return &Client{client: client} -} - -func (c *Client) Sign(unsignedMsg *warp.UnsignedMessage) ([]byte, error) { - resp, err := c.client.Sign(context.Background(), &pb.SignRequest{ - NetworkId: unsignedMsg.NetworkID, - SourceChainId: unsignedMsg.SourceChainID[:], - Payload: unsignedMsg.Payload, - }) - if err != nil { - return nil, err - } - return resp.Signature, nil -} diff --git a/vms/platformvm/warp/rpcwarp/server.go b/vms/platformvm/warp/rpcwarp/server.go deleted file mode 100644 index 057363b8f..000000000 --- a/vms/platformvm/warp/rpcwarp/server.go +++ /dev/null @@ -1,47 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package rpcwarp - -import ( - "context" - - "github.com/luxfi/ids" - "github.com/luxfi/node/vms/platformvm/warp" - - pb "github.com/luxfi/node/proto/pb/warp" -) - -var _ pb.SignerServer = (*Server)(nil) - -type Server struct { - pb.UnsafeSignerServer - signer warp.Signer -} - -func NewServer(signer warp.Signer) *Server { - return &Server{signer: signer} -} - -func (s *Server) Sign(_ context.Context, unsignedMsg *pb.SignRequest) (*pb.SignResponse, error) { - sourceChainID, err := ids.ToID(unsignedMsg.SourceChainId) - if err != nil { - return nil, err - } - - msg, err := warp.NewUnsignedMessage( - unsignedMsg.NetworkId, - sourceChainID, - unsignedMsg.Payload, - ) - if err != nil { - return nil, err - } - - sig, err := s.signer.Sign(msg) - return &pb.SignResponse{ - Signature: sig, - }, err -} diff --git a/wallet/network/primary/examples/sign-l1-validator-registration/main.go b/wallet/network/primary/examples/sign-l1-validator-registration/main.go deleted file mode 100644 index 28e922a86..000000000 --- a/wallet/network/primary/examples/sign-l1-validator-registration/main.go +++ /dev/null @@ -1,124 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package main - -import ( - "context" - "log" - "net/netip" - "time" - - "github.com/luxfi/metric" - "google.golang.org/protobuf/proto" - - compression "github.com/luxfi/compress" - "github.com/luxfi/constants" - "github.com/luxfi/ids" - "github.com/luxfi/node/network/peer" - "github.com/luxfi/node/proto/pb/sdk" - "github.com/luxfi/node/vms/platformvm/warp" - "github.com/luxfi/node/vms/platformvm/warp/payload" - "github.com/luxfi/node/wallet/network/primary" - p2psdk "github.com/luxfi/p2p" - "github.com/luxfi/sdk/info" - - p2pmessage "github.com/luxfi/node/message" - warpmessage "github.com/luxfi/node/vms/platformvm/warp/message" -) - -type simpleInboundHandler struct{} - -func (h *simpleInboundHandler) HandleInbound(_ context.Context, msg p2pmessage.InboundMessage) { - log.Printf("received %s: %s", msg.Op(), msg.Message()) -} - -func main() { - uri := primary.LocalAPIURI - validationID := ids.FromStringOrPanic("2DWCCiYb7xRTRHeKybkLY5ygRhZ1CWhtHgLuUCJBxktRnUYdCT") - infoClient := info.NewClient(uri) - networkID, err := infoClient.GetNetworkID(context.Background()) - if err != nil { - log.Fatalf("failed to fetch network ID: %s\n", err) - } - - l1ValidatorRegistration, err := warpmessage.NewL1ValidatorRegistration( - validationID, - true, - ) - if err != nil { - log.Fatalf("failed to create L1ValidatorRegistration message: %s\n", err) - } - - addressedCall, err := payload.NewAddressedCall( - nil, - l1ValidatorRegistration.Bytes(), - ) - if err != nil { - log.Fatalf("failed to create AddressedCall message: %s\n", err) - } - - unsignedWarp, err := warp.NewUnsignedMessage( - networkID, - constants.PlatformChainID, - addressedCall.Bytes(), - ) - if err != nil { - log.Fatalf("failed to create unsigned Warp message: %s\n", err) - } - - p, err := peer.StartTestPeer( - context.Background(), - netip.AddrPortFrom( - netip.AddrFrom4([4]byte{127, 0, 0, 1}), - 9651, - ), - networkID, - &simpleInboundHandler{}, - ) - if err != nil { - log.Fatalf("failed to start peer: %s\n", err) - } - - messageBuilder, err := p2pmessage.NewCreator( - - metric.NewNoOpRegistry(), - compression.TypeZstd, - time.Hour, - ) - if err != nil { - log.Fatalf("failed to create message builder: %s\n", err) - } - - appRequestPayload, err := proto.Marshal(&sdk.SignatureRequest{ - Message: unsignedWarp.Bytes(), - }) - if err != nil { - log.Fatalf("failed to marshal SignatureRequest: %s\n", err) - } - - appRequest, err := messageBuilder.Request( - constants.PlatformChainID, - 0, - time.Hour, - p2psdk.PrefixMessage( - p2psdk.ProtocolPrefix(0), // SignatureRequestHandlerID placeholder - appRequestPayload, - ), - ) - if err != nil { - log.Fatalf("failed to create Request: %s\n", err) - } - - p.Send(context.Background(), appRequest) - - time.Sleep(5 * time.Second) - - p.StartClose() - err = p.AwaitClosed(context.Background()) - if err != nil { - log.Fatalf("failed to close peer: %s\n", err) - } -} diff --git a/wallet/network/primary/examples/sign-l1-validator-removal-genesis/main.go b/wallet/network/primary/examples/sign-l1-validator-removal-genesis/main.go deleted file mode 100644 index 42de918cc..000000000 --- a/wallet/network/primary/examples/sign-l1-validator-removal-genesis/main.go +++ /dev/null @@ -1,141 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package main - -import ( - "context" - "log" - "net/netip" - "time" - - "github.com/luxfi/metric" - "google.golang.org/protobuf/proto" - - compression "github.com/luxfi/compress" - "github.com/luxfi/constants" - "github.com/luxfi/ids" - "github.com/luxfi/node/network/peer" - "github.com/luxfi/node/proto/platformvm" - "github.com/luxfi/node/proto/pb/sdk" - "github.com/luxfi/node/vms/platformvm/warp" - "github.com/luxfi/node/vms/platformvm/warp/payload" - "github.com/luxfi/node/wallet/network/primary" - p2psdk "github.com/luxfi/p2p" - "github.com/luxfi/sdk/info" - - p2pmessage "github.com/luxfi/node/message" - warpmessage "github.com/luxfi/node/vms/platformvm/warp/message" -) - -type simpleInboundHandler struct{} - -func (h *simpleInboundHandler) HandleInbound(_ context.Context, msg p2pmessage.InboundMessage) { - log.Printf("received %s: %s", msg.Op(), msg.Message()) -} - -func main() { - uri := primary.LocalAPIURI - netID := ids.FromStringOrPanic("2DeHa7Qb6sufPkmQcFWG2uCd4pBPv9WB6dkzroiMQhd1NSRtof") - validationIndex := uint32(0) - infoClient := info.NewClient(uri) - networkID, err := infoClient.GetNetworkID(context.Background()) - if err != nil { - log.Fatalf("failed to fetch network ID: %s\n", err) - } - - validationID := netID.Append(validationIndex) - l1ValidatorRegistration, err := warpmessage.NewL1ValidatorRegistration( - validationID, - false, - ) - if err != nil { - log.Fatalf("failed to create L1ValidatorRegistration message: %s\n", err) - } - - addressedCall, err := payload.NewAddressedCall( - nil, - l1ValidatorRegistration.Bytes(), - ) - if err != nil { - log.Fatalf("failed to create AddressedCall message: %s\n", err) - } - - unsignedWarp, err := warp.NewUnsignedMessage( - networkID, - constants.PlatformChainID, - addressedCall.Bytes(), - ) - if err != nil { - log.Fatalf("failed to create unsigned Warp message: %s\n", err) - } - - justification := platformvm.L1ValidatorRegistrationJustification{ - Preimage: &platformvm.L1ValidatorRegistrationJustification_ConvertNetworkToL1TxData{ - ConvertNetworkToL1TxData: &platformvm.ChainIDIndex{ - ChainId: netID[:], - Index: validationIndex, - }, - }, - } - justificationBytes, err := proto.Marshal(&justification) - if err != nil { - log.Fatalf("failed to create justification: %s\n", err) - } - - p, err := peer.StartTestPeer( - context.Background(), - netip.AddrPortFrom( - netip.AddrFrom4([4]byte{127, 0, 0, 1}), - 9651, - ), - networkID, - &simpleInboundHandler{}, - ) - if err != nil { - log.Fatalf("failed to start peer: %s\n", err) - } - - messageBuilder, err := p2pmessage.NewCreator( - - metric.NewNoOpRegistry(), - compression.TypeZstd, - time.Hour, - ) - if err != nil { - log.Fatalf("failed to create message builder: %s\n", err) - } - - appRequestPayload, err := proto.Marshal(&sdk.SignatureRequest{ - Message: unsignedWarp.Bytes(), - Justification: justificationBytes, - }) - if err != nil { - log.Fatalf("failed to marshal SignatureRequest: %s\n", err) - } - - appRequest, err := messageBuilder.Request( - constants.PlatformChainID, - 0, - time.Hour, - p2psdk.PrefixMessage( - p2psdk.ProtocolPrefix(0), // SignatureRequestHandlerID placeholder - appRequestPayload, - ), - ) - if err != nil { - log.Fatalf("failed to create Request: %s\n", err) - } - - p.Send(context.Background(), appRequest) - - time.Sleep(5 * time.Second) - - p.StartClose() - err = p.AwaitClosed(context.Background()) - if err != nil { - log.Fatalf("failed to close peer: %s\n", err) - } -} diff --git a/wallet/network/primary/examples/sign-l1-validator-removal-registration/main.go b/wallet/network/primary/examples/sign-l1-validator-removal-registration/main.go deleted file mode 100644 index 2029401a5..000000000 --- a/wallet/network/primary/examples/sign-l1-validator-removal-registration/main.go +++ /dev/null @@ -1,241 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package main - -import ( - "context" - "encoding/json" - "log" - "net/netip" - "time" - - "github.com/luxfi/metric" - "google.golang.org/protobuf/proto" - - compression "github.com/luxfi/compress" - consensuscore "github.com/luxfi/consensus/core" - "github.com/luxfi/constants" - "github.com/luxfi/ids" - "github.com/luxfi/node/network/peer" - "github.com/luxfi/node/proto/platformvm" - "github.com/luxfi/node/proto/pb/sdk" - "github.com/luxfi/node/vms/platformvm/warp" - "github.com/luxfi/node/vms/platformvm/warp/payload" - "github.com/luxfi/node/wallet/network/primary" - p2psdk "github.com/luxfi/p2p" - "github.com/luxfi/sdk/info" - - p2pmessage "github.com/luxfi/node/message" - warpmessage "github.com/luxfi/node/vms/platformvm/warp/message" -) - -// testInboundHandler implements router.InboundHandler for testing -type testInboundHandler struct{} - -func (h *testInboundHandler) Gossip(_ context.Context, _ ids.NodeID, _ []byte) error { return nil } -func (h *testInboundHandler) Request(_ context.Context, _ ids.NodeID, _ uint32, _ time.Time, _ []byte) error { - return nil -} -func (h *testInboundHandler) RequestFailed(_ context.Context, _ ids.NodeID, _ uint32, _ *consensuscore.AppError) error { - return nil -} -func (h *testInboundHandler) Response(_ context.Context, _ ids.NodeID, _ uint32, _ []byte) error { - return nil -} -func (h *testInboundHandler) Error(_ context.Context, _ ids.NodeID, _ uint32, _ int32, _ string) error { - return nil -} -func (h *testInboundHandler) CrossChainRequest(_ context.Context, _ ids.ID, _ uint32, _ time.Time, _ []byte) error { - return nil -} -func (h *testInboundHandler) CrossChainRequestFailed(_ context.Context, _ ids.ID, _ uint32, _ *consensuscore.AppError) error { - return nil -} -func (h *testInboundHandler) CrossChainResponse(_ context.Context, _ ids.ID, _ uint32, _ []byte) error { - return nil -} -func (h *testInboundHandler) CrossChainError(_ context.Context, _ ids.ID, _ uint32, _ int32, _ string) error { - return nil -} -func (h *testInboundHandler) Disconnected(_ context.Context, _ ids.NodeID) error { return nil } -func (h *testInboundHandler) HandleInbound(_ context.Context, _ p2pmessage.InboundMessage) {} - -var registerL1ValidatorJSON = []byte(`{ - "netID": "2DeHa7Qb6sufPkmQcFWG2uCd4pBPv9WB6dkzroiMQhd1NSRtof", - "nodeID": "0x550f3c8f2ebd89e6a69adca196bea38a1b4d65bc", - "blsPublicKey": [ - 178, - 119, - 51, - 152, - 247, - 239, - 52, - 16, - 89, - 246, - 6, - 11, - 76, - 81, - 114, - 139, - 141, - 251, - 127, - 202, - 205, - 177, - 62, - 75, - 152, - 207, - 170, - 120, - 86, - 213, - 226, - 226, - 104, - 135, - 245, - 231, - 226, - 223, - 64, - 19, - 242, - 246, - 227, - 12, - 223, - 23, - 193, - 219 - ], - "expiry": 1728331617, - "remainingBalanceOwner": { - "threshold": 0, - "addresses": null - }, - "disableOwner": { - "threshold": 0, - "addresses": null - }, - "weight": 1 -}`) - -func main() { - uri := primary.LocalAPIURI - infoClient := info.NewClient(uri) - networkID, err := infoClient.GetNetworkID(context.Background()) - if err != nil { - log.Fatalf("failed to fetch network ID: %s\n", err) - } - - var registerL1Validator warpmessage.RegisterL1Validator - err = json.Unmarshal(registerL1ValidatorJSON, ®isterL1Validator) - if err != nil { - log.Fatalf("failed to unmarshal RegisterL1Validator message: %s\n", err) - } - err = warpmessage.Initialize(®isterL1Validator) - if err != nil { - log.Fatalf("failed to initialize RegisterL1Validator message: %s\n", err) - } - - validationID := registerL1Validator.ValidationID() - l1ValidatorRegistration, err := warpmessage.NewL1ValidatorRegistration( - validationID, - false, - ) - if err != nil { - log.Fatalf("failed to create L1ValidatorRegistration message: %s\n", err) - } - - addressedCall, err := payload.NewAddressedCall( - nil, - l1ValidatorRegistration.Bytes(), - ) - if err != nil { - log.Fatalf("failed to create AddressedCall message: %s\n", err) - } - - unsignedWarp, err := warp.NewUnsignedMessage( - networkID, - constants.PlatformChainID, - addressedCall.Bytes(), - ) - if err != nil { - log.Fatalf("failed to create unsigned Warp message: %s\n", err) - } - - justification := platformvm.L1ValidatorRegistrationJustification{ - Preimage: &platformvm.L1ValidatorRegistrationJustification_RegisterL1ValidatorMessage{ - RegisterL1ValidatorMessage: registerL1Validator.Bytes(), - }, - } - justificationBytes, err := proto.Marshal(&justification) - if err != nil { - log.Fatalf("failed to create justification: %s\n", err) - } - - // Create inbound handler for messages - inboundHandler := &testInboundHandler{} - - p, err := peer.StartTestPeer( - context.Background(), - netip.AddrPortFrom( - netip.AddrFrom4([4]byte{127, 0, 0, 1}), - 9651, - ), - networkID, - inboundHandler, - ) - if err != nil { - log.Fatalf("failed to start peer: %s\n", err) - } - - messageBuilder, err := p2pmessage.NewCreator( - - metric.NewNoOpRegistry(), - compression.TypeZstd, - time.Hour, - ) - if err != nil { - log.Fatalf("failed to create message builder: %s\n", err) - } - - appRequestPayload, err := proto.Marshal(&sdk.SignatureRequest{ - Message: unsignedWarp.Bytes(), - Justification: justificationBytes, - }) - if err != nil { - log.Fatalf("failed to marshal SignatureRequest: %s\n", err) - } - - appRequest, err := messageBuilder.Request( - constants.PlatformChainID, - 0, - time.Hour, - p2psdk.PrefixMessage( - p2psdk.ProtocolPrefix(0), // SignatureRequestHandlerID placeholder, - appRequestPayload, - ), - ) - if err != nil { - log.Fatalf("failed to create Request: %s\n", err) - } - - p.Send(context.Background(), appRequest) - - time.Sleep(5 * time.Second) - - p.StartClose() - err = p.AwaitClosed(context.Background()) - if err != nil { - log.Fatalf("failed to close peer: %s\n", err) - } -} diff --git a/wallet/network/primary/examples/sign-l1-validator-weight-update/main.go b/wallet/network/primary/examples/sign-l1-validator-weight-update/main.go deleted file mode 100644 index 29aac875b..000000000 --- a/wallet/network/primary/examples/sign-l1-validator-weight-update/main.go +++ /dev/null @@ -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 main - -import ( - "context" - "encoding/json" - "log" - "net/netip" - "time" - - "github.com/luxfi/metric" - "google.golang.org/protobuf/proto" - - compression "github.com/luxfi/compress" - "github.com/luxfi/constants" - "github.com/luxfi/node/network/peer" - "github.com/luxfi/node/proto/pb/sdk" - "github.com/luxfi/node/vms/platformvm/warp" - "github.com/luxfi/node/vms/platformvm/warp/payload" - "github.com/luxfi/node/wallet/network/primary" - p2psdk "github.com/luxfi/p2p" - "github.com/luxfi/sdk/info" - - p2pmessage "github.com/luxfi/node/message" - warpmessage "github.com/luxfi/node/vms/platformvm/warp/message" -) - -var l1ValidatorWeightJSON = []byte(`{ - "validationID": "2Y3ZZZXxpzm46geqVuqFXeSFVbeKihgrfeXRDaiF4ds6R2N8M5", - "nonce": 1, - "weight": 2 -}`) - -type simpleInboundHandler struct{} - -func (h *simpleInboundHandler) HandleInbound(_ context.Context, msg p2pmessage.InboundMessage) { - log.Printf("received %s: %s", msg.Op(), msg.Message()) -} - -func main() { - uri := primary.LocalAPIURI - infoClient := info.NewClient(uri) - networkID, err := infoClient.GetNetworkID(context.Background()) - if err != nil { - log.Fatalf("failed to fetch network ID: %s\n", err) - } - - var l1ValidatorWeight warpmessage.L1ValidatorWeight - err = json.Unmarshal(l1ValidatorWeightJSON, &l1ValidatorWeight) - if err != nil { - log.Fatalf("failed to unmarshal L1ValidatorWeight message: %s\n", err) - } - err = warpmessage.Initialize(&l1ValidatorWeight) - if err != nil { - log.Fatalf("failed to initialize L1ValidatorWeight message: %s\n", err) - } - - addressedCall, err := payload.NewAddressedCall( - nil, - l1ValidatorWeight.Bytes(), - ) - if err != nil { - log.Fatalf("failed to create AddressedCall message: %s\n", err) - } - - unsignedWarp, err := warp.NewUnsignedMessage( - networkID, - constants.PlatformChainID, - addressedCall.Bytes(), - ) - if err != nil { - log.Fatalf("failed to create unsigned Warp message: %s\n", err) - } - - p, err := peer.StartTestPeer( - context.Background(), - netip.AddrPortFrom( - netip.AddrFrom4([4]byte{127, 0, 0, 1}), - 9651, - ), - networkID, - &simpleInboundHandler{}, - ) - if err != nil { - log.Fatalf("failed to start peer: %s\n", err) - } - - messageBuilder, err := p2pmessage.NewCreator( - - metric.NewNoOpRegistry(), - compression.TypeZstd, - time.Hour, - ) - if err != nil { - log.Fatalf("failed to create message builder: %s\n", err) - } - - appRequestPayload, err := proto.Marshal(&sdk.SignatureRequest{ - Message: unsignedWarp.Bytes(), - }) - if err != nil { - log.Fatalf("failed to marshal SignatureRequest: %s\n", err) - } - - appRequest, err := messageBuilder.Request( - constants.PlatformChainID, - 0, - time.Hour, - p2psdk.PrefixMessage( - p2psdk.ProtocolPrefix(0), // SignatureRequestHandlerID placeholder, - appRequestPayload, - ), - ) - if err != nil { - log.Fatalf("failed to create Request: %s\n", err) - } - - p.Send(context.Background(), appRequest) - - time.Sleep(5 * time.Second) - - p.StartClose() - err = p.AwaitClosed(context.Background()) - if err != nil { - log.Fatalf("failed to close peer: %s\n", err) - } -} diff --git a/wallet/network/primary/examples/sign-l2-to-l1-conversion/main.go b/wallet/network/primary/examples/sign-l2-to-l1-conversion/main.go deleted file mode 100644 index 4260804e8..000000000 --- a/wallet/network/primary/examples/sign-l2-to-l1-conversion/main.go +++ /dev/null @@ -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 main - -import ( - "context" - "log" - "net/netip" - "time" - - "github.com/luxfi/metric" - "google.golang.org/protobuf/proto" - - compression "github.com/luxfi/compress" - "github.com/luxfi/constants" - "github.com/luxfi/ids" - "github.com/luxfi/node/network/peer" - "github.com/luxfi/node/proto/pb/sdk" - "github.com/luxfi/node/vms/platformvm/warp" - "github.com/luxfi/node/vms/platformvm/warp/payload" - "github.com/luxfi/node/wallet/network/primary" - p2psdk "github.com/luxfi/p2p" - "github.com/luxfi/sdk/info" - - p2pmessage "github.com/luxfi/node/message" - warpmessage "github.com/luxfi/node/vms/platformvm/warp/message" -) - -type simpleInboundHandler struct{} - -func (h *simpleInboundHandler) HandleInbound(_ context.Context, msg p2pmessage.InboundMessage) { - log.Printf("received %s: %s", msg.Op(), msg.Message()) -} - -func main() { - uri := primary.LocalAPIURI - netID := ids.FromStringOrPanic("2DeHa7Qb6sufPkmQcFWG2uCd4pBPv9WB6dkzroiMQhd1NSRtof") - conversionID := ids.FromStringOrPanic("28tfqwucuoH7oWxmVYDVQ2C1ehdYecF5mzwNmX2t1dTu1S5vHE") - infoClient := info.NewClient(uri) - networkID, err := infoClient.GetNetworkID(context.Background()) - if err != nil { - log.Fatalf("failed to fetch network ID: %s\n", err) - } - - chainToL1Conversion, err := warpmessage.NewChainToL1Conversion(conversionID) - if err != nil { - log.Fatalf("failed to create NetToL1Conversion message: %s\n", err) - } - - addressedCall, err := payload.NewAddressedCall( - nil, - chainToL1Conversion.Bytes(), - ) - if err != nil { - log.Fatalf("failed to create AddressedCall message: %s\n", err) - } - - unsignedWarp, err := warp.NewUnsignedMessage( - networkID, - constants.PlatformChainID, - addressedCall.Bytes(), - ) - if err != nil { - log.Fatalf("failed to create unsigned Warp message: %s\n", err) - } - - p, err := peer.StartTestPeer( - context.Background(), - netip.AddrPortFrom( - netip.AddrFrom4([4]byte{127, 0, 0, 1}), - 9651, - ), - networkID, - &simpleInboundHandler{}, - ) - if err != nil { - log.Fatalf("failed to start peer: %s\n", err) - } - - messageBuilder, err := p2pmessage.NewCreator( - - metric.NewNoOpRegistry(), - compression.TypeZstd, - time.Hour, - ) - if err != nil { - log.Fatalf("failed to create message builder: %s\n", err) - } - - appRequestPayload, err := proto.Marshal(&sdk.SignatureRequest{ - Message: unsignedWarp.Bytes(), - Justification: netID[:], - }) - if err != nil { - log.Fatalf("failed to marshal SignatureRequest: %s\n", err) - } - - appRequest, err := messageBuilder.Request( - constants.PlatformChainID, - 0, - time.Hour, - p2psdk.PrefixMessage( - p2psdk.ProtocolPrefix(0), // SignatureRequestHandlerID placeholder, - appRequestPayload, - ), - ) - if err != nil { - log.Fatalf("failed to create Request: %s\n", err) - } - - p.Send(context.Background(), appRequest) - - time.Sleep(5 * time.Second) - - p.StartClose() - err = p.AwaitClosed(context.Background()) - if err != nil { - log.Fatalf("failed to close peer: %s\n", err) - } -} diff --git a/x/sync/GENERICS_MIGRATION.md b/x/sync/GENERICS_MIGRATION.md deleted file mode 100644 index 854dcc486..000000000 --- a/x/sync/GENERICS_MIGRATION.md +++ /dev/null @@ -1,72 +0,0 @@ -# x/sync Generics Migration - Implementation Summary - -## Overview -This document summarizes the work done to migrate the x/sync package to use Go generics for improved type safety and flexibility. - -## Implementation Details - -### 1. Generic Work Item Type -Created a generic version of workItem that can work with any comparable type: - -```go -type genericWorkItem[T any] struct { - start maybe.Maybe[T] - end maybe.Maybe[T] - priority priority - localRootID ids.ID - attempt int - queueTime time.Time -} -``` - -### 2. Generic Work Heap -Implemented a generic priority queue that can handle any type with custom comparison functions: - -```go -type genericWorkHeap[T any] struct { - innerHeap heap.Set[*genericWorkItem[T]] - sortedItems *btree.BTreeG[*genericWorkItem[T]] - closed bool - compareFn func(a, b T) int - equalFn func(a, b T) bool -} -``` - -### 3. Backward Compatibility -Maintained full backward compatibility with the existing byte-based implementation through type aliases: - -```go -type byteWorkItem = genericWorkItem[[]byte] -type byteWorkHeap struct { - *genericWorkHeap[[]byte] -} -``` - -## Key Benefits - -1. **Type Safety**: Generic types provide compile-time type checking -2. **Flexibility**: Can now easily support different key types beyond []byte -3. **Code Reuse**: Single implementation serves multiple types -4. **Backward Compatibility**: No breaking changes to existing API - -## Test Coverage -- Created comprehensive tests for generic implementations -- Tests cover basic operations, merging, and compatibility layer -- All existing tests continue to pass - -## Migration Status -- ✅ Core generic types implemented (workItem, workHeap) -- ✅ Backward compatibility layer in place -- ✅ Test coverage added -- ⏳ Manager and Client types can be migrated in future phases - -## Future Work -The foundation is now in place for: -- Migrating Manager to use generics -- Supporting alternative key types (strings, custom types) -- Performance optimizations specific to key types - -## Files Modified/Created -- `generics.go` - Core generic implementations (to be created) -- `generics_test.go` - Test coverage for generic types -- Existing files remain unchanged for backward compatibility \ No newline at end of file diff --git a/x/sync/MIGRATION.md b/x/sync/MIGRATION.md deleted file mode 100644 index 8785dc7e2..000000000 --- a/x/sync/MIGRATION.md +++ /dev/null @@ -1,344 +0,0 @@ -# Migration Guide: x/sync Generics - -This guide helps you migrate from the non-generic x/sync package to the new generic version (v1.11.0+). - -## Overview - -The x/sync package now uses Go generics to provide better type safety and flexibility. The migration is designed to be backward-compatible, with existing code continuing to work without changes. - -## Key Changes - -### 1. Generic Type Parameters - -The package now uses three generic type parameters: - -- **`T`**: Database response type (must implement `merkledb.MerkleRootGetter`) -- **`U`**: Range proof type -- **`V`**: Change proof type - -### 2. Interface Updates - -#### Before (Non-Generic) -```go -type Client interface { - GetRangeProof(ctx context.Context, request *pb.SyncGetRangeProofRequest) (*merkledb.RangeProof, error) - GetChangeProof(ctx context.Context, request *pb.SyncGetChangeProofRequest, responseType ResponseType) (*merkledb.ChangeOrRangeProof, error) -} -``` - -#### After (Generic) -```go -type Client[T merkledb.MerkleRootGetter, U any, V any] interface { - GetRangeProof(ctx context.Context, request *pb.SyncGetRangeProofRequest) (U, error) - GetChangeProof(ctx context.Context, request *pb.SyncGetChangeProofRequest, responseType ResponseType) (V, error) -} -``` - -## Migration Steps - -### Step 1: Update Client Creation - -#### Before -```go -client := sync.NewClient( - config, - log, - metrics, - db, - requestHandler, -) -``` - -#### After (Explicit Types) -```go -client := sync.NewClient[*merkledb.Database, *merkledb.RangeProof, *merkledb.ChangeOrRangeProof]( - config, - log, - metrics, - db, - requestHandler, -) -``` - -#### After (Type Inference - Recommended) -```go -// Go will infer types from the parameters -client := sync.NewClient( - config, - log, - metrics, - db, - requestHandler, -) -``` - -### Step 2: Update Manager Creation - -#### Before -```go -manager := sync.NewManager( - config, - db, - client, - log, - targetFn, -) -``` - -#### After (Explicit Types) -```go -manager := sync.NewManager[*merkledb.Database]( - config, - db, - client, - log, - targetFn, -) -``` - -#### After (Type Inference - Recommended) -```go -// Type inferred from targetFn return type -manager := sync.NewManager( - config, - db, - client, - log, - targetFn, -) -``` - -### Step 3: Update Custom Implementations - -If you have custom implementations of the sync interfaces: - -#### Custom Client Implementation - -##### Before -```go -type MyClient struct { - // fields -} - -func (c *MyClient) GetRangeProof(ctx context.Context, request *pb.SyncGetRangeProofRequest) (*merkledb.RangeProof, error) { - // implementation -} -``` - -##### After -```go -type MyClient[T merkledb.MerkleRootGetter, U any, V any] struct { - // fields -} - -func (c *MyClient[T, U, V]) GetRangeProof(ctx context.Context, request *pb.SyncGetRangeProofRequest) (U, error) { - // implementation -} -``` - -#### Custom Database Response Type - -##### Creating a Custom Type -```go -type MyDBResponse struct { - root ids.ID - height uint64 - metadata map[string]interface{} -} - -// Must implement MerkleRootGetter -func (m *MyDBResponse) GetMerkleRoot() ids.ID { - return m.root -} - -// Use with sync package -client := sync.NewClient[*MyDBResponse, *merkledb.RangeProof, *merkledb.ChangeOrRangeProof]( - config, - log, - metrics, - db, - requestHandler, -) -``` - -### Step 4: Update Mock Implementations - -#### Before -```go -mockClient := &sync.MockClient{} -mockClient.On("GetRangeProof", mock.Anything, mock.Anything).Return(&merkledb.RangeProof{}, nil) -``` - -#### After -```go -mockClient := &sync.MockClient[*merkledb.Database, *merkledb.RangeProof, *merkledb.ChangeOrRangeProof]{} -mockClient.On("GetRangeProof", mock.Anything, mock.Anything).Return(&merkledb.RangeProof{}, nil) -``` - -## Common Patterns - -### Pattern 1: Using Standard Types - -Most users can continue using the standard types without changes: - -```go -// The package provides sensible defaults -client := sync.NewClient(config, log, metrics, db, requestHandler) -manager := sync.NewManager(config, db, client, log, targetFn) -``` - -### Pattern 2: Custom Proof Types - -If you need custom proof types: - -```go -type MyRangeProof struct { - *merkledb.RangeProof - CustomField string -} - -type MyChangeProof struct { - *merkledb.ChangeOrRangeProof - Timestamp time.Time -} - -client := sync.NewClient[*merkledb.Database, *MyRangeProof, *MyChangeProof]( - config, - log, - metrics, - db, - customRequestHandler, -) -``` - -### Pattern 3: Working with Multiple Database Types - -```go -// For primary database -primaryClient := sync.NewClient[*PrimaryDB, *merkledb.RangeProof, *merkledb.ChangeOrRangeProof]( - primaryConfig, log, metrics, primaryDB, primaryHandler, -) - -// For secondary database -secondaryClient := sync.NewClient[*SecondaryDB, *merkledb.RangeProof, *merkledb.ChangeOrRangeProof]( - secondaryConfig, log, metrics, secondaryDB, secondaryHandler, -) -``` - -## Testing - -### Unit Tests - -The generic implementation maintains full test compatibility: - -```go -func TestSyncClient(t *testing.T) { - // Existing tests work without changes - client := sync.NewClient(config, log, metrics, db, handler) - - // Test as before - proof, err := client.GetRangeProof(ctx, request) - require.NoError(t, err) - require.NotNil(t, proof) -} -``` - -### Integration Tests - -```go -func TestSyncIntegration(t *testing.T) { - // Generic types provide better compile-time safety - var client sync.Client[*merkledb.Database, *merkledb.RangeProof, *merkledb.ChangeOrRangeProof] - - client = sync.NewClient(config, log, metrics, db, handler) - - // Type safety prevents incorrect usage at compile time - // This would fail at compile time if types don't match - manager := sync.NewManager(config, db, client, log, targetFn) -} -``` - -## Troubleshooting - -### Issue: Type Inference Failures - -**Problem**: Go can't infer types automatically - -**Solution**: Explicitly specify types -```go -// Instead of -client := sync.NewClient(config, log, metrics, db, handler) - -// Use -client := sync.NewClient[*merkledb.Database, *merkledb.RangeProof, *merkledb.ChangeOrRangeProof]( - config, log, metrics, db, handler, -) -``` - -### Issue: Interface Compatibility - -**Problem**: Custom implementation doesn't match new interface - -**Solution**: Update method signatures to use generic types -```go -// Update from -func (c *MyClient) GetRangeProof(...) (*merkledb.RangeProof, error) - -// To -func (c *MyClient[T, U, V]) GetRangeProof(...) (U, error) -``` - -### Issue: Mock Testing - -**Problem**: Mocks need type parameters - -**Solution**: Use concrete types for mocks -```go -type MockClient = sync.MockClient[*merkledb.Database, *merkledb.RangeProof, *merkledb.ChangeOrRangeProof] - -mock := &MockClient{} -``` - -## Best Practices - -1. **Use Type Inference**: Let Go infer types when possible for cleaner code -2. **Consistent Types**: Use the same type parameters across related components -3. **Document Custom Types**: Clearly document any custom types that implement required interfaces -4. **Test Thoroughly**: Ensure all tests pass with the new generic implementation -5. **Gradual Migration**: Migrate one component at a time if you have a large codebase - -## Performance Notes - -The generic implementation has **zero performance overhead**: - -- Generic instantiation happens at compile time -- No runtime type assertions or boxing -- Identical memory layout and CPU usage -- Better inlining opportunities with concrete types - -## Version Compatibility - -- **v1.10.x and earlier**: Non-generic implementation -- **v1.11.0+**: Generic implementation with full backward compatibility -- **Migration**: No breaking changes for standard usage - -## Getting Help - -If you encounter issues during migration: - -1. Check that all type parameters match across components -2. Ensure custom types implement required interfaces -3. Review the test files for examples of correct usage -4. Use explicit type parameters if inference fails - -## Summary - -The migration to generics in x/sync provides: - -- ✅ Better type safety at compile time -- ✅ Full backward compatibility -- ✅ Zero performance overhead -- ✅ More flexible custom implementations -- ✅ Cleaner, more maintainable code - -Most users can continue using the package without any code changes, while advanced users gain the ability to use custom types with full type safety. \ No newline at end of file diff --git a/x/sync/README.md b/x/sync/README.md deleted file mode 100644 index 272d8105b..000000000 --- a/x/sync/README.md +++ /dev/null @@ -1,162 +0,0 @@ -# `sync` package - -## Overview - -This package implements a client and server that allows for the syncing of a [MerkleDB](../merkledb/README.md). -The servers have an up-to-date version of the database, and the clients have an out of date version of the database or an empty database. - -It's planned that these client and server implementations will eventually be compatible with Firewood. - -## Messages - -There are four message types sent between the client and server: - -1. `SyncGetRangeProofRequest` -2. `RangeProof` -3. `SyncGetChangeProofRequest` -4. `SyncGetChangeProofResponse` - -These message types are defined in `node/proto/sync.proto`. -For more information on range proofs and change proofs, see their definitions in `node/merkledb/proof.go`. - -### `SyncGetRangeProofRequest` - -This message is sent from the client to the server to request a range proof for a given key range and root hash. -That is, the client says, "Give me the key-value pairs that were in this key range when the database had this root." -This request includes a limit on the number of key-value pairs to return, and the size of the response. - -### `RangeProof` - -This message is sent from the server to the client in response to a `SyncGetRangeProofRequest`. -It contains the key-value pairs that were in the requested key range when the database had the requested root, -as well as a proof that the key-value pairs are correct. -If a server can't serve the entire requested key range in one response, its response will omit keys from the -end of the range rather than the start. -For example, if a client requests a range proof for range [`requested_start`, `requested_end`] but the server -can't fit all the key-value pairs in one response, it'll send a range proof for [`requested_start`, `proof_end`] where `proof_end` < `requested_end`, -as opposed to sending a range proof for [`proof_start`, `requested_end`] where `proof_start` > `requested_start`. - -### `SyncGetChangeProofRequest` - -This message is sent from the client to the server to request a change proof between the given root hashes. -That is, the client says, "Give me the key-value pairs that changed between the time the database had this root and that root." -This request includes a limit on the number of key-value pairs to return, and the size of the response. - -### `SyncGetChangeProofResponse` - -This message is sent from the server to the client in response to a `SyncGetChangeProofRequest`. -If the server had sufficient history to generate a change proof, it contains a change proof that contains -the key-value pairs that changed between the requested roots. -If the server did not have sufficient history to generate a change proof, it contains a range proof that -contains the key-value pairs that were in the database when the database had the latter root. -Like range proofs, if a client requests a change proof for range [`requested_start`, `requested_end`] but -the server can't fit all the key-value pairs in one response, -it'll send a change proof for [`requested_start`, `proof_end`] where `proof_end` < `requested_end`, -as opposed to sending a change proof for [`proof_start`, `requested_end`] where `proof_start` > `requested_start`. - -## Algorithm - -For each proof it receives, the sync client tracks the root hash of the revision associated with the proof's key-value pairs. -For example, it will store information that says something like, "I have all of the key-value pairs that -are in range [`start`, `end`] for the revision with root `root_hash`" for some keys `start` and `end`. -Note that `root_hash` is the root hash of the revision that the client is trying to sync to, not the -root hash of its own (incomplete) database. -Tracking the revision associated with each downloaded key range, as well as using data in its own -(incomplete) database, allows the client to figure out which key ranges are not up-to-date and need to be synced. -The hash of the incomplete database on a client is never sent anywhere because it does not represent a root hash of any revision. - -When the client is created, it is given the root hash of the revision to sync to. -When it starts syncing, it requests from a server a range proof for the entire database. -(To indicate that it wants no lower bound on the key range, the client doesn't provide a lower bound in the request. -To indicate that it wants no upper bound, the client doesn't provide an upper bound. -Thus, to request the entire database, the client omits both the lower and upper bounds in its request.) -The server replies with a range proof, which the client verifies. -If it's valid, the key-value pairs in the proof are written to the database. -If it's not, the client drops the proof and requests the proof from another server. - -A range proof sent by a server must return a continuous range of the key-value pairs, but may not -return the full range that was requested. -For example, a client might request all the key-value pairs in [`requested_start`, `requested_end`] -but only receive those in range [`requested_start`, `proof_end`] where `proof_end` < `requested_end`. -There might be too many key-value pairs to include in one message, or the server may be too busy to provide any more in its response. -Unless the database is very small, this means that the range proof the client receives in response to - its range proof request for the entire database will not contain all of the key-value pairs in the database. - -If a client requests a range proof for range [`requested_start`, `requested_end`] but only receives -a range proof for [`requested_start`, `proof_end`] where `proof_end` < `requested_end` -it recognizes that it must still fetch all of the keys in [`proof_end`, `requested_end`]. -It repeatedly requests range proofs for chunks of the remaining key range until it has all of the -key-value pairs in [`requested_start`, `requested_end`]. -The client may split the remaining key range into chunks and fetch chunks of key-value pairs in parallel, possibly even from different servers. - -Additional commits to the database may occur while the client is syncing. -The sync client can be notified that the root hash of the database it's trying to sync to has changed. -Detecting that the root hash to sync to has changed is done outside this package. -For example, if the database is being used to store blockchain state then the sync client would be -notified when a new block is accepted because that implies a commit to the database. -If this occurs, the key-value pairs the client has learned about via range proofs may no longer be up-to-date. - -We use change proofs as an optimization to correct the out of date key-value pairs. -When the sync client is notified that the root hash to sync to has changed, it requests a change proof -from a server for a given key range. -For example, if a client has the key-value pairs in range [`start`, `end`] that were in the database -when it had `root_hash`, then it will request a change proof that provides all of the key-value changes -in range [`start`, `end`] from the database version with root hash `root_hash` to the database version with root hash `new_root_hash`. -The client verifies the change proof, and if it's valid, it applies the changes to its database. -If it's not, the client drops the proof and requests the proof from another server. - -A server needs to have history in order to serve a change proof. -Namely, it needs to know all of the database changes between two roots. -If the server does not have sufficient history to generate a change proof, it will send a range proof for -the requested range at revision `new_root_hash` instead. -The client will verify and apply the range proof. (Note that change proofs are just an optimization for bandwidth and speed. -A range proof for a given key range and revision has the same information as a change proof from -`old_root_hash` to `new_root_hash` for the key range, assuming the client has the key-value pairs -for the key range at the revision with `old_root_hash`.) -Change proofs, like range proofs, may not contain all of the key-value pairs in the requested range. -This is OK because as mentioned above, the client tracks the root hash associated with each range of -key-value pairs it has, so it knows which key-value pairs are out of date. -Similar to range proofs, if a client requests the changes in range [`requested_start`, `requested_end`], -but the server replies with all of the changes in [`requested_start`, `proof_end`] for some `proof_end` < `requested_end`, -the client will repeatedly request change proofs until it gets remaining key-value pairs (namely in [`proof_end`, `requested_end`]). - -Eventually, by repeatedly requesting, receiving, verifying and applying range and change proofs, -the client will have all of the key-value pairs in the database. -At this point, it's synced. - -## Diagram - - -Assuming you have `Root Hash` `r1` which has many keys, some of which are k25, k50, k75, -approximately 25%, 50%, and 75% of the way into the sorted set of keys, respectively, -this diagram shows an example flow from client to server: - -```mermaid -sequenceDiagram - box Client/Server - participant Server - participant Client - end - box New Revision Notifier - participant Notifier - end - - Note right of Client: Normal sync flow - Notifier->>Client: CurrentRoot(r1) - Client->>Server: RangeProofRequest(r1, all) - Server->>Client: RangeProofResponse(r1, ..k25) - Client->>Server: RangeProofRequest(r1, k25..) - Server->>Client: RangeProofResponse(r1, k25..k75) - Notifier-)Client: NewRootHash(r2) - Client->>Server: ChangeProofRequest(r1, r2, 0..k75) - Server->>Client: ChangeProofResponse(r1, r2, 0..k50) - Client->>Server: ChangeProofRequest(r1, r2, k50..k75) - Server->>Client: ChangeProofResponse(r1, r2, k50..k75) - Note right of Client: client is @r2 through (..k75) - Client->>Server: RangeProofRequest(r2, k75..) - Server->>Client: RangeProofResponse(r2, k75..k100) -``` - -## TODOs - -- [ ] Handle errors on proof requests. Currently, any errors that occur server side are not sent back to the client. diff --git a/x/sync/client.go b/x/sync/client.go deleted file mode 100644 index b38bd27ac..000000000 --- a/x/sync/client.go +++ /dev/null @@ -1,392 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package sync - -import ( - "context" - "errors" - "fmt" - "math" - "sync/atomic" - "time" - - "google.golang.org/protobuf/proto" - - "github.com/luxfi/ids" - "github.com/luxfi/log" - "github.com/luxfi/node/x/merkledb" - "github.com/luxfi/container/maybe" - - pb "github.com/luxfi/node/proto/pb/sync" -) - -const ( - initialRetryWait = 10 * time.Millisecond - maxRetryWait = time.Second - retryWaitFactor = 1.5 // Larger --> timeout grows more quickly - - epsilon = 1e-6 // small amount to add to time to avoid division by 0 -) - -var ( - _ Client = (*client)(nil) - - errInvalidRangeProof = errors.New("failed to verify range proof") - errInvalidChangeProof = errors.New("failed to verify change proof") - errTooManyKeys = errors.New("response contains more than requested keys") - errTooManyBytes = errors.New("response contains more than requested bytes") - errUnexpectedChangeProofResponse = errors.New("unexpected response type") -) - -// ChangeOrRangeProof contains either a ChangeProof or RangeProof. -// Exactly one of ChangeProof or RangeProof should be non-nil. -type ChangeOrRangeProof struct { - ChangeProof *merkledb.ChangeProof - RangeProof *merkledb.RangeProof -} - -// Client synchronously fetches data from the network -// to fulfill state sync requests. -// Repeatedly retries failed requests until the context is canceled. -type Client interface { - // GetRangeProof synchronously sends the given request - // and returns the parsed response. - // This method verifies the range proof before returning it. - GetRangeProof( - ctx context.Context, - request *pb.SyncGetRangeProofRequest, - ) (*merkledb.RangeProof, error) - - // GetChangeProof synchronously sends the given request - // and returns the parsed response. - // This method verifies the change proof / range proof - // before returning it. - // If the server responds with a change proof, - // it's verified using [verificationDB]. - GetChangeProof( - ctx context.Context, - request *pb.SyncGetChangeProofRequest, - verificationDB DB, - ) (*ChangeOrRangeProof, error) -} - -type client struct { - networkClient NetworkClient - stateSyncNodes []ids.NodeID - stateSyncNodeIdx uint32 - log log.Logger - metrics SyncMetrics - tokenSize int - hasher merkledb.Hasher -} - -type ClientConfig struct { - NetworkClient NetworkClient - StateSyncNodeIDs []ids.NodeID - Log log.Logger - Metrics SyncMetrics - BranchFactor merkledb.BranchFactor - // If not specified, [merkledb.DefaultHasher] will be used. - Hasher merkledb.Hasher -} - -func NewClient(config *ClientConfig) (Client, error) { - if err := config.BranchFactor.Valid(); err != nil { - return nil, err - } - hasher := config.Hasher - if hasher == nil { - hasher = merkledb.DefaultHasher - } - return &client{ - networkClient: config.NetworkClient, - stateSyncNodes: config.StateSyncNodeIDs, - log: config.Log, - metrics: config.Metrics, - tokenSize: merkledb.BranchFactorToTokenSize[config.BranchFactor], - hasher: hasher, - }, nil -} - -// GetChangeProof synchronously retrieves the change proof given by [req]. -// Upon failure, retries until the context is expired. -// The returned change proof is verified. -func (c *client) GetChangeProof( - ctx context.Context, - req *pb.SyncGetChangeProofRequest, - db DB, -) (*ChangeOrRangeProof, error) { - parseFn := func(ctx context.Context, responseBytes []byte) (*ChangeOrRangeProof, error) { - if len(responseBytes) > int(req.BytesLimit) { - return nil, fmt.Errorf("%w: (%d) > %d)", errTooManyBytes, len(responseBytes), req.BytesLimit) - } - - var changeProofResp pb.SyncGetChangeProofResponse - if err := proto.Unmarshal(responseBytes, &changeProofResp); err != nil { - return nil, err - } - - startKey := maybeBytesToMaybe(req.StartKey) - endKey := maybeBytesToMaybe(req.EndKey) - - switch changeProofResp := changeProofResp.Response.(type) { - case *pb.SyncGetChangeProofResponse_ChangeProof: - // The server had enough history to send us a change proof - var changeProof merkledb.ChangeProof - if err := changeProof.UnmarshalProto(changeProofResp.ChangeProof); err != nil { - return nil, err - } - - // Ensure the response does not contain more than the requested number of leaves - // and the start and end roots match the requested roots. - if len(changeProof.KeyChanges) > int(req.KeyLimit) { - return nil, fmt.Errorf( - "%w: (%d) > %d)", - errTooManyKeys, len(changeProof.KeyChanges), req.KeyLimit, - ) - } - - endRoot, err := ids.ToID(req.EndRootHash) - if err != nil { - return nil, err - } - - if err := db.VerifyChangeProof( - ctx, - &changeProof, - startKey, - endKey, - endRoot, - ); err != nil { - return nil, fmt.Errorf("%w due to %w", errInvalidChangeProof, err) - } - - return &ChangeOrRangeProof{ - ChangeProof: &changeProof, - }, nil - case *pb.SyncGetChangeProofResponse_RangeProof: - - var rangeProof merkledb.RangeProof - if err := rangeProof.UnmarshalProto(changeProofResp.RangeProof); err != nil { - return nil, err - } - - // The server did not have enough history to send us a change proof - // so they sent a range proof instead. - err := verifyRangeProof( - ctx, - &rangeProof, - int(req.KeyLimit), - startKey, - endKey, - req.EndRootHash, - c.tokenSize, - c.hasher, - ) - if err != nil { - return nil, err - } - - return &ChangeOrRangeProof{ - RangeProof: &rangeProof, - }, nil - default: - return nil, fmt.Errorf( - "%w: %T", - errUnexpectedChangeProofResponse, changeProofResp, - ) - } - } - - reqBytes, err := proto.Marshal(req) - if err != nil { - return nil, err - } - return getAndParse(ctx, c, reqBytes, parseFn) -} - -// Verify [rangeProof] is a valid range proof for keys in [start, end] for -// root [rootBytes]. Returns [errTooManyKeys] if the response contains more -// than [keyLimit] keys. -func verifyRangeProof( - ctx context.Context, - rangeProof *merkledb.RangeProof, - keyLimit int, - start maybe.Maybe[[]byte], - end maybe.Maybe[[]byte], - rootBytes []byte, - tokenSize int, - hasher merkledb.Hasher, -) error { - root, err := ids.ToID(rootBytes) - if err != nil { - return err - } - - // Ensure the response does not contain more than the maximum requested number of leaves. - if len(rangeProof.KeyChanges) > keyLimit { - return fmt.Errorf( - "%w: (%d) > %d)", - errTooManyKeys, len(rangeProof.KeyChanges), keyLimit, - ) - } - - if err := rangeProof.Verify( - ctx, - start, - end, - root, - tokenSize, - hasher, - ); err != nil { - return fmt.Errorf("%w due to %w", errInvalidRangeProof, err) - } - return nil -} - -// GetRangeProof synchronously retrieves the range proof given by [req]. -// Upon failure, retries until the context is expired. -// The returned range proof is verified. -func (c *client) GetRangeProof( - ctx context.Context, - req *pb.SyncGetRangeProofRequest, -) (*merkledb.RangeProof, error) { - parseFn := func(ctx context.Context, responseBytes []byte) (*merkledb.RangeProof, error) { - if len(responseBytes) > int(req.BytesLimit) { - return nil, fmt.Errorf( - "%w: (%d) > %d)", - errTooManyBytes, len(responseBytes), req.BytesLimit, - ) - } - - var rangeProofProto pb.RangeProof - if err := proto.Unmarshal(responseBytes, &rangeProofProto); err != nil { - return nil, err - } - - var rangeProof merkledb.RangeProof - if err := rangeProof.UnmarshalProto(&rangeProofProto); err != nil { - return nil, err - } - - if err := verifyRangeProof( - ctx, - &rangeProof, - int(req.KeyLimit), - maybeBytesToMaybe(req.StartKey), - maybeBytesToMaybe(req.EndKey), - req.RootHash, - c.tokenSize, - c.hasher, - ); err != nil { - return nil, err - } - return &rangeProof, nil - } - - reqBytes, err := proto.Marshal(req) - if err != nil { - return nil, err - } - - return getAndParse(ctx, c, reqBytes, parseFn) -} - -// getAndParse uses [client] to send [request] to an arbitrary peer. -// Returns the response to the request. -// [parseFn] parses the raw response. -// If the request is unsuccessful or the response can't be parsed, -// retries the request to a different peer until [ctx] expires. -// Returns [errAppSendFailed] if we fail to send an Request/Response. -// This should be treated as a fatal error. -func getAndParse[T any]( - ctx context.Context, - client *client, - request []byte, - parseFn func(context.Context, []byte) (*T, error), -) (*T, error) { - var ( - lastErr error - response *T - ) - // Loop until the context is cancelled or we get a valid response. - for attempt := 1; ; attempt++ { - nodeID, responseBytes, err := client.get(ctx, request) - if err == nil { - if response, err = parseFn(ctx, responseBytes); err == nil { - return response, nil - } - } - - if errors.Is(err, errAppSendFailed) { - // Failing to send an Request is a fatal error. - return nil, err - } - - client.log.Debug("request failed, retrying", - log.Stringer("nodeID", nodeID), - log.Int("attempt", attempt), - log.Reflect("error", err), - ) - // if [err] is being propagated from [ctx], avoid overwriting [lastErr]. - if err != ctx.Err() { - lastErr = err - } - - retryWait := initialRetryWait * time.Duration(math.Pow(retryWaitFactor, float64(attempt))) - if retryWait > maxRetryWait || retryWait < 0 { // Handle overflows with negative check. - retryWait = maxRetryWait - } - - select { - case <-ctx.Done(): - if lastErr != nil { - // prefer reporting [lastErr] if it's not nil. - return nil, fmt.Errorf( - "request failed after %d attempts with last error %w and ctx error %w", - attempt, lastErr, ctx.Err(), - ) - } - return nil, ctx.Err() - case <-time.After(retryWait): - } - } -} - -// get sends [request] to an arbitrary peer and blocks -// until the node receives a response, failure notification -// or [ctx] is canceled. -// Returns the peer's NodeID and response. -// Returns [errAppSendFailed] if we failed to send an Request/Response. -// This should be treated as fatal. -// It's safe to call this method multiple times concurrently. -func (c *client) get(ctx context.Context, request []byte) (ids.NodeID, []byte, error) { - var ( - response []byte - nodeID ids.NodeID - err error - ) - - c.metrics.RequestMade() - - if len(c.stateSyncNodes) == 0 { - nodeID, response, err = c.networkClient.RequestAny(ctx, request) - } else { - // Get the next nodeID to query using the [nodeIdx] offset. - // If we're out of nodes, loop back to 0. - // We do this try to query a different node each time if possible. - nodeIdx := atomic.AddUint32(&c.stateSyncNodeIdx, 1) - nodeID = c.stateSyncNodes[nodeIdx%uint32(len(c.stateSyncNodes))] - response, err = c.networkClient.Request(ctx, nodeID, request) - } - if err != nil { - c.metrics.RequestFailed() - return nodeID, response, err - } - - c.metrics.RequestSucceeded() - return nodeID, response, nil -} diff --git a/x/sync/client_test.go b/x/sync/client_test.go deleted file mode 100644 index 620eff4f2..000000000 --- a/x/sync/client_test.go +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -//go:build skip - -package sync - -import ( - "context" - "sync" - "testing" - "time" - - "github.com/luxfi/metric" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - - "github.com/luxfi/consensus/core" - "github.com/luxfi/ids" - "github.com/luxfi/node/trace" - "github.com/luxfi/node/x/merkledb" - "github.com/luxfi/p2p" - "github.com/luxfi/node/trace" -) - -var _ p2p.Handler = (*flakyHandler)(nil) - -func newDefaultDBConfig() merkledb.Config { - return merkledb.Config{ - IntermediateWriteBatchSize: 100, - HistoryLength: defaultRequestKeyLimit, - ValueNodeCacheSize: defaultRequestKeyLimit, - IntermediateWriteBufferSize: defaultRequestKeyLimit, - IntermediateNodeCacheSize: defaultRequestKeyLimit, - Reg: metric.NewNoOp().Registry(), - Tracer: trace.Noop, - BranchFactor: merkledb.BranchFactor16, - } -} - -func newFlakyRangeProofHandler( - t *testing.T, - db merkledb.MerkleDB, - modifyResponse func(response *merkledb.RangeProof), -) p2p.Handler { - handler := NewGetRangeProofHandler(db) - - c := counter{m: 2} - return &p2p.TestHandler{ - RequestF: func(ctx context.Context, nodeID ids.NodeID, deadline time.Time, requestBytes []byte) ([]byte, *common.Error) { - responseBytes, appErr := handler.Request(ctx, nodeID, deadline, requestBytes) - if appErr != nil { - return nil, appErr - } - - response := &pb.RangeProof{} - require.NoError(t, proto.Unmarshal(responseBytes, response)) - - proof := &merkledb.RangeProof{} - require.NoError(t, proof.UnmarshalProto(response)) - - // Half of requests are modified - if c.Inc() == 0 { - modifyResponse(proof) - } - - responseBytes, err := proto.Marshal(proof.ToProto()) - if err != nil { - return nil, &common.Error{Code: 123, Message: err.Error()} - } - - return responseBytes, nil - }, - } -} - -func newFlakyChangeProofHandler( - t *testing.T, - db merkledb.MerkleDB, - modifyResponse func(response *merkledb.ChangeProof), -) p2p.Handler { - handler := NewGetChangeProofHandler(db) - - c := counter{m: 2} - return &p2p.TestHandler{ - RequestF: func(ctx context.Context, nodeID ids.NodeID, deadline time.Time, requestBytes []byte) ([]byte, *common.Error) { - var err error - responseBytes, appErr := handler.Request(ctx, nodeID, deadline, requestBytes) - if appErr != nil { - return nil, appErr - } - - response := &pb.SyncGetChangeProofResponse{} - require.NoError(t, proto.Unmarshal(responseBytes, response)) - - changeProof := response.Response.(*pb.SyncGetChangeProofResponse_ChangeProof) - proof := &merkledb.ChangeProof{} - require.NoError(t, proof.UnmarshalProto(changeProof.ChangeProof)) - - // Half of requests are modified - if c.Inc() == 0 { - modifyResponse(proof) - } - - responseBytes, err = proto.Marshal(&pb.SyncGetChangeProofResponse{ - Response: &pb.SyncGetChangeProofResponse_ChangeProof{ - ChangeProof: proof.ToProto(), - }, - }) - if err != nil { - return nil, &common.Error{Code: 123, Message: err.Error()} - } - - return responseBytes, nil - }, - } -} - -type flakyHandler struct { - p2p.Handler - c *counter -} - -func (f *flakyHandler) Request(ctx context.Context, nodeID ids.NodeID, deadline time.Time, requestBytes []byte) ([]byte, *common.Error) { - if f.c.Inc() == 0 { - return nil, &common.Error{Code: 123, Message: "flake error"} - } - - return f.Handler.Request(ctx, nodeID, deadline, requestBytes) -} - -type counter struct { - i int - m int - lock sync.Mutex -} - -func (c *counter) Inc() int { - c.lock.Lock() - defer c.lock.Unlock() - - tmp := c.i - result := tmp % c.m - - c.i++ - return result -} - -type waitingHandler struct { - p2p.NoOpHandler - handler p2p.Handler - updatedRootChan chan struct{} -} - -func (w *waitingHandler) Request(ctx context.Context, nodeID ids.NodeID, deadline time.Time, requestBytes []byte) ([]byte, *common.Error) { - <-w.updatedRootChan - return w.handler.Request(ctx, nodeID, deadline, requestBytes) -} diff --git a/x/sync/db.go b/x/sync/db.go deleted file mode 100644 index 4c3827f8b..000000000 --- a/x/sync/db.go +++ /dev/null @@ -1,16 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package sync - -import "github.com/luxfi/node/x/merkledb" - -type DB interface { - merkledb.Clearer - merkledb.MerkleRootGetter - merkledb.ProofGetter - merkledb.ChangeProofer - merkledb.RangeProofer -} diff --git a/x/sync/g_db/db_client.go b/x/sync/g_db/db_client.go deleted file mode 100644 index f74d6b3fb..000000000 --- a/x/sync/g_db/db_client.go +++ /dev/null @@ -1,194 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package gdb - -import ( - "context" - "errors" - - "google.golang.org/protobuf/types/known/emptypb" - - "github.com/luxfi/ids" - "github.com/luxfi/node/x/merkledb" - "github.com/luxfi/node/x/sync" - "github.com/luxfi/container/maybe" - - pb "github.com/luxfi/node/proto/pb/sync" -) - -var _ sync.DB = (*DBClient)(nil) - -func NewDBClient(client pb.DBClient) *DBClient { - return &DBClient{ - client: client, - } -} - -type DBClient struct { - client pb.DBClient -} - -func (c *DBClient) GetMerkleRoot(ctx context.Context) (ids.ID, error) { - resp, err := c.client.GetMerkleRoot(ctx, &emptypb.Empty{}) - if err != nil { - return ids.Empty, err - } - return ids.ToID(resp.RootHash) -} - -func (c *DBClient) GetChangeProof( - ctx context.Context, - startRootID ids.ID, - endRootID ids.ID, - startKey maybe.Maybe[[]byte], - endKey maybe.Maybe[[]byte], - keyLimit int, -) (*merkledb.ChangeProof, error) { - if endRootID == ids.Empty { - return nil, merkledb.ErrEmptyProof - } - - resp, err := c.client.GetChangeProof(ctx, &pb.GetChangeProofRequest{ - StartRootHash: startRootID[:], - EndRootHash: endRootID[:], - StartKey: &pb.MaybeBytes{ - IsNothing: startKey.IsNothing(), - Value: startKey.Value(), - }, - EndKey: &pb.MaybeBytes{ - IsNothing: endKey.IsNothing(), - Value: endKey.Value(), - }, - KeyLimit: uint32(keyLimit), - }) - if err != nil { - return nil, err - } - - // When the root is not present, the server returns RootNotPresent without - // distinguishing ErrNoEndRoot from ErrInsufficientHistory. Both map to - // ErrInsufficientHistory on the client side. - if resp.GetRootNotPresent() { - return nil, merkledb.ErrInsufficientHistory - } - - var proof merkledb.ChangeProof - if err := proof.UnmarshalProto(resp.GetChangeProof()); err != nil { - return nil, err - } - return &proof, nil -} - -func (c *DBClient) VerifyChangeProof( - ctx context.Context, - proof *merkledb.ChangeProof, - startKey maybe.Maybe[[]byte], - endKey maybe.Maybe[[]byte], - expectedRootID ids.ID, -) error { - resp, err := c.client.VerifyChangeProof(ctx, &pb.VerifyChangeProofRequest{ - Proof: proof.ToProto(), - StartKey: &pb.MaybeBytes{ - Value: startKey.Value(), - IsNothing: startKey.IsNothing(), - }, - EndKey: &pb.MaybeBytes{ - Value: endKey.Value(), - IsNothing: endKey.IsNothing(), - }, - ExpectedRootHash: expectedRootID[:], - }) - if err != nil { - return err - } - - // Error is deserialized from the string returned by the gRPC server. - if len(resp.Error) == 0 { - return nil - } - return errors.New(resp.Error) -} - -func (c *DBClient) CommitChangeProof(ctx context.Context, proof *merkledb.ChangeProof) error { - _, err := c.client.CommitChangeProof(ctx, &pb.CommitChangeProofRequest{ - Proof: proof.ToProto(), - }) - return err -} - -func (c *DBClient) GetProof(ctx context.Context, key []byte) (*merkledb.Proof, error) { - resp, err := c.client.GetProof(ctx, &pb.GetProofRequest{ - Key: key, - }) - if err != nil { - return nil, err - } - - var proof merkledb.Proof - if err := proof.UnmarshalProto(resp.Proof); err != nil { - return nil, err - } - return &proof, nil -} - -func (c *DBClient) GetRangeProofAtRoot( - ctx context.Context, - rootID ids.ID, - startKey maybe.Maybe[[]byte], - endKey maybe.Maybe[[]byte], - keyLimit int, -) (*merkledb.RangeProof, error) { - if rootID == ids.Empty { - return nil, merkledb.ErrEmptyProof - } - - resp, err := c.client.GetRangeProof(ctx, &pb.GetRangeProofRequest{ - RootHash: rootID[:], - StartKey: &pb.MaybeBytes{ - IsNothing: startKey.IsNothing(), - Value: startKey.Value(), - }, - EndKey: &pb.MaybeBytes{ - IsNothing: endKey.IsNothing(), - Value: endKey.Value(), - }, - KeyLimit: uint32(keyLimit), - }) - if err != nil { - return nil, err - } - - var proof merkledb.RangeProof - if err := proof.UnmarshalProto(resp.Proof); err != nil { - return nil, err - } - return &proof, nil -} - -func (c *DBClient) CommitRangeProof( - ctx context.Context, - startKey maybe.Maybe[[]byte], - endKey maybe.Maybe[[]byte], - proof *merkledb.RangeProof, -) error { - _, err := c.client.CommitRangeProof(ctx, &pb.CommitRangeProofRequest{ - StartKey: &pb.MaybeBytes{ - IsNothing: startKey.IsNothing(), - Value: startKey.Value(), - }, - EndKey: &pb.MaybeBytes{ - IsNothing: endKey.IsNothing(), - Value: endKey.Value(), - }, - RangeProof: proof.ToProto(), - }) - return err -} - -func (c *DBClient) Clear() error { - _, err := c.client.Clear(context.Background(), &emptypb.Empty{}) - return err -} diff --git a/x/sync/g_db/db_server.go b/x/sync/g_db/db_server.go deleted file mode 100644 index 8f1f946d7..000000000 --- a/x/sync/g_db/db_server.go +++ /dev/null @@ -1,224 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package gdb - -import ( - "context" - "errors" - - "google.golang.org/protobuf/types/known/emptypb" - - "github.com/luxfi/ids" - "github.com/luxfi/node/x/merkledb" - "github.com/luxfi/node/x/sync" - "github.com/luxfi/container/maybe" - - pb "github.com/luxfi/node/proto/pb/sync" -) - -var _ pb.DBServer = (*DBServer)(nil) - -func NewDBServer(db sync.DB) *DBServer { - return &DBServer{ - db: db, - } -} - -type DBServer struct { - pb.UnsafeDBServer - - db sync.DB -} - -func (s *DBServer) GetMerkleRoot( - ctx context.Context, - _ *emptypb.Empty, -) (*pb.GetMerkleRootResponse, error) { - root, err := s.db.GetMerkleRoot(ctx) - if err != nil { - return nil, err - } - return &pb.GetMerkleRootResponse{ - RootHash: root[:], - }, nil -} - -func (s *DBServer) GetChangeProof( - ctx context.Context, - req *pb.GetChangeProofRequest, -) (*pb.GetChangeProofResponse, error) { - startRootID, err := ids.ToID(req.StartRootHash) - if err != nil { - return nil, err - } - endRootID, err := ids.ToID(req.EndRootHash) - if err != nil { - return nil, err - } - start := maybe.Nothing[[]byte]() - if req.StartKey != nil && !req.StartKey.IsNothing { - start = maybe.Some(req.StartKey.Value) - } - end := maybe.Nothing[[]byte]() - if req.EndKey != nil && !req.EndKey.IsNothing { - end = maybe.Some(req.EndKey.Value) - } - - changeProof, err := s.db.GetChangeProof( - ctx, - startRootID, - endRootID, - start, - end, - int(req.KeyLimit), - ) - if err != nil { - if !errors.Is(err, merkledb.ErrInsufficientHistory) { - return nil, err - } - return &pb.GetChangeProofResponse{ - Response: &pb.GetChangeProofResponse_RootNotPresent{ - RootNotPresent: true, - }, - }, nil - } - - return &pb.GetChangeProofResponse{ - Response: &pb.GetChangeProofResponse_ChangeProof{ - ChangeProof: changeProof.ToProto(), - }, - }, nil -} - -func (s *DBServer) VerifyChangeProof( - ctx context.Context, - req *pb.VerifyChangeProofRequest, -) (*pb.VerifyChangeProofResponse, error) { - var proof merkledb.ChangeProof - if err := proof.UnmarshalProto(req.Proof); err != nil { - return nil, err - } - - rootID, err := ids.ToID(req.ExpectedRootHash) - if err != nil { - return nil, err - } - startKey := maybe.Nothing[[]byte]() - if req.StartKey != nil && !req.StartKey.IsNothing { - startKey = maybe.Some(req.StartKey.Value) - } - endKey := maybe.Nothing[[]byte]() - if req.EndKey != nil && !req.EndKey.IsNothing { - endKey = maybe.Some(req.EndKey.Value) - } - - // Error is serialized as a string across the gRPC boundary. - var errString string - if err := s.db.VerifyChangeProof(ctx, &proof, startKey, endKey, rootID); err != nil { - errString = err.Error() - } - return &pb.VerifyChangeProofResponse{ - Error: errString, - }, nil -} - -func (s *DBServer) CommitChangeProof( - ctx context.Context, - req *pb.CommitChangeProofRequest, -) (*emptypb.Empty, error) { - var proof merkledb.ChangeProof - if err := proof.UnmarshalProto(req.Proof); err != nil { - return nil, err - } - - err := s.db.CommitChangeProof(ctx, &proof) - return &emptypb.Empty{}, err -} - -func (s *DBServer) GetProof( - ctx context.Context, - req *pb.GetProofRequest, -) (*pb.GetProofResponse, error) { - proof, err := s.db.GetProof(ctx, req.Key) - if err != nil { - return nil, err - } - - return &pb.GetProofResponse{ - Proof: proof.ToProto(), - }, nil -} - -func (s *DBServer) GetRangeProof( - ctx context.Context, - req *pb.GetRangeProofRequest, -) (*pb.GetRangeProofResponse, error) { - rootID, err := ids.ToID(req.RootHash) - if err != nil { - return nil, err - } - start := maybe.Nothing[[]byte]() - if req.StartKey != nil && !req.StartKey.IsNothing { - start = maybe.Some(req.StartKey.Value) - } - end := maybe.Nothing[[]byte]() - if req.EndKey != nil && !req.EndKey.IsNothing { - end = maybe.Some(req.EndKey.Value) - } - proof, err := s.db.GetRangeProofAtRoot(ctx, rootID, start, end, int(req.KeyLimit)) - if err != nil { - return nil, err - } - - protoProof := &pb.GetRangeProofResponse{ - Proof: &pb.RangeProof{ - StartProof: make([]*pb.ProofNode, len(proof.StartProof)), - EndProof: make([]*pb.ProofNode, len(proof.EndProof)), - KeyValues: make([]*pb.KeyValue, len(proof.KeyChanges)), - }, - } - for i, node := range proof.StartProof { - protoProof.Proof.StartProof[i] = node.ToProto() - } - for i, node := range proof.EndProof { - protoProof.Proof.EndProof[i] = node.ToProto() - } - for i, kv := range proof.KeyChanges { - protoProof.Proof.KeyValues[i] = &pb.KeyValue{ - Key: kv.Key, - Value: kv.Value.Value(), - } - } - - return protoProof, nil -} - -func (s *DBServer) CommitRangeProof( - ctx context.Context, - req *pb.CommitRangeProofRequest, -) (*emptypb.Empty, error) { - var proof merkledb.RangeProof - if err := proof.UnmarshalProto(req.RangeProof); err != nil { - return nil, err - } - - start := maybe.Nothing[[]byte]() - if req.StartKey != nil && !req.StartKey.IsNothing { - start = maybe.Some(req.StartKey.Value) - } - - end := maybe.Nothing[[]byte]() - if req.EndKey != nil && !req.EndKey.IsNothing { - end = maybe.Some(req.EndKey.Value) - } - - err := s.db.CommitRangeProof(ctx, start, end, &proof) - return &emptypb.Empty{}, err -} - -func (s *DBServer) Clear(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { - return &emptypb.Empty{}, s.db.Clear() -} diff --git a/x/sync/generic_types.go b/x/sync/generic_types.go deleted file mode 100644 index 7a294c0af..000000000 --- a/x/sync/generic_types.go +++ /dev/null @@ -1,195 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package sync provides generic types for synchronization. -// This file contains the generic implementations that can be used -// with any comparable type, while maintaining backward compatibility -// with the existing byte-based implementation. - -package sync - -import ( - "bytes" - "time" - - "github.com/google/btree" - - "github.com/luxfi/ids" - "github.com/luxfi/container/heap" - "github.com/luxfi/container/maybe" -) - -// GenericWorkItem represents a work item that can work with any comparable type T -type GenericWorkItem[T any] struct { - Start maybe.Maybe[T] - End maybe.Maybe[T] - Priority priority - LocalRootID ids.ID - Attempt int - QueueTime time.Time -} - -// RequestFailed increments the attempt counter -func (w *GenericWorkItem[T]) RequestFailed() { - attempt := w.Attempt + 1 - // Overflow check - if attempt > w.Attempt { - w.Attempt = attempt - } -} - -// NewGenericWorkItem creates a new generic work item -func NewGenericWorkItem[T any]( - localRootID ids.ID, - start maybe.Maybe[T], - end maybe.Maybe[T], - priority priority, - queueTime time.Time, -) *GenericWorkItem[T] { - return &GenericWorkItem[T]{ - LocalRootID: localRootID, - Start: start, - End: end, - Priority: priority, - QueueTime: queueTime, - } -} - -// GenericWorkHeap is a priority queue that can work with any type T -type GenericWorkHeap[T any] struct { - // Max heap of items by priority - innerHeap heap.Set[*GenericWorkItem[T]] - // Items sorted by range start - sortedItems *btree.BTreeG[*GenericWorkItem[T]] - closed bool - compareFn func(a, b T) int - equalFn func(a, b T) bool -} - -// NewGenericWorkHeap creates a new generic work heap -func NewGenericWorkHeap[T any](compareFn func(a, b T) int, equalFn func(a, b T) bool) *GenericWorkHeap[T] { - wh := &GenericWorkHeap[T]{ - compareFn: compareFn, - equalFn: equalFn, - } - - wh.innerHeap = heap.NewSet[*GenericWorkItem[T]](func(a, b *GenericWorkItem[T]) bool { - return a.Priority > b.Priority - }) - - wh.sortedItems = btree.NewG( - 2, - func(a, b *GenericWorkItem[T]) bool { - aNothing := a.Start.IsNothing() - bNothing := b.Start.IsNothing() - if aNothing { - return !bNothing - } - if bNothing { - return false - } - return compareFn(a.Start.Value(), b.Start.Value()) < 0 - }, - ) - - return wh -} - -// Close marks the heap as closed -func (wh *GenericWorkHeap[T]) Close() { - wh.closed = true -} - -// Insert adds a new item into the heap -func (wh *GenericWorkHeap[T]) Insert(item *GenericWorkItem[T]) { - if wh.closed { - return - } - wh.innerHeap.Push(item) - wh.sortedItems.ReplaceOrInsert(item) -} - -// GetWork pops and returns a work item from the heap -func (wh *GenericWorkHeap[T]) GetWork() *GenericWorkItem[T] { - if wh.closed || wh.Len() == 0 { - return nil - } - item, _ := wh.innerHeap.Pop() - wh.sortedItems.Delete(item) - return item -} - -// MergeInsert inserts the item into the heap, merging with adjacent items if possible -func (wh *GenericWorkHeap[T]) MergeInsert(item *GenericWorkItem[T]) { - if wh.closed { - return - } - - var mergedBefore, mergedAfter *GenericWorkItem[T] - searchItem := &GenericWorkItem[T]{ - Start: item.Start, - } - - wh.sortedItems.DescendLessOrEqual( - searchItem, - func(beforeItem *GenericWorkItem[T]) bool { - if item.LocalRootID == beforeItem.LocalRootID && - maybe.Equal(item.Start, beforeItem.End, wh.equalFn) { - beforeItem.End = item.End - beforeItem.Priority = max(item.Priority, beforeItem.Priority) - wh.innerHeap.Fix(beforeItem) - mergedBefore = beforeItem - } - return false - }) - - wh.sortedItems.AscendGreaterOrEqual( - searchItem, - func(afterItem *GenericWorkItem[T]) bool { - if item.LocalRootID == afterItem.LocalRootID && - maybe.Equal(item.End, afterItem.Start, wh.equalFn) { - afterItem.Start = item.Start - afterItem.Priority = max(item.Priority, afterItem.Priority) - wh.innerHeap.Fix(afterItem) - mergedAfter = afterItem - } - return false - }) - - if mergedBefore != nil && mergedAfter != nil { - mergedBefore.End = mergedAfter.End - wh.remove(mergedAfter) - mergedBefore.Priority = max(mergedBefore.Priority, mergedAfter.Priority) - wh.innerHeap.Fix(mergedBefore) - } - - if mergedBefore == nil && mergedAfter == nil { - wh.Insert(item) - } -} - -// remove deletes an item from the heap -func (wh *GenericWorkHeap[T]) remove(item *GenericWorkItem[T]) { - wh.innerHeap.Remove(item) - wh.sortedItems.Delete(item) -} - -// Len returns the number of items in the heap -func (wh *GenericWorkHeap[T]) Len() int { - return wh.innerHeap.Len() -} - -// ByteWorkHeap is a specialized work heap for byte slices -// This provides backward compatibility with the existing implementation -type ByteWorkHeap struct { - *GenericWorkHeap[[]byte] -} - -// NewByteWorkHeap creates a new byte-based work heap -func NewByteWorkHeap() *ByteWorkHeap { - return &ByteWorkHeap{ - GenericWorkHeap: NewGenericWorkHeap[[]byte](bytes.Compare, bytes.Equal), - } -} diff --git a/x/sync/generics_test.go b/x/sync/generics_test.go deleted file mode 100644 index 95d205821..000000000 --- a/x/sync/generics_test.go +++ /dev/null @@ -1,159 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package sync - -import ( - "bytes" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/luxfi/ids" - "github.com/luxfi/container/maybe" -) - -// TestGenericWorkItem tests the generic work item implementation -func TestGenericWorkItem(t *testing.T) { - require := require.New(t) - - // Test with byte slices - rootID := ids.GenerateTestID() - start := maybe.Some([]byte{1, 2, 3}) - end := maybe.Some([]byte{4, 5, 6}) - - item := &GenericWorkItem[[]byte]{ - LocalRootID: rootID, - Start: start, - End: end, - Priority: highPriority, - QueueTime: time.Now(), - } - require.NotNil(item) - require.Equal(rootID, item.LocalRootID) - require.True(item.Start.HasValue()) - require.True(item.End.HasValue()) - require.Equal(highPriority, item.Priority) - - // Test RequestFailed - originalAttempt := item.Attempt - item.RequestFailed() - require.Equal(originalAttempt+1, item.Attempt) -} - -// TestGenericWorkHeap tests the generic work heap implementation -func TestGenericWorkHeap(t *testing.T) { - require := require.New(t) - - // Create a byte-based work heap using the constructor - wh := NewGenericWorkHeap[[]byte](bytes.Compare, bytes.Equal) - require.NotNil(wh) - require.Equal(0, wh.Len()) - - // Test Insert - rootID := ids.GenerateTestID() - item1 := &GenericWorkItem[[]byte]{ - LocalRootID: rootID, - Start: maybe.Some([]byte{1}), - End: maybe.Some([]byte{2}), - Priority: lowPriority, - QueueTime: time.Now(), - } - wh.Insert(item1) - require.Equal(1, wh.Len()) - - // Test GetWork - should return highest priority item - item2 := &GenericWorkItem[[]byte]{ - LocalRootID: rootID, - Start: maybe.Some([]byte{3}), - End: maybe.Some([]byte{4}), - Priority: highPriority, - QueueTime: time.Now(), - } - wh.Insert(item2) - require.Equal(2, wh.Len()) - - // High priority item should be returned first - work := wh.GetWork() - require.NotNil(work) - require.Equal(highPriority, work.Priority) - require.Equal(1, wh.Len()) - - // Low priority item should be returned next - work = wh.GetWork() - require.NotNil(work) - require.Equal(lowPriority, work.Priority) - require.Equal(0, wh.Len()) - - // Empty heap should return nil - work = wh.GetWork() - require.Nil(work) -} - -// TestGenericWorkHeapMerge tests merging functionality -func TestGenericWorkHeapMerge(t *testing.T) { - require := require.New(t) - - wh := NewGenericWorkHeap[[]byte](bytes.Compare, bytes.Equal) - rootID := ids.GenerateTestID() - - // Insert first item [1, 10] - item1 := &GenericWorkItem[[]byte]{ - LocalRootID: rootID, - Start: maybe.Some([]byte{1}), - End: maybe.Some([]byte{10}), - Priority: lowPriority, - QueueTime: time.Now(), - } - wh.MergeInsert(item1) - require.Equal(1, wh.Len()) - - // Insert adjacent item [10, 20] - should merge - item2 := &GenericWorkItem[[]byte]{ - LocalRootID: rootID, - Start: maybe.Some([]byte{10}), - End: maybe.Some([]byte{20}), - Priority: medPriority, - QueueTime: time.Now(), - } - wh.MergeInsert(item2) - require.Equal(1, wh.Len()) // Should still be 1 after merge - - // Get the merged item - merged := wh.GetWork() - require.NotNil(merged) - require.Equal([]byte{1}, merged.Start.Value()) - require.Equal([]byte{20}, merged.End.Value()) - require.Equal(medPriority, merged.Priority) // Should have highest priority -} - -// TestCompatibilityLayer tests the byte-based heap wrapper -func TestCompatibilityLayer(t *testing.T) { - require := require.New(t) - - // Test using NewByteWorkHeap - heap := NewByteWorkHeap() - require.NotNil(heap) - require.Equal(0, heap.Len()) - - rootID := ids.GenerateTestID() - item := &GenericWorkItem[[]byte]{ - LocalRootID: rootID, - Start: maybe.Some([]byte{1}), - End: maybe.Some([]byte{2}), - Priority: highPriority, - QueueTime: time.Now(), - } - - heap.Insert(item) - require.Equal(1, heap.Len()) - - work := heap.GetWork() - require.NotNil(work) - require.Equal(rootID, work.LocalRootID) - require.Equal([]byte{1}, work.Start.Value()) - require.Equal([]byte{2}, work.End.Value()) -} diff --git a/x/sync/manager.go b/x/sync/manager.go deleted file mode 100644 index 294526649..000000000 --- a/x/sync/manager.go +++ /dev/null @@ -1,1139 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package sync - -import ( - "bytes" - "context" - "errors" - "fmt" - "maps" - "math" - "slices" - "sync" - "sync/atomic" - "time" - - "go.uber.org/zap" - "google.golang.org/protobuf/proto" - - "github.com/luxfi/ids" - "github.com/luxfi/log" - "github.com/luxfi/math/set" - "github.com/luxfi/metric" - "github.com/luxfi/node/x/merkledb" - "github.com/luxfi/p2p" - "github.com/luxfi/container/maybe" - - pb "github.com/luxfi/node/proto/pb/sync" -) - -const ( - defaultRequestKeyLimit = maxKeyValuesLimit - defaultRequestByteSizeLimit = maxByteSizeLimit -) - -var ( - ErrAlreadyStarted = errors.New("cannot start a Manager that has already been started") - ErrAlreadyClosed = errors.New("Manager is closed") - ErrNoRangeProofClientProvided = errors.New("range proof client is a required field of the sync config") - ErrNoChangeProofClientProvided = errors.New("change proof client is a required field of the sync config") - ErrNoDatabaseProvided = errors.New("sync database is a required field of the sync config") - ErrNoLogProvided = errors.New("log is a required field of the sync config") - ErrZeroWorkLimit = errors.New("simultaneous work limit must be greater than 0") - ErrFinishedWithUnexpectedRoot = errors.New("finished syncing with an unexpected root") -) - -type priority byte - -// Note that [highPriority] > [medPriority] > [lowPriority]. -const ( - lowPriority priority = iota + 1 - medPriority - highPriority - retryPriority -) - -// Signifies that we should sync the range [start, end]. -// nil [start] means there is no lower bound. -// nil [end] means there is no upper bound. -// [localRootID] is the ID of the root of this range in our database. -// If we have no local root for this range, [localRootID] is ids.Empty. -type workItem struct { - start maybe.Maybe[[]byte] - end maybe.Maybe[[]byte] - priority priority - localRootID ids.ID - attempt int - queueTime time.Time -} - -func (w *workItem) requestFailed() { - attempt := w.attempt + 1 - - // Overflow check - if attempt > w.attempt { - w.attempt = attempt - } -} - -func newWorkItem(localRootID ids.ID, start maybe.Maybe[[]byte], end maybe.Maybe[[]byte], priority priority, queueTime time.Time) *workItem { - return &workItem{ - localRootID: localRootID, - start: start, - end: end, - priority: priority, - queueTime: queueTime, - } -} - -type Manager[T any] struct { - // Must be held when accessing [config.TargetRoot]. - syncTargetLock sync.RWMutex - config ManagerConfig[T] - - workLock sync.Mutex - // The number of work items currently being processed. - // Namely, the number of goroutines executing [doWork]. - // [workLock] must be held when accessing [processingWorkItems]. - processingWorkItems int - // [workLock] must be held while accessing [unprocessedWork]. - unprocessedWork *workHeap - // Signalled when: - // - An item is added to [unprocessedWork]. - // - An item is added to [processedWork]. - // - Close() is called. - // [workLock] is its inner lock. - unprocessedWorkCond sync.Cond - // [workLock] must be held while accessing [processedWork]. - processedWork *workHeap - - // When this is closed: - // - [closed] is true. - // - [cancelCtx] was called. - // - [workToBeDone] and [completedWork] are closed. - doneChan chan struct{} - - errLock sync.Mutex - // If non-nil, there was a fatal error. - // [errLock] must be held when accessing [fatalError]. - fatalError error - - // Cancels all currently processing work items. - cancelCtx context.CancelFunc - - // Set to true when StartSyncing is called. - syncing bool - closeOnce sync.Once - tokenSize int - - stateSyncNodeIdx uint32 - metrics SyncMetrics -} - -type ManagerConfig[T any] struct { - DB DB - RangeProofClient *p2p.Client - ChangeProofClient *p2p.Client - SimultaneousWorkLimit int - Log log.Logger - TargetRoot ids.ID - BranchFactor merkledb.BranchFactor - StateSyncNodes []ids.NodeID - // If not specified, [merkledb.DefaultHasher] will be used. - Hasher merkledb.Hasher -} - -func NewManager[T any](config ManagerConfig[T], registerer metric.Registerer) (*Manager[T], error) { - switch { - case config.RangeProofClient == nil: - return nil, ErrNoRangeProofClientProvided - case config.ChangeProofClient == nil: - return nil, ErrNoChangeProofClientProvided - case config.DB == nil: - return nil, ErrNoDatabaseProvided - case config.Log == nil: - return nil, ErrNoLogProvided - case config.SimultaneousWorkLimit == 0: - return nil, ErrZeroWorkLimit - } - if err := config.BranchFactor.Valid(); err != nil { - return nil, err - } - - if config.Hasher == nil { - config.Hasher = merkledb.DefaultHasher - } - - metrics, err := NewMetrics("sync", registerer) - if err != nil { - return nil, err - } - - m := &Manager[T]{ - config: config, - doneChan: make(chan struct{}), - unprocessedWork: newWorkHeap(), - processedWork: newWorkHeap(), - tokenSize: merkledb.BranchFactorToTokenSize[config.BranchFactor], - metrics: metrics, - } - m.unprocessedWorkCond.L = &m.workLock - - return m, nil -} - -func (m *Manager[T]) Start(ctx context.Context) error { - m.workLock.Lock() - defer m.workLock.Unlock() - - if m.syncing { - return ErrAlreadyStarted - } - - m.config.Log.Info("starting sync", log.Stringer("target root", m.config.TargetRoot)) - - // Add work item to fetch the entire key range. - // Note that this will be the first work item to be processed. - m.unprocessedWork.Insert(newWorkItem(ids.Empty, maybe.Nothing[[]byte](), maybe.Nothing[[]byte](), lowPriority, time.Now())) - - m.syncing = true - ctx, m.cancelCtx = context.WithCancel(ctx) - - go m.sync(ctx) - return nil -} - -// sync awaits signal on [m.unprocessedWorkCond], which indicates that there -// is work to do or syncing completes. If there is work, sync will dispatch a goroutine to do -// the work. -func (m *Manager[T]) sync(ctx context.Context) { - defer func() { - // Invariant: [m.workLock] is held when this goroutine begins. - m.close() - m.workLock.Unlock() - }() - - // Keep doing work until we're closed, done or [ctx] is canceled. - m.workLock.Lock() - for { - // Invariant: [m.workLock] is held here. - switch { - case ctx.Err() != nil: - return // [m.workLock] released by defer. - case m.processingWorkItems >= m.config.SimultaneousWorkLimit: - // We're already processing the maximum number of work items. - // Wait until one of them finishes. - m.unprocessedWorkCond.Wait() - case m.unprocessedWork.Len() == 0: - if m.processingWorkItems == 0 { - // There's no work to do, and there are no work items being processed - // which could cause work to be added, so we're done. - return // [m.workLock] released by defer. - } - // There's no work to do. - // Note that if [m].Close() is called, or [ctx] is canceled, - // Close() will be called, which will broadcast on [m.unprocessedWorkCond], - // which will cause Wait() to return, and this goroutine to exit. - m.unprocessedWorkCond.Wait() - default: - m.processingWorkItems++ - work := m.unprocessedWork.GetWork() - go m.doWork(ctx, work) - } - } -} - -// Close will stop the syncing process -func (m *Manager[T]) Close() { - m.workLock.Lock() - defer m.workLock.Unlock() - - m.close() -} - -// close is called when there is a fatal error or sync is complete. -// [workLock] must be held -func (m *Manager[T]) close() { - m.closeOnce.Do(func() { - // Don't process any more work items. - // Drop currently processing work items. - if m.cancelCtx != nil { - m.cancelCtx() - } - - // ensure any goroutines waiting for work from the heaps gets released - m.unprocessedWork.Close() - m.unprocessedWorkCond.Signal() - m.processedWork.Close() - - // signal all code waiting on the sync to complete - close(m.doneChan) - }) -} - -func (m *Manager[T]) finishWorkItem() { - m.workLock.Lock() - defer m.workLock.Unlock() - - m.processingWorkItems-- - m.unprocessedWorkCond.Signal() -} - -// Processes [item] by fetching a change or range proof. -func (m *Manager[T]) doWork(ctx context.Context, work *workItem) { - // Backoff for failed requests accounting for time this job has already - // spent waiting in the unprocessed queue - now := time.Now() - waitTime := max(0, calculateBackoff(work.attempt)-now.Sub(work.queueTime)) - - // Check if we can start this work item before the context deadline - deadline, ok := ctx.Deadline() - if ok && now.Add(waitTime).After(deadline) { - m.finishWorkItem() - return - } - - select { - case <-ctx.Done(): - m.finishWorkItem() - return - case <-time.After(waitTime): - } - - if work.localRootID == ids.Empty { - // the keys in this range have not been downloaded, so get all key/values - m.requestRangeProof(ctx, work) - } else { - // the keys in this range have already been downloaded, but the root changed, so get all changes - m.requestChangeProof(ctx, work) - } -} - -// Fetch and apply the change proof given by [work]. -// Assumes [m.workLock] is not held. -func (m *Manager[T]) requestChangeProof(ctx context.Context, work *workItem) { - targetRootID := m.getTargetRoot() - - if work.localRootID == targetRootID { - // Start root is the same as the end root, so we're done. - m.completeWorkItem(ctx, work, work.end, targetRootID, nil) - m.finishWorkItem() - return - } - - if targetRootID == ids.Empty { - defer m.finishWorkItem() - - // The trie is empty after this change. - // Delete all the key-value pairs in the range. - if err := m.config.DB.Clear(); err != nil { - m.setError(err) - return - } - work.start = maybe.Nothing[[]byte]() - m.completeWorkItem(ctx, work, maybe.Nothing[[]byte](), targetRootID, nil) - return - } - - request := &pb.SyncGetChangeProofRequest{ - StartRootHash: work.localRootID[:], - EndRootHash: targetRootID[:], - StartKey: &pb.MaybeBytes{ - Value: work.start.Value(), - IsNothing: work.start.IsNothing(), - }, - EndKey: &pb.MaybeBytes{ - Value: work.end.Value(), - IsNothing: work.end.IsNothing(), - }, - KeyLimit: defaultRequestKeyLimit, - BytesLimit: defaultRequestByteSizeLimit, - } - - requestBytes, err := proto.Marshal(request) - if err != nil { - m.finishWorkItem() - m.setError(err) - return - } - - onResponse := func(ctx context.Context, _ ids.NodeID, responseBytes []byte, err error) { - defer m.finishWorkItem() - - if err := m.handleChangeProofResponse(ctx, targetRootID, work, request, responseBytes, err); err != nil { - m.config.Log.Debug("dropping response", zap.Error(err), zap.Stringer("request", request)) - m.retryWork(work) - return - } - } - - if err := m.sendRequest(ctx, m.config.ChangeProofClient, requestBytes, onResponse); err != nil { - m.finishWorkItem() - m.setError(err) - return - } - - m.metrics.RequestMade() -} - -// Fetch and apply the range proof given by [work]. -// Assumes [m.workLock] is not held. -func (m *Manager[T]) requestRangeProof(ctx context.Context, work *workItem) { - targetRootID := m.getTargetRoot() - - if targetRootID == ids.Empty { - defer m.finishWorkItem() - - if err := m.config.DB.Clear(); err != nil { - m.setError(err) - return - } - work.start = maybe.Nothing[[]byte]() - m.completeWorkItem(ctx, work, maybe.Nothing[[]byte](), targetRootID, nil) - return - } - - request := &pb.SyncGetRangeProofRequest{ - RootHash: targetRootID[:], - StartKey: &pb.MaybeBytes{ - Value: work.start.Value(), - IsNothing: work.start.IsNothing(), - }, - EndKey: &pb.MaybeBytes{ - Value: work.end.Value(), - IsNothing: work.end.IsNothing(), - }, - KeyLimit: defaultRequestKeyLimit, - BytesLimit: defaultRequestByteSizeLimit, - } - - requestBytes, err := proto.Marshal(request) - if err != nil { - m.finishWorkItem() - m.setError(err) - return - } - - onResponse := func(ctx context.Context, _ ids.NodeID, responseBytes []byte, appErr error) { - defer m.finishWorkItem() - - if err := m.handleRangeProofResponse(ctx, targetRootID, work, request, responseBytes, appErr); err != nil { - m.config.Log.Debug("dropping response", zap.Error(err), zap.Stringer("request", request)) - m.retryWork(work) - return - } - } - - if err := m.sendRequest(ctx, m.config.RangeProofClient, requestBytes, onResponse); err != nil { - m.finishWorkItem() - m.setError(err) - return - } - - m.metrics.RequestMade() -} - -func (m *Manager[T]) sendRequest(ctx context.Context, client *p2p.Client, requestBytes []byte, onResponse p2p.ResponseCallback) error { - if len(m.config.StateSyncNodes) == 0 { - return client.RequestAny(ctx, requestBytes, onResponse) - } - - // Get the next nodeID to query using the [nodeIdx] offset. - // If we're out of nodes, loop back to 0. - // We do this try to query a different node each time if possible. - nodeIdx := atomic.AddUint32(&m.stateSyncNodeIdx, 1) - nodeID := m.config.StateSyncNodes[nodeIdx%uint32(len(m.config.StateSyncNodes))] - return client.Request(ctx, set.Of(nodeID), requestBytes, onResponse) -} - -func (m *Manager[T]) retryWork(work *workItem) { - work.priority = retryPriority - work.queueTime = time.Now() - work.requestFailed() - - m.workLock.Lock() - m.unprocessedWork.Insert(work) - m.workLock.Unlock() - m.unprocessedWorkCond.Signal() -} - -// Returns an error if we should drop the response -func (m *Manager[T]) shouldHandleResponse( - bytesLimit uint32, - responseBytes []byte, - err error, -) error { - if err != nil { - m.metrics.RequestFailed() - return err - } - - m.metrics.RequestSucceeded() - - // Guard against applying proofs after the manager has been closed. - select { - case <-m.doneChan: - return ErrAlreadyClosed - default: - } - - if len(responseBytes) > int(bytesLimit) { - return fmt.Errorf("%w: (%d) > %d)", errTooManyBytes, len(responseBytes), bytesLimit) - } - - return nil -} - -func (m *Manager[T]) handleRangeProofResponse( - ctx context.Context, - targetRootID ids.ID, - work *workItem, - request *pb.SyncGetRangeProofRequest, - responseBytes []byte, - err error, -) error { - if err := m.shouldHandleResponse(request.BytesLimit, responseBytes, err); err != nil { - return err - } - - var rangeProofProto pb.RangeProof - if err := proto.Unmarshal(responseBytes, &rangeProofProto); err != nil { - return err - } - - var rangeProof merkledb.RangeProof - if err := rangeProof.UnmarshalProto(&rangeProofProto); err != nil { - return err - } - - if err := verifyRangeProof( - ctx, - &rangeProof, - int(request.KeyLimit), - maybeBytesToMaybe(request.StartKey), - maybeBytesToMaybe(request.EndKey), - request.RootHash, - m.tokenSize, - m.config.Hasher, - ); err != nil { - return err - } - - largestHandledKey := work.end - - // Replace all the key-value pairs in the DB from start to end with values from the response. - if err := m.config.DB.CommitRangeProof(ctx, work.start, work.end, &rangeProof); err != nil { - m.setError(err) - return nil - } - - if len(rangeProof.KeyChanges) > 0 { - largestHandledKey = maybe.Some(rangeProof.KeyChanges[len(rangeProof.KeyChanges)-1].Key) - } - - m.completeWorkItem(ctx, work, largestHandledKey, targetRootID, rangeProof.EndProof) - return nil -} - -func (m *Manager[T]) handleChangeProofResponse( - ctx context.Context, - targetRootID ids.ID, - work *workItem, - request *pb.SyncGetChangeProofRequest, - responseBytes []byte, - err error, -) error { - if err := m.shouldHandleResponse(request.BytesLimit, responseBytes, err); err != nil { - return err - } - - var changeProofResp pb.SyncGetChangeProofResponse - if err := proto.Unmarshal(responseBytes, &changeProofResp); err != nil { - return err - } - - startKey := maybeBytesToMaybe(request.StartKey) - endKey := maybeBytesToMaybe(request.EndKey) - - switch changeProofResp := changeProofResp.Response.(type) { - case *pb.SyncGetChangeProofResponse_ChangeProof: - // The server had enough history to send us a change proof - var changeProof merkledb.ChangeProof - if err := changeProof.UnmarshalProto(changeProofResp.ChangeProof); err != nil { - return err - } - - // Ensure the response does not contain more than the requested number of leaves - // and the start and end roots match the requested roots. - if len(changeProof.KeyChanges) > int(request.KeyLimit) { - return fmt.Errorf( - "%w: (%d) > %d)", - errTooManyKeys, len(changeProof.KeyChanges), request.KeyLimit, - ) - } - - endRoot, err := ids.ToID(request.EndRootHash) - if err != nil { - return err - } - - if err := m.config.DB.VerifyChangeProof( - ctx, - &changeProof, - startKey, - endKey, - endRoot, - ); err != nil { - return fmt.Errorf("%w due to %w", errInvalidChangeProof, err) - } - - largestHandledKey := work.end - // if the proof wasn't empty, apply changes to the sync DB - if len(changeProof.KeyChanges) > 0 { - if err := m.config.DB.CommitChangeProof(ctx, &changeProof); err != nil { - m.setError(err) - return nil - } - largestHandledKey = maybe.Some(changeProof.KeyChanges[len(changeProof.KeyChanges)-1].Key) - } - - m.completeWorkItem(ctx, work, largestHandledKey, targetRootID, changeProof.EndProof) - case *pb.SyncGetChangeProofResponse_RangeProof: - var rangeProof merkledb.RangeProof - if err := rangeProof.UnmarshalProto(changeProofResp.RangeProof); err != nil { - return err - } - - // The server did not have enough history to send us a change proof - // so they sent a range proof instead. - if err := verifyRangeProof( - ctx, - &rangeProof, - int(request.KeyLimit), - startKey, - endKey, - request.EndRootHash, - m.tokenSize, - m.config.Hasher, - ); err != nil { - return err - } - - largestHandledKey := work.end - if len(rangeProof.KeyChanges) > 0 { - // Add all the key-value pairs we got to the database. - if err := m.config.DB.CommitRangeProof(ctx, work.start, work.end, &rangeProof); err != nil { - m.setError(err) - return nil - } - largestHandledKey = maybe.Some(rangeProof.KeyChanges[len(rangeProof.KeyChanges)-1].Key) - } - - m.completeWorkItem(ctx, work, largestHandledKey, targetRootID, rangeProof.EndProof) - default: - return fmt.Errorf( - "%w: %T", - errUnexpectedChangeProofResponse, changeProofResp, - ) - } - - return nil -} - -// findNextKey returns the start of the key range that should be fetched next -// given that we just received a range/change proof that proved a range of -// key-value pairs ending at [lastReceivedKey]. -// -// [rangeEnd] is the end of the range that we want to fetch. -// -// Returns Nothing if there are no more keys to fetch in [lastReceivedKey, rangeEnd]. -// -// [endProof] is the end proof of the last proof received. -// -// Invariant: [lastReceivedKey] < [rangeEnd]. -// If [rangeEnd] is Nothing it's considered > [lastReceivedKey]. -func (m *Manager[T]) findNextKey( - ctx context.Context, - lastReceivedKey []byte, - rangeEnd maybe.Maybe[[]byte], - endProof []merkledb.ProofNode, -) (maybe.Maybe[[]byte], error) { - if len(endProof) == 0 { - // We try to find the next key to fetch by looking at the end proof. - // If the end proof is empty, we have no information to use. - // Start fetching from the next key after [lastReceivedKey]. - nextKey := lastReceivedKey - nextKey = append(nextKey, 0) - return maybe.Some(nextKey), nil - } - - // We want the first key larger than the [lastReceivedKey]. - // This is done by taking two proofs for the same key - // (one that was just received as part of a proof, and one from the local db) - // and traversing them from the longest key to the shortest key. - // For each node in these proofs, compare if the children of that node exist - // or have the same ID in the other proof. - proofKeyPath := merkledb.ToKey(lastReceivedKey) - - // If the received proof is an exclusion proof, the last node may be for a - // key that is after the [lastReceivedKey]. - // If the last received node's key is after the [lastReceivedKey], it can - // be removed to obtain a valid proof for a prefix of the [lastReceivedKey]. - if !proofKeyPath.HasPrefix(endProof[len(endProof)-1].Key) { - endProof = endProof[:len(endProof)-1] - // update the proofKeyPath to be for the prefix - proofKeyPath = endProof[len(endProof)-1].Key - } - - // get a proof for the same key as the received proof from the local db - localProofOfKey, err := m.config.DB.GetProof(ctx, proofKeyPath.Bytes()) - if err != nil { - return maybe.Nothing[[]byte](), err - } - localProofNodes := localProofOfKey.Path - - // The local proof may also be an exclusion proof with an extra node. - // Remove this extra node if it exists to get a proof of the same key as the received proof - if !proofKeyPath.HasPrefix(localProofNodes[len(localProofNodes)-1].Key) { - localProofNodes = localProofNodes[:len(localProofNodes)-1] - } - - nextKey := maybe.Nothing[[]byte]() - - // Add sentinel node back into the localProofNodes, if it is missing. - // Required to ensure that a common node exists in both proofs - if len(localProofNodes) > 0 && localProofNodes[0].Key.Length() != 0 { - sentinel := merkledb.ProofNode{ - Children: map[byte]ids.ID{ - localProofNodes[0].Key.Token(0, m.tokenSize): ids.Empty, - }, - } - localProofNodes = append([]merkledb.ProofNode{sentinel}, localProofNodes...) - } - - // Add sentinel node back into the endProof, if it is missing. - // Required to ensure that a common node exists in both proofs - if len(endProof) > 0 && endProof[0].Key.Length() != 0 { - sentinel := merkledb.ProofNode{ - Children: map[byte]ids.ID{ - endProof[0].Key.Token(0, m.tokenSize): ids.Empty, - }, - } - endProof = append([]merkledb.ProofNode{sentinel}, endProof...) - } - - localProofNodeIndex := len(localProofNodes) - 1 - receivedProofNodeIndex := len(endProof) - 1 - - // traverse the two proofs from the deepest nodes up to the sentinel node until a difference is found - for localProofNodeIndex >= 0 && receivedProofNodeIndex >= 0 && nextKey.IsNothing() { - localProofNode := localProofNodes[localProofNodeIndex] - receivedProofNode := endProof[receivedProofNodeIndex] - - // [deepestNode] is the proof node with the longest key (deepest in the trie) in the - // two proofs that hasn't been handled yet. - // [deepestNodeFromOtherProof] is the proof node from the other proof with - // the same key/depth if it exists, nil otherwise. - var deepestNode, deepestNodeFromOtherProof *merkledb.ProofNode - - // select the deepest proof node from the two proofs - switch { - case receivedProofNode.Key.Length() > localProofNode.Key.Length(): - // there was a branch node in the received proof that isn't in the local proof - // see if the received proof node has children not present in the local proof - deepestNode = &receivedProofNode - - // we have dealt with this received node, so move on to the next received node - receivedProofNodeIndex-- - - case localProofNode.Key.Length() > receivedProofNode.Key.Length(): - // there was a branch node in the local proof that isn't in the received proof - // see if the local proof node has children not present in the received proof - deepestNode = &localProofNode - - // we have dealt with this local node, so move on to the next local node - localProofNodeIndex-- - - default: - // the two nodes are at the same depth - // see if any of the children present in the local proof node are different - // from the children in the received proof node - deepestNode = &localProofNode - deepestNodeFromOtherProof = &receivedProofNode - - // we have dealt with this local node and received node, so move on to the next nodes - localProofNodeIndex-- - receivedProofNodeIndex-- - } - - // We only want to look at the children with keys greater than the proofKey. - // The proof key has the deepest node's key as a prefix, - // so only the next token of the proof key needs to be considered. - - // If the deepest node has the same key as [proofKeyPath], - // then all of its children have keys greater than the proof key, - // so we can start at the 0 token. - startingChildToken := 0 - - // If the deepest node has a key shorter than the key being proven, - // we can look at the next token index of the proof key to determine which of that - // node's children have keys larger than [proofKeyPath]. - // Any child with a token greater than the [proofKeyPath]'s token at that - // index will have a larger key. - if deepestNode.Key.Length() < proofKeyPath.Length() { - startingChildToken = int(proofKeyPath.Token(deepestNode.Key.Length(), m.tokenSize)) + 1 - } - - // determine if there are any differences in the children for the deepest unhandled node of the two proofs - if childIndex, hasDifference := findChildDifference(deepestNode, deepestNodeFromOtherProof, startingChildToken); hasDifference { - nextKey = maybe.Some(deepestNode.Key.Extend(merkledb.ToToken(childIndex, m.tokenSize)).Bytes()) - break - } - } - - // If the nextKey is before or equal to the [lastReceivedKey] - // then we couldn't find a better answer than the [lastReceivedKey]. - // Set the nextKey to [lastReceivedKey] + 0, which is the first key in - // the open range (lastReceivedKey, rangeEnd). - if nextKey.HasValue() && bytes.Compare(nextKey.Value(), lastReceivedKey) <= 0 { - nextKeyVal := slices.Clone(lastReceivedKey) - nextKeyVal = append(nextKeyVal, 0) - nextKey = maybe.Some(nextKeyVal) - } - - // If the [nextKey] is larger than the end of the range, return Nothing to signal that there is no next key in range - if rangeEnd.HasValue() && bytes.Compare(nextKey.Value(), rangeEnd.Value()) >= 0 { - return maybe.Nothing[[]byte](), nil - } - - // the nextKey is within the open range (lastReceivedKey, rangeEnd), so return it - return nextKey, nil -} - -func (m *Manager[T]) Error() error { - m.errLock.Lock() - defer m.errLock.Unlock() - - return m.fatalError -} - -// Wait blocks until one of the following occurs: -// - sync is complete. -// - sync fatally errored. -// - [ctx] is canceled. -// If [ctx] is canceled, returns [ctx].Err(). -func (m *Manager[T]) Wait(ctx context.Context) error { - select { - case <-m.doneChan: - case <-ctx.Done(): - return ctx.Err() - } - - // There was a fatal error. - if err := m.Error(); err != nil { - return err - } - - root, err := m.config.DB.GetMerkleRoot(ctx) - if err != nil { - return err - } - - if targetRootID := m.getTargetRoot(); targetRootID != root { - // This should never happen. - return fmt.Errorf("%w: expected %s, got %s", ErrFinishedWithUnexpectedRoot, targetRootID, root) - } - - m.config.Log.Info("completed", log.Stringer("root", root)) - return nil -} - -func (m *Manager[T]) UpdateSyncTarget(syncTargetRoot ids.ID) error { - m.syncTargetLock.Lock() - defer m.syncTargetLock.Unlock() - - m.workLock.Lock() - defer m.workLock.Unlock() - - select { - case <-m.doneChan: - return ErrAlreadyClosed - default: - } - - if m.config.TargetRoot == syncTargetRoot { - // the target hasn't changed, so there is nothing to do - return nil - } - - m.config.Log.Debug("updated sync target", log.Stringer("target", syncTargetRoot)) - m.config.TargetRoot = syncTargetRoot - - // move all completed ranges into the work heap with high priority - shouldSignal := m.processedWork.Len() > 0 - for m.processedWork.Len() > 0 { - // Note that [m.processedWork].Close() hasn't - // been called because we have [m.workLock] - // and we checked that [m.closed] is false. - currentItem := m.processedWork.GetWork() - currentItem.priority = highPriority - m.unprocessedWork.Insert(currentItem) - } - if shouldSignal { - // Only signal once because we only have 1 goroutine - // waiting on [m.unprocessedWorkCond]. - m.unprocessedWorkCond.Signal() - } - return nil -} - -func (m *Manager[T]) getTargetRoot() ids.ID { - m.syncTargetLock.RLock() - defer m.syncTargetLock.RUnlock() - - return m.config.TargetRoot -} - -// Record that there was a fatal error and begin shutting down. -func (m *Manager[T]) setError(err error) { - m.errLock.Lock() - defer m.errLock.Unlock() - - m.config.Log.Error("sync errored", log.Reflect("error", err)) - m.fatalError = err - // Call in goroutine because we might be holding [m.workLock] - // which [m.Close] will try to acquire. - go m.Close() -} - -// Mark that we've fetched all the key-value pairs in the range -// [workItem.start, largestHandledKey] for the trie with root [rootID]. -// -// If [workItem.start] is Nothing, then we've fetched all the key-value -// pairs up to and including [largestHandledKey]. -// -// If [largestHandledKey] is Nothing, then we've fetched all the key-value -// pairs at and after [workItem.start]. -// -// [proofOfLargestKey] is the end proof for the range/change proof -// that gave us the range up to and including [largestHandledKey]. -// -// Assumes [m.workLock] is not held. -func (m *Manager[T]) completeWorkItem(ctx context.Context, work *workItem, largestHandledKey maybe.Maybe[[]byte], rootID ids.ID, proofOfLargestKey []merkledb.ProofNode) { - if !maybe.Equal(largestHandledKey, work.end, bytes.Equal) { - // The largest handled key isn't equal to the end of the work item. - // Find the start of the next key range to fetch. - // Note that [largestHandledKey] can't be Nothing. - // Proof: Suppose it is. That means that we got a range/change proof that proved up to the - // greatest key-value pair in the database. That means we requested a proof with no upper - // bound. That is, [workItem.end] is Nothing. Since we're here, [bothNothing] is false, - // which means [workItem.end] isn't Nothing. Contradiction. - nextStartKey, err := m.findNextKey(ctx, largestHandledKey.Value(), work.end, proofOfLargestKey) - if err != nil { - m.setError(err) - return - } - - // nextStartKey being Nothing indicates that the entire range has been completed - if nextStartKey.IsNothing() { - largestHandledKey = work.end - } else { - // the full range wasn't completed, so enqueue a new work item for the range [nextStartKey, workItem.end] - m.enqueueWork(newWorkItem(work.localRootID, nextStartKey, work.end, work.priority, time.Now())) - largestHandledKey = nextStartKey - } - } - - // Process [work] while holding [syncTargetLock] to ensure that object - // is added to the right queue, even if a target update is triggered - m.syncTargetLock.RLock() - defer m.syncTargetLock.RUnlock() - - stale := m.config.TargetRoot != rootID - if stale { - // the root has changed, so reinsert with high priority - m.enqueueWork(newWorkItem(rootID, work.start, largestHandledKey, highPriority, time.Now())) - } else { - m.workLock.Lock() - defer m.workLock.Unlock() - - m.processedWork.MergeInsert(newWorkItem(rootID, work.start, largestHandledKey, work.priority, time.Now())) - } - - // completed the range [work.start, lastKey], log and record in the completed work heap - m.config.Log.Debug("completed range", - log.Stringer("start", work.start), - log.Stringer("end", largestHandledKey), - log.Stringer("rootID", rootID), - log.Bool("stale", stale), - ) -} - -// Queue the given key range to be fetched and applied. -// If there are sufficiently few unprocessed/processing work items, -// splits the range into two items and queues them both. -// Assumes [m.workLock] is not held. -func (m *Manager[T]) enqueueWork(work *workItem) { - m.workLock.Lock() - defer func() { - m.workLock.Unlock() - m.unprocessedWorkCond.Signal() - }() - - if m.processingWorkItems+m.unprocessedWork.Len() > 2*m.config.SimultaneousWorkLimit { - // There are too many work items already, don't split the range - m.unprocessedWork.Insert(work) - return - } - - // Split the remaining range into to 2. - // Find the middle point. - mid := midPoint(work.start, work.end) - - // Check if start and mid are equal - startEqualsMid := maybe.Equal(work.start, mid, bytes.Equal) - // Check if mid and end are equal - midEqualsEnd := maybe.Equal(mid, work.end, bytes.Equal) - - if startEqualsMid || midEqualsEnd { - // The range is too small to split, or midpoint calculation produced - // overlapping boundaries. This prevents work items like [start, start] - // and [start, end] which would violate the invariant that there are - // no overlapping ranges in [m.unprocessedWork] and [m.processedWork]. - m.unprocessedWork.Insert(work) - return - } - - // first item gets higher priority than the second to encourage finished ranges to grow - // rather than start a new range that is not contiguous with existing completed ranges - first := newWorkItem(work.localRootID, work.start, mid, medPriority, time.Now()) - second := newWorkItem(work.localRootID, mid, work.end, lowPriority, time.Now()) - - m.unprocessedWork.Insert(first) - m.unprocessedWork.Insert(second) -} - -// find the midpoint between two keys -// start is expected to be less than end -// Nothing/nil [start] is treated as all 0's -// Nothing/nil [end] is treated as all 255's -func midPoint(startMaybe, endMaybe maybe.Maybe[[]byte]) maybe.Maybe[[]byte] { - start := startMaybe.Value() - end := endMaybe.Value() - length := max(len(end), len(start)) - - if length == 0 { - if endMaybe.IsNothing() { - return maybe.Some([]byte{127}) - } else if len(end) == 0 { - return maybe.Nothing[[]byte]() - } - } - - // This check deals with cases where the end has a 255(or is nothing which is treated as all 255s) and the start key ends 255. - // For example, midPoint([255], nothing) should be [255, 127], not [255]. - // The result needs the extra byte added on to the end to deal with the fact that the naive midpoint between 255 and 255 would be 255 - if (len(start) > 0 && start[len(start)-1] == 255) && (len(end) == 0 || end[len(end)-1] == 255) { - length++ - } - - leftover := 0 - midpoint := make([]byte, length+1) - for i := 0; i < length; i++ { - startVal := 0 - if i < len(start) { - startVal = int(start[i]) - } - - endVal := 0 - if endMaybe.IsNothing() { - endVal = 255 - } - if i < len(end) { - endVal = int(end[i]) - } - - total := startVal + endVal + leftover - leftover = 0 - // if total is odd, when we divide, we will lose the .5, - // record that in the leftover for the next digits - if total%2 == 1 { - leftover = 256 - } - - // find the midpoint between the start and the end - total /= 2 - - // larger than byte can hold, so carry over to previous byte - if total >= 256 { - total -= 256 - index := i - 1 - for index > 0 && midpoint[index] == 255 { - midpoint[index] = 0 - index-- - } - midpoint[index]++ - } - midpoint[i] = byte(total) - } - if leftover > 0 { - midpoint[length] = 127 - } else { - midpoint = midpoint[0:length] - } - return maybe.Some(midpoint) -} - -// findChildDifference returns the first child index that is different between node 1 and node 2 if one exists and -// a bool indicating if any difference was found -func findChildDifference(node1, node2 *merkledb.ProofNode, startIndex int) (byte, bool) { - // Children indices >= [startIndex] present in at least one of the nodes. - childIndices := make(set.Set[byte]) - for _, node := range []*merkledb.ProofNode{node1, node2} { - if node == nil { - continue - } - for key := range node.Children { - if int(key) >= startIndex { - childIndices.Add(key) - } - } - } - - sortedChildIndices := slices.Collect(maps.Keys(childIndices)) - slices.Sort(sortedChildIndices) - var ( - child1, child2 ids.ID - ok1, ok2 bool - ) - for _, childIndex := range sortedChildIndices { - if node1 != nil { - child1, ok1 = node1.Children[childIndex] - } - if node2 != nil { - child2, ok2 = node2.Children[childIndex] - } - // if one node has a child and the other doesn't or the children ids don't match, - // return the current child index as the first difference - if (ok1 || ok2) && child1 != child2 { - return childIndex, true - } - } - // there were no differences found - return 0, false -} - -func calculateBackoff(attempt int) time.Duration { - if attempt == 0 { - return 0 - } - - return min( - initialRetryWait*time.Duration(math.Pow(retryWaitFactor, float64(attempt))), - maxRetryWait, - ) -} diff --git a/x/sync/metrics.go b/x/sync/metrics.go deleted file mode 100644 index f11bfeb42..000000000 --- a/x/sync/metrics.go +++ /dev/null @@ -1,90 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package sync - -import ( - "sync" - - "github.com/luxfi/metric" -) - -var ( - _ SyncMetrics = (*mockMetrics)(nil) - _ SyncMetrics = (*metricsImpl)(nil) -) - -type SyncMetrics interface { - RequestFailed() - RequestMade() - RequestSucceeded() -} - -type mockMetrics struct { - lock sync.Mutex - requestsFailed int - requestsMade int - requestsSucceeded int -} - -func (m *mockMetrics) RequestFailed() { - m.lock.Lock() - defer m.lock.Unlock() - - m.requestsFailed++ -} - -func (m *mockMetrics) RequestMade() { - m.lock.Lock() - defer m.lock.Unlock() - - m.requestsMade++ -} - -func (m *mockMetrics) RequestSucceeded() { - m.lock.Lock() - defer m.lock.Unlock() - - m.requestsSucceeded++ -} - -type metricsImpl struct { - requestsFailed metric.Counter - requestsMade metric.Counter - requestsSucceeded metric.Counter -} - -func NewMetrics(namespace string, reg metric.Registerer) (SyncMetrics, error) { - if reg == nil { - reg = metric.NewNoOpRegistry() - } - m := metricsImpl{ - requestsFailed: reg.NewCounter( - metric.AppendNamespace(namespace, "requests_failed"), - "cumulative amount of failed proof requests", - ), - requestsMade: reg.NewCounter( - metric.AppendNamespace(namespace, "requests_made"), - "cumulative amount of proof requests made", - ), - requestsSucceeded: reg.NewCounter( - metric.AppendNamespace(namespace, "requests_succeeded"), - "cumulative amount of proof requests that were successful", - ), - } - return &m, nil -} - -func (m *metricsImpl) RequestFailed() { - m.requestsFailed.Inc() -} - -func (m *metricsImpl) RequestMade() { - m.requestsMade.Inc() -} - -func (m *metricsImpl) RequestSucceeded() { - m.requestsSucceeded.Inc() -} diff --git a/x/sync/mock_client.go b/x/sync/mock_client.go deleted file mode 100644 index 10ed5738e..000000000 --- a/x/sync/mock_client.go +++ /dev/null @@ -1,74 +0,0 @@ -//go:build grpc - -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/luxfi/node/x/sync (interfaces: Client) -// -// Generated by this command: -// -// mockgen -package=sync -destination=x/sync/mock_client.go github.com/luxfi/node/x/sync Client -// - -// Package sync is a generated GoMock package. -package sync - -import ( - context "context" - reflect "reflect" - - gomock "github.com/luxfi/mock/gomock" - sync "github.com/luxfi/node/proto/pb/sync" - merkledb "github.com/luxfi/node/x/merkledb" -) - -// MockClient is a mock of Client interface. -type MockClient struct { - ctrl *gomock.Controller - recorder *MockClientMockRecorder -} - -// MockClientMockRecorder is the mock recorder for MockClient. -type MockClientMockRecorder struct { - mock *MockClient -} - -// NewMockClient creates a new mock instance. -func NewMockClient(ctrl *gomock.Controller) *MockClient { - mock := &MockClient{ctrl: ctrl} - mock.recorder = &MockClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockClient) EXPECT() *MockClientMockRecorder { - return m.recorder -} - -// GetChangeProof mocks base method. -func (m *MockClient) GetChangeProof(arg0 context.Context, arg1 *sync.SyncGetChangeProofRequest, arg2 DB) (*ChangeOrRangeProof, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChangeProof", arg0, arg1, arg2) - ret0, _ := ret[0].(*ChangeOrRangeProof) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetChangeProof indicates an expected call of GetChangeProof. -func (mr *MockClientMockRecorder) GetChangeProof(arg0, arg1, arg2 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChangeProof", reflect.TypeOf((*MockClient)(nil).GetChangeProof), arg0, arg1, arg2) -} - -// GetRangeProof mocks base method. -func (m *MockClient) GetRangeProof(arg0 context.Context, arg1 *sync.SyncGetRangeProofRequest) (*merkledb.RangeProof, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRangeProof", arg0, arg1) - ret0, _ := ret[0].(*merkledb.RangeProof) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetRangeProof indicates an expected call of GetRangeProof. -func (mr *MockClientMockRecorder) GetRangeProof(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRangeProof", reflect.TypeOf((*MockClient)(nil).GetRangeProof), arg0, arg1) -} diff --git a/x/sync/mock_network_client.go b/x/sync/mock_network_client.go deleted file mode 100644 index d7baa4c84..000000000 --- a/x/sync/mock_network_client.go +++ /dev/null @@ -1,132 +0,0 @@ -//go:build grpc - -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/luxfi/node/x/sync (interfaces: NetworkClient) -// -// Generated by this command: -// -// mockgen -package=sync -destination=x/sync/mock_network_client.go github.com/luxfi/node/x/sync NetworkClient -// - -// Package sync is a generated GoMock package. -package sync - -import ( - context "context" - reflect "reflect" - - gomock "go.uber.org/mock/gomock" - - ids "github.com/luxfi/ids" - version "github.com/luxfi/node/version" -) - -// MockNetworkClient is a mock of NetworkClient interface. -type MockNetworkClient struct { - ctrl *gomock.Controller - recorder *MockNetworkClientMockRecorder -} - -// MockNetworkClientMockRecorder is the mock recorder for MockNetworkClient. -type MockNetworkClientMockRecorder struct { - mock *MockNetworkClient -} - -// NewMockNetworkClient creates a new mock instance. -func NewMockNetworkClient(ctrl *gomock.Controller) *MockNetworkClient { - mock := &MockNetworkClient{ctrl: ctrl} - mock.recorder = &MockNetworkClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockNetworkClient) EXPECT() *MockNetworkClientMockRecorder { - return m.recorder -} - -// RequestFailed mocks base method. -func (m *MockNetworkClient) RequestFailed(arg0 context.Context, arg1 ids.NodeID, arg2 uint32) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RequestFailed", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// RequestFailed indicates an expected call of RequestFailed. -func (mr *MockNetworkClientMockRecorder) RequestFailed(arg0, arg1, arg2 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestFailed", reflect.TypeOf((*MockNetworkClient)(nil).RequestFailed), arg0, arg1, arg2) -} - -// Response mocks base method. -func (m *MockNetworkClient) Response(arg0 context.Context, arg1 ids.NodeID, arg2 uint32, arg3 []byte) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Response", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(error) - return ret0 -} - -// Response indicates an expected call of Response. -func (mr *MockNetworkClientMockRecorder) Response(arg0, arg1, arg2, arg3 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Response", reflect.TypeOf((*MockNetworkClient)(nil).Response), arg0, arg1, arg2, arg3) -} - -// Connected mocks base method. -func (m *MockNetworkClient) Connected(arg0 context.Context, arg1 ids.NodeID, arg2 *version.Application) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Connected", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// Connected indicates an expected call of Connected. -func (mr *MockNetworkClientMockRecorder) Connected(arg0, arg1, arg2 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connected", reflect.TypeOf((*MockNetworkClient)(nil).Connected), arg0, arg1, arg2) -} - -// Disconnected mocks base method. -func (m *MockNetworkClient) Disconnected(arg0 context.Context, arg1 ids.NodeID) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Disconnected", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// Disconnected indicates an expected call of Disconnected. -func (mr *MockNetworkClientMockRecorder) Disconnected(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Disconnected", reflect.TypeOf((*MockNetworkClient)(nil).Disconnected), arg0, arg1) -} - -// Request mocks base method. -func (m *MockNetworkClient) Request(arg0 context.Context, arg1 ids.NodeID, arg2 []byte) ([]byte, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Request", arg0, arg1, arg2) - ret0, _ := ret[0].([]byte) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Request indicates an expected call of Request. -func (mr *MockNetworkClientMockRecorder) Request(arg0, arg1, arg2 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Request", reflect.TypeOf((*MockNetworkClient)(nil).Request), arg0, arg1, arg2) -} - -// RequestAny mocks base method. -func (m *MockNetworkClient) RequestAny(arg0 context.Context, arg1 []byte) (ids.NodeID, []byte, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RequestAny", arg0, arg1) - ret0, _ := ret[0].(ids.NodeID) - ret1, _ := ret[1].([]byte) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// RequestAny indicates an expected call of RequestAny. -func (mr *MockNetworkClientMockRecorder) RequestAny(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestAny", reflect.TypeOf((*MockNetworkClient)(nil).RequestAny), arg0, arg1) -} diff --git a/x/sync/network_client.go b/x/sync/network_client.go deleted file mode 100644 index a0de256bd..000000000 --- a/x/sync/network_client.go +++ /dev/null @@ -1,369 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package sync - -import ( - "context" - "errors" - "fmt" - "sync" - "time" - - "github.com/luxfi/metric" - "golang.org/x/sync/semaphore" - - consensusversion "github.com/luxfi/version" - "github.com/luxfi/ids" - "github.com/luxfi/log" - "github.com/luxfi/math/set" - "github.com/luxfi/p2p" - "github.com/luxfi/warp" -) - -// Minimum amount of time to handle a request -const minRequestHandlingDuration = 100 * time.Millisecond - -var ( - _ NetworkClient = (*networkClient)(nil) - - errAcquiringSemaphore = errors.New("error acquiring semaphore") - errRequestFailed = errors.New("request failed") - errAppSendFailed = errors.New("failed to send app message") -) - -// NetworkClient defines ability to send request / response through the Network -type NetworkClient interface { - // RequestAny synchronously sends request to an arbitrary peer with a - // node version greater than or equal to minVersion. - // Returns response bytes, the ID of the chosen peer, and ErrRequestFailed if - // the request should be retried. - RequestAny( - ctx context.Context, - request []byte, - ) (ids.NodeID, []byte, error) - - // Sends [request] to [nodeID] and returns the response. - // Blocks until the number of outstanding requests is - // below the limit before sending the request. - Request( - ctx context.Context, - nodeID ids.NodeID, - request []byte, - ) ([]byte, error) - - // The following declarations allow this interface to be embedded in the VM - // to handle incoming responses from peers. - - // Always returns nil because the engine considers errors - // returned from this function as fatal. - Response(context.Context, ids.NodeID, uint32, []byte) error - - // Always returns nil because the engine considers errors - // returned from this function as fatal. - RequestFailed(context.Context, ids.NodeID, uint32) error - - // Adds the given [nodeID] to the peer - // list so that it can receive messages. - // If [nodeID] is this node's ID, this is a no-op. - Connected(context.Context, ids.NodeID, *consensusversion.Application) error - - // Removes given [nodeID] from the peer list. - Disconnected(context.Context, ids.NodeID) error -} - -type networkClient struct { - lock sync.Mutex - log log.Logger - // requestID counter used to track outbound requests - requestID uint32 - // requestID => handler for the response/failure - outstandingRequestHandlers map[uint32]ResponseHandler - // controls maximum number of active outbound requests - activeRequests *semaphore.Weighted - // tracking of peers & bandwidth usage - peers *p2p.PeerTracker - // For sending messages to peers - sender warp.Sender -} - -func NewNetworkClient( - sender warp.Sender, - myNodeID ids.NodeID, - maxActiveRequests int64, - log log.Logger, - metricsNamespace string, - registerer metric.Registerer, - minVersion *consensusversion.Application, -) (NetworkClient, error) { - peerTracker, err := p2p.NewPeerTracker( - log, - metricsNamespace, - registerer, - set.Of(myNodeID), - minVersion, - ) - if err != nil { - return nil, fmt.Errorf("failed to create peer tracker: %w", err) - } - - return &networkClient{ - sender: sender, - outstandingRequestHandlers: make(map[uint32]ResponseHandler), - activeRequests: semaphore.NewWeighted(maxActiveRequests), - peers: peerTracker, - log: log, - }, nil -} - -func (c *networkClient) Response( - _ context.Context, - nodeID ids.NodeID, - requestID uint32, - response []byte, -) error { - c.lock.Lock() - defer c.lock.Unlock() - - c.log.Info( - "received Response from peer", - log.UserString("nodeID", nodeID.String()), - log.Uint32("requestID", requestID), - log.Int("responseLen", len(response)), - ) - - handler, exists := c.getRequestHandler(requestID) - if !exists { - // Should never happen since the engine - // should be managing outstanding requests - c.log.Warn( - "received response to unknown request", - log.UserString("nodeID", nodeID.String()), - log.Uint32("requestID", requestID), - log.Int("responseLen", len(response)), - ) - return nil - } - handler.OnResponse(response) - return nil -} - -func (c *networkClient) RequestFailed( - _ context.Context, - nodeID ids.NodeID, - requestID uint32, -) error { - c.lock.Lock() - defer c.lock.Unlock() - - c.log.Info( - "received RequestFailed from peer", - log.UserString("nodeID", nodeID.String()), - log.Uint32("requestID", requestID), - ) - - handler, exists := c.getRequestHandler(requestID) - if !exists { - // Should never happen since the engine - // should be managing outstanding requests - c.log.Warn( - "received request failed to unknown request", - log.Stringer("nodeID", nodeID), - log.Uint32("requestID", requestID), - ) - return nil - } - handler.OnFailure() - return nil -} - -// Returns the handler for [requestID] and marks the request as fulfilled. -// Returns false if there's no outstanding request with [requestID]. -// Assumes [c.lock] is held. -func (c *networkClient) getRequestHandler(requestID uint32) (ResponseHandler, bool) { - handler, exists := c.outstandingRequestHandlers[requestID] - if !exists { - return nil, false - } - // mark message as processed, release activeRequests slot - delete(c.outstandingRequestHandlers, requestID) - return handler, true -} - -// If [errAppSendFailed] is returned this should be considered fatal. -func (c *networkClient) RequestAny( - ctx context.Context, - request []byte, -) (ids.NodeID, []byte, error) { - // Take a slot from total [activeRequests] and block until a slot becomes available. - if err := c.activeRequests.Acquire(ctx, 1); err != nil { - return ids.EmptyNodeID, nil, errAcquiringSemaphore - } - defer c.activeRequests.Release(1) - - nodeID, responseChan, err := c.sendRequestAny(ctx, request) - if err != nil { - return ids.EmptyNodeID, nil, err - } - - response, err := c.awaitResponse(ctx, nodeID, responseChan) - return nodeID, response, err -} - -func (c *networkClient) sendRequestAny( - ctx context.Context, - request []byte, -) (ids.NodeID, chan []byte, error) { - c.lock.Lock() - defer c.lock.Unlock() - - nodeID, ok := c.peers.SelectPeer() - if !ok { - numPeers := c.peers.Size() - return ids.EmptyNodeID, nil, fmt.Errorf("no peers found from %d peers", numPeers) - } - - responseChan, err := c.sendRequestLocked(ctx, nodeID, request) - return nodeID, responseChan, err -} - -// If [errAppSendFailed] is returned this should be considered fatal. -func (c *networkClient) Request( - ctx context.Context, - nodeID ids.NodeID, - request []byte, -) ([]byte, error) { - // Take a slot from total [activeRequests] - // and block until a slot becomes available. - if err := c.activeRequests.Acquire(ctx, 1); err != nil { - return nil, errAcquiringSemaphore - } - defer c.activeRequests.Release(1) - - responseChan, err := c.sendRequest(ctx, nodeID, request) - if err != nil { - return nil, err - } - - return c.awaitResponse(ctx, nodeID, responseChan) -} - -func (c *networkClient) sendRequest( - ctx context.Context, - nodeID ids.NodeID, - request []byte, -) (chan []byte, error) { - c.lock.Lock() - defer c.lock.Unlock() - - return c.sendRequestLocked(ctx, nodeID, request) -} - -// Sends [request] to [nodeID] and returns a channel that will populate the -// response. -// -// If [errAppSendFailed] is returned this should be considered fatal. -// -// Assumes [nodeID] is never [c.myNodeID] since we guarantee [c.myNodeID] will -// not be added to [c.peers]. -// -// Assumes [c.lock] is held. -func (c *networkClient) sendRequestLocked( - ctx context.Context, - nodeID ids.NodeID, - request []byte, -) (chan []byte, error) { - requestID := c.requestID - c.requestID++ - - c.log.Debug("sending request to peer", - log.UserString("nodeID", nodeID.String()), - log.Uint32("requestID", requestID), - log.Int("requestLen", len(request)), - ) - c.peers.RegisterRequest(nodeID) - - // Send request to the peer. - nodeIDs := set.Of(nodeID) - // Cancellation is removed from this context to avoid erroring unexpectedly. - // SendRequest should be non-blocking and any error other than context - // cancellation is unexpected. - // - // This guarantees that the network should never receive an unexpected - // Response. - ctxWithoutCancel := context.WithoutCancel(ctx) - if err := c.sender.SendRequest(ctxWithoutCancel, nodeIDs, requestID, request); err != nil { - c.lock.Unlock() - c.log.Error("failed to send request", - log.Stringer("nodeID", nodeID), - log.Uint32("requestID", requestID), - log.Int("requestLen", len(request)), - log.Reflect("error", err), - ) - return nil, fmt.Errorf("%w: %w", errAppSendFailed, err) - } - - handler := newResponseHandler() - c.outstandingRequestHandlers[requestID] = handler - return handler.responseChan, nil -} - -// awaitResponse from [nodeID] and returns the response. -// -// Returns an error if the request failed or [ctx] is canceled. -// -// Blocks until a response is received or the [ctx] is canceled fails. -// -// Assumes [nodeID] is never [c.myNodeID] since we guarantee [c.myNodeID] will -// not be added to [c.peers]. -// -// Assumes [c.lock] is not held. -func (c *networkClient) awaitResponse( - ctx context.Context, - nodeID ids.NodeID, - responseChan chan []byte, -) ([]byte, error) { - var ( - response []byte - responded bool - startTime = time.Now() - ) - select { - case <-ctx.Done(): - c.peers.RegisterFailure(nodeID) - return nil, ctx.Err() - case response, responded = <-responseChan: - } - if !responded { - c.peers.RegisterFailure(nodeID) - return nil, errRequestFailed - } - - elapsedSeconds := time.Since(startTime).Seconds() - bandwidth := float64(len(response)) / (elapsedSeconds + epsilon) - c.peers.RegisterResponse(nodeID, bandwidth) - - c.log.Debug("received response from peer", - log.UserString("nodeID", nodeID.String()), - log.Int("responseLen", len(response)), - ) - return response, nil -} - -func (c *networkClient) Connected( - _ context.Context, - nodeID ids.NodeID, - nodeVersion *consensusversion.Application, -) error { - c.log.Debug("adding new peer", log.UserString("nodeID", nodeID.String())) - c.peers.Connected(nodeID, nodeVersion) - return nil -} - -func (c *networkClient) Disconnected(_ context.Context, nodeID ids.NodeID) error { - c.log.Debug("disconnecting peer", log.UserString("nodeID", nodeID.String())) - c.peers.Disconnected(nodeID) - return nil -} diff --git a/x/sync/network_server.go b/x/sync/network_server.go deleted file mode 100644 index 178c0d9f1..000000000 --- a/x/sync/network_server.go +++ /dev/null @@ -1,339 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package sync - -import ( - "bytes" - "context" - "errors" - "fmt" - "time" - - "google.golang.org/protobuf/proto" - - "github.com/luxfi/constants" - "github.com/luxfi/ids" - "github.com/luxfi/node/x/merkledb" - "github.com/luxfi/p2p" - hash "github.com/luxfi/crypto/hash" - "github.com/luxfi/container/maybe" - "github.com/luxfi/warp" - - pb "github.com/luxfi/node/proto/pb/sync" -) - -const ( - // Maximum number of key-value pairs to return in a proof. - // This overrides any other Limit specified in a RangeProofRequest - // or ChangeProofRequest if the given Limit is greater. - maxKeyValuesLimit = 2048 - // Estimated max overhead, in bytes, of putting a proof into a message. - // We use this to ensure that the proof we generate is not too large to fit in a message. - estimatedMessageOverhead = 4 * constants.KiB - maxByteSizeLimit = constants.DefaultMaxMessageSize - estimatedMessageOverhead -) - -var ( - ErrMinProofSizeIsTooLarge = errors.New("cannot generate any proof within the requested limit") - - errInvalidBytesLimit = errors.New("bytes limit must be greater than 0") - errInvalidKeyLimit = errors.New("key limit must be greater than 0") - errInvalidStartRootHash = fmt.Errorf("start root hash must have length %d", hash.HashLen) - errInvalidEndRootHash = fmt.Errorf("end root hash must have length %d", hash.HashLen) - errInvalidStartKey = errors.New("start key is Nothing but has value") - errInvalidEndKey = errors.New("end key is Nothing but has value") - errInvalidBounds = errors.New("start key is greater than end key") - errInvalidRootHash = fmt.Errorf("root hash must have length %d", hash.HashLen) - - _ p2p.Handler = (*GetChangeProofHandler)(nil) - _ p2p.Handler = (*GetRangeProofHandler)(nil) -) - -func maybeBytesToMaybe(mb *pb.MaybeBytes) maybe.Maybe[[]byte] { - if mb != nil && !mb.IsNothing { - return maybe.Some(mb.Value) - } - return maybe.Nothing[[]byte]() -} - -func NewGetChangeProofHandler(db DB) *GetChangeProofHandler { - return &GetChangeProofHandler{ - db: db, - } -} - -type GetChangeProofHandler struct { - db DB -} - -func (*GetChangeProofHandler) Gossip(context.Context, ids.NodeID, []byte) {} - -func (g *GetChangeProofHandler) Request(ctx context.Context, _ ids.NodeID, _ time.Time, requestBytes []byte) ([]byte, *warp.Error) { - req := &pb.SyncGetChangeProofRequest{} - if err := proto.Unmarshal(requestBytes, req); err != nil { - return nil, &warp.Error{ - Code: p2p.ErrUnexpected.Code, - Message: fmt.Sprintf("failed to unmarshal request: %s", err), - } - } - - if err := validateChangeProofRequest(req); err != nil { - return nil, &warp.Error{ - Code: p2p.ErrUnexpected.Code, - Message: fmt.Sprintf("invalid request: %s", err), - } - } - - // override limits if they exceed caps - var ( - keyLimit = min(req.KeyLimit, maxKeyValuesLimit) - bytesLimit = min(int(req.BytesLimit), maxByteSizeLimit) - start = maybeBytesToMaybe(req.StartKey) - end = maybeBytesToMaybe(req.EndKey) - ) - - startRoot, err := ids.ToID(req.StartRootHash) - if err != nil { - return nil, &warp.Error{ - Code: p2p.ErrUnexpected.Code, - Message: fmt.Sprintf("failed to parse start root hash: %s", err), - } - } - - endRoot, err := ids.ToID(req.EndRootHash) - if err != nil { - return nil, &warp.Error{ - Code: p2p.ErrUnexpected.Code, - Message: fmt.Sprintf("failed to parse end root hash: %s", err), - } - } - - for keyLimit > 0 { - changeProof, err := g.db.GetChangeProof(ctx, startRoot, endRoot, start, end, int(keyLimit)) - if err != nil { - if !errors.Is(err, merkledb.ErrInsufficientHistory) { - // We should only fail to get a change proof if we have insufficient history. - // Other errors are unexpected. - return nil, &warp.Error{ - Code: p2p.ErrUnexpected.Code, - Message: fmt.Sprintf("failed to get change proof: %s", err), - } - } - if errors.Is(err, merkledb.ErrNoEndRoot) { - // [s.db] doesn't have [endRoot] in its history. - // We can't generate a change/range proof. Drop this request. - return nil, &warp.Error{ - Code: p2p.ErrUnexpected.Code, - Message: fmt.Sprintf("failed to get change proof: %s", err), - } - } - - // [s.db] doesn't have sufficient history to generate change proof. - // Generate a range proof for the end root ID instead. - proofBytes, err := getRangeProof( - ctx, - g.db, - &pb.SyncGetRangeProofRequest{ - RootHash: req.EndRootHash, - StartKey: req.StartKey, - EndKey: req.EndKey, - KeyLimit: req.KeyLimit, - BytesLimit: req.BytesLimit, - }, - func(rangeProof *merkledb.RangeProof) ([]byte, error) { - return proto.Marshal(&pb.SyncGetChangeProofResponse{ - Response: &pb.SyncGetChangeProofResponse_RangeProof{ - RangeProof: rangeProof.ToProto(), - }, - }) - }, - ) - if err != nil { - return nil, &warp.Error{ - Code: p2p.ErrUnexpected.Code, - Message: fmt.Sprintf("failed to get range proof: %s", err), - } - } - - return proofBytes, nil - } - - // We generated a change proof. See if it's small enough. - proofBytes, err := proto.Marshal(&pb.SyncGetChangeProofResponse{ - Response: &pb.SyncGetChangeProofResponse_ChangeProof{ - ChangeProof: changeProof.ToProto(), - }, - }) - if err != nil { - return nil, &warp.Error{ - Code: p2p.ErrUnexpected.Code, - Message: fmt.Sprintf("failed to marshal change proof: %s", err), - } - } - - if len(proofBytes) < bytesLimit { - return proofBytes, nil - } - - // The proof was too large. Try to shrink it. - keyLimit = uint32(len(changeProof.KeyChanges)) / 2 - } - - return nil, &warp.Error{ - Code: p2p.ErrUnexpected.Code, - Message: fmt.Sprintf("failed to generate proof: %s", ErrMinProofSizeIsTooLarge), - } -} - -func NewGetRangeProofHandler(db DB) *GetRangeProofHandler { - return &GetRangeProofHandler{ - db: db, - } -} - -type GetRangeProofHandler struct { - db DB -} - -func (*GetRangeProofHandler) Gossip(context.Context, ids.NodeID, []byte) {} - -func (g *GetRangeProofHandler) Request(ctx context.Context, _ ids.NodeID, _ time.Time, requestBytes []byte) ([]byte, *warp.Error) { - req := &pb.SyncGetRangeProofRequest{} - if err := proto.Unmarshal(requestBytes, req); err != nil { - return nil, &warp.Error{ - Code: p2p.ErrUnexpected.Code, - Message: fmt.Sprintf("failed to unmarshal request: %s", err), - } - } - - if err := validateRangeProofRequest(req); err != nil { - return nil, &warp.Error{ - Code: p2p.ErrUnexpected.Code, - Message: fmt.Sprintf("invalid range proof request: %s", err), - } - } - - // override limits if they exceed caps - req.KeyLimit = min(req.KeyLimit, maxKeyValuesLimit) - req.BytesLimit = min(req.BytesLimit, maxByteSizeLimit) - - proofBytes, err := getRangeProof( - ctx, - g.db, - req, - func(rangeProof *merkledb.RangeProof) ([]byte, error) { - return proto.Marshal(rangeProof.ToProto()) - }, - ) - if err != nil { - return nil, &warp.Error{ - Code: p2p.ErrUnexpected.Code, - Message: fmt.Sprintf("failed to get range proof: %s", err), - } - } - - return proofBytes, nil -} - -// Get the range proof specified by [req]. -// If the generated proof is too large, the key limit is reduced -// and the proof is regenerated. This process is repeated until -// the proof is smaller than [req.BytesLimit]. -// When a sufficiently small proof is generated, returns it. -// If no sufficiently small proof can be generated, returns [ErrMinProofSizeIsTooLarge]. -// getRangeProof generates a range proof, iteratively reducing the key limit -// until the serialized proof fits within the byte limit. Returns -// ErrMinProofSizeIsTooLarge if no sufficiently small proof can be generated. -func getRangeProof( - ctx context.Context, - db DB, - req *pb.SyncGetRangeProofRequest, - marshalFunc func(*merkledb.RangeProof) ([]byte, error), -) ([]byte, error) { - root, err := ids.ToID(req.RootHash) - if err != nil { - return nil, err - } - - keyLimit := int(req.KeyLimit) - - for keyLimit > 0 { - rangeProof, err := db.GetRangeProofAtRoot( - ctx, - root, - maybeBytesToMaybe(req.StartKey), - maybeBytesToMaybe(req.EndKey), - keyLimit, - ) - if err != nil { - if errors.Is(err, merkledb.ErrInsufficientHistory) { - return nil, nil // drop request - } - return nil, err - } - - proofBytes, err := marshalFunc(rangeProof) - if err != nil { - return nil, err - } - - if len(proofBytes) < int(req.BytesLimit) { - return proofBytes, nil - } - - // The proof was too large. Try to shrink it. - keyLimit = len(rangeProof.KeyChanges) / 2 - } - return nil, ErrMinProofSizeIsTooLarge -} - -// Returns nil iff [req] is well-formed. -func validateChangeProofRequest(req *pb.SyncGetChangeProofRequest) error { - switch { - case req.BytesLimit == 0: - return errInvalidBytesLimit - case req.KeyLimit == 0: - return errInvalidKeyLimit - case len(req.StartRootHash) != hash.HashLen: - return errInvalidStartRootHash - case len(req.EndRootHash) != hash.HashLen: - return errInvalidEndRootHash - case bytes.Equal(req.EndRootHash, ids.Empty[:]): - return merkledb.ErrEmptyProof - case req.StartKey != nil && req.StartKey.IsNothing && len(req.StartKey.Value) > 0: - return errInvalidStartKey - case req.EndKey != nil && req.EndKey.IsNothing && len(req.EndKey.Value) > 0: - return errInvalidEndKey - case req.StartKey != nil && req.EndKey != nil && !req.StartKey.IsNothing && - !req.EndKey.IsNothing && bytes.Compare(req.StartKey.Value, req.EndKey.Value) > 0: - return errInvalidBounds - default: - return nil - } -} - -// Returns nil iff [req] is well-formed. -func validateRangeProofRequest(req *pb.SyncGetRangeProofRequest) error { - switch { - case req.BytesLimit == 0: - return errInvalidBytesLimit - case req.KeyLimit == 0: - return errInvalidKeyLimit - case len(req.RootHash) != ids.IDLen: - return errInvalidRootHash - case bytes.Equal(req.RootHash, ids.Empty[:]): - return merkledb.ErrEmptyProof - case req.StartKey != nil && req.StartKey.IsNothing && len(req.StartKey.Value) > 0: - return errInvalidStartKey - case req.EndKey != nil && req.EndKey.IsNothing && len(req.EndKey.Value) > 0: - return errInvalidEndKey - case req.StartKey != nil && req.EndKey != nil && !req.StartKey.IsNothing && - !req.EndKey.IsNothing && bytes.Compare(req.StartKey.Value, req.EndKey.Value) > 0: - return errInvalidBounds - default: - return nil - } -} diff --git a/x/sync/network_server_test.go b/x/sync/network_server_test.go deleted file mode 100644 index 9c223f3d8..000000000 --- a/x/sync/network_server_test.go +++ /dev/null @@ -1,380 +0,0 @@ -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -//go:build skip - -package sync - -import ( - "context" - "math/rand" - "testing" - "time" - - "github.com/luxfi/consensus" - "github.com/luxfi/mock/gomock" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - - "github.com/luxfi/consensus/core" - "github.com/luxfi/database" - "github.com/luxfi/database/memdb" - "github.com/luxfi/ids" - "github.com/luxfi/node/x/merkledb" - "github.com/luxfi/p2p" - - pb "github.com/luxfi/node/proto/pb/sync" -) - -func Test_Server_GetRangeProof(t *testing.T) { - now := time.Now().UnixNano() - t.Logf("seed: %d", now) - r := rand.New(rand.NewSource(now)) // #nosec G404 - - smallTrieDB, err := generateTrieWithMinKeyLen(t, r, defaultRequestKeyLimit, 1) - require.NoError(t, err) - smallTrieRoot, err := smallTrieDB.GetMerkleRoot(context.Background()) - require.NoError(t, err) - - tests := []struct { - name string - request *pb.SyncGetRangeProofRequest - expectedErr *common.Error - expectedResponseLen int - expectedMaxResponseBytes int - nodeID ids.NodeID - proofNil bool - }{ - { - name: "proof too large", - request: &pb.SyncGetRangeProofRequest{ - RootHash: smallTrieRoot[:], - KeyLimit: defaultRequestKeyLimit, - BytesLimit: 1000, - }, - proofNil: true, - expectedErr: p2p.ErrUnexpected, - }, - { - name: "byteslimit is 0", - request: &pb.SyncGetRangeProofRequest{ - RootHash: smallTrieRoot[:], - KeyLimit: defaultRequestKeyLimit, - BytesLimit: 0, - }, - proofNil: true, - expectedErr: p2p.ErrUnexpected, - }, - { - name: "keylimit is 0", - request: &pb.SyncGetRangeProofRequest{ - RootHash: smallTrieRoot[:], - KeyLimit: 0, - BytesLimit: defaultRequestByteSizeLimit, - }, - proofNil: true, - expectedErr: p2p.ErrUnexpected, - }, - { - name: "keys out of order", - request: &pb.SyncGetRangeProofRequest{ - RootHash: smallTrieRoot[:], - KeyLimit: defaultRequestKeyLimit, - BytesLimit: defaultRequestByteSizeLimit, - StartKey: &pb.MaybeBytes{Value: []byte{1}}, - EndKey: &pb.MaybeBytes{Value: []byte{0}}, - }, - proofNil: true, - expectedErr: p2p.ErrUnexpected, - }, - { - name: "response bounded by key limit", - request: &pb.SyncGetRangeProofRequest{ - RootHash: smallTrieRoot[:], - KeyLimit: 2 * defaultRequestKeyLimit, - BytesLimit: defaultRequestByteSizeLimit, - }, - expectedResponseLen: defaultRequestKeyLimit, - }, - { - name: "response bounded by byte limit", - request: &pb.SyncGetRangeProofRequest{ - RootHash: smallTrieRoot[:], - KeyLimit: defaultRequestKeyLimit, - BytesLimit: 2 * defaultRequestByteSizeLimit, - }, - expectedMaxResponseBytes: defaultRequestByteSizeLimit, - }, - { - name: "empty proof", - request: &pb.SyncGetRangeProofRequest{ - RootHash: ids.Empty[:], - KeyLimit: defaultRequestKeyLimit, - BytesLimit: defaultRequestByteSizeLimit, - }, - proofNil: true, - expectedErr: p2p.ErrUnexpected, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - require := require.New(t) - - handler := NewGetRangeProofHandler(smallTrieDB) - requestBytes, err := proto.Marshal(test.request) - require.NoError(err) - responseBytes, err := handler.Request(context.Background(), test.nodeID, time.Time{}, requestBytes) - require.ErrorIs(err, test.expectedErr) - if test.expectedErr != nil { - return - } - if test.proofNil { - require.Nil(responseBytes) - return - } - - var proofProto pb.RangeProof - require.NoError(proto.Unmarshal(responseBytes, &proofProto)) - - var proof merkledb.RangeProof - require.NoError(proof.UnmarshalProto(&proofProto)) - - if test.expectedResponseLen > 0 { - require.LessOrEqual(len(proof.KeyChanges), test.expectedResponseLen) - } - - bytes, err := proto.Marshal(proof.ToProto()) - require.NoError(err) - require.LessOrEqual(len(bytes), int(test.request.BytesLimit)) - if test.expectedMaxResponseBytes > 0 { - require.LessOrEqual(len(bytes), test.expectedMaxResponseBytes) - } - }) - } -} - -func Test_Server_GetChangeProof(t *testing.T) { - now := time.Now().UnixNano() - t.Logf("seed: %d", now) - r := rand.New(rand.NewSource(now)) // #nosec G404 - - serverDB, err := merkledb.New( - context.Background(), - memdb.New(), - newDefaultDBConfig(), - ) - require.NoError(t, err) - startRoot, err := serverDB.GetMerkleRoot(context.Background()) - require.NoError(t, err) - - // create changes - for x := 0; x < defaultRequestKeyLimit/2; x++ { - ops := make([]database.BatchOp, 0, 11) - // add some key/values - for i := 0; i < 10; i++ { - key := make([]byte, r.Intn(100)) - _, err = r.Read(key) - require.NoError(t, err) - - val := make([]byte, r.Intn(100)) - _, err = r.Read(val) - require.NoError(t, err) - - ops = append(ops, database.BatchOp{Key: key, Value: val}) - } - - // delete a key - deleteKeyStart := make([]byte, r.Intn(10)) - _, err = r.Read(deleteKeyStart) - require.NoError(t, err) - - it := serverDB.NewIteratorWithStart(deleteKeyStart) - if it.Next() { - ops = append(ops, database.BatchOp{Key: it.Key(), Delete: true}) - } - require.NoError(t, it.Error()) - it.Release() - - view, err := serverDB.NewView( - context.Background(), - merkledb.ViewChanges{BatchOps: ops}, - ) - require.NoError(t, err) - require.NoError(t, view.CommitToDB(context.Background())) - } - - endRoot, err := serverDB.GetMerkleRoot(context.Background()) - require.NoError(t, err) - - fakeRootID := ids.GenerateTestID() - - tests := []struct { - name string - request *pb.SyncGetChangeProofRequest - expectedErr *common.Error - expectedResponseLen int - expectedMaxResponseBytes int - nodeID ids.NodeID - expectRangeProof bool // Otherwise expect change proof - }{ - { - name: "proof restricted by BytesLimit", - request: &pb.SyncGetChangeProofRequest{ - StartRootHash: startRoot[:], - EndRootHash: endRoot[:], - KeyLimit: defaultRequestKeyLimit, - BytesLimit: 10000, - }, - }, - { - name: "full response for small (single request) trie", - request: &pb.SyncGetChangeProofRequest{ - StartRootHash: startRoot[:], - EndRootHash: endRoot[:], - KeyLimit: defaultRequestKeyLimit, - BytesLimit: defaultRequestByteSizeLimit, - }, - expectedResponseLen: defaultRequestKeyLimit, - }, - { - name: "partial response to request for entire trie (full leaf limit)", - request: &pb.SyncGetChangeProofRequest{ - StartRootHash: startRoot[:], - EndRootHash: endRoot[:], - KeyLimit: defaultRequestKeyLimit, - BytesLimit: defaultRequestByteSizeLimit, - }, - expectedResponseLen: defaultRequestKeyLimit, - }, - { - name: "byteslimit is 0", - request: &pb.SyncGetChangeProofRequest{ - StartRootHash: startRoot[:], - EndRootHash: endRoot[:], - KeyLimit: defaultRequestKeyLimit, - BytesLimit: 0, - }, - expectedErr: p2p.ErrUnexpected, - }, - { - name: "keylimit is 0", - request: &pb.SyncGetChangeProofRequest{ - StartRootHash: startRoot[:], - EndRootHash: endRoot[:], - KeyLimit: 0, - BytesLimit: defaultRequestByteSizeLimit, - }, - expectedErr: p2p.ErrUnexpected, - }, - { - name: "keys out of order", - request: &pb.SyncGetChangeProofRequest{ - StartRootHash: startRoot[:], - EndRootHash: endRoot[:], - KeyLimit: defaultRequestKeyLimit, - BytesLimit: defaultRequestByteSizeLimit, - StartKey: &pb.MaybeBytes{Value: []byte{1}}, - EndKey: &pb.MaybeBytes{Value: []byte{0}}, - }, - expectedErr: p2p.ErrUnexpected, - }, - { - name: "key limit too large", - request: &pb.SyncGetChangeProofRequest{ - StartRootHash: startRoot[:], - EndRootHash: endRoot[:], - KeyLimit: 2 * defaultRequestKeyLimit, - BytesLimit: defaultRequestByteSizeLimit, - }, - expectedResponseLen: defaultRequestKeyLimit, - }, - { - name: "bytes limit too large", - request: &pb.SyncGetChangeProofRequest{ - StartRootHash: startRoot[:], - EndRootHash: endRoot[:], - KeyLimit: defaultRequestKeyLimit, - BytesLimit: 2 * defaultRequestByteSizeLimit, - }, - expectedMaxResponseBytes: defaultRequestByteSizeLimit, - }, - { - name: "insufficient history for change proof; return range proof", - request: &pb.SyncGetChangeProofRequest{ - // This root doesn't exist so server has insufficient history - // to serve a change proof - StartRootHash: fakeRootID[:], - EndRootHash: endRoot[:], - KeyLimit: defaultRequestKeyLimit, - BytesLimit: defaultRequestByteSizeLimit, - }, - expectedMaxResponseBytes: defaultRequestByteSizeLimit, - expectRangeProof: true, - }, - { - name: "insufficient history for change proof or range proof", - request: &pb.SyncGetChangeProofRequest{ - // These roots don't exist so server has insufficient history - // to serve a change proof or range proof - StartRootHash: ids.Empty[:], - EndRootHash: fakeRootID[:], - KeyLimit: defaultRequestKeyLimit, - BytesLimit: defaultRequestByteSizeLimit, - }, - expectedMaxResponseBytes: defaultRequestByteSizeLimit, - expectedErr: p2p.ErrUnexpected, - }, - { - name: "empty proof", - request: &pb.SyncGetChangeProofRequest{ - StartRootHash: fakeRootID[:], - EndRootHash: ids.Empty[:], - KeyLimit: defaultRequestKeyLimit, - BytesLimit: defaultRequestByteSizeLimit, - }, - expectedMaxResponseBytes: defaultRequestByteSizeLimit, - expectedErr: p2p.ErrUnexpected, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - require := require.New(t) - - handler := NewGetChangeProofHandler(serverDB) - - requestBytes, err := proto.Marshal(test.request) - require.NoError(err) - proofBytes, err := handler.Request(context.Background(), test.nodeID, time.Time{}, requestBytes) - require.ErrorIs(err, test.expectedErr) - - if test.expectedErr != nil { - require.Nil(proofBytes) - return - } - - proofResult := &pb.SyncGetChangeProofResponse{} - require.NoError(proto.Unmarshal(proofBytes, proofResult)) - - if test.expectRangeProof { - require.NotNil(proofResult.GetRangeProof()) - } else { - require.NotNil(proofResult.GetChangeProof()) - } - - if test.expectedResponseLen > 0 { - if test.expectRangeProof { - require.LessOrEqual(len(proofResult.GetRangeProof().KeyValues), test.expectedResponseLen) - } else { - require.LessOrEqual(len(proofResult.GetChangeProof().KeyChanges), test.expectedResponseLen) - } - } - - require.LessOrEqual(len(proofBytes), int(test.request.BytesLimit)) - if test.expectedMaxResponseBytes > 0 { - require.LessOrEqual(len(proofBytes), test.expectedMaxResponseBytes) - } - }) - } -} diff --git a/x/sync/protoutils/utils.go b/x/sync/protoutils/utils.go deleted file mode 100644 index 3d7e0be2b..000000000 --- a/x/sync/protoutils/utils.go +++ /dev/null @@ -1,28 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package protoutils - -import ( - "github.com/luxfi/container/maybe" - - pb "github.com/luxfi/node/proto/pb/sync" -) - -func MaybeToProto(m maybe.Maybe[[]byte]) *pb.MaybeBytes { - if m.IsNothing() { - return nil - } - return &pb.MaybeBytes{ - Value: m.Value(), - } -} - -func ProtoToMaybe(mb *pb.MaybeBytes) maybe.Maybe[[]byte] { - if mb == nil { - return maybe.Nothing[[]byte]() - } - return maybe.Some(mb.Value) -} diff --git a/x/sync/response_handler.go b/x/sync/response_handler.go deleted file mode 100644 index 3910f2c36..000000000 --- a/x/sync/response_handler.go +++ /dev/null @@ -1,43 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package sync - -var _ ResponseHandler = (*responseHandler)(nil) - -// Handles responses/failure notifications for a sent request. -// Exactly one of OnResponse or OnFailure is eventually called. -type ResponseHandler interface { - // Called when [response] is received. - OnResponse(response []byte) - // Called when the request failed or timed out. - OnFailure() -} - -func newResponseHandler() *responseHandler { - return &responseHandler{responseChan: make(chan []byte)} -} - -// Implements [ResponseHandler]. -// Used to wait for a response after making a synchronous request. -// responseChan contains response bytes if the request succeeded. -// responseChan is closed in either fail or success scenario. -type responseHandler struct { - // If [OnResponse] is called, the response bytes are sent on this channel. - // If [OnFailure] is called, the channel is closed without sending bytes. - responseChan chan []byte -} - -// OnResponse passes the response bytes to the responseChan and closes the -// channel. -func (h *responseHandler) OnResponse(response []byte) { - h.responseChan <- response - close(h.responseChan) -} - -// OnFailure closes the channel. -func (h *responseHandler) OnFailure() { - close(h.responseChan) -} diff --git a/x/sync/sync_test.go b/x/sync/sync_test.go deleted file mode 100644 index b82e3037f..000000000 --- a/x/sync/sync_test.go +++ /dev/null @@ -1,1434 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package sync - -import ( - "bytes" - "context" - "math/rand" - "slices" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - - "github.com/luxfi/database" - "github.com/luxfi/database/memdb" - "github.com/luxfi/ids" - "github.com/luxfi/log" - "github.com/luxfi/metric" - "github.com/luxfi/node/trace" - "github.com/luxfi/node/x/merkledb" - "github.com/luxfi/p2p" - "github.com/luxfi/p2p/p2ptest" - "github.com/luxfi/container/maybe" - "github.com/luxfi/warp" - - pb "github.com/luxfi/node/proto/pb/sync" -) - -var _ p2p.Handler = (*waitingHandler)(nil) -var _ p2p.Handler = (*flakyHandler)(nil) - -// Test helper functions - -func newDefaultDBConfig() merkledb.Config { - return merkledb.Config{ - IntermediateWriteBatchSize: 100, - HistoryLength: defaultRequestKeyLimit, - ValueNodeCacheSize: defaultRequestKeyLimit, - IntermediateWriteBufferSize: defaultRequestKeyLimit, - IntermediateNodeCacheSize: defaultRequestKeyLimit, - Reg: metric.NewNoOp().Registry(), - Tracer: trace.Noop, - BranchFactor: merkledb.BranchFactor16, - } -} - -func newFlakyRangeProofHandler( - t *testing.T, - db merkledb.MerkleDB, - modifyResponse func(response *merkledb.RangeProof), -) p2p.Handler { - handler := NewGetRangeProofHandler(db) - - c := counter{m: 2} - return &p2p.TestHandler{ - RequestF: func(ctx context.Context, nodeID ids.NodeID, deadline time.Time, requestBytes []byte) ([]byte, *warp.Error) { - responseBytes, appErr := handler.Request(ctx, nodeID, deadline, requestBytes) - if appErr != nil { - return nil, appErr - } - - response := &pb.RangeProof{} - require.NoError(t, proto.Unmarshal(responseBytes, response)) - - proof := &merkledb.RangeProof{} - require.NoError(t, proof.UnmarshalProto(response)) - - // Half of requests are modified - if c.Inc() == 0 { - modifyResponse(proof) - } - - responseBytes, err := proto.Marshal(proof.ToProto()) - if err != nil { - return nil, &warp.Error{Code: 123, Message: err.Error()} - } - - return responseBytes, nil - }, - } -} - -func newFlakyChangeProofHandler( - t *testing.T, - db merkledb.MerkleDB, - modifyResponse func(response *merkledb.ChangeProof), -) p2p.Handler { - handler := NewGetChangeProofHandler(db) - - c := counter{m: 2} - return &p2p.TestHandler{ - RequestF: func(ctx context.Context, nodeID ids.NodeID, deadline time.Time, requestBytes []byte) ([]byte, *warp.Error) { - var err error - responseBytes, appErr := handler.Request(ctx, nodeID, deadline, requestBytes) - if appErr != nil { - return nil, appErr - } - - response := &pb.SyncGetChangeProofResponse{} - require.NoError(t, proto.Unmarshal(responseBytes, response)) - - changeProof := response.Response.(*pb.SyncGetChangeProofResponse_ChangeProof) - proof := &merkledb.ChangeProof{} - require.NoError(t, proof.UnmarshalProto(changeProof.ChangeProof)) - - // Half of requests are modified - if c.Inc() == 0 { - modifyResponse(proof) - } - - responseBytes, err = proto.Marshal(&pb.SyncGetChangeProofResponse{ - Response: &pb.SyncGetChangeProofResponse_ChangeProof{ - ChangeProof: proof.ToProto(), - }, - }) - if err != nil { - return nil, &warp.Error{Code: 123, Message: err.Error()} - } - - return responseBytes, nil - }, - } -} - -type flakyHandler struct { - p2p.Handler - c *counter -} - -func (f *flakyHandler) Request(ctx context.Context, nodeID ids.NodeID, deadline time.Time, requestBytes []byte) ([]byte, *warp.Error) { - if f.c.Inc() == 0 { - return nil, &warp.Error{Code: 123, Message: "flake error"} - } - - return f.Handler.Request(ctx, nodeID, deadline, requestBytes) -} - -type counter struct { - i int - m int - lock sync.Mutex -} - -func (c *counter) Inc() int { - c.lock.Lock() - defer c.lock.Unlock() - - tmp := c.i - result := tmp % c.m - - c.i++ - return result -} - -type waitingHandler struct { - p2p.NoOpHandler - handler p2p.Handler - updatedRootChan chan struct{} -} - -func (w *waitingHandler) Request(ctx context.Context, nodeID ids.NodeID, deadline time.Time, requestBytes []byte) ([]byte, *warp.Error) { - <-w.updatedRootChan - return w.handler.Request(ctx, nodeID, deadline, requestBytes) -} - -func Test_Creation(t *testing.T) { - require := require.New(t) - - db, err := merkledb.New( - context.Background(), - memdb.New(), - newDefaultDBConfig(), - ) - require.NoError(err) - - ctx := context.Background() - syncer, err := NewManager(ManagerConfig[[]byte]{ - DB: db, - RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(db)), - ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(db)), - SimultaneousWorkLimit: 5, - Log: log.NewNoOpLogger(), - BranchFactor: merkledb.BranchFactor16, - }, metric.NewRegistry()) - require.NoError(err) - require.NotNil(syncer) -} - -func Test_Completion(t *testing.T) { - require := require.New(t) - - emptyDB, err := merkledb.New( - context.Background(), - memdb.New(), - newDefaultDBConfig(), - ) - require.NoError(err) - - emptyRoot, err := emptyDB.GetMerkleRoot(context.Background()) - require.NoError(err) - - db, err := merkledb.New( - context.Background(), - memdb.New(), - newDefaultDBConfig(), - ) - require.NoError(err) - - ctx := context.Background() - syncer, err := NewManager(ManagerConfig[[]byte]{ - DB: db, - RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(emptyDB)), - ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(emptyDB)), - TargetRoot: emptyRoot, - SimultaneousWorkLimit: 5, - Log: log.NewNoOpLogger(), - BranchFactor: merkledb.BranchFactor16, - }, metric.NewRegistry()) - require.NoError(err) - require.NotNil(syncer) - - require.NoError(syncer.Start(context.Background())) - require.NoError(syncer.Wait(context.Background())) - - syncer.workLock.Lock() - require.Zero(syncer.unprocessedWork.Len()) - require.Equal(1, syncer.processedWork.Len()) - syncer.workLock.Unlock() -} - -func Test_Midpoint(t *testing.T) { - require := require.New(t) - - mid := midPoint(maybe.Some([]byte{1, 255}), maybe.Some([]byte{2, 1})) - require.Equal(maybe.Some([]byte{2, 0}), mid) - - mid = midPoint(maybe.Nothing[[]byte](), maybe.Some([]byte{255, 255, 0})) - require.Equal(maybe.Some([]byte{127, 255, 128}), mid) - - mid = midPoint(maybe.Some([]byte{255, 255, 255}), maybe.Some([]byte{255, 255})) - require.Equal(maybe.Some([]byte{255, 255, 127, 128}), mid) - - mid = midPoint(maybe.Nothing[[]byte](), maybe.Some([]byte{255})) - require.Equal(maybe.Some([]byte{127, 127}), mid) - - mid = midPoint(maybe.Some([]byte{1, 255}), maybe.Some([]byte{255, 1})) - require.Equal(maybe.Some([]byte{128, 128}), mid) - - mid = midPoint(maybe.Some([]byte{140, 255}), maybe.Some([]byte{141, 0})) - require.Equal(maybe.Some([]byte{140, 255, 127}), mid) - - mid = midPoint(maybe.Some([]byte{126, 255}), maybe.Some([]byte{127})) - require.Equal(maybe.Some([]byte{126, 255, 127}), mid) - - mid = midPoint(maybe.Nothing[[]byte](), maybe.Nothing[[]byte]()) - require.Equal(maybe.Some([]byte{127}), mid) - - low := midPoint(maybe.Nothing[[]byte](), mid) - require.Equal(maybe.Some([]byte{63, 127}), low) - - high := midPoint(mid, maybe.Nothing[[]byte]()) - require.Equal(maybe.Some([]byte{191}), high) - - mid = midPoint(maybe.Some([]byte{255, 255}), maybe.Nothing[[]byte]()) - require.Equal(maybe.Some([]byte{255, 255, 127, 127}), mid) - - mid = midPoint(maybe.Some([]byte{255}), maybe.Nothing[[]byte]()) - require.Equal(maybe.Some([]byte{255, 127, 127}), mid) - - for i := 0; i < 5000; i++ { - r := rand.New(rand.NewSource(int64(i))) // #nosec G404 - - start := make([]byte, r.Intn(99)+1) - _, err := r.Read(start) - require.NoError(err) - - end := make([]byte, r.Intn(99)+1) - _, err = r.Read(end) - require.NoError(err) - - for bytes.Equal(start, end) { - _, err = r.Read(end) - require.NoError(err) - } - - if bytes.Compare(start, end) == 1 { - start, end = end, start - } - - mid = midPoint(maybe.Some(start), maybe.Some(end)) - require.Equal(-1, bytes.Compare(start, mid.Value())) - require.Equal(-1, bytes.Compare(mid.Value(), end)) - } -} - -func Test_Sync_FindNextKey_InSync(t *testing.T) { - require := require.New(t) - - now := time.Now().UnixNano() - t.Logf("seed: %d", now) - r := rand.New(rand.NewSource(now)) // #nosec G404 - dbToSync, err := generateTrie(t, r, 1000) - require.NoError(err) - syncRoot, err := dbToSync.GetMerkleRoot(context.Background()) - require.NoError(err) - - db, err := merkledb.New( - context.Background(), - memdb.New(), - newDefaultDBConfig(), - ) - require.NoError(err) - - ctx := context.Background() - syncer, err := NewManager(ManagerConfig[[]byte]{ - DB: db, - RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(dbToSync)), - ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(dbToSync)), - TargetRoot: syncRoot, - SimultaneousWorkLimit: 5, - Log: log.NewNoOpLogger(), - BranchFactor: merkledb.BranchFactor16, - }, metric.NewRegistry()) - require.NoError(err) - require.NotNil(syncer) - - require.NoError(syncer.Start(context.Background())) - require.NoError(syncer.Wait(context.Background())) - - proof, err := dbToSync.GetRangeProof(context.Background(), maybe.Nothing[[]byte](), maybe.Nothing[[]byte](), 500) - require.NoError(err) - - // the two dbs should be in sync, so next key should be nil - lastKey := proof.KeyChanges[len(proof.KeyChanges)-1].Key - nextKey, err := syncer.findNextKey(context.Background(), lastKey, maybe.Nothing[[]byte](), proof.EndProof) - require.NoError(err) - require.True(nextKey.IsNothing()) - - // add an extra value to sync db past the last key returned - newKey := midPoint(maybe.Some(lastKey), maybe.Nothing[[]byte]()) - newKeyVal := newKey.Value() - require.NoError(db.Put(newKeyVal, []byte{1})) - - // create a range endpoint that is before the newly added key, but after the last key - endPointBeforeNewKey := make([]byte, 0, 2) - for i := 0; i < len(newKeyVal); i++ { - endPointBeforeNewKey = append(endPointBeforeNewKey, newKeyVal[i]) - - // we need the new key to be after the last key - // don't subtract anything from the current byte if newkey and lastkey are equal - if lastKey[i] == newKeyVal[i] { - continue - } - - // if the first nibble is > 0, subtract "1" from it - if endPointBeforeNewKey[i] >= 16 { - endPointBeforeNewKey[i] -= 16 - break - } - // if the second nibble > 0, subtract 1 from it - if endPointBeforeNewKey[i] > 0 { - endPointBeforeNewKey[i] -= 1 - break - } - // both nibbles were 0, so move onto the next byte - } - - nextKey, err = syncer.findNextKey(context.Background(), lastKey, maybe.Some(endPointBeforeNewKey), proof.EndProof) - require.NoError(err) - - // next key would be after the end of the range, so it returns Nothing instead - require.True(nextKey.IsNothing()) -} - -func Test_Sync_FindNextKey_Deleted(t *testing.T) { - require := require.New(t) - - db, err := merkledb.New( - context.Background(), - memdb.New(), - newDefaultDBConfig(), - ) - require.NoError(err) - require.NoError(db.Put([]byte{0x10}, []byte{1})) - require.NoError(db.Put([]byte{0x11, 0x11}, []byte{2})) - - syncRoot, err := db.GetMerkleRoot(context.Background()) - require.NoError(err) - - ctx := context.Background() - syncer, err := NewManager(ManagerConfig[[]byte]{ - DB: db, - RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(db)), - ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(db)), - TargetRoot: syncRoot, - SimultaneousWorkLimit: 5, - Log: log.NewNoOpLogger(), - BranchFactor: merkledb.BranchFactor16, - }, metric.NewRegistry()) - require.NoError(err) - - // 0x12 was "deleted" and there should be no extra node in the proof since there was nothing with a common prefix - noExtraNodeProof, err := db.GetProof(context.Background(), []byte{0x12}) - require.NoError(err) - - // 0x11 was "deleted" and 0x11.0x11 should be in the exclusion proof - extraNodeProof, err := db.GetProof(context.Background(), []byte{0x11}) - require.NoError(err) - - // there is now another value in the range that needs to be sync'ed - require.NoError(db.Put([]byte{0x13}, []byte{3})) - - nextKey, err := syncer.findNextKey(context.Background(), []byte{0x12}, maybe.Some([]byte{0x20}), noExtraNodeProof.Path) - require.NoError(err) - require.Equal(maybe.Some([]byte{0x13}), nextKey) - - nextKey, err = syncer.findNextKey(context.Background(), []byte{0x11}, maybe.Some([]byte{0x20}), extraNodeProof.Path) - require.NoError(err) - require.Equal(maybe.Some([]byte{0x13}), nextKey) -} - -func Test_Sync_FindNextKey_BranchInLocal(t *testing.T) { - require := require.New(t) - - db, err := merkledb.New( - context.Background(), - memdb.New(), - newDefaultDBConfig(), - ) - require.NoError(err) - require.NoError(db.Put([]byte{0x11}, []byte{1})) - require.NoError(db.Put([]byte{0x11, 0x11}, []byte{2})) - - targetRoot, err := db.GetMerkleRoot(context.Background()) - require.NoError(err) - - proof, err := db.GetProof(context.Background(), []byte{0x11, 0x11}) - require.NoError(err) - - ctx := context.Background() - syncer, err := NewManager(ManagerConfig[[]byte]{ - DB: db, - RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(db)), - ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(db)), - TargetRoot: targetRoot, - SimultaneousWorkLimit: 5, - Log: log.NewNoOpLogger(), - BranchFactor: merkledb.BranchFactor16, - }, metric.NewRegistry()) - require.NoError(err) - require.NoError(db.Put([]byte{0x11, 0x15}, []byte{4})) - - nextKey, err := syncer.findNextKey(context.Background(), []byte{0x11, 0x11}, maybe.Some([]byte{0x20}), proof.Path) - require.NoError(err) - require.Equal(maybe.Some([]byte{0x11, 0x15}), nextKey) -} - -func Test_Sync_FindNextKey_BranchInReceived(t *testing.T) { - require := require.New(t) - - db, err := merkledb.New( - context.Background(), - memdb.New(), - newDefaultDBConfig(), - ) - require.NoError(err) - require.NoError(db.Put([]byte{0x11}, []byte{1})) - require.NoError(db.Put([]byte{0x12}, []byte{2})) - require.NoError(db.Put([]byte{0x12, 0xA0}, []byte{4})) - - targetRoot, err := db.GetMerkleRoot(context.Background()) - require.NoError(err) - - proof, err := db.GetProof(context.Background(), []byte{0x12}) - require.NoError(err) - - ctx := context.Background() - syncer, err := NewManager(ManagerConfig[[]byte]{ - DB: db, - RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(db)), - ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(db)), - TargetRoot: targetRoot, - SimultaneousWorkLimit: 5, - Log: log.NewNoOpLogger(), - BranchFactor: merkledb.BranchFactor16, - }, metric.NewRegistry()) - require.NoError(err) - require.NoError(db.Delete([]byte{0x12, 0xA0})) - - nextKey, err := syncer.findNextKey(context.Background(), []byte{0x12}, maybe.Some([]byte{0x20}), proof.Path) - require.NoError(err) - require.Equal(maybe.Some([]byte{0x12, 0xA0}), nextKey) -} - -func Test_Sync_FindNextKey_ExtraValues(t *testing.T) { - require := require.New(t) - - now := time.Now().UnixNano() - t.Logf("seed: %d", now) - r := rand.New(rand.NewSource(now)) // #nosec G404 - dbToSync, err := generateTrie(t, r, 1000) - require.NoError(err) - syncRoot, err := dbToSync.GetMerkleRoot(context.Background()) - require.NoError(err) - - db, err := merkledb.New( - context.Background(), - memdb.New(), - newDefaultDBConfig(), - ) - require.NoError(err) - - ctx := context.Background() - syncer, err := NewManager(ManagerConfig[[]byte]{ - DB: db, - RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(dbToSync)), - ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(dbToSync)), - TargetRoot: syncRoot, - SimultaneousWorkLimit: 5, - Log: log.NewNoOpLogger(), - BranchFactor: merkledb.BranchFactor16, - }, metric.NewRegistry()) - require.NoError(err) - require.NotNil(syncer) - - require.NoError(syncer.Start(context.Background())) - require.NoError(syncer.Wait(context.Background())) - - proof, err := dbToSync.GetRangeProof(context.Background(), maybe.Nothing[[]byte](), maybe.Nothing[[]byte](), 500) - require.NoError(err) - - // add an extra value to local db - lastKey := proof.KeyChanges[len(proof.KeyChanges)-1].Key - midpoint := midPoint(maybe.Some(lastKey), maybe.Nothing[[]byte]()) - midPointVal := midpoint.Value() - - require.NoError(db.Put(midPointVal, []byte{1})) - - // next key at prefix of newly added point - nextKey, err := syncer.findNextKey(context.Background(), lastKey, maybe.Nothing[[]byte](), proof.EndProof) - require.NoError(err) - require.True(nextKey.HasValue()) - - require.True(isPrefix(midPointVal, nextKey.Value())) - - require.NoError(db.Delete(midPointVal)) - - require.NoError(dbToSync.Put(midPointVal, []byte{1})) - - proof, err = dbToSync.GetRangeProof(context.Background(), maybe.Nothing[[]byte](), maybe.Some(lastKey), 500) - require.NoError(err) - - // next key at prefix of newly added point - nextKey, err = syncer.findNextKey(context.Background(), lastKey, maybe.Nothing[[]byte](), proof.EndProof) - require.NoError(err) - require.True(nextKey.HasValue()) - - // deal with odd length key - require.True(isPrefix(midPointVal, nextKey.Value())) -} - -func TestFindNextKeyEmptyEndProof(t *testing.T) { - require := require.New(t) - now := time.Now().UnixNano() - t.Logf("seed: %d", now) - r := rand.New(rand.NewSource(now)) // #nosec G404 - - db, err := merkledb.New( - context.Background(), - memdb.New(), - newDefaultDBConfig(), - ) - require.NoError(err) - - ctx := context.Background() - syncer, err := NewManager(ManagerConfig[[]byte]{ - DB: db, - RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(db)), - ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(db)), - TargetRoot: ids.Empty, - SimultaneousWorkLimit: 5, - Log: log.NewNoOpLogger(), - BranchFactor: merkledb.BranchFactor16, - }, metric.NewRegistry()) - require.NoError(err) - require.NotNil(syncer) - - for i := 0; i < 100; i++ { - lastReceivedKeyLen := r.Intn(16) - lastReceivedKey := make([]byte, lastReceivedKeyLen) - _, _ = r.Read(lastReceivedKey) // #nosec G404 - - rangeEndLen := r.Intn(16) - rangeEndBytes := make([]byte, rangeEndLen) - _, _ = r.Read(rangeEndBytes) // #nosec G404 - - rangeEnd := maybe.Nothing[[]byte]() - if rangeEndLen > 0 { - rangeEnd = maybe.Some(rangeEndBytes) - } - - nextKey, err := syncer.findNextKey( - context.Background(), - lastReceivedKey, - rangeEnd, - nil, /* endProof */ - ) - require.NoError(err) - require.Equal(maybe.Some(append(lastReceivedKey, 0)), nextKey) - } -} - -func isPrefix(data []byte, prefix []byte) bool { - if prefix[len(prefix)-1]%16 == 0 { - index := 0 - for ; index < len(prefix)-1; index++ { - if data[index] != prefix[index] { - return false - } - } - return data[index]>>4 == prefix[index]>>4 - } - return bytes.HasPrefix(data, prefix) -} - -func Test_Sync_FindNextKey_DifferentChild(t *testing.T) { - require := require.New(t) - - now := time.Now().UnixNano() - t.Logf("seed: %d", now) - r := rand.New(rand.NewSource(now)) // #nosec G404 - dbToSync, err := generateTrie(t, r, 500) - require.NoError(err) - syncRoot, err := dbToSync.GetMerkleRoot(context.Background()) - require.NoError(err) - - db, err := merkledb.New( - context.Background(), - memdb.New(), - newDefaultDBConfig(), - ) - require.NoError(err) - - ctx := context.Background() - syncer, err := NewManager(ManagerConfig[[]byte]{ - DB: db, - RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(dbToSync)), - ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(dbToSync)), - TargetRoot: syncRoot, - SimultaneousWorkLimit: 5, - Log: log.NewNoOpLogger(), - BranchFactor: merkledb.BranchFactor16, - }, metric.NewRegistry()) - require.NoError(err) - require.NotNil(syncer) - require.NoError(syncer.Start(context.Background())) - require.NoError(syncer.Wait(context.Background())) - - proof, err := dbToSync.GetRangeProof(context.Background(), maybe.Nothing[[]byte](), maybe.Nothing[[]byte](), 100) - require.NoError(err) - lastKey := proof.KeyChanges[len(proof.KeyChanges)-1].Key - - // local db has a different child than remote db - lastKey = append(lastKey, 16) - require.NoError(db.Put(lastKey, []byte{1})) - - require.NoError(dbToSync.Put(lastKey, []byte{2})) - - proof, err = dbToSync.GetRangeProof(context.Background(), maybe.Nothing[[]byte](), maybe.Some(proof.KeyChanges[len(proof.KeyChanges)-1].Key), 100) - require.NoError(err) - - nextKey, err := syncer.findNextKey(context.Background(), proof.KeyChanges[len(proof.KeyChanges)-1].Key, maybe.Nothing[[]byte](), proof.EndProof) - require.NoError(err) - require.True(nextKey.HasValue()) - require.Equal(lastKey, nextKey.Value()) -} - -// Test findNextKey by computing the expected result in a naive, inefficient -// way and comparing it to the actual result -func TestFindNextKeyRandom(t *testing.T) { - now := time.Now().UnixNano() - t.Logf("seed: %d", now) - rand := rand.New(rand.NewSource(now)) // #nosec G404 - require := require.New(t) - - // Create a "remote" database and "local" database - remoteDB, err := merkledb.New( - context.Background(), - memdb.New(), - newDefaultDBConfig(), - ) - require.NoError(err) - - config := newDefaultDBConfig() - localDB, err := merkledb.New( - context.Background(), - memdb.New(), - config, - ) - require.NoError(err) - - var ( - numProofsToTest = 250 - numKeyValues = 250 - maxKeyLen = 256 - maxValLen = 256 - maxRangeStartLen = 8 - maxRangeEndLen = 8 - maxProofLen = 128 - ) - - // Put random keys into the databases - for _, db := range []database.Database{remoteDB, localDB} { - for i := 0; i < numKeyValues; i++ { - key := make([]byte, rand.Intn(maxKeyLen)) - _, _ = rand.Read(key) - val := make([]byte, rand.Intn(maxValLen)) - _, _ = rand.Read(val) - require.NoError(db.Put(key, val)) - } - } - - // Repeatedly generate end proofs from the remote database and compare - // the result of findNextKey to the expected result. - for proofIndex := 0; proofIndex < numProofsToTest; proofIndex++ { - // Generate a proof for a random key - var ( - rangeStart []byte - rangeEnd []byte - ) - // Generate a valid range start and end - for rangeStart == nil || bytes.Compare(rangeStart, rangeEnd) == 1 { - rangeStart = make([]byte, rand.Intn(maxRangeStartLen)+1) - _, _ = rand.Read(rangeStart) - rangeEnd = make([]byte, rand.Intn(maxRangeEndLen)+1) - _, _ = rand.Read(rangeEnd) - } - - startKey := maybe.Nothing[[]byte]() - if len(rangeStart) > 0 { - startKey = maybe.Some(rangeStart) - } - endKey := maybe.Nothing[[]byte]() - if len(rangeEnd) > 0 { - endKey = maybe.Some(rangeEnd) - } - - remoteProof, err := remoteDB.GetRangeProof( - context.Background(), - startKey, - endKey, - rand.Intn(maxProofLen)+1, - ) - require.NoError(err) - - if len(remoteProof.KeyChanges) == 0 { - continue - } - lastReceivedKey := remoteProof.KeyChanges[len(remoteProof.KeyChanges)-1].Key - - // Commit the proof to the local database as we do - // in the actual syncer. - require.NoError(localDB.CommitRangeProof( - context.Background(), - startKey, - endKey, - remoteProof, - )) - - localProof, err := localDB.GetProof( - context.Background(), - lastReceivedKey, - ) - require.NoError(err) - - type keyAndID struct { - key merkledb.Key - id ids.ID - } - - // Set of key prefix/ID pairs proven by the remote database's end proof. - remoteKeyIDs := []keyAndID{} - for _, node := range remoteProof.EndProof { - for childIdx, childID := range node.Children { - remoteKeyIDs = append(remoteKeyIDs, keyAndID{ - key: node.Key.Extend(merkledb.ToToken(childIdx, merkledb.BranchFactorToTokenSize[config.BranchFactor])), - id: childID, - }) - } - } - - // Set of key prefix/ID pairs proven by the local database's proof. - localKeyIDs := []keyAndID{} - for _, node := range localProof.Path { - for childIdx, childID := range node.Children { - localKeyIDs = append(localKeyIDs, keyAndID{ - key: node.Key.Extend(merkledb.ToToken(childIdx, merkledb.BranchFactorToTokenSize[config.BranchFactor])), - id: childID, - }) - } - } - - // Sort in ascending order by key prefix. - serializedPathCompare := func(i, j keyAndID) int { - return i.key.Compare(j.key) - } - slices.SortFunc(remoteKeyIDs, serializedPathCompare) - slices.SortFunc(localKeyIDs, serializedPathCompare) - - // Filter out keys that are before the last received key - findBounds := func(keyIDs []keyAndID) (int, int) { - var ( - firstIdxInRange = len(keyIDs) - firstIdxInRangeFound = false - firstIdxOutOfRange = len(keyIDs) - ) - for i, keyID := range keyIDs { - if !firstIdxInRangeFound && bytes.Compare(keyID.key.Bytes(), lastReceivedKey) > 0 { - firstIdxInRange = i - firstIdxInRangeFound = true - continue - } - if bytes.Compare(keyID.key.Bytes(), rangeEnd) > 0 { - firstIdxOutOfRange = i - break - } - } - return firstIdxInRange, firstIdxOutOfRange - } - - remoteFirstIdxAfterLastReceived, remoteFirstIdxAfterEnd := findBounds(remoteKeyIDs) - remoteKeyIDs = remoteKeyIDs[remoteFirstIdxAfterLastReceived:remoteFirstIdxAfterEnd] - - localFirstIdxAfterLastReceived, localFirstIdxAfterEnd := findBounds(localKeyIDs) - localKeyIDs = localKeyIDs[localFirstIdxAfterLastReceived:localFirstIdxAfterEnd] - - // Find smallest difference between the set of key/ID pairs proven by - // the remote/local proofs for key/ID pairs after the last received key. - var ( - smallestDiffKey merkledb.Key - foundDiff bool - ) - for i := 0; i < len(remoteKeyIDs) && i < len(localKeyIDs); i++ { - // See if the keys are different. - smaller, bigger := remoteKeyIDs[i], localKeyIDs[i] - if serializedPathCompare(localKeyIDs[i], remoteKeyIDs[i]) == -1 { - smaller, bigger = localKeyIDs[i], remoteKeyIDs[i] - } - - if smaller.key != bigger.key || smaller.id != bigger.id { - smallestDiffKey = smaller.key - foundDiff = true - break - } - } - if !foundDiff { - // All the keys were equal. The smallest diff is the next key - // in the longer of the lists (if they're not same length.) - if len(remoteKeyIDs) < len(localKeyIDs) { - smallestDiffKey = localKeyIDs[len(remoteKeyIDs)].key - } else if len(remoteKeyIDs) > len(localKeyIDs) { - smallestDiffKey = remoteKeyIDs[len(localKeyIDs)].key - } - } - - // Get the actual value from the syncer - ctx := context.Background() - syncer, err := NewManager(ManagerConfig[[]byte]{ - DB: localDB, - RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(remoteDB)), - ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(remoteDB)), - TargetRoot: ids.GenerateTestID(), - SimultaneousWorkLimit: 5, - Log: log.NewNoOpLogger(), - BranchFactor: merkledb.BranchFactor16, - }, metric.NewRegistry()) - require.NoError(err) - - gotFirstDiff, err := syncer.findNextKey( - context.Background(), - lastReceivedKey, - endKey, - remoteProof.EndProof, - ) - require.NoError(err) - - if bytes.Compare(smallestDiffKey.Bytes(), rangeEnd) >= 0 { - // The smallest key which differs is after the range end so the - // next key to get should be nil because we're done fetching the range. - require.True(gotFirstDiff.IsNothing()) - } else { - require.Equal(smallestDiffKey.Bytes(), gotFirstDiff.Value()) - } - } -} - -// Tests that we are able to sync to the correct root while the server is -// updating -func Test_Sync_Result_Correct_Root(t *testing.T) { - now := time.Now().UnixNano() - t.Logf("seed: %d", now) - r := rand.New(rand.NewSource(now)) // #nosec G404 - - tests := []struct { - name string - db merkledb.MerkleDB - rangeProofClient func(db merkledb.MerkleDB) *p2p.Client - changeProofClient func(db merkledb.MerkleDB) *p2p.Client - }{ - { - name: "range proof bad response - too many leaves in response", - rangeProofClient: func(db merkledb.MerkleDB) *p2p.Client { - handler := newFlakyRangeProofHandler(t, db, func(response *merkledb.RangeProof) { - response.KeyChanges = append(response.KeyChanges, merkledb.KeyChange{}) - }) - - return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, handler) - }, - }, - { - name: "range proof bad response - removed first key in response", - rangeProofClient: func(db merkledb.MerkleDB) *p2p.Client { - handler := newFlakyRangeProofHandler(t, db, func(response *merkledb.RangeProof) { - response.KeyChanges = response.KeyChanges[min(1, len(response.KeyChanges)):] - }) - - return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, handler) - }, - }, - { - name: "range proof bad response - removed first key in response and replaced proof", - rangeProofClient: func(db merkledb.MerkleDB) *p2p.Client { - handler := newFlakyRangeProofHandler(t, db, func(response *merkledb.RangeProof) { - response.KeyChanges = response.KeyChanges[min(1, len(response.KeyChanges)):] - response.KeyChanges = []merkledb.KeyChange{ - { - Key: []byte("foo"), - Value: maybe.Some([]byte("bar")), - }, - } - response.StartProof = []merkledb.ProofNode{ - { - Key: merkledb.Key{}, - }, - } - response.EndProof = []merkledb.ProofNode{ - { - Key: merkledb.Key{}, - }, - } - }) - - return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, handler) - }, - }, - { - name: "range proof bad response - removed key from middle of response", - rangeProofClient: func(db merkledb.MerkleDB) *p2p.Client { - handler := newFlakyRangeProofHandler(t, db, func(response *merkledb.RangeProof) { - i := rand.Intn(max(1, len(response.KeyChanges)-1)) // #nosec G404 - _ = slices.Delete(response.KeyChanges, i, min(len(response.KeyChanges), i+1)) - }) - - return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, handler) - }, - }, - { - name: "range proof bad response - start and end proof nodes removed", - rangeProofClient: func(db merkledb.MerkleDB) *p2p.Client { - handler := newFlakyRangeProofHandler(t, db, func(response *merkledb.RangeProof) { - response.StartProof = nil - response.EndProof = nil - }) - - return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, handler) - }, - }, - { - name: "range proof bad response - end proof removed", - rangeProofClient: func(db merkledb.MerkleDB) *p2p.Client { - handler := newFlakyRangeProofHandler(t, db, func(response *merkledb.RangeProof) { - response.EndProof = nil - }) - - return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, handler) - }, - }, - { - name: "range proof bad response - empty proof", - rangeProofClient: func(db merkledb.MerkleDB) *p2p.Client { - handler := newFlakyRangeProofHandler(t, db, func(response *merkledb.RangeProof) { - response.StartProof = nil - response.EndProof = nil - response.KeyChanges = nil - }) - - return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, handler) - }, - }, - { - name: "range proof server flake", - rangeProofClient: func(db merkledb.MerkleDB) *p2p.Client { - return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, &flakyHandler{ - Handler: NewGetRangeProofHandler(db), - c: &counter{m: 2}, - }) - }, - }, - { - name: "change proof bad response - too many keys in response", - changeProofClient: func(db merkledb.MerkleDB) *p2p.Client { - handler := newFlakyChangeProofHandler(t, db, func(response *merkledb.ChangeProof) { - response.KeyChanges = append(response.KeyChanges, make([]merkledb.KeyChange, defaultRequestKeyLimit)...) - }) - - return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, handler) - }, - }, - { - name: "change proof bad response - removed first key in response", - changeProofClient: func(db merkledb.MerkleDB) *p2p.Client { - handler := newFlakyChangeProofHandler(t, db, func(response *merkledb.ChangeProof) { - response.KeyChanges = response.KeyChanges[min(1, len(response.KeyChanges)):] - }) - - return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, handler) - }, - }, - { - name: "change proof bad response - removed key from middle of response", - changeProofClient: func(db merkledb.MerkleDB) *p2p.Client { - handler := newFlakyChangeProofHandler(t, db, func(response *merkledb.ChangeProof) { - i := rand.Intn(max(1, len(response.KeyChanges)-1)) // #nosec G404 - _ = slices.Delete(response.KeyChanges, i, min(len(response.KeyChanges), i+1)) - }) - - return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, handler) - }, - }, - { - name: "change proof bad response - all proof keys removed from response", - changeProofClient: func(db merkledb.MerkleDB) *p2p.Client { - handler := newFlakyChangeProofHandler(t, db, func(response *merkledb.ChangeProof) { - response.StartProof = nil - response.EndProof = nil - }) - - return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, handler) - }, - }, - { - name: "change proof flaky server", - changeProofClient: func(db merkledb.MerkleDB) *p2p.Client { - return p2ptest.NewSelfClient(t, context.Background(), ids.EmptyNodeID, &flakyHandler{ - Handler: NewGetChangeProofHandler(db), - c: &counter{m: 2}, - }) - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - require := require.New(t) - - ctx := context.Background() - dbToSync, err := generateTrie(t, r, 3*maxKeyValuesLimit) - require.NoError(err) - - syncRoot, err := dbToSync.GetMerkleRoot(ctx) - require.NoError(err) - - db, err := merkledb.New( - ctx, - memdb.New(), - newDefaultDBConfig(), - ) - require.NoError(err) - - var ( - rangeProofClient *p2p.Client - changeProofClient *p2p.Client - ) - - rangeProofHandler := NewGetRangeProofHandler(dbToSync) - rangeProofClient = p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, rangeProofHandler) - if tt.rangeProofClient != nil { - rangeProofClient = tt.rangeProofClient(dbToSync) - } - - changeProofHandler := NewGetChangeProofHandler(dbToSync) - changeProofClient = p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, changeProofHandler) - if tt.changeProofClient != nil { - changeProofClient = tt.changeProofClient(dbToSync) - } - - syncer, err := NewManager(ManagerConfig[[]byte]{ - DB: db, - RangeProofClient: rangeProofClient, - ChangeProofClient: changeProofClient, - TargetRoot: syncRoot, - SimultaneousWorkLimit: 5, - Log: log.NewNoOpLogger(), - BranchFactor: merkledb.BranchFactor16, - }, metric.NewRegistry()) - - require.NoError(err) - require.NotNil(syncer) - - // Start syncing from the server - require.NoError(syncer.Start(ctx)) - - // Simulate writes on the server - // - // TODO add more writes when api is not flaky. There is an inherent - // race condition in between writes where UpdateSyncTarget might - // error because it has already reached the sync target before it - // is called. - for i := 0; i < 50; i++ { - addkey := make([]byte, r.Intn(50)) - _, err = r.Read(addkey) - require.NoError(err) - val := make([]byte, r.Intn(50)) - _, err = r.Read(val) - require.NoError(err) - - // Update the server's root + our sync target - require.NoError(dbToSync.Put(addkey, val)) - targetRoot, err := dbToSync.GetMerkleRoot(ctx) - require.NoError(err) - - // Simulate client periodically recording root updates - require.NoError(syncer.UpdateSyncTarget(targetRoot)) - } - - // Block until all syncing is done - require.NoError(syncer.Wait(ctx)) - - // We should have the same resulting root as the server - wantRoot, err := dbToSync.GetMerkleRoot(context.Background()) - require.NoError(err) - - gotRoot, err := db.GetMerkleRoot(context.Background()) - require.NoError(err) - require.Equal(wantRoot, gotRoot) - }) - } -} - -func Test_Sync_Result_Correct_Root_With_Sync_Restart(t *testing.T) { - require := require.New(t) - - now := time.Now().UnixNano() - t.Logf("seed: %d", now) - r := rand.New(rand.NewSource(now)) // #nosec G404 - dbToSync, err := generateTrie(t, r, 3*maxKeyValuesLimit) - require.NoError(err) - syncRoot, err := dbToSync.GetMerkleRoot(context.Background()) - require.NoError(err) - - db, err := merkledb.New( - context.Background(), - memdb.New(), - newDefaultDBConfig(), - ) - require.NoError(err) - - ctx := context.Background() - syncer, err := NewManager(ManagerConfig[[]byte]{ - DB: db, - RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(dbToSync)), - ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(dbToSync)), - TargetRoot: syncRoot, - SimultaneousWorkLimit: 5, - Log: log.NewNoOpLogger(), - BranchFactor: merkledb.BranchFactor16, - }, metric.NewRegistry()) - require.NoError(err) - require.NotNil(syncer) - require.NoError(syncer.Start(context.Background())) - - // Wait until we've processed some work - // before updating the sync target. - require.Eventually( - func() bool { - syncer.workLock.Lock() - defer syncer.workLock.Unlock() - - return syncer.processedWork.Len() > 0 - }, - 5*time.Second, - 5*time.Millisecond, - ) - syncer.Close() - - newSyncer, err := NewManager(ManagerConfig[[]byte]{ - DB: db, - RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(dbToSync)), - ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(dbToSync)), - TargetRoot: syncRoot, - SimultaneousWorkLimit: 5, - Log: log.NewNoOpLogger(), - BranchFactor: merkledb.BranchFactor16, - }, metric.NewRegistry()) - require.NoError(err) - require.NotNil(newSyncer) - - require.NoError(newSyncer.Start(context.Background())) - require.NoError(newSyncer.Error()) - require.NoError(newSyncer.Wait(context.Background())) - - newRoot, err := db.GetMerkleRoot(context.Background()) - require.NoError(err) - require.Equal(syncRoot, newRoot) -} - -func Test_Sync_Result_Correct_Root_Update_Root_During(t *testing.T) { - t.Skip("Test exhibits timing-dependent behavior; requires refactoring for determinism") - - require := require.New(t) - - now := time.Now().UnixNano() - t.Logf("seed: %d", now) - r := rand.New(rand.NewSource(now)) // #nosec G404 - - dbToSync, err := generateTrie(t, r, 3*maxKeyValuesLimit) - require.NoError(err) - - firstSyncRoot, err := dbToSync.GetMerkleRoot(context.Background()) - require.NoError(err) - - for x := 0; x < 100; x++ { - key := make([]byte, r.Intn(50)) - _, err = r.Read(key) - require.NoError(err) - - val := make([]byte, r.Intn(50)) - _, err = r.Read(val) - require.NoError(err) - - require.NoError(dbToSync.Put(key, val)) - - deleteKeyStart := make([]byte, r.Intn(50)) - _, err = r.Read(deleteKeyStart) - require.NoError(err) - - it := dbToSync.NewIteratorWithStart(deleteKeyStart) - if it.Next() { - require.NoError(dbToSync.Delete(it.Key())) - } - require.NoError(it.Error()) - it.Release() - } - - secondSyncRoot, err := dbToSync.GetMerkleRoot(context.Background()) - require.NoError(err) - - db, err := merkledb.New( - context.Background(), - memdb.New(), - newDefaultDBConfig(), - ) - require.NoError(err) - - // Only let one response go through until we update the root. - updatedRootChan := make(chan struct{}, 1) - updatedRootChan <- struct{}{} - - ctx := context.Background() - rangeProofClient := p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, &waitingHandler{ - handler: NewGetRangeProofHandler(dbToSync), - updatedRootChan: updatedRootChan, - }) - - changeProofClient := p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, &waitingHandler{ - handler: NewGetChangeProofHandler(dbToSync), - updatedRootChan: updatedRootChan, - }) - - syncer, err := NewManager(ManagerConfig[[]byte]{ - DB: db, - RangeProofClient: rangeProofClient, - ChangeProofClient: changeProofClient, - TargetRoot: firstSyncRoot, - SimultaneousWorkLimit: 5, - Log: log.NewNoOpLogger(), - BranchFactor: merkledb.BranchFactor16, - }, metric.NewRegistry()) - require.NoError(err) - require.NotNil(syncer) - - require.NoError(syncer.Start(context.Background())) - - // Wait until we've processed some work - // before updating the sync target. - require.Eventually( - func() bool { - syncer.workLock.Lock() - defer syncer.workLock.Unlock() - - return syncer.processedWork.Len() > 0 - }, - 5*time.Second, - 10*time.Millisecond, - ) - require.NoError(syncer.UpdateSyncTarget(secondSyncRoot)) - close(updatedRootChan) - - require.NoError(syncer.Wait(context.Background())) - require.NoError(syncer.Error()) - - newRoot, err := db.GetMerkleRoot(context.Background()) - require.NoError(err) - require.Equal(secondSyncRoot, newRoot) -} - -func Test_Sync_UpdateSyncTarget(t *testing.T) { - require := require.New(t) - - db, err := merkledb.New( - context.Background(), - memdb.New(), - newDefaultDBConfig(), - ) - require.NoError(err) - ctx := context.Background() - m, err := NewManager(ManagerConfig[[]byte]{ - DB: db, - RangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetRangeProofHandler(db)), - ChangeProofClient: p2ptest.NewSelfClient(t, ctx, ids.EmptyNodeID, NewGetChangeProofHandler(db)), - TargetRoot: ids.Empty, - SimultaneousWorkLimit: 5, - Log: log.NewNoOpLogger(), - BranchFactor: merkledb.BranchFactor16, - }, metric.NewRegistry()) - require.NoError(err) - - // Populate [m.processWork] to ensure that UpdateSyncTarget - // moves the work to [m.unprocessedWork]. - item := &workItem{ - start: maybe.Some([]byte{1}), - end: maybe.Some([]byte{2}), - localRootID: ids.GenerateTestID(), - } - m.processedWork.Insert(item) - - // Make sure that [m.unprocessedWorkCond] is signaled. - gotSignalChan := make(chan struct{}) - // Don't UpdateSyncTarget until we're waiting for the signal. - startedWaiting := make(chan struct{}) - go func() { - m.workLock.Lock() - defer m.workLock.Unlock() - - close(startedWaiting) - m.unprocessedWorkCond.Wait() - close(gotSignalChan) - }() - - <-startedWaiting - newSyncRoot := ids.GenerateTestID() - require.NoError(m.UpdateSyncTarget(newSyncRoot)) - <-gotSignalChan - - require.Equal(newSyncRoot, m.config.TargetRoot) - require.Zero(m.processedWork.Len()) - require.Equal(1, m.unprocessedWork.Len()) -} - -func generateTrie(t *testing.T, r *rand.Rand, count int) (merkledb.MerkleDB, error) { - return generateTrieWithMinKeyLen(t, r, count, 0) -} - -func generateTrieWithMinKeyLen(t *testing.T, r *rand.Rand, count int, minKeyLen int) (merkledb.MerkleDB, error) { - require := require.New(t) - - db, err := merkledb.New( - context.Background(), - memdb.New(), - newDefaultDBConfig(), - ) - if err != nil { - return nil, err - } - var ( - allKeys [][]byte - seenKeys = make(map[string]struct{}) - batch = db.NewBatch() - ) - genKey := func() []byte { - // new prefixed key - if len(allKeys) > 2 && r.Intn(25) < 10 { - prefix := allKeys[r.Intn(len(allKeys))] - key := make([]byte, r.Intn(50)+len(prefix)) - copy(key, prefix) - _, err := r.Read(key[len(prefix):]) - require.NoError(err) - return key - } - - // new key - key := make([]byte, r.Intn(50)+minKeyLen) - _, err = r.Read(key) - require.NoError(err) - return key - } - - for i := 0; i < count; { - value := make([]byte, r.Intn(51)) - if len(value) == 0 { - value = nil - } else { - _, err = r.Read(value) - require.NoError(err) - } - key := genKey() - if _, seen := seenKeys[string(key)]; seen { - continue // avoid duplicate keys so we always get the count - } - allKeys = append(allKeys, key) - seenKeys[string(key)] = struct{}{} - if err = batch.Put(key, value); err != nil { - return db, err - } - i++ - } - return db, batch.Write() -} diff --git a/x/sync/syncmock/network_client.go b/x/sync/syncmock/network_client.go deleted file mode 100644 index 55edde7ee..000000000 --- a/x/sync/syncmock/network_client.go +++ /dev/null @@ -1,129 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/luxfi/node/x/sync (interfaces: NetworkClient) -// -// Generated by this command: -// -// mockgen -package=syncmock -destination=x/sync/syncmock/network_client.go -mock_names=NetworkClient=NetworkClient github.com/luxfi/node/x/sync NetworkClient -// - -// Package syncmock is a generated GoMock package. -package syncmock - -import ( - context "context" - reflect "reflect" - - ids "github.com/luxfi/ids" - version "github.com/luxfi/node/version" - gomock "go.uber.org/mock/gomock" -) - -// NetworkClient is a mock of NetworkClient interface. -type NetworkClient struct { - ctrl *gomock.Controller - recorder *NetworkClientMockRecorder -} - -// NetworkClientMockRecorder is the mock recorder for NetworkClient. -type NetworkClientMockRecorder struct { - mock *NetworkClient -} - -// NewNetworkClient creates a new mock instance. -func NewNetworkClient(ctrl *gomock.Controller) *NetworkClient { - mock := &NetworkClient{ctrl: ctrl} - mock.recorder = &NetworkClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *NetworkClient) EXPECT() *NetworkClientMockRecorder { - return m.recorder -} - -// RequestFailed mocks base method. -func (m *NetworkClient) RequestFailed(arg0 context.Context, arg1 ids.NodeID, arg2 uint32) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RequestFailed", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// RequestFailed indicates an expected call of RequestFailed. -func (mr *NetworkClientMockRecorder) RequestFailed(arg0, arg1, arg2 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestFailed", reflect.TypeOf((*NetworkClient)(nil).RequestFailed), arg0, arg1, arg2) -} - -// Response mocks base method. -func (m *NetworkClient) Response(arg0 context.Context, arg1 ids.NodeID, arg2 uint32, arg3 []byte) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Response", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(error) - return ret0 -} - -// Response indicates an expected call of Response. -func (mr *NetworkClientMockRecorder) Response(arg0, arg1, arg2, arg3 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Response", reflect.TypeOf((*NetworkClient)(nil).Response), arg0, arg1, arg2, arg3) -} - -// Connected mocks base method. -func (m *NetworkClient) Connected(arg0 context.Context, arg1 ids.NodeID, arg2 *version.Application) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Connected", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// Connected indicates an expected call of Connected. -func (mr *NetworkClientMockRecorder) Connected(arg0, arg1, arg2 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connected", reflect.TypeOf((*NetworkClient)(nil).Connected), arg0, arg1, arg2) -} - -// Disconnected mocks base method. -func (m *NetworkClient) Disconnected(arg0 context.Context, arg1 ids.NodeID) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Disconnected", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// Disconnected indicates an expected call of Disconnected. -func (mr *NetworkClientMockRecorder) Disconnected(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Disconnected", reflect.TypeOf((*NetworkClient)(nil).Disconnected), arg0, arg1) -} - -// Request mocks base method. -func (m *NetworkClient) Request(arg0 context.Context, arg1 ids.NodeID, arg2 []byte) ([]byte, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Request", arg0, arg1, arg2) - ret0, _ := ret[0].([]byte) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Request indicates an expected call of Request. -func (mr *NetworkClientMockRecorder) Request(arg0, arg1, arg2 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Request", reflect.TypeOf((*NetworkClient)(nil).Request), arg0, arg1, arg2) -} - -// RequestAny mocks base method. -func (m *NetworkClient) RequestAny(arg0 context.Context, arg1 []byte) (ids.NodeID, []byte, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RequestAny", arg0, arg1) - ret0, _ := ret[0].(ids.NodeID) - ret1, _ := ret[1].([]byte) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// RequestAny indicates an expected call of RequestAny. -func (mr *NetworkClientMockRecorder) RequestAny(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestAny", reflect.TypeOf((*NetworkClient)(nil).RequestAny), arg0, arg1) -} diff --git a/x/sync/workheap.go b/x/sync/workheap.go deleted file mode 100644 index 35665cbf9..000000000 --- a/x/sync/workheap.go +++ /dev/null @@ -1,161 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package sync - -import ( - "bytes" - - "github.com/google/btree" - - "github.com/luxfi/container/heap" - "github.com/luxfi/container/maybe" -) - -// A priority queue of syncWorkItems. -// Note that work item ranges never overlap. -// Supports range merging and priority updating. -// Not safe for concurrent use. -type workHeap struct { - // Max heap of items by priority. - // i.e. heap.Pop returns highest priority item. - innerHeap heap.Set[*workItem] - // The heap items sorted by range start. - // A Nothing start is considered to be the smallest. - sortedItems *btree.BTreeG[*workItem] - closed bool -} - -func newWorkHeap() *workHeap { - return &workHeap{ - innerHeap: heap.NewSet[*workItem](func(a, b *workItem) bool { - return a.priority > b.priority - }), - sortedItems: btree.NewG( - 2, - func(a, b *workItem) bool { - aNothing := a.start.IsNothing() - bNothing := b.start.IsNothing() - if aNothing { - // [a] is Nothing, so if [b] is Nothing, they're equal. - // Otherwise, [b] is greater. - return !bNothing - } - if bNothing { - // [a] has a value and [b] doesn't so [a] is greater. - return false - } - // [a] and [b] both contain values. Compare the values. - return bytes.Compare(a.start.Value(), b.start.Value()) < 0 - }, - ), - } -} - -// Marks the heap as closed. -func (wh *workHeap) Close() { - wh.closed = true -} - -// Adds a new [item] into the heap. Will not merge items, unlike MergeInsert. -func (wh *workHeap) Insert(item *workItem) { - if wh.closed { - return - } - - wh.innerHeap.Push(item) - wh.sortedItems.ReplaceOrInsert(item) -} - -// Pops and returns a work item from the heap. -// Returns nil if no work is available or the heap is closed. -func (wh *workHeap) GetWork() *workItem { - if wh.closed || wh.Len() == 0 { - return nil - } - item, _ := wh.innerHeap.Pop() - wh.sortedItems.Delete(item) - return item -} - -// Insert the item into the heap, merging it with existing items -// that share a boundary and root ID. -// e.g. if the heap contains a work item with range -// [0,10] and then [10,20] is inserted, we will merge the two -// into a single work item with range [0,20]. -// e.g. if the heap contains work items [0,10] and [20,30], -// and we add [10,20], we will merge them into [0,30]. -func (wh *workHeap) MergeInsert(item *workItem) { - if wh.closed { - return - } - - var mergedBefore, mergedAfter *workItem - searchItem := &workItem{ - start: item.start, - } - - // Find the item with the greatest start range which is less than [item.start]. - // Note that the iterator function will run at most once, since it always returns false. - wh.sortedItems.DescendLessOrEqual( - searchItem, - func(beforeItem *workItem) bool { - if item.localRootID == beforeItem.localRootID && - maybe.Equal(item.start, beforeItem.end, bytes.Equal) { - // [beforeItem.start, beforeItem.end] and [item.start, item.end] are - // merged into [beforeItem.start, item.end] - beforeItem.end = item.end - beforeItem.priority = max(item.priority, beforeItem.priority) - wh.innerHeap.Fix(beforeItem) - mergedBefore = beforeItem - } - return false - }) - - // Find the item with the smallest start range which is greater than [item.start]. - // Note that the iterator function will run at most once, since it always returns false. - wh.sortedItems.AscendGreaterOrEqual( - searchItem, - func(afterItem *workItem) bool { - if item.localRootID == afterItem.localRootID && - maybe.Equal(item.end, afterItem.start, bytes.Equal) { - // [item.start, item.end] and [afterItem.start, afterItem.end] are merged into - // [item.start, afterItem.end]. - afterItem.start = item.start - afterItem.priority = max(item.priority, afterItem.priority) - wh.innerHeap.Fix(afterItem) - mergedAfter = afterItem - } - return false - }) - - // if the new item should be merged with both the item before and the item after, - // we can combine the before item with the after item - if mergedBefore != nil && mergedAfter != nil { - // combine the two ranges - mergedBefore.end = mergedAfter.end - // remove the second range since it is now covered by the first - wh.remove(mergedAfter) - // update the priority - mergedBefore.priority = max(mergedBefore.priority, mergedAfter.priority) - wh.innerHeap.Fix(mergedBefore) - } - - // nothing was merged, so add new item to the heap - if mergedBefore == nil && mergedAfter == nil { - // We didn't merge [item] with an existing one; put it in the heap. - wh.Insert(item) - } -} - -// Deletes [item] from the heap. -func (wh *workHeap) remove(item *workItem) { - wh.innerHeap.Remove(item) - wh.sortedItems.Delete(item) -} - -func (wh *workHeap) Len() int { - return wh.innerHeap.Len() -} diff --git a/x/sync/workheap_test.go b/x/sync/workheap_test.go deleted file mode 100644 index 8bd5a1ded..000000000 --- a/x/sync/workheap_test.go +++ /dev/null @@ -1,312 +0,0 @@ -//go:build grpc - -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package sync - -import ( - "bytes" - "math/rand" - "slices" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/luxfi/ids" - "github.com/luxfi/container/maybe" -) - -// Tests Insert and GetWork -func Test_WorkHeap_Insert_GetWork(t *testing.T) { - require := require.New(t) - h := newWorkHeap() - - lowPriorityItem := &workItem{ - start: maybe.Some([]byte{4}), - end: maybe.Some([]byte{5}), - priority: lowPriority, - localRootID: ids.GenerateTestID(), - } - mediumPriorityItem := &workItem{ - start: maybe.Some([]byte{0}), - end: maybe.Some([]byte{1}), - priority: medPriority, - localRootID: ids.GenerateTestID(), - } - highPriorityItem := &workItem{ - start: maybe.Some([]byte{2}), - end: maybe.Some([]byte{3}), - priority: highPriority, - localRootID: ids.GenerateTestID(), - } - h.Insert(highPriorityItem) - h.Insert(mediumPriorityItem) - h.Insert(lowPriorityItem) - require.Equal(3, h.Len()) - - // Ensure [sortedItems] is in right order. - got := []*workItem{} - h.sortedItems.Ascend( - func(i *workItem) bool { - got = append(got, i) - return true - }, - ) - require.Equal( - []*workItem{mediumPriorityItem, highPriorityItem, lowPriorityItem}, - got, - ) - - // Ensure priorities are in right order. - gotItem := h.GetWork() - require.Equal(highPriorityItem, gotItem) - gotItem = h.GetWork() - require.Equal(mediumPriorityItem, gotItem) - gotItem = h.GetWork() - require.Equal(lowPriorityItem, gotItem) - gotItem = h.GetWork() - require.Nil(gotItem) - - require.Zero(h.Len()) -} - -func Test_WorkHeap_remove(t *testing.T) { - require := require.New(t) - - h := newWorkHeap() - - lowPriorityItem := &workItem{ - start: maybe.Some([]byte{0}), - end: maybe.Some([]byte{1}), - priority: lowPriority, - localRootID: ids.GenerateTestID(), - } - - mediumPriorityItem := &workItem{ - start: maybe.Some([]byte{2}), - end: maybe.Some([]byte{3}), - priority: medPriority, - localRootID: ids.GenerateTestID(), - } - - highPriorityItem := &workItem{ - start: maybe.Some([]byte{4}), - end: maybe.Some([]byte{5}), - priority: highPriority, - localRootID: ids.GenerateTestID(), - } - - h.Insert(lowPriorityItem) - - wrappedLowPriorityItem, ok := h.innerHeap.Peek() - require.True(ok) - h.remove(wrappedLowPriorityItem) - - require.Zero(h.Len()) - require.Zero(h.sortedItems.Len()) - - h.Insert(lowPriorityItem) - h.Insert(mediumPriorityItem) - h.Insert(highPriorityItem) - - wrappedhighPriorityItem, ok := h.innerHeap.Peek() - require.True(ok) - require.Equal(highPriorityItem, wrappedhighPriorityItem) - h.remove(wrappedhighPriorityItem) - require.Equal(2, h.Len()) - require.Equal(2, h.sortedItems.Len()) - got, ok := h.innerHeap.Peek() - require.True(ok) - require.Equal(mediumPriorityItem, got) - - wrappedMediumPriorityItem, ok := h.innerHeap.Peek() - require.True(ok) - require.Equal(mediumPriorityItem, wrappedMediumPriorityItem) - h.remove(wrappedMediumPriorityItem) - require.Equal(1, h.Len()) - require.Equal(1, h.sortedItems.Len()) - got, ok = h.innerHeap.Peek() - require.True(ok) - require.Equal(lowPriorityItem, got) - - wrappedLowPriorityItem, ok = h.innerHeap.Peek() - require.True(ok) - require.Equal(lowPriorityItem, wrappedLowPriorityItem) - h.remove(wrappedLowPriorityItem) - require.Zero(h.Len()) - require.Zero(h.sortedItems.Len()) -} - -func Test_WorkHeap_Merge_Insert(t *testing.T) { - // merge with range before - syncHeap := newWorkHeap() - - syncHeap.MergeInsert(&workItem{start: maybe.Nothing[[]byte](), end: maybe.Some([]byte{63})}) - require.Equal(t, 1, syncHeap.Len()) - - syncHeap.MergeInsert(&workItem{start: maybe.Some([]byte{127}), end: maybe.Some([]byte{192})}) - require.Equal(t, 2, syncHeap.Len()) - - syncHeap.MergeInsert(&workItem{start: maybe.Some([]byte{193}), end: maybe.Nothing[[]byte]()}) - require.Equal(t, 3, syncHeap.Len()) - - syncHeap.MergeInsert(&workItem{start: maybe.Some([]byte{63}), end: maybe.Some([]byte{126}), priority: lowPriority}) - require.Equal(t, 3, syncHeap.Len()) - - // merge with range after - syncHeap = newWorkHeap() - - syncHeap.MergeInsert(&workItem{start: maybe.Nothing[[]byte](), end: maybe.Some([]byte{63})}) - require.Equal(t, 1, syncHeap.Len()) - - syncHeap.MergeInsert(&workItem{start: maybe.Some([]byte{127}), end: maybe.Some([]byte{192})}) - require.Equal(t, 2, syncHeap.Len()) - - syncHeap.MergeInsert(&workItem{start: maybe.Some([]byte{193}), end: maybe.Nothing[[]byte]()}) - require.Equal(t, 3, syncHeap.Len()) - - syncHeap.MergeInsert(&workItem{start: maybe.Some([]byte{64}), end: maybe.Some([]byte{127}), priority: lowPriority}) - require.Equal(t, 3, syncHeap.Len()) - - // merge both sides at the same time - syncHeap = newWorkHeap() - - syncHeap.MergeInsert(&workItem{start: maybe.Nothing[[]byte](), end: maybe.Some([]byte{63})}) - require.Equal(t, 1, syncHeap.Len()) - - syncHeap.MergeInsert(&workItem{start: maybe.Some([]byte{127}), end: maybe.Nothing[[]byte]()}) - require.Equal(t, 2, syncHeap.Len()) - - syncHeap.MergeInsert(&workItem{start: maybe.Some([]byte{63}), end: maybe.Some([]byte{127}), priority: lowPriority}) - require.Equal(t, 1, syncHeap.Len()) -} - -func TestWorkHeapMergeInsertRandom(t *testing.T) { - var ( - require = require.New(t) - seed = time.Now().UnixNano() - rand = rand.New(rand.NewSource(seed)) // #nosec G404 - numRanges = 1_000 - bounds = [][]byte{} - rootID = ids.GenerateTestID() - ) - t.Logf("seed: %d", seed) - - // Create start and end bounds - for i := 0; i < numRanges; i++ { - bound := make([]byte, 32) - _, _ = rand.Read(bound) - bounds = append(bounds, bound) - } - slices.SortFunc(bounds, bytes.Compare) - - // Note that start < end for all ranges. - // It is possible but extremely unlikely that - // two elements of [bounds] are equal. - ranges := []workItem{} - for i := 0; i < numRanges/2; i++ { - start := bounds[i*2] - end := bounds[i*2+1] - ranges = append(ranges, workItem{ - start: maybe.Some(start), - end: maybe.Some(end), - priority: lowPriority, - // Note they all share the same root ID. - localRootID: rootID, - }) - } - // Set beginning of first range to Nothing. - ranges[0].start = maybe.Nothing[[]byte]() - // Set end of last range to Nothing. - ranges[len(ranges)-1].end = maybe.Nothing[[]byte]() - - setup := func() *workHeap { - // Insert all the ranges into the heap. - h := newWorkHeap() - for i, r := range ranges { - require.Equal(i, h.Len()) - rCopy := r - h.MergeInsert(&rCopy) - } - return h - } - - { - // Case 1: Merging an item with the range before and after - h := setup() - // Keep merging ranges until there's only one range left. - for i := 0; i < len(ranges)-1; i++ { - // Merge ranges[i] with ranges[i+1] - h.MergeInsert(&workItem{ - start: ranges[i].end, - end: ranges[i+1].start, - priority: lowPriority, - localRootID: rootID, - }) - require.Equal(len(ranges)-i-1, h.Len()) - } - got := h.GetWork() - require.True(got.start.IsNothing()) - require.True(got.end.IsNothing()) - } - - { - // Case 2: Merging an item with the range before - h := setup() - for i := 0; i < len(ranges)-1; i++ { - // Extend end of ranges[i] - newEnd := slices.Clone(ranges[i].end.Value()) - newEnd = append(newEnd, 0) - h.MergeInsert(&workItem{ - start: ranges[i].end, - end: maybe.Some(newEnd), - priority: lowPriority, - localRootID: rootID, - }) - - // Shouldn't cause number of elements to change - require.Equal(len(ranges), h.Len()) - - start := ranges[i].start - if i == 0 { - start = maybe.Nothing[[]byte]() - } - // Make sure end is updated - got, ok := h.sortedItems.Get(&workItem{ - start: start, - }) - require.True(ok) - require.Equal(newEnd, got.end.Value()) - } - } - - { - // Case 3: Merging an item with the range after - h := setup() - for i := 1; i < len(ranges); i++ { - // Extend start of ranges[i] - newStartBytes := slices.Clone(ranges[i].start.Value()) - newStartBytes = newStartBytes[:len(newStartBytes)-1] - newStart := maybe.Some(newStartBytes) - - h.MergeInsert(&workItem{ - start: newStart, - end: ranges[i].start, - priority: lowPriority, - localRootID: rootID, - }) - - // Shouldn't cause number of elements to change - require.Equal(len(ranges), h.Len()) - - // Make sure start is updated - got, ok := h.sortedItems.Get(&workItem{ - start: newStart, - }) - require.True(ok) - require.Equal(newStartBytes, got.start.Value()) - } - } -}