Skip to content

Commit 4cd51dc

Browse files
committed
contractcourt: track historic justice tx variants across rebuild cycles
The breach arbiter rebuilds justice tx variants after each spend detection (e.g. when an HTLC transitions to second-level). The tx that ultimately confirms may have been created in an earlier rebuild cycle and is no longer present in the current justiceTxVariants struct. Add a historicJusticeTxs map that records every justice tx variant ever created (keyed by txid) via recordJusticeTxVariants(). The notifyConfirmedJusticeTx function now falls back to this map when the confirmed spend doesn't match any current variant, ensuring the aux sweeper receives NotifyBroadcast for asset proof generation. Also improve the split-broadcast path: rebuild justice tx variants from the updated breach info before splitting, and re-attempt the spendAll variant first (which may now succeed after second-level spends have been incorporated). Add logging to createJusticeTx for input counts and variant creation.
1 parent ffaed67 commit 4cd51dc

2 files changed

Lines changed: 104 additions & 6 deletions

File tree

contractcourt/breach_arbitrator.go

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -820,7 +820,9 @@ func updateBreachInfo(breachInfo *retributionInfo, spends []spend,
820820
// haven't already notified about it, we call NotifyBroadcast on the aux sweeper
821821
// to generate asset-level proofs.
822822
func (b *BreachArbitrator) notifyConfirmedJusticeTx(spends []spend,
823-
justiceTxs *justiceTxVariants, notifiedTxs map[chainhash.Hash]bool) {
823+
justiceTxs *justiceTxVariants,
824+
historicJusticeTxs map[chainhash.Hash]*justiceTxCtx,
825+
notifiedTxs map[chainhash.Hash]bool) {
824826

825827
// Check each spend to see if it's from one of our justice txs.
826828
for _, s := range spends {
@@ -864,6 +866,14 @@ func (b *BreachArbitrator) notifyConfirmedJusticeTx(spends []spend,
864866
}
865867
}
866868

869+
// Check the historic map of all justice tx variants ever
870+
// created. This handles the case where the confirmed tx
871+
// was from a previous rebuild cycle and the current
872+
// justiceTxs has been replaced with newer variants.
873+
if justiceCtx == nil {
874+
justiceCtx = historicJusticeTxs[spendingTxHash]
875+
}
876+
867877
// If this is one of our justice txs, notify the aux sweeper.
868878
if justiceCtx != nil {
869879
bumpReq := sweep.BumpRequest{
@@ -896,6 +906,28 @@ func (b *BreachArbitrator) notifyConfirmedJusticeTx(spends []spend,
896906
}
897907
}
898908

909+
// recordJusticeTxVariants records all non-nil justice tx variants into the
910+
// historic map keyed by their txid. This allows notifyConfirmedJusticeTx to
911+
// match confirmed spends against justice txs from previous rebuild cycles.
912+
func recordJusticeTxVariants(variants *justiceTxVariants,
913+
history map[chainhash.Hash]*justiceTxCtx) {
914+
915+
record := func(jtx *justiceTxCtx) {
916+
if jtx == nil {
917+
return
918+
}
919+
hash := jtx.justiceTx.TxHash()
920+
history[hash] = jtx
921+
}
922+
923+
record(variants.spendAll)
924+
record(variants.spendCommitOuts)
925+
record(variants.spendHTLCs)
926+
for _, tx := range variants.spendSecondLevelHTLCs {
927+
record(tx)
928+
}
929+
}
930+
899931
// exactRetribution is a goroutine which is executed once a contract breach has
900932
// been detected by a breachObserver. This function is responsible for
901933
// punishing a counterparty for violating the channel contract by sweeping ALL
@@ -951,6 +983,13 @@ func (b *BreachArbitrator) exactRetribution(
951983
// handle duplicate calls idempotently.
952984
notifiedJusticeTxs := make(map[chainhash.Hash]bool)
953985

986+
// Track all historically created justice tx contexts by their txid.
987+
// This is needed because justiceTxs is rebuilt after each spend
988+
// detection, and the tx that actually confirmed may have been from
989+
// an earlier rebuild cycle. Without this history, we can't match
990+
// the confirmed tx to call NotifyBroadcast on the aux sweeper.
991+
historicJusticeTxs := make(map[chainhash.Hash]*justiceTxCtx)
992+
954993
// Compute both the total value of funds being swept and the
955994
// amount of funds that were revoked from the counter party.
956995
var totalFunds, revokedFunds btcutil.Amount
@@ -964,6 +1003,7 @@ justiceTxBroadcast:
9641003
brarLog.Errorf("Unable to create justice tx: %v", err)
9651004
return
9661005
}
1006+
recordJusticeTxVariants(justiceTxs, historicJusticeTxs)
9671007
finalTx := justiceTxs.spendAll
9681008

9691009
brarLog.Debugf("Broadcasting justice tx: %v", lnutils.SpewLogClosure(
@@ -1017,7 +1057,9 @@ Loop:
10171057
// justice transaction, and if so, notify the aux
10181058
// sweeper.
10191059
b.notifyConfirmedJusticeTx(
1020-
spends, justiceTxs, notifiedJusticeTxs,
1060+
spends, justiceTxs,
1061+
historicJusticeTxs,
1062+
notifiedJusticeTxs,
10211063
)
10221064

10231065
// Update the breach info with the new spends.
@@ -1085,6 +1127,44 @@ Loop:
10851127
"height %v), splitting justice tx.",
10861128
epoch.Height, breachInfo.breachHeight)
10871129

1130+
// Rebuild justice tx variants from the current
1131+
// breach info, which may have been updated by
1132+
// spend detection (e.g. second-level HTLC spends
1133+
// replacing commit-level outputs).
1134+
justiceTxs, err = b.createJusticeTx(
1135+
breachInfo.breachedOutputs,
1136+
)
1137+
if err != nil {
1138+
brarLog.Errorf("Unable to recreate "+
1139+
"justice tx for split: %v", err)
1140+
continue Loop
1141+
}
1142+
recordJusticeTxVariants(
1143+
justiceTxs, historicJusticeTxs,
1144+
)
1145+
1146+
// Re-attempt the spendAll variant first, in
1147+
// case the breach info was updated since the
1148+
// initial broadcast. This avoids splitting into
1149+
// small txs that can't pay fees when a combined
1150+
// tx would work.
1151+
if justiceTxs.spendAll != nil {
1152+
label := labels.MakeLabel(
1153+
labels.LabelTypeJusticeTransaction,
1154+
nil,
1155+
)
1156+
err = b.cfg.PublishTransaction(
1157+
justiceTxs.spendAll.justiceTx,
1158+
label,
1159+
)
1160+
if err != nil {
1161+
brarLog.Warnf("Unable to "+
1162+
"broadcast updated "+
1163+
"spendAll justice "+
1164+
"tx: %v", err)
1165+
}
1166+
}
1167+
10881168
// Otherwise we'll attempt to publish the two separate
10891169
// justice transactions that sweeps the commitment
10901170
// outputs and the HTLC outputs separately. This is to
@@ -1675,6 +1755,10 @@ func (b *BreachArbitrator) createJusticeTx(
16751755
}
16761756
}
16771757

1758+
brarLog.Infof("createJusticeTx: %d total inputs (%d commit, "+
1759+
"%d htlc, %d second-level)", len(allInputs),
1760+
len(commitInputs), len(htlcInputs), len(secondLevelInputs))
1761+
16781762
var (
16791763
txs = &justiceTxVariants{}
16801764
err error
@@ -1685,31 +1769,42 @@ func (b *BreachArbitrator) createJusticeTx(
16851769
if err != nil {
16861770
return nil, err
16871771
}
1772+
brarLog.Infof("createJusticeTx: spendAll created successfully "+
1773+
"(%d inputs, txid=%v)", len(allInputs),
1774+
txs.spendAll.justiceTx.TxHash())
16881775

16891776
txs.spendCommitOuts, err = b.createSweepTx(commitInputs...)
16901777
if err != nil {
16911778
brarLog.Errorf("could not create sweep tx for commitment "+
16921779
"outputs: %v", err)
1780+
} else if txs.spendCommitOuts != nil {
1781+
brarLog.Infof("createJusticeTx: spendCommitOuts created "+
1782+
"successfully (%d inputs)", len(commitInputs))
16931783
}
16941784

16951785
txs.spendHTLCs, err = b.createSweepTx(htlcInputs...)
16961786
if err != nil {
16971787
brarLog.Errorf("could not create sweep tx for HTLC outputs: %v",
16981788
err)
1789+
} else if txs.spendHTLCs != nil {
1790+
brarLog.Infof("createJusticeTx: spendHTLCs created "+
1791+
"successfully (%d inputs)", len(htlcInputs))
16991792
}
17001793

17011794
// TODO(roasbeef): only register one of them?
17021795

17031796
secondLevelSweeps := make([]*justiceTxCtx, 0, len(secondLevelInputs))
1704-
for _, input := range secondLevelInputs {
1797+
for i, input := range secondLevelInputs {
17051798
sweepTx, err := b.createSweepTx(input)
17061799
if err != nil {
17071800
brarLog.Errorf("could not create sweep tx for "+
1708-
"second-level HTLC output: %v", err)
1801+
"second-level HTLC output %d: %v", i, err)
17091802

17101803
continue
17111804
}
17121805

1806+
brarLog.Infof("createJusticeTx: individual second-level "+
1807+
"sweep %d created successfully", i)
17131808
secondLevelSweeps = append(secondLevelSweeps, sweepTx)
17141809
}
17151810
txs.spendSecondLevelHTLCs = secondLevelSweeps

contractcourt/breach_arbitrator_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2676,8 +2676,10 @@ func TestNotifyConfirmedJusticeTx(t *testing.T) {
26762676
}
26772677

26782678
// Call the function under test.
2679+
historicTxs := make(map[chainhash.Hash]*justiceTxCtx)
26792680
brar.notifyConfirmedJusticeTx(
2680-
tc.spends, tc.justiceTxs, tc.notifiedTxs,
2681+
tc.spends, tc.justiceTxs,
2682+
historicTxs, tc.notifiedTxs,
26812683
)
26822684

26832685
// Verify the number of NotifyBroadcast calls.
@@ -2746,8 +2748,9 @@ func TestNotifyConfirmedJusticeTxNoAuxSweeper(t *testing.T) {
27462748

27472749
// Should not panic and should not mark as notified since there's no
27482750
// aux sweeper to notify.
2751+
historicTxs := make(map[chainhash.Hash]*justiceTxCtx)
27492752
brar.notifyConfirmedJusticeTx(
2750-
spends, justiceTxs, notifiedTxs,
2753+
spends, justiceTxs, historicTxs, notifiedTxs,
27512754
)
27522755

27532756
// The tx should still be marked as notified even without an aux

0 commit comments

Comments
 (0)