lnwallet: extend the SignDescriptor to include a PrivateTweak

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.
This commit is contained in:
Olaoluwa Osuntokun 2016-11-18 17:11:59 -08:00
parent c81e0a3ebb
commit e942e70651
No known key found for this signature in database
GPG Key ID: 9CC5B105D03521A2
4 changed files with 136 additions and 12 deletions

@ -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. // 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() b.cacheMtx.RLock()
if output, ok := b.utxoCache[*prevOut]; ok { if output, ok := b.utxoCache[*prevOut]; ok {
b.cacheMtx.RUnlock() b.cacheMtx.RUnlock()
@ -72,7 +72,7 @@ func (b *BtcWallet) fetchOutputAddr(script []byte) (waddrmgr.ManagedAddress, err
return nil, fmt.Errorf("address not found") 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. // passed public key.
// TODO(roasbeef): alternatively can extract all the data pushes within the // TODO(roasbeef): alternatively can extract all the data pushes within the
// script, then attempt to match keys one by one // 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) { func (b *BtcWallet) SignOutputRaw(tx *wire.MsgTx, signDesc *lnwallet.SignDescriptor) ([]byte, error) {
witnessScript := signDesc.WitnessScript witnessScript := signDesc.WitnessScript
// First attempt to fetch the private key which corresponds to the
// specified public key.
privKey, err := b.fetchPrivKey(signDesc.PubKey) privKey, err := b.fetchPrivKey(signDesc.PubKey)
if err != nil { if err != nil {
return nil, err 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 amt := signDesc.Output.Value
sig, err := txscript.RawTxInWitnessSignature(tx, signDesc.SigHashes, 0, sig, err := txscript.RawTxInWitnessSignature(tx, signDesc.SigHashes,
amt, witnessScript, txscript.SigHashAll, privKey) signDesc.InputIndex, amt, witnessScript, txscript.SigHashAll,
privKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }

@ -140,7 +140,7 @@ type WalletController interface {
// SendOutputs funds, signs, and broadcasts a Bitcoin transaction // SendOutputs funds, signs, and broadcasts a Bitcoin transaction
// paying out to the specified outputs. In the case the wallet has // 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. // should be returned.
SendOutputs(outputs []*wire.TxOut) (*wire.ShaHash, error) SendOutputs(outputs []*wire.TxOut) (*wire.ShaHash, error)
@ -219,6 +219,15 @@ type SignDescriptor struct {
// key corresponding to this public key. // key corresponding to this public key.
PubKey *btcec.PublicKey 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 // WitnessScript is the full script required to properly redeem the
// output. This field will only be populated if a p2wsh or a p2sh // output. This field will only be populated if a p2wsh or a p2sh
// output is being signed. // output is being signed.

@ -1120,11 +1120,115 @@ func testTransactionSubscriptions(miner *rpctest.Harness, w *lnwallet.LightningW
select { select {
case <-time.After(time.Second * 5): case <-time.After(time.Second * 5):
t.Fatalf("transactions not received after 3 seconds") 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){ var walletTests = []func(miner *rpctest.Harness, w *lnwallet.LightningWallet, test *testing.T){
// TODO(roasbeef): reservation tests should prob be split out
testDualFundingReservationWorkflow, testDualFundingReservationWorkflow,
testSingleFunderReservationWorkflowInitiator, testSingleFunderReservationWorkflowInitiator,
testSingleFunderReservationWorkflowResponder, testSingleFunderReservationWorkflowResponder,
@ -1133,6 +1237,7 @@ var walletTests = []func(miner *rpctest.Harness, w *lnwallet.LightningWallet, te
testFundingReservationInvalidCounterpartySigs, testFundingReservationInvalidCounterpartySigs,
testTransactionSubscriptions, testTransactionSubscriptions,
testListTransactionDetails, testListTransactionDetails,
testSignOutputPrivateTweak,
} }
type testLnWallet struct { type testLnWallet struct {

@ -271,9 +271,9 @@ type LightningWallet struct {
// update the commitment state. // update the commitment state.
Signer Signer 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. // 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 // rootKey is the root HD key derived from a WalletController private
// key. This rootKey is used to derive all LN specific secrets. // 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, chainNotifier: notifier,
Signer: signer, Signer: signer,
WalletController: wallet, WalletController: wallet,
chainIO: bio, ChainIO: bio,
ChannelDB: cdb, ChannelDB: cdb,
msgChan: make(chan interface{}, msgBufferSize), msgChan: make(chan interface{}, msgBufferSize),
nextFundingID: 0, nextFundingID: 0,
@ -961,7 +961,7 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs
// Fetch the alleged previous output along with the // Fetch the alleged previous output along with the
// pkscript referenced by this input. // pkscript referenced by this input.
prevOut := txin.PreviousOutPoint prevOut := txin.PreviousOutPoint
output, err := l.chainIO.GetUtxo(&prevOut.Hash, prevOut.Index) output, err := l.ChainIO.GetUtxo(&prevOut.Hash, prevOut.Index)
if output == nil { if output == nil {
msg.err <- fmt.Errorf("input to funding tx does not exist: %v", err) msg.err <- fmt.Errorf("input to funding tx does not exist: %v", err)
return return
@ -1200,7 +1200,7 @@ func (l *LightningWallet) handleChannelOpen(req *channelOpenMsg) {
// Finally, create and officially open the payment channel! // Finally, create and officially open the payment channel!
// TODO(roasbeef): CreationTime once tx is 'open' // 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.partialState)
res.chanOpen <- channel res.chanOpen <- channel
@ -1242,7 +1242,7 @@ out:
// Finally, create and officially open the payment channel! // Finally, create and officially open the payment channel!
// TODO(roasbeef): CreationTime once tx is 'open' // 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.partialState)
res.chanOpen <- channel res.chanOpen <- channel
} }