1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-03-15 12:20:13 +01:00

Merge branch 'master' into wip-android

This commit is contained in:
pm47 2017-12-15 18:28:52 +01:00
commit 734ec9d914
18 changed files with 226 additions and 91 deletions

View file

@ -45,5 +45,12 @@ FROM openjdk:8u151-jre-slim
WORKDIR /app WORKDIR /app
# Eclair only needs the eclair-node-*.jar to run # Eclair only needs the eclair-node-*.jar to run
COPY --from=BUILD /usr/src/eclair-node/target/eclair-node-*.jar . COPY --from=BUILD /usr/src/eclair-node/target/eclair-node-*.jar .
RUN ln `ls` eclair-node RUN ln `ls` eclair-node.jar
ENTRYPOINT [ "java", "-jar", "eclair-node" ]
ENV ECLAIR_DATADIR=/data
ENV JAVA_OPTS=
RUN mkdir -p "$ECLAIR_DATADIR"
VOLUME [ "/data" ]
ENTRYPOINT java $JAVA_OPTS -Declair.datadir=$ECLAIR_DATADIR -jar eclair-node.jar

View file

@ -140,6 +140,23 @@ java -Declair.datadir=/tmp/node1 -jar eclair-node-gui-<version>-<commit_id>.jar
close | channelId, scriptPubKey (optional) | close a channel and send the funds to the given scriptPubKey close | channelId, scriptPubKey (optional) | close a channel and send the funds to the given scriptPubKey
help | | display available methods help | | display available methods
## Docker
A [Dockerfile](Dockerfile) images are built on each commit on [docker hub](https://hub.docker.com/r/ACINQ/eclair) for running a dockerized eclair-node.
You can use the `JAVA_OPTS` environment variable to set arguments to `eclair-node`.
```
docker run -ti --rm -e "JAVA_OPTS=-Xmx512m -Declair.api.binding-ip=0.0.0.0 -Declair.node-alias=node-pm -Declair.printToConsole" acinq\eclair
```
If you want to persist the data directory, you can make the volume to your host with the `-v` argument, as the following example:
```
docker run -ti --rm -v "/path_on_host:/data" -e "JAVA_OPTS=-Declair.printToConsole" acinq\eclair
```
## Resources ## Resources
- [1] [The Bitcoin Lightning Network: Scalable Off-Chain Instant Payments](https://lightning.network/lightning-network-paper.pdf) by Joseph Poon and Thaddeus Dryja - [1] [The Bitcoin Lightning Network: Scalable Off-Chain Instant Payments](https://lightning.network/lightning-network-paper.pdf) by Joseph Poon and Thaddeus Dryja
- [2] [Reaching The Ground With Lightning](https://github.com/ElementsProject/lightning/raw/master/doc/deployable-lightning.pdf) by Rusty Russell - [2] [Reaching The Ground With Lightning](https://github.com/ElementsProject/lightning/raw/master/doc/deployable-lightning.pdf) by Rusty Russell

View file

@ -18,6 +18,9 @@ case $1 in
"channels") "channels")
eval curl "$CURL_OPTS -d '{ \"method\": \"channels\", \"params\" : [] }' $URL" | jq ".result[]" eval curl "$CURL_OPTS -d '{ \"method\": \"channels\", \"params\" : [] }' $URL" | jq ".result[]"
;; ;;
"channelsto")
eval curl "$CURL_OPTS -d '{ \"method\": \"channelsto\", \"params\" : [\"${2?"missing node id"}\"] }' $URL" | jq ".result[]"
;;
"channel") "channel")
eval curl "$CURL_OPTS -d '{ \"method\": \"channel\", \"params\" : [\"${2?"missing channel id"}\"] }' $URL" | jq ".result | { nodeid, channelId, state, balanceMsat: .data.commitments.localCommit.spec.toLocalMsat, capacitySat: .data.commitments.commitInput.txOut.amount.amount }" eval curl "$CURL_OPTS -d '{ \"method\": \"channel\", \"params\" : [\"${2?"missing channel id"}\"] }' $URL" | jq ".result | { nodeid, channelId, state, balanceMsat: .data.commitments.localCommit.spec.toLocalMsat, capacitySat: .data.commitments.commitInput.txOut.amount.amount }"
;; ;;

View file

@ -62,8 +62,8 @@ eclair {
mindepth-blocks = 2 mindepth-blocks = 2
expiry-delta-blocks = 144 expiry-delta-blocks = 144
fee-base-msat = 546000 fee-base-msat = 10000
fee-proportional-millionths = 10 fee-proportional-millionths = 100 // fee charged per transferred satoshi in millionths of a satoshi (100 = 0.1%)
// maximum local vs remote feerate mismatch; 1.0 means 100% // maximum local vs remote feerate mismatch; 1.0 means 100%
// actual check is abs((local feerate - remote fee rate) / (local fee rate + remote fee rate)/2) > fee rate mismatch // actual check is abs((local feerate - remote fee rate) / (local fee rate + remote fee rate)/2) > fee rate mismatch

View file

@ -266,8 +266,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
signature = localSigOfRemoteTx signature = localSigOfRemoteTx
) )
val channelId = toLongId(fundingTx.hash, fundingTxOutputIndex) val channelId = toLongId(fundingTx.hash, fundingTxOutputIndex)
context.parent ! ChannelIdAssigned(self, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages context.parent ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages
context.system.eventStream.publish(ChannelIdAssigned(self, temporaryChannelId, channelId)) context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId))
goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), open.channelFlags, fundingCreated) sending fundingCreated goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), open.channelFlags, fundingCreated) sending fundingCreated
case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) => case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) =>
@ -314,8 +314,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
originChannels = Map.empty, originChannels = Map.empty,
remoteNextCommitInfo = Right(randomKey.publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array, remoteNextCommitInfo = Right(randomKey.publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array,
commitInput, ShaChain.init, channelId = channelId) commitInput, ShaChain.init, channelId = channelId)
context.parent ! ChannelIdAssigned(self, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages context.parent ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages
context.system.eventStream.publish(ChannelIdAssigned(self, temporaryChannelId, channelId)) context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId))
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) context.system.eventStream.publish(ChannelSignatureReceived(self, commitments))
log.info(s"waiting for them to publish the funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") log.info(s"waiting for them to publish the funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}")
blockchain ! WatchSpent(self, commitInput.outPoint.txid, commitInput.outPoint.index.toInt, commitments.commitInput.txOut.publicKeyScript, BITCOIN_FUNDING_SPENT) // TODO: should we wait for an acknowledgment from the watcher? blockchain ! WatchSpent(self, commitInput.outPoint.txid, commitInput.outPoint.index.toInt, commitments.commitInput.txOut.publicKeyScript, BITCOIN_FUNDING_SPENT) // TODO: should we wait for an acknowledgment from the watcher?
@ -764,7 +764,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
} }
relayer ! channelUpdate relayer ! channelUpdate
d.commitments.localChanges.proposed.collect { d.commitments.localChanges.proposed.collect {
case add: UpdateAddHtlc => relayer ! AddHtlcFailed(d.channelId, ChannelUnavailable(d.channelId), d.commitments.originChannels(add.id), Some(channelUpdate)) case add: UpdateAddHtlc => relayer ! Status.Failure(AddHtlcFailed(d.channelId, ChannelUnavailable(d.channelId), d.commitments.originChannels(add.id), Some(channelUpdate)))
} }
goto(OFFLINE) using d.copy(channelUpdate = channelUpdate) goto(OFFLINE) using d.copy(channelUpdate = channelUpdate)
@ -1066,7 +1066,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case add if ripemd160(add.paymentHash) == extracted => case add if ripemd160(add.paymentHash) == extracted =>
val origin = d.commitments.originChannels(add.id) val origin = d.commitments.originChannels(add.id)
log.warning(s"found a match between paymentHash160=$extracted and origin=$origin: htlc timed out") log.warning(s"found a match between paymentHash160=$extracted and origin=$origin: htlc timed out")
relayer ! AddHtlcFailed(d.channelId, HtlcTimedout(d.channelId), origin, None) relayer ! Status.Failure(AddHtlcFailed(d.channelId, HtlcTimedout(d.channelId), origin, None))
} }
// TODO: should we handle local htlcs here as well? currently timed out htlcs that we sent will never have an answer // TODO: should we handle local htlcs here as well? currently timed out htlcs that we sent will never have an answer
// TODO: we do not handle the case where htlcs transactions end up being unconfirmed this can happen if an htlc-success tx is published right before a htlc timed out // TODO: we do not handle the case where htlcs transactions end up being unconfirmed this can happen if an htlc-success tx is published right before a htlc timed out

