diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-node/src/main/scala/fr/acinq/eclair/channel/Channel.scala index cd3c7d5c8..ca944f91f 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -6,6 +6,7 @@ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel.Helpers.{Closing, Funding} import fr.acinq.eclair.crypto.{Generators, ShaChain} +import fr.acinq.eclair.transactions.Transactions.{ClaimHtlcSuccessTx, ClaimHtlcTimeoutTx} import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire._ @@ -300,13 +301,17 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto case Event(cmd: CMD_CLOSE, d: DATA_WAIT_FOR_FUNDING_LOCKED_INTERNAL) => blockchain ! Publish(d.commitments.localCommit.publishableTxs._1.tx) blockchain ! WatchConfirmed(self, d.commitments.localCommit.publishableTxs._1.tx.txid, d.params.minimumDepth, BITCOIN_CLOSE_DONE) - goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.localCommit.publishableTxs._1.tx)) + // there can't be htlcs at this stage + val localCommitPublished = LocalCommitPublished(d.commitments.localCommit.publishableTxs._1.tx, Nil, Nil, Nil) + goto(CLOSING) using DATA_CLOSING(d.commitments, localCommitPublished = Some(localCommitPublished)) case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_LOCKED_INTERNAL) => log.error(s"peer sent $e, closing connection") // see bolt #2: A node MUST fail the connection if it receives an err message blockchain ! Publish(d.commitments.localCommit.publishableTxs._1.tx) blockchain ! WatchConfirmed(self, d.commitments.localCommit.publishableTxs._1.tx.txid, d.params.minimumDepth, BITCOIN_CLOSE_DONE) - goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.localCommit.publishableTxs._1.tx)) + // there can't be htlcs at this stage + val localCommitPublished = LocalCommitPublished(d.commitments.localCommit.publishableTxs._1.tx, Nil, Nil, Nil) + goto(CLOSING) using DATA_CLOSING(d.commitments, localCommitPublished = Some(localCommitPublished)) }) when(WAIT_FOR_FUNDING_LOCKED)(handleExceptions { @@ -322,7 +327,9 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto case Event(cmd: CMD_CLOSE, d: DATA_NORMAL) => blockchain ! Publish(d.commitments.localCommit.publishableTxs._1.tx) blockchain ! WatchConfirmed(self, d.commitments.localCommit.publishableTxs._1.tx.txid, d.params.minimumDepth, BITCOIN_CLOSE_DONE) - goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.localCommit.publishableTxs._1.tx)) + // there can't be htlcs at this stage + val localCommitPublished = LocalCommitPublished(d.commitments.localCommit.publishableTxs._1.tx, Nil, Nil, Nil) + goto(CLOSING) using DATA_CLOSING(d.commitments, localCommitPublished = Some(localCommitPublished)) case Event(e: Error, d: DATA_NORMAL) => handleRemoteError(e, d) }) @@ -636,11 +643,11 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto case Event(BITCOIN_CLOSE_DONE, d: DATA_CLOSING) if d.mutualClosePublished.isDefined => goto(CLOSED) - case Event(BITCOIN_SPEND_OURS_DONE, d: DATA_CLOSING) if d.ourCommitPublished.isDefined => goto(CLOSED) + case Event(BITCOIN_SPEND_OURS_DONE, d: DATA_CLOSING) if d.localCommitPublished.isDefined => goto(CLOSED) - case Event(BITCOIN_SPEND_THEIRS_DONE, d: DATA_CLOSING) if d.theirCommitPublished.isDefined => goto(CLOSED) + case Event(BITCOIN_SPEND_THEIRS_DONE, d: DATA_CLOSING) if d.remoteCommitPublished.isDefined => goto(CLOSED) - case Event(BITCOIN_STEAL_DONE, d: DATA_CLOSING) if d.revokedPublished.size > 0 => goto(CLOSED) + case Event(BITCOIN_STEAL_DONE, d: DATA_CLOSING) if d.revokedCommitPublished.size > 0 => goto(CLOSED) case Event(e: Error, d: DATA_CLOSING) => stay // nothing to do, there is already a spending tx published } @@ -797,53 +804,54 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto val txs = txs1 ++ txs2 txs.map(tx => blockchain ! PublishAsap(tx))*/ + // TODO: remove Nils + val localCommitPublished = LocalCommitPublished(tx, Nil, Nil, Nil) + val nextData = d match { - case closing: DATA_CLOSING => closing.copy(ourCommitPublished = Some(tx)) - case _ => DATA_CLOSING(d.commitments, ourCommitPublished = Some(tx)) + case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished)) + case _ => DATA_CLOSING(d.commitments, localCommitPublished = Some(localCommitPublished)) } goto(CLOSING) using nextData } def handleRemoteSpentCurrent(tx: Transaction, d: HasCommitments) = { - log.warning(s"they published their current commit in txid=${ - tx.txid - }") - assert(tx.txid == d.commitments.remoteCommit.txid) + log.warning(s"they published their current commit in txid=${tx.txid}") + require(tx.txid == d.commitments.remoteCommit.txid, "txid mismatch") blockchain ! WatchConfirmed(self, tx.txid, 3, BITCOIN_SPEND_THEIRS_DONE) // TODO hardcoded mindepth - val txs1: Seq[Transaction] = ??? - //claimReceivedHtlcs(tx, Commitments.makeRemoteTxTemplate(d.commitments), d.commitments) - val txs2: Seq[Transaction] = ??? - //claimSentHtlcs(tx, Commitments.makeRemoteTxTemplate(d.commitments), d.commitments) - val txs = txs1 ++ txs2 - txs.map(tx => blockchain ! PublishAsap(tx)) + val claimTxs = Helpers.Closing.claimRemoteCommitTxOutputs(d.commitments, tx) + claimTxs.map(txinfo => blockchain ! PublishAsap(txinfo.tx)) + + val remoteCommitPublished = RemoteCommitPublished( + commitTx = tx, + claimHtlcSuccessTxs = claimTxs.collect { case c: ClaimHtlcSuccessTx => c.tx }, + claimHtlcTimeoutTxs = claimTxs.collect { case c: ClaimHtlcTimeoutTx => c.tx } + ) val nextData = d match { - case closing: DATA_CLOSING => closing.copy(theirCommitPublished = Some(tx)) - case _ => DATA_CLOSING(d.commitments, theirCommitPublished = Some(tx)) + case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished)) + case _ => DATA_CLOSING(d.commitments, remoteCommitPublished = Some(remoteCommitPublished)) } goto(CLOSING) using nextData } def handleRemoteSpentOther(tx: Transaction, d: HasCommitments) = { - log.warning(s"funding tx spent in txid=${ - tx.txid - }") + log.warning(s"funding tx spent in txid=${tx.txid}") d.commitments.txDb.get(tx.txid) match { case Some(spendingTx) => - log.warning(s"txid=${ - tx.txid - } was a revoked commitment, publishing the punishment tx") + log.warning(s"txid=${tx.txid} was a revoked commitment, publishing the punishment tx") them ! Error(0, "Anchor has been spent".getBytes) blockchain ! Publish(spendingTx) - blockchain ! WatchConfirmed(self, spendingTx.txid, 3, BITCOIN_STEAL_DONE) - // TODO hardcoded mindepth + blockchain ! WatchConfirmed(self, spendingTx.txid, 3, BITCOIN_STEAL_DONE) // TODO hardcoded mindepth + + val remoteCommitPublished = RevokedCommitPublished(tx) + val nextData = d match { - case closing: DATA_CLOSING => closing.copy(revokedPublished = closing.revokedPublished :+ tx) - case _ => DATA_CLOSING(d.commitments, revokedPublished = Seq(tx)) + case closing: DATA_CLOSING => closing.copy(revokedCommitPublished = closing.revokedCommitPublished :+ remoteCommitPublished) + case _ => DATA_CLOSING(d.commitments, revokedCommitPublished = remoteCommitPublished :: Nil) } goto(CLOSING) using nextData case None => diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala b/eclair-node/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala index d87fe5bb0..0cc0e8580 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala @@ -121,6 +121,10 @@ trait HasCommitments extends Data { def commitments: Commitments } +case class LocalCommitPublished(commitTx: Transaction, htlcSuccessTxs: Seq[Transaction], htlcTimeoutTxs: Seq[Transaction], claimHtlcDelayedTx: Seq[Transaction]) +case class RemoteCommitPublished(commitTx: Transaction, claimHtlcSuccessTxs: Seq[Transaction], claimHtlcTimeoutTxs: Seq[Transaction]) +case class RevokedCommitPublished(commitTxs: Transaction) + final case class DATA_WAIT_FOR_OPEN_CHANNEL(localParams: LocalParams, autoSignInterval: Option[FiniteDuration]) extends Data final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(temporaryChannelId: Long, localParams: LocalParams, fundingSatoshis: Long, pushMsat: Long, autoSignInterval: Option[FiniteDuration]) extends Data final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: Long, params: ChannelParams, pushMsat: Long, remoteFirstPerCommitmentPoint: BinaryData) extends Data @@ -136,10 +140,10 @@ final case class DATA_NEGOTIATING(channelId: Long, params: ChannelParams, commit final case class DATA_CLOSING(commitments: Commitments, ourSignature: Option[ClosingSigned] = None, mutualClosePublished: Option[Transaction] = None, - ourCommitPublished: Option[Transaction] = None, - theirCommitPublished: Option[Transaction] = None, - revokedPublished: Seq[Transaction] = Seq()) extends Data with HasCommitments { - assert(mutualClosePublished.isDefined || ourCommitPublished.isDefined || theirCommitPublished.isDefined || revokedPublished.size > 0, "there should be at least one tx published in this state") + localCommitPublished: Option[LocalCommitPublished] = None, + remoteCommitPublished: Option[RemoteCommitPublished] = None, + revokedCommitPublished: Seq[RevokedCommitPublished] = Nil) extends Data with HasCommitments { + require(mutualClosePublished.isDefined || localCommitPublished.isDefined || remoteCommitPublished.isDefined || revokedCommitPublished.size > 0, "there should be at least one tx published in this state") } final case class ChannelParams(localParams: LocalParams, diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-node/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 9ed99507c..0d91f971b 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -1,7 +1,7 @@ package fr.acinq.eclair.channel import fr.acinq.bitcoin.Crypto.{Point, Scalar} -import fr.acinq.bitcoin.{BinaryData, Crypto, Satoshi, Transaction} +import fr.acinq.bitcoin.{BinaryData, Crypto, Satoshi} import fr.acinq.eclair.crypto.LightningCrypto.sha256 import fr.acinq.eclair.crypto.{Generators, ShaChain} import fr.acinq.eclair.transactions.Transactions._ @@ -86,7 +86,7 @@ object Commitments { } def sendFulfill(commitments: Commitments, cmd: CMD_FULFILL_HTLC): (Commitments, UpdateFulfillHtlc) = { - commitments.localCommit.spec.htlcs.collectFirst { case u: Htlc if u.add.id == cmd.id => u.add } match { + commitments.localCommit.spec.htlcs.collectFirst { case u: Htlc if u.direction == IN && u.add.id == cmd.id => u.add } match { case Some(htlc) if htlc.paymentHash == sha256(cmd.r) => val fulfill = UpdateFulfillHtlc(commitments.channelId, cmd.id, cmd.r) val commitments1 = addLocalProposal(commitments, fulfill) @@ -97,7 +97,7 @@ object Commitments { } def receiveFulfill(commitments: Commitments, fulfill: UpdateFulfillHtlc): (Commitments, UpdateAddHtlc) = { - commitments.remoteCommit.spec.htlcs.collectFirst { case u: Htlc if u.add.id == fulfill.id => u.add } match { + commitments.remoteCommit.spec.htlcs.collectFirst { case u: Htlc if u.direction == IN && u.add.id == fulfill.id => u.add } match { case Some(htlc) if htlc.paymentHash == sha256(fulfill.paymentPreimage) => (addRemoteProposal(commitments, fulfill), htlc) case Some(htlc) => throw new RuntimeException(s"invalid htlc preimage for htlc id=${fulfill.id}") case None => throw new RuntimeException(s"unknown htlc id=${fulfill.id}") // TODO: we should fail the channel @@ -220,7 +220,7 @@ object Commitments { // TODO: Long or Int?? val revocation = RevokeAndAck( channelId = commitments.channelId, - perCommitmentSecret = localPerCommitmentSecret, + perCommitmentSecret = localPerCommitmentSecret.toBin.take(32), nextPerCommitmentPoint = localNextPerCommitmentPoint, htlcTimeoutSignatures = timeoutHtlcSigs.toList ) @@ -238,7 +238,7 @@ object Commitments { import commitments._ // we receive a revocation because we just sent them a sig for their next commit tx remoteNextCommitInfo match { - case Left(_) if Scalar(revocation.perCommitmentSecret).toPoint != remoteCommit.remotePerCommitmentPoint => + case Left(_) if Scalar(revocation.perCommitmentSecret :+ 1.toByte).toPoint != remoteCommit.remotePerCommitmentPoint => throw new RuntimeException("invalid preimage") case Left(theirNextCommit) => // we rebuild the transactions a 2nd time but we are just interested in HTLC-timeout txs because we need to check their sig diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-node/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 4b7239cde..faa625e03 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -3,10 +3,11 @@ package fr.acinq.eclair.channel import fr.acinq.bitcoin.Script._ import fr.acinq.bitcoin.{OutPoint, _} import fr.acinq.eclair.crypto.Generators +import fr.acinq.eclair.crypto.LightningCrypto.sha256 import fr.acinq.eclair.transactions.Scripts._ -import fr.acinq.eclair.transactions.Transactions.{ClosingTx, CommitTx, InputInfo} +import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions._ -import fr.acinq.eclair.wire.ClosingSigned +import fr.acinq.eclair.wire.{ClosingSigned, UpdateAddHtlc, UpdateFulfillHtlc} import scala.util.Try @@ -94,131 +95,49 @@ object Helpers { } /** - * Claim a revoked commit tx using the matching revocation preimage, which allows us to claim all its inputs without a - * delay * - * @param theirTxTemplate revoked commit tx template - * @param revocationPreimage revocation preimage (which must match this specific commit tx) - * @param privateKey private key to send the claimed funds to (the returned tx will include a single P2WPKH output) - * @return a signed transaction that spends the revoked commit tx - */ - def claimRevokedCommitTx(theirTxTemplate: CommitTx, revocationPreimage: BinaryData, privateKey: BinaryData): Transaction = ??? - - /*{ - val theirTx = theirTxTemplate.makeTx - val outputs = collection.mutable.ListBuffer.empty[TxOut] - - // first, find out how much we can claim - val outputsToClaim = (theirTxTemplate.localOutput.toSeq ++ theirTxTemplate.htlcReceivedOutputs ++ theirTxTemplate.htlcOfferedOutputs).filter(o => theirTx.txOut.indexOf(o.txOut) != -1) - val totalAmount = outputsToClaim.map(_.amount).sum // TODO: substract a small network fee - - // create a tx that sends everything to our private key - val tx = Transaction(version = 2, - txIn = Seq.empty[TxIn], - txOut = TxOut(totalAmount, pay2wpkh(Crypto.publicKeyFromPrivateKey(privateKey))) :: Nil, - lockTime = 0) - - // create tx inputs that spend each output that we can spend - val inputs = outputsToClaim.map(outputTemplate => { - val index = theirTx.txOut.indexOf(outputTemplate.txOut) - TxIn(OutPoint(theirTx, index), signatureScript = BinaryData.empty, sequence = 0xffffffffL) - }) - assert(inputs.length == outputsToClaim.length) - - // and sign them - val tx1 = tx.copy(txIn = inputs) - val witnesses = for (i <- 0 until tx1.txIn.length) yield { - val sig = Transaction.signInput(tx1, i, outputsToClaim(i).redeemScript, SIGHASH_ALL, outputsToClaim(i).amount, 1, privateKey) - val witness = ScriptWitness(sig :: revocationPreimage :: outputsToClaim(i).redeemScript :: Nil) - witness - } - - tx1.updateWitnesses(witnesses) - }*/ - - /** - * claim an HTLC that we received using its payment preimage. This is used only when the other party publishes its - * current commit tx which contains pending HTLCs. - * - * @param tx commit tx published by the other party - * @param htlcTemplate HTLC template for an HTLC in the commit tx for which we have the preimage - * @param paymentPreimage HTLC preimage - * @param privateKey private key which matches the pubkey that the HTLC was sent to - * @return a signed transaction that spends the HTLC in their published commit tx. - * This tx is not spendable right away: it has both an absolute CLTV time-out and a relative CSV time-out - * before which it can be published - */ - //def claimReceivedHtlc(tx: Transaction, htlcTemplate: ReceivedHTLC, paymentPreimage: BinaryData, privateKey: BinaryData): Transaction = ??? - /*{ - require(htlcTemplate.htlc.add.paymentHash == BinaryData(Crypto.sha256(paymentPreimage)), "invalid payment preimage") - // find its index in their tx - val index = tx.txOut.indexOf(htlcTemplate.txOut) - - val tx1 = Transaction(version = 2, - txIn = TxIn(OutPoint(tx, index), BinaryData.empty, sequence = Common.toSelfDelay2csv(htlcTemplate.delay)) :: Nil, - txOut = TxOut(htlcTemplate.amount, Common.pay2pkh(Crypto.publicKeyFromPrivateKey(privateKey))) :: Nil, - lockTime = ??? /*Scripts.locktime2long_cltv(htlcTemplate.htlc.add.expiry)*/) - - val sig = Transaction.signInput(tx1, 0, htlcTemplate.redeemScript, SIGHASH_ALL, htlcTemplate.amount, 1, privateKey) - val witness = ScriptWitness(sig :: paymentPreimage :: htlcTemplate.redeemScript :: Nil) - val tx2 = tx1.updateWitness(0, witness) - tx2 - }*/ - - /** * claim all the HTLCs that we've received from their current commit tx * - * @param txTemplate commit tx published by the other party * @param commitments our commitment data, which include payment preimages * @return a list of transactions (one per HTLC that we can claim) */ - //def claimReceivedHtlcs(tx: Transaction, txTemplate: CommitTxTemplate, commitments: Commitments): Seq[Transaction] = ??? - /*{ - val preImages = commitments.localChanges.all.collect { case UpdateFulfillHtlc(_, id, paymentPreimage) => paymentPreimage } - // TODO: FIXME !!! - //val htlcTemplates = txTemplate.htlcSent - val htlcTemplates = txTemplate.htlcReceived ++ txTemplate.htlcSent + def claimRemoteCommitTxOutputs(commitments: Commitments, tx: Transaction): Seq[TransactionWithInputInfo] = { + import commitments._ + require(remoteCommit.txid == tx.txid, "txid mismatch, provided tx is not the current remote commit tx") + val (remoteCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = Commitments.makeRemoteTxs(remoteCommit.index, localParams, remoteParams, commitInput, remoteCommit.remotePerCommitmentPoint, remoteCommit.spec) + require(remoteCommitTx.tx.txid == tx.txid, "txid mismatch, cannot recompute the current remote commit tx") - //@tailrec - def loop(htlcs: Seq[HTLCTemplate], acc: Seq[Transaction] = Seq.empty[Transaction]): Seq[Transaction] = Nil + val localPubkey = Generators.derivePubKey(localParams.paymentKey.toPoint, remoteCommit.remotePerCommitmentPoint) + val remotePubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remoteCommit.remotePerCommitmentPoint) + val localPrivkey = Generators.derivePrivKey(localParams.paymentKey, remoteCommit.remotePerCommitmentPoint) - /*{ - htlcs.headOption match { - case Some(head) => - preImages.find(preImage => head.htlc.add.rHash == bin2sha256(Crypto.sha256(preImage))) match { - case Some(preImage) => loop(htlcs.tail, claimReceivedHtlc(tx, head, preImage, commitments.ourParams.finalPrivKey) +: acc) - case None => loop(htlcs.tail, acc) - } - case None => acc + // those are the preimages to existing received htlcs + val preimages = commitments.localChanges.all.collect { case u: UpdateFulfillHtlc => u.paymentPreimage } + + // TODO: final key is the payment pubkey so that it matches the main outputs, is that the best option? + + // remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa + val txes = commitments.remoteCommit.spec.htlcs.collect { + // incoming htlc for which we have the preimage: we spend it directly + case Htlc(OUT, add: UpdateAddHtlc, _) if preimages.exists(r => sha256(r) == add.paymentHash) => + val preimage = preimages.find(r => sha256(r) == add.paymentHash).get + val tx = Transactions.makeClaimHtlcSuccessTx(remoteCommitTx.tx, localPubkey, remotePubkey, localPubkey, add) + val sig = Transactions.sign(tx, localPrivkey) + Transactions.addSigs(tx, sig, preimage) + // NB: 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 Htlc(IN, add: UpdateAddHtlc, _) => + val tx = Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, localPubkey, remotePubkey, localPubkey, add) + val sig = Transactions.sign(tx, localPrivkey) + Transactions.addSigs(tx, sig) } - }*/ - loop(htlcTemplates) - }*/ - //def claimSentHtlc(tx: Transaction, htlcTemplate: OfferedHTLCOutputTemplate, privateKey: BinaryData): Transaction = ??? - /*{ - val index = tx.txOut.indexOf(htlcTemplate.txOut) - val tx1 = Transaction( - version = 2, - txIn = TxIn(OutPoint(tx, index), Array.emptyByteArray, sequence = Common.toSelfDelay2csv(htlcTemplate.delay)) :: Nil, - txOut = TxOut(htlcTemplate.amount, Common.pay2pkh(Crypto.publicKeyFromPrivateKey(privateKey))) :: Nil, - lockTime = ??? /*Scripts.locktime2long_cltv(htlcTemplate.htlc.add.expiry)*/) + // OPTIONAL: let's check transactions are actually spendable + require(txes.forall(Transactions.checkSpendable(_).isSuccess), "the tx we produced are not spendable!") - val sig = Transaction.signInput(tx1, 0, htlcTemplate.redeemScript, SIGHASH_ALL, htlcTemplate.amount, 1, privateKey) - val witness = ScriptWitness(sig :: Hash.Zeroes :: htlcTemplate.redeemScript :: Nil) - tx1.updateWitness(0, witness) - }*/ + txes.toSeq + } - // TODO: fix this! - //def claimSentHtlcs(tx: Transaction, txTemplate: CommitTxTemplate, commitments: Commitments): Seq[Transaction] = Nil - - /*{ - // txTemplate could be our template (we published our commit tx) or their template (they published their commit tx) - val htlcs1 = txTemplate.htlcSent.filter(_.ourKey == commitments.ourParams.finalPubKey) - val htlcs2 = txTemplate.htlcReceived.filter(_.theirKey == commitments.ourParams.finalPubKey) - val htlcs = htlcs1 ++ htlcs2 - htlcs.map(htlcTemplate => claimSentHtlc(tx, htlcTemplate, commitments.ourParams.finalPrivKey)) - }*/ } } \ No newline at end of file diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/crypto/Generators.scala b/eclair-node/src/main/scala/fr/acinq/eclair/crypto/Generators.scala index 7d3dd4428..e6223a571 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/crypto/Generators.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/crypto/Generators.scala @@ -13,7 +13,7 @@ object Generators { case length if length < 32 => Array.fill(32 - length)(0.toByte) ++ data } - def perCommitSecret(seed: BinaryData, index: Int): Scalar = Scalar(ShaChain.shaChainFromSeed(seed, index) :+ 1.toByte) + def perCommitSecret(seed: BinaryData, index: Int): Scalar = Scalar(ShaChain.shaChainFromSeed(seed, 0xFFFFFFFFFFFFFFFFL - index) :+ 1.toByte) def perCommitPoint(seed: BinaryData, index: Int): Point = perCommitSecret(seed, index).toPoint diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-node/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index bcc46ace1..552dd1d91 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -20,6 +20,7 @@ object Transactions { def input: InputInfo def tx: Transaction } + case class CommitTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo case class HtlcSuccessTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo case class HtlcTimeoutTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo @@ -29,6 +30,18 @@ object Transactions { case class ClosingTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo // @formatter:on + /** + * When *local* *current* [[CommitTx]] is published: + * - [[HtlcSuccessTx]] spends htlc-received outputs of [[CommitTx]] for which we have the preimage + * - [[ClaimHtlcDelayedTx]] spends [[HtlcSuccessTx]] after a delay + * - [[HtlcTimeoutTx]] spends htlc-sent outputs of [[CommitTx]] after a timeout + * - [[ClaimHtlcDelayedTx]] spends [[HtlcTimeoutTx]] after a delay + * + * When *remote* *current* [[CommitTx]] is published: + * - [[ClaimHtlcSuccessTx]] spends htlc-received outputs of [[CommitTx]] for which we have the preimage + * - [[ClaimHtlcTimeoutTx]] spends htlc-received outputs of [[CommitTx]] for which we have the preimage + */ + val commitWeight = 724 val htlcTimeoutWeight = 634 val htlcSuccessWeight = 671 @@ -114,6 +127,7 @@ object Transactions { .filter(htlc => (MilliSatoshi(htlc.add.amountMsat) - htlcSuccessFee).compare(localDustLimit) > 0) .map(htlc => TxOut(MilliSatoshi(htlc.add.amountMsat), pay2wsh(htlcReceived(localPubkey, remotePubkey, ripemd160(htlc.add.paymentHash), htlc.add.expiry)))) + // TODO: txnumber can't be > 2^48 val txnumber = obscuredCommitTxNumber(commitTxNumber, localPaymentBasePoint, remotePaymentBasePoint) val tx = Transaction( diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala b/eclair-node/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala index 693937b7d..3b4e903c3 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala @@ -45,6 +45,7 @@ trait StateTestsHelperMethods extends TestKitBase { r2s.expectMsgType[RevokeAndAck] r2s.forward(s) awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.localCommit.index == rCommitIndex + 1) + awaitCond(s.stateData.asInstanceOf[HasCommitments].commitments.remoteCommit.index == rCommitIndex + 1) } } diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 636fb800e..d11e8ca48 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -261,19 +261,19 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods { within(30 seconds) { val sender = TestProbe() - val (r1, htlc1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - // a->b (regular) - val (r2, htlc2) = addHtlc(8000000, alice, bob, alice2bob, bob2alice) - // a->b (regular) - val (r3, htlc3) = addHtlc(300000, bob, alice, bob2alice, alice2bob) - // b->a (dust) - val (r4, htlc4) = addHtlc(1000000, alice, bob, alice2bob, bob2alice) - // a->b (regular) - val (r5, htlc5) = addHtlc(50000000, bob, alice, bob2alice, alice2bob) - // b->a (regular) - val (r6, htlc6) = addHtlc(500000, alice, bob, alice2bob, bob2alice) - // a->b (dust) - val (r7, htlc7) = addHtlc(4000000, bob, alice, bob2alice, alice2bob) // b->a (regular) + val (r1, htlc1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) // a->b (regular) + + val (r2, htlc2) = addHtlc(8000000, alice, bob, alice2bob, bob2alice) // a->b (regular) + + val (r3, htlc3) = addHtlc(300000, bob, alice, bob2alice, alice2bob) // b->a (dust) + + val (r4, htlc4) = addHtlc(1000000, alice, bob, alice2bob, bob2alice) // a->b (regular) + + val (r5, htlc5) = addHtlc(50000000, bob, alice, bob2alice, alice2bob) // b->a (regular) + + val (r6, htlc6) = addHtlc(500000, alice, bob, alice2bob, bob2alice) // a->b (dust) + + val (r7, htlc7) = addHtlc(4000000, bob, alice, bob2alice, alice2bob) // b->a (regular) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") @@ -288,6 +288,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods { bob2alice.expectMsgType[CommitSig] bob2alice.forward(alice) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 1) assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs._2.size == 3) } } @@ -395,19 +396,19 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods { test("recv RevokeAndAck (multiple htlcs in both directions)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain) => within(30 seconds) { val sender = TestProbe() - val (r1, htlc1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) - // a->b (regular) - val (r2, htlc2) = addHtlc(8000000, alice, bob, alice2bob, bob2alice) - // a->b (regular) - val (r3, htlc3) = addHtlc(300000, bob, alice, bob2alice, alice2bob) - // b->a (dust) - val (r4, htlc4) = addHtlc(1000000, alice, bob, alice2bob, bob2alice) - // a->b (regular) - val (r5, htlc5) = addHtlc(50000000, bob, alice, bob2alice, alice2bob) - // b->a (regular) - val (r6, htlc6) = addHtlc(500000, alice, bob, alice2bob, bob2alice) - // a->b (dust) - val (r7, htlc7) = addHtlc(4000000, bob, alice, bob2alice, alice2bob) // b->a (regular) + val (r1, htlc1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) // a->b (regular) + + val (r2, htlc2) = addHtlc(8000000, alice, bob, alice2bob, bob2alice) // a->b (regular) + + val (r3, htlc3) = addHtlc(300000, bob, alice, bob2alice, alice2bob) // b->a (dust) + + val (r4, htlc4) = addHtlc(1000000, alice, bob, alice2bob, bob2alice) // a->b (regular) + + val (r5, htlc5) = addHtlc(50000000, bob, alice, bob2alice, alice2bob) // b->a (regular) + + val (r6, htlc6) = addHtlc(500000, alice, bob, alice2bob, bob2alice) // a->b (dust) + + val (r7, htlc7) = addHtlc(4000000, bob, alice, bob2alice, alice2bob) // b->a (regular) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") @@ -425,7 +426,10 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods { // actual test begins alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) + awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteCommit.index == 1) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteCommit.spec.htlcs.size == 7) } } @@ -782,32 +786,25 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods { within(30 seconds) { val sender = TestProbe() - val (r1, htlc1) = addHtlc(300000000, alice, bob, alice2bob, bob2alice) - // id 1 - val (r2, htlc2) = addHtlc(200000000, alice, bob, alice2bob, bob2alice) - // id 2 - val (r3, htlc3) = addHtlc(100000000, alice, bob, alice2bob, bob2alice) // id 3 + val (ra1, htlca1) = addHtlc(250000000, alice, bob, alice2bob, bob2alice) + val (ra2, htlca2) = addHtlc(100000000, alice, bob, alice2bob, bob2alice) + val (rb1, htlcb1) = addHtlc(50000000, bob, alice, bob2alice, alice2bob) + val (rb2, htlcb2) = addHtlc(55000000, bob, alice, bob2alice, alice2bob) sign(alice, bob, alice2bob, bob2alice) - fulfillHtlc(1, r1, bob, alice, bob2alice, alice2bob) sign(bob, alice, bob2alice, alice2bob) sign(alice, bob, alice2bob, bob2alice) - val (r4, htlc4) = addHtlc(150000000, bob, alice, bob2alice, alice2bob) - // id 1 - val (r5, htlc5) = addHtlc(120000000, bob, alice, bob2alice, alice2bob) // id 2 - sign(bob, alice, bob2alice, alice2bob) - sign(alice, bob, alice2bob, bob2alice) - fulfillHtlc(2, r2, bob, alice, bob2alice, alice2bob) - fulfillHtlc(1, r4, alice, bob, alice2bob, bob2alice) + fulfillHtlc(2, ra2, bob, alice, bob2alice, alice2bob) + fulfillHtlc(1, rb1, alice, bob, alice2bob, bob2alice) // at this point here is the situation from alice pov and what she should do : // balances : - // alice's balance : 400 000 000 => nothing to do - // bob's balance : 30 000 000 => nothing to do + // alice's balance : 450 000 000 => nothing to do + // bob's balance : 95 000 000 => nothing to do // htlcs : - // alice -> bob : 200 000 000 (bob has the r) => if bob does not use the r, wait for the timeout and spend - // alice -> bob : 100 000 000 (bob does not have the r) => wait for the timeout and spend - // bob -> alice : 150 000 000 (alice has the r) => spend immediately using the r - // bob -> alice : 120 000 000 (alice does not have the r) => nothing to do, bob will get his money back after the timeout + // alice -> bob : 250 000 000 (bob does not have the preimage) => wait for the timeout and spend + // alice -> bob : 100 000 000 (bob has the preimage) => if bob does not use the preimage, wait for the timeout and spend + // bob -> alice : 50 000 000 (alice has the preimage) => spend immediately using the preimage + // bob -> alice : 55 000 000 (alice does not have the preimage) => nothing to do, bob will get his money back after the timeout // bob publishes his current commit tx val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs._1.tx @@ -816,8 +813,8 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods { alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid + // alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage val amountClaimed = (for (i <- 0 until 3) yield { - // alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the htlc val claimHtlcTx = alice2blockchain.expectMsgType[PublishAsap].tx assert(claimHtlcTx.txIn.size == 1) val previousOutputs = Map(claimHtlcTx.txIn(0).outPoint -> bobCommitTx.txOut(claimHtlcTx.txIn(0).outPoint.index.toInt)) @@ -825,10 +822,12 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods { assert(claimHtlcTx.txOut.size == 1) claimHtlcTx.txOut(0).amount }).sum - assert(amountClaimed == Satoshi(450000)) + assert(amountClaimed == Satoshi(400000)) awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].theirCommitPublished == Some(bobCommitTx)) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimHtlcSuccessTxs.size == 1) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimHtlcTimeoutTxs.size == 2) } } @@ -836,25 +835,12 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods { within(30 seconds) { val sender = TestProbe() - // alice sends 300 000 sat and bob fulfills - // we reuse the same r (it doesn't matter here) - val (r, htlc) = addHtlc(300000000, alice, bob, alice2bob, bob2alice) - sign(alice, bob, alice2bob, bob2alice) - - sender.send(bob, CMD_FULFILL_HTLC(1, r)) - sender.expectMsg("ok") - val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc] - bob2alice.forward(alice) - - sign(bob, alice, bob2alice, alice2bob) - - // at this point we have : - // alice = 700 000 - // bob = 300 000 + // initally we have : + // alice = 800 000 + // bob = 200 000 def send(): Transaction = { - // alice sends 1 000 sat - // we reuse the same r (it doesn't matter here) - val (r, htlc) = addHtlc(1000000, alice, bob, alice2bob, bob2alice) + // alice sends 10 000 sat + val (r, htlc) = addHtlc(10000000, alice, bob, alice2bob, bob2alice) sign(alice, bob, alice2bob, bob2alice) bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs._1.tx @@ -866,9 +852,12 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods { // let's say that bob published this tx val revokedTx = txs(3) // channel state for this revoked tx is as follows: - // alice = 696 000 - // bob = 300 000 - // a->b = 4 000 + // alice = 760 000 + // bob = 200 000 + // a->b = 10 000 + // a->b = 10 000 + // a->b = 10 000 + // a->b = 10 000 alice ! (BITCOIN_FUNDING_SPENT, revokedTx) alice2bob.expectMsgType[Error] val punishTx = alice2blockchain.expectMsgType[Publish].tx @@ -932,7 +921,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods { assert(amountClaimed == Satoshi(450000)) awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].ourCommitPublished == Some(aliceCommitTx)) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished == Some(aliceCommitTx)) } } diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index c1be3a8e2..1390ec3f0 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -398,7 +398,7 @@ class ShutdownStateSpec extends StateSpecBaseClass with StateTestsHelperMethods assert(amountClaimed == Satoshi(500000)) awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].theirCommitPublished == Some(bobCommitTx)) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished == Some(bobCommitTx)) } } @@ -439,7 +439,7 @@ class ShutdownStateSpec extends StateSpecBaseClass with StateTestsHelperMethods alice2blockchain.expectNoMsg() awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].ourCommitPublished == Some(aliceCommitTx)) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished == Some(aliceCommitTx)) } } diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 5fd05da75..5504f9d1c 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -154,7 +154,7 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods { alice2blockchain.expectMsgType[WatchConfirmed].txId == aliceCommitTx.txid awaitCond(alice.stateName == CLOSING) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] - assert(initialState.ourCommitPublished == Some(aliceCommitTx)) + assert(initialState.localCommitPublished == Some(aliceCommitTx)) // actual test starts here alice ! (BITCOIN_FUNDING_SPENT, aliceCommitTx) @@ -211,7 +211,7 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods { alice2blockchain.expectMsg(Publish(aliceCommitTx)) alice2blockchain.expectMsgType[WatchConfirmed].txId == aliceCommitTx.txid awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].ourCommitPublished == Some(aliceCommitTx)) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished == Some(aliceCommitTx)) // actual test starts here alice ! BITCOIN_SPEND_OURS_DONE @@ -229,7 +229,7 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods { alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(theirCommitPublished = Some(bobCommitTx))) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(remoteCommitPublished = Some(RemoteCommitPublished(bobCommitTx, Nil, Nil)))) } } @@ -241,7 +241,7 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods { assert(bobCommitTx.txOut.size == 2) // two main outputs alice ! (BITCOIN_FUNDING_SPENT, bobCommitTx) alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(theirCommitPublished = Some(bobCommitTx))) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(remoteCommitPublished = Some(RemoteCommitPublished(bobCommitTx, Nil, Nil)))) // actual test starts here alice ! BITCOIN_SPEND_THEIRS_DONE @@ -260,7 +260,7 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods { alice2blockchain.expectMsgType[Publish] alice2blockchain.expectMsgType[WatchConfirmed] - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(revokedPublished = Seq(bobRevokedTx))) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(revokedCommitPublished = Seq(RevokedCommitPublished(bobRevokedTx)))) } } @@ -273,9 +273,9 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods { // alice publishes and watches the stealing tx alice2blockchain.expectMsgType[Publish] alice2blockchain.expectMsgType[WatchConfirmed] - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(revokedPublished = initialState.revokedPublished :+ bobRevokedTx)) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(revokedCommitPublished = initialState.revokedCommitPublished :+ RevokedCommitPublished(bobRevokedTx))) } - assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedPublished.size == bobCommitTxes.size - 1) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == bobCommitTxes.size - 1) } } @@ -288,7 +288,7 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods { // alice publishes and watches the stealing tx alice2blockchain.expectMsgType[Publish] alice2blockchain.expectMsgType[WatchConfirmed] - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(revokedPublished = Seq(bobRevokedTx))) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(revokedCommitPublished = Seq(RevokedCommitPublished(bobRevokedTx)))) // actual test starts here alice ! BITCOIN_STEAL_DONE