diff --git a/cmd/ledger/main.go b/cmd/ledger/main.go index 0dd48ca6b98..522e20eb1dc 100644 --- a/cmd/ledger/main.go +++ b/cmd/ledger/main.go @@ -127,6 +127,10 @@ func main() { ledgerService := remote.NewService(ledgerStorage, logger) ledgerpb.RegisterLedgerServiceServer(grpcServer, ledgerService) + // Register the info service so clients can discover this server's mode + // before issuing mode-specific RPCs. This binary serves the full ledger. + ledgerpb.RegisterLedgerInfoServiceServer(grpcServer, remote.NewInfoService(ledgerpb.LedgerMode_LEDGER_MODE_FULL)) + // Create listeners based on provided flags type listenerInfo struct { listener net.Listener diff --git a/engine/execution/state/state.go b/engine/execution/state/state.go index 54cc5828a44..36192741b44 100644 --- a/engine/execution/state/state.go +++ b/engine/execution/state/state.go @@ -285,7 +285,10 @@ func (s *state) CreateStorageSnapshot( } // make sure we have trie state for this block - ledgerHasState := s.ls.HasState(ledger.State(commit)) + ledgerHasState, err := s.ls.HasState(ledger.State(commit)) + if err != nil { + return nil, header, fmt.Errorf("cannot check ledger state for commit %x (block %v): %w", commit, blockID, err) + } if !ledgerHasState { return nil, header, fmt.Errorf("state not found in ledger for commit %x (block %v): %w", commit, blockID, ErrExecutionStatePruned) } diff --git a/engine/execution/state/state_storehouse_test.go b/engine/execution/state/state_storehouse_test.go index 3fb51e87d19..05b74c0da8a 100644 --- a/engine/execution/state/state_storehouse_test.go +++ b/engine/execution/state/state_storehouse_test.go @@ -191,8 +191,12 @@ func TestExecutionStateWithStorehouse(t *testing.T) { require.Equal(t, flow.RegisterValue("carrot"), b2) // verify has state - require.True(t, l.HasState(led.State(sc2))) - require.False(t, l.HasState(led.State(unittest.StateCommitmentFixture()))) + hasState, err := l.HasState(led.State(sc2)) + require.NoError(t, err) + require.True(t, hasState) + hasState, err = l.HasState(led.State(unittest.StateCommitmentFixture())) + require.NoError(t, err) + require.False(t, hasState) })) } diff --git a/engine/execution/state/state_test.go b/engine/execution/state/state_test.go index 9cf96405024..44ad664378e 100644 --- a/engine/execution/state/state_test.go +++ b/engine/execution/state/state_test.go @@ -113,8 +113,12 @@ func TestExecutionStateWithTrieStorage(t *testing.T) { require.Equal(t, flow.RegisterValue("carrot"), b2) // verify has state - require.True(t, l.HasState(led.State(sc2))) - require.False(t, l.HasState(led.State(unittest.StateCommitmentFixture()))) + hasState, err := l.HasState(led.State(sc2)) + require.NoError(t, err) + require.True(t, hasState) + hasState, err = l.HasState(led.State(unittest.StateCommitmentFixture())) + require.NoError(t, err) + require.False(t, hasState) })) t.Run("commit write and read previous state", prepareTest(func( diff --git a/ledger/complete/ledger.go b/ledger/complete/ledger.go index 82966abb3b6..fe2bbd0808d 100644 --- a/ledger/complete/ledger.go +++ b/ledger/complete/ledger.go @@ -425,8 +425,10 @@ func (l *Ledger) MostRecentTouchedState() (ledger.State, error) { } // HasState returns true if the given state exists inside the ledger -func (l *Ledger) HasState(state ledger.State) bool { - return l.forest.HasTrie(ledger.RootHash(state)) +// +// No error returns are expected during normal operation. +func (l *Ledger) HasState(state ledger.State) (bool, error) { + return l.forest.HasTrie(ledger.RootHash(state)), nil } // DumpTrieAsJSON export trie at specific state as JSONL (each line is JSON encoding of a payload) diff --git a/ledger/complete/payloadless_ledger.go b/ledger/complete/payloadless_ledger.go index d840afe2237..e68b72ef734 100644 --- a/ledger/complete/payloadless_ledger.go +++ b/ledger/complete/payloadless_ledger.go @@ -84,8 +84,10 @@ func (l *PayloadlessLedger) InitialState() ledger.State { } // HasState returns true if the given state exists inside the ledger. -func (l *PayloadlessLedger) HasState(state ledger.State) bool { - return l.forest.HasTrie(ledger.RootHash(state)) +// +// No error returns are expected during normal operation. +func (l *PayloadlessLedger) HasState(state ledger.State) (bool, error) { + return l.forest.HasTrie(ledger.RootHash(state)), nil } // HasPaths reports, for each key in `query`, whether the key has an allocated diff --git a/ledger/complete/payloadless_ledger_test.go b/ledger/complete/payloadless_ledger_test.go index ce18d8588e8..7aeeee69bd0 100644 --- a/ledger/complete/payloadless_ledger_test.go +++ b/ledger/complete/payloadless_ledger_test.go @@ -93,7 +93,9 @@ func TestPayloadlessLedger_Set(t *testing.T) { require.NoError(t, err) assert.NotEqual(t, state, newState) assert.False(t, trieUpdate.IsEmpty()) - assert.True(t, l.HasState(newState)) + hasState, err := l.HasState(newState) + require.NoError(t, err) + assert.True(t, hasState) }) } @@ -260,8 +262,14 @@ func TestPayloadlessLedger_Equivalence_Set(t *testing.T) { require.NoError(t, err) assert.Equal(t, fullNew, plNew, "states should agree after identical update") - assert.True(t, full.HasState(fullNew)) - assert.True(t, pl.HasState(plNew)) + + fullHasState, err := full.HasState(fullNew) + require.NoError(t, err) + assert.True(t, fullHasState) + + plHasState, err := pl.HasState(plNew) + require.NoError(t, err) + assert.True(t, plHasState) } // TestPayloadlessLedger_Equivalence_Reads verifies that for every allocated diff --git a/ledger/factory/factory_test.go b/ledger/factory/factory_test.go index d42b50dd09d..1e79b8e11fe 100644 --- a/ledger/factory/factory_test.go +++ b/ledger/factory/factory_test.go @@ -50,15 +50,19 @@ func TestRemoteLedgerClient(t *testing.T) { // Both should have the same initial state assert.Equal(t, localInitialState, remoteInitialState) - localHasState := localLedger.HasState(localInitialState) - remoteHasState := remoteLedger.HasState(remoteInitialState) + localHasState, err := localLedger.HasState(localInitialState) + require.NoError(t, err) + remoteHasState, err := remoteLedger.HasState(remoteInitialState) + require.NoError(t, err) assert.Equal(t, localHasState, remoteHasState, "HasState should return the same result for local and remote ledger") assert.True(t, localHasState) // Test with non-existent state dummyState := ledger.DummyState - localHasState = localLedger.HasState(dummyState) - remoteHasState = remoteLedger.HasState(dummyState) + localHasState, err = localLedger.HasState(dummyState) + require.NoError(t, err) + remoteHasState, err = remoteLedger.HasState(dummyState) + require.NoError(t, err) assert.Equal(t, localHasState, remoteHasState, "HasState for non-existent state should return the same result") assert.False(t, localHasState) }) diff --git a/ledger/ledger.go b/ledger/ledger.go index c7255648b31..6f8cd83c79b 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -26,7 +26,9 @@ type Ledger interface { InitialState() State // HasState returns true if the given state exists inside the ledger - HasState(state State) bool + // + // No error returns are expected during normal operation. + HasState(state State) (bool, error) // GetSingleValue returns value for a given key at specific state GetSingleValue(query *QuerySingleValue) (value Value, err error) @@ -48,6 +50,56 @@ type Ledger interface { StateByIndex(index int) (State, error) } +// PayloadlessLedger is the payloadless-mode counterpart of [Ledger]. It is a +// stateful fork-aware key/value storage that stores only leaf hashes +// (HashLeaf(path, value)) per register rather than the original payload values. +// Reads therefore return leaf hashes, not values, and the proof type is +// [PayloadlessTrieBatchProof] rather than [Proof] (an encoded TrieBatchProof). +// +// In production, *complete.PayloadlessLedger satisfies this interface by +// construction. The interface lives here (and not in ledger/complete) so +// downstream consumers — committer, remote gRPC service, future verification +// clients — can depend on the payloadless ledger without importing +// ledger/complete and pulling in WAL/forest infrastructure. +type PayloadlessLedger interface { + // PayloadlessLedger implements methods needed to be ReadyDone aware + module.ReadyDoneAware + + // InitialState returns the initial state of the ledger + InitialState() State + + // HasState returns true if the given state exists inside the ledger + // + // No error returns are expected during normal operation. + HasState(state State) (bool, error) + + // HasPaths reports, for each key in the query, whether the corresponding + // path has an allocated register at the query's state. Used by callers + // that need register-existence checks without retrieving leaf hashes. + HasPaths(query *Query) ([]bool, error) + + // GetSingleLeafHash returns the leaf hash for a single key at the + // query's state. Returns nil if the path is unallocated or the leaf + // represents an empty register. + GetSingleLeafHash(query *QuerySingleValue) (*hash.Hash, error) + + // GetLeafHashes returns leaf hashes for the given slice of keys at the + // query's state. A nil entry indicates an unallocated path or an empty + // leaf. The returned slice is aligned with the query's Keys order. + GetLeafHashes(query *Query) ([]*hash.Hash, error) + + // Set updates a list of keys with new values at the given state and + // returns the new state and the resulting trie update. The trie update + // records the writes regardless of payloadless storage; only the + // payload bytes are discarded. + Set(update *Update) (newState State, trieUpdate *TrieUpdate, err error) + + // Prove returns a payloadless batch proof for the given keys at the + // query's state. Encoded with [EncodePayloadlessTrieBatchProof] on the + // wire; consumers must decode with [DecodePayloadlessTrieBatchProof]. + Prove(query *Query) (*PayloadlessTrieBatchProof, error) +} + // Query holds all data needed for a ledger read or ledger proof type Query struct { state State diff --git a/ledger/mock/ledger.go b/ledger/mock/ledger.go index 8bad7e84dcd..385182baaf5 100644 --- a/ledger/mock/ledger.go +++ b/ledger/mock/ledger.go @@ -207,7 +207,7 @@ func (_c *Ledger_GetSingleValue_Call) RunAndReturn(run func(query *ledger.QueryS } // HasState provides a mock function for the type Ledger -func (_mock *Ledger) HasState(state ledger.State) bool { +func (_mock *Ledger) HasState(state ledger.State) (bool, error) { ret := _mock.Called(state) if len(ret) == 0 { @@ -215,12 +215,21 @@ func (_mock *Ledger) HasState(state ledger.State) bool { } var r0 bool + var r1 error + if returnFunc, ok := ret.Get(0).(func(ledger.State) (bool, error)); ok { + return returnFunc(state) + } if returnFunc, ok := ret.Get(0).(func(ledger.State) bool); ok { r0 = returnFunc(state) } else { r0 = ret.Get(0).(bool) } - return r0 + if returnFunc, ok := ret.Get(1).(func(ledger.State) error); ok { + r1 = returnFunc(state) + } else { + r1 = ret.Error(1) + } + return r0, r1 } // Ledger_HasState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HasState' @@ -247,12 +256,12 @@ func (_c *Ledger_HasState_Call) Run(run func(state ledger.State)) *Ledger_HasSta return _c } -func (_c *Ledger_HasState_Call) Return(b bool) *Ledger_HasState_Call { - _c.Call.Return(b) +func (_c *Ledger_HasState_Call) Return(b bool, err error) *Ledger_HasState_Call { + _c.Call.Return(b, err) return _c } -func (_c *Ledger_HasState_Call) RunAndReturn(run func(state ledger.State) bool) *Ledger_HasState_Call { +func (_c *Ledger_HasState_Call) RunAndReturn(run func(state ledger.State) (bool, error)) *Ledger_HasState_Call { _c.Call.Return(run) return _c } diff --git a/ledger/partial/ledger.go b/ledger/partial/ledger.go index a807556af5d..454da0b43e0 100644 --- a/ledger/partial/ledger.go +++ b/ledger/partial/ledger.go @@ -63,8 +63,10 @@ func (l *Ledger) InitialState() ledger.State { } // HasState returns true if the given state exists inside the ledger -func (l *Ledger) HasState(other ledger.State) bool { - return l.state.Equals(other) +// +// No error returns are expected during normal operation. +func (l *Ledger) HasState(other ledger.State) (bool, error) { + return l.state.Equals(other), nil } // GetSingleValue reads value of a given key at the given state diff --git a/ledger/payloadless_proof_test.go b/ledger/payloadless_proof_test.go index a9efa8444d9..f89721a9fc6 100644 --- a/ledger/payloadless_proof_test.go +++ b/ledger/payloadless_proof_test.go @@ -273,3 +273,36 @@ func TestPayloadlessTrieBatchProofEquals(t *testing.T) { require.False(t, bp1.Equals(bp2)) }) } + +// TestPayloadlessTrieBatchProof_EncodeDecodeRoundtrip verifies that a batch proof +// survives an encode/decode roundtrip and that decoding rejects input carrying +// unexpected trailing bytes after the declared proofs. +func TestPayloadlessTrieBatchProof_EncodeDecodeRoundtrip(t *testing.T) { + leafHash := hash.HashLeaf(hash.DummyHash, []byte("v")) + + p := NewPayloadlessTrieProof() + p.Path = Path(hash.DummyHash) + p.LeafHash = &leafHash + p.Inclusion = true + p.Steps = 3 + p.Flags[0] = 0x01 + p.Interims = []hash.Hash{hash.DummyHash} + + bp := NewPayloadlessTrieBatchProof() + bp.AppendProof(p) + bp.AppendProof(NewPayloadlessTrieProof()) + + encoded := EncodePayloadlessTrieBatchProof(bp) + + t.Run("roundtrip", func(t *testing.T) { + decoded, err := DecodePayloadlessTrieBatchProof(encoded) + require.NoError(t, err) + require.True(t, bp.Equals(decoded)) + }) + + t.Run("rejects trailing bytes", func(t *testing.T) { + tampered := append(append([]byte{}, encoded...), 0xDE, 0xAD) + _, err := DecodePayloadlessTrieBatchProof(tampered) + require.Error(t, err) + }) +} diff --git a/ledger/protobuf/ledger.pb.go b/ledger/protobuf/ledger.pb.go index 602d79a9ba9..d7d09fd1394 100644 --- a/ledger/protobuf/ledger.pb.go +++ b/ledger/protobuf/ledger.pb.go @@ -21,6 +21,35 @@ var _ = math.Inf // proto package needs to be updated. const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package +// LedgerMode reports the operating mode of the ledger server. +type LedgerMode int32 + +const ( + LedgerMode_LEDGER_MODE_UNSPECIFIED LedgerMode = 0 + LedgerMode_LEDGER_MODE_FULL LedgerMode = 1 + LedgerMode_LEDGER_MODE_PAYLOADLESS LedgerMode = 2 +) + +var LedgerMode_name = map[int32]string{ + 0: "LEDGER_MODE_UNSPECIFIED", + 1: "LEDGER_MODE_FULL", + 2: "LEDGER_MODE_PAYLOADLESS", +} + +var LedgerMode_value = map[string]int32{ + "LEDGER_MODE_UNSPECIFIED": 0, + "LEDGER_MODE_FULL": 1, + "LEDGER_MODE_PAYLOADLESS": 2, +} + +func (x LedgerMode) String() string { + return proto.EnumName(LedgerMode_name, int32(x)) +} + +func (LedgerMode) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_63585974d4c6a2c4, []int{0} +} + // State represents a ledger state (32-byte hash) type State struct { Hash []byte `protobuf:"bytes,1,opt,name=hash,proto3" json:"hash,omitempty"` @@ -692,7 +721,211 @@ func (m *ProofResponse) GetProof() []byte { return nil } +// ServerInfoResponse reports server metadata such as the operating mode. +type ServerInfoResponse struct { + Mode LedgerMode `protobuf:"varint,1,opt,name=mode,proto3,enum=ledger.LedgerMode" json:"mode,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ServerInfoResponse) Reset() { *m = ServerInfoResponse{} } +func (m *ServerInfoResponse) String() string { return proto.CompactTextString(m) } +func (*ServerInfoResponse) ProtoMessage() {} +func (*ServerInfoResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_63585974d4c6a2c4, []int{15} +} + +func (m *ServerInfoResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ServerInfoResponse.Unmarshal(m, b) +} +func (m *ServerInfoResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ServerInfoResponse.Marshal(b, m, deterministic) +} +func (m *ServerInfoResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_ServerInfoResponse.Merge(m, src) +} +func (m *ServerInfoResponse) XXX_Size() int { + return xxx_messageInfo_ServerInfoResponse.Size(m) +} +func (m *ServerInfoResponse) XXX_DiscardUnknown() { + xxx_messageInfo_ServerInfoResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_ServerInfoResponse proto.InternalMessageInfo + +func (m *ServerInfoResponse) GetMode() LedgerMode { + if m != nil { + return m.Mode + } + return LedgerMode_LEDGER_MODE_UNSPECIFIED +} + +// LeafHash is a 32-byte HashLeaf(path, value). +// An empty `hash` (length 0) indicates the path is unallocated. +type LeafHash struct { + Hash []byte `protobuf:"bytes,1,opt,name=hash,proto3" json:"hash,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *LeafHash) Reset() { *m = LeafHash{} } +func (m *LeafHash) String() string { return proto.CompactTextString(m) } +func (*LeafHash) ProtoMessage() {} +func (*LeafHash) Descriptor() ([]byte, []int) { + return fileDescriptor_63585974d4c6a2c4, []int{16} +} + +func (m *LeafHash) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_LeafHash.Unmarshal(m, b) +} +func (m *LeafHash) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_LeafHash.Marshal(b, m, deterministic) +} +func (m *LeafHash) XXX_Merge(src proto.Message) { + xxx_messageInfo_LeafHash.Merge(m, src) +} +func (m *LeafHash) XXX_Size() int { + return xxx_messageInfo_LeafHash.Size(m) +} +func (m *LeafHash) XXX_DiscardUnknown() { + xxx_messageInfo_LeafHash.DiscardUnknown(m) +} + +var xxx_messageInfo_LeafHash proto.InternalMessageInfo + +func (m *LeafHash) GetHash() []byte { + if m != nil { + return m.Hash + } + return nil +} + +// LeafHashResponse contains a single leaf hash. +type LeafHashResponse struct { + LeafHash *LeafHash `protobuf:"bytes,1,opt,name=leaf_hash,json=leafHash,proto3" json:"leaf_hash,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *LeafHashResponse) Reset() { *m = LeafHashResponse{} } +func (m *LeafHashResponse) String() string { return proto.CompactTextString(m) } +func (*LeafHashResponse) ProtoMessage() {} +func (*LeafHashResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_63585974d4c6a2c4, []int{17} +} + +func (m *LeafHashResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_LeafHashResponse.Unmarshal(m, b) +} +func (m *LeafHashResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_LeafHashResponse.Marshal(b, m, deterministic) +} +func (m *LeafHashResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_LeafHashResponse.Merge(m, src) +} +func (m *LeafHashResponse) XXX_Size() int { + return xxx_messageInfo_LeafHashResponse.Size(m) +} +func (m *LeafHashResponse) XXX_DiscardUnknown() { + xxx_messageInfo_LeafHashResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_LeafHashResponse proto.InternalMessageInfo + +func (m *LeafHashResponse) GetLeafHash() *LeafHash { + if m != nil { + return m.LeafHash + } + return nil +} + +// LeafHashesResponse contains a slice of leaf hashes, one per input key, +// in the same order as the request. +type LeafHashesResponse struct { + LeafHashes []*LeafHash `protobuf:"bytes,1,rep,name=leaf_hashes,json=leafHashes,proto3" json:"leaf_hashes,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *LeafHashesResponse) Reset() { *m = LeafHashesResponse{} } +func (m *LeafHashesResponse) String() string { return proto.CompactTextString(m) } +func (*LeafHashesResponse) ProtoMessage() {} +func (*LeafHashesResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_63585974d4c6a2c4, []int{18} +} + +func (m *LeafHashesResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_LeafHashesResponse.Unmarshal(m, b) +} +func (m *LeafHashesResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_LeafHashesResponse.Marshal(b, m, deterministic) +} +func (m *LeafHashesResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_LeafHashesResponse.Merge(m, src) +} +func (m *LeafHashesResponse) XXX_Size() int { + return xxx_messageInfo_LeafHashesResponse.Size(m) +} +func (m *LeafHashesResponse) XXX_DiscardUnknown() { + xxx_messageInfo_LeafHashesResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_LeafHashesResponse proto.InternalMessageInfo + +func (m *LeafHashesResponse) GetLeafHashes() []*LeafHash { + if m != nil { + return m.LeafHashes + } + return nil +} + +// HasPathsResponse reports, for each input key, whether the key has an +// allocated register at the requested state. +type HasPathsResponse struct { + Exists []bool `protobuf:"varint,1,rep,packed,name=exists,proto3" json:"exists,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *HasPathsResponse) Reset() { *m = HasPathsResponse{} } +func (m *HasPathsResponse) String() string { return proto.CompactTextString(m) } +func (*HasPathsResponse) ProtoMessage() {} +func (*HasPathsResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_63585974d4c6a2c4, []int{19} +} + +func (m *HasPathsResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_HasPathsResponse.Unmarshal(m, b) +} +func (m *HasPathsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_HasPathsResponse.Marshal(b, m, deterministic) +} +func (m *HasPathsResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_HasPathsResponse.Merge(m, src) +} +func (m *HasPathsResponse) XXX_Size() int { + return xxx_messageInfo_HasPathsResponse.Size(m) +} +func (m *HasPathsResponse) XXX_DiscardUnknown() { + xxx_messageInfo_HasPathsResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_HasPathsResponse proto.InternalMessageInfo + +func (m *HasPathsResponse) GetExists() []bool { + if m != nil { + return m.Exists + } + return nil +} + func init() { + proto.RegisterEnum("ledger.LedgerMode", LedgerMode_name, LedgerMode_value) proto.RegisterType((*State)(nil), "ledger.State") proto.RegisterType((*KeyPart)(nil), "ledger.KeyPart") proto.RegisterType((*Key)(nil), "ledger.Key") @@ -708,46 +941,67 @@ func init() { proto.RegisterType((*SetResponse)(nil), "ledger.SetResponse") proto.RegisterType((*ProveRequest)(nil), "ledger.ProveRequest") proto.RegisterType((*ProofResponse)(nil), "ledger.ProofResponse") + proto.RegisterType((*ServerInfoResponse)(nil), "ledger.ServerInfoResponse") + proto.RegisterType((*LeafHash)(nil), "ledger.LeafHash") + proto.RegisterType((*LeafHashResponse)(nil), "ledger.LeafHashResponse") + proto.RegisterType((*LeafHashesResponse)(nil), "ledger.LeafHashesResponse") + proto.RegisterType((*HasPathsResponse)(nil), "ledger.HasPathsResponse") } func init() { proto.RegisterFile("ledger.proto", fileDescriptor_63585974d4c6a2c4) } var fileDescriptor_63585974d4c6a2c4 = []byte{ - // 563 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x54, 0xdd, 0x6a, 0xdb, 0x4c, - 0x10, 0xc5, 0x76, 0xe4, 0xcf, 0x19, 0xd9, 0x5f, 0xcb, 0xd6, 0x2e, 0xc6, 0x26, 0x34, 0xa8, 0x18, - 0xd2, 0x3f, 0x09, 0x6c, 0x5f, 0x15, 0x7a, 0x53, 0x68, 0xdd, 0x92, 0x52, 0x8c, 0xd4, 0xf6, 0x22, - 0xbd, 0x30, 0x72, 0x3c, 0x96, 0x45, 0x14, 0xad, 0xaa, 0x5d, 0xdb, 0xe8, 0x8d, 0xfb, 0x18, 0x65, - 0x7f, 0x14, 0x49, 0x6e, 0x08, 0x0d, 0xe4, 0x46, 0xec, 0xce, 0x9c, 0xb3, 0xe7, 0x8c, 0x66, 0x77, - 0xa0, 0x1d, 0xe1, 0x2a, 0xc0, 0xd4, 0x4e, 0x52, 0xca, 0x29, 0x69, 0xaa, 0xdd, 0x60, 0x18, 0x50, - 0x1a, 0x44, 0xe8, 0xc8, 0xe8, 0x72, 0xbb, 0x76, 0xf0, 0x3a, 0xe1, 0x99, 0x02, 0x59, 0x43, 0x30, - 0x3c, 0xee, 0x73, 0x24, 0x04, 0x8e, 0x36, 0x3e, 0xdb, 0xf4, 0x6b, 0xa7, 0xb5, 0xb3, 0xb6, 0x2b, - 0xd7, 0xd6, 0x04, 0xfe, 0x3b, 0xc7, 0x6c, 0xee, 0xa7, 0x5c, 0xa4, 0x79, 0x96, 0xa0, 0x4c, 0x77, - 0x5c, 0xb9, 0x26, 0x5d, 0x30, 0x76, 0x7e, 0xb4, 0xc5, 0x7e, 0x5d, 0x72, 0xd4, 0xc6, 0x7a, 0x0d, - 0x8d, 0x73, 0xcc, 0xc8, 0x08, 0x8c, 0xc4, 0x4f, 0x39, 0xeb, 0xd7, 0x4e, 0x1b, 0x67, 0xe6, 0xf8, - 0x91, 0xad, 0xbd, 0xe9, 0x03, 0x5d, 0x95, 0xb5, 0xc6, 0x60, 0xfc, 0x10, 0x34, 0x21, 0xb0, 0xf2, - 0xb9, 0x9f, 0xeb, 0x8b, 0x35, 0xe9, 0x41, 0x33, 0x64, 0x8b, 0x38, 0x8c, 0xa4, 0x42, 0xcb, 0x35, - 0x42, 0xf6, 0x35, 0x8c, 0xac, 0x09, 0xb4, 0xa5, 0x67, 0x17, 0x7f, 0x6d, 0x91, 0x71, 0xf2, 0x1c, - 0x0c, 0x26, 0xf6, 0x92, 0x6b, 0x8e, 0x3b, 0xb9, 0x94, 0x02, 0xa9, 0x9c, 0x35, 0x85, 0x8e, 0x26, - 0xb1, 0x84, 0xc6, 0x0c, 0xff, 0x8d, 0xe5, 0xc0, 0xe3, 0x4f, 0x3e, 0xab, 0x12, 0x87, 0x70, 0xbc, - 0xf1, 0xd9, 0xa2, 0x20, 0xb7, 0xdc, 0xd6, 0x46, 0x83, 0xac, 0x9f, 0xd0, 0x9b, 0x21, 0xf7, 0xc2, - 0x38, 0x88, 0x50, 0x16, 0x76, 0x1f, 0x93, 0xe4, 0x04, 0x1a, 0x57, 0x98, 0xc9, 0x6a, 0xcd, 0xb1, - 0x59, 0xfa, 0x65, 0xae, 0x88, 0x8b, 0x1a, 0xf4, 0x99, 0x45, 0x0d, 0xaa, 0x03, 0x07, 0x87, 0x2a, - 0x94, 0x6e, 0x88, 0x0b, 0x30, 0x43, 0x7e, 0x2f, 0x1f, 0xcf, 0xe0, 0xe8, 0x0a, 0x33, 0xd6, 0xaf, - 0xcb, 0xde, 0x55, 0x8c, 0xc8, 0x84, 0x35, 0x05, 0x53, 0x9e, 0xa9, 0x7d, 0x8c, 0xa0, 0x29, 0xb5, - 0xf2, 0x6e, 0x1f, 0x18, 0xd1, 0x49, 0x2b, 0x03, 0xf0, 0x1e, 0xd8, 0x49, 0x49, 0xba, 0x71, 0x97, - 0xf4, 0x05, 0x98, 0x5e, 0xc9, 0xf0, 0x4b, 0x38, 0x8e, 0x71, 0xbf, 0xb8, 0x43, 0xbf, 0x15, 0xe3, - 0xde, 0xd3, 0x16, 0x4c, 0x9e, 0x86, 0xb8, 0xd8, 0x26, 0x2b, 0x81, 0x56, 0x97, 0x1d, 0x44, 0xe8, - 0xbb, 0x8c, 0x58, 0xdf, 0xa0, 0x3d, 0x4f, 0xe9, 0x0e, 0x1f, 0xf6, 0x17, 0x8f, 0xa0, 0x33, 0x4f, - 0x29, 0x5d, 0xdf, 0x78, 0xee, 0x82, 0x91, 0x88, 0x80, 0x7e, 0x22, 0x6a, 0x33, 0xfe, 0x5d, 0x87, - 0xce, 0x17, 0xc9, 0xf5, 0x30, 0xdd, 0x85, 0x97, 0x48, 0xde, 0x41, 0xfb, 0x73, 0x1c, 0xf2, 0xd0, - 0x8f, 0x94, 0xff, 0xa7, 0xb6, 0x1a, 0x00, 0x76, 0x3e, 0x00, 0xec, 0x0f, 0x62, 0x00, 0x0c, 0x7a, - 0x55, 0x5f, 0xb9, 0xcc, 0x5b, 0x68, 0xe5, 0x57, 0x9e, 0x74, 0x0f, 0x20, 0xb2, 0xbe, 0x41, 0x3f, - 0x8f, 0xfe, 0xf5, 0x34, 0x3e, 0xc2, 0xff, 0xd5, 0xdb, 0x4f, 0x4e, 0x72, 0xec, 0xad, 0xaf, 0xa2, - 0xf0, 0x50, 0xbd, 0xd7, 0x36, 0x34, 0x66, 0xc8, 0x09, 0x29, 0x91, 0x73, 0xc6, 0x93, 0x4a, 0xac, - 0xc0, 0x7b, 0x65, 0xbc, 0x77, 0x0b, 0xbe, 0xdc, 0xfe, 0x29, 0x18, 0xb2, 0x63, 0x45, 0x81, 0xe5, - 0x06, 0x16, 0xae, 0x2a, 0x0d, 0x78, 0xff, 0xea, 0xe2, 0x45, 0x10, 0xf2, 0xcd, 0x76, 0x69, 0x5f, - 0xd2, 0x6b, 0x87, 0xc6, 0xeb, 0x88, 0xee, 0x1d, 0xf1, 0x79, 0x13, 0x50, 0x47, 0x31, 0x6e, 0x86, - 0xec, 0xb2, 0x29, 0x57, 0x93, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x7f, 0xcc, 0x9c, 0x5b, 0x94, - 0x05, 0x00, 0x00, + // 823 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x56, 0x6d, 0x8f, 0xdb, 0x44, + 0x10, 0x26, 0xc9, 0x39, 0xf8, 0xc6, 0x49, 0x49, 0x97, 0x5c, 0x89, 0x12, 0x15, 0x2a, 0xa3, 0x43, + 0xe5, 0xa0, 0x89, 0xf0, 0xdd, 0x07, 0x84, 0x40, 0x70, 0x90, 0x5c, 0x1a, 0xd5, 0x6d, 0x23, 0x9b, + 0x20, 0x51, 0x90, 0xa2, 0xbd, 0x66, 0x92, 0x58, 0xf5, 0x79, 0x83, 0x77, 0x73, 0x87, 0xff, 0x1f, + 0x3f, 0x86, 0x9f, 0x81, 0xbc, 0xeb, 0xd7, 0x5c, 0x28, 0x54, 0xaa, 0x10, 0x5f, 0x92, 0xdd, 0x99, + 0x79, 0x66, 0x9e, 0xd9, 0x79, 0x91, 0xa1, 0xe1, 0xe3, 0x62, 0x85, 0x61, 0x7f, 0x13, 0x32, 0xc1, + 0x48, 0x5d, 0xdd, 0xba, 0xbd, 0x15, 0x63, 0x2b, 0x1f, 0x07, 0x52, 0x7a, 0xb9, 0x5d, 0x0e, 0xf0, + 0x6a, 0x23, 0x22, 0x65, 0x64, 0xf6, 0x40, 0x73, 0x05, 0x15, 0x48, 0x08, 0x1c, 0xac, 0x29, 0x5f, + 0x77, 0x2a, 0x0f, 0x2a, 0x0f, 0x1b, 0x8e, 0x3c, 0x9b, 0xa7, 0xf0, 0xee, 0x13, 0x8c, 0xa6, 0x34, + 0x14, 0xb1, 0x5a, 0x44, 0x1b, 0x94, 0xea, 0xa6, 0x23, 0xcf, 0xa4, 0x0d, 0xda, 0x35, 0xf5, 0xb7, + 0xd8, 0xa9, 0x4a, 0x8c, 0xba, 0x98, 0x9f, 0x43, 0xed, 0x09, 0x46, 0xe4, 0x18, 0xb4, 0x0d, 0x0d, + 0x05, 0xef, 0x54, 0x1e, 0xd4, 0x1e, 0x1a, 0xd6, 0x7b, 0xfd, 0x84, 0x5b, 0xe2, 0xd0, 0x51, 0x5a, + 0xd3, 0x02, 0xed, 0xa7, 0x18, 0x16, 0x07, 0x58, 0x50, 0x41, 0xd3, 0xf8, 0xf1, 0x99, 0x1c, 0x41, + 0xdd, 0xe3, 0xf3, 0xc0, 0xf3, 0x65, 0x04, 0xdd, 0xd1, 0x3c, 0xfe, 0xcc, 0xf3, 0xcd, 0x53, 0x68, + 0x48, 0xce, 0x0e, 0xfe, 0xb6, 0x45, 0x2e, 0xc8, 0xc7, 0xa0, 0xf1, 0xf8, 0x2e, 0xb1, 0x86, 0xd5, + 0x4c, 0x43, 0x29, 0x23, 0xa5, 0x33, 0xcf, 0xa0, 0x99, 0x80, 0xf8, 0x86, 0x05, 0x1c, 0xff, 0x1d, + 0x6a, 0x00, 0xad, 0xc7, 0x94, 0x97, 0x81, 0x3d, 0x38, 0x5c, 0x53, 0x3e, 0xcf, 0xc1, 0xba, 0xa3, + 0xaf, 0x13, 0x23, 0xf3, 0x17, 0x38, 0x1a, 0xa3, 0x70, 0xbd, 0x60, 0xe5, 0xa3, 0x4c, 0xec, 0x4d, + 0x48, 0x92, 0xfb, 0x50, 0x7b, 0x85, 0x91, 0xcc, 0xd6, 0xb0, 0x8c, 0xc2, 0x93, 0x39, 0xb1, 0x3c, + 0xce, 0x21, 0xf1, 0x99, 0xe7, 0xa0, 0x2a, 0xb0, 0xe3, 0x54, 0x59, 0x25, 0x05, 0x71, 0x00, 0xc6, + 0x28, 0xde, 0x88, 0xc7, 0x47, 0x70, 0xf0, 0x0a, 0x23, 0xde, 0xa9, 0xca, 0xda, 0x95, 0x88, 0x48, + 0x85, 0x79, 0x06, 0x86, 0xf4, 0x99, 0xf0, 0x38, 0x86, 0xba, 0x8c, 0x95, 0x56, 0x7b, 0x87, 0x48, + 0xa2, 0x34, 0x23, 0x00, 0xf7, 0x2d, 0x33, 0x29, 0x84, 0xae, 0xbd, 0x2e, 0xf4, 0x0b, 0x30, 0xdc, + 0x02, 0xe1, 0x13, 0x38, 0x0c, 0xf0, 0x66, 0xfe, 0x9a, 0xf8, 0x7a, 0x80, 0x37, 0x6e, 0x42, 0xc1, + 0x10, 0xa1, 0x87, 0xf3, 0xed, 0x66, 0x11, 0x5b, 0xab, 0x66, 0x87, 0x58, 0x34, 0x93, 0x12, 0xf3, + 0x47, 0x68, 0x4c, 0x43, 0x76, 0x8d, 0x6f, 0xf7, 0x89, 0x8f, 0xa1, 0x39, 0x0d, 0x19, 0x5b, 0x66, + 0x9c, 0xdb, 0xa0, 0x6d, 0x62, 0x41, 0x32, 0x22, 0xea, 0x62, 0x7e, 0x0d, 0xc4, 0xc5, 0xf0, 0x1a, + 0xc3, 0x49, 0xb0, 0x64, 0x99, 0xed, 0x27, 0x70, 0x70, 0xc5, 0x16, 0x8a, 0xc1, 0x1d, 0x8b, 0xa4, + 0xde, 0x6d, 0xf9, 0xf7, 0x94, 0x2d, 0xd0, 0x91, 0x7a, 0xf3, 0x43, 0xd0, 0x6d, 0xa4, 0xcb, 0xc7, + 0x94, 0xaf, 0xf7, 0x6e, 0x80, 0x73, 0x68, 0xa5, 0xfa, 0xcc, 0xf7, 0x23, 0x38, 0xf4, 0x91, 0x2e, + 0xe7, 0x99, 0xb1, 0x61, 0xb5, 0xf2, 0x00, 0x89, 0xb1, 0xee, 0x27, 0x27, 0x73, 0x0c, 0x24, 0x95, + 0x22, 0xcf, 0x9c, 0x7c, 0x01, 0x46, 0xe6, 0x24, 0x6b, 0x9b, 0xdb, 0x6e, 0xc0, 0xcf, 0xa0, 0xe6, + 0x89, 0x9c, 0xc5, 0x29, 0x15, 0xeb, 0xdc, 0xcd, 0x3d, 0xa8, 0xe3, 0xef, 0x1e, 0x4f, 0xd6, 0x8c, + 0xee, 0x24, 0xb7, 0x93, 0x5f, 0x01, 0xf2, 0x5c, 0x49, 0x0f, 0x3e, 0xb0, 0x47, 0xc3, 0xf1, 0xc8, + 0x99, 0x3f, 0x7d, 0x3e, 0x1c, 0xcd, 0x67, 0xcf, 0xdc, 0xe9, 0xe8, 0x87, 0xc9, 0xc5, 0x64, 0x34, + 0x6c, 0xbd, 0x43, 0xda, 0xd0, 0x2a, 0x2a, 0x2f, 0x66, 0xb6, 0xdd, 0xaa, 0xec, 0x42, 0xa6, 0xe7, + 0x3f, 0xdb, 0xcf, 0xcf, 0x87, 0xf6, 0xc8, 0x75, 0x5b, 0x55, 0xeb, 0xcf, 0x2a, 0x34, 0x95, 0xfb, + 0xf8, 0xe9, 0xbd, 0x97, 0x48, 0xbe, 0x81, 0xc6, 0x24, 0xf0, 0x84, 0x47, 0x7d, 0xd5, 0x33, 0xf7, + 0xfa, 0x6a, 0xe9, 0xf6, 0xd3, 0xa5, 0xdb, 0x1f, 0xc5, 0x4b, 0xb7, 0x7b, 0x54, 0xee, 0x85, 0x34, + 0x8d, 0xaf, 0x40, 0x4f, 0xd7, 0x0c, 0x69, 0xef, 0x98, 0xc8, 0x9e, 0xea, 0x76, 0x52, 0xe9, 0xad, + 0x75, 0x74, 0x01, 0x77, 0xca, 0x1b, 0x87, 0xdc, 0x4f, 0x6d, 0xf7, 0x6e, 0xa2, 0x9c, 0x43, 0x79, + 0x97, 0xf4, 0xa1, 0x36, 0x46, 0x41, 0x48, 0x01, 0x9c, 0x22, 0xde, 0x2f, 0xc9, 0x72, 0x7b, 0xb7, + 0x68, 0xef, 0xee, 0xb1, 0x2f, 0x8e, 0xdc, 0x19, 0x68, 0x72, 0x4a, 0xf2, 0x04, 0x8b, 0x43, 0x93, + 0xb3, 0x2a, 0x35, 0xbd, 0x35, 0x83, 0xbb, 0xea, 0xa5, 0xe3, 0xf6, 0x4e, 0x5f, 0xfb, 0xbb, 0x78, + 0x8f, 0xa4, 0x3d, 0xff, 0xb7, 0x6f, 0xdd, 0xcd, 0x59, 0xec, 0xce, 0x87, 0xf5, 0x47, 0x0d, 0x3a, + 0x53, 0x1a, 0xf9, 0x8c, 0x2e, 0x7c, 0xe4, 0xfc, 0x7f, 0x53, 0xcc, 0x2f, 0x25, 0x56, 0xf6, 0xf8, + 0xde, 0x4a, 0x14, 0x91, 0xe5, 0x49, 0xb0, 0xe1, 0x6e, 0x56, 0xee, 0x6c, 0xa4, 0xff, 0xa1, 0x13, + 0x3a, 0xb7, 0xe6, 0x2d, 0xf5, 0xf6, 0x2d, 0x34, 0xc7, 0x28, 0xf2, 0xb9, 0xdd, 0x4b, 0xa6, 0xbb, + 0x0b, 0x2f, 0xcc, 0xf7, 0x7f, 0xd2, 0x1d, 0xdf, 0x7f, 0xf6, 0xe2, 0xd3, 0x95, 0x27, 0xd6, 0xdb, + 0xcb, 0xfe, 0x4b, 0x76, 0x35, 0x60, 0xc1, 0xd2, 0x67, 0x37, 0x83, 0xf8, 0xe7, 0xd1, 0x8a, 0x0d, + 0x14, 0x22, 0xfb, 0xec, 0xb9, 0xac, 0xcb, 0xd3, 0xe9, 0x5f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x94, + 0x98, 0xb3, 0x08, 0x26, 0x09, 0x00, 0x00, } diff --git a/ledger/protobuf/ledger.proto b/ledger/protobuf/ledger.proto index de24e694b65..dfee8b52dad 100644 --- a/ledger/protobuf/ledger.proto +++ b/ledger/protobuf/ledger.proto @@ -116,3 +116,81 @@ message ProofResponse { bytes proof = 1; // Encoded Proof (opaque to gRPC) } +// LedgerMode reports the operating mode of the ledger server. +enum LedgerMode { + LEDGER_MODE_UNSPECIFIED = 0; + LEDGER_MODE_FULL = 1; + LEDGER_MODE_PAYLOADLESS = 2; +} + +// ServerInfoResponse reports server metadata such as the operating mode. +message ServerInfoResponse { + LedgerMode mode = 1; +} + +// LedgerInfoService reports mode and other metadata about the ledger server. +// It is registered unconditionally on every ledger gRPC server regardless of +// whether the process is running in full or payloadless mode. Clients call +// ServerInfo at startup to verify they are talking to a server in the +// expected mode. +service LedgerInfoService { + // ServerInfo returns metadata about this ledger server, including its + // operating mode. + rpc ServerInfo(google.protobuf.Empty) returns (ServerInfoResponse); +} + +// LeafHash is a 32-byte HashLeaf(path, value). +// An empty `hash` (length 0) indicates the path is unallocated. +message LeafHash { + bytes hash = 1; +} + +// LeafHashResponse contains a single leaf hash. +message LeafHashResponse { + LeafHash leaf_hash = 1; +} + +// LeafHashesResponse contains a slice of leaf hashes, one per input key, +// in the same order as the request. +message LeafHashesResponse { + repeated LeafHash leaf_hashes = 1; +} + +// HasPathsResponse reports, for each input key, whether the key has an +// allocated register at the requested state. +message HasPathsResponse { + repeated bool exists = 1; +} + +// PayloadlessLedgerService provides remote access to a payloadless ledger. +// Unlike LedgerService, reads return leaf hashes (HashLeaf(path, value)) +// rather than payload values. A server registers either LedgerService or +// PayloadlessLedgerService at startup, never both. +service PayloadlessLedgerService { + // InitialState returns the initial state of the ledger. + rpc InitialState(google.protobuf.Empty) returns (StateResponse); + + // HasState checks if the given state exists in the ledger. + rpc HasState(StateRequest) returns (HasStateResponse); + + // HasPaths reports, for each input key, whether the key has an allocated + // register at the requested state. + rpc HasPaths(GetRequest) returns (HasPathsResponse); + + // GetSingleLeafHash returns the leaf hash for a single key at a specific + // state. An empty hash indicates the path is unallocated. + rpc GetSingleLeafHash(GetSingleValueRequest) returns (LeafHashResponse); + + // GetLeafHashes returns leaf hashes for multiple keys at a specific state. + rpc GetLeafHashes(GetRequest) returns (LeafHashesResponse); + + // Set updates keys with new values at a specific state and returns the + // new state. The server discards the keys after hashing; only the values + // contribute to the trie. + rpc Set(SetRequest) returns (SetResponse); + + // Prove returns a payloadless batch proof for the given keys at a + // specific state. Proofs carry leaf hashes rather than payload values. + rpc Prove(ProveRequest) returns (ProofResponse); +} + diff --git a/ledger/protobuf/ledger_grpc.pb.go b/ledger/protobuf/ledger_grpc.pb.go index 9563796331c..98d80a96dcd 100644 --- a/ledger/protobuf/ledger_grpc.pb.go +++ b/ledger/protobuf/ledger_grpc.pb.go @@ -305,3 +305,434 @@ var LedgerService_ServiceDesc = grpc.ServiceDesc{ Streams: []grpc.StreamDesc{}, Metadata: "ledger.proto", } + +const ( + LedgerInfoService_ServerInfo_FullMethodName = "/ledger.LedgerInfoService/ServerInfo" +) + +// LedgerInfoServiceClient is the client API for LedgerInfoService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type LedgerInfoServiceClient interface { + // ServerInfo returns metadata about this ledger server, including its + // operating mode. + ServerInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ServerInfoResponse, error) +} + +type ledgerInfoServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewLedgerInfoServiceClient(cc grpc.ClientConnInterface) LedgerInfoServiceClient { + return &ledgerInfoServiceClient{cc} +} + +func (c *ledgerInfoServiceClient) ServerInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ServerInfoResponse, error) { + out := new(ServerInfoResponse) + err := c.cc.Invoke(ctx, LedgerInfoService_ServerInfo_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// LedgerInfoServiceServer is the server API for LedgerInfoService service. +// All implementations must embed UnimplementedLedgerInfoServiceServer +// for forward compatibility +type LedgerInfoServiceServer interface { + // ServerInfo returns metadata about this ledger server, including its + // operating mode. + ServerInfo(context.Context, *emptypb.Empty) (*ServerInfoResponse, error) + mustEmbedUnimplementedLedgerInfoServiceServer() +} + +// UnimplementedLedgerInfoServiceServer must be embedded to have forward compatible implementations. +type UnimplementedLedgerInfoServiceServer struct { +} + +func (UnimplementedLedgerInfoServiceServer) ServerInfo(context.Context, *emptypb.Empty) (*ServerInfoResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ServerInfo not implemented") +} +func (UnimplementedLedgerInfoServiceServer) mustEmbedUnimplementedLedgerInfoServiceServer() {} + +// UnsafeLedgerInfoServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to LedgerInfoServiceServer will +// result in compilation errors. +type UnsafeLedgerInfoServiceServer interface { + mustEmbedUnimplementedLedgerInfoServiceServer() +} + +func RegisterLedgerInfoServiceServer(s grpc.ServiceRegistrar, srv LedgerInfoServiceServer) { + s.RegisterService(&LedgerInfoService_ServiceDesc, srv) +} + +func _LedgerInfoService_ServerInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LedgerInfoServiceServer).ServerInfo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: LedgerInfoService_ServerInfo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LedgerInfoServiceServer).ServerInfo(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +// LedgerInfoService_ServiceDesc is the grpc.ServiceDesc for LedgerInfoService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var LedgerInfoService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ledger.LedgerInfoService", + HandlerType: (*LedgerInfoServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ServerInfo", + Handler: _LedgerInfoService_ServerInfo_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ledger.proto", +} + +const ( + PayloadlessLedgerService_InitialState_FullMethodName = "/ledger.PayloadlessLedgerService/InitialState" + PayloadlessLedgerService_HasState_FullMethodName = "/ledger.PayloadlessLedgerService/HasState" + PayloadlessLedgerService_HasPaths_FullMethodName = "/ledger.PayloadlessLedgerService/HasPaths" + PayloadlessLedgerService_GetSingleLeafHash_FullMethodName = "/ledger.PayloadlessLedgerService/GetSingleLeafHash" + PayloadlessLedgerService_GetLeafHashes_FullMethodName = "/ledger.PayloadlessLedgerService/GetLeafHashes" + PayloadlessLedgerService_Set_FullMethodName = "/ledger.PayloadlessLedgerService/Set" + PayloadlessLedgerService_Prove_FullMethodName = "/ledger.PayloadlessLedgerService/Prove" +) + +// PayloadlessLedgerServiceClient is the client API for PayloadlessLedgerService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type PayloadlessLedgerServiceClient interface { + // InitialState returns the initial state of the ledger. + InitialState(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StateResponse, error) + // HasState checks if the given state exists in the ledger. + HasState(ctx context.Context, in *StateRequest, opts ...grpc.CallOption) (*HasStateResponse, error) + // HasPaths reports, for each input key, whether the key has an allocated + // register at the requested state. + HasPaths(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*HasPathsResponse, error) + // GetSingleLeafHash returns the leaf hash for a single key at a specific + // state. An empty hash indicates the path is unallocated. + GetSingleLeafHash(ctx context.Context, in *GetSingleValueRequest, opts ...grpc.CallOption) (*LeafHashResponse, error) + // GetLeafHashes returns leaf hashes for multiple keys at a specific state. + GetLeafHashes(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*LeafHashesResponse, error) + // Set updates keys with new values at a specific state and returns the + // new state. The server discards the keys after hashing; only the values + // contribute to the trie. + Set(ctx context.Context, in *SetRequest, opts ...grpc.CallOption) (*SetResponse, error) + // Prove returns a payloadless batch proof for the given keys at a + // specific state. Proofs carry leaf hashes rather than payload values. + Prove(ctx context.Context, in *ProveRequest, opts ...grpc.CallOption) (*ProofResponse, error) +} + +type payloadlessLedgerServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewPayloadlessLedgerServiceClient(cc grpc.ClientConnInterface) PayloadlessLedgerServiceClient { + return &payloadlessLedgerServiceClient{cc} +} + +func (c *payloadlessLedgerServiceClient) InitialState(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StateResponse, error) { + out := new(StateResponse) + err := c.cc.Invoke(ctx, PayloadlessLedgerService_InitialState_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *payloadlessLedgerServiceClient) HasState(ctx context.Context, in *StateRequest, opts ...grpc.CallOption) (*HasStateResponse, error) { + out := new(HasStateResponse) + err := c.cc.Invoke(ctx, PayloadlessLedgerService_HasState_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *payloadlessLedgerServiceClient) HasPaths(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*HasPathsResponse, error) { + out := new(HasPathsResponse) + err := c.cc.Invoke(ctx, PayloadlessLedgerService_HasPaths_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *payloadlessLedgerServiceClient) GetSingleLeafHash(ctx context.Context, in *GetSingleValueRequest, opts ...grpc.CallOption) (*LeafHashResponse, error) { + out := new(LeafHashResponse) + err := c.cc.Invoke(ctx, PayloadlessLedgerService_GetSingleLeafHash_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *payloadlessLedgerServiceClient) GetLeafHashes(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*LeafHashesResponse, error) { + out := new(LeafHashesResponse) + err := c.cc.Invoke(ctx, PayloadlessLedgerService_GetLeafHashes_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *payloadlessLedgerServiceClient) Set(ctx context.Context, in *SetRequest, opts ...grpc.CallOption) (*SetResponse, error) { + out := new(SetResponse) + err := c.cc.Invoke(ctx, PayloadlessLedgerService_Set_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *payloadlessLedgerServiceClient) Prove(ctx context.Context, in *ProveRequest, opts ...grpc.CallOption) (*ProofResponse, error) { + out := new(ProofResponse) + err := c.cc.Invoke(ctx, PayloadlessLedgerService_Prove_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// PayloadlessLedgerServiceServer is the server API for PayloadlessLedgerService service. +// All implementations must embed UnimplementedPayloadlessLedgerServiceServer +// for forward compatibility +type PayloadlessLedgerServiceServer interface { + // InitialState returns the initial state of the ledger. + InitialState(context.Context, *emptypb.Empty) (*StateResponse, error) + // HasState checks if the given state exists in the ledger. + HasState(context.Context, *StateRequest) (*HasStateResponse, error) + // HasPaths reports, for each input key, whether the key has an allocated + // register at the requested state. + HasPaths(context.Context, *GetRequest) (*HasPathsResponse, error) + // GetSingleLeafHash returns the leaf hash for a single key at a specific + // state. An empty hash indicates the path is unallocated. + GetSingleLeafHash(context.Context, *GetSingleValueRequest) (*LeafHashResponse, error) + // GetLeafHashes returns leaf hashes for multiple keys at a specific state. + GetLeafHashes(context.Context, *GetRequest) (*LeafHashesResponse, error) + // Set updates keys with new values at a specific state and returns the + // new state. The server discards the keys after hashing; only the values + // contribute to the trie. + Set(context.Context, *SetRequest) (*SetResponse, error) + // Prove returns a payloadless batch proof for the given keys at a + // specific state. Proofs carry leaf hashes rather than payload values. + Prove(context.Context, *ProveRequest) (*ProofResponse, error) + mustEmbedUnimplementedPayloadlessLedgerServiceServer() +} + +// UnimplementedPayloadlessLedgerServiceServer must be embedded to have forward compatible implementations. +type UnimplementedPayloadlessLedgerServiceServer struct { +} + +func (UnimplementedPayloadlessLedgerServiceServer) InitialState(context.Context, *emptypb.Empty) (*StateResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method InitialState not implemented") +} +func (UnimplementedPayloadlessLedgerServiceServer) HasState(context.Context, *StateRequest) (*HasStateResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method HasState not implemented") +} +func (UnimplementedPayloadlessLedgerServiceServer) HasPaths(context.Context, *GetRequest) (*HasPathsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method HasPaths not implemented") +} +func (UnimplementedPayloadlessLedgerServiceServer) GetSingleLeafHash(context.Context, *GetSingleValueRequest) (*LeafHashResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetSingleLeafHash not implemented") +} +func (UnimplementedPayloadlessLedgerServiceServer) GetLeafHashes(context.Context, *GetRequest) (*LeafHashesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetLeafHashes not implemented") +} +func (UnimplementedPayloadlessLedgerServiceServer) Set(context.Context, *SetRequest) (*SetResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Set not implemented") +} +func (UnimplementedPayloadlessLedgerServiceServer) Prove(context.Context, *ProveRequest) (*ProofResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Prove not implemented") +} +func (UnimplementedPayloadlessLedgerServiceServer) mustEmbedUnimplementedPayloadlessLedgerServiceServer() { +} + +// UnsafePayloadlessLedgerServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to PayloadlessLedgerServiceServer will +// result in compilation errors. +type UnsafePayloadlessLedgerServiceServer interface { + mustEmbedUnimplementedPayloadlessLedgerServiceServer() +} + +func RegisterPayloadlessLedgerServiceServer(s grpc.ServiceRegistrar, srv PayloadlessLedgerServiceServer) { + s.RegisterService(&PayloadlessLedgerService_ServiceDesc, srv) +} + +func _PayloadlessLedgerService_InitialState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PayloadlessLedgerServiceServer).InitialState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PayloadlessLedgerService_InitialState_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PayloadlessLedgerServiceServer).InitialState(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _PayloadlessLedgerService_HasState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(StateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PayloadlessLedgerServiceServer).HasState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PayloadlessLedgerService_HasState_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PayloadlessLedgerServiceServer).HasState(ctx, req.(*StateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PayloadlessLedgerService_HasPaths_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PayloadlessLedgerServiceServer).HasPaths(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PayloadlessLedgerService_HasPaths_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PayloadlessLedgerServiceServer).HasPaths(ctx, req.(*GetRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PayloadlessLedgerService_GetSingleLeafHash_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetSingleValueRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PayloadlessLedgerServiceServer).GetSingleLeafHash(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PayloadlessLedgerService_GetSingleLeafHash_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PayloadlessLedgerServiceServer).GetSingleLeafHash(ctx, req.(*GetSingleValueRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PayloadlessLedgerService_GetLeafHashes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PayloadlessLedgerServiceServer).GetLeafHashes(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PayloadlessLedgerService_GetLeafHashes_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PayloadlessLedgerServiceServer).GetLeafHashes(ctx, req.(*GetRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PayloadlessLedgerService_Set_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PayloadlessLedgerServiceServer).Set(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PayloadlessLedgerService_Set_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PayloadlessLedgerServiceServer).Set(ctx, req.(*SetRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PayloadlessLedgerService_Prove_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ProveRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PayloadlessLedgerServiceServer).Prove(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PayloadlessLedgerService_Prove_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PayloadlessLedgerServiceServer).Prove(ctx, req.(*ProveRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// PayloadlessLedgerService_ServiceDesc is the grpc.ServiceDesc for PayloadlessLedgerService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var PayloadlessLedgerService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ledger.PayloadlessLedgerService", + HandlerType: (*PayloadlessLedgerServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "InitialState", + Handler: _PayloadlessLedgerService_InitialState_Handler, + }, + { + MethodName: "HasState", + Handler: _PayloadlessLedgerService_HasState_Handler, + }, + { + MethodName: "HasPaths", + Handler: _PayloadlessLedgerService_HasPaths_Handler, + }, + { + MethodName: "GetSingleLeafHash", + Handler: _PayloadlessLedgerService_GetSingleLeafHash_Handler, + }, + { + MethodName: "GetLeafHashes", + Handler: _PayloadlessLedgerService_GetLeafHashes_Handler, + }, + { + MethodName: "Set", + Handler: _PayloadlessLedgerService_Set_Handler, + }, + { + MethodName: "Prove", + Handler: _PayloadlessLedgerService_Prove_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ledger.proto", +} diff --git a/ledger/remote/client.go b/ledger/remote/client.go index 9f2119bf677..a7ed8a45200 100644 --- a/ledger/remote/client.go +++ b/ledger/remote/client.go @@ -9,7 +9,9 @@ import ( "github.com/rs/zerolog" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" "github.com/onflow/flow-go/ledger" @@ -20,6 +22,7 @@ import ( type Client struct { conn *grpc.ClientConn client ledgerpb.LedgerServiceClient + infoClient ledgerpb.LedgerInfoServiceClient logger zerolog.Logger done chan struct{} once sync.Once @@ -82,71 +85,17 @@ func NewClient(grpcAddr string, logger zerolog.Logger, opts ...ClientOption) (*C opt(cfg) } - // Handle Unix domain socket addresses - // gRPC client accepts "unix:///absolute/path" or "unix://relative/path" format - // For convenience, if an absolute path is provided (starts with /), automatically add the unix:// prefix - if strings.HasPrefix(grpcAddr, "/") { - grpcAddr = "unix://" + grpcAddr - logger.Debug().Str("address", grpcAddr).Msg("using Unix domain socket (auto-prefixed)") - } else if strings.HasPrefix(grpcAddr, "unix://") { - logger.Debug().Str("address", grpcAddr).Msg("using Unix domain socket") - } - - // Create gRPC connection with max message size configuration. - // Default to 1 GiB (instead of standard 4 MiB) to handle large proofs that can exceed 4MB. - // This was increased to fix "grpc: received message larger than max" errors when generating - // proofs for blocks with many state changes. - // Retry connection with exponential backoff until the service becomes available. - // After approximately 40 minutes of retrying (90 attempts), the client will give up and crash. - var conn *grpc.ClientConn - retryDelay := 100 * time.Millisecond - maxRetryDelay := 30 * time.Second - maxRetries := 90 // ~40 minutes total wait time with exponential backoff capped at 30s - - for attempt := 0; ; attempt++ { - var err error - conn, err = grpc.NewClient( - grpcAddr, - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithDefaultCallOptions( - grpc.MaxCallRecvMsgSize(int(cfg.maxResponseSize)), - grpc.MaxCallSendMsgSize(int(cfg.maxRequestSize)), - ), - ) - if err == nil { - logger.Info().Str("address", grpcAddr).Msg("successfully connected to ledger service") - break - } - - if attempt >= maxRetries { - logger.Fatal(). - Err(err). - Int("attempts", attempt). - Str("address", grpcAddr). - Msg("failed to connect to ledger service after maximum retries, crashing node") - } - - logger.Warn(). - Err(err). - Int("attempt", attempt+1). - Int("max_attempts", maxRetries). - Dur("retry_delay", retryDelay). - Time("retry_at", time.Now().Add(retryDelay)). - Str("address", grpcAddr). - Msg("failed to connect to ledger service, retrying...") - - time.Sleep(retryDelay) - // Exponential backoff with max cap - retryDelay = min(maxRetryDelay, time.Duration(float64(retryDelay)*1.5)) - } + conn := dialLedgerServer(grpcAddr, cfg, logger) client := ledgerpb.NewLedgerServiceClient(conn) + infoClient := ledgerpb.NewLedgerInfoServiceClient(conn) ctx, cancel := context.WithCancel(context.Background()) return &Client{ conn: conn, client: client, + infoClient: infoClient, logger: logger, done: make(chan struct{}), ctx: ctx, @@ -194,7 +143,14 @@ func (c *Client) InitialState() ledger.State { } // HasState returns true if the given state exists in the ledger. -func (c *Client) HasState(state ledger.State) bool { +// +// A gRPC failure is surfaced to the caller as an error (an exception) rather +// than collapsed into a false return: false must mean "state genuinely +// absent", not "the server was unreachable", otherwise callers (e.g. execution +// state) would misreport a reachable state as pruned. +// +// No error returns are expected during normal operation. +func (c *Client) HasState(state ledger.State) (bool, error) { ctx, cancel := c.callCtx() defer cancel() req := &ledgerpb.StateRequest{ @@ -205,11 +161,10 @@ func (c *Client) HasState(state ledger.State) bool { resp, err := c.client.HasState(ctx, req) if err != nil { - c.logger.Error().Err(err).Msg("failed to check state") - return false + return false, fmt.Errorf("failed to check state: %w", err) } - return resp.HasState + return resp.HasState, nil } // GetSingleValue returns a single value for a given key at a specific state. @@ -372,51 +327,199 @@ func (c *Client) Prove(query *ledger.Query) (ledger.Proof, error) { } // Ready returns a channel that is closed when the client is ready. -// For a remote client, this waits for the ledger service to be ready by -// calling InitialState() with retries to ensure the service has finished initialization. +// +// Readiness runs the two-phase handshake in [awaitReady]: it first verifies +// the server is running in FULL mode (crashing via log.Fatal on a mismatch — +// clients of a payloadless server must use [PayloadlessClient], not [Client]), +// then waits for the ledger service to finish initialization (WAL replay). func (c *Client) Ready() <-chan struct{} { ready := make(chan struct{}) go func() { defer close(ready) - // Wait for the ledger service to be ready by calling InitialState() - // This ensures the service has finished WAL replay and is ready to serve requests - // Retry with exponential backoff (delay capped at 30s) - maxRetries := 30 - retryDelay := 100 * time.Millisecond - maxRetryDelay := 30 * time.Second - - for i := 0; i < maxRetries; i++ { - ctx, cancel := c.callCtx() - _, err := c.client.InitialState(ctx, &emptypb.Empty{}) - cancel() - if err == nil { - c.logger.Info().Msg("ledger service ready") - return - } + awaitReady( + c.ctx, + c.infoClient, + func(ctx context.Context) error { + _, err := c.client.InitialState(ctx, &emptypb.Empty{}) + return err + }, + ledgerpb.LedgerMode_LEDGER_MODE_FULL, + c.callTimeout, + "ledger", + c.logger, + ) + }() + return ready +} - // Check if the client context was cancelled (shutdown in progress) - if c.ctx.Err() != nil { - c.logger.Info().Msg("client shutdown during ready check") - return - } +// awaitReady runs the two-phase startup handshake shared by [Client] and +// [PayloadlessClient]. The caller is responsible for closing its ready channel +// once this returns. +// +// Phase 1 — mode gate: [verifyServerMode] probes LedgerInfoService.ServerInfo +// and crashes via log.Fatal if the server does not report `expectedMode`. +// ServerInfo is stateless and registered on every server regardless of mode, +// so it is reachable as soon as the server accepts connections — before WAL +// replay finishes. Probing it first lets us surface a wrong-mode +// misconfiguration with a clear error before depending on any mode-specific +// RPC (a mode-specific InitialState against a wrong-mode server returns gRPC +// Unimplemented, which is indistinguishable from "still initializing"). +// +// Phase 2 — readiness gate: `initialState` (the caller's mode-specific +// InitialState RPC) is retried until the server finishes initialization. If it +// never succeeds, this proceeds anyway rather than blocking forever; the first +// real RPC will surface a clearer error if the service is truly unavailable. +// +// `serviceName` is used only in log messages (e.g. "ledger", "payloadless +// ledger"). +func awaitReady( + ctx context.Context, + infoClient ledgerpb.LedgerInfoServiceClient, + initialState func(context.Context) error, + expectedMode ledgerpb.LedgerMode, + callTimeout time.Duration, + serviceName string, + logger zerolog.Logger, +) { + // Phase 1: mode gate. + verifyServerMode(ctx, infoClient, callTimeout, expectedMode, logger) + if ctx.Err() != nil { + logger.Info().Msg("client shutdown during ready check") + return + } + + // Phase 2: readiness gate. + err := retryUntilReady(ctx, initialState, callTimeout, logger, serviceName+" service not ready, retrying...") + if err != nil { + if ctx.Err() != nil { + logger.Info().Msg("client shutdown during ready check") + return + } + // Close the channel anyway to avoid blocking forever; the first real + // RPC will surface a more specific error if the service is truly down. + logger.Warn().Err(err).Msgf("%s service not ready after retries, proceeding anyway", serviceName) + return + } + logger.Info().Msgf("%s service ready", serviceName) +} - if i < maxRetries-1 { - c.logger.Warn(). - Err(err). - Int("attempt", i+1). - Dur("retry_delay", retryDelay). - Time("retry_at", time.Now().Add(retryDelay)). - Msg("ledger service not ready, retrying...") - time.Sleep(retryDelay) - retryDelay = min(time.Duration(float64(retryDelay)*1.5), maxRetryDelay) - } else { - c.logger.Warn().Err(err).Msg("ledger service not ready after retries, proceeding anyway") - // Still close the channel to avoid blocking forever - // The execution node will fail later with a more specific error if the service is truly not ready +// retryUntilReady repeatedly invokes `attempt` (each call bounded by +// `callTimeout`) until it returns nil, `ctx` is cancelled, or the retry budget +// is exhausted. Delays grow geometrically (1.5x), capped at 30s. It returns the +// final error (nil on success, `ctx.Err()` if the context was cancelled). +// +// `notReadyMsg` is logged before each retry. +func retryUntilReady( + ctx context.Context, + attempt func(context.Context) error, + callTimeout time.Duration, + logger zerolog.Logger, + notReadyMsg string, +) error { + maxRetries := 30 + retryDelay := 100 * time.Millisecond + maxRetryDelay := 30 * time.Second + + var err error + for i := 0; i < maxRetries; i++ { + callCtx, cancel := context.WithTimeout(ctx, callTimeout) + err = attempt(callCtx) + cancel() + if err == nil { + return nil + } + if ctx.Err() != nil { + return ctx.Err() + } + if i < maxRetries-1 { + logger.Warn(). + Err(err). + Int("attempt", i+1). + Dur("retry_delay", retryDelay). + Msg(notReadyMsg) + time.Sleep(retryDelay) + retryDelay = min(time.Duration(float64(retryDelay)*1.5), maxRetryDelay) + } + } + return err +} + +// verifyServerMode probes LedgerInfoService.ServerInfo (retrying on transient +// transport failures) and crashes the process via log.Fatal if the server's +// reported mode does not match `expected`. +// +// A mode mismatch is a configuration error (the deployment paired a client of +// one mode with a server of the other); it is not retryable and not safe to +// ignore — every subsequent RPC against a wrong-mode server would either return +// gRPC Unimplemented or, worse, succeed against a method that happens to share +// its name but returns incompatibly-typed data. +// +// `expected` should be either FULL or PAYLOADLESS; UNSPECIFIED is treated as +// "client is misconfigured" and also crashes. +// +// Two non-mismatch outcomes are logged and allowed to proceed rather than +// crashing: +// - the server does not implement the info service (gRPC Unimplemented), +// e.g. an older server mid-rolling-upgrade — retrying cannot help, so this +// returns immediately; and +// - ServerInfo stays unreachable across all retries — the connection may be +// transiently flaky, and the next real RPC will surface a clearer error. +// +// Crashing in either case would prevent the client from recovering from a brief +// network blip or an upgrade-ordering race during startup. +func verifyServerMode( + ctx context.Context, + infoClient ledgerpb.LedgerInfoServiceClient, + callTimeout time.Duration, + expected ledgerpb.LedgerMode, + logger zerolog.Logger, +) { + maxRetries := 30 + retryDelay := 100 * time.Millisecond + maxRetryDelay := 30 * time.Second + + for i := 0; i < maxRetries; i++ { + infoCtx, cancel := context.WithTimeout(ctx, callTimeout) + resp, err := infoClient.ServerInfo(infoCtx, &emptypb.Empty{}) + cancel() + + if err == nil { + if resp.Mode != expected { + logger.Fatal(). + Str("expected", expected.String()). + Str("actual", resp.Mode.String()). + Msg("ledger server mode mismatch: client connected to wrong-mode server") } + logger.Info().Str("mode", resp.Mode.String()).Msg("ledger server mode verified") + return } - }() - return ready + + // An older or misconfigured server that does not register the info + // service returns Unimplemented; that will not change without a + // restart, so stop retrying and let the mode-specific RPCs surface any + // real problem. + if status.Code(err) == codes.Unimplemented { + logger.Warn().Err(err).Msg("ledger info service not implemented by server; skipping mode check") + return + } + + // Check if the client context was cancelled (shutdown in progress). + if ctx.Err() != nil { + return + } + + if i < maxRetries-1 { + logger.Warn(). + Err(err). + Int("attempt", i+1). + Dur("retry_delay", retryDelay). + Msg("ledger info service not reachable, retrying mode check...") + time.Sleep(retryDelay) + retryDelay = min(time.Duration(float64(retryDelay)*1.5), maxRetryDelay) + } else { + logger.Warn().Err(err).Msg("ledger info service not reachable after retries; skipping mode check") + } + } } // Done returns a channel that is closed when the client is done. @@ -465,3 +568,64 @@ func ledgerKeyToProtoKey(key ledger.Key) *ledgerpb.Key { Parts: parts, } } + +// dialLedgerServer establishes a gRPC connection to a ledger server with +// retry. `grpcAddr` may be a TCP address (e.g. "localhost:9000") or a Unix +// domain socket (either "unix:///path" or just "/path" — the prefix is +// auto-added for the latter). +// +// Retries with exponential backoff (capped at 30s) for up to ~40 minutes. If +// the server does not become reachable in that window, the process exits +// via log.Fatal. +// +// Used by both [NewClient] and [NewPayloadlessClient]; the two share the +// same dial behavior because the underlying gRPC server is the same in both +// modes — only the registered services differ. +func dialLedgerServer(grpcAddr string, cfg *clientConfig, logger zerolog.Logger) *grpc.ClientConn { + if strings.HasPrefix(grpcAddr, "/") { + grpcAddr = "unix://" + grpcAddr + logger.Debug().Str("address", grpcAddr).Msg("using Unix domain socket (auto-prefixed)") + } else if strings.HasPrefix(grpcAddr, "unix://") { + logger.Debug().Str("address", grpcAddr).Msg("using Unix domain socket") + } + + // Default to 1 GiB (instead of standard 4 MiB) to handle large proofs. + retryDelay := 100 * time.Millisecond + maxRetryDelay := 30 * time.Second + maxRetries := 90 // ~40 minutes total wait + + for attempt := 0; ; attempt++ { + conn, err := grpc.NewClient( + grpcAddr, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithDefaultCallOptions( + grpc.MaxCallRecvMsgSize(int(cfg.maxResponseSize)), + grpc.MaxCallSendMsgSize(int(cfg.maxRequestSize)), + ), + ) + if err == nil { + logger.Info().Str("address", grpcAddr).Msg("successfully connected to ledger service") + return conn + } + + if attempt >= maxRetries { + logger.Fatal(). + Err(err). + Int("attempts", attempt). + Str("address", grpcAddr). + Msg("failed to connect to ledger service after maximum retries, crashing node") + } + + logger.Warn(). + Err(err). + Int("attempt", attempt+1). + Int("max_attempts", maxRetries). + Dur("retry_delay", retryDelay). + Time("retry_at", time.Now().Add(retryDelay)). + Str("address", grpcAddr). + Msg("failed to connect to ledger service, retrying...") + + time.Sleep(retryDelay) + retryDelay = min(maxRetryDelay, time.Duration(float64(retryDelay)*1.5)) + } +} diff --git a/ledger/remote/info_service.go b/ledger/remote/info_service.go new file mode 100644 index 00000000000..5373c937db1 --- /dev/null +++ b/ledger/remote/info_service.go @@ -0,0 +1,36 @@ +package remote + +import ( + "context" + + "google.golang.org/protobuf/types/known/emptypb" + + ledgerpb "github.com/onflow/flow-go/ledger/protobuf" +) + +// InfoService implements the gRPC LedgerInfoService interface. It is +// registered on every ledger gRPC server, regardless of the server's mode, +// so clients can discover the mode of the server they connected to before +// issuing mode-specific RPCs. +// +// InfoService is stateless and concurrency-safe. +type InfoService struct { + ledgerpb.UnimplementedLedgerInfoServiceServer + mode ledgerpb.LedgerMode +} + +// NewInfoService creates a new info service that reports the given mode. +// Callers MUST pass either [ledgerpb.LedgerMode_LEDGER_MODE_FULL] or +// [ledgerpb.LedgerMode_LEDGER_MODE_PAYLOADLESS]; passing UNSPECIFIED produces +// a server that reports UNSPECIFIED, which clients will treat as a +// misconfigured server and refuse to use. +func NewInfoService(mode ledgerpb.LedgerMode) *InfoService { + return &InfoService{mode: mode} +} + +// ServerInfo returns the server's operating mode. +// +// No error returns are expected during normal operation. +func (s *InfoService) ServerInfo(_ context.Context, _ *emptypb.Empty) (*ledgerpb.ServerInfoResponse, error) { + return &ledgerpb.ServerInfoResponse{Mode: s.mode}, nil +} diff --git a/ledger/remote/payloadless_client.go b/ledger/remote/payloadless_client.go new file mode 100644 index 00000000000..0905f93f1ab --- /dev/null +++ b/ledger/remote/payloadless_client.go @@ -0,0 +1,349 @@ +package remote + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/rs/zerolog" + "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/emptypb" + + "github.com/onflow/flow-go/ledger" + "github.com/onflow/flow-go/ledger/common/hash" + ledgerpb "github.com/onflow/flow-go/ledger/protobuf" +) + +// PayloadlessClient is a gRPC client for a payloadless ledger server. Reads +// return leaf hashes (HashLeaf(path, value)) rather than payload values. +// +// PayloadlessClient mirrors [Client] in dial / retry / lifecycle behavior; +// the difference is the registered service it talks to and the leaf-hash +// return types on the read methods. Both clients share the same dial helper +// and the same mode-discovery check in Ready(). +type PayloadlessClient struct { + conn *grpc.ClientConn + client ledgerpb.PayloadlessLedgerServiceClient + infoClient ledgerpb.LedgerInfoServiceClient + logger zerolog.Logger + done chan struct{} + once sync.Once + ctx context.Context + cancel context.CancelFunc + callTimeout time.Duration +} + +// NewPayloadlessClient creates a new payloadless remote ledger client. +// +// `grpcAddr` accepts the same forms as [NewClient]. Connection-establishment +// retries are identical: ~40 minutes of exponential backoff before +// log.Fatal. Mode verification happens in [Ready] (not here) — a wrong-mode +// server will be detected the first time the caller waits on Ready(). +func NewPayloadlessClient(grpcAddr string, logger zerolog.Logger, opts ...ClientOption) (*PayloadlessClient, error) { + logger = logger.With().Str("component", "remote_payloadless_ledger_client").Logger() + + cfg := defaultClientConfig() + for _, opt := range opts { + opt(cfg) + } + + conn := dialLedgerServer(grpcAddr, cfg, logger) + + ctx, cancel := context.WithCancel(context.Background()) + + return &PayloadlessClient{ + conn: conn, + client: ledgerpb.NewPayloadlessLedgerServiceClient(conn), + infoClient: ledgerpb.NewLedgerInfoServiceClient(conn), + logger: logger, + done: make(chan struct{}), + ctx: ctx, + cancel: cancel, + callTimeout: cfg.callTimeout, + }, nil +} + +// Close closes the gRPC connection. +func (c *PayloadlessClient) Close() error { + if c.conn != nil { + err := c.conn.Close() + c.conn = nil + return err + } + return nil +} + +// callCtx returns a context for gRPC calls with the configured timeout, +// derived from the client's lifecycle context so cancellations propagate. +func (c *PayloadlessClient) callCtx() (context.Context, context.CancelFunc) { + return context.WithTimeout(c.ctx, c.callTimeout) +} + +// InitialState returns the initial state of the payloadless ledger. +func (c *PayloadlessClient) InitialState() ledger.State { + ctx, cancel := c.callCtx() + defer cancel() + resp, err := c.client.InitialState(ctx, &emptypb.Empty{}) + if err != nil { + c.logger.Fatal().Err(err).Msg("failed to get initial state") + return ledger.DummyState + } + + var state ledger.State + if len(resp.State.Hash) != len(state) { + c.logger.Fatal(). + Int("expected", len(state)). + Int("got", len(resp.State.Hash)). + Msg("invalid state hash length") + return ledger.DummyState + } + copy(state[:], resp.State.Hash) + return state +} + +// HasState returns true if the given state exists in the payloadless ledger. +// +// A gRPC failure is surfaced to the caller rather than collapsed into a false +// return: false must mean "state genuinely absent", not "the server was +// unreachable", otherwise callers (e.g. execution state) would misreport a +// reachable state as pruned. +// +// No error returns are expected during normal operation. +func (c *PayloadlessClient) HasState(state ledger.State) (bool, error) { + ctx, cancel := c.callCtx() + defer cancel() + req := &ledgerpb.StateRequest{State: &ledgerpb.State{Hash: state[:]}} + + resp, err := c.client.HasState(ctx, req) + if err != nil { + return false, fmt.Errorf("failed to check state: %w", err) + } + return resp.HasState, nil +} + +// HasPaths reports, for each key in `query.Keys()`, whether the key has an +// allocated register at `query.State()`. +// +// Expected error returns during normal operation: +// - generic error wrapping the underlying gRPC failure when the call fails. +func (c *PayloadlessClient) HasPaths(query *ledger.Query) ([]bool, error) { + ctx, cancel := c.callCtx() + defer cancel() + state := query.State() + req := &ledgerpb.GetRequest{ + State: &ledgerpb.State{Hash: state[:]}, + Keys: make([]*ledgerpb.Key, len(query.Keys())), + } + for i, key := range query.Keys() { + req.Keys[i] = ledgerKeyToProtoKey(key) + } + + resp, err := c.client.HasPaths(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to check has paths: %w", err) + } + return resp.Exists, nil +} + +// GetSingleLeafHash returns the leaf hash for a single key at a specific +// state. Returns nil if the key has no allocated register. +// +// Expected error returns during normal operation: +// - generic error wrapping the underlying gRPC failure when the call fails. +func (c *PayloadlessClient) GetSingleLeafHash(query *ledger.QuerySingleValue) (*hash.Hash, error) { + ctx, cancel := c.callCtx() + defer cancel() + state := query.State() + req := &ledgerpb.GetSingleValueRequest{ + State: &ledgerpb.State{Hash: state[:]}, + Key: ledgerKeyToProtoKey(query.Key()), + } + + resp, err := c.client.GetSingleLeafHash(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get single leaf hash: %w", err) + } + + return decodeProtoLeafHash(resp.LeafHash) +} + +// GetLeafHashes returns leaf hashes for multiple keys at a specific state. +// A nil entry in the returned slice indicates an unallocated register. +// +// Expected error returns during normal operation: +// - generic error wrapping the underlying gRPC failure when the call fails. +func (c *PayloadlessClient) GetLeafHashes(query *ledger.Query) ([]*hash.Hash, error) { + ctx, cancel := c.callCtx() + defer cancel() + state := query.State() + req := &ledgerpb.GetRequest{ + State: &ledgerpb.State{Hash: state[:]}, + Keys: make([]*ledgerpb.Key, len(query.Keys())), + } + for i, key := range query.Keys() { + req.Keys[i] = ledgerKeyToProtoKey(key) + } + + resp, err := c.client.GetLeafHashes(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get leaf hashes: %w", err) + } + + leafHashes := make([]*hash.Hash, len(resp.LeafHashes)) + for i, protoLH := range resp.LeafHashes { + lh, err := decodeProtoLeafHash(protoLH) + if err != nil { + return nil, fmt.Errorf("failed to decode leaf hash at index %d: %w", i, err) + } + leafHashes[i] = lh + } + return leafHashes, nil +} + +// Set updates keys with new values at a specific state and returns the new +// state plus the trie update that was applied. +// +// Expected error returns during normal operation: +// - generic error wrapping the underlying gRPC failure when the call fails, +// or when the response is malformed. +func (c *PayloadlessClient) Set(update *ledger.Update) (ledger.State, *ledger.TrieUpdate, error) { + // Empty updates short-circuit, matching the behavior of the local ledger. + if update.Size() == 0 { + return update.State(), + &ledger.TrieUpdate{ + RootHash: ledger.RootHash(update.State()), + Paths: []ledger.Path{}, + Payloads: []*ledger.Payload{}, + }, + nil + } + + ctx, cancel := c.callCtx() + defer cancel() + state := update.State() + req := &ledgerpb.SetRequest{ + State: &ledgerpb.State{Hash: state[:]}, + Keys: make([]*ledgerpb.Key, len(update.Keys())), + Values: make([]*ledgerpb.Value, len(update.Values())), + } + + for i, key := range update.Keys() { + req.Keys[i] = ledgerKeyToProtoKey(key) + } + for i, value := range update.Values() { + req.Values[i] = &ledgerpb.Value{ + Data: value, + IsNil: value == nil, + } + } + + resp, err := c.client.Set(ctx, req) + if err != nil { + return ledger.DummyState, nil, fmt.Errorf("failed to set values: %w", err) + } + + if resp == nil || resp.NewState == nil { + return ledger.DummyState, nil, fmt.Errorf("invalid response: missing new state") + } + + var newState ledger.State + if len(resp.NewState.Hash) != len(newState) { + return ledger.DummyState, nil, fmt.Errorf("invalid new state hash length") + } + copy(newState[:], resp.NewState.Hash) + + trieUpdate, err := decodeTrieUpdateFromTransport(resp.TrieUpdate) + if err != nil { + return ledger.DummyState, nil, fmt.Errorf("failed to decode trie update: %w", err) + } + + return newState, trieUpdate, nil +} + +// Prove returns a payloadless batch proof for the given keys at a specific +// state. The proof is decoded with [ledger.DecodePayloadlessTrieBatchProof]. +// +// Expected error returns during normal operation: +// - generic error wrapping the underlying gRPC failure or a decode failure. +func (c *PayloadlessClient) Prove(query *ledger.Query) (*ledger.PayloadlessTrieBatchProof, error) { + ctx, cancel := c.callCtx() + defer cancel() + state := query.State() + req := &ledgerpb.ProveRequest{ + State: &ledgerpb.State{Hash: state[:]}, + Keys: make([]*ledgerpb.Key, len(query.Keys())), + } + for i, key := range query.Keys() { + req.Keys[i] = ledgerKeyToProtoKey(key) + } + + resp, err := c.client.Prove(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to generate proof: %w", err) + } + + bp, err := ledger.DecodePayloadlessTrieBatchProof(resp.Proof) + if err != nil { + return nil, fmt.Errorf("failed to decode payloadless batch proof: %w", err) + } + return bp, nil +} + +// Ready returns a channel that is closed when the client is ready. +// +// Readiness runs the two-phase handshake in [awaitReady]: it first verifies +// the server is running in PAYLOADLESS mode (crashing via log.Fatal on a +// mismatch — clients of a full server must use [Client], not +// [PayloadlessClient]), then waits for the ledger service to finish +// initialization (WAL replay). +func (c *PayloadlessClient) Ready() <-chan struct{} { + ready := make(chan struct{}) + go func() { + defer close(ready) + awaitReady( + c.ctx, + c.infoClient, + func(ctx context.Context) error { + _, err := c.client.InitialState(ctx, &emptypb.Empty{}) + return err + }, + ledgerpb.LedgerMode_LEDGER_MODE_PAYLOADLESS, + c.callTimeout, + "payloadless ledger", + c.logger, + ) + }() + return ready +} + +// Done returns a channel that is closed when the client is done. Idempotent. +func (c *PayloadlessClient) Done() <-chan struct{} { + c.once.Do(func() { + go func() { + defer close(c.done) + c.cancel() + if err := c.Close(); err != nil { + c.logger.Error().Err(err).Msg("error closing gRPC connection") + } + }() + }) + return c.done +} + +// decodeProtoLeafHash converts a proto LeafHash to a *hash.Hash. An empty +// `hash` field (length 0) represents an unallocated register and returns nil. +// +// Expected error returns during normal operation: +// - generic error when the hash field has an unexpected (non-zero, non-HashLen) length. +func decodeProtoLeafHash(protoLH *ledgerpb.LeafHash) (*hash.Hash, error) { + if protoLH == nil || len(protoLH.Hash) == 0 { + return nil, nil + } + if len(protoLH.Hash) != hash.HashLen { + return nil, fmt.Errorf("invalid leaf hash length: got %d, want %d", len(protoLH.Hash), hash.HashLen) + } + var h hash.Hash + copy(h[:], protoLH.Hash) + return &h, nil +} diff --git a/ledger/remote/payloadless_service.go b/ledger/remote/payloadless_service.go new file mode 100644 index 00000000000..1004b13e918 --- /dev/null +++ b/ledger/remote/payloadless_service.go @@ -0,0 +1,286 @@ +package remote + +import ( + "context" + + "github.com/rs/zerolog" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + + "github.com/onflow/flow-go/ledger" + ledgerpb "github.com/onflow/flow-go/ledger/protobuf" +) + +// PayloadlessService implements the gRPC PayloadlessLedgerService interface +// on top of a [ledger.PayloadlessLedger]. Reads return leaf hashes rather +// than payload values. +// +// A ledger gRPC server registers either [Service] (full mode) or +// [PayloadlessService] (payloadless mode), never both. The mode is chosen at +// startup config. +type PayloadlessService struct { + ledgerpb.UnimplementedPayloadlessLedgerServiceServer + ledger ledger.PayloadlessLedger + logger zerolog.Logger +} + +// NewPayloadlessService creates a new payloadless ledger gRPC service. +// In production the ledger argument is a *complete.PayloadlessLedger; tests +// may pass any value that satisfies [ledger.PayloadlessLedger]. +func NewPayloadlessService(l ledger.PayloadlessLedger, logger zerolog.Logger) *PayloadlessService { + return &PayloadlessService{ + ledger: l, + logger: logger, + } +} + +// InitialState returns the initial state of the payloadless ledger. +// +// No error returns are expected during normal operation. +func (s *PayloadlessService) InitialState(_ context.Context, _ *emptypb.Empty) (*ledgerpb.StateResponse, error) { + state := s.ledger.InitialState() + return &ledgerpb.StateResponse{ + State: &ledgerpb.State{Hash: state[:]}, + }, nil +} + +// HasState checks if the given state exists in the payloadless ledger. +// +// Expected error returns during normal operation: +// - gRPC InvalidArgument: when `req.State` is nil or has the wrong length. +func (s *PayloadlessService) HasState(_ context.Context, req *ledgerpb.StateRequest) (*ledgerpb.HasStateResponse, error) { + if req.State == nil || len(req.State.Hash) != len(ledger.State{}) { + return nil, status.Error(codes.InvalidArgument, "invalid state") + } + var state ledger.State + copy(state[:], req.State.Hash) + hasState, err := s.ledger.HasState(state) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to check state: %v", err) + } + return &ledgerpb.HasStateResponse{HasState: hasState}, nil +} + +// HasPaths reports, for each key in `req.Keys`, whether the key has an +// allocated register at `req.State`. +// +// Expected error returns during normal operation: +// - gRPC InvalidArgument: when `req.State` is nil or has the wrong length, +// or when `req.Keys` is empty. +func (s *PayloadlessService) HasPaths(_ context.Context, req *ledgerpb.GetRequest) (*ledgerpb.HasPathsResponse, error) { + if req.State == nil || len(req.State.Hash) != len(ledger.State{}) { + return nil, status.Error(codes.InvalidArgument, "invalid state") + } + if len(req.Keys) == 0 { + return nil, status.Error(codes.InvalidArgument, "keys cannot be empty") + } + + var state ledger.State + copy(state[:], req.State.Hash) + + keys, err := protoKeysToLedgerKeys(req.Keys) + if err != nil { + return nil, err + } + + query, err := ledger.NewQuery(state, keys) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + exists, err := s.ledger.HasPaths(query) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + return &ledgerpb.HasPathsResponse{Exists: exists}, nil +} + +// GetSingleLeafHash returns the leaf hash for a single key. An unallocated +// register is reported as an empty `hash` field. +// +// Expected error returns during normal operation: +// - gRPC InvalidArgument: when `req.State` is nil or has the wrong length, +// or when `req.Key` is nil. +func (s *PayloadlessService) GetSingleLeafHash(_ context.Context, req *ledgerpb.GetSingleValueRequest) (*ledgerpb.LeafHashResponse, error) { + if req.State == nil || len(req.State.Hash) != len(ledger.State{}) { + return nil, status.Error(codes.InvalidArgument, "invalid state") + } + + var state ledger.State + copy(state[:], req.State.Hash) + + key, err := protoKeyToLedgerKey(req.Key) + if err != nil { + return nil, err + } + + query, err := ledger.NewQuerySingleValue(state, key) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + leafHash, err := s.ledger.GetSingleLeafHash(query) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + resp := &ledgerpb.LeafHashResponse{LeafHash: &ledgerpb.LeafHash{}} + if leafHash != nil { + resp.LeafHash.Hash = leafHash[:] + } + return resp, nil +} + +// GetLeafHashes returns leaf hashes for multiple keys. Unallocated registers +// are reported as `LeafHash` entries with empty `hash` fields. +// +// Expected error returns during normal operation: +// - gRPC InvalidArgument: when `req.State` is nil or has the wrong length, +// or when `req.Keys` is empty. +func (s *PayloadlessService) GetLeafHashes(_ context.Context, req *ledgerpb.GetRequest) (*ledgerpb.LeafHashesResponse, error) { + if req.State == nil || len(req.State.Hash) != len(ledger.State{}) { + return nil, status.Error(codes.InvalidArgument, "invalid state") + } + if len(req.Keys) == 0 { + return nil, status.Error(codes.InvalidArgument, "keys cannot be empty") + } + + var state ledger.State + copy(state[:], req.State.Hash) + + keys, err := protoKeysToLedgerKeys(req.Keys) + if err != nil { + return nil, err + } + + query, err := ledger.NewQuery(state, keys) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + leafHashes, err := s.ledger.GetLeafHashes(query) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + protoHashes := make([]*ledgerpb.LeafHash, len(leafHashes)) + for i, lh := range leafHashes { + entry := &ledgerpb.LeafHash{} + if lh != nil { + entry.Hash = lh[:] + } + protoHashes[i] = entry + } + + return &ledgerpb.LeafHashesResponse{LeafHashes: protoHashes}, nil +} + +// Set updates keys with new values at a specific state and returns the new +// state. The server discards the keys after hashing; only the values +// contribute to the trie. +// +// Expected error returns during normal operation: +// - gRPC InvalidArgument: when `req.State` is nil/wrong length, keys are +// empty, or keys/values lengths mismatch. +func (s *PayloadlessService) Set(_ context.Context, req *ledgerpb.SetRequest) (*ledgerpb.SetResponse, error) { + if req.State == nil || len(req.State.Hash) != len(ledger.State{}) { + return nil, status.Error(codes.InvalidArgument, "invalid state") + } + if len(req.Keys) == 0 { + return nil, status.Error(codes.InvalidArgument, "keys cannot be empty") + } + if len(req.Keys) != len(req.Values) { + return nil, status.Error(codes.InvalidArgument, "keys and values length mismatch") + } + + var state ledger.State + copy(state[:], req.State.Hash) + + keys, err := protoKeysToLedgerKeys(req.Keys) + if err != nil { + return nil, err + } + + values := make([]ledger.Value, len(req.Values)) + for i, protoValue := range req.Values { + if len(protoValue.Data) == 0 { + if protoValue.IsNil { + values[i] = nil + } else { + values[i] = ledger.Value([]byte{}) + } + } else { + values[i] = ledger.Value(protoValue.Data) + } + } + + update, err := ledger.NewUpdate(state, keys, values) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + newState, trieUpdate, err := s.ledger.Set(update) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + trieUpdateBytes := encodeTrieUpdateForTransport(trieUpdate) + + return &ledgerpb.SetResponse{ + NewState: &ledgerpb.State{Hash: newState[:]}, + TrieUpdate: trieUpdateBytes, + }, nil +} + +// Prove returns a payloadless batch proof for the given keys at a specific +// state. The proof is encoded with [ledger.EncodePayloadlessTrieBatchProof]. +// +// Expected error returns during normal operation: +// - gRPC InvalidArgument: when `req.State` is nil/wrong length or `req.Keys` +// is empty. +func (s *PayloadlessService) Prove(_ context.Context, req *ledgerpb.ProveRequest) (*ledgerpb.ProofResponse, error) { + if req.State == nil || len(req.State.Hash) != len(ledger.State{}) { + return nil, status.Error(codes.InvalidArgument, "invalid state") + } + if len(req.Keys) == 0 { + return nil, status.Error(codes.InvalidArgument, "keys cannot be empty") + } + + var state ledger.State + copy(state[:], req.State.Hash) + + keys, err := protoKeysToLedgerKeys(req.Keys) + if err != nil { + return nil, err + } + + query, err := ledger.NewQuery(state, keys) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + batchProof, err := s.ledger.Prove(query) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + return &ledgerpb.ProofResponse{ + Proof: ledger.EncodePayloadlessTrieBatchProof(batchProof), + }, nil +} + +// protoKeysToLedgerKeys decodes a slice of proto keys via protoKeyToLedgerKey. +// Returns the first conversion error (already wrapped as a gRPC status). +func protoKeysToLedgerKeys(protoKeys []*ledgerpb.Key) ([]ledger.Key, error) { + keys := make([]ledger.Key, len(protoKeys)) + for i, pk := range protoKeys { + k, err := protoKeyToLedgerKey(pk) + if err != nil { + return nil, err + } + keys[i] = k + } + return keys, nil +} diff --git a/ledger/remote/service.go b/ledger/remote/service.go index b98bdb245a4..8265cf0b360 100644 --- a/ledger/remote/service.go +++ b/ledger/remote/service.go @@ -46,7 +46,10 @@ func (s *Service) HasState(ctx context.Context, req *ledgerpb.StateRequest) (*le var state ledger.State copy(state[:], req.State.Hash) - hasState := s.ledger.HasState(state) + hasState, err := s.ledger.HasState(state) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to check state: %v", err) + } return &ledgerpb.HasStateResponse{ HasState: hasState, }, nil diff --git a/ledger/trie_encoder.go b/ledger/trie_encoder.go index d7bc6f98438..cc328025359 100644 --- a/ledger/trie_encoder.go +++ b/ledger/trie_encoder.go @@ -20,10 +20,12 @@ const ( // CAUTION: if payload key encoding is changed, convertEncodedPayloadKey() // must be modified to convert encoded payload key from one version to // another version. - PayloadVersion = uint16(1) - TrieUpdateVersion = uint16(0) // Use payload version 0 encoding - TrieProofVersion = uint16(0) // Use payload version 0 encoding - TrieBatchProofVersion = uint16(0) // Use payload version 0 encoding + PayloadVersion = uint16(1) + TrieUpdateVersion = uint16(0) // Use payload version 0 encoding + TrieProofVersion = uint16(0) // Use payload version 0 encoding + TrieBatchProofVersion = uint16(0) // Use payload version 0 encoding + PayloadlessTrieProofVersion = uint16(0) + PayloadlessTrieBatchProofVersion = uint16(0) ) // Type capture the type of encoded entity (e.g. State, Key, Value, Path) @@ -55,12 +57,17 @@ const ( TypeUpdate // TypeTrieUpdate - type for trie update TypeTrieUpdate + // TypePayloadlessProof - type for payloadless trie proofs + // (leaf-hash-bearing proofs produced by payloadless tries) + TypePayloadlessProof + // TypePayloadlessBatchProof - type for payloadless trie batch proofs + TypePayloadlessBatchProof // this is used to flag types from the future typeUnsuported ) func (e Type) String() string { - return [...]string{"Unknown", "State", "KeyPart", "Key", "Value", "Path", "Payload", "Proof", "BatchProof", "Query", "Update", "Trie Update"}[e] + return [...]string{"Unknown", "State", "KeyPart", "Key", "Value", "Path", "Payload", "Proof", "BatchProof", "Query", "Update", "Trie Update", "Payloadless Proof", "Payloadless Batch Proof"}[e] } // CheckVersion extracts encoding bytes from a raw encoded message @@ -975,3 +982,247 @@ func decodeTrieBatchProof(inp []byte, version uint16) (*TrieBatchProof, error) { } return bp, nil } + +// EncodePayloadlessTrieProof encodes the content of a payloadless proof into a byte slice. +// +// The encoding mirrors [EncodeTrieProof] with one substitution: instead of an +// embedded payload, a leaf-hash field is encoded as a single presence byte +// (1 = present, 0 = nil) followed by [hash.HashLen] bytes of leaf hash when +// present. +func EncodePayloadlessTrieProof(p *PayloadlessTrieProof) []byte { + if p == nil { + return []byte{} + } + buffer := utils.AppendUint16([]byte{}, PayloadlessTrieProofVersion) + buffer = utils.AppendUint8(buffer, TypePayloadlessProof) + buffer = append(buffer, encodePayloadlessTrieProof(p)...) + return buffer +} + +func encodePayloadlessTrieProof(p *PayloadlessTrieProof) []byte { + // first byte is reserved for inclusion flag + buffer := make([]byte, 1) + if p.Inclusion { + buffer[0] |= 1 << 7 + } + + // steps + buffer = utils.AppendUint8(buffer, p.Steps) + + // flags size and content + buffer = utils.AppendUint8(buffer, uint8(len(p.Flags))) + buffer = append(buffer, p.Flags...) + + // path size and content + buffer = utils.AppendUint16(buffer, uint16(PathLen)) + buffer = append(buffer, p.Path[:]...) + + // leaf hash: 1 presence byte + (optional) HashLen bytes + if p.LeafHash != nil { + buffer = utils.AppendUint8(buffer, 1) + buffer = append(buffer, p.LeafHash[:]...) + } else { + buffer = utils.AppendUint8(buffer, 0) + } + + // interims + buffer = utils.AppendUint8(buffer, uint8(len(p.Interims))) + for _, inter := range p.Interims { + buffer = utils.AppendUint16(buffer, uint16(len(inter))) + buffer = append(buffer, inter[:]...) + } + + return buffer +} + +// DecodePayloadlessTrieProof constructs a payloadless proof from an encoded +// byte slice produced by [EncodePayloadlessTrieProof]. +// +// Expected error returns during normal operation: +// - generic error wrapping a sentinel from the codec when the encoded version +// is unsupported or the byte slice is truncated/malformed. +func DecodePayloadlessTrieProof(encodedProof []byte) (*PayloadlessTrieProof, error) { + rest, _, err := CheckVersion(encodedProof, PayloadlessTrieProofVersion) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless proof: %w", err) + } + rest, err = CheckType(rest, TypePayloadlessProof) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless proof: %w", err) + } + return decodePayloadlessTrieProof(rest) +} + +func decodePayloadlessTrieProof(inp []byte) (*PayloadlessTrieProof, error) { + pInst := NewPayloadlessTrieProof() + + // inclusion flag + byteInclusion, rest, err := utils.ReadSlice(inp, 1) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless proof: %w", err) + } + pInst.Inclusion = bitutils.ReadBit(byteInclusion, 0) == 1 + + // steps + steps, rest, err := utils.ReadUint8(rest) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless proof: %w", err) + } + pInst.Steps = steps + + // flags + flagsSize, rest, err := utils.ReadUint8(rest) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless proof: %w", err) + } + flags, rest, err := utils.ReadSlice(rest, int(flagsSize)) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless proof: %w", err) + } + pInst.Flags = flags + + // path + pathSize, rest, err := utils.ReadUint16(rest) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless proof: %w", err) + } + pathBytes, rest, err := utils.ReadSlice(rest, int(pathSize)) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless proof: %w", err) + } + pInst.Path, err = ToPath(pathBytes) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless proof: %w", err) + } + + // leaf hash presence + (optional) HashLen bytes + present, rest, err := utils.ReadUint8(rest) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless proof: %w", err) + } + if present == 1 { + hashBytes, restAfterHash, err := utils.ReadSlice(rest, hash.HashLen) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless proof: %w", err) + } + lh, err := hash.ToHash(hashBytes) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless proof: %w", err) + } + pInst.LeafHash = &lh + rest = restAfterHash + } + + // interims + interimsLen, rest, err := utils.ReadUint8(rest) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless proof: %w", err) + } + interims := make([]hash.Hash, interimsLen) + var interimSize uint16 + var interim hash.Hash + var interimBytes []byte + for i := 0; i < int(interimsLen); i++ { + interimSize, rest, err = utils.ReadUint16(rest) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless proof: %w", err) + } + interimBytes, rest, err = utils.ReadSlice(rest, int(interimSize)) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless proof: %w", err) + } + interim, err = hash.ToHash(interimBytes) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless proof: %w", err) + } + interims[i] = interim + } + pInst.Interims = interims + + // Reject trailing bytes: a well-formed single proof consumes its entire + // input. Leftover bytes indicate a malformed or tampered encoding (proofs + // may originate from untrusted remote peers). This also guards the batch + // decoder, which hands each length-prefixed sub-proof slice to this function. + if len(rest) != 0 { + return nil, fmt.Errorf("error decoding payloadless proof: %d unexpected trailing bytes", len(rest)) + } + + return pInst, nil +} + +// EncodePayloadlessTrieBatchProof encodes a payloadless batch proof into a +// byte slice. The format mirrors [EncodeTrieBatchProof]. +func EncodePayloadlessTrieBatchProof(bp *PayloadlessTrieBatchProof) []byte { + if bp == nil { + return []byte{} + } + buffer := utils.AppendUint16([]byte{}, PayloadlessTrieBatchProofVersion) + buffer = utils.AppendUint8(buffer, TypePayloadlessBatchProof) + buffer = append(buffer, encodePayloadlessTrieBatchProof(bp)...) + return buffer +} + +func encodePayloadlessTrieBatchProof(bp *PayloadlessTrieBatchProof) []byte { + buffer := make([]byte, 0) + buffer = utils.AppendUint32(buffer, uint32(len(bp.Proofs))) + for _, p := range bp.Proofs { + encP := encodePayloadlessTrieProof(p) + buffer = utils.AppendUint64(buffer, uint64(len(encP))) + buffer = append(buffer, encP...) + } + return buffer +} + +// DecodePayloadlessTrieBatchProof constructs a payloadless batch proof from +// an encoded byte slice produced by [EncodePayloadlessTrieBatchProof]. +// +// Expected error returns during normal operation: +// - generic error wrapping a sentinel from the codec when the encoded version +// is unsupported or the byte slice is truncated/malformed. +func DecodePayloadlessTrieBatchProof(encodedBatchProof []byte) (*PayloadlessTrieBatchProof, error) { + rest, _, err := CheckVersion(encodedBatchProof, PayloadlessTrieBatchProofVersion) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless batch proof: %w", err) + } + rest, err = CheckType(rest, TypePayloadlessBatchProof) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless batch proof: %w", err) + } + bp, err := decodePayloadlessTrieBatchProof(rest) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless batch proof: %w", err) + } + return bp, nil +} + +func decodePayloadlessTrieBatchProof(inp []byte) (*PayloadlessTrieBatchProof, error) { + bp := NewPayloadlessTrieBatchProof() + numOfProofs, rest, err := utils.ReadUint32(inp) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless batch proof (content): %w", err) + } + for i := 0; i < int(numOfProofs); i++ { + var encProofSize uint64 + var encProof []byte + encProofSize, rest, err = utils.ReadUint64(rest) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless batch proof (content): %w", err) + } + encProof, rest, err = utils.ReadSlice(rest, int(encProofSize)) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless batch proof (content): %w", err) + } + proof, err := decodePayloadlessTrieProof(encProof) + if err != nil { + return nil, fmt.Errorf("error decoding payloadless batch proof (content): %w", err) + } + bp.Proofs = append(bp.Proofs, proof) + } + // Reject trailing bytes: the input must be fully consumed once the declared + // number of sub-proofs has been read. Leftover bytes indicate a malformed or + // tampered encoding (proofs may originate from untrusted remote peers). + if len(rest) != 0 { + return nil, fmt.Errorf("error decoding payloadless batch proof (content): %d unexpected trailing bytes", len(rest)) + } + return bp, nil +}