From d4703fce7d9b53e8c57884255a7a47a6b72b95ac Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 26 Mar 2020 17:31:11 +0100 Subject: [PATCH 1/6] Sort commit transaction outputs using BIP69 + CLTV as tie-breaker for offered HTLCs --- .../bitcoind/BitcoinCoreWallet.scala | 2 +- .../fr/acinq/eclair/channel/Commitments.scala | 26 ++- .../fr/acinq/eclair/channel/Helpers.scala | 26 ++- .../eclair/transactions/CommitmentSpec.scala | 7 +- .../eclair/transactions/Transactions.scala | 165 +++++++++++++----- .../test/resources/bolt3-tx-test-vectors.txt | 20 +++ .../channel/states/e/NormalStateSpec.scala | 2 - .../eclair/transactions/TestVectorsSpec.scala | 50 ++++-- .../transactions/TransactionsSpec.scala | 144 +++++++++++---- 9 files changed, 326 insertions(+), 116 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala index efdd802f69..66174f8983 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala @@ -102,7 +102,7 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC // now let's sign the funding tx SignTransactionResponse(fundingTx, true) <- signTransactionOrUnlock(unsignedFundingTx) // there will probably be a change output, so we need to find which output is ours - outputIndex = Transactions.findPubKeyScriptIndex(fundingTx, pubkeyScript, outputsAlreadyUsed = Set.empty, amount_opt = None) + outputIndex = Transactions.findPubKeyScriptIndex(fundingTx, pubkeyScript, amount_opt = None) _ = logger.debug(s"created funding txid=${fundingTx.txid} outputIndex=$outputIndex fee=$fee") } yield MakeFundingTxResponse(fundingTx, outputIndex, fee) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 922077070d..24ecf56fb4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -571,27 +571,41 @@ object Commitments { } } - def makeLocalTxs(keyManager: KeyManager, channelVersion: ChannelVersion, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, localPerCommitmentPoint: PublicKey, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { + def makeLocalTxs(keyManager: KeyManager, + channelVersion: ChannelVersion, + commitTxNumber: Long, + localParams: LocalParams, + remoteParams: RemoteParams, + commitmentInput: InputInfo, + localPerCommitmentPoint: PublicKey, + spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) val localDelayedPaymentPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, localPerCommitmentPoint) val remotePaymentPubkey = Generators.derivePubKey(remoteParams.paymentBasepoint, localPerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint) val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) - val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, keyManager.paymentPoint(channelKeyPath).publicKey, remoteParams.paymentBasepoint, localParams.isFunder, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec) - val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec) + val outputs = makeCommitTxOutputs(localParams.isFunder, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec) + val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, keyManager.paymentPoint(channelKeyPath).publicKey, remoteParams.paymentBasepoint, localParams.isFunder, outputs) + val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, spec.feeratePerKw, outputs) (commitTx, htlcTimeoutTxs, htlcSuccessTxs) } - def makeRemoteTxs(keyManager: KeyManager, channelVersion: ChannelVersion, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, remotePerCommitmentPoint: PublicKey, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { + def makeRemoteTxs(keyManager: KeyManager, + channelVersion: ChannelVersion, + commitTxNumber: Long, localParams: LocalParams, + remoteParams: RemoteParams, commitmentInput: InputInfo, + remotePerCommitmentPoint: PublicKey, + spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, keyManager.paymentPoint(channelKeyPath).publicKey, !localParams.isFunder, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec) - val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec) + val outputs = makeCommitTxOutputs(!localParams.isFunder, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec) + val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, keyManager.paymentPoint(channelKeyPath).publicKey, !localParams.isFunder, outputs) + val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, spec.feeratePerKw, outputs) (commitTx, htlcTimeoutTxs, htlcSuccessTxs) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 89c05d10ec..c2fa37a5b0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -602,6 +602,9 @@ object Helpers { val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remoteCommit.remotePerCommitmentPoint) val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) + val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remoteCommit.remotePerCommitmentPoint) + val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) + val outputs = makeCommitTxOutputs(!localParams.isFunder, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, remoteCommit.spec) // we need to use a rather high fee for htlc-claim because we compete with the counterparty val feeratePerKwHtlc = feeEstimator.getFeeratePerKw(target = 2) @@ -610,13 +613,11 @@ object Helpers { val preimages = commitments.localChanges.all.collect { case u: UpdateFulfillHtlc => u.paymentPreimage } // remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa - var outputsAlreadyUsed = Set.empty[Int] // this is needed to handle cases where we have several identical htlcs val txes = remoteCommit.spec.htlcs.collect { // incoming htlc for which we have the preimage: we spend it directly case DirectedHtlc(OUT, add: UpdateAddHtlc) if preimages.exists(r => sha256(r) == add.paymentHash) => generateTx("claim-htlc-success")(Try { val preimage = preimages.find(r => sha256(r) == add.paymentHash).get - val txinfo = Transactions.makeClaimHtlcSuccessTx(remoteCommitTx.tx, outputsAlreadyUsed, localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc) - outputsAlreadyUsed = outputsAlreadyUsed + txinfo.input.outPoint.index.toInt + val txinfo = Transactions.makeClaimHtlcSuccessTx(remoteCommitTx.tx, outputs, localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc) val sig = keyManager.sign(txinfo, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint) Transactions.addSigs(txinfo, sig, preimage) }) @@ -625,8 +626,7 @@ object Helpers { // 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 DirectedHtlc(IN, add: UpdateAddHtlc) => generateTx("claim-htlc-timeout")(Try { - val txinfo = Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, outputsAlreadyUsed, localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc) - outputsAlreadyUsed = outputsAlreadyUsed + txinfo.input.outPoint.index.toInt + val txinfo = Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, outputs, localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc) val sig = keyManager.sign(txinfo, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint) Transactions.addSigs(txinfo, sig) }) @@ -697,9 +697,9 @@ object Helpers { val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) + val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) val feeratePerKwMain = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) // we need to use a high fee here for punishment txes because after a delay they can be spent by the counterparty @@ -707,9 +707,9 @@ object Helpers { // first we will claim our main output right away val mainTx = generateTx("claim-p2wpkh-output")(Try { - val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, localParams.dustLimit, localPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain) + val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, localParams.dustLimit, localPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain) val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint) - Transactions.addSigs(claimMain, localPubkey, sig) + Transactions.addSigs(claimMain, localPaymentPubkey, sig) }) // then we punish them by stealing their main output @@ -726,16 +726,14 @@ object Helpers { htlcInfos.map { case (paymentHash, cltvExpiry) => Scripts.htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash), cltvExpiry) } ++ htlcInfos.map { case (paymentHash, _) => Scripts.htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash)) } ) - .map(redeemScript => (Script.write(pay2wsh(redeemScript)) -> Script.write(redeemScript))) + .map(redeemScript => Script.write(pay2wsh(redeemScript)) -> Script.write(redeemScript)) .toMap // and finally we steal the htlc outputs - var outputsAlreadyUsed = Set.empty[Int] // this is needed to handle cases where we have several identical htlcs - val htlcPenaltyTxs = tx.txOut.collect { case txOut if htlcsRedeemScripts.contains(txOut.publicKeyScript) => + val htlcPenaltyTxs = tx.txOut.zipWithIndex.collect { case (txOut, outputIndex) if htlcsRedeemScripts.contains(txOut.publicKeyScript) => val htlcRedeemScript = htlcsRedeemScripts(txOut.publicKeyScript) generateTx("htlc-penalty")(Try { - val htlcPenalty = Transactions.makeHtlcPenaltyTx(tx, outputsAlreadyUsed, htlcRedeemScript, localParams.dustLimit, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty) - outputsAlreadyUsed = outputsAlreadyUsed + htlcPenalty.input.outPoint.index.toInt + val htlcPenalty = Transactions.makeHtlcPenaltyTx(tx, outputIndex, htlcRedeemScript, localParams.dustLimit, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty) val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret) Transactions.addSigs(htlcPenalty, sig, remoteRevocationPubkey) }) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala index cbd4a9f374..d2861baf70 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala @@ -29,7 +29,12 @@ case object IN extends Direction { def opposite = OUT } case object OUT extends Direction { def opposite = IN } // @formatter:on -case class DirectedHtlc(direction: Direction, add: UpdateAddHtlc) +sealed trait CommitmentOutput +object CommitmentOutput { + case object ToLocal extends CommitmentOutput + case object ToRemote extends CommitmentOutput +} +case class DirectedHtlc(direction: Direction, add: UpdateAddHtlc) extends CommitmentOutput final case class CommitmentSpec(htlcs: Set[DirectedHtlc], feeratePerKw: Long, toLocal: MilliSatoshi, toRemote: MilliSatoshi) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index 7bbc08da87..cc477a2131 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -206,7 +206,40 @@ object Transactions { def decodeTxNumber(sequence: Long, locktime: Long): Long = ((sequence & 0xffffffL) << 24) + (locktime & 0xffffffL) - def makeCommitTx(commitTxInput: InputInfo, commitTxNumber: Long, localPaymentBasePoint: PublicKey, remotePaymentBasePoint: PublicKey, localIsFunder: Boolean, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, remotePaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, spec: CommitmentSpec): CommitTx = { + /** + * Represent a link between a commitment spec item (to-local, to-remote, htlc) and the actual output in the commit tx + + * @param output transaction output + * @param redeemScript redeem script that matches this output (most of them are p2wsh) + * @param specItem commitment spec item this output is built from + */ + case class CommitmentOutputLink(output: TxOut, redeemScript: Seq[ScriptElt], specItem: CommitmentOutput) + + /** Type alias for a collection of CommitmentOutputLink */ + type CommitmentOutputs = Seq[CommitmentOutputLink] + + object CommitmentOutputLink { + /** + * We sort HTLC outputs according to BIP69 + CLTV as tie-breaker for offered HTLC, we do this only for the outgoing + * HTLC because we must agree with the remote on the order of HTLC-Timeout transactions even for identical HTLC outputs. + * See https://github.com/lightningnetwork/lightning-rfc/issues/448#issuecomment-432074187. + */ + def sort(a: CommitmentOutputLink, b: CommitmentOutputLink): Boolean = (a.specItem, b.specItem) match { + case (DirectedHtlc(OUT, htlcA), DirectedHtlc(OUT, htlcB)) if htlcA.paymentHash == htlcB.paymentHash && htlcA.amountMsat == htlcB.amountMsat => + htlcA.cltvExpiry <= htlcB.cltvExpiry + case _ => LexicographicalOrdering.isLessThan(a.output, b.output) + } + } + + def makeCommitTxOutputs(localIsFunder: Boolean, + localDustLimit: Satoshi, + localRevocationPubkey: PublicKey, + toLocalDelay: CltvExpiryDelta, + localDelayedPaymentPubkey: PublicKey, + remotePaymentPubkey: PublicKey, + localHtlcPubkey: PublicKey, + remoteHtlcPubkey: PublicKey, + spec: CommitmentSpec): CommitmentOutputs = { val commitFee = commitTxFee(localDustLimit, spec) val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localIsFunder) { @@ -214,31 +247,62 @@ object Transactions { } else { (spec.toLocal.truncateToSatoshi, spec.toRemote.truncateToSatoshi - commitFee) } // NB: we don't care if values are < 0, they will be trimmed if they are < dust limit anyway + val outputs = collection.mutable.ArrayBuffer.empty[CommitmentOutputLink] + + if (toLocalAmount >= localDustLimit) outputs.append( + CommitmentOutputLink( + TxOut(toLocalAmount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))), + toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey), + CommitmentOutput.ToLocal)) + + if (toRemoteAmount >= localDustLimit) outputs.append( + CommitmentOutputLink( + TxOut(toRemoteAmount, pay2wpkh(remotePaymentPubkey)), + pay2pkh(remotePaymentPubkey), + CommitmentOutput.ToRemote)) + + trimOfferedHtlcs(localDustLimit, spec).foreach { htlc => + val redeemScript = htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes)) + outputs.append(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(redeemScript)), redeemScript, htlc)) + } - val toLocalDelayedOutput_opt = if (toLocalAmount >= localDustLimit) Some(TxOut(toLocalAmount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)))) else None - val toRemoteOutput_opt = if (toRemoteAmount >= localDustLimit) Some(TxOut(toRemoteAmount, pay2wpkh(remotePaymentPubkey))) else None + trimReceivedHtlcs(localDustLimit, spec).foreach { htlc => + val redeemScript = htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), htlc.add.cltvExpiry) + outputs.append(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(redeemScript)), redeemScript, htlc)) + } - val htlcOfferedOutputs = trimOfferedHtlcs(localDustLimit, spec) - .map(htlc => TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes))))) - val htlcReceivedOutputs = trimReceivedHtlcs(localDustLimit, spec) - .map(htlc => TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), htlc.add.cltvExpiry)))) + outputs.sortWith(CommitmentOutputLink.sort) + } + def makeCommitTx(commitTxInput: InputInfo, + commitTxNumber: Long, + localPaymentBasePoint: PublicKey, + remotePaymentBasePoint: PublicKey, + localIsFunder: Boolean, + outputs: CommitmentOutputs): CommitTx = { val txnumber = obscuredCommitTxNumber(commitTxNumber, localIsFunder, localPaymentBasePoint, remotePaymentBasePoint) val (sequence, locktime) = encodeTxNumber(txnumber) val tx = Transaction( version = 2, txIn = TxIn(commitTxInput.outPoint, ByteVector.empty, sequence = sequence) :: Nil, - txOut = toLocalDelayedOutput_opt.toSeq ++ toRemoteOutput_opt.toSeq ++ htlcOfferedOutputs ++ htlcReceivedOutputs, + txOut = outputs.map(_.output), lockTime = locktime) - CommitTx(commitTxInput, LexicographicalOrdering.sort(tx)) + + CommitTx(commitTxInput, tx) } - def makeHtlcTimeoutTx(commitTx: Transaction, outputsAlreadyUsed: Set[Int], localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, feeratePerKw: Long, htlc: UpdateAddHtlc): HtlcTimeoutTx = { + def makeHtlcTimeoutTx(commitTx: Transaction, + output: CommitmentOutputLink, + outputIndex: Int, + localDustLimit: Satoshi, + localRevocationPubkey: PublicKey, + toLocalDelay: CltvExpiryDelta, + localDelayedPaymentPubkey: PublicKey, + feeratePerKw: Long): HtlcTimeoutTx = { val fee = weight2fee(feeratePerKw, htlcTimeoutWeight) - val redeemScript = htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.paymentHash.bytes)) - val pubkeyScript = write(pay2wsh(redeemScript)) - val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = Some(htlc.amountMsat.truncateToSatoshi)) + val redeemScript = output.redeemScript + val DirectedHtlc(OUT, htlc) = output.specItem val amount = htlc.amountMsat.truncateToSatoshi - fee if (amount < localDustLimit) { throw AmountBelowDustLimit @@ -251,11 +315,18 @@ object Transactions { lockTime = htlc.cltvExpiry.toLong)) } - def makeHtlcSuccessTx(commitTx: Transaction, outputsAlreadyUsed: Set[Int], localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, feeratePerKw: Long, htlc: UpdateAddHtlc): HtlcSuccessTx = { + def makeHtlcSuccessTx(commitTx: Transaction, + output: CommitmentOutputLink, + outputIndex: Int, + localDustLimit: Satoshi, + localRevocationPubkey: PublicKey, + toLocalDelay: CltvExpiryDelta, + localDelayedPaymentPubkey: PublicKey, + feeratePerKw: Long): HtlcSuccessTx = { val fee = weight2fee(feeratePerKw, htlcSuccessWeight) - val redeemScript = htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.paymentHash.bytes), htlc.cltvExpiry) - val pubkeyScript = write(pay2wsh(redeemScript)) - val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = Some(htlc.amountMsat.truncateToSatoshi)) + val redeemScript = output.redeemScript + val DirectedHtlc(IN, htlc) = output.specItem + val amount = htlc.amountMsat.truncateToSatoshi - fee if (amount < localDustLimit) { throw AmountBelowDustLimit @@ -268,25 +339,32 @@ object Transactions { lockTime = 0), htlc.paymentHash) } - def makeHtlcTxs(commitTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, spec: CommitmentSpec): (Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { - var outputsAlreadyUsed = Set.empty[Int] // this is needed to handle cases where we have several identical htlcs - val htlcTimeoutTxs = trimOfferedHtlcs(localDustLimit, spec).map { htlc => - val htlcTx = makeHtlcTimeoutTx(commitTx, outputsAlreadyUsed, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec.feeratePerKw, htlc.add) - outputsAlreadyUsed = outputsAlreadyUsed + htlcTx.input.outPoint.index.toInt - htlcTx + def makeHtlcTxs(commitTx: Transaction, + localDustLimit: Satoshi, + localRevocationPubkey: PublicKey, + toLocalDelay: CltvExpiryDelta, + localDelayedPaymentPubkey: PublicKey, + feeratePerKw: Long, + outputs: CommitmentOutputs): (Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { + + val htlcTimeoutTxs = outputs.zipWithIndex.collect { + case (co@CommitmentOutputLink(_, _, DirectedHtlc(OUT, _)), outputIndex) => + makeHtlcTimeoutTx(commitTx, co, outputIndex, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, feeratePerKw) } - val htlcSuccessTxs = trimReceivedHtlcs(localDustLimit, spec).map { htlc => - val htlcTx = makeHtlcSuccessTx(commitTx, outputsAlreadyUsed, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec.feeratePerKw, htlc.add) - outputsAlreadyUsed = outputsAlreadyUsed + htlcTx.input.outPoint.index.toInt - htlcTx + val htlcSuccessTxs = outputs.zipWithIndex.collect { + case (co@CommitmentOutputLink(_, _, DirectedHtlc(IN, _)), outputIndex) => + makeHtlcSuccessTx(commitTx, co, outputIndex, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, feeratePerKw) } (htlcTimeoutTxs, htlcSuccessTxs) } - def makeClaimHtlcSuccessTx(commitTx: Transaction, outputsAlreadyUsed: Set[Int], localDustLimit: Satoshi, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: ByteVector, htlc: UpdateAddHtlc, feeratePerKw: Long): ClaimHtlcSuccessTx = { + def makeClaimHtlcSuccessTx(commitTx: Transaction, outputs: CommitmentOutputs, localDustLimit: Satoshi, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: ByteVector, htlc: UpdateAddHtlc, feeratePerKw: Long): ClaimHtlcSuccessTx = { val redeemScript = htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlc.paymentHash.bytes)) - val pubkeyScript = write(pay2wsh(redeemScript)) - val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = Some(htlc.amountMsat.truncateToSatoshi)) + val Some(outputIndex) = outputs.zipWithIndex.find { + case (CommitmentOutputLink(_, _, DirectedHtlc(OUT, outgoingHtlc)), _) => outgoingHtlc.id == htlc.id + case _ => false + }.map(_._2) + val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) val tx = Transaction( @@ -306,10 +384,13 @@ object Transactions { ClaimHtlcSuccessTx(input, tx1) } - def makeClaimHtlcTimeoutTx(commitTx: Transaction, outputsAlreadyUsed: Set[Int], localDustLimit: Satoshi, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: ByteVector, htlc: UpdateAddHtlc, feeratePerKw: Long): ClaimHtlcTimeoutTx = { + def makeClaimHtlcTimeoutTx(commitTx: Transaction, outputs: CommitmentOutputs, localDustLimit: Satoshi, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: ByteVector, htlc: UpdateAddHtlc, feeratePerKw: Long): ClaimHtlcTimeoutTx = { val redeemScript = htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlc.paymentHash.bytes), htlc.cltvExpiry) - val pubkeyScript = write(pay2wsh(redeemScript)) - val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = Some(htlc.amountMsat.truncateToSatoshi)) + val Some(outputIndex) = outputs.zipWithIndex.find { + case (CommitmentOutputLink(_, _, DirectedHtlc(IN, incomingHtlc)), _) => incomingHtlc.id == htlc.id + case _ => false + }.map(_._2) + val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) // unsigned tx @@ -334,7 +415,7 @@ object Transactions { def makeClaimP2WPKHOutputTx(delayedOutputTx: Transaction, localDustLimit: Satoshi, localPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: Long): ClaimP2WPKHOutputTx = { val redeemScript = Script.pay2pkh(localPaymentPubkey) val pubkeyScript = write(pay2wpkh(localPaymentPubkey)) - val outputIndex = findPubKeyScriptIndex(delayedOutputTx, pubkeyScript, outputsAlreadyUsed = Set.empty, amount_opt = None) + val outputIndex = findPubKeyScriptIndex(delayedOutputTx, pubkeyScript, amount_opt = None) val input = InputInfo(OutPoint(delayedOutputTx, outputIndex), delayedOutputTx.txOut(outputIndex), write(redeemScript)) // unsigned tx @@ -360,7 +441,7 @@ object Transactions { def makeClaimDelayedOutputTx(delayedOutputTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: Long): ClaimDelayedOutputTx = { val redeemScript = toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) val pubkeyScript = write(pay2wsh(redeemScript)) - val outputIndex = findPubKeyScriptIndex(delayedOutputTx, pubkeyScript, outputsAlreadyUsed = Set.empty, amount_opt = None) + val outputIndex = findPubKeyScriptIndex(delayedOutputTx, pubkeyScript, amount_opt = None) val input = InputInfo(OutPoint(delayedOutputTx, outputIndex), delayedOutputTx.txOut(outputIndex), write(redeemScript)) // unsigned transaction @@ -386,7 +467,7 @@ object Transactions { def makeClaimDelayedOutputPenaltyTx(delayedOutputTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: Long): ClaimDelayedOutputPenaltyTx = { val redeemScript = toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) val pubkeyScript = write(pay2wsh(redeemScript)) - val outputIndex = findPubKeyScriptIndex(delayedOutputTx, pubkeyScript, outputsAlreadyUsed = Set.empty, amount_opt = None) + val outputIndex = findPubKeyScriptIndex(delayedOutputTx, pubkeyScript, amount_opt = None) val input = InputInfo(OutPoint(delayedOutputTx, outputIndex), delayedOutputTx.txOut(outputIndex), write(redeemScript)) // unsigned transaction @@ -412,7 +493,7 @@ object Transactions { def makeMainPenaltyTx(commitTx: Transaction, localDustLimit: Satoshi, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: ByteVector, toRemoteDelay: CltvExpiryDelta, remoteDelayedPaymentPubkey: PublicKey, feeratePerKw: Long): MainPenaltyTx = { val redeemScript = toLocalDelayed(remoteRevocationPubkey, toRemoteDelay, remoteDelayedPaymentPubkey) val pubkeyScript = write(pay2wsh(redeemScript)) - val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed = Set.empty, amount_opt = None) + val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, amount_opt = None) val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) // unsigned transaction @@ -438,10 +519,8 @@ object Transactions { /** * We already have the redeemScript, no need to build it */ - def makeHtlcPenaltyTx(commitTx: Transaction, outputsAlreadyUsed: Set[Int], redeemScript: ByteVector, localDustLimit: Satoshi, localFinalScriptPubKey: ByteVector, feeratePerKw: Long): HtlcPenaltyTx = { - val pubkeyScript = write(pay2wsh(redeemScript)) - val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = None) - val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), redeemScript) + def makeHtlcPenaltyTx(commitTx: Transaction, htlcOutputIndex: Int, redeemScript: ByteVector, localDustLimit: Satoshi, localFinalScriptPubKey: ByteVector, feeratePerKw: Long): HtlcPenaltyTx = { + val input = InputInfo(OutPoint(commitTx, htlcOutputIndex), commitTx.txOut(htlcOutputIndex), redeemScript) // unsigned transaction val tx = Transaction( @@ -483,10 +562,10 @@ object Transactions { ClosingTx(commitTxInput, LexicographicalOrdering.sort(tx)) } - def findPubKeyScriptIndex(tx: Transaction, pubkeyScript: ByteVector, outputsAlreadyUsed: Set[Int], amount_opt: Option[Satoshi]): Int = { + def findPubKeyScriptIndex(tx: Transaction, pubkeyScript: ByteVector, amount_opt: Option[Satoshi]): Int = { val outputIndex = tx.txOut .zipWithIndex - .indexWhere { case (txOut, index) => amount_opt.map(_ == txOut.amount).getOrElse(true) && txOut.publicKeyScript == pubkeyScript && !outputsAlreadyUsed.contains(index) } // it's not enough to only resolve on pubkeyScript because we may have duplicates + .indexWhere { case (txOut, _) => amount_opt.forall(_ == txOut.amount) && txOut.publicKeyScript == pubkeyScript } if (outputIndex >= 0) { outputIndex } else { diff --git a/eclair-core/src/test/resources/bolt3-tx-test-vectors.txt b/eclair-core/src/test/resources/bolt3-tx-test-vectors.txt index 608eef1c6e..9a37f098f9 100644 --- a/eclair-core/src/test/resources/bolt3-tx-test-vectors.txt +++ b/eclair-core/src/test/resources/bolt3-tx-test-vectors.txt @@ -340,3 +340,23 @@ # local_signature = 3044022031a82b51bd014915fe68928d1abf4b9885353fb896cac10c3fdd88d7f9c7f2e00220716bda819641d2c63e65d3549b6120112e1aeaf1742eed94a471488e79e206b1 output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8001c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de8431100400473044022031a82b51bd014915fe68928d1abf4b9885353fb896cac10c3fdd88d7f9c7f2e00220716bda819641d2c63e65d3549b6120112e1aeaf1742eed94a471488e79e206b101473044022064901950be922e62cbe3f2ab93de2b99f37cff9fc473e73e394b27f88ef0731d02206d1dfa227527b4df44a07599289e207d6fd9cca60c0365682dcd3deaf739567e01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 num_htlcs: 0 + + name: commitment tx with 3 htlc outputs, 2 offered having the same amount and preimage + to_local_msat: 6988000000 + to_remote_msat: 3000000000 + local_feerate_per_kw: 253 + # HTLC 0 received amount 1000000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac6868 + # HTLC 5 offered amount 5000000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc93ebefbb1b73f0af055dcc27a0b504ad7688ac6868 + # HTLC 6 offered amount 5000000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc93ebefbb1b73f0af055dcc27a0b504ad7688ac6868 + # HTLC 5 and 6 have CLTV 505 and 506, respectively, and preimage 0505050505050505050505050505050505050505050505050505050505050505 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8005e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2a8813000000000000220020305c12e1a0bc21e283c131cea1c66d68857d28b7b2fce0a6fbc40c164852121b8813000000000000220020305c12e1a0bc21e283c131cea1c66d68857d28b7b2fce0a6fbc40c164852121bc0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110a79f6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220586f17b3f3f6eec96a0dc7040f1a33174c707e032010421fa1965497cd1c81e602204476e6a4cc17d433e8ad82edff2ffa47c954ecb52aad9cc34382befb66f6e00001473044022069797b84fad1e0e7b02e1edeec80cb6e0b6f01170700bca273788715f55e0c560220260833550768b1db3ed88f8c005cf2560821b1879ec69834cd9765de25ec350601475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + # local_htlc_signature = 3044022077803ffab08308ac6ceaba6024855029f31d88e1420ece6aac1cf35258efb42702207db2cfe24de7b19a6e0e95082c595643fe18b7e0f0071297b2bb940807e774d2 + # remote_htlc_signature = 304502210087be93ccb1fd373ebd489c2dbe5f3a95a5cd7b173255f63367d063843f0ed263022027de0683bc4ac9afe1cdff48f16bb1a8e27789328b534e1cc39bf4dd0448ed69 + output htlc_success_tx 0: 020000000001012ac263f51690e216ddc176c047106d762592174f52ff72614b037f23124a67440000000000000000000137030000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050048304502210087be93ccb1fd373ebd489c2dbe5f3a95a5cd7b173255f63367d063843f0ed263022027de0683bc4ac9afe1cdff48f16bb1a8e27789328b534e1cc39bf4dd0448ed6901473044022077803ffab08308ac6ceaba6024855029f31d88e1420ece6aac1cf35258efb42702207db2cfe24de7b19a6e0e95082c595643fe18b7e0f0071297b2bb940807e774d2012000000000000000000000000000000000000000000000000000000000000000008a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac686800000000 + # local_htlc_signature = 30440220579ac2d6fdbdb0cb7bb07b9d2a52e9cb87aab26156b6290c504c1781f025daa00220310d6a5dc91c4c49cc362c1942b2af84130052c8ec597c1287d9cc18cd349f39 + # remote_htlc_signature = 3045022100e75ab7170a3cd3266d0abd38566ca8e6f9102949a4394e497a855dd3c0d7158b02201b91a0f3b18c512d8223eb4fdccb563d1a64b828882ca0b183340ee36dff152f + output htlc_timeout_tx 1: 020000000001012ac263f51690e216ddc176c047106d762592174f52ff72614b037f23124a674401000000000000000001e1120000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100e75ab7170a3cd3266d0abd38566ca8e6f9102949a4394e497a855dd3c0d7158b02201b91a0f3b18c512d8223eb4fdccb563d1a64b828882ca0b183340ee36dff152f014730440220579ac2d6fdbdb0cb7bb07b9d2a52e9cb87aab26156b6290c504c1781f025daa00220310d6a5dc91c4c49cc362c1942b2af84130052c8ec597c1287d9cc18cd349f3901008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc93ebefbb1b73f0af055dcc27a0b504ad7688ac6868f9010000 + # local_htlc_signature = 3045022100a62f73f9345a7821335391f00c64ebcba11628dd2e5058fc6f9827561381092402202c858823d25922fa2bafdac27f9bcd103d99d940823a581abe70bb0ab8ca9352 + # remote_htlc_signature = 304402206afc9e5ad67a329bbbc63e00f9ce5fb90d07121cf57c4db0b9dd05881f1ee89d02200917f67fff5c5acfcd5e4825e6e651671af6c261eb69b6a44d159edae724f308 + output htlc_timeout_tx 2: 020000000001012ac263f51690e216ddc176c047106d762592174f52ff72614b037f23124a674402000000000000000001e1120000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402206afc9e5ad67a329bbbc63e00f9ce5fb90d07121cf57c4db0b9dd05881f1ee89d02200917f67fff5c5acfcd5e4825e6e651671af6c261eb69b6a44d159edae724f30801483045022100a62f73f9345a7821335391f00c64ebcba11628dd2e5058fc6f9827561381092402202c858823d25922fa2bafdac27f9bcd103d99d940823a581abe70bb0ab8ca935201008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc93ebefbb1b73f0af055dcc27a0b504ad7688ac6868fa010000 + num_htlcs: 3 \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 31725c408e..f757fa5552 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -2086,8 +2086,6 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv BITCOIN_FUNDING_SPENT (revoked commit)") { f => import f._ - val sender = TestProbe() - // initially we have : // alice = 800 000 // bob = 200 000 diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala index a7f88649a9..7e5e13fdba 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi, Script, ScriptFlags, Transaction} +import fr.acinq.bitcoin.{ByteVector32, Crypto, Script, ScriptFlags, Transaction} import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.transactions.Transactions.{HtlcSuccessTx, HtlcTimeoutTx, TransactionWithInputInfo} @@ -182,15 +182,17 @@ class TestVectorsSpec extends FunSuite with Logging { logger.info(s"to_remote_msat: ${spec.toRemote}") logger.info(s"local_feerate_per_kw: ${spec.feeratePerKw}") + val outputs = Transactions.makeCommitTxOutputs( + true, Local.dustLimit, Local.revocation_pubkey, Local.toSelfDelay, + Local.delayed_payment_privkey.publicKey, Remote.payment_privkey.publicKey, + Local.payment_privkey.publicKey, Remote.payment_privkey.publicKey, // note: we have payment_key = htlc_key + spec) + val commitTx = { val tx = Transactions.makeCommitTx( commitmentInput, Local.commitTxNumber, Local.payment_basepoint, Remote.payment_basepoint, - true, Local.dustLimit, - Local.revocation_pubkey, Local.toSelfDelay, - Local.delayed_payment_privkey.publicKey, Remote.payment_privkey.publicKey, - Local.payment_privkey.publicKey, Remote.payment_privkey.publicKey, // note: we have payment_key = htlc_key - spec) + true, outputs) val local_sig = Transactions.sign(tx, Local.funding_privkey) val remote_sig = Transactions.sign(tx, Remote.funding_privkey) @@ -216,11 +218,7 @@ class TestVectorsSpec extends FunSuite with Logging { val tx = Transactions.makeCommitTx( commitmentInput, Local.commitTxNumber, Local.payment_basepoint, Remote.payment_basepoint, - true, Local.dustLimit, - Local.revocation_pubkey, Local.toSelfDelay, - Local.delayed_payment_privkey.publicKey, Remote.payment_privkey.publicKey, - Local.payment_privkey.publicKey, Remote.payment_privkey.publicKey, // note: we have payment_key = htlc_key - spec) + true, outputs) val local_sig = Transactions.sign(tx, Local.funding_privkey) logger.info(s"# local_signature = ${local_sig.dropRight(1).toHex}") @@ -237,8 +235,9 @@ class TestVectorsSpec extends FunSuite with Logging { Local.dustLimit, Local.revocation_pubkey, Local.toSelfDelay, Local.delayed_payment_privkey.publicKey, - Local.payment_privkey.publicKey, Remote.payment_privkey.publicKey, // note: we have payment_key = htlc_key - spec) + spec.feeratePerKw, + outputs + ) logger.info(s"num_htlcs: ${(unsignedHtlcTimeoutTxs ++ unsignedHtlcSuccessTxs).length}") val htlcTxs: Seq[TransactionWithInputInfo] = (unsignedHtlcTimeoutTxs ++ unsignedHtlcSuccessTxs).sortBy(_.input.outPoint.index) @@ -482,4 +481,29 @@ class TestVectorsSpec extends FunSuite with Logging { val check = (0 to 4).flatMap(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).toSet.map { s: String => Transaction.read(s) } assert(htlcTxs.map(_.tx).toSet == check) } + + // Added to the spec in https://github.com/lightningnetwork/lightning-rfc/pull/539 + test("commitment tx with 3 htlc outputs, 2 offered having the same amount and preimage") { + val name = "commitment tx with 3 htlc outputs, 2 offered having the same amount and preimage" + + val preimage = hex"0505050505050505050505050505050505050505050505050505050505050505" + + val someHtlc = Seq( + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, 1000000.msat, Crypto.sha256(paymentPreimages(0)), CltvExpiry(500), TestConstants.emptyOnionPacket)), + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, 5000000.msat, Crypto.sha256(preimage), CltvExpiry(505), TestConstants.emptyOnionPacket)), + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, 5000000.msat, Crypto.sha256(preimage), CltvExpiry(506), TestConstants.emptyOnionPacket)) + ) + + val spec = CommitmentSpec(htlcs = someHtlc.toSet, feeratePerKw = 253, toLocal = 6988000000L.msat, toRemote = 3000000000L.msat) + + val (commitTx, htlcTxs) = run(spec) + + assert(commitTx.tx == Transaction.read(results(name)("output commit_tx"))) + assert(commitTx.tx.txOut.length == 5) + + assert(htlcTxs.size == 3) // one htlc-success-tx + two htlc-timeout-tx + assert(htlcTxs(0).tx == Transaction.read(results(name)("output htlc_success_tx 0"))) + assert(htlcTxs(1).tx == Transaction.read(results(name)("output htlc_timeout_tx 1"))) + assert(htlcTxs(2).tx == Transaction.read(results(name)("output htlc_timeout_tx 2"))) + } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index d88099179b..47c5fa0dcc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -24,10 +24,12 @@ import fr.acinq.bitcoin.{Btc, ByteVector32, Crypto, MilliBtc, Protocol, Satoshi, import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.transactions.Scripts.{htlcOffered, htlcReceived, toLocalDelayed} import fr.acinq.eclair.transactions.Transactions.{addSigs, _} -import fr.acinq.eclair.wire.UpdateAddHtlc -import fr.acinq.eclair.{MilliSatoshi, TestConstants, randomBytes32, _} +import fr.acinq.eclair.wire.{OnionRoutingPacket, UpdateAddHtlc} +import fr.acinq.eclair.{MilliSatoshi, randomBytes32, _} +import fr.acinq.eclair._ import grizzled.slf4j.Logging import org.scalatest.FunSuite +import scodec.bits._ import scala.io.Source import scala.util.{Failure, Random, Success, Try} @@ -37,6 +39,19 @@ import scala.util.{Failure, Random, Success, Try} */ class TransactionsSpec extends FunSuite with Logging { + val localFundingPriv = PrivateKey(randomBytes32) + val remoteFundingPriv = PrivateKey(randomBytes32) + val localRevocationPriv = PrivateKey(randomBytes32) + val localPaymentPriv = PrivateKey(randomBytes32) + val localDelayedPaymentPriv = PrivateKey(randomBytes32) + val remotePaymentPriv = PrivateKey(randomBytes32) + val localHtlcPriv = PrivateKey(randomBytes32) + val remoteHtlcPriv = PrivateKey(randomBytes32) + val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32).publicKey)) + val commitInput = Funding.makeFundingInputInfo(randomBytes32, 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey) + val toLocalDelay = CltvExpiryDelta(144) + val localDustLimit = Satoshi(546) + val feeratePerKw = 22000 test("encode/decode sequence and locktime (one example)") { @@ -74,12 +89,6 @@ class TransactionsSpec extends FunSuite with Logging { } test("check pre-computed transaction weights") { - val localRevocationPriv = PrivateKey(randomBytes32) - val localPaymentPriv = PrivateKey(randomBytes32) - val remotePaymentPriv = PrivateKey(randomBytes32) - val localHtlcPriv = PrivateKey(randomBytes32) - val remoteHtlcPriv = PrivateKey(randomBytes32) - val localFinalPriv = PrivateKey(randomBytes32) val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32).publicKey)) val localDustLimit = 546 sat val toLocalDelay = CltvExpiryDelta(144) @@ -130,7 +139,7 @@ class TransactionsSpec extends FunSuite with Logging { val redeemScript = htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), htlc.cltvExpiry) val pubKeyScript = write(pay2wsh(redeemScript)) val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) - val htlcPenaltyTx = makeHtlcPenaltyTx(commitTx, outputsAlreadyUsed = Set.empty, Script.write(redeemScript), localDustLimit, finalPubKeyScript, feeratePerKw) + val htlcPenaltyTx = makeHtlcPenaltyTx(commitTx, 0, Script.write(redeemScript), localDustLimit, finalPubKeyScript, feeratePerKw) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(htlcPenaltyTx, PlaceHolderSig, localRevocationPriv.publicKey).tx) assert(htlcPenaltyWeight == weight) @@ -142,9 +151,11 @@ class TransactionsSpec extends FunSuite with Logging { // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcSuccessTx val paymentPreimage = randomBytes32 val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000) msat, sha256(paymentPreimage), CltvExpiryDelta(144).toCltvExpiry(blockHeight), TestConstants.emptyOnionPacket) + val spec = CommitmentSpec(Set(DirectedHtlc(OUT, htlc)), feeratePerKw, toLocal = 0 msat, toRemote = 0 msat) + val outputs = makeCommitTxOutputs(true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, spec) val pubKeyScript = write(pay2wsh(htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash)))) val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) - val claimHtlcSuccessTx = makeClaimHtlcSuccessTx(commitTx, outputsAlreadyUsed = Set.empty, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw) + val claimHtlcSuccessTx = makeClaimHtlcSuccessTx(commitTx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(claimHtlcSuccessTx, PlaceHolderSig, paymentPreimage).tx) assert(claimHtlcSuccessWeight == weight) @@ -153,12 +164,14 @@ class TransactionsSpec extends FunSuite with Logging { { // ClaimHtlcTimeoutTx - // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcSuccessTx + // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcTimeoutTx val paymentPreimage = randomBytes32 - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000) msat, sha256(paymentPreimage), CltvExpiryDelta(144).toCltvExpiry(blockHeight), TestConstants.emptyOnionPacket) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000) msat, sha256(paymentPreimage), toLocalDelay.toCltvExpiry(blockHeight), TestConstants.emptyOnionPacket) + val spec = CommitmentSpec(Set(DirectedHtlc(IN, htlc)), feeratePerKw, toLocal = 0 msat, toRemote = 0 msat) + val outputs = makeCommitTxOutputs(true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, spec) val pubKeyScript = write(pay2wsh(htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), htlc.cltvExpiry))) val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) - val claimClaimHtlcTimeoutTx = makeClaimHtlcTimeoutTx(commitTx, outputsAlreadyUsed = Set.empty, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw) + val claimClaimHtlcTimeoutTx = makeClaimHtlcTimeoutTx(commitTx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(claimClaimHtlcTimeoutTx, PlaceHolderSig).tx) assert(claimHtlcTimeoutWeight == weight) @@ -167,19 +180,8 @@ class TransactionsSpec extends FunSuite with Logging { } test("generate valid commitment and htlc transactions") { - val localFundingPriv = PrivateKey(randomBytes32 :+ 1.toByte) - val remoteFundingPriv = PrivateKey(randomBytes32 :+ 1.toByte) - val localRevocationPriv = PrivateKey(randomBytes32 :+ 1.toByte) - val localPaymentPriv = PrivateKey(randomBytes32 :+ 1.toByte) - val localDelayedPaymentPriv = PrivateKey(randomBytes32 :+ 1.toByte) - val remotePaymentPriv = PrivateKey(randomBytes32 :+ 1.toByte) - val localHtlcPriv = PrivateKey(randomBytes32 :+ 1.toByte) - val remoteHtlcPriv = PrivateKey(randomBytes32 :+ 1.toByte) val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32).publicKey)) val commitInput = Funding.makeFundingInputInfo(randomBytes32, 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey) - val toLocalDelay = CltvExpiryDelta(144) - val localDustLimit = 546 sat - val feeratePerKw = 22000 // htlc1 and htlc2 are regular IN/OUT htlcs val paymentPreimage1 = randomBytes32 @@ -202,9 +204,11 @@ class TransactionsSpec extends FunSuite with Logging { toLocal = millibtc2satoshi(MilliBtc(400)).toMilliSatoshi, toRemote = millibtc2satoshi(MilliBtc(300)).toMilliSatoshi) + val outputs = makeCommitTxOutputs(true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, spec) + val commitTxNumber = 0x404142434445L val commitTx = { - val txinfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, spec) + val txinfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, true, outputs) val localSig = Transactions.sign(txinfo, localPaymentPriv) val remoteSig = Transactions.sign(txinfo, remotePaymentPriv) Transactions.addSigs(txinfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) @@ -217,7 +221,7 @@ class TransactionsSpec extends FunSuite with Logging { val check = ((commitTx.tx.txIn.head.sequence & 0xffffff) << 24) | (commitTx.tx.lockTime & 0xffffff) assert((check ^ num) == commitTxNumber) } - val (htlcTimeoutTxs, htlcSuccessTxs) = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, spec) + val (htlcTimeoutTxs, htlcSuccessTxs) = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.feeratePerKw, outputs) assert(htlcTimeoutTxs.size == 2) // htlc1 and htlc3 assert(htlcSuccessTxs.size == 2) // htlc2 and htlc4 @@ -234,20 +238,20 @@ class TransactionsSpec extends FunSuite with Logging { { // local spends delayed output of htlc1 timeout tx - val claimHtlcDelayed = makeClaimDelayedOutputTx(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val claimHtlcDelayed = makeClaimDelayedOutputTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) val localSig = sign(claimHtlcDelayed, localDelayedPaymentPriv) val signedTx = addSigs(claimHtlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit intercept[RuntimeException] { - makeClaimDelayedOutputTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + makeClaimDelayedOutputTx(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) } } { // remote spends local->remote htlc1/htlc3 output directly in case of success for ((htlc, paymentPreimage) <- (htlc1, paymentPreimage1) :: (htlc3, paymentPreimage3) :: Nil) { - val claimHtlcSuccessTx = makeClaimHtlcSuccessTx(commitTx.tx, outputsAlreadyUsed = Set.empty, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw) + val claimHtlcSuccessTx = makeClaimHtlcSuccessTx(commitTx.tx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw) val localSig = sign(claimHtlcSuccessTx, remoteHtlcPriv) val signed = addSigs(claimHtlcSuccessTx, localSig, paymentPreimage) assert(checkSpendable(signed).isSuccess) @@ -256,7 +260,7 @@ class TransactionsSpec extends FunSuite with Logging { { // local spends remote->local htlc2/htlc4 output with htlc success tx using payment preimage - for ((htlcSuccessTx, paymentPreimage) <- (htlcSuccessTxs(0), paymentPreimage2) :: (htlcSuccessTxs(1), paymentPreimage4) :: Nil) { + for ((htlcSuccessTx, paymentPreimage) <- (htlcSuccessTxs(1), paymentPreimage2) :: (htlcSuccessTxs(0), paymentPreimage4) :: Nil) { val localSig = sign(htlcSuccessTx, localHtlcPriv) val remoteSig = sign(htlcSuccessTx, remoteHtlcPriv) val signedTx = addSigs(htlcSuccessTx, localSig, remoteSig, paymentPreimage) @@ -268,13 +272,13 @@ class TransactionsSpec extends FunSuite with Logging { { // local spends delayed output of htlc2 success tx - val claimHtlcDelayed = makeClaimDelayedOutputTx(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val claimHtlcDelayed = makeClaimDelayedOutputTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) val localSig = sign(claimHtlcDelayed, localDelayedPaymentPriv) val signedTx = addSigs(claimHtlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc4 timeout tx because it is below the dust limit intercept[RuntimeException] { - makeClaimDelayedOutputTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + makeClaimDelayedOutputTx(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) } } @@ -288,7 +292,7 @@ class TransactionsSpec extends FunSuite with Logging { { // remote spends remote->local htlc output directly in case of timeout - val claimHtlcTimeoutTx = makeClaimHtlcTimeoutTx(commitTx.tx, outputsAlreadyUsed = Set.empty, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc2, feeratePerKw) + val claimHtlcTimeoutTx = makeClaimHtlcTimeoutTx(commitTx.tx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc2, feeratePerKw) val localSig = sign(claimHtlcTimeoutTx, remoteHtlcPriv) val signed = addSigs(claimHtlcTimeoutTx, localSig) assert(checkSpendable(signed).isSuccess) @@ -297,7 +301,11 @@ class TransactionsSpec extends FunSuite with Logging { { // remote spends offered HTLC output with revocation key val script = Script.write(Scripts.htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc1.paymentHash))) - val htlcPenaltyTx = makeHtlcPenaltyTx(commitTx.tx, outputsAlreadyUsed = Set.empty, script, localDustLimit, finalPubKeyScript, feeratePerKw) + val Some(htlcOutputIndex) = outputs.zipWithIndex.find { + case (CommitmentOutputLink(_, _, DirectedHtlc(_, someHtlc)), _) => someHtlc.id == htlc1.id + case _ => false + }.map(_._2) + val htlcPenaltyTx = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) val sig = sign(htlcPenaltyTx, localRevocationPriv) val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) assert(checkSpendable(signed).isSuccess) @@ -306,7 +314,11 @@ class TransactionsSpec extends FunSuite with Logging { { // remote spends received HTLC output with revocation key val script = Script.write(Scripts.htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc2.paymentHash), htlc2.cltvExpiry)) - val htlcPenaltyTx = makeHtlcPenaltyTx(commitTx.tx, outputsAlreadyUsed = Set.empty, script, localDustLimit, finalPubKeyScript, feeratePerKw) + val Some(htlcOutputIndex) = outputs.zipWithIndex.find { + case (CommitmentOutputLink(_, _, DirectedHtlc(_, someHtlc)), _) => someHtlc.id == htlc2.id + case _ => false + }.map(_._2) + val htlcPenaltyTx = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) val sig = sign(htlcPenaltyTx, localRevocationPriv) val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) assert(checkSpendable(signed).isSuccess) @@ -314,6 +326,66 @@ class TransactionsSpec extends FunSuite with Logging { } + test("sort the htlc outputs using BIP69 and cltv expiry") { + val localFundingPriv = PrivateKey(hex"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1") + val remoteFundingPriv = PrivateKey(hex"a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2") + val localRevocationPriv = PrivateKey(hex"a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3") + val localPaymentPriv = PrivateKey(hex"a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4") + val localDelayedPaymentPriv = PrivateKey(hex"a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5") + val remotePaymentPriv = PrivateKey(hex"a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6") + val localHtlcPriv = PrivateKey(hex"a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7") + val remoteHtlcPriv = PrivateKey(hex"a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8") + val commitInput = Funding.makeFundingInputInfo(ByteVector32(hex"a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0"), 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey) + + // htlc1 and htlc2 are two regular incoming HTLCs with different amounts. + // htlc2 and htlc3 have the same amounts and should be sorted according to their scriptPubKey + // htlc4 is identical to htlc3 and htlc5 has same payment_hash/amount but different CLTV + val paymentPreimage1 = ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111") + val paymentPreimage2 = ByteVector32(hex"2222222222222222222222222222222222222222222222222222222222222222") + val paymentPreimage3 = ByteVector32(hex"3333333333333333333333333333333333333333333333333333333333333333") + val htlc1 = UpdateAddHtlc(randomBytes32, 1, millibtc2satoshi(MilliBtc(100)).toMilliSatoshi, sha256(paymentPreimage1), CltvExpiry(300), TestConstants.emptyOnionPacket) + val htlc2 = UpdateAddHtlc(randomBytes32, 2, millibtc2satoshi(MilliBtc(200)).toMilliSatoshi, sha256(paymentPreimage2), CltvExpiry(300), TestConstants.emptyOnionPacket) + val htlc3 = UpdateAddHtlc(randomBytes32, 3, millibtc2satoshi(MilliBtc(200)).toMilliSatoshi, sha256(paymentPreimage3), CltvExpiry(300), TestConstants.emptyOnionPacket) + val htlc4 = UpdateAddHtlc(randomBytes32, 4, millibtc2satoshi(MilliBtc(200)).toMilliSatoshi, sha256(paymentPreimage3), CltvExpiry(300), TestConstants.emptyOnionPacket) + val htlc5 = UpdateAddHtlc(randomBytes32, 5, millibtc2satoshi(MilliBtc(200)).toMilliSatoshi, sha256(paymentPreimage3), CltvExpiry(301), TestConstants.emptyOnionPacket) + + val spec = CommitmentSpec( + htlcs = Set( + DirectedHtlc(OUT, htlc1), + DirectedHtlc(OUT, htlc2), + DirectedHtlc(OUT, htlc3), + DirectedHtlc(OUT, htlc4), + DirectedHtlc(OUT, htlc5) + ), + feeratePerKw = feeratePerKw, + toLocal = millibtc2satoshi(MilliBtc(400)).toMilliSatoshi, + toRemote = millibtc2satoshi(MilliBtc(300)).toMilliSatoshi) + + val commitTxNumber = 0x404142434446L + val (commitTx, outputs) = { + val outputs = makeCommitTxOutputs(true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, spec) + val txinfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, true, outputs) + val localSig = Transactions.sign(txinfo, localPaymentPriv) + val remoteSig = Transactions.sign(txinfo, remotePaymentPriv) + (Transactions.addSigs(txinfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig), outputs) + } + + // htlc1 comes before htlc2 because of the smaller amount (BIP69) + // htlc2 and htlc3 have the same amount but htlc2 comes first because its pubKeyScript is lexicographically smaller than htlc3's + // htlc5 comes after htlc3 and htlc4 because of the higher CLTV + val htlcOut1 :: htlcOut2 :: htlcOut3 :: htlcOut4 :: htlcOut5 :: _ = commitTx.tx.txOut.toList + assert(htlcOut1.amount == 10000000.sat) + for (htlcOut <- Seq(htlcOut2, htlcOut3, htlcOut4, htlcOut5)) { + assert(htlcOut.amount == 20000000.sat) + } + + assert(htlcOut2.publicKeyScript.toHex < htlcOut3.publicKeyScript.toHex) + assert(outputs.find(_.specItem == DirectedHtlc(OUT, htlc2)).map(_.output.publicKeyScript).contains(htlcOut2.publicKeyScript)) + assert(outputs.find(_.specItem == DirectedHtlc(OUT, htlc3)).map(_.output.publicKeyScript).contains(htlcOut3.publicKeyScript)) + assert(outputs.find(_.specItem == DirectedHtlc(OUT, htlc4)).map(_.output.publicKeyScript).contains(htlcOut4.publicKeyScript)) + assert(outputs.find(_.specItem == DirectedHtlc(OUT, htlc5)).map(_.output.publicKeyScript).contains(htlcOut5.publicKeyScript)) + } + def checkSuccessOrFailTest[T](input: Try[T]) = input match { case Success(_) => () case Failure(t) => fail(t) @@ -365,4 +437,4 @@ class TransactionsSpec extends FunSuite with Logging { assert(fee === test.expectedFee) }) } -} +} \ No newline at end of file From f59111a57ddfb14124a13a5a985fc54d82e105eb Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 27 Mar 2020 10:42:54 +0100 Subject: [PATCH 2/6] Type DirectedHtlc: We now use a small hierarchy of classes to represent HTLC directions. There is also a type alias for a collection of commitment output links. --- .../fr/acinq/eclair/channel/Channel.scala | 7 +-- .../fr/acinq/eclair/channel/Helpers.scala | 7 ++- .../eclair/transactions/CommitmentSpec.scala | 20 ++++++- .../eclair/transactions/Transactions.scala | 51 ++++++++--------- .../fr/acinq/eclair/wire/ChannelCodecs.scala | 8 ++- .../payment/PostRestartHtlcCleanerSpec.scala | 12 ++-- .../transactions/CommitmentSpecSpec.scala | 12 ++-- .../eclair/transactions/TestVectorsSpec.scala | 18 +++--- .../transactions/TransactionsSpec.scala | 55 ++++++++++--------- .../acinq/eclair/wire/ChannelCodecsSpec.scala | 24 ++++---- 10 files changed, 121 insertions(+), 93 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index c76fe4bf15..8e066dcb8c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -736,10 +736,9 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // we persist htlc data in order to be able to claim htlc outputs in case a revoked tx is published by our // counterparty, so only htlcs above remote's dust_limit matter val trimmedHtlcs = Transactions.trimOfferedHtlcs(d.commitments.remoteParams.dustLimit, nextRemoteCommit.spec) ++ Transactions.trimReceivedHtlcs(d.commitments.remoteParams.dustLimit, nextRemoteCommit.spec) - trimmedHtlcs collect { - case DirectedHtlc(_, u) => - log.info(s"adding paymentHash=${u.paymentHash} cltvExpiry=${u.cltvExpiry} to htlcs db for commitNumber=$nextCommitNumber") - nodeParams.db.channels.addHtlcInfo(d.channelId, nextCommitNumber, u.paymentHash, u.cltvExpiry) + trimmedHtlcs.map(_.add).foreach { htlc => + log.info(s"adding paymentHash=${htlc.paymentHash} cltvExpiry=${htlc.cltvExpiry} to htlcs db for commitNumber=$nextCommitNumber") + nodeParams.db.channels.addHtlcInfo(d.channelId, nextCommitNumber, htlc.paymentHash, htlc.cltvExpiry) } if (!Helpers.aboveReserve(d.commitments) && Helpers.aboveReserve(commitments1)) { // we just went above reserve (can't go below), let's refresh our channel_update to enable/disable it accordingly diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index c2fa37a5b0..9a9f2f944f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -614,8 +614,9 @@ object Helpers { // remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa val txes = remoteCommit.spec.htlcs.collect { - // incoming htlc for which we have the preimage: we spend it directly - case DirectedHtlc(OUT, add: UpdateAddHtlc) if preimages.exists(r => sha256(r) == add.paymentHash) => generateTx("claim-htlc-success")(Try { + // incoming htlc for which we have the preimage: we spend it directly. + // NB: we are looking at the remote's commitment, from its point of view it's an outgoing htlc. + case IncomingHtlc(add: UpdateAddHtlc) if preimages.exists(r => sha256(r) == add.paymentHash) => generateTx("claim-htlc-success")(Try { val preimage = preimages.find(r => sha256(r) == add.paymentHash).get val txinfo = Transactions.makeClaimHtlcSuccessTx(remoteCommitTx.tx, outputs, localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc) val sig = keyManager.sign(txinfo, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint) @@ -625,7 +626,7 @@ object Helpers { // (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 DirectedHtlc(IN, add: UpdateAddHtlc) => generateTx("claim-htlc-timeout")(Try { + case OutgoingHtlc(add: UpdateAddHtlc) => generateTx("claim-htlc-timeout")(Try { val txinfo = Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, outputs, localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc) val sig = keyManager.sign(txinfo, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint) Transactions.addSigs(txinfo, sig) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala index d2861baf70..1a0cfa1562 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala @@ -33,8 +33,21 @@ sealed trait CommitmentOutput object CommitmentOutput { case object ToLocal extends CommitmentOutput case object ToRemote extends CommitmentOutput + case class InHtlc(incomingHtlc: IncomingHtlc) extends CommitmentOutput + case class OutHtlc(outgoingHtlc: OutgoingHtlc) extends CommitmentOutput } -case class DirectedHtlc(direction: Direction, add: UpdateAddHtlc) extends CommitmentOutput + +sealed trait DirectedHtlc { + def direction: Direction + val add: UpdateAddHtlc + def opposite: DirectedHtlc = this match { + case IncomingHtlc(_) => OutgoingHtlc(add) + case OutgoingHtlc(_) => IncomingHtlc(add) + } +} + +case class IncomingHtlc(add: UpdateAddHtlc) extends DirectedHtlc { override def direction: Direction = IN } +case class OutgoingHtlc(add: UpdateAddHtlc) extends DirectedHtlc { override def direction: Direction = OUT } final case class CommitmentSpec(htlcs: Set[DirectedHtlc], feeratePerKw: Long, toLocal: MilliSatoshi, toRemote: MilliSatoshi) { @@ -50,7 +63,10 @@ object CommitmentSpec { } def addHtlc(spec: CommitmentSpec, direction: Direction, update: UpdateAddHtlc): CommitmentSpec = { - val htlc = DirectedHtlc(direction, update) + val htlc = direction match { + case IN => IncomingHtlc(update) + case OUT => OutgoingHtlc(update) + } direction match { case OUT => spec.copy(toLocal = spec.toLocal - htlc.add.amountMsat, htlcs = spec.htlcs + htlc) case IN => spec.copy(toRemote = spec.toRemote - htlc.add.amountMsat, htlcs = spec.htlcs + htlc) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index cc477a2131..360adbb546 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -23,6 +23,7 @@ import fr.acinq.bitcoin.Script._ import fr.acinq.bitcoin.SigVersion._ import fr.acinq.bitcoin._ import fr.acinq.eclair._ +import fr.acinq.eclair.transactions.CommitmentOutput.{InHtlc, OutHtlc, ToLocal, ToRemote} import fr.acinq.eclair.transactions.Scripts._ import fr.acinq.eclair.wire.UpdateAddHtlc import scodec.bits.ByteVector @@ -123,22 +124,20 @@ object Transactions { /** Offered HTLCs below this amount will be trimmed. */ def offeredHtlcTrimThreshold(dustLimit: Satoshi, spec: CommitmentSpec): Satoshi = dustLimit + weight2fee(spec.feeratePerKw, htlcTimeoutWeight) - def trimOfferedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec): Seq[DirectedHtlc] = { + def trimOfferedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec): Seq[OutgoingHtlc] = { val threshold = offeredHtlcTrimThreshold(dustLimit, spec) spec.htlcs - .filter(_.direction == OUT) - .filter(htlc => htlc.add.amountMsat >= threshold) + .collect { case o:OutgoingHtlc if o.add.amountMsat >= threshold => o } .toSeq } /** Received HTLCs below this amount will be trimmed. */ def receivedHtlcTrimThreshold(dustLimit: Satoshi, spec: CommitmentSpec): Satoshi = dustLimit + weight2fee(spec.feeratePerKw, htlcSuccessWeight) - def trimReceivedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec): Seq[DirectedHtlc] = { + def trimReceivedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec): Seq[IncomingHtlc] = { val threshold = receivedHtlcTrimThreshold(dustLimit, spec) spec.htlcs - .filter(_.direction == IN) - .filter(htlc => htlc.add.amountMsat >= threshold) + .collect { case i:IncomingHtlc if i.add.amountMsat >= threshold => i } .toSeq } @@ -211,12 +210,12 @@ object Transactions { * @param output transaction output * @param redeemScript redeem script that matches this output (most of them are p2wsh) - * @param specItem commitment spec item this output is built from + * @param commitmentOutput commitment spec item this output is built from */ - case class CommitmentOutputLink(output: TxOut, redeemScript: Seq[ScriptElt], specItem: CommitmentOutput) + case class CommitmentOutputLink[T <: CommitmentOutput](output: TxOut, redeemScript: Seq[ScriptElt], commitmentOutput: T) - /** Type alias for a collection of CommitmentOutputLink */ - type CommitmentOutputs = Seq[CommitmentOutputLink] + /** Type alias for a collection of commitment output links */ + type CommitmentOutputs = Seq[CommitmentOutputLink[CommitmentOutput]] object CommitmentOutputLink { /** @@ -224,8 +223,8 @@ object Transactions { * HTLC because we must agree with the remote on the order of HTLC-Timeout transactions even for identical HTLC outputs. * See https://github.com/lightningnetwork/lightning-rfc/issues/448#issuecomment-432074187. */ - def sort(a: CommitmentOutputLink, b: CommitmentOutputLink): Boolean = (a.specItem, b.specItem) match { - case (DirectedHtlc(OUT, htlcA), DirectedHtlc(OUT, htlcB)) if htlcA.paymentHash == htlcB.paymentHash && htlcA.amountMsat == htlcB.amountMsat => + def sort(a: CommitmentOutputLink[CommitmentOutput], b: CommitmentOutputLink[CommitmentOutput]): Boolean = (a.commitmentOutput, b.commitmentOutput) match { + case (OutHtlc(OutgoingHtlc(htlcA)), OutHtlc(OutgoingHtlc(htlcB))) if htlcA.paymentHash == htlcB.paymentHash && htlcA.amountMsat == htlcB.amountMsat => htlcA.cltvExpiry <= htlcB.cltvExpiry case _ => LexicographicalOrdering.isLessThan(a.output, b.output) } @@ -247,28 +246,28 @@ object Transactions { } else { (spec.toLocal.truncateToSatoshi, spec.toRemote.truncateToSatoshi - commitFee) } // NB: we don't care if values are < 0, they will be trimmed if they are < dust limit anyway - val outputs = collection.mutable.ArrayBuffer.empty[CommitmentOutputLink] + val outputs = collection.mutable.ArrayBuffer.empty[CommitmentOutputLink[CommitmentOutput]] if (toLocalAmount >= localDustLimit) outputs.append( CommitmentOutputLink( TxOut(toLocalAmount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))), toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey), - CommitmentOutput.ToLocal)) + ToLocal)) if (toRemoteAmount >= localDustLimit) outputs.append( CommitmentOutputLink( TxOut(toRemoteAmount, pay2wpkh(remotePaymentPubkey)), pay2pkh(remotePaymentPubkey), - CommitmentOutput.ToRemote)) + ToRemote)) trimOfferedHtlcs(localDustLimit, spec).foreach { htlc => val redeemScript = htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes)) - outputs.append(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(redeemScript)), redeemScript, htlc)) + outputs.append(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(redeemScript)), redeemScript, OutHtlc(htlc))) } trimReceivedHtlcs(localDustLimit, spec).foreach { htlc => val redeemScript = htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), htlc.add.cltvExpiry) - outputs.append(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(redeemScript)), redeemScript, htlc)) + outputs.append(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(redeemScript)), redeemScript, InHtlc(htlc))) } outputs.sortWith(CommitmentOutputLink.sort) @@ -293,7 +292,7 @@ object Transactions { } def makeHtlcTimeoutTx(commitTx: Transaction, - output: CommitmentOutputLink, + output: CommitmentOutputLink[OutHtlc], outputIndex: Int, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, @@ -302,7 +301,7 @@ object Transactions { feeratePerKw: Long): HtlcTimeoutTx = { val fee = weight2fee(feeratePerKw, htlcTimeoutWeight) val redeemScript = output.redeemScript - val DirectedHtlc(OUT, htlc) = output.specItem + val htlc = output.commitmentOutput.outgoingHtlc.add val amount = htlc.amountMsat.truncateToSatoshi - fee if (amount < localDustLimit) { throw AmountBelowDustLimit @@ -316,7 +315,7 @@ object Transactions { } def makeHtlcSuccessTx(commitTx: Transaction, - output: CommitmentOutputLink, + output: CommitmentOutputLink[InHtlc], outputIndex: Int, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, @@ -325,7 +324,7 @@ object Transactions { feeratePerKw: Long): HtlcSuccessTx = { val fee = weight2fee(feeratePerKw, htlcSuccessWeight) val redeemScript = output.redeemScript - val DirectedHtlc(IN, htlc) = output.specItem + val htlc = output.commitmentOutput.incomingHtlc.add val amount = htlc.amountMsat.truncateToSatoshi - fee if (amount < localDustLimit) { @@ -348,11 +347,13 @@ object Transactions { outputs: CommitmentOutputs): (Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { val htlcTimeoutTxs = outputs.zipWithIndex.collect { - case (co@CommitmentOutputLink(_, _, DirectedHtlc(OUT, _)), outputIndex) => + case (CommitmentOutputLink(o, s, OutHtlc(ou)), outputIndex) => + val co = CommitmentOutputLink(o, s, OutHtlc(ou)) makeHtlcTimeoutTx(commitTx, co, outputIndex, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, feeratePerKw) } val htlcSuccessTxs = outputs.zipWithIndex.collect { - case (co@CommitmentOutputLink(_, _, DirectedHtlc(IN, _)), outputIndex) => + case (CommitmentOutputLink(o, s, InHtlc(in)), outputIndex) => + val co = CommitmentOutputLink(o, s, InHtlc(in)) makeHtlcSuccessTx(commitTx, co, outputIndex, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, feeratePerKw) } (htlcTimeoutTxs, htlcSuccessTxs) @@ -361,7 +362,7 @@ object Transactions { def makeClaimHtlcSuccessTx(commitTx: Transaction, outputs: CommitmentOutputs, localDustLimit: Satoshi, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: ByteVector, htlc: UpdateAddHtlc, feeratePerKw: Long): ClaimHtlcSuccessTx = { val redeemScript = htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlc.paymentHash.bytes)) val Some(outputIndex) = outputs.zipWithIndex.find { - case (CommitmentOutputLink(_, _, DirectedHtlc(OUT, outgoingHtlc)), _) => outgoingHtlc.id == htlc.id + case (CommitmentOutputLink(_, _, OutHtlc(OutgoingHtlc(outgoingHtlc))), _) => outgoingHtlc.id == htlc.id case _ => false }.map(_._2) @@ -387,7 +388,7 @@ object Transactions { def makeClaimHtlcTimeoutTx(commitTx: Transaction, outputs: CommitmentOutputs, localDustLimit: Satoshi, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: ByteVector, htlc: UpdateAddHtlc, feeratePerKw: Long): ClaimHtlcTimeoutTx = { val redeemScript = htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlc.paymentHash.bytes), htlc.cltvExpiry) val Some(outputIndex) = outputs.zipWithIndex.find { - case (CommitmentOutputLink(_, _, DirectedHtlc(IN, incomingHtlc)), _) => incomingHtlc.id == htlc.id + case (CommitmentOutputLink(_, _, InHtlc(IncomingHtlc(incomingHtlc))), _) => incomingHtlc.id == htlc.id case _ => false }.map(_._2) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala index f520680627..25cf904712 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala @@ -32,6 +32,7 @@ import grizzled.slf4j.Logging import scodec.bits.BitVector import scodec.codecs._ import scodec.{Attempt, Codec} +import shapeless.HNil import scala.compat.Platform import scala.concurrent.duration._ @@ -92,9 +93,14 @@ object ChannelCodecs extends Logging { (wire: BitVector) => bool.decode(wire).map(_.map(b => if (b) IN else OUT)) ) + import shapeless.:: val htlcCodec: Codec[DirectedHtlc] = ( ("direction" | directionCodec) :: - ("add" | updateAddHtlcCodec)).as[DirectedHtlc] + ("add" | updateAddHtlcCodec)).xmap( + { case IN :: add :: HNil => IncomingHtlc(add) + case OUT :: add :: HNil => OutgoingHtlc(add) }, + { htlc => htlc.direction :: htlc.add :: HNil } + ) def setCodec[T](codec: Codec[T]): Codec[Set[T]] = Codec[Set[T]]( (elems: Set[T]) => listOfN(uint16, codec).encode(elems.toList), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala index c59a6f0201..1480ad0fca 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala @@ -27,7 +27,7 @@ import fr.acinq.eclair.payment.OutgoingPacket.buildCommand import fr.acinq.eclair.payment.PaymentPacketSpec._ import fr.acinq.eclair.payment.relay.{CommandBuffer, Origin, Relayer} import fr.acinq.eclair.router.ChannelHop -import fr.acinq.eclair.transactions.{DirectedHtlc, Direction, IN, OUT} +import fr.acinq.eclair.transactions.{DirectedHtlc, Direction, IN, IncomingHtlc, OUT, OutgoingHtlc} import fr.acinq.eclair.wire.Onion.FinalLegacyPayload import fr.acinq.eclair.wire._ import fr.acinq.eclair.{CltvExpiry, LongToBtcAmount, NodeParams, TestConstants, TestkitBaseClass, randomBytes32} @@ -368,13 +368,15 @@ object PostRestartHtlcCleanerSpec { def buildHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32, direction: Direction): DirectedHtlc = { val (cmd, _) = buildCommand(Upstream.Local(UUID.randomUUID()), paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry)) val add = UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) - DirectedHtlc(direction, add) + direction match { + case IN => IncomingHtlc(add) + case OUT => OutgoingHtlc(add) + } } def buildFinalHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = { val (cmd, _) = buildCommand(Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, TestConstants.Bob.nodeParams.nodeId, channelUpdate_ab) :: Nil, FinalLegacyPayload(finalAmount, finalExpiry)) - val add = UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) - DirectedHtlc(IN, add) + IncomingHtlc(UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)) } def buildForwardFail(add: UpdateAddHtlc, origin: Origin) = @@ -406,7 +408,7 @@ object PostRestartHtlcCleanerSpec { nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id2, parentId, None, paymentHash2, PaymentType.Standard, add2.amountMsat, 2500 msat, c, 0, None, OutgoingPaymentStatus.Pending)) nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id3, parentId, None, paymentHash2, PaymentType.Standard, add3.amountMsat, 2500 msat, c, 0, None, OutgoingPaymentStatus.Pending)) nodeParams.db.channels.addOrUpdateChannel(ChannelCodecsSpec.makeChannelDataNormal( - Seq(add1, add2, add3).map(add => DirectedHtlc(OUT, add)), + Seq(add1, add2, add3).map(add => OutgoingHtlc(add)), Map(add1.id -> origin1, add2.id -> origin2, add3.id -> origin3)) ) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala index b0a3a3953d..be61250c12 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala @@ -29,15 +29,15 @@ class CommitmentSpecSpec extends FunSuite { val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, (2000 * 1000) msat, H, CltvExpiry(400), TestConstants.emptyOnionPacket) val spec1 = CommitmentSpec.reduce(spec, add1 :: Nil, Nil) - assert(spec1 === spec.copy(htlcs = Set(DirectedHtlc(OUT, add1)), toLocal = 3000000 msat)) + assert(spec1 === spec.copy(htlcs = Set(OutgoingHtlc(add1)), toLocal = 3000000 msat)) val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, (1000 * 1000) msat, H, CltvExpiry(400), TestConstants.emptyOnionPacket) val spec2 = CommitmentSpec.reduce(spec1, add2 :: Nil, Nil) - assert(spec2 === spec1.copy(htlcs = Set(DirectedHtlc(OUT, add1), DirectedHtlc(OUT, add2)), toLocal = 2000000 msat)) + assert(spec2 === spec1.copy(htlcs = Set(OutgoingHtlc(add1), OutgoingHtlc(add2)), toLocal = 2000000 msat)) val ful1 = UpdateFulfillHtlc(ByteVector32.Zeroes, add1.id, R) val spec3 = CommitmentSpec.reduce(spec2, Nil, ful1 :: Nil) - assert(spec3 === spec2.copy(htlcs = Set(DirectedHtlc(OUT, add2)), toRemote = 2000000 msat)) + assert(spec3 === spec2.copy(htlcs = Set(OutgoingHtlc(add2)), toRemote = 2000000 msat)) val fail1 = UpdateFailHtlc(ByteVector32.Zeroes, add2.id, R) val spec4 = CommitmentSpec.reduce(spec3, Nil, fail1 :: Nil) @@ -51,15 +51,15 @@ class CommitmentSpecSpec extends FunSuite { val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, (2000 * 1000) msat, H, CltvExpiry(400), TestConstants.emptyOnionPacket) val spec1 = CommitmentSpec.reduce(spec, Nil, add1 :: Nil) - assert(spec1 === spec.copy(htlcs = Set(DirectedHtlc(IN, add1)), toRemote = (3000 * 1000 msat))) + assert(spec1 === spec.copy(htlcs = Set(IncomingHtlc(add1)), toRemote = (3000 * 1000 msat))) val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, (1000 * 1000) msat, H, CltvExpiry(400), TestConstants.emptyOnionPacket) val spec2 = CommitmentSpec.reduce(spec1, Nil, add2 :: Nil) - assert(spec2 === spec1.copy(htlcs = Set(DirectedHtlc(IN, add1), DirectedHtlc(IN, add2)), toRemote = (2000 * 1000) msat)) + assert(spec2 === spec1.copy(htlcs = Set(IncomingHtlc(add1), IncomingHtlc(add2)), toRemote = (2000 * 1000) msat)) val ful1 = UpdateFulfillHtlc(ByteVector32.Zeroes, add1.id, R) val spec3 = CommitmentSpec.reduce(spec2, ful1 :: Nil, Nil) - assert(spec3 === spec2.copy(htlcs = Set(DirectedHtlc(IN, add2)), toLocal = (2000 * 1000) msat)) + assert(spec3 === spec2.copy(htlcs = Set(IncomingHtlc(add2)), toLocal = (2000 * 1000) msat)) val fail1 = UpdateFailHtlc(ByteVector32.Zeroes, add2.id, R) val spec4 = CommitmentSpec.reduce(spec3, fail1 :: Nil, Nil) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala index 7e5e13fdba..3419cb7b91 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala @@ -153,12 +153,12 @@ class TestVectorsSpec extends FunSuite with Logging { ByteVector32(hex"0404040404040404040404040404040404040404040404040404040404040404") ) - val htlcs = Seq( - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, 1000000 msat, Crypto.sha256(paymentPreimages(0)), CltvExpiry(500), TestConstants.emptyOnionPacket)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, 2000000 msat, Crypto.sha256(paymentPreimages(1)), CltvExpiry(501), TestConstants.emptyOnionPacket)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, 2000000 msat, Crypto.sha256(paymentPreimages(2)), CltvExpiry(502), TestConstants.emptyOnionPacket)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, 3000000 msat, Crypto.sha256(paymentPreimages(3)), CltvExpiry(503), TestConstants.emptyOnionPacket)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, 4000000 msat, Crypto.sha256(paymentPreimages(4)), CltvExpiry(504), TestConstants.emptyOnionPacket)) + val htlcs = Seq[DirectedHtlc]( + IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 1000000 msat, Crypto.sha256(paymentPreimages(0)), CltvExpiry(500), TestConstants.emptyOnionPacket)), + IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 2000000 msat, Crypto.sha256(paymentPreimages(1)), CltvExpiry(501), TestConstants.emptyOnionPacket)), + OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 2000000 msat, Crypto.sha256(paymentPreimages(2)), CltvExpiry(502), TestConstants.emptyOnionPacket)), + OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 3000000 msat, Crypto.sha256(paymentPreimages(3)), CltvExpiry(503), TestConstants.emptyOnionPacket)), + IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 4000000 msat, Crypto.sha256(paymentPreimages(4)), CltvExpiry(504), TestConstants.emptyOnionPacket)) ) val htlcScripts = htlcs.map(htlc => htlc.direction match { case OUT => Scripts.htlcOffered(Local.payment_privkey.publicKey, Remote.payment_privkey.publicKey, Local.revocation_pubkey, Crypto.ripemd160(htlc.add.paymentHash)) @@ -489,9 +489,9 @@ class TestVectorsSpec extends FunSuite with Logging { val preimage = hex"0505050505050505050505050505050505050505050505050505050505050505" val someHtlc = Seq( - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, 1000000.msat, Crypto.sha256(paymentPreimages(0)), CltvExpiry(500), TestConstants.emptyOnionPacket)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, 5000000.msat, Crypto.sha256(preimage), CltvExpiry(505), TestConstants.emptyOnionPacket)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, 5000000.msat, Crypto.sha256(preimage), CltvExpiry(506), TestConstants.emptyOnionPacket)) + IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 1000000.msat, Crypto.sha256(paymentPreimages(0)), CltvExpiry(500), TestConstants.emptyOnionPacket)), + OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 5000000.msat, Crypto.sha256(preimage), CltvExpiry(505), TestConstants.emptyOnionPacket)), + OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 5000000.msat, Crypto.sha256(preimage), CltvExpiry(506), TestConstants.emptyOnionPacket)) ) val spec = CommitmentSpec(htlcs = someHtlc.toSet, feeratePerKw = 253, toLocal = 6988000000L.msat, toRemote = 3000000000L.msat) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index 47c5fa0dcc..0007dde654 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -22,11 +22,11 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, ripemd160, sha256} import fr.acinq.bitcoin.Script.{pay2wpkh, pay2wsh, write} import fr.acinq.bitcoin.{Btc, ByteVector32, Crypto, MilliBtc, Protocol, Satoshi, Script, Transaction, TxOut, millibtc2satoshi} import fr.acinq.eclair.channel.Helpers.Funding +import fr.acinq.eclair.transactions.CommitmentOutput.{InHtlc, OutHtlc} import fr.acinq.eclair.transactions.Scripts.{htlcOffered, htlcReceived, toLocalDelayed} import fr.acinq.eclair.transactions.Transactions.{addSigs, _} -import fr.acinq.eclair.wire.{OnionRoutingPacket, UpdateAddHtlc} +import fr.acinq.eclair.wire.UpdateAddHtlc import fr.acinq.eclair.{MilliSatoshi, randomBytes32, _} -import fr.acinq.eclair._ import grizzled.slf4j.Logging import org.scalatest.FunSuite import scodec.bits._ @@ -77,11 +77,11 @@ class TransactionsSpec extends FunSuite with Logging { test("compute fees") { // see BOLT #3 specs - val htlcs = Set( - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, 5000000 msat, ByteVector32.Zeroes, CltvExpiry(552), TestConstants.emptyOnionPacket)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, 1000000 msat, ByteVector32.Zeroes, CltvExpiry(553), TestConstants.emptyOnionPacket)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, 7000000 msat, ByteVector32.Zeroes, CltvExpiry(550), TestConstants.emptyOnionPacket)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, 800000 msat, ByteVector32.Zeroes, CltvExpiry(551), TestConstants.emptyOnionPacket)) + val htlcs = Set[DirectedHtlc]( + OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 5000000 msat, ByteVector32.Zeroes, CltvExpiry(552), TestConstants.emptyOnionPacket)), + OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 1000000 msat, ByteVector32.Zeroes, CltvExpiry(553), TestConstants.emptyOnionPacket)), + IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 7000000 msat, ByteVector32.Zeroes, CltvExpiry(550), TestConstants.emptyOnionPacket)), + IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 800000 msat, ByteVector32.Zeroes, CltvExpiry(551), TestConstants.emptyOnionPacket)) ) val spec = CommitmentSpec(htlcs, feeratePerKw = 5000, toLocal = 0 msat, toRemote = 0 msat) val fee = Transactions.commitTxFee(546 sat, spec) @@ -151,7 +151,7 @@ class TransactionsSpec extends FunSuite with Logging { // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcSuccessTx val paymentPreimage = randomBytes32 val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000) msat, sha256(paymentPreimage), CltvExpiryDelta(144).toCltvExpiry(blockHeight), TestConstants.emptyOnionPacket) - val spec = CommitmentSpec(Set(DirectedHtlc(OUT, htlc)), feeratePerKw, toLocal = 0 msat, toRemote = 0 msat) + val spec = CommitmentSpec(Set(OutgoingHtlc(htlc)), feeratePerKw, toLocal = 0 msat, toRemote = 0 msat) val outputs = makeCommitTxOutputs(true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, spec) val pubKeyScript = write(pay2wsh(htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash)))) val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) @@ -167,7 +167,7 @@ class TransactionsSpec extends FunSuite with Logging { // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcTimeoutTx val paymentPreimage = randomBytes32 val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000) msat, sha256(paymentPreimage), toLocalDelay.toCltvExpiry(blockHeight), TestConstants.emptyOnionPacket) - val spec = CommitmentSpec(Set(DirectedHtlc(IN, htlc)), feeratePerKw, toLocal = 0 msat, toRemote = 0 msat) + val spec = CommitmentSpec(Set(IncomingHtlc(htlc)), feeratePerKw, toLocal = 0 msat, toRemote = 0 msat) val outputs = makeCommitTxOutputs(true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, spec) val pubKeyScript = write(pay2wsh(htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), htlc.cltvExpiry))) val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) @@ -195,10 +195,10 @@ class TransactionsSpec extends FunSuite with Logging { val htlc4 = UpdateAddHtlc(ByteVector32.Zeroes, 3, (localDustLimit + weight2fee(feeratePerKw, htlcSuccessWeight)).toMilliSatoshi, sha256(paymentPreimage4), CltvExpiry(300), TestConstants.emptyOnionPacket) val spec = CommitmentSpec( htlcs = Set( - DirectedHtlc(OUT, htlc1), - DirectedHtlc(IN, htlc2), - DirectedHtlc(OUT, htlc3), - DirectedHtlc(IN, htlc4) + OutgoingHtlc(htlc1), + IncomingHtlc(htlc2), + OutgoingHtlc(htlc3), + IncomingHtlc(htlc4) ), feeratePerKw = feeratePerKw, toLocal = millibtc2satoshi(MilliBtc(400)).toMilliSatoshi, @@ -302,7 +302,7 @@ class TransactionsSpec extends FunSuite with Logging { // remote spends offered HTLC output with revocation key val script = Script.write(Scripts.htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc1.paymentHash))) val Some(htlcOutputIndex) = outputs.zipWithIndex.find { - case (CommitmentOutputLink(_, _, DirectedHtlc(_, someHtlc)), _) => someHtlc.id == htlc1.id + case (CommitmentOutputLink(_, _, OutHtlc(OutgoingHtlc(someHtlc))), _) => someHtlc.id == htlc1.id case _ => false }.map(_._2) val htlcPenaltyTx = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) @@ -315,7 +315,7 @@ class TransactionsSpec extends FunSuite with Logging { // remote spends received HTLC output with revocation key val script = Script.write(Scripts.htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc2.paymentHash), htlc2.cltvExpiry)) val Some(htlcOutputIndex) = outputs.zipWithIndex.find { - case (CommitmentOutputLink(_, _, DirectedHtlc(_, someHtlc)), _) => someHtlc.id == htlc2.id + case (CommitmentOutputLink(_, _, InHtlc(IncomingHtlc(someHtlc))), _) => someHtlc.id == htlc2.id case _ => false }.map(_._2) val htlcPenaltyTx = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) @@ -351,11 +351,11 @@ class TransactionsSpec extends FunSuite with Logging { val spec = CommitmentSpec( htlcs = Set( - DirectedHtlc(OUT, htlc1), - DirectedHtlc(OUT, htlc2), - DirectedHtlc(OUT, htlc3), - DirectedHtlc(OUT, htlc4), - DirectedHtlc(OUT, htlc5) + OutgoingHtlc(htlc1), + OutgoingHtlc(htlc2), + OutgoingHtlc(htlc3), + OutgoingHtlc(htlc4), + OutgoingHtlc(htlc5) ), feeratePerKw = feeratePerKw, toLocal = millibtc2satoshi(MilliBtc(400)).toMilliSatoshi, @@ -380,10 +380,10 @@ class TransactionsSpec extends FunSuite with Logging { } assert(htlcOut2.publicKeyScript.toHex < htlcOut3.publicKeyScript.toHex) - assert(outputs.find(_.specItem == DirectedHtlc(OUT, htlc2)).map(_.output.publicKeyScript).contains(htlcOut2.publicKeyScript)) - assert(outputs.find(_.specItem == DirectedHtlc(OUT, htlc3)).map(_.output.publicKeyScript).contains(htlcOut3.publicKeyScript)) - assert(outputs.find(_.specItem == DirectedHtlc(OUT, htlc4)).map(_.output.publicKeyScript).contains(htlcOut4.publicKeyScript)) - assert(outputs.find(_.specItem == DirectedHtlc(OUT, htlc5)).map(_.output.publicKeyScript).contains(htlcOut5.publicKeyScript)) + assert(outputs.find(_.commitmentOutput == OutHtlc(OutgoingHtlc(htlc2))).map(_.output.publicKeyScript).contains(htlcOut2.publicKeyScript)) + assert(outputs.find(_.commitmentOutput == OutHtlc(OutgoingHtlc(htlc3))).map(_.output.publicKeyScript).contains(htlcOut3.publicKeyScript)) + assert(outputs.find(_.commitmentOutput == OutHtlc(OutgoingHtlc(htlc4))).map(_.output.publicKeyScript).contains(htlcOut4.publicKeyScript)) + assert(outputs.find(_.commitmentOutput == OutHtlc(OutgoingHtlc(htlc5))).map(_.output.publicKeyScript).contains(htlcOut5.publicKeyScript)) } def checkSuccessOrFailTest[T](input: Try[T]) = input match { @@ -391,8 +391,11 @@ class TransactionsSpec extends FunSuite with Logging { case Failure(t) => fail(t) } - def htlc(direction: Direction, amount: Satoshi): DirectedHtlc = - DirectedHtlc(direction, UpdateAddHtlc(ByteVector32.Zeroes, 0, amount.toMilliSatoshi, ByteVector32.Zeroes, CltvExpiry(144), TestConstants.emptyOnionPacket)) + def htlc(direction: Direction, amount: Satoshi): DirectedHtlc = direction match { + case IN => IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, amount.toMilliSatoshi, ByteVector32.Zeroes, CltvExpiry(144), TestConstants.emptyOnionPacket)) + case OUT => OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, amount.toMilliSatoshi, ByteVector32.Zeroes, CltvExpiry(144), TestConstants.emptyOnionPacket)) + } + test("BOLT 2 fee tests") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala index caed96b65d..d97d6b2d40 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala @@ -144,8 +144,8 @@ class ChannelCodecsSpec extends FunSuite { cltvExpiry = CltvExpiry(Random.nextInt(Int.MaxValue)), paymentHash = randomBytes32, onionRoutingPacket = TestConstants.emptyOnionPacket) - val htlc1 = DirectedHtlc(direction = IN, add = add) - val htlc2 = DirectedHtlc(direction = OUT, add = add) + val htlc1 = IncomingHtlc(add) + val htlc2 = OutgoingHtlc(add) assert(htlcCodec.decodeValue(htlcCodec.encode(htlc1).require).require === htlc1) assert(htlcCodec.decodeValue(htlcCodec.encode(htlc2).require).require === htlc2) } @@ -165,9 +165,9 @@ class ChannelCodecsSpec extends FunSuite { cltvExpiry = CltvExpiry(Random.nextInt(Int.MaxValue)), paymentHash = randomBytes32, onionRoutingPacket = TestConstants.emptyOnionPacket) - val htlc1 = DirectedHtlc(direction = IN, add = add1) - val htlc2 = DirectedHtlc(direction = OUT, add = add2) - val htlcs = Set(htlc1, htlc2) + val htlc1 = IncomingHtlc(add1) + val htlc2 = OutgoingHtlc(add2) + val htlcs = Set[DirectedHtlc](htlc1, htlc2) assert(setCodec(htlcCodec).decodeValue(setCodec(htlcCodec).encode(htlcs).require).require === htlcs) val o = CommitmentSpec( htlcs = Set(htlc1, htlc2), @@ -395,12 +395,12 @@ object ChannelCodecsSpec { ByteVector32(hex"0404040404040404040404040404040404040404040404040404040404040404") ) - val htlcs = Seq( - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, 1000000 msat, Crypto.sha256(paymentPreimages(0)), CltvExpiry(500), TestConstants.emptyOnionPacket)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 1, 2000000 msat, Crypto.sha256(paymentPreimages(1)), CltvExpiry(501), TestConstants.emptyOnionPacket)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 30, 2000000 msat, Crypto.sha256(paymentPreimages(2)), CltvExpiry(502), TestConstants.emptyOnionPacket)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 31, 3000000 msat, Crypto.sha256(paymentPreimages(3)), CltvExpiry(503), TestConstants.emptyOnionPacket)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 2, 4000000 msat, Crypto.sha256(paymentPreimages(4)), CltvExpiry(504), TestConstants.emptyOnionPacket)) + val htlcs = Seq[DirectedHtlc]( + IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 1000000 msat, Crypto.sha256(paymentPreimages(0)), CltvExpiry(500), TestConstants.emptyOnionPacket)), + IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 1, 2000000 msat, Crypto.sha256(paymentPreimages(1)), CltvExpiry(501), TestConstants.emptyOnionPacket)), + OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 30, 2000000 msat, Crypto.sha256(paymentPreimages(2)), CltvExpiry(502), TestConstants.emptyOnionPacket)), + OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 31, 3000000 msat, Crypto.sha256(paymentPreimages(3)), CltvExpiry(503), TestConstants.emptyOnionPacket)), + IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 2, 4000000 msat, Crypto.sha256(paymentPreimages(4)), CltvExpiry(504), TestConstants.emptyOnionPacket)) ) val normal = makeChannelDataNormal(htlcs, Map(42L -> Local(UUID.randomUUID, None), 15000L -> Relayed(ByteVector32(ByteVector.fill(32)(42)), 43, 11000000 msat, 10000000 msat))) @@ -412,7 +412,7 @@ object ChannelCodecsSpec { val commitmentInput = Funding.makeFundingInputInfo(fundingTx.hash, 0, fundingAmount, keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey) val localCommit = LocalCommit(0, CommitmentSpec(htlcs.toSet, 1500, 50000000 msat, 70000000 msat), PublishableTxs(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), Nil)) - val remoteCommit = RemoteCommit(0, CommitmentSpec(htlcs.map(htlc => htlc.copy(direction = htlc.direction.opposite)).toSet, 1500, 50000 msat, 700000 msat), ByteVector32(hex"0303030303030303030303030303030303030303030303030303030303030303"), PrivateKey(ByteVector.fill(32)(4)).publicKey) + val remoteCommit = RemoteCommit(0, CommitmentSpec(htlcs.map(_.opposite).toSet, 1500, 50000 msat, 700000 msat), ByteVector32(hex"0303030303030303030303030303030303030303030303030303030303030303"), PrivateKey(ByteVector.fill(32)(4)).publicKey) val commitments = Commitments(ChannelVersion.STANDARD, localParams, remoteParams, channelFlags = 0x01.toByte, localCommit, remoteCommit, LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), localNextHtlcId = 32L, remoteNextHtlcId = 4L, From 49d3e9f880fc8130170ea22dbe931cd16bd9a91f Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 27 Mar 2020 12:18:52 +0100 Subject: [PATCH 3/6] fixup! Match on correct htlc type in 'claimRemoteCommitTxOutput' --- .../src/main/scala/fr/acinq/eclair/channel/Helpers.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 9a9f2f944f..d1aa167bac 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -616,7 +616,7 @@ object Helpers { val txes = remoteCommit.spec.htlcs.collect { // incoming htlc for which we have the preimage: we spend it directly. // NB: we are looking at the remote's commitment, from its point of view it's an outgoing htlc. - case IncomingHtlc(add: UpdateAddHtlc) if preimages.exists(r => sha256(r) == add.paymentHash) => generateTx("claim-htlc-success")(Try { + case OutgoingHtlc(add: UpdateAddHtlc) if preimages.exists(r => sha256(r) == add.paymentHash) => generateTx("claim-htlc-success")(Try { val preimage = preimages.find(r => sha256(r) == add.paymentHash).get val txinfo = Transactions.makeClaimHtlcSuccessTx(remoteCommitTx.tx, outputs, localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc) val sig = keyManager.sign(txinfo, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint) @@ -626,7 +626,7 @@ object Helpers { // (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 OutgoingHtlc(add: UpdateAddHtlc) => generateTx("claim-htlc-timeout")(Try { + case IncomingHtlc(add: UpdateAddHtlc) => generateTx("claim-htlc-timeout")(Try { val txinfo = Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, outputs, localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc) val sig = keyManager.sign(txinfo, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint) Transactions.addSigs(txinfo, sig) From a3dcaa10f853a2cb1ee44f879754cf279b304021 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 27 Mar 2020 12:59:39 +0100 Subject: [PATCH 4/6] Address PR feedback: formatting --- .../fr/acinq/eclair/wire/ChannelCodecs.scala | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala index 25cf904712..cae13cac9b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala @@ -32,7 +32,7 @@ import grizzled.slf4j.Logging import scodec.bits.BitVector import scodec.codecs._ import scodec.{Attempt, Codec} -import shapeless.HNil +import shapeless.{HNil, ::} import scala.compat.Platform import scala.concurrent.duration._ @@ -93,14 +93,15 @@ object ChannelCodecs extends Logging { (wire: BitVector) => bool.decode(wire).map(_.map(b => if (b) IN else OUT)) ) - import shapeless.:: - val htlcCodec: Codec[DirectedHtlc] = ( - ("direction" | directionCodec) :: - ("add" | updateAddHtlcCodec)).xmap( - { case IN :: add :: HNil => IncomingHtlc(add) - case OUT :: add :: HNil => OutgoingHtlc(add) }, - { htlc => htlc.direction :: htlc.add :: HNil } - ) + val htlcCodec: Codec[DirectedHtlc] = (("direction" | directionCodec) :: ("add" | updateAddHtlcCodec)).xmap( + { + case IN :: add :: HNil => IncomingHtlc(add) + case OUT :: add :: HNil => OutgoingHtlc(add) + }, + { + htlc => htlc.direction :: htlc.add :: HNil + } + ) def setCodec[T](codec: Codec[T]): Codec[Set[T]] = Codec[Set[T]]( (elems: Set[T]) => listOfN(uint16, codec).encode(elems.toList), From 60ba2e4c9138ad8e8609db58af1020a808c68609 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 30 Mar 2020 09:22:21 +0200 Subject: [PATCH 5/6] Address PR feedback: refactor code, add non-reg test for new HTLC codec --- .../acinq/eclair/transactions/Transactions.scala | 14 ++++++-------- .../scala/fr/acinq/eclair/wire/ChannelCodecs.scala | 12 +++--------- .../fr/acinq/eclair/wire/ChannelCodecsSpec.scala | 12 ++++++++++++ 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index 360adbb546..fa5478f741 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -361,10 +361,9 @@ object Transactions { def makeClaimHtlcSuccessTx(commitTx: Transaction, outputs: CommitmentOutputs, localDustLimit: Satoshi, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: ByteVector, htlc: UpdateAddHtlc, feeratePerKw: Long): ClaimHtlcSuccessTx = { val redeemScript = htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlc.paymentHash.bytes)) - val Some(outputIndex) = outputs.zipWithIndex.find { - case (CommitmentOutputLink(_, _, OutHtlc(OutgoingHtlc(outgoingHtlc))), _) => outgoingHtlc.id == htlc.id - case _ => false - }.map(_._2) + val Some(outputIndex) = outputs.zipWithIndex.collectFirst { + case (CommitmentOutputLink(_, _, OutHtlc(OutgoingHtlc(outgoingHtlc))), outIndex) if outgoingHtlc.id == htlc.id => outIndex + } val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) @@ -387,10 +386,9 @@ object Transactions { def makeClaimHtlcTimeoutTx(commitTx: Transaction, outputs: CommitmentOutputs, localDustLimit: Satoshi, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: ByteVector, htlc: UpdateAddHtlc, feeratePerKw: Long): ClaimHtlcTimeoutTx = { val redeemScript = htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlc.paymentHash.bytes), htlc.cltvExpiry) - val Some(outputIndex) = outputs.zipWithIndex.find { - case (CommitmentOutputLink(_, _, InHtlc(IncomingHtlc(incomingHtlc))), _) => incomingHtlc.id == htlc.id - case _ => false - }.map(_._2) + val Some(outputIndex) = outputs.zipWithIndex.collectFirst { + case (CommitmentOutputLink(_, _, InHtlc(IncomingHtlc(incomingHtlc))), outIndex) if incomingHtlc.id == htlc.id => outIndex + } val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala index cae13cac9b..40f6df37ea 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala @@ -93,15 +93,9 @@ object ChannelCodecs extends Logging { (wire: BitVector) => bool.decode(wire).map(_.map(b => if (b) IN else OUT)) ) - val htlcCodec: Codec[DirectedHtlc] = (("direction" | directionCodec) :: ("add" | updateAddHtlcCodec)).xmap( - { - case IN :: add :: HNil => IncomingHtlc(add) - case OUT :: add :: HNil => OutgoingHtlc(add) - }, - { - htlc => htlc.direction :: htlc.add :: HNil - } - ) + val htlcCodec: Codec[DirectedHtlc] = discriminated[DirectedHtlc].by(directionCodec) + .typecase(IN, updateAddHtlcCodec.as[IncomingHtlc]) + .typecase(OUT, updateAddHtlcCodec.as[OutgoingHtlc]) def setCodec[T](codec: Codec[T]): Codec[Set[T]] = Codec[Set[T]]( (elems: Set[T]) => listOfN(uint16, codec).encode(elems.toList), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala index d97d6b2d40..978e8e87e5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala @@ -137,6 +137,18 @@ class ChannelCodecsSpec extends FunSuite { } test("encode/decode htlc") { + // these encoded HTLC were produced by a previous version of the codec (at commit 8932785e001ddfe32839b3f83468ea19cf00b289) + val encodedHtlc1 = hex"89d5618930919bd77c07ea3931e7791010a9637c3d06e091b2dad38f21bbcffa00000000384ffa48000000003dbf35101f8724fbd36096ea5f462be20d20bc2f93ebc2c8a3f00b7a78c5158b5a0e9f6b1666923e8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + val encodedHtlc2 = hex"09d5618930919bd77c07ea3931e7791010a9637c3d06e091b2dad38f21bbcffa00000000384ffa48000000003dbf35101f8724fbd36096ea5f462be20d20bc2f93ebc2c8a3f00b7a78c5158b5a0e9f6b1666923e8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + val h1 = htlcCodec.decodeValue(encodedHtlc1.toBitVector).require + val h2 = htlcCodec.decodeValue(encodedHtlc2.toBitVector).require + + assert(h1.direction == IN) + assert(h2.direction == OUT) + assert(htlcCodec.encode(h1).require.bytes === encodedHtlc1) + assert(htlcCodec.encode(h2).require.bytes === encodedHtlc2) + val add = UpdateAddHtlc( channelId = randomBytes32, id = Random.nextInt(Int.MaxValue), From e6d9182a8405e6bbc279949be4a472f95cb7642f Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 30 Mar 2020 10:17:03 +0200 Subject: [PATCH 6/6] Extract non-reg test for htlc codecs in a separate test. --- .../acinq/eclair/wire/ChannelCodecsSpec.scala | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala index 978e8e87e5..21aa6db03e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala @@ -137,18 +137,6 @@ class ChannelCodecsSpec extends FunSuite { } test("encode/decode htlc") { - // these encoded HTLC were produced by a previous version of the codec (at commit 8932785e001ddfe32839b3f83468ea19cf00b289) - val encodedHtlc1 = hex"89d5618930919bd77c07ea3931e7791010a9637c3d06e091b2dad38f21bbcffa00000000384ffa48000000003dbf35101f8724fbd36096ea5f462be20d20bc2f93ebc2c8a3f00b7a78c5158b5a0e9f6b1666923e8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - val encodedHtlc2 = hex"09d5618930919bd77c07ea3931e7791010a9637c3d06e091b2dad38f21bbcffa00000000384ffa48000000003dbf35101f8724fbd36096ea5f462be20d20bc2f93ebc2c8a3f00b7a78c5158b5a0e9f6b1666923e8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - - val h1 = htlcCodec.decodeValue(encodedHtlc1.toBitVector).require - val h2 = htlcCodec.decodeValue(encodedHtlc2.toBitVector).require - - assert(h1.direction == IN) - assert(h2.direction == OUT) - assert(htlcCodec.encode(h1).require.bytes === encodedHtlc1) - assert(htlcCodec.encode(h2).require.bytes === encodedHtlc2) - val add = UpdateAddHtlc( channelId = randomBytes32, id = Random.nextInt(Int.MaxValue), @@ -226,6 +214,20 @@ class ChannelCodecsSpec extends FunSuite { assert(spentMapCodec.decodeValue(spentMapCodec.encode(map).require).require === map) } + test("backward compatibility of htlc codecs") { + // these encoded HTLC were produced by a previous version of the codec (at commit 8932785e001ddfe32839b3f83468ea19cf00b289) + val encodedHtlc1 = hex"89d5618930919bd77c07ea3931e7791010a9637c3d06e091b2dad38f21bbcffa00000000384ffa48000000003dbf35101f8724fbd36096ea5f462be20d20bc2f93ebc2c8a3f00b7a78c5158b5a0e9f6b1666923e8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + val encodedHtlc2 = hex"09d5618930919bd77c07ea3931e7791010a9637c3d06e091b2dad38f21bbcffa00000000384ffa48000000003dbf35101f8724fbd36096ea5f462be20d20bc2f93ebc2c8a3f00b7a78c5158b5a0e9f6b1666923e8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + val h1 = htlcCodec.decodeValue(encodedHtlc1.toBitVector).require + val h2 = htlcCodec.decodeValue(encodedHtlc2.toBitVector).require + + assert(h1.direction == IN) + assert(h2.direction == OUT) + assert(htlcCodec.encode(h1).require.bytes === encodedHtlc1) + assert(htlcCodec.encode(h2).require.bytes === encodedHtlc2) + } + test("basic serialization test (NORMAL)") { val data = normal val bin = ChannelCodecs.DATA_NORMAL_Codec.encode(data).require