diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index b2de25ee7c..fc78e6c470 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -68,6 +68,11 @@ eclair { max-to-local-delay-blocks = 2016 // maximum number of blocks that we are ready to accept for our own delayed outputs (2016 ~ 2 weeks) mindepth-blocks = 3 expiry-delta-blocks = 144 + // When we receive the pre-image for an HTLC and want to fulfill it but the upstream peer stops responding, we want to + // avoid letting its HTLC-timeout transaction become enforceable on-chain (otherwise there is a race condition between + // our HTLC-success and their HTLC-timeout). + // We will close the channel when the HTLC-timeout will happen in less than this number. + fulfill-safety-before-timeout-blocks = 6 fee-base-msat = 1000 fee-proportional-millionths = 100 // fee charged per transferred satoshi in millionths of a satoshi (100 = 0.01%) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index 2e40b4f4ce..41f8e358ff 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -21,7 +21,6 @@ import java.util.BitSet import scodec.bits.ByteVector - /** * Created by PM on 13/02/2017. */ @@ -36,12 +35,13 @@ object Features { val CHANNEL_RANGE_QUERIES_BIT_MANDATORY = 6 val CHANNEL_RANGE_QUERIES_BIT_OPTIONAL = 7 + val VARIABLE_LENGTH_ONION_MANDATORY = 8 + val VARIABLE_LENGTH_ONION_OPTIONAL = 9 def hasFeature(features: BitSet, bit: Int): Boolean = features.get(bit) def hasFeature(features: ByteVector, bit: Int): Boolean = hasFeature(BitSet.valueOf(features.reverse.toArray), bit) - /** * Check that the features that we understand are correctly specified, and that there are no mandatory features that * we don't understand (even bits) @@ -51,7 +51,8 @@ object Features { for (i <- 0 until bitset.length() by 2) { if (bitset.get(i) && !supportedMandatoryFeatures.contains(i)) return false } - return true + + true } /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 9266d701e7..d707fa9e6c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -50,6 +50,7 @@ case class NodeParams(keyManager: KeyManager, maxHtlcValueInFlightMsat: UInt64, maxAcceptedHtlcs: Int, expiryDeltaBlocks: Int, + fulfillSafetyBeforeTimeoutBlocks: Int, htlcMinimumMsat: Int, toRemoteDelayBlocks: Int, maxToLocalDelayBlocks: Int, @@ -149,6 +150,10 @@ object NodeParams { val offeredCLTV = config.getInt("to-remote-delay-blocks") require(maxToLocalCLTV <= Channel.MAX_TO_SELF_DELAY && offeredCLTV <= Channel.MAX_TO_SELF_DELAY, s"CLTV delay values too high, max is ${Channel.MAX_TO_SELF_DELAY}") + val expiryDeltaBlocks = config.getInt("expiry-delta-blocks") + val fulfillSafetyBeforeTimeoutBlocks = config.getInt("fulfill-safety-before-timeout-blocks") + require(fulfillSafetyBeforeTimeoutBlocks < expiryDeltaBlocks, "fulfill-safety-before-timeout-blocks must be smaller than expiry-delta-blocks") + val nodeAlias = config.getString("node-alias") require(nodeAlias.getBytes("UTF-8").length <= 32, "invalid alias, too long (max allowed 32 bytes)") @@ -156,7 +161,7 @@ object NodeParams { val p = PublicKey(ByteVector.fromValidHex(e.getString("nodeid"))) val gf = ByteVector.fromValidHex(e.getString("global-features")) val lf = ByteVector.fromValidHex(e.getString("local-features")) - (p -> (gf, lf)) + p -> (gf, lf) }.toMap val socksProxy_opt = if (config.getBoolean("socks5.enabled")) { @@ -187,7 +192,8 @@ object NodeParams { dustLimitSatoshis = dustLimitSatoshis, maxHtlcValueInFlightMsat = UInt64(config.getLong("max-htlc-value-in-flight-msat")), maxAcceptedHtlcs = maxAcceptedHtlcs, - expiryDeltaBlocks = config.getInt("expiry-delta-blocks"), + expiryDeltaBlocks = expiryDeltaBlocks, + fulfillSafetyBeforeTimeoutBlocks = fulfillSafetyBeforeTimeoutBlocks, htlcMinimumMsat = config.getInt("htlc-minimum-msat"), toRemoteDelayBlocks = config.getInt("to-remote-delay-blocks"), maxToLocalDelayBlocks = config.getInt("max-to-local-delay-blocks"), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 4f9efdb61c..b798b409fe 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.channel import akka.actor.{ActorRef, FSM, OneForOneStrategy, Props, Status, SupervisorStrategy} import akka.event.Logging.MDC import akka.pattern.pipe -import fr.acinq.bitcoin.Crypto.{PublicKey, PrivateKey, sha256} +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256} import fr.acinq.bitcoin._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain._ @@ -98,7 +98,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId import nodeParams.keyManager // we pass these to helpers classes so that they have the logging context - implicit def implicitLog = log + implicit def implicitLog: akka.event.LoggingAdapter = log val forwarder = context.actorOf(Props(new Forwarder(nodeParams)), "forwarder") @@ -146,7 +146,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId when(WAIT_FOR_INIT_INTERNAL)(handleExceptions { case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, _, localParams, remote, _, channelFlags), Nothing) => - context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, true, temporaryChannelId)) + context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, isFunder = true, temporaryChannelId)) forwarder ! remote val open = OpenChannel(nodeParams.chainHash, temporaryChannelId = temporaryChannelId, @@ -270,7 +270,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId Try(Helpers.validateParamsFundee(nodeParams, open)) match { case Failure(t) => handleLocalError(t, d, Some(open)) case Success(_) => - context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, false, open.temporaryChannelId)) + context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, isFunder = false, open.temporaryChannelId)) // TODO: maybe also check uniqueness of temporary channel id val minimumDepth = nodeParams.minDepthBlocks val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId, @@ -762,7 +762,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // we were waiting for our pending htlcs to be signed before replying with our local shutdown val localShutdown = Shutdown(d.channelId, commitments1.localParams.defaultFinalScriptPubKey) // note: it means that we had pending htlcs to sign, therefore we go to SHUTDOWN, not to NEGOTIATING - require(commitments1.remoteCommit.spec.htlcs.size > 0, "we must have just signed new htlcs, otherwise we would have sent our Shutdown earlier") + require(commitments1.remoteCommit.spec.htlcs.nonEmpty, "we must have just signed new htlcs, otherwise we would have sent our Shutdown earlier") goto(SHUTDOWN) using store(DATA_SHUTDOWN(commitments1, localShutdown, d.remoteShutdown.get)) sending localShutdown } else { stay using store(d.copy(commitments = commitments1)) @@ -849,8 +849,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } } - case Event(c@CurrentBlockCount(count), d: DATA_NORMAL) if d.commitments.timedoutOutgoingHtlcs(count).nonEmpty => - handleLocalError(HtlcTimedout(d.channelId, d.commitments.timedoutOutgoingHtlcs(count)), d, Some(c)) + case Event(c: CurrentBlockCount, d: DATA_NORMAL) => handleNewBlock(c, d) case Event(c@CurrentFeerates(feeratesPerKw), d: DATA_NORMAL) => handleCurrentFeerate(c, d) @@ -926,7 +925,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_NORMAL) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) - case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_NORMAL) if Some(tx.txid) == d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.txid) => handleRemoteSpentNext(tx, d) + case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_NORMAL) if d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.txid).contains(tx.txid) => handleRemoteSpentNext(tx, d) case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_NORMAL) => handleRemoteSpentOther(tx, d) @@ -1124,15 +1123,14 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(r: RevocationTimeout, d: DATA_SHUTDOWN) => handleRevocationTimeout(r, d) - case Event(c@CurrentBlockCount(count), d: DATA_SHUTDOWN) if d.commitments.timedoutOutgoingHtlcs(count).nonEmpty => - handleLocalError(HtlcTimedout(d.channelId, d.commitments.timedoutOutgoingHtlcs(count)), d, Some(c)) + case Event(c: CurrentBlockCount, d: DATA_SHUTDOWN) => handleNewBlock(c, d) case Event(c@CurrentFeerates(feerates), d: DATA_SHUTDOWN) => handleCurrentFeerate(c, d) case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_SHUTDOWN) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) - case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_SHUTDOWN) if Some(tx.txid) == d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.txid) => handleRemoteSpentNext(tx, d) + case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_SHUTDOWN) if d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.txid).contains(tx.txid) => handleRemoteSpentNext(tx, d) case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_SHUTDOWN) => handleRemoteSpentOther(tx, d) @@ -1146,7 +1144,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(c@ClosingSigned(_, remoteClosingFee, remoteSig), d: DATA_NEGOTIATING) => log.info(s"received closingFeeSatoshis=$remoteClosingFee") Closing.checkClosingSignature(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, Satoshi(remoteClosingFee), remoteSig) match { - case Success(signedClosingTx) if Some(remoteClosingFee) == d.closingTxProposed.last.lastOption.map(_.localClosingSigned.feeSatoshis) || d.closingTxProposed.flatten.size >= MAX_NEGOTIATION_ITERATIONS => + case Success(signedClosingTx) if d.closingTxProposed.last.lastOption.map(_.localClosingSigned.feeSatoshis).contains(remoteClosingFee) || d.closingTxProposed.flatten.size >= MAX_NEGOTIATION_ITERATIONS => // we close when we converge or when there were too many iterations handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) case Success(signedClosingTx) => @@ -1156,7 +1154,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId localClosingFee = lastLocalClosingFee.getOrElse(Closing.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey)), remoteClosingFee = Satoshi(remoteClosingFee)) val (closingTx, closingSigned) = Closing.makeClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nextClosingFee) - if (Some(nextClosingFee) == lastLocalClosingFee) { + if (lastLocalClosingFee.contains(nextClosingFee)) { // next computed fee is the same than the one we previously sent (probably because of rounding), let's close now handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) } else if (nextClosingFee == Satoshi(remoteClosingFee)) { @@ -1181,7 +1179,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_NEGOTIATING) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) - case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_NEGOTIATING) if Some(tx.txid) == d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.txid) => handleRemoteSpentNext(tx, d) + case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_NEGOTIATING) if d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.txid).contains(tx.txid) => handleRemoteSpentNext(tx, d) case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_NEGOTIATING) => handleRemoteSpentOther(tx, d) @@ -1197,19 +1195,19 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Success((commitments1, _)) => log.info(s"got valid payment preimage, recalculating transactions to redeem the corresponding htlc on-chain") val localCommitPublished1 = d.localCommitPublished.map { - case localCommitPublished => + localCommitPublished => val localCommitPublished1 = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, commitments1, localCommitPublished.commitTx) doPublish(localCommitPublished1) localCommitPublished1 } val remoteCommitPublished1 = d.remoteCommitPublished.map { - case remoteCommitPublished => + remoteCommitPublished => val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx) doPublish(remoteCommitPublished1) remoteCommitPublished1 } val nextRemoteCommitPublished1 = d.nextRemoteCommitPublished.map { - case remoteCommitPublished => + remoteCommitPublished => val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx) doPublish(remoteCommitPublished1) remoteCommitPublished1 @@ -1231,22 +1229,22 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } else if (d.mutualCloseProposed.map(_.txid).contains(tx.txid)) { // at any time they can publish a closing tx with any sig we sent them handleMutualClose(tx, Right(d)) - } else if (Some(tx.txid) == d.localCommitPublished.map(_.commitTx.txid)) { + } else if (d.localCommitPublished.map(_.commitTx.txid).contains(tx.txid)) { // this is because WatchSpent watches never expire and we are notified multiple times stay - } else if (Some(tx.txid) == d.remoteCommitPublished.map(_.commitTx.txid)) { + } else if (d.remoteCommitPublished.map(_.commitTx.txid).contains(tx.txid)) { // this is because WatchSpent watches never expire and we are notified multiple times stay - } else if (Some(tx.txid) == d.nextRemoteCommitPublished.map(_.commitTx.txid)) { + } else if (d.nextRemoteCommitPublished.map(_.commitTx.txid).contains(tx.txid)) { // this is because WatchSpent watches never expire and we are notified multiple times stay - } else if (Some(tx.txid) == d.futureRemoteCommitPublished.map(_.commitTx.txid)) { + } else if (d.futureRemoteCommitPublished.map(_.commitTx.txid).contains(tx.txid)) { // this is because WatchSpent watches never expire and we are notified multiple times stay } else if (tx.txid == d.commitments.remoteCommit.txid) { // counterparty may attempt to spend its last commit tx at any time handleRemoteSpentCurrent(tx, d) - } else if (Some(tx.txid) == d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.txid)) { + } else if (d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.txid).contains(tx.txid)) { // counterparty may attempt to spend its last commit tx at any time handleRemoteSpentNext(tx, d) } else { @@ -1291,7 +1289,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val futureRemoteCommitPublished1 = d.futureRemoteCommitPublished.map(Closing.updateRemoteCommitPublished(_, tx)) val revokedCommitPublished1 = d.revokedCommitPublished.map(Closing.updateRevokedCommitPublished(_, tx)) // if the local commitment tx just got confirmed, let's send an event telling when we will get the main output refund - if (localCommitPublished1.map(_.commitTx.txid) == Some(tx.txid)) { + if (localCommitPublished1.map(_.commitTx.txid).contains(tx.txid)) { context.system.eventStream.publish(LocalCommitConfirmed(self, remoteNodeId, d.channelId, blockHeight + d.commitments.remoteParams.toSelfDelay)) } // we may need to fail some htlcs in case a commitment tx was published and they have reached the timeout threshold @@ -1329,7 +1327,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // we update the channel data val d1 = d.copy(localCommitPublished = localCommitPublished1, remoteCommitPublished = remoteCommitPublished1, nextRemoteCommitPublished = nextRemoteCommitPublished1, futureRemoteCommitPublished = futureRemoteCommitPublished1, revokedCommitPublished = revokedCommitPublished1) // and we also send events related to fee - Closing.networkFeePaid(tx, d1) map { case (fee, desc) => feePaid(fee, tx, desc, d.channelId) } + Closing.networkFeePaid(tx, d1) foreach { case (fee, desc) => feePaid(fee, tx, desc, d.channelId) } // then let's see if any of the possible close scenarii can be considered done val closingType_opt = Closing.isClosed(d1, Some(tx)) // finally, if one of the unilateral closes is done, we move to CLOSED state, otherwise we stay (note that we don't store the state) @@ -1363,7 +1361,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case d: HasCommitments => log.info(s"deleting database record for channelId=${d.channelId}") nodeParams.db.channels.removeChannel(d.channelId) - case _ => {} + case _ => } log.info("shutting down") stop(FSM.Normal) @@ -1406,11 +1404,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId goto(SYNCING) using d1 sending channelReestablish - case Event(c@CurrentBlockCount(count), d: HasCommitments) if d.commitments.timedoutOutgoingHtlcs(count).nonEmpty => - // note: this can only happen if state is NORMAL or SHUTDOWN - // -> in NEGOTIATING there are no more htlcs - // -> in CLOSING we either have mutual closed (so no more htlcs), or already have unilaterally closed (so no action required), and we can't be in OFFLINE state anyway - handleLocalError(HtlcTimedout(d.channelId, d.commitments.timedoutOutgoingHtlcs(count)), d, Some(c)) + // note: this can only happen if state is NORMAL or SHUTDOWN + // -> in NEGOTIATING there are no more htlcs + // -> in CLOSING we either have mutual closed (so no more htlcs), or already have unilaterally closed (so no action required), and we can't be in OFFLINE state anyway + case Event(c: CurrentBlockCount, d: HasCommitments) => handleNewBlock(c, d) case Event(c: CurrentFeerates, d: HasCommitments) => handleOfflineFeerate(c, d) @@ -1462,7 +1459,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // if next_remote_revocation_number is greater than our local commitment index, it means that either we are using an outdated commitment, or they are lying // but first we need to make sure that the last per_commitment_secret that they claim to have received from us is correct for that next_remote_revocation_number minus 1 if (keyManager.commitmentSecret(d.commitments.localParams.channelKeyPath, nextRemoteRevocationNumber - 1) == yourLastPerCommitmentSecret) { - log.warning(s"counterparty proved that we have an outdated (revoked) local commitment!!! ourCommitmentNumber=${d.commitments.localCommit.index} theirCommitmentNumber=${nextRemoteRevocationNumber}") + log.warning(s"counterparty proved that we have an outdated (revoked) local commitment!!! ourCommitmentNumber=${d.commitments.localCommit.index} theirCommitmentNumber=$nextRemoteRevocationNumber") // their data checks out, we indeed seem to be using an old revoked commitment, and must absolutely *NOT* publish it, because that would be a cheating attempt and they // would punish us by taking all the funds in the channel val exc = PleasePublishYourCommitment(d.channelId) @@ -1474,7 +1471,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } case ChannelReestablish(_, nextLocalCommitmentNumber, _, _, _) if !Helpers.checkRemoteCommit(d, nextLocalCommitmentNumber) => // if next_local_commit_number is more than one more our remote commitment index, it means that either we are using an outdated commitment, or they are lying - log.warning(s"counterparty says that they have a more recent commitment than the one we know of!!! ourCommitmentNumber=${d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.index).getOrElse(d.commitments.remoteCommit.index)} theirCommitmentNumber=${nextLocalCommitmentNumber}") + log.warning(s"counterparty says that they have a more recent commitment than the one we know of!!! ourCommitmentNumber=${d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.index).getOrElse(d.commitments.remoteCommit.index)} theirCommitmentNumber=$nextLocalCommitmentNumber") // there is no way to make sure that they are saying the truth, the best thing to do is ask them to publish their commitment right now // maybe they will publish their commitment, in that case we need to remember their commitment point in order to be able to claim our outputs // not that if they don't comply, we could publish our own commitment (it is not stale, otherwise we would be in the case above) @@ -1547,8 +1544,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId goto(NEGOTIATING) using d.copy(closingTxProposed = closingTxProposed1) sending d.localShutdown } - case Event(c@CurrentBlockCount(count), d: HasCommitments) if d.commitments.timedoutOutgoingHtlcs(count).nonEmpty => - handleLocalError(HtlcTimedout(d.channelId, d.commitments.timedoutOutgoingHtlcs(count)), d, Some(c)) + case Event(c: CurrentBlockCount, d: HasCommitments) => handleNewBlock(c, d) case Event(c: CurrentFeerates, d: HasCommitments) => handleOfflineFeerate(c, d) @@ -1713,13 +1709,13 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId /** * This function is used to return feedback to user at channel opening */ - def replyToUser(message: Either[Channel.ChannelError, String]) = { + def replyToUser(message: Either[Channel.ChannelError, String]): Unit = { val m = message match { case Left(LocalError(t)) => Status.Failure(t) case Left(RemoteError(e)) => Status.Failure(new RuntimeException(s"peer sent error: ascii='${e.toAscii}' bin=${e.data.toHex}")) case Right(s) => s } - origin_opt.map(_ ! m) + origin_opt.foreach(_ ! m) } def handleCurrentFeerate(c: CurrentFeerates, d: HasCommitments) = { @@ -1770,10 +1766,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId /** * When we are funder, we use this function to detect when our funding tx has been double-spent (by another transaction * that we made for some reason). If the funding tx has been double spent we can forget about the channel. - * - * @param fundingTx */ - def checkDoubleSpent(fundingTx: Transaction) = { + def checkDoubleSpent(fundingTx: Transaction): Unit = { log.debug(s"checking status of funding tx txid=${fundingTx.txid}") wallet.doubleSpent(fundingTx).onComplete { case Success(true) => @@ -1798,7 +1792,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // we also check if the funding tx has been double-spent checkDoubleSpent(fundingTx) context.system.scheduler.scheduleOnce(1 day, blockchain, GetTxWithMeta(txid)) - case None if (now.seconds - waitingSince.seconds) > FUNDING_TIMEOUT_FUNDEE && (now.seconds - lastBlockTimestamp.seconds) < 1.hour => + case None if (now.seconds - waitingSince.seconds) > FUNDING_TIMEOUT_FUNDEE && (now.seconds - lastBlockTimestamp.seconds) < 1.hour => // if we are fundee, we give up after some time // NB: we want to be sure that the blockchain is in sync to prevent false negatives log.warning(s"funding tx hasn't been published in ${(now.seconds - waitingSince.seconds).toDays} days and blockchain is fresh from ${(now.seconds - lastBlockTimestamp.seconds).toMinutes} minutes ago") @@ -1858,6 +1852,34 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } } + def handleNewBlock(c: CurrentBlockCount, d: HasCommitments) = { + val timedOutOutgoing = d.commitments.timedOutOutgoingHtlcs(c.blockCount) + val almostTimedOutIncoming = d.commitments.almostTimedOutIncomingHtlcs(c.blockCount, nodeParams.fulfillSafetyBeforeTimeoutBlocks) + if (timedOutOutgoing.nonEmpty) { + // Downstream timed out. + handleLocalError(HtlcTimedout(d.channelId, timedOutOutgoing), d, Some(c)) + } else if (almostTimedOutIncoming.nonEmpty) { + // Upstream is close to timing out. + val relayedFulfills = d.commitments.localChanges.all.collect { case u: UpdateFulfillHtlc => u.id }.toSet + val offendingRelayedHtlcs = almostTimedOutIncoming.filter(htlc => relayedFulfills.contains(htlc.id)) + if (offendingRelayedHtlcs.nonEmpty) { + handleLocalError(HtlcWillTimeoutUpstream(d.channelId, offendingRelayedHtlcs), d, Some(c)) + } else { + // There might be pending fulfill commands that we haven't relayed yet. + // Since this involves a DB call, we only want to check it if all the previous checks failed (this is the slow path). + val pendingRelayFulfills = nodeParams.db.pendingRelay.listPendingRelay(d.channelId).collect { case CMD_FULFILL_HTLC(id, r, _) => id } + val offendingPendingRelayFulfills = almostTimedOutIncoming.filter(htlc => pendingRelayFulfills.contains(htlc.id)) + if (offendingPendingRelayFulfills.nonEmpty) { + handleLocalError(HtlcWillTimeoutUpstream(d.channelId, offendingPendingRelayFulfills), d, Some(c)) + } else { + stay + } + } + } else { + stay + } + } + def handleLocalError(cause: Throwable, d: Data, msg: Option[Any]) = { cause match { case _: ForcedLocalCommit => log.warning(s"force-closing channel at user request") @@ -1908,7 +1930,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId goto(CLOSING) using store(nextData) } - def doPublish(closingTx: Transaction) = { + def doPublish(closingTx: Transaction): Unit = { blockchain ! PublishAsap(closingTx) blockchain ! WatchConfirmed(self, closingTx, nodeParams.minDepthBlocks, BITCOIN_TX_CONFIRMED(closingTx)) } @@ -1943,11 +1965,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId /** * This helper method will publish txes only if they haven't yet reached minDepth - * - * @param txes - * @param irrevocablySpent */ - def publishIfNeeded(txes: Iterable[Transaction], irrevocablySpent: Map[OutPoint, ByteVector32]) = { + def publishIfNeeded(txes: Iterable[Transaction], irrevocablySpent: Map[OutPoint, ByteVector32]): Unit = { val (skip, process) = txes.partition(Closing.inputsAlreadySpent(_, irrevocablySpent)) process.foreach { tx => log.info(s"publishing txid=${tx.txid}") @@ -1958,11 +1977,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId /** * This helper method will watch txes only if they haven't yet reached minDepth - * - * @param txes - * @param irrevocablySpent */ - def watchConfirmedIfNeeded(txes: Iterable[Transaction], irrevocablySpent: Map[OutPoint, ByteVector32]) = { + def watchConfirmedIfNeeded(txes: Iterable[Transaction], irrevocablySpent: Map[OutPoint, ByteVector32]): Unit = { val (skip, process) = txes.partition(Closing.inputsAlreadySpent(_, irrevocablySpent)) process.foreach(tx => blockchain ! WatchConfirmed(self, tx, nodeParams.minDepthBlocks, BITCOIN_TX_CONFIRMED(tx))) skip.foreach(tx => log.info(s"no need to watch txid=${tx.txid}, it has already been confirmed")) @@ -1970,18 +1986,14 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId /** * This helper method will watch txes only if the utxo they spend hasn't already been irrevocably spent - * - * @param parentTx - * @param txes - * @param irrevocablySpent */ - def watchSpentIfNeeded(parentTx: Transaction, txes: Iterable[Transaction], irrevocablySpent: Map[OutPoint, ByteVector32]) = { + def watchSpentIfNeeded(parentTx: Transaction, txes: Iterable[Transaction], irrevocablySpent: Map[OutPoint, ByteVector32]): Unit = { val (skip, process) = txes.partition(Closing.inputsAlreadySpent(_, irrevocablySpent)) process.foreach(tx => blockchain ! WatchSpent(self, parentTx, tx.txIn.head.outPoint.index.toInt, BITCOIN_OUTPUT_SPENT)) skip.foreach(tx => log.info(s"no need to watch txid=${tx.txid}, it has already been confirmed")) } - def doPublish(localCommitPublished: LocalCommitPublished) = { + def doPublish(localCommitPublished: LocalCommitPublished): Unit = { import localCommitPublished._ val publishQueue = List(commitTx) ++ claimMainDelayedOutputTx ++ htlcSuccessTxs ++ htlcTimeoutTxs ++ claimHtlcDelayedTxs @@ -2045,7 +2057,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId goto(CLOSING) using store(nextData) } - def doPublish(remoteCommitPublished: RemoteCommitPublished) = { + def doPublish(remoteCommitPublished: RemoteCommitPublished): Unit = { import remoteCommitPublished._ val publishQueue = claimMainOutputTx ++ claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs @@ -2087,7 +2099,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } } - def doPublish(revokedCommitPublished: RevokedCommitPublished) = { + def doPublish(revokedCommitPublished: RevokedCommitPublished): Unit = { import revokedCommitPublished._ val publishQueue = claimMainOutputTx ++ mainPenaltyTx ++ htlcPenaltyTxs ++ claimHtlcDelayedPenaltyTxs @@ -2130,7 +2142,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId log.debug(s"localNextHtlcId=${d.commitments.localNextHtlcId}->${commitments1.localNextHtlcId}") log.debug(s"remoteNextHtlcId=${d.commitments.remoteNextHtlcId}->${commitments1.remoteNextHtlcId}") - def resendRevocation = { + def resendRevocation(): Unit = { // let's see the state of remote sigs if (commitments1.localCommit.index == channelReestablish.nextRemoteRevocationNumber) { // nothing to do @@ -2155,24 +2167,24 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // they had received the new sig but their revocation was lost during the disconnection // they will send us the revocation, nothing to do here log.debug(s"waiting for them to re-send their last revocation") - resendRevocation + resendRevocation() case Left(waitingForRevocation) if waitingForRevocation.nextRemoteCommit.index == channelReestablish.nextLocalCommitmentNumber => // we had sent a new sig and were waiting for their revocation // they didn't receive the new sig because of the disconnection // we just resend the same updates and the same sig val revWasSentLast = commitments1.localCommit.index > waitingForRevocation.sentAfterLocalCommitIndex - if (!revWasSentLast) resendRevocation + if (!revWasSentLast) resendRevocation() log.debug(s"re-sending previously local signed changes: ${commitments1.localChanges.signed.map(Commitments.msg2String(_)).mkString(",")}") commitments1.localChanges.signed.foreach(forwarder ! _) log.debug(s"re-sending the exact same previous sig") forwarder ! waitingForRevocation.sent - if (revWasSentLast) resendRevocation + if (revWasSentLast) resendRevocation() case Right(_) if commitments1.remoteCommit.index + 1 == channelReestablish.nextLocalCommitmentNumber => // there wasn't any sig in-flight when the disconnection occurred - resendRevocation + resendRevocation() case _ => throw CommitmentSyncError(d.channelId) } @@ -2185,8 +2197,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // let's now fail all pending htlc for which we are the final payee val htlcsToFail = commitments1.remoteCommit.spec.htlcs.collect { - case DirectedHtlc(OUT, add) if Sphinx.parsePacket(nodeParams.privateKey, add.paymentHash, add.onionRoutingPacket) - .map(_.nextPacket.isLastPacket).getOrElse(true) => add // we also fail htlcs which onion we can't decode (message won't be precise) + case DirectedHtlc(OUT, add) if Sphinx.PaymentPacket.peel(nodeParams.privateKey, add.paymentHash, add.onionRoutingPacket).fold( + _ => true, // we also fail htlcs which onion we can't decode (message won't be precise) + p => p.isLastPacket + ) => add } log.debug(s"failing htlcs=${htlcsToFail.map(Commitments.msg2String(_)).mkString(",")}") @@ -2217,7 +2231,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Right(u) => Relayed(u.channelId, u.id, u.amountMsat, c.amountMsat) // this is a relayed payment } - def feePaid(fee: Satoshi, tx: Transaction, desc: String, channelId: ByteVector32) = { + def feePaid(fee: Satoshi, tx: Transaction, desc: String, channelId: ByteVector32): Unit = { log.info(s"paid feeSatoshi=${fee.amount} for txid=${tx.txid} desc=$desc") context.system.eventStream.publish(NetworkFeePaid(self, remoteNodeId, channelId, tx, fee, desc)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala index f5b45e77c6..3546f22af6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.channel import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{ByteVector32, Transaction} -import fr.acinq.eclair.{ShortChannelId, UInt64} +import fr.acinq.eclair.UInt64 import fr.acinq.eclair.payment.Origin import fr.acinq.eclair.wire.{ChannelUpdate, UpdateAddHtlc} @@ -27,6 +27,7 @@ import fr.acinq.eclair.wire.{ChannelUpdate, UpdateAddHtlc} */ class ChannelException(val channelId: ByteVector32, message: String) extends RuntimeException(message) + // @formatter:off case class DebugTriggeredException (override val channelId: ByteVector32) extends ChannelException(channelId, "debug-mode triggered failure") case class InvalidChainHash (override val channelId: ByteVector32, local: ByteVector32, remote: ByteVector32) extends ChannelException(channelId, s"invalid chainHash (local=$local remote=$remote)") @@ -49,6 +50,7 @@ case class InvalidFinalScript (override val channelId: ByteVect case class FundingTxTimedout (override val channelId: ByteVector32) extends ChannelException(channelId, "funding tx timed out") case class FundingTxSpent (override val channelId: ByteVector32, spendingTx: Transaction) extends ChannelException(channelId, s"funding tx has been spent by txid=${spendingTx.txid}") case class HtlcTimedout (override val channelId: ByteVector32, htlcs: Set[UpdateAddHtlc]) extends ChannelException(channelId, s"one or more htlcs timed out: ids=${htlcs.take(10).map(_.id).mkString}") // we only display the first 10 ids +case class HtlcWillTimeoutUpstream (override val channelId: ByteVector32, htlcs: Set[UpdateAddHtlc]) extends ChannelException(channelId, s"one or more htlcs that should be fulfilled are close to timing out upstream: ids=${htlcs.take(10).map(_.id).mkString}") // we only display the first 10 ids case class HtlcOverridenByLocalCommit (override val channelId: ByteVector32) extends ChannelException(channelId, "htlc was overriden by local commit") case class FeerateTooSmall (override val channelId: ByteVector32, remoteFeeratePerKw: Long) extends ChannelException(channelId, s"remote fee rate is too small: remoteFeeratePerKw=$remoteFeeratePerKw") case class FeerateTooDifferent (override val channelId: ByteVector32, localFeeratePerKw: Long, remoteFeeratePerKw: Long) extends ChannelException(channelId, s"local/remote feerates are too different: remoteFeeratePerKw=$remoteFeeratePerKw localFeeratePerKw=$localFeeratePerKw") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala index 4ce00a554e..48e6d6b57f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala @@ -21,10 +21,9 @@ import java.util.UUID import akka.actor.ActorRef import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, Transaction} -import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions.CommitTx -import fr.acinq.eclair.wire.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel, Shutdown, UpdateAddHtlc} +import fr.acinq.eclair.wire.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc} import fr.acinq.eclair.{ShortChannelId, UInt64} import scodec.bits.{BitVector, ByteVector} @@ -107,7 +106,7 @@ case class BITCOIN_PARENT_TX_CONFIRMED(childTx: Transaction) extends BitcoinEven */ sealed trait Command -final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: ByteVector32, cltvExpiry: Long, onion: ByteVector = Sphinx.LAST_PACKET.serialize, upstream: Either[UUID, UpdateAddHtlc], commit: Boolean = false, previousFailures: Seq[AddHtlcFailed] = Seq.empty) extends Command +final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: ByteVector32, cltvExpiry: Long, onion: OnionRoutingPacket, upstream: Either[UUID, UpdateAddHtlc], commit: Boolean = false, previousFailures: Seq[AddHtlcFailed] = Seq.empty) extends Command final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, commit: Boolean = false) extends Command final case class CMD_FAIL_HTLC(id: Long, reason: Either[ByteVector, FailureMessage], commit: Boolean = false) extends Command final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: ByteVector32, failureCode: Int, commit: Boolean = false) extends Command 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 579f8bac9f..5e10917ee3 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 @@ -26,8 +26,6 @@ import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire._ import fr.acinq.eclair.{Globals, UInt64} -import scala.util.{Failure, Success} - // @formatter:off case class LocalChanges(proposed: List[UpdateMessage], signed: List[UpdateMessage], acked: List[UpdateMessage]) { def all: List[UpdateMessage] = proposed ++ signed ++ acked @@ -62,11 +60,23 @@ case class Commitments(channelVersion: ChannelVersion, def hasNoPendingHtlcs: Boolean = localCommit.spec.htlcs.isEmpty && remoteCommit.spec.htlcs.isEmpty && remoteNextCommitInfo.isRight - def timedoutOutgoingHtlcs(blockheight: Long): Set[UpdateAddHtlc] = + def timedOutOutgoingHtlcs(blockheight: Long): Set[UpdateAddHtlc] = (localCommit.spec.htlcs.filter(htlc => htlc.direction == OUT && blockheight >= htlc.add.cltvExpiry) ++ remoteCommit.spec.htlcs.filter(htlc => htlc.direction == IN && blockheight >= htlc.add.cltvExpiry) ++ remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.htlcs.filter(htlc => htlc.direction == IN && blockheight >= htlc.add.cltvExpiry)).getOrElse(Set.empty[DirectedHtlc])).map(_.add) + /** + * HTLCs that are close to timing out upstream are potentially dangerous. If we received the pre-image for those + * HTLCs, we need to get a remote signed updated commitment that removes this HTLC. + * Otherwise when we get close to the upstream timeout, we risk an on-chain race condition between their HTLC timeout + * and our HTLC success in case of a force-close. + */ + def almostTimedOutIncomingHtlcs(blockheight: Long, fulfillSafety: Int): Set[UpdateAddHtlc] = { + localCommit.spec.htlcs.collect { + case htlc if htlc.direction == IN && blockheight >= htlc.add.cltvExpiry - fulfillSafety => htlc.add + } + } + def addLocalProposal(proposal: UpdateMessage): Commitments = Commitments.addLocalProposal(this, proposal) def addRemoteProposal(proposal: UpdateMessage): Commitments = Commitments.addRemoteProposal(this, proposal) @@ -87,12 +97,13 @@ case class Commitments(channelVersion: ChannelVersion, } object Commitments { + /** - * add a change to our proposed change list + * Add a change to our proposed change list. * - * @param commitments - * @param proposal - * @return an updated commitment instance + * @param commitments current commitments. + * @param proposal proposed change to add. + * @return an updated commitment instance. */ private def addLocalProposal(commitments: Commitments, proposal: UpdateMessage): Commitments = commitments.copy(localChanges = commitments.localChanges.copy(proposed = commitments.localChanges.proposed :+ proposal)) @@ -212,14 +223,14 @@ object Commitments { val fulfill = UpdateFulfillHtlc(commitments.channelId, cmd.id, cmd.r) val commitments1 = addLocalProposal(commitments, fulfill) (commitments1, fulfill) - case Some(htlc) => throw InvalidHtlcPreimage(commitments.channelId, cmd.id) + case Some(_) => throw InvalidHtlcPreimage(commitments.channelId, cmd.id) case None => throw UnknownHtlcId(commitments.channelId, cmd.id) } def receiveFulfill(commitments: Commitments, fulfill: UpdateFulfillHtlc): Either[Commitments, (Commitments, Origin, UpdateAddHtlc)] = getHtlcCrossSigned(commitments, OUT, fulfill.id) match { case Some(htlc) if htlc.paymentHash == sha256(fulfill.paymentPreimage) => Right((addRemoteProposal(commitments, fulfill), commitments.originChannels(fulfill.id), htlc)) - case Some(htlc) => throw InvalidHtlcPreimage(commitments.channelId, fulfill.id) + case Some(_) => throw InvalidHtlcPreimage(commitments.channelId, fulfill.id) case None => throw UnknownHtlcId(commitments.channelId, fulfill.id) } @@ -235,16 +246,16 @@ object Commitments { throw UnknownHtlcId(commitments.channelId, cmd.id) case Some(htlc) => // we need the shared secret to build the error packet - Sphinx.parsePacket(nodeSecret, htlc.paymentHash, htlc.onionRoutingPacket).map(_.sharedSecret) match { - case Success(sharedSecret) => + Sphinx.PaymentPacket.peel(nodeSecret, htlc.paymentHash, htlc.onionRoutingPacket) match { + case Right(Sphinx.DecryptedPacket(_, _, sharedSecret)) => val reason = cmd.reason match { - case Left(forwarded) => Sphinx.forwardErrorPacket(forwarded, sharedSecret) - case Right(failure) => Sphinx.createErrorPacket(sharedSecret, failure) + case Left(forwarded) => Sphinx.FailurePacket.wrap(forwarded, sharedSecret) + case Right(failure) => Sphinx.FailurePacket.create(sharedSecret, failure) } val fail = UpdateFailHtlc(commitments.channelId, cmd.id, reason) val commitments1 = addLocalProposal(commitments, fail) (commitments1, fail) - case Failure(_) => throw new CannotExtractSharedSecret(commitments.channelId, htlc) + case Left(_) => throw CannotExtractSharedSecret(commitments.channelId, htlc) } case None => throw UnknownHtlcId(commitments.channelId, cmd.id) } @@ -263,7 +274,7 @@ object Commitments { } => // we have already sent a fail/fulfill for this htlc throw UnknownHtlcId(commitments.channelId, cmd.id) - case Some(htlc) => + case Some(_) => val fail = UpdateFailMalformedHtlc(commitments.channelId, cmd.id, cmd.onionHash, cmd.failureCode) val commitments1 = addLocalProposal(commitments, fail) (commitments1, fail) @@ -348,9 +359,9 @@ object Commitments { def remoteHasUnsignedOutgoingHtlcs(commitments: Commitments): Boolean = commitments.remoteChanges.proposed.collectFirst { case u: UpdateAddHtlc => u }.isDefined - def localHasChanges(commitments: Commitments): Boolean = commitments.remoteChanges.acked.size > 0 || commitments.localChanges.proposed.size > 0 + def localHasChanges(commitments: Commitments): Boolean = commitments.remoteChanges.acked.nonEmpty || commitments.localChanges.proposed.nonEmpty - def remoteHasChanges(commitments: Commitments): Boolean = commitments.localChanges.acked.size > 0 || commitments.remoteChanges.proposed.size > 0 + def remoteHasChanges(commitments: Commitments): Boolean = commitments.localChanges.acked.nonEmpty || commitments.remoteChanges.proposed.nonEmpty def revocationPreimage(seed: ByteVector32, index: Long): ByteVector32 = ShaChain.shaChainFromSeed(seed, 0xFFFFFFFFFFFFFFFFL - index) @@ -428,7 +439,7 @@ object Commitments { val sortedHtlcTxs: Seq[TransactionWithInputInfo] = (htlcTimeoutTxs ++ htlcSuccessTxs).sortBy(_.input.outPoint.index) if (commit.htlcSignatures.size != sortedHtlcTxs.size) { - throw new HtlcSigCountMismatch(commitments.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size) + throw HtlcSigCountMismatch(commitments.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size) } val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(localParams.channelKeyPath), localPerCommitmentPoint)) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint) @@ -436,13 +447,13 @@ object Commitments { val htlcTxsAndSigs = (sortedHtlcTxs, htlcSigs, commit.htlcSignatures).zipped.toList.collect { case (htlcTx: HtlcTimeoutTx, localSig, remoteSig) => if (Transactions.checkSpendable(Transactions.addSigs(htlcTx, localSig, remoteSig)).isFailure) { - throw new InvalidHtlcSignature(commitments.channelId, htlcTx.tx) + throw InvalidHtlcSignature(commitments.channelId, htlcTx.tx) } HtlcTxAndSigs(htlcTx, localSig, remoteSig) case (htlcTx: HtlcSuccessTx, localSig, remoteSig) => // we can't check that htlc-success tx are spendable because we need the payment preimage; thus we only check the remote sig - if (Transactions.checkSig(htlcTx, remoteSig, remoteHtlcPubkey) == false) { - throw new InvalidHtlcSignature(commitments.channelId, htlcTx.tx) + if (!Transactions.checkSig(htlcTx, remoteSig, remoteHtlcPubkey)) { + throw InvalidHtlcSignature(commitments.channelId, htlcTx.tx) } HtlcTxAndSigs(htlcTx, localSig, remoteSig) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Mac.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Mac.scala new file mode 100644 index 0000000000..494cd10201 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Mac.scala @@ -0,0 +1,59 @@ +/* + * Copyright 2019 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.crypto + +import fr.acinq.bitcoin.ByteVector32 +import org.spongycastle.crypto.digests.SHA256Digest +import org.spongycastle.crypto.macs.HMac +import org.spongycastle.crypto.params.KeyParameter +import scodec.bits.ByteVector + +/** + * Created by t-bast on 04/07/19. + */ + +/** + * Create and verify message authentication codes. + */ +trait Mac32 { + + def mac(message: ByteVector): ByteVector32 + + def verify(mac: ByteVector32, message: ByteVector): Boolean + +} + +case class Hmac256(key: ByteVector) extends Mac32 { + + override def mac(message: ByteVector): ByteVector32 = Mac32.hmac256(key, message) + + override def verify(mac: ByteVector32, message: ByteVector): Boolean = this.mac(message) === mac + +} + +object Mac32 { + + def hmac256(key: ByteVector, message: ByteVector): ByteVector32 = { + val mac = new HMac(new SHA256Digest()) + mac.init(new KeyParameter(key.toArray)) + mac.update(message.toArray, 0, message.length.toInt) + val output = new Array[Byte](32) + mac.doFinal(output, 0) + ByteVector32(ByteVector.view(output)) + } + +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index 861746a8b1..7142924b7a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -16,17 +16,13 @@ package fr.acinq.eclair.crypto -import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream, OutputStream} -import java.nio.ByteOrder - import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, Crypto, Protocol} -import fr.acinq.eclair.wire.{FailureMessage, FailureMessageCodecs} +import fr.acinq.bitcoin.{ByteVector32, Crypto} +import fr.acinq.eclair.wire +import fr.acinq.eclair.wire.{FailureMessage, FailureMessageCodecs, OnionCodecs} import grizzled.slf4j.Logging -import org.spongycastle.crypto.digests.SHA256Digest -import org.spongycastle.crypto.macs.HMac -import org.spongycastle.crypto.params.KeyParameter -import scodec.bits.{BitVector, ByteVector} +import scodec.Attempt +import scodec.bits.ByteVector import scala.annotation.tailrec import scala.util.{Failure, Success, Try} @@ -36,35 +32,13 @@ import scala.util.{Failure, Success, Try} * see https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md */ object Sphinx extends Logging { - val Version = 0.toByte - // length of a MAC + // We use HMAC-SHA256 which returns 32-bytes message authentication codes. val MacLength = 32 - // length of a payload: 33 bytes (1 bytes for realm, 32 bytes for a realm-specific packet) - val PayloadLength = 33 - - // max number of hops - val MaxHops = 20 - - // onion packet length - val PacketLength = 1 + 33 + MacLength + MaxHops * (PayloadLength + MacLength) + def mac(key: ByteVector, message: ByteVector): ByteVector32 = Mac32.hmac256(key, message) - // last packet (all zeroes except for the version byte) - val LAST_PACKET = Packet(Version, ByteVector.fill(33)(0), ByteVector32.Zeroes, ByteVector.fill(MaxHops * (PayloadLength + MacLength))(0)) - - def hmac256(key: ByteVector, message: ByteVector): ByteVector32 = { - val mac = new HMac(new SHA256Digest()) - mac.init(new KeyParameter(key.toArray)) - mac.update(message.toArray, 0, message.length.toInt) - val output = new Array[Byte](32) - mac.doFinal(output, 0) - ByteVector32(ByteVector.view(output)) - } - - def mac(key: ByteVector, message: ByteVector): ByteVector32 = hmac256(key, message) - - def generateKey(keyType: ByteVector, secret: ByteVector32): ByteVector32 = hmac256(keyType, secret) + def generateKey(keyType: ByteVector, secret: ByteVector32): ByteVector32 = Mac32.hmac256(keyType, secret) def generateKey(keyType: String, secret: ByteVector32): ByteVector32 = generateKey(ByteVector.view(keyType.getBytes("UTF-8")), secret) @@ -74,298 +48,313 @@ object Sphinx extends Logging { def computeSharedSecret(pub: PublicKey, secret: PrivateKey): ByteVector32 = Crypto.sha256(pub.multiply(secret).value) - def computeblindingFactor(pub: PublicKey, secret: ByteVector): ByteVector32 = Crypto.sha256(pub.value ++ secret) + def computeBlindingFactor(pub: PublicKey, secret: ByteVector): ByteVector32 = Crypto.sha256(pub.value ++ secret) def blind(pub: PublicKey, blindingFactor: ByteVector32): PublicKey = pub.multiply(PrivateKey(blindingFactor)) def blind(pub: PublicKey, blindingFactors: Seq[ByteVector32]): PublicKey = blindingFactors.foldLeft(pub)(blind) /** - * computes the ephemeral public keys and shared secrets for all nodes on the route. + * Compute the ephemeral public keys and shared secrets for all nodes on the route. * - * @param sessionKey this node's session key - * @param publicKeys public keys of each node on the route - * @return a tuple (ephemeral public keys, shared secrets) + * @param sessionKey this node's session key. + * @param publicKeys public keys of each node on the route. + * @return a tuple (ephemeral public keys, shared secrets). */ def computeEphemeralPublicKeysAndSharedSecrets(sessionKey: PrivateKey, publicKeys: Seq[PublicKey]): (Seq[PublicKey], Seq[ByteVector32]) = { val ephemeralPublicKey0 = blind(PublicKey(Crypto.curve.getG), sessionKey.value) val secret0 = computeSharedSecret(publicKeys.head, sessionKey) - val blindingFactor0 = computeblindingFactor(ephemeralPublicKey0, secret0) + val blindingFactor0 = computeBlindingFactor(ephemeralPublicKey0, secret0) computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys.tail, Seq(ephemeralPublicKey0), Seq(blindingFactor0), Seq(secret0)) } @tailrec - def computeEphemeralPublicKeysAndSharedSecrets(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], ephemeralPublicKeys: Seq[PublicKey], blindingFactors: Seq[ByteVector32], sharedSecrets: Seq[ByteVector32]): (Seq[PublicKey], Seq[ByteVector32]) = { + private def computeEphemeralPublicKeysAndSharedSecrets(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], ephemeralPublicKeys: Seq[PublicKey], blindingFactors: Seq[ByteVector32], sharedSecrets: Seq[ByteVector32]): (Seq[PublicKey], Seq[ByteVector32]) = { if (publicKeys.isEmpty) (ephemeralPublicKeys, sharedSecrets) else { val ephemeralPublicKey = blind(ephemeralPublicKeys.last, blindingFactors.last) val secret = computeSharedSecret(blind(publicKeys.head, blindingFactors), sessionKey) - val blindingFactor = computeblindingFactor(ephemeralPublicKey, secret) + val blindingFactor = computeBlindingFactor(ephemeralPublicKey, secret) computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys.tail, ephemeralPublicKeys :+ ephemeralPublicKey, blindingFactors :+ blindingFactor, sharedSecrets :+ secret) } } - def generateFiller(keyType: String, sharedSecrets: Seq[ByteVector32], hopSize: Int, maxNumberOfHops: Int = MaxHops): ByteVector = { - sharedSecrets.foldLeft(ByteVector.empty)((padding, secret) => { - val key = generateKey(keyType, secret) - val padding1 = padding ++ ByteVector.fill(hopSize)(0) - val stream = generateStream(key, hopSize * (maxNumberOfHops + 1)).takeRight(padding1.length) - padding1.xor(stream) - }) - } - - case class Packet(version: Int, publicKey: ByteVector, hmac: ByteVector32, routingInfo: ByteVector) { - require(publicKey.length == 33, "onion packet public key length should be 33") - require(hmac.length == MacLength, s"onion packet hmac length should be $MacLength") - require(routingInfo.length == MaxHops * (PayloadLength + MacLength), s"onion packet routing info length should be ${MaxHops * (PayloadLength + MacLength)}") - - def isLastPacket: Boolean = hmac == ByteVector32.Zeroes - - def serialize: ByteVector = Packet.write(this) - } - - object Packet { - def read(in: InputStream): Packet = { - val version = in.read - val publicKey = new Array[Byte](33) - in.read(publicKey) - val routingInfo = new Array[Byte](MaxHops * (PayloadLength + MacLength)) - in.read(routingInfo) - val hmac = new Array[Byte](MacLength) - in.read(hmac) - Packet(version, ByteVector.view(publicKey), ByteVector32(ByteVector.view(hmac)), ByteVector.view(routingInfo)) - } - - def read(in: ByteVector): Packet = read(new ByteArrayInputStream(in.toArray)) - - def write(packet: Packet, out: OutputStream): OutputStream = { - out.write(packet.version) - out.write(packet.publicKey.toArray) - out.write(packet.routingInfo.toArray) - out.write(packet.hmac.toArray) - out - } - - def write(packet: Packet): ByteVector = { - val out = new ByteArrayOutputStream(PacketLength) - write(packet, out) - ByteVector.view(out.toByteArray) - } - - def isLastPacket(packet: ByteVector): Boolean = Packet.read(packet).hmac == ByteVector32.Zeroes - } - /** - * - * @param payload payload for this node - * @param nextPacket packet for the next node - * @param sharedSecret shared secret for the sending node, which we will need to return error messages + * Peek at the first bytes of the per-hop payload to extract its length. */ - case class ParsedPacket(payload: ByteVector, nextPacket: Packet, sharedSecret: ByteVector32) - - /** - * - * @param privateKey this node's private key - * @param associatedData associated data - * @param rawPacket packet received by this node - * @return a ParsedPacket(payload, packet, shared secret) object where: - * - payload is the per-hop payload for this node - * - packet is the next packet, to be forwarded using the info that is given in payload (channel id for now) - * - shared secret is the secret we share with the node that sent the packet. We need it to propagate failure - * messages upstream. - */ - def parsePacket(privateKey: PrivateKey, associatedData: ByteVector, rawPacket: ByteVector): Try[ParsedPacket] = Try { - require(rawPacket.length == PacketLength, s"onion packet length is ${rawPacket.length}, it should be ${PacketLength}") - val packet = Packet.read(rawPacket) - val sharedSecret = computeSharedSecret(PublicKey(packet.publicKey), privateKey) - val mu = generateKey("mu", sharedSecret) - val check = mac(mu, packet.routingInfo ++ associatedData) - require(check == packet.hmac, "invalid header mac") - - val rho = generateKey("rho", sharedSecret) - val bin = (packet.routingInfo ++ ByteVector.fill(PayloadLength + MacLength)(0)) xor generateStream(rho, PayloadLength + MacLength + MaxHops * (PayloadLength + MacLength)) - val payload = bin.take(PayloadLength) - val hmac = ByteVector32(bin.slice(PayloadLength, PayloadLength + MacLength)) - val nextRouteInfo = bin.drop(PayloadLength + MacLength) - - val nextPubKey = blind(PublicKey(packet.publicKey), computeblindingFactor(PublicKey(packet.publicKey), sharedSecret)) - - ParsedPacket(payload, Packet(Version, nextPubKey.value, hmac, nextRouteInfo), sharedSecret) - } - - @tailrec - private def extractSharedSecrets(packet: ByteVector, privateKey: PrivateKey, associatedData: ByteVector32, acc: Seq[ByteVector32] = Nil): Try[Seq[ByteVector32]] = { - parsePacket(privateKey, associatedData, packet) match { - case Success(ParsedPacket(_, nextPacket, sharedSecret)) if nextPacket.isLastPacket => Success(acc :+ sharedSecret) - case Success(ParsedPacket(_, nextPacket, sharedSecret)) => extractSharedSecrets(nextPacket.serialize, privateKey, associatedData, acc :+ sharedSecret) - case Failure(t) => Failure(t) + def peekPayloadLength(payload: ByteVector): Int = { + payload.head match { + case 0 => + // The 1.0 BOLT spec used 65-bytes frames inside the onion payload. + // The first byte of the frame (called `realm`) is set to 0x00, followed by 32 bytes of per-hop data, followed by a 32-bytes mac. + 65 + case _ => + // The 1.1 BOLT spec changed the frame format to use variable-length per-hop payloads. + // The first bytes contain a varint encoding the length of the payload data (not including the trailing mac). + // Since messages are always smaller than 65535 bytes, this varint will either be 1 or 3 bytes long. + MacLength + OnionCodecs.payloadLengthDecoder.decode(payload.bits).require.value.toInt } } /** - * Compute the next packet from the current packet and node parameters. - * Packets are constructed in reverse order: - * - you first build the last packet - * - then you call makeNextPacket(...) until you've build the final onion packet that will be sent to the first node - * in the route + * Decrypting an onion packet yields a payload for the current node and the encrypted packet for the next node. * - * @param payload payload for this packed - * @param associatedData associated data - * @param ephemeralPublicKey ephemeral key for this packed - * @param sharedSecret shared secret - * @param packet current packet (1 + all zeroes if this is the last packet) - * @param routingInfoFiller optional routing info filler, needed only when you're constructing the last packet - * @return the next packet + * @param payload decrypted payload for this node. + * @param nextPacket packet for the next node. + * @param sharedSecret shared secret for the sending node, which we will need to return failure messages. */ - private def makeNextPacket(payload: ByteVector, associatedData: ByteVector32, ephemeralPublicKey: ByteVector, sharedSecret: ByteVector32, packet: Packet, routingInfoFiller: ByteVector = ByteVector.empty): Packet = { - require(payload.length == PayloadLength) + case class DecryptedPacket(payload: ByteVector, nextPacket: wire.OnionRoutingPacket, sharedSecret: ByteVector32) { - val nextRoutingInfo = { - val routingInfo1 = payload ++ packet.hmac ++ packet.routingInfo.dropRight(PayloadLength + MacLength) - val routingInfo2 = routingInfo1 xor generateStream(generateKey("rho", sharedSecret), MaxHops * (PayloadLength + MacLength)) - routingInfo2.dropRight(routingInfoFiller.length) ++ routingInfoFiller - } + val isLastPacket: Boolean = nextPacket.hmac == ByteVector32.Zeroes - val nextHmac = mac(generateKey("mu", sharedSecret), nextRoutingInfo ++ associatedData) - val nextPacket = Packet(Version, ephemeralPublicKey, nextHmac, nextRoutingInfo) - nextPacket } - /** + * A encrypted onion packet with all the associated shared secrets. * - * @param packet onion packet + * @param packet encrypted onion packet. * @param sharedSecrets shared secrets (one per node in the route). Known (and needed) only if you're creating the - * packet. Empty if you're just forwarding the packet to the next node + * packet. Empty if you're just forwarding the packet to the next node. */ - case class PacketAndSecrets(packet: Packet, sharedSecrets: Seq[(ByteVector32, PublicKey)]) - - /** - * A properly decoded error from a node in the route - * - * @param originNode - * @param failureMessage - */ - case class ErrorPacket(originNode: PublicKey, failureMessage: FailureMessage) + case class PacketAndSecrets(packet: wire.OnionRoutingPacket, sharedSecrets: Seq[(ByteVector32, PublicKey)]) + + sealed trait OnionRoutingPacket { + + /** + * Supported packet version. Note that since this value is outside of the onion encrypted payload, intermediate + * nodes may or may not use this value when forwarding the packet to the next node. + */ + def Version = 0 + + /** + * Length of the encrypted onion payload. + */ + def PayloadLength: Int + + /** + * Generate a deterministic filler to prevent intermediate nodes from knowing their position in the route. + * See https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#filler-generation + * + * @param keyType type of key used (depends on the onion we're building). + * @param sharedSecrets shared secrets for all the hops. + * @param payloads payloads for all the hops. + * @return filler bytes. + */ + def generateFiller(keyType: String, sharedSecrets: Seq[ByteVector32], payloads: Seq[ByteVector]): ByteVector = { + require(sharedSecrets.length == payloads.length, "the number of secrets should equal the number of payloads") + + (sharedSecrets zip payloads).foldLeft(ByteVector.empty)((padding, secretAndPayload) => { + val (secret, perHopPayload) = secretAndPayload + val perHopPayloadLength = peekPayloadLength(perHopPayload) + require(perHopPayloadLength == perHopPayload.length + MacLength, s"invalid payload: length isn't correctly encoded: $perHopPayload") + val key = generateKey(keyType, secret) + val padding1 = padding ++ ByteVector.fill(perHopPayloadLength)(0) + val stream = generateStream(key, PayloadLength + perHopPayloadLength).takeRight(padding1.length) + padding1.xor(stream) + }) + } - /** - * Builds an encrypted onion packet that contains payloads and routing information for all nodes in the list - * - * @param sessionKey session key - * @param publicKeys node public keys (one per node) - * @param payloads payloads (one per node) - * @param associatedData associated data - * @return an OnionPacket(onion packet, shared secrets). the onion packet can be sent to the first node in the list, and the - * shared secrets (one per node) can be used to parse returned error messages if needed - */ - def makePacket(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], payloads: Seq[ByteVector], associatedData: ByteVector32): PacketAndSecrets = { - val (ephemeralPublicKeys, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) - val filler = generateFiller("rho", sharedsecrets.dropRight(1), PayloadLength + MacLength, MaxHops) + /** + * Decrypt the incoming packet, extract the per-hop payload and build the packet for the next node. + * + * @param privateKey this node's private key. + * @param associatedData associated data. + * @param packet packet received by this node. + * @return a DecryptedPacket(payload, packet, shared secret) object where: + * - payload is the per-hop payload for this node. + * - packet is the next packet, to be forwarded using the info that is given in the payload. + * - shared secret is the secret we share with the node that sent the packet. We need it to propagate + * failure messages upstream. + * or a BadOnion error containing the hash of the invalid onion. + */ + def peel(privateKey: PrivateKey, associatedData: ByteVector, packet: wire.OnionRoutingPacket): Either[wire.BadOnion, DecryptedPacket] = packet.version match { + case 0 => Try(PublicKey(packet.publicKey, checkValid = true)) match { + case Success(packetEphKey) => + val sharedSecret = computeSharedSecret(packetEphKey, privateKey) + val mu = generateKey("mu", sharedSecret) + val check = mac(mu, packet.payload ++ associatedData) + if (check == packet.hmac) { + val rho = generateKey("rho", sharedSecret) + // Since we don't know the length of the per-hop payload (we will learn it once we decode the first bytes), + // we have to pessimistically generate a long cipher stream. + val stream = generateStream(rho, 2 * PayloadLength) + val bin = (packet.payload ++ ByteVector.fill(PayloadLength)(0)) xor stream + + val perHopPayloadLength = peekPayloadLength(bin) + val perHopPayload = bin.take(perHopPayloadLength - MacLength) + + val hmac = ByteVector32(bin.slice(perHopPayloadLength - MacLength, perHopPayloadLength)) + val nextOnionPayload = bin.drop(perHopPayloadLength).take(PayloadLength) + val nextPubKey = blind(packetEphKey, computeBlindingFactor(packetEphKey, sharedSecret)) + + Right(DecryptedPacket(perHopPayload, wire.OnionRoutingPacket(Version, nextPubKey.value, nextOnionPayload, hmac), sharedSecret)) + } else { + Left(wire.InvalidOnionHmac(hash(packet))) + } + case Failure(_) => Left(wire.InvalidOnionKey(hash(packet))) + } + case _ => Left(wire.InvalidOnionVersion(hash(packet))) + } - val lastPacket = makeNextPacket(payloads.last, associatedData, ephemeralPublicKeys.last.value, sharedsecrets.last, LAST_PACKET, filler) + /** + * Wrap the given packet in an additional layer of onion encryption, adding an encrypted payload for a specific + * node. + * + * Packets are constructed in reverse order: + * - you first create the packet for the final recipient + * - then you call wrap(...) until you've built the final onion packet that will be sent to the first node in the + * route + * + * @param payload per-hop payload for the target node. + * @param associatedData associated data. + * @param ephemeralPublicKey ephemeral key shared with the target node. + * @param sharedSecret shared secret with this hop. + * @param packet current packet (None if the packet hasn't been initialized). + * @param onionPayloadFiller optional onion payload filler, needed only when you're constructing the last packet. + * @return the next packet. + */ + def wrap(payload: ByteVector, associatedData: ByteVector32, ephemeralPublicKey: PublicKey, sharedSecret: ByteVector32, packet: Option[wire.OnionRoutingPacket], onionPayloadFiller: ByteVector = ByteVector.empty): wire.OnionRoutingPacket = { + require(payload.length <= PayloadLength - MacLength, s"packet payload cannot exceed ${PayloadLength - MacLength} bytes") + + val (currentMac, currentPayload): (ByteVector32, ByteVector) = packet match { + // Packet construction starts with an empty mac and payload. + case None => (ByteVector32.Zeroes, ByteVector.fill(PayloadLength)(0)) + case Some(p) => (p.hmac, p.payload) + } - @tailrec - def loop(hoppayloads: Seq[ByteVector], ephkeys: Seq[PublicKey], sharedSecrets: Seq[ByteVector32], packet: Packet): Packet = { - if (hoppayloads.isEmpty) packet else { - val nextPacket = makeNextPacket(hoppayloads.last, associatedData, ephkeys.last.value, sharedSecrets.last, packet) - loop(hoppayloads.dropRight(1), ephkeys.dropRight(1), sharedSecrets.dropRight(1), nextPacket) + val nextOnionPayload = { + val onionPayload1 = payload ++ currentMac ++ currentPayload.dropRight(payload.length + MacLength) + val onionPayload2 = onionPayload1 xor generateStream(generateKey("rho", sharedSecret), PayloadLength) + onionPayload2.dropRight(onionPayloadFiller.length) ++ onionPayloadFiller } + + val nextHmac = mac(generateKey("mu", sharedSecret), nextOnionPayload ++ associatedData) + val nextPacket = wire.OnionRoutingPacket(Version, ephemeralPublicKey.value, nextOnionPayload, nextHmac) + nextPacket } - val packet = loop(payloads.dropRight(1), ephemeralPublicKeys.dropRight(1), sharedsecrets.dropRight(1), lastPacket) - PacketAndSecrets(packet, sharedsecrets.zip(publicKeys)) - } + /** + * Create an encrypted onion packet that contains payloads for all nodes in the list. + * + * @param sessionKey session key. + * @param publicKeys node public keys (one per node). + * @param payloads payloads (one per node). + * @param associatedData associated data. + * @return An onion packet with all shared secrets. The onion packet can be sent to the first node in the list, and + * the shared secrets (one per node) can be used to parse returned failure messages if needed. + */ + def create(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], payloads: Seq[ByteVector], associatedData: ByteVector32): PacketAndSecrets = { + val (ephemeralPublicKeys, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) + val filler = generateFiller("rho", sharedsecrets.dropRight(1), payloads.dropRight(1)) + + val lastPacket = wrap(payloads.last, associatedData, ephemeralPublicKeys.last, sharedsecrets.last, None, filler) + + @tailrec + def loop(hopPayloads: Seq[ByteVector], ephKeys: Seq[PublicKey], sharedSecrets: Seq[ByteVector32], packet: wire.OnionRoutingPacket): wire.OnionRoutingPacket = { + if (hopPayloads.isEmpty) packet else { + val nextPacket = wrap(hopPayloads.last, associatedData, ephKeys.last, sharedSecrets.last, Some(packet)) + loop(hopPayloads.dropRight(1), ephKeys.dropRight(1), sharedSecrets.dropRight(1), nextPacket) + } + } - /* - error packet format: - +----------------+----------------------------------+-----------------+----------------------+-----+ - | HMAC(32 bytes) | failure message length (2 bytes) | failure message | pad length (2 bytes) | pad | - +----------------+----------------------------------+-----------------+----------------------+-----+ - with failure message length + pad length = 256 - */ - val MaxErrorPayloadLength = 256 - val ErrorPacketLength = MacLength + MaxErrorPayloadLength + 2 + 2 + val packet = loop(payloads.dropRight(1), ephemeralPublicKeys.dropRight(1), sharedsecrets.dropRight(1), lastPacket) + PacketAndSecrets(packet, sharedsecrets.zip(publicKeys)) + } - /** - * - * @param sharedSecret destination node's shared secret that was computed when the original onion for the HTLC - * was created or forwarded: see makePacket() and makeNextPacket() - * @param failure failure message - * @return an error packet that can be sent to the destination node - */ - def createErrorPacket(sharedSecret: ByteVector32, failure: FailureMessage): ByteVector = { - val message: ByteVector = FailureMessageCodecs.failureMessageCodec.encode(failure).require.toByteVector - require(message.length <= MaxErrorPayloadLength, s"error message length is ${message.length}, it must be less than $MaxErrorPayloadLength") - val um = Sphinx.generateKey("um", sharedSecret) - val padlen = MaxErrorPayloadLength - message.length - val payload = Protocol.writeUInt16(message.length.toInt, ByteOrder.BIG_ENDIAN) ++ message ++ Protocol.writeUInt16(padlen.toInt, ByteOrder.BIG_ENDIAN) ++ ByteVector.fill(padlen.toInt)(0) - logger.debug(s"um key: $um") - logger.debug(s"error payload: ${payload.toHex}") - logger.debug(s"raw error packet: ${(Sphinx.mac(um, payload) ++ payload).toHex}") - forwardErrorPacket(Sphinx.mac(um, payload) ++ payload, sharedSecret) - } + /** + * When an invalid onion is received, its hash should be included in the failure message. + */ + def hash(onion: wire.OnionRoutingPacket): ByteVector32 = + Crypto.sha256(wire.OnionCodecs.onionRoutingPacketCodec(onion.payload.length.toInt).encode(onion).require.toByteVector) - /** - * - * @param packet error packet - * @return the failure message that is embedded in the error packet - */ - private def extractFailureMessage(packet: ByteVector): FailureMessage = { - require(packet.length == ErrorPacketLength, s"invalid error packet length ${packet.length}, must be $ErrorPacketLength") - val (mac, payload) = packet.splitAt(Sphinx.MacLength) - val len = Protocol.uint16(payload.toArray, ByteOrder.BIG_ENDIAN) - require((len >= 0) && (len <= MaxErrorPayloadLength), s"message length must be less than $MaxErrorPayloadLength") - FailureMessageCodecs.failureMessageCodec.decode(BitVector(payload.drop(2).take(len))).require.value } /** - * - * @param packet error packet - * @param sharedSecret destination node's shared secret - * @return an obfuscated error packet that can be sent to the destination node + * A payment onion packet is used when offering an HTLC to a remote node. */ - def forwardErrorPacket(packet: ByteVector, sharedSecret: ByteVector32): ByteVector = { - require(packet.length == ErrorPacketLength, s"invalid error packet length ${packet.length}, must be $ErrorPacketLength") - val key = generateKey("ammag", sharedSecret) - val stream = generateStream(key, ErrorPacketLength) - logger.debug(s"ammag key: $key") - logger.debug(s"error stream: $stream") - packet xor stream - } + object PaymentPacket extends OnionRoutingPacket { + + override val PayloadLength = 1300 - /** - * - * @param sharedSecret this node's share secret - * @param packet error packet - * @return true if the packet's mac is valid, which means that it has been properly de-obfuscated - */ - private def checkMac(sharedSecret: ByteVector32, packet: ByteVector): Boolean = { - val (mac, payload) = packet.splitAt(Sphinx.MacLength) - val um = Sphinx.generateKey("um", sharedSecret) - ByteVector32(mac) == Sphinx.mac(um, payload) } /** - * Parse and de-obfuscate an error packet. Node shared secrets are applied until the packet's MAC becomes valid, - * which means that it was sent by the corresponding node. + * A properly decrypted failure from a node in the route. * - * @param packet error packet - * @param sharedSecrets nodes shared secrets - * @return Success(secret, failure message) if the origin of the packet could be identified and the packet de-obfuscated, Failure otherwise + * @param originNode public key of the node that generated the failure. + * @param failureMessage friendly failure message. */ - def parseErrorPacket(packet: ByteVector, sharedSecrets: Seq[(ByteVector32, PublicKey)]): Try[ErrorPacket] = Try { - require(packet.length == ErrorPacketLength, s"invalid error packet length ${packet.length}, must be $ErrorPacketLength") - - @tailrec - def loop(packet: ByteVector, sharedSecrets: Seq[(ByteVector32, PublicKey)]): ErrorPacket = sharedSecrets match { - case Nil => throw new RuntimeException(s"couldn't parse error packet=$packet with sharedSecrets=$sharedSecrets") - case (secret, pubkey) :: tail => - val packet1 = forwardErrorPacket(packet, secret) - if (checkMac(secret, packet1)) ErrorPacket(pubkey, extractFailureMessage(packet1)) else loop(packet1, tail) + case class DecryptedFailurePacket(originNode: PublicKey, failureMessage: FailureMessage) + + object FailurePacket { + + val MaxPayloadLength = 256 + val PacketLength = MacLength + MaxPayloadLength + 2 + 2 + + /** + * Create a failure packet that will be returned to the sender. + * Each intermediate hop will add a layer of encryption and forward to the previous hop. + * Note that malicious intermediate hops may drop the packet or alter it (which breaks the mac). + * + * @param sharedSecret destination node's shared secret that was computed when the original onion for the HTLC + * was created or forwarded: see OnionPacket.create() and OnionPacket.wrap(). + * @param failure failure message. + * @return a failure packet that can be sent to the destination node. + */ + def create(sharedSecret: ByteVector32, failure: FailureMessage): ByteVector = { + val um = generateKey("um", sharedSecret) + val packet = FailureMessageCodecs.failureOnionCodec(Hmac256(um)).encode(failure).require.toByteVector + logger.debug(s"um key: $um") + logger.debug(s"raw error packet: ${packet.toHex}") + wrap(packet, sharedSecret) + } + + /** + * Wrap the given packet in an additional layer of onion encryption for the previous hop. + * + * @param packet failure packet. + * @param sharedSecret destination node's shared secret. + * @return an encrypted failure packet that can be sent to the destination node. + */ + def wrap(packet: ByteVector, sharedSecret: ByteVector32): ByteVector = { + require(packet.length == PacketLength, s"invalid error packet length ${packet.length}, must be $PacketLength") + val key = generateKey("ammag", sharedSecret) + val stream = generateStream(key, PacketLength) + logger.debug(s"ammag key: $key") + logger.debug(s"error stream: $stream") + packet xor stream + } + + /** + * Decrypt a failure packet. Node shared secrets are applied until the packet's MAC becomes valid, which means that + * it was sent by the corresponding node. + * Note that malicious nodes in the route may have altered the packet, triggering a decryption failure. + * + * @param packet failure packet. + * @param sharedSecrets nodes shared secrets. + * @return Success(secret, failure message) if the origin of the packet could be identified and the packet + * decrypted, Failure otherwise. + */ + def decrypt(packet: ByteVector, sharedSecrets: Seq[(ByteVector32, PublicKey)]): Try[DecryptedFailurePacket] = Try { + require(packet.length == PacketLength, s"invalid error packet length ${packet.length}, must be $PacketLength") + + @tailrec + def loop(packet: ByteVector, sharedSecrets: Seq[(ByteVector32, PublicKey)]): DecryptedFailurePacket = sharedSecrets match { + case Nil => throw new RuntimeException(s"couldn't parse error packet=$packet with sharedSecrets=$sharedSecrets") + case (secret, pubkey) :: tail => + val packet1 = wrap(packet, secret) + val um = generateKey("um", secret) + FailureMessageCodecs.failureOnionCodec(Hmac256(um)).decode(packet1.toBitVector) match { + case Attempt.Successful(value) => DecryptedFailurePacket(pubkey, value.value) + case _ => loop(packet1, tail) + } + } + + loop(packet, sharedSecrets) } - loop(packet, sharedSecrets) } + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala index 5ba608af54..7d29142d2e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala @@ -174,8 +174,8 @@ object Switchboard extends Logging { .flatMap(_.commitments.remoteCommit.spec.htlcs) .filter(_.direction == OUT) .map(_.add) - .map(Relayer.tryParsePacket(_, privateKey)) - .collect { case Success(RelayPayload(add, _, _)) => add } // we only consider htlcs that are relayed, not the ones for which we are the final node + .map(Relayer.decryptPacket(_, privateKey)) + .collect { case Right(RelayPayload(add, _, _)) => add } // we only consider htlcs that are relayed, not the ones for which we are the final node // Here we do it differently because we need the origin information. val relayed_out = channels diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala index 918d02f065..94e33deb94 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.payment import akka.actor.{Actor, ActorLogging, ActorRef, Props} import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.crypto.Sphinx.ErrorPacket +import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket import fr.acinq.eclair.payment.PaymentLifecycle.{PaymentFailed, PaymentResult, RemoteFailure, SendPayment} import fr.acinq.eclair.router.{Announcements, Data} import fr.acinq.eclair.wire.{IncorrectOrUnknownPaymentDetails} @@ -62,7 +62,7 @@ class Autoprobe(nodeParams: NodeParams, router: ActorRef, paymentInitiator: Acto case paymentResult: PaymentResult => paymentResult match { - case PaymentFailed(_, _, _ :+ RemoteFailure(_, ErrorPacket(targetNodeId, IncorrectOrUnknownPaymentDetails(_)))) => + case PaymentFailed(_, _, _ :+ RemoteFailure(_, DecryptedFailurePacket(targetNodeId, IncorrectOrUnknownPaymentDetails(_)))) => log.info(s"payment probe successful to node=$targetNodeId") case _ => log.info(s"payment probe failed with paymentResult=$paymentResult") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index cf878ed627..907a68a25c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -20,10 +20,9 @@ import java.util.UUID import akka.actor.{ActorRef, FSM, Props, Status} import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, MilliSatoshi} +import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} import fr.acinq.eclair._ import fr.acinq.eclair.channel.{AddHtlcFailed, CMD_ADD_HTLC, Channel, Register} -import fr.acinq.eclair.crypto.Sphinx.{ErrorPacket, Packet} import fr.acinq.eclair.crypto.{Sphinx, TransportHandler} import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus} import fr.acinq.eclair.payment.PaymentLifecycle._ @@ -86,8 +85,8 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis stop(FSM.Normal) case Event(fail: UpdateFailHtlc, WaitingForComplete(s, c, _, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops)) => - Sphinx.parseErrorPacket(fail.reason, sharedSecrets) match { - case Success(e@ErrorPacket(nodeId, failureMessage)) if nodeId == c.targetNodeId => + Sphinx.FailurePacket.decrypt(fail.reason, sharedSecrets) match { + case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) if nodeId == c.targetNodeId => // if destination node returns an error, we fail the payment immediately log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)") reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ RemoteFailure(hops, e))) @@ -96,7 +95,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis case res if failures.size + 1 >= c.maxAttempts => // otherwise we never try more than maxAttempts, no matter the kind of error returned val failure = res match { - case Success(e@ErrorPacket(nodeId, failureMessage)) => + case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => log.info(s"received an error message from nodeId=$nodeId (failure=$failureMessage)") RemoteFailure(hops, e) case Failure(t) => @@ -114,12 +113,12 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis log.warning(s"blacklisting intermediate nodes=${blacklist.mkString(",")}") router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes ++ blacklist, ignoreChannels, c.routeParams) goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ UnreadableRemoteFailure(hops)) - case Success(e@ErrorPacket(nodeId, failureMessage: Node)) => + case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Node)) => log.info(s"received 'Node' type error message from nodeId=$nodeId, trying to route around it (failure=$failureMessage)") // let's try to route around this node router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams) goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e)) - case Success(e@ErrorPacket(nodeId, failureMessage: Update)) => + case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) => log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)") if (Announcements.checkSig(failureMessage.update, nodeId)) { getChannelUpdateForNode(nodeId, hops) match { @@ -152,7 +151,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams) } goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e)) - case Success(e@ErrorPacket(nodeId, failureMessage)) => + case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => log.info(s"received an error message from nodeId=$nodeId, trying to use a different channel (failure=$failureMessage)") // let's try again without the channel outgoing from nodeId val faultyChannel = hops.find(_.nodeId == nodeId).map(hop => ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId)) @@ -216,7 +215,7 @@ object PaymentLifecycle { case class PaymentSucceeded(id: UUID, amountMsat: Long, paymentHash: ByteVector32, paymentPreimage: ByteVector32, route: Seq[Hop]) extends PaymentResult // note: the amount includes fees sealed trait PaymentFailure case class LocalFailure(t: Throwable) extends PaymentFailure - case class RemoteFailure(route: Seq[Hop], e: ErrorPacket) extends PaymentFailure + case class RemoteFailure(route: Seq[Hop], e: Sphinx.DecryptedFailurePacket) extends PaymentFailure case class UnreadableRemoteFailure(route: Seq[Hop]) extends PaymentFailure case class PaymentFailed(id: UUID, paymentHash: ByteVector32, failures: Seq[PaymentFailure]) extends PaymentResult @@ -237,12 +236,12 @@ object PaymentLifecycle { require(nodes.size == payloads.size) val sessionKey = randomKey val payloadsbin: Seq[ByteVector] = payloads - .map(LightningMessageCodecs.perHopPayloadCodec.encode) + .map(OnionCodecs.perHopPayloadCodec.encode) .map { case Attempt.Successful(bitVector) => bitVector.toByteVector case Attempt.Failure(cause) => throw new RuntimeException(s"serialization error: $cause") } - Sphinx.makePacket(sessionKey, nodes, payloadsbin, associatedData) + Sphinx.PaymentPacket.create(sessionKey, nodes, payloadsbin, associatedData) } /** @@ -267,7 +266,7 @@ object PaymentLifecycle { val nodes = hops.map(_.nextNodeId) // BOLT 2 requires that associatedData == paymentHash val onion = buildOnion(nodes, payloads, paymentHash) - CMD_ADD_HTLC(firstAmountMsat, paymentHash, firstExpiry, Packet.write(onion.packet), upstream = Left(id), commit = true) -> onion.sharedSecrets + CMD_ADD_HTLC(firstAmountMsat, paymentHash, firstExpiry, onion.packet, upstream = Left(id), commit = true) -> onion.sharedSecrets } /** @@ -312,6 +311,6 @@ object PaymentLifecycle { */ def hasAlreadyFailedOnce(nodeId: PublicKey, failures: Seq[PaymentFailure]): Boolean = failures - .collectFirst { case RemoteFailure(_, ErrorPacket(origin, u: Update)) if origin == nodeId => u.update } + .collectFirst { case RemoteFailure(_, Sphinx.DecryptedFailurePacket(origin, u: Update)) if origin == nodeId => u.update } .isDefined } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala index 1a75ee2083..d5d6b9f17e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala @@ -21,7 +21,7 @@ import java.util.UUID import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status} import akka.event.LoggingAdapter import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, Crypto, MilliSatoshi} +import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.db.OutgoingPaymentStatus @@ -29,11 +29,10 @@ import fr.acinq.eclair.payment.PaymentLifecycle.{PaymentFailed, PaymentSucceeded import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire._ import fr.acinq.eclair.{NodeParams, ShortChannelId, nodeFee} -import scodec.bits.BitVector +import grizzled.slf4j.Logging import scodec.{Attempt, DecodeResult} import scala.collection.mutable -import scala.util.{Failure, Success, Try} // @formatter:off @@ -74,13 +73,13 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR def main(channelUpdates: Map[ShortChannelId, OutgoingChannel], node2channels: mutable.HashMap[PublicKey, mutable.Set[ShortChannelId]] with mutable.MultiMap[PublicKey, ShortChannelId]): Receive = { case GetUsableBalances => sender ! channelUpdates.values - .filter(o => Announcements.isEnabled(o.channelUpdate.channelFlags)) - .map(o => UsableBalances( - remoteNodeId = o.nextNodeId, - shortChannelId = o.channelUpdate.shortChannelId, - canSendMsat = o.commitments.availableBalanceForSendMsat, - canReceiveMsat = o.commitments.availableBalanceForReceiveMsat, - isPublic = o.commitments.announceChannel)) + .filter(o => Announcements.isEnabled(o.channelUpdate.channelFlags)) + .map(o => UsableBalances( + remoteNodeId = o.nextNodeId, + shortChannelId = o.channelUpdate.shortChannelId, + canSendMsat = o.commitments.availableBalanceForSendMsat, + canReceiveMsat = o.commitments.availableBalanceForReceiveMsat, + isPublic = o.commitments.announceChannel)) case LocalChannelUpdate(_, channelId, shortChannelId, remoteNodeId, _, channelUpdate, commitments) => log.debug(s"updating local channel info for channelId=$channelId shortChannelId=$shortChannelId remoteNodeId=$remoteNodeId channelUpdate={} commitments={}", channelUpdate, commitments) @@ -100,8 +99,8 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR case ForwardAdd(add, previousFailures) => log.debug(s"received forwarding request for htlc #${add.id} paymentHash=${add.paymentHash} from channelId=${add.channelId}") - tryParsePacket(add, nodeParams.privateKey) match { - case Success(p: FinalPayload) => + decryptPacket(add, nodeParams.privateKey) match { + case Right(p: FinalPayload) => handleFinal(p) match { case Left(cmdFail) => log.info(s"rejecting htlc #${add.id} paymentHash=${add.paymentHash} from channelId=${add.channelId} reason=${cmdFail.reason}") @@ -110,7 +109,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR log.debug(s"forwarding htlc #${add.id} paymentHash=${add.paymentHash} to payment-handler") paymentHandler forward addHtlc } - case Success(r: RelayPayload) => + case Right(r: RelayPayload) => handleRelay(r, channelUpdates, node2channels, previousFailures, nodeParams.chainHash) match { case RelayFailure(cmdFail) => log.info(s"rejecting htlc #${add.id} paymentHash=${add.paymentHash} from channelId=${add.channelId} to shortChannelId=${r.payload.shortChannelId} reason=${cmdFail.reason}") @@ -119,9 +118,9 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR log.info(s"forwarding htlc #${add.id} paymentHash=${add.paymentHash} from channelId=${add.channelId} to shortChannelId=$selectedShortChannelId") register ! Register.ForwardShortId(selectedShortChannelId, cmdAdd) } - case Failure(t) => - log.warning(s"couldn't parse onion: reason=${t.getMessage}") - val cmdFail = CMD_FAIL_MALFORMED_HTLC(add.id, Crypto.sha256(add.onionRoutingPacket), failureCode = FailureMessageCodecs.BADONION, commit = true) + case Left(badOnion) => + log.warning(s"couldn't parse onion: reason=${badOnion.message}") + val cmdFail = CMD_FAIL_MALFORMED_HTLC(add.id, badOnion.onionHash, FailureMessageCodecs.failureCode(badOnion), commit = true) log.info(s"rejecting htlc #${add.id} paymentHash=${add.paymentHash} from channelId=${add.channelId} reason=malformed onionHash=${cmdFail.onionHash} failureCode=${cmdFail.failureCode}") commandBuffer ! CommandBuffer.CommandSend(add.channelId, add.id, cmdFail) } @@ -207,7 +206,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR } -object Relayer { +object Relayer extends Logging { def props(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorRef) = Props(classOf[Relayer], nodeParams, register, paymentHandler) case class OutgoingChannel(nextNodeId: PublicKey, channelUpdate: ChannelUpdate, commitments: Commitments) @@ -215,34 +214,39 @@ object Relayer { // @formatter:off sealed trait NextPayload case class FinalPayload(add: UpdateAddHtlc, payload: PerHopPayload) extends NextPayload - case class RelayPayload(add: UpdateAddHtlc, payload: PerHopPayload, nextPacket: Sphinx.Packet) extends NextPayload { + case class RelayPayload(add: UpdateAddHtlc, payload: PerHopPayload, nextPacket: OnionRoutingPacket) extends NextPayload { val relayFeeMsat: Long = add.amountMsat - payload.amtToForward val expiryDelta: Long = add.cltvExpiry - payload.outgoingCltvValue } // @formatter:on /** - * Parse and decode the onion of a received htlc, and find out if the payment is to be relayed, + * Decrypt the onion of a received htlc, and find out if the payment is to be relayed, * or if our node is the last one in the route * * @param add incoming htlc * @param privateKey this node's private key - * @return the payload for the next hop + * @return the payload for the next hop or an error. */ - def tryParsePacket(add: UpdateAddHtlc, privateKey: PrivateKey): Try[NextPayload] = - Sphinx - .parsePacket(privateKey, add.paymentHash, add.onionRoutingPacket) - .flatMap { - case Sphinx.ParsedPacket(payload, nextPacket, _) => - LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(payload)) match { - case Attempt.Successful(DecodeResult(perHopPayload, _)) if nextPacket.isLastPacket => - Success(FinalPayload(add, perHopPayload)) - case Attempt.Successful(DecodeResult(perHopPayload, _)) => - Success(RelayPayload(add, perHopPayload, nextPacket)) - case Attempt.Failure(cause) => - Failure(new RuntimeException(cause.messageWithContext)) - } - } + def decryptPacket(add: UpdateAddHtlc, privateKey: PrivateKey): Either[BadOnion, NextPayload] = + Sphinx.PaymentPacket.peel(privateKey, add.paymentHash, add.onionRoutingPacket) match { + case Right(p@Sphinx.DecryptedPacket(payload, nextPacket, _)) => + OnionCodecs.perHopPayloadCodec.decode(payload.bits) match { + case Attempt.Successful(DecodeResult(perHopPayload, remainder)) => + if (remainder.nonEmpty) { + logger.warn(s"${remainder.length} bits remaining after per-hop payload decoding: there might be an issue with the onion codec") + } + if (p.isLastPacket) { + Right(FinalPayload(add, perHopPayload)) + } else { + Right(RelayPayload(add, perHopPayload, nextPacket)) + } + case Attempt.Failure(_) => + // Onion is correctly encrypted but the content of the per-hop payload couldn't be parsed. + Left(InvalidOnionPayload(Sphinx.PaymentPacket.hash(add.onionRoutingPacket))) + } + case Left(badOnion) => Left(badOnion) + } /** * Handle an incoming htlc when we are the last node @@ -370,7 +374,7 @@ object Relayer { case Some(channelUpdate) if relayPayload.relayFeeMsat < nodeFee(channelUpdate.feeBaseMsat, channelUpdate.feeProportionalMillionths, payload.amtToForward) => RelayFailure(CMD_FAIL_HTLC(add.id, Right(FeeInsufficient(add.amountMsat, channelUpdate)), commit = true)) case Some(channelUpdate) => - RelaySuccess(channelUpdate.shortChannelId, CMD_ADD_HTLC(payload.amtToForward, add.paymentHash, payload.outgoingCltvValue, nextPacket.serialize, upstream = Right(add), commit = true, previousFailures = previousFailures)) + RelaySuccess(channelUpdate.shortChannelId, CMD_ADD_HTLC(payload.amtToForward, add.paymentHash, payload.outgoingCltvValue, nextPacket, upstream = Right(add), commit = true, previousFailures = previousFailures)) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala index 2ade1fe802..1e76f48b7b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala @@ -19,11 +19,10 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256, verifySignature} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering} import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{ShortChannelId, serializationResult} +import fr.acinq.eclair.{Features, ShortChannelId, serializationResult} import scodec.bits.{BitVector, ByteVector} import shapeless.HNil -import scala.concurrent.duration._ import scala.compat.Platform import scala.concurrent.duration._ @@ -75,8 +74,9 @@ object Announcements { } def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], timestamp: Long = Platform.currentTime.milliseconds.toSeconds): NodeAnnouncement = { - require(alias.size <= 32) - val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, ByteVector.empty, nodeAddresses, unknownFields = ByteVector.empty) + require(alias.length <= 32) + val features = BitVector.fromLong(1 << Features.VARIABLE_LENGTH_ONION_OPTIONAL).bytes + val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, features, nodeAddresses, unknownFields = ByteVector.empty) val sig = Crypto.sign(witness, nodeSecret) NodeAnnouncement( signature = sig, @@ -84,7 +84,7 @@ object Announcements { nodeId = nodeSecret.publicKey, rgbColor = color, alias = alias, - features = ByteVector.empty, + features = features, addresses = nodeAddresses ) } @@ -119,8 +119,6 @@ object Announcements { /** * This method compares channel updates, ignoring fields that don't matter, like signature or timestamp * - * @param u1 - * @param u2 * @return true if channel updates are "equal" */ def areSame(u1: ChannelUpdate, u2: ChannelUpdate): Boolean = diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala index b0e81166a5..e56a3645fd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala @@ -20,6 +20,7 @@ import java.net.{Inet4Address, Inet6Address, InetAddress} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, ByteVector64} +import fr.acinq.eclair.crypto.Mac32 import fr.acinq.eclair.{ShortChannelId, UInt64} import org.apache.commons.codec.binary.Base32 import scodec.bits.{BitVector, ByteVector} @@ -125,4 +126,17 @@ object CommonCodecs { def zeropaddedstring(size: Int): Codec[String] = fixedSizeBytes(32, utf8).xmap(s => s.takeWhile(_ != '\u0000'), s => s) + /** + * When encoding, prepend a valid mac to the output of the given codec. + * When decoding, verify that a valid mac is prepended. + */ + def prependmac[A](codec: Codec[A], mac: Mac32) = Codec[A]( + (a: A) => codec.encode(a).map(bits => mac.mac(bits.toByteVector).bits ++ bits), + (bits: BitVector) => ("mac" | bytes32).decode(bits) match { + case Attempt.Successful(DecodeResult(msgMac, remainder)) if mac.verify(msgMac, remainder.toByteVector) => codec.decode(remainder) + case Attempt.Successful(_) => Attempt.Failure(scodec.Err("invalid mac")) + case Attempt.Failure(err) => Attempt.Failure(err) + } + ) + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala index fc1233283b..a7cee716dd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala @@ -17,10 +17,11 @@ package fr.acinq.eclair.wire import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.eclair.crypto.Mac32 import fr.acinq.eclair.wire.CommonCodecs.{sha256, uint64overflow} -import fr.acinq.eclair.wire.LightningMessageCodecs.channelUpdateCodec +import fr.acinq.eclair.wire.LightningMessageCodecs.{channelUpdateCodec, lightningMessageCodec} import scodec.codecs._ -import scodec.Attempt +import scodec.{Attempt, Codec} /** * see https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md @@ -41,6 +42,7 @@ case object RequiredNodeFeatureMissing extends Perm with Node { def message = "p case class InvalidOnionVersion(onionHash: ByteVector32) extends BadOnion with Perm { def message = "onion version was not understood by the processing node" } case class InvalidOnionHmac(onionHash: ByteVector32) extends BadOnion with Perm { def message = "onion HMAC was incorrect when it reached the processing node" } case class InvalidOnionKey(onionHash: ByteVector32) extends BadOnion with Perm { def message = "ephemeral key was unparsable by the processing node" } +case class InvalidOnionPayload(onionHash: ByteVector32) extends BadOnion with Perm { def message = "onion per-hop payload could not be parsed" } case class TemporaryChannelFailure(update: ChannelUpdate) extends Update { def message = s"channel ${update.shortChannelId} is currently unavailable" } case object PermanentChannelFailure extends Perm { def message = "channel is permanently unavailable" } case object RequiredChannelFeatureMissing extends Perm { def message = "channel requires features not present in the onion" } @@ -64,9 +66,9 @@ object FailureMessageCodecs { val NODE = 0x2000 val UPDATE = 0x1000 - val channelUpdateCodecWithType = LightningMessageCodecs.lightningMessageCodec.narrow[ChannelUpdate](f => Attempt.successful(f.asInstanceOf[ChannelUpdate]), g => g) + val channelUpdateCodecWithType = lightningMessageCodec.narrow[ChannelUpdate](f => Attempt.successful(f.asInstanceOf[ChannelUpdate]), g => g) - // NB: for historical reasons some implementations were including/ommitting the message type (258 for ChannelUpdate) + // NB: for historical reasons some implementations were including/omitting the message type (258 for ChannelUpdate) // this codec supports both versions for decoding, and will encode with the message type val channelUpdateWithLengthCodec = variableSizeBytes(uint16, choice(channelUpdateCodecWithType, channelUpdateCodec)) @@ -75,6 +77,7 @@ object FailureMessageCodecs { .typecase(NODE | 2, provide(TemporaryNodeFailure)) .typecase(PERM | 2, provide(PermanentNodeFailure)) .typecase(PERM | NODE | 3, provide(RequiredNodeFeatureMissing)) + .typecase(BADONION | PERM, sha256.as[InvalidOnionPayload]) .typecase(BADONION | PERM | 4, sha256.as[InvalidOnionVersion]) .typecase(BADONION | PERM | 5, sha256.as[InvalidOnionHmac]) .typecase(BADONION | PERM | 6, sha256.as[InvalidOnionKey]) @@ -93,4 +96,24 @@ object FailureMessageCodecs { .typecase(18, ("expiry" | uint32).as[FinalIncorrectCltvExpiry]) .typecase(19, ("amountMsat" | uint64overflow).as[FinalIncorrectHtlcAmount]) .typecase(21, provide(ExpiryTooFar)) + + /** + * Return the failure code for a given failure message. This method actually encodes the failure message, which is a + * bit clunky and not particularly efficient. It shouldn't be used on the application's hot path. + */ + def failureCode(failure: FailureMessage): Int = failureMessageCodec.encode(failure).flatMap(uint16.decode).require.value + + /** + * An onion-encrypted failure from an intermediate node: + * +----------------+----------------------------------+-----------------+----------------------+-----+ + * | HMAC(32 bytes) | failure message length (2 bytes) | failure message | pad length (2 bytes) | pad | + * +----------------+----------------------------------+-----------------+----------------------+-----+ + * with failure message length + pad length = 256 + */ + def failureOnionCodec(mac: Mac32): Codec[FailureMessage] = CommonCodecs.prependmac( + paddedFixedSizeBytesDependent( + 260, + "failureMessage" | variableSizeBytes(uint16, FailureMessageCodecs.failureMessageCodec), + nBits => "padding" | variableSizeBytes(uint16, ignore(nBits - 2 * 8)) // two bytes are used to encode the padding length + ).as[FailureMessage], mac) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala index 132576af7f..b7e3891c43 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala @@ -16,20 +16,10 @@ package fr.acinq.eclair.wire -import java.net.{Inet4Address, Inet6Address, InetAddress} - -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64} -import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.wire import fr.acinq.eclair.wire.CommonCodecs._ -import scodec.bits.ByteVector -import scodec.codecs._ -import scodec.{Attempt, Codec, Err} - -import scala.util.{Failure, Success, Try} - import scodec.Codec +import scodec.codecs._ /** * Created by PM on 15/11/2016. @@ -123,7 +113,7 @@ object LightningMessageCodecs { ("amountMsat" | uint64overflow) :: ("paymentHash" | bytes32) :: ("expiry" | uint32) :: - ("onionRoutingPacket" | bytes(Sphinx.PacketLength))).as[UpdateAddHtlc] + ("onionRoutingPacket" | OnionCodecs.paymentOnionPacketCodec)).as[UpdateAddHtlc] val updateFulfillHtlcCodec: Codec[UpdateFulfillHtlc] = ( ("channelId" | bytes32) :: @@ -191,7 +181,7 @@ object LightningMessageCodecs { val nodeAnnouncementCodec: Codec[NodeAnnouncement] = ( ("signature" | bytes64) :: nodeAnnouncementWitnessCodec).as[NodeAnnouncement] - + val channelUpdateWitnessCodec = ("chainHash" | bytes32) :: ("shortChannelId" | shortchannelid) :: @@ -270,11 +260,4 @@ object LightningMessageCodecs { .typecase(264, replyChannelRangeCodec) .typecase(265, gossipTimestampFilterCodec) - val perHopPayloadCodec: Codec[PerHopPayload] = ( - ("realm" | constant(ByteVector.fromByte(0))) :: - ("short_channel_id" | shortchannelid) :: - ("amt_to_forward" | uint64overflow) :: - ("outgoing_cltv_value" | uint32) :: - ("unused_with_v0_version_on_header" | ignore(8 * 12))).as[PerHopPayload] - } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala index 0e4aef640a..89be06884a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala @@ -20,8 +20,8 @@ import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress} import java.nio.charset.StandardCharsets import com.google.common.base.Charsets +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, ByteVector64} -import fr.acinq.bitcoin.Crypto.{PublicKey, PrivateKey} import fr.acinq.eclair.{ShortChannelId, UInt64} import scodec.bits.ByteVector @@ -123,7 +123,7 @@ case class UpdateAddHtlc(channelId: ByteVector32, amountMsat: Long, paymentHash: ByteVector32, cltvExpiry: Long, - onionRoutingPacket: ByteVector) extends HtlcMessage with UpdateMessage with HasChannelId + onionRoutingPacket: OnionRoutingPacket) extends HtlcMessage with UpdateMessage with HasChannelId case class UpdateFulfillHtlc(channelId: ByteVector32, id: Long, @@ -225,10 +225,6 @@ case class ChannelUpdate(signature: ByteVector64, require(((messageFlags & 1) != 0) == htlcMaximumMsat.isDefined, "htlcMaximumMsat is not consistent with messageFlags") } -case class PerHopPayload(shortChannelId: ShortChannelId, - amtToForward: Long, - outgoingCltvValue: Long) - case class QueryShortChannelIds(chainHash: ByteVector32, data: ByteVector) extends RoutingMessage with HasChainHash diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala new file mode 100644 index 0000000000..32d942f547 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala @@ -0,0 +1,65 @@ +/* + * Copyright 2019 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.wire + +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.crypto.Sphinx +import scodec.bits.{BitVector, ByteVector} +import scodec.codecs._ +import scodec.{Codec, DecodeResult, Decoder} + +/** + * Created by t-bast on 05/07/2019. + */ + +case class OnionRoutingPacket(version: Int, + publicKey: ByteVector, + payload: ByteVector, + hmac: ByteVector32) + +case class PerHopPayload(shortChannelId: ShortChannelId, + amtToForward: Long, + outgoingCltvValue: Long) + +object OnionCodecs { + + def onionRoutingPacketCodec(payloadLength: Int): Codec[OnionRoutingPacket] = ( + ("version" | uint8) :: + ("publicKey" | bytes(33)) :: + ("onionPayload" | bytes(payloadLength)) :: + ("hmac" | CommonCodecs.bytes32)).as[OnionRoutingPacket] + + val paymentOnionPacketCodec: Codec[OnionRoutingPacket] = onionRoutingPacketCodec(Sphinx.PaymentPacket.PayloadLength) + + val perHopPayloadCodec: Codec[PerHopPayload] = ( + ("realm" | constant(ByteVector.fromByte(0))) :: + ("short_channel_id" | CommonCodecs.shortchannelid) :: + ("amt_to_forward" | CommonCodecs.uint64overflow) :: + ("outgoing_cltv_value" | uint32) :: + ("unused_with_v0_version_on_header" | ignore(8 * 12))).as[PerHopPayload] + + /** + * The 1.1 BOLT spec changed the onion frame format to use variable-length per-hop payloads. + * The first bytes contain a varint encoding the length of the payload data (not including the trailing mac). + * That varint is considered to be part of the payload, so the payload length includes the number of bytes used by + * the varint prefix. + */ + val payloadLengthDecoder = Decoder[Long]((bits: BitVector) => + CommonCodecs.varintoverflow.decode(bits).map(d => DecodeResult(d.value + (bits.length - d.remainder.length) / 8, d.remainder))) + +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala index a0813d961b..102e50e532 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -43,12 +43,18 @@ class FeaturesSpec extends FunSuite { assert(areSupported(features) && hasFeature(features, OPTION_DATA_LOSS_PROTECT_OPTIONAL) && hasFeature(features, INITIAL_ROUTING_SYNC_BIT_OPTIONAL)) } + test("'variable_length_onion' feature") { + assert(hasFeature(hex"0100", Features.VARIABLE_LENGTH_ONION_MANDATORY)) + assert(hasFeature(hex"0200", Features.VARIABLE_LENGTH_ONION_OPTIONAL)) + } + test("features compatibility") { assert(areSupported(Protocol.writeUInt64(1l << INITIAL_ROUTING_SYNC_BIT_OPTIONAL, ByteOrder.BIG_ENDIAN))) assert(areSupported(Protocol.writeUInt64(1L << OPTION_DATA_LOSS_PROTECT_MANDATORY, ByteOrder.BIG_ENDIAN))) assert(areSupported(Protocol.writeUInt64(1l << OPTION_DATA_LOSS_PROTECT_OPTIONAL, ByteOrder.BIG_ENDIAN))) - assert(areSupported(hex"14") == false) - assert(areSupported(hex"0141") == false) + assert(areSupported(Protocol.writeUInt64(1l << VARIABLE_LENGTH_ONION_OPTIONAL, ByteOrder.BIG_ENDIAN))) + assert(!areSupported(hex"14")) + assert(!areSupported(hex"0141")) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 79770b5833..e604a51dc2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -23,26 +23,27 @@ import fr.acinq.bitcoin.{Block, ByteVector32, Script} import fr.acinq.eclair.NodeParams.BITCOIND import fr.acinq.eclair.crypto.LocalKeyManager import fr.acinq.eclair.db._ -import fr.acinq.eclair.db.sqlite._ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.router.RouterConf import fr.acinq.eclair.wire.{Color, NodeAddress} import scodec.bits.ByteVector + import scala.concurrent.duration._ /** * Created by PM on 26/04/2016. */ object TestConstants { + val fundingSatoshis = 1000000L val pushMsat = 200000000L val feeratePerKw = 10000L + val emptyOnionPacket = wire.OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(1300)(0), ByteVector32.Zeroes) def sqliteInMemory() = DriverManager.getConnection("jdbc:sqlite::memory:") def inMemoryDb(connection: Connection = sqliteInMemory()): Databases = Databases.databaseByConnections(connection, connection, connection) - object Alice { val seed = ByteVector32(ByteVector.fill(32)(1)) val keyManager = new LocalKeyManager(seed, Block.RegtestGenesisBlock.hash) @@ -60,6 +61,7 @@ object TestConstants { maxHtlcValueInFlightMsat = UInt64(150000000), maxAcceptedHtlcs = 100, expiryDeltaBlocks = 144, + fulfillSafetyBeforeTimeoutBlocks = 6, htlcMinimumMsat = 0, minDepthBlocks = 3, toRemoteDelayBlocks = 144, @@ -69,7 +71,7 @@ object TestConstants { feeProportionalMillionth = 10, reserveToFundingRatio = 0.01, // note: not used (overridden below) maxReserveToFundingRatio = 0.05, - db = inMemoryDb(sqliteInMemory), + db = inMemoryDb(sqliteInMemory()), revocationTimeout = 20 seconds, pingInterval = 30 seconds, pingTimeout = 10 seconds, @@ -126,6 +128,7 @@ object TestConstants { maxHtlcValueInFlightMsat = UInt64.MaxValue, // Bob has no limit on the combined max value of in-flight htlcs maxAcceptedHtlcs = 30, expiryDeltaBlocks = 144, + fulfillSafetyBeforeTimeoutBlocks = 6, htlcMinimumMsat = 1000, minDepthBlocks = 3, toRemoteDelayBlocks = 144, @@ -135,7 +138,7 @@ object TestConstants { feeProportionalMillionth = 10, reserveToFundingRatio = 0.01, // note: not used (overridden below) maxReserveToFundingRatio = 0.05, - db = inMemoryDb(sqliteInMemory), + db = inMemoryDb(sqliteInMemory()), revocationTimeout = 20 seconds, pingInterval = 30 seconds, pingTimeout = 10 seconds, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala index 18aff515ef..9ff1329b8e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala @@ -51,7 +51,7 @@ class ThroughputSpec extends FunSuite { case ('add, tgt: ActorRef) => val r = randomBytes32 val h = Crypto.sha256(r) - tgt ! CMD_ADD_HTLC(1, h, 1, upstream = Left(UUID.randomUUID())) + tgt ! CMD_ADD_HTLC(1, h, 1, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) context.become(run(h2r + (h -> r))) case ('sig, tgt: ActorRef) => tgt ! CMD_SIGN diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala index 8ced14a635..f2cf9e0940 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala @@ -18,27 +18,22 @@ package fr.acinq.eclair.channel.states import java.util.UUID -import akka.actor.Actor import akka.testkit.{TestFSMRef, TestKitBase, TestProbe} import fr.acinq.bitcoin.{ByteVector32, Crypto} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw import fr.acinq.eclair.channel._ -import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.PaymentLifecycle import fr.acinq.eclair.router.Hop import fr.acinq.eclair.wire._ import fr.acinq.eclair.{Globals, NodeParams, TestConstants, randomBytes32} -import scodec.bits.ByteVector /** * Created by PM on 23/08/2016. */ trait StateTestsHelperMethods extends TestKitBase { - def defaultOnion: ByteVector = ByteVector.fill(Sphinx.PacketLength)(0) - case class SetupFixture(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, 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 b37eb9418b..89d0c58b8d 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 @@ -27,13 +27,14 @@ import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.UInt64.Conversions._ import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw -import fr.acinq.eclair.channel.Channel.{BroadcastChannelUpdate, PeriodicRefresh, Reconnected, RevocationTimeout} -import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.Channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods +import fr.acinq.eclair.channel.{ChannelErrorOccured, _} +import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.transactions.Transactions.{htlcSuccessWeight, htlcTimeoutWeight, weight2fee} +import fr.acinq.eclair.transactions.Transactions.{HtlcSuccessTx, htlcSuccessWeight, htlcTimeoutWeight, weight2fee} import fr.acinq.eclair.transactions.{IN, OUT} import fr.acinq.eclair.wire.{AnnouncementSignatures, ChannelUpdate, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass, randomBytes32} @@ -50,6 +51,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { type FixtureParam = SetupFixture + implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging + override def withFixture(test: OneArgTest): Outcome = { val setup = init() import setup._ @@ -66,7 +69,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val sender = TestProbe() val h = randomBytes32 - val add = CMD_ADD_HTLC(50000000, h, 400144, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(50000000, h, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) sender.expectMsg("ok") val htlc = alice2bob.expectMsgType[UpdateAddHtlc] @@ -84,7 +87,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() val h = randomBytes32 for (i <- 0 until 10) { - sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") val htlc = alice2bob.expectMsgType[UpdateAddHtlc] assert(htlc.id == i && htlc.paymentHash == h) @@ -96,8 +99,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val sender = TestProbe() val h = randomBytes32 - val originHtlc = UpdateAddHtlc(channelId = randomBytes32, id = 5656, amountMsat = 50000000, cltvExpiry = 400144, paymentHash = h, onionRoutingPacket = ByteVector.fill(1254)(0)) - val cmd = CMD_ADD_HTLC(originHtlc.amountMsat - 10000, h, originHtlc.cltvExpiry - 7, upstream = Right(originHtlc)) + val originHtlc = UpdateAddHtlc(channelId = randomBytes32, id = 5656, amountMsat = 50000000, cltvExpiry = 400144, paymentHash = h, onionRoutingPacket = TestConstants.emptyOnionPacket) + val cmd = CMD_ADD_HTLC(originHtlc.amountMsat - 10000, h, originHtlc.cltvExpiry - 7, TestConstants.emptyOnionPacket, upstream = Right(originHtlc)) sender.send(alice, cmd) sender.expectMsg("ok") val htlc = alice2bob.expectMsgType[UpdateAddHtlc] @@ -116,7 +119,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val currentBlockCount = Globals.blockCount.get val expiryTooSmall = currentBlockCount + 3 - val add = CMD_ADD_HTLC(500000000, randomBytes32, cltvExpiry = expiryTooSmall, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(500000000, randomBytes32, expiryTooSmall, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ExpiryTooSmall(channelId(alice), currentBlockCount + Channel.MIN_CLTV_EXPIRY, expiryTooSmall, currentBlockCount) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -129,7 +132,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val currentBlockCount = Globals.blockCount.get val expiryTooBig = currentBlockCount + Channel.MAX_CLTV_EXPIRY + 1 - val add = CMD_ADD_HTLC(500000000, randomBytes32, cltvExpiry = expiryTooBig, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(500000000, randomBytes32, expiryTooBig, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ExpiryTooBig(channelId(alice), maximum = currentBlockCount + Channel.MAX_CLTV_EXPIRY, actual = expiryTooBig, blockCount = currentBlockCount) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -140,7 +143,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val add = CMD_ADD_HTLC(50, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(50, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = HtlcValueTooSmall(channelId(alice), 1000, 50) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -151,7 +154,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val add = CMD_ADD_HTLC(Int.MaxValue, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(Int.MaxValue, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = InsufficientFunds(channelId(alice), amountMsat = Int.MaxValue, missingSatoshis = 1376443, reserveSatoshis = 20000, feesSatoshis = 8960) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -162,16 +165,16 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - sender.send(alice, CMD_ADD_HTLC(500000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(500000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - sender.send(alice, CMD_ADD_HTLC(200000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(200000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - sender.send(alice, CMD_ADD_HTLC(67600000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(67600000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - val add = CMD_ADD_HTLC(1000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(1000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = InsufficientFunds(channelId(alice), amountMsat = 1000000, missingSatoshis = 1000, reserveSatoshis = 20000, feesSatoshis = 12400) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -182,13 +185,13 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - sender.send(alice, CMD_ADD_HTLC(300000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(300000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - sender.send(alice, CMD_ADD_HTLC(300000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(300000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - val add = CMD_ADD_HTLC(500000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(500000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = InsufficientFunds(channelId(alice), amountMsat = 500000000, missingSatoshis = 332400, reserveSatoshis = 20000, feesSatoshis = 12400) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -199,7 +202,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val add = CMD_ADD_HTLC(151000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(151000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(bob, add) val error = HtlcValueTooHighInFlight(channelId(bob), maximum = 150000000, actual = 151000000) sender.expectMsg(Failure(AddHtlcFailed(channelId(bob), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -212,11 +215,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] // Bob accepts a maximum of 30 htlcs for (i <- 0 until 30) { - sender.send(alice, CMD_ADD_HTLC(10000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] } - val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = TooManyAcceptedHtlcs(channelId(alice), maximum = 30) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -227,7 +230,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val add1 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) + val add1 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add1) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] @@ -235,7 +238,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.expectMsg("ok") alice2bob.expectMsgType[CommitSig] // this is over channel-capacity - val add2 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) + val add2 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add2) val error = InsufficientFunds(channelId(alice), add2.amountMsat, 564012, 20000, 10680) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add2.paymentHash, error, Local(add2.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add2)))) @@ -249,10 +252,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.send(alice, CMD_CLOSE(None)) sender.expectMsg("ok") alice2bob.expectMsgType[Shutdown] - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined && !alice.stateData.asInstanceOf[DATA_NORMAL].remoteShutdown.isDefined) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined && alice.stateData.asInstanceOf[DATA_NORMAL].remoteShutdown.isEmpty) // actual test starts here - val add = CMD_ADD_HTLC(500000000, randomBytes32, cltvExpiry = 400144, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(500000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = NoMoreHtlcsClosingInProgress(channelId(alice)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -264,14 +267,14 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] // let's make alice send an htlc - val add1 = CMD_ADD_HTLC(500000000, randomBytes32, cltvExpiry = 400144, upstream = Left(UUID.randomUUID())) + val add1 = CMD_ADD_HTLC(500000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add1) sender.expectMsg("ok") // at the same time bob initiates a closing sender.send(bob, CMD_CLOSE(None)) sender.expectMsg("ok") // this command will be received by alice right after having received the shutdown - val add2 = CMD_ADD_HTLC(100000000, randomBytes32, cltvExpiry = 300000, upstream = Left(UUID.randomUUID())) + val add2 = CMD_ADD_HTLC(100000000, randomBytes32, 300000, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) // messages cross alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) @@ -285,7 +288,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc") { f => import f._ val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 150000, randomBytes32, 400144, defaultOnion) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 150000, randomBytes32, 400144, TestConstants.emptyOnionPacket) bob ! htlc awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ htlc), remoteNextHtlcId = 1))) // bob won't forward the add before it is cross-signed @@ -295,7 +298,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (unexpected id)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 42, 150000, randomBytes32, 400144, defaultOnion) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 42, 150000, randomBytes32, 400144, TestConstants.emptyOnionPacket) bob ! htlc.copy(id = 0) bob ! htlc.copy(id = 1) bob ! htlc.copy(id = 2) @@ -312,7 +315,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (value too small)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 150, randomBytes32, cltvExpiry = 400144, defaultOnion) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 150, randomBytes32, cltvExpiry = 400144, TestConstants.emptyOnionPacket) alice2bob.forward(bob, htlc) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) === HtlcValueTooSmall(channelId(bob), minimum = 1000, actual = 150).getMessage) @@ -327,7 +330,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (insufficient funds)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Long.MaxValue, randomBytes32, 400144, defaultOnion) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Long.MaxValue, randomBytes32, 400144, TestConstants.emptyOnionPacket) alice2bob.forward(bob, htlc) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) === InsufficientFunds(channelId(bob), amountMsat = Long.MaxValue, missingSatoshis = 9223372036083735L, reserveSatoshis = 20000, feesSatoshis = 8960).getMessage) @@ -342,10 +345,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs 1/2)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 400000000, randomBytes32, 400144, defaultOnion)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 200000000, randomBytes32, 400144, defaultOnion)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 167600000, randomBytes32, 400144, defaultOnion)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 3, 10000000, randomBytes32, 400144, defaultOnion)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 400000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 200000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 167600000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 3, 10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) === InsufficientFunds(channelId(bob), amountMsat = 10000000, missingSatoshis = 11720, reserveSatoshis = 20000, feesSatoshis = 14120).getMessage) awaitCond(bob.stateName == CLOSING) @@ -359,9 +362,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs 2/2)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 300000000, randomBytes32, 400144, defaultOnion)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 300000000, randomBytes32, 400144, defaultOnion)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 500000000, randomBytes32, 400144, defaultOnion)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 300000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 300000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 500000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) === InsufficientFunds(channelId(bob), amountMsat = 500000000, missingSatoshis = 332400, reserveSatoshis = 20000, feesSatoshis = 12400).getMessage) awaitCond(bob.stateName == CLOSING) @@ -375,7 +378,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (over max inflight htlc value)") { f => import f._ val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - alice2bob.forward(alice, UpdateAddHtlc(ByteVector32.Zeroes, 0, 151000000, randomBytes32, 400144, defaultOnion)) + alice2bob.forward(alice, UpdateAddHtlc(ByteVector32.Zeroes, 0, 151000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) val error = alice2bob.expectMsgType[Error] assert(new String(error.data.toArray) === HtlcValueTooHighInFlight(channelId(alice), maximum = 150000000, actual = 151000000).getMessage) awaitCond(alice.stateName == CLOSING) @@ -391,9 +394,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx // Bob accepts a maximum of 30 htlcs for (i <- 0 until 30) { - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, i, 1000000, randomBytes32, 400144, defaultOnion)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, i, 1000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) } - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 30, 1000000, randomBytes32, 400144, defaultOnion)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 30, 1000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) === TooManyAcceptedHtlcs(channelId(bob), maximum = 30).getMessage) awaitCond(bob.stateName == CLOSING) @@ -418,7 +421,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_SIGN (two identical htlcs in each direction)") { f => import f._ val sender = TestProbe() - val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] @@ -465,19 +468,19 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(a2b_2 > aliceMinOffer && a2b_2 > bobMinReceive) assert(b2a_1 > aliceMinReceive && b2a_1 > bobMinOffer) assert(b2a_2 < aliceMinReceive && b2a_2 > bobMinOffer) - sender.send(alice, CMD_ADD_HTLC(a2b_1 * 1000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(a2b_1 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) - sender.send(alice, CMD_ADD_HTLC(a2b_2 * 1000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(a2b_2 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) - sender.send(bob, CMD_ADD_HTLC(b2a_1 * 1000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(bob, CMD_ADD_HTLC(b2a_1 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") bob2alice.expectMsgType[UpdateAddHtlc] bob2alice.forward(alice) - sender.send(bob, CMD_ADD_HTLC(b2a_2 * 1000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(bob, CMD_ADD_HTLC(b2a_2 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") bob2alice.expectMsgType[UpdateAddHtlc] bob2alice.forward(alice) @@ -497,7 +500,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_SIGN (htlcs with same pubkeyScript but different amounts)") { f => import f._ val sender = TestProbe() - val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) val epsilons = List(3, 1, 5, 7, 6) // unordered on purpose val htlcCount = epsilons.size for (i <- epsilons) { @@ -694,12 +697,12 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val r = randomBytes32 val h = Crypto.sha256(r) - sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) - sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) @@ -965,7 +968,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() val (_, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - sender.send(bob, CMD_FAIL_MALFORMED_HTLC(htlc.id, Crypto.sha256(htlc.onionRoutingPacket), FailureMessageCodecs.BADONION)) + sender.send(bob, CMD_FAIL_MALFORMED_HTLC(htlc.id, Sphinx.PaymentPacket.hash(htlc.onionRoutingPacket), FailureMessageCodecs.BADONION)) sender.expectMsg("ok") val fail = bob2alice.expectMsgType[UpdateFailMalformedHtlc] bob2alice.forward(alice) @@ -1177,7 +1180,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // actual test begins val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - sender.send(bob, CMD_FAIL_MALFORMED_HTLC(htlc.id, Crypto.sha256(htlc.onionRoutingPacket), FailureMessageCodecs.BADONION)) + sender.send(bob, CMD_FAIL_MALFORMED_HTLC(htlc.id, Sphinx.PaymentPacket.hash(htlc.onionRoutingPacket), FailureMessageCodecs.BADONION)) sender.expectMsg("ok") val fail = bob2alice.expectMsgType[UpdateFailMalformedHtlc] awaitCond(bob.stateData == initialState.copy( @@ -1241,7 +1244,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { crossSign(alice, bob, alice2bob, bob2alice) // Bob fails the HTLC because he cannot parse it val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - sender.send(bob, CMD_FAIL_MALFORMED_HTLC(htlc.id, Crypto.sha256(htlc.onionRoutingPacket), FailureMessageCodecs.BADONION)) + sender.send(bob, CMD_FAIL_MALFORMED_HTLC(htlc.id, Sphinx.PaymentPacket.hash(htlc.onionRoutingPacket), FailureMessageCodecs.BADONION)) sender.expectMsg("ok") val fail = bob2alice.expectMsgType[UpdateFailMalformedHtlc] bob2alice.forward(alice) @@ -1269,7 +1272,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // actual test begins val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val fail = UpdateFailMalformedHtlc(ByteVector32.Zeroes, htlc.id, Crypto.sha256(htlc.onionRoutingPacket), 42) + val fail = UpdateFailMalformedHtlc(ByteVector32.Zeroes, htlc.id, Sphinx.PaymentPacket.hash(htlc.onionRoutingPacket), 42) sender.send(alice, fail) val error = alice2bob.expectMsgType[Error] assert(new String(error.data.toArray) === InvalidFailureCode(ByteVector32.Zeroes).getMessage) @@ -1677,10 +1680,120 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { alice2blockchain.expectMsgType[PublishAsap] // main delayed alice2blockchain.expectMsgType[PublishAsap] // htlc timeout alice2blockchain.expectMsgType[PublishAsap] // htlc delayed - val watch = alice2blockchain.expectMsgType[WatchConfirmed] + val watch = alice2blockchain.expectMsgType[WatchConfirmed] assert(watch.event === BITCOIN_TX_CONFIRMED(aliceCommitTx)) } + test("recv CurrentBlockCount (fulfilled signed htlc ignored by upstream peer)") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccured]) + + // actual test begins: + // * Bob receives the HTLC pre-image and wants to fulfill + // * Alice does not react to the fulfill (drops the message for some reason) + // * When the HTLC timeout on Alice side is near, Bob needs to close the channel to avoid an on-chain race + // condition between his HTLC-success and Alice's HTLC-timeout + val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] + val initialCommitTx = initialState.commitments.localCommit.publishableTxs.commitTx.tx + val HtlcSuccessTx(_, htlcSuccessTx, _) = initialState.commitments.localCommit.publishableTxs.htlcTxsAndSigs.head.txinfo + + sender.send(bob, CMD_FULFILL_HTLC(htlc.id, r, commit = true)) + sender.expectMsg("ok") + bob2alice.expectMsgType[UpdateFulfillHtlc] + sender.send(bob, CurrentBlockCount(htlc.cltvExpiry - Bob.nodeParams.fulfillSafetyBeforeTimeoutBlocks)) + + val ChannelErrorOccured(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccured] + assert(isFatal) + assert(err.isInstanceOf[HtlcWillTimeoutUpstream]) + + bob2blockchain.expectMsg(PublishAsap(initialCommitTx)) + bob2blockchain.expectMsgType[PublishAsap] // main delayed + assert(bob2blockchain.expectMsgType[PublishAsap].tx.txOut === htlcSuccessTx.txOut) + bob2blockchain.expectMsgType[PublishAsap] // htlc delayed + assert(bob2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(initialCommitTx)) + alice2blockchain.expectNoMsg(500 millis) + } + + test("recv CurrentBlockCount (fulfilled proposed htlc ignored by upstream peer)") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccured]) + + // actual test begins: + // * Bob receives the HTLC pre-image and wants to fulfill but doesn't sign + // * Alice does not react to the fulfill (drops the message for some reason) + // * When the HTLC timeout on Alice side is near, Bob needs to close the channel to avoid an on-chain race + // condition between his HTLC-success and Alice's HTLC-timeout + val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] + val initialCommitTx = initialState.commitments.localCommit.publishableTxs.commitTx.tx + val HtlcSuccessTx(_, htlcSuccessTx, _) = initialState.commitments.localCommit.publishableTxs.htlcTxsAndSigs.head.txinfo + + sender.send(bob, CMD_FULFILL_HTLC(htlc.id, r, commit = false)) + sender.expectMsg("ok") + bob2alice.expectMsgType[UpdateFulfillHtlc] + sender.send(bob, CurrentBlockCount(htlc.cltvExpiry - Bob.nodeParams.fulfillSafetyBeforeTimeoutBlocks)) + + val ChannelErrorOccured(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccured] + assert(isFatal) + assert(err.isInstanceOf[HtlcWillTimeoutUpstream]) + + bob2blockchain.expectMsg(PublishAsap(initialCommitTx)) + bob2blockchain.expectMsgType[PublishAsap] // main delayed + assert(bob2blockchain.expectMsgType[PublishAsap].tx.txOut === htlcSuccessTx.txOut) + bob2blockchain.expectMsgType[PublishAsap] // htlc delayed + assert(bob2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(initialCommitTx)) + alice2blockchain.expectNoMsg(500 millis) + } + + test("recv CurrentBlockCount (fulfilled proposed htlc acked but not committed by upstream peer)") { f => + import f._ + val sender = TestProbe() + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccured]) + + // actual test begins: + // * Bob receives the HTLC pre-image and wants to fulfill + // * Alice acks but doesn't commit + // * When the HTLC timeout on Alice side is near, Bob needs to close the channel to avoid an on-chain race + // condition between his HTLC-success and Alice's HTLC-timeout + val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] + val initialCommitTx = initialState.commitments.localCommit.publishableTxs.commitTx.tx + val HtlcSuccessTx(_, htlcSuccessTx, _) = initialState.commitments.localCommit.publishableTxs.htlcTxsAndSigs.head.txinfo + + sender.send(bob, CMD_FULFILL_HTLC(htlc.id, r, commit = true)) + sender.expectMsg("ok") + bob2alice.expectMsgType[UpdateFulfillHtlc] + bob2alice.forward(alice) + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + sender.send(bob, CurrentBlockCount(htlc.cltvExpiry - Bob.nodeParams.fulfillSafetyBeforeTimeoutBlocks)) + + val ChannelErrorOccured(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccured] + assert(isFatal) + assert(err.isInstanceOf[HtlcWillTimeoutUpstream]) + + bob2blockchain.expectMsg(PublishAsap(initialCommitTx)) + bob2blockchain.expectMsgType[PublishAsap] // main delayed + assert(bob2blockchain.expectMsgType[PublishAsap].tx.txOut === htlcSuccessTx.txOut) + bob2blockchain.expectMsgType[PublishAsap] // htlc delayed + assert(bob2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(initialCommitTx)) + alice2blockchain.expectNoMsg(500 millis) + } + test("recv CurrentFeerate (when funder, triggers an UpdateFee)") { f => import f._ val sender = TestProbe() @@ -1901,7 +2014,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // alice = 800 000 // bob = 200 000 - val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index 8ad000e222..b757b7c0cd 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -16,21 +16,22 @@ package fr.acinq.eclair.channel.states.e -import akka.actor.Status import java.util.UUID - -import akka.testkit.TestProbe +import akka.actor.Status +import akka.testkit.{TestActorRef, TestProbe} import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{ByteVector32, ScriptFlags, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratesPerKw import fr.acinq.eclair.blockchain.{CurrentFeerates, PublishAsap, WatchEventSpent} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods +import fr.acinq.eclair.payment.CommandBuffer +import fr.acinq.eclair.payment.CommandBuffer.CommandSend import fr.acinq.eclair.router.Announcements +import fr.acinq.eclair.transactions.Transactions.HtlcSuccessTx import fr.acinq.eclair.wire._ import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass, randomBytes32} import org.scalatest.Outcome - import scala.concurrent.duration._ /** @@ -53,6 +54,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { } def aliceInit = Init(TestConstants.Alice.nodeParams.globalFeatures, TestConstants.Alice.nodeParams.localFeatures) + def bobInit = Init(TestConstants.Bob.nodeParams.globalFeatures, TestConstants.Bob.nodeParams.localFeatures) /** @@ -62,7 +64,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - sender.send(alice, CMD_ADD_HTLC(1000000, ByteVector32.Zeroes, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(1000000, ByteVector32.Zeroes, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) val ab_add_0 = alice2bob.expectMsgType[UpdateAddHtlc] // add ->b alice2bob.forward(bob) @@ -139,7 +141,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - sender.send(alice, CMD_ADD_HTLC(1000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(1000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) val ab_add_0 = alice2bob.expectMsgType[UpdateAddHtlc] // add ->b alice2bob.forward(bob, ab_add_0) @@ -181,7 +183,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // b will re-send the lost revocation val ba_rev_0_re = bob2alice.expectMsg(ba_rev_0) // rev ->a - bob2alice.forward(alice, ba_rev_0) + bob2alice.forward(alice, ba_rev_0_re) // and b will attempt a new signature bob2alice.expectMsg(ba_sig_0) @@ -257,7 +259,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // we start by storing the current state val oldStateData = alice.stateData // then we add an htlc and sign it - val (ra1, htlca1) = addHtlc(250000000, alice, bob, alice2bob, bob2alice) + addHtlc(250000000, alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") alice2bob.expectMsgType[CommitSig] @@ -363,7 +365,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val channelUpdate = channelUpdateListener.expectMsgType[LocalChannelUpdate](20 seconds).channelUpdate assert(channelUpdate.feeBaseMsat === 4200) assert(channelUpdate.feeProportionalMillionths === 123456) - assert(Announcements.isEnabled(channelUpdate.channelFlags) == true) + assert(Announcements.isEnabled(channelUpdate.channelFlags)) // no more messages channelUpdateListener.expectNoMsg(300 millis) @@ -383,13 +385,76 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { channelUpdateListener.expectNoMsg(300 millis) // we attempt to send a payment - sender.send(alice, CMD_ADD_HTLC(4200, randomBytes32, 123456, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(4200, randomBytes32, 123456, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) val failure = sender.expectMsgType[Status.Failure] val AddHtlcFailed(_, _, ChannelUnavailable(_), _, _, _) = failure.cause // alice will broadcast a new disabled channel_update val update = channelUpdateListener.expectMsgType[LocalChannelUpdate] - assert(Announcements.isEnabled(update.channelUpdate.channelFlags) == false) + assert(!Announcements.isEnabled(update.channelUpdate.channelFlags)) + } + + test("pending non-relayed fulfill htlcs will timeout upstream") { f => + import f._ + val sender = TestProbe() + val register = TestProbe() + val commandBuffer = TestActorRef(new CommandBuffer(bob.underlyingActor.nodeParams, register.ref)) + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccured]) + + val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] + val initialCommitTx = initialState.commitments.localCommit.publishableTxs.commitTx.tx + val HtlcSuccessTx(_, htlcSuccessTx, _) = initialState.commitments.localCommit.publishableTxs.htlcTxsAndSigs.head.txinfo + + sender.send(alice, INPUT_DISCONNECTED) + sender.send(bob, INPUT_DISCONNECTED) + awaitCond(alice.stateName == OFFLINE) + awaitCond(bob.stateName == OFFLINE) + + // We simulate a pending fulfill on that HTLC but not relayed. + // When it is close to expiring upstream, we should close the channel. + sender.send(commandBuffer, CommandSend(htlc.channelId, htlc.id, CMD_FULFILL_HTLC(htlc.id, r, commit = true))) + sender.send(bob, CurrentBlockCount(htlc.cltvExpiry - bob.underlyingActor.nodeParams.fulfillSafetyBeforeTimeoutBlocks)) + + val ChannelErrorOccured(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccured] + assert(isFatal) + assert(err.isInstanceOf[HtlcWillTimeoutUpstream]) + + bob2blockchain.expectMsg(PublishAsap(initialCommitTx)) + bob2blockchain.expectMsgType[PublishAsap] // main delayed + assert(bob2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(initialCommitTx)) + bob2blockchain.expectMsgType[WatchConfirmed] // main delayed + + bob2blockchain.expectMsg(PublishAsap(initialCommitTx)) + bob2blockchain.expectMsgType[PublishAsap] // main delayed + assert(bob2blockchain.expectMsgType[PublishAsap].tx.txOut === htlcSuccessTx.txOut) + bob2blockchain.expectMsgType[PublishAsap] // htlc delayed + alice2blockchain.expectNoMsg(500 millis) + } + + test("pending non-relayed fail htlcs will timeout upstream") { f => + import f._ + val sender = TestProbe() + val register = TestProbe() + val commandBuffer = TestActorRef(new CommandBuffer(bob.underlyingActor.nodeParams, register.ref)) + val (_, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + + sender.send(alice, INPUT_DISCONNECTED) + sender.send(bob, INPUT_DISCONNECTED) + awaitCond(alice.stateName == OFFLINE) + awaitCond(bob.stateName == OFFLINE) + + // We simulate a pending failure on that HTLC. + // Even if we get close to expiring upstream we shouldn't close the channel, because we have nothing to lose. + sender.send(commandBuffer, CommandSend(htlc.channelId, htlc.id, CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(0))))) + sender.send(bob, CurrentBlockCount(htlc.cltvExpiry - bob.underlyingActor.nodeParams.fulfillSafetyBeforeTimeoutBlocks)) + + bob2blockchain.expectNoMsg(250 millis) + alice2blockchain.expectNoMsg(250 millis) } test("handle feerate changes while offline (funder scenario)") { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index dd73f294f4..8946196271 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -20,7 +20,7 @@ import java.util.UUID import akka.actor.Status.Failure import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.{PrivateKey} +import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi, ScriptFlags, Transaction} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw @@ -103,7 +103,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_ADD_HTLC") { f => import f._ val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, r1, cltvExpiry = 300000, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(500000000, r1, cltvExpiry = 300000, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), None, Some(add)))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index 6ae3d67399..85a1b45fcc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -30,7 +30,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.payment.Local import fr.acinq.eclair.wire.{ClosingSigned, Error, Shutdown} -import fr.acinq.eclair.{Globals, TestkitBaseClass} +import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass} import org.scalatest.{Outcome, Tag} import scodec.bits.ByteVector @@ -72,7 +72,7 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods import f._ alice2bob.expectMsgType[ClosingSigned] val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = 300000, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(500000000, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = 300000, onion = TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), None, Some(add)))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index ec19d818e7..e9054c14a5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -278,7 +278,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // actual test starts here val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = 300000, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(500000000, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = 300000, onion = TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), None, Some(add)))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/MacSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/MacSpec.scala new file mode 100644 index 0000000000..47a8f077aa --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/MacSpec.scala @@ -0,0 +1,62 @@ +/* + * Copyright 2019 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.crypto + +import fr.acinq.bitcoin.ByteVector32 +import org.scalatest.FunSuite +import scodec.bits.HexStringSyntax + +/** + * Created by t-bast on 04/07/19. + */ + +class MacSpec extends FunSuite { + + test("HMAC-256 mac/verify") { + val keys = Seq( + hex"0000000000000000000000000000000000000000000000000000000000000000", + hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", + hex"24653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c7f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007" + ) + val messages = Seq( + hex"2a", + hex"451", + hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", + hex"fd0001000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff" + ) + + for (key <- keys) { + val instance = Hmac256(key) + for (message <- messages) { + assert(instance.verify(instance.mac(message), message)) + } + } + } + + test("HMAC-256 invalid macs") { + val instance = Hmac256(ByteVector32.Zeroes) + val testCases = Seq( + (hex"0000000000000000000000000000000000000000000000000000000000000000", hex"2a"), + (hex"4aa79e2da0cb5beae9b5dad4006909cb402e4201e191733bc2b5279629e4ed80", hex"fd0001000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233") + ).map(testCase => (ByteVector32(testCase._1), testCase._2)) + + for ((mac, message) <- testCases) { + assert(!instance.verify(mac, message)) + } + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index f0b8c42f70..59a622b48b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.crypto import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.eclair.wire import fr.acinq.eclair.wire._ import org.scalatest.FunSuite import scodec.bits._ @@ -27,7 +28,6 @@ import scala.util.Success /** * Created by fabrice on 10/01/17. */ - class SphinxSpec extends FunSuite { import Sphinx._ @@ -54,7 +54,7 @@ class SphinxSpec extends FunSuite { hop_blinding_factor[4] = 0xc96e00dddaf57e7edcd4fb5954be5b65b09f17cb6d20651b4e90315be5779205 hop_ephemeral_pubkey[4] = 0x03a214ebd875aab6ddfd77f22c5e7311d7f77f17a169e599f157bbcdae8bf071f4 */ - test("generate ephemeral keys and secrets") { + test("generate ephemeral keys and secrets (reference test vector)") { val (ephkeys, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) assert(ephkeys(0) == PublicKey(hex"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")) assert(sharedsecrets(0) == ByteVector32(hex"53eb63ea8a3fec3b3cd433b85cd62a4b145e1dda09391b348c4e1cd36a03ea66")) @@ -68,111 +68,272 @@ class SphinxSpec extends FunSuite { assert(sharedsecrets(4) == ByteVector32(hex"b5756b9b542727dbafc6765a49488b023a725d631af688fc031217e90770c328")) } - /* - filler = 0xc6b008cf6414ed6e4c42c291eb505e9f22f5fe7d0ecdd15a833f4d016ac974d33adc6ea3293e20859e87ebfb937ba406abd025d14af692b12e9c9c2adbe307a679779259676211c071e614fdb386d1ff02db223a5b2fae03df68d321c7b29f7c7240edd3fa1b7cb6903f89dc01abf41b2eb0b49b6b8d73bb0774b58204c0d0e96d3cce45ad75406be0bc009e327b3e712a4bd178609c00b41da2daf8a4b0e1319f07a492ab4efb056f0f599f75e6dc7e0d10ce1cf59088ab6e873de377343880f7a24f0e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac - */ - test("generate filler") { + test("generate filler with fixed-size payloads (reference test vector)") { val (_, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) - val filler = generateFiller("rho", sharedsecrets.dropRight(1), PayloadLength + MacLength, 20) + val filler = PaymentPacket.generateFiller("rho", sharedsecrets.dropRight(1), referenceFixedSizePayloads.dropRight(1)) assert(filler == hex"c6b008cf6414ed6e4c42c291eb505e9f22f5fe7d0ecdd15a833f4d016ac974d33adc6ea3293e20859e87ebfb937ba406abd025d14af692b12e9c9c2adbe307a679779259676211c071e614fdb386d1ff02db223a5b2fae03df68d321c7b29f7c7240edd3fa1b7cb6903f89dc01abf41b2eb0b49b6b8d73bb0774b58204c0d0e96d3cce45ad75406be0bc009e327b3e712a4bd178609c00b41da2daf8a4b0e1319f07a492ab4efb056f0f599f75e6dc7e0d10ce1cf59088ab6e873de377343880f7a24f0e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac") } - test("create packet (reference test vector)") { - val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, payloads, associatedData) - assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f917265f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf") + test("generate filler with variable-size payloads") { + val (_, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) + val filler = PaymentPacket.generateFiller("rho", sharedsecrets.dropRight(1), referenceVariableSizePayloads.dropRight(1)) + assert(filler == hex"b77d99c935d3f32469844f7e09340a91ded147557bdd0456c369f7e449587c0f5666faab58040146db49024db88553729bce12b860391c29c1779f022ae48a9cb314ca35d73fc91addc92632bcf7ba6fd9f38e6fd30fabcedbd5407b6648073c38331ee7ab0332f41f550c180e1601f8c25809ed75b3a1e78635a2ef1b828e92c9658e76e49f995d72cf9781eec0c838901d0bdde3ac21c13b4979ac9e738a1c4d0b9741d58e777ad1aed01263ad1390d36a18a6b92f4f799dcf75edbb43b7515e8d72cb4f827a9af0e7b9338d07b1a24e0305b5535f5b851b1144bad6238b9d9482b5ba6413f1aafac3cdde5067966ed8b78f7c1c5f916a05f874d5f17a2b7d0ae75d66a5f1bb6ff932570dc5a0cf3ce04eb5d26bc55c2057af1f8326e20a7d6f0ae644f09d00fac80de60f20aceee85be41a074d3e1dda017db79d0070b99f54736396f206ee3777abd4c00a4bb95c871750409261e3b01e59a3793a9c20159aae4988c68397a1443be6370fd9614e46108291e615691729faea58537209fa668a172d066d0efff9bc77c2bd34bd77870ad79effd80140990e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a") + } + + test("peek at per-hop payload length") { + val testCases = Map( + 34 -> hex"01", + 41 -> hex"08", + 65 -> hex"00", + 285 -> hex"fc", + 288 -> hex"fd00fd", + 65570 -> hex"fdffff" + ) + + for ((expected, payload) <- testCases) { + assert(peekPayloadLength(payload) === expected) + } + } + + test("is last packet") { + val testCases = Seq( + // Bolt 1.0 payloads use the next packet's hmac to signal termination. + (true, DecryptedPacket(hex"00", OnionRoutingPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.Zeroes), ByteVector32.One)), + (false, DecryptedPacket(hex"00", OnionRoutingPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.One), ByteVector32.One)), + // Bolt 1.1 payloads currently also use the next packet's hmac to signal termination. + (true, DecryptedPacket(hex"0101", OnionRoutingPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.Zeroes), ByteVector32.One)), + (false, DecryptedPacket(hex"0101", OnionRoutingPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.One), ByteVector32.One)), + (false, DecryptedPacket(hex"0100", OnionRoutingPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.One), ByteVector32.One)), + (false, DecryptedPacket(hex"0101", OnionRoutingPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.One), ByteVector32.One)) + ) + + for ((expected, packet) <- testCases) { + assert(packet.isLastPacket === expected) + } + } + + test("bad onion") { + val badOnions = Seq[wire.OnionRoutingPacket]( + wire.OnionRoutingPacket(1, ByteVector.fill(33)(0), ByteVector.fill(65)(1), ByteVector32.Zeroes), + wire.OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(65)(1), ByteVector32.Zeroes), + wire.OnionRoutingPacket(0, publicKeys.head.value, ByteVector.fill(42)(1), ByteVector32(hex"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a")) + ) + + val expected = Seq[BadOnion]( + InvalidOnionVersion(ByteVector32(hex"2f89b15c6cb0bb256d7a71b66de0d50cd3dd806f77d1cc1a3b0d86a0becd28ce")), + InvalidOnionKey(ByteVector32(hex"d2602c65fc331d6ae728331ae50e602f35929312ca7a951dc5ce250031b6b999")), + InvalidOnionHmac(ByteVector32(hex"3c01a86e6bc51b44a2718745fbbbc71a5c5dde5f46a489da17046c9d097bb303")) + ) + + for ((packet, expected) <- badOnions zip expected) { + val Left(onionErr) = PaymentPacket.peel(privKeys.head, associatedData, packet) + assert(onionErr === expected) + } + } + + test("create packet with fixed-size payloads (reference test vector)") { + val PacketAndSecrets(onion, sharedSecrets) = PaymentPacket.create(sessionKey, publicKeys, referenceFixedSizePayloads, associatedData) + assert(serializePaymentOnion(onion) == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f917265f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf") - val Success(Sphinx.ParsedPacket(payload0, nextPacket0, sharedSecret0)) = Sphinx.parsePacket(privKeys(0), associatedData, onion.serialize) - val Success(Sphinx.ParsedPacket(payload1, nextPacket1, sharedSecret1)) = Sphinx.parsePacket(privKeys(1), associatedData, nextPacket0.serialize) - val Success(Sphinx.ParsedPacket(payload2, nextPacket2, sharedSecret2)) = Sphinx.parsePacket(privKeys(2), associatedData, nextPacket1.serialize) - val Success(Sphinx.ParsedPacket(payload3, nextPacket3, sharedSecret3)) = Sphinx.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) - val Success(Sphinx.ParsedPacket(payload4, nextPacket4, sharedSecret4)) = Sphinx.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) - assert(Seq(payload0, payload1, payload2, payload3, payload4) == payloads) + val Right(DecryptedPacket(payload0, nextPacket0, sharedSecret0)) = PaymentPacket.peel(privKeys(0), associatedData, onion) + val Right(DecryptedPacket(payload1, nextPacket1, sharedSecret1)) = PaymentPacket.peel(privKeys(1), associatedData, nextPacket0) + val Right(DecryptedPacket(payload2, nextPacket2, sharedSecret2)) = PaymentPacket.peel(privKeys(2), associatedData, nextPacket1) + val Right(DecryptedPacket(payload3, nextPacket3, sharedSecret3)) = PaymentPacket.peel(privKeys(3), associatedData, nextPacket2) + val Right(DecryptedPacket(payload4, nextPacket4, sharedSecret4)) = PaymentPacket.peel(privKeys(4), associatedData, nextPacket3) + assert(Seq(payload0, payload1, payload2, payload3, payload4) == referenceFixedSizePayloads) + assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) val packets = Seq(nextPacket0, nextPacket1, nextPacket2, nextPacket3, nextPacket4) assert(packets(0).hmac == ByteVector32(hex"9b122c79c8aee73ea2cdbc22eca15bbcc9409a4cdd73d2b3fcd4fe26a492d376")) assert(packets(1).hmac == ByteVector32(hex"548e58057ab0a0e6c2d8ad8e855d89f9224279a5652895ea14f60bffb81590eb")) assert(packets(2).hmac == ByteVector32(hex"0daed5f832ef34ea8d0d2cc0699134287a2739c77152d9edc8fe5ccce7ec838f")) assert(packets(3).hmac == ByteVector32(hex"62cc962876e734e089e79eda497077fb411fac5f36afd43329040ecd1e16c6d9")) - // this means that node #4 us the last node + // this means that node #4 is the last node + assert(packets(4).hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) + } + + test("create packet with variable-size payloads (reference test vector)") { + val PacketAndSecrets(onion, sharedSecrets) = PaymentPacket.create(sessionKey, publicKeys, referenceVariableSizePayloads, associatedData) + assert(serializePaymentOnion(onion) == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71a060daf367132b378b3a3883c0e2c0e026b8900b2b5cdbc784e1a3bb913f88a9c50f7d61ab590531cf08000178a333a347f8b4072ed056f820f77774345e183a342ec4729f3d84accf515e88adddb85ecc08daba68404bae9a8e8d7178977d7094a1ae549f89338c0777551f874159eb42d3a59fb9285ad4e24883f27de23942ec966611e99bee1cee503455be9e8e642cef6cef7b9864130f692283f8a973d47a8f1c1726b6e59969385975c766e35737c8d76388b64f748ee7943ffb0e2ee45c57a1abc40762ae598723d21bd184e2b338f68ebff47219357bd19cd7e01e2337b806ef4d717888e129e59cd3dc31e6201ccb2fd6d7499836f37a993262468bcb3a4dcd03a22818aca49c6b7b9b8e9e870045631d8e039b066ff86e0d1b7291f71cefa7264c70404a8e538b566c17ccc5feab231401e6c08a01bd5edfc1aa8e3e533b96e82d1f91118d508924b923531929aea889fcdf050597c681185f336b1da63b0939aa2b7c50b21b5eb7b6ad66c81fab98a3cdf73f658149e7e9ced4edde5d38c9b8f92e16f6b4ab13d7fca6a0e4ecc9f9de611a90da6e99c39551094c56e3196f282c5dffd9fc4b2fc12f3bca8e6fe47eb45fbdd3be21a8a8d200797eae3c9a0497132f92410d804977408494dff49dd3d8bce248e0b74fd9e6f0f7102c25ddfa02bd9ad9f746abbfa337ef811d5345a9e16b60de1767b209645ba40bd1f9a5f75bc04feca9b27c5554be4fe83fac2cb83aa447a817bb85ae966c68b420063833fada375e2f515965e687a45699632902672c654d1d18d7bcbf55e8fa57f63f2da449f8e1e606e8722df081e5f193fc4179feb99ad22819afdeef211f7c54afdba92aeef0c00b7bc2b65a4813c01f907a8377585708f2d4c940a25328e585714c8ded0a9a4d7a6de1027c1cb7a0198cd3db68b58c0704dfd0cfbe624e9cd18cc0ae5d96697bb476708b9ee0403d211e64e0d5a7683a7a9a140c02f0ff1c6e67a302941b4052bdea8a63e70a3ad62c5b89c698f1fd3c7685cb49705096cad702d02d93bcb1c27a409f4c9bddec001205ca4a2740f19b50900be81c7e847f1a863deea8d35701f1355cad8db57b1d4eb2ab4e29587734785abfb46ddede71928213d7d089dfdeda052827f459f1688cc0935bd47e7bcec27427c8376dcce7e22699567c0d145f8a7db33f6758815f1f15f9f7a9760dec4f34ae095edda4c64e9735bdd029c4e32c2ee31ba47ec5e6bdb97813d52dbd15b4e0b7a2c7f790ae64104d99f38c127f0a093288fa34144adb16b8968d4fa7656fcec99de8503dd46d3b03620a71c7cd085364abd30dccf7fbda25a1cdc102600149c9af1c97aa0372cd2e1909f28ac5c686f432b310e79528c9b8b9e8f314c1e74621ce6308ad2278b81d460892e0d9dd38b7c76d58be6dfd10ae7583ee1e7ef5b3f6f78dc60af0950df1b00cc55b6d178ba2e476bea0eaeef49323b83f05804159e7aef4eed4cc60dd07be76f067dfd0bcfb0b806b69ba921336a20c43c832d0cab8fa3ddeb29e3bf07b0d98a112eb07802756235a49d44a8b82a950d84e95e01971f0e106ccb337f07384e21620e0ad39e16ed9edca123226cf55ac44f449eeb53e38a7f27d101806e4823e4efcc887414240ee6826c4a5cb1c6443ad36ebf905a435c1d9054e54173911b17b5b40f60b3d9fd5f12eac54ca1e20191f5f18544d5fd3d665e9bcef96fb44b76110aa64d9db4c86c9513cbdad546538e8aec521fbe83ceac5e74a15629f1ed0b870a1d0d1e5680b6d6100d1bd3f3b9043bd35b8919c4088f1949b8be89e4701eb870f8ed64fafa446c78df3ea") + + val Right(DecryptedPacket(payload0, nextPacket0, sharedSecret0)) = PaymentPacket.peel(privKeys(0), associatedData, onion) + val Right(DecryptedPacket(payload1, nextPacket1, sharedSecret1)) = PaymentPacket.peel(privKeys(1), associatedData, nextPacket0) + val Right(DecryptedPacket(payload2, nextPacket2, sharedSecret2)) = PaymentPacket.peel(privKeys(2), associatedData, nextPacket1) + val Right(DecryptedPacket(payload3, nextPacket3, sharedSecret3)) = PaymentPacket.peel(privKeys(3), associatedData, nextPacket2) + val Right(DecryptedPacket(payload4, nextPacket4, sharedSecret4)) = PaymentPacket.peel(privKeys(4), associatedData, nextPacket3) + assert(Seq(payload0, payload1, payload2, payload3, payload4) == referenceVariableSizePayloads) + assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) + + val packets = Seq(nextPacket0, nextPacket1, nextPacket2, nextPacket3, nextPacket4) + assert(packets(0).hmac == ByteVector32(hex"e125e4acea319d02932a96d7dc065940bdf20d94ab8d5f9b0d816be457ee20d2")) + assert(packets(1).hmac == ByteVector32(hex"f132f8609ca84fc4667dada7d22684c515e7231dabd45e64f92a1277af306c21")) + assert(packets(2).hmac == ByteVector32(hex"4a630bdc56575e956627d7f191e731fabf110ef0044f8f64f4dcea79c2dcb995")) + assert(packets(3).hmac == ByteVector32(hex"a2a89cf333e198b68904ce59ddceb9f989ebfa1ad534fa74ee85e41ff303c3a0")) + assert(packets(4).hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) + } + + test("create packet with variable-size payloads filling the onion") { + val PacketAndSecrets(onion, sharedSecrets) = PaymentPacket.create(sessionKey, publicKeys, variableSizePayloadsFull, associatedData) + assert(serializePaymentOnion(onion) == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866196ef84350c2a76fc232b5d46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7141453e5f8d22b6101810ae541ce499a09b4a9d9f80d1845c8960c85fc6d1a87bf74b2ce49922898e9353fa268086c00ae8b7f718405b72ad3829dbb38c85e02a00427eb4bdbda8fcd42b44708a9efde49cf776b75ebb389bf84d0bfbf58590e510e034572a01e409c309396778760423a8d8754c52e9a01a8f0e271cba5068bab5ee5bd0b5cd98276b0e04d60ba6a0f6bafd75ff41903ab352a1f47586eae3c6c8e437d4308766f71052b46ba2efbd87c0a781e8b3f456300fc7efbefc78ab515338666aed2070e674143c30b520b9cc1782ba8b46454db0d4ce72589cfc2eafb2db452ec98573ad08496483741de5376bfc7357fc6ea629e31236ba6ba7703014959129141a1719788ec83884f2e9151a680e2a96d2bcc67a8a2935aa11acee1f9d04812045b4ae5491220313756b5b9a0a6f867f2a95be1fab14870f04eeab694d9594620632b14ec4b424b495914f3dc587f75cd4582c113bb61e34a0fa7f79f97463be4e3c6fb99516889ed020acee419bb173d38e5ba18a00065e11fd733cf9ae46505dbb4ef70ef2f502601f4f6ee1fdb9d17435e15080e962f24760843f35bac1ac079b694ff7c347c1ed6a87f02b0758fbf00917764716c68ed7d6e6c0e75ccdb6dc7fa59554784b3ad906127ea77a6cdd814662ee7d57a939e28d77b3da47efc072436a3fd7f9c40515af8c4903764301e62b57153a5ca03ff5bb49c7dc8d3b2858100fb4aa5df7a94a271b73a76129445a3ea180d84d19029c003c164db926ed6983e5219028721a294f145e3fcc20915b8a2147efc8b5d508339f64970feee3e2da9b9c9348c1a0a4df7527d0ae3f8ae507a5beb5c73c2016ecf387a3cd8b79df80a8e9412e707cb9c761a0809a84c606a779567f9f0edf685b38c98877e90d02aedd096ed841e50abf2114ce01efbff04788fb280f870eca20c7ec353d5c381903e7d08fc57695fd79c27d43e7bd603a876068d3f1c7f45af99003e5eec7e8d8c91e395320f1fc421ef3552ea033129429383304b760c8f93de342417c3223c2112a623c3514480cdfae8ec15a99abfca71b03a8396f19edc3d5000bcfb77b5544813476b1b521345f4da396db09e783870b97bc2034bd11611db30ed2514438b046f1eb7093eceddfb1e73880786cd7b540a3896eaadd0a0692e4b19439815b5f2ec855ec8ececce889442a64037e956452a3f7b86cb3780b3e316c8dde464bc74a60a85b613f849eb0b29daf81892877bd4be9ba5997fc35544d3c2a00e5e1f45dc925607d952c6a89721bd0b6f6aec03314d667166a5b8b18471403be7018b2479aaef6c7c6c554a50a98b717dff06d50be39fb36dc03e678e0a52fc615be46b223e3bee83fa0c7c47a1f29fb94f1e9eebf6c9ecf8fc79ae847df2effb60d07aba301fc536546ec4899eedb4fec9a9bed79e3a83c4b32757745778e977e485c67c0f12bbc82c0b3bb0f4df0bd13d046fed4446f54cd85bfce55ef781a80e5f63d289d08de001237928c2a4e0c8694d0c1e68cc23f2409f30009019085e831a928e7bc5b00a1f29d25482f7fd0b6dad30e6ef8edc68ddf7db404ea7d11540fc2cee74863d64af4c945457e04b7bea0a5fb8636edadb1e1d6f2630d61062b781c1821f46eddadf269ea1fada829547590081b16bc116e074cae0224a375f2d9ce16e836687c89cd285e3b40f1e59ce2caa3d1d8cf37ee4d5e3abe7ef0afd6ffeb4fd6905677b950894863c828ab8d93519566f69fa3c2129da763bf58d9c4d2837d4d9e13821258f7e7098b34f695a589bd9eb568ba51ee3014b2d3ba1d4cf9ebaed0231ed57ecea7bd918216") + + val Right(DecryptedPacket(payload0, nextPacket0, sharedSecret0)) = PaymentPacket.peel(privKeys(0), associatedData, onion) + val Right(DecryptedPacket(payload1, nextPacket1, sharedSecret1)) = PaymentPacket.peel(privKeys(1), associatedData, nextPacket0) + val Right(DecryptedPacket(payload2, nextPacket2, sharedSecret2)) = PaymentPacket.peel(privKeys(2), associatedData, nextPacket1) + val Right(DecryptedPacket(payload3, nextPacket3, sharedSecret3)) = PaymentPacket.peel(privKeys(3), associatedData, nextPacket2) + val Right(DecryptedPacket(payload4, nextPacket4, sharedSecret4)) = PaymentPacket.peel(privKeys(4), associatedData, nextPacket3) + assert(Seq(payload0, payload1, payload2, payload3, payload4) == variableSizePayloadsFull) + assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) + + val packets = Seq(nextPacket0, nextPacket1, nextPacket2, nextPacket3, nextPacket4) + assert(packets(0).hmac == ByteVector32(hex"859cd694cf604442547246f4fae144f255e71e30cb366b9775f488cac713f0db")) + assert(packets(1).hmac == ByteVector32(hex"259982a8af80bd3b8018443997fa5f74c48b488fff62e531be54b887d53fe0ac")) + assert(packets(2).hmac == ByteVector32(hex"58110c95368305b73ae15d22b884fda0482c60993d3ba4e506e37ff5021efb13")) + assert(packets(3).hmac == ByteVector32(hex"f45e7099e32b8973f54cbfd1f6c48e7e0b90718ad7b00a88e1e98cebeb6d3916")) assert(packets(4).hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) } - test("last node replies with an error message") { - // route: origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 - - // origin build the onion packet - val PacketAndSecrets(packet, sharedSecrets) = makePacket(sessionKey, publicKeys, payloads, associatedData) - - // each node parses and forwards the packet - // node #0 - val Success(ParsedPacket(payload0, packet1, sharedSecret0)) = parsePacket(privKeys(0), associatedData, packet.serialize) - // node #1 - val Success(ParsedPacket(payload1, packet2, sharedSecret1)) = parsePacket(privKeys(1), associatedData, packet1.serialize) - // node #2 - val Success(ParsedPacket(payload2, packet3, sharedSecret2)) = parsePacket(privKeys(2), associatedData, packet2.serialize) - // node #3 - val Success(ParsedPacket(payload3, packet4, sharedSecret3)) = parsePacket(privKeys(3), associatedData, packet3.serialize) - // node #4 - val Success(ParsedPacket(payload4, packet5, sharedSecret4)) = parsePacket(privKeys(4), associatedData, packet4.serialize) - assert(packet5.isLastPacket) - - // node #4 want to reply with an error message - val error = createErrorPacket(sharedSecret4, TemporaryNodeFailure) - assert(error == hex"a5e6bd0c74cb347f10cce367f949098f2457d14c046fd8a22cb96efb30b0fdcda8cb9168b50f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4") - // assert(error == hex"69b1e5a3e05a7b5478e6529cd1749fdd8c66da6f6db42078ff8497ac4e117e91a8cb9168b58f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c2") - // error sent back to 3, 2, 1 and 0 - val error1 = forwardErrorPacket(error, sharedSecret3) - assert(error1 == hex"c49a1ce81680f78f5f2000cda36268de34a3f0a0662f55b4e837c83a8773c22aa081bab1616a0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a9395b3f70b766ebbd4ec2fae2b6955bd8f32684c15abfe8fd3a6261e52650e8807a92158d9f1463261a925e4bfba44bd20b166d532f0017185c3a6ac7957adefe45559e3072c8dc35abeba835a8cb01a71a15c736911126f27d46a36168ca5ef7dccd4e2886212602b181463e0dd30185c96348f9743a02aca8ec27c0b90dca270") - // assert(error1 == hex"08cd44478211b8a4370ab1368b5ffe8c9c92fb830ff4ad6e3b0a316df9d24176a081bab161ea0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a93") - - val error2 = forwardErrorPacket(error1, sharedSecret2) - assert(error2 == hex"a5d3e8634cfe78b2307d87c6d90be6fe7855b4f2cc9b1dfb19e92e4b79103f61ff9ac25f412ddfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd0497e16cc402d779c8d48d0fa6315536ef0660f3f4e1865f5b38ea49c7da4fd959de4e83ff3ab686f059a45c65ba2af4a6a79166aa0f496bf04d06987b6d2ea205bdb0d347718b9aeff5b61dfff344993a275b79717cd815b6ad4c0beb568c4ac9c36ff1c315ec1119a1993c4b61e6eaa0375e0aaf738ac691abd3263bf937e3") - // assert(error2 == hex"6984b0ccd86f37995857363df13670acd064bfd1a540e521cad4d71c07b1bc3dff9ac25f41addfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd") - - val error3 = forwardErrorPacket(error2, sharedSecret1) - assert(error3 == hex"aac3200c4968f56b21f53e5e374e3a2383ad2b1b6501bbcc45abc31e59b26881b7dfadbb56ec8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8949de95e935eed0319cf3cf19ebea61d76ba92532497fcdc9411d06bcd4275094d0a4a3c5d3a945e43305a5a9256e333e1f64dbca5fcd4e03a39b9012d197506e06f29339dfee3331995b21615337ae060233d39befea925cc262873e0530408e6990f1cbd233a150ef7b004ff6166c70c68d9f8c853c1abca640b8660db2921") - // assert(error3 == hex"669478a3ddf9ba4049df8fa51f73ac712b9c20380cda431696963a492713ebddb7dfadbb566c8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8") - - val error4 = forwardErrorPacket(error3, sharedSecret0) - assert(error4 == hex"9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12d") - // assert(error4 == hex"500d8596f76d3045bfdbf99914b98519fe76ea130dc22338c473ab68d74378b13a06a19f891145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366") - - - // origin parses error packet and can see that it comes from node #4 - val Success(ErrorPacket(pubkey, failure)) = parseErrorPacket(error4, sharedSecrets) - assert(pubkey == publicKeys(4)) - assert(failure == TemporaryNodeFailure) + test("create packet with single variable-size payload filling the onion") { + val PacketAndSecrets(onion, _) = PaymentPacket.create(sessionKey, publicKeys.take(1), variableSizeOneHopPayload, associatedData) + assert(serializePaymentOnion(onion) == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661918f5b235c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7141453e5f8d22b6351810ae541ce499a09b4a9d9f80d1845c8960c85fc6d1a87bd24b2cc49922898e9353fa268086c00ae8b7f718405b72ad380cdbb38c85e02a00427eb4bdbda8fcd42b44708a9efde49cf753b75ebb389bf84d0bfbf58590e510e034572a01e409c30939e2e4a090ecc89c371820af54e06e4ad5495d4e58718385cca5414552e078fedf284fdc2cc5c070cba21a6a8d4b77525ddbc9a9fca9b2f29aac5783ee8badd709f81c73ff60556cf2ee623af073b5a84799acc1ca46b764f74b97068c7826cc0579794a540d7a55e49eac26a6930340132e946a983240b0cd1b732e305c1042f580c4b26f140fc1cab3ee6f620958e0979f85eddf586c410ce42e93a4d7c803ead45fc47cf4396d284632314d789e73cf3f534126c63fe244069d9e8a7c4f98e7e530fc588e648ef4e641364981b5377542d5e7a4aaab6d35f6df7d3a9d7ca715213599ee02c4dbea4dc78860febe1d29259c64b59b3333ffdaebbaff4e7b31c27a3791f6bf848a58df7c69bb2b1852d2ad357b9919ffdae570b27dc709fba087273d3a4de9e6a6be66db647fb6a8d1a503b3f481befb96745abf5cc4a6bba0f780d5c7759b9e303a2a6b17eb05b6e660f4c474959db183e1cae060e1639227ee0bca03978a238dc4352ed764da7d4f3ed5337f6d0376dff72615beeeeaaeef79ab93e4bcbf18cd8424eb2b6ad7f33d2b4ffd5ea08372e6ed1d984152df17e04c6f73540988d7dd979e020424a163c271151a255966be7edef42167b8facca633649739bab97572b485658cde409e5d4a0f653f1a5911141634e3d2b6079b19347df66f9820755fd517092dae62fb278b0bafcc7ad682f7921b3a455e0c6369988779e26f0458b31bffd7e4e5bfb31944e80f100b2553c3b616e75be18328dc430f6618d55cd7d0962bb916d26ed4b117c46fa29e0a112c02c36020b34a96762db628fa3490828ec2079962ad816ef20ea0bca78fb2b7f7aedd4c47e375e64294d151ff03083730336dea64934003a27730cc1c7dec5049ddba8188123dd191aa71390d43a49fb792a3da7082efa6cced73f00eccea18145fbc84925349f7b552314ab8ed4c491e392aed3b1f03eb79474c294b42e2eba1528da26450aa592cba7ea22e965c54dff0fd6fdfd6b52b9a0f5f762e27fb0e6c3cd326a1ca1c5973de9be881439f702830affeb0c034c18ac8d5c2f135c964bf69de50d6e99bde88e90321ba843d9753c8f83666105d25fafb1a11ea22d62ef6f1fc34ca4e60c35d69773a104d9a44728c08c20b6314327301a2c400a71e1424c12628cf9f4a67990ade8a2203b0edb96c6082d4673b7309cd52c4b32b02951db2f66c6c72bd6c7eac2b50b83830c75cdfc3d6e9c2b592c45ed5fa5f6ec0da85710b7e1562aea363e28665835791dc574d9a70b2e5e2b9973ab590d45b94d244fc4256926c5a55b01cd0aca21fe5f9c907691fb026d0c56788b03ca3f08db0abb9f901098dde2ec4003568bc3ca27475ff86a7cb0aabd9e5136c5de064d16774584b252024109bb02004dba1fabf9e8277de097a0ab0dc8f6e26fcd4a28fb9d27cd4a2f6b13e276ed259a39e1c7e60f3c32c5cc4c4f96bd981edcb5e2c76a517cdc285aa2ca571d1e3d463ecd7614ae227df17af7445305bd7c661cf7dba658b0adcf36b0084b74a5fa408e272f703770ac5351334709112c5d4e4fe987e0c27b670412696f52b33245c229775da550729938268ee4e7a282e4a60b25dbb28ea8877a5069f819e5d1d31d9140bbc627ff3df267d22e5f0e151db066577845d71b7cd4484089f3f59194963c8f02bd7a637") + + val Right(DecryptedPacket(payload, nextPacket, _)) = PaymentPacket.peel(privKeys(0), associatedData, onion) + assert(payload == variableSizeOneHopPayload.head) + assert(nextPacket.hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) + } + + test("create packet with invalid payload") { + // In this test vector, the payload length (encoded as a varint in the first bytes) isn't equal to the actual + // payload length. + val incorrectVarint = Seq( + hex"fd2a0101234567", + hex"000000000000000000000000000000000000000000000000000000000000000000" + ) + + assertThrows[IllegalArgumentException](PaymentPacket.create(sessionKey, publicKeys.take(2), incorrectVarint, associatedData)) } - test("intermediate node replies with an error message") { - // route: origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 + test("decrypt failure message") { + val sharedSecrets = Seq( + hex"0101010101010101010101010101010101010101010101010101010101010101", + hex"0202020202020202020202020202020202020202020202020202020202020202", + hex"0303030303030303030303030303030303030303030303030303030303030303" + ).map(ByteVector32(_)) + + val expected = DecryptedFailurePacket(publicKeys.head, InvalidOnionKey(ByteVector32.One)) + + val packet1 = FailurePacket.create(sharedSecrets.head, expected.failureMessage) + assert(packet1.length === FailurePacket.PacketLength) + + val Success(decrypted1) = FailurePacket.decrypt(packet1, Seq(0).map(i => (sharedSecrets(i), publicKeys(i)))) + assert(decrypted1 === expected) - // origin build the onion packet - val PacketAndSecrets(packet, sharedSecrets) = makePacket(sessionKey, publicKeys, payloads, associatedData) + val packet2 = FailurePacket.wrap(packet1, sharedSecrets(1)) + assert(packet2.length === FailurePacket.PacketLength) - // each node parses and forwards the packet - // node #0 - val Success(ParsedPacket(payload0, packet1, sharedSecret0)) = parsePacket(privKeys(0), associatedData, packet.serialize) - // node #1 - val Success(ParsedPacket(payload1, packet2, sharedSecret1)) = parsePacket(privKeys(1), associatedData, packet1.serialize) - // node #2 - val Success(ParsedPacket(payload2, packet3, sharedSecret2)) = parsePacket(privKeys(2), associatedData, packet2.serialize) + val Success(decrypted2) = FailurePacket.decrypt(packet2, Seq(1, 0).map(i => (sharedSecrets(i), publicKeys(i)))) + assert(decrypted2 === expected) - // node #2 want to reply with an error message - val error = createErrorPacket(sharedSecret2, InvalidRealm) + val packet3 = FailurePacket.wrap(packet2, sharedSecrets(2)) + assert(packet3.length === FailurePacket.PacketLength) + + val Success(decrypted3) = FailurePacket.decrypt(packet3, Seq(2, 1, 0).map(i => (sharedSecrets(i), publicKeys(i)))) + assert(decrypted3 === expected) + } + + test("decrypt invalid failure message") { + val sharedSecrets = Seq( + hex"0101010101010101010101010101010101010101010101010101010101010101", + hex"0202020202020202020202020202020202020202020202020202020202020202", + hex"0303030303030303030303030303030303030303030303030303030303030303" + ).map(ByteVector32(_)) + + val packet = FailurePacket.wrap( + FailurePacket.wrap( + FailurePacket.create(sharedSecrets.head, InvalidOnionPayload(ByteVector32.Zeroes)), + sharedSecrets(1)), + sharedSecrets(2)) + + assert(FailurePacket.decrypt(packet, Seq(0, 2, 1).map(i => (sharedSecrets(i), publicKeys(i)))).isFailure) + } - // error sent back to 1 and 0 - val error1 = forwardErrorPacket(error, sharedSecret1) - val error2 = forwardErrorPacket(error1, sharedSecret0) + test("last node replies with a failure message (reference test vector)") { + for (payloads <- Seq(referenceFixedSizePayloads, referenceVariableSizePayloads, variableSizePayloadsFull)) { + // route: origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 + + // origin build the onion packet + val PacketAndSecrets(packet, sharedSecrets) = PaymentPacket.create(sessionKey, publicKeys, payloads, associatedData) + + // each node parses and forwards the packet + // node #0 + val Right(DecryptedPacket(_, packet1, sharedSecret0)) = PaymentPacket.peel(privKeys(0), associatedData, packet) + // node #1 + val Right(DecryptedPacket(_, packet2, sharedSecret1)) = PaymentPacket.peel(privKeys(1), associatedData, packet1) + // node #2 + val Right(DecryptedPacket(_, packet3, sharedSecret2)) = PaymentPacket.peel(privKeys(2), associatedData, packet2) + // node #3 + val Right(DecryptedPacket(_, packet4, sharedSecret3)) = PaymentPacket.peel(privKeys(3), associatedData, packet3) + // node #4 + val Right(lastPacket@DecryptedPacket(_, _, sharedSecret4)) = PaymentPacket.peel(privKeys(4), associatedData, packet4) + assert(lastPacket.isLastPacket) + + // node #4 want to reply with an error message + val error = FailurePacket.create(sharedSecret4, TemporaryNodeFailure) + assert(error === hex"a5e6bd0c74cb347f10cce367f949098f2457d14c046fd8a22cb96efb30b0fdcda8cb9168b50f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4") + // error sent back to 3, 2, 1 and 0 + val error1 = FailurePacket.wrap(error, sharedSecret3) + assert(error1 === hex"c49a1ce81680f78f5f2000cda36268de34a3f0a0662f55b4e837c83a8773c22aa081bab1616a0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a9395b3f70b766ebbd4ec2fae2b6955bd8f32684c15abfe8fd3a6261e52650e8807a92158d9f1463261a925e4bfba44bd20b166d532f0017185c3a6ac7957adefe45559e3072c8dc35abeba835a8cb01a71a15c736911126f27d46a36168ca5ef7dccd4e2886212602b181463e0dd30185c96348f9743a02aca8ec27c0b90dca270") + + val error2 = FailurePacket.wrap(error1, sharedSecret2) + assert(error2 === hex"a5d3e8634cfe78b2307d87c6d90be6fe7855b4f2cc9b1dfb19e92e4b79103f61ff9ac25f412ddfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd0497e16cc402d779c8d48d0fa6315536ef0660f3f4e1865f5b38ea49c7da4fd959de4e83ff3ab686f059a45c65ba2af4a6a79166aa0f496bf04d06987b6d2ea205bdb0d347718b9aeff5b61dfff344993a275b79717cd815b6ad4c0beb568c4ac9c36ff1c315ec1119a1993c4b61e6eaa0375e0aaf738ac691abd3263bf937e3") + + val error3 = FailurePacket.wrap(error2, sharedSecret1) + assert(error3 === hex"aac3200c4968f56b21f53e5e374e3a2383ad2b1b6501bbcc45abc31e59b26881b7dfadbb56ec8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8949de95e935eed0319cf3cf19ebea61d76ba92532497fcdc9411d06bcd4275094d0a4a3c5d3a945e43305a5a9256e333e1f64dbca5fcd4e03a39b9012d197506e06f29339dfee3331995b21615337ae060233d39befea925cc262873e0530408e6990f1cbd233a150ef7b004ff6166c70c68d9f8c853c1abca640b8660db2921") + + val error4 = FailurePacket.wrap(error3, sharedSecret0) + assert(error4 === hex"9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12d") + + // origin parses error packet and can see that it comes from node #4 + val Success(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error4, sharedSecrets) + assert(pubkey === publicKeys(4)) + assert(failure === TemporaryNodeFailure) + } + } - // origin parses error packet and can see that it comes from node #2 - val Success(ErrorPacket(pubkey, failure)) = parseErrorPacket(error2, sharedSecrets) - assert(pubkey == publicKeys(2)) - assert(failure == InvalidRealm) + test("intermediate node replies with a failure message (reference test vector)") { + for (payloads <- Seq(referenceFixedSizePayloads, referenceVariableSizePayloads, variableSizePayloadsFull)) { + // route: origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 + + // origin build the onion packet + val PacketAndSecrets(packet, sharedSecrets) = PaymentPacket.create(sessionKey, publicKeys, payloads, associatedData) + + // each node parses and forwards the packet + // node #0 + val Right(DecryptedPacket(_, packet1, sharedSecret0)) = PaymentPacket.peel(privKeys(0), associatedData, packet) + // node #1 + val Right(DecryptedPacket(_, packet2, sharedSecret1)) = PaymentPacket.peel(privKeys(1), associatedData, packet1) + // node #2 + val Right(DecryptedPacket(_, _, sharedSecret2)) = PaymentPacket.peel(privKeys(2), associatedData, packet2) + + // node #2 want to reply with an error message + val error = FailurePacket.create(sharedSecret2, InvalidRealm) + + // error sent back to 1 and 0 + val error1 = FailurePacket.wrap(error, sharedSecret1) + val error2 = FailurePacket.wrap(error1, sharedSecret0) + + // origin parses error packet and can see that it comes from node #2 + val Success(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error2, sharedSecrets) + assert(pubkey === publicKeys(2)) + assert(failure === InvalidRealm) + } } } object SphinxSpec { + + def serializePaymentOnion(onion: OnionRoutingPacket): ByteVector = + OnionCodecs.paymentOnionPacketCodec.encode(onion).require.toByteVector + val privKeys = Seq( PrivateKey(hex"4141414141414141414141414141414141414141414141414141414141414141"), PrivateKey(hex"4242424242424242424242424242424242424242424242424242424242424242"), @@ -190,12 +351,42 @@ object SphinxSpec { )) val sessionKey: PrivateKey = PrivateKey(hex"4141414141414141414141414141414141414141414141414141414141414141") - val payloads = Seq( + + // This test vector uses payloads with a fixed size. + // origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 + val referenceFixedSizePayloads = Seq( hex"000000000000000000000000000000000000000000000000000000000000000000", hex"000101010101010101000000000000000100000001000000000000000000000000", hex"000202020202020202000000000000000200000002000000000000000000000000", hex"000303030303030303000000000000000300000003000000000000000000000000", - hex"000404040404040404000000000000000400000004000000000000000000000000") + hex"000404040404040404000000000000000400000004000000000000000000000000" + ) + + // This test vector uses variable-size payloads intertwined with fixed-size payloads. + // origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 + val referenceVariableSizePayloads = Seq( + hex"000000000000000000000000000000000000000000000000000000000000000000", + hex"140101010101010101000000000000000100000001", + hex"fd0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", + hex"140303030303030303000000000000000300000003", + hex"000404040404040404000000000000000400000004000000000000000000000000" + ) + + // This test vector uses variable-sized payloads and fills the whole onion packet. + // origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 + val variableSizePayloadsFull = Seq( + hex"8b09000000000000000030000000000000000000000000000000000000000000000000000000000025000000000000000000000000000000000000000000000000250000000000000000000000000000000000000000000000002500000000000000000000000000000000000000000000000025000000000000000000000000000000000000000000000000", + hex"fd012a08000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000200000000000000000000000000000000000000020000000000000000000000000000000000000002000000000000000000000000000000000000000200000000000000000000000000000000000000020000000000000000000000000000000000000002000000000000000000000000000000000000000200000000000000000000000000000000000000020000000000000000000000000000000000000002000000000000000000000000000000000000000", + hex"620800000000000000900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + hex"fc120000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000", + hex"fd01582200000000000000000000000000000000000000000022000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000" + ) + + // This test vector uses a single variable-sized payload filling the whole onion payload. + // origin -> recipient + val variableSizeOneHopPayload = Seq( + hex"fd04f16500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ) val associatedData = ByteVector32(hex"4242424242424242424242424242424242424242424242424242424242424242") } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index 53b1fc33aa..8b2c2e79da 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -31,7 +31,7 @@ import fr.acinq.eclair.blockchain.{Watch, WatchConfirmed} import fr.acinq.eclair.channel.Channel.{BroadcastChannelUpdate, PeriodicRefresh} import fr.acinq.eclair.channel.Register.{Forward, ForwardShortId} import fr.acinq.eclair.channel._ -import fr.acinq.eclair.crypto.Sphinx.ErrorPacket +import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket import fr.acinq.eclair.io.Peer.{Disconnect, PeerRoutingMessage} import fr.acinq.eclair.io.{NodeURI, Peer} import fr.acinq.eclair.payment.PaymentLifecycle.{State => _, _} @@ -343,7 +343,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService assert(failed.id == paymentId) assert(failed.paymentHash === pr.paymentHash) assert(failed.failures.size === 1) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(100000000L))) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(100000000L))) } test("send an HTLC A->D with a lower amount than requested") { @@ -363,7 +363,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService assert(failed.id == paymentId) assert(failed.paymentHash === pr.paymentHash) assert(failed.failures.size === 1) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(100000000L))) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(100000000L))) } test("send an HTLC A->D with too much overpayment") { @@ -383,7 +383,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService assert(paymentId == failed.id) assert(failed.paymentHash === pr.paymentHash) assert(failed.failures.size === 1) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(600000000L))) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(600000000L))) } test("send an HTLC A->D with a reasonable overpayment") { @@ -651,7 +651,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService assert(failed.id == paymentId) assert(failed.paymentHash === paymentHash) assert(failed.failures.size === 1) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure)) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure)) // we then generate enough blocks to confirm all delayed transactions sender.send(bitcoincli, BitcoinReq("generate", 150)) sender.expectMsgType[JValue](10 seconds) @@ -717,7 +717,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService assert(failed.id == paymentId) assert(failed.paymentHash === paymentHash) assert(failed.failures.size === 1) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure)) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure)) // we then generate enough blocks to confirm all delayed transactions sender.send(bitcoincli, BitcoinReq("generate", 145)) sender.expectMsgType[JValue](10 seconds) 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 1f1f86f200..2c72623576 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 @@ -22,7 +22,7 @@ import java.util.concurrent.CountDownLatch import akka.actor.{Actor, ActorLogging, ActorRef, Stash} import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.eclair.TestUtils +import fr.acinq.eclair.{TestConstants, TestUtils} import fr.acinq.eclair.channel._ import fr.acinq.eclair.transactions.{IN, OUT} @@ -57,7 +57,7 @@ class SynchronizationPipe(latch: CountDownLatch) extends Actor with ActorLogging script match { case offer(x, amount, rhash) :: rest => - resolve(x) ! CMD_ADD_HTLC(amount.toInt, ByteVector32.fromValidHex(rhash), 144, upstream = Left(UUID.randomUUID())) + resolve(x) ! CMD_ADD_HTLC(amount.toInt, ByteVector32.fromValidHex(rhash), 144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) exec(rest, a, b) case fulfill(x, id, r) :: rest => resolve(x) ! CMD_FULFILL_HTLC(id.toInt, ByteVector32.fromValidHex(r)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/HtlcReaperSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/HtlcReaperSpec.scala index c1197b1214..4378f5cf1d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/HtlcReaperSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/HtlcReaperSpec.scala @@ -19,10 +19,9 @@ package fr.acinq.eclair.io import akka.actor.{ActorSystem, Props} import akka.testkit.{TestKit, TestProbe} import fr.acinq.eclair.channel._ -import fr.acinq.eclair.randomBytes32 +import fr.acinq.eclair.{TestConstants, randomBytes32} import fr.acinq.eclair.wire.{ChannelCodecsSpec, TemporaryNodeFailure, UpdateAddHtlc} import org.scalatest.FunSuiteLike -import scodec.bits.ByteVector import scala.concurrent.duration._ @@ -37,11 +36,11 @@ class HtlcReaperSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val data = ChannelCodecsSpec.normal // assuming that data has incoming htlcs 0 and 1, we don't care about the amount/payment_hash/onion fields - val add0 = UpdateAddHtlc(data.channelId, 0, 20000, randomBytes32, 100, ByteVector.empty) - val add1 = UpdateAddHtlc(data.channelId, 1, 30000, randomBytes32, 100, ByteVector.empty) + val add0 = UpdateAddHtlc(data.channelId, 0, 20000, randomBytes32, 100, TestConstants.emptyOnionPacket) + val add1 = UpdateAddHtlc(data.channelId, 1, 30000, randomBytes32, 100, TestConstants.emptyOnionPacket) // unrelated htlc - val add99 = UpdateAddHtlc(randomBytes32, 0, 12345678, randomBytes32, 100, ByteVector.empty) + val add99 = UpdateAddHtlc(randomBytes32, 0, 12345678, randomBytes32, 100, TestConstants.emptyOnionPacket) val brokenHtlcs = Seq(add0, add1, add99) val brokenHtlcKiller = system.actorOf(Props[HtlcReaper], name = "htlc-reaper") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala index 8eab3c0e7f..ccc65efe0b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala @@ -19,14 +19,12 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.{Block, ByteVector32} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.channel.{CMD_ADD_HTLC, CMD_FAIL_HTLC} -import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.Relayer.{OutgoingChannel, RelayFailure, RelayPayload, RelaySuccess} import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{ShortChannelId, randomBytes32, randomKey} +import fr.acinq.eclair.{ShortChannelId, TestConstants, randomBytes32, randomKey} import fr.acinq.eclair.payment.HtlcGenerationSpec.makeCommitments import org.scalatest.FunSuite -import scodec.bits.ByteVector import scala.collection.mutable @@ -40,9 +38,9 @@ class ChannelSelectionSpec extends FunSuite { test("convert to CMD_FAIL_HTLC/CMD_ADD_HTLC") { val relayPayload = RelayPayload( - add = UpdateAddHtlc(randomBytes32, 42, 1000000, randomBytes32, 70, ByteVector.empty), + add = UpdateAddHtlc(randomBytes32, 42, 1000000, randomBytes32, 70, TestConstants.emptyOnionPacket), payload = PerHopPayload(ShortChannelId(12345), amtToForward = 998900, outgoingCltvValue = 60), - nextPacket = Sphinx.LAST_PACKET // just a placeholder + nextPacket = TestConstants.emptyOnionPacket // just a placeholder ) val channelUpdate = dummyUpdate(ShortChannelId(12345), 10, 100, 1000, 100, 10000000, true) @@ -50,7 +48,7 @@ class ChannelSelectionSpec extends FunSuite { implicit val log = akka.event.NoLogging // nominal case - assert(Relayer.relayOrFail(relayPayload, Some(channelUpdate)) === RelaySuccess(ShortChannelId(12345), CMD_ADD_HTLC(relayPayload.payload.amtToForward, relayPayload.add.paymentHash, relayPayload.payload.outgoingCltvValue, relayPayload.nextPacket.serialize, upstream = Right(relayPayload.add), commit = true))) + assert(Relayer.relayOrFail(relayPayload, Some(channelUpdate)) === RelaySuccess(ShortChannelId(12345), CMD_ADD_HTLC(relayPayload.payload.amtToForward, relayPayload.add.paymentHash, relayPayload.payload.outgoingCltvValue, relayPayload.nextPacket, upstream = Right(relayPayload.add), commit = true))) // no channel_update assert(Relayer.relayOrFail(relayPayload, channelUpdate_opt = None) === RelayFailure(CMD_FAIL_HTLC(relayPayload.add.id, Right(UnknownNextPeer), commit = true))) // channel disabled @@ -67,15 +65,15 @@ class ChannelSelectionSpec extends FunSuite { assert(Relayer.relayOrFail(relayPayload_insufficientfee, Some(channelUpdate)) === RelayFailure(CMD_FAIL_HTLC(relayPayload.add.id, Right(FeeInsufficient(relayPayload_insufficientfee.add.amountMsat, channelUpdate)), commit = true))) // note that a generous fee is ok! val relayPayload_highfee = relayPayload.copy(payload = relayPayload.payload.copy(amtToForward = 900000)) - assert(Relayer.relayOrFail(relayPayload_highfee, Some(channelUpdate)) === RelaySuccess(ShortChannelId(12345), CMD_ADD_HTLC(relayPayload_highfee.payload.amtToForward, relayPayload_highfee.add.paymentHash, relayPayload_highfee.payload.outgoingCltvValue, relayPayload_highfee.nextPacket.serialize, upstream = Right(relayPayload.add), commit = true))) + assert(Relayer.relayOrFail(relayPayload_highfee, Some(channelUpdate)) === RelaySuccess(ShortChannelId(12345), CMD_ADD_HTLC(relayPayload_highfee.payload.amtToForward, relayPayload_highfee.add.paymentHash, relayPayload_highfee.payload.outgoingCltvValue, relayPayload_highfee.nextPacket, upstream = Right(relayPayload.add), commit = true))) } test("channel selection") { val relayPayload = RelayPayload( - add = UpdateAddHtlc(randomBytes32, 42, 1000000, randomBytes32, 70, ByteVector.empty), + add = UpdateAddHtlc(randomBytes32, 42, 1000000, randomBytes32, 70, TestConstants.emptyOnionPacket), payload = PerHopPayload(ShortChannelId(12345), amtToForward = 998900, outgoingCltvValue = 60), - nextPacket = Sphinx.LAST_PACKET // just a placeholder + nextPacket = TestConstants.emptyOnionPacket // just a placeholder ) val (a, b) = (randomKey.publicKey, randomKey.publicKey) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala index 6131b590d2..9114a8e418 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala @@ -22,16 +22,14 @@ import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, DeterministicWallet} import fr.acinq.eclair.channel.{Channel, ChannelVersion, Commitments} import fr.acinq.eclair.crypto.Sphinx -import fr.acinq.eclair.crypto.Sphinx.{PacketAndSecrets, ParsedPacket} +import fr.acinq.eclair.crypto.Sphinx.{DecryptedPacket, PacketAndSecrets} import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.router.Hop -import fr.acinq.eclair.wire.{ChannelUpdate, LightningMessageCodecs, PerHopPayload} +import fr.acinq.eclair.wire.{ChannelUpdate, OnionCodecs, PerHopPayload} import fr.acinq.eclair.{ShortChannelId, TestConstants, nodeFee, randomBytes32} import org.scalatest.FunSuite import scodec.bits.ByteVector -import scala.util.Success - /** * Created by PM on 31/05/2016. */ @@ -68,30 +66,30 @@ class HtlcGenerationSpec extends FunSuite { val (_, _, payloads) = buildPayloads(finalAmountMsat, finalExpiry, hops.drop(1)) val nodes = hops.map(_.nextNodeId) val PacketAndSecrets(packet_b, _) = buildOnion(nodes, payloads, paymentHash) - assert(packet_b.serialize.size === Sphinx.PacketLength) + assert(packet_b.payload.length === Sphinx.PaymentPacket.PayloadLength) // let's peel the onion - val Success(ParsedPacket(bin_b, packet_c, _)) = Sphinx.parsePacket(priv_b.privateKey, paymentHash, packet_b.serialize) - val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(bin_b.toBitVector).require.value - assert(packet_c.serialize.size === Sphinx.PacketLength) + val Right(DecryptedPacket(bin_b, packet_c, _)) = Sphinx.PaymentPacket.peel(priv_b.privateKey, paymentHash, packet_b) + val payload_b = OnionCodecs.perHopPayloadCodec.decode(bin_b.toBitVector).require.value + assert(packet_c.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_b.amtToForward === amount_bc) assert(payload_b.outgoingCltvValue === expiry_bc) - val Success(ParsedPacket(bin_c, packet_d, _)) = Sphinx.parsePacket(priv_c.privateKey, paymentHash, packet_c.serialize) - val payload_c = LightningMessageCodecs.perHopPayloadCodec.decode(bin_c.toBitVector).require.value - assert(packet_d.serialize.size === Sphinx.PacketLength) + val Right(DecryptedPacket(bin_c, packet_d, _)) = Sphinx.PaymentPacket.peel(priv_c.privateKey, paymentHash, packet_c) + val payload_c = OnionCodecs.perHopPayloadCodec.decode(bin_c.toBitVector).require.value + assert(packet_d.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_c.amtToForward === amount_cd) assert(payload_c.outgoingCltvValue === expiry_cd) - val Success(ParsedPacket(bin_d, packet_e, _)) = Sphinx.parsePacket(priv_d.privateKey, paymentHash, packet_d.serialize) - val payload_d = LightningMessageCodecs.perHopPayloadCodec.decode(bin_d.toBitVector).require.value - assert(packet_e.serialize.size === Sphinx.PacketLength) + val Right(DecryptedPacket(bin_d, packet_e, _)) = Sphinx.PaymentPacket.peel(priv_d.privateKey, paymentHash, packet_d) + val payload_d = OnionCodecs.perHopPayloadCodec.decode(bin_d.toBitVector).require.value + assert(packet_e.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_d.amtToForward === amount_de) assert(payload_d.outgoingCltvValue === expiry_de) - val Success(ParsedPacket(bin_e, packet_random, _)) = Sphinx.parsePacket(priv_e.privateKey, paymentHash, packet_e.serialize) - val payload_e = LightningMessageCodecs.perHopPayloadCodec.decode(bin_e.toBitVector).require.value - assert(packet_random.serialize.size === Sphinx.PacketLength) + val Right(DecryptedPacket(bin_e, packet_random, _)) = Sphinx.PaymentPacket.peel(priv_e.privateKey, paymentHash, packet_e) + val payload_e = OnionCodecs.perHopPayloadCodec.decode(bin_e.toBitVector).require.value + assert(packet_random.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_e.amtToForward === finalAmountMsat) assert(payload_e.outgoingCltvValue === finalExpiry) } @@ -103,30 +101,30 @@ class HtlcGenerationSpec extends FunSuite { assert(add.amountMsat > finalAmountMsat) assert(add.cltvExpiry === finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta) assert(add.paymentHash === paymentHash) - assert(add.onion.length === Sphinx.PacketLength) + assert(add.onion.payload.length === Sphinx.PaymentPacket.PayloadLength) // let's peel the onion - val Success(ParsedPacket(bin_b, packet_c, _)) = Sphinx.parsePacket(priv_b.privateKey, paymentHash, add.onion) - val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(bin_b.toBitVector).require.value - assert(packet_c.serialize.size === Sphinx.PacketLength) + val Right(DecryptedPacket(bin_b, packet_c, _)) = Sphinx.PaymentPacket.peel(priv_b.privateKey, paymentHash, add.onion) + val payload_b = OnionCodecs.perHopPayloadCodec.decode(bin_b.toBitVector).require.value + assert(packet_c.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_b.amtToForward === amount_bc) assert(payload_b.outgoingCltvValue === expiry_bc) - val Success(ParsedPacket(bin_c, packet_d, _)) = Sphinx.parsePacket(priv_c.privateKey, paymentHash, packet_c.serialize) - val payload_c = LightningMessageCodecs.perHopPayloadCodec.decode(bin_c.toBitVector).require.value - assert(packet_d.serialize.size === Sphinx.PacketLength) + val Right(DecryptedPacket(bin_c, packet_d, _)) = Sphinx.PaymentPacket.peel(priv_c.privateKey, paymentHash, packet_c) + val payload_c = OnionCodecs.perHopPayloadCodec.decode(bin_c.toBitVector).require.value + assert(packet_d.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_c.amtToForward === amount_cd) assert(payload_c.outgoingCltvValue === expiry_cd) - val Success(ParsedPacket(bin_d, packet_e, _)) = Sphinx.parsePacket(priv_d.privateKey, paymentHash, packet_d.serialize) - val payload_d = LightningMessageCodecs.perHopPayloadCodec.decode(bin_d.toBitVector).require.value - assert(packet_e.serialize.size === Sphinx.PacketLength) + val Right(DecryptedPacket(bin_d, packet_e, _)) = Sphinx.PaymentPacket.peel(priv_d.privateKey, paymentHash, packet_d) + val payload_d = OnionCodecs.perHopPayloadCodec.decode(bin_d.toBitVector).require.value + assert(packet_e.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_d.amtToForward === amount_de) assert(payload_d.outgoingCltvValue === expiry_de) - val Success(ParsedPacket(bin_e, packet_random, _)) = Sphinx.parsePacket(priv_e.privateKey, paymentHash, packet_e.serialize) - val payload_e = LightningMessageCodecs.perHopPayloadCodec.decode(bin_e.toBitVector).require.value - assert(packet_random.serialize.size === Sphinx.PacketLength) + val Right(DecryptedPacket(bin_e, packet_random, _)) = Sphinx.PaymentPacket.peel(priv_e.privateKey, paymentHash, packet_e) + val payload_e = OnionCodecs.perHopPayloadCodec.decode(bin_e.toBitVector).require.value + assert(packet_random.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_e.amtToForward === finalAmountMsat) assert(payload_e.outgoingCltvValue === finalExpiry) } @@ -137,12 +135,12 @@ class HtlcGenerationSpec extends FunSuite { assert(add.amountMsat === finalAmountMsat) assert(add.cltvExpiry === finalExpiry) assert(add.paymentHash === paymentHash) - assert(add.onion.size === Sphinx.PacketLength) + assert(add.onion.payload.length === Sphinx.PaymentPacket.PayloadLength) // let's peel the onion - val Success(ParsedPacket(bin_b, packet_random, _)) = Sphinx.parsePacket(priv_b.privateKey, paymentHash, add.onion) - val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(bin_b.toBitVector).require.value - assert(packet_random.serialize.size === Sphinx.PacketLength) + val Right(DecryptedPacket(bin_b, packet_random, _)) = Sphinx.PaymentPacket.peel(priv_b.privateKey, paymentHash, add.onion) + val payload_b = OnionCodecs.perHopPayloadCodec.decode(bin_b.toBitVector).require.value + assert(packet_random.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_b.amtToForward === finalAmountMsat) assert(payload_b.outgoingCltvValue === finalExpiry) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala index 9edf987b85..3915d24895 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala @@ -17,15 +17,15 @@ package fr.acinq.eclair.payment import akka.actor.Status.Failure -import akka.actor.{ActorSystem, Status} +import akka.actor.ActorSystem import akka.testkit.{TestActorRef, TestKit, TestProbe} -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} +import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC} -import fr.acinq.eclair.payment.PaymentLifecycle.{ReceivePayment} +import fr.acinq.eclair.payment.PaymentLifecycle.ReceivePayment import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.wire.{FinalExpiryTooSoon, UpdateAddHtlc} -import fr.acinq.eclair.{Globals, ShortChannelId, randomKey} +import fr.acinq.eclair.{Globals, ShortChannelId, TestConstants, randomKey} import org.scalatest.FunSuiteLike import scodec.bits.ByteVector @@ -54,7 +54,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike assert(nodeParams.db.payments.getPendingPaymentRequestAndPreimage(pr.paymentHash).isDefined) assert(!nodeParams.db.payments.getPendingPaymentRequestAndPreimage(pr.paymentHash).get._2.isExpired) - val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, TestConstants.emptyOnionPacket) sender.send(handler, add) sender.expectMsgType[CMD_FULFILL_HTLC] @@ -68,7 +68,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val pr = sender.expectMsgType[PaymentRequest] assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).isEmpty) - val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, TestConstants.emptyOnionPacket) sender.send(handler, add) sender.expectMsgType[CMD_FULFILL_HTLC] val paymentRelayed = eventListener.expectMsgType[PaymentReceived] @@ -81,7 +81,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val pr = sender.expectMsgType[PaymentRequest] assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).isEmpty) - val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, cltvExpiry = Globals.blockCount.get() + 3, ByteVector.empty) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, cltvExpiry = Globals.blockCount.get() + 3, TestConstants.emptyOnionPacket) sender.send(handler, add) assert(sender.expectMsgType[CMD_FAIL_HTLC].reason == Right(FinalExpiryTooSoon)) eventListener.expectNoMsg(300 milliseconds) @@ -164,7 +164,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike sender.send(handler, ReceivePayment(Some(amountMsat), "some desc", expirySeconds_opt = Some(0))) val pr = sender.expectMsgType[PaymentRequest] - val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, TestConstants.emptyOnionPacket) sender.send(handler, add) sender.expectMsgType[CMD_FAIL_HTLC] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index deb2a51441..79192309bf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -26,8 +26,7 @@ import fr.acinq.bitcoin.{Block, ByteVector32, MilliSatoshi, Satoshi, Transaction import fr.acinq.eclair.blockchain.{UtxoStatus, ValidateRequest, ValidateResult, WatchSpentBasic} import fr.acinq.eclair.channel.Register.ForwardShortId import fr.acinq.eclair.channel.{AddHtlcFailed, ChannelUnavailable} -import fr.acinq.eclair.crypto.{KeyManager, Sphinx} -import fr.acinq.eclair.crypto.Sphinx.ErrorPacket +import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.db.OutgoingPaymentStatus import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.payment.PaymentLifecycle._ @@ -220,7 +219,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val WaitingForComplete(_, _, cmd1, Nil, _, _, _, hops) = paymentFSM.stateData relayer.expectMsg(ForwardShortId(channelId_ab, cmd1)) - sender.send(paymentFSM, UpdateFailMalformedHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash, FailureMessageCodecs.BADONION)) + sender.send(paymentFSM, UpdateFailMalformedHtlc(ByteVector32.Zeroes, 0, randomBytes32, FailureMessageCodecs.BADONION)) // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) @@ -252,7 +251,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val failure = TemporaryChannelFailure(channelUpdate_bc) relayer.expectMsg(ForwardShortId(channelId_ab, cmd1)) - sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.createErrorPacket(sharedSecrets1.head._1, failure))) + sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))) // payment lifecycle will ask the router to temporarily exclude this channel from its route calculations routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(channelUpdate_bc.shortChannelId, b, c))) @@ -263,7 +262,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) // we allow 2 tries, so we send a 2nd request to the router - sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) + sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) } test("payment failed (Update)") { fixture => @@ -295,7 +294,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, cltvExpiryDelta = 42, htlcMinimumMsat = channelUpdate_bc.htlcMinimumMsat, feeBaseMsat = channelUpdate_bc.feeBaseMsat, feeProportionalMillionths = channelUpdate_bc.feeProportionalMillionths, htlcMaximumMsat = channelUpdate_bc.htlcMaximumMsat.get) val failure = IncorrectCltvExpiry(5, channelUpdate_bc_modified) // and node replies with a failure containing a new channel update - sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.createErrorPacket(sharedSecrets1.head._1, failure))) + sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))) // payment lifecycle forwards the embedded channelUpdate to the router routerForwarder.expectMsg(channelUpdate_bc_modified) @@ -312,7 +311,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val channelUpdate_bc_modified_2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, cltvExpiryDelta = 43, htlcMinimumMsat = channelUpdate_bc.htlcMinimumMsat, feeBaseMsat = channelUpdate_bc.feeBaseMsat, feeProportionalMillionths = channelUpdate_bc.feeProportionalMillionths, htlcMaximumMsat = channelUpdate_bc.htlcMaximumMsat.get) val failure2 = IncorrectCltvExpiry(5, channelUpdate_bc_modified_2) // and node replies with a failure containing a new channel update - sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.createErrorPacket(sharedSecrets2.head._1, failure2))) + sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets2.head._1, failure2))) // this time the payment lifecycle will ask the router to temporarily exclude this channel from its route calculations routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(channelUpdate_bc.shortChannelId, b, c))) @@ -324,7 +323,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(router) // this time the router can't find a route: game over - sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: RemoteFailure(hops2, ErrorPacket(b, failure2)) :: LocalFailure(RouteNotFound) :: Nil)) + sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, Sphinx.DecryptedFailurePacket(b, failure)) :: RemoteFailure(hops2, Sphinx.DecryptedFailurePacket(b, failure2)) :: LocalFailure(RouteNotFound) :: Nil)) awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.FAILED)) } @@ -355,7 +354,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val failure = PermanentChannelFailure relayer.expectMsg(ForwardShortId(channelId_ab, cmd1)) - sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.createErrorPacket(sharedSecrets1.head._1, failure))) + sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))) // payment lifecycle forwards the embedded channelUpdate to the router awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) @@ -363,7 +362,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(router) // we allow 2 tries, so we send a 2nd request to the router, which won't find another route - sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) + sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.FAILED)) } @@ -452,8 +451,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { } test("filter errors properly") { _ => - val failures = LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, ErrorPacket(a, TemporaryNodeFailure)) :: LocalFailure(AddHtlcFailed(ByteVector32.Zeroes, ByteVector32.Zeroes, ChannelUnavailable(ByteVector32.Zeroes), Local(UUID.randomUUID(), None), None, None)) :: LocalFailure(RouteNotFound) :: Nil + val failures = LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)) :: LocalFailure(AddHtlcFailed(ByteVector32.Zeroes, ByteVector32.Zeroes, ChannelUnavailable(ByteVector32.Zeroes), Local(UUID.randomUUID(), None), None, None)) :: LocalFailure(RouteNotFound) :: Nil val filtered = PaymentLifecycle.transformForUser(failures) - assert(filtered == LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, ErrorPacket(a, TemporaryNodeFailure)) :: LocalFailure(ChannelUnavailable(ByteVector32.Zeroes)) :: Nil) + assert(filtered == LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)) :: LocalFailure(ChannelUnavailable(ByteVector32.Zeroes)) :: Nil) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala index fd481b5ba2..c6f6a27d41 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala @@ -20,7 +20,7 @@ import java.util.UUID import akka.actor.{ActorRef, Status} import akka.testkit.TestProbe -import fr.acinq.bitcoin.{ByteVector32, Crypto, MilliSatoshi} +import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.PaymentLifecycle.buildCommand @@ -48,7 +48,6 @@ class RelayerSpec extends TestkitBaseClass { val register = TestProbe() val paymentHandler = TestProbe() // we are node B in the route A -> B -> C -> .... - //val relayer = system.actorOf(Relayer.props(TestConstants.Bob.nodeParams.copy(nodeKey = priv_b), register.ref, paymentHandler.ref)) val relayer = system.actorOf(Relayer.props(TestConstants.Bob.nodeParams, register.ref, paymentHandler.ref)) withFixture(test.toNoArgTest(FixtureParam(relayer, register, paymentHandler))) } @@ -192,10 +191,10 @@ class RelayerSpec extends TestkitBaseClass { val (cmd1, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, randomBytes32, hops) val add_ab1 = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd1.amountMsat, cmd1.paymentHash, cmd1.cltvExpiry, cmd1.onion) - sender.send(relayer, ForwardAdd(add_ab)) + sender.send(relayer, ForwardAdd(add_ab1)) val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message - assert(fail.id === add_ab.id) + assert(fail.id === add_ab1.id) assert(fail.reason === Right(UnknownNextPeer)) register.expectNoMsg(100 millis) @@ -229,15 +228,16 @@ class RelayerSpec extends TestkitBaseClass { // we use this to build a valid onion val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) - // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, ByteVector.fill(Sphinx.PacketLength)(0)) + // and then manually build an htlc with an invalid onion (hmac) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion.copy(hmac = cmd.onion.hmac.reverse)) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) sender.send(relayer, ForwardAdd(add_ab)) val fail = register.expectMsgType[Register.Forward[CMD_FAIL_MALFORMED_HTLC]].message assert(fail.id === add_ab.id) - assert(fail.onionHash == Crypto.sha256(add_ab.onionRoutingPacket)) + assert(fail.onionHash == Sphinx.PaymentPacket.hash(add_ab.onionRoutingPacket)) + assert(fail.failureCode === (FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 5)) register.expectNoMsg(100 millis) paymentHandler.expectNoMsg(100 millis) @@ -383,7 +383,7 @@ class RelayerSpec extends TestkitBaseClass { system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent]) // we build a fake htlc for the downstream channel - val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = 10000000L, paymentHash = ByteVector32.Zeroes, cltvExpiry = 4200, onionRoutingPacket = ByteVector.empty) + val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = 10000000L, paymentHash = ByteVector32.Zeroes, cltvExpiry = 4200, onionRoutingPacket = TestConstants.emptyOnionPacket) val fulfill_ba = UpdateFulfillHtlc(channelId = channelId_bc, id = 42, paymentPreimage = ByteVector32.Zeroes) val origin = Relayed(channelId_ab, 150, 11000000L, 10000000L) sender.send(relayer, ForwardFulfill(fulfill_ba, origin, add_bc)) @@ -401,8 +401,8 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // we build a fake htlc for the downstream channel - val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = 10000000L, paymentHash = ByteVector32.Zeroes, cltvExpiry = 4200, onionRoutingPacket = ByteVector.empty) - val fail_ba = UpdateFailHtlc(channelId = channelId_bc, id = 42, reason = Sphinx.createErrorPacket(ByteVector32(ByteVector.fill(32)(1)), TemporaryChannelFailure(channelUpdate_cd))) + val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = 10000000L, paymentHash = ByteVector32.Zeroes, cltvExpiry = 4200, onionRoutingPacket = TestConstants.emptyOnionPacket) + val fail_ba = UpdateFailHtlc(channelId = channelId_bc, id = 42, reason = Sphinx.FailurePacket.create(ByteVector32(ByteVector.fill(32)(1)), TemporaryChannelFailure(channelUpdate_cd))) val origin = Relayed(channelId_ab, 150, 11000000L, 10000000L) sender.send(relayer, ForwardFail(fail_ba, origin, add_bc)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala index 432101f27b..d64bfab29f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala @@ -49,6 +49,7 @@ class AnnouncementsSpec extends FunSuite { test("create valid signed node announcement") { val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses) + assert(Features.hasFeature(ann.features, Features.VARIABLE_LENGTH_ONION_OPTIONAL)) assert(checkSig(ann)) assert(checkSig(ann.copy(timestamp = 153)) === false) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala index ec31ac1936..1b95b50856 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala @@ -17,10 +17,9 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.{ByteVector32, Crypto} -import fr.acinq.eclair.randomBytes32 +import fr.acinq.eclair.{TestConstants, randomBytes32} import fr.acinq.eclair.wire.{UpdateAddHtlc, UpdateFailHtlc, UpdateFulfillHtlc} import org.scalatest.FunSuite -import scodec.bits.ByteVector class CommitmentSpecSpec extends FunSuite { @@ -29,11 +28,11 @@ class CommitmentSpecSpec extends FunSuite { val R = randomBytes32 val H = Crypto.sha256(R) - val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, 2000 * 1000, H, 400, ByteVector.empty) + val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, 2000 * 1000, H, 400, TestConstants.emptyOnionPacket) val spec1 = CommitmentSpec.reduce(spec, add1 :: Nil, Nil) assert(spec1 === spec.copy(htlcs = Set(DirectedHtlc(OUT, add1)), toLocalMsat = 3000 * 1000)) - val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, 1000 * 1000, H, 400, ByteVector.empty) + val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, 1000 * 1000, H, 400, TestConstants.emptyOnionPacket) val spec2 = CommitmentSpec.reduce(spec1, add2 :: Nil, Nil) assert(spec2 === spec1.copy(htlcs = Set(DirectedHtlc(OUT, add1), DirectedHtlc(OUT, add2)), toLocalMsat = 2000 * 1000)) @@ -51,11 +50,11 @@ class CommitmentSpecSpec extends FunSuite { val R = randomBytes32 val H = Crypto.sha256(R) - val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, 2000 * 1000, H, 400, ByteVector.empty) + val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, 2000 * 1000, H, 400, TestConstants.emptyOnionPacket) val spec1 = CommitmentSpec.reduce(spec, Nil, add1 :: Nil) assert(spec1 === spec.copy(htlcs = Set(DirectedHtlc(IN, add1)), toRemoteMsat = 3000 * 1000)) - val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, 1000 * 1000, H, 400, ByteVector.empty) + val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, 1000 * 1000, H, 400, TestConstants.emptyOnionPacket) val spec2 = CommitmentSpec.reduce(spec1, Nil, add2 :: Nil) assert(spec2 === spec1.copy(htlcs = Set(DirectedHtlc(IN, add1), DirectedHtlc(IN, add2)), toRemoteMsat = 2000 * 1000)) 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 f32d4f046a..013d70b8fb 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 @@ -18,6 +18,7 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin._ +import fr.acinq.eclair.TestConstants import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.transactions.Transactions.{HtlcSuccessTx, HtlcTimeoutTx, TransactionWithInputInfo} @@ -153,11 +154,11 @@ class TestVectorsSpec extends FunSuite with Logging { ) val htlcs = Seq( - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(1000000).amount, Crypto.sha256(paymentPreimages(0)), 500, ByteVector.empty)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(1)), 501, ByteVector.empty)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(2)), 502, ByteVector.empty)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(3000000).amount, Crypto.sha256(paymentPreimages(3)), 503, ByteVector.empty)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(4000000).amount, Crypto.sha256(paymentPreimages(4)), 504, ByteVector.empty)) + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(1000000).amount, Crypto.sha256(paymentPreimages(0)), 500, TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(1)), 501, TestConstants.emptyOnionPacket)), + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(2)), 502, TestConstants.emptyOnionPacket)), + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(3000000).amount, Crypto.sha256(paymentPreimages(3)), 503, TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(4000000).amount, Crypto.sha256(paymentPreimages(4)), 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)) 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 12ca3f87a0..f3819c8bc8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -22,13 +22,12 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, ripemd160, sha256} import fr.acinq.bitcoin.Script.{pay2wpkh, pay2wsh, write} import fr.acinq.bitcoin._ import fr.acinq.eclair.channel.Helpers.Funding -import fr.acinq.eclair.randomBytes32 +import fr.acinq.eclair.{TestConstants, randomBytes32} import fr.acinq.eclair.transactions.Scripts.{htlcOffered, htlcReceived, toLocalDelayed} import fr.acinq.eclair.transactions.Transactions.{addSigs, _} import fr.acinq.eclair.wire.UpdateAddHtlc import grizzled.slf4j.Logging import org.scalatest.FunSuite -import scodec.bits.ByteVector import scala.io.Source import scala.util.{Failure, Random, Success, Try} @@ -64,10 +63,10 @@ class TransactionsSpec extends FunSuite with Logging { test("compute fees") { // see BOLT #3 specs val htlcs = Set( - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(5000000).amount, ByteVector32.Zeroes, 552, ByteVector.empty)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(1000000).amount, ByteVector32.Zeroes, 553, ByteVector.empty)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(7000000).amount, ByteVector32.Zeroes, 550, ByteVector.empty)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(800000).amount, ByteVector32.Zeroes, 551, ByteVector.empty)) + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(5000000).amount, ByteVector32.Zeroes, 552, TestConstants.emptyOnionPacket)), + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(1000000).amount, ByteVector32.Zeroes, 553, TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(7000000).amount, ByteVector32.Zeroes, 550, TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(800000).amount, ByteVector32.Zeroes, 551, TestConstants.emptyOnionPacket)) ) val spec = CommitmentSpec(htlcs, feeratePerKw = 5000, toLocalMsat = 0, toRemoteMsat = 0) val fee = Transactions.commitTxFee(Satoshi(546), spec) @@ -126,7 +125,7 @@ class TransactionsSpec extends FunSuite with Logging { // HtlcPenaltyTx // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcSuccessTx val paymentPreimage = randomBytes32 - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), cltvExpiry = 400144, ByteVector.empty) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), cltvExpiry = 400144, TestConstants.emptyOnionPacket) val redeemScript = htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), htlc.cltvExpiry) val pubKeyScript = write(pay2wsh(redeemScript)) val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(htlc.amountMsat / 1000), pubKeyScript) :: Nil, lockTime = 0) @@ -141,7 +140,7 @@ class TransactionsSpec extends FunSuite with Logging { // ClaimHtlcSuccessTx // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcSuccessTx val paymentPreimage = randomBytes32 - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), cltvExpiry = 400144, ByteVector.empty) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), cltvExpiry = 400144, TestConstants.emptyOnionPacket) val pubKeyScript = write(pay2wsh(htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash)))) val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(htlc.amountMsat / 1000), pubKeyScript) :: Nil, lockTime = 0) val claimHtlcSuccessTx = makeClaimHtlcSuccessTx(commitTx, outputsAlreadyUsed = Set.empty, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw) @@ -155,7 +154,7 @@ class TransactionsSpec extends FunSuite with Logging { // ClaimHtlcTimeoutTx // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcSuccessTx val paymentPreimage = randomBytes32 - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), cltvExpiry = 400144, ByteVector.empty) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), cltvExpiry = 400144, TestConstants.emptyOnionPacket) val pubKeyScript = write(pay2wsh(htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), htlc.cltvExpiry))) val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(htlc.amountMsat / 1000), pubKeyScript) :: Nil, lockTime = 0) val claimClaimHtlcTimeoutTx = makeClaimHtlcTimeoutTx(commitTx, outputsAlreadyUsed = Set.empty, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw) @@ -184,14 +183,14 @@ class TransactionsSpec extends FunSuite with Logging { // htlc1 and htlc2 are regular IN/OUT htlcs val paymentPreimage1 = randomBytes32 - val htlc1 = UpdateAddHtlc(ByteVector32.Zeroes, 0, millibtc2satoshi(MilliBtc(100)).amount * 1000, sha256(paymentPreimage1), 300, ByteVector.empty) + val htlc1 = UpdateAddHtlc(ByteVector32.Zeroes, 0, millibtc2satoshi(MilliBtc(100)).amount * 1000, sha256(paymentPreimage1), 300, TestConstants.emptyOnionPacket) val paymentPreimage2 = randomBytes32 - val htlc2 = UpdateAddHtlc(ByteVector32.Zeroes, 1, millibtc2satoshi(MilliBtc(200)).amount * 1000, sha256(paymentPreimage2), 300, ByteVector.empty) + val htlc2 = UpdateAddHtlc(ByteVector32.Zeroes, 1, millibtc2satoshi(MilliBtc(200)).amount * 1000, sha256(paymentPreimage2), 300, TestConstants.emptyOnionPacket) // htlc3 and htlc4 are dust htlcs IN/OUT htlcs, with an amount large enough to be included in the commit tx, but too small to be claimed at 2nd stage val paymentPreimage3 = randomBytes32 - val htlc3 = UpdateAddHtlc(ByteVector32.Zeroes, 2, (localDustLimit + weight2fee(feeratePerKw, htlcTimeoutWeight)).amount * 1000, sha256(paymentPreimage3), 300, ByteVector.empty) + val htlc3 = UpdateAddHtlc(ByteVector32.Zeroes, 2, (localDustLimit + weight2fee(feeratePerKw, htlcTimeoutWeight)).amount * 1000, sha256(paymentPreimage3), 300, TestConstants.emptyOnionPacket) val paymentPreimage4 = randomBytes32 - val htlc4 = UpdateAddHtlc(ByteVector32.Zeroes, 3, (localDustLimit + weight2fee(feeratePerKw, htlcSuccessWeight)).amount * 1000, sha256(paymentPreimage4), 300, ByteVector.empty) + val htlc4 = UpdateAddHtlc(ByteVector32.Zeroes, 3, (localDustLimit + weight2fee(feeratePerKw, htlcSuccessWeight)).amount * 1000, sha256(paymentPreimage4), 300, TestConstants.emptyOnionPacket) val spec = CommitmentSpec( htlcs = Set( DirectedHtlc(OUT, htlc1), @@ -321,7 +320,7 @@ class TransactionsSpec extends FunSuite with Logging { } def htlc(direction: Direction, amount: Satoshi): DirectedHtlc = - DirectedHtlc(direction, UpdateAddHtlc(ByteVector32.Zeroes, 0, amount.amount * 1000, ByteVector32.Zeroes, 144, ByteVector.empty)) + DirectedHtlc(direction, UpdateAddHtlc(ByteVector32.Zeroes, 0, amount.amount * 1000, ByteVector32.Zeroes, 144, TestConstants.emptyOnionPacket)) test("BOLT 2 fee tests") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala index c5ba94558b..81f6e6e0d6 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 @@ -26,13 +26,14 @@ import fr.acinq.eclair._ import fr.acinq.eclair.api.JsonSupport import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel._ -import fr.acinq.eclair.crypto.{LocalKeyManager, ShaChain, Sphinx} +import fr.acinq.eclair.crypto.{LocalKeyManager, ShaChain} import fr.acinq.eclair.payment.{Local, Relayed} import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.CommitTx import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.ChannelCodecs._ import org.json4s.jackson.Serialization +import fr.acinq.eclair.{TestConstants, UInt64, randomBytes, randomBytes32, randomKey} import org.scalatest.FunSuite import scodec.bits._ import scodec.{Attempt, DecodeResult} @@ -131,7 +132,7 @@ class ChannelCodecsSpec extends FunSuite { amountMsat = Random.nextInt(Int.MaxValue), cltvExpiry = Random.nextInt(Int.MaxValue), paymentHash = randomBytes32, - onionRoutingPacket = randomBytes(Sphinx.PacketLength)) + onionRoutingPacket = TestConstants.emptyOnionPacket) val htlc1 = DirectedHtlc(direction = IN, add = add) val htlc2 = DirectedHtlc(direction = OUT, add = add) assert(htlcCodec.decodeValue(htlcCodec.encode(htlc1).require).require === htlc1) @@ -145,14 +146,14 @@ class ChannelCodecsSpec extends FunSuite { amountMsat = Random.nextInt(Int.MaxValue), cltvExpiry = Random.nextInt(Int.MaxValue), paymentHash = randomBytes32, - onionRoutingPacket = randomBytes(Sphinx.PacketLength)) + onionRoutingPacket = TestConstants.emptyOnionPacket) val add2 = UpdateAddHtlc( channelId = randomBytes32, id = Random.nextInt(Int.MaxValue), amountMsat = Random.nextInt(Int.MaxValue), cltvExpiry = Random.nextInt(Int.MaxValue), paymentHash = randomBytes32, - onionRoutingPacket = randomBytes(Sphinx.PacketLength)) + onionRoutingPacket = TestConstants.emptyOnionPacket) val htlc1 = DirectedHtlc(direction = IN, add = add1) val htlc2 = DirectedHtlc(direction = OUT, add = add2) val htlcs = Set(htlc1, htlc2) @@ -363,11 +364,11 @@ object ChannelCodecsSpec { ) val htlcs = Seq( - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(1000000).amount, Crypto.sha256(paymentPreimages(0)), 500, ByteVector.fill(Sphinx.PacketLength)(0))), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 1, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(1)), 501, ByteVector.fill(Sphinx.PacketLength)(0))), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 30, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(2)), 502, ByteVector.fill(Sphinx.PacketLength)(0))), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 31, MilliSatoshi(3000000).amount, Crypto.sha256(paymentPreimages(3)), 503, ByteVector.fill(Sphinx.PacketLength)(0))), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 2, MilliSatoshi(4000000).amount, Crypto.sha256(paymentPreimages(4)), 504, ByteVector.fill(Sphinx.PacketLength)(0))) + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(1000000).amount, Crypto.sha256(paymentPreimages(0)), 500, TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 1, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(1)), 501, TestConstants.emptyOnionPacket)), + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 30, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(2)), 502, TestConstants.emptyOnionPacket)), + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 31, MilliSatoshi(3000000).amount, Crypto.sha256(paymentPreimages(3)), 503, TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 2, MilliSatoshi(4000000).amount, Crypto.sha256(paymentPreimages(4)), 504, TestConstants.emptyOnionPacket)) ) val fundingTx = Transaction.read("0200000001adbb20ea41a8423ea937e76e8151636bf6093b70eaff942930d20576600521fd000000006b48304502210090587b6201e166ad6af0227d3036a9454223d49a1f11839c1a362184340ef0240220577f7cd5cca78719405cbf1de7414ac027f0239ef6e214c90fcaab0454d84b3b012103535b32d5eb0a6ed0982a0479bbadc9868d9836f6ba94dd5a63be16d875069184ffffffff028096980000000000220020c015c4a6be010e21657068fc2e6a9d02b27ebe4d490a25846f7237f104d1a3cd20256d29010000001600143ca33c2e4446f4a305f23c80df8ad1afdcf652f900000000") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/CommonCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/CommonCodecsSpec.scala index 221efba9dd..6d46c1d051 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/CommonCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/CommonCodecsSpec.scala @@ -19,7 +19,9 @@ package fr.acinq.eclair.wire import java.net.{Inet4Address, Inet6Address, InetAddress} import com.google.common.net.InetAddresses +import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.PrivateKey +import fr.acinq.eclair.crypto.Hmac256 import fr.acinq.eclair.wire.CommonCodecs._ import fr.acinq.eclair.{UInt64, randomBytes32} import org.scalatest.FunSuite @@ -47,6 +49,16 @@ class CommonCodecsSpec extends FunSuite { } } + test("encode/decode UInt64") { + val refs = Seq( + UInt64(hex"ffffffffffffffff"), + UInt64(hex"fffffffffffffffe"), + UInt64(hex"efffffffffffffff"), + UInt64(hex"effffffffffffffe") + ) + assert(refs.forall(value => uint64.decode(uint64.encode(value).require).require.value === value)) + } + test("encode/decode with varint codec") { val expected = Map( UInt64(0L) -> hex"00", @@ -222,16 +234,21 @@ class CommonCodecsSpec extends FunSuite { } } - test("encode/decode UInt64") { - val codec = uint64 - Seq( - UInt64(hex"ffffffffffffffff"), - UInt64(hex"fffffffffffffffe"), - UInt64(hex"efffffffffffffff"), - UInt64(hex"effffffffffffffe") - ).map(value => { - assert(codec.decode(codec.encode(value).require).require.value === value) - }) + test("encode/decode with prependmac codec") { + val mac = Hmac256(ByteVector32.Zeroes) + val testCases = Seq( + (uint64, UInt64(561), hex"d5b500b8843e19a34d8ab54740db76a7ea597e4ff2ada3827420f87c7e60b7c6 0000000000000231"), + (varint, UInt64(65535), hex"71e17e5b97deb6916f7ad97a53650769d4e4f0b1e580ff35ca332200d61e765c fdffff") + ) + + for ((codec, expected, bin) <- testCases) { + val macCodec = prependmac(codec, mac) + val decoded = macCodec.decode(bin.toBitVector).require.value + assert(decoded === expected) + + val encoded = macCodec.encode(expected).require.toByteVector + assert(encoded === bin) + } } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala index f48af32c47..3b3ecd7a61 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala @@ -16,8 +16,10 @@ package fr.acinq.eclair.wire -import fr.acinq.bitcoin.{Block, ByteVector64} +import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64} +import fr.acinq.eclair.crypto.Hmac256 import fr.acinq.eclair.{ShortChannelId, randomBytes32, randomBytes64} +import fr.acinq.eclair.wire.FailureMessageCodecs._ import org.scalatest.FunSuite import scodec.bits._ @@ -42,30 +44,81 @@ class FailureMessageCodecsSpec extends FunSuite { test("encode/decode all channel messages") { val msgs: List[FailureMessage] = InvalidRealm :: TemporaryNodeFailure :: PermanentNodeFailure :: RequiredNodeFeatureMissing :: - InvalidOnionVersion(randomBytes32) :: InvalidOnionHmac(randomBytes32) :: InvalidOnionKey(randomBytes32) :: + InvalidOnionVersion(randomBytes32) :: InvalidOnionHmac(randomBytes32) :: InvalidOnionKey(randomBytes32) :: InvalidOnionPayload(randomBytes32) :: TemporaryChannelFailure(channelUpdate) :: PermanentChannelFailure :: RequiredChannelFeatureMissing :: UnknownNextPeer :: AmountBelowMinimum(123456, channelUpdate) :: FeeInsufficient(546463, channelUpdate) :: IncorrectCltvExpiry(1211, channelUpdate) :: ExpiryTooSoon(channelUpdate) :: IncorrectOrUnknownPaymentDetails(123456L) :: IncorrectPaymentAmount :: FinalExpiryTooSoon :: FinalIncorrectCltvExpiry(1234) :: ChannelDisabled(0, 1, channelUpdate) :: ExpiryTooFar :: Nil msgs.foreach { - case msg => { - val encoded = FailureMessageCodecs.failureMessageCodec.encode(msg).require - val decoded = FailureMessageCodecs.failureMessageCodec.decode(encoded).require + msg => { + val encoded = failureMessageCodec.encode(msg).require + val decoded = failureMessageCodec.decode(encoded).require assert(msg === decoded.value) } } } + test("bad onion failure code") { + val msgs = Map( + (BADONION | PERM | 4) -> InvalidOnionVersion(randomBytes32), + (BADONION | PERM | 5) -> InvalidOnionHmac(randomBytes32), + (BADONION | PERM | 6) -> InvalidOnionKey(randomBytes32), + (BADONION | PERM) -> InvalidOnionPayload(randomBytes32) + ) + + for ((code, message) <- msgs) { + assert(failureCode(message) === code) + } + } + + test("encode/decode failure onion") { + val codec = failureOnionCodec(Hmac256(ByteVector32.Zeroes)) + val testCases = Map( + InvalidOnionKey(ByteVector32(hex"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a")) -> hex"41a824e2d630111669fa3e52b600a518f369691909b4e89205dc624ee17ed2c1 0022 c006 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a 00de 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + IncorrectOrUnknownPaymentDetails(42) -> hex"ba6e122b2941619e2106e8437bf525356ffc8439ac3b2245f68546e298a08cc6 000a 400f 000000000000002a 00f6 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ) + + for ((expected, bin) <- testCases) { + val decoded = codec.decode(bin.toBitVector).require.value + assert(decoded === expected) + + val encoded = codec.encode(expected).require.toByteVector + assert(encoded === bin) + } + } + + test("decode invalid failure onion packet") { + val codec = failureOnionCodec(Hmac256(ByteVector32.Zeroes)) + val testCases = Seq( + // Invalid failure message. + hex"fd2f3eb163dacfa7fe2ec1a7dc73c33438e7ca97c561475cf0dc96dc15a75039 0020 c005 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a 00e0 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + // Invalid mac. + hex"0000000000000000000000000000000000000000000000000000000000000000 0022 c006 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a 00de 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + // Padding too small. + hex"7bfb2aa46218240684f623322ae48af431d06986c82e210bb0cee83c7ddb2ba8 0002 4001 0002 0000", + // Padding length doesn't match actual padding. + hex"8c92256e45bbe765130d952e6c043cf594ab25224701f5477fce0e50ee88fa21 0002 4001 0002 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + // Padding too big. + hex"6f9e2c0e44b3692dac37523c6ff054cc9b26ecab1a78ed6906a46848bffc2bd5 0002 4001 00ff 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + // Padding length doesn't match actual padding. + hex"3898307b7c01781628ff6f854a4a78524541e4afde9b44046bdb84093f082d9d 0002 4001 00ff 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ) + + for (testCase <- testCases) { + assert(codec.decode(testCase.toBitVector).isFailure) + } + } + test("support encoding of channel_update with/without type in failure messages") { val tmp_channel_failure_notype = hex"10070080cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f45782196fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000008260500041300005b91b52f0003000e00000000000003e80000000100000001" val tmp_channel_failure_withtype = hex"100700820102cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f45782196fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000008260500041300005b91b52f0003000e00000000000003e80000000100000001" val ref = TemporaryChannelFailure(ChannelUpdate(ByteVector64(hex"cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f4578219"), Block.LivenetGenesisBlock.hash, ShortChannelId(0x826050004130000L), 1536275759, 0, 3, 14, 1000, 1, 1, None)) - val u = FailureMessageCodecs.failureMessageCodec.decode(tmp_channel_failure_notype.toBitVector).require.value + val u = failureMessageCodec.decode(tmp_channel_failure_notype.toBitVector).require.value assert(u === ref) - val bin = ByteVector(FailureMessageCodecs.failureMessageCodec.encode(u).require.toByteArray) + val bin = ByteVector(failureMessageCodec.encode(u).require.toByteArray) assert(bin === tmp_channel_failure_withtype) - val u2 = FailureMessageCodecs.failureMessageCodec.decode(bin.toBitVector).require.value + val u2 = failureMessageCodec.decode(bin.toBitVector).require.value assert(u2 === ref) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala index 700b552971..ff2c26b5a2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala @@ -21,11 +21,10 @@ import java.net.{Inet4Address, InetAddress} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64} import fr.acinq.eclair._ -import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.LightningMessageCodecs._ import org.scalatest.FunSuite -import scodec.bits.{BitVector, ByteVector, HexStringSyntax} +import scodec.bits.{ByteVector, HexStringSyntax} /** * Created by PM on 31/05/2016. @@ -44,21 +43,12 @@ class LightningMessageCodecsSpec extends FunSuite { def publicKey(fill: Byte) = PrivateKey(ByteVector.fill(32)(fill)).publicKey test("encode/decode live node_announcements") { - val anns = List( - hex"a58338c9660d135fd7d087eb62afd24a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f61704cf1ae93608df027014ade7ff592f27ce26900005acdf50702d2eabbbacc7c25bbd73b39e65d28237705f7bde76f557e94fb41cb18a9ec00841122116c6e302e646563656e7465722e776f726c64000000000000000000000000000000130200000000000000000000ffffae8a0b082607" - //hex"d5bfb0be26412eed9bbab186772bd3885610e289ed305e729869a5bcbd97ea431863b6fa884b021162ed5e66264c4087630e4403669bab29f3c533c4089e508c00005ab521eb030e9226f19cd3ba8a58fb280d00f5f94f3c10f1b4618a5f9bffd43534c966ebd4030e9256495247494e41574f4c465f3200000000000000000000000000000000000000000f03cec0cb03c68094bbb48792002608" - //hex"9746cd4d25a5cf2b04f3d986a073973b0318282e32e2758939b6650cd13cf65e4225ceaa98b02f070614e907661278a1479542afb12b9867511e0d31d995209800005ab646a302dc523b9db431de52d7adb79cf81dd3d780002f4ce952706053edc9da30d9b9f702dc5256495247494e41574f4c460000000000000000000000000000000000000000000016031bb5481aa82769f4446e1002260701584473f82607", - //hex"a483677744b63d892a85fb7460fd6cb0504f802600956eb18cfaad05fbbe775328e4a7060476d2c0f3b7a6d505bb4de9377a55b27d1477baf14c367287c3de7900005abb440002dc523b9db431de52d7adb79cf81dd3d780002f4ce952706053edc9da30d9b9f702dc5256495247494e41574f4c460000000000000000000000000000000000000000000016031bb5481aa82769f4446e1002260701584473f82607", - //hex"3ecfd85bcb3bafb5bad14ab7f6323a2df33e161c37c2897e576762fa90ffe46078d231ebbf7dce3eff4b440d091a10ea9d092e698a321bb9c6b30869e2782c9900005abbebe202dc523b9db431de52d7adb79cf81dd3d780002f4ce952706053edc9da30d9b9f702dc5256495247494e41574f4c460000000000000000000000000000000000000000000016031bb5481aa82769f4446e1002260701584473f82607", - //hex"ad40baf5c7151777cc8896bc70ad2d0fd2afff47f4befb3883a78911b781a829441382d82625b77a47b9c2c71d201aab7187a6dc80e7d2d036dcb1186bac273c00005abffc330341f5ff2992997613aff5675d6796232a63ab7f30136219774da8aba431df37c80341f563377a6763723364776d777a7a3261652e6f6e696f6e00000000000000000000000f0317f2614763b32d9ce804fc002607" - ) - - anns.foreach { ann => - val bin = ann.toBitVector - val node = nodeAnnouncementCodec.decode(bin).require.value - val bin2 = nodeAnnouncementCodec.encode(node).require - assert(bin === bin2) - } + val ann = hex"a58338c9660d135fd7d087eb62afd24a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f61704cf1ae93608df027014ade7ff592f27ce2690001025acdf50702d2eabbbacc7c25bbd73b39e65d28237705f7bde76f557e94fb41cb18a9ec00841122116c6e302e646563656e7465722e776f726c64000000000000000000000000000000130200000000000000000000ffffae8a0b082607" + val bin = ann.bits + + val node = nodeAnnouncementCodec.decode(bin).require.value + val bin2 = nodeAnnouncementCodec.encode(node).require + assert(bin === bin2) } test("encode/decode all channel messages") { @@ -70,14 +60,14 @@ class LightningMessageCodecsSpec extends FunSuite { val update_fee = UpdateFee(randomBytes32, 2) val shutdown = Shutdown(randomBytes32, bin(47, 0)) val closing_signed = ClosingSigned(randomBytes32, 2, randomBytes64) - val update_add_htlc = UpdateAddHtlc(randomBytes32, 2, 3, bin32(0), 4, bin(Sphinx.PacketLength, 0)) + val update_add_htlc = UpdateAddHtlc(randomBytes32, 2, 3, bin32(0), 4, TestConstants.emptyOnionPacket) val update_fulfill_htlc = UpdateFulfillHtlc(randomBytes32, 2, bin32(0)) val update_fail_htlc = UpdateFailHtlc(randomBytes32, 2, bin(154, 0)) val update_fail_malformed_htlc = UpdateFailMalformedHtlc(randomBytes32, 2, randomBytes32, 1111) val commit_sig = CommitSig(randomBytes32, randomBytes64, randomBytes64 :: randomBytes64 :: randomBytes64 :: Nil) val revoke_and_ack = RevokeAndAck(randomBytes32, scalar(0), point(1)) val channel_announcement = ChannelAnnouncement(randomBytes64, randomBytes64, randomBytes64, randomBytes64, bin(7, 9), Block.RegtestGenesisBlock.hash, ShortChannelId(1), randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey) - val node_announcement = NodeAnnouncement(randomBytes64, bin(0, 0), 1, randomKey.publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil) + val node_announcement = NodeAnnouncement(randomBytes64, bin(1, 2), 1, randomKey.publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil) val channel_update = ChannelUpdate(randomBytes64, Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2, 42, 0, 3, 4, 5, 6, None) val announcement_signatures = AnnouncementSignatures(randomBytes32, ShortChannelId(42), randomBytes64, randomBytes64) val gossip_timestamp_filter = GossipTimestampFilter(Block.RegtestGenesisBlock.blockId, 100000, 1500) @@ -102,29 +92,14 @@ class LightningMessageCodecsSpec extends FunSuite { } } - test("encode/decode per-hop payload") { - val payload = PerHopPayload(shortChannelId = ShortChannelId(42), amtToForward = 142000, outgoingCltvValue = 500000) - val bin = LightningMessageCodecs.perHopPayloadCodec.encode(payload).require - assert(bin.toByteVector.size === 33) - val payload1 = LightningMessageCodecs.perHopPayloadCodec.decode(bin).require.value - assert(payload === payload1) - - // realm (the first byte) should be 0 - val bin1 = bin.toByteVector.update(0, 1) - intercept[IllegalArgumentException] { - val payload2 = LightningMessageCodecs.perHopPayloadCodec.decode(bin1.toBitVector).require.value - assert(payload2 === payload1) - } - } - test("decode channel_update with htlc_maximum_msat") { // this was generated by c-lightning val bin = hex"010258fff7d0e987e2cdd560e3bb5a046b4efe7b26c969c2f51da1dceec7bcb8ae1b634790503d5290c1a6c51d681cf8f4211d27ed33a257dcc1102862571bf1792306226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f0005a100000200005bc75919010100060000000000000001000000010000000a000000003a699d00" - val update = LightningMessageCodecs.lightningMessageCodec.decode(BitVector(bin.toArray)).require.value.asInstanceOf[ChannelUpdate] + val update = lightningMessageCodec.decode(bin.bits).require.value.asInstanceOf[ChannelUpdate] assert(update === ChannelUpdate(ByteVector64(hex"58fff7d0e987e2cdd560e3bb5a046b4efe7b26c969c2f51da1dceec7bcb8ae1b634790503d5290c1a6c51d681cf8f4211d27ed33a257dcc1102862571bf17923"), ByteVector32(hex"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), ShortChannelId(0x5a10000020000L), 1539791129, 1, 1, 6, 1, 1, 10, Some(980000000L))) val nodeId = PublicKey(hex"03370c9bac836e557eb4f017fe8f9cc047f44db39c1c4e410ff0f7be142b817ae4") assert(Announcements.checkSig(update, nodeId)) - val bin2 = ByteVector(LightningMessageCodecs.lightningMessageCodec.encode(update).require.toByteArray) + val bin2 = ByteVector(lightningMessageCodec.encode(update).require.toByteArray) assert(bin === bin2) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala new file mode 100644 index 0000000000..763a9ce42a --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala @@ -0,0 +1,74 @@ +/* + * Copyright 2019 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.wire + +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.wire.OnionCodecs._ +import org.scalatest.FunSuite +import scodec.bits.HexStringSyntax + +/** + * Created by t-bast on 05/07/2019. + */ + +class OnionCodecsSpec extends FunSuite { + + test("encode/decode onion packet") { + val bin = hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f917265f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf" + val expected = OnionRoutingPacket(0, hex"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", hex"e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f9172", ByteVector32(hex"65f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf")) + + val decoded = paymentOnionPacketCodec.decode(bin.bits).require.value + assert(decoded === expected) + + val encoded = paymentOnionPacketCodec.encode(decoded).require + assert(encoded.toByteVector === bin) + } + + test("encode/decode per-hop payload") { + val payload = PerHopPayload(shortChannelId = ShortChannelId(42), amtToForward = 142000, outgoingCltvValue = 500000) + val bin = perHopPayloadCodec.encode(payload).require + assert(bin.toByteVector.size === 33) + val payload1 = perHopPayloadCodec.decode(bin).require.value + assert(payload === payload1) + + // realm (the first byte) should be 0 + val bin1 = bin.toByteVector.update(0, 1) + intercept[IllegalArgumentException] { + val payload2 = perHopPayloadCodec.decode(bin1.bits).require.value + assert(payload2 === payload1) + } + } + + test("decode payload length") { + val testCases = Seq( + (1, hex"00"), + (43, hex"2a 0000"), + (253, hex"fc 0000"), + (256, hex"fd00fd 000000"), + (260, hex"fd0101 00"), + (65538, hex"fdffff 00"), + (65541, hex"fe00010000 00"), + (4294967305L, hex"ff0000000100000000 00") + ) + + for ((payloadLength, bin) <- testCases) { + assert(payloadLengthDecoder.decode(bin.bits).require.value === payloadLength) + } + } + +}