diff --git a/lntest/mock/walletcontroller.go b/lntest/mock/walletcontroller.go index 72da14b14..f524695a5 100644 --- a/lntest/mock/walletcontroller.go +++ b/lntest/mock/walletcontroller.go @@ -189,6 +189,11 @@ func (w *WalletController) FundPsbt(*psbt.Packet, int32, chainfee.SatPerKWeight, return 0, nil } +// SignPsbt currently does nothing. +func (w *WalletController) SignPsbt(*psbt.Packet) error { + return nil +} + // FinalizePsbt currently does nothing. func (w *WalletController) FinalizePsbt(_ *psbt.Packet, _ string) error { return nil diff --git a/lnwallet/btcwallet/psbt.go b/lnwallet/btcwallet/psbt.go index 8e657f0c5..45cbcd6a0 100644 --- a/lnwallet/btcwallet/psbt.go +++ b/lnwallet/btcwallet/psbt.go @@ -1,13 +1,33 @@ package btcwallet import ( + "bytes" + "fmt" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil/psbt" "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) +var ( + // PsbtKeyTypeInputSignatureTweakSingle is a custom/proprietary PSBT key + // for an input that specifies what single tweak should be applied to + // the key before signing the input. The value 51 is leet speak for + // "si", short for "single". + PsbtKeyTypeInputSignatureTweakSingle = []byte{0x51} + + // PsbtKeyTypeInputSignatureTweakDouble is a custom/proprietary PSBT key + // for an input that specifies what double tweak should be applied to + // the key before signing the input. The value d0 is leet speak for + // "do", short for "double". + PsbtKeyTypeInputSignatureTweakDouble = []byte{0xd0} +) + // FundPsbt creates a fully populated PSBT packet that contains enough inputs to // fund the outputs specified in the passed in packet with the specified fee // rate. If there is change left, a change output from the internal wallet is @@ -64,6 +84,177 @@ func (b *BtcWallet) FundPsbt(packet *psbt.Packet, minConfs int32, ) } +// SignPsbt expects a partial transaction with all inputs and outputs fully +// declared and tries to sign all unsigned inputs that have all required fields +// (UTXO information, BIP32 derivation information, witness or sig scripts) set. +// If no error is returned, the PSBT is ready to be given to the next signer or +// to be finalized if lnd was the last signer. +// +// NOTE: This method only signs inputs (and only those it can sign), it does not +// perform any other tasks (such as coin selection, UTXO locking or +// input/output/fee value validation, PSBT finalization). Any input that is +// incomplete will be skipped. +func (b *BtcWallet) SignPsbt(packet *psbt.Packet) error { + // Let's check that this is actually something we can and want to sign. + // We need at least one input and one output. + err := psbt.VerifyInputOutputLen(packet, true, true) + if err != nil { + return err + } + + // Go through each input that doesn't have final witness data attached + // to it already and try to sign it. If there is nothing more to sign or + // there are inputs that we don't know how to sign, we won't return any + // error. So it's possible we're not the final signer. + tx := packet.UnsignedTx + sigHashes := txscript.NewTxSigHashes(tx) + for idx := range tx.TxIn { + in := packet.Inputs[idx] + + // We can only sign if we have UTXO information available. Since + // we don't finalize, we just skip over any input that we know + // we can't do anything with. Since we only support signing + // witness inputs, we only look at the witness UTXO being set. + if in.WitnessUtxo == nil { + continue + } + + // Skip this input if it's got final witness data attached. + if len(in.FinalScriptWitness) > 0 { + continue + } + + // Skip this input if there is no BIP32 derivation info + // available. + if len(in.Bip32Derivation) == 0 { + continue + } + + // TODO(guggero): For multisig, we'll need to find out what key + // to use and there should be multiple derivation paths in the + // BIP32 derivation field. + + // Let's try and derive the key now. This method will decide if + // it's a BIP49/84 key for normal on-chain funds or a key of the + // custom purpose 1017 key scope. + derivationInfo := in.Bip32Derivation[0] + privKey, err := b.deriveKeyByBIP32Path(derivationInfo.Bip32Path) + if err != nil { + log.Warnf("SignPsbt: Skipping input %d, error "+ + "deriving signing key: %v", idx, err) + continue + } + + // We need to make sure we actually derived the key that was + // expected to be derived. + pubKeysEqual := bytes.Equal( + derivationInfo.PubKey, + privKey.PubKey().SerializeCompressed(), + ) + if !pubKeysEqual { + log.Warnf("SignPsbt: Skipping input %d, derived "+ + "public key %x does not match bip32 "+ + "derivation info public key %x", idx, + privKey.PubKey().SerializeCompressed(), + derivationInfo.PubKey) + continue + } + + // Do we need to tweak anything? Single or double tweaks are + // sent as custom/proprietary fields in the PSBT input section. + privKey = maybeTweakPrivKeyPsbt(in.Unknowns, privKey) + pubKeyBytes := privKey.PubKey().SerializeCompressed() + + // Extract the correct witness and/or legacy scripts now, + // depending on the type of input we sign. The txscript package + // has the peculiar requirement that the PkScript of a P2PKH + // must be given as the witness script in order for it to arrive + // at the correct sighash. That's why we call it subScript here + // instead of witness script. + subScript, scriptSig, err := prepareScripts(in) + if err != nil { + // We derived the correct key so we _are_ expected to + // sign this. Not being able to sign at this point means + // there's something wrong. + return fmt.Errorf("error deriving script for input "+ + "%d: %v", idx, err) + } + + // We have everything we need for signing the input now. + sig, err := txscript.RawTxInWitnessSignature( + tx, sigHashes, idx, in.WitnessUtxo.Value, subScript, + in.SighashType, privKey, + ) + if err != nil { + return fmt.Errorf("error signing input %d: %v", idx, + err) + } + packet.Inputs[idx].FinalScriptSig = scriptSig + packet.Inputs[idx].PartialSigs = append( + packet.Inputs[idx].PartialSigs, &psbt.PartialSig{ + PubKey: pubKeyBytes, + Signature: sig, + }, + ) + } + + return nil +} + +// prepareScripts returns the appropriate witness and/or legacy scripts, +// depending on the type of input that should be signed. +func prepareScripts(in psbt.PInput) ([]byte, []byte, error) { + switch { + // It's a NP2WKH input: + case len(in.RedeemScript) > 0: + builder := txscript.NewScriptBuilder() + builder.AddData(in.RedeemScript) + sigScript, err := builder.Script() + if err != nil { + return nil, nil, fmt.Errorf("error building np2wkh "+ + "script: %v", err) + } + + return in.RedeemScript, sigScript, nil + + // It's a P2WSH input: + case len(in.WitnessScript) > 0: + return in.WitnessScript, nil, nil + + // It's a P2WKH input: + default: + return in.WitnessUtxo.PkScript, nil, nil + } +} + +// maybeTweakPrivKeyPsbt examines if there are any tweak parameters given in the +// custom/proprietary PSBT fields and may perform a mapping on the passed +// private key in order to utilize the tweaks, if populated. +func maybeTweakPrivKeyPsbt(unknowns []*psbt.Unknown, + privKey *btcec.PrivateKey) *btcec.PrivateKey { + + // There can be other custom/unknown keys in a PSBT that we just ignore. + // Key tweaking is optional and only one tweak (single _or_ double) can + // ever be applied (at least for any use cases described in the BOLT + // spec). + for _, u := range unknowns { + if bytes.Equal(u.Key, PsbtKeyTypeInputSignatureTweakSingle) { + return input.TweakPrivKey(privKey, u.Value) + } + + if bytes.Equal(u.Key, PsbtKeyTypeInputSignatureTweakDouble) { + doubleTweakKey, _ := btcec.PrivKeyFromBytes( + btcec.S256(), u.Value, + ) + return input.DeriveRevocationPrivKey( + privKey, doubleTweakKey, + ) + } + } + + return privKey +} + // FinalizePsbt expects a partial transaction with all inputs and outputs fully // declared and tries to sign all inputs that belong to the specified account. // Lnd must be the last signer of the transaction. That means, if there are any diff --git a/lnwallet/btcwallet/psbt_test.go b/lnwallet/btcwallet/psbt_test.go new file mode 100644 index 000000000..5210d7a7c --- /dev/null +++ b/lnwallet/btcwallet/psbt_test.go @@ -0,0 +1,337 @@ +package btcwallet + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + "testing" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/psbt" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" + "github.com/stretchr/testify/require" +) + +var ( + netParams = &chaincfg.RegressionNetParams + testValue int64 = 345678 + testCSVTimeout uint32 = 2016 + + testCommitSecretBytes, _ = hex.DecodeString( + "9f1f0db609718cf70c580aec6a0e570c3f086ec85a2a6119295b1d64240d" + + "aca5", + ) + testCommitSecret, testCommitPoint = btcec.PrivKeyFromBytes( + btcec.S256(), testCommitSecretBytes, + ) + + remoteRevocationBasePubKeyBytes, _ = hex.DecodeString( + "02baf067bfd1a6cf7229c7c459b106d384ad33e948ea1d561f2034475ff1" + + "7359fb", + ) + remoteRevocationBasePubKey, _ = btcec.ParsePubKey( + remoteRevocationBasePubKeyBytes, btcec.S256(), + ) + + testTweakSingle, _ = hex.DecodeString( + "020143a30cf6b71ca2af01efbd1758a04b4c7f5c2299f2ea63a8a6b58107" + + "63b1ed", + ) +) + +// testInputType is a type that represents different types of inputs that are +// signed within a PSBT. +type testInputType uint8 + +const ( + plainP2WKH testInputType = 0 + tweakedP2WKH testInputType = 1 + nestedP2WKH testInputType = 2 + singleKeyP2WSH testInputType = 3 + singleKeyDoubleTweakedP2WSH testInputType = 4 +) + +func (i testInputType) keyPath() []uint32 { + switch i { + case nestedP2WKH: + return []uint32{ + hardenedKey(waddrmgr.KeyScopeBIP0049Plus.Purpose), + hardenedKey(0), + hardenedKey(0), + 0, 0, + } + + case singleKeyP2WSH: + return []uint32{ + hardenedKey(keychain.BIP0043Purpose), + hardenedKey(netParams.HDCoinType), + hardenedKey(uint32(keychain.KeyFamilyPaymentBase)), + 0, 7, + } + + case singleKeyDoubleTweakedP2WSH: + return []uint32{ + hardenedKey(keychain.BIP0043Purpose), + hardenedKey(netParams.HDCoinType), + hardenedKey(uint32(keychain.KeyFamilyDelayBase)), + 0, 9, + } + + default: + return []uint32{ + hardenedKey(waddrmgr.KeyScopeBIP0084.Purpose), + hardenedKey(0), + hardenedKey(0), + 0, 0, + } + } +} + +func (i testInputType) output(t *testing.T, + privKey *btcec.PrivateKey) (*wire.TxOut, []byte) { + + var ( + addr btcutil.Address + witnessScript []byte + err error + ) + switch i { + case plainP2WKH: + h := btcutil.Hash160(privKey.PubKey().SerializeCompressed()) + addr, err = btcutil.NewAddressWitnessPubKeyHash(h, netParams) + require.NoError(t, err) + + case tweakedP2WKH: + privKey = input.TweakPrivKey(privKey, testTweakSingle) + + h := btcutil.Hash160(privKey.PubKey().SerializeCompressed()) + addr, err = btcutil.NewAddressWitnessPubKeyHash(h, netParams) + require.NoError(t, err) + + case nestedP2WKH: + h := btcutil.Hash160(privKey.PubKey().SerializeCompressed()) + witnessAddr, err := btcutil.NewAddressWitnessPubKeyHash( + h, netParams, + ) + require.NoError(t, err) + + witnessProgram, err := txscript.PayToAddrScript(witnessAddr) + require.NoError(t, err) + + addr, err = btcutil.NewAddressScriptHash( + witnessProgram, netParams, + ) + require.NoError(t, err) + + case singleKeyP2WSH: + // We're simulating a delay-to-self script which we're going to + // spend through the time lock path. We don't actually need to + // know the private key of the remote revocation base key. + revokeKey := input.DeriveRevocationPubkey( + remoteRevocationBasePubKey, testCommitPoint, + ) + witnessScript, err = input.CommitScriptToSelf( + testCSVTimeout, privKey.PubKey(), revokeKey, + ) + require.NoError(t, err) + + h := sha256.Sum256(witnessScript) + addr, err = btcutil.NewAddressWitnessScriptHash(h[:], netParams) + require.NoError(t, err) + + case singleKeyDoubleTweakedP2WSH: + // We're simulating breaching a remote party's delay-to-self + // output which we're going to spend through the revocation + // path. In that case the self key is the other party's self key + // and, we only know the revocation base private key and commit + // secret. + revokeKey := input.DeriveRevocationPubkey( + privKey.PubKey(), testCommitPoint, + ) + witnessScript, err = input.CommitScriptToSelf( + testCSVTimeout, remoteRevocationBasePubKey, revokeKey, + ) + require.NoError(t, err) + + h := sha256.Sum256(witnessScript) + addr, err = btcutil.NewAddressWitnessScriptHash(h[:], netParams) + require.NoError(t, err) + + default: + t.Fatalf("invalid input type") + } + + pkScript, err := txscript.PayToAddrScript(addr) + require.NoError(t, err) + return &wire.TxOut{ + Value: testValue, + PkScript: pkScript, + }, witnessScript +} + +func (i testInputType) decorateInput(t *testing.T, privKey *btcec.PrivateKey, + in *psbt.PInput) { + + switch i { + case tweakedP2WKH: + in.Unknowns = []*psbt.Unknown{{ + Key: PsbtKeyTypeInputSignatureTweakSingle, + Value: testTweakSingle, + }} + + case nestedP2WKH: + h := btcutil.Hash160(privKey.PubKey().SerializeCompressed()) + witnessAddr, err := btcutil.NewAddressWitnessPubKeyHash( + h, netParams, + ) + require.NoError(t, err) + + witnessProgram, err := txscript.PayToAddrScript(witnessAddr) + require.NoError(t, err) + in.RedeemScript = witnessProgram + + case singleKeyDoubleTweakedP2WSH: + in.Unknowns = []*psbt.Unknown{{ + Key: PsbtKeyTypeInputSignatureTweakDouble, + Value: testCommitSecret.Serialize(), + }} + } +} + +func (i testInputType) beforeFinalize(t *testing.T, packet *psbt.Packet) { + in := &packet.Inputs[0] + sigBytes := in.PartialSigs[0].Signature + pubKeyBytes := in.PartialSigs[0].PubKey + + var witnessStack wire.TxWitness + switch i { + case singleKeyP2WSH: + witnessStack = make([][]byte, 3) + witnessStack[0] = sigBytes + witnessStack[1] = nil + witnessStack[2] = in.WitnessScript + + case singleKeyDoubleTweakedP2WSH: + // Place a 1 as the first item in the evaluated witness stack to + // force script execution to the revocation clause. + witnessStack = make([][]byte, 3) + witnessStack[0] = sigBytes + witnessStack[1] = []byte{1} + witnessStack[2] = in.WitnessScript + + default: + witnessStack = make([][]byte, 2) + witnessStack[0] = sigBytes + witnessStack[1] = pubKeyBytes + } + + var err error + in.FinalScriptWitness, err = serializeTxWitness(witnessStack) + require.NoError(t, err) +} + +// serializeTxWitness return the wire witness stack into raw bytes. +func serializeTxWitness(txWitness wire.TxWitness) ([]byte, error) { + var witnessBytes bytes.Buffer + err := psbt.WriteTxWitness(&witnessBytes, txWitness) + if err != nil { + return nil, fmt.Errorf("error serializing witness: %v", err) + } + + return witnessBytes.Bytes(), nil +} + +// TestSignPsbt tests the PSBT signing functionality. +func TestSignPsbt(t *testing.T) { + w, cleanup := newTestWallet(t, netParams, seedBytes) + defer cleanup() + + testCases := []struct { + name string + inputType testInputType + }{{ + name: "plain P2WKH", + inputType: plainP2WKH, + }, { + name: "tweaked P2WKH", + inputType: tweakedP2WKH, + }, { + name: "nested P2WKH", + inputType: nestedP2WKH, + }, { + name: "single key P2WSH", + inputType: singleKeyP2WSH, + }, { + name: "single key double tweaked P2WSH", + inputType: singleKeyDoubleTweakedP2WSH, + }} + + for _, tc := range testCases { + tc := tc + + // This is the private key we're going to sign with. + privKey, err := w.deriveKeyByBIP32Path(tc.inputType.keyPath()) + require.NoError(t, err) + + txOut, witnessScript := tc.inputType.output(t, privKey) + + // Create the reference transaction that has the input that is + // going to be spent by our PSBT. + refTx := wire.NewMsgTx(2) + refTx.AddTxIn(&wire.TxIn{}) + refTx.AddTxOut(txOut) + + // Create the unsigned spend transaction that is going to be the + // main content of our PSBT. + spendTx := wire.NewMsgTx(2) + spendTx.LockTime = testCSVTimeout + spendTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Hash: refTx.TxHash(), + Index: 0, + }, + Sequence: testCSVTimeout, + }) + spendTx.AddTxOut(txOut) + + // Convert it to a PSBT now and add all required signing + // metadata to it. + packet, err := psbt.NewFromUnsignedTx(spendTx) + require.NoError(t, err) + packet.Inputs[0].WitnessScript = witnessScript + packet.Inputs[0].SighashType = txscript.SigHashAll + packet.Inputs[0].WitnessUtxo = refTx.TxOut[0] + packet.Inputs[0].Bip32Derivation = []*psbt.Bip32Derivation{{ + PubKey: privKey.PubKey().SerializeCompressed(), + Bip32Path: tc.inputType.keyPath(), + }} + tc.inputType.decorateInput(t, privKey, &packet.Inputs[0]) + + // Let the wallet do its job. We expect to be the only signer + // for this PSBT, so we'll be able to finalize it later. + err = w.SignPsbt(packet) + require.NoError(t, err) + + // If the witness stack needs to be assembled, give the caller + // the option to do that now. + tc.inputType.beforeFinalize(t, packet) + + finalTx, err := psbt.Extract(packet) + require.NoError(t, err) + + vm, err := txscript.NewEngine( + refTx.TxOut[0].PkScript, finalTx, 0, + txscript.StandardVerifyFlags, nil, nil, + refTx.TxOut[0].Value, + ) + require.NoError(t, err) + require.NoError(t, vm.Execute()) + } +} diff --git a/lnwallet/interface.go b/lnwallet/interface.go index 455d48b00..70b7b0331 100644 --- a/lnwallet/interface.go +++ b/lnwallet/interface.go @@ -364,6 +364,19 @@ type WalletController interface { FundPsbt(packet *psbt.Packet, minConfs int32, feeRate chainfee.SatPerKWeight, account string) (int32, error) + // SignPsbt expects a partial transaction with all inputs and outputs + // fully declared and tries to sign all unsigned inputs that have all + // required fields (UTXO information, BIP32 derivation information, + // witness or sig scripts) set. + // If no error is returned, the PSBT is ready to be given to the next + // signer or to be finalized if lnd was the last signer. + // + // NOTE: This method only signs inputs (and only those it can sign), it + // does not perform any other tasks (such as coin selection, UTXO + // locking or input/output/fee value validation, PSBT finalization). Any + // input that is incomplete will be skipped. + SignPsbt(packet *psbt.Packet) error + // FinalizePsbt expects a partial transaction with all inputs and // outputs fully declared and tries to sign all inputs that belong to // the specified account. Lnd must be the last signer of the