Skip to content

Commit

Permalink
Implement anchor outputs HTLC transactions
Browse files Browse the repository at this point in the history
Add support for HTLC transactions with a 1-block relative delay.
Add missing anchor outputs spec tests.
Add missing tests for some revoked paths.
  • Loading branch information
t-bast committed Jul 15, 2020
1 parent e177b53 commit 0ea4378
Show file tree
Hide file tree
Showing 9 changed files with 432 additions and 141 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ object Commitments {
// combine the sigs to make signed txes
val htlcTxsAndSigs = (sortedHtlcTxs, htlcSigs, commit.htlcSignatures).zipped.toList.collect {
case (htlcTx: HtlcTimeoutTx, localSig, remoteSig) =>
if (Transactions.checkSpendable(Transactions.addSigs(htlcTx, localSig, remoteSig)).isFailure) {
if (Transactions.checkSpendable(Transactions.addSigs(htlcTx, localSig, remoteSig, commitmentFormat)).isFailure) {
throw InvalidHtlcSignature(commitments.channelId, htlcTx.tx)
}
HtlcTxAndSigs(htlcTx, localSig, remoteSig)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -545,15 +545,15 @@ object Helpers {
case HtlcTxAndSigs(txinfo@HtlcSuccessTx(_, _, paymentHash), localSig, remoteSig) if preimages.exists(r => sha256(r) == paymentHash) =>
generateTx("htlc-success") {
val preimage = preimages.find(r => sha256(r) == paymentHash).get
Right(Transactions.addSigs(txinfo, localSig, remoteSig, preimage))
Right(Transactions.addSigs(txinfo, localSig, remoteSig, preimage, commitmentFormat))
}

// (incoming htlc for which we don't have the preimage: nothing to do, it will timeout eventually and they will get their funds back)

// outgoing htlc: they may or may not have the preimage, the only thing to do is try to get back our funds after timeout
case HtlcTxAndSigs(txinfo: HtlcTimeoutTx, localSig, remoteSig) =>
generateTx("htlc-timeout") {
Right(Transactions.addSigs(txinfo, localSig, remoteSig))
Right(Transactions.addSigs(txinfo, localSig, remoteSig, commitmentFormat))
}
}.flatten

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ object Scripts {
* @param sig raw ECDSA signature (r,s)
* @param sighash sighash flags
*/
def der(sig: ByteVector64, sighash: Byte = SIGHASH_ALL.toByte): ByteVector = Crypto.compact2der(sig) :+ sighash
def der(sig: ByteVector64, sighash: Int = SIGHASH_ALL): ByteVector = Crypto.compact2der(sig) :+ sighash.toByte

def htlcRemoteSighash(commitmentFormat: CommitmentFormat): Int = commitmentFormat match {
case DefaultCommitmentFormat => SIGHASH_ALL
case AnchorOutputsCommitmentFormat => SIGHASH_SINGLE | SIGHASH_ANYONECANPAY
}

def multiSig2of2(pubkey1: PublicKey, pubkey2: PublicKey): Seq[ScriptElt] =
if (LexicographicalOrdering.isLessThan(pubkey1.value, pubkey2.value)) {
Expand Down Expand Up @@ -200,8 +205,8 @@ object Scripts {
/**
* This is the witness script of the 2nd-stage HTLC Success transaction (consumes htlcOffered script from commit tx)
*/
def witnessHtlcSuccess(localSig: ByteVector64, remoteSig: ByteVector64, paymentPreimage: ByteVector32, htlcOfferedScript: ByteVector) =
ScriptWitness(ByteVector.empty :: der(remoteSig) :: der(localSig) :: paymentPreimage.bytes :: htlcOfferedScript :: Nil)
def witnessHtlcSuccess(localSig: ByteVector64, remoteSig: ByteVector64, paymentPreimage: ByteVector32, htlcOfferedScript: ByteVector, commitmentFormat: CommitmentFormat) =
ScriptWitness(ByteVector.empty :: der(remoteSig, htlcRemoteSighash(commitmentFormat)) :: der(localSig) :: paymentPreimage.bytes :: htlcOfferedScript :: Nil)

/** Extract the payment preimage from a 2nd-stage HTLC Success transaction's witness script */
def extractPreimageFromHtlcSuccess: PartialFunction[ScriptWitness, ByteVector32] = {
Expand Down Expand Up @@ -253,8 +258,8 @@ object Scripts {
/**
* This is the witness script of the 2nd-stage HTLC Timeout transaction (consumes htlcOffered script from commit tx)
*/
def witnessHtlcTimeout(localSig: ByteVector64, remoteSig: ByteVector64, htlcOfferedScript: ByteVector) =
ScriptWitness(ByteVector.empty :: der(remoteSig) :: der(localSig) :: ByteVector.empty :: htlcOfferedScript :: Nil)
def witnessHtlcTimeout(localSig: ByteVector64, remoteSig: ByteVector64, htlcOfferedScript: ByteVector, commitmentFormat: CommitmentFormat) =
ScriptWitness(ByteVector.empty :: der(remoteSig, htlcRemoteSighash(commitmentFormat)) :: der(localSig) :: ByteVector.empty :: htlcOfferedScript :: Nil)

/** Extract the payment hash from a 2nd-stage HTLC Timeout transaction's witness script */
def extractPaymentHashFromHtlcTimeout: PartialFunction[ScriptWitness, ByteVector] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,11 @@ object Transactions {

def decodeTxNumber(sequence: Long, locktime: Long): Long = ((sequence & 0xffffffL) << 24) + (locktime & 0xffffffL)

def getHtlcTxInputSequence(commitmentFormat: CommitmentFormat): Long = commitmentFormat match {
case DefaultCommitmentFormat => 0 // htlc txs immediately spend the commit tx
case AnchorOutputsCommitmentFormat => 1 // htlc txs have a 1-block delay to allow CPFP carve-out on anchors
}

/**
* Represent a link between a commitment spec item (to-local, to-remote, anchors, htlc) and the actual output in the commit tx
*
Expand Down Expand Up @@ -374,7 +379,7 @@ object Transactions {
val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript))
Right(HtlcTimeoutTx(input, Transaction(
version = 2,
txIn = TxIn(input.outPoint, ByteVector.empty, 0x00000000L) :: Nil,
txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil,
txOut = TxOut(amount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))) :: Nil,
lockTime = htlc.cltvExpiry.toLong)))
}
Expand All @@ -399,7 +404,7 @@ object Transactions {
val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript))
Right(HtlcSuccessTx(input, Transaction(
version = 2,
txIn = TxIn(input.outPoint, ByteVector.empty, 0x00000000L) :: Nil,
txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil,
txOut = TxOut(amount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))) :: Nil,
lockTime = 0), htlc.paymentHash))
}
Expand Down Expand Up @@ -442,9 +447,13 @@ object Transactions {
} match {
case Some(outputIndex) =>
val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript))
val sequence = commitmentFormat match {
case DefaultCommitmentFormat => 0xffffffffL // RBF disabled
case AnchorOutputsCommitmentFormat => 1 // txs have a 1-block delay to allow CPFP carve-out on anchors
}
val tx = Transaction(
version = 2,
txIn = TxIn(input.outPoint, ByteVector.empty, 0xffffffffL) :: Nil,
txIn = TxIn(input.outPoint, ByteVector.empty, sequence) :: Nil,
txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil,
lockTime = 0)
val weight = addSigs(ClaimHtlcSuccessTx(input, tx), PlaceHolderSig, ByteVector32.Zeroes).tx.weight()
Expand Down Expand Up @@ -479,7 +488,7 @@ object Transactions {
// unsigned tx
val tx = Transaction(
version = 2,
txIn = TxIn(input.outPoint, ByteVector.empty, 0x00000000L) :: Nil,
txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil,
txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil,
lockTime = htlc.cltvExpiry.toLong)
val weight = addSigs(ClaimHtlcTimeoutTx(input, tx), PlaceHolderSig).tx.weight()
Expand Down Expand Up @@ -590,13 +599,13 @@ object Transactions {
}
}

def makeClaimDelayedOutputPenaltyTx(commitTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: Long): Either[TxGenerationSkipped, ClaimDelayedOutputPenaltyTx] = {
def makeClaimDelayedOutputPenaltyTx(htlcTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: Long): Either[TxGenerationSkipped, ClaimDelayedOutputPenaltyTx] = {
val redeemScript = toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)
val pubkeyScript = write(pay2wsh(redeemScript))
findPubKeyScriptIndex(commitTx, pubkeyScript, amount_opt = None) match {
findPubKeyScriptIndex(htlcTx, pubkeyScript, amount_opt = None) match {
case Left(skip) => Left(skip)
case Right(outputIndex) =>
val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript))
val input = InputInfo(OutPoint(htlcTx, outputIndex), htlcTx.txOut(outputIndex), write(redeemScript))
// unsigned transaction
val tx = Transaction(
version = 2,
Expand Down Expand Up @@ -708,15 +717,15 @@ object Transactions {
val PlaceHolderSig = ByteVector64(ByteVector.fill(64)(0xaa))
assert(der(PlaceHolderSig).size == 72)

def sign(tx: Transaction, inputIndex: Int, redeemScript: ByteVector, amount: Satoshi, key: PrivateKey): ByteVector64 = {
val sigDER = Transaction.signInput(tx, inputIndex, redeemScript, SIGHASH_ALL, amount, SIGVERSION_WITNESS_V0, key)
private def sign(tx: Transaction, redeemScript: ByteVector, amount: Satoshi, key: PrivateKey, sighashType: Int): ByteVector64 = {
val sigDER = Transaction.signInput(tx, inputIndex = 0, redeemScript, sighashType, amount, SIGVERSION_WITNESS_V0, key)
val sig64 = Crypto.der2compact(sigDER)
sig64
}

def sign(txinfo: TransactionWithInputInfo, key: PrivateKey): ByteVector64 = {
def sign(txinfo: TransactionWithInputInfo, key: PrivateKey, sighashType: Int = SIGHASH_ALL): ByteVector64 = {
require(txinfo.tx.txIn.lengthCompare(1) == 0, "only one input allowed")
sign(txinfo.tx, inputIndex = 0, txinfo.input.redeemScript, txinfo.input.txOut.amount, key)
sign(txinfo.tx, txinfo.input.redeemScript, txinfo.input.txOut.amount, key, sighashType)
}

def addSigs(commitTx: CommitTx, localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: ByteVector64, remoteSig: ByteVector64): CommitTx = {
Expand All @@ -734,13 +743,13 @@ object Transactions {
htlcPenaltyTx.copy(tx = htlcPenaltyTx.tx.updateWitness(0, witness))
}

def addSigs(htlcSuccessTx: HtlcSuccessTx, localSig: ByteVector64, remoteSig: ByteVector64, paymentPreimage: ByteVector32): HtlcSuccessTx = {
val witness = witnessHtlcSuccess(localSig, remoteSig, paymentPreimage, htlcSuccessTx.input.redeemScript)
def addSigs(htlcSuccessTx: HtlcSuccessTx, localSig: ByteVector64, remoteSig: ByteVector64, paymentPreimage: ByteVector32, commitmentFormat: CommitmentFormat): HtlcSuccessTx = {
val witness = witnessHtlcSuccess(localSig, remoteSig, paymentPreimage, htlcSuccessTx.input.redeemScript, commitmentFormat)
htlcSuccessTx.copy(tx = htlcSuccessTx.tx.updateWitness(0, witness))
}

def addSigs(htlcTimeoutTx: HtlcTimeoutTx, localSig: ByteVector64, remoteSig: ByteVector64): HtlcTimeoutTx = {
val witness = witnessHtlcTimeout(localSig, remoteSig, htlcTimeoutTx.input.redeemScript)
def addSigs(htlcTimeoutTx: HtlcTimeoutTx, localSig: ByteVector64, remoteSig: ByteVector64, commitmentFormat: CommitmentFormat): HtlcTimeoutTx = {
val witness = witnessHtlcTimeout(localSig, remoteSig, htlcTimeoutTx.input.redeemScript, commitmentFormat)
htlcTimeoutTx.copy(tx = htlcTimeoutTx.tx.updateWitness(0, witness))
}

Expand Down
Loading

0 comments on commit 0ea4378

Please sign in to comment.