Skip to content

Commit 3a4e864

Browse files
authored
Merge pull request #779 from lightninglabs/custom-channels-breach-test
itest: add breach itest for custom channels
2 parents fa32097 + b38ed00 commit 3a4e864

File tree

6 files changed

+334
-10
lines changed

6 files changed

+334
-10
lines changed

go.mod

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ require (
2020
github.com/lightninglabs/loop/swapserverrpc v1.0.8
2121
github.com/lightninglabs/pool v0.6.5-beta.0.20240604070222-e121aadb3289
2222
github.com/lightninglabs/pool/auctioneerrpc v1.1.2
23-
github.com/lightninglabs/taproot-assets v0.3.3-0.20240621202612-eae23c4f77e8
24-
github.com/lightningnetwork/lnd v0.18.0-beta.rc3.0.20240621222000-c6e4d621d2b0
23+
github.com/lightninglabs/taproot-assets v0.3.3-0.20240625161215-838206d62c99
24+
github.com/lightningnetwork/lnd v0.18.0-beta.rc3.0.20240625154246-4e968d9b520c
2525
github.com/lightningnetwork/lnd/cert v1.2.2
2626
github.com/lightningnetwork/lnd/fn v1.1.0
2727
github.com/lightningnetwork/lnd/kvdb v1.4.8
@@ -227,3 +227,5 @@ replace github.com/lightninglabs/lightning-terminal/autopilotserverrpc => ./auto
227227
replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display
228228

229229
go 1.22.3
230+
231+
toolchain go1.22.4

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1172,12 +1172,12 @@ github.com/lightninglabs/pool/auctioneerrpc v1.1.2 h1:Dbg+9Z9jXnhimR27EN37foc4aB
11721172
github.com/lightninglabs/pool/auctioneerrpc v1.1.2/go.mod h1:1wKDzN2zEP8srOi0B9iySlEsPdoPhw6oo3Vbm1v4Mhw=
11731173
github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display h1:pRdza2wleRN1L2fJXd6ZoQ9ZegVFTAb2bOQfruJPKcY=
11741174
github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
1175-
github.com/lightninglabs/taproot-assets v0.3.3-0.20240621202612-eae23c4f77e8 h1:bjdVqtwUirxbi8mkq86oo4Q9WqFnfadzwnxTXNhAtoQ=
1176-
github.com/lightninglabs/taproot-assets v0.3.3-0.20240621202612-eae23c4f77e8/go.mod h1:FAmcLipYgjm2jpqI2fiSOax8Oah/pekkGLfhJc6dwD0=
1175+
github.com/lightninglabs/taproot-assets v0.3.3-0.20240625161215-838206d62c99 h1:eMWI/Ob3Gv+7dHs7b9WA9Rpsvc32w4jPj+iKQ6+lD4s=
1176+
github.com/lightninglabs/taproot-assets v0.3.3-0.20240625161215-838206d62c99/go.mod h1:KhiaNUkgI3zIYNzfUoEClJjInXt5vScmnLVIvuUzWXY=
11771177
github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f h1:Pua7+5TcFEJXIIZ1I2YAUapmbcttmLj4TTi786bIi3s=
11781178
github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI=
1179-
github.com/lightningnetwork/lnd v0.18.0-beta.rc3.0.20240621222000-c6e4d621d2b0 h1:v72KQn3kiNmPICIYsZSRnBdnDZpOQ3CUNc20E7v3Z3M=
1180-
github.com/lightningnetwork/lnd v0.18.0-beta.rc3.0.20240621222000-c6e4d621d2b0/go.mod h1:L3IArArdRrWtuw+wNsUlibuGmf/08Odsm/zo3+bPXuM=
1179+
github.com/lightningnetwork/lnd v0.18.0-beta.rc3.0.20240625154246-4e968d9b520c h1:10hVKzgsnpuzOOgkYAhThUtDiq3fBBJBeZWdir+0ptk=
1180+
github.com/lightningnetwork/lnd v0.18.0-beta.rc3.0.20240625154246-4e968d9b520c/go.mod h1:L3IArArdRrWtuw+wNsUlibuGmf/08Odsm/zo3+bPXuM=
11811181
github.com/lightningnetwork/lnd/cert v1.2.2 h1:71YK6hogeJtxSxw2teq3eGeuy4rHGKcFf0d0Uy4qBjI=
11821182
github.com/lightningnetwork/lnd/cert v1.2.2/go.mod h1:jQmFn/Ez4zhDgq2hnYSw8r35bqGVxViXhX6Cd7HXM6U=
11831183
github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0=

itest/litd_custom_channels_test.go

Lines changed: 225 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -941,6 +941,227 @@ func testCustomChannelsForceClose(_ context.Context, net *NetworkHarness,
941941
t.Logf("Dave UTXOs: %v", toProtoJSON(t.t, daveUTXOs))
942942
}
943943

944+
func testCustomChannelsBreach(_ context.Context, net *NetworkHarness,
945+
t *harnessTest) {
946+
947+
lndArgs := slices.Clone(lndArgsTemplate)
948+
litdArgs := slices.Clone(litdArgsTemplate)
949+
950+
// Zane will act as our Universe server for the duration of the test.
951+
zane, err := net.NewNode(
952+
t.t, "Zane", lndArgs, false, true, litdArgs...,
953+
)
954+
require.NoError(t.t, err)
955+
956+
// For our litd args, make sure that they all seen Zane as the main
957+
// Universe server.
958+
litdArgs = append(litdArgs, fmt.Sprintf(
959+
"--taproot-assets.proofcourieraddr=%s://%s",
960+
proof.UniverseRpcCourierType, zane.Cfg.LitAddr(),
961+
))
962+
963+
// Charlie will be the breached party. We set --nolisten to ensure Dave
964+
// won't be able to connect to him and trigger the channel protection
965+
// logic automatically. We also can't have Charlie automatically
966+
// reconnect too early, otherwise DLP would be initiated instead of the
967+
// breach we want to provoke.
968+
charlieFlags := append(
969+
slices.Clone(lndArgs), "--nolisten", "--minbackoff=1h",
970+
)
971+
972+
// For this simple test, we'll just have Carol -> Dave as an assets
973+
// channel.
974+
charlie, err := net.NewNode(
975+
t.t, "Charlie", charlieFlags, false, true, litdArgs...,
976+
)
977+
require.NoError(t.t, err)
978+
979+
dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...)
980+
require.NoError(t.t, err)
981+
982+
// Next we'll connect all the nodes and also fund them with some coins.
983+
nodes := []*HarnessNode{charlie, dave}
984+
connectAllNodes(t.t, net, nodes)
985+
fundAllNodes(t.t, net, nodes)
986+
987+
charlieTap := newTapClient(t.t, charlie)
988+
daveTap := newTapClient(t.t, dave)
989+
990+
ctxb := context.Background()
991+
992+
// Now we'll make an asset for Charlie that we'll use in the test to
993+
// open a channel.
994+
mintedAssets := itest.MintAssetsConfirmBatch(
995+
t.t, t.lndHarness.Miner.Client, charlieTap,
996+
[]*mintrpc.MintAssetRequest{
997+
{
998+
Asset: itestAsset,
999+
},
1000+
},
1001+
)
1002+
cents := mintedAssets[0]
1003+
assetID := cents.AssetGenesis.AssetId
1004+
1005+
t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount)
1006+
syncUniverses(t.t, charlieTap, dave)
1007+
t.Logf("Universes synced between all nodes, distributing assets...")
1008+
1009+
// TODO(roasbeef): consolidate w/ the other test
1010+
1011+
// Next we can open an asset channel from Charlie -> Dave, then kick
1012+
// off the main scenario.
1013+
t.Logf("Opening asset channels...")
1014+
assetFundResp, err := charlieTap.FundChannel(
1015+
ctxb, &tchrpc.FundChannelRequest{
1016+
AssetAmount: fundingAmount,
1017+
AssetId: assetID,
1018+
PeerPubkey: dave.PubKey[:],
1019+
FeeRateSatPerVbyte: 5,
1020+
},
1021+
)
1022+
require.NoError(t.t, err)
1023+
t.Logf("Funded channel between Charlie and Dave: %v", assetFundResp)
1024+
1025+
// With the channel open, mine a block to confirm it.
1026+
mineBlocks(t, net, 6, 1)
1027+
1028+
time.Sleep(time.Second * 1)
1029+
1030+
// Next, we'll make keysend payments from Charlie to Dave. we'll use
1031+
// this to reach a state where both parties have funds in the channel.
1032+
const (
1033+
numPayments = 5
1034+
keySendAmount = 100
1035+
btcAmt = int64(5_000)
1036+
)
1037+
for i := 0; i < numPayments; i++ {
1038+
sendAssetKeySendPayment(
1039+
t.t, charlie, dave, keySendAmount, assetID,
1040+
fn.Some(btcAmt),
1041+
)
1042+
}
1043+
1044+
logBalance(t.t, nodes, assetID, "after keysend -- breach state")
1045+
1046+
// Now we'll create an on disk snapshot that we'll use to restore back
1047+
// to as our breached state.
1048+
require.NoError(t.t, net.StopAndBackupDB(dave))
1049+
connectAllNodes(t.t, net, nodes)
1050+
1051+
// We'll send one more keysend payment now to revoke the state we were
1052+
// just at above.
1053+
sendAssetKeySendPayment(
1054+
t.t, charlie, dave, keySendAmount, assetID, fn.Some(btcAmt),
1055+
)
1056+
logBalance(t.t, nodes, assetID, "after keysend -- final state")
1057+
1058+
// With the final state achieved, we'll now restore Dave (who will be
1059+
// force closing) to that old state, the breach state.
1060+
require.NoError(t.t, net.StopAndRestoreDB(dave))
1061+
1062+
// With Dave restored, we'll now execute the force close.
1063+
t.Logf("Force close by Dave to breach...")
1064+
daveChanPoint := &lnrpc.ChannelPoint{
1065+
OutputIndex: uint32(assetFundResp.OutputIndex),
1066+
FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
1067+
FundingTxidStr: assetFundResp.Txid,
1068+
},
1069+
}
1070+
_, breachTxid, err := net.CloseChannel(dave, daveChanPoint, true)
1071+
require.NoError(t.t, err)
1072+
1073+
t.Logf("Channel closed! Mining blocks, close_txid=%v", breachTxid)
1074+
1075+
// Next, we'll mine a block to confirm the breach transaction.
1076+
mineBlocks(t, net, 1, 1)
1077+
1078+
// We should be able to find the transfer of the breach for both
1079+
// parties.
1080+
charlieBreachTransfer := locateAssetTransfers(
1081+
t.t, charlieTap, *breachTxid,
1082+
)
1083+
daveBreachTransfer := locateAssetTransfers(
1084+
t.t, daveTap, *breachTxid,
1085+
)
1086+
1087+
t.Logf("Charlie breach transfer: %v",
1088+
toProtoJSON(t.t, charlieBreachTransfer))
1089+
t.Logf("Dave breach transfer: %v",
1090+
toProtoJSON(t.t, daveBreachTransfer))
1091+
1092+
// With the breach transaction mined, Charlie should now have a
1093+
// transaction in the mempool sweeping the *both* commitment outputs.
1094+
charlieJusticeTxid, err := waitForNTxsInMempool(
1095+
net.Miner.Client, 1, time.Second*5,
1096+
)
1097+
require.NoError(t.t, err)
1098+
1099+
t.Logf("Charlie justice txid: %v", charlieJusticeTxid)
1100+
1101+
// Next, we'll mine a block to confirm Charlie's justice transaction.
1102+
mineBlocks(t, net, 1, 1)
1103+
1104+
// Charlie should now have a transfer for his justice transaction.
1105+
charlieJusticeTransfer := locateAssetTransfers(
1106+
t.t, charlieTap, *charlieJusticeTxid[0],
1107+
)
1108+
1109+
t.Logf("Charlie justice transfer: %v",
1110+
toProtoJSON(t.t, charlieJusticeTransfer))
1111+
1112+
// Charlie's balance should now be the same as before the breach
1113+
// attempt: the amount he minted at the very start.
1114+
charlieBalance := itestAsset.Amount
1115+
assertAssetBalance(t.t, charlieTap, assetID, charlieBalance)
1116+
1117+
t.Logf("Charlie balance after breach: %d", charlieBalance)
1118+
1119+
// Charlie should now have 2 total UTXOs: the change from the funding
1120+
// output, and now the sweep output from the justice transaction.
1121+
charlieUTXOs := assertNumAssetUTXOs(t.t, charlieTap, 2)
1122+
1123+
t.Logf("Charlie UTXOs after breach: %v", toProtoJSON(t.t, charlieUTXOs))
1124+
}
1125+
1126+
func assertNumAssetUTXOs(t *testing.T, tapdClient *tapClient,
1127+
numUTXOs int) *taprpc.ListUtxosResponse {
1128+
1129+
ctxb := context.Background()
1130+
1131+
err := wait.NoError(func() error {
1132+
clientUTXOs, err := tapdClient.ListUtxos(
1133+
ctxb, &taprpc.ListUtxosRequest{},
1134+
)
1135+
if err != nil {
1136+
return err
1137+
}
1138+
1139+
if len(clientUTXOs.ManagedUtxos) != numUTXOs {
1140+
return fmt.Errorf("expected %v UTXO, got %d", numUTXOs,
1141+
len(clientUTXOs.ManagedUtxos))
1142+
}
1143+
1144+
return nil
1145+
}, defaultTimeout)
1146+
1147+
clientUTXOs, err2 := tapdClient.ListUtxos(
1148+
ctxb, &taprpc.ListUtxosRequest{},
1149+
)
1150+
require.NoError(t, err2)
1151+
1152+
if err != nil {
1153+
t.Logf("wrong amount of UTXOs, got %d, expected %d: %v",
1154+
len(clientUTXOs.ManagedUtxos), numUTXOs,
1155+
toProtoJSON(t, clientUTXOs))
1156+
1157+
t.Fatalf("failed to assert UTXOs: %v", err)
1158+
1159+
return nil
1160+
}
1161+
1162+
return clientUTXOs
1163+
}
1164+
9441165
func locateAssetTransfers(t *testing.T, tapdClient *tapClient,
9451166
txid chainhash.Hash) *taprpc.AssetTransfer {
9461167

@@ -953,12 +1174,12 @@ func locateAssetTransfers(t *testing.T, tapdClient *tapClient,
9531174
},
9541175
)
9551176
if err != nil {
956-
return fmt.Errorf("unable to list charlie "+
957-
"transfers: %w", err)
1177+
return fmt.Errorf("unable to list %v transfers: %w",
1178+
tapdClient.node.Name(), err)
9581179
}
9591180
if len(forceCloseTransfer.Transfers) != 1 {
960-
return fmt.Errorf("charlie is missing force close " +
961-
"transfer")
1181+
return fmt.Errorf("%v is missing force close "+
1182+
"transfer", tapdClient.node.Name())
9621183
}
9631184

