@@ -22,6 +22,16 @@ var ErrPayloadHashMismatch = errors.New("payload hash mismatch: storehouse value
2222// - (nil, error) for any other errors
2323type 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