Skip to content

Commit a66eeaa

Browse files
committed
refactor reconstructPayloadlessProof
1 parent 8cf9ad5 commit a66eeaa

2 files changed

Lines changed: 241 additions & 218 deletions

File tree

ledger/complete/payloadless/proof.go

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ var ErrPayloadHashMismatch = errors.New("payload hash mismatch: storehouse value
2222
// - (nil, error) for any other errors
2323
type RegisterValueReader func(registerID flow.RegisterID) (flow.RegisterValue, error)
2424

25+
// registerTarget pairs a register ID with its corresponding ledger key. The
26+
// register ID drives value lookup via [RegisterValueReader]; the ledger key is
27+
// used to build the reconstructed payload. Callers that have already converted
28+
// register IDs to keys (e.g. to derive trie paths) can stash the keys here to
29+
// avoid a second [convert.RegisterIDToLedgerKey] call per leaf.
30+
type registerTarget struct {
31+
registerID flow.RegisterID
32+
key ledger.Key
33+
}
34+
2535
// ProveAndReconstruct generates a reconstructed full batch proof for the
2636
// given register IDs using a payloadless ledger and a value source. The
2737
// returned bytes encode a *ledger.TrieBatchProof — wire-compatible with the
@@ -31,13 +41,28 @@ type RegisterValueReader func(registerID flow.RegisterID) (flow.RegisterValue, e
3141
// The flow:
3242
// 1. Convert register IDs to ledger keys and derive their paths via
3343
// pathfinder.KeysToPaths.
34-
// 2. Build a path → registerID map so ReconstructPayloadlessProof can
35-
// recover the register ID for each leaf in the (path-sorted) proof.
44+
// 2. Build a path → (registerID, key) map so reconstructPayloadlessProof
45+
// can recover the register ID for each leaf in the (path-sorted) proof
46+
// and reuse the already-allocated key when building the payload.
3647
// 3. Call ledger.Prove() to get a *PayloadlessTrieBatchProof (leaf hashes,
3748
// no values).
38-
// 4. Hand the proof, map, and valueReader to ReconstructPayloadlessProof
49+
// 4. Hand the proof, map, and valueReader to reconstructPayloadlessProof
3950
// to verify each leaf hash and re-encode as a full *TrieBatchProof.
4051
//
52+
// TODO(perf): overlap step 3 with the value reads from step 4. Today the
53+
// steps run sequentially: Prove finishes, then per-leaf value reads run
54+
// inline inside reconstructPayloadlessProof. The two I/O phases are
55+
// independent and can run in parallel:
56+
// - Phase A (parallel): l.Prove(query) and one valueReader call per
57+
// registerID, fanned out via an errgroup with a bounded SetLimit (the
58+
// reader's backend has its own concurrency limits — don't fan out
59+
// blindly to N).
60+
// - Phase B: once both complete, run a pure verify+build pass over the
61+
// assembled (proof, value) pairs — no I/O.
62+
//
63+
// The per-leaf verify work (HashLeaf + payload build) is microseconds and
64+
// is not worth pipelining at finer grain.
65+
//
4166
// Expected errors during normal operation:
4267
// - [ErrPayloadHashMismatch] if storehouse value doesn't match the leaf
4368
// hash carried in the proof for some path.
@@ -54,16 +79,18 @@ func ProveAndReconstruct(
5479
keys = append(keys, convert.RegisterIDToLedgerKey(id))
5580
}
5681

57-
// Build the path → registerID map. We compute paths the same way the
58-
// ledger does internally, so the resulting paths match the ones
59-
// carried by the returned proofs.
82+
// Build the path → (registerID, key) map. We compute paths the same way
83+
// the ledger does internally, so the resulting paths match the ones
84+
// carried by the returned proofs. The already-allocated keys are
85+
// stashed here so the reconstruction step does not have to convert
86+
// register IDs to keys a second time.
6087
paths, err := pathfinder.KeysToPaths(keys, pathFinderVersion)
6188
if err != nil {
6289
return nil, fmt.Errorf("failed to derive paths from keys: %w", err)
6390
}
64-
pathToRegisterID := make(map[ledger.Path]flow.RegisterID, len(paths))
91+
pathToTarget := make(map[ledger.Path]registerTarget, len(paths))
6592
for i, p := range paths {
66-
pathToRegisterID[p] = registerIDs[i]
93+
pathToTarget[p] = registerTarget{registerID: registerIDs[i], key: keys[i]}
6794
}
6895

6996
query, err := ledger.NewQuery(state, keys)
@@ -76,44 +103,38 @@ func ProveAndReconstruct(
76103
return nil, fmt.Errorf("failed to generate proof from ledger: %w", err)
77104
}
78105

79-
return ReconstructPayloadlessProof(batchProof, pathToRegisterID, valueReader)
106+
return reconstructPayloadlessProof(batchProof, pathToTarget, valueReader)
80107
}
81108

82-
// ReconstructPayloadlessProof turns a *PayloadlessTrieBatchProof (each leaf
109+
// reconstructPayloadlessProof turns a *PayloadlessTrieBatchProof (each leaf
83110
// carrying a leaf hash, not a value) into encoded bytes of a full
84111
// *ledger.TrieBatchProof (each leaf carrying a *Payload). Used when a
85112
// downstream consumer expects the wire format of the full mtrie's proofs.
86113
//
87114
// For each inclusion proof:
88-
// - The proof's `Path` is used to look up the register ID in
89-
// `pathToRegisterID`.
90-
// - The register ID is passed to `valueReader` to fetch the actual value.
115+
// - The proof's `Path` is used to look up the target in `pathToTarget`.
116+
// - The target's register ID is passed to `valueReader` to fetch the
117+
// actual value.
91118
// - The leaf hash is verified against `HashLeaf(path, actualValue)`.
92-
// - The reconstructed proof's `Payload` is built as
93-
// `NewPayload(RegisterIDToLedgerKey(registerID), actualValue)`.
119+
// - The reconstructed proof's `Payload` is built from the target's
120+
// pre-allocated ledger key and the fetched value.
94121
//
95122
// Non-inclusion proofs (and inclusion proofs of empty/unallocated leaves,
96123
// signalled by `LeafHash == nil`) carry `EmptyPayload()` on the reconstructed
97124
// side — the full-mtrie convention for "this path has no allocated value."
98125
//
99-
// The caller already has the register IDs (it requested the proof for them),
100-
// so the map takes register IDs directly rather than ledger keys to avoid an
101-
// unnecessary `LedgerKeyToRegisterID` round-trip per proof.
102-
//
103126
// Expected errors during normal operation:
104127
// - [ErrPayloadHashMismatch] if the supplied value does not hash to the
105128
// proof's stored leaf hash.
106-
func ReconstructPayloadlessProof(
129+
func reconstructPayloadlessProof(
107130
batchProof *ledger.PayloadlessTrieBatchProof,
108-
pathToRegisterID map[ledger.Path]flow.RegisterID,
131+
pathToTarget map[ledger.Path]registerTarget,
109132
valueReader RegisterValueReader,
110133
) ([]byte, error) {
111134
fullBatch := ledger.NewTrieBatchProofWithEmptyProofs(batchProof.Size())
112135

113136
for i, proof := range batchProof.Proofs {
114137
full := fullBatch.Proofs[i]
115-
// Structural fields are carried over verbatim. They are byte-equivalent
116-
// between the two proof types by construction (see Spec 001).
117138
full.Path = proof.Path
118139
full.Interims = proof.Interims
119140
full.Inclusion = proof.Inclusion
@@ -127,18 +148,21 @@ func ReconstructPayloadlessProof(
127148
continue
128149
}
129150

130-
// Recover the register ID for this path. The payloadless proof does
131-
// not carry the key; the caller must have provided pathToRegisterID
132-
// covering every path the underlying ledger returned a proof for.
133-
registerID, ok := pathToRegisterID[proof.Path]
151+
// Recover the (registerID, key) target for this path. The payloadless
152+
// proof does not carry the key; the caller must have provided
153+
// pathToTarget covering every path the underlying ledger returned a
154+
// proof for.
155+
target, ok := pathToTarget[proof.Path]
134156
if !ok {
135-
return nil, fmt.Errorf("no register ID provided for path %x in proof", proof.Path[:])
157+
return nil, fmt.Errorf("no register target provided for path %x in proof", proof.Path[:])
136158
}
137159

138-
// TODO: parallelize value reads if the round-trip latency matters
139-
actualValue, err := valueReader(registerID)
160+
// TODO(perf): see ProveAndReconstruct. Once values are pre-fetched in
161+
// parallel with l.Prove and passed in alongside pathToTarget, this
162+
// call becomes a map lookup, not a synchronous read.
163+
actualValue, err := valueReader(target.registerID)
140164
if err != nil {
141-
return nil, fmt.Errorf("failed to read register value for %s: %w", registerID, err)
165+
return nil, fmt.Errorf("failed to read register value for %s: %w", target.registerID, err)
142166
}
143167

144168
// Verify the supplied value hashes to the same leaf hash carried in
@@ -149,10 +173,10 @@ func ReconstructPayloadlessProof(
149173
if expectedHash != *proof.LeafHash {
150174
return nil, fmt.Errorf(
151175
"proof reconstruction failed for register %s: storehouse value (len=%d) does not match leaf hash in proof: %w",
152-
registerID, len(actualValue), ErrPayloadHashMismatch)
176+
target.registerID, len(actualValue), ErrPayloadHashMismatch)
153177
}
154178

155-
full.Payload = ledger.NewPayload(convert.RegisterIDToLedgerKey(registerID), actualValue)
179+
full.Payload = ledger.NewPayload(target.key, actualValue)
156180
}
157181

158182
return ledger.EncodeTrieBatchProof(fullBatch), nil

0 commit comments

Comments
 (0)