diff --git a/itest/assertions.go b/itest/assertions.go index d6e3f1163..bf7edef14 100644 --- a/itest/assertions.go +++ b/itest/assertions.go @@ -1416,6 +1416,36 @@ func AssertNumGroups(t *testing.T, client taprpc.TaprootAssetsClient, require.Equal(t, num, NumGroups(t, client)) } +// AssertNumBurns makes sure a given number of burns exists, then returns them. +func AssertNumBurns(t *testing.T, client taprpc.TaprootAssetsClient, + num int, req *taprpc.ListBurnsRequest) []*taprpc.AssetBurn { + + ctxb := context.Background() + if req == nil { + req = &taprpc.ListBurnsRequest{} + } + + var result []*taprpc.AssetBurn + err := wait.NoError(func() error { + burns, err := client.ListBurns(ctxb, req) + if err != nil { + return err + } + + if len(burns.Burns) != num { + return fmt.Errorf("wanted %d burns, got %d", num, + len(burns.Burns)) + } + + result = burns.Burns + + return nil + }, defaultTimeout) + require.NoError(t, err) + + return result +} + // NumGroups returns the current number of asset groups present. func NumGroups(t *testing.T, client taprpc.TaprootAssetsClient) int { ctxb := context.Background() diff --git a/itest/burn_test.go b/itest/burn_test.go index d4f597d05..ec65142a5 100644 --- a/itest/burn_test.go +++ b/itest/burn_test.go @@ -64,21 +64,21 @@ func testBurnAssets(t *harnessTest) { // - 800 units to scriptKey4 // anchor index 3 (automatic change output): // - 300 units to new script key - outputAmounts := []uint64{1100, 1200, 1600, 800, 300} + outputAmounts := []uint64{300, 1100, 1200, 1600, 800} vPkt := tappsbt.ForInteractiveSend( - simpleAssetID, outputAmounts[0], scriptKey1, 0, 0, 0, + simpleAssetID, outputAmounts[1], scriptKey1, 0, 0, 0, anchorInternalKeyDesc1, asset.V0, chainParams, ) tappsbt.AddOutput( - vPkt, outputAmounts[1], scriptKey2, 0, anchorInternalKeyDesc1, + vPkt, outputAmounts[2], scriptKey2, 0, anchorInternalKeyDesc1, asset.V0, ) tappsbt.AddOutput( - vPkt, outputAmounts[2], scriptKey3, 1, anchorInternalKeyDesc2, + vPkt, outputAmounts[3], scriptKey3, 1, anchorInternalKeyDesc2, asset.V0, ) tappsbt.AddOutput( - vPkt, outputAmounts[3], scriptKey4, 2, anchorInternalKeyDesc3, + vPkt, outputAmounts[4], scriptKey4, 2, anchorInternalKeyDesc3, asset.V0, ) @@ -120,7 +120,7 @@ func testBurnAssets(t *harnessTest) { Asset: &taprpc.BurnAssetRequest_AssetId{ AssetId: simpleAssetID[:], }, - AmountToBurn: outputAmounts[2], + AmountToBurn: outputAmounts[3], ConfirmationText: taprootassets.AssetBurnConfirmationText, }) require.ErrorContains( @@ -152,7 +152,7 @@ func testBurnAssets(t *harnessTest) { AssertAssetOutboundTransferWithOutputs( t.t, minerClient, t.tapd, burnResp.BurnTransfer, simpleAssetGen.AssetId, - []uint64{burnAmt, outputAmounts[2] - burnAmt}, 1, 2, 2, true, + []uint64{outputAmounts[3] - burnAmt, burnAmt}, 1, 2, 2, true, ) // We'll now assert that the burned asset has the correct state. @@ -175,11 +175,8 @@ func testBurnAssets(t *harnessTest) { t.t, t.tapd, simpleAssetGen.AssetId, simpleAsset.Amount-burnAmt, ) - burns, err := t.tapd.ListBurns(ctxt, &taprpc.ListBurnsRequest{}) - require.NoError(t.t, err) - - require.Len(t.t, burns.Burns, 1) - burn := burns.Burns[0] + burns := AssertNumBurns(t.t, t.tapd, 1, nil) + burn := burns[0] require.Equal(t.t, uint64(burnAmt), burn.Amount) require.Equal(t.t, burnResp.BurnTransfer.AnchorTxHash, burn.AnchorTxid) require.Equal(t.t, burn.AssetId, simpleAssetID[:]) @@ -188,7 +185,7 @@ func testBurnAssets(t *harnessTest) { // The burned asset should be pruned from the tree when we next spend // the anchor output it was in (together with the change). So let's test // that we can successfully spend the change output. - secondSendAmt := outputAmounts[2] - burnAmt + secondSendAmt := outputAmounts[3] - burnAmt fullSendAddr, stream := NewAddrWithEventStream( t.t, t.tapd, &taprpc.NewAddrRequest{ AssetId: simpleAssetGen.AssetId, @@ -232,7 +229,7 @@ func testBurnAssets(t *harnessTest) { // two largest inputs we have, the one over 1500 we sent above and the // 1200 from the initial fan out transfer. const changeAmt = 300 - multiBurnAmt := outputAmounts[1] + secondSendAmt - changeAmt + multiBurnAmt := outputAmounts[2] + secondSendAmt - changeAmt burnResp, err = t.tapd.BurnAsset(ctxt, &taprpc.BurnAssetRequest{ Asset: &taprpc.BurnAssetRequest_AssetId{ AssetId: simpleAssetGen.AssetId, @@ -250,7 +247,7 @@ func testBurnAssets(t *harnessTest) { AssertAssetOutboundTransferWithOutputs( t.t, minerClient, t.tapd, burnResp.BurnTransfer, simpleAssetGen.AssetId, - []uint64{multiBurnAmt, changeAmt}, 4, 5, 2, true, + []uint64{changeAmt, multiBurnAmt}, 4, 5, 2, true, ) // Our final asset balance should be reduced by both successful burn @@ -290,18 +287,15 @@ func testBurnAssets(t *harnessTest) { AssertAssetOutboundTransferWithOutputs( t.t, minerClient, t.tapd, burnResp.BurnTransfer, simpleGroupGen.AssetId, - []uint64{burnAmt, simpleGroup.Amount - burnAmt}, 5, 6, 2, true, + []uint64{simpleGroup.Amount - burnAmt, burnAmt}, 5, 6, 2, true, ) AssertBalanceByID( t.t, t.tapd, simpleGroupGen.AssetId, simpleGroup.Amount-burnAmt, ) - burns, err = t.tapd.ListBurns(ctxt, &taprpc.ListBurnsRequest{}) - require.NoError(t.t, err) - - require.Len(t.t, burns.Burns, 4) + burns = AssertNumBurns(t.t, t.tapd, 4, nil) var groupBurn *taprpc.AssetBurn - for _, b := range burns.Burns { + for _, b := range burns { if bytes.Equal(b.AssetId, simpleGroupGen.AssetId) { groupBurn = b } @@ -356,30 +350,21 @@ func testBurnAssets(t *harnessTest) { // Fetch the burns related to the simple asset id, which should have a // total of 2 burns (tc1 & tc4). - burns, err = t.tapd.ListBurns(ctxt, &taprpc.ListBurnsRequest{ + AssertNumBurns(t.t, t.tapd, 2, &taprpc.ListBurnsRequest{ AssetId: simpleAssetGen.AssetId, }) - require.NoError(t.t, err) - - require.Len(t.t, burns.Burns, 2) // Fetch the burns related to the group key of the grouped asset in tc5. // There should be 1 burn. - burns, err = t.tapd.ListBurns(ctxt, &taprpc.ListBurnsRequest{ + AssertNumBurns(t.t, t.tapd, 1, &taprpc.ListBurnsRequest{ TweakedGroupKey: simpleGroup.AssetGroup.TweakedGroupKey, }) - require.NoError(t.t, err) - - require.Len(t.t, burns.Burns, 1) // Fetch the burns associated with the txhash of the burn in tc5. There // should be 1 burn returned. - burns, err = t.tapd.ListBurns(ctxt, &taprpc.ListBurnsRequest{ + AssertNumBurns(t.t, t.tapd, 1, &taprpc.ListBurnsRequest{ AnchorTxid: groupBurnTxHash, }) - require.NoError(t.t, err) - - require.Len(t.t, burns.Burns, 1) } // testBurnGroupedAssets tests that some amount of an asset from an asset group @@ -464,7 +449,7 @@ func testBurnGroupedAssets(t *harnessTest) { // Assert that the asset burn transfer occurred correctly. AssertAssetOutboundTransferWithOutputs( t.t, miner, t.tapd, burnResp.BurnTransfer, - burnAssetID, []uint64{burnAmt, postBurnAmt}, 0, 1, 2, true, + burnAssetID, []uint64{postBurnAmt, burnAmt}, 0, 1, 2, true, ) // Ensure that the burnt asset has the correct state. diff --git a/itest/psbt_test.go b/itest/psbt_test.go index 00512b8b4..7085e0be7 100644 --- a/itest/psbt_test.go +++ b/itest/psbt_test.go @@ -871,7 +871,7 @@ func runPsbtInteractiveSplitSendTest(ctxt context.Context, t *harnessTest, ConfirmAndAssertOutboundTransferWithOutputs( t.t, t.lndHarness.Miner().Client, sender, sendResp, genInfo.AssetId, - []uint64{sendAmt, changeAmt}, i/2, (i/2)+1, + []uint64{changeAmt, sendAmt}, i/2, (i/2)+1, numOutputs, ) @@ -1015,6 +1015,17 @@ func testPsbtInteractiveAltLeafAnchoring(t *harnessTest) { vPkt.Outputs[1].AltLeaves = nil fundResp := fundPacket(t, sender, vPkt) + + // The funding added a change output at the first index. So all the + // indexes will be shifted below. The output with index 1 becomes index + // 2 (where we cleared the alt leaves). + fundedvPkt, err := tappsbt.Decode(fundResp.FundedPsbt) + require.NoError(t.t, err) + require.Len(t.t, fundedvPkt.Outputs, 3) + require.Nil(t.t, fundedvPkt.Outputs[0].AltLeaves) + require.NotNil(t.t, fundedvPkt.Outputs[1].AltLeaves) + require.Nil(t.t, fundedvPkt.Outputs[2].AltLeaves) + signActiveResp, err := sender.SignVirtualPsbt( ctxt, &wrpc.SignVirtualPsbtRequest{ FundedPsbt: fundResp.FundedPsbt, @@ -1037,7 +1048,7 @@ func testPsbtInteractiveAltLeafAnchoring(t *harnessTest) { require.NoError(t.t, err) signedvPktCopy := signedvPkt.Copy() - require.NoError(t.t, signedvPkt.Outputs[1].SetAltLeaves(altLeaves3)) + require.NoError(t.t, signedvPkt.Outputs[2].SetAltLeaves(altLeaves3)) signedvPktBytes, err := tappsbt.Encode(signedvPkt) require.NoError(t.t, err) @@ -1075,7 +1086,7 @@ func testPsbtInteractiveAltLeafAnchoring(t *harnessTest) { // Now, let's set non-conflicting altLeaves for the second vOutput, and // complete the transfer via the PSBT flow. This should succeed. - require.NoError(t.t, signedvPktCopy.Outputs[1].SetAltLeaves(altLeaves2)) + require.NoError(t.t, signedvPktCopy.Outputs[2].SetAltLeaves(altLeaves2)) signedvPktBytes, err = tappsbt.Encode(signedvPktCopy) require.NoError(t.t, err) @@ -1105,7 +1116,7 @@ func testPsbtInteractiveAltLeafAnchoring(t *harnessTest) { ) expectedAmounts := []uint64{ - partialAmt, partialAmt * 2, partialAmt, + partialAmt, partialAmt, partialAmt * 2, } ConfirmAndAssertOutboundTransferWithOutputs( t.t, t.lndHarness.Miner().Client, sender, publishResp, @@ -1136,8 +1147,8 @@ func testPsbtInteractiveAltLeafAnchoring(t *harnessTest) { string(receiverScriptKey2Bytes): allAltLeaves, } - for _, asset := range receiverAssets.Assets { - AssertProofAltLeaves(t.t, receiver, asset, leafMap) + for _, receiverAsset := range receiverAssets.Assets { + AssertProofAltLeaves(t.t, receiver, receiverAsset, leafMap) } } @@ -1215,7 +1226,7 @@ func testPsbtInteractiveTapscriptSibling(t *harnessTest) { ConfirmAndAssertOutboundTransferWithOutputs( t.t, t.lndHarness.Miner().Client, alice, sendResp, - genInfo.AssetId, []uint64{sendAmt, changeAmt}, 0, 1, 2, + genInfo.AssetId, []uint64{changeAmt, sendAmt}, 0, 1, 2, ) // This is an interactive transfer, so we do need to manually send the @@ -1314,9 +1325,9 @@ func testPsbtMultiSend(t *harnessTest) { senderScriptKey2, _ := DeriveKeys(t.t, sender) // We create the output at anchor index 0 for the first address. - outputAmounts := []uint64{1200, 1300, 1400, 800, 300} + outputAmounts := []uint64{300, 1200, 1300, 1400, 800} vPkt := tappsbt.ForInteractiveSend( - id, outputAmounts[0], receiverScriptKey1, 0, 0, 0, + id, outputAmounts[1], receiverScriptKey1, 0, 0, 0, receiverAnchorIntKeyDesc1, asset.V0, chainParams, ) @@ -1325,15 +1336,15 @@ func testPsbtMultiSend(t *harnessTest) { // still leave 300 units as change which we expect to end up at anchor // index 3. tappsbt.AddOutput( - vPkt, outputAmounts[1], receiverScriptKey2, 1, + vPkt, outputAmounts[2], receiverScriptKey2, 1, receiverAnchorIntKeyDesc2, asset.V0, ) tappsbt.AddOutput( - vPkt, outputAmounts[2], senderScriptKey1, 2, + vPkt, outputAmounts[3], senderScriptKey1, 2, senderAnchorIntKeyDesc1, asset.V0, ) tappsbt.AddOutput( - vPkt, outputAmounts[3], senderScriptKey2, 2, + vPkt, outputAmounts[4], senderScriptKey2, 2, senderAnchorIntKeyDesc1, asset.V0, ) @@ -1426,7 +1437,7 @@ func testPsbtMultiSend(t *harnessTest) { // that shared the anchor output and the other one is treated as a // passive asset. sendAssetAndAssert( - ctxt, t, t.tapd, secondTapd, outputAmounts[2], 0, + ctxt, t, t.tapd, secondTapd, outputAmounts[3], 0, genInfo, rpcAssets[0], 2, 3, 2, ) } @@ -1632,7 +1643,7 @@ func testMultiInputPsbtSingleAssetID(t *harnessTest) { ConfirmAndAssertOutboundTransferWithOutputs( t.t, t.lndHarness.Miner().Client, secondaryTapd, sendResp, genInfo.AssetId, - []uint64{sendAmt, changeAmt}, currentTransferIdx, numTransfers, + []uint64{changeAmt, sendAmt}, currentTransferIdx, numTransfers, numOutputs, ) diff --git a/tapchannel/allocation_test.go b/tapchannel/allocation_test.go deleted file mode 100644 index 8ca318467..000000000 --- a/tapchannel/allocation_test.go +++ /dev/null @@ -1,402 +0,0 @@ -package tapchannel - -import ( - "testing" - - "github.com/btcsuite/btcd/wire" - "github.com/lightninglabs/taproot-assets/address" - "github.com/lightninglabs/taproot-assets/asset" - "github.com/lightninglabs/taproot-assets/commitment" - "github.com/lightninglabs/taproot-assets/fn" - "github.com/lightninglabs/taproot-assets/internal/test" - "github.com/lightninglabs/taproot-assets/proof" - "github.com/lightninglabs/taproot-assets/tappsbt" - "github.com/stretchr/testify/require" -) - -var ( - testParams = &address.RegressionNetTap - - tx = wire.MsgTx{ - TxOut: []*wire.TxOut{ - { - Value: 1000, - PkScript: []byte("foo"), - }, - }, - } -) - -func makeProof(t *testing.T, a *asset.Asset) *proof.Proof { - tapCommitment, err := commitment.FromAssets( - fn.Ptr(commitment.TapCommitmentV2), a, - ) - require.NoError(t, err) - - _, commitmentProof, err := tapCommitment.Proof( - a.TapCommitmentKey(), a.AssetCommitmentKey(), - ) - require.NoError(t, err) - - return &proof.Proof{ - Asset: *a, - AnchorTx: tx, - InclusionProof: proof.TaprootProof{ - OutputIndex: 0, - InternalKey: test.RandPubKey(t), - CommitmentProof: &proof.CommitmentProof{ - Proof: *commitmentProof, - }, - }, - } -} - -func grindAssetID(t *testing.T, prefix byte) asset.Genesis { - for { - assetID := asset.RandGenesis(t, asset.Normal) - if assetID.ID()[0] == prefix { - return assetID - } - } -} - -func TestDistributeCoinsErrors(t *testing.T) { - _, err := DistributeCoins(nil, nil, testParams) - require.ErrorIs(t, err, ErrMissingInputs) - - _, err = DistributeCoins([]*proof.Proof{{}}, nil, testParams) - require.ErrorIs(t, err, ErrMissingAllocations) - - assetCollectible := asset.RandAsset(t, asset.Collectible) - proofCollectible := makeProof(t, assetCollectible) - _, err = DistributeCoins( - []*proof.Proof{proofCollectible}, []*Allocation{{}}, testParams, - ) - require.ErrorIs(t, err, ErrNormalAssetsOnly) - - assetNormal := asset.RandAsset(t, asset.Normal) - proofNormal := makeProof(t, assetNormal) - _, err = DistributeCoins( - []*proof.Proof{proofNormal}, []*Allocation{ - { - Amount: assetNormal.Amount / 2, - }, - }, testParams, - ) - require.ErrorIs(t, err, ErrInputOutputSumMismatch) -} - -func TestDistributeCoins(t *testing.T) { - t.Parallel() - - assetID1 := grindAssetID(t, 0x01) - groupKey1 := &asset.GroupKey{ - GroupPubKey: *test.RandPubKey(t), - } - - assetID2 := grindAssetID(t, 0x02) - groupKey2 := &asset.GroupKey{ - GroupPubKey: *test.RandPubKey(t), - } - - assetID3 := grindAssetID(t, 0x03) - groupKey3 := &asset.GroupKey{ - GroupPubKey: *test.RandPubKey(t), - } - - assetID1Tranche1 := asset.NewAssetNoErr( - t, assetID1, 100, 0, 0, asset.RandScriptKey(t), groupKey1, - ) - assetID1Tranche2 := asset.NewAssetNoErr( - t, assetID1, 200, 0, 0, asset.RandScriptKey(t), groupKey1, - ) - assetID1Tranche3 := asset.NewAssetNoErr( - t, assetID1, 300, 0, 0, asset.RandScriptKey(t), groupKey1, - ) - - assetID2Tranche1 := asset.NewAssetNoErr( - t, assetID2, 1000, 0, 0, asset.RandScriptKey(t), groupKey2, - ) - assetID2Tranche2 := asset.NewAssetNoErr( - t, assetID2, 2000, 0, 0, asset.RandScriptKey(t), groupKey2, - ) - assetID2Tranche3 := asset.NewAssetNoErr( - t, assetID2, 3000, 0, 0, asset.RandScriptKey(t), groupKey2, - ) - - assetID3Tranche1 := asset.NewAssetNoErr( - t, assetID3, 10000, 0, 0, asset.RandScriptKey(t), groupKey3, - ) - assetID3Tranche2 := asset.NewAssetNoErr( - t, assetID3, 20000, 0, 0, asset.RandScriptKey(t), groupKey3, - ) - assetID3Tranche3 := asset.NewAssetNoErr( - t, assetID3, 30000, 0, 0, asset.RandScriptKey(t), groupKey3, - ) - - var ( - simple = tappsbt.TypeSimple - split = tappsbt.TypeSplitRoot - ) - testCases := []struct { - name string - inputs []*proof.Proof - allocations []*Allocation - expectedInputs map[asset.ID][]asset.ScriptKey - expectedOutputs map[asset.ID][]*tappsbt.VOutput - }{ - { - name: "single asset, split", - inputs: []*proof.Proof{ - makeProof(t, assetID1Tranche1), - }, - allocations: []*Allocation{ - { - Type: CommitAllocationToLocal, - Amount: 50, - }, - { - Type: CommitAllocationToRemote, - SplitRoot: true, - Amount: 50, - OutputIndex: 1, - }, - }, - expectedInputs: map[asset.ID][]asset.ScriptKey{ - assetID1.ID(): { - assetID1Tranche1.ScriptKey, - }, - }, - expectedOutputs: map[asset.ID][]*tappsbt.VOutput{ - assetID1.ID(): { - { - Amount: 50, - Type: simple, - Interactive: true, - AnchorOutputIndex: 0, - }, - { - Amount: 50, - Type: split, - Interactive: true, - AnchorOutputIndex: 1, - }, - }, - }, - }, - { - name: "multiple assets, split", - inputs: []*proof.Proof{ - makeProof(t, assetID2Tranche1), - makeProof(t, assetID2Tranche2), - }, - allocations: []*Allocation{ - { - Type: CommitAllocationToLocal, - SplitRoot: true, - Amount: 1200, - }, - { - Type: CommitAllocationToRemote, - Amount: 1800, - OutputIndex: 1, - }, - }, - expectedInputs: map[asset.ID][]asset.ScriptKey{ - assetID2.ID(): { - assetID2Tranche1.ScriptKey, - assetID2Tranche2.ScriptKey, - }, - }, - expectedOutputs: map[asset.ID][]*tappsbt.VOutput{ - assetID2.ID(): { - { - Amount: 1200, - Type: split, - Interactive: true, - AnchorOutputIndex: 0, - }, - { - Amount: 1800, - Type: simple, - Interactive: true, - AnchorOutputIndex: 1, - }, - }, - }, - }, - { - name: "multiple assets, one consumed fully", - inputs: []*proof.Proof{ - makeProof(t, assetID1Tranche1), - makeProof(t, assetID2Tranche1), - }, - allocations: []*Allocation{ - { - Type: CommitAllocationToLocal, - SplitRoot: true, - Amount: 1050, - }, - { - Type: CommitAllocationToRemote, - Amount: 50, - OutputIndex: 1, - }, - }, - expectedInputs: map[asset.ID][]asset.ScriptKey{ - assetID1.ID(): { - assetID1Tranche1.ScriptKey, - }, - assetID2.ID(): { - assetID2Tranche1.ScriptKey, - }, - }, - expectedOutputs: map[asset.ID][]*tappsbt.VOutput{ - assetID1.ID(): { - { - Amount: 100, - Type: simple, - Interactive: true, - AnchorOutputIndex: 0, - }, - }, - assetID2.ID(): { - { - Amount: 950, - Type: split, - Interactive: true, - AnchorOutputIndex: 0, - }, - { - Amount: 50, - Type: simple, - Interactive: true, - AnchorOutputIndex: 1, - }, - }, - }, - }, - { - name: "lots of assets", - inputs: []*proof.Proof{ - makeProof(t, assetID1Tranche1), - makeProof(t, assetID1Tranche2), - makeProof(t, assetID1Tranche3), - makeProof(t, assetID2Tranche1), - makeProof(t, assetID2Tranche2), - makeProof(t, assetID2Tranche3), - makeProof(t, assetID3Tranche1), - makeProof(t, assetID3Tranche2), - makeProof(t, assetID3Tranche3), - }, - allocations: []*Allocation{ - { - Type: CommitAllocationToLocal, - SplitRoot: true, - Amount: 3600, - }, - { - Type: CommitAllocationToRemote, - Amount: 63000, - OutputIndex: 1, - }, - }, - expectedInputs: map[asset.ID][]asset.ScriptKey{ - assetID1.ID(): { - assetID1Tranche1.ScriptKey, - assetID1Tranche2.ScriptKey, - assetID1Tranche3.ScriptKey, - }, - assetID2.ID(): { - assetID2Tranche1.ScriptKey, - assetID2Tranche2.ScriptKey, - assetID2Tranche3.ScriptKey, - }, - assetID3.ID(): { - assetID3Tranche1.ScriptKey, - assetID3Tranche2.ScriptKey, - assetID3Tranche3.ScriptKey, - }, - }, - expectedOutputs: map[asset.ID][]*tappsbt.VOutput{ - assetID1.ID(): { - { - Amount: 600, - Type: simple, - Interactive: true, - AnchorOutputIndex: 0, - }, - }, - assetID2.ID(): { - { - Amount: 3000, - Type: split, - Interactive: true, - AnchorOutputIndex: 0, - }, - { - Amount: 3000, - Type: simple, - Interactive: true, - AnchorOutputIndex: 1, - }, - }, - assetID3.ID(): { - { - Amount: 60000, - Type: simple, - Interactive: true, - AnchorOutputIndex: 1, - }, - }, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - packets, err := DistributeCoins( - tc.inputs, tc.allocations, testParams, - ) - require.NoError(t, err) - - assertPackets( - t, packets, tc.expectedInputs, - tc.expectedOutputs, - ) - }) - } -} - -func assertPackets(t *testing.T, packets []*tappsbt.VPacket, - expectedInputs map[asset.ID][]asset.ScriptKey, - expectedOutputs map[asset.ID][]*tappsbt.VOutput) { - - for assetID, scriptKeys := range expectedInputs { - packetsByID := fn.Filter( - packets, func(p *tappsbt.VPacket) bool { - return p.Inputs[0].PrevID.ID == assetID - }, - ) - require.Len(t, packetsByID, 1) - - packet := packetsByID[0] - inputKeys := fn.Map( - packet.Inputs, - func(i *tappsbt.VInput) asset.ScriptKey { - pubKey, err := i.PrevID.ScriptKey.ToPubKey() - require.NoError(t, err) - - return asset.NewScriptKey(pubKey) - }, - ) - - require.Equal(t, scriptKeys, inputKeys) - - outputsByID := expectedOutputs[assetID] - require.Equal(t, len(outputsByID), len(packet.Outputs)) - for i, output := range packet.Outputs { - require.Equal(t, outputsByID[i], output) - } - } -} diff --git a/tapchannel/aux_closer.go b/tapchannel/aux_closer.go index eaff1ece3..b110ebd09 100644 --- a/tapchannel/aux_closer.go +++ b/tapchannel/aux_closer.go @@ -70,7 +70,7 @@ type assetCloseInfo struct { // allocations is the list of allocations for the remote+local party. // There'll be at most 4 of these: local+remote BTC outputs, // local+remote asset outputs. - allocations []*Allocation + allocations []*tapsend.Allocation // vPackets is the list of virtual packets that we'll use to anchor the // outputs. @@ -101,12 +101,11 @@ func NewAuxChanCloser(cfg AuxChanCloserCfg) *AuxChanCloser { } } -// createCloseAlloc is a helper function that creates an allocation for an -// asset close. -func createCloseAlloc(isLocal, isInitiator bool, closeAsset *asset.Asset, - shutdownMsg tapchannelmsg.AuxShutdownMsg) (*Allocation, error) { - - assetID := closeAsset.ID() +// createCloseAlloc is a helper function that creates an allocation for an asset +// close. This does not set a script key, as the script key will be set for each +// packet after the coins have been distributed. +func createCloseAlloc(isLocal, isInitiator bool, outputSum uint64, + shutdownMsg tapchannelmsg.AuxShutdownMsg) (*tapsend.Allocation, error) { // The sort pkScript for the allocation will just be the internal key, // mapped to a BIP 86 taproot output key. @@ -114,9 +113,16 @@ func createCloseAlloc(isLocal, isInitiator bool, closeAsset *asset.Asset, shutdownMsg.AssetInternalKey.Val, ).SerializeCompressed() - scriptKey, ok := shutdownMsg.ScriptKeys.Val[assetID] - if !ok { - return nil, fmt.Errorf("no script key for asset %v", assetID) + scriptKeyGen := func(assetID asset.ID) (asset.ScriptKey, error) { + var emptyKey asset.ScriptKey + + scriptKey, ok := shutdownMsg.ScriptKeys.Val[assetID] + if !ok { + return emptyKey, fmt.Errorf("no script key for asset "+ + "%v", assetID) + } + + return asset.NewScriptKey(&scriptKey), nil } var proofDeliveryUrl *url.URL @@ -132,18 +138,18 @@ func createCloseAlloc(isLocal, isInitiator bool, closeAsset *asset.Asset, "address: %w", err) } - return &Allocation{ - Type: func() AllocationType { + return &tapsend.Allocation{ + Type: func() tapsend.AllocationType { if isLocal { - return CommitAllocationToLocal + return tapsend.CommitAllocationToLocal } - return CommitAllocationToRemote + return tapsend.CommitAllocationToRemote }(), SplitRoot: isInitiator, InternalKey: shutdownMsg.AssetInternalKey.Val, - ScriptKey: asset.NewScriptKey(&scriptKey), - Amount: closeAsset.Amount, + GenScriptKey: scriptKeyGen, + Amount: outputSum, AssetVersion: asset.V0, BtcAmount: tapsend.DummyAmtSats, SortTaprootKeyBytes: sortKeyBytes, @@ -256,41 +262,38 @@ func (a *AuxChanCloser) AuxCloseOutputs( // outputs. We track the amount that'll go to the anchor assets, so we // can subtract this from the settled BTC amount. var ( - closeAllocs []*Allocation - localAlloc, remoteAlloc *Allocation + closeAllocs []*tapsend.Allocation + localAlloc, remoteAlloc *tapsend.Allocation localAssetAnchorAmt, remoteAssetAnchorAmt btcutil.Amount ) - for _, localAssetProof := range commitState.LocalAssets.Val.Outputs { - localAsset := localAssetProof.Proof.Val.Asset - - closeAlloc, err := createCloseAlloc( - true, desc.Initiator, &localAsset, localShutdown, + sumAmounts := func(accu uint64, o *tapchannelmsg.AssetOutput) uint64 { + return accu + o.Amount.Val + } + localSum := fn.Reduce(commitState.LocalAssets.Val.Outputs, sumAmounts) + remoteSum := fn.Reduce(commitState.RemoteAssets.Val.Outputs, sumAmounts) + if localSum > 0 { + localAlloc, err = createCloseAlloc( + true, desc.Initiator, localSum, localShutdown, ) if err != nil { return none, err } - localAlloc = closeAlloc - - localAssetAnchorAmt += closeAlloc.BtcAmount + localAssetAnchorAmt += localAlloc.BtcAmount - closeAllocs = append(closeAllocs, closeAlloc) + closeAllocs = append(closeAllocs, localAlloc) } - for _, remoteAssetProof := range commitState.RemoteAssets.Val.Outputs { - remoteAsset := remoteAssetProof.Proof.Val.Asset - - closeAlloc, err := createCloseAlloc( - false, !desc.Initiator, &remoteAsset, remoteShutdown, + if remoteSum > 0 { + remoteAlloc, err = createCloseAlloc( + false, !desc.Initiator, remoteSum, remoteShutdown, ) if err != nil { return none, err } - remoteAlloc = closeAlloc - - remoteAssetAnchorAmt += closeAlloc.BtcAmount + remoteAssetAnchorAmt += remoteAlloc.BtcAmount - closeAllocs = append(closeAllocs, closeAlloc) + closeAllocs = append(closeAllocs, remoteAlloc) } // Next, we'll create allocations for the (up to) two settled outputs @@ -317,8 +320,8 @@ func (a *AuxChanCloser) AuxCloseOutputs( // Snip off the first two bytes, as we'll be getting a P2TR // output from the higher level. We want a raw pubkey here. sortScript := o.PkScript[2:] - closeAllocs = append(closeAllocs, &Allocation{ - Type: AllocationTypeNoAssets, + closeAllocs = append(closeAllocs, &tapsend.Allocation{ + Type: tapsend.AllocationTypeNoAssets, BtcAmount: amtAfterAnchor, SortTaprootKeyBytes: sortScript, InternalKey: localShutdown.BtcInternalKey.Val, @@ -347,8 +350,8 @@ func (a *AuxChanCloser) AuxCloseOutputs( // Snip off the first two bytes, as we'll be getting a P2TR // output from the higher level. We want a raw pubkey here. sortScript := o.PkScript[2:] - closeAllocs = append(closeAllocs, &Allocation{ - Type: AllocationTypeNoAssets, + closeAllocs = append(closeAllocs, &tapsend.Allocation{ + Type: tapsend.AllocationTypeNoAssets, BtcAmount: amtAfterAnchor, SortTaprootKeyBytes: sortScript, InternalKey: remoteShutdown.BtcInternalKey.Val, @@ -361,15 +364,15 @@ func (a *AuxChanCloser) AuxCloseOutputs( // With all allocations created, we now sort them to ensure that we // have a stable and deterministic order that both parties can arrive // at. We then assign the output indexes according to that order. - InPlaceAllocationSort(closeAllocs) + tapsend.InPlaceAllocationSort(closeAllocs) for idx := range closeAllocs { closeAllocs[idx].OutputIndex = uint32(idx) } // Now that we have the complete set of allocations, we'll distribute // them to create the vPackets we'll need to anchor everything. - vPackets, err := DistributeCoins( - inputProofs, closeAllocs, a.cfg.ChainParams, + vPackets, err := tapsend.DistributeCoins( + inputProofs, closeAllocs, a.cfg.ChainParams, true, tappsbt.V1, ) if err != nil { return none, fmt.Errorf("unable to distribute coins: %w", err) @@ -414,7 +417,7 @@ func (a *AuxChanCloser) AuxCloseOutputs( return none, fmt.Errorf("unable to create output "+ "commitments: %w", err) } - err = AssignOutputCommitments(closeAllocs, outCommitments) + err = tapsend.AssignOutputCommitments(closeAllocs, outCommitments) if err != nil { return none, fmt.Errorf("unable to assign alloc output "+ "commitments: %w", err) @@ -432,11 +435,11 @@ func (a *AuxChanCloser) AuxCloseOutputs( // With the taproot keys updated, we know the pkScripts needed, so // we'll create the wallet option for the co-op close. var closeOutputs []lnwallet.CloseOutput - assetAllocations := fn.Filter(closeAllocs, FilterByTypeExclude( - AllocationTypeNoAssets, + assetAllocations := fn.Filter(closeAllocs, tapsend.FilterByTypeExclude( + tapsend.AllocationTypeNoAssets, )) for _, alloc := range assetAllocations { - pkScript, err := alloc.finalPkScript() + pkScript, err := alloc.FinalPkScript() if err != nil { return none, fmt.Errorf("unable to make final "+ "pkScript: %w", err) @@ -447,7 +450,7 @@ func (a *AuxChanCloser) AuxCloseOutputs( PkScript: pkScript, Value: int64(alloc.BtcAmount), }, - IsLocal: alloc.Type == CommitAllocationToLocal, + IsLocal: alloc.Type == tapsend.CommitAllocationToLocal, }) } @@ -682,7 +685,7 @@ func (a *AuxChanCloser) FinalizeClose(desc chancloser.AuxCloseDesc, for idx := range closeInfo.vPackets { vPkt := closeInfo.vPackets[idx] for outIdx := range vPkt.Outputs { - exclusionCreator := NonAssetExclusionProofs( + exclusionCreator := tapsend.NonAssetExclusionProofs( closeInfo.allocations, ) diff --git a/tapchannel/aux_funding_controller.go b/tapchannel/aux_funding_controller.go index c605f32e1..850f4c621 100644 --- a/tapchannel/aux_funding_controller.go +++ b/tapchannel/aux_funding_controller.go @@ -914,13 +914,16 @@ func (f *FundingController) sendInputOwnershipProofs(peerPub btcec.PublicKey, // We'll now send the signed inputs to the remote party. // // TODO(roasbeef): generalize for multi-asset - fundingAsset := vPkt.Outputs[0].Asset.Copy() + fundingOut, err := vPkt.FirstNonSplitRootOutput() + if err != nil { + return fmt.Errorf("unable to get funding asset: %w", err) + } assetOutputMsg := cmsg.NewTxAssetOutputProof( - fundingState.pid, *fundingAsset, true, + fundingState.pid, *fundingOut.Asset, true, ) log.Debugf("Sending TLV for funding asset output to remote party: %v", - limitSpewer.Sdump(fundingAsset)) + limitSpewer.Sdump(fundingOut.Asset)) err = f.cfg.PeerMessenger.SendMessage(ctx, peerPub, assetOutputMsg) if err != nil { @@ -1167,12 +1170,22 @@ func (f *FundingController) completeChannelFunding(ctx context.Context, fundingPackets := fundedVpkt.VPackets for idx := range fundingPackets { fundingPkt := fundingPackets[idx] - fundingPkt.Outputs[0].AnchorOutputBip32Derivation = nil - fundingPkt.Outputs[0].AnchorOutputTaprootBip32Derivation = nil + + // The funding output is the first non-split output (the split + // output is only present if there is change from the channel + // funding). + fundingOut, err := fundingPkt.FirstNonSplitRootOutput() + if err != nil { + return nil, fmt.Errorf("unable to find funding output "+ + "in funded packet: %w", err) + } + + fundingOut.AnchorOutputBip32Derivation = nil + fundingOut.AnchorOutputTaprootBip32Derivation = nil fundingInternalKeyDesc := keychain.KeyDescriptor{ PubKey: fundingInternalKey, } - fundingPkt.Outputs[0].SetAnchorInternalKey( + fundingOut.SetAnchorInternalKey( fundingInternalKeyDesc, f.cfg.ChainParams.HDCoinType, ) } @@ -1617,11 +1630,16 @@ func (f *FundingController) processFundingReq(fundingFlows fundingFlowIndex, // we can derive the tapscript root that'll be used alongside the // internal key (which we'll only learn from lnd later as we finalize // the funding PSBT). - fundingAssets := fn.Map( - fundingVpkt.VPackets, func(pkt *tappsbt.VPacket) *asset.Asset { - return pkt.Outputs[0].Asset.Copy() - }, - ) + fundingAssets := make([]*asset.Asset, 0, len(fundingVpkt.VPackets)) + for _, pkt := range fundingVpkt.VPackets { + fundingOut, err := pkt.FirstNonSplitRootOutput() + if err != nil { + return fmt.Errorf("unable to find funding output in "+ + "packet: %w", err) + } + + fundingAssets = append(fundingAssets, fundingOut.Asset.Copy()) + } fundingCommitVersion, err := tappsbt.CommitmentVersion( fundingVpkt.VPackets[0].Version, ) diff --git a/tapchannel/aux_sweeper.go b/tapchannel/aux_sweeper.go index 7de9abb14..6b5fac818 100644 --- a/tapchannel/aux_sweeper.go +++ b/tapchannel/aux_sweeper.go @@ -205,7 +205,7 @@ func (a *AuxSweeper) createSweepVpackets(sweepInputs []*cmsg.AssetOutput, return lfn.Err[returnType](err) } - allocs := make([]*Allocation, 0, len(sweepInputs)) + allocs := make([]*tapsend.Allocation, 0, len(sweepInputs)) ctx := context.Background() // If this is a second level HTLC sweep, then we already have @@ -233,38 +233,40 @@ func (a *AuxSweeper) createSweepVpackets(sweepInputs []*cmsg.AssetOutput, // Otherwise, for each out we want to sweep, we'll construct an // allocation that we'll use to deliver the funds back to the // wallet. - for _, localAsset := range sweepInputs { - // For each output, we'll need to create a new script - // key to use for the sweep transaction. + sweepAssetSum := tapchannelmsg.OutputSum(sweepInputs) + + // For this local allocation we'll need to create a new script + // key to use for the sweep transaction. + scriptKeyGen := func(asset.ID) (asset.ScriptKey, error) { + var emptyKey asset.ScriptKey + scriptKey, err := a.cfg.AddrBook.NextScriptKey( ctx, asset.TaprootAssetsKeyFamily, ) if err != nil { - return lfn.Err[[]*tappsbt.VPacket](err) + return emptyKey, err } - // With the script key created, we can make a new - // allocation that will be used to sweep the funds back - // to our wallet. - // - // We leave out the internal key here, as we'll make it - // later once we actually have the other set of inputs - // we need to sweep. - allocs = append(allocs, &Allocation{ - Type: CommitAllocationToLocal, - // We don't need to worry about sorting, as - // we'll always be the first output index in the - // transaction. - OutputIndex: 0, - Amount: localAsset.Amount.Val, - AssetVersion: asset.V1, - BtcAmount: tapsend.DummyAmtSats, - ScriptKey: scriptKey, - SortTaprootKeyBytes: schnorr.SerializePubKey( - scriptKey.PubKey, - ), - }) + return scriptKey, nil } + + // With the script key created, we can make a new allocation + // that will be used to sweep the funds back to our wallet. + // + // We leave out the internal key here, as we'll make it later + // once we actually have the other set of inputs we need to + // sweep. + allocs = append(allocs, &tapsend.Allocation{ + Type: tapsend.CommitAllocationToLocal, + // We don't need to worry about sorting, as + // we'll always be the first output index in the + // transaction. + OutputIndex: 0, + Amount: sweepAssetSum, + AssetVersion: asset.V1, + BtcAmount: tapsend.DummyAmtSats, + GenScriptKey: scriptKeyGen, + }) } log.Infof("Created %v allocations for commit tx sweep: %v", @@ -280,8 +282,8 @@ func (a *AuxSweeper) createSweepVpackets(sweepInputs []*cmsg.AssetOutput, // With the proofs constructed, we can now distribute the coins to // create the vPackets that we'll pass on to the next stage. - vPackets, err := DistributeCoins( - inputProofs, allocs, &a.cfg.ChainParams, + vPackets, err := tapsend.DistributeCoins( + inputProofs, allocs, &a.cfg.ChainParams, true, tappsbt.V1, ) if err != nil { return lfn.Errf[returnType]("error distributing coins: %w", err) @@ -1036,6 +1038,7 @@ func assetOutputToVPacket(fundingInputProofs map[asset.ID]*proof.Proof, } vPkt, err = tappsbt.FromProofs( []*proof.Proof{fundingInputProof}, chainParams, + tappsbt.V1, ) if err != nil { return fmt.Errorf("unable to create "+ @@ -1093,23 +1096,25 @@ func assetOutputToVPacket(fundingInputProofs map[asset.ID]*proof.Proof, // allocations for the anchor outputs. We'll use this later to create the proper // exclusion proofs. func anchorOutputAllocations( - keyRing *lnwallet.CommitmentKeyRing) lfn.Result[[]*Allocation] { + keyRing *lnwallet.CommitmentKeyRing) lfn.Result[[]*tapsend.Allocation] { + + anchorAlloc := func( + k *btcec.PublicKey) lfn.Result[*tapsend.Allocation] { - anchorAlloc := func(k *btcec.PublicKey) lfn.Result[*Allocation] { anchorTree, err := input.NewAnchorScriptTree(k) if err != nil { - return lfn.Err[*Allocation](err) + return lfn.Err[*tapsend.Allocation](err) } sibling, scriptTree, err := LeavesFromTapscriptScriptTree( anchorTree, ) if err != nil { - return lfn.Err[*Allocation](err) + return lfn.Err[*tapsend.Allocation](err) } - return lfn.Ok(&Allocation{ - Type: AllocationTypeNoAssets, + return lfn.Ok(&tapsend.Allocation{ + Type: tapsend.AllocationTypeNoAssets, Amount: 0, BtcAmount: lnwallet.AnchorSize, InternalKey: scriptTree.InternalKey, @@ -1123,9 +1128,10 @@ func anchorOutputAllocations( localAnchor := anchorAlloc(keyRing.ToLocalKey) remoteAnchor := anchorAlloc(keyRing.ToRemoteKey) + type resultType = lfn.Result[[]*tapsend.Allocation] return lfn.AndThen2( localAnchor, remoteAnchor, - func(a1, a2 *Allocation) lfn.Result[[]*Allocation] { + func(a1, a2 *tapsend.Allocation) resultType { // Before we return the anchors, we'll make sure that // they end up in the right sort order. scriptCompare := bytes.Compare( @@ -1140,7 +1146,7 @@ func anchorOutputAllocations( a1.OutputIndex = 1 } - return lfn.Ok([]*Allocation{a1, a2}) + return lfn.Ok([]*tapsend.Allocation{a1, a2}) }, ) } @@ -1617,7 +1623,7 @@ func (a *AuxSweeper) importCommitTx(req lnwallet.ResolutionReq, return fmt.Errorf("unable to create anchor "+ "allocations: %w", err) } - exclusionCreator := NonAssetExclusionProofs(anchorAllocations) + exclusionCreator := tapsend.NonAssetExclusionProofs(anchorAllocations) for idx := range vPkts { vPkt := vPkts[idx] for outIdx := range vPkt.Outputs { diff --git a/tapchannel/commitment.go b/tapchannel/commitment.go index 5dfd2932e..bf7ead093 100644 --- a/tapchannel/commitment.go +++ b/tapchannel/commitment.go @@ -479,8 +479,8 @@ func GenerateCommitmentAllocations(prevState *cmsg.Commitment, whoseCommit lntypes.ChannelParty, ourBalance, theirBalance lnwire.MilliSatoshi, originalView *lnwallet.HtlcView, chainParams *address.ChainParams, - keys lnwallet.CommitmentKeyRing) ([]*Allocation, *cmsg.Commitment, - error) { + keys lnwallet.CommitmentKeyRing) ([]*tapsend.Allocation, + *cmsg.Commitment, error) { log.Tracef("Generating allocations, whoseCommit=%v, ourBalance=%d, "+ "theirBalance=%d", whoseCommit, ourBalance, theirBalance) @@ -555,7 +555,9 @@ func GenerateCommitmentAllocations(prevState *cmsg.Commitment, // Now we can distribute the inputs according to the allocations. This // creates a virtual packet for each distinct asset ID that is committed // to the channel. - vPackets, err := DistributeCoins(inputProofs, allocations, chainParams) + vPackets, err := tapsend.DistributeCoins( + inputProofs, allocations, chainParams, true, tappsbt.V1, + ) if err != nil { return nil, nil, fmt.Errorf("unable to distribute coins: %w", err) @@ -607,7 +609,7 @@ func GenerateCommitmentAllocations(prevState *cmsg.Commitment, // The output commitment is all we need to create the auxiliary leaves. // We map the output commitments (which are keyed by on-chain output // index) back to the allocation. - err = AssignOutputCommitments(allocations, outCommitments) + err = tapsend.AssignOutputCommitments(allocations, outCommitments) if err != nil { return nil, nil, fmt.Errorf("unable to assign alloc output "+ "commitments: %w", err) @@ -631,7 +633,9 @@ func GenerateCommitmentAllocations(prevState *cmsg.Commitment, for outIdx := range vPkt.Outputs { proofSuffix, err := tapsend.CreateProofSuffixCustom( fakeCommitTx, vPkt, outCommitments, outIdx, - vPackets, NonAssetExclusionProofs(allocations), + vPackets, tapsend.NonAssetExclusionProofs( + allocations, + ), ) if err != nil { return nil, nil, fmt.Errorf("unable to create "+ @@ -661,7 +665,7 @@ func CreateAllocations(chanState lnwallet.AuxChanState, ourBalance, wantLocalCommitAnchor, wantRemoteCommitAnchor bool, filteredView *DecodedView, whoseCommit lntypes.ChannelParty, keys lnwallet.CommitmentKeyRing, - nonAssetView *DecodedView) ([]*Allocation, error) { + nonAssetView *DecodedView) ([]*tapsend.Allocation, error) { log.Tracef("Creating allocations, whoseCommit=%v, initiator=%v, "+ "ourBalance=%d, theirBalance=%d, ourAssetBalance=%d, "+ @@ -683,8 +687,8 @@ func CreateAllocations(chanState lnwallet.AuxChanState, ourBalance, len(filteredView.TheirUpdates) + len(nonAssetView.OurUpdates) + len(nonAssetView.TheirUpdates) + 4 - allocations = make([]*Allocation, 0, numAllocations) - addAlloc = func(a *Allocation) { + allocations = make([]*tapsend.Allocation, 0, numAllocations) + addAlloc = func(a *tapsend.Allocation) { allocations = append(allocations, a) } ) @@ -771,9 +775,9 @@ func CreateAllocations(chanState lnwallet.AuxChanState, ourBalance, haveHtlcSplitRoot = true } - allocType := CommitAllocationHtlcOutgoing + allocType := tapsend.CommitAllocationHtlcOutgoing if isIncoming { - allocType = CommitAllocationHtlcIncoming + allocType = tapsend.CommitAllocationHtlcIncoming } // If HTLC is dust, do not create allocation for it. @@ -803,7 +807,18 @@ func CreateAllocations(chanState lnwallet.AuxChanState, ourBalance, schnorr.SerializePubKey(htlcTree.TaprootKey), schnorr.SerializePubKey(tweakedTree.TaprootKey)) - allocations = append(allocations, &Allocation{ + scriptKey := asset.ScriptKey{ + PubKey: asset.NewScriptKey( + tweakedTree.TaprootKey, + ).PubKey, + TweakedScriptKey: &asset.TweakedScriptKey{ + RawKey: keychain.KeyDescriptor{ + PubKey: tweakedTree.InternalKey, + }, + Tweak: tweakedTree.TapscriptRoot, + }, + } + allocations = append(allocations, &tapsend.Allocation{ Type: allocType, Amount: rfqmsg.Sum(htlc.AssetBalances), AssetVersion: asset.V1, @@ -811,24 +826,14 @@ func CreateAllocations(chanState lnwallet.AuxChanState, ourBalance, BtcAmount: htlc.Amount.ToSatoshis(), InternalKey: htlcTree.InternalKey, NonAssetLeaves: sibling, - ScriptKey: asset.ScriptKey{ - PubKey: asset.NewScriptKey( - tweakedTree.TaprootKey, - ).PubKey, - TweakedScriptKey: &asset.TweakedScriptKey{ - RawKey: keychain.KeyDescriptor{ - PubKey: tweakedTree.InternalKey, - }, - Tweak: tweakedTree.TapscriptRoot, - }, - }, + GenScriptKey: tapsend.StaticScriptKeyGen(scriptKey), SortTaprootKeyBytes: schnorr.SerializePubKey( // This _must_ remain the non-tweaked key, since // this is used for sorting _before_ applying // any TAP tweaks. htlcTree.TaprootKey, ), - CLTV: htlc.Timeout, + SortCLTV: htlc.Timeout, HtlcIndex: htlc.HtlcIndex, }) @@ -882,15 +887,15 @@ func CreateAllocations(chanState lnwallet.AuxChanState, ourBalance, return nil } - allocations = append(allocations, &Allocation{ - Type: AllocationTypeNoAssets, + allocations = append(allocations, &tapsend.Allocation{ + Type: tapsend.AllocationTypeNoAssets, BtcAmount: htlc.Amount.ToSatoshis(), InternalKey: htlcTree.InternalKey, NonAssetLeaves: sibling, SortTaprootKeyBytes: schnorr.SerializePubKey( htlcTree.TaprootKey, ), - CLTV: htlc.Timeout, + SortCLTV: htlc.Timeout, HtlcIndex: htlc.HtlcIndex, }) @@ -916,7 +921,7 @@ func CreateAllocations(chanState lnwallet.AuxChanState, ourBalance, // With all allocations created, we now sort them to ensure that we have // a stable and deterministic order that both parties can arrive at. We // then assign the output indexes according to that order. - InPlaceAllocationSort(allocations) + tapsend.InPlaceAllocationSort(allocations) for idx := range allocations { allocations[idx].OutputIndex = uint32(idx) } @@ -932,7 +937,7 @@ func addCommitmentOutputs(chanType channeldb.ChannelType, localChanCfg, theirBalance btcutil.Amount, ourAssetBalance, theirAssetBalance uint64, wantLocalCommitAnchor, wantRemoteCommitAnchor bool, keys lnwallet.CommitmentKeyRing, leaseExpiry uint32, - addAllocation func(a *Allocation)) error { + addAllocation func(a *tapsend.Allocation)) error { // Start with the commitment anchor outputs. localAnchor, remoteAnchor, err := lnwallet.CommitScriptAnchors( @@ -951,9 +956,9 @@ func addCommitmentOutputs(chanType channeldb.ChannelType, localChanCfg, "sibling: %w", err) } - addAllocation(&Allocation{ + addAllocation(&tapsend.Allocation{ // Commitment anchor outputs never carry assets. - Type: AllocationTypeNoAssets, + Type: tapsend.AllocationTypeNoAssets, Amount: 0, BtcAmount: lnwallet.AnchorSize, InternalKey: scriptTree.InternalKey, @@ -972,9 +977,9 @@ func addCommitmentOutputs(chanType channeldb.ChannelType, localChanCfg, "script sibling: %w", err) } - addAllocation(&Allocation{ + addAllocation(&tapsend.Allocation{ // Commitment anchor outputs never carry assets. - Type: AllocationTypeNoAssets, + Type: tapsend.AllocationTypeNoAssets, Amount: 0, BtcAmount: lnwallet.AnchorSize, InternalKey: scriptTree.InternalKey, @@ -1006,25 +1011,26 @@ func addCommitmentOutputs(chanType channeldb.ChannelType, localChanCfg, "sibling: %w", err) } - allocation := &Allocation{ - Type: CommitAllocationToLocal, + scriptKey := asset.ScriptKey{ + PubKey: asset.NewScriptKey( + toLocalTree.TaprootKey, + ).PubKey, + TweakedScriptKey: &asset.TweakedScriptKey{ + RawKey: keychain.KeyDescriptor{ + PubKey: toLocalTree.InternalKey, + }, + Tweak: toLocalTree.TapscriptRoot, + }, + } + allocation := &tapsend.Allocation{ + Type: tapsend.CommitAllocationToLocal, Amount: ourAssetBalance, AssetVersion: asset.V1, SplitRoot: initiator, BtcAmount: ourBalance, InternalKey: toLocalTree.InternalKey, NonAssetLeaves: sibling, - ScriptKey: asset.ScriptKey{ - PubKey: asset.NewScriptKey( - toLocalTree.TaprootKey, - ).PubKey, - TweakedScriptKey: &asset.TweakedScriptKey{ - RawKey: keychain.KeyDescriptor{ - PubKey: toLocalTree.InternalKey, - }, - Tweak: toLocalTree.TapscriptRoot, - }, - }, + GenScriptKey: tapsend.StaticScriptKeyGen(scriptKey), SortTaprootKeyBytes: schnorr.SerializePubKey( toLocalTree.TaprootKey, ), @@ -1033,12 +1039,12 @@ func addCommitmentOutputs(chanType channeldb.ChannelType, localChanCfg, // If there are no assets, only BTC (for example due to a push // amount), the allocation looks simpler. if ourAssetBalance == 0 { - allocation = &Allocation{ - Type: AllocationTypeNoAssets, + allocation = &tapsend.Allocation{ + Type: tapsend.AllocationTypeNoAssets, BtcAmount: ourBalance, InternalKey: toLocalTree.InternalKey, NonAssetLeaves: sibling, - ScriptKey: asset.NewScriptKey( + GenScriptKey: tapsend.StaticScriptPubKeyGen( toLocalTree.TaprootKey, ), SortTaprootKeyBytes: schnorr.SerializePubKey( @@ -1068,26 +1074,26 @@ func addCommitmentOutputs(chanType channeldb.ChannelType, localChanCfg, "sibling: %w", err) } - allocation := &Allocation{ - Type: CommitAllocationToRemote, + scriptKey := asset.ScriptKey{ + PubKey: asset.NewScriptKey( + toRemoteTree.TaprootKey, + ).PubKey, + TweakedScriptKey: &asset.TweakedScriptKey{ + RawKey: keychain.KeyDescriptor{ + PubKey: toRemoteTree.InternalKey, + }, + Tweak: toRemoteTree.TapscriptRoot, + }, + } + allocation := &tapsend.Allocation{ + Type: tapsend.CommitAllocationToRemote, Amount: theirAssetBalance, AssetVersion: asset.V1, SplitRoot: !initiator, BtcAmount: theirBalance, InternalKey: toRemoteTree.InternalKey, NonAssetLeaves: sibling, - ScriptKey: asset.ScriptKey{ - PubKey: asset.NewScriptKey( - toRemoteTree.TaprootKey, - ).PubKey, - TweakedScriptKey: &asset.TweakedScriptKey{ - RawKey: keychain.KeyDescriptor{ - //nolint:lll - PubKey: toRemoteTree.InternalKey, - }, - Tweak: toRemoteTree.TapscriptRoot, - }, - }, + GenScriptKey: tapsend.StaticScriptKeyGen(scriptKey), SortTaprootKeyBytes: schnorr.SerializePubKey( toRemoteTree.TaprootKey, ), @@ -1096,12 +1102,12 @@ func addCommitmentOutputs(chanType channeldb.ChannelType, localChanCfg, // If there are no assets, only BTC (for example due to a push // amount), the allocation looks simpler. if theirAssetBalance == 0 { - allocation = &Allocation{ - Type: AllocationTypeNoAssets, + allocation = &tapsend.Allocation{ + Type: tapsend.AllocationTypeNoAssets, BtcAmount: theirBalance, InternalKey: toRemoteTree.InternalKey, NonAssetLeaves: sibling, - ScriptKey: asset.NewScriptKey( + GenScriptKey: tapsend.StaticScriptPubKeyGen( toRemoteTree.TaprootKey, ), SortTaprootKeyBytes: schnorr.SerializePubKey( @@ -1141,7 +1147,7 @@ func LeavesFromTapscriptScriptTree( } // ToCommitment converts the allocations to a Commitment struct. -func ToCommitment(allocations []*Allocation, +func ToCommitment(allocations []*tapsend.Allocation, vPackets []*tappsbt.VPacket) (*cmsg.Commitment, error) { var ( @@ -1154,7 +1160,9 @@ func ToCommitment(allocations []*Allocation, // Start with the to_local output. There should be at most one of these // outputs. - toLocal := fn.Filter(allocations, FilterByType(CommitAllocationToLocal)) + toLocal := fn.Filter(allocations, tapsend.FilterByType( + tapsend.CommitAllocationToLocal, + )) switch { case len(toLocal) > 1: return nil, fmt.Errorf("expected at most one to local output, "+ @@ -1176,9 +1184,9 @@ func ToCommitment(allocations []*Allocation, } // The same for the to_remote, at most one should exist. - toRemote := fn.Filter( - allocations, FilterByType(CommitAllocationToRemote), - ) + toRemote := fn.Filter(allocations, tapsend.FilterByType( + tapsend.CommitAllocationToRemote, + )) switch { case len(toRemote) > 1: return nil, fmt.Errorf("expected at most one to remote "+ @@ -1199,9 +1207,9 @@ func ToCommitment(allocations []*Allocation, } } - outgoing := fn.Filter( - allocations, FilterByType(CommitAllocationHtlcOutgoing), - ) + outgoing := fn.Filter(allocations, tapsend.FilterByType( + tapsend.CommitAllocationHtlcOutgoing, + )) for _, a := range outgoing { htlcLeaf, err := a.AuxLeaf() if err != nil { @@ -1230,9 +1238,9 @@ func ToCommitment(allocations []*Allocation, } } - incoming := fn.Filter( - allocations, FilterByType(CommitAllocationHtlcIncoming), - ) + incoming := fn.Filter(allocations, tapsend.FilterByType( + tapsend.CommitAllocationHtlcIncoming, + )) for _, a := range incoming { htlcLeaf, err := a.AuxLeaf() if err != nil { @@ -1269,7 +1277,7 @@ func ToCommitment(allocations []*Allocation, // collectOutputs collects all virtual transaction outputs for a given // allocation from the given packets. -func collectOutputs(a *Allocation, +func collectOutputs(a *tapsend.Allocation, allPackets []*tappsbt.VPacket) ([]*cmsg.AssetOutput, error) { var outputs []*cmsg.AssetOutput @@ -1303,7 +1311,7 @@ func createSecondLevelHtlcAllocations(chanType channeldb.ChannelType, initiator bool, htlcOutputs []*cmsg.AssetOutput, htlcAmt btcutil.Amount, commitCsvDelay uint32, keys lnwallet.CommitmentKeyRing, outputIndex fn.Option[uint32], htlcTimeout fn.Option[uint32], - htlcIndex uint64) ([]*Allocation, error) { + htlcIndex uint64) ([]*tapsend.Allocation, error) { // TODO(roasbeef): thaw height not implemented for taproot chans rn // (lease expiry) @@ -1338,8 +1346,8 @@ func createSecondLevelHtlcAllocations(chanType channeldb.ChannelType, schnorr.SerializePubKey(htlcTree.TaprootKey), schnorr.SerializePubKey(tweakedTree.TaprootKey)) - allocations := []*Allocation{{ - Type: SecondLevelHtlcAllocation, + allocations := []*tapsend.Allocation{{ + Type: tapsend.SecondLevelHtlcAllocation, // If we're making the second-level transaction just to sign, // then we'll have an output index of zero. Otherwise, we'll // want to use the output index as appears in the final @@ -1353,13 +1361,15 @@ func createSecondLevelHtlcAllocations(chanType channeldb.ChannelType, ), InternalKey: htlcTree.InternalKey, NonAssetLeaves: sibling, - ScriptKey: asset.NewScriptKey(tweakedTree.TaprootKey), + GenScriptKey: tapsend.StaticScriptPubKeyGen( + tweakedTree.TaprootKey, + ), SortTaprootKeyBytes: schnorr.SerializePubKey( // This _must_ remain the non-tweaked key, since this is // used for sorting _before_ applying any TAP tweaks. htlcTree.TaprootKey, ), - CLTV: htlcTimeout.UnwrapOr(0), + SortCLTV: htlcTimeout.UnwrapOr(0), HtlcIndex: htlcIndex, }} @@ -1372,7 +1382,7 @@ func CreateSecondLevelHtlcPackets(chanState lnwallet.AuxChanState, commitTx *wire.MsgTx, htlcAmt btcutil.Amount, keys lnwallet.CommitmentKeyRing, chainParams *address.ChainParams, htlcOutputs []*cmsg.AssetOutput, htlcTimeout fn.Option[uint32], - htlcIndex uint64) ([]*tappsbt.VPacket, []*Allocation, error) { + htlcIndex uint64) ([]*tappsbt.VPacket, []*tapsend.Allocation, error) { allocations, err := createSecondLevelHtlcAllocations( chanState.ChanType, chanState.IsInitiator, @@ -1391,7 +1401,9 @@ func CreateSecondLevelHtlcPackets(chanState lnwallet.AuxChanState, }, ) - vPackets, err := DistributeCoins(inputProofs, allocations, chainParams) + vPackets, err := tapsend.DistributeCoins( + inputProofs, allocations, chainParams, true, tappsbt.V1, + ) if err != nil { return nil, nil, fmt.Errorf("error distributing coins: %w", err) } @@ -1445,7 +1457,7 @@ func CreateSecondLevelHtlcTx(chanState lnwallet.AuxChanState, // The output commitment is all we need to create the auxiliary leaves. // We map the output commitments (which are keyed by on-chain output // index) back to the allocation. - err = AssignOutputCommitments(allocations, outCommitments) + err = tapsend.AssignOutputCommitments(allocations, outCommitments) if err != nil { return none, fmt.Errorf("unable to assign output commitments: "+ "%w", err) @@ -1464,7 +1476,7 @@ func CreateSecondLevelHtlcTx(chanState lnwallet.AuxChanState, // FakeCommitTx creates a fake commitment on-chain transaction from the given // funding outpoint and allocations. The transaction is not signed. func FakeCommitTx(fundingOutpoint wire.OutPoint, - allocations []*Allocation) (*wire.MsgTx, error) { + allocations []*tapsend.Allocation) (*wire.MsgTx, error) { fakeCommitTx := wire.NewMsgTx(2) fakeCommitTx.TxIn = []*wire.TxIn{ @@ -1475,7 +1487,7 @@ func FakeCommitTx(fundingOutpoint wire.OutPoint, fakeCommitTx.TxOut = make([]*wire.TxOut, len(allocations)) for _, a := range allocations { - pkScript, err := a.finalPkScript() + pkScript, err := a.FinalPkScript() if err != nil { return nil, fmt.Errorf("error getting final pk "+ "script: %w", err) @@ -1495,7 +1507,8 @@ func FakeCommitTx(fundingOutpoint wire.OutPoint, // the allocation's OutputIndex. The transaction inputs are sorted by the // default BIP69 sort. func InPlaceCustomCommitSort(tx *wire.MsgTx, cltvs []uint32, - htlcIndexes []input.HtlcIndex, allocations []*Allocation) error { + htlcIndexes []input.HtlcIndex, + allocations []*tapsend.Allocation) error { if len(tx.TxOut) != len(allocations) { return fmt.Errorf("output and allocation size mismatch") @@ -1515,7 +1528,7 @@ func InPlaceCustomCommitSort(tx *wire.MsgTx, cltvs []uint32, newCltvs := make([]uint32, len(cltvs)) for i, original := range txOutOriginal { - var allocation *Allocation + var allocation *tapsend.Allocation for _, a := range allocations { match, err := a.MatchesOutput( original.PkScript, original.Value, cltvs[i], diff --git a/tapfreighter/fund.go b/tapfreighter/fund.go index eb2f6396f..41130f588 100644 --- a/tapfreighter/fund.go +++ b/tapfreighter/fund.go @@ -13,10 +13,10 @@ import ( "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/commitment" - "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/proof" "github.com/lightninglabs/taproot-assets/tappsbt" "github.com/lightninglabs/taproot-assets/tapsend" + "golang.org/x/exp/maps" ) // createFundedPacketWithInputs funds a set of virtual transaction with the @@ -25,80 +25,139 @@ import ( // single asset ID/tranche or group key with multiple tranches). func createFundedPacketWithInputs(ctx context.Context, exporter proof.Exporter, keyRing KeyRing, addrBook AddrBook, fundDesc *tapsend.FundingDescriptor, - vPkt *tappsbt.VPacket, + vPktTemplate *tappsbt.VPacket, selectedCommitments []*AnchoredCommitment) (*FundedVPacket, error) { - if vPkt.ChainParams == nil { + if vPktTemplate.ChainParams == nil { return nil, errors.New("chain params not set in virtual packet") } + chainParams := vPktTemplate.ChainParams log.Infof("Selected %v asset inputs for send of %d to %s", len(selectedCommitments), fundDesc.Amount, &fundDesc.AssetSpecifier) - assetType := selectedCommitments[0].Asset.Type - - totalInputAmt := uint64(0) + var inputSum uint64 + inputProofs := make( + map[asset.PrevID]*proof.Proof, len(selectedCommitments), + ) + selectedCommitmentsByPrevID := make( + map[asset.PrevID]*AnchoredCommitment, len(selectedCommitments), + ) for _, anchorAsset := range selectedCommitments { - // We only use the sum of all assets of the same TAP commitment - // key to avoid counting passive assets as well. We'll filter - // out the passive assets from the selected commitments in a - // later step. + // We only use the inputs of assets of the same TAP commitment + // as we want to fund for. These are the active assets that + // we're going to distribute. All other assets are passive and + // will be detected and added later. if anchorAsset.Asset.TapCommitmentKey() != fundDesc.TapCommitmentKey() { continue } - totalInputAmt += anchorAsset.Asset.Amount + // We'll also include an inclusion proof for the input asset in + // the virtual transaction. With that a signer can verify that + // the asset was actually committed to in the anchor output. + inputProof, err := fetchInputProof( + ctx, exporter, anchorAsset.Asset, + anchorAsset.AnchorPoint, + ) + if err != nil { + return nil, fmt.Errorf("error fetching input proof: %w", + err) + } + + inputSum += anchorAsset.Asset.Amount + inputProofs[anchorAsset.PrevID()] = inputProof + selectedCommitmentsByPrevID[anchorAsset.PrevID()] = anchorAsset } - inputCommitments, err := setVPacketInputs( - ctx, exporter, selectedCommitments, vPkt, - ) + // We try to identify and annotate any script keys in the template that + // might be ours. + err := annotateLocalScriptKeys(ctx, vPktTemplate, addrBook) if err != nil { - return nil, err + return nil, fmt.Errorf("error annotating local script "+ + "keys: %w", err) } - fullValue, err := tapsend.ValidateInputs( - inputCommitments, assetType, fundDesc.AssetSpecifier, - fundDesc.Amount, + allocations, interactive, err := tapsend.AllocationsFromTemplate( + vPktTemplate, inputSum, ) if err != nil { - return nil, err + return nil, fmt.Errorf("error extracting allocations: %w", err) } - // Make sure we'll recognize local script keys in the virtual packet - // later on in the process by annotating them with the full descriptor - // information. - if err := annotateLocalScriptKeys(ctx, vPkt, addrBook); err != nil { - return nil, err + allPackets, err := tapsend.DistributeCoins( + maps.Values(inputProofs), allocations, chainParams, interactive, + vPktTemplate.Version, + ) + if err != nil { + return nil, fmt.Errorf("unable to distribute coins: %w", err) } - // If we don't spend the full value, we need to create a change output. - changeAmount := totalInputAmt - fundDesc.Amount - err = createChangeOutput(ctx, vPkt, keyRing, fullValue, changeAmount) - if err != nil { - return nil, err + // Add all the input information to the virtual packets and also make + // sure we have proper change output keys for non-zero change outputs. + for _, vPkt := range allPackets { + for idx := range vPkt.Inputs { + prevID := vPkt.Inputs[idx].PrevID + assetInput, ok := selectedCommitmentsByPrevID[prevID] + if !ok { + return nil, fmt.Errorf("input commitment not "+ + "found for prevID %v", prevID) + } + + inputProof, ok := inputProofs[prevID] + if !ok { + return nil, fmt.Errorf("input proof not found "+ + "for prevID %v", prevID) + } + + err = createAndSetInput( + vPkt, idx, assetInput, inputProof, + ) + if err != nil { + return nil, fmt.Errorf("unable to create and "+ + "set input: %w", err) + } + } + + err = deriveChangeOutputKey(ctx, vPkt, keyRing) + if err != nil { + return nil, fmt.Errorf("unable to derive change "+ + "output key: %w", err) + } } // Before we can prepare output assets for our send, we need to generate // a new internal key for the anchor outputs. We assume any output that // hasn't got an internal key set is going to a local anchor, and we // provide the internal key for that. - packets := []*tappsbt.VPacket{vPkt} - err = generateOutputAnchorInternalKeys(ctx, packets, keyRing) + err = generateOutputAnchorInternalKeys(ctx, allPackets, keyRing) if err != nil { return nil, fmt.Errorf("unable to generate output anchor "+ "internal keys: %w", err) } - if err := tapsend.PrepareOutputAssets(ctx, vPkt); err != nil { - return nil, fmt.Errorf("unable to prepare outputs: %w", err) + for _, vPkt := range allPackets { + if err := tapsend.PrepareOutputAssets(ctx, vPkt); err != nil { + log.Errorf("Error preparing output assets: %v, "+ + "packets: %v", err, limitSpewer.Sdump(vPkt)) + return nil, fmt.Errorf("unable to prepare outputs: %w", + err) + } + } + + // Extract just the TAP commitments by input from the selected anchored + // commitments. + inputCommitments := make( + tappsbt.InputCommitments, len(selectedCommitmentsByPrevID), + ) + for prevID, anchorAsset := range selectedCommitmentsByPrevID { + inputCommitments[prevID] = anchorAsset.Commitment } return &FundedVPacket{ - VPackets: packets, + VPackets: allPackets, InputCommitments: inputCommitments, }, nil } @@ -138,56 +197,35 @@ func annotateLocalScriptKeys(ctx context.Context, vPkt *tappsbt.VPacket, return nil } -// createChangeOutput creates a change output for the given virtual packet if -// it isn't fully spent. -func createChangeOutput(ctx context.Context, vPkt *tappsbt.VPacket, - keyRing KeyRing, fullValue bool, changeAmount uint64) error { - - // If we're spending the full value, we don't need a change output. We - // currently assume that if it's a full-value non-interactive spend that - // the packet was created with the correct function in the tappsbt - // packet that adds the NUMS script key output for the tombstone. If - // the user doesn't set that, then an error will be returned from the - // tapsend.PrepareOutputAssets function. But we should probably change - // that and allow the user to specify a minimum packet template and add - // whatever else is needed to it automatically. - if fullValue { +// deriveChangeOutputKey makes sure the change output has a proper key that goes +// back to the local node, assuming there is a change output and it isn't a +// zero-value tombstone. +func deriveChangeOutputKey(ctx context.Context, vPkt *tappsbt.VPacket, + keyRing KeyRing) error { + + // If we don't have a split output then there's no change. + if !vPkt.HasSplitRootOutput() { return nil } - // We expect some change back, or have passive assets to commit to, so - // let's make sure we create a transfer output. changeOut, err := vPkt.SplitRootOutput() if err != nil { - lastOut := vPkt.Outputs[len(vPkt.Outputs)-1] - splitOutIndex := lastOut.AnchorOutputIndex + 1 - changeOut = &tappsbt.VOutput{ - Type: tappsbt.TypeSplitRoot, - Interactive: lastOut.Interactive, - AnchorOutputIndex: splitOutIndex, - - // We want to handle deriving a real key in a - // generic manner, so we'll do that just below. - ScriptKey: asset.NUMSScriptKey, - } - - vPkt.Outputs = append(vPkt.Outputs, changeOut) + return err } - // Since we know we're going to receive some change back, we - // need to make sure it is going to an address that we control. - // This should only be the case where we create the default - // change output with the NUMS key to avoid deriving too many - // keys prematurely. We don't need to derive a new key if we - // only have passive assets to commit to, since they all have - // their own script key and the output is more of a placeholder - // to attach the passive assets to. + // Since we know we're going to receive some change back, we need to + // make sure it is going to an address that we control. This should only + // be the case where we create the default change output with the NUMS + // key to avoid deriving too many keys prematurely. We don't need to + // derive a new key if we only have passive assets to commit to, since + // they all have their own script key and the output is more of a + // placeholder to attach the passive assets to. unSpendable, err := changeOut.ScriptKey.IsUnSpendable() if err != nil { return fmt.Errorf("cannot determine if script key is "+ "spendable: %w", err) } - if unSpendable { + if unSpendable && changeOut.Amount > 0 { changeScriptKey, err := keyRing.DeriveNextKey( ctx, asset.TaprootAssetsKeyFamily, ) @@ -202,30 +240,6 @@ func createChangeOutput(ctx context.Context, vPkt *tappsbt.VPacket, ) } - // For existing change outputs, we'll just update the amount - // since we might not have known what coin would've been - // selected and how large the change would turn out to be. - changeOut.Amount = changeAmount - - // The asset version of the output should be the max of the set - // of input versions. We need to set this now as in - // PrepareOutputAssets locators are created which includes the - // version from the vOut. If we don't set it here, a v1 asset - // spent that becomes change will be a v0 if combined with such - // inputs. - // - // TODO(roasbeef): remove as not needed? - maxVersion := func(maxVersion asset.Version, - vInput *tappsbt.VInput) asset.Version { - - if vInput.Asset().Version > maxVersion { - return vInput.Asset().Version - } - - return maxVersion - } - changeOut.AssetVersion = fn.Reduce(vPkt.Inputs, maxVersion) - return nil } @@ -357,53 +371,6 @@ func generateOutputAnchorInternalKeys(ctx context.Context, return nil } -// setVPacketInputs sets the inputs of the given vPkt to the given send eligible -// commitments. It also returns the assets that were used as inputs. -func setVPacketInputs(ctx context.Context, exporter proof.Exporter, - eligibleCommitments []*AnchoredCommitment, - vPkt *tappsbt.VPacket) (tappsbt.InputCommitments, error) { - - vPkt.Inputs = make([]*tappsbt.VInput, len(eligibleCommitments)) - inputCommitments := make(tappsbt.InputCommitments) - - for idx := range eligibleCommitments { - // If the key found for the input UTXO cannot be identified as - // belonging to the lnd wallet, we won't be able to sign for it. - // This would happen if a user manually imported an asset that - // was issued/received for/on another node. We should probably - // not create asset entries for such imported assets in the - // first place, as we won't be able to spend it anyway. But for - // now we just put this check in place. - assetInput := eligibleCommitments[idx] - - // We'll also include an inclusion proof for the input asset in - // the virtual transaction. With that a signer can verify that - // the asset was actually committed to in the anchor output. - inputProof, err := fetchInputProof( - ctx, exporter, assetInput.Asset, assetInput.AnchorPoint, - ) - if err != nil { - return nil, fmt.Errorf("error fetching input proof: %w", - err) - } - - // Create the virtual packet input including the chain anchor - // information. - err = createAndSetInput( - vPkt, idx, assetInput, inputProof, - ) - if err != nil { - return nil, fmt.Errorf("unable to create and set "+ - "input: %w", err) - } - - prevID := vPkt.Inputs[idx].PrevID - inputCommitments[prevID] = assetInput.Commitment - } - - return inputCommitments, nil -} - // createAndSetInput creates a virtual packet input for the given asset input // and sets it on the given virtual packet. func createAndSetInput(vPkt *tappsbt.VPacket, idx int, @@ -473,15 +440,8 @@ func createAndSetInput(vPkt *tappsbt.VPacket, idx int, // At this point, we have a valid "coin" to spend in the commitment, so // we'll add the relevant information to the virtual TX's input. - prevID := asset.PrevID{ - OutPoint: assetInput.AnchorPoint, - ID: assetInput.Asset.ID(), - ScriptKey: asset.ToSerialized( - assetInput.Asset.ScriptKey.PubKey, - ), - } vPkt.Inputs[idx] = &tappsbt.VInput{ - PrevID: prevID, + PrevID: assetInput.PrevID(), Anchor: tappsbt.Anchor{ Value: assetInput.AnchorOutputValue, PkScript: anchorPkScript, diff --git a/tapfreighter/fund_test.go b/tapfreighter/fund_test.go index ed200cf5d..04447a8f1 100644 --- a/tapfreighter/fund_test.go +++ b/tapfreighter/fund_test.go @@ -16,6 +16,7 @@ import ( "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/commitment" + "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/internal/test" "github.com/lightninglabs/taproot-assets/proof" "github.com/lightninglabs/taproot-assets/tapgarden" @@ -32,13 +33,20 @@ var ( ) type mockExporter struct { - singleProof proof.Proof + proofs []*proof.Proof } -func (m *mockExporter) FetchProof(context.Context, - proof.Locator) (proof.Blob, error) { +func (m *mockExporter) FetchProof(_ context.Context, + loc proof.Locator) (proof.Blob, error) { - f, err := proof.NewFile(proof.V0, m.singleProof) + singleProof, err := fn.First(m.proofs, func(p *proof.Proof) bool { + return p.Asset.ScriptKey.PubKey.IsEqual(&loc.ScriptKey) + }) + if err != nil { + return nil, err + } + + f, err := proof.NewFile(proof.V0, *singleProof) if err != nil { return nil, err } @@ -81,8 +89,8 @@ func (m *mockAddrBook) FetchInternalKeyLocator(_ context.Context, var _ AddrBook = (*mockAddrBook)(nil) -func randProof(t *testing.T, amount uint64, - internalKey keychain.KeyDescriptor) proof.Proof { +func randProof(t *testing.T, amount uint64, internalKey keychain.KeyDescriptor, + groupKey *asset.GroupKey) proof.Proof { oddTxBlockHex, err := os.ReadFile(oddTxBlockHexFileName) require.NoError(t, err) @@ -101,7 +109,7 @@ func randProof(t *testing.T, amount uint64, txMerkleProof := proof.TxMerkleProof{} mintCommitment, assets, err := commitment.Mint( - nil, randGen, nil, &commitment.AssetDetails{ + nil, randGen, groupKey, &commitment.AssetDetails{ Type: randGen.Type, ScriptKey: test.PubToKeyDesc(scriptKey), Amount: &amount, @@ -240,10 +248,12 @@ func TestFundPacket(t *testing.T) { ctx := context.Background() internalKey, _ := test.RandKeyDesc(t) + grpInternalKey1, _ := test.RandKeyDesc(t) + grpInternalKey2, _ := test.RandKeyDesc(t) scriptKey := asset.RandScriptKey(t) const mintAmount = 500 - inputProof := randProof(t, mintAmount, internalKey) + inputProof := randProof(t, mintAmount, internalKey, nil) inputAsset := inputProof.Asset assetID := inputAsset.ID() @@ -256,10 +266,48 @@ func TestFundPacket(t *testing.T) { inputCommitment, err := commitment.FromAssets(nil, &inputProof.Asset) require.NoError(t, err) + groupPubKey := test.RandPubKey(t) + groupKey := &asset.GroupKey{ + GroupPubKey: *groupPubKey, + } + + groupProof1 := randProof(t, mintAmount*2, grpInternalKey1, groupKey) + groupInputAsset1 := groupProof1.Asset + groupAssetID1 := groupInputAsset1.ID() + + groupProof2 := randProof(t, mintAmount*2, grpInternalKey2, groupKey) + groupInputAsset2 := groupProof2.Asset + groupAssetID2 := groupInputAsset2.ID() + + grpInputPrevID1 := asset.PrevID{ + OutPoint: groupProof1.OutPoint(), + ID: groupAssetID1, + ScriptKey: asset.ToSerialized( + groupInputAsset1.ScriptKey.PubKey, + ), + } + grpInputPrevID2 := asset.PrevID{ + OutPoint: groupProof2.OutPoint(), + ID: groupAssetID2, + ScriptKey: asset.ToSerialized( + groupInputAsset2.ScriptKey.PubKey, + ), + } + + grpInputCommitment1, err := commitment.FromAssets( + nil, &groupInputAsset1, + ) + require.NoError(t, err) + grpInputCommitment2, err := commitment.FromAssets( + nil, &groupInputAsset2, + ) + require.NoError(t, err) + testCases := []struct { name string fundDesc *tapsend.FundingDescriptor vPkt *tappsbt.VPacket + inputProofs []*proof.Proof selectedCommitments []*AnchoredCommitment keysDerived int expectedErr string @@ -283,6 +331,7 @@ func TestFundPacket(t *testing.T) { Interactive: false, }}, }, + inputProofs: []*proof.Proof{&inputProof}, selectedCommitments: []*AnchoredCommitment{{ AnchorPoint: inputProof.OutPoint(), InternalKey: internalKey, @@ -297,21 +346,21 @@ func TestFundPacket(t *testing.T) { r *tapgarden.MockKeyRing) [][]*tappsbt.VOutput { pkt0Outputs := []*tappsbt.VOutput{{ - Amount: 20, - Type: tappsbt.TypeSimple, - ScriptKey: scriptKey, + Amount: mintAmount - 20, + Type: tappsbt.TypeSplitRoot, + ScriptKey: r.ScriptKeyAt(t, 0), AnchorOutputInternalKey: r.PubKeyAt( t, 1, ), - AnchorOutputIndex: 0, + AnchorOutputIndex: 1, }, { - Amount: mintAmount - 20, - Type: tappsbt.TypeSplitRoot, - ScriptKey: r.ScriptKeyAt(t, 0), + Amount: 20, + Type: tappsbt.TypeSimple, + ScriptKey: scriptKey, AnchorOutputInternalKey: r.PubKeyAt( t, 2, ), - AnchorOutputIndex: 1, + AnchorOutputIndex: 0, }} return [][]*tappsbt.VOutput{pkt0Outputs} @@ -333,14 +382,40 @@ func TestFundPacket(t *testing.T) { Interactive: false, }}, }, + inputProofs: []*proof.Proof{&inputProof}, selectedCommitments: []*AnchoredCommitment{{ AnchorPoint: inputProof.OutPoint(), InternalKey: internalKey, Commitment: inputCommitment, Asset: &inputAsset, }}, - keysDerived: 1, - expectedErr: "single output must be interactive", + keysDerived: 2, + expectedInputCommitments: tappsbt.InputCommitments{ + inputPrevID: inputCommitment, + }, + expectedOutputs: func(t *testing.T, + r *tapgarden.MockKeyRing) [][]*tappsbt.VOutput { + + pkt0Outputs := []*tappsbt.VOutput{{ + Amount: 0, + Type: tappsbt.TypeSplitRoot, + ScriptKey: asset.NUMSScriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 0, + ), + AnchorOutputIndex: 1, + }, { + Amount: mintAmount, + Type: tappsbt.TypeSimple, + ScriptKey: scriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 1, + ), + AnchorOutputIndex: 0, + }} + + return [][]*tappsbt.VOutput{pkt0Outputs} + }, }, { name: "single input, full value, change present", @@ -364,6 +439,7 @@ func TestFundPacket(t *testing.T) { AnchorOutputIndex: 1, }}, }, + inputProofs: []*proof.Proof{&inputProof}, selectedCommitments: []*AnchoredCommitment{{ AnchorPoint: inputProof.OutPoint(), InternalKey: internalKey, @@ -398,12 +474,278 @@ func TestFundPacket(t *testing.T) { return [][]*tappsbt.VOutput{pkt0Outputs} }, }, + { + name: "multi input, full value, change present", + fundDesc: &tapsend.FundingDescriptor{ + AssetSpecifier: asset.NewSpecifierFromGroupKey( + *groupPubKey, + ), + Amount: mintAmount * 4, + }, + vPkt: &tappsbt.VPacket{ + ChainParams: testParams, + Outputs: []*tappsbt.VOutput{{ + Type: tappsbt.TypeSplitRoot, + Amount: 0, + ScriptKey: asset.NUMSScriptKey, + Interactive: false, + }, { + Amount: mintAmount * 4, + ScriptKey: scriptKey, + Interactive: false, + AnchorOutputIndex: 1, + }}, + }, + inputProofs: []*proof.Proof{&groupProof1, &groupProof2}, + selectedCommitments: []*AnchoredCommitment{{ + AnchorPoint: groupProof1.OutPoint(), + InternalKey: grpInternalKey1, + Commitment: grpInputCommitment1, + Asset: &groupInputAsset1, + }, { + AnchorPoint: groupProof2.OutPoint(), + InternalKey: grpInternalKey2, + Commitment: grpInputCommitment2, + Asset: &groupInputAsset2, + }}, + keysDerived: 2, + expectedInputCommitments: tappsbt.InputCommitments{ + grpInputPrevID1: grpInputCommitment1, + grpInputPrevID2: grpInputCommitment2, + }, + // We test that we have two virtual packets, both with + // one input and two outputs. In the first vOutput, we + // always each have the tombstone zero-value output, + // since this is a full-value spend across two vPackets. + // The vOutputs across the two vPackets should each go + // to the same anchor output. And the same anchor output + // key should be derived for the same output indexes. + expectedOutputs: func(t *testing.T, + r *tapgarden.MockKeyRing) [][]*tappsbt.VOutput { + + pkt0Outputs := []*tappsbt.VOutput{{ + Amount: 0, + Type: tappsbt.TypeSplitRoot, + ScriptKey: asset.NUMSScriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 0, + ), + AnchorOutputIndex: 0, + }, { + Amount: mintAmount * 2, + Type: tappsbt.TypeSimple, + ScriptKey: scriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 1, + ), + AnchorOutputIndex: 1, + }} + pkt1Outputs := []*tappsbt.VOutput{{ + Amount: 0, + Type: tappsbt.TypeSplitRoot, + ScriptKey: asset.NUMSScriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 0, + ), + AnchorOutputIndex: 0, + }, { + Amount: mintAmount * 2, + Type: tappsbt.TypeSimple, + ScriptKey: scriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 1, + ), + AnchorOutputIndex: 1, + }} + + // Actually, the two vPackets should be the + // same, just different inputs. + require.Equal(t, pkt0Outputs, pkt1Outputs) + + return [][]*tappsbt.VOutput{ + pkt0Outputs, pkt1Outputs, + } + }, + }, + { + name: "multi input, partial amount, no change present", + fundDesc: &tapsend.FundingDescriptor{ + AssetSpecifier: asset.NewSpecifierFromGroupKey( + *groupPubKey, + ), + Amount: mintAmount * 3, + }, + vPkt: &tappsbt.VPacket{ + ChainParams: testParams, + Outputs: []*tappsbt.VOutput{{ + Amount: mintAmount * 3, + ScriptKey: scriptKey, + Interactive: false, + }}, + }, + inputProofs: []*proof.Proof{&groupProof1, &groupProof2}, + selectedCommitments: []*AnchoredCommitment{{ + AnchorPoint: groupProof1.OutPoint(), + InternalKey: grpInternalKey1, + Commitment: grpInputCommitment1, + Asset: &groupInputAsset1, + }, { + AnchorPoint: groupProof2.OutPoint(), + InternalKey: grpInternalKey2, + Commitment: grpInputCommitment2, + Asset: &groupInputAsset2, + }}, + keysDerived: 3, + expectedInputCommitments: tappsbt.InputCommitments{ + grpInputPrevID1: grpInputCommitment1, + grpInputPrevID2: grpInputCommitment2, + }, + // We test that we have two virtual packets, both with + // one input and two outputs. In the first vOutput, we + // always each have the tombstone zero-value output, + // since this is a full-value spend across two vPackets. + // The vOutputs across the two vPackets should each go + // to the same anchor output. And the same anchor output + // key should be derived for the same output indexes. + expectedOutputs: func(t *testing.T, + r *tapgarden.MockKeyRing) [][]*tappsbt.VOutput { + + pkt0Outputs := []*tappsbt.VOutput{{ + Amount: mintAmount, + Type: tappsbt.TypeSplitRoot, + ScriptKey: r.ScriptKeyAt(t, 0), + AnchorOutputInternalKey: r.PubKeyAt( + t, 1, + ), + AnchorOutputIndex: 1, + }, { + Amount: mintAmount, + Type: tappsbt.TypeSimple, + ScriptKey: scriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 2, + ), + AnchorOutputIndex: 0, + }} + pkt1Outputs := []*tappsbt.VOutput{{ + Amount: 0, + Type: tappsbt.TypeSplitRoot, + ScriptKey: asset.NUMSScriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 1, + ), + AnchorOutputIndex: 1, + }, { + Amount: mintAmount * 2, + Type: tappsbt.TypeSimple, + ScriptKey: scriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 2, + ), + AnchorOutputIndex: 0, + }} + + return [][]*tappsbt.VOutput{ + pkt0Outputs, pkt1Outputs, + } + }, + }, + { + name: "multi input, partial amount, change present", + fundDesc: &tapsend.FundingDescriptor{ + AssetSpecifier: asset.NewSpecifierFromGroupKey( + *groupPubKey, + ), + Amount: mintAmount * 3, + }, + vPkt: &tappsbt.VPacket{ + ChainParams: testParams, + Outputs: []*tappsbt.VOutput{{ + Type: tappsbt.TypeSplitRoot, + Amount: 0, + ScriptKey: asset.NUMSScriptKey, + Interactive: false, + }, { + Amount: mintAmount * 3, + ScriptKey: scriptKey, + Interactive: false, + AnchorOutputIndex: 1, + }}, + }, + inputProofs: []*proof.Proof{&groupProof1, &groupProof2}, + selectedCommitments: []*AnchoredCommitment{{ + AnchorPoint: groupProof1.OutPoint(), + InternalKey: grpInternalKey1, + Commitment: grpInputCommitment1, + Asset: &groupInputAsset1, + }, { + AnchorPoint: groupProof2.OutPoint(), + InternalKey: grpInternalKey2, + Commitment: grpInputCommitment2, + Asset: &groupInputAsset2, + }}, + keysDerived: 3, + expectedInputCommitments: tappsbt.InputCommitments{ + grpInputPrevID1: grpInputCommitment1, + grpInputPrevID2: grpInputCommitment2, + }, + // We test that we have two virtual packets, both with + // one input and two outputs. The first vPacket will be + // a full-value spend, so the change should be the NUMS + // key. The second vPacket is a partial spend, so there + // should be change and a key derived for it. The + // vOutputs across the two vPackets should each go to + // the same anchor output. And the same anchor output + // key should be derived for the same output indexes. + expectedOutputs: func(t *testing.T, + r *tapgarden.MockKeyRing) [][]*tappsbt.VOutput { + + pkt0Outputs := []*tappsbt.VOutput{{ + Amount: mintAmount, + Type: tappsbt.TypeSplitRoot, + ScriptKey: r.ScriptKeyAt(t, 0), + AnchorOutputInternalKey: r.PubKeyAt( + t, 1, + ), + AnchorOutputIndex: 0, + }, { + Amount: mintAmount, + Type: tappsbt.TypeSimple, + ScriptKey: scriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 2, + ), + AnchorOutputIndex: 1, + }} + pkt1Outputs := []*tappsbt.VOutput{{ + Amount: 0, + Type: tappsbt.TypeSplitRoot, + ScriptKey: asset.NUMSScriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 1, + ), + AnchorOutputIndex: 0, + }, { + Amount: mintAmount * 2, + Type: tappsbt.TypeSimple, + ScriptKey: scriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 2, + ), + AnchorOutputIndex: 1, + }} + + return [][]*tappsbt.VOutput{ + pkt0Outputs, pkt1Outputs, + } + }, + }, } for _, tc := range testCases { t.Run(tc.name, func(tt *testing.T) { exporter := &mockExporter{ - singleProof: inputProof, + proofs: tc.inputProofs, } addrBook := &mockAddrBook{} keyRing := tapgarden.NewMockKeyRing() diff --git a/tapfreighter/interface.go b/tapfreighter/interface.go index b16131abc..9c1f78f8a 100644 --- a/tapfreighter/interface.go +++ b/tapfreighter/interface.go @@ -97,6 +97,15 @@ type AnchoredCommitment struct { Asset *asset.Asset } +// PrevID returns the previous ID of the asset commitment. +func (c *AnchoredCommitment) PrevID() asset.PrevID { + return asset.PrevID{ + OutPoint: c.AnchorPoint, + ID: c.Asset.ID(), + ScriptKey: asset.ToSerialized(c.Asset.ScriptKey.PubKey), + } +} + var ( // ErrMatchingAssetsNotFound is returned when an instance of // AssetStoreListCoins cannot satisfy the given asset identification diff --git a/tapfreighter/log.go b/tapfreighter/log.go index db2ae16b5..7429f51de 100644 --- a/tapfreighter/log.go +++ b/tapfreighter/log.go @@ -2,6 +2,7 @@ package tapfreighter import ( "github.com/btcsuite/btclog" + "github.com/davecgh/go-spew/spew" ) // Subsystem defines the logging code for this subsystem. @@ -24,3 +25,10 @@ func DisableLog() { func UseLogger(logger btclog.Logger) { log = logger } + +// limitSpewer is a spew.ConfigState that limits the depth of the output +// to 4 levels, so it can safely be used for things that contain an MS-SMT tree. +var limitSpewer = &spew.ConfigState{ + Indent: " ", + MaxDepth: 7, +} diff --git a/tappsbt/create.go b/tappsbt/create.go index cbf510aa5..5e262144b 100644 --- a/tappsbt/create.go +++ b/tappsbt/create.go @@ -54,6 +54,7 @@ func FromAddresses(receiverAddrs []*address.Tap, // process. pkt.Outputs = append(pkt.Outputs, &VOutput{ Amount: 0, + Interactive: false, Type: TypeSplitRoot, AnchorOutputIndex: 0, ScriptKey: asset.NUMSScriptKey, @@ -171,12 +172,12 @@ func OwnershipProofPacket(ownedAsset *asset.Asset, // FromProofs creates a packet from the given proofs that adds them as inputs to // the packet. -func FromProofs(proofs []*proof.Proof, - params *address.ChainParams) (*VPacket, error) { +func FromProofs(proofs []*proof.Proof, params *address.ChainParams, + version VPacketVersion) (*VPacket, error) { pkt := &VPacket{ ChainParams: params, - Version: V1, + Version: version, } for idx := range proofs { diff --git a/tapchannel/allocation.go b/tapsend/allocation.go similarity index 53% rename from tapchannel/allocation.go rename to tapsend/allocation.go index 35450d4f1..d28c3b6e3 100644 --- a/tapchannel/allocation.go +++ b/tapsend/allocation.go @@ -1,7 +1,8 @@ -package tapchannel +package tapsend import ( "bytes" + "errors" "fmt" "net/url" "sort" @@ -17,7 +18,6 @@ import ( "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/proof" "github.com/lightninglabs/taproot-assets/tappsbt" - "github.com/lightninglabs/taproot-assets/tapsend" "github.com/lightningnetwork/lnd/input" ) @@ -30,18 +30,34 @@ var ( // were provided. ErrMissingAllocations = fmt.Errorf("no allocations provided") + // ErrInputTypesNotEqual is an error that is returned if the input types + // are not all the same. + ErrInputTypesNotEqual = fmt.Errorf("input types not all equal") + + // ErrInputGroupMismatch is an error that is returned if the input + // assets don't all belong to the same asset group. + ErrInputGroupMismatch = fmt.Errorf("input assets not all of same group") + // ErrInputOutputSumMismatch is an error that is returned if the sum of // the input asset proofs does not match the sum of the output // allocations. ErrInputOutputSumMismatch = fmt.Errorf("input and output sum mismatch") - // ErrNormalAssetsOnly is an error that is returned if an allocation - // contains an asset that is not a normal asset (e.g. a collectible). - ErrNormalAssetsOnly = fmt.Errorf("only normal assets are supported") - // ErrCommitmentNotSet is an error that is returned if the output // commitment is not set for an allocation. ErrCommitmentNotSet = fmt.Errorf("output commitment not set") + + // ErrInvalidSibling is an error that is returned if both non-asset + // leaves and sibling preimage are set for an allocation. + ErrInvalidSibling = errors.New( + "both non-asset leaves and sibling preimage set", + ) + + // ErrScriptKeyGenMissing is an error that is returned if the script key + // generator function is not set. + ErrScriptKeyGenMissing = errors.New( + "script key generator function not set for asset allocation", + ) ) // AllocationType is an enum that defines the different types of asset @@ -77,6 +93,27 @@ const ( SecondLevelHtlcAllocation AllocationType = 5 ) +// ScriptKeyGen is a function type that is used for generating a script key for +// an asset specific script key. +type ScriptKeyGen func(assetID asset.ID) (asset.ScriptKey, error) + +// StaticScriptKeyGen is a helper function that returns a script key generator +// function that always returns the same script key. +func StaticScriptKeyGen(scriptKey asset.ScriptKey) ScriptKeyGen { + return func(asset.ID) (asset.ScriptKey, error) { + return scriptKey, nil + } +} + +// StaticScriptPubKeyGen is a helper function that returns a script key +// generator function that always returns the same script key, provided as a +// public key. +func StaticScriptPubKeyGen(scriptPubKey *btcec.PublicKey) ScriptKeyGen { + return func(asset.ID) (asset.ScriptKey, error) { + return asset.NewScriptKey(scriptPubKey), nil + } +} + // Allocation is a struct that tracks how many units of assets should be // allocated to a specific output of an on-chain transaction. An allocation can // be seen as a recipe/instruction to distribute a certain number of asset units @@ -103,13 +140,25 @@ type Allocation struct { // NonAssetLeaves is the full list of TapLeaf nodes that aren't any // asset commitments. This is used to construct the tapscript sibling - // for the asset commitment. If this is a non-asset allocation and the - // list of leaves is empty, then we assume a BIP-0086 output. + // for the asset commitment. This is mutually exclusive to the + // SiblingPreimage field below, only one of them (or none) should be + // set. If this is a non-asset allocation and both NonAssetLeaves is + // empty and no SiblingPreimage is set, then we assume a BIP-0086 + // output. NonAssetLeaves []txscript.TapLeaf - // ScriptKey is the Taproot tweaked key encoding the different spend - // conditions possible for the asset allocation. - ScriptKey asset.ScriptKey + // SiblingPreimage is the tapscript sibling preimage that is used to + // create the tapscript sibling for the asset commitment. This is + // mutually exclusive to the NonAssetLeaves above, only one of them (or + // none) should be set. If this is a non-asset allocation and both + // NonAssetLeaves is empty and no SiblingPreimage is set, then we assume + // a BIP-0086 output. + SiblingPreimage *commitment.TapscriptPreimage + + // GenScriptKey is a function that returns the Taproot tweaked key + // encoding the different spend conditions possible for the asset + // allocation for a certain asset ID. + GenScriptKey ScriptKeyGen // Amount is the amount of units that should be allocated in total. // Available units from different UTXOs are distributed up to this total @@ -128,15 +177,19 @@ type Allocation struct { // commitment present. This field should be used for sorting purposes. SortTaprootKeyBytes []byte - // CLTV is the CLTV timeout for the asset allocation. This is only - // relevant for sorting purposes and is expected to be zero for any + // SortCLTV is the SortCLTV timeout for the asset allocation. This is + // only relevant for sorting purposes and is expected to be zero for any // non-HTLC allocation. - CLTV uint32 + SortCLTV uint32 // Sequence is the CSV value for the asset allocation. This is only - // relevant for HTLC second level transactions. + // relevant for HTLC second level transactions. This value will be set + // as the relative time lock on the virtual output. Sequence uint32 + // LockTime is the actual CLTV value that will be set on the output. + LockTime uint64 + // HtlcIndex is the index of the HTLC that the allocation is for. This // is only relevant for HTLC allocations. HtlcIndex input.HtlcIndex @@ -148,15 +201,45 @@ type Allocation struct { // ProofDeliveryAddress is the address the proof courier should use to // upload the proof for this allocation. ProofDeliveryAddress *url.URL + + // AltLeaves represent data used to construct an Asset commitment, that + // will be inserted in the output anchor Tap commitment. These + // data-carrying leaves are used for a purpose distinct from + // representing individual Taproot Assets. + AltLeaves []asset.AltLeaf[asset.Asset] +} + +// Validate checks that the allocation is correctly set up and that the fields +// are consistent with each other. +func (a *Allocation) Validate() error { + // Make sure the two mutually exclusive fields aren't set at the same + // time. + if len(a.NonAssetLeaves) > 0 && a.SiblingPreimage != nil { + return ErrInvalidSibling + } + + // The script key generator function is required for any allocation that + // carries assets. + if a.Type != AllocationTypeNoAssets && a.GenScriptKey == nil { + return ErrScriptKeyGenMissing + } + + return nil } // tapscriptSibling returns the tapscript sibling preimage from the non-asset // leaves of the allocation. If there are no non-asset leaves, nil is returned. func (a *Allocation) tapscriptSibling() (*commitment.TapscriptPreimage, error) { - if len(a.NonAssetLeaves) == 0 { + if len(a.NonAssetLeaves) == 0 && a.SiblingPreimage == nil { return nil, nil } + // The sibling preimage has precedence. Only one of the two fields + // should be set in any case. + if a.SiblingPreimage != nil { + return a.SiblingPreimage, nil + } + treeNodes, err := asset.TapTreeNodesFromLeaves(a.NonAssetLeaves) if err != nil { return nil, fmt.Errorf("error creating tapscript tree nodes: "+ @@ -172,10 +255,10 @@ func (a *Allocation) tapscriptSibling() (*commitment.TapscriptPreimage, error) { return sibling, err } -// finalPkScript returns the pkScript calculated from the internal key, +// FinalPkScript returns the pkScript calculated from the internal key, // tapscript sibling and merkle root of the output commitment. If the output // commitment is not set, ErrCommitmentNotSet is returned. -func (a *Allocation) finalPkScript() ([]byte, error) { +func (a *Allocation) FinalPkScript() ([]byte, error) { // If this is a normal commitment anchor output without any assets, then // we'll map the sort Taproot output key to a script directly. if a.Type == AllocationTypeNoAssets { @@ -229,13 +312,13 @@ func (a *Allocation) AuxLeaf() (txscript.TapLeaf, error) { func (a *Allocation) MatchesOutput(pkScript []byte, value int64, cltv uint32, htlcIndex input.HtlcIndex) (bool, error) { - finalPkScript, err := a.finalPkScript() + finalPkScript, err := a.FinalPkScript() if err != nil { return false, err } outputsEqual := bytes.Equal(pkScript, finalPkScript) && - value == int64(a.BtcAmount) && cltv == a.CLTV && + value == int64(a.BtcAmount) && cltv == a.SortCLTV && htlcIndex == a.HtlcIndex return outputsEqual, nil @@ -328,7 +411,8 @@ func sortPiecesWithProofs(pieces []*piece) { // asset outputs (asset UTXOs of different sizes from different tranches/asset // IDs) according to the distribution rules provided as "allocations". func DistributeCoins(inputs []*proof.Proof, allocations []*Allocation, - chainParams *address.ChainParams) ([]*tappsbt.VPacket, error) { + chainParams *address.ChainParams, interactive bool, + vPktVersion tappsbt.VPacketVersion) ([]*tappsbt.VPacket, error) { if len(inputs) == 0 { return nil, ErrMissingInputs @@ -339,18 +423,35 @@ func DistributeCoins(inputs []*proof.Proof, allocations []*Allocation, } // Count how many asset units are available for distribution. - var inputSum uint64 + var ( + inputSum uint64 + firstType = inputs[0].Asset.Type + firstTapKey = inputs[0].Asset.TapCommitmentKey() + ) for _, inputProof := range inputs { - if inputProof.Asset.Type != asset.Normal { - return nil, ErrNormalAssetsOnly + // We can't have mixed types (normal and collectibles) within + // the same allocation. + if firstType != inputProof.Asset.Type { + return nil, ErrInputTypesNotEqual + } + + // Allocating assets from different asset groups is not allowed. + if firstTapKey != inputProof.Asset.TapCommitmentKey() { + return nil, ErrInputGroupMismatch } inputSum += inputProof.Asset.Amount } - // Sum up the amounts that are to be allocated to the outputs. + // Sum up the amounts that are to be allocated to the outputs. We also + // validate that all the required fields are set and no conflicting + // fields are set. var outputSum uint64 for _, allocation := range allocations { + if err := allocation.Validate(); err != nil { + return nil, fmt.Errorf("invalid allocation: %w", err) + } + outputSum += allocation.Amount } @@ -383,7 +484,9 @@ func DistributeCoins(inputs []*proof.Proof, allocations []*Allocation, }, ) - pkt, err := tappsbt.FromProofs(proofsByID, chainParams) + pkt, err := tappsbt.FromProofs( + proofsByID, chainParams, vPktVersion, + ) if err != nil { return nil, err } @@ -412,64 +515,22 @@ func DistributeCoins(inputs []*proof.Proof, allocations []*Allocation, // Find the next piece that has assets left to allocate. toFill := a.Amount for pieceIdx := range pieces { - p := pieces[pieceIdx] - - // Skip fully allocated pieces - if p.available() == 0 { - continue - } - - // We know we have something to allocate, so let's now - // create a new vOutput for the allocation. - allocating := toFill - if p.available() < toFill { - allocating = p.available() - } - - // We only need a split root output if this piece is - // being split. If we consume it fully in this - // allocation, we can use a simple output. - consumeFully := p.allocated == 0 && - toFill >= p.available() - - outType := tappsbt.TypeSimple - if a.SplitRoot && !consumeFully { - outType = tappsbt.TypeSplitRoot - } - - sibling, err := a.tapscriptSibling() + fillDelta, updatedPiece, err := allocatePiece( + *pieces[pieceIdx], *a, toFill, interactive, + ) if err != nil { return nil, err } - deliveryAddr := a.ProofDeliveryAddress - vOut := &tappsbt.VOutput{ - Amount: allocating, - AssetVersion: a.AssetVersion, - Type: outType, - Interactive: true, - AnchorOutputIndex: a.OutputIndex, - AnchorOutputInternalKey: a.InternalKey, - AnchorOutputTapscriptSibling: sibling, - ScriptKey: a.ScriptKey, - ProofDeliveryAddress: deliveryAddr, - RelativeLockTime: uint64( - a.Sequence, - ), - } - p.packet.Outputs = append(p.packet.Outputs, vOut) - - // TODO(guggero): If sequence > 0, set the sequence - // on the inputs of the packet. - - p.allocated += allocating - toFill -= allocating + pieces[pieceIdx] = updatedPiece + toFill -= fillDelta // If the piece has enough assets to fill the - // allocation, we can exit the loop. If it only fills - // part of the allocation, we'll continue to the next - // piece. - if toFill == 0 { + // allocation, we can exit the loop, unless we also need + // to create a tombstone output for a non-interactive + // send. If it only fills part of the allocation, we'll + // continue to the next piece. + if toFill == 0 && interactive { break } } @@ -478,7 +539,7 @@ func DistributeCoins(inputs []*proof.Proof, allocations []*Allocation, packets := fn.Map(pieces, func(p *piece) *tappsbt.VPacket { return p.packet }) - err := tapsend.ValidateVPacketVersions(packets) + err := ValidateVPacketVersions(packets) if err != nil { return nil, err } @@ -486,6 +547,93 @@ func DistributeCoins(inputs []*proof.Proof, allocations []*Allocation, return packets, nil } +// allocatePiece allocates assets from the given piece to the given allocation, +// if there are units left to allocate. This adds a virtual output to the piece +// and updates the amount of allocated assets. The function returns the amount +// of assets that were allocated and the updated piece. +func allocatePiece(p piece, a Allocation, toFill uint64, + interactive bool) (uint64, *piece, error) { + + sibling, err := a.tapscriptSibling() + if err != nil { + return 0, nil, err + } + + scriptKey, err := a.GenScriptKey(p.assetID) + if err != nil { + return 0, nil, fmt.Errorf("error generating script key for "+ + "allocation: %w", err) + } + + deliveryAddr := a.ProofDeliveryAddress + vOut := &tappsbt.VOutput{ + AssetVersion: a.AssetVersion, + Interactive: interactive, + AnchorOutputIndex: a.OutputIndex, + AnchorOutputInternalKey: a.InternalKey, + AnchorOutputTapscriptSibling: sibling, + ScriptKey: scriptKey, + ProofDeliveryAddress: deliveryAddr, + LockTime: a.LockTime, + RelativeLockTime: uint64(a.Sequence), + AltLeaves: a.AltLeaves, + } + + // If we've allocated all pieces, or we don't need to allocate anything + // to this piece, we might only need to create a tombstone output. + if p.available() == 0 || toFill == 0 { + // We don't need a tombstone output for interactive transfers, + // or recipient outputs (outputs that don't go back to the + // sender). + if interactive || !a.SplitRoot { + return 0, &p, nil + } + + // Create a zero-amount tombstone output for the split root, if + // there is no change. + vOut.Type = tappsbt.TypeSplitRoot + p.packet.Outputs = append(p.packet.Outputs, vOut) + + return 0, &p, nil + } + + // We know we have something to allocate, so let's now create a new + // vOutput for the allocation. + allocating := toFill + if p.available() < toFill { + allocating = p.available() + } + + // We only need a split root output if this piece is being split. If we + // consume it fully in this allocation, we can use a simple output. + consumeFully := p.allocated == 0 && toFill >= p.available() + + // If we're creating a non-interactive packet (e.g. for a TAP address + // based send), we definitely need a split root, even if there is no + // change. If there is change, then we also need a split root, even if + // we're creating a fully interactive packet. + needSplitRoot := a.SplitRoot && (!interactive || !consumeFully) + + // The only exception is when the split root output is the only output, + // because it's not being used at all and goes back to the sender. + splitRootIsOnlyOutput := a.SplitRoot && consumeFully + + outType := tappsbt.TypeSimple + if needSplitRoot && !splitRootIsOnlyOutput { + outType = tappsbt.TypeSplitRoot + } + + // We just need to update the type and amount for this virtual output, + // everything else can be taken from the allocation itself. + vOut.Type = outType + vOut.Amount = allocating + p.packet.Outputs = append(p.packet.Outputs, vOut) + + p.allocated += allocating + + return allocating, &p, nil +} + // AssignOutputCommitments assigns the output commitments keyed by the output // index to the corresponding allocations. func AssignOutputCommitments(allocations []*Allocation, @@ -515,10 +663,10 @@ func AssignOutputCommitments(allocations []*Allocation, // NonAssetExclusionProofs returns an exclusion proof generator that creates // exclusion proofs for non-asset P2TR outputs in the given allocations. func NonAssetExclusionProofs( - allocations []*Allocation) tapsend.ExclusionProofGenerator { + allocations []*Allocation) ExclusionProofGenerator { return func(target *proof.BaseProofParams, - isAnchor tapsend.IsAnchor) error { + isAnchor IsAnchor) error { for _, alloc := range allocations { // We only need exclusion proofs for allocations that @@ -549,3 +697,108 @@ func NonAssetExclusionProofs( return nil } } + +// AllocationsFromTemplate creates a list of allocations from a spend template. +// If there is no split output present in the template, one is created to carry +// potential change or a zero-value tombstone output in case of a +// non-interactive transfer. The script key for those change/tombstone outputs +// are set to the NUMS script key and need to be replaced with an actual script +// key (if the change is non-zero) after the coin distribution has been +// performed. +func AllocationsFromTemplate(tpl *tappsbt.VPacket, + inputSum uint64) ([]*Allocation, bool, error) { + + if len(tpl.Outputs) == 0 { + return nil, false, fmt.Errorf("spend template has no outputs") + } + + // We first detect if the outputs are interactive or not. They need to + // all say the same thing, otherwise we can't proceed. + isInteractive := tpl.Outputs[0].Interactive + for idx := 1; idx < len(tpl.Outputs); idx++ { + if tpl.Outputs[idx].Interactive != isInteractive { + return nil, false, fmt.Errorf("outputs have " + + "different interactive flags") + } + } + + // Calculate the total amount that is being spent. + var outputAmount uint64 + for _, out := range tpl.Outputs { + outputAmount += out.Amount + } + + // Validate the change amount so we can use it later. + if outputAmount > inputSum { + return nil, false, fmt.Errorf("output amount exceeds input sum") + } + changeAmount := inputSum - outputAmount + + // In case there is no change/tombstone output, we assume the anchor + // output indexes are still increasing. So we'll just use the next one + // after the last output's anchor output index. + splitOutIndex := tpl.Outputs[len(tpl.Outputs)-1].AnchorOutputIndex + 1 + + // If there is no change/tombstone output in the template, we always + // create one. This will not be used (e.g. turned into an actual virtual + // output) by the allocation logic if is not needed (when it's an + // interactive full-value send). + localAllocation := &Allocation{ + Type: CommitAllocationToLocal, + OutputIndex: splitOutIndex, + SplitRoot: true, + GenScriptKey: StaticScriptKeyGen(asset.NUMSScriptKey), + } + + // If we have a split root defined in the template, we'll use that as + // the template for the local allocation. + if tpl.HasSplitRootOutput() { + splitRootOut, err := tpl.SplitRootOutput() + if err != nil { + return nil, false, err + } + + setAllocationFieldsFromOutput(localAllocation, splitRootOut) + } + + // We do need to overwrite the amount of the local allocation with the + // change amount now. We do _NOT_, however, derive change script keys + // yet, since we don't know if some of the packets created by the coin + // distribution might remain an un-spendable zero-amount tombstone + // output, and we don't want to derive change script keys for those. + localAllocation.Amount = changeAmount + + // We now create the remote allocations for each non-split output. + remoteAllocations := make([]*Allocation, 0, len(tpl.Outputs)) + normalOuts := fn.Filter(tpl.Outputs, tappsbt.VOutIsNotSplitRoot) + for _, out := range normalOuts { + remoteAllocation := &Allocation{ + Type: CommitAllocationToRemote, + SplitRoot: false, + } + + setAllocationFieldsFromOutput(remoteAllocation, out) + remoteAllocations = append(remoteAllocations, remoteAllocation) + } + + allAllocations := append( + []*Allocation{localAllocation}, remoteAllocations..., + ) + + return allAllocations, isInteractive, nil +} + +// setAllocationFieldsFromOutput sets the fields of the given allocation from +// the given virtual output. +func setAllocationFieldsFromOutput(alloc *Allocation, vOut *tappsbt.VOutput) { + alloc.Amount = vOut.Amount + alloc.AssetVersion = vOut.AssetVersion + alloc.OutputIndex = vOut.AnchorOutputIndex + alloc.InternalKey = vOut.AnchorOutputInternalKey + alloc.GenScriptKey = StaticScriptKeyGen(vOut.ScriptKey) + alloc.Sequence = uint32(vOut.RelativeLockTime) + alloc.LockTime = vOut.LockTime + alloc.ProofDeliveryAddress = vOut.ProofDeliveryAddress + alloc.AltLeaves = vOut.AltLeaves + alloc.SiblingPreimage = vOut.AnchorOutputTapscriptSibling +} diff --git a/tapchannel/allocation_sort.go b/tapsend/allocation_sort.go similarity index 95% rename from tapchannel/allocation_sort.go rename to tapsend/allocation_sort.go index 12be25943..39def152d 100644 --- a/tapchannel/allocation_sort.go +++ b/tapsend/allocation_sort.go @@ -1,4 +1,4 @@ -package tapchannel +package tapsend import ( "bytes" @@ -28,7 +28,7 @@ func InPlaceAllocationSort(allocations []*Allocation) { bytes.Compare( i.SortTaprootKeyBytes, j.SortTaprootKeyBytes, ), - cmp.Compare(i.CLTV, j.CLTV), + cmp.Compare(i.SortCLTV, j.SortCLTV), cmp.Compare(i.HtlcIndex, j.HtlcIndex), ) }) diff --git a/tapchannel/allocation_sort_test.go b/tapsend/allocation_sort_test.go similarity index 80% rename from tapchannel/allocation_sort_test.go rename to tapsend/allocation_sort_test.go index a2f385992..becf5ae75 100644 --- a/tapchannel/allocation_sort_test.go +++ b/tapsend/allocation_sort_test.go @@ -1,4 +1,4 @@ -package tapchannel +package tapsend import ( "testing" @@ -21,90 +21,90 @@ func TestInPlaceAllocationSort(t *testing.T) { { BtcAmount: 2000, SortTaprootKeyBytes: []byte("b"), - CLTV: 300, + SortCLTV: 300, }, { BtcAmount: 3000, SortTaprootKeyBytes: []byte("a"), - CLTV: 100, + SortCLTV: 100, }, { BtcAmount: 1000, SortTaprootKeyBytes: []byte("a"), - CLTV: 200, + SortCLTV: 200, }, { BtcAmount: 1000, SortTaprootKeyBytes: []byte("b"), - CLTV: 100, + SortCLTV: 100, }, { BtcAmount: 1000, SortTaprootKeyBytes: []byte("b"), - CLTV: 100, + SortCLTV: 100, HtlcIndex: 1, }, { BtcAmount: 1000, SortTaprootKeyBytes: []byte("b"), - CLTV: 100, + SortCLTV: 100, HtlcIndex: 9, }, { BtcAmount: 1000, SortTaprootKeyBytes: []byte("b"), - CLTV: 100, + SortCLTV: 100, HtlcIndex: 3, }, { BtcAmount: 1000, SortTaprootKeyBytes: []byte("a"), - CLTV: 100, + SortCLTV: 100, }, }, expected: []*Allocation{ { BtcAmount: 1000, SortTaprootKeyBytes: []byte("a"), - CLTV: 100, + SortCLTV: 100, }, { BtcAmount: 1000, SortTaprootKeyBytes: []byte("a"), - CLTV: 200, + SortCLTV: 200, }, { BtcAmount: 1000, SortTaprootKeyBytes: []byte("b"), - CLTV: 100, + SortCLTV: 100, }, { BtcAmount: 1000, SortTaprootKeyBytes: []byte("b"), - CLTV: 100, + SortCLTV: 100, HtlcIndex: 1, }, { BtcAmount: 1000, SortTaprootKeyBytes: []byte("b"), - CLTV: 100, + SortCLTV: 100, HtlcIndex: 3, }, { BtcAmount: 1000, SortTaprootKeyBytes: []byte("b"), - CLTV: 100, + SortCLTV: 100, HtlcIndex: 9, }, { BtcAmount: 2000, SortTaprootKeyBytes: []byte("b"), - CLTV: 300, + SortCLTV: 300, }, { BtcAmount: 3000, SortTaprootKeyBytes: []byte("a"), - CLTV: 100, + SortCLTV: 100, }, }, }, diff --git a/tapsend/allocation_test.go b/tapsend/allocation_test.go new file mode 100644 index 000000000..13d59b1b8 --- /dev/null +++ b/tapsend/allocation_test.go @@ -0,0 +1,1072 @@ +package tapsend + +import ( + "testing" + + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/taproot-assets/address" + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/commitment" + "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/internal/test" + "github.com/lightninglabs/taproot-assets/proof" + "github.com/lightninglabs/taproot-assets/tappsbt" + "github.com/stretchr/testify/require" +) + +var ( + testParams = &address.RegressionNetTap + + tx = wire.MsgTx{ + TxOut: []*wire.TxOut{ + { + Value: 1000, + PkScript: []byte("foo"), + }, + }, + } +) + +func makeProof(t *testing.T, a *asset.Asset) *proof.Proof { + tapCommitment, err := commitment.FromAssets( + fn.Ptr(commitment.TapCommitmentV2), a, + ) + require.NoError(t, err) + + _, commitmentProof, err := tapCommitment.Proof( + a.TapCommitmentKey(), a.AssetCommitmentKey(), + ) + require.NoError(t, err) + + return &proof.Proof{ + Asset: *a, + AnchorTx: tx, + InclusionProof: proof.TaprootProof{ + OutputIndex: 0, + InternalKey: test.RandPubKey(t), + CommitmentProof: &proof.CommitmentProof{ + Proof: *commitmentProof, + }, + }, + } +} + +func grindAssetID(t *testing.T, prefix byte) asset.Genesis { + for { + assetID := asset.RandGenesis(t, asset.Normal) + if assetID.ID()[0] == prefix { + return assetID + } + } +} + +func TestDistributeCoinsErrors(t *testing.T) { + _, err := DistributeCoins(nil, nil, testParams, true, tappsbt.V1) + require.ErrorIs(t, err, ErrMissingInputs) + + _, err = DistributeCoins( + []*proof.Proof{{}}, nil, testParams, true, tappsbt.V1, + ) + require.ErrorIs(t, err, ErrMissingAllocations) + + assetNormal := asset.RandAsset(t, asset.Normal) + proofNormal := makeProof(t, assetNormal) + assetCollectible := asset.RandAsset(t, asset.Collectible) + proofCollectible := makeProof(t, assetCollectible) + _, err = DistributeCoins( + []*proof.Proof{proofNormal, proofCollectible}, + []*Allocation{{}}, testParams, true, tappsbt.V1, + ) + require.ErrorIs(t, err, ErrInputTypesNotEqual) + + assetNormal2 := asset.RandAsset(t, asset.Normal) + proofNormal2 := makeProof(t, assetNormal2) + _, err = DistributeCoins( + []*proof.Proof{proofNormal, proofNormal2}, + []*Allocation{{}}, testParams, true, tappsbt.V1, + ) + require.ErrorIs(t, err, ErrInputGroupMismatch) + + _, err = DistributeCoins( + []*proof.Proof{proofNormal}, []*Allocation{ + { + Amount: assetNormal.Amount / 2, + GenScriptKey: StaticScriptKeyGen( + asset.RandScriptKey(t), + ), + }, + }, testParams, true, tappsbt.V1, + ) + require.ErrorIs(t, err, ErrInputOutputSumMismatch) + + _, err = DistributeCoins( + []*proof.Proof{proofNormal}, []*Allocation{{ + Amount: assetNormal.Amount, + NonAssetLeaves: make([]txscript.TapLeaf, 1), + SiblingPreimage: &commitment.TapscriptPreimage{}, + }}, testParams, true, tappsbt.V1, + ) + require.ErrorIs(t, err, ErrInvalidSibling) + + _, err = DistributeCoins( + []*proof.Proof{proofNormal}, []*Allocation{{ + Type: CommitAllocationToLocal, + Amount: assetNormal.Amount, + }}, testParams, true, tappsbt.V1, + ) + require.ErrorIs(t, err, ErrScriptKeyGenMissing) +} + +func TestDistributeCoins(t *testing.T) { + t.Parallel() + + groupKey := &asset.GroupKey{ + GroupPubKey: *test.RandPubKey(t), + } + + assetID1 := grindAssetID(t, 0x01) + assetID2 := grindAssetID(t, 0x02) + assetID3 := grindAssetID(t, 0x03) + + assetID1Tranche1 := asset.NewAssetNoErr( + t, assetID1, 100, 0, 0, asset.RandScriptKey(t), groupKey, + ) + assetID1Tranche2 := asset.NewAssetNoErr( + t, assetID1, 200, 0, 0, asset.RandScriptKey(t), groupKey, + ) + assetID1Tranche3 := asset.NewAssetNoErr( + t, assetID1, 300, 0, 0, asset.RandScriptKey(t), groupKey, + ) + + assetID2Tranche1 := asset.NewAssetNoErr( + t, assetID2, 1000, 0, 0, asset.RandScriptKey(t), groupKey, + ) + assetID2Tranche2 := asset.NewAssetNoErr( + t, assetID2, 2000, 0, 0, asset.RandScriptKey(t), groupKey, + ) + assetID2Tranche3 := asset.NewAssetNoErr( + t, assetID2, 3000, 0, 0, asset.RandScriptKey(t), groupKey, + ) + + assetID3Tranche1 := asset.NewAssetNoErr( + t, assetID3, 10000, 0, 0, asset.RandScriptKey(t), groupKey, + ) + assetID3Tranche2 := asset.NewAssetNoErr( + t, assetID3, 20000, 0, 0, asset.RandScriptKey(t), groupKey, + ) + assetID3Tranche3 := asset.NewAssetNoErr( + t, assetID3, 30000, 0, 0, asset.RandScriptKey(t), groupKey, + ) + + var ( + simple = tappsbt.TypeSimple + split = tappsbt.TypeSplitRoot + ) + testCases := []struct { + name string + inputs []*proof.Proof + interactive bool + allocations []*Allocation + vPktVersion tappsbt.VPacketVersion + expectedInputs map[asset.ID][]asset.ScriptKey + expectedOutputs map[asset.ID][]*tappsbt.VOutput + }{ + { + name: "single asset, split, interactive", + inputs: []*proof.Proof{ + makeProof(t, assetID1Tranche1), + }, + interactive: true, + allocations: []*Allocation{ + { + Type: CommitAllocationToLocal, + Amount: 50, + }, + { + Type: CommitAllocationToRemote, + SplitRoot: true, + Amount: 50, + OutputIndex: 1, + }, + }, + vPktVersion: tappsbt.V1, + expectedInputs: map[asset.ID][]asset.ScriptKey{ + assetID1.ID(): { + assetID1Tranche1.ScriptKey, + }, + }, + expectedOutputs: map[asset.ID][]*tappsbt.VOutput{ + assetID1.ID(): { + { + Amount: 50, + Type: simple, + Interactive: true, + AnchorOutputIndex: 0, + }, + { + Amount: 50, + Type: split, + Interactive: true, + AnchorOutputIndex: 1, + }, + }, + }, + }, + { + name: "single asset, split, non-interactive", + inputs: []*proof.Proof{ + makeProof(t, assetID1Tranche1), + }, + interactive: false, + allocations: []*Allocation{ + { + Type: CommitAllocationToLocal, + Amount: 50, + }, + { + Type: CommitAllocationToRemote, + SplitRoot: true, + Amount: 50, + OutputIndex: 1, + }, + }, + expectedInputs: map[asset.ID][]asset.ScriptKey{ + assetID1.ID(): { + assetID1Tranche1.ScriptKey, + }, + }, + expectedOutputs: map[asset.ID][]*tappsbt.VOutput{ + assetID1.ID(): { + { + Amount: 50, + Type: simple, + Interactive: false, + AnchorOutputIndex: 0, + }, + { + Amount: 50, + Type: split, + Interactive: false, + AnchorOutputIndex: 1, + }, + }, + }, + }, + { + name: "single asset, full value, interactive", + inputs: []*proof.Proof{ + makeProof(t, assetID1Tranche1), + }, + interactive: true, + allocations: []*Allocation{ + { + Type: CommitAllocationToLocal, + Amount: 100, + }, + { + Type: CommitAllocationToRemote, + SplitRoot: true, + Amount: 0, + OutputIndex: 1, + }, + }, + expectedInputs: map[asset.ID][]asset.ScriptKey{ + assetID1.ID(): { + assetID1Tranche1.ScriptKey, + }, + }, + expectedOutputs: map[asset.ID][]*tappsbt.VOutput{ + assetID1.ID(): { + { + Amount: 100, + Type: simple, + Interactive: true, + AnchorOutputIndex: 0, + }, + }, + }, + }, + { + name: "single asset, full value, interactive, has " + + "split output", + inputs: []*proof.Proof{ + makeProof(t, assetID1Tranche1), + }, + interactive: true, + allocations: []*Allocation{ + { + Type: CommitAllocationToLocal, + Amount: 0, + SplitRoot: true, + }, + { + Type: CommitAllocationToRemote, + Amount: 100, + OutputIndex: 1, + }, + }, + expectedInputs: map[asset.ID][]asset.ScriptKey{ + assetID1.ID(): { + assetID1Tranche1.ScriptKey, + }, + }, + expectedOutputs: map[asset.ID][]*tappsbt.VOutput{ + assetID1.ID(): { + { + Amount: 100, + Type: simple, + Interactive: true, + AnchorOutputIndex: 1, + }, + }, + }, + }, + { + name: "single asset, full value, non-interactive", + inputs: []*proof.Proof{ + makeProof(t, assetID1Tranche1), + }, + interactive: false, + allocations: []*Allocation{ + { + Type: CommitAllocationToLocal, + Amount: 100, + }, + { + Type: CommitAllocationToRemote, + SplitRoot: true, + Amount: 0, + OutputIndex: 1, + }, + }, + expectedInputs: map[asset.ID][]asset.ScriptKey{ + assetID1.ID(): { + assetID1Tranche1.ScriptKey, + }, + }, + expectedOutputs: map[asset.ID][]*tappsbt.VOutput{ + assetID1.ID(): { + { + Amount: 100, + Type: simple, + Interactive: false, + AnchorOutputIndex: 0, + }, + { + Amount: 0, + Type: split, + Interactive: false, + AnchorOutputIndex: 1, + }, + }, + }, + }, + { + name: "multiple assets, split, interactive", + inputs: []*proof.Proof{ + makeProof(t, assetID2Tranche1), + makeProof(t, assetID2Tranche2), + }, + interactive: true, + allocations: []*Allocation{ + { + Type: CommitAllocationToLocal, + SplitRoot: true, + Amount: 1200, + }, + { + Type: CommitAllocationToRemote, + Amount: 1800, + OutputIndex: 1, + }, + }, + vPktVersion: tappsbt.V1, + expectedInputs: map[asset.ID][]asset.ScriptKey{ + assetID2.ID(): { + assetID2Tranche1.ScriptKey, + assetID2Tranche2.ScriptKey, + }, + }, + expectedOutputs: map[asset.ID][]*tappsbt.VOutput{ + assetID2.ID(): { + { + Amount: 1200, + Type: split, + Interactive: true, + AnchorOutputIndex: 0, + }, + { + Amount: 1800, + Type: simple, + Interactive: true, + AnchorOutputIndex: 1, + }, + }, + }, + }, + { + name: "multiple assets, split, non-interactive", + inputs: []*proof.Proof{ + makeProof(t, assetID2Tranche1), + makeProof(t, assetID2Tranche2), + }, + interactive: false, + allocations: []*Allocation{ + { + Type: CommitAllocationToLocal, + SplitRoot: true, + Amount: 1200, + }, + { + Type: CommitAllocationToRemote, + Amount: 1800, + OutputIndex: 1, + }, + }, + expectedInputs: map[asset.ID][]asset.ScriptKey{ + assetID2.ID(): { + assetID2Tranche1.ScriptKey, + assetID2Tranche2.ScriptKey, + }, + }, + expectedOutputs: map[asset.ID][]*tappsbt.VOutput{ + assetID2.ID(): { + { + Amount: 1200, + Type: split, + Interactive: false, + AnchorOutputIndex: 0, + }, + { + Amount: 1800, + Type: simple, + Interactive: false, + AnchorOutputIndex: 1, + }, + }, + }, + }, + { + name: "multiple assets, one consumed fully, " + + "interactive", + inputs: []*proof.Proof{ + makeProof(t, assetID1Tranche1), + makeProof(t, assetID2Tranche1), + }, + interactive: true, + allocations: []*Allocation{ + { + Type: CommitAllocationToLocal, + SplitRoot: true, + Amount: 1050, + }, + { + Type: CommitAllocationToRemote, + Amount: 50, + OutputIndex: 1, + }, + }, + expectedInputs: map[asset.ID][]asset.ScriptKey{ + assetID1.ID(): { + assetID1Tranche1.ScriptKey, + }, + assetID2.ID(): { + assetID2Tranche1.ScriptKey, + }, + }, + expectedOutputs: map[asset.ID][]*tappsbt.VOutput{ + assetID1.ID(): { + { + Amount: 100, + Type: simple, + Interactive: true, + AnchorOutputIndex: 0, + }, + }, + assetID2.ID(): { + { + Amount: 950, + Type: split, + Interactive: true, + AnchorOutputIndex: 0, + }, + { + Amount: 50, + Type: simple, + Interactive: true, + AnchorOutputIndex: 1, + }, + }, + }, + }, + { + name: "multiple assets, one consumed fully, " + + "non-interactive", + inputs: []*proof.Proof{ + makeProof(t, assetID1Tranche1), + makeProof(t, assetID2Tranche1), + }, + interactive: false, + allocations: []*Allocation{ + { + Type: CommitAllocationToLocal, + SplitRoot: true, + Amount: 50, + }, + { + Type: CommitAllocationToRemote, + Amount: 1050, + OutputIndex: 1, + }, + }, + expectedInputs: map[asset.ID][]asset.ScriptKey{ + assetID1.ID(): { + assetID1Tranche1.ScriptKey, + }, + assetID2.ID(): { + assetID2Tranche1.ScriptKey, + }, + }, + expectedOutputs: map[asset.ID][]*tappsbt.VOutput{ + assetID1.ID(): { + { + Amount: 50, + Type: split, + Interactive: false, + AnchorOutputIndex: 0, + }, + { + Amount: 50, + Type: simple, + Interactive: false, + AnchorOutputIndex: 1, + }, + }, + assetID2.ID(): { + { + Amount: 0, + Type: split, + Interactive: false, + AnchorOutputIndex: 0, + }, + { + Amount: 1000, + Type: simple, + Interactive: false, + AnchorOutputIndex: 1, + }, + }, + }, + }, + { + name: "lots of assets, interactive", + inputs: []*proof.Proof{ + makeProof(t, assetID1Tranche1), + makeProof(t, assetID1Tranche2), + makeProof(t, assetID1Tranche3), + makeProof(t, assetID2Tranche1), + makeProof(t, assetID2Tranche2), + makeProof(t, assetID2Tranche3), + makeProof(t, assetID3Tranche1), + makeProof(t, assetID3Tranche2), + makeProof(t, assetID3Tranche3), + }, + interactive: true, + allocations: []*Allocation{ + { + Type: CommitAllocationToLocal, + SplitRoot: true, + Amount: 3600, + }, + { + Type: CommitAllocationToRemote, + Amount: 63000, + OutputIndex: 1, + }, + }, + expectedInputs: map[asset.ID][]asset.ScriptKey{ + assetID1.ID(): { + assetID1Tranche1.ScriptKey, + assetID1Tranche2.ScriptKey, + assetID1Tranche3.ScriptKey, + }, + assetID2.ID(): { + assetID2Tranche1.ScriptKey, + assetID2Tranche2.ScriptKey, + assetID2Tranche3.ScriptKey, + }, + assetID3.ID(): { + assetID3Tranche1.ScriptKey, + assetID3Tranche2.ScriptKey, + assetID3Tranche3.ScriptKey, + }, + }, + expectedOutputs: map[asset.ID][]*tappsbt.VOutput{ + assetID1.ID(): { + { + Amount: 600, + Type: simple, + Interactive: true, + AnchorOutputIndex: 0, + }, + }, + assetID2.ID(): { + { + Amount: 3000, + Type: split, + Interactive: true, + AnchorOutputIndex: 0, + }, + { + Amount: 3000, + Type: simple, + Interactive: true, + AnchorOutputIndex: 1, + }, + }, + assetID3.ID(): { + { + Amount: 60000, + Type: simple, + Interactive: true, + AnchorOutputIndex: 1, + }, + }, + }, + }, + { + name: "lots of assets, non-interactive", + inputs: []*proof.Proof{ + makeProof(t, assetID1Tranche1), + makeProof(t, assetID1Tranche2), + makeProof(t, assetID1Tranche3), + makeProof(t, assetID2Tranche1), + makeProof(t, assetID2Tranche2), + makeProof(t, assetID2Tranche3), + makeProof(t, assetID3Tranche1), + makeProof(t, assetID3Tranche2), + makeProof(t, assetID3Tranche3), + }, + interactive: false, + allocations: []*Allocation{ + { + Type: CommitAllocationToLocal, + SplitRoot: true, + Amount: 3600, + }, + { + Type: CommitAllocationToRemote, + Amount: 63000, + OutputIndex: 1, + }, + }, + expectedInputs: map[asset.ID][]asset.ScriptKey{ + assetID1.ID(): { + assetID1Tranche1.ScriptKey, + assetID1Tranche2.ScriptKey, + assetID1Tranche3.ScriptKey, + }, + assetID2.ID(): { + assetID2Tranche1.ScriptKey, + assetID2Tranche2.ScriptKey, + assetID2Tranche3.ScriptKey, + }, + assetID3.ID(): { + assetID3Tranche1.ScriptKey, + assetID3Tranche2.ScriptKey, + assetID3Tranche3.ScriptKey, + }, + }, + expectedOutputs: map[asset.ID][]*tappsbt.VOutput{ + assetID1.ID(): { + { + Amount: 600, + Type: simple, + Interactive: false, + AnchorOutputIndex: 0, + }, + }, + assetID2.ID(): { + { + Amount: 3000, + Type: split, + Interactive: false, + AnchorOutputIndex: 0, + }, + { + Amount: 3000, + Type: simple, + Interactive: false, + AnchorOutputIndex: 1, + }, + }, + assetID3.ID(): { + { + Amount: 0, + Type: split, + Interactive: false, + AnchorOutputIndex: 0, + }, + { + Amount: 60000, + Type: simple, + Interactive: false, + AnchorOutputIndex: 1, + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // We don't care about script keys in this test so we + // just set static ones. + dummyScriptKey := asset.RandScriptKey(t) + scriptKeyGen := StaticScriptKeyGen(dummyScriptKey) + for _, allocation := range tc.allocations { + allocation.GenScriptKey = scriptKeyGen + } + for _, outputs := range tc.expectedOutputs { + for _, output := range outputs { + output.ScriptKey = dummyScriptKey + } + } + + packets, err := DistributeCoins( + tc.inputs, tc.allocations, testParams, + tc.interactive, tc.vPktVersion, + ) + require.NoError(t, err) + + assertPackets( + t, packets, tc.expectedInputs, + tc.expectedOutputs, + ) + for _, pkt := range packets { + require.Equal(t, tc.vPktVersion, pkt.Version) + } + }) + } +} + +func assertPackets(t *testing.T, packets []*tappsbt.VPacket, + expectedInputs map[asset.ID][]asset.ScriptKey, + expectedOutputs map[asset.ID][]*tappsbt.VOutput) { + + for assetID, scriptKeys := range expectedInputs { + packetsByID := fn.Filter( + packets, func(p *tappsbt.VPacket) bool { + return p.Inputs[0].PrevID.ID == assetID + }, + ) + require.Len(t, packetsByID, 1) + + packet := packetsByID[0] + inputKeys := fn.Map( + packet.Inputs, + func(i *tappsbt.VInput) asset.ScriptKey { + pubKey, err := i.PrevID.ScriptKey.ToPubKey() + require.NoError(t, err) + + return asset.NewScriptKey(pubKey) + }, + ) + + require.Equal(t, scriptKeys, inputKeys) + + outputsByID := expectedOutputs[assetID] + require.Equal(t, len(outputsByID), len(packet.Outputs)) + for i, output := range packet.Outputs { + require.Equal(t, outputsByID[i], output) + } + } +} + +// TestAllocatePiece tests the allocation of a piece of an asset. +func TestAllocatePiece(t *testing.T) { + dummyScriptKey := asset.RandScriptKey(t) + scriptKeyGen := StaticScriptKeyGen(dummyScriptKey) + tests := []struct { + name string + piece piece + allocation Allocation + toFill uint64 + interactive bool + expectedErr string + expectedAlloc uint64 + expectedOutput bool + expectedOutputType tappsbt.VOutputType + }{ + { + name: "valid allocation", + piece: piece{ + assetID: asset.ID{1}, + totalAvailable: 100, + allocated: 0, + packet: &tappsbt.VPacket{}, + }, + allocation: Allocation{ + Amount: 50, + GenScriptKey: scriptKeyGen, + }, + toFill: 50, + interactive: true, + expectedAlloc: 50, + expectedOutput: true, + expectedOutputType: tappsbt.TypeSimple, + }, + { + name: "allocation exceeds available", + piece: piece{ + assetID: asset.ID{1}, + totalAvailable: 100, + allocated: 0, + packet: &tappsbt.VPacket{}, + }, + allocation: Allocation{ + Amount: 150, + GenScriptKey: scriptKeyGen, + }, + toFill: 150, + interactive: true, + expectedAlloc: 100, + expectedOutput: true, + expectedOutputType: tappsbt.TypeSimple, + }, + { + name: "allocation with zero to fill", + piece: piece{ + assetID: asset.ID{1}, + totalAvailable: 100, + allocated: 0, + packet: &tappsbt.VPacket{}, + }, + allocation: Allocation{ + Amount: 0, + GenScriptKey: scriptKeyGen, + }, + toFill: 0, + interactive: true, + expectedAlloc: 0, + expectedOutput: false, + }, + { + name: "allocation with split root", + piece: piece{ + assetID: asset.ID{1}, + totalAvailable: 100, + allocated: 0, + packet: &tappsbt.VPacket{}, + }, + allocation: Allocation{ + Amount: 50, + SplitRoot: true, + GenScriptKey: scriptKeyGen, + }, + toFill: 50, + interactive: false, + expectedAlloc: 50, + expectedOutput: true, + expectedOutputType: tappsbt.TypeSplitRoot, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(tt *testing.T) { + allocated, updatedPiece, err := allocatePiece( + tc.piece, tc.allocation, tc.toFill, + tc.interactive, + ) + if tc.expectedErr != "" { + require.ErrorContains(tt, err, tc.expectedErr) + return + } + + require.NoError(tt, err) + require.Equal(tt, tc.expectedAlloc, allocated) + require.Equal( + tt, tc.piece.totalAvailable-tc.expectedAlloc, + updatedPiece.available(), + ) + + if tc.expectedOutput { + require.Len(tt, updatedPiece.packet.Outputs, 1) + require.Equal( + tt, tc.expectedOutputType, + updatedPiece.packet.Outputs[0].Type, + ) + } + }) + } +} + +// TestAllocationsFromTemplate tests that we can correctly turn a virtual packet +// template in a set of allocations. +func TestAllocationsFromTemplate(t *testing.T) { + t.Parallel() + + dummyScriptKey := asset.RandScriptKey(t) + dummyAltLeaves := asset.ToAltLeaves(asset.RandAltLeaves(t, true)) + + var ( + simple = tappsbt.TypeSimple + split = tappsbt.TypeSplitRoot + ) + testCases := []struct { + name string + template *tappsbt.VPacket + inputSum uint64 + expectErr string + interactive bool + allocations []*Allocation + }{ + { + name: "no outputs", + template: &tappsbt.VPacket{}, + expectErr: "spend template has no outputs", + }, + { + name: "mixed interactive and non-interactive", + template: &tappsbt.VPacket{ + Outputs: []*tappsbt.VOutput{ + { + Interactive: true, + }, + { + Interactive: false, + }, + }, + }, + expectErr: "different interactive flags", + }, + { + name: "output greater than input", + template: &tappsbt.VPacket{ + Outputs: []*tappsbt.VOutput{ + { + Amount: 100, + }, + { + Amount: 200, + }, + }, + }, + inputSum: 100, + expectErr: "output amount exceeds input sum", + }, + { + name: "single asset, split, interactive, no " + + "change", + template: &tappsbt.VPacket{ + Outputs: []*tappsbt.VOutput{ + { + Amount: 50, + ScriptKey: dummyScriptKey, + Interactive: true, + AltLeaves: dummyAltLeaves, + }, + }, + }, + inputSum: 100, + interactive: true, + allocations: []*Allocation{ + { + Type: CommitAllocationToLocal, + SplitRoot: true, + Amount: 50, + OutputIndex: 1, + GenScriptKey: StaticScriptKeyGen( + asset.NUMSScriptKey, + ), + }, + { + Type: CommitAllocationToRemote, + Amount: 50, + GenScriptKey: StaticScriptKeyGen( + dummyScriptKey, + ), + AltLeaves: dummyAltLeaves, + }, + }, + }, + { + name: "single asset, split, interactive, w/ " + + "change", + template: &tappsbt.VPacket{ + Outputs: []*tappsbt.VOutput{ + { + Type: split, + Interactive: true, + }, + { + Type: simple, + Interactive: true, + Amount: 50, + AnchorOutputIndex: 1, + }, + }, + }, + inputSum: 100, + interactive: true, + allocations: []*Allocation{ + { + Type: CommitAllocationToLocal, + SplitRoot: true, + Amount: 50, + OutputIndex: 0, + }, + { + Type: CommitAllocationToRemote, + Amount: 50, + OutputIndex: 1, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + allocs, interactive, err := AllocationsFromTemplate( + tc.template, tc.inputSum, + ) + + if tc.expectErr != "" { + require.Contains(t, err.Error(), tc.expectErr) + return + } + + require.NoError(t, err) + + require.Equal(t, tc.interactive, interactive) + + // We first check that the allocations return the + // correct script key from their generator. + for idx := range allocs { + // If the script key doesn't matter, we can + // skip this part and also ignore the generator + // in the comparison. + if tc.allocations[idx].GenScriptKey == nil { + allocs[idx].GenScriptKey = nil + + continue + } + + var id asset.ID + expected, _ := tc.allocations[idx].GenScriptKey( + id, + ) + actual, _ := allocs[idx].GenScriptKey(id) + require.Equal(t, expected, actual) + + // We then remove the generator from the + // allocation to make it easier to compare. + allocs[idx].GenScriptKey = nil + tc.allocations[idx].GenScriptKey = nil + } + + require.Equal(t, tc.allocations, allocs) + }) + } +} diff --git a/tapsend/send.go b/tapsend/send.go index 8ed294303..794944fb7 100644 --- a/tapsend/send.go +++ b/tapsend/send.go @@ -277,108 +277,6 @@ func DescribeAddrs(addrs []*address.Tap) (*FundingDescriptor, error) { return desc, nil } -// AssetFromTapCommitment uses a script key to extract an asset from a given -// Taproot Asset commitment. -func AssetFromTapCommitment(tapCommitment *commitment.TapCommitment, - specifier asset.Specifier, - inputScriptKey btcec.PublicKey) (*asset.Asset, error) { - - // The top-level Taproot Asset tree must have a non-empty asset tree at - // the leaf specified by the funding descriptor's asset (group) specific - // commitment locator. - tapKey := asset.TapCommitmentKey(specifier) - assetCommitments := tapCommitment.Commitments() - assetCommitment, ok := assetCommitments[tapKey] - if !ok { - return nil, fmt.Errorf("input commitment does "+ - "not contain asset=%s: %w", &specifier, - ErrMissingInputAsset) - } - - // Determine whether issuance is disabled for the asset. - issuanceDisabled := !specifier.HasGroupPubKey() - - assetId, err := specifier.UnwrapIdOrErr() - if err != nil { - return nil, fmt.Errorf("asset from tap commitment: %w", err) - } - - // The asset tree must have a non-empty Asset at the location - // specified by the sender's script key. - assetCommitmentKey := asset.AssetCommitmentKey( - assetId, &inputScriptKey, issuanceDisabled, - ) - inputAsset, ok := assetCommitment.Asset(assetCommitmentKey) - if !ok { - return nil, fmt.Errorf("input commitment does not "+ - "contain leaf with script_key=%x: %w", - inputScriptKey.SerializeCompressed(), - ErrMissingInputAsset) - } - - return inputAsset, nil -} - -// ValidateInputs validates a set of inputs against a funding request. It -// returns true if the inputs would be spent fully, otherwise false. -func ValidateInputs(inputCommitments tappsbt.InputCommitments, - expectedAssetType asset.Type, specifier asset.Specifier, - outputAmount uint64) (bool, error) { - - // Extract the input assets from the input commitments. - inputAssets := make([]*asset.Asset, 0, len(inputCommitments)) - for prevID := range inputCommitments { - tapCommitment := inputCommitments[prevID] - senderScriptKey, err := prevID.ScriptKey.ToPubKey() - if err != nil { - return false, fmt.Errorf("unable to parse sender "+ - "script key: %v", err) - } - - // Gain the asset that we'll use as an input and in the process - // validate the selected input and commitment. - inputAsset, err := AssetFromTapCommitment( - tapCommitment, specifier, *senderScriptKey, - ) - if err != nil { - return false, err - } - - // Ensure input asset has the expected type. - if inputAsset.Type != expectedAssetType { - return false, fmt.Errorf("unexpected input asset type") - } - - inputAssets = append(inputAssets, inputAsset) - } - - // Validate total amount of input assets and determine full value spend - // status. - var isFullValueSpend bool - switch expectedAssetType { - case asset.Normal: - // Sum the total amount of the input assets. - var totalInputsAmount uint64 - for _, inputAsset := range inputAssets { - totalInputsAmount += inputAsset.Amount - } - - // Ensure that the input assets are sufficient to cover the amount - // being sent. - if totalInputsAmount < outputAmount { - return false, ErrInsufficientInputAssets - } - - // Check if the input assets are fully spent. - isFullValueSpend = totalInputsAmount == outputAmount - - case asset.Collectible: - isFullValueSpend = true - } - - return isFullValueSpend, nil -} - // ValidateCommitmentKeysUnique makes sure the outputs of a set of virtual // packets don't lead to collisions in and of the trees (e.g. two asset outputs // with the same asset ID and script key in the same anchor output) or with diff --git a/tapsend/send_test.go b/tapsend/send_test.go index bbcaa4014..ad5501f33 100644 --- a/tapsend/send_test.go +++ b/tapsend/send_test.go @@ -1873,224 +1873,6 @@ func TestProofVerifyFullValueSplit(t *testing.T) { require.NoError(t, err) } -// TestAddressValidInput tests edge cases around validating inputs for asset -// transfers with isValidInput. -func TestAddressValidInput(t *testing.T) { - t.Parallel() - - for _, testCase := range addressValidInputTestCases { - success := t.Run(testCase.name, func(t *testing.T) { - inputAsset, checkedInputAsset, err := testCase.f(t) - require.ErrorIs(t, err, testCase.err) - if testCase.err == nil { - require.True(t, inputAsset.DeepEqual( - checkedInputAsset, - )) - } - }) - if !success { - return - } - } -} - -func addrToFundDesc(addr address.Tap) *tapsend.FundingDescriptor { - assetSpecifier := asset.NewSpecifierOptionalGroupPubKey( - addr.AssetID, addr.GroupKey, - ) - - return &tapsend.FundingDescriptor{ - AssetSpecifier: assetSpecifier, - Amount: addr.Amount, - } -} - -type addressValidInputTestCase struct { - name string - f func(t *testing.T) (*asset.Asset, *asset.Asset, error) - err error -} - -var addressValidInputTestCases = []addressValidInputTestCase{{ - name: "valid normal", - f: func(t *testing.T) (*asset.Asset, *asset.Asset, error) { - state := initSpendScenario(t) - fundDesc := addrToFundDesc(state.address1) - - inputAsset, err := tapsend.AssetFromTapCommitment( - &state.asset1TapTree, fundDesc.AssetSpecifier, - state.spenderScriptKey, - ) - if err != nil { - return nil, nil, err - } - - fullValue, err := tapsend.ValidateInputs( - tappsbt.InputCommitments{ - state.asset1PrevID: &state.asset1TapTree, - }, inputAsset.Type, fundDesc.AssetSpecifier, - fundDesc.Amount, - ) - if err != nil { - return nil, nil, err - } - require.True(t, fullValue) - - return &state.asset1, inputAsset, nil - }, - err: nil, -}, { - name: "valid collectible with group key", - f: func(t *testing.T) (*asset.Asset, *asset.Asset, error) { - state := initSpendScenario(t) - fundDesc := addrToFundDesc(state.address1CollectGroup) - - inputAsset, err := tapsend.AssetFromTapCommitment( - &state.asset1CollectGroupTapTree, - fundDesc.AssetSpecifier, state.spenderScriptKey, - ) - if err != nil { - return nil, nil, err - } - - inputCommitment := &state.asset1CollectGroupTapTree - fullValue, err := tapsend.ValidateInputs( - tappsbt.InputCommitments{ - state.asset1CollectGroupPrevID: inputCommitment, - }, inputAsset.Type, fundDesc.AssetSpecifier, - fundDesc.Amount, - ) - if err != nil { - return nil, nil, err - } - require.True(t, fullValue) - - return &state.asset1CollectGroup, inputAsset, nil - }, - err: nil, -}, { - name: "valid asset split", - f: func(t *testing.T) (*asset.Asset, *asset.Asset, error) { - state := initSpendScenario(t) - fundDesc := addrToFundDesc(state.address1) - - inputAsset, err := tapsend.AssetFromTapCommitment( - &state.asset2TapTree, fundDesc.AssetSpecifier, - state.spenderScriptKey, - ) - if err != nil { - return nil, nil, err - } - - fullValue, err := tapsend.ValidateInputs( - tappsbt.InputCommitments{ - state.asset2PrevID: &state.asset2TapTree, - }, inputAsset.Type, fundDesc.AssetSpecifier, - fundDesc.Amount, - ) - if err != nil { - return nil, nil, err - } - require.False(t, fullValue) - - return &state.asset2, inputAsset, nil - }, - err: nil, -}, { - name: "normal with insufficient amount", - f: func(t *testing.T) (*asset.Asset, *asset.Asset, error) { - state := initSpendScenario(t) - fundDesc := addrToFundDesc(state.address2) - - inputAsset, err := tapsend.AssetFromTapCommitment( - &state.asset1TapTree, fundDesc.AssetSpecifier, - state.spenderScriptKey, - ) - if err != nil { - return nil, nil, err - } - - fullValue, err := tapsend.ValidateInputs( - tappsbt.InputCommitments{ - state.asset1PrevID: &state.asset1TapTree, - }, inputAsset.Type, fundDesc.AssetSpecifier, - fundDesc.Amount, - ) - if err != nil { - return nil, nil, err - } - require.True(t, fullValue) - - return &state.asset1, inputAsset, nil - }, - err: tapsend.ErrInsufficientInputAssets, -}, { - name: "collectible with missing input asset", - f: func(t *testing.T) (*asset.Asset, *asset.Asset, error) { - state := initSpendScenario(t) - fundDesc := addrToFundDesc(state.address1CollectGroup) - - inputAsset, err := tapsend.AssetFromTapCommitment( - &state.asset1TapTree, fundDesc.AssetSpecifier, - state.spenderScriptKey, - ) - if err != nil { - return nil, nil, err - } - - fullValue, err := tapsend.ValidateInputs( - tappsbt.InputCommitments{ - state.asset1PrevID: &state.asset1TapTree, - }, inputAsset.Type, fundDesc.AssetSpecifier, - fundDesc.Amount, - ) - if err != nil { - return nil, nil, err - } - require.False(t, fullValue) - - return &state.asset1, inputAsset, nil - }, - err: tapsend.ErrMissingInputAsset, -}, { - name: "normal with bad sender script key", - f: func(t *testing.T) (*asset.Asset, *asset.Asset, error) { - state := initSpendScenario(t) - - address1testnet, err := address.New( - address.V0, state.genesis1, nil, nil, - state.receiverPubKey, state.receiverPubKey, - state.normalAmt1, nil, &address.TestNet3Tap, - address.RandProofCourierAddr(t), - ) - require.NoError(t, err) - - fundDesc := addrToFundDesc(*address1testnet) - - inputAsset, err := tapsend.AssetFromTapCommitment( - &state.asset1TapTree, fundDesc.AssetSpecifier, - state.receiverPubKey, - ) - if err != nil { - return nil, nil, err - } - - fullValue, err := tapsend.ValidateInputs( - tappsbt.InputCommitments{ - state.asset1PrevID: &state.asset1TapTree, - }, inputAsset.Type, fundDesc.AssetSpecifier, - fundDesc.Amount, - ) - if err != nil { - return nil, nil, err - } - require.True(t, fullValue) - - return &state.asset1, inputAsset, nil - }, - err: tapsend.ErrMissingInputAsset, -}} - // TestPayToAddrScript tests edge cases around creating a P2TR script with // PayToAddrScript. func TestPayToAddrScript(t *testing.T) {