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

Merge branch 'master' into wip-android

This commit is contained in:
pm47 2017-12-06 13:57:25 +01:00
commit 4afc498226
33 changed files with 873 additions and 487 deletions

6
.dockerignore Normal file
View file

@ -0,0 +1,6 @@
Dockerfile
.dockerignore
.git
**/*.idea
**/*.iml
**/target

49
Dockerfile Normal file
View file

@ -0,0 +1,49 @@
FROM openjdk:8u121-jdk-alpine as BUILD
# Setup maven, we don't use https://hub.docker.com/_/maven/ as it declare .m2 as volume, we loose all mvn cache
# We can alternatively do as proposed by https://github.com/carlossg/docker-maven#packaging-a-local-repository-with-the-image
# this was meant to make the image smaller, but we use multi-stage build so we don't care
RUN apk add --no-cache curl tar bash
ARG MAVEN_VERSION=3.5.2
ARG USER_HOME_DIR="/root"
ARG SHA=707b1f6e390a65bde4af4cdaf2a24d45fc19a6ded00fff02e91626e3e42ceaff
ARG BASE_URL=https://apache.osuosl.org/maven/maven-3/${MAVEN_VERSION}/binaries
RUN mkdir -p /usr/share/maven /usr/share/maven/ref \
&& curl -fsSL -o /tmp/apache-maven.tar.gz ${BASE_URL}/apache-maven-${MAVEN_VERSION}-bin.tar.gz \
&& echo "${SHA} /tmp/apache-maven.tar.gz" | sha256sum -c - \
&& tar -xzf /tmp/apache-maven.tar.gz -C /usr/share/maven --strip-components=1 \
&& rm -f /tmp/apache-maven.tar.gz \
&& ln -s /usr/share/maven/bin/mvn /usr/bin/mvn
ENV MAVEN_HOME /usr/share/maven
ENV MAVEN_CONFIG "$USER_HOME_DIR/.m2"
# Let's fetch eclair dependencies, so that Docker can cache them
# This way we won't have to fetch dependencies again if only the source code changes
# The easiest way to reliably get dependencies is to build the project with no sources
WORKDIR /usr/src
COPY pom.xml pom.xml
COPY eclair-core/pom.xml eclair-core/pom.xml
COPY eclair-node/pom.xml eclair-node/pom.xml
COPY eclair-node-gui/pom.xml eclair-node-gui/pom.xml
RUN mkdir -p eclair-core/src/main/scala && touch eclair-core/src/main/scala/empty.scala
# Blank build. We only care about eclair-node, and we use install because eclair-node depends on eclair-core
RUN mvn install -pl eclair-node -am clean
# Only then do we copy the sources
COPY . .
# And this time we can build in offline mode
RUN mvn package -pl eclair-node -am -DskipTests -o
# It might be good idea to run the tests here, so that the docker build fail if the code is bugged
# We currently use a debian image for runtime because of some jni-related issue with sqlite
FROM openjdk:8u151-jre-slim
WORKDIR /app
# Eclair only needs the eclair-node-*.jar to run
COPY --from=BUILD /usr/src/eclair-node/target/eclair-node-*.jar .
RUN ln `ls` eclair-node
ENTRYPOINT [ "java", "-jar", "eclair-node" ]

View file

@ -15,17 +15,6 @@
<build>
<plugins>
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>revision</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.googlecode.maven-download-plugin</groupId>
<artifactId>download-maven-plugin</artifactId>

View file

