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:
parent
c81e0a3ebb
commit
e942e70651
@ -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
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user