diff --git a/README.md b/README.md index 64fcd1d..51a6e1c 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@ + [genimportscript](#genimportscript) + [forceclose](#forceclose) + [rescueclosed](#rescueclosed) + + [rescuefunding](#rescuefunding) + [showrootkey](#showrootkey) + + [signrescuefunding](#signrescuefunding) + [summary](#summary) + [sweeptimelock](#sweeptimelock) + [vanitygen](#vanitygen) @@ -207,21 +209,23 @@ Help Options: -h, --help Show this help message Available commands: - chanbackup Create a channel.backup file from a channel database. - compactdb Open a source channel.db database file in safe/read-only mode and copy it to a fresh database, compacting it in the process. - derivekey Derive a key with a specific derivation path from the BIP32 HD root key. - dumpbackup Dump the content of a channel.backup file. - dumpchannels Dump all channel information from lnd's channel database. - filterbackup Filter an lnd channel.backup file and remove certain channels. - fixoldbackup Fixes an old channel.backup file that is affected by the lnd issue #3881 (unable to derive shachain root key). - 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. - showrootkey Extract and show the BIP32 HD root key from the 24 word lnd aezeed. - 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 - walletinfo Shows relevant information about an lnd wallet.db file and optionally extracts the BIP32 HD root key. + chanbackup Create a channel.backup file from a channel database. + compactdb Open a source channel.db database file in safe/read-only mode and copy it to a fresh database, compacting it in the process. + derivekey Derive a key with a specific derivation path from the BIP32 HD root key. + dumpbackup Dump the content of a channel.backup file. + dumpchannels Dump all channel information from lnd's channel database. + filterbackup Filter an lnd channel.backup file and remove certain channels. + fixoldbackup Fixes an old channel.backup file that is affected by the lnd issue #3881 (unable to derive shachain root key). + 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 prefix. + walletinfo Shows relevant information about an lnd wallet.db file and optionally extracts the BIP32 HD root key. ``` ## Commands @@ -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 (:) as it is recorded in the DB. + --confirmedchannelpoint= The channel outpoint that got confirmed on chain (:). 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 \ + --rootkey xprvxxxxxxxxxx +``` + +If successful, this will create a final on-chain transaction that can be +broadcast by any Bitcoin node. + ### summary ```text diff --git a/cmd/chantools/main.go b/cmd/chantools/main.go index 6d7400e..bc539d0 100644 --- a/cmd/chantools/main.go +++ b/cmd/chantools/main.go @@ -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 diff --git a/cmd/chantools/rescuefunding.go b/cmd/chantools/rescuefunding.go index 3686b82..effd3f6 100644 --- a/cmd/chantools/rescuefunding.go +++ b/cmd/chantools/rescuefunding.go @@ -3,25 +3,43 @@ 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'/'/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 (:)."` - FundingAmount int64 `long:"fundingamount" description:"The exact amount in satoshis that is locked in the funding output."` - 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."` + 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 (:) as it is recorded in the DB."` + ConfirmedOutPoint string `long:"confirmedchannelpoint" description:"The channel outpoint that got confirmed on chain (:). 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."` } func (c *rescueFundingCommand) Execute(_ []string) error { @@ -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) + signer := &lnd.Signer{ + ExtendedKey: extendedKey, + ChainParams: chainParams, + } + + // Check that we have a channel DB. + if c.ChannelDB == "" { + return fmt.Errorf("channel DB is required") + } + db, err := channeldb.Open( + path.Dir(c.ChannelDB), path.Base(c.ChannelDB), + channeldb.OptionSetSyncFreelist(true), + channeldb.OptionReadOnly(true), + ) if err != nil { - return fmt.Errorf("error parsing other node's xpub: %v", err) + return fmt.Errorf("error opening rescue DB: %v", err) } - // Decode target funding address. - hash, isScript, err := lnd.DecodeAddressHash(c.FundingAddr, chainParams) + // 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 decoding funding address: %v", err) + return fmt.Errorf("error parsing channel point: %v", err) } - if !isScript { - return fmt.Errorf("funding address must be a P2WSH address") + + // 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) + } } - return rescueFunding(extendedKey, otherPub, hash) + // 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(localNodeKey *hdkeychain.ExtendedKey, - otherNodekey *hdkeychain.ExtendedKey, scriptHash []byte) error { +func rescueFunding(db *channeldb.DB, signer *lnd.Signer, dbFundingPoint, + chainPoint *wire.OutPoint, sweepPKScript []byte, + feeRate btcutil.Amount) 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, - }) + // First of all make sure the channel can be found in the DB. + pendingChan, err := db.FetchChannel(*dbFundingPoint) if err != nil { - return fmt.Errorf("could not derive local multisig key: %v", - err) + 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, } - log.Infof("Looking for matching multisig keys, this will take a while") - localIndex, otherIndex, script, err := findMatchingIndices( - localMultisig, otherNodekey, scriptHash, + // 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 keys: %v", err) + return fmt.Errorf("could not derive funding script: %v", err) } - log.Infof("Found local key with index %d and other key with index %d "+ - "for witness script %x", localIndex, otherIndex, script) + // 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") + } - // 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 -} + // 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(), + }}, + } -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) - } + // 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}, } - return 0, 0, nil, fmt.Errorf("no matching pubkeys found") + 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) + + return nil } diff --git a/cmd/chantools/signrescuefunding.go b/cmd/chantools/signrescuefunding.go new file mode 100644 index 0000000..53f7c76 --- /dev/null +++ b/cmd/chantools/signrescuefunding.go @@ -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") +} diff --git a/cmd/chantools/sweeptimelock.go b/cmd/chantools/sweeptimelock.go index c487822..4a24367 100644 --- a/cmd/chantools/sweeptimelock.go +++ b/cmd/chantools/sweeptimelock.go @@ -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) { diff --git a/go.mod b/go.mod index 4184bc2..9500f4e 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 7da0a60..e43a213 100644 --- a/go.sum +++ b/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= diff --git a/lnd/channel.go b/lnd/channel.go index 68e3c10..c04c8bd 100644 --- a/lnd/channel.go +++ b/lnd/channel.go @@ -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 : 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 +} diff --git a/lnd/hdkeychain.go b/lnd/hdkeychain.go index bf823c8..4ef92b9 100644 --- a/lnd/hdkeychain.go +++ b/lnd/hdkeychain.go @@ -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 diff --git a/lnd/signer.go b/lnd/signer.go index 0f82ac6..239daad 100644 --- a/lnd/signer.go +++ b/lnd/signer.go @@ -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.