Skip to content

Commit 8fcd27c

Browse files
author
ffranr
authored
Merge pull request #1074 from lightninglabs/spend-change-undelivered-proof
Spend transaction change outputs even if undelivered proof(s)
2 parents 4ec0dd4 + 6f0f23f commit 8fcd27c

16 files changed

+1437
-859
lines changed

itest/send_test.go

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,248 @@ func testReattemptFailedSendUniCourier(t *harnessTest) {
951951
wg.Wait()
952952
}
953953

954+
// testSpendChangeOutputWhenProofTransferFail tests that a tapd node is able
955+
// to spend a change output even if the proof transfer for the previous
956+
// transaction fails.
957+
func testSpendChangeOutputWhenProofTransferFail(t *harnessTest) {
958+
var (
959+
ctxb = context.Background()
960+
wg sync.WaitGroup
961+
)
962+
963+
// For this test we will use the universe server as the proof courier.
964+
proofCourier := t.universeServer
965+
966+
// Make a new tapd node which will send an asset to a receiving tapd
967+
// node.
968+
sendTapd := setupTapdHarness(
969+
t.t, t, t.lndHarness.Bob, t.universeServer,
970+
func(params *tapdHarnessParams) {
971+
params.expectErrExit = true
972+
params.proofCourier = proofCourier
973+
},
974+
)
975+
defer func() {
976+
// Any node that has been started within an itest should be
977+
// explicitly stopped within the same itest.
978+
require.NoError(t.t, sendTapd.stop(!*noDelete))
979+
}()
980+
981+
// Use the primary tapd node as the receiver node.
982+
recvTapd := t.tapd
983+
984+
// Use the sending node to mint an asset for sending.
985+
rpcAssets := MintAssetsConfirmBatch(
986+
t.t, t.lndHarness.Miner.Client, sendTapd,
987+
[]*mintrpc.MintAssetRequest{simpleAssets[0]},
988+
)
989+
990+
genInfo := rpcAssets[0].AssetGenesis
991+
992+
// After minting an asset with the sending node, we need to synchronize
993+
// the Universe state to ensure the receiving node is updated and aware
994+
// of the asset.
995+
t.syncUniverseState(sendTapd, recvTapd, len(rpcAssets))
996+
997+
// Create a new address for the receiver node. We will use the universe
998+
// server as the proof courier.
999+
proofCourierAddr := fmt.Sprintf(
1000+
"%s://%s", proof.UniverseRpcCourierType,
1001+
proofCourier.service.rpcHost(),
1002+
)
1003+
t.Logf("Proof courier address: %s", proofCourierAddr)
1004+
1005+
recvAddr, err := recvTapd.NewAddr(ctxb, &taprpc.NewAddrRequest{
1006+
AssetId: genInfo.AssetId,
1007+
Amt: 10,
1008+
ProofCourierAddr: proofCourierAddr,
1009+
})
1010+
require.NoError(t.t, err)
1011+
AssertAddrCreated(t.t, recvTapd, rpcAssets[0], recvAddr)
1012+
1013+
// Soon we will be attempting to send an asset to the receiver node. We
1014+
// want any associated proof delivery attempt to fail. Therefore, we
1015+
// will take the proof courier service offline.
1016+
t.Log("Stopping proof courier service")
1017+
require.NoError(t.t, proofCourier.Stop())
1018+
1019+
// Now that the proof courier service is offline, the sending node's
1020+
// attempt to transfer the asset proof should fail.
1021+
//
1022+
// We will soon start the asset transfer process. However, before we
1023+
// start, we subscribe to the send events from the sending tapd node so
1024+
// that we can be sure that a proof delivery has been attempted
1025+
// unsuccessfully. We assert that at least a single proof delivery
1026+
// attempt has been made by identifying a backoff wait event.
1027+
events := SubscribeSendEvents(t.t, sendTapd)
1028+
1029+
wg.Add(1)
1030+
go func() {
1031+
defer wg.Done()
1032+
1033+
// Define a target event selector to match the backoff wait
1034+
// event. This function selects for a specific event type.
1035+
targetEventSelector := func(
1036+
event *tapdevrpc.SendAssetEvent) bool {
1037+
1038+
return AssertSendEventProofTransferBackoffWaitTypeSend(
1039+
t, event,
1040+
)
1041+
}
1042+
1043+
// Set the context timeout for detecting a single proof delivery
1044+
// attempt to something reasonable.
1045+
timeout := 2 * defaultProofTransferReceiverAckTimeout
1046+
1047+
assertAssetNtfsEvent(
1048+
t, events, timeout, targetEventSelector, 1,
1049+
)
1050+
}()
1051+
1052+
// Start asset transfer and then mine to confirm the associated on-chain
1053+
// tx. The on-chain tx should be mined successfully, but we expect the
1054+
// asset proof transfer to be unsuccessful.
1055+
sendAssetsToAddr(t, sendTapd, recvAddr)
1056+
MineBlocks(t.t, t.lndHarness.Miner.Client, 1, 1)
1057+
1058+
// There may be a delay between mining the anchoring transaction and
1059+
// recognizing its on-chain confirmation. To handle this potential
1060+
// delay, we use require.Eventually to ensure the transfer details are
1061+
// correctly listed after confirmation.
1062+
require.Eventually(t.t, func() bool {
1063+
// Ensure that the transaction took place as expected.
1064+
listTransfersResp, err := sendTapd.ListTransfers(
1065+
ctxb, &taprpc.ListTransfersRequest{},
1066+
)
1067+
require.NoError(t.t, err)
1068+
1069+
require.Len(t.t, listTransfersResp.Transfers, 1)
1070+
1071+
firstTransfer := listTransfersResp.Transfers[0]
1072+
require.NotEqual(t.t, firstTransfer.AnchorTxHeightHint, 0)
1073+
require.NotEmpty(t.t, firstTransfer.AnchorTxBlockHash)
1074+
1075+
// Assert proof transfer status for each transfer output.
1076+
require.Len(t.t, firstTransfer.Outputs, 2)
1077+
1078+
// First output should have a proof delivery status of not
1079+
// applicable. This indicates that a proof will not be delivered
1080+
// for this output.
1081+
firstOutput := firstTransfer.Outputs[0]
1082+
require.Equal(
1083+
t.t, taprpc.ProofDeliveryStatusNotApplicable,
1084+
firstOutput.ProofDeliveryStatus,
1085+
)
1086+
1087+
// The second output should have a proof delivery status of
1088+
// pending. This indicates that the proof deliver has not yet
1089+
// completed successfully.
1090+
secondOutput := firstTransfer.Outputs[1]
1091+
require.Equal(
1092+
t.t, taprpc.ProofDeliveryStatusPending,
1093+
secondOutput.ProofDeliveryStatus,
1094+
)
1095+
1096+
return true
1097+
}, defaultWaitTimeout, 200*time.Millisecond)
1098+
1099+
// Wait to ensure that the asset transfer proof deliver attempt has been
1100+
// made.
1101+
wg.Wait()
1102+
1103+
// Attempt to send the change output to the receiver node. This
1104+
// operation should select the change output from the previous
1105+
// transaction and transmit it to the receiver node, despite the fact
1106+
// that proof delivery for the previous transaction remains incomplete
1107+
// (due to the proof courier being shut down). We will generate a new
1108+
// address for this new transaction.
1109+
recvAddr, err = recvTapd.NewAddr(ctxb, &taprpc.NewAddrRequest{
1110+
AssetId: genInfo.AssetId,
1111+
Amt: 42,
1112+
ProofCourierAddr: proofCourierAddr,
1113+
})
1114+
require.NoError(t.t, err)
1115+
AssertAddrCreated(t.t, recvTapd, rpcAssets[0], recvAddr)
1116+
1117+
sendAssetsToAddr(t, sendTapd, recvAddr)
1118+
MineBlocks(t.t, t.lndHarness.Miner.Client, 1, 1)
1119+
1120+
// There may be a delay between mining the anchoring transaction and
1121+
// recognizing its on-chain confirmation. To handle this potential
1122+
// delay, we use require.Eventually to ensure the transfer details are
1123+
// correctly listed after confirmation.
1124+
require.Eventually(t.t, func() bool {
1125+
// Ensure that the transaction took place as expected.
1126+
listTransfersResp, err := sendTapd.ListTransfers(
1127+
ctxb, &taprpc.ListTransfersRequest{},
1128+
)
1129+
require.NoError(t.t, err)
1130+
1131+
require.Len(t.t, listTransfersResp.Transfers, 2)
1132+
1133+
// Inspect the first transfer.
1134+
firstTransfer := listTransfersResp.Transfers[0]
1135+
require.NotEqual(t.t, firstTransfer.AnchorTxHeightHint, 0)
1136+
require.NotEmpty(t.t, firstTransfer.AnchorTxBlockHash)
1137+
1138+
// Assert proof transfer status for each transfer output.
1139+
require.Len(t.t, firstTransfer.Outputs, 2)
1140+
1141+
// First output should have a proof delivery status of not
1142+
// applicable. This indicates that a proof will not be delivered
1143+
// for this output.
1144+
firstOutput := firstTransfer.Outputs[0]
1145+
require.Equal(
1146+
t.t, taprpc.ProofDeliveryStatusNotApplicable,
1147+
firstOutput.ProofDeliveryStatus,
1148+
)
1149+
1150+
// The second output should have a proof delivery status of
1151+
// pending. This indicates that the proof deliver has not yet
1152+
// completed successfully.
1153+
secondOutput := firstTransfer.Outputs[1]
1154+
require.Equal(
1155+
t.t, taprpc.ProofDeliveryStatusPending,
1156+
secondOutput.ProofDeliveryStatus,
1157+
)
1158+
1159+
// Inspect the second transfer.
1160+
secondTransfer := listTransfersResp.Transfers[1]
1161+
require.NotEqual(t.t, secondTransfer.AnchorTxHeightHint, 0)
1162+
require.NotEmpty(t.t, secondTransfer.AnchorTxBlockHash)
1163+
1164+
// Assert proof transfer status for each transfer output.
1165+
require.Len(t.t, secondTransfer.Outputs, 2)
1166+
1167+
// First output should have a proof delivery status of not
1168+
// applicable. This indicates that a proof will not be delivered
1169+
// for this output.
1170+
firstOutput = secondTransfer.Outputs[0]
1171+
require.Equal(
1172+
t.t, taprpc.ProofDeliveryStatusNotApplicable,
1173+
firstOutput.ProofDeliveryStatus,
1174+
)
1175+
1176+
// The second output should have a proof delivery status of
1177+
// pending. This indicates that the proof deliver has not yet
1178+
// completed successfully.
1179+
secondOutput = secondTransfer.Outputs[1]
1180+
require.Equal(
1181+
t.t, taprpc.ProofDeliveryStatusPending,
1182+
secondOutput.ProofDeliveryStatus,
1183+
)
1184+
1185+
return true
1186+
}, defaultWaitTimeout, 200*time.Millisecond)
1187+
1188+
// Restart the proof courier service.
1189+
t.Log("Starting proof courier service")
1190+
require.NoError(t.t, proofCourier.Start(nil))
1191+
1192+
// TODO(ffranr): Assert proof transfer complete after proof courier
1193+
// restart.
1194+
}
1195+
9541196
// testReattemptFailedReceiveUniCourier ensures that a failed attempt to receive
9551197
// an asset proof is retried by the receiving Tapd node. This test focuses on
9561198
// the universe proof courier.

