Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/ledger/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion engine/execution/state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
8 changes: 6 additions & 2 deletions engine/execution/state/state_storehouse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}))
}

Expand Down
8 changes: 6 additions & 2 deletions engine/execution/state/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 4 additions & 2 deletions ledger/complete/ledger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions ledger/complete/payloadless_ledger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 11 additions & 3 deletions ledger/complete/payloadless_ledger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}

Expand Down Expand Up @@ -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
Expand Down
12 changes: 8 additions & 4 deletions ledger/factory/factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand Down
54 changes: 53 additions & 1 deletion ledger/ledger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

includes a breaking change: HasState now returns a (bool, error), instead of bool. This means this version would require the ledger service to be restarted with compatible version. Be careful during HCU


// GetSingleValue returns value for a given key at specific state
GetSingleValue(query *QuerySingleValue) (value Value, err error)
Expand All @@ -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
Expand Down
19 changes: 14 additions & 5 deletions ledger/mock/ledger.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions ledger/partial/ledger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions ledger/payloadless_proof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
Loading