@ -52,7 +52,7 @@ eclair {
default-feerate-per-kb = 20000 // default bitcoin core value
max-htlc-value-in-flight-msat = 100000000000 // 1 BTC ~= unlimited
htlc-minimum-msat = 1000000
htlc-minimum-msat = 10000
max-accepted-htlcs = 30
reserve-to-funding-ratio = 0.01 // recommended by BOLT #2
@ -63,7 +63,7 @@ eclair {
expiry-delta-blocks = 144
fee-base-msat = 546000
fee-proportional-millionth = 10
fee-proportional-millionths = 10
// 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
@ -81,21 +81,4 @@ eclair {
auto-reconnect = true
payment-handler = "local"
}
akka {
loggers = ["akka.event.slf4j.Slf4jLogger"]
loglevel = "DEBUG"
actor {
debug {
# enable DEBUG logging of all LoggingFSMs for events, transitions and timers
fsm = on
}
}
http {
host-connection-pool {
max-open-requests = 64
}
}
}

View file

@ -53,7 +53,9 @@ case class NodeParams(extendedPrivateKey: ExtendedPrivateKey,
chainHash: BinaryData,
channelFlags: Byte,
channelExcludeDuration: FiniteDuration,
watcherType: WatcherType)
watcherType: WatcherType) {
val nodeId = privateKey.publicKey
}
object NodeParams {
@ -140,7 +142,7 @@ object NodeParams {
minDepthBlocks = config.getInt("mindepth-blocks"),
smartfeeNBlocks = 3,
feeBaseMsat = config.getInt("fee-base-msat"),
feeProportionalMillionth = config.getInt("fee-proportional-millionth"),
feeProportionalMillionth = config.getInt("fee-proportional-millionths"),
reserveToFundingRatio = config.getDouble("reserve-to-funding-ratio"),
maxReserveToFundingRatio = config.getDouble("max-reserve-to-funding-ratio"),
channelsDb = channelsDb,

View file

@ -1,5 +1,7 @@
package fr.acinq.eclair.channel
import java.nio.charset.StandardCharsets
import akka.actor.{ActorRef, FSM, LoggingFSM, OneForOneStrategy, Props, Status, SupervisorStrategy}
import akka.event.Logging.MDC
import akka.pattern.pipe
@ -12,7 +14,7 @@ import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel.Helpers.{Closing, Funding}
import fr.acinq.eclair.crypto.{Generators, ShaChain, Sphinx}
import fr.acinq.eclair.payment._
import fr.acinq.eclair.router.{Announcements, TickBroadcast}
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire.{ChannelReestablish, _}
import org.bitcoinj.script.{Script => BitcoinjScript}
@ -48,6 +50,9 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
import Channel._
// we pass these to helpers classes so that they have the logging context
implicit def implicitLog = log
val forwarder = context.actorOf(Props(new Forwarder(nodeParams)), "forwarder")
// this will be used to detect htlc timeouts
@ -140,15 +145,19 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
// no need to go OFFLINE, we can directly switch to CLOSING
goto(CLOSING) using closing
case d: HasCommitments =>
d match {
case DATA_NORMAL(_, Some(shortChannelId), _, _, _) =>
context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, shortChannelId))
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDeltaBlocks, nodeParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth, enable = false)
relayer ! channelUpdate
case _ => ()
case normal: DATA_NORMAL =>
context.system.eventStream.publish(ShortChannelIdAssigned(self, normal.channelId, normal.channelUpdate.shortChannelId))
// we rebuild a channel_update for two reasons:
// - we want to reload values from configuration
// - if eclair was previously killed, it might not have had time to publish a channel_update with enable=false
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, normal.channelUpdate.shortChannelId, nodeParams.expiryDeltaBlocks, normal.commitments.remoteParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth, enable = false)
if (normal.channelAnnouncement.isDefined) {
router ! channelUpdate
}
goto(OFFLINE) using d
relayer ! channelUpdate
goto(OFFLINE) using normal.copy(channelUpdate = channelUpdate)
case _ => goto(OFFLINE) using data
}
})
@ -199,7 +208,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Event(CMD_CLOSE(_), _) => goto(CLOSED)
case Event(e: Error, _) => handleRemoteErrorNoCommitments(e)
case Event(e: Error, d: DATA_WAIT_FOR_OPEN_CHANNEL) => handleRemoteError(e, d)
case Event(INPUT_DISCONNECTED, _) => goto(CLOSED)
})
@ -238,7 +247,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Event(CMD_CLOSE(_), _) => goto(CLOSED)
case Event(e: Error, _) => handleRemoteErrorNoCommitments(e)
case Event(e: Error, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => handleRemoteError(e, d)
case Event(INPUT_DISCONNECTED, _) => goto(CLOSED)
})
@ -263,12 +272,13 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) =>
log.error(t, s"wallet returned error: ")
val error = Error(d.temporaryChannelId, "aborting channel creation".getBytes)
val exc = ChannelFundingError(d.temporaryChannelId)
val error = Error(d.temporaryChannelId, exc.getMessage.getBytes)
goto(CLOSED) sending error
case Event(CMD_CLOSE(_), _) => goto(CLOSED)
case Event(e: Error, _) => handleRemoteErrorNoCommitments(e)
case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => handleRemoteError(e, d)
case Event(INPUT_DISCONNECTED, _) => goto(CLOSED)
})
@ -284,7 +294,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
Transactions.checkSpendable(signedLocalCommitTx) match {
case Failure(cause) =>
log.error(cause, "their FundingCreated message contains an invalid signature")
val error = Error(temporaryChannelId, cause.getMessage.getBytes)
val exc = InvalidCommitmentSignature(temporaryChannelId, signedLocalCommitTx.tx)
val error = Error(temporaryChannelId, exc.getMessage.getBytes)
// we haven't anything at stake yet, we can just stop
goto(CLOSED) sending error
case Success(_) =>
@ -314,7 +325,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Event(CMD_CLOSE(_), _) => goto(CLOSED)
case Event(e: Error, _) => handleRemoteErrorNoCommitments(e)
case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_CREATED) => handleRemoteError(e, d)
case Event(INPUT_DISCONNECTED, _) => goto(CLOSED)
})
@ -324,11 +335,11 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
// we make sure that their sig checks out and that our first commit tx is spendable
val localSigOfLocalTx = Transactions.sign(localCommitTx, localParams.fundingPrivKey)
val signedLocalCommitTx = Transactions.addSigs(localCommitTx, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig)
Transactions.checkSpendable(fundingTx, signedLocalCommitTx.tx)
.flatMap(_ => Transactions.checkSpendable(signedLocalCommitTx)) match {
Transactions.checkSpendable(signedLocalCommitTx) match {
case Failure(cause) =>
log.error(cause, "their FundingSigned message contains an invalid signature")
val error = Error(channelId, cause.getMessage.getBytes)
val exc = InvalidCommitmentSignature(channelId, signedLocalCommitTx.tx)
val error = Error(channelId, exc.getMessage.getBytes)
// we rollback the funding tx, it will never be published
wallet.rollback(fundingTx)
// we haven't published anything yet, we can just stop
@ -365,7 +376,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_SIGNED) =>
// we rollback the funding tx, it will never be published
wallet.rollback(d.fundingTx)
handleRemoteErrorNoCommitments(e)
handleRemoteError(e, d)
})
when(WAIT_FOR_FUNDING_CONFIRMED)(handleExceptions {
@ -379,27 +390,34 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
val nextPerCommitmentPoint = Generators.perCommitPoint(commitments.localParams.shaSeed, 1)
val fundingLocked = FundingLocked(commitments.channelId, nextPerCommitmentPoint)
deferred.map(self ! _)
goto(WAIT_FOR_FUNDING_LOCKED) using store(DATA_WAIT_FOR_FUNDING_LOCKED(commitments, fundingLocked)) sending fundingLocked
// this is the temporary channel id that we will use in our channel_update message, the goal is to be able to use our channel
// as soon as it reaches NORMAL state, and before it is announced on the network
// (this id might be updated when the funding tx gets deeply buried, if there was a reorg in the meantime)
val shortChannelId = toShortId(blockHeight, txIndex, commitments.commitInput.outPoint.index.toInt)
goto(WAIT_FOR_FUNDING_LOCKED) using store(DATA_WAIT_FOR_FUNDING_LOCKED(commitments, shortChannelId, fundingLocked)) sending fundingLocked
case Event(BITCOIN_FUNDING_PUBLISH_FAILED, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) =>
val error = Error(d.channelId, "Funding tx publish failure".getBytes)
log.error(s"failed to publish funding tx")
val exc = ChannelFundingError(d.channelId)
val error = Error(d.channelId, exc.getMessage.getBytes)
goto(ERR_FUNDING_PUBLISH_FAILED) sending error
// TODO: not implemented, users will have to manually close
case Event(BITCOIN_FUNDING_TIMEOUT, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) =>
val error = Error(d.channelId, "Funding tx timed out".getBytes)
val exc = FundingTxTimedout(d.channelId)
val error = Error(d.channelId, exc.getMessage.getBytes)
goto(ERR_FUNDING_TIMEOUT) sending error
case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if d.commitments.announceChannel =>
log.info(s"received remote announcement signatures, delaying")
log.debug(s"received remote announcement signatures, delaying")
// we may receive their announcement sigs before our watcher notifies us that the channel has reached min_conf (especially during testing when blocks are generated in bulk)
// note: no need to persist their message, in case of disconnection they will resend it
context.system.scheduler.scheduleOnce(2 seconds, self, remoteAnnSigs)
stay
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, _), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleInformationLeak(d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleInformationLeak(tx, d)
case Event(CMD_CLOSE(_), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => spendLocalCurrent(d)
@ -407,29 +425,32 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
})
when(WAIT_FOR_FUNDING_LOCKED)(handleExceptions {
case Event(FundingLocked(_, nextPerCommitmentPoint), d@DATA_WAIT_FOR_FUNDING_LOCKED(commitments, _)) =>
if (d.commitments.announceChannel) {
// used for announcement of channel (if minDepth >= ANNOUNCEMENTS_MINCONF this event will fire instantly)
blockchain ! WatchConfirmed(self, commitments.commitInput.outPoint.txid, commitments.commitInput.txOut.publicKeyScript, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED)
} else if (d.commitments.announceChannel && nodeParams.watcherType == BITCOINJ && d.commitments.localParams.isFunder && System.getProperty("spvtest") != null) {
case Event(FundingLocked(_, nextPerCommitmentPoint), d@DATA_WAIT_FOR_FUNDING_LOCKED(commitments, shortChannelId, _)) =>
if (d.commitments.announceChannel && nodeParams.watcherType == BITCOINJ && d.commitments.localParams.isFunder && System.getProperty("spvtest") != null) {
// bitcoinj-based watcher currently can't get the tx index in block (which is used to calculate the short id)
// instead, we rely on a hack by trusting the index the counterparty sends us
// but in testing when connecting to bitcoinj impl together we make the funder choose some random data
log.warning("using hardcoded short id for testing with bitcoinj!!!!!")
context.system.scheduler.scheduleOnce(5 seconds, self, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, Random.nextInt(100), Random.nextInt(100)))
} else {
// used to get the final shortChannelId, used in announcements (if minDepth >= ANNOUNCEMENTS_MINCONF this event will fire instantly)
blockchain ! WatchConfirmed(self, commitments.commitInput.outPoint.txid, commitments.commitInput.txOut.publicKeyScript, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED)
}
goto(NORMAL) using store(DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), None, None, None, None))
context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, shortChannelId))
val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDeltaBlocks, d.commitments.remoteParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth, enable = true)
relayer ! initialChannelUpdate
goto(NORMAL) using store(DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), shortChannelId, None, initialChannelUpdate, None, None, None))
case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_FUNDING_LOCKED) if d.commitments.announceChannel =>
log.info(s"received remote announcement signatures, delaying")
log.debug(s"received remote announcement signatures, delaying")
// we may receive their announcement sigs before our watcher notifies us that the channel has reached min_conf (especially during testing when blocks are generated in bulk)
// note: no need to persist their message, in case of disconnection they will resend it
context.system.scheduler.scheduleOnce(2 seconds, self, remoteAnnSigs)
stay
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_WAIT_FOR_FUNDING_LOCKED) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_WAIT_FOR_FUNDING_LOCKED) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, _), d: DATA_WAIT_FOR_FUNDING_LOCKED) => handleInformationLeak(d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_WAIT_FOR_FUNDING_LOCKED) => handleInformationLeak(tx, d)
case Event(CMD_CLOSE(_), d: DATA_WAIT_FOR_FUNDING_LOCKED) => spendLocalCurrent(d)
@ -451,22 +472,22 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) if d.localShutdown.isDefined || d.remoteShutdown.isDefined =>
// note: spec would allow us to keep sending new htlcs after having received their shutdown (and not sent ours)
// but we want to converge as fast as possible and they would probably not route them anyway
val error = ClosingInProgress(d.channelId)
handleCommandAddError(error, origin(c))
val error = NoMoreHtlcsClosingInProgress(d.channelId)
handleCommandError(AddHtlcFailed(d.channelId, error, origin(c), Some(d.channelUpdate)))
case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) =>
Try(Commitments.sendAdd(d.commitments, c, origin(c))) match {
case Success(Right((commitments1, add))) =>
if (c.commit) self ! CMD_SIGN
handleCommandSuccess(sender, d.copy(commitments = commitments1)) sending add
case Success(Left(error)) => handleCommandAddError(error, origin(c))
case Failure(cause) => handleCommandError(cause)
case Success(Left(error)) => handleCommandError(AddHtlcFailed(d.channelId, error, origin(c), Some(d.channelUpdate)))
case Failure(cause) => handleCommandError(AddHtlcFailed(d.channelId, cause, origin(c), Some(d.channelUpdate)))
}
case Event(add: UpdateAddHtlc, d: DATA_NORMAL) =>
Try(Commitments.receiveAdd(d.commitments, add)) match {
case Success(commitments1) => stay using d.copy(commitments = commitments1)
case Failure(cause) => handleLocalError(cause, d)
case Failure(cause) => handleLocalError(cause, d, Some(add))
}
case Event(c: CMD_FULFILL_HTLC, d: DATA_NORMAL) =>
@ -483,7 +504,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
relayer ! ForwardFulfill(fulfill, origin)
stay using d.copy(commitments = commitments1)
case Success(Left(_)) => stay
case Failure(cause) => handleLocalError(cause, d)
case Failure(cause) => handleLocalError(cause, d, Some(fulfill))
}
case Event(c: CMD_FAIL_HTLC, d: DATA_NORMAL) =>
@ -508,7 +529,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
relayer ! ForwardFail(fail, origin)
stay using d.copy(commitments = commitments1)
case Success(Left(_)) => stay
case Failure(cause) => handleLocalError(cause, d)
case Failure(cause) => handleLocalError(cause, d, Some(fail))
}
case Event(fail: UpdateFailMalformedHtlc, d: DATA_NORMAL) =>
@ -517,7 +538,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
relayer ! ForwardFailMalformed(fail, origin)
stay using d.copy(commitments = commitments1)
case Success(Left(_)) => stay
case Failure(cause) => handleLocalError(cause, d)
case Failure(cause) => handleLocalError(cause, d, Some(fail))
}
case Event(c: CMD_UPDATE_FEE, d: DATA_NORMAL) =>
@ -531,7 +552,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Event(fee: UpdateFee, d: DATA_NORMAL) =>
Try(Commitments.receiveFee(d.commitments, fee, nodeParams.maxFeerateMismatch)) match {
case Success(commitments1) => stay using d.copy(commitments = commitments1)
case Failure(cause) => handleLocalError(cause, d)
case Failure(cause) => handleLocalError(cause, d, Some(fee))
}
case Event(CMD_SIGN, d: DATA_NORMAL) =>
@ -563,7 +584,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
}
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1))
stay using store(d.copy(commitments = commitments1)) sending revocation
case Failure(cause) => handleLocalError(cause, d)
case Failure(cause) => handleLocalError(cause, d, Some(commit))
}
case Event(revocation: RevokeAndAck, d: DATA_NORMAL) =>
@ -591,7 +612,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
} else {
stay using store(d.copy(commitments = commitments1))
}
case Failure(cause) => handleLocalError(cause, d)
case Failure(cause) => handleLocalError(cause, d, Some(revocation))
}
case Event(CMD_CLOSE(localScriptPubKey_opt), d: DATA_NORMAL) =>
@ -625,9 +646,9 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
// there are no htlcs => go to NEGOTIATING
if (!Closing.isValidFinalScriptPubkey(remoteScriptPubKey)) {
handleLocalError(InvalidFinalScript(d.channelId), d)
handleLocalError(InvalidFinalScript(d.channelId), d, Some(remoteShutdown))
} else if (Commitments.remoteHasUnsignedOutgoingHtlcs(d.commitments)) {
handleLocalError(CannotCloseWithUnsignedOutgoingHtlcs(d.channelId), d)
handleLocalError(CannotCloseWithUnsignedOutgoingHtlcs(d.channelId), d, Some(remoteShutdown))
} else if (Commitments.localHasUnsignedOutgoingHtlcs(d.commitments)) { // do we have unsigned outgoing htlcs?
require(d.localShutdown.isEmpty, "can't have pending unsigned outgoing htlcs after having sent Shutdown")
// are we in the middle of a signature?
@ -657,64 +678,42 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
if (d.commitments.hasNoPendingHtlcs) {
// there are no pending signed htlcs, let's go directly to NEGOTIATING
val closingSigned = Closing.makeFirstClosingTx(d.commitments, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey)
goto(NEGOTIATING) using store(DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, closingSigned)) sending sendList :+ closingSigned
goto(NEGOTIATING) using store(DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, closingSigned :: Nil)) sending sendList :+ closingSigned
} else {
// there are some pending signed htlcs, we need to fail/fullfill them
goto(SHUTDOWN) using store(DATA_SHUTDOWN(d.commitments, localShutdown, remoteShutdown)) sending sendList
}
}
case Event(CurrentBlockCount(count), d: DATA_NORMAL) if d.commitments.hasTimedoutOutgoingHtlcs(count) =>
handleLocalError(HtlcTimedout(d.channelId), d)
case Event(c@CurrentBlockCount(count), d: DATA_NORMAL) if d.commitments.hasTimedoutOutgoingHtlcs(count) =>
handleLocalError(HtlcTimedout(d.channelId), d, Some(c))
case Event(CurrentFeerates(feeratesPerKw), d: DATA_NORMAL) =>
case Event(c@CurrentFeerates(feeratesPerKw), d: DATA_NORMAL) =>
val networkFeeratePerKw = feeratesPerKw.block_1
d.commitments.localParams.isFunder match {
case true if Helpers.shouldUpdateFee(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.updateFeeMinDiffRatio) =>
self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true)
stay
case false if Helpers.isFeeDiffTooHigh(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.maxFeerateMismatch) =>
handleLocalError(FeerateTooDifferent(d.channelId, localFeeratePerKw = networkFeeratePerKw, remoteFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw), d)
handleLocalError(FeerateTooDifferent(d.channelId, localFeeratePerKw = networkFeeratePerKw, remoteFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw), d, Some(c))
case _ => stay
}
case Event(WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, blockHeight, txIndex), d: DATA_NORMAL) if d.commitments.announceChannel && d.shortChannelId.isEmpty =>
case Event(WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, blockHeight, txIndex), d: DATA_NORMAL) if d.channelAnnouncement.isEmpty =>
val shortChannelId = toShortId(blockHeight, txIndex, d.commitments.commitInput.outPoint.index.toInt)
log.info(s"funding tx is deeply buried at blockHeight=$blockHeight txIndex=$txIndex, sending announcements")
val annSignatures = Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, shortChannelId)
stay using store(d.copy(localAnnouncementSignatures = Some(annSignatures))) sending annSignatures
log.info(s"funding tx is deeply buried at blockHeight=$blockHeight txIndex=$txIndex shortChannelId=$shortChannelId")
// we re-announce this shortChannelId, because it might be different from the one we were using before if there was a reorg
context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, shortChannelId))
val annSignatures_opt = if (d.commitments.announceChannel) Some(Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, shortChannelId)) else None
stay using store(d.copy(shortChannelId = shortChannelId, localAnnouncementSignatures = annSignatures_opt)) sending annSignatures_opt.toSeq
case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_NORMAL) if d.commitments.announceChannel =>
// channels are publicly announced if both parties want it (defined as feature bit)
d.localAnnouncementSignatures match {
case Some(localAnnSigs) if d.shortChannelId.isDefined =>
// this can happen if our announcement_signatures was lost during a disconnection
// specs says that we "MUST respond to the first announcement_signatures message after reconnection with its own announcement_signatures message"
// current implementation always replies to announcement_signatures, not only the first time
log.info(s"re-sending our announcement sigs")
stay sending localAnnSigs
case Some(localAnnSigs) =>
require(localAnnSigs.shortChannelId == remoteAnnSigs.shortChannelId, s"shortChannelId mismatch: local=${localAnnSigs.shortChannelId} remote=${remoteAnnSigs.shortChannelId}")
log.info(s"announcing channelId=${d.channelId} on the network with shortId=${localAnnSigs.shortChannelId}")
import d.commitments.{localParams, remoteParams}
val channelAnn = Announcements.makeChannelAnnouncement(nodeParams.chainHash, localAnnSigs.shortChannelId, localParams.nodeId, remoteParams.nodeId, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, localAnnSigs.nodeSignature, remoteAnnSigs.nodeSignature, localAnnSigs.bitcoinSignature, remoteAnnSigs.bitcoinSignature)
val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses)
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, localAnnSigs.shortChannelId, nodeParams.expiryDeltaBlocks, nodeParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth)
router ! channelAnn
router ! nodeAnn
router ! channelUpdate
relayer ! channelUpdate
// TODO: remove this later when we use testnet/mainnet
// let's trigger the broadcast immediately so that we don't wait for 60 seconds to announce our newly created channel
// we give 3 seconds for the router-watcher roundtrip
context.system.scheduler.scheduleOnce(3 seconds, router, TickBroadcast)
context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, localAnnSigs.shortChannelId))
// we acknowledge our AnnouncementSignatures message
stay using store(d.copy(shortChannelId = Some(localAnnSigs.shortChannelId))) // note: we don't clear our announcement sigs because we may need to re-send them
case None =>
log.info(s"received remote announcement signatures, delaying")
(d.localAnnouncementSignatures, d.channelAnnouncement) match {
case (None, _) =>
// our watcher didn't notify yet that the tx has reached ANNOUNCEMENTS_MINCONF confirmations, let's delay remote's message
// note: no need to persist their message, in case of disconnection they will resend it
log.debug(s"received remote announcement signatures, delaying")
context.system.scheduler.scheduleOnce(5 seconds, self, remoteAnnSigs)
if (nodeParams.watcherType == BITCOINJ) {
log.warning(s"HACK: since we cannot get the tx index with bitcoinj, we copy the value sent by remote")
@ -722,36 +721,52 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
self ! WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, blockHeight, txIndex)
}
stay
}
case Event(TickRefreshChannelUpdate, d: DATA_NORMAL) if d.shortChannelId.isDefined =>
d.shortChannelId match {
case Some(shortChannelId) => // periodic refresh is used as a keep alive
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDeltaBlocks, nodeParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth)
case (Some(localAnnSigs), None) =>
require(localAnnSigs.shortChannelId == remoteAnnSigs.shortChannelId, s"shortChannelId mismatch: local=${localAnnSigs.shortChannelId} remote=${remoteAnnSigs.shortChannelId}")
log.info(s"announcing channelId=${d.channelId} on the network with shortId=${localAnnSigs.shortChannelId}")
import d.commitments.{localParams, remoteParams}
val channelAnn = Announcements.makeChannelAnnouncement(nodeParams.chainHash, localAnnSigs.shortChannelId, localParams.nodeId, remoteParams.nodeId, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, localAnnSigs.nodeSignature, remoteAnnSigs.nodeSignature, localAnnSigs.bitcoinSignature, remoteAnnSigs.bitcoinSignature)
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, enable = true)
router ! channelAnn
router ! channelUpdate
stay
case None => stay // channel is not announced
relayer ! channelUpdate
stay using store(d.copy(channelAnnouncement = Some(channelAnn), channelUpdate = channelUpdate)) // note: we don't clear our announcement sigs because we may need to re-send them
case (Some(localAnnSigs), Some(_)) =>
// they send their announcement sigs, but we already have a valid channel annoucement
// this can happen if our announcement_signatures was lost during a disconnection
// specs says that we "MUST respond to the first announcement_signatures message after reconnection with its own announcement_signatures message"
// current implementation always replies to announcement_signatures, not only the first time
log.info(s"re-sending our announcement sigs")
stay sending localAnnSigs
}
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_NORMAL) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
case Event(TickRefreshChannelUpdate, d: DATA_NORMAL) =>
// periodic refresh is used as a keep alive
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, enable = true)
if (d.channelAnnouncement.isDefined) {
router ! channelUpdate
}
relayer ! channelUpdate
stay using d.copy(channelUpdate = channelUpdate)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), 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 tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_NORMAL) => handleRemoteSpentOther(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) => handleRemoteSpentOther(tx, d)
case Event(INPUT_DISCONNECTED, d: DATA_NORMAL) =>
// we disable the channel
log.info(s"disabling the channel (disconnected)")
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, enable = false)
if (d.channelAnnouncement.isDefined) {
router ! channelUpdate
}
relayer ! channelUpdate
d.commitments.localChanges.proposed.collect {
case add: UpdateAddHtlc => relayer ! ForwardLocalFail(ChannelUnavailable(d.channelId), d.commitments.originChannels(add.id))
case add: UpdateAddHtlc => relayer ! AddHtlcFailed(d.channelId, ChannelUnavailable(d.channelId), d.commitments.originChannels(add.id), Some(channelUpdate))
}
d.shortChannelId match {
case Some(shortChannelId) =>
// if channel has be announced, we disable it
log.info(s"disabling the channel (disconnected)")
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDeltaBlocks, nodeParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth, enable = false)
router ! channelUpdate
case None => {}
}
goto(OFFLINE)
goto(OFFLINE) using d.copy(channelUpdate = channelUpdate)
case Event(e: Error, d: DATA_NORMAL) => handleRemoteError(e, d)
@ -785,7 +800,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
relayer ! ForwardFulfill(fulfill, origin)
stay using d.copy(commitments = commitments1)
case Success(Left(_)) => stay
case Failure(cause) => handleLocalError(cause, d)
case Failure(cause) => handleLocalError(cause, d, Some(fulfill))
}
case Event(c: CMD_FAIL_HTLC, d: DATA_SHUTDOWN) =>
@ -810,7 +825,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
relayer ! ForwardFail(fail, origin)
stay using d.copy(commitments = commitments1)
case Success(Left(_)) => stay
case Failure(cause) => handleLocalError(cause, d)
case Failure(cause) => handleLocalError(cause, d, Some(fail))
}
case Event(fail: UpdateFailMalformedHtlc, d: DATA_SHUTDOWN) =>
@ -819,7 +834,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
relayer ! ForwardFailMalformed(fail, origin)
stay using d.copy(commitments = commitments1)
case Success(Left(_)) => stay
case Failure(cause) => handleLocalError(cause, d)
case Failure(cause) => handleLocalError(cause, d, Some(fail))
}
case Event(c: CMD_UPDATE_FEE, d: DATA_SHUTDOWN) =>
@ -833,7 +848,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Event(fee: UpdateFee, d: DATA_SHUTDOWN) =>
Try(Commitments.receiveFee(d.commitments, fee, nodeParams.maxFeerateMismatch)) match {
case Success(commitments1) => stay using d.copy(commitments = commitments1)
case Failure(cause) => handleLocalError(cause, d)
case Failure(cause) => handleLocalError(cause, d, Some(fee))
}
case Event(CMD_SIGN, d: DATA_SHUTDOWN) =>
@ -854,8 +869,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
stay using d.copy(commitments = d.commitments.copy(remoteNextCommitInfo = Left(waitForRevocation.copy(reSignAsap = true))))
}
case Event(msg: CommitSig, d@DATA_SHUTDOWN(_, localShutdown, remoteShutdown)) =>
Try(Commitments.receiveCommit(d.commitments, msg)) map {
case Event(commit: CommitSig, d@DATA_SHUTDOWN(_, localShutdown, remoteShutdown)) =>
Try(Commitments.receiveCommit(d.commitments, commit)) map {
case (commitments1, revocation) =>
// we always reply with a revocation
log.debug(s"received a new sig:\n${Commitments.specs2String(commitments1)}")
@ -864,24 +879,24 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
} match {
case Success((commitments1, revocation)) if commitments1.hasNoPendingHtlcs =>
val closingSigned = Closing.makeFirstClosingTx(commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey)
goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, closingSigned)) sending revocation :: closingSigned :: Nil
goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, closingSigned :: Nil)) sending revocation :: closingSigned :: Nil
case Success((commitments1, revocation)) =>
if (Commitments.localHasChanges(commitments1)) {
// if we have newly acknowledged changes let's sign them
self ! CMD_SIGN
}
stay using store(d.copy(commitments = commitments1)) sending revocation
case Failure(cause) => handleLocalError(cause, d)
case Failure(cause) => handleLocalError(cause, d, Some(commit))
}
case Event(msg: RevokeAndAck, d@DATA_SHUTDOWN(commitments, localShutdown, remoteShutdown)) =>
case Event(revocation: RevokeAndAck, d@DATA_SHUTDOWN(commitments, localShutdown, remoteShutdown)) =>
// we received a revocation because we sent a signature
// => all our changes have been acked including the shutdown message
Try(Commitments.receiveRevocation(commitments, msg)) match {
Try(Commitments.receiveRevocation(commitments, revocation)) match {
case Success(commitments1) if commitments1.hasNoPendingHtlcs =>
log.debug(s"received a new rev, switching to NEGOTIATING spec:\n${Commitments.specs2String(commitments1)}")
val closingSigned = Closing.makeFirstClosingTx(commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey)
goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, closingSigned)) sending closingSigned
goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, closingSigned :: Nil)) sending closingSigned
case Success(commitments1) =>
// BOLT 2: A sending node SHOULD fail to route any HTLC added after it sent shutdown.
d.commitments.remoteChanges.signed.collect {
@ -894,28 +909,28 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
}
log.debug(s"received a new rev, spec:\n${Commitments.specs2String(commitments1)}")
stay using store(d.copy(commitments = commitments1))
case Failure(cause) => handleLocalError(cause, d)
case Failure(cause) => handleLocalError(cause, d, Some(revocation))
}
case Event(CurrentBlockCount(count), d: DATA_SHUTDOWN) if d.commitments.hasTimedoutOutgoingHtlcs(count) =>
handleLocalError(HtlcTimedout(d.channelId), d)
case Event(c@CurrentBlockCount(count), d: DATA_SHUTDOWN) if d.commitments.hasTimedoutOutgoingHtlcs(count) =>
handleLocalError(HtlcTimedout(d.channelId), d, Some(c))
case Event(CurrentFeerates(feerates), d: DATA_SHUTDOWN) =>
case Event(c@CurrentFeerates(feerates), d: DATA_SHUTDOWN) =>
val networkFeeratePerKw = feerates.block_1
d.commitments.localParams.isFunder match {
case true if Helpers.shouldUpdateFee(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.updateFeeMinDiffRatio) =>
self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true)
stay
case false if Helpers.isFeeDiffTooHigh(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.maxFeerateMismatch) =>
handleLocalError(FeerateTooDifferent(d.channelId, localFeeratePerKw = networkFeeratePerKw, remoteFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw), d)
handleLocalError(FeerateTooDifferent(d.channelId, localFeeratePerKw = networkFeeratePerKw, remoteFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw), d, Some(c))
case _ => stay
}
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_SHUTDOWN) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, 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: Transaction), 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 Some(tx.txid) == d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.txid) => handleRemoteSpentNext(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_SHUTDOWN) => handleRemoteSpentOther(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_SHUTDOWN) => handleRemoteSpentOther(tx, d)
case Event(CMD_CLOSE(_), d: DATA_SHUTDOWN) => handleCommandError(ClosingAlreadyInProgress(d.channelId))
@ -924,32 +939,34 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
})
when(NEGOTIATING)(handleExceptions {
case Event(ClosingSigned(_, remoteClosingFee, remoteSig), d: DATA_NEGOTIATING) =>
case Event(c@ClosingSigned(_, remoteClosingFee, remoteSig), d: DATA_NEGOTIATING) =>
log.info(s"received closingFeeSatoshis=$remoteClosingFee")
Closing.checkClosingSignature(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, Satoshi(remoteClosingFee), remoteSig) match {
case Success(signedClosingTx) if remoteClosingFee == d.localClosingSigned.feeSatoshis =>
handleMutualClose(signedClosingTx, d)
case Success(signedClosingTx) if remoteClosingFee == d.localClosingSigned.last.feeSatoshis =>
doPublish(signedClosingTx)
handleMutualClose(signedClosingTx, Left(d))
case Success(signedClosingTx) =>
val nextClosingFee = Closing.nextClosingFee(Satoshi(d.localClosingSigned.feeSatoshis), Satoshi(remoteClosingFee))
val nextClosingFee = Closing.nextClosingFee(Satoshi(d.localClosingSigned.last.feeSatoshis), Satoshi(remoteClosingFee))
val (_, closingSigned) = Closing.makeClosingTx(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nextClosingFee)
log.info(s"proposing closingFeeSatoshis=${closingSigned.feeSatoshis}")
if (nextClosingFee == Satoshi(remoteClosingFee)) {
handleMutualClose(signedClosingTx, store(d)) sending closingSigned
doPublish(signedClosingTx)
handleMutualClose(signedClosingTx, Left(store(d))) sending closingSigned
} else {
stay using store(d.copy(localClosingSigned = closingSigned)) sending closingSigned
stay using store(d.copy(localClosingSigned = d.localClosingSigned :+ closingSigned)) sending closingSigned
}
case Failure(cause) =>
log.error(cause, "cannot verify their close signature")
throw InvalidCloseSignature(d.channelId)
case Failure(cause) => handleLocalError(cause, d, Some(c))
}
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_NEGOTIATING) if tx.txid == Closing.makeClosingTx(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, Satoshi(d.localClosingSigned.feeSatoshis))._1.tx.txid =>
// happens when we agreed on a closeSig, but we don't know it yet: we receive the watcher notification before their ClosingSigned (which will match ours)
stay
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_NEGOTIATING) if d.localClosingSigned.exists(closingSigned => tx.txIn.head.witness.stack.contains(closingSigned.signature)) =>
// they can publish a closing tx with any sig we sent them, even if we are not done negotiating
handleMutualClose(tx, Left(d))
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_NEGOTIATING) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
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: Transaction), 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 Some(tx.txid) == d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.txid) => handleRemoteSpentNext(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_NEGOTIATING) => handleRemoteSpentOther(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_NEGOTIATING) => handleRemoteSpentOther(tx, d)
case Event(CMD_CLOSE(_), d: DATA_NEGOTIATING) => handleCommandError(ClosingAlreadyInProgress(d.channelId))
@ -984,10 +1001,10 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Failure(cause) => handleCommandError(cause)
}
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_CLOSING) =>
if (Some(tx.txid) == d.mutualClosePublished.map(_.txid)) {
// we just published a mutual close tx, we are notified but it's alright
stay
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_CLOSING) =>
if (d.localClosingSigned.exists(closingSigned => tx.txIn.head.witness.stack.contains(closingSigned.signature))) {
// 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)) {
// this is because WatchSpent watches never expire and we are notified multiple times
stay
@ -1008,7 +1025,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
handleRemoteSpentOther(tx, d)
}
case Event(WatchEventSpent(BITCOIN_OUTPUT_SPENT, tx: Transaction), d: DATA_CLOSING) =>
case Event(WatchEventSpent(BITCOIN_OUTPUT_SPENT, tx), d: DATA_CLOSING) =>
// one of the outputs of the local/remote/revoked commit was spent
// we just put a watch to be notified when it is confirmed
blockchain ! WatchConfirmed(self, tx, nodeParams.minDepthBlocks, BITCOIN_TX_CONFIRMED(tx))
@ -1049,7 +1066,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case add if ripemd160(add.paymentHash) == extracted =>
val origin = d.commitments.originChannels(add.id)
log.warning(s"found a match between paymentHash160=$extracted and origin=$origin: htlc timed out")
relayer ! ForwardLocalFail(HtlcTimedout(d.channelId), origin)
relayer ! 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: 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
@ -1063,7 +1080,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
val nextRemoteCommitPublished1 = d.nextRemoteCommitPublished.map(Closing.updateRemoteCommitPublished(_, tx))
val revokedCommitPublished1 = d.revokedCommitPublished.map(Closing.updateRevokedCommitPublished(_, tx))
// then let's see if any of the possible close scenarii can be considered done
val mutualCloseDone = d.mutualClosePublished.map(_.txid == tx.txid).getOrElse(false) // this case is trivial, in a mutual close scenario we only need to make sure that the closing tx is confirmed
val mutualCloseDone = d.mutualClosePublished.exists(_.txid == tx.txid) // this case is trivial, in a mutual close scenario we only need to make sure that one of the closing txes is confirmed
val localCommitDone = localCommitPublished1.map(Closing.isLocalCommitDone(_)).getOrElse(false)
val remoteCommitDone = remoteCommitPublished1.map(Closing.isRemoteCommitDone(_)).getOrElse(false)
val nextRemoteCommitDone = nextRemoteCommitPublished1.map(Closing.isRemoteCommitDone(_)).getOrElse(false)
@ -1077,9 +1094,16 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
stay using d1
}
case Event(_: ChannelReestablish, d: DATA_CLOSING) =>
// they haven't detected that we were closing and are trying to reestablish a connection
// we give them one of the published txes as a hint
val exc = FundingTxSpent(d.channelId, d.spendingTxes.head) // spendingTx != Nil that's a requirement of DATA_CLOSING)
val error = Error(d.channelId, exc.getMessage.getBytes)
stay sending error
case Event(CMD_CLOSE(_), d: DATA_CLOSING) => handleCommandError(ClosingAlreadyInProgress(d.channelId))
case Event(e: Error, d: DATA_CLOSING) => stay // nothing to do, there is already a spending tx published
case Event(e: Error, d: DATA_CLOSING) => handleRemoteError(e, d)
case Event(INPUT_DISCONNECTED | INPUT_RECONNECTED(_), _) => stay // we don't really care at this point
})
@ -1114,19 +1138,19 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
)
goto(SYNCING) sending channelReestablish
case Event(CMD_CLOSE(_), d: HasCommitments) => handleLocalError(ForcedLocalCommit(d.channelId, "can't do a mutual close while disconnected"), d) replying "ok"
case Event(c@CMD_CLOSE(_), d: HasCommitments) => handleLocalError(ForcedLocalCommit(d.channelId, "can't do a mutual close while disconnected"), d, Some(c)) replying "ok"
case Event(CurrentBlockCount(count), d: HasCommitments) if d.commitments.hasTimedoutOutgoingHtlcs(count) =>
case Event(c@CurrentBlockCount(count), d: HasCommitments) if d.commitments.hasTimedoutOutgoingHtlcs(count) =>
// 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)
handleLocalError(HtlcTimedout(d.channelId), d, Some(c))
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: HasCommitments) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: HasCommitments) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: HasCommitments) if Some(tx.txid) == d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.txid) => handleRemoteSpentNext(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: HasCommitments) if Some(tx.txid) == d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.txid) => handleRemoteSpentNext(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: HasCommitments) => handleRemoteSpentOther(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: HasCommitments) => handleRemoteSpentOther(tx, d)
})
@ -1160,26 +1184,27 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
forwarder ! localShutdown
}
// even if we were just disconnected/reconnected, we need to put back the watch because the event may have been
// fired while we were in OFFLINE (if not, the operation is idempotent anyway)
// NB: in BITCOINJ mode we currently can't get the tx index in block (which is used to calculate the short id)
// instead, we rely on a hack by trusting the index the counterparty sends us
if (d.commitments.announceChannel && d.localAnnouncementSignatures.isEmpty && nodeParams.watcherType != BITCOINJ) {
blockchain ! WatchConfirmed(self, d.commitments.commitInput.outPoint.txid, d.commitments.commitInput.txOut.publicKeyScript, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED)
}
// rfc: a node SHOULD retransmit the announcement_signatures message if it has not received an announcement_signatures message
if (d.localAnnouncementSignatures.isDefined && d.shortChannelId.isEmpty) {
forwarder ! d.localAnnouncementSignatures.get
}
d.shortChannelId.map {
case shortChannelId =>
// we re-enable the channel
log.info(s"enabling the channel (reconnected)")
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDeltaBlocks, nodeParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth, enable = true)
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, nodeParams.expiryDeltaBlocks, d.commitments.remoteParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth, enable = true)
(d.localAnnouncementSignatures, d.channelAnnouncement) match {
case (None, None) if nodeParams.watcherType != BITCOINJ =>
// even if we were just disconnected/reconnected, we need to put back the watch because the event may have been
// fired while we were in OFFLINE (if not, the operation is idempotent anyway)
blockchain ! WatchConfirmed(self, d.commitments.commitInput.outPoint.txid, d.commitments.commitInput.txOut.publicKeyScript, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED)
case (None, None) if nodeParams.watcherType == BITCOINJ =>
// NB: in BITCOINJ mode we currently can't get the tx index in block (which is used to calculate the short id)
// instead, we rely on a hack by trusting the index the counterparty sends us)
case (Some(localAnnSigs), None) =>
// BOLT 7: a node SHOULD retransmit the announcement_signatures message if it has not received an announcement_signatures message
forwarder ! localAnnSigs
case (_, Some(channelAnn)) =>
// we might have been down for a long time (more than 2 weeks) and other nodes in the network might have forgotten the channel
log.info(s"re-announcing channelId=${d.channelId} on the network with shortId=${channelAnn.shortChannelId}")
router ! channelAnn
router ! channelUpdate
}
goto(NORMAL) using d.copy(commitments = commitments1)
relayer ! channelUpdate
goto(NORMAL) using d.copy(commitments = commitments1, channelUpdate = channelUpdate)
case Event(channelReestablish: ChannelReestablish, d: DATA_SHUTDOWN) =>
val commitments1 = handleSync(channelReestablish, d)
@ -1190,16 +1215,15 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
forwarder ! d.localClosingSigned
goto(NEGOTIATING)
case Event(CMD_CLOSE(_), d: HasCommitments) => handleLocalError(ForcedLocalCommit(d.channelId, "can't do a mutual close while syncing"), d)
case Event(c@CMD_CLOSE(_), d: HasCommitments) => handleLocalError(ForcedLocalCommit(d.channelId, "can't do a mutual close while syncing"), d, Some(c))
case Event(CurrentBlockCount(count), d: HasCommitments) if d.commitments.hasTimedoutOutgoingHtlcs(count) =>
handleLocalError(HtlcTimedout(d.channelId), d)
case Event(c@CurrentBlockCount(count), d: HasCommitments) if d.commitments.hasTimedoutOutgoingHtlcs(count) => handleLocalError(HtlcTimedout(d.channelId), d, Some(c))
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: HasCommitments) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: HasCommitments) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: HasCommitments) if Some(tx.txid) == d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.txid) => handleRemoteSpentNext(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: HasCommitments) if Some(tx.txid) == d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.txid) => handleRemoteSpentNext(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: HasCommitments) => handleRemoteSpentOther(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: HasCommitments) => handleRemoteSpentOther(tx, d)
case Event(e: Error, d: HasCommitments) => handleRemoteError(e, d)
})
@ -1218,7 +1242,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
whenUnhandled {
case Event(INPUT_PUBLISH_LOCALCOMMIT, d: HasCommitments) => handleLocalError(ForcedLocalCommit(d.channelId, "manual unilateral close"), d)
case Event(c@INPUT_PUBLISH_LOCALCOMMIT, d: HasCommitments) => handleLocalError(ForcedLocalCommit(d.channelId, "manual unilateral close"), d, Some(c))
case Event(INPUT_DISCONNECTED, _) => goto(OFFLINE)
@ -1240,7 +1264,10 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Event(c: CMD_ADD_HTLC, d: HasCommitments) =>
log.info(s"rejecting htlc request in state=$stateName")
val error = ChannelUnavailable(d.channelId)
handleCommandAddError(error, origin(c))
d match {
case normal: DATA_NORMAL => handleCommandError(AddHtlcFailed(d.channelId, error, origin(c), Some(normal.channelUpdate))) // can happen if we are in OFFLINE or SYNCING state (channelUpdate will have enable=false)
case _ => handleCommandError(AddHtlcFailed(d.channelId, error, origin(c), None))
}
// we only care about this event in NORMAL and SHUTDOWN state, and we never unregister to the event stream
case Event(CurrentBlockCount(_), _) => stay
@ -1282,15 +1309,6 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
stay using newData replying "ok"
}
def handleCommandAddError(cause: Throwable, origin: Origin) = {
relayer ! ForwardLocalFail(cause, origin)
cause match {
case _: ChannelException => log.error(s"$cause")
case _ => log.error(cause, "")
}
stay
}
def handleCommandError(cause: Throwable) = {
cause match {
case _: ChannelException => log.error(s"$cause")
@ -1299,28 +1317,39 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
stay replying Status.Failure(cause)
}
def handleLocalError(cause: Throwable, d: HasCommitments) = {
log.error(cause, "")
def handleLocalError(cause: Throwable, d: HasCommitments, msg: Option[Any]) = {
log.error(cause, s"error while processing msg=${msg.getOrElse("n/a")} in state=$stateData ")
val error = Error(d.channelId, cause.getMessage.getBytes)
spendLocalCurrent(d) sending error
}
def handleRemoteErrorNoCommitments(e: Error) = {
// when there is no commitment yet, we just go to CLOSED state in case an error occurs
log.error(s"peer sent $e, closing connection") // see bolt #2: A node MUST fail the connection if it receives an err message
goto(CLOSED)
def handleRemoteError(e: Error, d: Data) = {
// see BOLT 1: only print out data verbatim if is composed of printable ASCII characters
log.error(s"peer sent error: ascii='${if (isAsciiPrintable(e.data)) new String(e.data, StandardCharsets.US_ASCII) else "n/a"}' bin=${e.data}")
d match {
case _: DATA_CLOSING => stay // nothing to do, there is already a spending tx published
//case negotiating: DATA_NEGOTIATING => stay TODO: (nitpick) would be nice to publish a closing tx instead if we have already received one of their sigs
case hasCommitments: HasCommitments => spendLocalCurrent(hasCommitments)
case _ => goto(CLOSED) // when there is no commitment yet, we just go to CLOSED state in case an error occurs
}
}
def handleRemoteError(e: Error, d: HasCommitments) = {
log.error(s"peer sent $e, closing connection") // see bolt #2: A node MUST fail the connection if it receives an err message
spendLocalCurrent(d)
}
def handleMutualClose(closingTx: Transaction, d: Either[DATA_NEGOTIATING, DATA_CLOSING]) = {
log.info(s"a closing tx has been published: closingTxId=${closingTx.txid}")
def handleMutualClose(closingTx: Transaction, d: DATA_NEGOTIATING) = {
log.info(s"closingTxId=${closingTx.txid}")
val mutualClosePublished = Some(closingTx)
doPublish(closingTx)
val nextData = DATA_CLOSING(d.commitments, mutualClosePublished)
val closingSigned = d match {
case Left(negotiating) => negotiating.localClosingSigned
case Right(closing) => closing.localClosingSigned
}
val index = closingSigned.indexWhere(closingSigned => closingTx.txIn.head.witness.stack.contains(closingSigned.signature))
if (index != closingSigned.size - 1) {
log.warning(s"closing tx was published before end of negotiation: closingTxId=${closingTx.txid} index=$index signatures=${closingSigned.size}")
}
val nextData = d match {
case Left(negotiating) => DATA_CLOSING(negotiating.commitments, negotiating.localClosingSigned, mutualClosePublished = closingTx :: Nil)
case Right(closing) => closing.copy(mutualClosePublished = closing.mutualClosePublished :+ closingTx)
}
goto(CLOSING) using store(nextData)
}
@ -1337,7 +1366,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
val nextData = d match {
case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished))
case _ => DATA_CLOSING(d.commitments, localCommitPublished = Some(localCommitPublished))
case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, negotiating.localClosingSigned, localCommitPublished = Some(localCommitPublished))
case _ => DATA_CLOSING(d.commitments, localClosingSigned = Nil, localCommitPublished = Some(localCommitPublished))
}
goto(CLOSING) using store(nextData)
@ -1369,7 +1399,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
val nextData = d match {
case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished))
case _ => DATA_CLOSING(d.commitments, remoteCommitPublished = Some(remoteCommitPublished))
case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, negotiating.localClosingSigned, remoteCommitPublished = Some(remoteCommitPublished))
case _ => DATA_CLOSING(d.commitments, localClosingSigned = Nil, remoteCommitPublished = Some(remoteCommitPublished))
}
goto(CLOSING) using store(nextData)
@ -1386,7 +1417,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
val nextData = d match {
case closing: DATA_CLOSING => closing.copy(nextRemoteCommitPublished = Some(remoteCommitPublished))
case _ => DATA_CLOSING(d.commitments, nextRemoteCommitPublished = Some(remoteCommitPublished))
case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, negotiating.localClosingSigned, nextRemoteCommitPublished = Some(remoteCommitPublished))
case _ => DATA_CLOSING(d.commitments, localClosingSigned = Nil, nextRemoteCommitPublished = Some(remoteCommitPublished))
}
goto(CLOSING) using store(nextData)
@ -1412,13 +1444,15 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
Helpers.Closing.claimRevokedRemoteCommitTxOutputs(d.commitments, tx) match {
case Some(revokedCommitPublished) =>
log.warning(s"txid=${tx.txid} was a revoked commitment, publishing the penalty tx")
val error = Error(d.channelId, "Funding tx has been spent".getBytes)
val exc = FundingTxSpent(d.channelId, tx)
val error = Error(d.channelId, exc.getMessage.getBytes)
doPublish(revokedCommitPublished)
val nextData = d match {
case closing: DATA_CLOSING => closing.copy(revokedCommitPublished = closing.revokedCommitPublished :+ revokedCommitPublished)
case _ => DATA_CLOSING(d.commitments, revokedCommitPublished = revokedCommitPublished :: Nil)
case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, negotiating.localClosingSigned, revokedCommitPublished = revokedCommitPublished :: Nil)
case _ => DATA_CLOSING(d.commitments, localClosingSigned = Nil, revokedCommitPublished = revokedCommitPublished :: Nil)
}
goto(CLOSING) using store(nextData) sending error
case None =>
@ -1446,10 +1480,11 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
revokedCommitPublished.htlcTimeoutTxs.foreach(tx => blockchain ! WatchSpent(self, revokedCommitPublished.commitTx, tx.txIn.head.outPoint.index.toInt, BITCOIN_OUTPUT_SPENT))
}
def handleInformationLeak(d: HasCommitments) = {
def handleInformationLeak(tx: Transaction, d: HasCommitments) = {
// this is never supposed to happen !!
log.error(s"our funding tx ${d.commitments.commitInput.outPoint.txid} was spent !!")
val error = Error(d.channelId, "Funding tx has been spent".getBytes)
log.error(s"our funding tx ${d.commitments.commitInput.outPoint.txid} was spent by txid=${tx.txid} !!")
val exc = FundingTxSpent(d.channelId, tx)
val error = Error(d.channelId, exc.getMessage.getBytes)
// let's try to spend our current local tx
val commitTx = d.commitments.localCommit.publishableTxs.commitTx.tx
@ -1546,7 +1581,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
s(event)
} catch {
case t: Throwable => event.stateData match {
case d: HasCommitments => handleLocalError(t, d)
case d: HasCommitments => handleLocalError(t, d, None)
case d: Data =>
log.error(t, "")
val error = Error(Helpers.getChannelId(d), t.getMessage.getBytes)

View file

@ -1,48 +1,57 @@
package fr.acinq.eclair.channel
import fr.acinq.bitcoin.BinaryData
import fr.acinq.bitcoin.{BinaryData, Transaction}
import fr.acinq.eclair.UInt64
import fr.acinq.eclair.payment.Origin
import fr.acinq.eclair.wire.ChannelUpdate
/**
* Created by PM on 11/04/2017.
*/
class ChannelException(channelId: BinaryData, message: String) extends RuntimeException(message) { def getChannelId() = channelId }
class ChannelException(val channelId: BinaryData, message: String) extends RuntimeException(message)
// @formatter:off
case class DebugTriggeredException (channelId: BinaryData) extends ChannelException(channelId, "debug-mode triggered failure")
case class InvalidChainHash (channelId: BinaryData, local: BinaryData, remote: BinaryData) extends ChannelException(channelId, s"invalid chain_hash (local=$local remote=$remote)")
case class InvalidFundingAmount (channelId: BinaryData, fundingSatoshis: Long, min: Long, max: Long) extends ChannelException(channelId, s"invalid funding_satoshis=$fundingSatoshis (min=$min max=$max)")
case class InvalidPushAmount (channelId: BinaryData, pushMsat: Long, max: Long) extends ChannelException(channelId, s"invalid push_msat=$pushMsat (max=$max)")
case class InvalidMaxAcceptedHtlcs (channelId: BinaryData, maxAcceptedHtlcs: Int, max: Int) extends ChannelException(channelId, s"invalid max_accepted_htlcs=$maxAcceptedHtlcs (max=$max)")
case class InvalidDustLimit (channelId: BinaryData, dustLimitSatoshis: Long, min: Long) extends ChannelException(channelId, s"invalid dust_limit_satoshis=$dustLimitSatoshis (min=$min)")
case class ChannelReserveTooHigh (channelId: BinaryData, channelReserveSatoshis: Long, reserveToFundingRatio: Double, maxReserveToFundingRatio: Double) extends ChannelException(channelId, s"channelReserveSatoshis too high: reserve=$channelReserveSatoshis fundingRatio=$reserveToFundingRatio maxFundingRatio=$maxReserveToFundingRatio")
case class ClosingInProgress (channelId: BinaryData) extends ChannelException(channelId, "cannot send new htlcs, closing in progress")
case class ClosingAlreadyInProgress (channelId: BinaryData) extends ChannelException(channelId, "closing already in progress")
case class CannotCloseWithUnsignedOutgoingHtlcs(channelId: BinaryData) extends ChannelException(channelId, "cannot close when there are unsigned outgoing htlcs")
case class ChannelUnavailable (channelId: BinaryData) extends ChannelException(channelId, "channel is unavailable (offline or closing)")
case class InvalidFinalScript (channelId: BinaryData) extends ChannelException(channelId, "invalid final script")
case class HtlcTimedout (channelId: BinaryData) extends ChannelException(channelId, s"one or more htlcs timed out")
case class FeerateTooDifferent (channelId: BinaryData, localFeeratePerKw: Long, remoteFeeratePerKw: Long) extends ChannelException(channelId, s"local/remote feerates are too different: remoteFeeratePerKw=$remoteFeeratePerKw localFeeratePerKw=$localFeeratePerKw")
case class InvalidCloseSignature (channelId: BinaryData) extends ChannelException(channelId, "cannot verify their close signature")
case class InvalidCommitmentSignature (channelId: BinaryData) extends ChannelException(channelId, "invalid commitment signature")
case class ForcedLocalCommit (channelId: BinaryData, reason: String) extends ChannelException(channelId, s"forced local commit: reason")
case class UnexpectedHtlcId (channelId: BinaryData, expected: Long, actual: Long) extends ChannelException(channelId, s"unexpected htlc id: expected=$expected actual=$actual")
case class InvalidPaymentHash (channelId: BinaryData) extends ChannelException(channelId, "invalid payment hash")
case class ExpiryTooSmall (channelId: BinaryData, minimum: Long, actual: Long, blockCount: Long) extends ChannelException(channelId, s"expiry too small: required=$minimum actual=$actual blockCount=$blockCount")
case class ExpiryCannotBeInThePast (channelId: BinaryData, expiry: Long, blockCount: Long) extends ChannelException(channelId, s"expiry can't be in the past: expiry=$expiry blockCount=$blockCount")
case class HtlcValueTooSmall (channelId: BinaryData, minimum: Long, actual: Long) extends ChannelException(channelId, s"htlc value too small: minimum=$minimum actual=$actual")
case class HtlcValueTooHighInFlight (channelId: BinaryData, maximum: UInt64, actual: UInt64) extends ChannelException(channelId, s"in-flight htlcs hold too much value: maximum=$maximum actual=$actual")
case class TooManyAcceptedHtlcs (channelId: BinaryData, maximum: Long) extends ChannelException(channelId, s"too many accepted htlcs: maximum=$maximum")
case class InsufficientFunds (channelId: BinaryData, amountMsat: Long, missingSatoshis: Long, reserveSatoshis: Long, feesSatoshis: Long) extends ChannelException(channelId, s"insufficient funds: missingSatoshis=$missingSatoshis reserveSatoshis=$reserveSatoshis fees=$feesSatoshis")
case class InvalidHtlcPreimage (channelId: BinaryData, id: Long) extends ChannelException(channelId, s"invalid htlc preimage for htlc id=$id")
case class UnknownHtlcId (channelId: BinaryData, id: Long) extends ChannelException(channelId, s"unknown htlc id=$id")
case class FundeeCannotSendUpdateFee (channelId: BinaryData) extends ChannelException(channelId, s"only the funder should send update_fee messages")
case class CannotAffordFees (channelId: BinaryData, missingSatoshis: Long, reserveSatoshis: Long, feesSatoshis: Long) extends ChannelException(channelId, s"can't pay the fee: missingSatoshis=$missingSatoshis reserveSatoshis=$reserveSatoshis feesSatoshis=$feesSatoshis")
case class CannotSignWithoutChanges (channelId: BinaryData) extends ChannelException(channelId, "cannot sign when there are no changes")
case class CannotSignBeforeRevocation (channelId: BinaryData) extends ChannelException(channelId, "cannot sign until next revocation hash is received")
case class UnexpectedRevocation (channelId: BinaryData) extends ChannelException(channelId, "received unexpected RevokeAndAck message")
case class InvalidRevocation (channelId: BinaryData) extends ChannelException(channelId, "invalid revocation")
case class CommitmentSyncError (channelId: BinaryData) extends ChannelException(channelId, "commitment sync error")
case class RevocationSyncError (channelId: BinaryData) extends ChannelException(channelId, "revocation sync error")
case class InvalidFailureCode (channelId: BinaryData) extends ChannelException(channelId, "UpdateFailMalformedHtlc message doesn't have BADONION bit set")
case class DebugTriggeredException (override val channelId: BinaryData) extends ChannelException(channelId, "debug-mode triggered failure")
case class InvalidChainHash (override val channelId: BinaryData, local: BinaryData, remote: BinaryData) extends ChannelException(channelId, s"invalid chain_hash (local=$local remote=$remote)")
case class InvalidFundingAmount (override val channelId: BinaryData, fundingSatoshis: Long, min: Long, max: Long) extends ChannelException(channelId, s"invalid funding_satoshis=$fundingSatoshis (min=$min max=$max)")
case class InvalidPushAmount (override val channelId: BinaryData, pushMsat: Long, max: Long) extends ChannelException(channelId, s"invalid push_msat=$pushMsat (max=$max)")
case class InvalidMaxAcceptedHtlcs (override val channelId: BinaryData, maxAcceptedHtlcs: Int, max: Int) extends ChannelException(channelId, s"invalid max_accepted_htlcs=$maxAcceptedHtlcs (max=$max)")
case class InvalidDustLimit (override val channelId: BinaryData, dustLimitSatoshis: Long, min: Long) extends ChannelException(channelId, s"invalid dust_limit_satoshis=$dustLimitSatoshis (min=$min)")
case class ChannelReserveTooHigh (override val channelId: BinaryData, channelReserveSatoshis: Long, reserveToFundingRatio: Double, maxReserveToFundingRatio: Double) extends ChannelException(channelId, s"channelReserveSatoshis too high: reserve=$channelReserveSatoshis fundingRatio=$reserveToFundingRatio maxFundingRatio=$maxReserveToFundingRatio")
case class ChannelFundingError (override val channelId: BinaryData) extends ChannelException(channelId, "channel funding error")
case class NoMoreHtlcsClosingInProgress (override val channelId: BinaryData) extends ChannelException(channelId, "cannot send new htlcs, closing in progress")
case class ClosingAlreadyInProgress (override val channelId: BinaryData) extends ChannelException(channelId, "closing already in progress")
case class CannotCloseWithUnsignedOutgoingHtlcs(override val channelId: BinaryData) extends ChannelException(channelId, "cannot close when there are unsigned outgoing htlcs")
case class ChannelUnavailable (override val channelId: BinaryData) extends ChannelException(channelId, "channel is unavailable (offline or closing)")
case class InvalidFinalScript (override val channelId: BinaryData) extends ChannelException(channelId, "invalid final script")
case class FundingTxTimedout (override val channelId: BinaryData) extends ChannelException(channelId, "funding tx timed out")
case class FundingTxSpent (override val channelId: BinaryData, spendingTx: Transaction) extends ChannelException(channelId, s"funding tx has been spent by txid=${spendingTx.txid}")
case class HtlcTimedout (override val channelId: BinaryData) extends ChannelException(channelId, "one or more htlcs timed out")
case class FeerateTooDifferent (override val channelId: BinaryData, localFeeratePerKw: Long, remoteFeeratePerKw: Long) extends ChannelException(channelId, s"local/remote feerates are too different: remoteFeeratePerKw=$remoteFeeratePerKw localFeeratePerKw=$localFeeratePerKw")
case class InvalidCommitmentSignature (override val channelId: BinaryData, tx: Transaction) extends ChannelException(channelId, s"invalid commitment signature: tx=${Transaction.write(tx)}")
case class InvalidHtlcSignature (override val channelId: BinaryData, tx: Transaction) extends ChannelException(channelId, s"invalid htlc signature: tx=${Transaction.write(tx)}")
case class InvalidCloseSignature (override val channelId: BinaryData, tx: Transaction) extends ChannelException(channelId, s"invalid close signature: tx=${Transaction.write(tx)}")
case class InvalidCloseFee (override val channelId: BinaryData, feeSatoshi: Long) extends ChannelException(channelId, s"invalid close fee: fee_satoshis=$feeSatoshi")
case class HtlcSigCountMismatch (override val channelId: BinaryData, expected: Int, actual: Int) extends ChannelException(channelId, s"htlc sig count mismatch: expected=$expected actual: $actual")
case class ForcedLocalCommit (override val channelId: BinaryData, reason: String) extends ChannelException(channelId, s"forced local commit: reason")
case class UnexpectedHtlcId (override val channelId: BinaryData, expected: Long, actual: Long) extends ChannelException(channelId, s"unexpected htlc id: expected=$expected actual=$actual")
case class InvalidPaymentHash (override val channelId: BinaryData) extends ChannelException(channelId, "invalid payment hash")
case class ExpiryTooSmall (override val channelId: BinaryData, minimum: Long, actual: Long, blockCount: Long) extends ChannelException(channelId, s"expiry too small: required=$minimum actual=$actual blockCount=$blockCount")
case class ExpiryCannotBeInThePast (override val channelId: BinaryData, expiry: Long, blockCount: Long) extends ChannelException(channelId, s"expiry can't be in the past: expiry=$expiry blockCount=$blockCount")
case class HtlcValueTooSmall (override val channelId: BinaryData, minimum: Long, actual: Long) extends ChannelException(channelId, s"htlc value too small: minimum=$minimum actual=$actual")
case class HtlcValueTooHighInFlight (override val channelId: BinaryData, maximum: UInt64, actual: UInt64) extends ChannelException(channelId, s"in-flight htlcs hold too much value: maximum=$maximum actual=$actual")
case class TooManyAcceptedHtlcs (override val channelId: BinaryData, maximum: Long) extends ChannelException(channelId, s"too many accepted htlcs: maximum=$maximum")
case class InsufficientFunds (override val channelId: BinaryData, amountMsat: Long, missingSatoshis: Long, reserveSatoshis: Long, feesSatoshis: Long) extends ChannelException(channelId, s"insufficient funds: missingSatoshis=$missingSatoshis reserveSatoshis=$reserveSatoshis fees=$feesSatoshis")
case class InvalidHtlcPreimage (override val channelId: BinaryData, id: Long) extends ChannelException(channelId, s"invalid htlc preimage for htlc id=$id")
case class UnknownHtlcId (override val channelId: BinaryData, id: Long) extends ChannelException(channelId, s"unknown htlc id=$id")
case class FundeeCannotSendUpdateFee (override val channelId: BinaryData) extends ChannelException(channelId, s"only the funder should send update_fee messages")
case class CannotAffordFees (override val channelId: BinaryData, missingSatoshis: Long, reserveSatoshis: Long, feesSatoshis: Long) extends ChannelException(channelId, s"can't pay the fee: missingSatoshis=$missingSatoshis reserveSatoshis=$reserveSatoshis feesSatoshis=$feesSatoshis")
case class CannotSignWithoutChanges (override val channelId: BinaryData) extends ChannelException(channelId, "cannot sign when there are no changes")
case class CannotSignBeforeRevocation (override val channelId: BinaryData) extends ChannelException(channelId, "cannot sign until next revocation hash is received")
case class UnexpectedRevocation (override val channelId: BinaryData) extends ChannelException(channelId, "received unexpected RevokeAndAck message")
case class InvalidRevocation (override val channelId: BinaryData) extends ChannelException(channelId, "invalid revocation")
case class CommitmentSyncError (override val channelId: BinaryData) extends ChannelException(channelId, "commitment sync error")
case class RevocationSyncError (override val channelId: BinaryData) extends ChannelException(channelId, "revocation sync error")
case class InvalidFailureCode (override val channelId: BinaryData) extends ChannelException(channelId, "UpdateFailMalformedHtlc message doesn't have BADONION bit set")
case class AddHtlcFailed (override val channelId: BinaryData, t: Throwable, origin: Origin, channelUpdate: Option[ChannelUpdate]) extends ChannelException(channelId, s"cannot add htlc with origin=$origin reason=${t.getMessage}")
// @formatter:on

View file

@ -7,7 +7,7 @@ import fr.acinq.eclair.UInt64
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, AnnouncementSignatures, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel, Shutdown, UpdateAddHtlc}
import fr.acinq.eclair.wire.{AcceptChannel, AnnouncementSignatures, ChannelAnnouncement, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel, Shutdown, UpdateAddHtlc}
/**
@ -133,23 +133,27 @@ final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: BinaryData,
final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: BinaryData, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, remoteFirstPerCommitmentPoint: Point, channelFlags: Byte, lastSent: AcceptChannel) extends Data
final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: BinaryData, localParams: LocalParams, remoteParams: RemoteParams, fundingTx: Transaction, localSpec: CommitmentSpec, localCommitTx: CommitTx, remoteCommit: RemoteCommit, channelFlags: Byte, lastSent: FundingCreated) extends Data
final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments, deferred: Option[FundingLocked], lastSent: Either[FundingCreated, FundingSigned]) extends Data with HasCommitments
final case class DATA_WAIT_FOR_FUNDING_LOCKED(commitments: Commitments, lastSent: FundingLocked) extends Data with HasCommitments
final case class DATA_WAIT_FOR_FUNDING_LOCKED(commitments: Commitments, shortChannelId: Long, lastSent: FundingLocked) extends Data with HasCommitments
final case class DATA_NORMAL(commitments: Commitments,
shortChannelId: Option[Long],
shortChannelId: Long,
channelAnnouncement: Option[ChannelAnnouncement],
channelUpdate: ChannelUpdate,
localAnnouncementSignatures: Option[AnnouncementSignatures],
localShutdown: Option[Shutdown],
remoteShutdown: Option[Shutdown]) extends Data with HasCommitments
final case class DATA_SHUTDOWN(commitments: Commitments,
localShutdown: Shutdown, remoteShutdown: Shutdown) extends Data with HasCommitments
final case class DATA_NEGOTIATING(commitments: Commitments,
localShutdown: Shutdown, remoteShutdown: Shutdown, localClosingSigned: ClosingSigned) extends Data with HasCommitments
localShutdown: Shutdown, remoteShutdown: Shutdown, localClosingSigned: List[ClosingSigned]) extends Data with HasCommitments
final case class DATA_CLOSING(commitments: Commitments,
mutualClosePublished: Option[Transaction] = None,
localClosingSigned: List[ClosingSigned],
mutualClosePublished: List[Transaction] = Nil,
localCommitPublished: Option[LocalCommitPublished] = None,
remoteCommitPublished: Option[RemoteCommitPublished] = None,
nextRemoteCommitPublished: Option[RemoteCommitPublished] = None,
revokedCommitPublished: List[RevokedCommitPublished] = Nil) extends Data with HasCommitments {
require(mutualClosePublished.isDefined || localCommitPublished.isDefined || remoteCommitPublished.isDefined || nextRemoteCommitPublished.isDefined || revokedCommitPublished.size > 0, "there should be at least one tx published in this state")
val spendingTxes = mutualClosePublished ::: localCommitPublished.map(_.commitTx).toList ::: remoteCommitPublished.map(_.commitTx).toList ::: nextRemoteCommitPublished.map(_.commitTx).toList ::: revokedCommitPublished.map(_.commitTx)
require(spendingTxes.size > 0, "there must be at least one tx published in this state")
}
final case class LocalParams(nodeId: PublicKey,

View file

@ -1,5 +1,6 @@
package fr.acinq.eclair.channel
import akka.event.LoggingAdapter
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, sha256}
import fr.acinq.bitcoin.{BinaryData, Crypto, Satoshi, Transaction}
import fr.acinq.eclair.crypto.{Generators, ShaChain, Sphinx}
@ -8,7 +9,6 @@ import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{Globals, UInt64}
import grizzled.slf4j.Logging
// @formatter:off
case class LocalChanges(proposed: List[UpdateMessage], signed: List[UpdateMessage], acked: List[UpdateMessage]) {
@ -54,7 +54,7 @@ case class Commitments(localParams: LocalParams, remoteParams: RemoteParams,
def announceChannel: Boolean = (channelFlags & 0x01) != 0
}
object Commitments extends Logging {
object Commitments {
/**
* add a change to our proposed change list
*
@ -355,7 +355,7 @@ object Commitments extends Logging {
}
}
def receiveCommit(commitments: Commitments, commit: CommitSig): (Commitments, RevokeAndAck) = {
def receiveCommit(commitments: Commitments, commit: CommitSig)(implicit log: LoggingAdapter): (Commitments, RevokeAndAck) = {
import commitments._
// they sent us a signature for *their* view of *our* next commit tx
// so in terms of rev.hashes and indexes we have:
@ -383,22 +383,28 @@ object Commitments extends Logging {
// no need to compute htlc sigs if commit sig doesn't check out
val signedCommitTx = Transactions.addSigs(localCommitTx, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, sig, commit.signature)
if (Transactions.checkSpendable(signedCommitTx).isFailure) {
throw InvalidCommitmentSignature(commitments.channelId)
throw InvalidCommitmentSignature(commitments.channelId, signedCommitTx.tx)
}
val sortedHtlcTxs: Seq[TransactionWithInputInfo] = (htlcTimeoutTxs ++ htlcSuccessTxs).sortBy(_.input.outPoint.index)
require(commit.htlcSignatures.size == sortedHtlcTxs.size, s"htlc sig count mismatch (received=${commit.htlcSignatures.size}, expected=${sortedHtlcTxs.size})")
if (commit.htlcSignatures.size != sortedHtlcTxs.size) {
throw new HtlcSigCountMismatch(commitments.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size)
}
val localHtlcKey = Generators.derivePrivKey(localParams.htlcKey, localPerCommitmentPoint)
val htlcSigs = sortedHtlcTxs.map(Transactions.sign(_, localHtlcKey))
val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint)
// combine the sigs to make signed txes
val htlcTxsAndSigs = (sortedHtlcTxs, htlcSigs, commit.htlcSignatures).zipped.toList.collect {
case (htlcTx: HtlcTimeoutTx, localSig, remoteSig) =>
require(Transactions.checkSpendable(Transactions.addSigs(htlcTx, localSig, remoteSig)).isSuccess, "bad sig")
if (Transactions.checkSpendable(Transactions.addSigs(htlcTx, localSig, remoteSig)).isFailure) {
throw new 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
require(Transactions.checkSig(htlcTx, remoteSig, remoteHtlcPubkey), "bad sig")
if (Transactions.checkSig(htlcTx, remoteSig, remoteHtlcPubkey) == false) {
throw new InvalidHtlcSignature(commitments.channelId, htlcTx.tx)
}
HtlcTxAndSigs(htlcTx, localSig, remoteSig)
}
@ -424,7 +430,7 @@ object Commitments extends Logging {
val originChannels1 = commitments.originChannels -- completedOutgoingHtlcs
val commitments1 = commitments.copy(localCommit = localCommit1, localChanges = ourChanges1, remoteChanges = theirChanges1, originChannels = originChannels1)
logger.debug(s"current commit: index=${localCommit1.index} htlc_in=${localCommit1.spec.htlcs.filter(_.direction == IN).size} htlc_out=${localCommit1.spec.htlcs.filter(_.direction == OUT).size} txid=${localCommit1.publishableTxs.commitTx.tx.txid} tx=${Transaction.write(localCommit1.publishableTxs.commitTx.tx)}")
log.debug(s"current commit: index=${localCommit1.index} htlc_in=${localCommit1.spec.htlcs.filter(_.direction == IN).size} htlc_out=${localCommit1.spec.htlcs.filter(_.direction == OUT).size} txid=${localCommit1.publishableTxs.commitTx.tx.txid} tx=${Transaction.write(localCommit1.publishableTxs.commitTx.tx)}")
(commitments1, revocation)
}

View file

@ -1,5 +1,6 @@
package fr.acinq.eclair.channel
import akka.event.LoggingAdapter
import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar, sha256}
import fr.acinq.bitcoin.Script._
import fr.acinq.bitcoin.{OutPoint, _}
@ -11,7 +12,6 @@ import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{Globals, NodeParams}
import grizzled.slf4j.Logging
import scala.concurrent.Await
import scala.util.{Failure, Success, Try}
@ -157,7 +157,7 @@ object Helpers {
}
object Closing extends Logging {
object Closing {
def isValidFinalScriptPubkey(scriptPubKey: BinaryData): Boolean = {
Try(Script.parse(scriptPubKey)) match {
@ -169,23 +169,24 @@ object Helpers {
}
}
def makeFirstClosingTx(commitments: Commitments, localScriptPubkey: BinaryData, remoteScriptPubkey: BinaryData): ClosingSigned = {
logger.debug(s"making first closing tx with commitments:\n${Commitments.specs2String(commitments)}")
def makeFirstClosingTx(commitments: Commitments, localScriptPubkey: BinaryData, remoteScriptPubkey: BinaryData)(implicit log: LoggingAdapter): ClosingSigned = {
log.debug(s"making first closing tx with commitments:\n${Commitments.specs2String(commitments)}")
import commitments._
val closingFee = {
// this is just to estimate the weight, it depends on size of the pubkey scripts
val dummyClosingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, Satoshi(0), Satoshi(0), localCommit.spec)
val closingWeight = Transaction.weight(Transactions.addSigs(dummyClosingTx, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, "aa" * 71, "bb" * 71).tx)
// no need to use a very high fee here
val feeratePerKw = Globals.feeratesPerKw.get.blocks_6
logger.info(s"using feeratePerKw=$feeratePerKw for closing tx")
// no need to use a very high fee here, so we target 6 blocks; also, we "MUST set fee_satoshis less than or equal to the base fee of the final commitment transaction"
val feeratePerKw = Math.min(Globals.feeratesPerKw.get.blocks_6, commitments.localCommit.spec.feeratePerKw)
log.info(s"using feeratePerKw=$feeratePerKw for initial closing tx")
Transactions.weight2fee(feeratePerKw, closingWeight)
}
val (_, closingSigned) = makeClosingTx(commitments, localScriptPubkey, remoteScriptPubkey, closingFee)
log.info(s"proposing closingFeeSatoshis=${closingSigned.feeSatoshis}")
closingSigned
}
def makeClosingTx(commitments: Commitments, localScriptPubkey: BinaryData, remoteScriptPubkey: BinaryData, closingFee: Satoshi): (ClosingTx, ClosingSigned) = {
def makeClosingTx(commitments: Commitments, localScriptPubkey: BinaryData, remoteScriptPubkey: BinaryData, closingFee: Satoshi)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = {
import commitments._
require(isValidFinalScriptPubkey(localScriptPubkey), "invalid localScriptPubkey")
require(isValidFinalScriptPubkey(remoteScriptPubkey), "invalid remoteScriptPubkey")
@ -194,26 +195,31 @@ object Helpers {
val closingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, dustLimitSatoshis, closingFee, localCommit.spec)
val localClosingSig = Transactions.sign(closingTx, commitments.localParams.fundingPrivKey)
val closingSigned = ClosingSigned(channelId, closingFee.amount, localClosingSig)
logger.debug(s"closingTx=${Transaction.write(closingTx.tx)}")
log.debug(s"closingTx=${Transaction.write(closingTx.tx)}")
(closingTx, closingSigned)
}
def checkClosingSignature(commitments: Commitments, localScriptPubkey: BinaryData, remoteScriptPubkey: BinaryData, remoteClosingFee: Satoshi, remoteClosingSig: BinaryData): Try[Transaction] = {
def checkClosingSignature(commitments: Commitments, localScriptPubkey: BinaryData, remoteScriptPubkey: BinaryData, remoteClosingFee: Satoshi, remoteClosingSig: BinaryData)(implicit log: LoggingAdapter): Try[Transaction] = {
import commitments._
val lastCommitFeeSatoshi = commitments.commitInput.txOut.amount.amount - commitments.localCommit.publishableTxs.commitTx.tx.txOut.map(_.amount.amount).sum
if (remoteClosingFee.amount > lastCommitFeeSatoshi) {
log.error(s"remote proposed a commit fee higher than the last commitment fee: remoteClosingFeeSatoshi=${remoteClosingFee.amount} lastCommitFeeSatoshi=$lastCommitFeeSatoshi")
throw new InvalidCloseFee(commitments.channelId, remoteClosingFee.amount)
}
val (closingTx, closingSigned) = makeClosingTx(commitments, localScriptPubkey, remoteScriptPubkey, remoteClosingFee)
val signedClosingTx = Transactions.addSigs(closingTx, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig)
Transactions.checkSpendable(signedClosingTx).map(x => signedClosingTx.tx)
Transactions.checkSpendable(signedClosingTx).map(x => signedClosingTx.tx).recover { case _ => throw InvalidCloseSignature(commitments.channelId, signedClosingTx.tx) }
}
def nextClosingFee(localClosingFee: Satoshi, remoteClosingFee: Satoshi): Satoshi = ((localClosingFee + remoteClosingFee) / 4) * 2
def generateTx(desc: String)(attempt: Try[TransactionWithInputInfo]): Option[TransactionWithInputInfo] = {
def generateTx(desc: String)(attempt: Try[TransactionWithInputInfo])(implicit log: LoggingAdapter): Option[TransactionWithInputInfo] = {
attempt match {
case Success(txinfo) =>
logger.warn(s"tx generation success: desc=$desc txid=${txinfo.tx.txid} amount=${txinfo.tx.txOut.map(_.amount.amount).sum} tx=${Transaction.write(txinfo.tx)}")
log.warning(s"tx generation success: desc=$desc txid=${txinfo.tx.txid} amount=${txinfo.tx.txOut.map(_.amount.amount).sum} tx=${Transaction.write(txinfo.tx)}")
Some(txinfo)
case Failure(t) =>
logger.warn(s"tx generation failure: desc=$desc reason: ${t.getMessage}")
log.warning(s"tx generation failure: desc=$desc reason: ${t.getMessage}")
None
}
}
@ -226,7 +232,7 @@ object Helpers {
* @param commitments our commitment data, which include payment preimages
* @return a list of transactions (one per HTLC that we can claim)
*/
def claimCurrentLocalCommitTxOutputs(commitments: Commitments, tx: Transaction): LocalCommitPublished = {
def claimCurrentLocalCommitTxOutputs(commitments: Commitments, tx: Transaction)(implicit log: LoggingAdapter): LocalCommitPublished = {
import commitments._
require(localCommit.publishableTxs.commitTx.tx.txid == tx.txid, "txid mismatch, provided tx is not the current local commit tx")
@ -294,7 +300,7 @@ object Helpers {
* @param commitments our commitment data, which include payment preimages
* @return a list of transactions (one per HTLC that we can claim)
*/
def claimRemoteCommitTxOutputs(commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction): RemoteCommitPublished = {
def claimRemoteCommitTxOutputs(commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction)(implicit log: LoggingAdapter): RemoteCommitPublished = {
import commitments.{commitInput, localParams, remoteParams}
require(remoteCommit.txid == tx.txid, "txid mismatch, provided tx is not the current remote commit tx")
val (remoteCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = Commitments.makeRemoteTxs(remoteCommit.index, localParams, remoteParams, commitInput, remoteCommit.remotePerCommitmentPoint, remoteCommit.spec)
@ -364,14 +370,14 @@ object Helpers {
*
* @return a [[RevokedCommitPublished]] object containing penalty transactions if the tx is a revoked commitment
*/
def claimRevokedRemoteCommitTxOutputs(commitments: Commitments, tx: Transaction): Option[RevokedCommitPublished] = {
def claimRevokedRemoteCommitTxOutputs(commitments: Commitments, tx: Transaction)(implicit log: LoggingAdapter): Option[RevokedCommitPublished] = {
import commitments._
require(tx.txIn.size == 1, "commitment tx should have 1 input")
val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn(0).sequence, tx.lockTime)
// this tx has been published by remote, so we need to invert local/remote params
val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, localParams.paymentBasepoint)
require(txnumber <= 0xffffffffffffL, "txnumber must be lesser than 48 bits long")
logger.warn(s"counterparty has published revoked commit txnumber=$txnumber")
log.warning(s"counterparty has published revoked commit txnumber=$txnumber")
// now we know what commit number this tx is referring to, we can derive the commitment point from the shachain
remotePerCommitmentSecrets.getHash(0xFFFFFFFFFFFFL - txnumber)
.map(d => Scalar(d))

View file

@ -140,7 +140,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, address_opt: Option[
}
stay
case Event(Pong(data), ConnectedData(transport, _, _)) =>
case Event(Pong(data), ConnectedData(_, _, _)) =>
// TODO: compute latency for remote peer ?
log.debug(s"received pong with ${data.length} bytes")
stay
@ -151,30 +151,14 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, address_opt: Option[
transport ! PoisonPill
stay
case Event(msg: Error, ConnectedData(_, _, channels)) =>
case Event(msg: Error, ConnectedData(transport, _, channels)) =>
// error messages are a bit special because they can contain either temporaryChannelId or channelId (see BOLT 1)
channels.get(TemporaryChannelId(msg.channelId)).orElse(channels.get(FinalChannelId(msg.channelId))) match {
channels.get(FinalChannelId(msg.channelId)).orElse(channels.get(TemporaryChannelId(msg.channelId))) match {
case Some(channel) => channel forward msg
case None => log.warning(s"couldn't resolve channel for $msg")
case None => transport ! Error(msg.channelId, UNKNOWN_CHANNEL_MESSAGE)
}
stay
case Event(msg: HasTemporaryChannelId, ConnectedData(_, _, channels)) if channels.contains(TemporaryChannelId(msg.temporaryChannelId)) =>
val channel = channels(TemporaryChannelId(msg.temporaryChannelId))
channel forward msg
stay
case Event(msg: HasChannelId, ConnectedData(_, _, channels)) if channels.contains(FinalChannelId(msg.channelId)) =>
val channel = channels(FinalChannelId(msg.channelId))
channel forward msg
stay
case Event(ChannelIdAssigned(channel, temporaryChannelId, channelId), d@ConnectedData(_, _, channels)) if channels.contains(TemporaryChannelId(temporaryChannelId)) =>
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)
// we won't clean it up, but we won't remember the temporary id on channel termination
stay using d.copy(channels = channels + (FinalChannelId(channelId) -> channel))
case Event(c: NewChannel, d@ConnectedData(transport, remoteInit, channels)) =>
log.info(s"requesting a new channel to $remoteNodeId with fundingSatoshis=${c.fundingSatoshis} and pushMsat=${c.pushMsat}")
val (channel, localParams) = createChannel(nodeParams, transport, funder = true, c.fundingSatoshis.toLong)
@ -183,13 +167,39 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, address_opt: Option[
channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis.amount, c.pushMsat.amount, networkFeeratePerKw, localParams, transport, remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags))
stay using d.copy(channels = channels + (TemporaryChannelId(temporaryChannelId) -> channel))
case Event(msg: OpenChannel, d@ConnectedData(transport, remoteInit, channels)) if !channels.contains(TemporaryChannelId(msg.temporaryChannelId)) =>
log.info(s"accepting a new channel to $remoteNodeId")
val (channel, localParams) = createChannel(nodeParams, transport, funder = false, fundingSatoshis = msg.fundingSatoshis)
val temporaryChannelId = msg.temporaryChannelId
channel ! INPUT_INIT_FUNDEE(temporaryChannelId, localParams, transport, remoteInit)
channel ! msg
stay using d.copy(channels = channels + (TemporaryChannelId(temporaryChannelId) -> channel))
case Event(msg: OpenChannel, d@ConnectedData(transport, remoteInit, channels)) =>
channels.get(TemporaryChannelId(msg.temporaryChannelId)) match {
case None =>
log.info(s"accepting a new channel to $remoteNodeId")
val (channel, localParams) = createChannel(nodeParams, transport, funder = false, fundingSatoshis = msg.fundingSatoshis)
val temporaryChannelId = msg.temporaryChannelId
channel ! INPUT_INIT_FUNDEE(temporaryChannelId, localParams, transport, remoteInit)
channel ! msg
stay using d.copy(channels = channels + (TemporaryChannelId(temporaryChannelId) -> channel))
case Some(_) =>
log.warning(s"ignoring open_channel with duplicate temporaryChannelId=${msg.temporaryChannelId}")
stay
}
case Event(msg: HasChannelId, ConnectedData(transport, _, channels)) =>
channels.get(FinalChannelId(msg.channelId)) match {
case Some(channel) => channel forward msg
case None => transport ! Error(msg.channelId, UNKNOWN_CHANNEL_MESSAGE)
}
stay
case Event(msg: HasTemporaryChannelId, ConnectedData(transport, _, channels)) =>
channels.get(TemporaryChannelId(msg.temporaryChannelId)) match {
case Some(channel) => channel forward msg
case None => transport ! Error(msg.temporaryChannelId, UNKNOWN_CHANNEL_MESSAGE)
}
stay
case Event(ChannelIdAssigned(channel, temporaryChannelId, channelId), d@ConnectedData(_, _, channels)) if channels.contains(TemporaryChannelId(temporaryChannelId)) =>
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)
// we won't clean it up, but we won't remember the temporary id on channel termination
stay using d.copy(channels = channels + (FinalChannelId(channelId) -> channel))
case Event(Rebroadcast(announcements, origins), ConnectedData(transport, _, _)) =>
// we filter out announcements that we received from this node
@ -259,6 +269,8 @@ object Peer {
val CHANNELID_ZERO = BinaryData("00" * 32)
val UNKNOWN_CHANNEL_MESSAGE = "unknown channel".getBytes()
def props(nodeParams: NodeParams, remoteNodeId: PublicKey, address_opt: Option[InetSocketAddress], watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, storedChannels: Set[HasCommitments]) = Props(new Peer(nodeParams, remoteNodeId, address_opt, watcher, router, relayer, wallet: EclairWallet, storedChannels))
def generateKey(nodeParams: NodeParams, keyPath: Seq[Long]): PrivateKey = DeterministicWallet.derivePrivateKey(nodeParams.extendedPrivateKey, keyPath).privateKey

View file

@ -72,4 +72,11 @@ package object eclair {
* way to tell what the script is.
*/
def isSegwitAddress(address: String) : Boolean = address.startsWith("2") || address.startsWith("3")
/**
* Tests whether the binary data is composed solely of printable ASCII characters (see BOLT 1)
*
* @param data to check
*/
def isAsciiPrintable(data: BinaryData): Boolean = data.data.forall(ch => ch >= 32 && ch < 127)
}

View file

@ -56,12 +56,7 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto
val firstHop = hops.head
val finalExpiry = Globals.blockCount.get().toInt + c.minFinalCltvExpiry.toInt
val (cmd, sharedSecrets) = buildCommand(c.amountMsat, finalExpiry, c.paymentHash, hops)
// TODO: HACK!!!! see Router.scala (we actually store the first node id in the sig)
if (firstHop.lastUpdate.signature.size == 32) {
register ! Register.Forward(firstHop.lastUpdate.signature, cmd)
} else {
register ! Register.ForwardShortId(firstHop.lastUpdate.shortChannelId, cmd)
}
register ! Register.ForwardShortId(firstHop.lastUpdate.shortChannelId, cmd)
goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(s, c, cmd, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops)
case Event(Status.Failure(t), WaitingForRoute(s, c, failures)) =>
@ -181,16 +176,16 @@ object PaymentLifecycle {
* - firstExpiry is the cltv expiry for the first htlc in the route
* - a sequence of payloads that will be used to build the onion
*/
def buildPayloads(finalAmountMsat: Long, finalExpiry: Int, hops: Seq[PaymentHop]): (Long, Int, Seq[PerHopPayload]) =
def buildPayloads(finalAmountMsat: Long, finalExpiry: Long, hops: Seq[PaymentHop]): (Long, Long, Seq[PerHopPayload]) =
hops.reverse.foldLeft((finalAmountMsat, finalExpiry, PerHopPayload(0L, finalAmountMsat, finalExpiry) :: Nil)) {
case ((msat, expiry, payloads), hop) =>
(msat + hop.nextFee(msat), expiry + hop.cltvExpiryDelta, PerHopPayload(hop.shortChannelId, msat, expiry) +: payloads)
}
// this is defined in BOLT 11
val defaultMinFinalCltvExpiry = 9
val defaultMinFinalCltvExpiry:Long = 9L
def buildCommand(finalAmountMsat: Long, finalExpiry: Int, paymentHash: BinaryData, hops: Seq[Hop]): (CMD_ADD_HTLC, Seq[(BinaryData, PublicKey)]) = {
def buildCommand(finalAmountMsat: Long, finalExpiry: Long, paymentHash: BinaryData, hops: Seq[Hop]): (CMD_ADD_HTLC, Seq[(BinaryData, PublicKey)]) = {
val (firstAmountMsat, firstExpiry, payloads) = buildPayloads(finalAmountMsat, finalExpiry, hops.drop(1))
val nodes = hops.map(_.nextNodeId)
// BOLT 2 requires that associatedData == paymentHash

View file

@ -20,7 +20,6 @@ case class Relayed(originChannelId: BinaryData, originHtlcId: Long, amountMsatIn
case class ForwardAdd(add: UpdateAddHtlc)
case class ForwardFulfill(fulfill: UpdateFulfillHtlc, to: Origin)
case class ForwardLocalFail(error: Throwable, to: Origin) // happens when the failure happened in a local channel (and not in some downstream channel)
case class ForwardFail(fail: UpdateFailHtlc, to: Origin)
case class ForwardFailMalformed(fail: UpdateFailMalformedHtlc, to: Origin)
@ -45,7 +44,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR
case ChannelStateChanged(channel, _, _, _, NORMAL | SHUTDOWN | CLOSING, d: HasCommitments) =>
import d.channelId
preimagesDb.listPreimages(channelId) match {
case Nil => {}
case Nil => ()
case preimages =>
log.info(s"re-sending ${preimages.size} unacked fulfills to channel $channelId")
preimages.map(p => CMD_FULFILL_HTLC(p._2, p._3, commit = false)).foreach(channel ! _)
@ -75,11 +74,11 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR
paymentHandler forward add
}
case Success((Attempt.Successful(DecodeResult(perHopPayload, _)), nextPacket, _)) =>
val channelUpdate_opt = channelUpdates.get(perHopPayload.channel_id)
channelUpdate_opt match {
channelUpdates.get(perHopPayload.channel_id) match {
case None =>
// TODO: clarify what we're supposed to do in the specs
sender ! CMD_FAIL_HTLC(add.id, Right(TemporaryNodeFailure), commit = true)
// if we don't (yet?) have a channel_update for the next channel, we consider the channel doesn't exist
// TODO: use a different channel to the same peer instead?
sender ! CMD_FAIL_HTLC(add.id, Right(UnknownNextPeer), commit = true)
case Some(channelUpdate) if !Announcements.isEnabled(channelUpdate.flags) =>
sender ! CMD_FAIL_HTLC(add.id, Right(ChannelDisabled(channelUpdate.flags, channelUpdate)), commit = true)
case Some(channelUpdate) if add.amountMsat < channelUpdate.htlcMinimumMsat =>
@ -90,7 +89,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR
sender ! CMD_FAIL_HTLC(add.id, Right(ExpiryTooSoon(channelUpdate)), commit = true)
case _ =>
log.info(s"forwarding htlc #${add.id} to shortChannelId=${perHopPayload.channel_id}")
register forward 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), _, _)) =>
log.error(s"couldn't parse payload: $cause")
@ -105,6 +104,20 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR
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))
case AddHtlcFailed(_, error, Local(Some(sender)), _) =>
sender ! Status.Failure(error)
case AddHtlcFailed(_, error, Relayed(originChannelId, originHtlcId, _, _), channelUpdate_opt) =>
val failure = (error, channelUpdate_opt) match {
case (_: ChannelUnavailable, Some(channelUpdate)) if !Announcements.isEnabled(channelUpdate.flags) => ChannelDisabled(channelUpdate.flags, channelUpdate)
case (_: InsufficientFunds, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
case (_: TooManyAcceptedHtlcs, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
case (_: HtlcTimedout, _) => PermanentChannelFailure
case _ => TemporaryNodeFailure
}
val cmd = CMD_FAIL_HTLC(originHtlcId, Right(failure), commit = true)
register ! Register.Forward(originChannelId, cmd)
case ForwardFulfill(fulfill, Local(Some(sender))) =>
sender ! fulfill
@ -119,18 +132,6 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR
log.debug(s"fulfill acked for channelId=$channelId htlcId=$htlcId")
preimagesDb.removePreimage(channelId, htlcId)
case ForwardLocalFail(error, Local(Some(sender))) =>
sender ! Status.Failure(error)
case ForwardLocalFail(error, Relayed(originChannelId, originHtlcId, _, _)) =>
// TODO: clarify what we're supposed to do in the specs depending on the error
val failure = error match {
case HtlcTimedout(_) => PermanentChannelFailure
case _ => TemporaryNodeFailure
}
val cmd = CMD_FAIL_HTLC(originHtlcId, Right(failure), commit = true)
register ! Register.Forward(originChannelId, cmd)
case ForwardFail(fail, Local(Some(sender))) =>
sender ! fail

View file

@ -38,7 +38,7 @@ case class Data(nodes: Map[PublicKey, NodeAnnouncement],
stash: Seq[RoutingMessage],
awaiting: Seq[ChannelAnnouncement],
origins: Map[RoutingMessage, ActorRef],
localChannels: Map[BinaryData, PublicKey],
localUpdates: Map[BinaryData, (ChannelDesc, ChannelUpdate)],
excludedChannels: Set[ChannelDesc]) // those channels are temporarily excluded from route calculation, because their node returned a TemporaryChannelFailure
sealed trait State
@ -101,7 +101,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data]
when(WAITING_FOR_VALIDATION) {
case Event(ParallelGetResponse(results), d) =>
val validated = results.map {
val validated = results.flatMap {
case IndividualResult(c, Some(tx), true) =>
// TODO: blacklisting
val (_, _, outputIndex) = fromShortId(c.shortChannelId)
@ -133,8 +133,18 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data]
// TODO: blacklist?
log.warning(s"could not retrieve tx for shortChannelId=${c.shortChannelId}")
None
}.flatten
// we reprocess node and channel-update announcements that may have been validated
}
// in case we just validated our first local channel, we announce the local node
// note that this will also make sure we always update our node announcement on restart (eg: alias, color), because
// even if we had stored a previous announcement, it would be overriden by this more recent one
if (!d.nodes.contains(nodeParams.nodeId) && validated.exists(isRelatedTo(_, nodeParams.nodeId))) {
log.info(s"first local channel validated, announcing local node")
val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses)
self ! nodeAnn
}
// we also reprocess node and channel_update announcements related to channels that were processed
val (resend, stash1) = d.stash.partition {
case n: NodeAnnouncement => results.exists(r => isRelatedTo(r.c, n.nodeId))
case u: ChannelUpdate => results.exists(r => r.c.shortChannelId == u.shortChannelId)
@ -146,10 +156,13 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data]
whenUnhandled {
case Event(ChannelStateChanged(_, _, _, _, channel.NORMAL, d: DATA_NORMAL), d1) =>
stay using d1.copy(localChannels = d1.localChannels + (d.commitments.channelId -> d.commitments.remoteParams.nodeId))
val channelDesc = ChannelDesc(d.channelUpdate.shortChannelId, d.commitments.localParams.nodeId, d.commitments.remoteParams.nodeId)
log.debug(s"added local channel_update for channelId=${d.channelId} update=${d.channelUpdate}")
stay using d1.copy(localUpdates = d1.localUpdates + (d.channelId -> (channelDesc, d.channelUpdate)))
case Event(ChannelStateChanged(_, _, _, channel.NORMAL, _, d: DATA_NEGOTIATING), d1) =>
stay using d1.copy(localChannels = d1.localChannels - d.commitments.channelId)
case Event(ChannelStateChanged(_, _, _, channel.NORMAL, _, d: HasCommitments), d1) =>
log.debug(s"removed local channel_update for channelId=${d.channelId}")
stay using d1.copy(localUpdates = d1.localUpdates - d.channelId)
case Event(_: ChannelStateChanged, _) => stay
@ -313,21 +326,13 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data]
stay
case Event(RouteRequest(start, end, ignoreNodes, ignoreChannels), d) =>
val localNodeId = nodeParams.privateKey.publicKey
// TODO: HACK!!!!! the following is a workaround to make our routing work with private/not-yet-announced channels, that do not have a channelUpdate
val fakeUpdates = d.localChannels.map { case (channelId, remoteNodeId) =>
// note that this id is deterministic, otherwise filterUpdates would not work
val fakeShortId = BigInt(channelId.take(7).toArray).toLong
val channelDesc = ChannelDesc(fakeShortId, localNodeId, remoteNodeId)
// note that we store the channelId in the sig, other values are not used because if it is selected this will be the first channel in the route
val channelUpdate = ChannelUpdate(signature = channelId, chainHash = nodeParams.chainHash, fakeShortId, 0, "0000", 0, 0, 0, 0)
(channelDesc -> channelUpdate)
}
// we replace local channelUpdates (we have them for regular public already-announced channels) by the ones we just generated
val updates1 = d.updates.filterKeys(_.a != localNodeId) ++ fakeUpdates
// we start with channel_updates of local channels
val updates0 = d.localUpdates.values.toMap
// we add them to the publicly-announced updates (channel_updates for announced channels will be deduped)
val updates1 = d.updates ++ updates0
// we then filter out the currently excluded channels
val updates2 = updates1.filterKeys(!d.excludedChannels.contains(_))
// we also filter out excluded channels
// we also filter out disabled channels, and channels/nodes that are blacklisted for this particular request
val updates3 = filterUpdates(updates2, ignoreNodes, ignoreChannels)
log.info(s"finding a route $start->$end with ignoreNodes=${ignoreNodes.map(_.toBin).mkString(",")} ignoreChannels=${ignoreChannels.map(_.toHexString).mkString(",")}")
findRoute(start, end, updates3).map(r => RouteResponse(r, ignoreNodes, ignoreChannels)) pipeTo sender
@ -380,7 +385,7 @@ object Router {
updates
.filterNot(u => ignoreNodes.map(_.toBin).contains(u._1.a) || ignoreNodes.map(_.toBin).contains(u._1.b))
.filterNot(u => ignoreChannels.contains(u._1.id))
.filterNot(u => !Announcements.isEnabled(u._2.flags))
.filter(u => Announcements.isEnabled(u._2.flags))
def findRouteDijkstra(localNodeId: PublicKey, targetNodeId: PublicKey, channels: Iterable[ChannelDesc]): Seq[ChannelDesc] = {
if (localNodeId == targetNodeId) throw CannotRouteToSelf

View file

@ -377,9 +377,6 @@ object Transactions {
closingTx.copy(tx = closingTx.tx.updateWitness(0, witness))
}
def checkSpendable(parent: Transaction, child: Transaction): Try[Unit] =
Try(Transaction.correctlySpends(child, parent :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))
def checkSpendable(txinfo: TransactionWithInputInfo): Try[Unit] =
Try(Transaction.correctlySpends(txinfo.tx, Map(txinfo.tx.txIn(0).outPoint -> txinfo.input.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))

View file

@ -196,11 +196,14 @@ object ChannelCodecs {
val DATA_WAIT_FOR_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = (
("commitments" | commitmentsCodec) ::
("shortChannelId" | uint64) ::
("lastSent" | fundingLockedCodec)).as[DATA_WAIT_FOR_FUNDING_LOCKED]
val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = (
("commitments" | commitmentsCodec) ::
("shortChannelId" | optional(bool, uint64)) ::
("shortChannelId" | uint64) ::
("channelAnnouncement" | optional(bool, channelAnnouncementCodec)) ::
("channelUpdate" | channelUpdateCodec) ::
("localAnnouncementSignatures" | optional(bool, announcementSignaturesCodec)) ::
("localShutdown" | optional(bool, shutdownCodec)) ::
("remoteShutdown" | optional(bool, shutdownCodec))).as[DATA_NORMAL]
@ -214,11 +217,12 @@ object ChannelCodecs {
("commitments" | commitmentsCodec) ::
("localShutdown" | shutdownCodec) ::
("remoteShutdown" | shutdownCodec) ::
("localClosingSigned" | closingSignedCodec)).as[DATA_NEGOTIATING]
("localClosingSigned" | listOfN(uint16, closingSignedCodec))).as[DATA_NEGOTIATING]
val DATA_CLOSING_Codec: Codec[DATA_CLOSING] = (
("commitments" | commitmentsCodec) ::
("mutualClosePublished" | optional(bool, txCodec)) ::
("localClosingSigned" | listOfN(uint16, closingSignedCodec)) ::
("mutualClosePublished" | listOfN(uint16, txCodec)) ::
("localCommitPublished" | optional(bool, localCommitPublishedCodec)) ::
("remoteCommitPublished" | optional(bool, remoteCommitPublishedCodec)) ::
("nextRemoteCommitPublished" | optional(bool, remoteCommitPublishedCodec)) ::

View file

@ -294,7 +294,7 @@ object LightningMessageCodecs {
("realm" | constant(ByteVector.fromByte(0))) ::
("channel_id" | uint64) ::
("amt_to_forward" | uint64) ::
("outgoing_cltv_value" | int32) :: // we use a signed int32, it is enough to store cltv for 40 000 years
("outgoing_cltv_value" | uint32) ::
("unused_with_v0_version_on_header" | ignore(8 * 12))).as[PerHopPayload]
}

View file

@ -157,4 +157,4 @@ case class ChannelUpdate(signature: BinaryData,
case class PerHopPayload(channel_id: Long,
amtToForward: Long,
outgoingCltvValue: Int)
outgoingCltvValue: Long)

View file

@ -0,0 +1,11 @@
akka {
loggers = ["akka.event.slf4j.Slf4jLogger"]
loglevel = "DEBUG"
actor {
debug {
# enable DEBUG logging of all LoggingFSMs for events, transitions and timers
fsm = on
}
}
}

View file

@ -77,6 +77,8 @@ trait StateTestsHelperMethods extends TestKitBase {
alice2bob.forward(bob)
bob2alice.expectMsgType[FundingLocked]
bob2alice.forward(alice)
alice2blockchain.expectMsgType[WatchConfirmed] // deeply buried
bob2blockchain.expectMsgType[WatchConfirmed] // deeply buried
awaitCond(alice.stateName == NORMAL)
awaitCond(bob.stateName == NORMAL)
}

View file

@ -1,6 +1,7 @@
package fr.acinq.eclair.channel.states.c
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.Transaction
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel._
@ -85,7 +86,7 @@ class WaitForFundingConfirmedStateSpec extends TestkitBaseClass with StateTestsH
test("recv BITCOIN_FUNDING_SPENT (other commit)") { case (alice, _, alice2bob, _, alice2blockchain) =>
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.publishableTxs.commitTx.tx
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, null)
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, Transaction(0, Nil, Nil, 0))
alice2bob.expectMsgType[Error]
alice2blockchain.expectMsg(PublishAsap(tx))
awaitCond(alice.stateName == ERR_INFORMATION_LEAK)

View file

@ -1,6 +1,7 @@
package fr.acinq.eclair.channel.states.c
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.Transaction
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel._
@ -71,10 +72,10 @@ class WaitForFundingLockedStateSpec extends TestkitBaseClass with StateTestsHelp
}
}
test("recv BITCOIN_FUNDING_SPENT (other commit)") { case (alice, _, alice2bob, bob2alice, alice2blockchain, router) =>
test("recv BITCOIN_FUNDING_SPENT (other commit)") { case (alice, _, alice2bob, _, alice2blockchain, _) =>
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.publishableTxs.commitTx.tx
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, null)
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, Transaction(0, Nil, Nil, 0))
alice2bob.expectMsgType[Error]
alice2blockchain.expectMsg(PublishAsap(tx))
alice2blockchain.expectMsgType[PublishAsap]
@ -82,7 +83,7 @@ class WaitForFundingLockedStateSpec extends TestkitBaseClass with StateTestsHelp
}
}
test("recv Error") { case (alice, _, alice2bob, bob2alice, alice2blockchain, router) =>
test("recv Error") { case (alice, _, _, _, alice2blockchain, _) =>
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.publishableTxs.commitTx.tx
alice ! Error("00" * 32, "oops".getBytes)

View file

@ -4,15 +4,16 @@ import akka.actor.Status.Failure
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.Crypto.Scalar
import fr.acinq.bitcoin.{BinaryData, Crypto, Satoshi, ScriptFlags, Transaction}
import fr.acinq.eclair.TestConstants.Bob
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.states.StateTestsHelperMethods
import fr.acinq.eclair.channel.{Data, State, _}
import fr.acinq.eclair.payment._
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions.{IN, OUT}
import fr.acinq.eclair.wire.{AnnouncementSignatures, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc}
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}
import org.junit.runner.RunWith
import org.scalatest.Tag
@ -33,6 +34,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
import setup._
within(30 seconds) {
reachNormal(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, test.tags)
relayer.expectMsgType[ChannelUpdate]
relayer.expectMsgType[ChannelUpdate]
awaitCond(alice.stateName == NORMAL)
awaitCond(bob.stateName == NORMAL)
}
@ -90,14 +93,14 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
}
}
test("recv CMD_ADD_HTLC (invalid payment hash)") { case (alice, _, alice2bob, _, _, _, relayer) =>
test("recv CMD_ADD_HTLC (invalid payment hash)") { case (alice, _, alice2bob, _, _, _, _) =>
within(30 seconds) {
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
val sender = TestProbe()
val add = CMD_ADD_HTLC(500000000, "11" * 42, expiry = 400144)
sender.send(alice, add)
val error = InvalidPaymentHash(channelId(alice))
//sender.expectMsg(Failure(InvalidPaymentHash(channelId(alice))))
relayer.expectMsg(ForwardLocalFail(error, Local(Some(sender.ref))))
sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), error, Local(Some(sender.ref)), Some(initialState.channelUpdate))))
alice2bob.expectNoMsg(200 millis)
}
}
@ -105,11 +108,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
test("recv CMD_ADD_HTLC (expiry too small)") { case (alice, _, alice2bob, _, _, _, relayer) =>
within(30 seconds) {
val sender = TestProbe()
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
val add = CMD_ADD_HTLC(500000000, "11" * 32, expiry = 300000)
sender.send(alice, add)
val error = ExpiryCannotBeInThePast(channelId(alice), 300000, 400000)
//sender.expectMsg(Failure(error))
relayer.expectMsg(ForwardLocalFail(error, Local(Some(sender.ref))))
sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), error, Local(Some(sender.ref)), Some(initialState.channelUpdate))))
alice2bob.expectNoMsg(200 millis)
}
}
@ -117,11 +120,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
test("recv CMD_ADD_HTLC (value too small)") { case (alice, _, alice2bob, _, _, _, relayer) =>
within(30 seconds) {
val sender = TestProbe()
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
val add = CMD_ADD_HTLC(50, "11" * 32, 400144)
sender.send(alice, add)
val error = HtlcValueTooSmall(channelId(alice), 1000, 50)
//sender.expectMsg(Failure(error))
relayer.expectMsg(ForwardLocalFail(error, Local(Some(sender.ref))))
sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), error, Local(Some(sender.ref)), Some(initialState.channelUpdate))))
alice2bob.expectNoMsg(200 millis)
}
}
@ -129,11 +132,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
test("recv CMD_ADD_HTLC (insufficient funds)") { case (alice, _, alice2bob, _, _, _, relayer) =>
within(30 seconds) {
val sender = TestProbe()
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
val add = CMD_ADD_HTLC(Int.MaxValue, "11" * 32, 400144)
sender.send(alice, add)
val error = InsufficientFunds(channelId(alice), amountMsat = Int.MaxValue, missingSatoshis = 1376443, reserveSatoshis = 20000, feesSatoshis = 8960)
//sender.expectMsg(Failure(error))
relayer.expectMsg(ForwardLocalFail(error, Local(Some(sender.ref))))
sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), error, Local(Some(sender.ref)), Some(initialState.channelUpdate))))
alice2bob.expectNoMsg(200 millis)
}
}
@ -141,6 +144,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs and 0 balance)") { case (alice, _, alice2bob, _, _, _, relayer) =>
within(30 seconds) {
val sender = TestProbe()
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
sender.send(alice, CMD_ADD_HTLC(500000000, "11" * 32, 400144))
sender.expectMsg("ok")
alice2bob.expectMsgType[UpdateAddHtlc]
@ -153,8 +157,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
val add = CMD_ADD_HTLC(1000000, "44" * 32, 400144)
sender.send(alice, add)
val error = InsufficientFunds(channelId(alice), amountMsat = 1000000, missingSatoshis = 1000, reserveSatoshis = 20000, feesSatoshis = 12400)
//sender.expectMsg(Failure(error))
relayer.expectMsg(ForwardLocalFail(error, Local(Some(sender.ref))))
sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), error, Local(Some(sender.ref)), Some(initialState.channelUpdate))))
alice2bob.expectNoMsg(200 millis)
}
}
@ -162,6 +165,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs 2/2)") { case (alice, _, alice2bob, _, _, _, relayer) =>
within(30 seconds) {
val sender = TestProbe()
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
sender.send(alice, CMD_ADD_HTLC(300000000, "11" * 32, 400144))
sender.expectMsg("ok")
alice2bob.expectMsgType[UpdateAddHtlc]
@ -171,8 +175,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
val add = CMD_ADD_HTLC(500000000, "33" * 32, 400144)
sender.send(alice, add)
val error = InsufficientFunds(channelId(alice), amountMsat = 500000000, missingSatoshis = 332400, reserveSatoshis = 20000, feesSatoshis = 12400)
//sender.expectMsg(Failure(error))
relayer.expectMsg(ForwardLocalFail(error, Local(Some(sender.ref))))
sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), error, Local(Some(sender.ref)), Some(initialState.channelUpdate))))
alice2bob.expectNoMsg(200 millis)
}
}
@ -180,11 +183,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
test("recv CMD_ADD_HTLC (over max inflight htlc value)") { case (_, bob, _, bob2alice, _, _, relayer) =>
within(30 seconds) {
val sender = TestProbe()
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
val add = CMD_ADD_HTLC(151000000, "11" * 32, 400144)
sender.send(bob, add)
val error = HtlcValueTooHighInFlight(channelId(bob), maximum = 150000000, actual = 151000000)
//sender.expectMsg(Failure(error))
relayer.expectMsg(ForwardLocalFail(error, Local(Some(sender.ref))))
sender.expectMsg(Failure(AddHtlcFailed(channelId(bob), error, Local(Some(sender.ref)), Some(initialState.channelUpdate))))
bob2alice.expectNoMsg(200 millis)
}
}
@ -192,6 +195,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
test("recv CMD_ADD_HTLC (over max accepted htlcs)") { case (alice, _, alice2bob, _, _, _, relayer) =>
within(30 seconds) {
val sender = TestProbe()
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, "11" * 32, 400144))
@ -201,15 +205,15 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
val add = CMD_ADD_HTLC(10000000, "33" * 32, 400144)
sender.send(alice, add)
val error = TooManyAcceptedHtlcs(channelId(alice), maximum = 30)
//sender.expectMsg(Failure(error))
relayer.expectMsg(ForwardLocalFail(error, Local(Some(sender.ref))))
sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), error, Local(Some(sender.ref)), Some(initialState.channelUpdate))))
alice2bob.expectNoMsg(200 millis)
}
}
test("recv CMD_ADD_HTLC (while waiting for a revocation)") { case (alice, _, alice2bob, _, _, _, relayer) =>
test("recv CMD_ADD_HTLC (over capacity)") { case (alice, _, alice2bob, _, _, _, relayer) =>
within(30 seconds) {
val sender = TestProbe()
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
val add1 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, "11" * 32, 400144)
sender.send(alice, add1)
sender.expectMsg("ok")
@ -220,8 +224,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
// this is over channel-capacity
val add2 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, "22" * 32, 400144)
sender.send(alice, add2)
//sender.expectMsgType[Failure]
relayer.expectMsgType[ForwardLocalFail]
val error = InsufficientFunds(channelId(alice), add2.amountMsat, 564012, 20000, 10680)
sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), error, Local(Some(sender.ref)), Some(initialState.channelUpdate))))
alice2bob.expectNoMsg(200 millis)
}
}
@ -229,6 +233,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
test("recv CMD_ADD_HTLC (after having sent Shutdown)") { case (alice, _, alice2bob, _, _, _, relayer) =>
within(30 seconds) {
val sender = TestProbe()
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
sender.send(alice, CMD_CLOSE(None))
sender.expectMsg("ok")
alice2bob.expectMsgType[Shutdown]
@ -237,9 +242,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
// actual test starts here
val add = CMD_ADD_HTLC(500000000, "11" * 32, expiry = 400144)
sender.send(alice, add)
val error = ClosingInProgress(channelId(alice))
//sender.expectMsg(Failure(error))
relayer.expectMsg(ForwardLocalFail(error, Local(Some(sender.ref))))
val error = NoMoreHtlcsClosingInProgress(channelId(alice))
sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), error, Local(Some(sender.ref)), Some(initialState.channelUpdate))))
alice2bob.expectNoMsg(200 millis)
}
}
@ -247,6 +251,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
test("recv CMD_ADD_HTLC (after having received Shutdown)") { case (alice, bob, alice2bob, bob2alice, _, _, relayer) =>
within(30 seconds) {
val sender = TestProbe()
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
// let's make alice send an htlc
val add1 = CMD_ADD_HTLC(500000000, "11" * 32, expiry = 400144)
sender.send(alice, add1)
@ -262,9 +267,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
bob2alice.expectMsgType[Shutdown]
bob2alice.forward(alice)
sender.send(alice, add2)
val error = ClosingInProgress(channelId(alice))
//sender.expectMsg(Failure(error))
relayer.expectMsg(ForwardLocalFail(error, Local(Some(sender.ref))))
val error = NoMoreHtlcsClosingInProgress(channelId(alice))
sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), error, Local(Some(sender.ref)), Some(initialState.channelUpdate))))
}
}
@ -424,7 +428,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
}
}
test("recv CMD_SIGN (no changes)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) =>
test("recv CMD_SIGN (no changes)") { case (alice, _, _, _, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
sender.send(alice, CMD_SIGN)
@ -433,7 +437,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
}
}
test("recv CMD_SIGN (while waiting for RevokeAndAck (no pending changes)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) =>
test("recv CMD_SIGN (while waiting for RevokeAndAck (no pending changes)") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
@ -452,7 +456,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
}
}
test("recv CMD_SIGN (while waiting for RevokeAndAck (with pending changes)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) =>
test("recv CMD_SIGN (while waiting for RevokeAndAck (with pending changes)") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r1, htlc1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
@ -556,6 +560,25 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
}
}
test("recv CommitSig (only fee update)") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
sender.send(alice, CMD_UPDATE_FEE(TestConstants.feeratePerKw + 1000, commit = false))
sender.expectMsg("ok")
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
// actual test begins (note that channel sends a CMD_SIGN to itself when it receives RevokeAndAck and there are changes)
alice2bob.expectMsgType[UpdateFee]
alice2bob.forward(bob)
alice2bob.expectMsgType[CommitSig]
alice2bob.forward(bob)
bob2alice.expectMsgType[RevokeAndAck]
bob2alice.forward(alice)
}
}
// TODO: maybe should be illegal?
ignore("recv CommitSig (two htlcs received with same r)") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
@ -606,15 +629,60 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
// actual test begins
sender.send(bob, CommitSig("00" * 32, "00" * 64, Nil))
bob2alice.expectMsgType[Error]
awaitCond(bob.stateName == CLOSING)
val error = bob2alice.expectMsgType[Error]
assert(new String(error.data).startsWith("invalid commitment signature"))
bob2blockchain.expectMsg(PublishAsap(tx))
bob2blockchain.expectMsgType[PublishAsap]
bob2blockchain.expectMsgType[WatchConfirmed]
}
}
test("recv RevokeAndAck (one htlc sent)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) =>
test("recv CommitSig (bad htlc sig count)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
val commitSig = alice2bob.expectMsgType[CommitSig]
// actual test begins
val badCommitSig = commitSig.copy(htlcSignatures = commitSig.htlcSignatures ::: commitSig.htlcSignatures)
sender.send(bob, badCommitSig)
val error = bob2alice.expectMsgType[Error]
assert(new String(error.data) === HtlcSigCountMismatch(channelId(bob), expected = 1, actual = 2).getMessage)
bob2blockchain.expectMsg(PublishAsap(tx))
bob2blockchain.expectMsgType[PublishAsap]
bob2blockchain.expectMsgType[WatchConfirmed]
}
}
test("recv CommitSig (invalid htlc sig)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
val commitSig = alice2bob.expectMsgType[CommitSig]
// actual test begins
val badCommitSig = commitSig.copy(htlcSignatures = commitSig.signature :: Nil)
sender.send(bob, badCommitSig)
val error = bob2alice.expectMsgType[Error]
assert(new String(error.data).startsWith("invalid htlc signature"))
bob2blockchain.expectMsg(PublishAsap(tx))
bob2blockchain.expectMsgType[PublishAsap]
bob2blockchain.expectMsgType[WatchConfirmed]
}
}
test("recv RevokeAndAck (one htlc sent)") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
@ -1646,7 +1714,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
val sender = TestProbe()
sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10))
val annSigs = alice2bob.expectMsgType[AnnouncementSignatures]
assert(alice.stateData.asInstanceOf[DATA_NORMAL] === initialState.copy(localAnnouncementSignatures = Some(annSigs)))
assert(alice.stateData.asInstanceOf[DATA_NORMAL] === initialState.copy(shortChannelId = annSigs.shortChannelId, localAnnouncementSignatures = Some(annSigs)))
}
}
@ -1658,9 +1726,13 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
val annSigsA = alice2bob.expectMsgType[AnnouncementSignatures]
sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10))
val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures]
import initialState.commitments.localParams
import initialState.commitments.remoteParams
val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, localParams.nodeId, remoteParams.nodeId, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature)
val channelUpdate = Announcements.makeChannelUpdate(Alice.nodeParams.chainHash, Alice.nodeParams.privateKey, remoteParams.nodeId, annSigsA.shortChannelId, Alice.nodeParams.expiryDeltaBlocks, Bob.nodeParams.htlcMinimumMsat, Alice.nodeParams.feeBaseMsat, Alice.nodeParams.feeProportionalMillionth)
// actual test starts here
bob2alice.forward(alice)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL] == initialState.copy(shortChannelId = Some(annSigsB.shortChannelId), localAnnouncementSignatures = Some(annSigsA)))
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL] == initialState.copy(shortChannelId = annSigsA.shortChannelId, channelAnnouncement = Some(channelAnn), channelUpdate = channelUpdate, localAnnouncementSignatures = Some(annSigsA)))
}
}
@ -1672,8 +1744,12 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
val annSigsA = alice2bob.expectMsgType[AnnouncementSignatures]
sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10))
val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures]
import initialState.commitments.localParams
import initialState.commitments.remoteParams
val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, localParams.nodeId, remoteParams.nodeId, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature)
val channelUpdate = Announcements.makeChannelUpdate(Alice.nodeParams.chainHash, Alice.nodeParams.privateKey, remoteParams.nodeId, annSigsA.shortChannelId, Alice.nodeParams.expiryDeltaBlocks, Bob.nodeParams.htlcMinimumMsat, Alice.nodeParams.feeBaseMsat, Alice.nodeParams.feeProportionalMillionth)
bob2alice.forward(alice)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL] == initialState.copy(shortChannelId = Some(annSigsB.shortChannelId), localAnnouncementSignatures = Some(annSigsA)))
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL] == initialState.copy(shortChannelId = annSigsA.shortChannelId, channelAnnouncement = Some(channelAnn), channelUpdate = channelUpdate, localAnnouncementSignatures = Some(annSigsA)))
// actual test starts here
// simulate bob re-sending its sigs