itest/test_list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ var testCases = []*testCase{
9696
name: "reattempt proof transfer on tapd restart",
9797
test: testReattemptProofTransferOnTapdRestart,
9898
},
99+
{
100+
name: "spend change output when proof transfer fail",
101+
test: testSpendChangeOutputWhenProofTransferFail,
102+
},
99103
{
100104
name: "reattempt failed receive uni courier",
101105
test: testReattemptFailedReceiveUniCourier,

rpcserver.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3367,6 +3367,9 @@ func marshalOutboundParcel(
33673367
return nil, err
33683368
}
33693369

3370+
// Marshall the proof delivery status.
3371+
proofDeliveryStatus := marshalOutputProofDeliveryStatus(out)
3372+
33703373
rpcOutputs[idx] = &taprpc.TransferOutput{
33713374
Anchor: rpcAnchor,
33723375
ScriptKey: scriptPubKey.SerializeCompressed(),
@@ -3378,20 +3381,46 @@ func marshalOutboundParcel(
33783381
SplitCommitRootHash: splitCommitRoot,
33793382
OutputType: rpcOutType,
33803383
AssetVersion: assetVersion,
3384+
ProofDeliveryStatus: proofDeliveryStatus,
33813385
}
33823386
}
33833387

33843388
anchorTxHash := parcel.AnchorTx.TxHash()
3389+
3390+
// Marshal the anchor tx block hash.
3391+
var anchorTxBlockHashBytes []byte
3392+
parcel.AnchorTxBlockHash.WhenSome(func(hash chainhash.Hash) {
3393+
anchorTxBlockHashBytes = hash[:]
3394+
})
3395+
33853396
return &taprpc.AssetTransfer{
33863397
TransferTimestamp: parcel.TransferTime.Unix(),
33873398
AnchorTxHash: anchorTxHash[:],
33883399
AnchorTxHeightHint: parcel.AnchorTxHeightHint,
33893400
AnchorTxChainFees: parcel.ChainFees,
3401+
AnchorTxBlockHash: anchorTxBlockHashBytes,
33903402
Inputs: rpcInputs,
33913403
Outputs: rpcOutputs,
33923404
}, nil
33933405
}
33943406

