From e942e706515afad68e69c36990fe408ad46b34a4 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 18 Nov 2016 17:11:59 -0800 Subject: [PATCH] lnwallet: extend the SignDescriptor to include a PrivateTweak MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit extends the SignDescriptor with a single attribute, the ‘PrivateTweak’. The duties of the Signer interface have also been augmented to properly derive a private key using the specified tweak, iff it’s non-nil. As currently defined in order to generate the proper private key based off of a PrivateTweak, the signer is to add the tweak value to the private key for the specified public key. This generated value is to be used for signing within the specified context. This change paves the way for automatic revoked output sweeping with signatures generated directly by the Signer interface, maintaining the structure of the abstraction. A test has been added at the interface level in order to excerise each WalletController’s implementation of the key derivation as currently defined. --- lnwallet/btcwallet/signer.go | 18 ++++-- lnwallet/interface.go | 11 +++- lnwallet/interface_test.go | 107 ++++++++++++++++++++++++++++++++++- lnwallet/wallet.go | 12 ++-- 4 files changed, 136 insertions(+), 12 deletions(-) diff --git a/lnwallet/btcwallet/signer.go b/lnwallet/btcwallet/signer.go index d4c2506a..470208fc 100644 --- a/lnwallet/btcwallet/signer.go +++ b/lnwallet/btcwallet/signer.go @@ -24,7 +24,7 @@ func (b *BtcWallet) FetchInputInfo(prevOut *wire.OutPoint) (*wire.TxOut, error) ) // First check to see if the output is already within the utxo cache. - // If so we can return directly saving usk a disk access. + // If so we can return directly saving a disk access. b.cacheMtx.RLock() if output, ok := b.utxoCache[*prevOut]; ok { b.cacheMtx.RUnlock() @@ -72,7 +72,7 @@ func (b *BtcWallet) fetchOutputAddr(script []byte) (waddrmgr.ManagedAddress, err return nil, fmt.Errorf("address not found") } -// fetchPrivKey attempts to retrieve the raw private key coresponding to the +// fetchPrivKey attempts to retrieve the raw private key corresponding to the // passed public key. // TODO(roasbeef): alternatively can extract all the data pushes within the // script, then attempt to match keys one by one @@ -98,14 +98,24 @@ func (b *BtcWallet) fetchPrivKey(pub *btcec.PublicKey) (*btcec.PrivateKey, error func (b *BtcWallet) SignOutputRaw(tx *wire.MsgTx, signDesc *lnwallet.SignDescriptor) ([]byte, error) { witnessScript := signDesc.WitnessScript + // First attempt to fetch the private key which corresponds to the + // specified public key. privKey, err := b.fetchPrivKey(signDesc.PubKey) if err != nil { return nil, err } + // If a tweak is specified, then we'll need to use this tweak to derive + // the final private key to be used for signing this output. + if signDesc.PrivateTweak != nil { + privKey = lnwallet.DeriveRevocationPrivKey(privKey, + signDesc.PrivateTweak) + } + amt := signDesc.Output.Value - sig, err := txscript.RawTxInWitnessSignature(tx, signDesc.SigHashes, 0, - amt, witnessScript, txscript.SigHashAll, privKey) + sig, err := txscript.RawTxInWitnessSignature(tx, signDesc.SigHashes, + signDesc.InputIndex, amt, witnessScript, txscript.SigHashAll, + privKey) if err != nil { return nil, err } diff --git a/lnwallet/interface.go b/lnwallet/interface.go index 2b0ce9cd..8ffc49a2 100644 --- a/lnwallet/interface.go +++ b/lnwallet/interface.go @@ -140,7 +140,7 @@ type WalletController interface { // SendOutputs funds, signs, and broadcasts a Bitcoin transaction // paying out to the specified outputs. In the case the wallet has - // insufficient funds, or the outputs are non-standard, and error + // insufficient funds, or the outputs are non-standard, an error // should be returned. SendOutputs(outputs []*wire.TxOut) (*wire.ShaHash, error) @@ -219,6 +219,15 @@ type SignDescriptor struct { // key corresponding to this public key. PubKey *btcec.PublicKey + // PrivateTweak is a scalar value that should be added to the private + // key corresponding to the above public key to obtain the private key + // to be used to sign this input. This value is typically a leaf node + // from the revocation tree. + // + // NOTE: If this value is nil, then the input can be signed using only + // the above public key. + PrivateTweak []byte + // WitnessScript is the full script required to properly redeem the // output. This field will only be populated if a p2wsh or a p2sh // output is being signed. diff --git a/lnwallet/interface_test.go b/lnwallet/interface_test.go index eea887b0..868cb1d2 100644 --- a/lnwallet/interface_test.go +++ b/lnwallet/interface_test.go @@ -1120,11 +1120,115 @@ func testTransactionSubscriptions(miner *rpctest.Harness, w *lnwallet.LightningW select { case <-time.After(time.Second * 5): t.Fatalf("transactions not received after 3 seconds") - case <-confirmedNtfns: // Fall through on successs + case <-confirmedNtfns: // Fall through on success + } +} + +func testSignOutputPrivateTweak(r *rpctest.Harness, w *lnwallet.LightningWallet, t *testing.T) { + t.Logf("Running private tweak test") + + // We'd like to test the ability of the wallet's Signer implementation + // to be able to sign with a private key derived from tweaking the + // specific public key. This scenario exercises the case when the + // wallet needs to sign for a sweep of a revoked output. + + // First, generate a new public key under th control of the wallet, + // then generate a revocation key using it. + pubkey, err := w.NewRawKey() + if err != nil { + t.Fatalf("unable to obtain public key: %v", err) + } + revocation := bytes.Repeat([]byte{2}, 32) + revocationKey := lnwallet.DeriveRevocationPubkey(pubkey, revocation) + + // With the revocation key generated, create a pkScript that pays to + // the revocation key using a simple p2wkh script. + pubkeyHash := btcutil.Hash160(revocationKey.SerializeCompressed()) + revokeAddr, err := btcutil.NewAddressWitnessPubKeyHash(pubkeyHash, + &chaincfg.SimNetParams) + if err != nil { + t.Fatalf("unable to create addr: %v", err) + } + revokeScript, err := txscript.PayToAddrScript(revokeAddr) + if err != nil { + t.Fatalf("unable to generate script: %v", err) + } + + // With the script fully assemebld, instruct the wallet to fund the + // output with a newly creaed transaction. + revokeOutput := &wire.TxOut{ + Value: btcutil.SatoshiPerBitcoin, + PkScript: revokeScript, + } + txid, err := w.SendOutputs([]*wire.TxOut{revokeOutput}) + if err != nil { + t.Fatalf("unable to create output: %v", err) + } + + // Query for the transaction generated above so we can located the + // index of our output. + tx, err := w.ChainIO.GetTransaction(txid) + if err != nil { + t.Fatalf("unable to query for tx: %v", err) + } + var outputIndex uint32 + if bytes.Equal(tx.TxOut[0].PkScript, revokeScript) { + outputIndex = 0 + } else { + outputIndex = 1 + } + + /// WIth the index located, we can create a transaction spending the + //referenced output. + sweepTx := wire.NewMsgTx() + sweepTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Hash: tx.TxSha(), + Index: outputIndex, + }, + }) + + // Now we can populate the sign descriptor which we'll use to generate + // the signature. Within the descriptor we set the private tweak value + // as the key in the script is derived based on this tweak value and + // the key we originally generated above. + signDesc := &lnwallet.SignDescriptor{ + PubKey: pubkey, + PrivateTweak: revocation, + WitnessScript: revokeScript, + Output: revokeOutput, + HashType: txscript.SigHashAll, + SigHashes: txscript.NewTxSigHashes(sweepTx), + InputIndex: 0, + } + + // With the descriptor created, we use it to generate a signature, then + // manually create a valid witness stack we'll use for signing. + spendSig, err := w.Signer.SignOutputRaw(sweepTx, signDesc) + if err != nil { + t.Fatalf("unable to generate signature: %v", err) + } + witness := make([][]byte, 2) + witness[0] = append(spendSig, byte(txscript.SigHashAll)) + witness[1] = revocationKey.SerializeCompressed() + sweepTx.TxIn[0].Witness = witness + + // Finally, attempt to validate the completed transaction. This should + // succeed if the wallet was able to properly generate the proper + // private key. + vm, err := txscript.NewEngine(revokeScript, + sweepTx, 0, txscript.StandardVerifyFlags, nil, + nil, int64(btcutil.SatoshiPerBitcoin)) + if err != nil { + t.Fatalf("unable to create engine: %v", err) + } + if err := vm.Execute(); err != nil { + t.Fatalf("revocation spend invalid: %v", err) } } var walletTests = []func(miner *rpctest.Harness, w *lnwallet.LightningWallet, test *testing.T){ + // TODO(roasbeef): reservation tests should prob be split out testDualFundingReservationWorkflow, testSingleFunderReservationWorkflowInitiator, testSingleFunderReservationWorkflowResponder, @@ -1133,6 +1237,7 @@ var walletTests = []func(miner *rpctest.Harness, w *lnwallet.LightningWallet, te testFundingReservationInvalidCounterpartySigs, testTransactionSubscriptions, testListTransactionDetails, + testSignOutputPrivateTweak, } type testLnWallet struct { diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 2f19b2f2..57e27ffa 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -271,9 +271,9 @@ type LightningWallet struct { // update the commitment state. Signer Signer - // chainIO is an instance of the BlockChainIO interface. chainIO is + // ChainIO is an instance of the BlockChainIO interface. ChainIO is // used to lookup the existence of outputs within the UTXO set. - chainIO BlockChainIO + ChainIO BlockChainIO // rootKey is the root HD key derived from a WalletController private // key. This rootKey is used to derive all LN specific secrets. @@ -341,7 +341,7 @@ func NewLightningWallet(cdb *channeldb.DB, notifier chainntnfs.ChainNotifier, chainNotifier: notifier, Signer: signer, WalletController: wallet, - chainIO: bio, + ChainIO: bio, ChannelDB: cdb, msgChan: make(chan interface{}, msgBufferSize), nextFundingID: 0, @@ -961,7 +961,7 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs // Fetch the alleged previous output along with the // pkscript referenced by this input. prevOut := txin.PreviousOutPoint - output, err := l.chainIO.GetUtxo(&prevOut.Hash, prevOut.Index) + output, err := l.ChainIO.GetUtxo(&prevOut.Hash, prevOut.Index) if output == nil { msg.err <- fmt.Errorf("input to funding tx does not exist: %v", err) return @@ -1200,7 +1200,7 @@ func (l *LightningWallet) handleChannelOpen(req *channelOpenMsg) { // Finally, create and officially open the payment channel! // TODO(roasbeef): CreationTime once tx is 'open' - channel, _ := NewLightningChannel(l.Signer, l.chainIO, l.chainNotifier, + channel, _ := NewLightningChannel(l.Signer, l.ChainIO, l.chainNotifier, res.partialState) res.chanOpen <- channel @@ -1242,7 +1242,7 @@ out: // Finally, create and officially open the payment channel! // TODO(roasbeef): CreationTime once tx is 'open' - channel, _ := NewLightningChannel(l.Signer, l.chainIO, l.chainNotifier, + channel, _ := NewLightningChannel(l.Signer, l.ChainIO, l.chainNotifier, res.partialState) res.chanOpen <- channel }