Skip to content

Commit c61d19f

Browse files
authored
Merge pull request #743 from lightninglabs/anchorvp-multi-inputs
Multi-input PSBT spend for homogeneous input asset ID
2 parents 3b56d4e + 820f4a8 commit c61d19f

File tree

4 files changed

+211
-34
lines changed

4 files changed

+211
-34
lines changed

itest/psbt_test.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,163 @@ func testPsbtMultiSend(t *harnessTest) {
953953
)
954954
}
955955

956+
// testMultiInputPsbtSingleAssetID tests to ensure that we can correctly
957+
// construct and spend a multi-input full value PSBT where each input has the
958+
// same asset ID.
959+
//
960+
// The test works as follows:
961+
// 1. Mint an asset on the primary tapd node.
962+
// 2. Send the asset to a secondary tapd node in two different send events.
963+
// 3. Send the asset back to the primary tapd node in a single multi-input PSBT
964+
// send event.
965+
func testMultiInputPsbtSingleAssetID(t *harnessTest) {
966+
var (
967+
ctxb = context.Background()
968+
primaryTapd = t.tapd
969+
)
970+
971+
// Mint a single asset.
972+
rpcAssets := MintAssetsConfirmBatch(
973+
t.t, t.lndHarness.Miner.Client, primaryTapd,
974+
[]*mintrpc.MintAssetRequest{simpleAssets[0]},
975+
)
976+
rpcAsset := rpcAssets[0]
977+
978+
// Set up a node that will serve as the final multi input PSBT sender
979+
// node.
980+
secondaryTapd := setupTapdHarness(
981+
t.t, t, t.lndHarness.Bob, t.universeServer,
982+
)
983+
defer func() {
984+
require.NoError(t.t, secondaryTapd.stop(!*noDelete))
985+
}()
986+
987+
// First of two send events from primary (minting) node to secondary
988+
// node.
989+
genInfo := rpcAsset.AssetGenesis
990+
addr, err := secondaryTapd.NewAddr(
991+
ctxb, &taprpc.NewAddrRequest{
992+
AssetId: genInfo.AssetId,
993+
Amt: 1000,
994+
},
995+
)
996+
require.NoError(t.t, err)
997+
AssertAddrCreated(t.t, secondaryTapd, rpcAsset, addr)
998+
999+
// Send the assets to the secondary node.
1000+
sendResp := sendAssetsToAddr(t, primaryTapd, addr)
1001+
1002+
ConfirmAndAssertOutboundTransfer(
1003+
t.t, t.lndHarness.Miner.Client, primaryTapd, sendResp,
1004+
genInfo.AssetId, []uint64{4000, 1000}, 0, 1,
1005+
)
1006+
1007+
AssertNonInteractiveRecvComplete(t.t, secondaryTapd, 1)
1008+
1009+
// Second of two send events from primary (minting) node to the
1010+
// secondary node.
1011+
addr, err = secondaryTapd.NewAddr(
1012+
ctxb, &taprpc.NewAddrRequest{
1013+
AssetId: genInfo.AssetId,
1014+
Amt: 4000,
1015+
},
1016+
)
1017+
require.NoError(t.t, err)
1018+
AssertAddrCreated(t.t, secondaryTapd, rpcAsset, addr)
1019+
1020+
// Send the assets to the secondary node.
1021+
sendResp = sendAssetsToAddr(t, primaryTapd, addr)
1022+
1023+
ConfirmAndAssertOutboundTransfer(
1024+
t.t, t.lndHarness.Miner.Client, primaryTapd, sendResp,
1025+
genInfo.AssetId, []uint64{0, 4000}, 1, 2,
1026+
)
1027+
1028+
AssertNonInteractiveRecvComplete(t.t, secondaryTapd, 2)
1029+
1030+
t.Logf("Two separate send events complete, now attempting to send " +
1031+
"back the full amount in a single multi-input PSBT send event")
1032+
1033+
// Ensure that the primary node has no assets before we begin.
1034+
primaryNodeAssets, err := primaryTapd.ListAssets(
1035+
ctxb, &taprpc.ListAssetRequest{},
1036+
)
1037+
require.NoError(t.t, err)
1038+
require.Empty(t.t, primaryNodeAssets.Assets)
1039+
1040+
// We need to derive two keys for the receiver node, one for the new
1041+
// script key and one for the internal key.
1042+
receiverScriptKey, receiverAnchorIntKeyDesc := deriveKeys(
1043+
t.t, primaryTapd,
1044+
)
1045+
1046+
var assetId asset.ID
1047+
copy(assetId[:], genInfo.AssetId)
1048+
1049+
var (
1050+
chainParams = &address.RegressionNetTap
1051+
sendAmt = uint64(5000)
1052+
)
1053+
1054+
vPkt := tappsbt.ForInteractiveSend(
1055+
assetId, sendAmt, receiverScriptKey, 0,
1056+
receiverAnchorIntKeyDesc, asset.V0, chainParams,
1057+
)
1058+
1059+
// Next, we'll attempt to fund the PSBT.
1060+
fundResp := fundPacket(t, secondaryTapd, vPkt)
1061+
signResp, err := secondaryTapd.SignVirtualPsbt(
1062+
ctxb, &wrpc.SignVirtualPsbtRequest{
1063+
FundedPsbt: fundResp.FundedPsbt,
1064+
},
1065+
)
1066+
require.NoError(t.t, err)
1067+
1068+
// And finally anchor the PSBT in the BTC chain to complete the
1069+
// transfer.
1070+
sendResp, err = secondaryTapd.AnchorVirtualPsbts(
1071+
ctxb, &wrpc.AnchorVirtualPsbtsRequest{
1072+
VirtualPsbts: [][]byte{signResp.SignedPsbt},
1073+
},
1074+
)
1075+
require.NoError(t.t, err)
1076+
1077+
var (
1078+
currentTransferIdx = 0
1079+
numTransfers = 1
1080+
numOutputs = 1
1081+
)
1082+
ConfirmAndAssetOutboundTransferWithOutputs(
1083+
t.t, t.lndHarness.Miner.Client, secondaryTapd,
1084+
sendResp, genInfo.AssetId,
1085+
[]uint64{sendAmt}, currentTransferIdx, numTransfers,
1086+
numOutputs,
1087+
)
1088+
1089+
// This is an interactive transfer. Therefore, we will manually transfer
1090+
// the proof from the sender to the receiver.
1091+
_ = sendProof(
1092+
t, secondaryTapd, primaryTapd,
1093+
receiverScriptKey.PubKey.SerializeCompressed(), genInfo,
1094+
)
1095+
1096+
// Finally, we make sure that the primary node has the asset.
1097+
primaryNodeAssets, err = primaryTapd.ListAssets(
1098+
ctxb, &taprpc.ListAssetRequest{},
1099+
)
1100+
require.NoError(t.t, err)
1101+
require.Len(t.t, primaryNodeAssets.Assets, 1)
1102+
1103+
// Ensure that the asset is the one we expect.
1104+
primaryNodeAsset := primaryNodeAssets.Assets[0]
1105+
1106+
require.Equal(t.t, primaryNodeAsset.Amount, sendAmt)
1107+
1108+
var foundAssetId asset.ID
1109+
copy(foundAssetId[:], primaryNodeAsset.AssetGenesis.AssetId)
1110+
require.Equal(t.t, assetId, foundAssetId)
1111+
}
1112+
9561113
func deriveKeys(t *testing.T, tapd *tapdHarness) (asset.ScriptKey,
9571114
keychain.KeyDescriptor) {
9581115

itest/test_list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ var testCases = []*testCase{
167167
name: "psbt multi send",
168168
test: testPsbtMultiSend,
169169
},
170+
{
171+
name: "multi input psbt single asset id",
172+
test: testMultiInputPsbtSingleAssetID,
173+
},
170174
{
171175
name: "universe REST API",
172176
test: testUniverseREST,

rpcserver.go

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1757,30 +1757,28 @@ func (r *rpcServer) AnchorVirtualPsbts(ctx context.Context,
17571757
return nil, fmt.Errorf("error decoding packet: %w", err)
17581758
}
17591759

1760-
if len(vPacket.Inputs) != 1 {
1761-
return nil, fmt.Errorf("only one input is currently supported")
1762-
}
1760+
// Query the asset store to gather tap commitments for all inputs.
1761+
inputCommitments := make(tappsbt.InputCommitments, len(vPacket.Inputs))
1762+
for idx := range vPacket.Inputs {
1763+
input := vPacket.Inputs[idx]
17631764

1764-
inputAsset := vPacket.Inputs[0].Asset()
1765-
prevID := vPacket.Inputs[0].PrevID
1766-
inputCommitment, err := r.cfg.AssetStore.FetchCommitment(
1767-
ctx, inputAsset.ID(), prevID.OutPoint, inputAsset.GroupKey,
1768-
&inputAsset.ScriptKey, true,
1769-
)
1770-
if err != nil {
1771-
return nil, fmt.Errorf("error fetching input commitment: %w",
1772-
err)
1773-
}
1765+
inputAsset := input.Asset()
1766+
prevID := input.PrevID
1767+
1768+
inputCommitment, err := r.cfg.AssetStore.FetchCommitment(
1769+
ctx, inputAsset.ID(), prevID.OutPoint,
1770+
inputAsset.GroupKey, &inputAsset.ScriptKey, true,
1771+
)
1772+
if err != nil {
1773+
return nil, fmt.Errorf("error fetching input "+
1774+
"commitment: %w", err)
1775+
}
17741776

1775-
rpcsLog.Debugf("Selected commitment for anchor point %v, requesting "+
1776-
"delivery", inputCommitment.AnchorPoint)
1777+
inputCommitments[idx] = inputCommitment.Commitment
1778+
}
17771779

17781780
resp, err := r.cfg.ChainPorter.RequestShipment(
1779-
tapfreighter.NewPreSignedParcel(
1780-
vPacket, tappsbt.InputCommitments{
1781-
0: inputCommitment.Commitment,
1782-
},
1783-
),
1781+
tapfreighter.NewPreSignedParcel(vPacket, inputCommitments),
17841782
)
17851783
if err != nil {
17861784
return nil, fmt.Errorf("error requesting delivery: %w", err)

tapscript/send.go

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -506,32 +506,50 @@ func PrepareOutputAssets(ctx context.Context, vPkt *tappsbt.VPacket) error {
506506
)
507507

508508
if isFullValueInteractiveSend {
509-
if len(inputs) != 1 {
510-
return fmt.Errorf("full value interactive send " +
511-
"must have exactly one input")
509+
// Sum the total amount of the input assets.
510+
inputsAmountSum := uint64(0)
511+
for idx := range inputs {
512+
input := inputs[idx]
513+
514+
// At the moment, we need to ensure that all inputs have
515+
// the same asset ID. We've already checked that above,
516+
// but we will do it again here for clarity.
517+
if inputs[idx].Asset().ID() != assetID {
518+
return fmt.Errorf("multiple input assets " +
519+
"must have the same asset ID")
520+
}
521+
522+
inputsAmountSum += input.Asset().Amount
512523
}
513524

514-
// TODO(ffranr): Add support for interactive full value multiple
515-
// input spend.
516-
input := inputs[0]
517-
vOut := outputs[recipientIndex]
525+
// At this point we know that each input has the same asset ID
526+
// we therefore arbitrarily select the first input as our
527+
// template output asset.
528+
firstInput := inputs[0]
518529

519530
// We'll now create a new copy of the old asset, swapping out
520531
// the script key. We blank out the tweaked key information as
521532
// this is now an external asset.
522-
vOut.Asset = input.Asset().Copy()
533+
vOut := outputs[recipientIndex]
534+
vOut.Asset = firstInput.Asset().Copy()
535+
vOut.Asset.Amount = inputsAmountSum
523536
vOut.Asset.ScriptKey = vOut.ScriptKey
524537

525-
// Record the PrevID of the input asset in a Witness for the new
526-
// asset. This Witness still needs a valid signature for the new
527-
// asset to be valid.
528-
vOut.Asset.PrevWitnesses = []asset.Witness{
529-
{
538+
// Gather previous witnesses from the input assets.
539+
prevWitnesses := make([]asset.Witness, len(inputs))
540+
for idx := range inputs {
541+
input := inputs[idx]
542+
543+
// Record the PrevID of the input asset in a Witness for
544+
// the new asset. This Witness still needs a valid
545+
// signature for the new asset to be valid.
546+
prevWitnesses[idx] = asset.Witness{
530547
PrevID: &input.PrevID,
531548
TxWitness: nil,
532549
SplitCommitment: nil,
533-
},
550+
}
534551
}
552+
vOut.Asset.PrevWitnesses = prevWitnesses
535553

536554
// Adjust the version for the requested send type.
537555
vOut.Asset.Version = vOut.AssetVersion

0 commit comments

Comments
 (0)