Merge pull request #10119 from guggero/remote-signing-imported-tapscript
wallet: fix imported tapscript address behavior in watch-only mode
This commit is contained in:
6 files changed
+173
-3
No files matched your search
@@ -25,6 +25,10 @@
|
|||||||
messages simultaneously. The fix ensures only a single goroutine processes the
|
messages simultaneously. The fix ensures only a single goroutine processes the
|
||||||
backlog at any given time using an atomic flag.
|
backlog at any given time using an atomic flag.
|
||||||
|
|
||||||
|
- [Fixed a bug in `btcwallet` that caused issues with Tapscript addresses being
|
||||||
|
imported in a watch-only (e.g. remote-signing)
|
||||||
|
setup](https://github.com/lightningnetwork/lnd/pull/10119).
|
||||||
|
|
||||||
# New Features
|
# New Features
|
||||||
|
|
||||||
## Functional Enhancements
|
## Functional Enhancements
|
||||||
@@ -71,4 +75,5 @@
|
|||||||
# Contributors (Alphabetical Order)
|
# Contributors (Alphabetical Order)
|
||||||
|
|
||||||
* Olaoluwa Osuntokun
|
* Olaoluwa Osuntokun
|
||||||
|
* Oliver Gugger
|
||||||
* Yong Yu
|
* Yong Yu
|
||||||
@@ -11,7 +11,7 @@ require (
|
|||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
|
||||||
github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c
|
github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c
|
||||||
github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b
|
github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b
|
||||||
github.com/btcsuite/btcwallet v0.16.14
|
github.com/btcsuite/btcwallet v0.16.15-0.20250805011126-a3632ae48ab3
|
||||||
github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5
|
github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5
|
||||||
github.com/btcsuite/btcwallet/wallet/txrules v1.2.2
|
github.com/btcsuite/btcwallet/wallet/txrules v1.2.2
|
||||||
github.com/btcsuite/btcwallet/walletdb v1.5.1
|
github.com/btcsuite/btcwallet/walletdb v1.5.1
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c/go.mod h1:w7xnGOhw
|
|||||||
github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b h1:MQ+Q6sDy37V1wP1Yu79A5KqJutolqUGwA99UZWQDWZM=
|
github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b h1:MQ+Q6sDy37V1wP1Yu79A5KqJutolqUGwA99UZWQDWZM=
|
||||||
github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b/go.mod h1:XItGUfVOxotJL8kkuk2Hj3EVow5KCugXl3wWfQ6K0AE=
|
github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b/go.mod h1:XItGUfVOxotJL8kkuk2Hj3EVow5KCugXl3wWfQ6K0AE=
|
||||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||||
github.com/btcsuite/btcwallet v0.16.14 h1:CofysgmI1ednkLsXontAdBoXJkbiim7unXnFKhLLjnE=
|
github.com/btcsuite/btcwallet v0.16.15-0.20250805011126-a3632ae48ab3 h1:MAjNRpj3XhCOrhchq4wq0qI34TIBX/DCnT6OLWejx68=
|
||||||
github.com/btcsuite/btcwallet v0.16.14/go.mod h1:H6dfoZcWPonM2wbVsR2ZBY0PKNZKdQyLAmnX8vL9JFA=
|
github.com/btcsuite/btcwallet v0.16.15-0.20250805011126-a3632ae48ab3/go.mod h1:H6dfoZcWPonM2wbVsR2ZBY0PKNZKdQyLAmnX8vL9JFA=
|
||||||
github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5 h1:Rr0njWI3r341nhSPesKQ2JF+ugDSzdPoeckS75SeDZk=
|
github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5 h1:Rr0njWI3r341nhSPesKQ2JF+ugDSzdPoeckS75SeDZk=
|
||||||
github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5/go.mod h1:+tXJ3Ym0nlQc/iHSwW1qzjmPs3ev+UVWMbGgfV1OZqU=
|
github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5/go.mod h1:+tXJ3Ym0nlQc/iHSwW1qzjmPs3ev+UVWMbGgfV1OZqU=
|
||||||
github.com/btcsuite/btcwallet/wallet/txrules v1.2.2 h1:YEO+Lx1ZJJAtdRrjuhXjWrYsmAk26wLTlNzxt2q0lhk=
|
github.com/btcsuite/btcwallet/wallet/txrules v1.2.2 h1:YEO+Lx1ZJJAtdRrjuhXjWrYsmAk26wLTlNzxt2q0lhk=
|
||||||
|
|||||||
@@ -1060,6 +1060,14 @@ func testFundPsbt(ht *lntest.HarnessTest) {
|
|||||||
alice := ht.NewNodeWithCoins("Alice", nil)
|
alice := ht.NewNodeWithCoins("Alice", nil)
|
||||||
bob := ht.NewNodeWithCoins("Bob", nil)
|
bob := ht.NewNodeWithCoins("Bob", nil)
|
||||||
|
|
||||||
|
runFundPsbt(ht, alice, bob)
|
||||||
|
}
|
||||||
|
|
||||||
|
// runFundPsbt tests the FundPsbt RPC use case where we want to fund a PSBT
|
||||||
|
// that already has an input specified. This is a pay-join scenario where Bob
|
||||||
|
// wants to send Alice some coins, but he wants to do so in a way that doesn't
|
||||||
|
// reveal the full amount he is sending.
|
||||||
|
func runFundPsbt(ht *lntest.HarnessTest, alice, bob *node.HarnessNode) {
|
||||||
// We test a pay-join between Alice and Bob. Bob wants to send Alice
|
// We test a pay-join between Alice and Bob. Bob wants to send Alice
|
||||||
// 5 million Satoshis in a non-obvious way. So Bob selects a UTXO that's
|
// 5 million Satoshis in a non-obvious way. So Bob selects a UTXO that's
|
||||||
// bigger than 5 million Satoshis and expects the change minus the send
|
// bigger than 5 million Satoshis and expects the change minus the send
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ var remoteSignerTestCases = []*lntest.TestCase{
|
|||||||
Name: "account import",
|
Name: "account import",
|
||||||
TestFunc: testRemoteSignerAccountImport,
|
TestFunc: testRemoteSignerAccountImport,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "tapscript import",
|
||||||
|
TestFunc: testRemoteSignerTapscriptImport,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "channel open",
|
Name: "channel open",
|
||||||
TestFunc: testRemoteSignerChannelOpen,
|
TestFunc: testRemoteSignerChannelOpen,
|
||||||
@@ -228,6 +232,24 @@ func testRemoteSignerAccountImport(ht *lntest.HarnessTest) {
|
|||||||
tc.fn(ht, watchOnly, carol)
|
tc.fn(ht, watchOnly, carol)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testRemoteSignerTapscriptImport(ht *lntest.HarnessTest) {
|
||||||
|
tc := remoteSignerTestCase{
|
||||||
|
name: "tapscript import",
|
||||||
|
sendCoins: true,
|
||||||
|
fn: func(tt *lntest.HarnessTest, wo, carol *node.HarnessNode) {
|
||||||
|
testTaprootImportTapscriptFullTree(ht, wo)
|
||||||
|
testTaprootImportTapscriptPartialReveal(ht, wo)
|
||||||
|
testTaprootImportTapscriptRootHashOnly(ht, wo)
|
||||||
|
testTaprootImportTapscriptFullKey(ht, wo)
|
||||||
|
|
||||||
|
testTaprootImportTapscriptFullKeyFundPsbt(ht, wo)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, watchOnly, carol := prepareRemoteSignerTest(ht, tc)
|
||||||
|
tc.fn(ht, watchOnly, carol)
|
||||||
|
}
|
||||||
|
|
||||||
func testRemoteSignerChannelOpen(ht *lntest.HarnessTest) {
|
func testRemoteSignerChannelOpen(ht *lntest.HarnessTest) {
|
||||||
tc := remoteSignerTestCase{
|
tc := remoteSignerTestCase{
|
||||||
name: "basic channel open close",
|
name: "basic channel open close",
|
||||||
@@ -328,6 +350,11 @@ func testRemoteSignerPSBT(ht *lntest.HarnessTest) {
|
|||||||
// that aren't in the wallet. But we also want to make
|
// that aren't in the wallet. But we also want to make
|
||||||
// sure we can fund and then sign PSBTs from our wallet.
|
// sure we can fund and then sign PSBTs from our wallet.
|
||||||
runFundAndSignPsbt(ht, wo)
|
runFundAndSignPsbt(ht, wo)
|
||||||
|
|
||||||
|
// We also have a more specific funding test that does
|
||||||
|
// a pay-join payment with Carol.
|
||||||
|
ht.FundCoins(btcutil.SatoshiPerBitcoin, carol)
|
||||||
|
runFundPsbt(ht, wo, carol)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ func testTaprootImportScripts(ht *lntest.HarnessTest) {
|
|||||||
testTaprootImportTapscriptPartialReveal(ht, alice)
|
testTaprootImportTapscriptPartialReveal(ht, alice)
|
||||||
testTaprootImportTapscriptRootHashOnly(ht, alice)
|
testTaprootImportTapscriptRootHashOnly(ht, alice)
|
||||||
testTaprootImportTapscriptFullKey(ht, alice)
|
testTaprootImportTapscriptFullKey(ht, alice)
|
||||||
|
|
||||||
|
testTaprootImportTapscriptFullKeyFundPsbt(ht, alice)
|
||||||
}
|
}
|
||||||
|
|
||||||
// testTaprootSendCoinsKeySpendBip86 tests sending to and spending from
|
// testTaprootSendCoinsKeySpendBip86 tests sending to and spending from
|
||||||
@@ -1359,6 +1361,134 @@ func testTaprootImportTapscriptFullKey(ht *lntest.HarnessTest,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testTaprootImportTapscriptFullKeyFundPsbt tests importing p2tr script
|
||||||
|
// addresses for which we only know the full Taproot key. We also test that we
|
||||||
|
// can use such an imported script to fund a PSBT.
|
||||||
|
func testTaprootImportTapscriptFullKeyFundPsbt(ht *lntest.HarnessTest,
|
||||||
|
alice *node.HarnessNode) {
|
||||||
|
|
||||||
|
// For the next step, we need a public key. Let's use a special family
|
||||||
|
// for this.
|
||||||
|
_, internalKey, derivationPath := deriveInternalKey(ht, alice)
|
||||||
|
|
||||||
|
// Let's create a taproot script output now. This is a hash lock with a
|
||||||
|
// simple preimage of "foobar".
|
||||||
|
leaf1 := testScriptHashLock(ht.T, []byte("foobar"))
|
||||||
|
|
||||||
|
tapscript := input.TapscriptFullTree(internalKey, leaf1)
|
||||||
|
rootHash := leaf1.TapHash()
|
||||||
|
taprootKey, err := tapscript.TaprootKey()
|
||||||
|
require.NoError(ht, err)
|
||||||
|
|
||||||
|
// Import the scripts and make sure we get the same address back as we
|
||||||
|
// calculated ourselves.
|
||||||
|
req := &walletrpc.ImportTapscriptRequest{
|
||||||
|
InternalPublicKey: schnorr.SerializePubKey(taprootKey),
|
||||||
|
Script: &walletrpc.ImportTapscriptRequest_FullKeyOnly{
|
||||||
|
FullKeyOnly: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
importResp := alice.RPC.ImportTapscript(req)
|
||||||
|
|
||||||
|
calculatedAddr, err := btcutil.NewAddressTaproot(
|
||||||
|
schnorr.SerializePubKey(taprootKey), harnessNetParams,
|
||||||
|
)
|
||||||
|
require.NoError(ht, err)
|
||||||
|
require.Equal(ht, calculatedAddr.String(), importResp.P2TrAddress)
|
||||||
|
|
||||||
|
// Send some coins to the generated tapscript address.
|
||||||
|
p2trOutpoint, p2trPkScript := sendToTaprootOutput(ht, alice, taprootKey)
|
||||||
|
|
||||||
|
p2trOutputRPC := &lnrpc.OutPoint{
|
||||||
|
TxidBytes: p2trOutpoint.Hash[:],
|
||||||
|
OutputIndex: p2trOutpoint.Index,
|
||||||
|
}
|
||||||
|
ht.AssertUTXOInWallet(alice, p2trOutputRPC, "imported")
|
||||||
|
ht.AssertWalletAccountBalance(alice, "imported", testAmount, 0)
|
||||||
|
|
||||||
|
// We now fund a PSBT that spends the imported tapscript address.
|
||||||
|
utxo := &wire.TxOut{
|
||||||
|
Value: testAmount,
|
||||||
|
PkScript: p2trPkScript,
|
||||||
|
}
|
||||||
|
_, sweepPkScript := newAddrWithScript(
|
||||||
|
ht, alice, lnrpc.AddressType_WITNESS_PUBKEY_HASH,
|
||||||
|
)
|
||||||
|
|
||||||
|
output := &wire.TxOut{
|
||||||
|
PkScript: sweepPkScript,
|
||||||
|
Value: 1,
|
||||||
|
}
|
||||||
|
packet, err := psbt.New(
|
||||||
|
[]*wire.OutPoint{&p2trOutpoint}, []*wire.TxOut{output}, 2, 0,
|
||||||
|
[]uint32{0},
|
||||||
|
)
|
||||||
|
require.NoError(ht, err)
|
||||||
|
|
||||||
|
// We have everything we need to know to sign the PSBT.
|
||||||
|
in := &packet.Inputs[0]
|
||||||
|
in.Bip32Derivation = []*psbt.Bip32Derivation{{
|
||||||
|
PubKey: internalKey.SerializeCompressed(),
|
||||||
|
Bip32Path: derivationPath,
|
||||||
|
}}
|
||||||
|
in.TaprootBip32Derivation = []*psbt.TaprootBip32Derivation{{
|
||||||
|
XOnlyPubKey: schnorr.SerializePubKey(internalKey),
|
||||||
|
Bip32Path: derivationPath,
|
||||||
|
}}
|
||||||
|
in.SighashType = txscript.SigHashDefault
|
||||||
|
in.TaprootMerkleRoot = rootHash[:]
|
||||||
|
in.WitnessUtxo = utxo
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
require.NoError(ht, packet.Serialize(&buf))
|
||||||
|
|
||||||
|
change := &walletrpc.PsbtCoinSelect_ExistingOutputIndex{
|
||||||
|
ExistingOutputIndex: 0,
|
||||||
|
}
|
||||||
|
fundResp := alice.RPC.FundPsbt(&walletrpc.FundPsbtRequest{
|
||||||
|
Template: &walletrpc.FundPsbtRequest_CoinSelect{
|
||||||
|
CoinSelect: &walletrpc.PsbtCoinSelect{
|
||||||
|
Psbt: buf.Bytes(),
|
||||||
|
ChangeOutput: change,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Fees: &walletrpc.FundPsbtRequest_SatPerVbyte{
|
||||||
|
SatPerVbyte: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sign the manually funded PSBT now.
|
||||||
|
signResp := alice.RPC.SignPsbt(&walletrpc.SignPsbtRequest{
|
||||||
|
FundedPsbt: fundResp.FundedPsbt,
|
||||||
|
})
|
||||||
|
|
||||||
|
signedPacket, err := psbt.NewFromRawBytes(
|
||||||
|
bytes.NewReader(signResp.SignedPsbt), false,
|
||||||
|
)
|
||||||
|
require.NoError(ht, err)
|
||||||
|
|
||||||
|
// We should be able to finalize the PSBT and extract the sweep TX now.
|
||||||
|
err = psbt.MaybeFinalizeAll(signedPacket)
|
||||||
|
require.NoError(ht, err)
|
||||||
|
|
||||||
|
sweepTx, err := psbt.Extract(signedPacket)
|
||||||
|
require.NoError(ht, err)
|
||||||
|
|
||||||
|
buf.Reset()
|
||||||
|
err = sweepTx.Serialize(&buf)
|
||||||
|
require.NoError(ht, err)
|
||||||
|
|
||||||
|
// Publish the sweep transaction and then mine it as well.
|
||||||
|
alice.RPC.PublishTransaction(&walletrpc.Transaction{
|
||||||
|
TxHex: buf.Bytes(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mine one block which should contain the sweep transaction.
|
||||||
|
block := ht.MineBlocksAndAssertNumTxes(1, 1)[0]
|
||||||
|
sweepTxHash := sweepTx.TxHash()
|
||||||
|
ht.AssertTxInBlock(block, sweepTxHash)
|
||||||
|
}
|
||||||
|
|
||||||
// clearWalletImportedTapscriptBalance manually assembles and then attempts to
|
// clearWalletImportedTapscriptBalance manually assembles and then attempts to
|
||||||
// sign a TX to sweep funds from an imported tapscript address.
|
// sign a TX to sweep funds from an imported tapscript address.
|
||||||
func clearWalletImportedTapscriptBalance(ht *lntest.HarnessTest,
|
func clearWalletImportedTapscriptBalance(ht *lntest.HarnessTest,
|
||||||
|
|||||||
Reference in new issue
Block a user