Skip to content

Commit 59acd27

Browse files
committed
itest: add vpsbt sighash coverage
1 parent 40aa396 commit 59acd27

File tree

2 files changed

+362
-0
lines changed

2 files changed

+362
-0
lines changed

itest/psbt_test.go

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/btcsuite/btcd/btcec/v2/schnorr"
1010
"github.com/btcsuite/btcd/btcutil/psbt"
11+
"github.com/btcsuite/btcd/txscript"
1112
"github.com/btcsuite/btcwallet/waddrmgr"
1213
tap "github.com/lightninglabs/taproot-assets"
1314
"github.com/lightninglabs/taproot-assets/address"
@@ -18,8 +19,11 @@ import (
1819
"github.com/lightninglabs/taproot-assets/taprpc"
1920
wrpc "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc"
2021
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
22+
"github.com/lightninglabs/taproot-assets/taprpc/tapdevrpc"
23+
"github.com/lightninglabs/taproot-assets/tapscript"
2124
"github.com/lightningnetwork/lnd/input"
2225
"github.com/lightningnetwork/lnd/keychain"
26+
"github.com/lightningnetwork/lnd/lntest/wait"
2327
"github.com/stretchr/testify/require"
2428
)
2529

@@ -1265,6 +1269,356 @@ func testMultiInputPsbtSingleAssetID(t *harnessTest) {
12651269
require.Len(t.t, secondaryNodeAssets.Assets, 0)
12661270
}
12671271

