Skip to content

Commit be39235

Browse files
authored
Merge pull request #1453 from lightninglabs/proof-system-v2
Proof-system-v1
2 parents e54e03b + 76c0706 commit be39235

36 files changed

+2964
-605
lines changed

asset/asset.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2164,3 +2164,52 @@ func FromAltLeaves(leaves []AltLeaf[Asset]) []*Asset {
21642164
return l.(*Asset)
21652165
})
21662166
}
2167+
2168+
// CollectSTXO returns the assets spent by the given output asset in the form of
2169+
// a minimal assets that can be used to create an STXO commitment.
2170+
func CollectSTXO(outAsset *Asset) ([]AltLeaf[Asset], error) {
2171+
// Genesis assets have no input asset, so they should have an empty
2172+
// STXO tree. Split leaves will also have a zero PrevID; we will use
2173+
// an empty STXO tree for them as well.
2174+
if !outAsset.IsTransferRoot() {
2175+
return nil, nil
2176+
}
2177+
2178+
// At this point, the asset must have at least one witness.
2179+
if len(outAsset.PrevWitnesses) == 0 {
2180+
return nil, fmt.Errorf("asset has no witnesses")
2181+
}
2182+
2183+
// We'll convert the PrevID of each witness into a minimal Asset, where
2184+
// the PrevID is the tweak for an un-spendable script key.
2185+
altLeaves := make([]*Asset, len(outAsset.PrevWitnesses))
2186+
for idx, wit := range outAsset.PrevWitnesses {
2187+
altLeaf, err := MakeSpentAsset(wit)
2188+
if err != nil {
2189+
return nil, fmt.Errorf("error collecting stxo for "+
2190+
"witness %d: %w", idx, err)
2191+
}
2192+
2193+
altLeaves[idx] = altLeaf
2194+
}
2195+
2196+
return ToAltLeaves(altLeaves), nil
2197+
}
2198+
2199+
// MakeSpentAsset creates an Alt Leaf with the minimal asset based on the PrevId
2200+
// of the witness.
2201+
func MakeSpentAsset(witness Witness) (*Asset, error) {
2202+
if witness.PrevID == nil {
2203+
return nil, fmt.Errorf("witness has no prevID")
2204+
}
2205+
2206+
prevIdKey := DeriveBurnKey(*witness.PrevID)
2207+
scriptKey := NewScriptKey(prevIdKey)
2208+
2209+
spentAsset, err := NewAltLeaf(scriptKey, ScriptV0)
2210+
if err != nil {
2211+
return nil, fmt.Errorf("error creating altLeaf: %w", err)
2212+
}
2213+
2214+
return spentAsset, nil
2215+
}

