Skip to content

rpcserver: VerifyProofResponse.DecodedProof omits predecessor IDs that DecodeProof can expose #2214

Description

@darioAnongba

Problem

VerifyProof returns a DecodedProof whose asset.prev_witnesses (and therefore the predecessor PrevInputAsset entries — anchor_point, asset_id, script_key) are always empty, whereas DecodeProof can populate them via with_prev_witnesses. For a lightweight/custom-anchor integration that verifies a proof file it received (e.g. SwapDK receiving assets over a custom anchor tx), VerifyProof is the natural single call: it both validates the proof chain and hands back a decoded summary. But that summary cannot tell the caller which asset(s) were spent to produce the proven asset — the predecessor inventory (input anchor outpoints + asset IDs + script keys). To obtain that, a caller must make a second, redundant DecodeProof call with with_prev_witnesses=true, re-decoding and re-parsing the same proof file. The asymmetry is undocumented, so integrations can reasonably assume VerifyProof's DecodedProof is complete and silently get zero predecessors.

Note this is distinct from num_additional_inputs, which VerifyProof does populate (it counts nested full input proofs) — that field gives a count but never the identifying PrevInputAsset fields needed to reconstruct the input set.

Current behaviour

  • VerifyProof decodes the file, verifies it, takes the last proof, and marshals it with prev witnesses (and meta reveal) hard-disabled:
    • rpcserver/rpcserver.go:2184decodedProof, err := r.marshalProof(ctx, p, false, false) (the two falses are withPrevWitnesses and withMetaReveal).
    • The response is assembled at rpcserver/rpcserver.go:2192; there is no field on the request (taprpc.ProofFile, taprpc/taprootassets.proto:1439) to opt into witnesses.
  • DecodeProof, by contrast, threads the caller's flag through:
    • rpcserver/rpcserver.go:2213 and rpcserver/rpcserver.go:2248r.marshalProof(ctx, p, req.WithPrevWitnesses, req.WithMetaReveal).
    • Request flag defined at taprpc/taprootassets.proto:1534 (bool with_prev_witnesses = 3).
  • The predecessor IDs live behind the withWitness gate in the shared asset marshaler:
    • rpcutils/marshal.go:741-775 — only when withWitness is true does it iterate a.PrevWitnesses, build a taprpc.PrevInputAsset (AnchorPoint, AssetId, ScriptKey at rpcutils/marshal.go:746-750), and append to rpcAsset.PrevWitnesses. With withWitness=false, PrevWitnesses is left nil.
  • marshalProof itself always sets num_additional_inputs regardless of the witness flag:
    • rpcserver/rpcserver.go:2437NumAdditionalInputs: uint32(len(p.AdditionalInputs)).
  • Proto shapes for reference: DecodedProof (taprpc/taprootassets.proto:1449), VerifyProofResponse (taprpc/taprootassets.proto:1513), PrevWitness (taprpc/taprootassets.proto:661), PrevInputAsset (taprpc/taprootassets.proto:1692).

Net effect: VerifyProofResponse.decoded_proof.asset.prev_witnesses is always empty; the only way to see predecessor IDs is a separate DecodeProof with with_prev_witnesses=true.

Proposed change

Pick one (in rough order of preference):

  1. Add a with_prev_witnesses (and optionally with_meta_reveal) field to the VerifyProof request and thread it into the marshalProof call at rpcserver/rpcserver.go:2184, matching DecodeProof. This lets a verify-and-inspect integration get predecessor IDs in one round trip. Because VerifyProof's request is taprpc.ProofFile (shared with other call sites via taprpc/taprootassets.proto:1439), a cleaner approach is to introduce a dedicated VerifyProofRequest message carrying raw_proof_file, genesis_point, and the new option(s), leaving ProofFile untouched.
  2. If the omission is intentional (e.g. to keep VerifyProof cheap and avoid extra key-ring lookups), document it explicitly: add a comment on VerifyProofResponse.decoded_proof / the prev_witnesses field noting that VerifyProof never populates prev witnesses and that callers needing predecessor IDs must use DecodeProof with with_prev_witnesses=true. Update the marshalProof call site at rpcserver/rpcserver.go:2184 with a one-line rationale.

Either way, the asymmetry between the two handlers should stop being silent.

Context

Surfaced while building the custom-anchor transaction builder in lightninglabs/tap-sdk#158: the SDK verifies inbound proof files and wants the predecessor inventory (input anchor outpoints + asset IDs) from the same call. Because VerifyProof's DecodedProof omits prev_witnesses, the SDK cannot infer predecessors from a verify summary and fails closed — it does not fabricate predecessor data from a VerifyProof result, and instead must fall back to a separate DecodeProof (with_prev_witnesses=true) call to recover them. Exposing the option on VerifyProof (or documenting the omission) removes the redundant round trip and the ambiguity.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    Status
    🆕 New

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions