lnwallet: update HTLC+commitment scripts

This commit updates the previous HTLC and commitment scripts to their
current latest evolution.

The HTLC scripts have been optimized for space savings, the
functionality itself has remained relatively unchanged. A trade off was
made to add additional bytes into the sigScript in order to avoid
extraneous CHECKSIG’s. The rationale is that an extra 1-2 bytes in the
sigScript to guide execution, are worthwhile since they’re in the
witness, and witness data may be pruned in the near future.

The primary change is within the commitment transaction itself. Instead
of using revocation hashes, we now use signature based revocation. This
saves space in the Script, and optimizes away an extra hashing
operation. Elkrem/shachain is still used but, we now use the pre-images
to homomorphically derive a public key which the other party will be
able to sign with, once we disclose the pre-image itself.

Finally, we have switched to using SHA-256 everywhere uniformly for
both revocation hashes, and payment hashes. The rationale is that the
output of ripemd160 is too small for modern security margins, and that
other coins/chains are more likely to have SHA-256 implemented, than
ripemd160.

A set of tests has also been included which contain (mostly) exhaustive
tests of all possible redemption paths for both commitment and HTLC.
This commit is contained in:
Olaoluwa Osuntokun 2016-06-27 11:33:59 -07:00
parent ac8736ff99
commit e22734f9cf
No known key found for this signature in database
GPG Key ID: 9CC5B105D03521A2
3 changed files with 1031 additions and 60 deletions