9641185
transfer = forceCloseTransfer.Transfers[0]

itest/litd_node.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ type LitNodeConfig struct {
8989

9090
LitPort int
9191
LitRESTPort int
92+
93+
// backupDBDir is the path where a database backup is stored, if any.
94+
backupDBDir string
9295
}
9396

9497
func (cfg *LitNodeConfig) LitAddr() string {
@@ -2062,3 +2065,38 @@ func connectLitRPC(ctx context.Context, hostPort, tlsCertPath,
20622065

20632066
return grpc.DialContext(ctx, hostPort, opts...)
20642067
}
2068+
2069+
// copyAll copies all files and directories from srcDir to dstDir recursively.
2070+
// Note that this function does not support links.
2071+
func copyAll(dstDir, srcDir string) error {
2072+
entries, err := os.ReadDir(srcDir)
2073+
if err != nil {
2074+
return err
2075+
}
2076+
2077+
for _, entry := range entries {
2078+
srcPath := filepath.Join(srcDir, entry.Name())
2079+
dstPath := filepath.Join(dstDir, entry.Name())
2080+
2081+
info, err := os.Stat(srcPath)
2082+
if err != nil {
2083+
return err
2084+
}
2085+
2086+
if info.IsDir() {
2087+
err := os.Mkdir(dstPath, info.Mode())
2088+
if err != nil && !os.IsExist(err) {
2089+
return err
2090+
}
2091+
2092+
err = copyAll(dstPath, srcPath)
2093+
if err != nil {
2094+
return err
2095+
}
2096+
} else if err := CopyFile(dstPath, srcPath); err != nil {
2097+
return err
2098+
}
2099+
}
2100+
2101+
return nil
2102+
}

itest/litd_test_list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,8 @@ var allTestCases = []*testCase{
2828
name: "test custom channels force close",
2929
test: testCustomChannelsForceClose,
3030
},
31+
{
32+
name: "test custom channels breach",
33+
test: testCustomChannelsBreach,
34+
},
3135
}

0 commit comments

Comments
 (0)