View file

@ -8,8 +8,8 @@ import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.fee.FeeratesPerKw
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.channel.{Data, State, _}
import fr.acinq.eclair.payment.{ForwardAdd, ForwardLocalFail, Local, PaymentLifecycle, _}
import fr.acinq.eclair.wire.{CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc}
import fr.acinq.eclair.payment.{ForwardAdd, Local, PaymentLifecycle, _}
import fr.acinq.eclair.wire.{ChannelUpdate, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc}
import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass}
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
@ -29,6 +29,8 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
import setup._
within(30 seconds) {
reachNormal(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)
relayer.expectMsgType[ChannelUpdate]
relayer.expectMsgType[ChannelUpdate]
val sender = TestProbe()
// alice sends an HTLC to bob
val r1: BinaryData = "11" * 32
@ -78,14 +80,13 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
}
}
test("recv CMD_ADD_HTLC") { case (alice, _, alice2bob, _, _, _, relayer) =>
test("recv CMD_ADD_HTLC") { case (alice, _, alice2bob, _, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val add = CMD_ADD_HTLC(500000000, "11" * 32, expiry = 300000)
sender.send(alice, add)
val error = ChannelUnavailable(channelId(alice))
//sender.expectMsg(Failure(error))
relayer.expectMsg(ForwardLocalFail(error, Local(Some(sender.ref))))
sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), error, Local(Some(sender.ref)), None)))
alice2bob.expectNoMsg(200 millis)
}
}

