Skip to content

Commit 66edc1b

Browse files
committed
tapdb: fix "mismatch of managed utxo and constructed..." error
Fixes the following error that occurred sometimes: unable to fund address send: error funding packet: unable to list eligible coins: unable to query commitments: mismatch of managed utxo and constructed tap commitment root This would happen when we inserted the split _ROOT_ output as a proof file (instead of going through the transfer flow which didn't have this issue), because the split commitment root wasn't properly written to the database in that case. Which means, not a single itest so far did send out the split root output to the counterparty as part of an interactive transfer. This was only discovered after turning the address v2 send flow into an interactive send, which does allow the split root output to be sent to the counterparty instead of only using it as the local change output.
1 parent 504440f commit 66edc1b

File tree

5 files changed

+190
-25
lines changed

5 files changed

+190
-25
lines changed

tapdb/assets_common.go

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -276,18 +276,32 @@ func upsertAssetsWithGenesis(ctx context.Context, q UpsertAssetStore,
276276
continue
277277
}
278278

279+
var (
280+
splitCommitmentRootHash []byte
281+
splitCommitmentRootValue sql.NullInt64
282+
)
283+
if a.SplitCommitmentRoot != nil {
284+
splitRootHash := a.SplitCommitmentRoot.NodeHash()
285+
splitCommitmentRootHash = splitRootHash[:]
286+
splitCommitmentRootValue = sqlInt64(
287+
a.SplitCommitmentRoot.NodeSum(),
288+
)
289+
}
290+
279291
// With all the dependent data inserted, we can now insert the
280292
// base asset information itself.
281293
assetIDs[idx], err = q.UpsertAsset(ctx, sqlc.UpsertAssetParams{
282-
GenesisID: genAssetID,
283-
Version: int32(a.Version),
284-
ScriptKeyID: scriptKeyID,
285-
AssetGroupWitnessID: groupWitnessID,
286-
ScriptVersion: int32(a.ScriptVersion),
287-
Amount: int64(a.Amount),
288-
LockTime: sqlInt32(a.LockTime),
289-
RelativeLockTime: sqlInt32(a.RelativeLockTime),
290-
AnchorUtxoID: anchorUtxoID,
294+
GenesisID: genAssetID,
295+
Version: int32(a.Version),
296+
ScriptKeyID: scriptKeyID,
297+
AssetGroupWitnessID: groupWitnessID,
298+
ScriptVersion: int32(a.ScriptVersion),
299+
Amount: int64(a.Amount),
300+
LockTime: sqlInt32(a.LockTime),
301+
RelativeLockTime: sqlInt32(a.RelativeLockTime),
302+
AnchorUtxoID: anchorUtxoID,
303+
SplitCommitmentRootHash: splitCommitmentRootHash,
304+
SplitCommitmentRootValue: splitCommitmentRootValue,
291305
})
292306
if err != nil {
293307
return 0, nil, fmt.Errorf("unable to insert asset: %w",

tapdb/assets_store.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/lightninglabs/taproot-assets/tapdb/sqlc"
2424
"github.com/lightninglabs/taproot-assets/tapfreighter"
2525
"github.com/lightninglabs/taproot-assets/tappsbt"
26+
"github.com/lightninglabs/taproot-assets/tapsend"
2627
"github.com/lightningnetwork/lnd/clock"
2728
"github.com/lightningnetwork/lnd/keychain"
2829
)
@@ -2343,10 +2344,18 @@ func (a *AssetStore) queryCommitments(ctx context.Context,
23432344
// Verify that the constructed Taproot Asset commitment matches
23442345
// the commitment root stored in the managed UTXO.
23452346
commitmentRoot := tapCommitment.TapscriptRoot(nil)
2347+
2348+
tapsend.LogCommitment(
2349+
"reconstructed input", int(anchorPoint.Index),
2350+
tapCommitment, &btcec.PublicKey{}, nil, nil,
2351+
)
2352+
23462353
anchorCommitmentRoot := anchorUTXO.TaprootAssetRoot
23472354
if !bytes.Equal(anchorCommitmentRoot, commitmentRoot[:]) {
2348-
return nil, fmt.Errorf("mismatch of managed utxo and " +
2349-
"constructed tap commitment root")
2355+
return nil, fmt.Errorf("mismatch of managed utxo and "+
2356+
"constructed tap commitment root, "+
2357+
"utxoRoot=%x, reconstructedRoot=%x",
2358+
anchorCommitmentRoot, commitmentRoot[:])
23502359
}
23512360

23522361
anchorPointToCommitment[anchorPoint] = tapCommitment

tapdb/assets_store_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3247,3 +3247,139 @@ func TestShouldSkipAssetCreation(t *testing.T) {
32473247
})
32483248
}
32493249
}
3250+
3251+
// createTestProofWithAnchor creates an annotated proof with specific anchor
3252+
// index.
3253+
func createTestProofWithAnchor(t *testing.T, testAsset *asset.Asset,
3254+
anchorIndex uint32) *proof.AnnotatedProof {
3255+
3256+
var blockHash chainhash.Hash
3257+
_, err := rand.Read(blockHash[:])
3258+
require.NoError(t, err)
3259+
3260+
anchorTx := wire.NewMsgTx(2)
3261+
anchorTx.AddTxIn(&wire.TxIn{})
3262+
3263+
// Add enough outputs to cover the requested index
3264+
for i := uint32(0); i <= anchorIndex; i++ {
3265+
anchorTx.AddTxOut(&wire.TxOut{
3266+
PkScript: bytes.Repeat([]byte{0x01}, 34),
3267+
Value: 10,
3268+
})
3269+
}
3270+
3271+
assetRoot, err := commitment.NewAssetCommitment(testAsset)
3272+
require.NoError(t, err)
3273+
3274+
commitVersion := test.RandFlip(nil, fn.Ptr(commitment.TapCommitmentV2))
3275+
taprootAssetRoot, err := commitment.NewTapCommitment(
3276+
commitVersion, assetRoot,
3277+
)
3278+
require.NoError(t, err)
3279+
3280+
testProof := randProof(t, testAsset)
3281+
proofBlob, err := testProof.Bytes()
3282+
require.NoError(t, err)
3283+
3284+
assetID := testAsset.ID()
3285+
anchorPoint := wire.OutPoint{
3286+
Hash: anchorTx.TxHash(),
3287+
Index: anchorIndex,
3288+
}
3289+
3290+
return &proof.AnnotatedProof{
3291+
Locator: proof.Locator{
3292+
AssetID: &assetID,
3293+
ScriptKey: *testAsset.ScriptKey.PubKey,
3294+
},
3295+
Blob: proofBlob,
3296+
AssetSnapshot: &proof.AssetSnapshot{
3297+
Asset: testAsset,
3298+
OutPoint: anchorPoint,
3299+
AnchorBlockHash: blockHash,
3300+
AnchorBlockHeight: uint32(test.RandIntn(1000) + 1),
3301+
AnchorTxIndex: test.RandInt[uint32](),
3302+
AnchorTx: anchorTx,
3303+
OutputIndex: anchorIndex,
3304+
InternalKey: test.RandPubKey(t),
3305+
ScriptRoot: taprootAssetRoot,
3306+
},
3307+
}
3308+
}
3309+
3310+
// TestUpsertAssetsWithSplitCommitments tests that split commitment data is
3311+
// properly stored and retrieved during asset upsert operations.
3312+
func TestUpsertAssetsWithSplitCommitments(t *testing.T) {
3313+
t.Parallel()
3314+
3315+
testCases := []struct {
3316+
name string
3317+
hasSplitCommit bool
3318+
splitValue uint64
3319+
}{
3320+
{
3321+
name: "asset with split commitment",
3322+
hasSplitCommit: true,
3323+
splitValue: 12345,
3324+
},
3325+
{
3326+
name: "asset without split commitment",
3327+
hasSplitCommit: false,
3328+
},
3329+
}
3330+
3331+
for _, tc := range testCases {
3332+
t.Run(tc.name, func(t *testing.T) {
3333+
t.Parallel()
3334+
3335+
ctx := context.Background()
3336+
dbHandle := NewDbHandle(t)
3337+
3338+
testAsset := randAsset(t)
3339+
3340+
var expectedHash mssmt.NodeHash
3341+
if tc.hasSplitCommit {
3342+
splitHash := test.RandBytes(32)
3343+
copy(expectedHash[:], splitHash)
3344+
3345+
rootNode := mssmt.NewComputedNode(
3346+
expectedHash, tc.splitValue,
3347+
)
3348+
testAsset.SplitCommitmentRoot = rootNode
3349+
}
3350+
3351+
annotatedProof := createTestProofWithAnchor(
3352+
t, testAsset, 0,
3353+
)
3354+
3355+
err := dbHandle.AssetStore.ImportProofs(
3356+
ctx, proof.MockVerifierCtx, false,
3357+
annotatedProof,
3358+
)
3359+
require.NoError(t, err)
3360+
3361+
assets, err := dbHandle.AssetStore.FetchAllAssets(
3362+
ctx, false, false, nil,
3363+
)
3364+
require.NoError(t, err)
3365+
require.Len(t, assets, 1)
3366+
3367+
dbAsset := assets[0].Asset
3368+
3369+
root := dbAsset.SplitCommitmentRoot
3370+
if tc.hasSplitCommit {
3371+
require.NotNil(t, root)
3372+
gotHash := root.NodeHash()
3373+
require.Equal(t, expectedHash, gotHash)
3374+
require.Equal(
3375+
t, tc.splitValue, root.NodeSum(),
3376+
)
3377+
} else {
3378+
require.Nil(
3379+
t, root,
3380+
"Split commitment root should be nil",
3381+
)
3382+
}
3383+
})
3384+
}
3385+
}

tapdb/sqlc/assets.sql.go

Lines changed: 17 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tapdb/sqlc/queries/assets.sql

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,10 @@ RETURNING gen_asset_id;
218218
-- name: UpsertAsset :one
219219
INSERT INTO assets (
220220
genesis_id, version, script_key_id, asset_group_witness_id, script_version,
221-
amount, lock_time, relative_lock_time, anchor_utxo_id, spent
221+
amount, lock_time, relative_lock_time, anchor_utxo_id, spent,
222+
split_commitment_root_hash, split_commitment_root_value
222223
) VALUES (
223-
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
224+
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
224225
)
225226
ON CONFLICT (genesis_id, script_key_id, anchor_utxo_id)
226227
-- This is a NOP, anchor_utxo_id is one of the unique fields that caused the

0 commit comments

Comments
 (0)