diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 20af886e..d1cff032 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -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 } diff --git a/lnwallet/script_utils.go b/lnwallet/script_utils.go index 41ccd941..0626daa1 100644 --- a/lnwallet/script_utils.go +++ b/lnwallet/script_utils.go @@ -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: 0 +// RECVR: 0 1 +// REVOK: 1 1 +// * reciever revoke +// +// OP_IF +// //Receiver +// OP_IF +// //Revoke +// +// OP_ELSE +// //Receive +// OP_SIZE 32 OP_EQUALVERIFY +// +// OP_ENDIF +// OP_SWAP +// OP_SHA256 OP_EQUALVERIFY +// OP_CHECKSIG +// OP_ELSE +// //Sender +// OP_CHECKLOCKTIMEVERIFY +// OP_CHECKSEQUENCEVERIFY +// OP_2DROP +// 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: 1 +// REVOK: 1 0 +// SENDR: 0 0 +// +// OP_IF +// //Receiver +// OP_SIZE 32 OP_EQUALVERIFY +// OP_SHA256 +// OP_EQUALVERIFY +// OP_CHECKSEQUENCEVERIFY OP_DROP +// OP_CHECKSIG +// OP_ELSE +// //Sender +// OP_IF +// //Revocation +// OP_SHA256 +// OP_EQUALVERIFY +// OP_ELSE +// //Refund +// OP_CHECKLOCKTIMEVERIFY OP_DROP +// OP_ENDIF +// 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: 1 +// SENDRSWEEP: 0 +// +// Output Script: +// OP_IF +// OP_CHECKSIG +// OP_ELSE +// OP_CHECKSIGVERIFY +// 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: + 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 +} diff --git a/lnwallet/script_utils_test.go b/lnwallet/script_utils_test.go new file mode 100644 index 00000000..03d37bd3 --- /dev/null +++ b/lnwallet/script_utils_test.go @@ -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())) + } + } +}