View file

@ -2,17 +2,20 @@ package fr.acinq.eclair.channel.states.g
import akka.actor.Status.Failure
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.Satoshi
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.fee.FeeratesPerKw
import fr.acinq.eclair.channel.Helpers.Closing
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.channel.{Data, State, _}
import fr.acinq.eclair.wire.{ClosingSigned, Error, Shutdown}
import fr.acinq.eclair.wire.{ClosingSigned, CommitSig, Error, Shutdown}
import fr.acinq.eclair.{Globals, TestkitBaseClass}
import org.junit.runner.RunWith
import org.scalatest.Tag
import org.scalatest.junit.JUnitRunner
import scala.concurrent.duration._
import scala.util.Success
/**
* Created by PM on 05/07/2016.
@ -37,7 +40,7 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods
// NB: at this point, bob has already computed and sent the first ClosingSigned message
// In order to force a fee negotiation, we will change the current fee before forwarding
// the Shutdown message to alice, so that alice computes a different initial closing fee.
if (test.tags.contains("fee2")) Globals.feeratesPerKw.set(FeeratesPerKw.single(4316)) else Globals.feeratesPerKw.set(FeeratesPerKw.single(20000))
if (test.tags.contains("fee2")) Globals.feeratesPerKw.set(FeeratesPerKw.single(4316)) else Globals.feeratesPerKw.set(FeeratesPerKw.single(5000))
bob2alice.forward(alice)
awaitCond(alice.stateName == NEGOTIATING)
awaitCond(bob.stateName == NEGOTIATING)
@ -45,18 +48,18 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods
}
}
test("recv ClosingSigned (theirCloseFee != ourCloseFee)") { case (alice, bob, alice2bob, bob2alice, _, _) =>
test("recv ClosingSigned (theirCloseFee != ourCloseFee)") { case (alice, _, alice2bob, bob2alice, _, _) =>
within(30 seconds) {
val aliceCloseSig1 = alice2bob.expectMsgType[ClosingSigned]
val bobCloseSig = bob2alice.expectMsgType[ClosingSigned]
assert(aliceCloseSig1.feeSatoshis == 2 * bobCloseSig.feeSatoshis)
assert(bobCloseSig.feeSatoshis == 2 * aliceCloseSig1.feeSatoshis)
// actual test starts here
val initialState = alice.stateData.asInstanceOf[DATA_NEGOTIATING]
bob2alice.forward(alice)
val aliceCloseSig2 = alice2bob.expectMsgType[ClosingSigned]
// BOLT 2: If the receiver [doesn't agree with the fee] it SHOULD propose a value strictly between the received fee-satoshis and its previously-sent fee-satoshis
assert(aliceCloseSig2.feeSatoshis < aliceCloseSig1.feeSatoshis && aliceCloseSig2.feeSatoshis > bobCloseSig.feeSatoshis)
awaitCond(alice.stateData.asInstanceOf[DATA_NEGOTIATING] == initialState.copy(localClosingSigned = aliceCloseSig2))
assert(aliceCloseSig2.feeSatoshis > aliceCloseSig1.feeSatoshis && aliceCloseSig2.feeSatoshis < bobCloseSig.feeSatoshis)
awaitCond(alice.stateData.asInstanceOf[DATA_NEGOTIATING] == initialState.copy(localClosingSigned = initialState.localClosingSigned :+ aliceCloseSig2))
}
}
@ -92,6 +95,34 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods
testFeeConverge(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)
}
test("recv ClosingSigned (fee too high)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) =>
within(30 seconds) {
val closingSigned = bob2alice.expectMsgType[ClosingSigned]
val sender = TestProbe()
val tx = bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localCommit.publishableTxs.commitTx.tx
sender.send(bob, closingSigned.copy(feeSatoshis = 99000)) // sig doesn't matter, it is checked later
val error = bob2alice.expectMsgType[Error]
assert(new String(error.data).startsWith("invalid close fee: fee_satoshis=99000"))
bob2blockchain.expectMsg(PublishAsap(tx))
bob2blockchain.expectMsgType[PublishAsap]
bob2blockchain.expectMsgType[WatchConfirmed]
}
}
test("recv ClosingSigned (invalid sig)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) =>
within(30 seconds) {
val closingSigned = bob2alice.expectMsgType[ClosingSigned]
val sender = TestProbe()
val tx = bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localCommit.publishableTxs.commitTx.tx
sender.send(bob, closingSigned.copy(signature = "00" * 64))
val error = bob2alice.expectMsgType[Error]
assert(new String(error.data).startsWith("invalid close signature"))
bob2blockchain.expectMsg(PublishAsap(tx))
bob2blockchain.expectMsgType[PublishAsap]
bob2blockchain.expectMsgType[WatchConfirmed]
}
}
test("recv BITCOIN_FUNDING_SPENT (counterparty's mutual close)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) =>
within(30 seconds) {
var aliceCloseFee, bobCloseFee = 0L
@ -111,10 +142,30 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods
assert(bob2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(mutualCloseTx))
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, mutualCloseTx)
alice2blockchain.expectNoMsg(1 second)
assert(alice.stateName == NEGOTIATING)
assert(alice.stateName == CLOSING)
}
}
test("recv BITCOIN_FUNDING_SPENT (an older mutual close)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) =>
within(30 seconds) {
val aliceClose1 = alice2bob.expectMsgType[ClosingSigned]
alice2bob.forward(bob)
val bobClose1 = bob2alice.expectMsgType[ClosingSigned]
bob2alice.forward(alice)
val aliceClose2 = alice2bob.expectMsgType[ClosingSigned]
assert(aliceClose2.feeSatoshis != bobClose1.feeSatoshis)
// at this point alice and bob have not yet converged on closing fees, but bob decides to publish a mutual close with one of the previous sigs
val d = bob.stateData.asInstanceOf[DATA_NEGOTIATING]
implicit val log = bob.underlyingActor.implicitLog
val Success(bobClosingTx) = Closing.checkClosingSignature(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, Satoshi(aliceClose1.feeSatoshis), aliceClose1.signature)
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobClosingTx)
alice2blockchain.expectNoMsg(1 second)
assert(alice.stateName == CLOSING)
}
}
test("recv CMD_CLOSE") { case (alice, _, _, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()

View file

@ -28,7 +28,8 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
import setup._
within(30 seconds) {
reachNormal(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)
relayer.expectMsgType[ChannelUpdate]
relayer.expectMsgType[ChannelUpdate]
val bobCommitTxes: List[Transaction] = (for (amt <- List(100000000, 200000000, 300000000)) yield {
val (r, htlc) = addHtlc(amt, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
@ -104,7 +105,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
test("recv BITCOIN_TX_CONFIRMED (mutual close)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _, _) =>
within(30 seconds) {
mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)
val mutualCloseTx = alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.get
val mutualCloseTx = alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.last
// actual test starts here
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(mutualCloseTx), 0, 0)
@ -112,7 +113,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
}
}
test("recv BITCOIN_FUNDING_SPENT (our commit)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _, _) =>
test("recv BITCOIN_FUNDING_SPENT (our commit)") { case (alice, _, _, _, alice2blockchain, _, _, _) =>
within(30 seconds) {
// an error occurs and alice publishes her commit tx
val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
@ -264,7 +265,6 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
test("recv BITCOIN_TX_CONFIRMED (one revoked tx)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _, bobCommitTxes) =>
within(30 seconds) {
mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)
val initialState = alice.stateData.asInstanceOf[DATA_CLOSING]
// bob publishes one of his revoked txes
val bobRevokedTx = bobCommitTxes.head
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx)
@ -284,6 +284,17 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
}
}
test("recv ChannelReestablish") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _, _) =>
within(30 seconds) {
mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)
val initialState = alice.stateData.asInstanceOf[DATA_CLOSING]
val sender = TestProbe()
sender.send(alice, ChannelReestablish(channelId(bob), 42, 42))
val error = alice2bob.expectMsgType[Error]
assert(new String(error.data) === FundingTxSpent(channelId(alice), initialState.spendingTxes.head).getMessage)
}
}
test("recv CMD_CLOSE") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _, _) =>
within(30 seconds) {
mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)

View file

@ -6,9 +6,10 @@ import fr.acinq.eclair.channel.Helpers.Funding
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.{ShaChain, Sphinx}
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, UpdateAddHtlc}
import fr.acinq.eclair.wire.{ChannelCodecs, ChannelUpdate, UpdateAddHtlc}
import fr.acinq.eclair.{UInt64, randomKey}
import org.junit.runner.RunWith
import org.scalatest.FunSuite
@ -96,5 +97,7 @@ object ChannelStateSpec {
remoteNextCommitInfo = Right(randomKey.publicKey),
commitInput = commitmentInput, remotePerCommitmentSecrets = ShaChain.init, channelId = "00" * 32)
val normal = DATA_NORMAL(commitments, Some(42), None, None, None)
val channelUpdate = Announcements.makeChannelUpdate("11" * 32, randomKey, randomKey.publicKey, 142553, 42, 15, 575, 53)
val normal = DATA_NORMAL(commitments, 42, None, channelUpdate, None, None, None)
}

View file

@ -7,8 +7,9 @@ import fr.acinq.bitcoin.{BinaryData, Crypto, MilliSatoshi}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.PaymentLifecycle.buildCommand
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{TestConstants, TestkitBaseClass}
import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass}
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
@ -64,7 +65,7 @@ class RelayerSpec extends TestkitBaseClass {
paymentHandler.expectNoMsg(500 millis)
}
test("fail to relay an htlc-add when there is no available upstream channel") { case (relayer, register, paymentHandler) =>
test("fail to relay an htlc-add when we have no channel_update for the next channel") { case (relayer, register, paymentHandler) =>
val sender = TestProbe()
// we use this to build a valid onion
@ -76,15 +77,33 @@ class RelayerSpec extends TestkitBaseClass {
val fail = sender.expectMsgType[CMD_FAIL_HTLC]
assert(fail.id === add_ab.id)
assert(fail.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) =>
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)
val channelUpdate_bc_disabled = channelUpdate_bc.copy(flags = Announcements.makeFlags(Announcements.isNode1(channelUpdate_bc.flags), enable = false))
relayer ! channelUpdate_bc_disabled
sender.send(relayer, ForwardAdd(add_ab))
val fail = sender.expectMsgType[CMD_FAIL_HTLC]
assert(fail.id === add_ab.id)
assert(fail.reason == Right(ChannelDisabled(channelUpdate_bc_disabled.flags, channelUpdate_bc_disabled)))
register.expectNoMsg(500 millis)
paymentHandler.expectNoMsg(500 millis)
}
test("fail to relay an htlc-add when the onion is malformed") { case (relayer, register, paymentHandler) =>
// TODO: we should use the new update_fail_malformed_htlc message (see BOLT 2)
val sender = TestProbe()
// we use this to build a valid onion
@ -107,7 +126,7 @@ class RelayerSpec extends TestkitBaseClass {
val sender = TestProbe()
// we use this to build a valid onion
val (cmd, secrets) = buildCommand(channelUpdate_bc.htlcMinimumMsat - 1, finalExpiry, paymentHash, hops.map(hop => hop.copy(lastUpdate = hop.lastUpdate.copy(feeBaseMsat = 0, feeProportionalMillionths = 0))))
val (cmd, _) = buildCommand(channelUpdate_bc.htlcMinimumMsat - 1, finalExpiry, paymentHash, hops.map(hop => hop.copy(lastUpdate = hop.lastUpdate.copy(feeBaseMsat = 0, feeProportionalMillionths = 0))))
// 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
@ -126,7 +145,7 @@ class RelayerSpec extends TestkitBaseClass {
val sender = TestProbe()
val hops1 = hops.updated(1, hops(1).copy(lastUpdate = hops(1).lastUpdate.copy(cltvExpiryDelta = 0)))
val (cmd, secrets) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1)
val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1)
// 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
@ -144,7 +163,7 @@ class RelayerSpec extends TestkitBaseClass {
test("fail to relay an htlc-add when expiry is too soon") { case (relayer, register, paymentHandler) =>
val sender = TestProbe()
val (cmd, secrets) = buildCommand(finalAmountMsat, 0, paymentHash, hops)
val (cmd, _) = buildCommand(finalAmountMsat, 0, 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
@ -164,7 +183,7 @@ class RelayerSpec extends TestkitBaseClass {
// to simulate this we use a zero-hop route A->B where A is the 'attacker'
val hops1 = hops.head :: Nil
val (cmd, secrets) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1)
val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1)
// and then manually build an htlc with a wrong expiry
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat - 1, cmd.paymentHash, cmd.expiry, cmd.onion)
relayer ! channelUpdate_bc
@ -184,7 +203,7 @@ class RelayerSpec extends TestkitBaseClass {
// to simulate this we use a zero-hop route A->B where A is the 'attacker'
val hops1 = hops.head :: Nil
val (cmd, secrets) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1)
val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1)
// and then manually build an htlc with a wrong expiry
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry - 1, cmd.onion)
relayer ! channelUpdate_bc
@ -199,6 +218,78 @@ class RelayerSpec extends TestkitBaseClass {
paymentHandler.expectNoMsg(500 millis)
}
test("fail to relay an htlc-add when next channel's balance is too low") { case (relayer, register, paymentHandler) =>
val sender = TestProbe()
val (cmd, _) = buildCommand(finalAmountMsat, Globals.blockCount.get().toInt + 10, 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 fwd = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]]
assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId)
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)))
val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(fail.id === add_ab.id)
assert(fail.reason == Right(TemporaryChannelFailure(channelUpdate_bc)))
register.expectNoMsg(500 millis)
paymentHandler.expectNoMsg(500 millis)
}
test("fail to relay an htlc-add when next channel has too many inflight htlcs") { case (relayer, register, paymentHandler) =>
val sender = TestProbe()
val (cmd, _) = buildCommand(finalAmountMsat, Globals.blockCount.get().toInt + 10, 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 fwd = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]]
assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId)
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)))
val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(fail.id === add_ab.id)
assert(fail.reason == Right(TemporaryChannelFailure(channelUpdate_bc)))
register.expectNoMsg(500 millis)
paymentHandler.expectNoMsg(500 millis)
}
test("fail to relay an htlc-add when next channel has a timed out htlc (and is thus closing)") { case (relayer, register, paymentHandler) =>
val sender = TestProbe()
val (cmd, _) = buildCommand(finalAmountMsat, Globals.blockCount.get().toInt + 10, 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 fwd = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]]
assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId)
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)))
val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(fail.id === add_ab.id)
assert(fail.reason == Right(PermanentChannelFailure))
register.expectNoMsg(500 millis)
paymentHandler.expectNoMsg(500 millis)
}
test("relay an htlc-fulfill") { case (relayer, register, _) =>
val sender = TestProbe()
val eventListener = TestProbe()

View file

@ -198,7 +198,6 @@ class LightningMessageCodecsSpec extends FunSuite {
msgs.foreach {
case msg => {
println(msg)
val encoded = lightningMessageCodec.encode(msg).require
val decoded = lightningMessageCodec.decode(encoded).require
assert(msg === decoded.value)

View file

@ -15,17 +15,6 @@
<build>
<plugins>
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>revision</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>

View file

@ -0,0 +1,17 @@
akka {
loggers = ["akka.event.slf4j.Slf4jLogger"]
loglevel = "DEBUG"
actor {
debug {
# enable DEBUG logging of all LoggingFSMs for events, transitions and timers
fsm = on
}
}
http {
host-connection-pool {
max-open-requests = 64
}
}
}

43
pom.xml
View file

@ -46,6 +46,9 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
<!-- default tag used when building without git -->
<git.commit.id>notag</git.commit.id>
<git.commit.id.abbrev>notag</git.commit.id.abbrev>
<scala.version>2.11.11</scala.version>
<scala.version.short>2.11</scala.version.short>
<akka.version>2.3.14</akka.version>
@ -56,6 +59,11 @@
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
@ -64,7 +72,7 @@
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
<version>2.2.2</version>
<version>2.2.3</version>
</plugin>
<plugin>
<groupId>com.github.chrisdchristo</groupId>
@ -79,38 +87,53 @@
<artifactId>versions-maven-plugin</artifactId>
<version>2.3</version>
</plugin>
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
<version>2.2.2</version>
<executions>
<execution>
<goals>
<goal>revision</goal>
</goals>
<configuration>
<failOnNoGitDirectory>false</failOnNoGitDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<args>
<args combine.children="append">
<arg>-deprecation</arg>
<arg>-feature</arg>
<arg>-language:postfixOps</arg>
<arg>-language:implicitConversions</arg>
<arg>-Xfatal-warnings</arg>
</args>
<scalaCompatVersion>${scala.version.short}</scalaCompatVersion>
</configuration>
<executions>
<execution>
<id>process-resources</id>
<phase>process-resources</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>scalac</id>
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
</execution>
<execution>
<id>attach-javadocs</id>
<id>scaladoc</id>
<goals>
<goal>doc-jar</goal>
</goals>
<configuration>
<args>
<arg>-no-link-warnings</arg>
</args>
</configuration>
</execution>
</executions>
</plugin>