itest/assertions.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -566,10 +566,12 @@ func AssertProofAltLeaves(t *testing.T, tapClient taprpc.TaprootAssetsClient,
566566
// not. E.x. a passive asset created inside the freighter will not be
567567
// anchored with any alt leaves.
568568
altLeavesBytes := decodeResp.DecodedProof.AltLeaves
569-
expectedAltLeaves, ok := leafMap[string(scriptKey)]
570-
emptyAltLeaves := len(altLeavesBytes) == 0
569+
expectedAltLeaves := leafMap[string(scriptKey)]
570+
emptyAltLeaves := len(expectedAltLeaves) == 0
571571

572-
require.Equal(t, ok, !emptyAltLeaves)
572+
// If we expect no alt leaves, there might be alt leaves in the proof,
573+
// but that is from an asset that wasn't transferred just now. We don't
574+
// need to check those alt leaves.
573575
if emptyAltLeaves {
574576
return
575577
}

itest/psbt_test.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,31 @@ func runPsbtInteractiveFullValueSendTest(ctxt context.Context, t *harnessTest,
656656
)
657657
require.NoError(t.t, err)
658658

659+
activeAsset, err := tappsbt.Decode(fundResp.FundedPsbt)
660+
require.NoError(t.t, err)
661+
662+
// We expect stxo alt leaves as well, so we'll create those to
663+
// use in an assertion comparison later.
664+
var stxoAltLeaves []*asset.Asset
665+
for _, output := range activeAsset.Outputs {
666+
if !output.Asset.IsTransferRoot() {
667+
continue
668+
}
669+
670+
witnesses, err := output.PrevWitnesses()
671+
require.NoError(t.t, err)
672+
for _, wit := range witnesses {
673+
altLeaf, err := asset.MakeSpentAsset(wit)
674+
require.NoError(t.t, err)
675+
676+
stxoAltLeaves = append(stxoAltLeaves, altLeaf)
677+
}
678+
}
679+
leafMap[string(receiverScriptKeyBytes)] = append(
680+
leafMap[string(receiverScriptKeyBytes)],
681+
stxoAltLeaves...,
682+
)
683+
659684
numOutputs := 1
660685
amounts := []uint64{fullAmt}
661686
ConfirmAndAssertOutboundTransferWithOutputs(
@@ -2489,6 +2514,175 @@ func testPsbtTrustlessSwap(t *harnessTest) {
24892514
require.Equal(t.t, bobScriptKeyBytes, bobAssets.Assets[0].ScriptKey)
24902515
}
24912516

2517+
// testPsbtSTXOExclusionProofs tests that we can properly send normal assets
2518+
// back and forth, using partial amounts, between nodes with the use of PSBTs,
2519+
// and that we see the expected STXO exclusion proofs.
2520+
func testPsbtSTXOExclusionProofs(t *harnessTest) {
2521+
// First, we'll make a normal asset with a bunch of units that we are
2522+
// going to send backand forth. We're also minting a passive asset that
2523+
// should remain where it is.
2524+
rpcAssets := MintAssetsConfirmBatch(
2525+
t.t, t.lndHarness.Miner().Client, t.tapd,
2526+
[]*mintrpc.MintAssetRequest{
2527+
simpleAssets[0],
2528+
// Our "passive" asset.
2529+
{
2530+
Asset: &mintrpc.MintAsset{
2531+
AssetType: taprpc.AssetType_NORMAL,
2532+
Name: "itestbuxx-passive",
2533+
AssetMeta: &taprpc.AssetMeta{
2534+
Data: []byte("some metadata"),
2535+
},
2536+
Amount: 123,
2537+
},
2538+
},
2539+
},
2540+
)
2541+
2542+
ctxb := context.Background()
2543+
ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout)
2544+
defer cancel()
2545+
2546+
mintedAsset := rpcAssets[0]
2547+
genInfo := rpcAssets[0].AssetGenesis
2548+
var assetId asset.ID
2549+
copy(assetId[:], genInfo.AssetId)
2550+
2551+
// Now that we have the asset created, we'll make a new node that'll
2552+
// serve as the node which'll receive the assets.
2553+
bobLnd := t.lndHarness.NewNodeWithCoins("Bob", nil)
2554+
bob := setupTapdHarness(t.t, t, bobLnd, t.universeServer)
2555+
defer func() {
2556+
require.NoError(t.t, bob.stop(!*noDelete))
2557+
}()
2558+
2559+
alice := t.tapd
2560+
2561+
// We need to derive two keys, one for the new script key and
2562+
// one for the internal key.
2563+
bobScriptKey, bobAnchorIntKeyDesc := DeriveKeys(t.t, bob)
2564+
2565+
var id [32]byte
2566+
copy(id[:], genInfo.AssetId)
2567+
sendAmt := uint64(2400)
2568+
2569+
vPkt := tappsbt.ForInteractiveSend(
2570+
id, sendAmt, bobScriptKey, 0, 0, 0,
2571+
bobAnchorIntKeyDesc, asset.V0, chainParams,
2572+
)
2573+
2574+
// Next, we'll attempt to complete a transfer with PSBTs from
2575+
// alice to bob, using the partial amount.
2576+
fundResp := fundPacket(t, alice, vPkt)
2577+
signResp, err := alice.SignVirtualPsbt(
2578+
ctxt, &wrpc.SignVirtualPsbtRequest{
2579+
FundedPsbt: fundResp.FundedPsbt,
2580+
},
2581+
)
2582+
require.NoError(t.t, err)
2583+
2584+
// Now we'll attempt to complete the transfer.
2585+
sendResp, err := alice.AnchorVirtualPsbts(
2586+
ctxt, &wrpc.AnchorVirtualPsbtsRequest{
2587+
VirtualPsbts: [][]byte{signResp.SignedPsbt},
2588+
},
2589+
)
2590+
require.NoError(t.t, err)
2591+
2592+
numOutputs := 2
2593+
changeAmt := mintedAsset.Amount - sendAmt
2594+
ConfirmAndAssertOutboundTransferWithOutputs(
2595+
t.t, t.lndHarness.Miner().Client, alice, sendResp,
2596+
genInfo.AssetId, []uint64{changeAmt, sendAmt}, 0, 1, numOutputs,
2597+
)
2598+
2599+
// We want the proof of the change asset since that is the root asset.
2600+
aliceScriptKeyBytes := sendResp.Transfer.Outputs[0].ScriptKey
2601+
proofResp := exportProof(
2602+
t, alice, sendResp, aliceScriptKeyBytes, genInfo,
2603+
)
2604+
proofFile, err := proof.DecodeFile(proofResp.RawProofFile)
2605+
require.NoError(t.t, err)
2606+
require.Equal(t.t, proofFile.NumProofs(), 2)
2607+
latestProof, err := proofFile.LastProof()
2608+
require.NoError(t.t, err)
2609+
2610+
// This proof should contain the STXO exclusion proofs
2611+
stxoProofs := latestProof.ExclusionProofs[0].CommitmentProof.STXOProofs
2612+
require.NotNil(t.t, stxoProofs)
2613+
2614+
// We expect a single exclusion proof for the change output, which is
2615+
// the input asset that we spent which should not be committed to in the
2616+
// other anchor output.
2617+
outpoint, err := wire.NewOutPointFromString(
2618+
mintedAsset.ChainAnchor.AnchorOutpoint,
2619+
)
2620+
require.NoError(t.t, err)
2621+
2622+
prevId := asset.PrevID{
2623+
OutPoint: *outpoint,
2624+
ID: id,
2625+
ScriptKey: asset.SerializedKey(mintedAsset.ScriptKey),
2626+
}
2627+
2628+
prevIdKey := asset.DeriveBurnKey(prevId)
2629+
expectedScriptKey := asset.NewScriptKey(prevIdKey)
2630+
2631+
pubKey := expectedScriptKey.PubKey
2632+
identifier := asset.ToSerialized(pubKey)
2633+
2634+
require.Len(t.t, stxoProofs, 1)
2635+
2636+
// If we derive the identifier from the script key we expect of the
2637+
// minimal asset, it should yield a proof when used as a key for the
2638+
// stxoProofs.
2639+
require.NotNil(t.t, stxoProofs[identifier])
2640+
2641+
// Create the minimal asset for which we expect to see the STXO
2642+
// exclusion.
2643+
minAsset, err := asset.NewAltLeaf(expectedScriptKey, asset.ScriptV0)
2644+
require.NoError(t.t, err)
2645+
2646+
// We need to copy the base exclusion proof for each STXO because we'll
2647+
// modify it with the specific asset and taproot proofs.
2648+
stxoProof := stxoProofs[identifier]
2649+
stxoExclProof := proof.MakeSTXOProof(
2650+
latestProof.ExclusionProofs[0], &stxoProof,
2651+
)
2652+
2653+
// Derive the possible taproot keys assuming the exclusion proof is
2654+
// correct.
2655+
derivedKeys, err := stxoExclProof.DeriveByAssetExclusion(
2656+
minAsset.AssetCommitmentKey(),
2657+
minAsset.TapCommitmentKey(),
2658+
)
2659+
require.NoError(t.t, err)
2660+
2661+
// Extract the actual taproot key from the anchor tx.
2662+
expectedTaprootKey, err := proof.ExtractTaprootKey(
2663+
&latestProof.AnchorTx, stxoExclProof.OutputIndex,
2664+
)
2665+
require.NoError(t.t, err)
2666+
expectedKey := schnorr.SerializePubKey(expectedTaprootKey)
2667+
2668+
// Convert the derived (possible) keys into their schnorr serialized
2669+
// counterparts.
2670+
serializedKeys := make([][]byte, 0, len(derivedKeys))
2671+
for derivedKey := range derivedKeys {
2672+
serializedKeys = append(
2673+
serializedKeys, derivedKey.SchnorrSerialized(),
2674+
)
2675+
}
2676+
2677+
// The derived keys should contain the expected key.
2678+
require.Contains(t.t, serializedKeys, expectedKey)
2679+
2680+
// This is an interactive transfer, so we do need to manually
2681+
// send the proof from the sender to the receiver.
2682+
bobScriptKeyBytes := bobScriptKey.PubKey.SerializeCompressed()
2683+
sendProof(t, alice, bob, sendResp, bobScriptKeyBytes, genInfo)
2684+
}
2685+
24922686
// testPsbtExternalCommit tests the ability to fully customize the BTC level of
24932687
// an asset transfer using a PSBT. This exercises the CommitVirtualPsbts and
24942688
// PublishAndLogTransfer RPCs. The test case moves some assets into an output

itest/test_list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ package itest
55
import "github.com/lightninglabs/taproot-assets/proof"
66

77
var testCases = []*testCase{
8+
{
9+
name: "psbt stxo exclusion proofs",
10+
test: testPsbtSTXOExclusionProofs,
11+
},
812
{
913
name: "mint assets",
1014
test: testMintAssets,

0 commit comments

Comments
 (0)