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 24ecf56fb4..44ef652005 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 @@ -22,6 +22,7 @@ import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto} import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets} import fr.acinq.eclair.crypto.{Generators, KeyManager, ShaChain, Sphinx} import fr.acinq.eclair.payment.relay.{Origin, Relayer} +import fr.acinq.eclair.transactions.DirectedHtlc._ import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire._ @@ -63,10 +64,13 @@ case class Commitments(channelVersion: ChannelVersion, def hasNoPendingHtlcs: Boolean = localCommit.spec.htlcs.isEmpty && remoteCommit.spec.htlcs.isEmpty && remoteNextCommitInfo.isRight - def timedOutOutgoingHtlcs(blockheight: Long): Set[UpdateAddHtlc] = - (localCommit.spec.htlcs.filter(htlc => htlc.direction == OUT && blockheight >= htlc.add.cltvExpiry.toLong) ++ - remoteCommit.spec.htlcs.filter(htlc => htlc.direction == IN && blockheight >= htlc.add.cltvExpiry.toLong) ++ - remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.htlcs.filter(htlc => htlc.direction == IN && blockheight >= htlc.add.cltvExpiry.toLong)).getOrElse(Set.empty[DirectedHtlc])).map(_.add) + def timedOutOutgoingHtlcs(blockheight: Long): Set[UpdateAddHtlc] = { + def expired(add: UpdateAddHtlc) = blockheight >= add.cltvExpiry.toLong + + localCommit.spec.htlcs.collect(outgoing).filter(expired) ++ + remoteCommit.spec.htlcs.collect(incoming).filter(expired) ++ + remoteNextCommitInfo.left.toSeq.flatMap(_.nextRemoteCommit.spec.htlcs.collect(incoming).filter(expired).toSet) + } /** * HTLCs that are close to timing out upstream are potentially dangerous. If we received the pre-image for those @@ -75,9 +79,9 @@ case class Commitments(channelVersion: ChannelVersion, * and our HTLC success in case of a force-close. */ def almostTimedOutIncomingHtlcs(blockheight: Long, fulfillSafety: CltvExpiryDelta): Set[UpdateAddHtlc] = { - localCommit.spec.htlcs.collect { - case htlc if htlc.direction == IN && blockheight >= (htlc.add.cltvExpiry - fulfillSafety).toLong => htlc.add - } + def nearlyExpired(add: UpdateAddHtlc) = blockheight >= (add.cltvExpiry - fulfillSafety).toLong + + localCommit.spec.htlcs.collect(incoming).filter(nearlyExpired) } def addLocalProposal(proposal: UpdateMessage): Commitments = Commitments.addLocalProposal(this, proposal) @@ -187,7 +191,7 @@ object Commitments { val remoteCommit1 = commitments1.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit).getOrElse(commitments1.remoteCommit) val reduced = CommitmentSpec.reduce(remoteCommit1.spec, commitments1.remoteChanges.acked, commitments1.localChanges.proposed) // the HTLC we are about to create is outgoing, but from their point of view it is incoming - val outgoingHtlcs = reduced.htlcs.filter(_.direction == IN) + val outgoingHtlcs = reduced.htlcs.collect(incoming) // note that the funder pays the fee, so if sender != funder, both sides will have to afford this payment val fees = commitTxFee(commitments1.remoteParams.dustLimit, reduced) @@ -207,7 +211,7 @@ object Commitments { } // NB: we need the `toSeq` because otherwise duplicate amountMsat would be removed (since outgoingHtlcs is a Set). - val htlcValueInFlight = outgoingHtlcs.toSeq.map(_.add.amountMsat).sum + val htlcValueInFlight = outgoingHtlcs.toSeq.map(_.amountMsat).sum if (commitments1.remoteParams.maxHtlcValueInFlightMsat < htlcValueInFlight) { // TODO: this should be a specific UPDATE error return Failure(HtlcValueTooHighInFlight(commitments.channelId, maximum = commitments1.remoteParams.maxHtlcValueInFlightMsat, actual = htlcValueInFlight)) @@ -232,7 +236,7 @@ object Commitments { // let's compute the current commitment *as seen by us* including this change val commitments1 = addRemoteProposal(commitments, add).copy(remoteNextHtlcId = commitments.remoteNextHtlcId + 1) val reduced = CommitmentSpec.reduce(commitments1.localCommit.spec, commitments1.localChanges.acked, commitments1.remoteChanges.proposed) - val incomingHtlcs = reduced.htlcs.filter(_.direction == IN) + val incomingHtlcs = reduced.htlcs.collect(incoming) // note that the funder pays the fee, so if sender != funder, both sides will have to afford this payment val fees = commitTxFee(commitments1.remoteParams.dustLimit, reduced) @@ -251,7 +255,7 @@ object Commitments { } // NB: we need the `toSeq` because otherwise duplicate amountMsat would be removed (since incomingHtlcs is a Set). - val htlcValueInFlight = incomingHtlcs.toSeq.map(_.add.amountMsat).sum + val htlcValueInFlight = incomingHtlcs.toSeq.map(_.amountMsat).sum if (commitments1.localParams.maxHtlcValueInFlightMsat < htlcValueInFlight) { throw HtlcValueTooHighInFlight(commitments.channelId, maximum = commitments1.localParams.maxHtlcValueInFlightMsat, actual = htlcValueInFlight) } @@ -263,16 +267,24 @@ object Commitments { commitments1 } - def getHtlcCrossSigned(commitments: Commitments, directionRelativeToLocal: Direction, htlcId: Long): Option[UpdateAddHtlc] = for { - localSigned <- commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit).getOrElse(commitments.remoteCommit).spec.findHtlcById(htlcId, directionRelativeToLocal.opposite) - remoteSigned <- commitments.localCommit.spec.findHtlcById(htlcId, directionRelativeToLocal) + def getOutgoingHtlcCrossSigned(commitments: Commitments, htlcId: Long): Option[UpdateAddHtlc] = for { + localSigned <- commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit).getOrElse(commitments.remoteCommit).spec.findIncomingHtlcById(htlcId) + remoteSigned <- commitments.localCommit.spec.findOutgoingHtlcById(htlcId) + } yield { + require(localSigned.add == remoteSigned.add) + localSigned.add + } + + def getIncomingHtlcCrossSigned(commitments: Commitments, htlcId: Long): Option[UpdateAddHtlc] = for { + localSigned <- commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit).getOrElse(commitments.remoteCommit).spec.findOutgoingHtlcById(htlcId) + remoteSigned <- commitments.localCommit.spec.findIncomingHtlcById(htlcId) } yield { require(localSigned.add == remoteSigned.add) localSigned.add } def sendFulfill(commitments: Commitments, cmd: CMD_FULFILL_HTLC): Try[(Commitments, UpdateFulfillHtlc)] = - getHtlcCrossSigned(commitments, IN, cmd.id) match { + getIncomingHtlcCrossSigned(commitments, cmd.id) match { case Some(htlc) if alreadyProposed(commitments.localChanges.proposed, htlc.id) => // we have already sent a fail/fulfill for this htlc Failure(UnknownHtlcId(commitments.channelId, cmd.id)) @@ -285,14 +297,14 @@ object Commitments { } def receiveFulfill(commitments: Commitments, fulfill: UpdateFulfillHtlc): Try[(Commitments, Origin, UpdateAddHtlc)] = - getHtlcCrossSigned(commitments, OUT, fulfill.id) match { + getOutgoingHtlcCrossSigned(commitments, fulfill.id) match { case Some(htlc) if htlc.paymentHash == sha256(fulfill.paymentPreimage) => Try((addRemoteProposal(commitments, fulfill), commitments.originChannels(fulfill.id), htlc)) case Some(_) => Failure(InvalidHtlcPreimage(commitments.channelId, fulfill.id)) case None => Failure(UnknownHtlcId(commitments.channelId, fulfill.id)) } def sendFail(commitments: Commitments, cmd: CMD_FAIL_HTLC, nodeSecret: PrivateKey): Try[(Commitments, UpdateFailHtlc)] = - getHtlcCrossSigned(commitments, IN, cmd.id) match { + getIncomingHtlcCrossSigned(commitments, cmd.id) match { case Some(htlc) if alreadyProposed(commitments.localChanges.proposed, htlc.id) => // we have already sent a fail/fulfill for this htlc Failure(UnknownHtlcId(commitments.channelId, cmd.id)) @@ -317,7 +329,7 @@ object Commitments { if ((cmd.failureCode & FailureMessageCodecs.BADONION) == 0) { Failure(InvalidFailureCode(commitments.channelId)) } else { - getHtlcCrossSigned(commitments, IN, cmd.id) match { + getIncomingHtlcCrossSigned(commitments, cmd.id) match { case Some(htlc) if alreadyProposed(commitments.localChanges.proposed, htlc.id) => // we have already sent a fail/fulfill for this htlc Failure(UnknownHtlcId(commitments.channelId, cmd.id)) @@ -331,7 +343,7 @@ object Commitments { } def receiveFail(commitments: Commitments, fail: UpdateFailHtlc): Try[(Commitments, Origin, UpdateAddHtlc)] = - getHtlcCrossSigned(commitments, OUT, fail.id) match { + getOutgoingHtlcCrossSigned(commitments, fail.id) match { case Some(htlc) => Try((addRemoteProposal(commitments, fail), commitments.originChannels(fail.id), htlc)) case None => Failure(UnknownHtlcId(commitments.channelId, fail.id)) } @@ -341,7 +353,7 @@ object Commitments { if ((fail.failureCode & FailureMessageCodecs.BADONION) == 0) { Failure(InvalidFailureCode(commitments.channelId)) } else { - getHtlcCrossSigned(commitments, OUT, fail.id) match { + getOutgoingHtlcCrossSigned(commitments, fail.id) match { case Some(htlc) => Try((addRemoteProposal(commitments, fail), commitments.originChannels(fail.id), htlc)) case None => Failure(UnknownHtlcId(commitments.channelId, fail.id)) } @@ -396,7 +408,7 @@ object Commitments { if (missing < 0.sat) { Failure(CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.localParams.channelReserve, fees = fees)) } else { - Success(commitments1) + Success(commitments1) } } } @@ -430,7 +442,7 @@ object Commitments { val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), remoteNextPerCommitmentPoint)) // NB: IN/OUT htlcs are inverted because this is the remote commit - log.info(s"built remote commit number=${remoteCommit.index + 1} toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.feeratePerKw} txid=${remoteCommitTx.tx.txid} tx={}", spec.htlcs.filter(_.direction == OUT).map(_.add.id).mkString(","), spec.htlcs.filter(_.direction == IN).map(_.add.id).mkString(","), remoteCommitTx.tx) + log.info(s"built remote commit number=${remoteCommit.index + 1} toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.feeratePerKw} txid=${remoteCommitTx.tx.txid} tx={}", spec.htlcs.collect(outgoing).map(_.id).mkString(","), spec.htlcs.collect(incoming).map(_.id).mkString(","), remoteCommitTx.tx) // don't sign if they don't get paid val commitSig = CommitSig( @@ -477,7 +489,7 @@ object Commitments { val (localCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeLocalTxs(keyManager, channelVersion, localCommit.index + 1, localParams, remoteParams, commitInput, localPerCommitmentPoint, spec) val sig = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath)) - log.info(s"built local commit number=${localCommit.index + 1} toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.feeratePerKw} txid=${localCommitTx.tx.txid} tx={}", spec.htlcs.filter(_.direction == IN).map(_.add.id).mkString(","), spec.htlcs.filter(_.direction == OUT).map(_.add.id).mkString(","), localCommitTx.tx) + log.info(s"built local commit number=${localCommit.index + 1} toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.feeratePerKw} txid=${localCommitTx.tx.txid} tx={}", spec.htlcs.collect(incoming).map(_.id).mkString(","), spec.htlcs.collect(outgoing).map(_.id).mkString(","), localCommitTx.tx) // TODO: should we have optional sig? (original comment: this tx will NOT be signed if our output is empty) @@ -543,18 +555,18 @@ object Commitments { // same for fails: we need to make sure that they are in neither commitment before propagating the fail upstream case fail: UpdateFailHtlc => val origin = commitments.originChannels(fail.id) - val add = commitments.remoteCommit.spec.findHtlcById(fail.id, IN).map(_.add).get + val add = commitments.remoteCommit.spec.findIncomingHtlcById(fail.id).map(_.add).get Relayer.ForwardRemoteFail(fail, origin, add) // same as above case fail: UpdateFailMalformedHtlc => val origin = commitments.originChannels(fail.id) - val add = commitments.remoteCommit.spec.findHtlcById(fail.id, IN).map(_.add).get + val add = commitments.remoteCommit.spec.findIncomingHtlcById(fail.id).map(_.add).get Relayer.ForwardRemoteFailMalformed(fail, origin, add) } // the outgoing following htlcs have been completed (fulfilled or failed) when we received this revocation // they have been removed from both local and remote commitment // (since fulfill/fail are sent by remote, they are (1) signed by them, (2) revoked by us, (3) signed by us, (4) revoked by them - val completedOutgoingHtlcs = commitments.remoteCommit.spec.htlcs.filter(_.direction == IN).map(_.add.id) -- theirNextCommit.spec.htlcs.filter(_.direction == IN).map(_.add.id) + val completedOutgoingHtlcs = commitments.remoteCommit.spec.htlcs.collect(incoming).map(_.id) -- theirNextCommit.spec.htlcs.collect(incoming).map(_.id) // we remove the newly completed htlcs from the origin map val originChannels1 = commitments.originChannels -- completedOutgoingHtlcs val commitments1 = commitments.copy( 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 d1aa167bac..8985f6cd7d 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 @@ -26,6 +26,7 @@ import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets} import fr.acinq.eclair.channel.Channel.REFRESH_CHANNEL_UPDATE_INTERVAL import fr.acinq.eclair.crypto.{Generators, KeyManager} import fr.acinq.eclair.db.ChannelsDb +import fr.acinq.eclair.transactions.DirectedHtlc._ import fr.acinq.eclair.transactions.Scripts._ import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions._ @@ -835,10 +836,9 @@ object Helpers { // if an outgoing htlc is in the remote commitment, then: // - either it is in the local commitment (it was never fulfilled) // - or we have already received the fulfill and forwarded it upstream - val outgoingHtlcs = localCommit.spec.htlcs.filter(_.direction == OUT).map(_.add) - outgoingHtlcs.collect { - case add if add.paymentHash == sha256(paymentPreimage) => (add, paymentPreimage) - } + localCommit.spec.htlcs.collect { + case OutgoingHtlc(add) if add.paymentHash == sha256(paymentPreimage) => (add, paymentPreimage) + } } } @@ -864,14 +864,14 @@ object Helpers { def timedoutHtlcs(localCommit: LocalCommit, localDustLimit: Satoshi, tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = if (tx.txid == localCommit.publishableTxs.commitTx.tx.txid) { // the tx is a commitment tx, we can immediately fail all dust htlcs (they don't have an output in the tx) - (localCommit.spec.htlcs.filter(_.direction == OUT) -- Transactions.trimOfferedHtlcs(localDustLimit, localCommit.spec)).map(_.add) + (localCommit.spec.htlcs.collect(outgoing) -- Transactions.trimOfferedHtlcs(localDustLimit, localCommit.spec).map(_.add)) } else { // maybe this is a timeout tx, in that case we can resolve and fail the corresponding htlc tx.txIn.map(_.witness match { case ScriptWitness(Seq(ByteVector.empty, remoteSig, localSig, ByteVector.empty, htlcOfferedScript)) => val paymentHash160 = htlcOfferedScript.slice(109, 109 + 20) log.info(s"extracted paymentHash160=$paymentHash160 from tx=$tx (htlc-timeout)") - localCommit.spec.htlcs.filter(_.direction == OUT).map(_.add).filter(add => ripemd160(add.paymentHash) == paymentHash160) + localCommit.spec.htlcs.collect(outgoing).filter(add => ripemd160(add.paymentHash) == paymentHash160) case _ => Set.empty }).toSet.flatten } @@ -886,14 +886,14 @@ object Helpers { def timedoutHtlcs(remoteCommit: RemoteCommit, remoteDustLimit: Satoshi, tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = if (tx.txid == remoteCommit.txid) { // the tx is a commitment tx, we can immediately fail all dust htlcs (they don't have an output in the tx) - (remoteCommit.spec.htlcs.filter(_.direction == IN) -- Transactions.trimReceivedHtlcs(remoteDustLimit, remoteCommit.spec)).map(_.add) + (remoteCommit.spec.htlcs.collect(incoming) -- Transactions.trimReceivedHtlcs(remoteDustLimit, remoteCommit.spec).map(_.add)) } else { // maybe this is a timeout tx, in that case we can resolve and fail the corresponding htlc tx.txIn.map(_.witness match { case ScriptWitness(Seq(remoteSig, ByteVector.empty, htlcReceivedScript)) => val paymentHash160 = htlcReceivedScript.slice(69, 69 + 20) log.info(s"extracted paymentHash160=$paymentHash160 from tx=$tx (claim-htlc-timeout)") - remoteCommit.spec.htlcs.filter(_.direction == IN).map(_.add).filter(add => ripemd160(add.paymentHash) == paymentHash160) + remoteCommit.spec.htlcs.collect(incoming).filter { add => ripemd160(add.paymentHash) == paymentHash160 } case _ => Set.empty }).toSet.flatten } @@ -906,11 +906,11 @@ object Helpers { */ def onchainOutgoingHtlcs(localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: Option[RemoteCommit], tx: Transaction): Set[UpdateAddHtlc] = { if (localCommit.publishableTxs.commitTx.tx.txid == tx.txid) { - localCommit.spec.htlcs.filter(_.direction == OUT).map(_.add) + localCommit.spec.htlcs.collect(outgoing) } else if (remoteCommit.txid == tx.txid) { - remoteCommit.spec.htlcs.filter(_.direction == IN).map(_.add) + remoteCommit.spec.htlcs.collect(incoming) } else if (nextRemoteCommit_opt.map(_.txid).contains(tx.txid)) { - nextRemoteCommit_opt.get.spec.htlcs.filter(_.direction == IN).map(_.add) + nextRemoteCommit_opt.get.spec.htlcs.collect(incoming) } else { Set.empty } @@ -930,14 +930,14 @@ object Helpers { // our commit got confirmed, so any htlc that we signed but they didn't sign will never reach the chain val mostRecentRemoteCommit = nextRemoteCommit_opt.getOrElse(remoteCommit) // NB: from the p.o.v of remote, their incoming htlcs are our outgoing htlcs - mostRecentRemoteCommit.spec.htlcs.filter(_.direction == IN).map(_.add) -- localCommit.spec.htlcs.filter(_.direction == OUT).map(_.add) + mostRecentRemoteCommit.spec.htlcs.collect(incoming) -- localCommit.spec.htlcs.collect(outgoing) } else if (remoteCommit.txid == tx.txid) { // their commit got confirmed nextRemoteCommit_opt match { case Some(nextRemoteCommit) => // we had signed a new commitment but they committed the previous one // any htlc that we signed in the new commitment that they didn't sign will never reach the chain - nextRemoteCommit.spec.htlcs.filter(_.direction == IN).map(_.add) -- localCommit.spec.htlcs.filter(_.direction == OUT).map(_.add) + nextRemoteCommit.spec.htlcs.collect(incoming) -- localCommit.spec.htlcs.collect(outgoing) case None => // their last commitment got confirmed, so no htlcs will be overridden, they will timeout or be fulfilled on chain Set.empty diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala index d7a849cd79..380d4eff53 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala @@ -26,7 +26,8 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.db._ import fr.acinq.eclair.payment.Monitoring.Tags import fr.acinq.eclair.payment.{IncomingPacket, PaymentFailed, PaymentSent} -import fr.acinq.eclair.transactions.{IN, OUT} +import fr.acinq.eclair.transactions.DirectedHtlc.outgoing +import fr.acinq.eclair.transactions.OutgoingHtlc import fr.acinq.eclair.wire.{TemporaryNodeFailure, UpdateAddHtlc} import fr.acinq.eclair.{LongToBtcAmount, NodeParams} import scodec.bits.ByteVector @@ -83,7 +84,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, commandBuffer: ActorRef, in val acked = brokenHtlcs.notRelayed .filter(_.add.channelId == data.channelId) // only consider htlcs related to this channel .filter { - case IncomingHtlc(htlc, preimage) if Commitments.getHtlcCrossSigned(data.commitments, IN, htlc.id).isDefined => + case IncomingHtlc(htlc, preimage) if Commitments.getIncomingHtlcCrossSigned(data.commitments, htlc.id).isDefined => // this htlc is cross signed in the current commitment, we can settle it preimage match { case Some(preimage) => @@ -292,8 +293,7 @@ object PostRestartHtlcCleaner { // we subsequently sign it. That's why we need to look in *their* commitment with direction=OUT. val htlcsIn = channels .flatMap(_.commitments.remoteCommit.spec.htlcs) - .filter(_.direction == OUT) - .map(_.add) + .collect(outgoing) .map(IncomingPacket.decrypt(_, privateKey, features)) .collect { // When we're not the final recipient, we'll only consider HTLCs that aren't relayed downstream, so no need to look for a preimage. @@ -333,8 +333,7 @@ object PostRestartHtlcCleaner { val channel2Htlc: Set[(ByteVector32, Long)] = channels .flatMap(_.commitments.remoteCommit.spec.htlcs) - .filter(_.direction == OUT) - .map(htlc => (htlc.add.channelId, htlc.add.id)) + .collect { case OutgoingHtlc(add) => (add.channelId, add.id) } .toSet val pendingRelay: Set[(ByteVector32, Long)] = relayDb.listPendingRelay() 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 1a0cfa1562..5932ed1489 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 @@ -23,37 +23,55 @@ import fr.acinq.eclair.wire._ * Created by PM on 07/12/2016. */ -// @formatter:off -sealed trait Direction { def opposite: Direction } -case object IN extends Direction { def opposite = OUT } -case object OUT extends Direction { def opposite = IN } -// @formatter:on - 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 + } sealed trait DirectedHtlc { - def direction: Direction val add: UpdateAddHtlc + def opposite: DirectedHtlc = this match { case IncomingHtlc(_) => OutgoingHtlc(add) case OutgoingHtlc(_) => IncomingHtlc(add) } + + def direction: String = this match { + case IncomingHtlc(_) => "IN" + case OutgoingHtlc(_) => "OUT" + } +} + +object DirectedHtlc { + def incoming: PartialFunction[DirectedHtlc, UpdateAddHtlc] = { + case h: IncomingHtlc => h.add + } + + def outgoing: PartialFunction[DirectedHtlc, UpdateAddHtlc] = { + case h: OutgoingHtlc => h.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 } +case class IncomingHtlc(add: UpdateAddHtlc) extends DirectedHtlc + +case class OutgoingHtlc(add: UpdateAddHtlc) extends DirectedHtlc final case class CommitmentSpec(htlcs: Set[DirectedHtlc], feeratePerKw: Long, toLocal: MilliSatoshi, toRemote: MilliSatoshi) { - def findHtlcById(id: Long, direction: Direction): Option[DirectedHtlc] = htlcs.find(htlc => htlc.add.id == id && htlc.direction == direction) + def findIncomingHtlcById(id: Long): Option[IncomingHtlc] = htlcs.collectFirst { case htlc: IncomingHtlc if htlc.add.id == id => htlc } + + def findOutgoingHtlcById(id: Long): Option[OutgoingHtlc] = htlcs.collectFirst { case htlc: OutgoingHtlc if htlc.add.id == id => htlc } - val totalFunds = toLocal + toRemote + htlcs.toSeq.map(_.add.amountMsat).sum + val totalFunds: MilliSatoshi = toLocal + toRemote + htlcs.toSeq.map(_.add.amountMsat).sum } object CommitmentSpec { @@ -62,54 +80,60 @@ object CommitmentSpec { case _ => false } - def addHtlc(spec: CommitmentSpec, direction: Direction, update: UpdateAddHtlc): CommitmentSpec = { - val htlc = direction match { - case IN => IncomingHtlc(update) - case OUT => OutgoingHtlc(update) + def addHtlc(spec: CommitmentSpec, directedHtlc: DirectedHtlc): CommitmentSpec = { + directedHtlc match { + case OutgoingHtlc(add) => spec.copy(toLocal = spec.toLocal - add.amountMsat, htlcs = spec.htlcs + directedHtlc) + case IncomingHtlc(add) => spec.copy(toRemote = spec.toRemote - add.amountMsat, htlcs = spec.htlcs + directedHtlc) + } + } + + def fulfillIncomingHtlc(spec: CommitmentSpec, htlcId: Long): CommitmentSpec = { + spec.findIncomingHtlcById(htlcId) match { + case Some(htlc) => spec.copy(toLocal = spec.toLocal + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) + case None => throw new RuntimeException(s"cannot find htlc id=$htlcId") } - 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) + } + + def fulfillOutgoingHtlc(spec: CommitmentSpec, htlcId: Long): CommitmentSpec = { + spec.findOutgoingHtlcById(htlcId) match { + case Some(htlc) => spec.copy(toRemote = spec.toRemote + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) + case None => throw new RuntimeException(s"cannot find htlc id=$htlcId") } } - // OUT means we are sending an UpdateFulfillHtlc message which means that we are fulfilling an HTLC that they sent - def fulfillHtlc(spec: CommitmentSpec, direction: Direction, htlcId: Long): CommitmentSpec = { - spec.findHtlcById(htlcId, direction.opposite) match { - case Some(htlc) if direction == OUT => spec.copy(toLocal = spec.toLocal + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) - case Some(htlc) if direction == IN => spec.copy(toRemote = spec.toRemote + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) + def failIncomingHtlc(spec: CommitmentSpec, htlcId: Long): CommitmentSpec = { + spec.findIncomingHtlcById(htlcId) match { + case Some(htlc) => spec.copy(toRemote = spec.toRemote + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) case None => throw new RuntimeException(s"cannot find htlc id=$htlcId") } } - // OUT means we are sending an UpdateFailHtlc message which means that we are failing an HTLC that they sent - def failHtlc(spec: CommitmentSpec, direction: Direction, htlcId: Long): CommitmentSpec = { - spec.findHtlcById(htlcId, direction.opposite) match { - case Some(htlc) if direction == OUT => spec.copy(toRemote = spec.toRemote + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) - case Some(htlc) if direction == IN => spec.copy(toLocal = spec.toLocal + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) + def failOutgoingHtlc(spec: CommitmentSpec, htlcId: Long): CommitmentSpec = { + spec.findOutgoingHtlcById(htlcId) match { + case Some(htlc) => spec.copy(toLocal = spec.toLocal + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) case None => throw new RuntimeException(s"cannot find htlc id=$htlcId") } } def reduce(localCommitSpec: CommitmentSpec, localChanges: List[UpdateMessage], remoteChanges: List[UpdateMessage]): CommitmentSpec = { val spec1 = localChanges.foldLeft(localCommitSpec) { - case (spec, u: UpdateAddHtlc) => addHtlc(spec, OUT, u) + case (spec, u: UpdateAddHtlc) => addHtlc(spec, OutgoingHtlc(u)) case (spec, _) => spec } val spec2 = remoteChanges.foldLeft(spec1) { - case (spec, u: UpdateAddHtlc) => addHtlc(spec, IN, u) + case (spec, u: UpdateAddHtlc) => addHtlc(spec, IncomingHtlc(u)) case (spec, _) => spec } val spec3 = localChanges.foldLeft(spec2) { - case (spec, u: UpdateFulfillHtlc) => fulfillHtlc(spec, OUT, u.id) - case (spec, u: UpdateFailHtlc) => failHtlc(spec, OUT, u.id) - case (spec, u: UpdateFailMalformedHtlc) => failHtlc(spec, OUT, u.id) + case (spec, u: UpdateFulfillHtlc) => fulfillIncomingHtlc(spec, u.id) + case (spec, u: UpdateFailHtlc) => failIncomingHtlc(spec, u.id) + case (spec, u: UpdateFailMalformedHtlc) => failIncomingHtlc(spec, u.id) case (spec, _) => spec } val spec4 = remoteChanges.foldLeft(spec3) { - case (spec, u: UpdateFulfillHtlc) => fulfillHtlc(spec, IN, u.id) - case (spec, u: UpdateFailHtlc) => failHtlc(spec, IN, u.id) - case (spec, u: UpdateFailMalformedHtlc) => failHtlc(spec, IN, u.id) + case (spec, u: UpdateFulfillHtlc) => fulfillOutgoingHtlc(spec, u.id) + case (spec, u: UpdateFailHtlc) => failOutgoingHtlc(spec, u.id) + case (spec, u: UpdateFailMalformedHtlc) => failOutgoingHtlc(spec, u.id) case (spec, _) => spec } val spec5 = (localChanges ++ remoteChanges).foldLeft(spec4) { 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 40f6df37ea..bf691eb787 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 @@ -88,14 +88,9 @@ object ChannelCodecs extends Logging { ("htlcBasepoint" | publicKey) :: ("features" | combinedFeaturesCodec)).as[RemoteParams] - val directionCodec: Codec[Direction] = Codec[Direction]( - (dir: Direction) => bool.encode(dir == IN), - (wire: BitVector) => bool.decode(wire).map(_.map(b => if (b) IN else OUT)) - ) - - val htlcCodec: Codec[DirectedHtlc] = discriminated[DirectedHtlc].by(directionCodec) - .typecase(IN, updateAddHtlcCodec.as[IncomingHtlc]) - .typecase(OUT, updateAddHtlcCodec.as[OutgoingHtlc]) + val htlcCodec: Codec[DirectedHtlc] = discriminated[DirectedHtlc].by(bool) + .typecase(true, updateAddHtlcCodec.as[IncomingHtlc]) + .typecase(false, 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/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index f757fa5552..b41a0e952f 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 @@ -35,8 +35,9 @@ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment.relay.Relayer._ import fr.acinq.eclair.payment.relay.{CommandBuffer, Origin} import fr.acinq.eclair.router.Announcements +import fr.acinq.eclair.transactions.DirectedHtlc.{incoming, outgoing} +import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions.{HtlcSuccessTx, htlcSuccessWeight, htlcTimeoutWeight, weight2fee} -import fr.acinq.eclair.transactions.{IN, OUT, Transactions} import fr.acinq.eclair.wire.{AnnouncementSignatures, ChannelUpdate, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} import fr.acinq.eclair.{TestConstants, TestkitBaseClass, randomBytes32, _} import org.scalatest.{Outcome, Tag} @@ -720,7 +721,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // bob replies immediately with a signature bob2alice.expectMsgType[CommitSig] - awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.htlcs.exists(h => h.add.id == htlc.id && h.direction == IN)) + awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.htlcs.collect(incoming).exists(_.id == htlc.id)) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs.size == 1) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toLocal == initialState.commitments.localCommit.spec.toLocal) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteChanges.acked.size == 0) @@ -745,7 +746,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { bob2alice.expectMsgType[CommitSig] bob2alice.forward(alice) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.htlcs.exists(h => h.add.id == htlc.id && h.direction == OUT)) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.htlcs.collect(outgoing).exists(_.id == htlc.id)) assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs.size == 1) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toLocal == initialState.commitments.localCommit.spec.toLocal) } @@ -822,7 +823,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] crossSign(alice, bob, alice2bob, bob2alice) - awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.htlcs.exists(h => h.add.id == htlc1.id && h.direction == IN)) + awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.htlcs.collect(incoming).exists(_.id == htlc1.id)) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs.size == 2) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toLocal == initialState.commitments.localCommit.spec.toLocal) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx.txOut.count(_.amount == 50000.sat) == 2) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala index a7a8ec32b5..ee2aa3cf26 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala @@ -23,7 +23,7 @@ import java.util.concurrent.CountDownLatch import akka.actor.{Actor, ActorLogging, ActorRef, Stash} import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.channel._ -import fr.acinq.eclair.transactions.{IN, OUT} +import fr.acinq.eclair.transactions.{IncomingHtlc, OutgoingHtlc} import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, TestConstants, TestUtils} /** @@ -132,15 +132,15 @@ class SynchronizationPipe(latch: CountDownLatch) extends Actor with ActorLogging val l = List( "LOCAL COMMITS:", s" Commit ${d.commitments.localCommit.index}:", - s" Offered htlcs: ${localCommit.spec.htlcs.filter(_.direction == OUT).map(h => (h.add.id, h.add.amountMsat)).mkString(" ")}", - s" Received htlcs: ${localCommit.spec.htlcs.filter(_.direction == IN).map(h => (h.add.id, h.add.amountMsat)).mkString(" ")}", + s" Offered htlcs: ${localCommit.spec.htlcs.collect { case OutgoingHtlc(add) => (add.id, add.amountMsat) }.mkString(" ")}", + s" Received htlcs: ${localCommit.spec.htlcs.collect { case IncomingHtlc(add) => (add.id, add.amountMsat) }.mkString(" ")}", s" Balance us: ${localCommit.spec.toLocal}", s" Balance them: ${localCommit.spec.toRemote}", s" Fee rate: ${localCommit.spec.feeratePerKw}", "REMOTE COMMITS:", s" Commit ${remoteCommit.index}:", - s" Offered htlcs: ${remoteCommit.spec.htlcs.filter(_.direction == OUT).map(h => (h.add.id, h.add.amountMsat)).mkString(" ")}", - s" Received htlcs: ${remoteCommit.spec.htlcs.filter(_.direction == IN).map(h => (h.add.id, h.add.amountMsat)).mkString(" ")}", + s" Offered htlcs: ${remoteCommit.spec.htlcs.collect { case OutgoingHtlc(add) => (add.id, add.amountMsat) }.mkString(" ")}", + s" Received htlcs: ${remoteCommit.spec.htlcs.collect { case IncomingHtlc(add) => (add.id, add.amountMsat) }.mkString(" ")}", s" Balance us: ${remoteCommit.spec.toLocal}", s" Balance them: ${remoteCommit.spec.toRemote}", s" Fee rate: ${remoteCommit.spec.feeratePerKw}") 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 1480ad0fca..2748b40a2d 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, IncomingHtlc, OUT, OutgoingHtlc} +import fr.acinq.eclair.transactions.{DirectedHtlc, IncomingHtlc, OutgoingHtlc} import fr.acinq.eclair.wire.Onion.FinalLegacyPayload import fr.acinq.eclair.wire._ import fr.acinq.eclair.{CltvExpiry, LongToBtcAmount, NodeParams, TestConstants, TestkitBaseClass, randomBytes32} @@ -80,19 +80,19 @@ class PostRestartHtlcCleanerSpec extends TestkitBaseClass { val trampolineRelayed = Origin.TrampolineRelayed((channelId_ab_1, 0L) :: (channelId_ab_2, 2L) :: Nil, None) val htlc_ab_1 = Seq( - buildHtlc(0, channelId_ab_1, trampolineRelayedPaymentHash, IN), - buildHtlc(1, channelId_ab_1, randomBytes32, IN), // not relayed - buildHtlc(2, channelId_ab_1, randomBytes32, OUT), - buildHtlc(3, channelId_ab_1, randomBytes32, OUT), - buildHtlc(4, channelId_ab_1, randomBytes32, IN), // not relayed - buildHtlc(5, channelId_ab_1, relayedPaymentHash, IN) + buildHtlcIn(0, channelId_ab_1, trampolineRelayedPaymentHash), + buildHtlcIn(1, channelId_ab_1, randomBytes32), // not relayed + buildHtlcOut(2, channelId_ab_1, randomBytes32), + buildHtlcOut(3, channelId_ab_1, randomBytes32), + buildHtlcIn(4, channelId_ab_1, randomBytes32), // not relayed + buildHtlcIn(5, channelId_ab_1, relayedPaymentHash) ) val htlc_ab_2 = Seq( - buildHtlc(0, channelId_ab_2, randomBytes32, IN), // not relayed - buildHtlc(1, channelId_ab_2, randomBytes32, OUT), - buildHtlc(2, channelId_ab_2, trampolineRelayedPaymentHash, IN), - buildHtlc(3, channelId_ab_2, randomBytes32, OUT), - buildHtlc(4, channelId_ab_2, randomBytes32, IN) // not relayed + buildHtlcIn(0, channelId_ab_2, randomBytes32), // not relayed + buildHtlcOut(1, channelId_ab_2, randomBytes32), + buildHtlcIn(2, channelId_ab_2, trampolineRelayedPaymentHash), + buildHtlcOut(3, channelId_ab_2, randomBytes32), + buildHtlcIn(4, channelId_ab_2, randomBytes32) // not relayed ) val channels = Seq( @@ -365,15 +365,15 @@ object PostRestartHtlcCleanerSpec { val (preimage1, preimage2) = (randomBytes32, randomBytes32) val (paymentHash1, paymentHash2) = (Crypto.sha256(preimage1), Crypto.sha256(preimage2)) - def buildHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32, direction: Direction): DirectedHtlc = { + def buildHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): UpdateAddHtlc = { 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) - direction match { - case IN => IncomingHtlc(add) - case OUT => OutgoingHtlc(add) - } + UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) } + def buildHtlcIn(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = IncomingHtlc(buildHtlc(htlcId, channelId, paymentHash)) + + def buildHtlcOut(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = OutgoingHtlc(buildHtlc(htlcId, channelId, paymentHash)) + 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)) IncomingHtlc(UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)) @@ -432,15 +432,15 @@ object PostRestartHtlcCleanerSpec { def setupTrampolinePayments(nodeParams: NodeParams): TrampolinePaymentTest = { // Upstream HTLCs. val htlc_ab_1 = Seq( - buildHtlc(0, channelId_ab_1, paymentHash1, IN), - buildHtlc(2, channelId_ab_1, randomBytes32, OUT), // ignored - buildHtlc(3, channelId_ab_1, randomBytes32, OUT), // ignored - buildHtlc(5, channelId_ab_1, paymentHash2, IN) + buildHtlcIn(0, channelId_ab_1, paymentHash1), + buildHtlcOut(2, channelId_ab_1, randomBytes32), // ignored + buildHtlcOut(3, channelId_ab_1, randomBytes32), // ignored + buildHtlcIn(5, channelId_ab_1, paymentHash2) ) val htlc_ab_2 = Seq( - buildHtlc(1, channelId_ab_2, randomBytes32, OUT), // ignored - buildHtlc(7, channelId_ab_2, paymentHash1, IN), - buildHtlc(9, channelId_ab_2, randomBytes32, OUT) // ignored + buildHtlcOut(1, channelId_ab_2, randomBytes32), // ignored + buildHtlcIn(7, channelId_ab_2, paymentHash1), + buildHtlcOut(9, channelId_ab_2, randomBytes32) // ignored ) val origin_1 = Origin.TrampolineRelayed((channelId_ab_1, 0L) :: (channelId_ab_2, 7L) :: Nil, None) @@ -448,18 +448,18 @@ object PostRestartHtlcCleanerSpec { // Downstream HTLCs. val htlc_bc_1 = Seq( - buildHtlc(1, channelId_bc_1, randomBytes32, IN), // ignored - buildHtlc(6, channelId_bc_1, paymentHash1, OUT), - buildHtlc(8, channelId_bc_1, paymentHash2, OUT) + buildHtlcIn(1, channelId_bc_1, randomBytes32), // ignored + buildHtlcOut(6, channelId_bc_1, paymentHash1), + buildHtlcOut(8, channelId_bc_1, paymentHash2) ) val htlc_bc_2 = Seq( - buildHtlc(0, channelId_bc_2, randomBytes32, IN), // ignored - buildHtlc(1, channelId_bc_2, paymentHash2, OUT) + buildHtlcIn(0, channelId_bc_2, randomBytes32), // ignored + buildHtlcOut(1, channelId_bc_2, paymentHash2) ) val htlc_bc_3 = Seq( - buildHtlc(3, channelId_bc_3, randomBytes32, IN), // ignored - buildHtlc(4, channelId_bc_3, paymentHash2, OUT), - buildHtlc(5, channelId_bc_3, randomBytes32, IN) // ignored + buildHtlcIn(3, channelId_bc_3, randomBytes32), // ignored + buildHtlcOut(4, channelId_bc_3, paymentHash2), + buildHtlcIn(5, channelId_bc_3, randomBytes32) // ignored ) val downstream_1_1 = UpdateAddHtlc(channelId_bc_1, 6L, finalAmount, paymentHash1, finalExpiry, TestConstants.emptyOnionPacket) 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 3419cb7b91..2cdd6ec1e9 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 @@ -160,18 +160,18 @@ class TestVectorsSpec extends FunSuite with Logging { 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)) - case IN => Scripts.htlcReceived(Local.payment_privkey.publicKey, Remote.payment_privkey.publicKey, Local.revocation_pubkey, Crypto.ripemd160(htlc.add.paymentHash), htlc.add.cltvExpiry) - }) + val htlcScripts = htlcs.map { + case OutgoingHtlc(add) => Scripts.htlcOffered(Local.payment_privkey.publicKey, Remote.payment_privkey.publicKey, Local.revocation_pubkey, Crypto.ripemd160(add.paymentHash)) + case IncomingHtlc(add) => Scripts.htlcReceived(Local.payment_privkey.publicKey, Remote.payment_privkey.publicKey, Local.revocation_pubkey, Crypto.ripemd160(add.paymentHash), add.cltvExpiry) + } - def dir2string(dir: Direction) = dir match { - case IN => "remote->local" - case OUT => "local->remote" + def dir2string(htlc: DirectedHtlc) = htlc match { + case _: IncomingHtlc => "remote->local" + case _: OutgoingHtlc => "local->remote" } for (i <- 0 until htlcs.length) { - logger.info(s"htlc $i direction: ${dir2string(htlcs(i).direction)}") + logger.info(s"htlc $i direction: ${dir2string(htlcs(i))}") logger.info(s"htlc $i amount_msat: ${htlcs(i).add.amountMsat}") logger.info(s"htlc $i expiry: ${htlcs(i).add.cltvExpiry}") logger.info(s"htlc $i payment_preimage: ${paymentPreimages(i)}") @@ -204,13 +204,13 @@ class TestVectorsSpec extends FunSuite with Logging { logger.info(s"# base commitment transaction fee = ${baseFee.toLong}") val actualFee = fundingAmount - commitTx.tx.txOut.map(_.amount).sum logger.info(s"# actual commitment transaction fee = ${actualFee.toLong}") - commitTx.tx.txOut.map(txOut => { + commitTx.tx.txOut.foreach(txOut => { txOut.publicKeyScript.length match { case 22 => logger.info(s"# to-remote amount ${txOut.amount.toLong} P2WPKH(${Remote.payment_privkey.publicKey})") case 34 => val index = htlcScripts.indexWhere(s => Script.write(Script.pay2wsh(s)) == txOut.publicKeyScript) if (index == -1) logger.info(s"# to-local amount ${txOut.amount.toLong} wscript ${Script.write(Scripts.toLocalDelayed(Local.revocation_pubkey, Local.toSelfDelay, Local.delayed_payment_privkey.publicKey))}") - else logger.info(s"# HTLC ${if (htlcs(index).direction == OUT) "offered" else "received"} amount ${txOut.amount.toLong} wscript ${Script.write(htlcScripts(index))}") + else logger.info(s"# HTLC ${if (htlcs(index).isInstanceOf[OutgoingHtlc]) "offered" else "received"} amount ${txOut.amount.toLong} wscript ${Script.write(htlcScripts(index))}") } }) 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 0007dde654..a39fcca9c4 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 @@ -391,11 +391,9 @@ class TransactionsSpec extends FunSuite with Logging { case Failure(t) => fail(t) } - 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)) - } + def htlcIn(amount: Satoshi): DirectedHtlc = IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, amount.toMilliSatoshi, ByteVector32.Zeroes, CltvExpiry(144), TestConstants.emptyOnionPacket)) + def htlcOut(amount: Satoshi): DirectedHtlc = OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, amount.toMilliSatoshi, ByteVector32.Zeroes, CltvExpiry(144), TestConstants.emptyOnionPacket)) test("BOLT 2 fee tests") { @@ -424,8 +422,8 @@ class TransactionsSpec extends FunSuite with Logging { val htlcs = htlcRegex.findAllIn(s).map(l => { val htlcRegex(direction, amount) = l direction match { - case "offered" => htlc(OUT, Satoshi(amount.toLong)) - case "received" => htlc(IN, Satoshi(amount.toLong)) + case "offered" => htlcOut(Satoshi(amount.toLong)) + case "received" => htlcIn(Satoshi(amount.toLong)) } }).toSet TestSetup(name, dustLimit, CommitmentSpec(htlcs = htlcs, feeratePerKw = feerate_per_kw.toLong, toLocal = MilliSatoshi(to_local_msat.toLong), toRemote = MilliSatoshi(to_remote_msat.toLong)), Satoshi(fee.toLong)) 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 21aa6db03e..18d055308b 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 @@ -131,11 +131,6 @@ class ChannelCodecsSpec extends FunSuite { assert(withGlobalFeaturesDecoded.features === hex"028a") } - test("encode/decode direction") { - assert(directionCodec.decodeValue(directionCodec.encode(IN).require).require === IN) - assert(directionCodec.decodeValue(directionCodec.encode(OUT).require).require === OUT) - } - test("encode/decode htlc") { val add = UpdateAddHtlc( channelId = randomBytes32, @@ -150,6 +145,37 @@ class ChannelCodecsSpec extends FunSuite { assert(htlcCodec.decodeValue(htlcCodec.encode(htlc2).require).require === htlc2) } + test("backward compatibility of htlc codec") { + // these encoded HTLC were produced by a previous version of the codec (at commit 8932785e001ddfe32839b3f83468ea19cf00b289) + val encodedHtlc1 = hex"89d5618930919bd77c07ea3931e7791010a9637c3d06e091b2dad38f21bbcffa00000000384ffa48000000003dbf35101f8724fbd36096ea5f462be20d20bc2f93ebc2c8a3f00b7a78c5158b5a0e9f6b1666923e800f1d073e2adcfba5904f1b8234af1c43a6e84a862a044d15f33addf8d41b3cfb7f96d815d2248322aeadd0ce7bacbcc44e611f66c35c439423c099e678c077203d5fc415ec37b798c6c74c9eed0806e6cb20f2b613855772c086cee60642b3b9c3919627c877a62a57dcf6ce003bedc53a8b7a0d7aa91b2304ef8b512fe1a9a043e410d7cd3009edffcb5d4c05cfb545aced4afea8bbe26c5ff492602edf9d4eb731541e60e48fd1ae5e33b04a614346fb16e09ccd9bcb8907fe9fc287757ea9280a03462299e950a274c1dc53fbae8c421e67d7de35709eda0f11bcd417c0f215667e8b8ccae1035d0281214af25bf690102b180e5d4b57323d02ab5cee5d3669b4300539d02eff553143f085cd70e5428b7af3262418aa7664d56c3fd29c00a2f88a6a5ee9685f45b6182c45d492b2170b092e4b5891247bcffe82623b86637bec291cca1dc729f5747d842ecdf2fc24eaf95c522cbebe9a841e7cff837e715b689f0b366b92a2850875636962ba42863ab6df12ee938ada6e6ae795f8b4fbe81adea478caa9899fed0d6ccdf7a2173b69b2d3ff1b93c82c08c4da63b426d2f94912109997e8ee5830c5ffe3b60c97438ae1521a2956e73a9a60f16dc13a5e6565904e04bf66ceda3db693fc7a0c6ad4f7dc8cb7f1ef54527c11589b7c35ce5b20e7f23a0ab107a406fa747435ff08096a7533a8ab7f5d3630d5c20c9161101f922c76079497e00e3ca62bce033b2bb065ea1733c50b5a06492d2b46715812003f29a8754b5dc1649082893e7be76550c58d98e81556e4ddf20a244f363bc23e756c95224335d0eeccd3da06a9161c4c72ae3d93afe902a806eadd2167d15c04cf3028fc61d0843bd270fd702a2c5af889ab5bc79a294847914f8dd409a9b990a96397d9046c385ca9810fb7c7b2c61491c67264257a601be7fe8c47a859b56af41caf06be7ea1cdb540719fc3bc2603675b79fd36a6f2911043b78da9f186d2a01f1209d0d91508e8ebecce09fd72823d0c166542f6d059fa8725d9d719a2532289c88f7a291a6bbe01f5b1f83cc2232d716f7dfc6a103fb8637d759aab939aaa278cffe04a64f4142564113080276bee7d3ec62e3f887838e3821f0dd713337972df994160edc29ccb9b9630c41a9ec7c994cbef2501a610e1c3684e697df230fd6f6f10526c9446e8307a1fb7e4988cdf7fc8aa32c8a09206113d8247aaae42e3942c0ffd291d67837d2c88231c85882667582eca1d2566134c4ee1301de8e1637f09467b473ba3e353992488048bd34b26dcc6f6f474751b7ac5bbad468c64eda2aeabfe6a92150a4faab142229d7934c4a24427441850d0deae5db802b02940435f39ceaa85e2d3d2269510881ab26926c3167487aa138d38b9cf650f59f0aa0b84297479271c2009cde61e5c58c26bf8a15aba86869af83941ec14972d93b6ae4a6ecf6584238150a61487d6bd394db40a10d710fd2d065850e52ea6536a74d88947448221c1ce493fecbf2070998e04d5263935488c2935f2d3afed4d0fc7472c03e652f928e6a18f78029043f219f652d992e104529149a978e5c660c0081fe6a179dbe62dcb597f3b4e497c6049b0255f8f306e4b18c97c339c98270abf86a4eb1af93b14d880eeda203bb3ba5b6e3113d0e003f8e55f3d446bd4dcda686b357ca0adf1fe25390767a40ff086a9258d04c19b0474488aaafac321f087d2bd0dc0e056ad9f5b5afa5f3d82bc3f18b33de9044529637fed05879f6bd440f331c06008dd38c2fb822c22fc4201e97f9ef9fc351807c045dece147d19fd01a68604c3cb6b5e0db1b4d1ebe387670021067d94206fbdc9ed33ac1f49d87f961cb5d44f48805e55f8637ca3de4ec9dd969944ed61de45970b7ef96d9f313a41de1cae380e0fe4b56729f275e2a0a87403c90e80" + val encodedHtlc2 = hex"09d5618930919bd77c07ea3931e7791010a9637c3d06e091b2dad38f21bbcffa00000000384ffa48000000003dbf35101f8724fbd36096ea5f462be20d20bc2f93ebc2c8a3f00b7a78c5158b5a0e9f6b1666923e800f1d073e2adcfba5904f1b8234af1c43a6e84a862a044d15f33addf8d41b3cfb7f96d815d2248322aeadd0ce7bacbcc44e611f66c35c439423c099e678c077203d5fc415ec37b798c6c74c9eed0806e6cb20f2b613855772c086cee60642b3b9c3919627c877a62a57dcf6ce003bedc53a8b7a0d7aa91b2304ef8b512fe1a9a043e410d7cd3009edffcb5d4c05cfb545aced4afea8bbe26c5ff492602edf9d4eb731541e60e48fd1ae5e33b04a614346fb16e09ccd9bcb8907fe9fc287757ea9280a03462299e950a274c1dc53fbae8c421e67d7de35709eda0f11bcd417c0f215667e8b8ccae1035d0281214af25bf690102b180e5d4b57323d02ab5cee5d3669b4300539d02eff553143f085cd70e5428b7af3262418aa7664d56c3fd29c00a2f88a6a5ee9685f45b6182c45d492b2170b092e4b5891247bcffe82623b86637bec291cca1dc729f5747d842ecdf2fc24eaf95c522cbebe9a841e7cff837e715b689f0b366b92a2850875636962ba42863ab6df12ee938ada6e6ae795f8b4fbe81adea478caa9899fed0d6ccdf7a2173b69b2d3ff1b93c82c08c4da63b426d2f94912109997e8ee5830c5ffe3b60c97438ae1521a2956e73a9a60f16dc13a5e6565904e04bf66ceda3db693fc7a0c6ad4f7dc8cb7f1ef54527c11589b7c35ce5b20e7f23a0ab107a406fa747435ff08096a7533a8ab7f5d3630d5c20c9161101f922c76079497e00e3ca62bce033b2bb065ea1733c50b5a06492d2b46715812003f29a8754b5dc1649082893e7be76550c58d98e81556e4ddf20a244f363bc23e756c95224335d0eeccd3da06a9161c4c72ae3d93afe902a806eadd2167d15c04cf3028fc61d0843bd270fd702a2c5af889ab5bc79a294847914f8dd409a9b990a96397d9046c385ca9810fb7c7b2c61491c67264257a601be7fe8c47a859b56af41caf06be7ea1cdb540719fc3bc2603675b79fd36a6f2911043b78da9f186d2a01f1209d0d91508e8ebecce09fd72823d0c166542f6d059fa8725d9d719a2532289c88f7a291a6bbe01f5b1f83cc2232d716f7dfc6a103fb8637d759aab939aaa278cffe04a64f4142564113080276bee7d3ec62e3f887838e3821f0dd713337972df994160edc29ccb9b9630c41a9ec7c994cbef2501a610e1c3684e697df230fd6f6f10526c9446e8307a1fb7e4988cdf7fc8aa32c8a09206113d8247aaae42e3942c0ffd291d67837d2c88231c85882667582eca1d2566134c4ee1301de8e1637f09467b473ba3e353992488048bd34b26dcc6f6f474751b7ac5bbad468c64eda2aeabfe6a92150a4faab142229d7934c4a24427441850d0deae5db802b02940435f39ceaa85e2d3d2269510881ab26926c3167487aa138d38b9cf650f59f0aa0b84297479271c2009cde61e5c58c26bf8a15aba86869af83941ec14972d93b6ae4a6ecf6584238150a61487d6bd394db40a10d710fd2d065850e52ea6536a74d88947448221c1ce493fecbf2070998e04d5263935488c2935f2d3afed4d0fc7472c03e652f928e6a18f78029043f219f652d992e104529149a978e5c660c0081fe6a179dbe62dcb597f3b4e497c6049b0255f8f306e4b18c97c339c98270abf86a4eb1af93b14d880eeda203bb3ba5b6e3113d0e003f8e55f3d446bd4dcda686b357ca0adf1fe25390767a40ff086a9258d04c19b0474488aaafac321f087d2bd0dc0e056ad9f5b5afa5f3d82bc3f18b33de9044529637fed05879f6bd440f331c06008dd38c2fb822c22fc4201e97f9ef9fc351807c045dece147d19fd01a68604c3cb6b5e0db1b4d1ebe387670021067d94206fbdc9ed33ac1f49d87f961cb5d44f48805e55f8637ca3de4ec9dd969944ed61de45970b7ef96d9f313a41de1cae380e0fe4b56729f275e2a0a87403c90e80" + + val ref = UpdateAddHtlc( + ByteVector32(hex"13aac312612337aef80fd47263cef2202152c6f87a0dc12365b5a71e43779ff4"), + 1889531024, + 2071882272 msat, + ByteVector32(hex"3f0e49f7a6c12dd4be8c57c41a41785f27d7859147e016f4f18a2b16b41d3ed6"), + CltvExpiry(751641725), + OnionRoutingPacket( + 0, + hex"1e3a0e7c55b9f74b209e3704695e38874dd0950c54089a2be675bbf1a83679f6ff", + hex"2db02ba44906455d5ba19cf75979889cc23ecd86b88728478133ccf180ee407abf882bd86f6f318d8e993dda100dcd9641e56c270aaee5810d9dcc0c85677387232c4f90ef4c54afb9ed9c0077db8a7516f41af552364609df16a25fc3534087c821af9a6013dbff96ba980b9f6a8b59da95fd5177c4d8bfe924c05dbf3a9d6e62a83cc1c91fa35cbc676094c2868df62dc1399b3797120ffd3f850eeafd525014068c4533d2a144e983b8a7f75d18843ccfafbc6ae13db41e2379a82f81e42accfd171995c206ba05024295e4b7ed202056301cba96ae647a0556b9dcba6cd368600a73a05dfeaa6287e10b9ae1ca8516f5e64c483154ecc9aad87fa5380145f114d4bdd2d0be8b6c30588ba925642e16125c96b12248f79ffd04c4770cc6f7d85239943b8e53eae8fb085d9be5f849d5f2b8a4597d7d35083cf9ff06fce2b6d13e166cd725450a10eac6d2c574850c756dbe25dd2715b4dcd5cf2bf169f7d035bd48f19553133fda1ad99bef442e76d365a7fe372790581189b4c7684da5f2922421332fd1dcb0618bffc76c192e8715c2a43452adce7534c1e2db8274bccacb209c097ecd9db47b6d27f8f418d5a9efb9196fe3dea8a4f822b136f86b9cb641cfe47415620f480df4e8e86bfe1012d4ea675156feba6c61ab841922c2203f2458ec0f292fc01c794c579c06765760cbd42e678a16b40c925a568ce2b024007e5350ea96bb82c92105127cf7cecaa18b1b31d02aadc9bbe414489e6c77847cead92a44866ba1dd99a7b40d522c3898e55c7b275fd205500dd5ba42cfa2b8099e6051f8c3a10877a4e1fae05458b5f11356b78f3452908f229f1ba81353732152c72fb208d870b953021f6f8f658c29238ce4c84af4c037cffd188f50b36ad5e8395e0d7cfd439b6a80e33f87784c06ceb6f3fa6d4de52220876f1b53e30da5403e2413a1b22a11d1d7d99c13fae5047a182cca85eda0b3f50e4bb3ae3344a64513911ef45234d77c03eb63f07984465ae2defbf8d4207f70c6faeb35572735544f19ffc094c9e8284ac82261004ed7dcfa7d8c5c7f10f071c7043e1bae2666f2e5bf3282c1db853997372c6188353d8f932997de4a034c21c386d09cd2fbe461fadede20a4d9288dd060f43f6fc93119beff9154659141240c227b048f555c85c728581ffa523acf06fa591046390b104cceb05d943a4acc26989dc2603bd1c2c6fe128cf68e7747c6a73249100917a6964db98dede8e8ea36f58b775a8d18c9db455d57fcd5242a149f556284453af2698944884e8830a1a1bd5cbb700560528086be739d550bc5a7a44d2a21103564d24d862ce90f54271a71739eca1eb3e154170852e8f24e3840139bcc3cb8b184d7f142b5750d0d35f07283d8292e5b276d5c94dd9ecb084702a14c290fad7a729b681421ae21fa5a0cb0a1ca5d4ca6d4e9b1128e890443839c927fd97e40e1331c09aa4c726a9118526be5a75fda9a1f8e8e5807cca5f251cd431ef0052087e433eca5b325c208a5229352f1cb8cc180103fcd42f3b7cc5b96b2fe769c92f8c093604abf1e60dc963192f86739304e157f0d49d635f27629b101ddb440776774b6dc6227a1c007f1cabe7a88d7a9b9b4d0d66af9415be3fc4a720ecf481fe10d524b1a09833608e8911555f58643e10fa57a1b81c0ad5b3eb6b5f4be7b05787e31667bd2088a52c6ffda0b0f3ed7a881e66380c011ba7185f7045845f88403d2ff3df3f86a300f808bbd9c28fa33fa034d0c098796d6bc1b6369a3d7c70ece00420cfb2840df7b93da67583e93b0ff2c396ba89e9100bcabf0c6f947bc9d93bb2d3289", + ByteVector32(hex"dac3bc8b2e16fdf2db3e627483bc395c701c1fc96ace53e4ebc54150e807921d")) + ) + val remaining = bin"0000000" // 7 bits remainder because the direction is encoded with 1 bit and we are dealing with bytes + + val DecodeResult(h1, r1) = htlcCodec.decode(encodedHtlc1.toBitVector).require + val DecodeResult(h2, r2) = htlcCodec.decode(encodedHtlc2.toBitVector).require + + assert(h1 == IncomingHtlc(ref)) + assert(h2 == OutgoingHtlc(ref)) + assert(r1 == remaining) + assert(r2 == remaining) + + assert(htlcCodec.encode(h1).require.bytes === encodedHtlc1) + assert(htlcCodec.encode(h2).require.bytes === encodedHtlc2) + } + test("encode/decode commitment spec") { val add1 = UpdateAddHtlc( channelId = randomBytes32, @@ -214,20 +240,6 @@ 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 @@ -590,4 +602,4 @@ object ChannelCodecsSpec { new InputInfoSerializer } -} \ No newline at end of file +} diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala index a0525f2a9d..441c002855 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala @@ -28,12 +28,13 @@ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.db.{IncomingPaymentStatus, OutgoingPaymentStatus} import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.RouteResponse -import fr.acinq.eclair.transactions.Direction +import fr.acinq.eclair.transactions.DirectedHtlc import fr.acinq.eclair.transactions.Transactions.{InputInfo, TransactionWithInputInfo} import fr.acinq.eclair.wire._ import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, ShortChannelId, UInt64} import org.json4s.JsonAST._ -import org.json4s.{CustomKeySerializer, CustomSerializer, TypeHints, jackson} +import org.json4s.jackson.Serialization +import org.json4s.{CustomKeySerializer, CustomSerializer, DefaultFormats, Extraction, TypeHints, jackson} import scodec.bits.ByteVector /** @@ -208,10 +209,16 @@ class NodeAddressSerializer extends CustomSerializer[NodeAddress](_ => ( { case n: NodeAddress => JString(HostAndPort.fromParts(n.socketAddress.getHostString, n.socketAddress.getPort).toString) })) -class DirectionSerializer extends CustomSerializer[Direction](_ => ( { +class DirectedHtlcSerializer extends CustomSerializer[DirectedHtlc](_ => ( { null }, { - case d: Direction => JString(d.toString) + case h: DirectedHtlc => new JObject(List(("direction", JString(h.direction)), ("add", Extraction.decompose(h.add)( + DefaultFormats + + new ByteVector32Serializer + + new ByteVectorSerializer + + new PublicKeySerializer + + new MilliSatoshiSerializer + + new CltvExpirySerializer)))) })) class PaymentRequestSerializer extends CustomSerializer[PaymentRequest](_ => ( { @@ -308,7 +315,7 @@ object JsonSupport extends Json4sSupport { new ThrowableSerializer + new FailureMessageSerializer + new NodeAddressSerializer + - new DirectionSerializer + + new DirectedHtlcSerializer + new PaymentRequestSerializer + new JavaUUIDSerializer + CustomTypeHints.incomingPaymentStatus + diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala index 6d1e811f12..6b2b96cee5 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala @@ -22,8 +22,8 @@ import java.util.UUID import fr.acinq.bitcoin.{ByteVector32, OutPoint, Transaction} import fr.acinq.eclair._ import fr.acinq.eclair.payment.{PaymentRequest, PaymentSettlingOnChain} -import fr.acinq.eclair.transactions.{IN, OUT} -import fr.acinq.eclair.wire.{NodeAddress, Tor2, Tor3} +import fr.acinq.eclair.transactions.{IncomingHtlc, OutgoingHtlc} +import fr.acinq.eclair.wire.{NodeAddress, OnionRoutingPacket, Tor2, Tor3, UpdateAddHtlc} import org.scalatest.{FunSuite, Matchers} import scodec.bits._ @@ -60,9 +60,25 @@ class JsonSerializersSpec extends FunSuite with Matchers { JsonSupport.serialization.write(tor3)(org.json4s.DefaultFormats + new NodeAddressSerializer) shouldBe s""""aaaqeayeaudaocajbifqydiob4ibceqtcqkrmfyydenbwha5dypsaijc.onion:9999"""" } - test("Direction serialization") { - JsonSupport.serialization.write(IN)(org.json4s.DefaultFormats + new DirectionSerializer) shouldBe s""""IN"""" - JsonSupport.serialization.write(OUT)(org.json4s.DefaultFormats + new DirectionSerializer) shouldBe s""""OUT"""" + test("DirectedHtlc serialization") { + val add = UpdateAddHtlc( + channelId = ByteVector32(hex"345b2b05ec046ffe0c14d3b61838c79980713ad1cf8ae7a45c172ce90c9c0b9f"), + id = 926, + amountMsat = 12365.msat, + paymentHash = ByteVector32(hex"9fcd45bbaa09c60c991ac0425704163c3f3d2d683c789fa409455b9c97792692"), + cltvExpiry = CltvExpiry(621500), + onionRoutingPacket = OnionRoutingPacket( + version = 0, + publicKey = hex"0270685ca81a8e4d4d01beec5781f4cc924684072ae52c507f8ebe9daf0caaab7b", + payload = hex"3c7a66997c681a3de1bae56438abeee4fc50a16554725a430ade1dc8db6bdd76704d45c6151c4051d710cf487e63f8cbe9f5e537e6e518d7998f11c40277d551bee036d227a421f344c0185b4533660d200acbc4f4aa6148e29f813bb537e950a79ba961b80ccaa6ad808cb88f858ee73f8b1f129d3214d194f76fc011c46e18c2caec0fcb69715e79cb381e449e5be20281e0aaa92defa089c98b7e29c22181f2d1af9f5fe2a37ede4ac3163b123aa72318201b1128b17053c381e7bf111620dfb7ea3dcc5c28cafdd9cb7bb1a4202b64199aa02af145c563ba1b9f10288203ce2666f486aacb2ee385dc0b67915864c95174dfaac1e0ea195329d1741cd1febb4b49b33f84e0d10a5ec8ee0a1a94f73abf081ec69bb863edfeb46d24ce424b025ac3f593a7ba9419ec9b17fb39f0fbeac80e0898f95b6709cbc95f7c097e22e3a6ca0efbc947bbcd4a9077f6bd9daba25b18bb16179fca9d9cb2ce49fc7cbd2de589237926cacb87ea60e2cc60a90b47575517921b5529b8a95823dd0c3d02a7747d74c4ca927ba6b70c06c1c1ef27e14d371e8dd8f5d9380a65b08ae1e6384f9b3575c5d7278de22ce80e63612a27f3b3f45dbe32ee855185293c719e5a7203a682a08fd810c46fa12b67e61349831f8fae3f558090ea988e1a22ec877b790ea09169055529247c4dd597857aad74eaeb3a5879e96453e681e213f2796ed704d620509f34f91d9d16f881fd397e2836a0a4d2f1bcd230067f7acb5381a2b17e8c5135e38c4d258afbe4f69ac7ad39b789e99686ee926b3ad31b98993673313b7b18a4faaea238d8055824fde7339a791fc7777ef28cc4a1a5d177b3c3882ced4921c6cd85ae82e1fe13fe680ae432a9918ce37b15f88d4d18fb16b69e5369d18c204aaf7ee49b830bf2328380e6ad96e8f6a9e01bc2c97ffbaa402d5406dc7b83c6eb5d515ffc3bea8c42cf299c9e2bea693515246c6ff859d33ba6c4da4c4c1706e0c6b4a574e927f02eb92b7d56722cff80c3e6f6b98d1c84cb576abdcc27a6bc7b110fc2ac5fead57f05ad854c3331ce1ff94c0dc97303540ee797d71566497af09f20e3554d467528e1fed8e69438171072fe2deca3979a8f5ec9043b9bc4da921b095c29dc0294148c1b7001dafda4c48600d1194f745e6d0689c561bf19d20758c4d25fac64d81780607a4106e220ef546fc4026af7b9da8defb2fe3c21d48798ac67c794fb40aabe44618a8911673466be06808c6f54a772b87bcfafb4d120a9bebffb8051bf24bb332eaa769cf175c1aadb0186f8946dc32513fd81fe1a61bfb860886bdd070359a43e06e74607d300bd2e01a3f1ee900c4039e8db742170228db61ef0c77724c49d1573144564a80cc1ebc0449b34f84be35187ceba3fbc2facf5ad1f1e15945e3c6c236579aca7bc97e4cc76a3310022693b64008562b254a7d11c0086813e551c4817bbb72a1d6fbfc84d326ce973651200f80aa8ab0976c53c390249ca8e7e5ec21b80e70c3e0205983d313b28a5d1b9d9149501e05d3257c8ae88c6308e9e00feeab19121d1032a582e68ca1f9f64a1fd91cb5d8613b985fd4be22a4d5c14a132c20811a75ee3cc61de0b3fbc3254d61995d086603032269888b942ec0971ad26ea4b8df1746c5ec1de904ddeb5045abc0a6ede9d6a199ed0782cb69efa3a4dc00747553dbef12fb8299ca364b4cdf2ac3eba03b0d8b273684116bba4458b5717bc4aca5406901173a89b3643ccc076f22779fccf1ad69981e24eef18c711a2d58dfe5834b41f9e7166d54dc8628e754baaca1cbb7db8256f88ebc889de6078ba83a1af14a4", + hmac = ByteVector32(hex"9442626f72c475963dbddf8a57ab2cef3013eb3d6a5e8afbea9e631dac4481f5") + ) + ) + + val expectedIn = """{"direction":"IN","add":{"channelId":"345b2b05ec046ffe0c14d3b61838c79980713ad1cf8ae7a45c172ce90c9c0b9f","id":926,"amountMsat":12365,"paymentHash":"9fcd45bbaa09c60c991ac0425704163c3f3d2d683c789fa409455b9c97792692","cltvExpiry":621500,"onionRoutingPacket":{"version":0,"publicKey":"0270685ca81a8e4d4d01beec5781f4cc924684072ae52c507f8ebe9daf0caaab7b","payload":"3c7a66997c681a3de1bae56438abeee4fc50a16554725a430ade1dc8db6bdd76704d45c6151c4051d710cf487e63f8cbe9f5e537e6e518d7998f11c40277d551bee036d227a421f344c0185b4533660d200acbc4f4aa6148e29f813bb537e950a79ba961b80ccaa6ad808cb88f858ee73f8b1f129d3214d194f76fc011c46e18c2caec0fcb69715e79cb381e449e5be20281e0aaa92defa089c98b7e29c22181f2d1af9f5fe2a37ede4ac3163b123aa72318201b1128b17053c381e7bf111620dfb7ea3dcc5c28cafdd9cb7bb1a4202b64199aa02af145c563ba1b9f10288203ce2666f486aacb2ee385dc0b67915864c95174dfaac1e0ea195329d1741cd1febb4b49b33f84e0d10a5ec8ee0a1a94f73abf081ec69bb863edfeb46d24ce424b025ac3f593a7ba9419ec9b17fb39f0fbeac80e0898f95b6709cbc95f7c097e22e3a6ca0efbc947bbcd4a9077f6bd9daba25b18bb16179fca9d9cb2ce49fc7cbd2de589237926cacb87ea60e2cc60a90b47575517921b5529b8a95823dd0c3d02a7747d74c4ca927ba6b70c06c1c1ef27e14d371e8dd8f5d9380a65b08ae1e6384f9b3575c5d7278de22ce80e63612a27f3b3f45dbe32ee855185293c719e5a7203a682a08fd810c46fa12b67e61349831f8fae3f558090ea988e1a22ec877b790ea09169055529247c4dd597857aad74eaeb3a5879e96453e681e213f2796ed704d620509f34f91d9d16f881fd397e2836a0a4d2f1bcd230067f7acb5381a2b17e8c5135e38c4d258afbe4f69ac7ad39b789e99686ee926b3ad31b98993673313b7b18a4faaea238d8055824fde7339a791fc7777ef28cc4a1a5d177b3c3882ced4921c6cd85ae82e1fe13fe680ae432a9918ce37b15f88d4d18fb16b69e5369d18c204aaf7ee49b830bf2328380e6ad96e8f6a9e01bc2c97ffbaa402d5406dc7b83c6eb5d515ffc3bea8c42cf299c9e2bea693515246c6ff859d33ba6c4da4c4c1706e0c6b4a574e927f02eb92b7d56722cff80c3e6f6b98d1c84cb576abdcc27a6bc7b110fc2ac5fead57f05ad854c3331ce1ff94c0dc97303540ee797d71566497af09f20e3554d467528e1fed8e69438171072fe2deca3979a8f5ec9043b9bc4da921b095c29dc0294148c1b7001dafda4c48600d1194f745e6d0689c561bf19d20758c4d25fac64d81780607a4106e220ef546fc4026af7b9da8defb2fe3c21d48798ac67c794fb40aabe44618a8911673466be06808c6f54a772b87bcfafb4d120a9bebffb8051bf24bb332eaa769cf175c1aadb0186f8946dc32513fd81fe1a61bfb860886bdd070359a43e06e74607d300bd2e01a3f1ee900c4039e8db742170228db61ef0c77724c49d1573144564a80cc1ebc0449b34f84be35187ceba3fbc2facf5ad1f1e15945e3c6c236579aca7bc97e4cc76a3310022693b64008562b254a7d11c0086813e551c4817bbb72a1d6fbfc84d326ce973651200f80aa8ab0976c53c390249ca8e7e5ec21b80e70c3e0205983d313b28a5d1b9d9149501e05d3257c8ae88c6308e9e00feeab19121d1032a582e68ca1f9f64a1fd91cb5d8613b985fd4be22a4d5c14a132c20811a75ee3cc61de0b3fbc3254d61995d086603032269888b942ec0971ad26ea4b8df1746c5ec1de904ddeb5045abc0a6ede9d6a199ed0782cb69efa3a4dc00747553dbef12fb8299ca364b4cdf2ac3eba03b0d8b273684116bba4458b5717bc4aca5406901173a89b3643ccc076f22779fccf1ad69981e24eef18c711a2d58dfe5834b41f9e7166d54dc8628e754baaca1cbb7db8256f88ebc889de6078ba83a1af14a4","hmac":"9442626f72c475963dbddf8a57ab2cef3013eb3d6a5e8afbea9e631dac4481f5"}}}""" + + JsonSupport.serialization.write(IncomingHtlc(add))(org.json4s.DefaultFormats + new DirectedHtlcSerializer) shouldBe expectedIn + JsonSupport.serialization.write(OutgoingHtlc(add))(org.json4s.DefaultFormats + new DirectedHtlcSerializer) shouldBe expectedIn.replace("IN", "OUT") } test("Payment Request") {