1272+
// testPsbtSighashNone tests that the SIGHASH_NONE flag of vPSBTs is properly
1273+
// accounted for in the generated signatures,
1274+
func testPsbtSighashNone(t *harnessTest) {
1275+
// First, we'll make a normal asset with enough units to allow us to
1276+
// send it around a few times.
1277+
rpcAssets := MintAssetsConfirmBatch(
1278+
t.t, t.lndHarness.Miner.Client, t.tapd,
1279+
[]*mintrpc.MintAssetRequest{issuableAssets[0]},
1280+
)
1281+
1282+
mintedAsset := rpcAssets[0]
1283+
genInfo := rpcAssets[0].AssetGenesis
1284+
1285+
ctxb := context.Background()
1286+
ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout)
1287+
defer cancel()
1288+
1289+
// Now that we have the asset created, we'll make a new node that'll
1290+
// serve as the node which'll receive the assets.
1291+
secondTapd := setupTapdHarness(
1292+
t.t, t, t.lndHarness.Bob, t.universeServer,
1293+
)
1294+
defer func() {
1295+
require.NoError(t.t, secondTapd.stop(!*noDelete))
1296+
}()
1297+
1298+
var (
1299+
alice = t.tapd
1300+
bob = secondTapd
1301+
numUnits = uint64(500)
1302+
)
1303+
1304+
// We need to derive two keys, one for the new script key and one for
1305+
// the internal key.
1306+
bobScriptKey, bobInternalKey := deriveKeys(t.t, bob)
1307+
1308+
// Now we create a script tree consisting of two simple scripts.
1309+
preImage := []byte("hash locks are cool")
1310+
leaf1 := test.ScriptHashLock(t.t, preImage)
1311+
leaf2 := test.ScriptSchnorrSig(t.t, bobScriptKey.RawKey.PubKey)
1312+
leaf1Hash := leaf1.TapHash()
1313+
leaf2Hash := leaf2.TapHash()
1314+
tapScript := input.TapscriptPartialReveal(
1315+
bobScriptKey.RawKey.PubKey, leaf2, leaf1Hash[:],
1316+
)
1317+
rootHash := tapScript.ControlBlock.RootHash(leaf2.Script)
1318+
1319+
sendToTapscriptAddr(
1320+
ctxt, t, alice, bob, numUnits, genInfo, mintedAsset,
1321+
bobScriptKey, bobInternalKey, tapScript, rootHash,
1322+
)
1323+
1324+
// Now try to send back those assets using the PSBT flow.
1325+
aliceAddr, err := alice.NewAddr(ctxb, &taprpc.NewAddrRequest{
1326+
AssetId: genInfo.AssetId,
1327+
Amt: numUnits / 5,
1328+
AssetVersion: mintedAsset.Version,
1329+
})
1330+
require.NoError(t.t, err)
1331+
AssertAddrCreated(t.t, alice, rpcAssets[0], aliceAddr)
1332+
1333+
fundResp := fundAddressSendPacket(t, bob, aliceAddr)
1334+
t.Logf("Funded PSBT: %v",
1335+
base64.StdEncoding.EncodeToString(fundResp.FundedPsbt))
1336+
1337+
fundedPacket, err := tappsbt.NewFromRawBytes(
1338+
bytes.NewReader(fundResp.FundedPsbt), false,
1339+
)
1340+
require.NoError(t.t, err)
1341+
1342+
// We can now ask the wallet to sign the script path, since we only need
1343+
// a signature.
1344+
controlBlockBytes, err := tapScript.ControlBlock.ToBytes()
1345+
require.NoError(t.t, err)
1346+
fundedPacket.Inputs[0].TaprootMerkleRoot = rootHash[:]
1347+
fundedPacket.Inputs[0].TaprootLeafScript = []*psbt.TaprootTapLeafScript{
1348+
{
1349+
ControlBlock: controlBlockBytes,
1350+
Script: leaf2.Script,
1351+
LeafVersion: leaf2.LeafVersion,
1352+
},
1353+
}
1354+
fundedPacket.Inputs[0].TaprootBip32Derivation[0].LeafHashes = [][]byte{
1355+
leaf2Hash[:],
1356+
}
1357+
1358+
// Before signing, we set the sighash of the first input to SIGHASH_NONE
1359+
// which allows us to alter the outputs of the PSBT after the signature
1360+
// has been generated.
1361+
fundedPacket.Inputs[0].SighashType = txscript.SigHashNone
1362+
1363+
var b bytes.Buffer
1364+
err = fundedPacket.Serialize(&b)
1365+
require.NoError(t.t, err)
1366+
1367+
signedResp, err := bob.SignVirtualPsbt(
1368+
ctxb, &wrpc.SignVirtualPsbtRequest{
1369+
FundedPsbt: b.Bytes(),
1370+
},
1371+
)
1372+
require.NoError(t.t, err)
1373+
require.Contains(t.t, signedResp.SignedInputs, uint32(0))
1374+
1375+
// Now we deserialize the signed packet again in order to edit it
1376+
// and then anchor it.
1377+
signedPacket, err := tappsbt.NewFromRawBytes(
1378+
bytes.NewReader(signedResp.SignedPsbt), false,
1379+
)
1380+
require.NoError(t.t, err)
1381+
1382+
// Edit the already signed PSBT and change the output amounts. This
1383+
// should be ok as we used SIGHASH_NONE for the input's signature.
1384+
signedPacket.Outputs[0].Amount -= 1
1385+
signedPacket.Outputs[1].Amount += 1
1386+
1387+
// Keep a backup of the PrevWitnesses as our input is already signed.
1388+
// When Bob re-creates the outputs for the vPSBT we will need to
1389+
// re-attach the witnesses to the new vPkt as the inputs are already
1390+
// signed.
1391+
witnessBackup := signedPacket.Outputs[0].Asset.PrevWitnesses
1392+
1393+
// Bob now creates the output assets.
1394+
err = tapscript.PrepareOutputAssets(context.Background(), signedPacket)
1395+
require.NoError(t.t, err)
1396+
1397+
// We attach the backed-up Previous Witnesses to the newly created
1398+
// outputs by Bob.
1399+
signedPacket.Outputs[0].Asset.PrevWitnesses = witnessBackup
1400+
signedPacket.Outputs[1].Asset.PrevWitnesses[0].SplitCommitment.RootAsset.
1401+
PrevWitnesses = witnessBackup
1402+
1403+
// Serialize the edited signed packet.
1404+
var buffer bytes.Buffer
1405+
err = signedPacket.Serialize(&buffer)
1406+
require.NoError(t.t, err)
1407+
signedBytes := buffer.Bytes()
1408+
1409+
// Now we'll attempt to complete the transfer.
1410+
sendResp, err := bob.AnchorVirtualPsbts(
1411+
ctxb, &wrpc.AnchorVirtualPsbtsRequest{
1412+
VirtualPsbts: [][]byte{signedBytes},
1413+
},
1414+
)
1415+
require.NoError(t.t, err)
1416+
1417+
ConfirmAndAssertOutboundTransfer(
1418+
t.t, t.lndHarness.Miner.Client, bob, sendResp,
1419+
genInfo.AssetId,
1420+
[]uint64{(4*numUnits)/5 - 1, (numUnits / 5) + 1}, 0, 1,
1421+
)
1422+
1423+
// This is an interactive/PSBT based transfer, so we do need to manually
1424+
// send the proof from the sender to the receiver because the proof
1425+
// courier address gets lost in the address->PSBT conversion.
1426+
_ = sendProof(t, bob, alice, sendResp, aliceAddr.ScriptKey, genInfo)
1427+
1428+
// If Bob was successful in his attempt to edit the outputs, Alice
1429+
// should see an asset with an amount of 399.
1430+
aliceAssets, err := alice.ListAssets(ctxb, &taprpc.ListAssetRequest{
1431+
WithWitness: true,
1432+
})
1433+
require.NoError(t.t, err)
1434+
1435+
found := false
1436+
for _, asset := range aliceAssets.Assets {
1437+
if asset.Amount == (numUnits/5)+1 {
1438+
found = true
1439+
}
1440+
}
1441+
1442+
require.True(t.t, found)
1443+
}
1444+
1445+
// testPsbtSighashNoneInvalid tests that the SIGHASH_NONE flag of vPSBTs is
1446+
// properly accounted for in the generated signatures. This case tests that the
1447+
// transfer is invalidated when the flag is not used.
1448+
func testPsbtSighashNoneInvalid(t *harnessTest) {
1449+
// First, we'll make a normal asset with enough units to allow us to
1450+
// send it around a few times.
1451+
rpcAssets := MintAssetsConfirmBatch(
1452+
t.t, t.lndHarness.Miner.Client, t.tapd,
1453+
[]*mintrpc.MintAssetRequest{issuableAssets[0]},
1454+
)
1455+
1456+
mintedAsset := rpcAssets[0]
1457+
genInfo := rpcAssets[0].AssetGenesis
1458+
1459+
ctxb := context.Background()
1460+
ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout)
1461+
defer cancel()
1462+
1463+
// Now that we have the asset created, we'll make a new node that'll
1464+
// serve as the node which'll receive the assets.
1465+
secondTapd := setupTapdHarness(
1466+
t.t, t, t.lndHarness.Bob, t.universeServer,
1467+
)
1468+
defer func() {
1469+
require.NoError(t.t, secondTapd.stop(!*noDelete))
1470+
}()
1471+
1472+
var (
1473+
alice = t.tapd
1474+
bob = secondTapd
1475+
numUnits = uint64(500)
1476+
)
1477+
1478+
// We need to derive two keys, one for the new script key and one for
1479+
// the internal key.
1480+
bobScriptKey, bobInternalKey := deriveKeys(t.t, bob)
1481+
1482+
// Now we create a script tree consisting of two simple scripts.
1483+
preImage := []byte("hash locks are cool")
1484+
leaf1 := test.ScriptHashLock(t.t, preImage)
1485+
leaf2 := test.ScriptSchnorrSig(t.t, bobScriptKey.RawKey.PubKey)
1486+
leaf1Hash := leaf1.TapHash()
1487+
leaf2Hash := leaf2.TapHash()
1488+
tapScript := input.TapscriptPartialReveal(
1489+
bobScriptKey.RawKey.PubKey, leaf2, leaf1Hash[:],
1490+
)
1491+
rootHash := tapScript.ControlBlock.RootHash(leaf2.Script)
1492+
1493+
sendToTapscriptAddr(
1494+
ctxt, t, alice, bob, numUnits, genInfo, mintedAsset,
1495+
bobScriptKey, bobInternalKey, tapScript, rootHash,
1496+
)
1497+
1498+
// Now try to send back those assets using the PSBT flow.
1499+
aliceAddr, err := alice.NewAddr(ctxb, &taprpc.NewAddrRequest{
1500+
AssetId: genInfo.AssetId,
1501+
Amt: numUnits / 5,
1502+
AssetVersion: mintedAsset.Version,
1503+
})
1504+
require.NoError(t.t, err)
1505+
AssertAddrCreated(t.t, alice, rpcAssets[0], aliceAddr)
1506+
1507+
fundResp := fundAddressSendPacket(t, bob, aliceAddr)
1508+
t.Logf("Funded PSBT: %v",
1509+
base64.StdEncoding.EncodeToString(fundResp.FundedPsbt))
1510+
1511+
fundedPacket, err := tappsbt.NewFromRawBytes(
1512+
bytes.NewReader(fundResp.FundedPsbt), false,
1513+
)
1514+
require.NoError(t.t, err)
1515+
1516+
// We can now ask the wallet to sign the script path, since we only need
1517+
// a signature.
1518+
controlBlockBytes, err := tapScript.ControlBlock.ToBytes()
1519+
require.NoError(t.t, err)
1520+
fundedPacket.Inputs[0].TaprootMerkleRoot = rootHash[:]
1521+
fundedPacket.Inputs[0].TaprootLeafScript = []*psbt.TaprootTapLeafScript{
1522+
{
1523+
ControlBlock: controlBlockBytes,
1524+
Script: leaf2.Script,
1525+
LeafVersion: leaf2.LeafVersion,
1526+
},
1527+
}
1528+
fundedPacket.Inputs[0].TaprootBip32Derivation[0].LeafHashes = [][]byte{
1529+
leaf2Hash[:],
1530+
}
1531+
1532+
// This is where we would normally set the sighash flag to SIGHASH_NONE,
1533+
// but instead we skip that step to verify that the VM will invalidate
1534+
// the transfer when any inputs or outputs are mutated.
1535+
1536+
var b bytes.Buffer
1537+
err = fundedPacket.Serialize(&b)
1538+
require.NoError(t.t, err)
1539+
1540+
signedResp, err := bob.SignVirtualPsbt(
1541+
ctxb, &wrpc.SignVirtualPsbtRequest{
1542+
FundedPsbt: b.Bytes(),
1543+
},
1544+
)
1545+
require.NoError(t.t, err)
1546+
require.Contains(t.t, signedResp.SignedInputs, uint32(0))
1547+
1548+
// Now we deserialize the signed packet again in order to edit it
1549+
// and then anchor it.
1550+
signedPacket, err := tappsbt.NewFromRawBytes(
1551+
bytes.NewReader(signedResp.SignedPsbt), false,
1552+
)
1553+
require.NoError(t.t, err)
1554+
1555+
// Edit the already signed PSBT and change the output amounts. This
1556+
// should be ok as we used SIGHASH_NONE for the input's signature.
1557+
signedPacket.Outputs[0].Amount -= 1
1558+
signedPacket.Outputs[1].Amount += 1
1559+
1560+
// Keep a backup of the PrevWitnesses as our input is already signed.
1561+
// When Bob re-creates the outputs for the vPSBT we will need to
1562+
// re-attach the witnesses to the new vPkt as the inputs are already
1563+
// signed.
1564+
witnessBackup := signedPacket.Outputs[0].Asset.PrevWitnesses
1565+
1566+
// Bob now creates the output assets.
1567+
err = tapscript.PrepareOutputAssets(context.Background(), signedPacket)
1568+
require.NoError(t.t, err)
1569+
1570+
// We attach the backed-up Previous Witnesses to the newly created
1571+
// outputs by Bob.
1572+
signedPacket.Outputs[0].Asset.PrevWitnesses = witnessBackup
1573+
signedPacket.Outputs[1].Asset.PrevWitnesses[0].SplitCommitment.RootAsset.
1574+
PrevWitnesses = witnessBackup
1575+
1576+
// Serialize the edited signed packet.
1577+
var buffer bytes.Buffer
1578+
err = signedPacket.Serialize(&buffer)
1579+
require.NoError(t.t, err)
1580+
signedBytes := buffer.Bytes()
1581+
1582+
// Now we'll attempt to complete the transfer.
1583+
sendResp, err := bob.AnchorVirtualPsbts(
1584+
ctxb, &wrpc.AnchorVirtualPsbtsRequest{
1585+
VirtualPsbts: [][]byte{signedBytes},
1586+
},
1587+
)
1588+
require.NoError(t.t, err)
1589+
1590+
ConfirmAndAssertOutboundTransfer(
1591+
t.t, t.lndHarness.Miner.Client, bob, sendResp,
1592+
genInfo.AssetId,
1593+
[]uint64{(4*numUnits)/5 - 1, (numUnits / 5) + 1}, 0, 1,
1594+
)
1595+
1596+
// Export Bob's faulty proof for this transfer.
1597+
var proofResp *taprpc.ProofFile
1598+
waitErr := wait.NoError(func() error {
1599+
resp, err := bob.ExportProof(ctxb, &taprpc.ExportProofRequest{
1600+
AssetId: genInfo.AssetId,
1601+
ScriptKey: aliceAddr.ScriptKey,
1602+
})
1603+
if err != nil {
1604+
return err
1605+
}
1606+
1607+
proofResp = resp
1608+
return nil
1609+
}, defaultWaitTimeout)
1610+
require.NoError(t.t, waitErr)
1611+
1612+
// Alice now attempts to import the proof. This will also trigger a
1613+
// transfer validation. This is where we expect the VM to invalidate
1614+
// the proof.
1615+
_, err = alice.ImportProof(ctxb, &tapdevrpc.ImportProofRequest{
1616+
ProofFile: proofResp.RawProofFile,
1617+
GenesisPoint: genInfo.GenesisPoint,
1618+
})
1619+
require.ErrorContains(t.t, err, "unable to verify proof")
1620+
}
1621+
12681622
func deriveKeys(t *testing.T, tapd *tapdHarness) (asset.ScriptKey,
12691623
keychain.KeyDescriptor) {
12701624

itest/test_list_on_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,14 @@ var testCases = []*testCase{
173173
name: "psbt multi send",
174174
test: testPsbtMultiSend,
175175
},
176+
{
177+
name: "psbt sighash none",
178+
test: testPsbtSighashNone,
179+
},
180+
{
181+
name: "psbt sighash none invalid",
182+
test: testPsbtSighashNoneInvalid,
183+
},
176184
{
177185
name: "multi input psbt single asset id",
178186
test: testMultiInputPsbtSingleAssetID,

0 commit comments

Comments
 (0)