Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
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
1 change: 1 addition & 0 deletions ledger/payloadless_ledger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package ledger

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

empty file

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