Skip to content

Commit a8f3ce4

Browse files
ffranrdarioAnongba
authored andcommitted
proof+tapsend: sanity-check STXO proofs at creation and enforce count
Harden transition proof generation by validating STXO inclusion proofs at creation time instead of relying solely on later verification. Changes: * Require alt leaves for transfer root assets and nonempty prev witnesses. * Ensure alt leaf count is at least the number of prev witnesses. * Validate each STXO proof is non-nil and includes a non-nil AssetProof. * Enforce a 1:1 count between STXO inclusion proofs and asset input witnesses. * Return clear errors for missing alt leaves, prev witnesses, or asset proofs. Tests: * Update `tapsend/proof_test.go` to assert the new failure modes. * Table test now expects `no alt leaves for transfer root asset` when STXO proofs are absent and checks error paths before proceeding. Result: * Fail fast on malformed proofs. * Prevents incomplete STXO proofs from entering the pipeline. * Guarantees proof count matches the number of inputs being spent.
1 parent 2db142a commit a8f3ce4

File tree

2 files changed

+60
-5
lines changed

2 files changed

+60
-5
lines changed

proof/append.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,10 +207,35 @@ func CreateTransitionProof(prevOut wire.OutPoint, params *TransitionParams,
207207
}
208208

209209
if proof.Asset.IsTransferRoot() && !cfg.NoSTXOProofs {
210+
assetCommitments := params.TaprootAssetRoot.Commitments()
211+
altCommitment, ok := assetCommitments[asset.EmptyGenesisID]
212+
if !ok {
213+
return nil, fmt.Errorf("no alt leaves for transfer " +
214+
"root asset")
215+
}
216+
217+
// If this is a transfer root, then we also expect there to be
218+
// prev witnesses.
219+
if len(proof.Asset.PrevWitnesses) == 0 {
220+
return nil, fmt.Errorf("no prev witnesses for " +
221+
"transfer root asset")
222+
}
223+
224+
// We should have at least as many alt leaves as we have
225+
// prev witnesses. We may have additional alt leaves which are
226+
// not related to stxo proofs.
227+
//
228+
// nolint: lll
229+
if len(altCommitment.Assets()) < len(proof.Asset.PrevWitnesses) {
230+
return nil, fmt.Errorf("not enough alt leaves for " +
231+
"transfer root asset")
232+
}
233+
210234
stxoInclusionProofs := make(
211235
map[asset.SerializedKey]commitment.Proof,
212236
len(proof.Asset.PrevWitnesses),
213237
)
238+
214239
for _, wit := range proof.Asset.PrevWitnesses {
215240
spentAsset, err := asset.MakeSpentAsset(wit)
216241
if err != nil {
@@ -227,12 +252,36 @@ func CreateTransitionProof(prevOut wire.OutPoint, params *TransitionParams,
227252
if err != nil {
228253
return nil, err
229254
}
255+
256+
// Sanity-check the STXO proof to ensure the asset proof
257+
// is present. STXO inclusion proofs must always include
258+
// a valid asset proof.
259+
if stxoProof == nil {
260+
return nil, fmt.Errorf("stxo inclusion proof " +
261+
"is nil")
262+
}
263+
264+
if stxoProof.AssetProof == nil {
265+
return nil, commitment.ErrMissingAssetProof
266+
}
267+
230268
keySerialized := asset.ToSerialized(
231269
spentAsset.ScriptKey.PubKey,
232270
)
233271
stxoInclusionProofs[keySerialized] = *stxoProof
234272
}
235273

274+
// For assets representing a root transfer (normal assets), each
275+
// spent input corresponds to an entry in PrevWitnesses.
276+
// Therefore, the number of PrevWitnesses should match the
277+
// number of STXO inclusion proofs.
278+
if len(stxoInclusionProofs) != len(proof.Asset.PrevWitnesses) {
279+
return nil, fmt.Errorf("stxo inclusion proof count "+
280+
"mismatch: expected %d, got %d",
281+
len(proof.Asset.PrevWitnesses),
282+
len(stxoInclusionProofs))
283+
}
284+
236285
if len(stxoInclusionProofs) == 0 {
237286
return nil, fmt.Errorf("no stxo inclusion proofs")
238287
}

tapsend/proof_test.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,9 @@ func TestCreateProofSuffix(t *testing.T) {
3737
stxoProof: true,
3838
},
3939
{
40-
name: "No stxo proof",
41-
stxoProof: false,
42-
expectedErr: "error verifying STXO proof: missing " +
43-
"asset proof",
40+
name: "No stxo proof",
41+
stxoProof: false,
42+
expectedErr: "no alt leaves for transfer root asset",
4443
},
4544
}
4645
for _, tc := range testCases {
@@ -125,7 +124,14 @@ func createProofSuffix(t *testing.T, stxoProof bool, expectedErr string) {
125124
outputCommitments, outIdx, testPackets,
126125
proof.WithVersion(proof.TransitionV1),
127126
)
128-
require.NoError(t, err)
127+
switch {
128+
case err != nil:
129+
require.Nil(t, proofSuffix)
130+
require.ErrorContains(t, err, expectedErr)
131+
continue
132+
default:
133+
require.NoError(t, err)
134+
}
129135

130136
ctx := context.Background()
131137
prev := &proof.AssetSnapshot{

0 commit comments

Comments
 (0)