3407+
// marshalOutputProofDeliveryStatus turns the output proof delivery status into
3408+
// the RPC counterpart.
3409+
func marshalOutputProofDeliveryStatus(
3410+
out tapfreighter.TransferOutput) taprpc.ProofDeliveryStatus {
3411+
3412+
proofDeliveryStatus := taprpc.ProofDeliveryStatusNotApplicable
3413+
out.ProofDeliveryComplete.WhenSome(func(complete bool) {
3414+
if complete {
3415+
proofDeliveryStatus = taprpc.ProofDeliveryStatusComplete
3416+
} else {
3417+
proofDeliveryStatus = taprpc.ProofDeliveryStatusPending
3418+
}
3419+
})
3420+
3421+
return proofDeliveryStatus
3422+
}
3423+
33953424
// marshalOutputType turns the transfer output type into the RPC counterpart.
33963425
func marshalOutputType(outputType tappsbt.VOutputType) (taprpc.OutputType,
33973426
error) {

tapdb/assets_store.go

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2809,9 +2809,10 @@ func (a *AssetStore) ConfirmProofDelivery(ctx context.Context,
28092809
return nil
28102810
}
28112811

2812-
// ConfirmParcelDelivery marks a spend event on disk as confirmed. This updates
2813-
// the on-chain reference information on disk to point to this new spend.
2814-
func (a *AssetStore) ConfirmParcelDelivery(ctx context.Context,
2812+
// LogAnchorTxConfirm updates the send package state on disk to reflect the
2813+
// confirmation of the anchor transaction, ensuring the on-chain reference
2814+
// information is up to date.
2815+
func (a *AssetStore) LogAnchorTxConfirm(ctx context.Context,
28152816
conf *tapfreighter.AssetConfirmEvent) error {
28162817

28172818
var (
@@ -3128,9 +3129,13 @@ func (a *AssetStore) reAnchorPassiveAssets(ctx context.Context,
31283129
return nil
31293130
}
31303131

3131-
// PendingParcels returns the set of parcels that haven't yet been finalized.
3132-
// This can be used to query the set of unconfirmed
3133-
// transactions for re-broadcast.
3132+
// PendingParcels returns the set of parcels that have not yet been finalized.
3133+
// A parcel is considered finalized once the on-chain anchor transaction is
3134+
// included in a block, and all pending transfer output proofs have been
3135+
// delivered to their target peers.
3136+
//
3137+
// NOTE: This can be used to query the set of unconfirmed transactions for
3138+
// re-broadcast and for the set of undelivered proofs.
31343139
func (a *AssetStore) PendingParcels(
31353140
ctx context.Context) ([]*tapfreighter.OutboundParcel, error) {
31363141

@@ -3140,7 +3145,7 @@ func (a *AssetStore) PendingParcels(
31403145
// QueryParcels returns the set of confirmed or unconfirmed parcels.
31413146
func (a *AssetStore) QueryParcels(ctx context.Context,
31423147
anchorTxHash *chainhash.Hash,
3143-
unconfirmedTxOnly bool) ([]*tapfreighter.OutboundParcel, error) {
3148+
pendingTransfersOnly bool) ([]*tapfreighter.OutboundParcel, error) {
31443149

31453150
var (
31463151
outboundParcels []*tapfreighter.OutboundParcel
@@ -3157,10 +3162,8 @@ func (a *AssetStore) QueryParcels(ctx context.Context,
31573162
}
31583163

31593164
transferQuery := TransferQuery{
3160-
// If we want unconfirmed transfers only, we set the
3161-
// UnconfOnly field to true.
3162-
UnconfOnly: unconfirmedTxOnly,
3163-
AnchorTxHash: anchorTxHashBytes,
3165+
AnchorTxHash: anchorTxHashBytes,
3166+
PendingTransfersOnly: sqlBool(pendingTransfersOnly),
31643167
}
31653168

31663169
// Query for asset transfers.
@@ -3210,9 +3213,22 @@ func (a *AssetStore) QueryParcels(ctx context.Context,
32103213
"anchor tx: %w", err)
32113214
}
32123215

3216+
// Marshal anchor tx block hash from the database to a
3217+
// Hash type.
3218+
var anchorTxBlockHash fn.Option[chainhash.Hash]
3219+
if len(dbT.AnchorTxBlockHash) > 0 {
3220+
var blockHash chainhash.Hash
3221+
copy(blockHash[:], dbT.AnchorTxBlockHash)
3222+
3223+
anchorTxBlockHash = fn.Some[chainhash.Hash](
3224+
blockHash,
3225+
)
3226+
}
3227+
32133228
parcel := &tapfreighter.OutboundParcel{
32143229
AnchorTx: anchorTx,
32153230
AnchorTxHeightHint: uint32(dbT.HeightHint),
3231+
AnchorTxBlockHash: anchorTxBlockHash,
32163232
TransferTime: dbT.TransferTimeUnix.UTC(),
32173233
ChainFees: dbAnchorTx.ChainFees,
32183234
Inputs: inputs,

0 commit comments

Comments
 (0)