Skip to content

Commit d9b6f4c

Browse files
committed
itest: add v1 upgrade path breach test
1 parent e4bbe52 commit d9b6f4c

File tree

2 files changed

+321
-0
lines changed

2 files changed

+321
-0
lines changed

itest/litd_custom_channels_test.go

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/btcsuite/btcd/btcec/v2/schnorr"
1515
"github.com/btcsuite/btcd/btcutil"
1616
"github.com/btcsuite/btcd/chaincfg/chainhash"
17+
"github.com/btcsuite/btcd/wire"
1718
"github.com/lightninglabs/taproot-assets/asset"
1819
"github.com/lightninglabs/taproot-assets/itest"
1920
"github.com/lightninglabs/taproot-assets/proof"
@@ -2302,6 +2303,318 @@ func testCustomChannelsBreach(ctx context.Context, net *NetworkHarness,
23022303
t.Logf("Charlie balance after breach: %d", charlieBalance)
23032304
}
23042305

2306+
// testCustomChannelsV1Upgrade tests the upgrade path of a taproot assets
2307+
// channel. It upgrades one of the peers to a version that utilizes feature bits
2308+
// and new features over the channel, testing that backwards compatibility is
2309+
// maintained along the way. We also introduce a channel breach, right at the
2310+
// point before we switched over to the new features, to test that sweeping is
2311+
// done properly.
2312+
func testCustomChannelsV1Upgrade(ctx context.Context, net *NetworkHarness,
2313+
t *harnessTest) {
2314+
2315+
lndArgs := slices.Clone(lndArgsTemplate)
2316+
litdArgs := slices.Clone(litdArgsTemplate)
2317+
2318+
zane, err := net.NewNode(
2319+
t.t, "Zane", lndArgs, false, true, litdArgs...,
2320+
)
2321+
require.NoError(t.t, err)
2322+
2323+
litdArgs = append(litdArgs, fmt.Sprintf(
2324+
"--taproot-assets.proofcourieraddr=%s://%s",
2325+
proof.UniverseRpcCourierType, zane.Cfg.LitAddr(),
2326+
))
2327+
2328+
davePort := port.NextAvailablePort()
2329+
daveFlags := append(
2330+
slices.Clone(lndArgs), "--nolisten", "--minbackoff=1h",
2331+
)
2332+
2333+
// For this simple test, we'll just have Charlie -> Dave as an assets
2334+
// channel.
2335+
dave, err := net.NewNodeWithPort(
2336+
t.t, "Dave", daveFlags, false, true, davePort,
2337+
litdArgs...,
2338+
)
2339+
require.NoError(t.t, err)
2340+
2341+
charlie, err := net.NewNode(t.t, "Charlie", lndArgs, false, true, litdArgs...)
2342+
require.NoError(t.t, err)
2343+
2344+
// Next we'll connect all the nodes and also fund them with some coins.
2345+
nodes := []*HarnessNode{dave, charlie}
2346+
connectAllNodes(t.t, net, nodes)
2347+
fundAllNodes(t.t, net, nodes)
2348+
2349+
universeTap := newTapClient(t.t, zane)
2350+
charlieTap := newTapClient(t.t, charlie)
2351+
daveTap := newTapClient(t.t, dave)
2352+
2353+
// Now we'll make an asset for Charlie that we'll use in the test to
2354+
// open a channel.
2355+
mintedAssets := itest.MintAssetsConfirmBatch(
2356+
t.t, t.lndHarness.Miner.Client, charlieTap,
2357+
[]*mintrpc.MintAssetRequest{
2358+
{
2359+
Asset: itestAsset,
2360+
},
2361+
},
2362+
)
2363+
cents := mintedAssets[0]
2364+
assetID := cents.AssetGenesis.AssetId
2365+
2366+
t.Logf("Minted %d itest asset cents, syncing universes...",
2367+
cents.Amount)
2368+
2369+
syncUniverses(t.t, charlieTap, dave)
2370+
t.Logf("Universes synced between all nodes, distributing assets...")
2371+
2372+
// Next we can open an asset channel from Charlie -> Dave, then kick
2373+
// off the main scenario.
2374+
t.Logf("Opening asset channels...")
2375+
assetFundResp, err := charlieTap.FundChannel(
2376+
ctx, &tchrpc.FundChannelRequest{
2377+
AssetAmount: fundingAmount,
2378+
AssetId: assetID,
2379+
PeerPubkey: dave.PubKey[:],
2380+
FeeRateSatPerVbyte: 5,
2381+
},
2382+
)
2383+
require.NoError(t.t, err)
2384+
t.Logf("Funded channel between Charlie and Dave: %v", assetFundResp)
2385+
2386+
// With the channel open, mine 6 blocks to confirm it.
2387+
mineBlocks(t, net, 6, 1)
2388+
2389+
// A transfer for the funding transaction should be found in Charlie's
2390+
// DB.
2391+
fundingTxid, err := chainhash.NewHashFromStr(assetFundResp.Txid)
2392+
require.NoError(t.t, err)
2393+
assetFundingTransfer := locateAssetTransfers(
2394+
t.t, charlieTap, *fundingTxid,
2395+
)
2396+
2397+
t.Logf("Channel funding transfer: %v",
2398+
toProtoJSON(t.t, assetFundingTransfer))
2399+
2400+
// Charlie's balance should reflect that the funding asset is now
2401+
// excluded from balance reporting by tapd.
2402+
itest.AssertBalances(
2403+
t.t, charlieTap, itestAsset.Amount-fundingAmount,
2404+
itest.WithAssetID(assetID), itest.WithNumUtxos(1),
2405+
)
2406+
2407+
// Make sure that Charlie properly uploaded funding proof to the
2408+
// Universe server.
2409+
fundingScriptTree := tapscript.NewChannelFundingScriptTree()
2410+
fundingScriptKey := fundingScriptTree.TaprootKey
2411+
fundingScriptTreeBytes := fundingScriptKey.SerializeCompressed()
2412+
assertUniverseProofExists(
2413+
t.t, universeTap, assetID, nil, fundingScriptTreeBytes,
2414+
fmt.Sprintf(
2415+
"%v:%v", assetFundResp.Txid, assetFundResp.OutputIndex,
2416+
),
2417+
)
2418+
2419+
// Make sure the channel shows the correct asset information.
2420+
assertAssetChan(
2421+
t.t, charlie, dave, fundingAmount, []*taprpc.Asset{cents},
2422+
)
2423+
2424+
// Before we start sending out payments, let's make sure each node can
2425+
// see the other one in the graph and has all required features.
2426+
require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave))
2427+
require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie))
2428+
2429+
logBalance(t.t, nodes, assetID, "start")
2430+
2431+
// Let's dispatch 5 asset & 5 keysend payments from Charlie to Dave. At
2432+
// this point Charlie is running the old version of LiT.
2433+
for range 5 {
2434+
sendAssetKeySendPayment(
2435+
t.t, charlie, dave, 50, assetID, fn.None[int64](),
2436+
)
2437+
sendKeySendPayment(t.t, charlie, dave, 1_000)
2438+
}
2439+
2440+
logBalance(t.t, nodes, assetID, "before upgrade")
2441+
2442+
// Let's assert that Charlie & Dave actually run different versions of
2443+
// taproot-assets. We expect Dave to be running the latest version,
2444+
// while Charlie is running an older version (v0.15.0).
2445+
daveInfo, err := daveTap.GetInfo(ctx, &taprpc.GetInfoRequest{})
2446+
require.NoError(t.t, err)
2447+
2448+
charlieInfo, err := charlieTap.GetInfo(ctx, &taprpc.GetInfoRequest{})
2449+
require.NoError(t.t, err)
2450+
2451+
require.NotEqual(t.t, daveInfo.Version, charlieInfo.Version)
2452+
2453+
res, err := charlie.ChannelBalance(ctx, &lnrpc.ChannelBalanceRequest{})
2454+
require.NoError(t.t, err)
2455+
2456+
charlieSatsBefore := res.LocalBalance
2457+
2458+
// Now we'll restart Charlie and assert that he upgraded. We also back
2459+
// up the DB at this point, in order to induce a breach later right at
2460+
// the switching point before upgrading the channel. We will verify that
2461+
// the breach transaction will be swept by the right party.
2462+
require.NoError(t.t, net.StopAndBackupDB(charlie, WithUpgrade()))
2463+
connectAllNodes(t.t, net, nodes)
2464+
2465+
charlieInfo, err = charlieTap.GetInfo(ctx, &taprpc.GetInfoRequest{})
2466+
require.NoError(t.t, err)
2467+
2468+
// Dave and Charlie should both be running the same version (latest).
2469+
require.Equal(t.t, daveInfo.Version, charlieInfo.Version)
2470+
2471+
// Let's send another 5 asset and keysend payments from Charlie to Dave.
2472+
// Charlie is now on the latest version of LiT and the channel upgraded.
2473+
for range 5 {
2474+
sendAssetKeySendPayment(
2475+
t.t, charlie, dave, 50, assetID, fn.None[int64](),
2476+
)
2477+
}
2478+
2479+
res, err = charlie.ChannelBalance(ctx, &lnrpc.ChannelBalanceRequest{})
2480+
require.NoError(t.t, err)
2481+
2482+
charlieSatsAfter := res.LocalBalance
2483+
2484+
// Because of no-op HTLCs, the satoshi balance of Charlie should not
2485+
// have shifted while sending the asset payments.
2486+
require.Equal(t.t, charlieSatsBefore, charlieSatsAfter)
2487+
2488+
logBalance(t.t, nodes, assetID, "after upgrade")
2489+
2490+
// Now let's restart Charlie and restore the DB to the previous snapshot
2491+
// which corresponds to a previous (invalid) and unupgraded channel
2492+
// state.
2493+
require.NoError(t.t, net.StopAndRestoreDB(charlie))
2494+
2495+
// With Charlie restored, we'll now execute the force close.
2496+
t.Logf("Force close by Charlie to breach...")
2497+
charlieChanPoint := &lnrpc.ChannelPoint{
2498+
OutputIndex: uint32(assetFundResp.OutputIndex),
2499+
FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
2500+
FundingTxidStr: assetFundResp.Txid,
2501+
},
2502+
}
2503+
_, breachTxid, err := net.CloseChannel(charlie, charlieChanPoint, true)
2504+
require.NoError(t.t, err)
2505+
2506+
t.Logf("Channel closed! Mining blocks, close_txid=%v", breachTxid)
2507+
2508+
// Next, we'll mine a block to confirm the breach transaction.
2509+
mineBlocks(t, net, 1, 1)
2510+
2511+
// We should be able to find the transfer of the breach for both
2512+
// parties.
2513+
charlieBreachTransfer := locateAssetTransfers(
2514+
t.t, charlieTap, *breachTxid,
2515+
)
2516+
daveBreachTransfer := locateAssetTransfers(
2517+
t.t, daveTap, *breachTxid,
2518+
)
2519+
2520+
t.Logf("Charlie breach transfer: %v",
2521+
toProtoJSON(t.t, charlieBreachTransfer))
2522+
t.Logf("Dave breach transfer: %v",
2523+
toProtoJSON(t.t, daveBreachTransfer))
2524+
2525+
require.Len(t.t, charlieBreachTransfer.Outputs, 2)
2526+
assetOutput := charlieBreachTransfer.Outputs[0]
2527+
assertUniverseProofExists(
2528+
t.t, universeTap, assetID, nil, assetOutput.ScriptKey,
2529+
assetOutput.Anchor.Outpoint,
2530+
)
2531+
2532+
op, err := wire.NewOutPointFromString(assetOutput.Anchor.Outpoint)
2533+
require.NoError(t.t, err)
2534+
2535+
// We'll manually export the proof of the breach transfer, in order to
2536+
// verify that it indeed did not use STXO proofs.
2537+
proofResp, err := daveTap.ExportProof(ctx, &taprpc.ExportProofRequest{
2538+
AssetId: assetID,
2539+
ScriptKey: assetOutput.ScriptKey,
2540+
Outpoint: &taprpc.OutPoint{
2541+
Txid: op.Hash[:],
2542+
OutputIndex: op.Index,
2543+
},
2544+
})
2545+
require.NoError(t.t, err)
2546+
2547+
proofFile, err := proof.DecodeFile(proofResp.RawProofFile)
2548+
require.NoError(t.t, err)
2549+
require.Equal(t.t, proofFile.NumProofs(), 3)
2550+
latestProof, err := proofFile.LastProof()
2551+
require.NoError(t.t, err)
2552+
2553+
// This proof should not contain the STXO exclusion proofs, since the
2554+
// breach occured right before the channel upgraded.
2555+
stxoProofs := latestProof.ExclusionProofs[0].CommitmentProof.STXOProofs
2556+
require.Nil(t.t, stxoProofs)
2557+
2558+
// With the breach transaction mined, Dave should now have a transaction
2559+
// in the mempool sweeping *both* commitment outputs.
2560+
daveJusticeTxid, err := waitForNTxsInMempool(
2561+
net.Miner.Client, 1, time.Second*5,
2562+
)
2563+
require.NoError(t.t, err)
2564+
2565+
t.Logf("Dave justice txid: %v", daveJusticeTxid)
2566+
2567+
// Next, we'll mine a block to confirm Dave's justice transaction.
2568+
mineBlocks(t, net, 1, 1)
2569+
2570+
// Dave should now have a transfer for his justice transaction.
2571+
daveJusticeTransfer := locateAssetTransfers(
2572+
t.t, daveTap, *daveJusticeTxid[0],
2573+
)
2574+
2575+
t.Logf("Dave justice transfer: %v",
2576+
toProtoJSON(t.t, daveJusticeTransfer))
2577+
2578+
// Dave should claim all of the asset balance that was put into the
2579+
// channel.
2580+
daveBalance := uint64(fundingAmount)
2581+
2582+
itest.AssertBalances(
2583+
t.t, daveTap, daveBalance, itest.WithAssetID(assetID),
2584+
itest.WithNumUtxos(2),
2585+
)
2586+
2587+
t.Logf("Dave balance after breach: %d", daveBalance)
2588+
2589+
require.Len(t.t, daveJusticeTransfer.Outputs, 2)
2590+
assetOutput = daveJusticeTransfer.Outputs[0]
2591+
op, err = wire.NewOutPointFromString(assetOutput.Anchor.Outpoint)
2592+
require.NoError(t.t, err)
2593+
2594+
// We'll now also export the proof for the justice transaction. Here we
2595+
// expect to find STXO proofs, as the sweeping party is an upgraded node
2596+
// that supports it.
2597+
proofResp, err = daveTap.ExportProof(ctx, &taprpc.ExportProofRequest{
2598+
AssetId: assetID,
2599+
ScriptKey: assetOutput.ScriptKey,
2600+
Outpoint: &taprpc.OutPoint{
2601+
Txid: op.Hash[:],
2602+
OutputIndex: op.Index,
2603+
},
2604+
})
2605+
require.NoError(t.t, err)
2606+
2607+
proofFile, err = proof.DecodeFile(proofResp.RawProofFile)
2608+
require.NoError(t.t, err)
2609+
require.Equal(t.t, 4, proofFile.NumProofs())
2610+
latestProof, err = proofFile.LastProof()
2611+
require.NoError(t.t, err)
2612+
2613+
// This proof should contain the STXO exclusion proofs
2614+
stxoProofs = latestProof.InclusionProof.CommitmentProof.STXOProofs
2615+
require.NotNil(t.t, stxoProofs)
2616+
}
2617+
23052618
// testCustomChannelsLiquidityEdgeCasesCore is the core logic of the liquidity
23062619
// edge cases. This test goes through certain scenarios that expose edge cases
23072620
// and behaviors that proved to be buggy in the past and have been directly

itest/litd_test_list_on_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,12 @@ var allTestCases = []*testCase{
150150
"Alice": "v0.14.1-alpha",
151151
},
152152
},
153+
{
154+
name: "custom channels v1 upgrade",
155+
test: testCustomChannelsV1Upgrade,
156+
noAliceBob: true,
157+
backwardCompat: map[string]string{
158+
"Charlie": "v0.15.0-alpha",
159+
},
160+
},
153161
}

0 commit comments

Comments
 (0)