mirror of
https://github.com/guggero/chantools
synced 2024-11-11 01:10:42 +00:00
Add rescuefunding and signrescuefunding commands
This commit is contained in:
parent
a95f475c6a
commit
4a633da99e
69
README.md
69
README.md
@ -16,7 +16,9 @@
|
||||
+ [genimportscript](#genimportscript)
|
||||
+ [forceclose](#forceclose)
|
||||
+ [rescueclosed](#rescueclosed)
|
||||
+ [rescuefunding](#rescuefunding)
|
||||
+ [showrootkey](#showrootkey)
|
||||
+ [signrescuefunding](#signrescuefunding)
|
||||
+ [summary](#summary)
|
||||
+ [sweeptimelock](#sweeptimelock)
|
||||
+ [vanitygen](#vanitygen)
|
||||
@ -217,10 +219,12 @@ Available commands:
|
||||
forceclose Force-close the last state that is in the channel.db provided.
|
||||
genimportscript Generate a script containing the on-chain keys of an lnd wallet that can be imported into other software like bitcoind.
|
||||
rescueclosed Try finding the private keys for funds that are in outputs of remotely force-closed channels.
|
||||
rescuefunding Rescue funds locked in a funding multisig output that never resulted in a proper channel. This is the command the initiator of the channel needs to run.
|
||||
showrootkey Extract and show the BIP32 HD root key from the 24 word lnd aezeed.
|
||||
signrescuefunding Rescue funds locked in a funding multisig output that never resulted in a proper channel. This is the command the remote node (the non-initiator) of the channel needs to run.
|
||||
summary Compile a summary about the current state of channels.
|
||||
sweeptimelock Sweep the force-closed state after the time lock has expired.
|
||||
vanitygen Generate a seed with a custom lnd node identity public key that starts with the given p
|
||||
vanitygen Generate a seed with a custom lnd node identity public key that starts with the given prefix.
|
||||
walletinfo Shows relevant information about an lnd wallet.db file and optionally extracts the BIP32 HD root key.
|
||||
```
|
||||
|
||||
@ -479,6 +483,43 @@ chantools --fromsummary results/summary-xxxx-yyyy.json \
|
||||
--rootkey xprvxxxxxxxxxx
|
||||
```
|
||||
|
||||
### rescuefunding
|
||||
|
||||
```text
|
||||
Usage:
|
||||
chantools [OPTIONS] rescuefunding [rescuefunding-OPTIONS]
|
||||
|
||||
[rescuefunding command options]
|
||||
--rootkey= BIP32 HD root key to use. Leave empty to prompt for lnd 24 word aezeed.
|
||||
--channeldb= The lnd channel.db file to rescue a channel from. Must contain the pending channel specified with --channelpoint.
|
||||
--channelpoint= The funding transaction outpoint of the channel to rescue (<txid>:<txindex>) as it is recorded in the DB.
|
||||
--confirmedchannelpoint= The channel outpoint that got confirmed on chain (<txid>:<txindex>). Normally this is the same as the --channelpoint so it will be set to that value if this is left empty.
|
||||
--sweepaddr= The address to sweep the rescued funds to.
|
||||
--satperbyte= The fee rate to use in satoshis/vByte.
|
||||
```
|
||||
|
||||
This is part 1 of a two phase process to rescue a channel funding output that
|
||||
was created on chain by accident but never resulted in a proper channel and no
|
||||
commitment transactions exist to spend the funds locked in the 2-of-2 multisig.
|
||||
|
||||
**You need the cooperation of the channel partner (remote node) for this to
|
||||
work**! They need to run the second command of this process:
|
||||
[`signrescuefunding`](#signrescuefunding)
|
||||
|
||||
Example command (run against the channel DB of the initiator node):
|
||||
|
||||
```bash
|
||||
chantools rescuefunding \
|
||||
--channeldb ~/.lnd/data/graph/mainnet/channel.db \
|
||||
--channelpoint xxxxxxx:xx \
|
||||
--sweepaddr bc1qxxxxxxxxx \
|
||||
--satperbyte 10 \
|
||||
--rootkey xprvxxxxxxxxxx
|
||||
```
|
||||
|
||||
If successful, this will create a PSBT that then has to be sent to the channel
|
||||
partner (remote node operator).
|
||||
|
||||
### showrootkey
|
||||
|
||||
This command converts the 24 word `lnd` aezeed phrase and password to the BIP32
|
||||
@ -491,6 +532,32 @@ Example command:
|
||||
chantools showrootkey
|
||||
```
|
||||
|
||||
### signrescuefunding
|
||||
|
||||
```text
|
||||
Usage:
|
||||
chantools [OPTIONS] signrescuefunding [signrescuefunding-OPTIONS]
|
||||
|
||||
[signrescuefunding command options]
|
||||
--rootkey= BIP32 HD root (m/) key to derive the key for our part of the signature from.
|
||||
--psbt= The Partially Signed Bitcoin Transaction that was provided by the initiator of the channel to rescue.
|
||||
```
|
||||
|
||||
This is part 2 of a two phase process to rescue a channel funding output that
|
||||
was created on chain by accident but never resulted in a proper channel and no
|
||||
commitment transactions exist to spend the funds locked in the 2-of-2 multisig.
|
||||
|
||||
Example command (run by the non-initiator of the channel):
|
||||
|
||||
```bash
|
||||
chantools signrescuefunding \
|
||||
--psbt <the_base64_encoded_psbt_from_step_1> \
|
||||
--rootkey xprvxxxxxxxxxx
|
||||
```
|
||||
|
||||
If successful, this will create a final on-chain transaction that can be
|
||||
broadcast by any Bitcoin node.
|
||||
|
||||
### summary
|
||||
|
||||
```text
|
||||
|
@ -23,7 +23,7 @@ import (
|
||||
|
||||
const (
|
||||
defaultAPIURL = "https://blockstream.info/api"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -137,12 +137,20 @@ func runCommandParser() error {
|
||||
"public key that starts with the given prefix.", "",
|
||||
&vanityGenCommand{},
|
||||
)
|
||||
// TODO: uncomment when command is fully implemented.
|
||||
//_, _ = parser.AddCommand(
|
||||
// "rescuefunding", "Rescue funds locked in a funding multisig "+
|
||||
// "output that never resulted in a proper channel.", "",
|
||||
// &rescueFundingCommand{},
|
||||
//)
|
||||
_, _ = parser.AddCommand(
|
||||
"rescuefunding", "Rescue funds locked in a funding multisig "+
|
||||
"output that never resulted in a proper channel. This "+
|
||||
"is the command the initiator of the channel needs to "+
|
||||
"run.", "",
|
||||
&rescueFundingCommand{},
|
||||
)
|
||||
_, _ = parser.AddCommand(
|
||||
"signrescuefunding", "Rescue funds locked in a funding "+
|
||||
"multisig output that never resulted in a proper "+
|
||||
"channel. This is the command the remote node (the non"+
|
||||
"-initiator) of the channel needs to run.", "",
|
||||
&signRescueFundingCommand{},
|
||||
)
|
||||
|
||||
_, err := parser.Parse()
|
||||
return err
|
||||
|
@ -3,23 +3,41 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/btcsuite/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcutil/psbt"
|
||||
"github.com/guggero/chantools/lnd"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"path"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxChannelLookup = 5000
|
||||
|
||||
// MultiSigWitnessSize 222 bytes
|
||||
// - NumberOfWitnessElements: 1 byte
|
||||
// - NilLength: 1 byte
|
||||
// - sigAliceLength: 1 byte
|
||||
// - sigAlice: 73 bytes
|
||||
// - sigBobLength: 1 byte
|
||||
// - sigBob: 73 bytes
|
||||
// - WitnessScriptLength: 1 byte
|
||||
// - WitnessScript (MultiSig)
|
||||
MultiSigWitnessSize = 1 + 1 + 1 + 73 + 1 + 73 + 1 + input.MultiSigSize
|
||||
)
|
||||
|
||||
var (
|
||||
PsbtKeyTypeOutputMissingSigPubkey = []byte{0xcc}
|
||||
)
|
||||
|
||||
type rescueFundingCommand struct {
|
||||
RootKey string `long:"rootkey" description:"BIP32 HD root (m/) key to derive the key for our node from."`
|
||||
OtherNodePub string `long:"othernodepub" description:"The extended public key (xpub) of the other node's multisig branch (m/1017'/<coin_type>'/0'/0)."`
|
||||
FundingAddr string `long:"fundingaddr" description:"The bech32 script address of the funding output where the coins to be spent are locked in."`
|
||||
FundingOutpoint string `long:"fundingoutpoint" description:"The funding transaction outpoint (<txid>:<txindex>)."`
|
||||
FundingAmount int64 `long:"fundingamount" description:"The exact amount in satoshis that is locked in the funding output."`
|
||||
RootKey string `long:"rootkey" description:"BIP32 HD root key to use. Leave empty to prompt for lnd 24 word aezeed."`
|
||||
ChannelDB string `long:"channeldb" description:"The lnd channel.db file to rescue a channel from. Must contain the pending channel specified with --channelpoint."`
|
||||
ChannelPoint string `long:"channelpoint" description:"The funding transaction outpoint of the channel to rescue (<txid>:<txindex>) as it is recorded in the DB."`
|
||||
ConfirmedOutPoint string `long:"confirmedchannelpoint" description:"The channel outpoint that got confirmed on chain (<txid>:<txindex>). Normally this is the same as the --channelpoint so it will be set to that value if this is left empty."`
|
||||
SweepAddr string `long:"sweepaddr" description:"The address to sweep the rescued funds to."`
|
||||
SatPerByte int64 `long:"satperbyte" description:"The fee rate to use in satoshis/vByte."`
|
||||
}
|
||||
@ -29,7 +47,7 @@ func (c *rescueFundingCommand) Execute(_ []string) error {
|
||||
|
||||
var (
|
||||
extendedKey *hdkeychain.ExtendedKey
|
||||
otherPub *hdkeychain.ExtendedKey
|
||||
chainOp *wire.OutPoint
|
||||
err error
|
||||
)
|
||||
|
||||
@ -39,107 +57,158 @@ func (c *rescueFundingCommand) Execute(_ []string) error {
|
||||
extendedKey, err = hdkeychain.NewKeyFromString(c.RootKey)
|
||||
|
||||
default:
|
||||
extendedKey, _, err = lnd.ReadAezeedFromTerminal(chainParams)
|
||||
extendedKey, _, err = lnd.ReadAezeedFromTerminal(
|
||||
chainParams,
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading root key: %v", err)
|
||||
}
|
||||
|
||||
// Read other node's xpub.
|
||||
otherPub, err = hdkeychain.NewKeyFromString(c.OtherNodePub)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing other node's xpub: %v", err)
|
||||
signer := &lnd.Signer{
|
||||
ExtendedKey: extendedKey,
|
||||
ChainParams: chainParams,
|
||||
}
|
||||
|
||||
// Decode target funding address.
|
||||
hash, isScript, err := lnd.DecodeAddressHash(c.FundingAddr, chainParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding funding address: %v", err)
|
||||
// Check that we have a channel DB.
|
||||
if c.ChannelDB == "" {
|
||||
return fmt.Errorf("channel DB is required")
|
||||
}
|
||||
if !isScript {
|
||||
return fmt.Errorf("funding address must be a P2WSH address")
|
||||
}
|
||||
|
||||
return rescueFunding(extendedKey, otherPub, hash)
|
||||
}
|
||||
|
||||
func rescueFunding(localNodeKey *hdkeychain.ExtendedKey,
|
||||
otherNodekey *hdkeychain.ExtendedKey, scriptHash []byte) error {
|
||||
|
||||
// First, we need to derive the correct branch from the local root key.
|
||||
localMultisig, err := lnd.DeriveChildren(localNodeKey, []uint32{
|
||||
lnd.HardenedKeyStart + uint32(keychain.BIP0043Purpose),
|
||||
lnd.HardenedKeyStart + chainParams.HDCoinType,
|
||||
lnd.HardenedKeyStart + uint32(keychain.KeyFamilyMultiSig),
|
||||
0,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not derive local multisig key: %v",
|
||||
err)
|
||||
}
|
||||
|
||||
log.Infof("Looking for matching multisig keys, this will take a while")
|
||||
localIndex, otherIndex, script, err := findMatchingIndices(
|
||||
localMultisig, otherNodekey, scriptHash,
|
||||
db, err := channeldb.Open(
|
||||
path.Dir(c.ChannelDB), path.Base(c.ChannelDB),
|
||||
channeldb.OptionSetSyncFreelist(true),
|
||||
channeldb.OptionReadOnly(true),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not derive keys: %v", err)
|
||||
return fmt.Errorf("error opening rescue DB: %v", err)
|
||||
}
|
||||
|
||||
log.Infof("Found local key with index %d and other key with index %d "+
|
||||
"for witness script %x", localIndex, otherIndex, script)
|
||||
// Parse channel point of channel to rescue as known to the DB.
|
||||
dbOp, err := lnd.ParseOutpoint(c.ChannelPoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing channel point: %v", err)
|
||||
}
|
||||
|
||||
// Parse channel point of channel to rescue as confirmed on chain (if
|
||||
// different).
|
||||
if len(c.ConfirmedOutPoint) == 0 {
|
||||
chainOp = dbOp
|
||||
} else {
|
||||
chainOp, err = lnd.ParseOutpoint(c.ChannelPoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing confirmed channel "+
|
||||
"point: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the sweep addr is a P2WKH address so we can do accurate
|
||||
// fee estimation.
|
||||
sweepScript, err := lnd.GetP2WPKHScript(c.SweepAddr, chainParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing sweep addr: %v", err)
|
||||
}
|
||||
|
||||
if c.SatPerByte < 0 {
|
||||
return fmt.Errorf("satperbyte must be greater than 0")
|
||||
}
|
||||
|
||||
return rescueFunding(
|
||||
db, signer, dbOp, chainOp, sweepScript,
|
||||
btcutil.Amount(c.SatPerByte),
|
||||
)
|
||||
}
|
||||
|
||||
func rescueFunding(db *channeldb.DB, signer *lnd.Signer, dbFundingPoint,
|
||||
chainPoint *wire.OutPoint, sweepPKScript []byte,
|
||||
feeRate btcutil.Amount) error {
|
||||
|
||||
// First of all make sure the channel can be found in the DB.
|
||||
pendingChan, err := db.FetchChannel(*dbFundingPoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading pending channel %s from DB: "+
|
||||
"%v", dbFundingPoint, err)
|
||||
}
|
||||
|
||||
// Prepare the wire part of the PSBT.
|
||||
txIn := &wire.TxIn{
|
||||
PreviousOutPoint: *chainPoint,
|
||||
Sequence: 0,
|
||||
}
|
||||
txOut := &wire.TxOut{
|
||||
PkScript: sweepPKScript,
|
||||
}
|
||||
|
||||
// Locate the output in the funding TX.
|
||||
utxo := pendingChan.FundingTxn.TxOut[dbFundingPoint.Index]
|
||||
|
||||
// We should also be able to create the funding script from the two
|
||||
// multisig keys.
|
||||
localKey := pendingChan.LocalChanCfg.MultiSigKey.PubKey
|
||||
remoteKey := pendingChan.RemoteChanCfg.MultiSigKey.PubKey
|
||||
witnessScript, fundingTxOut, err := input.GenFundingPkScript(
|
||||
localKey.SerializeCompressed(), remoteKey.SerializeCompressed(),
|
||||
utxo.Value,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not derive funding script: %v", err)
|
||||
}
|
||||
|
||||
// Some last sanity check that we're working with the correct data.
|
||||
if !bytes.Equal(fundingTxOut.PkScript, utxo.PkScript) {
|
||||
return fmt.Errorf("funding output script does not match UTXO")
|
||||
}
|
||||
|
||||
// Now the rest of the known data for the PSBT.
|
||||
pIn := psbt.PInput{
|
||||
WitnessUtxo: utxo,
|
||||
WitnessScript: witnessScript,
|
||||
Unknowns: []*psbt.Unknown{{
|
||||
// We add the public key the other party needs to sign
|
||||
// with as a proprietary field so we can easily read it
|
||||
// out with the signrescuefunding command.
|
||||
Key: PsbtKeyTypeOutputMissingSigPubkey,
|
||||
Value: remoteKey.SerializeCompressed(),
|
||||
}},
|
||||
}
|
||||
|
||||
// Estimate the transaction weight so we can do the fee estimation.
|
||||
var estimator input.TxWeightEstimator
|
||||
estimator.AddWitnessInput(MultiSigWitnessSize)
|
||||
estimator.AddP2WKHOutput()
|
||||
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
|
||||
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight()))
|
||||
txOut.Value = utxo.Value - int64(totalFee)
|
||||
|
||||
// Let's now create the PSBT as we have everything we need so far.
|
||||
wireTx := &wire.MsgTx{
|
||||
Version: 2,
|
||||
TxIn: []*wire.TxIn{txIn},
|
||||
TxOut: []*wire.TxOut{txOut},
|
||||
}
|
||||
packet, err := psbt.NewFromUnsignedTx(wireTx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating PSBT: %v", err)
|
||||
}
|
||||
packet.Inputs[0] = pIn
|
||||
|
||||
// Now we add our partial signature.
|
||||
err = signer.AddPartialSignature(
|
||||
packet, pendingChan.LocalChanCfg.MultiSigKey, utxo,
|
||||
witnessScript, 0,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding partial signature: %v", err)
|
||||
}
|
||||
|
||||
// We're done, we can now output the finished PSBT.
|
||||
base64, err := packet.B64Encode()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error encoding PSBT: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Partially signed transaction created. Send this to the "+
|
||||
"other peer \nand ask them to run the 'chantools "+
|
||||
"signrescuefunding' command: \n\n%s\n\n", base64)
|
||||
|
||||
// TODO(guggero):
|
||||
// * craft PSBT with input, sweep output and partial signature
|
||||
// * do fee estimation based on full amount
|
||||
// * create `signpsbt` command for the other node operator
|
||||
return nil
|
||||
}
|
||||
|
||||
func findMatchingIndices(localNodeKey *hdkeychain.ExtendedKey,
|
||||
otherNodekey *hdkeychain.ExtendedKey, scriptHash []byte) (uint32,
|
||||
uint32, []byte, error) {
|
||||
|
||||
// Loop through both the local and the remote indices of the branches up
|
||||
// to MaxChannelLookup.
|
||||
for local := uint32(0); local < MaxChannelLookup; local++ {
|
||||
for other := uint32(0); other < MaxChannelLookup; other++ {
|
||||
localKey, err := localNodeKey.Child(local)
|
||||
if err != nil {
|
||||
return 0, 0, nil, fmt.Errorf("error "+
|
||||
"deriving local key: %v", err)
|
||||
}
|
||||
localPub, err := localKey.ECPubKey()
|
||||
if err != nil {
|
||||
return 0, 0, nil, fmt.Errorf("error "+
|
||||
"deriving local pubkey: %v", err)
|
||||
}
|
||||
otherKey, err := otherNodekey.Child(other)
|
||||
if err != nil {
|
||||
return 0, 0, nil, fmt.Errorf("error "+
|
||||
"deriving other key: %v", err)
|
||||
}
|
||||
otherPub, err := otherKey.ECPubKey()
|
||||
if err != nil {
|
||||
return 0, 0, nil, fmt.Errorf("error "+
|
||||
"deriving other pubkey: %v", err)
|
||||
}
|
||||
script, out, err := input.GenFundingPkScript(
|
||||
localPub.SerializeCompressed(),
|
||||
otherPub.SerializeCompressed(), 123,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, 0, nil, fmt.Errorf("error "+
|
||||
"generating funding script: %v", err)
|
||||
}
|
||||
if bytes.Contains(out.PkScript, scriptHash) {
|
||||
return local, other, script, nil
|
||||
}
|
||||
}
|
||||
if local > 0 && local%100 == 0 {
|
||||
log.Infof("Checked %d of %d local keys", local,
|
||||
MaxChannelLookup)
|
||||
}
|
||||
}
|
||||
return 0, 0, nil, fmt.Errorf("no matching pubkeys found")
|
||||
}
|
||||
|
168
cmd/chantools/signrescuefunding.go
Normal file
168
cmd/chantools/signrescuefunding.go
Normal file
@ -0,0 +1,168 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcutil/psbt"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
|
||||
"github.com/btcsuite/btcutil/hdkeychain"
|
||||
"github.com/guggero/chantools/lnd"
|
||||
)
|
||||
|
||||
type signRescueFundingCommand struct {
|
||||
RootKey string `long:"rootkey" description:"BIP32 HD root (m/) key to derive the key for our part of the signature from."`
|
||||
Psbt string `long:"psbt" description:"The Partially Signed Bitcoin Transaction that was provided by the initiator of the channel to rescue."`
|
||||
}
|
||||
|
||||
func (c *signRescueFundingCommand) Execute(_ []string) error {
|
||||
setupChainParams(cfg)
|
||||
|
||||
var (
|
||||
extendedKey *hdkeychain.ExtendedKey
|
||||
err error
|
||||
)
|
||||
|
||||
// Check that root key is valid or fall back to console input.
|
||||
switch {
|
||||
case c.RootKey != "":
|
||||
extendedKey, err = hdkeychain.NewKeyFromString(c.RootKey)
|
||||
|
||||
default:
|
||||
extendedKey, _, err = lnd.ReadAezeedFromTerminal(chainParams)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading root key: %v", err)
|
||||
}
|
||||
|
||||
signer := &lnd.Signer{
|
||||
ExtendedKey: extendedKey,
|
||||
ChainParams: chainParams,
|
||||
}
|
||||
|
||||
// Decode the PSBT.
|
||||
packet, err := psbt.NewFromRawBytes(
|
||||
bytes.NewReader([]byte(c.Psbt)), true,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding PSBT: %v", err)
|
||||
}
|
||||
|
||||
return signRescueFunding(extendedKey, packet, signer)
|
||||
}
|
||||
|
||||
func signRescueFunding(rootKey *hdkeychain.ExtendedKey,
|
||||
packet *psbt.Packet, signer *lnd.Signer) error {
|
||||
|
||||
// First, we need to derive the correct branch from the local root key.
|
||||
localMultisig, err := lnd.DeriveChildren(rootKey, []uint32{
|
||||
lnd.HardenedKeyStart + uint32(keychain.BIP0043Purpose),
|
||||
lnd.HardenedKeyStart + chainParams.HDCoinType,
|
||||
lnd.HardenedKeyStart + uint32(keychain.KeyFamilyMultiSig),
|
||||
0,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not derive local multisig key: %v",
|
||||
err)
|
||||
}
|
||||
|
||||
// Now let's check that the packet has the expected proprietary key with
|
||||
// our pubkey that we need to sign with.
|
||||
if len(packet.Inputs) != 1 {
|
||||
return fmt.Errorf("invalid PSBT, expected 1 input, got %d",
|
||||
len(packet.Inputs))
|
||||
}
|
||||
if len(packet.Inputs[0].Unknowns) != 1 {
|
||||
return fmt.Errorf("invalid PSBT, expected 1 unknown in input, "+
|
||||
"got %d", len(packet.Inputs[0].Unknowns))
|
||||
}
|
||||
unknown := packet.Inputs[0].Unknowns[0]
|
||||
if !bytes.Equal(unknown.Key, PsbtKeyTypeOutputMissingSigPubkey) {
|
||||
return fmt.Errorf("invalid PSBT, unknown has invalid key %x, "+
|
||||
"expected %x", unknown.Key,
|
||||
PsbtKeyTypeOutputMissingSigPubkey)
|
||||
}
|
||||
targetKey, err := btcec.ParsePubKey(unknown.Value, btcec.S256())
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid PSBT, proprietary key has invalid "+
|
||||
"pubkey: %v", err)
|
||||
}
|
||||
|
||||
// Now we can look up the local key and check the PSBT further, then
|
||||
// add our signature.
|
||||
localKeyDesc, err := findLocalMultisigKey(localMultisig, targetKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not find local multisig key: %v", err)
|
||||
}
|
||||
if len(packet.Inputs[0].WitnessScript) == 0 {
|
||||
return fmt.Errorf("invalid PSBT, missing witness script")
|
||||
}
|
||||
witnessScript := packet.Inputs[0].WitnessScript
|
||||
if packet.Inputs[0].WitnessUtxo == nil {
|
||||
return fmt.Errorf("invalid PSBT, witness UTXO missing")
|
||||
}
|
||||
utxo := packet.Inputs[0].WitnessUtxo
|
||||
|
||||
err = signer.AddPartialSignature(
|
||||
packet, *localKeyDesc, utxo, witnessScript, 0,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding partial signature: %v", err)
|
||||
}
|
||||
|
||||
// We're almost done. Now we just need to make sure we can finalize and
|
||||
// extract the final TX.
|
||||
err = psbt.MaybeFinalizeAll(packet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finalizing PSBT: %v", err)
|
||||
}
|
||||
finalTx, err := psbt.Extract(packet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to extract final TX: %v", err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err = finalTx.Serialize(&buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to serialize final TX: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Success, we counter signed the PSBT and extracted the "+
|
||||
"final\ntransaction. Please publish this using any bitcoin "+
|
||||
"node:\n\n%x\n\n", buf.Bytes())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findLocalMultisigKey(multisigBranch *hdkeychain.ExtendedKey,
|
||||
targetPubkey *btcec.PublicKey) (*keychain.KeyDescriptor, error) {
|
||||
|
||||
// Loop through the local multisig keys to find the target key.
|
||||
for index := uint32(0); index < MaxChannelLookup; index++ {
|
||||
currentKey, err := multisigBranch.Child(index)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error deriving child key: %v",
|
||||
err)
|
||||
}
|
||||
|
||||
currentPubkey, err := currentKey.ECPubKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error deriving public key: %v",
|
||||
err)
|
||||
}
|
||||
|
||||
if !targetPubkey.IsEqual(currentPubkey) {
|
||||
continue
|
||||
}
|
||||
|
||||
return &keychain.KeyDescriptor{
|
||||
PubKey: currentPubkey,
|
||||
KeyLocator: keychain.KeyLocator{
|
||||
Family: keychain.KeyFamilyMultiSig,
|
||||
Index: index,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no matching pubkeys found")
|
||||
}
|
@ -182,7 +182,7 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string,
|
||||
}
|
||||
|
||||
// Add our sweep destination output.
|
||||
sweepScript, err := getP2WPKHScript(sweepAddr)
|
||||
sweepScript, err := lnd.GetP2WPKHScript(sweepAddr, chainParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -256,20 +256,6 @@ func pubKeyFromHex(pubKeyHex string) (*btcec.PublicKey, error) {
|
||||
)
|
||||
}
|
||||
|
||||
func getP2WPKHScript(addr string) ([]byte, error) {
|
||||
targetPubKeyHash, _, err := lnd.DecodeAddressHash(
|
||||
addr, chainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
builder := txscript.NewScriptBuilder()
|
||||
builder.AddOp(txscript.OP_0)
|
||||
builder.AddData(targetPubKeyHash)
|
||||
|
||||
return builder.Script()
|
||||
}
|
||||
|
||||
func bruteForceDelay(delayPubkey, revocationPubkey *btcec.PublicKey,
|
||||
targetScriptHex string, maxCsvTimeout int) (int32, []byte, []byte,
|
||||
error) {
|
||||
|
7
go.mod
7
go.mod
@ -6,6 +6,7 @@ require (
|
||||
github.com/btcsuite/btcd v0.20.1-beta
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
|
||||
github.com/btcsuite/btcutil v0.0.0-20191219182022-e17c9730c422
|
||||
github.com/btcsuite/btcutil/psbt v0.0.0-00010101000000-000000000000
|
||||
github.com/btcsuite/btcwallet v0.11.1-0.20200219004649-ae9416ad7623
|
||||
github.com/btcsuite/btcwallet/walletdb v1.2.0
|
||||
github.com/coreos/bbolt v1.3.3
|
||||
@ -15,7 +16,7 @@ require (
|
||||
github.com/lightningnetwork/lnd v0.8.0-beta-rc3.0.20191224233846-f289a39c1a00
|
||||
github.com/ltcsuite/ltcd v0.0.0-20191228044241-92166e412499 // indirect
|
||||
github.com/miekg/dns v1.1.26 // indirect
|
||||
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876
|
||||
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect
|
||||
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.3 // indirect
|
||||
@ -23,4 +24,8 @@ require (
|
||||
|
||||
replace github.com/lightningnetwork/lnd => github.com/guggero/lnd v0.9.0-beta-rc4.0.20200826102054-8c9171307182
|
||||
|
||||
replace github.com/btcsuite/btcutil => github.com/btcsuite/btcutil v1.0.2
|
||||
|
||||
replace github.com/btcsuite/btcutil/psbt => github.com/btcsuite/btcutil/psbt v1.0.2
|
||||
|
||||
go 1.13
|
||||
|
6
go.sum
6
go.sum
@ -34,6 +34,10 @@ github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6
|
||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||
github.com/btcsuite/btcutil v0.0.0-20191219182022-e17c9730c422 h1:EqnrgSSg0SFWRlEZLExgjtuUR/IPnuQ6qw6nwRda4Uk=
|
||||
github.com/btcsuite/btcutil v0.0.0-20191219182022-e17c9730c422/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||
github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts=
|
||||
github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts=
|
||||
github.com/btcsuite/btcutil/psbt v1.0.2 h1:gCVY3KxdoEVU7Q6TjusPO+GANIwVgr9yTLqM+a6CZr8=
|
||||
github.com/btcsuite/btcutil/psbt v1.0.2/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjqGocxDMNS1KuGQ=
|
||||
github.com/btcsuite/btcwallet v0.11.1-0.20200219004649-ae9416ad7623 h1:ZuJRjucNsTmlrbZncsqzD0z3EaXrOobCx2I4lc12R4g=
|
||||
github.com/btcsuite/btcwallet v0.11.1-0.20200219004649-ae9416ad7623/go.mod h1:1O1uRHMPXHdwA4/od8nqYqrgclVKp+wtfXUAqHmeRvE=
|
||||
github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0 h1:KGHMW5sd7yDdDMkCZ/JpP0KltolFsQcB973brBnfj4c=
|
||||
@ -204,6 +208,8 @@ golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 h1:sKJQZMuxjOAR/Uo2LBfU90onWEf1dF4C+0hPJCc9Mpc=
|
||||
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d h1:2+ZP7EfsZV7Vvmx3TIqSlSzATMkTAKqM14YGFPoSKjI=
|
||||
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
|
@ -1,10 +1,14 @@
|
||||
package lnd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LightningChannel struct {
|
||||
@ -75,3 +79,29 @@ func (lc *LightningChannel) SignedCommitTx() (*wire.MsgTx, error) {
|
||||
|
||||
return commitTx, nil
|
||||
}
|
||||
|
||||
// ParseOutpoint parses a transaction outpoint in the format <txid>:<idx> into
|
||||
// the wire format.
|
||||
func ParseOutpoint(s string) (*wire.OutPoint, error) {
|
||||
split := strings.Split(s, ":")
|
||||
if len(split) != 2 {
|
||||
return nil, fmt.Errorf("expecting channel point to be in " +
|
||||
"format of: txid:index")
|
||||
}
|
||||
|
||||
index, err := strconv.ParseInt(split[1], 10, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode output index: %v",
|
||||
err)
|
||||
}
|
||||
|
||||
txid, err := chainhash.NewHashFromStr(split[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse hex string: %v", err)
|
||||
}
|
||||
|
||||
return &wire.OutPoint{
|
||||
Hash: *txid,
|
||||
Index: uint32(index),
|
||||
}, nil
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package lnd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@ -169,6 +170,30 @@ func DecodeAddressHash(addr string, chainParams *chaincfg.Params) ([]byte, bool,
|
||||
return targetHash, isScriptHash, nil
|
||||
}
|
||||
|
||||
// GetP2WPKHScript creates a P2WKH output script from an address. If the address
|
||||
// is not a P2WKH address, an error is returned.
|
||||
func GetP2WPKHScript(addr string, chainParams *chaincfg.Params) ([]byte,
|
||||
error) {
|
||||
|
||||
targetPubKeyHash, isScriptHash, err := DecodeAddressHash(
|
||||
addr, chainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isScriptHash {
|
||||
return nil, fmt.Errorf("address %s is not a P2WKH address",
|
||||
addr)
|
||||
}
|
||||
|
||||
builder := txscript.NewScriptBuilder()
|
||||
builder.AddOp(txscript.OP_0)
|
||||
builder.AddData(targetPubKeyHash)
|
||||
|
||||
return builder.Script()
|
||||
}
|
||||
|
||||
type HDKeyRing struct {
|
||||
ExtendedKey *hdkeychain.ExtendedKey
|
||||
ChainParams *chaincfg.Params
|
||||
|
@ -2,12 +2,12 @@ package lnd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcutil/psbt"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
)
|
||||
@ -64,6 +64,45 @@ func (s *Signer) FetchPrivKey(descriptor *keychain.KeyDescriptor) (
|
||||
return key.ECPrivKey()
|
||||
}
|
||||
|
||||
func (s *Signer) AddPartialSignature(packet *psbt.Packet,
|
||||
keyDesc keychain.KeyDescriptor, utxo *wire.TxOut, witnessScript []byte,
|
||||
inputIndex int) error {
|
||||
|
||||
// Now we add our partial signature.
|
||||
signDesc := &input.SignDescriptor{
|
||||
KeyDesc: keyDesc,
|
||||
WitnessScript: witnessScript,
|
||||
Output: utxo,
|
||||
InputIndex: inputIndex,
|
||||
HashType: txscript.SigHashAll,
|
||||
SigHashes: txscript.NewTxSigHashes(packet.UnsignedTx),
|
||||
}
|
||||
ourSigRaw, err := s.SignOutputRaw(packet.UnsignedTx, signDesc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error signing with our key: %v", err)
|
||||
}
|
||||
ourSig := append(ourSigRaw, byte(txscript.SigHashAll))
|
||||
|
||||
// Great, we were able to create our sig, let's add it to the PSBT.
|
||||
updater, err := psbt.NewUpdater(packet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating PSBT updater: %v", err)
|
||||
}
|
||||
status, err := updater.Sign(
|
||||
0, ourSig, keyDesc.PubKey.SerializeCompressed(), nil,
|
||||
witnessScript,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding signature to PSBT: %v", err)
|
||||
}
|
||||
if status != 0 {
|
||||
return fmt.Errorf("unexpected status for signature update, "+
|
||||
"got %d wanted 0", status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// maybeTweakPrivKey examines the single tweak parameters on the passed sign
|
||||
// descriptor and may perform a mapping on the passed private key in order to
|
||||
// utilize the tweaks, if populated.
|
||||
|
Loading…
Reference in New Issue
Block a user