View file

@ -14,7 +14,7 @@ case class ChannelCreated(channel: ActorRef, peer: ActorRef, remoteNodeId: Publi
case class ChannelRestored(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, channelId: BinaryData, currentData: HasCommitments) extends ChannelEvent case class ChannelRestored(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, channelId: BinaryData, currentData: HasCommitments) extends ChannelEvent
case class ChannelIdAssigned(channel: ActorRef, temporaryChannelId: BinaryData, channelId: BinaryData) extends ChannelEvent case class ChannelIdAssigned(channel: ActorRef, remoteNodeId: PublicKey, temporaryChannelId: BinaryData, channelId: BinaryData) extends ChannelEvent
case class ShortChannelIdAssigned(channel: ActorRef, channelId: BinaryData, shortChannelId: Long) extends ChannelEvent case class ShortChannelIdAssigned(channel: ActorRef, channelId: BinaryData, shortChannelId: Long) extends ChannelEvent

View file

@ -3,7 +3,8 @@ package fr.acinq.eclair.channel
import akka.actor.Status.Failure import akka.actor.Status.Failure
import akka.actor.{Actor, ActorLogging, ActorRef, Terminated} import akka.actor.{Actor, ActorLogging, ActorRef, Terminated}
import fr.acinq.bitcoin.BinaryData import fr.acinq.bitcoin.BinaryData
import fr.acinq.eclair.channel.Register.{Forward, ForwardFailure, ForwardShortId, ForwardShortIdFailure} import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.channel.Register._
/** /**
* Created by PM on 26/01/2016. * Created by PM on 26/01/2016.
@ -16,32 +17,34 @@ class Register extends Actor with ActorLogging {
context.system.eventStream.subscribe(self, classOf[ChannelIdAssigned]) context.system.eventStream.subscribe(self, classOf[ChannelIdAssigned])
context.system.eventStream.subscribe(self, classOf[ShortChannelIdAssigned]) context.system.eventStream.subscribe(self, classOf[ShortChannelIdAssigned])
override def receive: Receive = main(Map.empty, Map.empty) override def receive: Receive = main(Map.empty, Map.empty, Map.empty)
def main(channels: Map[BinaryData, ActorRef], shortIds: Map[Long, BinaryData]): Receive = { def main(channels: Map[BinaryData, ActorRef], shortIds: Map[Long, BinaryData], channelsTo: Map[BinaryData, PublicKey]): Receive = {
case ChannelCreated(channel, _, _, _, temporaryChannelId) => case ChannelCreated(channel, _, remoteNodeId, _, temporaryChannelId) =>
context.watch(channel) context.watch(channel)
context become main(channels + (temporaryChannelId -> channel), shortIds) context become main(channels + (temporaryChannelId -> channel), shortIds, channelsTo + (temporaryChannelId -> remoteNodeId))
case ChannelRestored(channel, _, _, _, channelId, _) => case ChannelRestored(channel, _, remoteNodeId, _, channelId, _) =>
context.watch(channel) context.watch(channel)
context become main(channels + (channelId -> channel), shortIds) context become main(channels + (channelId -> channel), shortIds, channelsTo + (channelId -> remoteNodeId))
case ChannelIdAssigned(channel, temporaryChannelId, channelId) => case ChannelIdAssigned(channel, remoteNodeId, temporaryChannelId, channelId) =>
context become main(channels + (channelId -> channel) - temporaryChannelId, shortIds) context become main(channels + (channelId -> channel) - temporaryChannelId, shortIds, channelsTo + (channelId -> remoteNodeId) - temporaryChannelId)
case ShortChannelIdAssigned(channel, channelId, shortChannelId) => case ShortChannelIdAssigned(_, channelId, shortChannelId) =>
context become main(channels, shortIds + (shortChannelId -> channelId)) context become main(channels, shortIds + (shortChannelId -> channelId), channelsTo)
case Terminated(actor) if channels.values.toSet.contains(actor) => case Terminated(actor) if channels.values.toSet.contains(actor) =>
val channelId = channels.find(_._2 == actor).get._1 val channelId = channels.find(_._2 == actor).get._1
val shortChannelId = shortIds.find(_._2 == channelId).map(_._1).getOrElse(0L) val shortChannelId = shortIds.find(_._2 == channelId).map(_._1).getOrElse(0L)
context become main(channels - channelId, shortIds - shortChannelId) context become main(channels - channelId, shortIds - shortChannelId, channelsTo - channelId)
case 'channels => sender ! channels case 'channels => sender ! channels
case 'shortIds => sender ! shortIds case 'shortIds => sender ! shortIds
case 'channelsTo => sender ! channelsTo
case fwd@Forward(channelId, msg) => case fwd@Forward(channelId, msg) =>
channels.get(channelId) match { channels.get(channelId) match {
case Some(channel) => channel forward msg case Some(channel) => channel forward msg
@ -63,6 +66,6 @@ object Register {
case class ForwardShortId[T](shortChannelId: Long, message: T) case class ForwardShortId[T](shortChannelId: Long, message: T)
case class ForwardFailure[T](fwd: Forward[T]) extends RuntimeException(s"channel ${fwd.channelId} not found") case class ForwardFailure[T](fwd: Forward[T]) extends RuntimeException(s"channel ${fwd.channelId} not found")
case class ForwardShortIdFailure[T](fwd: ForwardShortId[T]) extends RuntimeException(s"channel ${fwd.shortChannelId} not found") case class ForwardShortIdFailure[T](fwd: ForwardShortId[T]) extends RuntimeException(s"channel ${fwd.shortChannelId.toHexString} not found")
// @formatter:on // @formatter:on
} }

View file

@ -195,7 +195,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, address_opt: Option[
} }
stay stay
case Event(ChannelIdAssigned(channel, temporaryChannelId, channelId), d@ConnectedData(_, _, channels)) if channels.contains(TemporaryChannelId(temporaryChannelId)) => case Event(ChannelIdAssigned(channel, _, temporaryChannelId, channelId), d@ConnectedData(_, _, channels)) if channels.contains(TemporaryChannelId(temporaryChannelId)) =>
log.info(s"channel id switch: previousId=$temporaryChannelId nextId=$channelId") log.info(s"channel id switch: previousId=$temporaryChannelId nextId=$channelId")
// NB: we keep the temporary channel id because the switch is not always acknowledged at this point (see https://github.com/lightningnetwork/lightning-rfc/pull/151) // NB: we keep the temporary channel id because the switch is not always acknowledged at this point (see https://github.com/lightningnetwork/lightning-rfc/pull/151)
// we won't clean it up, but we won't remember the temporary id on channel termination // we won't clean it up, but we won't remember the temporary id on channel termination

View file

@ -21,9 +21,9 @@ object PaymentHop {
* @param msat an amount to send to a payment recipient * @param msat an amount to send to a payment recipient
* @return a sequence of extra hops with a pre-calculated fee for a given msat amount * @return a sequence of extra hops with a pre-calculated fee for a given msat amount
*/ */
def buildExtra(reversePath: Seq[Hop], msat: Long): Seq[ExtraHop] = (List.empty[ExtraHop] /: reversePath) { def buildExtra(reversePath: Seq[Hop], msat: Long): Seq[ExtraHop] = reversePath.foldLeft(List.empty[ExtraHop]) {
case (Nil, hop) => ExtraHop(hop.nodeId, hop.shortChannelId, hop.nextFee(msat), hop.cltvExpiryDelta) :: Nil case (Nil, hop) => ExtraHop(hop.nodeId, hop.shortChannelId, hop.feeBaseMsat, hop.feeProportionalMillionths, hop.cltvExpiryDelta) :: Nil
case (head :: rest, hop) => ExtraHop(hop.nodeId, hop.shortChannelId, hop.nextFee(msat + head.fee), hop.cltvExpiryDelta) :: head :: rest case (head :: rest, hop) => ExtraHop(hop.nodeId, hop.shortChannelId, hop.feeBaseMsat, hop.feeProportionalMillionths, hop.cltvExpiryDelta) :: head :: rest
} }
} }
@ -40,6 +40,10 @@ trait PaymentHop {
case class Hop(nodeId: PublicKey, nextNodeId: PublicKey, lastUpdate: ChannelUpdate) extends PaymentHop { case class Hop(nodeId: PublicKey, nextNodeId: PublicKey, lastUpdate: ChannelUpdate) extends PaymentHop {
def nextFee(msat: Long): Long = PaymentHop.nodeFee(lastUpdate.feeBaseMsat, lastUpdate.feeProportionalMillionths, msat) def nextFee(msat: Long): Long = PaymentHop.nodeFee(lastUpdate.feeBaseMsat, lastUpdate.feeProportionalMillionths, msat)
def feeBaseMsat: Long = lastUpdate.feeBaseMsat
def feeProportionalMillionths: Long = lastUpdate.feeProportionalMillionths
def cltvExpiryDelta: Int = lastUpdate.cltvExpiryDelta def cltvExpiryDelta: Int = lastUpdate.cltvExpiryDelta
def shortChannelId: Long = lastUpdate.shortChannelId def shortChannelId: Long = lastUpdate.shortChannelId

View file

@ -15,7 +15,7 @@ import scala.util.Try
/** /**
* Lightning Payment Request * Lightning Payment Request
* see https://github.com/lightningnetwork/lightning-rfc/pull/183 * see https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md
* *
* @param prefix currency prefix; lnbc for bitcoin, lntb for bitcoin testnet * @param prefix currency prefix; lnbc for bitcoin, lntb for bitcoin testnet
* @param amount amount to pay (empty string means no amount is specified) * @param amount amount to pay (empty string means no amount is specified)
@ -217,15 +217,16 @@ object PaymentRequest {
* *
* @param nodeId node id * @param nodeId node id
* @param shortChannelId channel id * @param shortChannelId channel id
* @param fee node fee * @param feeBaseMast node fixed fee
* @param feeProportionalMillionths node proportional fee
* @param cltvExpiryDelta node cltv expiry delta * @param cltvExpiryDelta node cltv expiry delta
*/ */
case class ExtraHop(nodeId: PublicKey, shortChannelId: Long, fee: Long, cltvExpiryDelta: Int) extends PaymentHop { case class ExtraHop(nodeId: PublicKey, shortChannelId: Long, feeBaseMast: Long, feeProportionalMillionths: Long, cltvExpiryDelta: Int) extends PaymentHop {
def pack: Seq[Byte] = nodeId.toBin ++ Protocol.writeUInt64(shortChannelId, ByteOrder.BIG_ENDIAN) ++ def pack: Seq[Byte] = nodeId.toBin ++ Protocol.writeUInt64(shortChannelId, ByteOrder.BIG_ENDIAN) ++
Protocol.writeUInt64(fee, ByteOrder.BIG_ENDIAN) ++ Protocol.writeUInt16(cltvExpiryDelta, ByteOrder.BIG_ENDIAN) Protocol.writeUInt32(feeBaseMast, ByteOrder.BIG_ENDIAN) ++ Protocol.writeUInt32(feeProportionalMillionths, ByteOrder.BIG_ENDIAN) ++ Protocol.writeUInt16(cltvExpiryDelta, ByteOrder.BIG_ENDIAN)
// Fee is already pre-calculated for extra hops // Fee is already pre-calculated for extra hops
def nextFee(msat: Long): Long = fee def nextFee(msat: Long): Long = PaymentHop.nodeFee(feeBaseMast, feeProportionalMillionths, msat)
} }
/** /**
@ -244,15 +245,16 @@ object PaymentRequest {
def parse(data: Seq[Byte]) = { def parse(data: Seq[Byte]) = {
val pubkey = data.slice(0, 33) val pubkey = data.slice(0, 33)
val shortChannelId = Protocol.uint64(data.slice(33, 33 + 8), ByteOrder.BIG_ENDIAN) val shortChannelId = Protocol.uint64(data.slice(33, 33 + 8), ByteOrder.BIG_ENDIAN)
val fee = Protocol.uint64(data.slice(33 + 8, 33 + 8 + 8), ByteOrder.BIG_ENDIAN) val fee_base_msat = Protocol.uint32(data.slice(33 + 8, 33 + 8 + 4), ByteOrder.BIG_ENDIAN)
val fee_proportional_millionths = Protocol.uint32(data.slice(33 + 8 + 4, 33 + 8 + 8), ByteOrder.BIG_ENDIAN)
val cltv = Protocol.uint16(data.slice(33 + 8 + 8, chunkLength), ByteOrder.BIG_ENDIAN) val cltv = Protocol.uint16(data.slice(33 + 8 + 8, chunkLength), ByteOrder.BIG_ENDIAN)
ExtraHop(PublicKey(pubkey), shortChannelId, fee, cltv) ExtraHop(PublicKey(pubkey), shortChannelId, fee_base_msat, fee_proportional_millionths, cltv)
} }
def parseAll(data: Seq[Byte]): Seq[ExtraHop] = def parseAll(data: Seq[Byte]): Seq[ExtraHop] =
data.grouped(chunkLength).map(parse).toList data.grouped(chunkLength).map(parse).toList
val chunkLength: Int = 33 + 8 + 8 + 2 val chunkLength: Int = 33 + 8 + 4 + 4 + 2
} }
/** /**

View file

@ -88,7 +88,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR
case Some(channelUpdate) if add.expiry < Globals.blockCount.get() + 3 => // TODO: hardcoded value case Some(channelUpdate) if add.expiry < Globals.blockCount.get() + 3 => // TODO: hardcoded value
sender ! CMD_FAIL_HTLC(add.id, Right(ExpiryTooSoon(channelUpdate)), commit = true) sender ! CMD_FAIL_HTLC(add.id, Right(ExpiryTooSoon(channelUpdate)), commit = true)
case _ => case _ =>
log.info(s"forwarding htlc #${add.id} to shortChannelId=${perHopPayload.channel_id}") log.info(s"forwarding htlc #${add.id} to shortChannelId=${perHopPayload.channel_id.toHexString}")
register ! Register.ForwardShortId(perHopPayload.channel_id, CMD_ADD_HTLC(perHopPayload.amtToForward, add.paymentHash, perHopPayload.outgoingCltvValue, nextPacket.serialize, upstream_opt = Some(add), commit = true)) register ! Register.ForwardShortId(perHopPayload.channel_id, CMD_ADD_HTLC(perHopPayload.amtToForward, add.paymentHash, perHopPayload.outgoingCltvValue, nextPacket.serialize, upstream_opt = Some(add), commit = true))
} }
case Success((Attempt.Failure(cause), _, _)) => case Success((Attempt.Failure(cause), _, _)) =>
@ -100,14 +100,14 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR
sender ! CMD_FAIL_MALFORMED_HTLC(add.id, Crypto.sha256(add.onionRoutingPacket), failureCode = FailureMessageCodecs.BADONION, commit = true) sender ! CMD_FAIL_MALFORMED_HTLC(add.id, Crypto.sha256(add.onionRoutingPacket), failureCode = FailureMessageCodecs.BADONION, commit = true)
} }
case Register.ForwardShortIdFailure(Register.ForwardShortId(shortChannelId, CMD_ADD_HTLC(_, _, _, _, Some(add), _))) => case Status.Failure(Register.ForwardShortIdFailure(Register.ForwardShortId(shortChannelId, CMD_ADD_HTLC(_, _, _, _, Some(add), _)))) =>
log.warning(s"couldn't resolve downstream channel $shortChannelId, failing htlc #${add.id}") log.warning(s"couldn't resolve downstream channel $shortChannelId, failing htlc #${add.id}")
register ! Register.Forward(add.channelId, CMD_FAIL_HTLC(add.id, Right(UnknownNextPeer), commit = true)) register ! Register.Forward(add.channelId, CMD_FAIL_HTLC(add.id, Right(UnknownNextPeer), commit = true))
case AddHtlcFailed(_, error, Local(Some(sender)), _) => case Status.Failure(AddHtlcFailed(_, error, Local(Some(sender)), _)) =>
sender ! Status.Failure(error) sender ! Status.Failure(error)
case AddHtlcFailed(_, error, Relayed(originChannelId, originHtlcId, _, _), channelUpdate_opt) => case Status.Failure(AddHtlcFailed(_, error, Relayed(originChannelId, originHtlcId, _, _), channelUpdate_opt)) =>
val failure = (error, channelUpdate_opt) match { val failure = (error, channelUpdate_opt) match {
case (_: ChannelUnavailable, Some(channelUpdate)) if !Announcements.isEnabled(channelUpdate.flags) => ChannelDisabled(channelUpdate.flags, channelUpdate) case (_: ChannelUnavailable, Some(channelUpdate)) if !Announcements.isEnabled(channelUpdate.flags) => ChannelDisabled(channelUpdate.flags, channelUpdate)
case (_: InsufficientFunds, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate) case (_: InsufficientFunds, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
@ -145,6 +145,8 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR
case ForwardFailMalformed(fail, Relayed(originChannelId, originHtlcId, _, _)) => case ForwardFailMalformed(fail, Relayed(originChannelId, originHtlcId, _, _)) =>
val cmd = CMD_FAIL_MALFORMED_HTLC(originHtlcId, fail.onionHash, fail.failureCode, commit = true) val cmd = CMD_FAIL_MALFORMED_HTLC(originHtlcId, fail.onionHash, fail.failureCode, commit = true)
register ! Register.Forward(originChannelId, cmd) register ! Register.Forward(originChannelId, cmd)
case "ok" => () // ignoring responses from channels
} }
} }

View file

@ -249,12 +249,12 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data]
case Event(WatchEventSpentBasic(BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(shortChannelId)), d) case Event(WatchEventSpentBasic(BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(shortChannelId)), d)
if d.channels.containsKey(shortChannelId) => if d.channels.containsKey(shortChannelId) =>
val lostChannel = d.channels(shortChannelId) val lostChannel = d.channels(shortChannelId)
log.info(s"funding tx of channelId=$shortChannelId has been spent") log.info(s"funding tx of channelId=${shortChannelId.toHexString} has been spent")
// we need to remove nodes that aren't tied to any channels anymore // we need to remove nodes that aren't tied to any channels anymore
val channels1 = d.channels - lostChannel.shortChannelId val channels1 = d.channels - lostChannel.shortChannelId
val lostNodes = Seq(lostChannel.nodeId1, lostChannel.nodeId2).filterNot(nodeId => hasChannels(nodeId, channels1.values)) val lostNodes = Seq(lostChannel.nodeId1, lostChannel.nodeId2).filterNot(nodeId => hasChannels(nodeId, channels1.values))
// let's clean the db and send the events // let's clean the db and send the events
log.info(s"pruning shortChannelId=$shortChannelId (spent)") log.info(s"pruning shortChannelId=${shortChannelId.toHexString} (spent)")
db.removeChannel(shortChannelId) // NB: this also removes channel updates db.removeChannel(shortChannelId) // NB: this also removes channel updates
context.system.eventStream.publish(ChannelLost(shortChannelId)) context.system.eventStream.publish(ChannelLost(shortChannelId))
lostNodes.foreach { lostNodes.foreach {
@ -287,7 +287,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data]
// let's clean the db and send the events // let's clean the db and send the events
staleChannels.foreach { staleChannels.foreach {
case shortChannelId => case shortChannelId =>
log.info(s"pruning shortChannelId=$shortChannelId (stale)") log.info(s"pruning shortChannelId=${shortChannelId.toHexString} (stale)")
db.removeChannel(shortChannelId) // NB: this also removes channel updates db.removeChannel(shortChannelId) // NB: this also removes channel updates
context.system.eventStream.publish(ChannelLost(shortChannelId)) context.system.eventStream.publish(ChannelLost(shortChannelId))
} }
@ -301,12 +301,12 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data]
case Event(ExcludeChannel(desc@ChannelDesc(shortChannelId, nodeId, _)), d) => case Event(ExcludeChannel(desc@ChannelDesc(shortChannelId, nodeId, _)), d) =>
val banDuration = nodeParams.channelExcludeDuration val banDuration = nodeParams.channelExcludeDuration
log.info(s"excluding shortChannelId=$shortChannelId from nodeId=$nodeId for duration=$banDuration") log.info(s"excluding shortChannelId=${shortChannelId.toHexString} from nodeId=$nodeId for duration=$banDuration")
context.system.scheduler.scheduleOnce(banDuration, self, LiftChannelExclusion(desc)) context.system.scheduler.scheduleOnce(banDuration, self, LiftChannelExclusion(desc))
stay using d.copy(excludedChannels = d.excludedChannels + desc) stay using d.copy(excludedChannels = d.excludedChannels + desc)
case Event(LiftChannelExclusion(desc@ChannelDesc(shortChannelId, nodeId, _)), d) => case Event(LiftChannelExclusion(desc@ChannelDesc(shortChannelId, nodeId, _)), d) =>
log.info(s"reinstating shortChannelId=$shortChannelId from nodeId=$nodeId") log.info(s"reinstating shortChannelId=${shortChannelId.toHexString} from nodeId=$nodeId")
stay using d.copy(excludedChannels = d.excludedChannels - desc) stay using d.copy(excludedChannels = d.excludedChannels - desc)
case Event('nodes, d) => case Event('nodes, d) =>

View file

@ -142,7 +142,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
test("connect nodes") { test("connect nodes") {
// //
// A ---- B ---- C ---- D // A ---- B ---- C ==== D
// | / \ // | / \
// --E--' F{1,2,3,4,5} // --E--' F{1,2,3,4,5}
// //
@ -154,15 +154,16 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
connect(nodes("A"), nodes("B"), 10000000, 0) connect(nodes("A"), nodes("B"), 10000000, 0)
connect(nodes("B"), nodes("C"), 2000000, 0) connect(nodes("B"), nodes("C"), 2000000, 0)
connect(nodes("C"), nodes("D"), 5000000, 0) connect(nodes("C"), nodes("D"), 5000000, 0)
connect(nodes("B"), nodes("E"), 5000000, 0) connect(nodes("C"), nodes("D"), 5000000, 0)
connect(nodes("E"), nodes("C"), 5000000, 0) connect(nodes("B"), nodes("E"), 10000000, 0)
connect(nodes("E"), nodes("C"), 10000000, 0)
connect(nodes("C"), nodes("F1"), 5000000, 0) connect(nodes("C"), nodes("F1"), 5000000, 0)
connect(nodes("C"), nodes("F2"), 5000000, 0) connect(nodes("C"), nodes("F2"), 5000000, 0)
connect(nodes("C"), nodes("F3"), 5000000, 0) connect(nodes("C"), nodes("F3"), 5000000, 0)
connect(nodes("C"), nodes("F4"), 5000000, 0) connect(nodes("C"), nodes("F4"), 5000000, 0)
connect(nodes("C"), nodes("F5"), 5000000, 0) connect(nodes("C"), nodes("F5"), 5000000, 0)
val numberOfChannels = 10 val numberOfChannels = 11
val channelEndpointsCount = 2 * numberOfChannels val channelEndpointsCount = 2 * numberOfChannels
// we make sure all channels have set up their WatchConfirmed for the funding tx // we make sure all channels have set up their WatchConfirmed for the funding tx
@ -211,7 +212,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
// generating more blocks so that all funding txes are buried under at least 6 blocks // generating more blocks so that all funding txes are buried under at least 6 blocks
sender.send(bitcoincli, BitcoinReq("generate", 4)) sender.send(bitcoincli, BitcoinReq("generate", 4))
sender.expectMsgType[JValue] sender.expectMsgType[JValue]
awaitAnnouncements(nodes, 10, 10, 20) awaitAnnouncements(nodes, 10, 11, 22)
} }
test("send an HTLC A->D") { test("send an HTLC A->D") {
@ -226,14 +227,15 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
sender.expectMsgType[PaymentSucceeded] sender.expectMsgType[PaymentSucceeded]
} }
test("send an HTLC A->D with an invalid expiry delta for C") { // TODO: reenable this test when we support route hints
ignore("send an HTLC A->D with an invalid expiry delta for B") {
val sender = TestProbe() val sender = TestProbe()
// to simulate this, we will update C's relay params // to simulate this, we will update B's relay params
// first we find out the short channel id for channel C-D, easiest way is to ask D's register which has only one channel // first we find out the short channel id for channel B-C
sender.send(nodes("D").register, 'shortIds) sender.send(nodes("B").router, 'channels)
val shortIdCD = sender.expectMsgType[Map[Long, BinaryData]].keys.head val shortIdBC = sender.expectMsgType[Iterable[ChannelAnnouncement]].find(c => Set(c.nodeId1, c.nodeId2) == Set(nodes("B").nodeParams.privateKey.publicKey, nodes("C").nodeParams.privateKey.publicKey)).get.shortChannelId
val channelUpdateCD = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, nodes("C").nodeParams.privateKey, nodes("D").nodeParams.privateKey.publicKey, shortIdCD, nodes("D").nodeParams.expiryDeltaBlocks + 1, nodes("D").nodeParams.htlcMinimumMsat, nodes("D").nodeParams.feeBaseMsat, nodes("D").nodeParams.feeProportionalMillionth) val channelUpdateBC = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, nodes("B").nodeParams.privateKey, nodes("C").nodeParams.privateKey.publicKey, shortIdBC, nodes("B").nodeParams.expiryDeltaBlocks + 1, nodes("C").nodeParams.htlcMinimumMsat, nodes("B").nodeParams.feeBaseMsat, nodes("B").nodeParams.feeProportionalMillionth)
sender.send(nodes("C").relayer, channelUpdateCD) sender.send(nodes("B").relayer, channelUpdateBC)
// first we retrieve a payment hash from D // first we retrieve a payment hash from D
val amountMsat = MilliSatoshi(4200000) val amountMsat = MilliSatoshi(4200000)
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
@ -241,23 +243,22 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
// then we make the actual payment // then we make the actual payment
val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey) val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
sender.send(nodes("A").paymentInitiator, sendReq) sender.send(nodes("A").paymentInitiator, sendReq)
// A will receive an error from C that include the updated channel update, then will retry the payment // A will receive an error from B that include the updated channel update, then will retry the payment
sender.expectMsgType[PaymentSucceeded](5 seconds) sender.expectMsgType[PaymentSucceeded](5 seconds)
// in the meantime, the router will have updated its state // in the meantime, the router will have updated its state
awaitCond({ awaitCond({
sender.send(nodes("A").router, 'updates) sender.send(nodes("A").router, 'updates)
sender.expectMsgType[Iterable[ChannelUpdate]].toSeq.contains(channelUpdateCD) sender.expectMsgType[Iterable[ChannelUpdate]].toSeq.contains(channelUpdateBC)
}, max = 20 seconds, interval = 1 second) }, max = 20 seconds, interval = 1 second)
// finally we retry the same payment, this time successfully
} }
test("send an HTLC A->D with an amount greater than capacity of C-D") { test("send an HTLC A->D with an amount greater than capacity of B-C") {
val sender = TestProbe() val sender = TestProbe()
// first we retrieve a payment hash from D // first we retrieve a payment hash from D
val amountMsat = MilliSatoshi(300000000L) val amountMsat = MilliSatoshi(300000000L)
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
val pr = sender.expectMsgType[PaymentRequest] val pr = sender.expectMsgType[PaymentRequest]
// then we make the payment (C-D has a smaller capacity than A-B and B-C) // then we make the payment (B-C has a smaller capacity than A-B and C-D)
val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey) val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
sender.send(nodes("A").paymentInitiator, sendReq) sender.send(nodes("A").paymentInitiator, sendReq)
// A will first receive an error from C, then retry and route around C: A->B->E->C->D // A will first receive an error from C, then retry and route around C: A->B->E->C->D
@ -269,7 +270,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
val pr = SendPayment(100000000L, "42" * 32, nodes("D").nodeParams.privateKey.publicKey) val pr = SendPayment(100000000L, "42" * 32, nodes("D").nodeParams.privateKey.publicKey)
sender.send(nodes("A").paymentInitiator, pr) sender.send(nodes("A").paymentInitiator, pr)
// A will first receive an error from C, then retry and route around C: A->B->E->C->D // A will receive an error from D and won't retry
val failed = sender.expectMsgType[PaymentFailed] val failed = sender.expectMsgType[PaymentFailed]
assert(failed.paymentHash === pr.paymentHash) assert(failed.paymentHash === pr.paymentHash)
assert(failed.failures.size === 1) assert(failed.failures.size === 1)
@ -325,6 +326,22 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
sender.expectMsgType[PaymentSucceeded] sender.expectMsgType[PaymentSucceeded]
} }
test("send multiple HTLCs A->D with a failover when a channel gets exhausted") {
val sender = TestProbe()
// there are two C-D channels with 5000000 sat, so we should be able to make 7 payments worth 1000000 sat each
for (i <- 0 until 7) {
// first we retrieve a payment hash from D for 2 mBTC
val amountMsat = MilliSatoshi(1000000000L)
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 payment"))
val pr = sender.expectMsgType[PaymentRequest]
// A send payment of 3 mBTC, more than asked but it should still be accepted
val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
sender.send(nodes("A").paymentInitiator, sendReq)
sender.expectMsgType[PaymentSucceeded]
}
}
/** /**
* We currently use p2pkh script Helpers.getFinalScriptPubKey * We currently use p2pkh script Helpers.getFinalScriptPubKey
* *
@ -405,7 +422,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString]) val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
(receivedByC diff previouslyReceivedByC).size == 1 (receivedByC diff previouslyReceivedByC).size == 1
}, max = 30 seconds, interval = 1 second) }, max = 30 seconds, interval = 1 second)
awaitAnnouncements(nodes.filter(_._1 == "A"), 9, 9, 18) awaitAnnouncements(nodes.filter(_._1 == "A"), 9, 10, 20)
} }
test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit)") { test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit)") {
@ -470,7 +487,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString]) val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
(receivedByC diff previouslyReceivedByC).size == 1 (receivedByC diff previouslyReceivedByC).size == 1
}, max = 30 seconds, interval = 1 second) }, max = 30 seconds, interval = 1 second)
awaitAnnouncements(nodes.filter(_._1 == "A"), 8, 8, 16) awaitAnnouncements(nodes.filter(_._1 == "A"), 8, 9, 18)
} }
test("propagate a failure upstream when a downstream htlc times out (local commit)") { test("propagate a failure upstream when a downstream htlc times out (local commit)") {
@ -516,7 +533,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString]) val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
(receivedByC diff previouslyReceivedByC).size == 2 (receivedByC diff previouslyReceivedByC).size == 2
}, max = 30 seconds, interval = 1 second) }, max = 30 seconds, interval = 1 second)
awaitAnnouncements(nodes.filter(_._1 == "A"), 7, 7, 14) awaitAnnouncements(nodes.filter(_._1 == "A"), 7, 8, 16)
} }
test("propagate a failure upstream when a downstream htlc times out (remote commit)") { test("propagate a failure upstream when a downstream htlc times out (remote commit)") {
@ -564,7 +581,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString]) val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
(receivedByC diff previouslyReceivedByC).size == 2 (receivedByC diff previouslyReceivedByC).size == 2
}, max = 30 seconds, interval = 1 second) }, max = 30 seconds, interval = 1 second)
awaitAnnouncements(nodes.filter(_._1 == "A"), 6, 6, 12) awaitAnnouncements(nodes.filter(_._1 == "A"), 6, 7, 14)
} }
test("punish a node that has published a revoked commit tx") { test("punish a node that has published a revoked commit tx") {
@ -616,7 +633,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
(receivedByC diff previouslyReceivedByC).size == 2 (receivedByC diff previouslyReceivedByC).size == 2
}, max = 30 seconds, interval = 1 second) }, max = 30 seconds, interval = 1 second)
// this will remove the channel // this will remove the channel
awaitAnnouncements(nodes.filter(_._1 == "A"), 5, 5, 10) awaitAnnouncements(nodes.filter(_._1 == "A"), 5, 6, 12)
} }
test("generate and validate lots of channels") { test("generate and validate lots of channels") {
@ -640,7 +657,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
announcements.foreach(ann => nodes("A").router ! ann) announcements.foreach(ann => nodes("A").router ! ann)
awaitCond({ awaitCond({
sender.send(nodes("D").router, 'channels) sender.send(nodes("D").router, 'channels)
sender.expectMsgType[Iterable[ChannelAnnouncement]](5 seconds).size == channels.size + 5 // 5 remaining channels because D->F{1-F4} have disappeared sender.expectMsgType[Iterable[ChannelAnnouncement]](5 seconds).size == channels.size + 6 // 6 remaining channels because D->F{1-F4} have disappeared
}, max = 120 seconds, interval = 1 second) }, max = 120 seconds, interval = 1 second)
} }

View file

@ -97,7 +97,7 @@ class PaymentRequestSpec extends FunSuite {
} }
test("On mainnet, with fallback address 1RustyRX2oai4EYYDpQGWvEL62BBGqN9T with extra routing info to go via nodes 029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 then 039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255") { test("On mainnet, with fallback address 1RustyRX2oai4EYYDpQGWvEL62BBGqN9T with extra routing info to go via nodes 029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 then 039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255") {
val ref = "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqqqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqqqqqqq7qqzqfnlkwydm8rg30gjku7wmxmk06sevjp53fmvrcfegvwy7d5443jvyhxsel0hulkstws7vqv400q4j3wgpk4crg49682hr4scqvmad43cqd5m7tf" val ref = "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqj9n4evl6mr5aj9f58zp6fyjzup6ywn3x6sk8akg5v4tgn2q8g4fhx05wf6juaxu9760yp46454gpg5mtzgerlzezqcqvjnhjh8z3g2qqdhhwkj"
val pr = PaymentRequest.read(ref) val pr = PaymentRequest.read(ref)
assert(pr.prefix == "lnbc") assert(pr.prefix == "lnbc")
assert(pr.amount === Some(MilliSatoshi(2000000000L))) assert(pr.amount === Some(MilliSatoshi(2000000000L)))
@ -106,9 +106,12 @@ class PaymentRequestSpec extends FunSuite {
assert(pr.nodeId == PublicKey(BinaryData("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"))) assert(pr.nodeId == PublicKey(BinaryData("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")))
assert(pr.description == Right(Crypto.sha256("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes))) assert(pr.description == Right(Crypto.sha256("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))
assert(pr.fallbackAddress === Some("1RustyRX2oai4EYYDpQGWvEL62BBGqN9T")) assert(pr.fallbackAddress === Some("1RustyRX2oai4EYYDpQGWvEL62BBGqN9T"))
assert(pr.routingInfo() === List(RoutingInfoTag(List(ExtraHop(PublicKey("029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), 72623859790382856L, 20, 3), ExtraHop(PublicKey("039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), 217304205466536202L, 30, 4))))) assert(pr.routingInfo() === List(RoutingInfoTag(List(
assert(BinaryData(Protocol.writeUInt64(72623859790382856L, ByteOrder.BIG_ENDIAN)) == BinaryData("0102030405060708")) ExtraHop(PublicKey("029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), 72623859790382856L, 1, 20, 3),
assert(BinaryData(Protocol.writeUInt64(217304205466536202L, ByteOrder.BIG_ENDIAN)) == BinaryData("030405060708090a")) ExtraHop(PublicKey("039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), 217304205466536202L, 2, 30, 4)
))))
assert(BinaryData(Protocol.writeUInt64(0x0102030405060708L, ByteOrder.BIG_ENDIAN)) == BinaryData("0102030405060708"))
assert(BinaryData(Protocol.writeUInt64(0x030405060708090aL, ByteOrder.BIG_ENDIAN)) == BinaryData("030405060708090a"))
assert(pr.tags.size == 4) assert(pr.tags.size == 4)
assert(PaymentRequest.write(pr.sign(priv)) == ref) assert(PaymentRequest.write(pr.sign(priv)) == ref)
} }

View file

@ -1,9 +1,10 @@
package fr.acinq.eclair.payment package fr.acinq.eclair.payment
import akka.actor.ActorRef import akka.actor.{ActorRef, Status}
import akka.testkit.TestProbe import akka.testkit.TestProbe
import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{BinaryData, Crypto, MilliSatoshi} import fr.acinq.bitcoin.{BinaryData, Crypto, MilliSatoshi}
import fr.acinq.eclair.randomBytes
import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.PaymentLifecycle.buildCommand import fr.acinq.eclair.payment.PaymentLifecycle.buildCommand
@ -37,12 +38,8 @@ class RelayerSpec extends TestkitBaseClass {
} }
} }
// node c is the next node in the route val channelId_ab: BinaryData = randomBytes(32)
val nodeId_a = PublicKey(a) val channelId_bc: BinaryData = randomBytes(32)
val nodeId_c = PublicKey(c)
val channelId_ab: BinaryData = "65514354" * 8
val channelId_bc: BinaryData = "64864544" * 8
val channel_flags = 0x00.toByte
def makeCommitments(channelId: BinaryData) = Commitments(null, null, 0.toByte, null, null, null, null, 0, 0, Map.empty, null, null, null, channelId) def makeCommitments(channelId: BinaryData) = Commitments(null, null, 0.toByte, null, null, null, null, 0, 0, Map.empty, null, null, null, channelId)
@ -83,6 +80,32 @@ class RelayerSpec extends TestkitBaseClass {
paymentHandler.expectNoMsg(500 millis) paymentHandler.expectNoMsg(500 millis)
} }
test("fail to relay an htlc-add when register returns an error") { case (relayer, register, paymentHandler) =>
val sender = TestProbe()
// we use this to build a valid onion
val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops)
// and then manually build an htlc
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion)
relayer ! channelUpdate_bc
sender.send(relayer, ForwardAdd(add_ab))
val fwd1 = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]]
assert(fwd1.shortChannelId === channelUpdate_bc.shortChannelId)
assert(fwd1.message.upstream_opt === Some(add_ab))
sender.send(relayer, Status.Failure(Register.ForwardShortIdFailure(fwd1)))
val fwd2 = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]]
assert(fwd2.channelId === channelId_ab)
assert(fwd2.message.id === add_ab.id)
assert(fwd2.message.reason === Right(UnknownNextPeer))
register.expectNoMsg(500 millis)
paymentHandler.expectNoMsg(500 millis)
}
test("fail to relay an htlc-add when the requested channel is disabled") { case (relayer, register, paymentHandler) => test("fail to relay an htlc-add when the requested channel is disabled") { case (relayer, register, paymentHandler) =>
val sender = TestProbe() val sender = TestProbe()
@ -232,7 +255,7 @@ class RelayerSpec extends TestkitBaseClass {
assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId) assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId)
assert(fwd.message.upstream_opt === Some(add_ab)) assert(fwd.message.upstream_opt === Some(add_ab))
sender.send(relayer, AddHtlcFailed(channelId_bc, new InsufficientFunds(channelId_bc, cmd.amountMsat, 100, 0, 0), Relayed(add_ab.channelId, add_ab.id, add_ab.amountMsat, cmd.amountMsat), Some(channelUpdate_bc))) sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, new InsufficientFunds(channelId_bc, cmd.amountMsat, 100, 0, 0), Relayed(add_ab.channelId, add_ab.id, add_ab.amountMsat, cmd.amountMsat), Some(channelUpdate_bc))))
val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(fail.id === add_ab.id) assert(fail.id === add_ab.id)
@ -256,7 +279,7 @@ class RelayerSpec extends TestkitBaseClass {
assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId) assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId)
assert(fwd.message.upstream_opt === Some(add_ab)) assert(fwd.message.upstream_opt === Some(add_ab))
sender.send(relayer, AddHtlcFailed(channelId_bc, new TooManyAcceptedHtlcs(channelId_bc, 30), Relayed(add_ab.channelId, add_ab.id, add_ab.amountMsat, cmd.amountMsat), Some(channelUpdate_bc))) sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, new TooManyAcceptedHtlcs(channelId_bc, 30), Relayed(add_ab.channelId, add_ab.id, add_ab.amountMsat, cmd.amountMsat), Some(channelUpdate_bc))))
val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(fail.id === add_ab.id) assert(fail.id === add_ab.id)
@ -280,7 +303,7 @@ class RelayerSpec extends TestkitBaseClass {
assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId) assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId)
assert(fwd.message.upstream_opt === Some(add_ab)) assert(fwd.message.upstream_opt === Some(add_ab))
sender.send(relayer, AddHtlcFailed(channelId_bc, new HtlcTimedout(channelId_bc), Relayed(add_ab.channelId, add_ab.id, add_ab.amountMsat, cmd.amountMsat), Some(channelUpdate_bc))) sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, new HtlcTimedout(channelId_bc), Relayed(add_ab.channelId, add_ab.id, add_ab.amountMsat, cmd.amountMsat), Some(channelUpdate_bc))))
val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(fail.id === add_ab.id) assert(fail.id === add_ab.id)

View file

@ -200,8 +200,10 @@ class RouteCalculationSpec extends FunSuite {
val reverseRoute = List(hopBA, hopCB) val reverseRoute = List(hopBA, hopCB)
val extraRoute = PaymentHop.buildExtra(reverseRoute, amount.amount) val extraRoute = PaymentHop.buildExtra(reverseRoute, amount.amount)
assert(extraRoute === List(ExtraHop(PublicKey("02f0b230e53723ccc331db140edc518be1ee5ab29a508104a4be2f5be922c928e8"), 24412456671576064L, 547005, 144), assert(extraRoute === List(
ExtraHop(PublicKey("032b4af42b5e8089a7a06005ead9ac4667527390ee39c998b7b0307f0d81d7f4ac"), 23366821113626624L, 547000, 144))) ExtraHop(PublicKey("02f0b230e53723ccc331db140edc518be1ee5ab29a508104a4be2f5be922c928e8"), 24412456671576064L, 546000, 10, 144),
ExtraHop(PublicKey("032b4af42b5e8089a7a06005ead9ac4667527390ee39c998b7b0307f0d81d7f4ac"), 23366821113626624L, 546000, 10, 144))
)
// Sender side // Sender side

View file

@ -0,0 +1,57 @@
package fr.acinq.eclair.gui.utils
import fr.acinq.bitcoin.MilliSatoshi
import org.junit.runner.RunWith
import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner
@RunWith(classOf[JUnitRunner])
class CoinUtilsSpec extends FunSuite {
test("Convert string amount to the correct BtcAmount") {
val am_btc: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1", CoinUtils.BTC_LABEL)
assert(am_btc == MilliSatoshi(100000000000L))
val am_mbtc: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1", CoinUtils.MILLI_BTC_LABEL)
assert(am_mbtc == MilliSatoshi(100000000L))
val am_sat: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1", CoinUtils.SATOSHI_LABEL)
assert(am_sat == MilliSatoshi(1000))
val am_msat: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1", CoinUtils.MILLI_SATOSHI_LABEL)
assert(am_msat == MilliSatoshi(1))
val am_zero: MilliSatoshi = CoinUtils.convertStringAmountToMsat("0", CoinUtils.MILLI_BTC_LABEL)
assert(am_zero == MilliSatoshi(0))
}
test("Convert decimal string amount to the correct BtcAmount") {
val am_btc_dec: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1.23456789876", CoinUtils.BTC_LABEL)
assert(am_btc_dec == MilliSatoshi(123456789876L))
val am_mbtc_dec: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1.23456789", CoinUtils.MILLI_BTC_LABEL)
assert(am_mbtc_dec == MilliSatoshi(123456789L))
val am_sat_dec: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1.23456789", CoinUtils.SATOSHI_LABEL)
assert(am_sat_dec == MilliSatoshi(1234))
val am_msat_dec: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1.234", CoinUtils.MILLI_SATOSHI_LABEL)
assert(am_msat_dec == MilliSatoshi(1))
}
test("Convert string amount with unknown unit") {
intercept[IllegalArgumentException](CoinUtils.convertStringAmountToMsat("1.23456789876", "foo"))
}
test("Convert string amount with a non numerical amount") {
intercept[NumberFormatException](CoinUtils.convertStringAmountToMsat("1.abcd", CoinUtils.MILLI_BTC_LABEL))
}
test("Convert string amount with an empty amount") {
intercept[NumberFormatException](CoinUtils.convertStringAmountToMsat("", CoinUtils.MILLI_BTC_LABEL))
}
test("Convert string amount with a invalid numerical amount") {
intercept[NumberFormatException](CoinUtils.convertStringAmountToMsat("1.23.45", CoinUtils.MILLI_BTC_LABEL))
}
test("Convert string amount with a negative numerical amount") {
intercept[IllegalArgumentException](CoinUtils.convertStringAmountToMsat("-1", CoinUtils.MILLI_BTC_LABEL))
}
}

View file

@ -66,11 +66,6 @@
<artifactId>maven-jar-plugin</artifactId> <artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version> <version>3.0.2</version>
</plugin> </plugin>
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
<version>2.2.3</version>
</plugin>
<plugin> <plugin>
<groupId>com.github.chrisdchristo</groupId> <groupId>com.github.chrisdchristo</groupId>
<artifactId>capsule-maven-plugin</artifactId> <artifactId>capsule-maven-plugin</artifactId>
@ -87,7 +82,7 @@
<plugin> <plugin>
<groupId>pl.project13.maven</groupId> <groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId> <artifactId>git-commit-id-plugin</artifactId>
<version>2.2.2</version> <version>2.2.3</version>
<executions> <executions>
<execution> <execution>
<goals> <goals>