@ -699,15 +699,23 @@ func createNewCommitmentTxns(fundingTxIn *wire.TxIn, state *channeldb.OpenChanne
return ourNewCommitTx, theirNewCommitTx, nil
}
// createCommitTx...
// TODO(roasbeef): fix inconsistency of 32 vs 20 byte revocation hashes everywhere...
// createCommitTx creates a commitment transaction, spending from specified
// funding output. The commitment transaction contains two outputs: one paying
// to the "owner" of the commitment transaction which can be spent after a
// relative block delay or revocation event, and the other paying the the
// counter-party within the channel, which can be spent immediately.
func createCommitTx(fundingOutput *wire.TxIn, selfKey, theirKey *btcec.PublicKey,
revokeHash []byte, csvTimeout uint32, amountToSelf,
amountToThem btcutil.Amount) (*wire.MsgTx, error) {
// First, we create the script for the delayed "pay-to-self" output.
ourRedeemScript, err := commitScriptToSelf(csvTimeout, selfKey, theirKey,
revokeHash)
// This output has 2 main redemption clauses: either we can redeem the
// output after a relative block delay, or the remote node can claim
// the funds with the revocation key if we broadcast a revoked
// commitment transaction.
revokeKey := deriveRevocationPubkey(theirKey, revokeHash)
ourRedeemScript, err := commitScriptToSelf(csvTimeout, selfKey,
revokeKey)
if err != nil {
return nil, err
}

@ -3,6 +3,7 @@ package lnwallet
import (
"bytes"
"fmt"
"math/big"
"github.com/btcsuite/fastsha256"
"github.com/roasbeef/btcd/btcec"
@ -125,96 +126,391 @@ func findScriptOutputIndex(tx *wire.MsgTx, script []byte) (bool, uint32) {
}
// senderHTLCScript constructs the public key script for an outgoing HTLC
// output payment for the sender's commitment transaction.
// output payment for the sender's version of the commitment transaction:
//
// Possible Input Scripts:
// SENDR: <sig> 0
// RECVR: <sig> <preimage> 0 1
// REVOK: <sig <preimage> 1 1
// * reciever revoke
//
// OP_IF
// //Receiver
// OP_IF
// //Revoke
// <revocation hash>
// OP_ELSE
// //Receive
// OP_SIZE 32 OP_EQUALVERIFY
// <payment hash>
// OP_ENDIF
// OP_SWAP
// OP_SHA256 OP_EQUALVERIFY
// <recv key> OP_CHECKSIG
// OP_ELSE
// //Sender
// <absolute blockheight> OP_CHECKLOCKTIMEVERIFY
// <relative blockheight> OP_CHECKSEQUENCEVERIFY
// OP_2DROP
// <sendr key> OP_CHECKSIG
// OP_ENDIF
func senderHTLCScript(absoluteTimeout, relativeTimeout uint32, senderKey,
receiverKey *btcec.PublicKey, revokeHash, paymentHash []byte) ([]byte, error) {
builder := txscript.NewScriptBuilder()
// Was the pre-image to the payment hash presented?
builder.AddOp(txscript.OP_HASH160)
builder.AddOp(txscript.OP_DUP)
builder.AddData(paymentHash)
builder.AddOp(txscript.OP_EQUAL)
// How about the pre-image for our commitment revocation hash?
builder.AddOp(txscript.OP_SWAP)
builder.AddData(revokeHash)
builder.AddOp(txscript.OP_EQUAL)
builder.AddOp(txscript.OP_SWAP)
builder.AddOp(txscript.OP_ADD)
// If either is present, then the receiver can claim immediately.
// The receiver of the HTLC places a 1 as the first item in the witness
// stack, forcing Script execution to enter the "if" clause within the
// main body of the script.
builder.AddOp(txscript.OP_IF)
builder.AddData(receiverKey.SerializeCompressed())
// Otherwise, we (the sender) need to wait for an absolute HTLC
// The receiver will place a 1 as the second item of the witness stack
// in the case the sender broadcasts a revoked commitment transaction.
// Executing this branch allows the receiver to claim the sender's
// funds as a result of their contract violation.
builder.AddOp(txscript.OP_IF)
builder.AddData(revokeHash)
// Alternatively, the receiver can place a 0 as the second item of the
// witness stack if they wish to claim the HTLC with the proper
// pre-image as normal. In order to prevent an over-sized pre-image
// attack (which can create undesirable redemption asymmerties, we
// strongly require that all HTLC pre-images are exactly 32 bytes.
builder.AddOp(txscript.OP_ELSE)
builder.AddOp(txscript.OP_SIZE)
builder.AddInt64(32)
builder.AddOp(txscript.OP_EQUALVERIFY)
builder.AddData(paymentHash)
builder.AddOp(txscript.OP_ENDIF)
builder.AddOp(txscript.OP_SWAP)
builder.AddOp(txscript.OP_SHA256)
builder.AddOp(txscript.OP_EQUALVERIFY)
// In either case, we require a valid signature by the receiver.
builder.AddData(receiverKey.SerializeCompressed())
builder.AddOp(txscript.OP_CHECKSIG)
// Otherwise, the sender of the HTLC will place a 0 as the first item
// of the witness stack in order to sweep the funds back after the HTLC
// times out.
builder.AddOp(txscript.OP_ELSE)
// In this case, the sender will need to wait for an absolute HTLC
// timeout, then afterwards a relative timeout before we claim re-claim
// the unsettled funds. This delay gives the other party a chance to
// present the pre-image to the revocation hash in the event that the
// sender (at this time) broadcasts this commitment transaction after
// it has been revoked.
builder.AddOp(txscript.OP_ELSE)
builder.AddInt64(int64(absoluteTimeout))
builder.AddOp(txscript.OP_CHECKLOCKTIMEVERIFY)
builder.AddInt64(int64(relativeTimeout))
builder.AddOp(OP_CHECKSEQUENCEVERIFY)
builder.AddOp(txscript.OP_2DROP)
builder.AddData(senderKey.SerializeCompressed())
builder.AddOp(txscript.OP_ENDIF)
builder.AddOp(txscript.OP_CHECKSIG)
builder.AddOp(txscript.OP_ENDIF)
return builder.Script()
}
// senderHTLCScript constructs the public key script for an incoming HTLC
// output payment for the receiver's commitment transaction.
// senderHtlcSpendRevoke constructs a valid witness allowing the reciever of an
// HTLC to claim the output with knowledge of the revocation preimage in the
// scenario that the sender of the HTLC broadcasts a previously revoked
// commitment transaction. A valid spend requires knowledge of the pre-image to
// the commitment transaction's revocation hash, and a valid signature under
// the receiver's public key.
func senderHtlcSpendRevoke(commitScript []byte, outputAmt btcutil.Amount,
reciverKey *btcec.PrivateKey, sweepTx *wire.MsgTx,
revokePreimage []byte) (wire.TxWitness, error) {
hashCache := txscript.NewTxSigHashes(sweepTx)
sweepSig, err := txscript.RawTxInWitnessSignature(
sweepTx, hashCache, 0, int64(outputAmt), commitScript,
txscript.SigHashAll, reciverKey)
if err != nil {
return nil, err
}
// In order to force script execution to enter the revocation clause,
// we place two one's as the first items in the final evalulated
// witness stack.
witnessStack := wire.TxWitness(make([][]byte, 5))
witnessStack[0] = sweepSig
witnessStack[1] = revokePreimage
witnessStack[2] = []byte{1}
witnessStack[3] = []byte{1}
witnessStack[4] = commitScript
return witnessStack, nil
}
// senderHtlcSpendRedeem constructs a valid witness allowing the receiver of an
// HTLC to redeem the pending output in the scenario that the sender broadcasts
// their version of the commitment transaction. A valid spend requires
// knowledge of the payment pre-image, and a valid signature under the
// receivers public key.
func senderHtlcSpendRedeem(commitScript []byte, outputAmt btcutil.Amount,
reciverKey *btcec.PrivateKey, sweepTx *wire.MsgTx,
paymentPreimage []byte) (wire.TxWitness, error) {
hashCache := txscript.NewTxSigHashes(sweepTx)
sweepSig, err := txscript.RawTxInWitnessSignature(
sweepTx, hashCache, 0, int64(outputAmt), commitScript,
txscript.SigHashAll, reciverKey)
if err != nil {
return nil, err
}
// We force script execution into the HTLC redemption clause by placing
// a one, then a zero as the first items in the final evalulated
// witness stack.
witnessStack := wire.TxWitness(make([][]byte, 5))
witnessStack[0] = sweepSig
witnessStack[1] = paymentPreimage
witnessStack[2] = []byte{0}
witnessStack[3] = []byte{1}
witnessStack[4] = commitScript
return witnessStack, nil
}
// htlcSpendTimeout constructs a valid witness allowing the sender of an HTLC
// to recover the pending funds after an absolute, then relative locktime
// period.
func senderHtlcSpendTimeout(commitScript []byte, outputAmt btcutil.Amount,
senderKey *btcec.PrivateKey, sweepTx *wire.MsgTx,
absoluteTimeout, relativeTimeout uint32) (wire.TxWitness, error) {
// Since the HTLC output has an absolute timeout before we're permitted
// to sweep the output, we need to set the locktime of this sweepign
// transaction to that aboslute value in order to pass Script
// verification.
sweepTx.LockTime = absoluteTimeout
// Additionally, we're required to wait a relative period of time
// before we can sweep the output in order to allow the other party to
// contest our claim of validity to this version of the commitment
// transaction.
sweepTx.TxIn[0].Sequence = lockTimeToSequence(false, relativeTimeout)
// Finally, OP_CSV requires that the version of the transaction
// spending a pkscript with OP_CSV within it *must* be >= 2.
sweepTx.Version = 2
hashCache := txscript.NewTxSigHashes(sweepTx)
sweepSig, err := txscript.RawTxInWitnessSignature(
sweepTx, hashCache, 0, int64(outputAmt), commitScript,
txscript.SigHashAll, senderKey)
if err != nil {
return nil, err
}
// We place a zero as the first item of the evaluated witness stack in
// order to force Script execution to the HTLC timeout clause.
witnessStack := wire.TxWitness(make([][]byte, 3))
witnessStack[0] = sweepSig
witnessStack[1] = []byte{0}
witnessStack[2] = commitScript
return witnessStack, nil
}
// receiverHTLCScript constructs the public key script for an incoming HTLC
// output payment for the receiver's version of the commitment transaction:
//
// Possible Input Scripts:
// RECVR: <sig> <preimage> 1
// REVOK: <sig> <preimage> 1 0
// SENDR: <sig> 0 0
//
// OP_IF
// //Receiver
// OP_SIZE 32 OP_EQUALVERIFY
// OP_SHA256
// <payment hash> OP_EQUALVERIFY
// <relative blockheight> OP_CHECKSEQUENCEVERIFY OP_DROP
// <receiver key> OP_CHECKSIG
// OP_ELSE
// //Sender
// OP_IF
// //Revocation
// OP_SHA256
// <revoke hash> OP_EQUALVERIFY
// OP_ELSE
// //Refund
// <absolute blockehight> OP_CHECKLOCKTIMEVERIFY OP_DROP
// OP_ENDIF
// <sender key> OP_CHECKSIG
// OP_ENDIF
func receiverHTLCScript(absoluteTimeout, relativeTimeout uint32, senderKey,
receiverKey *btcec.PublicKey, revokeHash, paymentHash []byte) ([]byte, error) {
builder := txscript.NewScriptBuilder()
// Was the pre-image to the payment hash presented?
builder.AddOp(txscript.OP_HASH160)
builder.AddOp(txscript.OP_DUP)
builder.AddData(paymentHash)
builder.AddOp(txscript.OP_EQUAL)
// The receiver of the script will place a 1 as the first item of the
// witness stack forcing Script execution to enter the "if" clause of
// the main body of the script.
builder.AddOp(txscript.OP_IF)
// If so, let the receiver redeem after a relative timeout. This added
// delay gives the sender (at this time) an opportunity to re-claim the
// pending HTLC in the event that the receiver (at this time) broadcasts
// this old commitment transaction after it has been revoked.
// In this clause, the receiver can redeem the HTLC after a relative timeout.
// This added delay gives the sender (at this time) an opportunity to
// re-claim the pending HTLC in the event that the receiver
// (at this time) broadcasts this old commitment transaction after it
// has been revoked. Additionally, we require that the pre-image is
// exactly 32-bytes in order to avoid undesirable redemption
// asymmerties in the multi-hop scenario.
builder.AddOp(txscript.OP_SIZE)
builder.AddInt64(32)
builder.AddOp(txscript.OP_EQUALVERIFY)
builder.AddOp(txscript.OP_SHA256)
builder.AddData(paymentHash)
builder.AddOp(txscript.OP_EQUALVERIFY)
builder.AddInt64(int64(relativeTimeout))
builder.AddOp(OP_CHECKSEQUENCEVERIFY)
builder.AddOp(txscript.OP_2DROP)
builder.AddOp(txscript.OP_DROP)
builder.AddData(receiverKey.SerializeCompressed())
builder.AddOp(txscript.OP_CHECKSIG)
// Otherwise, if the sender has the revocation pre-image to the
// receiver's commitment transactions, then let them claim the
// funds immediately.
// Otherwise, the sender will place a 0 as the first item of the
// witness stack forcing exeuction to enter the "else" clause of the
// main body of the script.
builder.AddOp(txscript.OP_ELSE)
// The sender will place a 1 as the second item of the witness stack
// in the scenario that the receiver broadcasts an invalidated
// commitment transaction, allowing the sender to sweep all the
// receiver's funds.
builder.AddOp(txscript.OP_IF)
builder.AddOp(txscript.OP_SHA256)
builder.AddData(revokeHash)
builder.AddOp(txscript.OP_EQUAL)
builder.AddOp(txscript.OP_EQUALVERIFY)
// If not, then the sender needs to wait for the HTLC timeout. This
// clause may be executed if the receiver fails to present the r-value
// in time. This prevents the pending funds from being locked up
// indefinately.
builder.AddOp(txscript.OP_NOTIF)
// The sender will place a 0 as the second item of the witness stack if
// they wish to sweep the HTLC after an absolute refund timeout. This
// time out clause prevents the pending funds from being locked up
// indefinately.
builder.AddOp(txscript.OP_ELSE)
builder.AddInt64(int64(absoluteTimeout))
builder.AddOp(txscript.OP_CHECKLOCKTIMEVERIFY)
builder.AddOp(txscript.OP_DROP)
builder.AddOp(txscript.OP_ENDIF)
// In either case, we also require a valid signature with the sender's
// commitment private key.
builder.AddData(senderKey.SerializeCompressed())
builder.AddOp(txscript.OP_ENDIF)
builder.AddOp(txscript.OP_CHECKSIG)
builder.AddOp(txscript.OP_ENDIF)
return builder.Script()
}
// receiverHtlcSpendRedeem constructs a valid witness allowing the receiver of
// an HTLC to redeem the conditional payment in the event that their commitment
// transaction is broadcast. Since this is a pay out to the receiving party as
// an output on their commitment transaction, a relative time delay is required
// before the output can be spent.
func receiverHtlcSpendRedeem(commitScript []byte, outputAmt btcutil.Amount,
reciverKey *btcec.PrivateKey, sweepTx *wire.MsgTx,
paymentPreimage []byte, relativeTimeout uint32) (wire.TxWitness, error) {
// In order to properly spend the transaction, we need to set the
// sequence number. We do this by convering the relative block delay
// into a sequence number value able to be interpeted by
// OP_CHECKSEQUENCEVERIFY.
sweepTx.TxIn[0].Sequence = lockTimeToSequence(false, relativeTimeout)
// Additionally, OP_CSV requires that the version of the transaction
// spending a pkscript with OP_CSV within it *must* be >= 2.
sweepTx.Version = 2
hashCache := txscript.NewTxSigHashes(sweepTx)
sweepSig, err := txscript.RawTxInWitnessSignature(
sweepTx, hashCache, 0, int64(outputAmt), commitScript,
txscript.SigHashAll, reciverKey)
if err != nil {
return nil, err
}
// Place a one as the first item in the evaluated witness stack to
// force script execution to the HTLC redemption clause.
witnessStack := wire.TxWitness(make([][]byte, 4))
witnessStack[0] = sweepSig
witnessStack[1] = paymentPreimage
witnessStack[2] = []byte{1}
witnessStack[3] = commitScript
return witnessStack, nil
}
// receiverHtlcSpendRevoke constructs a valid witness allowing the sender of an
// HTLC within a previously revoked commitment transaction to re-claim the
// pending funds in the case that the receiver broadcasts this revoked
// commitment transaction.
func receiverHtlcSpendRevoke(commitScript []byte, outputAmt btcutil.Amount,
senderKey *btcec.PrivateKey, sweepTx *wire.MsgTx,
revokePreimage []byte) (wire.TxWitness, error) {
// TODO(roasbeef): move sig generate outside func, or just factor out?
hashCache := txscript.NewTxSigHashes(sweepTx)
sweepSig, err := txscript.RawTxInWitnessSignature(
sweepTx, hashCache, 0, int64(outputAmt), commitScript,
txscript.SigHashAll, senderKey)
if err != nil {
return nil, err
}
// We place a zero, then one as the first items in the evaluated
// witness stack in order to force script execution to the HTLC
// revocation clause.
witnessStack := wire.TxWitness(make([][]byte, 5))
witnessStack[0] = sweepSig
witnessStack[1] = revokePreimage
witnessStack[2] = []byte{1}
witnessStack[3] = []byte{0}
witnessStack[4] = commitScript
return witnessStack, nil
}
// receiverHtlcSpendTimeout constructs a valid witness allowing the sender of
// an HTLC to recover the pending funds after an absolute timeout in the
// scenario that the receiver of the HTLC broadcasts their version of the
// commitment transaction.
func receiverHtlcSpendTimeout(commitScript []byte, outputAmt btcutil.Amount,
senderKey *btcec.PrivateKey, sweepTx *wire.MsgTx,
absoluteTimeout uint32) (wire.TxWitness, error) {
// The HTLC output has an absolute time period before we are permitted
// to recover the pending funds. Therefore we need to set the locktime
// on this sweeping transaction in order to pass Script verification.
sweepTx.LockTime = absoluteTimeout
hashCache := txscript.NewTxSigHashes(sweepTx)
sweepSig, err := txscript.RawTxInWitnessSignature(
sweepTx, hashCache, 0, int64(outputAmt), commitScript,
txscript.SigHashAll, senderKey)
if err != nil {
return nil, err
}
witnessStack := wire.TxWitness(make([][]byte, 4))
witnessStack[0] = sweepSig
witnessStack[1] = []byte{0}
witnessStack[2] = []byte{0}
witnessStack[3] = commitScript
return witnessStack, nil
}
// lockTimeToSequence converts the passed relative locktime to a sequence
// number in accordance to BIP-68.
// See: https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki
@ -238,44 +534,199 @@ func lockTimeToSequence(isSeconds bool, locktime uint32) uint32 {
// commitment transaction paying to the "owner" of said commitment transaction.
// If the other party learns of the pre-image to the revocation hash, then they
// can claim all the settled funds in the channel, plus the unsettled funds.
func commitScriptToSelf(csvTimeout uint32, selfKey, theirKey *btcec.PublicKey, revokeHash []byte) ([]byte, error) {
//
// Possible Input Scripts:
// REVOKE: <sig> 1
// SENDRSWEEP: <sig> 0
//
// Output Script:
// OP_IF
// <revokeKey> OP_CHECKSIG
// OP_ELSE
// <timeKey> OP_CHECKSIGVERIFY
// <numRelativeBlocks> OP_CHECKSEQUENCEVERIFY
// OP_ENDIF
func commitScriptToSelf(csvTimeout uint32, selfKey, revokeKey *btcec.PublicKey) ([]byte, error) {
// This script is spendable under two conditions: either the 'csvTimeout'
// has passed and we can redeem our funds, or they have the pre-image
// to 'revokeHash'.
// has passed and we can redeem our funds, or they can produce a valid
// signature with the revocation public key. The revocation public key
// will *only* be known to the other party if we have divulged the
// revocation hash, allowing them to homomorphically derive the proper
// private key which coresponds to the revoke public key.
builder := txscript.NewScriptBuilder()
// If the pre-image for the revocation hash is presented, then allow a
// spend provided the proper signature.
builder.AddOp(txscript.OP_HASH160)
builder.AddData(revokeHash)
builder.AddOp(txscript.OP_EQUAL)
builder.AddOp(txscript.OP_IF)
builder.AddData(theirKey.SerializeCompressed())
// If a valid signature using the revocation key is presented, then
// allow an immediate spend provided the proper signature.
builder.AddData(revokeKey.SerializeCompressed())
builder.AddOp(txscript.OP_CHECKSIG)
builder.AddOp(txscript.OP_ELSE)
// Otherwise, we can re-claim our funds after a CSV delay of
// 'csvTimeout' timeout blocks, and a valid signature.
builder.AddData(selfKey.SerializeCompressed())
builder.AddOp(txscript.OP_CHECKSIGVERIFY)
builder.AddInt64(int64(csvTimeout))
builder.AddOp(OP_CHECKSEQUENCEVERIFY)
builder.AddOp(txscript.OP_DROP)
builder.AddData(selfKey.SerializeCompressed())
builder.AddOp(txscript.OP_ENDIF)
builder.AddOp(txscript.OP_CHECKSIG)
return builder.Script()
}
// commitScriptUnencumbered constructs the public key script on the commitment
// transaction paying to the "other" party. This output is spendable
// immediately, requiring no contestation period.
// transaction paying to the "other" party. The constructed output is a normal
// p2wkh output spendable immediately, requiring no contestation period.
func commitScriptUnencumbered(key *btcec.PublicKey) ([]byte, error) {
// This script goes to the "other" party, and it spendable immediately.
builder := txscript.NewScriptBuilder()
builder.AddOp(txscript.OP_DUP)
builder.AddOp(txscript.OP_HASH160)
builder.AddOp(txscript.OP_0)
builder.AddData(btcutil.Hash160(key.SerializeCompressed()))
builder.AddOp(txscript.OP_EQUALVERIFY)
builder.AddOp(txscript.OP_CHECKSIG)
return builder.Script()
}
// commitSpendTimeout constructs a valid witness allowing the owner of a
// particular commitment transaction to spend the output returning settled
// funds back to themselves after an absolute block timeout.
func commitSpendTimeout(commitScript []byte, outputAmt btcutil.Amount,
blockTimeout uint32, selfKey *btcec.PrivateKey,
sweepTx *wire.MsgTx) (wire.TxWitness, error) {
// In order to properly spend the transaction, we need to set the
// sequence number. We do this by convering the relative block delay
// into a sequence number value able to be interpeted by
// OP_CHECKSEQUENCEVERIFY.
sweepTx.TxIn[0].Sequence = lockTimeToSequence(false, blockTimeout)
// Additionally, OP_CSV requires that the version of the transaction
// spending a pkscript with OP_CSV within it *must* be >= 2.
sweepTx.Version = 2
// With the sequence number in place, we're now able to properly sign
// off on the sweep transaction.
hashCache := txscript.NewTxSigHashes(sweepTx)
sweepSig, err := txscript.RawTxInWitnessSignature(
sweepTx, hashCache, 0, int64(outputAmt), commitScript,
txscript.SigHashAll, selfKey)
if err != nil {
return nil, err
}
// Place a zero as the first item in the evaluated witness stack to
// force script execution to the timeout spend clause.
witnessStack := wire.TxWitness(make([][]byte, 3))
witnessStack[0] = sweepSig
witnessStack[1] = []byte{0}
witnessStack[2] = commitScript
return witnessStack, nil
}
// commitSpendRevoke constructs a valid witness allowing a node to sweep the
// settled output of a malicious counter-party who broadcasts a revoked
// commitment trransaction.
func commitSpendRevoke(commitScript []byte, outputAmt btcutil.Amount,
revocationPriv *btcec.PrivateKey, sweepTx *wire.MsgTx) (wire.TxWitness, error) {
hashCache := txscript.NewTxSigHashes(sweepTx)
sweepSig, err := txscript.RawTxInWitnessSignature(
sweepTx, hashCache, 0, int64(outputAmt), commitScript,
txscript.SigHashAll, revocationPriv)
if err != nil {
return nil, err
}
// Place a 1 as the first item in the evaluated witness stack to
// force script execution to the revocation clause.
witnessStack := wire.TxWitness(make([][]byte, 3))
witnessStack[0] = sweepSig
witnessStack[1] = []byte{1}
witnessStack[2] = commitScript
return witnessStack, nil
}
// commitSpendNoDelay constructs a valid witness allowing a node to spend their
// settled no-delay output on the counter-party's commitment transaction.
func commitSpendNoDelay(commitScript []byte, outputAmt btcutil.Amount,
commitPriv *btcec.PrivateKey, sweepTx *wire.MsgTx) (wire.TxWitness, error) {
// This is just a regular p2wkh spend which looks something like:
// * witness: <sig> <pubkey>
hashCache := txscript.NewTxSigHashes(sweepTx)
witness, err := txscript.WitnessScript(sweepTx, hashCache, 0,
int64(outputAmt), commitScript, txscript.SigHashAll,
commitPriv, true)
if err != nil {
return nil, err
}
return wire.TxWitness(witness), nil
}
// deriveRevocationPubkey derives the revocation public key given the
// counter-party's commitment key, and revocation pre-image derived via a
// pseudo-random-function. In the event that we (for some reason) broadcast a
// revoked commitment transaction, then if the other party knows the revocation
// pre-image, then they'll be able to derive the corresponding private key to
// this private key by exploting the homomorphism in the elliptic curve group:
// * https://en.wikipedia.org/wiki/Group_homomorphism#Homomorphisms_of_abelian_groups
//
// The derivation is performed as follows:
//
// revokeKey := commitKey + revokePoint
// := G*k + G+h
// := G * (k+h)
//
// Therefore, once we divulge the revocation pre-image, the remote peer is able to
// compute the proper private key for the revokeKey by computing:
// revokePriv := commitPriv + revokePreimge mod N
//
// Where N is the order of the sub-group.
func deriveRevocationPubkey(commitPubKey *btcec.PublicKey,
revokePreimage []byte) *btcec.PublicKey {
// First we need to convert the revocation hash into a point on the
// elliptic curve.
revokePointX, revokePointY := btcec.S256().ScalarBaseMult(revokePreimage)
// Now that we have the revocation point, we add this to their commitment
// public key in order to obtain the revocation public key.
revokeX, revokeY := btcec.S256().Add(commitPubKey.X, commitPubKey.Y,
revokePointX, revokePointY)
return &btcec.PublicKey{X: revokeX, Y: revokeY}
}
// deriveRevocationPrivKey derives the revocation private key given a node's
// commitment private key, and the pre-image to a previously seen revocation
// hash. Using this derived private key, a node is able to claim the output
// within the commitment transaction of a node in the case that they broadcast
// a previously revoked commitment transaction.
//
// The private key is derived as follwos:
// revokePriv := commitPriv + revokePrimge mod N
//
// Where N is the order of the sub-group.
func deriveRevocationPrivKey(commitPrivKey *btcec.PrivateKey,
revokePreimage []byte) *btcec.PrivateKey {
// Convert the revocation pre-image into a scalar value so we can
// manipulate it within the curve's defined finite field.
revokeScalar := new(big.Int).SetBytes(revokePreimage)
// To derive the revocation private key, we simply add the revocation
// pre-image to the commitment private key.
//
// This works since:
// P = G*a + G*b
// = G*(a+b)
// = G*p
revokePriv := revokeScalar.Add(revokeScalar, commitPrivKey.D)
revokePriv = revokePriv.Mod(revokePriv, btcec.S256().N)
privRevoke, _ := btcec.PrivKeyFromBytes(btcec.S256(), revokePriv.Bytes())
return privRevoke
}

@ -0,0 +1,512 @@
package lnwallet
import (
"bytes"
"fmt"
"testing"
"github.com/btcsuite/fastsha256"
"github.com/roasbeef/btcd/btcec"
"github.com/roasbeef/btcd/txscript"
"github.com/roasbeef/btcd/wire"
"github.com/roasbeef/btcutil"
)
// TestCommitmentSpendValidation test the spendability of both outputs within
// the commitment transaction.
//
// The following spending cases are covered by this test:
// * Alice's spend from the delayed output on her commitment transaciton.
// * Bob's spend from Alice's delayed output when she broadcasts a revoked
// commitment transaction.
// * Bob's spend from his unencumbered output within Alice's commitment
// transaction.
func TestCommitmentSpendValidation(t *testing.T) {
// We generate a fake output, and the coresponding txin. This output
// doesn't need to exist, as we'll only be validating spending from the
// transaction that references this.
fundingOut := &wire.OutPoint{
Hash: testHdSeed,
Index: 50,
}
fakeFundingTxIn := wire.NewTxIn(fundingOut, nil, nil)
// We also set up set some resources for the commitment transaction.
// Each side currently has 1 BTC within the channel, with a total
// channel capacity of 2BTC.
aliceKeyPriv, aliceKeyPub := btcec.PrivKeyFromBytes(btcec.S256(),
testWalletPrivKey)
bobKeyPriv, bobKeyPub := btcec.PrivKeyFromBytes(btcec.S256(),
bobsPrivKey)
channelBalance := btcutil.Amount(1 * 10e8)
csvTimeout := uint32(5)
revocationHash := testHdSeed[:]
// With all the test data set up, we create the commitment transaction.
// We only focus on a single party's transactions, as the scripts are
// identical with the roles reversed.
//
// This is Alice's commitment transaction, so she must wait a CSV delay
// of 5 blocks before sweeping the output, while bob can spend
// immediately with either the revocation key, or his regular key.
commitmentTx, err := createCommitTx(fakeFundingTxIn, aliceKeyPub,
bobKeyPub, revocationHash, csvTimeout, channelBalance, channelBalance)
if err != nil {
t.Fatalf("unable to create commitment transaction: %v", nil)
}
delayOutput := commitmentTx.TxOut[0]
regularOutput := commitmentTx.TxOut[1]
// We're testing an uncooperative close, output sweep, so construct a
// transaction which sweeps the funds to a random address.
targetOutput, err := commitScriptUnencumbered(aliceKeyPub)
if err != nil {
t.Fatalf("unable to create target output: %v")
}
sweepTx := wire.NewMsgTx()
sweepTx.AddTxIn(wire.NewTxIn(&wire.OutPoint{commitmentTx.TxSha(), 0}, nil, nil))
sweepTx.AddTxOut(&wire.TxOut{
PkScript: targetOutput,
Value: 0.5 * 10e8,
})
// First, we'll test spending with Alice's key after the timeout.
revokePubKey := deriveRevocationPubkey(bobKeyPub, revocationHash)
delayScript, err := commitScriptToSelf(csvTimeout, aliceKeyPub, revokePubKey)
if err != nil {
t.Fatalf("unable to generate alice delay script: %v")
}
aliceWitnessSpend, err := commitSpendTimeout(delayScript, channelBalance,
csvTimeout, aliceKeyPriv, sweepTx)
if err != nil {
t.Fatalf("unable to generate delay commit spend witness :%v")
}
sweepTx.TxIn[0].Witness = aliceWitnessSpend
vm, err := txscript.NewEngine(delayOutput.PkScript,
sweepTx, 0, txscript.StandardVerifyFlags, nil,
nil, int64(channelBalance))
if err != nil {
t.Fatalf("unable to create engine: %v", err)
}
if err := vm.Execute(); err != nil {
t.Fatalf("spend from delay output is invalid: %v", err)
}
// Next, we'll test bob spending with the derived revocation key to
// simulate the scenario when alice broadcasts this commitmen
// transaction after it's been revoked.
revokePrivKey := deriveRevocationPrivKey(bobKeyPriv, revocationHash)
bobWitnessSpend, err := commitSpendRevoke(delayScript, channelBalance,
revokePrivKey, sweepTx)
if err != nil {
t.Fatalf("unable to generate revocation witness: %v", err)
}
sweepTx.TxIn[0].Witness = bobWitnessSpend
vm, err = txscript.NewEngine(delayOutput.PkScript,
sweepTx, 0, txscript.StandardVerifyFlags, nil,
nil, int64(channelBalance))
if err != nil {
t.Fatalf("unable to create engine: %v", err)
}
if err := vm.Execute(); err != nil {
t.Fatalf("revocation spend is invalid: %v", err)
}
// Finally, we test bob sweeping his output as normal in the case that
// alice broadcasts this commitment transaction.
bobScriptp2wkh, err := commitScriptUnencumbered(bobKeyPub)
if err != nil {
t.Fatalf("unable to create bob p2wkh script: %v", err)
}
bobRegularSpend, err := commitSpendNoDelay(bobScriptp2wkh,
channelBalance, bobKeyPriv, sweepTx)
if err != nil {
t.Fatalf("unable to create bob regular spend: %v", err)
}
sweepTx.TxIn[0].Witness = bobRegularSpend
vm, err = txscript.NewEngine(regularOutput.PkScript,
sweepTx, 0, txscript.StandardVerifyFlags, nil,
nil, int64(channelBalance))
if err != nil {
t.Fatalf("unable to create engine: %v", err)
}
if err := vm.Execute(); err != nil {
t.Fatalf("bob p2wkh spend is invalid: %v", err)
}
}
// TestRevocationKeyDerivation tests that given a public key, and a revocation
// hash, the homomorphic revocation public and private key derivation work
// properly.
func TestRevocationKeyDerivation(t *testing.T) {
revocationPreimage := testHdSeed[:]
priv, pub := btcec.PrivKeyFromBytes(btcec.S256(), testWalletPrivKey)
revocationPub := deriveRevocationPubkey(pub, revocationPreimage)
revocationPriv := deriveRevocationPrivKey(priv, revocationPreimage)
x, y := btcec.S256().ScalarBaseMult(revocationPriv.D.Bytes())
derivedRevPub := &btcec.PublicKey{
Curve: btcec.S256(),
X: x,
Y: y,
}
// The the revocation public key derived from the original public key,
// and the one derived from the private key should be identical.
if !revocationPub.IsEqual(derivedRevPub) {
t.Fatalf("derived public keys don't match!")
}
}
func makeWitnessTestCase(t *testing.T, f func() (wire.TxWitness, error)) func() wire.TxWitness {
return func() wire.TxWitness {
witness, err := f()
if err != nil {
t.Fatalf("unable to create witness test case: %v", err)
}
return witness
}
}
// TestHTLCSenderSpendValidation tests all possible valid+invalid redemption
// paths in the script used within the sender's commitment transaction for an
// outgoing HTLC.
//
// The following cases are exercised by this test:
// sender script:
// * reciever spends
// * revoke w/ sig
// * HTLC with invalid pre-image size
// * HTLC with valid pre-image size + sig
// * sender spends
// * invalid lock-time for CLTV
// * invalid sequence for CSV
// * valid lock-time+sequence, valid sig
func TestHTLCSenderSpendValidation(t *testing.T) {
// We generate a fake output, and the coresponding txin. This output
// doesn't need to exist, as we'll only be validating spending from the
// transaction that references this.
fundingOut := &wire.OutPoint{
Hash: testHdSeed,
Index: 50,
}
fakeFundingTxIn := wire.NewTxIn(fundingOut, nil, nil)
revokePreimage := testHdSeed[:]
revokeHash := fastsha256.Sum256(revokePreimage)
paymentPreimage := revokeHash
paymentPreimage[0] ^= 1
paymentHash := fastsha256.Sum256(paymentPreimage[:])
aliceKeyPriv, aliceKeyPub := btcec.PrivKeyFromBytes(btcec.S256(),
testWalletPrivKey)
bobKeyPriv, bobKeyPub := btcec.PrivKeyFromBytes(btcec.S256(),
bobsPrivKey)
paymentAmt := btcutil.Amount(1 * 10e8)
cltvTimeout := uint32(8)
csvTimeout := uint32(5)
htlcScript, err := senderHTLCScript(cltvTimeout, csvTimeout,
aliceKeyPub, bobKeyPub, revokeHash[:], paymentHash[:])
if err != nil {
t.Fatalf("unable to create htlc sender script: %v", err)
}
htlcWitnessScript, err := witnessScriptHash(htlcScript)
if err != nil {
t.Fatalf("unable to create p2wsh htlc script: %v", err)
}
// This will be Alice's commitment transaction. In this scenario Alice
// is sending an HTLC to a node she has a a path to (could be Bob,
// could be multiple hops down, it doesn't really matter).
senderCommitTx := wire.NewMsgTx()
senderCommitTx.AddTxIn(fakeFundingTxIn)
senderCommitTx.AddTxOut(&wire.TxOut{
Value: int64(paymentAmt),
PkScript: htlcWitnessScript,
})
prevOut := &wire.OutPoint{
Hash: senderCommitTx.TxSha(),
Index: 0,
}
sweepTx := wire.NewMsgTx()
sweepTx.AddTxIn(wire.NewTxIn(prevOut, nil, nil))
sweepTx.AddTxOut(
&wire.TxOut{
PkScript: []byte("doesn't matter"),
Value: 1 * 10e8,
},
)
testCases := []struct {
witness func() wire.TxWitness
valid bool
}{
{
// revoke w/ sig
// TODO(roasbeef): test invalid revoke
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
return senderHtlcSpendRevoke(htlcScript, paymentAmt,
bobKeyPriv, sweepTx,
revokePreimage)
}),
true,
},
{
// HTLC with invalid pre-image size
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
return senderHtlcSpendRedeem(htlcScript, paymentAmt,
bobKeyPriv, sweepTx,
// Invalid pre-image length
bytes.Repeat([]byte{1}, 45))
}),
false,
},
{
// HTLC with valid pre-image size + sig
// TODO(roabeef): invalid pre-image
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
return senderHtlcSpendRedeem(htlcScript, paymentAmt,
bobKeyPriv, sweepTx,
paymentPreimage[:])
}),
true,
},
{
// invalid lock-time for CLTV
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
return senderHtlcSpendTimeout(htlcScript, paymentAmt,
aliceKeyPriv, sweepTx, cltvTimeout-2, csvTimeout)
}),
false,
},
{
// invalid sequence for CSV
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
return senderHtlcSpendTimeout(htlcScript, paymentAmt,
aliceKeyPriv, sweepTx, cltvTimeout, csvTimeout-2)
}),
false,
},
{
// valid lock-time+sequence, valid sig
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
return senderHtlcSpendTimeout(htlcScript, paymentAmt,
aliceKeyPriv, sweepTx, cltvTimeout, csvTimeout)
}),
true,
},
}
for i, testCase := range testCases {
sweepTx.TxIn[0].Witness = testCase.witness()
vm, err := txscript.NewEngine(htlcWitnessScript,
sweepTx, 0, txscript.StandardVerifyFlags, nil,
nil, int64(paymentAmt))
if err != nil {
t.Fatalf("unable to create engine: %v", err)
}
var debugBuf bytes.Buffer
done := false
for !done {
dis, err := vm.DisasmPC()
if err != nil {
t.Fatalf("stepping (%v)\n", err)
}
debugBuf.WriteString(fmt.Sprintf("stepping %v\n", dis))
done, err = vm.Step()
if err != nil && testCase.valid {
fmt.Println(debugBuf.String())
t.Fatalf("spend test case #%v failed, spend should be valid: %v", i, err)
} else if err == nil && !testCase.valid && done {
fmt.Println(debugBuf.String())
t.Fatalf("spend test case #%v succeed, spend should be invalid: %v", i, err)
}
debugBuf.WriteString(fmt.Sprintf("Stack: ", vm.GetStack()))
debugBuf.WriteString(fmt.Sprintf("AltStack: ", vm.GetAltStack()))
}
}
}
// TestHTLCReceiverSpendValidation tests all possible valid+invalid redemption
// paths in the script used within the reciever's commitment transaction for an
// incoming HTLC.
//
// The following cases are exercised by this test:
// * reciever spends
// * HTLC redemption w/ invalid preimage size
// * HTLC redemption w/ invalid sequence
// * HTLC redemption w/ valid preimage size
// * sender spends
// * revoke w/ sig
// * refund w/ invalid lock time
// * refund w/ valid lock time
func TestHTLCReceiverSpendValidation(t *testing.T) {
// We generate a fake output, and the coresponding txin. This output
// doesn't need to exist, as we'll only be validating spending from the
// transaction that references this.
fundingOut := &wire.OutPoint{
Hash: testHdSeed,
Index: 50,
}
fakeFundingTxIn := wire.NewTxIn(fundingOut, nil, nil)
revokePreimage := testHdSeed[:]
revokeHash := fastsha256.Sum256(revokePreimage)
paymentPreimage := revokeHash
paymentPreimage[0] ^= 1
paymentHash := fastsha256.Sum256(paymentPreimage[:])
aliceKeyPriv, aliceKeyPub := btcec.PrivKeyFromBytes(btcec.S256(),
testWalletPrivKey)
bobKeyPriv, bobKeyPub := btcec.PrivKeyFromBytes(btcec.S256(),
bobsPrivKey)
paymentAmt := btcutil.Amount(1 * 10e8)
cltvTimeout := uint32(8)
csvTimeout := uint32(5)
htlcScript, err := receiverHTLCScript(cltvTimeout, csvTimeout,
aliceKeyPub, bobKeyPub, revokeHash[:], paymentHash[:])
if err != nil {
t.Fatalf("unable to create htlc sender script: %v", err)
}
htlcWitnessScript, err := witnessScriptHash(htlcScript)
if err != nil {
t.Fatalf("unable to create p2wsh htlc script: %v", err)
}
// This will be Bob's commitment transaction. In this scenario Alice
// is sending an HTLC to a node she has a a path to (could be Bob,
// could be multiple hops down, it doesn't really matter).
recieverCommitTx := wire.NewMsgTx()
recieverCommitTx.AddTxIn(fakeFundingTxIn)
recieverCommitTx.AddTxOut(&wire.TxOut{
Value: int64(paymentAmt),
PkScript: htlcWitnessScript,
})
prevOut := &wire.OutPoint{
Hash: recieverCommitTx.TxSha(),
Index: 0,
}
sweepTx := wire.NewMsgTx()
sweepTx.AddTxIn(wire.NewTxIn(prevOut, nil, nil))
sweepTx.AddTxOut(
&wire.TxOut{
PkScript: []byte("doesn't matter"),
Value: 1 * 10e8,
},
)
testCases := []struct {
witness func() wire.TxWitness
valid bool
}{
{
// HTLC redemption w/ invalid preimage size
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
return receiverHtlcSpendRedeem(htlcScript,
paymentAmt, bobKeyPriv, sweepTx,
bytes.Repeat([]byte{1}, 45), csvTimeout,
)
}),
false,
},
{
// HTLC redemption w/ invalid sequence
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
return receiverHtlcSpendRedeem(htlcScript,
paymentAmt, bobKeyPriv, sweepTx,
paymentPreimage[:], csvTimeout-2,
)
}),
false,
},
{
// HTLC redemption w/ valid preimage size
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
return receiverHtlcSpendRedeem(htlcScript,
paymentAmt, bobKeyPriv, sweepTx,
paymentPreimage[:], csvTimeout,
)
}),
true,
},
{
// revoke w/ sig
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
return receiverHtlcSpendRevoke(htlcScript, paymentAmt,
aliceKeyPriv, sweepTx, revokePreimage[:],
)
}),
true,
},
{
// refund w/ invalid lock time
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
return receiverHtlcSpendTimeout(htlcScript, paymentAmt,
aliceKeyPriv, sweepTx, cltvTimeout-2)
}),
false,
},
{
// refund w/ valid lock time
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
return receiverHtlcSpendTimeout(htlcScript, paymentAmt,
aliceKeyPriv, sweepTx, cltvTimeout)
}),
true,
},
}
for i, testCase := range testCases {
sweepTx.TxIn[0].Witness = testCase.witness()
vm, err := txscript.NewEngine(htlcWitnessScript,
sweepTx, 0, txscript.StandardVerifyFlags, nil,
nil, int64(paymentAmt))
if err != nil {
t.Fatalf("unable to create engine: %v", err)
}
var debugBuf bytes.Buffer
done := false
for !done {
dis, err := vm.DisasmPC()
if err != nil {
t.Fatalf("stepping (%v)\n", err)
}
debugBuf.WriteString(fmt.Sprintf("stepping %v\n", dis))
done, err = vm.Step()
if err != nil && testCase.valid {
fmt.Println(debugBuf.String())
t.Fatalf("spend test case #%v failed, spend should be valid: %v", i, err)
} else if err == nil && !testCase.valid && done {
fmt.Println(debugBuf.String())
t.Fatalf("spend test case #%v succeed, spend should be invalid: %v", i, err)
}
debugBuf.WriteString(fmt.Sprintf("Stack: ", vm.GetStack()))
debugBuf.WriteString(fmt.Sprintf("AltStack: ", vm.GetAltStack()))
}
}
}