1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-23 14:40:34 +01:00

added a bunch of tests to NORMAL state

This commit is contained in:
pm47 2016-07-11 19:35:54 +02:00
parent 51bb812fb5
commit c79b101291
13 changed files with 484 additions and 59 deletions

View file

@ -12,6 +12,7 @@ import lightning.open_channel.anchor_offer.{WILL_CREATE_ANCHOR, WONT_CREATE_ANCH
import scala.util.{Failure, Success, Try}
import scala.concurrent.duration._
import scala.util.control.NonFatal
/**
* Created by PM on 20/08/2015.
@ -45,7 +46,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
888 888 Y8888 888 888
8888888 888 Y888 8888888 888
*/
when(OPEN_WAIT_FOR_OPEN_NOANCHOR) {
when(OPEN_WAIT_FOR_OPEN_NOANCHOR)(handleExceptions {
case Event(open_channel(delay, theirRevocationHash, theirNextRevocationHash, commitKey, finalKey, WILL_CREATE_ANCHOR, minDepth, initialFeeRate), DATA_OPEN_WAIT_FOR_OPEN(ourParams)) =>
val theirParams = TheirChannelParams(delay, commitKey, finalKey, minDepth, initialFeeRate)
goto(OPEN_WAIT_FOR_ANCHOR) using DATA_OPEN_WAIT_FOR_ANCHOR(ourParams, theirParams, theirRevocationHash, theirNextRevocationHash)
@ -55,9 +56,9 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
goto(CLOSED)
case Event(CMD_CLOSE(_), _) => goto(CLOSED)
}
})
when(OPEN_WAIT_FOR_OPEN_WITHANCHOR) {
when(OPEN_WAIT_FOR_OPEN_WITHANCHOR)(handleExceptions {
case Event(open_channel(delay, theirRevocationHash, theirNextRevocationHash, commitKey, finalKey, WONT_CREATE_ANCHOR, minDepth, initialFeeRate), DATA_OPEN_WAIT_FOR_OPEN(ourParams)) =>
val theirParams = TheirChannelParams(delay, commitKey, finalKey, minDepth, initialFeeRate)
log.debug(s"their params: $theirParams")
@ -76,9 +77,9 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
goto(CLOSED)
case Event(CMD_CLOSE(_), _) => goto(CLOSED)
}
})
when(OPEN_WAIT_FOR_ANCHOR) {
when(OPEN_WAIT_FOR_ANCHOR)(handleExceptions {
case Event(open_anchor(anchorTxHash, anchorOutputIndex, anchorAmount), DATA_OPEN_WAIT_FOR_ANCHOR(ourParams, theirParams, theirRevocationHash, theirNextRevocationHash)) =>
val anchorTxid = anchorTxHash.reverse //see https://github.com/ElementsProject/lightning/issues/17
@ -109,9 +110,9 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
goto(CLOSED)
case Event(CMD_CLOSE(_), _) => goto(CLOSED)
}
})
when(OPEN_WAIT_FOR_COMMIT_SIG) {
when(OPEN_WAIT_FOR_COMMIT_SIG)(handleExceptions {
case Event(open_commit_sig(theirSig), DATA_OPEN_WAIT_FOR_COMMIT_SIG(ourParams, theirParams, anchorTx, anchorOutputIndex, theirCommitment, theirNextRevocationHash)) =>
val anchorAmount = anchorTx.txOut(anchorOutputIndex).amount
val theirSpec = theirCommitment.spec
@ -145,9 +146,9 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
goto(CLOSED)
case Event(CMD_CLOSE(_), _) => goto(CLOSED)
}
})
when(OPEN_WAITING_THEIRANCHOR) {
when(OPEN_WAITING_THEIRANCHOR)(handleExceptions {
case Event(msg@open_complete(blockId_opt), d: DATA_OPEN_WAITING) =>
log.info(s"received their open_complete, deferring message")
stay using d.copy(deferred = Some(msg))
@ -174,9 +175,9 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
goto(CLOSED)
case Event(CMD_CLOSE(_), _) => goto(CLOSED)
}
})
when(OPEN_WAITING_OURANCHOR) {
when(OPEN_WAITING_OURANCHOR)(handleExceptions {
case Event(msg@open_complete(blockId_opt), d: DATA_OPEN_WAITING) =>
log.info(s"received their open_complete, deferring message")
stay using d.copy(deferred = Some(msg))
@ -209,10 +210,9 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
})
}
when(OPEN_WAIT_FOR_COMPLETE_THEIRANCHOR) {
when(OPEN_WAIT_FOR_COMPLETE_THEIRANCHOR)(handleExceptions {
case Event(open_complete(blockid_opt), d: DATA_NORMAL) =>
Register.create_alias(theirNodeId, d.commitments.anchorId)
goto(NORMAL)
@ -228,9 +228,9 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
goto(CLOSED)
case Event(CMD_CLOSE(_), _) => goto(CLOSED)
}
})
when(OPEN_WAIT_FOR_COMPLETE_OURANCHOR) {
when(OPEN_WAIT_FOR_COMPLETE_OURANCHOR)(handleExceptions {
case Event(open_complete(blockid_opt), d: DATA_NORMAL) =>
Register.create_alias(theirNodeId, d.commitments.anchorId)
goto(NORMAL)
@ -252,7 +252,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
}
})
/*
@ -267,25 +267,45 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
*/
when(NORMAL) {
case Event(CMD_ADD_HTLC(amount, rHash, expiry, nodeIds, origin, id_opt), d@DATA_NORMAL(commitments, htlcIdx, _)) =>
// TODO: should we take pending htlcs into account?
// TODO: assert(commitment.state.commit_changes(staged).us.pay_msat >= amount, "insufficient funds!")
// TODO: nodeIds are ignored
val id: Long = id_opt.getOrElse(htlcIdx + 1)
val htlc = update_add_htlc(id, amount, rHash, expiry, routing(ByteString.EMPTY))
them ! htlc
stay using d.copy(htlcIdx = htlc.id, commitments = commitments.addOurProposal(htlc))
// TODO : this should probably be done in Commitments.scala
// our available funds as seen by them, including all pending changes
val reduced = reduce(commitments.theirCommit.spec, commitments.theirChanges.acked, commitments.ourChanges.acked ++ commitments.ourChanges.signed ++ commitments.ourChanges.proposed)
// the pending htlcs that we sent to them (seen as IN from their pov) have already been deduced from our balance
val available = reduced.amount_them_msat + reduced.htlcs.filter(_.direction == OUT).map(-_.amountMsat).sum
if (amount > available) {
sender ! s"insufficient funds (available=$available msat)"
stay
} else {
// TODO: nodeIds are ignored
val id: Long = id_opt.getOrElse(htlcIdx + 1)
val htlc = update_add_htlc(id, amount, rHash, expiry, routing(ByteString.EMPTY))
them ! htlc
sender ! "ok"
stay using d.copy(htlcIdx = htlc.id, commitments = commitments.addOurProposal(htlc))
}
case Event(htlc@update_add_htlc(htlcId, amount, rHash, expiry, nodeIds), d@DATA_NORMAL(commitments, _, _)) =>
// TODO: should we take pending htlcs into account?
// assert(commitment.state.commit_changes(staged).them.pay_msat >= amount, "insufficient funds!") // TODO : we should fail the channel
// TODO: nodeIds are ignored
stay using d.copy(commitments = commitments.addTheirProposal(htlc))
// TODO : this should probably be done in Commitments.scala
// their available funds as seen by us, including all pending changes
val reduced = reduce(commitments.ourCommit.spec, commitments.ourChanges.acked, commitments.theirChanges.acked ++ commitments.theirChanges.proposed)
// the pending htlcs that they sent to us (seen as IN from our pov) have already been deduced from their balance
val available = reduced.amount_them_msat + reduced.htlcs.filter(_.direction == OUT).map(-_.amountMsat).sum
if (amount > available) {
log.error("they sent an htlc but had insufficient funds")
them ! error(Some("Insufficient funds"))
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
} else {
// TODO: nodeIds are ignored
stay using d.copy(commitments = commitments.addTheirProposal(htlc))
}
case Event(CMD_FULFILL_HTLC(id, r), d: DATA_NORMAL) =>
val (commitments1, fullfill) = Commitments.sendFulfill(d.commitments, CMD_FULFILL_HTLC(id, r))
them ! fullfill
sender ! "ok"
stay using d.copy(commitments = commitments1)
case Event(fulfill@update_fulfill_htlc(id, r), d: DATA_NORMAL) =>
@ -300,9 +320,27 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
stay using d.copy(commitments = Commitments.receiveFail(d.commitments, fail))
case Event(CMD_SIGN, d: DATA_NORMAL) =>
val (commitments1, commit) = Commitments.sendCommit(d.commitments)
them ! commit
stay using d.copy(commitments = commitments1)
if (d.commitments.theirNextCommitInfo.isLeft) {
sender ! "cannot sign until next revocation hash is received"
stay
} /*else if (d.commitments.ourChanges.proposed.isEmpty) {
//TODO : check this
sender ! "cannot sign when there are no changes"
stay
}*/ else {
Try(Commitments.sendCommit(d.commitments)) match {
case Success((commitments1, commit)) =>
them ! commit
sender ! "ok"
stay using d.copy(commitments = commitments1)
case Failure(cause) =>
log.error(cause, "")
them ! error(Some("Bad signature"))
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
}
}
case Event(msg@update_commit(theirSig), d: DATA_NORMAL) =>
Try(Commitments.receiveCommit(d.commitments, msg)) match {
@ -310,7 +348,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
them ! revocation
stay using d.copy(commitments = commitments1)
case Failure(cause) =>
log.error(cause, "received a bad signature")
log.error(cause, "")
them ! error(Some("Bad signature"))
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
@ -320,8 +358,16 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
case Event(msg@update_revocation(revocationPreimage, nextRevocationHash), d: DATA_NORMAL) =>
// we received a revocation because we sent a signature
// => all our changes have been acked
// TODO: check preimage
stay using d.copy(commitments = Commitments.receiveRevocation(d.commitments, msg))
Try(Commitments.receiveRevocation(d.commitments, msg)) match {
case Success(commitments1) =>
stay using d.copy(commitments = commitments1)
case Failure(cause) =>
log.error(cause, "")
them ! error(Some("Bad revocation"))
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
}
case Event(theirClearing@close_clearing(theirScriptPubKey), d@DATA_NORMAL(commitments, _, ourClearingOpt)) =>
val ourClearing: close_clearing = ourClearingOpt.getOrElse {
@ -411,7 +457,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
stay using d.copy(commitments = commitments1)
}
case Failure(cause) =>
log.error(cause, "received a bad signature")
log.error(cause, "")
them ! error(Some("Bad signature"))
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
@ -479,7 +525,9 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
stay()
case Event(close_signature(theirCloseFee, theirSig), d: DATA_CLOSING) =>
throw new RuntimeException(s"unexpected closing fee: $theirCloseFee ours is ${d.ourSignature.map(_.closeFee)}")
throw new RuntimeException(s"unexpected closing fee: $theirCloseFee ours is ${
d.ourSignature.map(_.closeFee)
}")
case Event(BITCOIN_CLOSE_DONE, _) => goto(CLOSED)
@ -528,6 +576,25 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
}
/**
* This helper function runs the state's default event handlers, and react to exceptions by unilaterally closing the channel
*/
def handleExceptions(s: StateFunction): StateFunction = {
case event =>
try {
s(event)
} catch {
case t: Throwable => event.stateData match {
case d: HasCommitments =>
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
case _ =>
goto(CLOSED)
}
}
}
}

View file

@ -82,14 +82,14 @@ sealed trait Command
/**
*
*
* @param amount
* @param amountMsat
* @param rHash
* @param expiry
* @param nodeIds
* @param originChannelId
* @param id should only be provided in tests otherwise it will be assigned automatically
*/
final case class CMD_ADD_HTLC(amount: Int, rHash: sha256_hash, expiry: locktime, nodeIds: Seq[String] = Seq.empty[String], originChannelId: Option[BinaryData] = None, id: Option[Long] = None) extends Command
final case class CMD_ADD_HTLC(amountMsat: Int, rHash: sha256_hash, expiry: locktime, nodeIds: Seq[String] = Seq.empty[String], originChannelId: Option[BinaryData] = None, id: Option[Long] = None) extends Command
final case class CMD_FULFILL_HTLC(id: Long, r: sha256_hash) extends Command
final case class CMD_FAIL_HTLC(id: Long, reason: String) extends Command
case object CMD_SIGN extends Command
@ -143,23 +143,27 @@ case class TheirCommit(index: Long, spec: CommitmentSpec, theirRevocationHash: s
final case class ClosingData(ourScriptPubKey: BinaryData, theirScriptPubKey: Option[BinaryData])
trait HasCommitments {
def commitments: Commitments
}
final case class DATA_OPEN_WAIT_FOR_OPEN (ourParams: OurChannelParams) extends Data
final case class DATA_OPEN_WITH_ANCHOR_WAIT_FOR_ANCHOR(ourParams: OurChannelParams, theirParams: TheirChannelParams, theirRevocationHash: BinaryData, theirNextRevocationHash: sha256_hash) extends Data
final case class DATA_OPEN_WAIT_FOR_ANCHOR (ourParams: OurChannelParams, theirParams: TheirChannelParams, theirRevocationHash: sha256_hash, theirNextRevocationHash: sha256_hash) extends Data
final case class DATA_OPEN_WAIT_FOR_COMMIT_SIG (ourParams: OurChannelParams, theirParams: TheirChannelParams, anchorTx: Transaction, anchorOutputIndex: Int, initialCommitment: TheirCommit, theirNextRevocationHash: sha256_hash) extends Data
final case class DATA_OPEN_WAITING (commitments: Commitments, deferred: Option[open_complete]) extends Data
final case class DATA_OPEN_WAITING (commitments: Commitments, deferred: Option[open_complete]) extends Data with HasCommitments
final case class DATA_NORMAL (commitments: Commitments, htlcIdx: Long,
ourClearing: Option[close_clearing]) extends Data
ourClearing: Option[close_clearing]) extends Data with HasCommitments
final case class DATA_CLEARING (commitments: Commitments, htlcIdx: Long,
ourClearing: close_clearing, theirClearing: close_clearing) extends Data
ourClearing: close_clearing, theirClearing: close_clearing) extends Data with HasCommitments
final case class DATA_NEGOCIATING (commitments: Commitments, htlcIdx: Long,
ourClearing: close_clearing, theirClearing: close_clearing, ourSignature: close_signature) extends Data
ourClearing: close_clearing, theirClearing: close_clearing, ourSignature: close_signature) extends Data with HasCommitments
final case class DATA_CLOSING (commitments: Commitments,
ourSignature: Option[close_signature] = None,
mutualClosePublished: Option[Transaction] = None,
ourCommitPublished: Option[Transaction] = None,
theirCommitPublished: Option[Transaction] = None,
revokedPublished: Seq[Transaction] = Seq()) extends Data {
revokedPublished: Seq[Transaction] = Seq()) extends Data with HasCommitments {
assert(mutualClosePublished.isDefined || ourCommitPublished.isDefined || theirCommitPublished.isDefined || revokedPublished.size > 0, "there should be at least one tx published in this state")
}

View file

@ -113,6 +113,10 @@ object Commitments {
// we will reply to this sig with our old revocation hash preimage (at index) and our next revocation hash (at index + 1)
// and will increment our index
// check that there have been changes
/*if (commitments.theirChanges.proposed.isEmpty)
throw new RuntimeException("cannot sign if there are no changes")*/
// check that their signature is valid
val spec = Helpers.reduce(ourCommit.spec, ourChanges.acked, theirChanges.acked ++ theirChanges.proposed)
val ourNextRevocationHash = Helpers.revocationHash(ourParams.shaSeed, ourCommit.index + 1)
@ -121,7 +125,7 @@ object Commitments {
val signedTx = Helpers.addSigs(ourParams, theirParams, anchorOutput.amount, ourTx, ourSig, commit.sig)
Helpers.checksig(ourParams, theirParams, anchorOutput, signedTx).get
// we will send our revocation preimage+ our next revocation hash
// we will send our revocation preimage + our next revocation hash
val ourRevocationPreimage = Helpers.revocationPreimage(ourParams.shaSeed, ourCommit.index)
val ourNextRevocationHash1 = Helpers.revocationHash(ourParams.shaSeed, ourCommit.index + 2)
val revocation = update_revocation(ourRevocationPreimage, ourNextRevocationHash1)
@ -138,8 +142,9 @@ object Commitments {
import commitments._
// we receive a revocation because we just sent them a sig for their next commit tx
theirNextCommitInfo match {
case Left(theirNextCommit) if BinaryData(Crypto.sha256(revocation.revocationPreimage)) != BinaryData(theirCommit.theirRevocationHash) =>
throw new RuntimeException("invalid preimage")
case Left(theirNextCommit) =>
assert(BinaryData(Crypto.sha256(revocation.revocationPreimage)) == BinaryData(theirCommit.theirRevocationHash), "invalid preimage")
commitments.copy(
ourChanges = ourChanges.copy(signed = Nil, acked = ourChanges.acked ++ ourChanges.signed),
theirCommit = theirNextCommit,

View file

@ -1,4 +1,4 @@
package fr.acinq.eclair.channel.simulator
package fr.acinq.eclair.channel.simulator.states.a
import akka.actor.ActorSystem
import akka.testkit.{TestFSMRef, TestKit, TestProbe}
@ -6,8 +6,8 @@ import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.channel._
import lightning.{error, open_channel}
import org.junit.runner.RunWith
import org.scalatest.{BeforeAndAfterAll, fixture}
import org.scalatest.junit.JUnitRunner
import org.scalatest.{BeforeAndAfterAll, fixture}
import scala.concurrent.duration._

View file

@ -1,4 +1,4 @@
package fr.acinq.eclair.channel.simulator
package fr.acinq.eclair.channel.simulator.states.a
import akka.actor.{ActorRef, ActorSystem}
import akka.testkit.{TestActorRef, TestFSMRef, TestKit, TestProbe}

View file

@ -1,4 +1,4 @@
package fr.acinq.eclair.channel.simulator
package fr.acinq.eclair.channel.simulator.states.b
import akka.actor.ActorSystem
import akka.testkit.{TestActorRef, TestFSMRef, TestKit, TestProbe}

View file

@ -1,11 +1,11 @@
package fr.acinq.eclair.channel.simulator
package fr.acinq.eclair.channel.simulator.states.b
import akka.actor.{ActorRef, ActorSystem}
import akka.testkit.{TestActorRef, TestFSMRef, TestKit, TestProbe}
import fr.acinq.eclair.TestBitcoinClient
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel.{OPEN_WAITING_OURANCHOR, OPEN_WAIT_FOR_OPEN_WITHANCHOR, _}
import fr.acinq.eclair.channel.{OPEN_WAITING_OURANCHOR, _}
import lightning._
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner

View file

@ -1,4 +1,4 @@
package fr.acinq.eclair.channel.simulator
package fr.acinq.eclair.channel.simulator.states.c
import akka.actor.{ActorRef, ActorSystem}
import akka.testkit.{TestActorRef, TestFSMRef, TestKit, TestProbe}

View file

@ -1,4 +1,4 @@
package fr.acinq.eclair.channel.simulator
package fr.acinq.eclair.channel.simulator.states.c
import akka.actor.ActorSystem
import akka.testkit.{TestActorRef, TestFSMRef, TestKit, TestProbe}

View file

@ -1,11 +1,11 @@
package fr.acinq.eclair.channel.simulator
package fr.acinq.eclair.channel.simulator.states.d
import akka.actor.{ActorRef, ActorSystem}
import akka.testkit.{TestActorRef, TestFSMRef, TestKit, TestProbe}
import fr.acinq.eclair.TestBitcoinClient
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel.{OPEN_WAITING_OURANCHOR, OPEN_WAIT_FOR_COMPLETE_OURANCHOR, _}
import fr.acinq.eclair.channel.{OPEN_WAIT_FOR_COMPLETE_OURANCHOR, _}
import lightning._
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner

View file

@ -1,11 +1,11 @@
package fr.acinq.eclair.channel.simulator
package fr.acinq.eclair.channel.simulator.states.d
import akka.actor.ActorSystem
import akka.testkit.{TestActorRef, TestFSMRef, TestKit, TestProbe}
import fr.acinq.eclair.TestBitcoinClient
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain.{PollingWatcher, WatchConfirmed, WatchLost, WatchSpent}
import fr.acinq.eclair.channel.{BITCOIN_ANCHOR_DEPTHOK, OPEN_WAITING_THEIRANCHOR, OPEN_WAIT_FOR_COMPLETE_THEIRANCHOR, _}
import fr.acinq.eclair.channel.{BITCOIN_ANCHOR_DEPTHOK, OPEN_WAIT_FOR_COMPLETE_THEIRANCHOR, _}
import lightning._
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
@ -52,7 +52,7 @@ class OpenWaitForCompleteTheirAnchorStateSpec extends TestKit(ActorSystem("test"
test("recv open_complete") { case (bob, alice2bob, bob2alice, bob2blockchain) =>
within(30 seconds) {
val msg = alice2bob.expectMsgType[open_complete]
alice2bob.expectMsgType[open_complete]
alice2bob.forward(bob)
awaitCond(bob.stateName == NORMAL)
}

View file

@ -0,0 +1,345 @@
package fr.acinq.eclair.channel.simulator.states.e
import akka.actor.ActorSystem
import akka.testkit.{TestActorRef, TestFSMRef, TestKit, TestProbe}
import com.google.protobuf.ByteString
import fr.acinq.bitcoin.Crypto
import fr.acinq.eclair._
import fr.acinq.eclair.TestBitcoinClient
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel.{BITCOIN_ANCHOR_DEPTHOK, _}
import lightning._
import lightning.locktime.Locktime.Blocks
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import org.scalatest.{BeforeAndAfterAll, fixture}
import scala.concurrent.duration._
/**
* Created by PM on 05/07/2016.
*/
@RunWith(classOf[JUnitRunner])
class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteLike with BeforeAndAfterAll {
type FixtureParam = Tuple6[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, TestProbe]
override def withFixture(test: OneArgTest) = {
val alice2bob = TestProbe()
val bob2alice = TestProbe()
val alice2blockchain = TestProbe()
val blockchainA = TestActorRef(new PollingWatcher(new TestBitcoinClient()))
val bob2blockchain = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, Alice.channelParams, "B"))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, Bob.channelParams, "A"))
alice2bob.expectMsgType[open_channel]
alice2bob.forward(bob)
bob2alice.expectMsgType[open_channel]
bob2alice.forward(alice)
alice2blockchain.expectMsgType[MakeAnchor]
alice2blockchain.forward(blockchainA)
alice2bob.expectMsgType[open_anchor]
alice2bob.forward(bob)
bob2alice.expectMsgType[open_commit_sig]
bob2alice.forward(alice)
alice2blockchain.expectMsgType[WatchConfirmed]
alice2blockchain.forward(blockchainA)
alice2blockchain.expectMsgType[WatchSpent]
alice2blockchain.forward(blockchainA)
alice2blockchain.expectMsgType[Publish]
alice2blockchain.forward(blockchainA)
bob2blockchain.expectMsgType[WatchConfirmed]
bob2blockchain.expectMsgType[WatchSpent]
bob ! BITCOIN_ANCHOR_DEPTHOK
bob2blockchain.expectMsgType[WatchLost]
bob2alice.expectMsgType[open_complete]
bob2alice.forward(alice)
alice2blockchain.expectMsgType[WatchLost]
alice2blockchain.forward(blockchainA)
alice2bob.expectMsgType[open_complete]
alice2bob.forward(bob)
awaitCond(alice.stateName == NORMAL)
awaitCond(bob.stateName == NORMAL)
// note : alice is funder and bob is fundee, so alice has all the money
test((alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain))
}
override def afterAll {
TestKit.shutdownActorSystem(system)
}
test("recv CMD_ADD_HTLC") { case (alice, _, alice2bob, _, _, _) =>
within(30 seconds) {
val h = sha256_hash(1, 2, 3, 4)
alice ! CMD_ADD_HTLC(500000, h, locktime(Blocks(3)))
val htlc = alice2bob.expectMsgType[update_add_htlc]
assert(htlc.id == 1 && htlc.rHash == h)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].htlcIdx == 1 && alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.proposed == htlc :: Nil)
}
}
test("recv CMD_ADD_HTLC (insufficient funds)") { case (alice, _, alice2bob, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
sender.send(alice, CMD_ADD_HTLC(Int.MaxValue, sha256_hash(1, 1, 1, 1), locktime(Blocks(3))))
sender.expectMsg("insufficient funds (available=1000000000 msat)")
}
}
test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs 1/2)") { case (alice, _, alice2bob, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
sender.send(alice, CMD_ADD_HTLC(500000000, sha256_hash(1, 1, 1, 1), locktime(Blocks(3))))
sender.expectMsg("ok")
sender.send(alice, CMD_ADD_HTLC(500000000, sha256_hash(2, 2, 2, 2), locktime(Blocks(3))))
sender.expectMsg("ok")
sender.send(alice, CMD_ADD_HTLC(500000000, sha256_hash(3, 3, 3, 3), locktime(Blocks(3))))
sender.expectMsg("insufficient funds (available=0 msat)")
}
}
test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs 2/2)") { case (alice, _, alice2bob, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
sender.send(alice, CMD_ADD_HTLC(300000000, sha256_hash(1, 1, 1, 1), locktime(Blocks(3))))
sender.expectMsg("ok")
sender.send(alice, CMD_ADD_HTLC(300000000, sha256_hash(2, 2, 2, 2), locktime(Blocks(3))))
sender.expectMsg("ok")
sender.send(alice, CMD_ADD_HTLC(500000000, sha256_hash(3, 3, 3, 3), locktime(Blocks(3))))
sender.expectMsg("insufficient funds (available=400000000 msat)")
}
}
test("recv update_add_htlc") { case (_, bob, alice2bob, _, _,_) =>
within(30 seconds) {
val initialData = bob.stateData.asInstanceOf[DATA_NORMAL]
val htlc = update_add_htlc(42, 150, sha256_hash(1, 2, 3, 4), locktime(Blocks(3)), routing(ByteString.EMPTY))
bob ! htlc
awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(theirChanges = initialData.commitments.theirChanges.copy(proposed = initialData.commitments.theirChanges.proposed :+ htlc))))
}
}
test("recv update_add_htlc (insufficient funds)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain) =>
within(30 seconds) {
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
val htlc = update_add_htlc(42, Int.MaxValue, sha256_hash(1, 2, 3, 4), locktime(Blocks(3)), routing(ByteString.EMPTY))
alice2bob.forward(bob, htlc)
bob2alice.expectMsgType[error]
awaitCond(bob.stateName == CLOSING)
bob2blockchain.expectMsg(Publish(tx))
bob2blockchain.expectMsgType[WatchConfirmed]
}
}
test("recv update_add_htlc (insufficient funds w/ pending htlcs 1/2)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain) =>
within(30 seconds) {
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
alice2bob.forward(bob, update_add_htlc(42, 500000000, sha256_hash(1, 1, 1, 1), locktime(Blocks(3)), routing(ByteString.EMPTY)))
alice2bob.forward(bob, update_add_htlc(43, 500000000, sha256_hash(2, 2, 2, 2), locktime(Blocks(3)), routing(ByteString.EMPTY)))
alice2bob.forward(bob, update_add_htlc(44, 500000000, sha256_hash(3, 3, 3, 3), locktime(Blocks(3)), routing(ByteString.EMPTY)))
bob2alice.expectMsgType[error]
awaitCond(bob.stateName == CLOSING)
bob2blockchain.expectMsg(Publish(tx))
bob2blockchain.expectMsgType[WatchConfirmed]
}
}
test("recv update_add_htlc (insufficient funds w/ pending htlcs 2/2)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain) =>
within(30 seconds) {
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
alice2bob.forward(bob, update_add_htlc(42, 300000000, sha256_hash(1, 1, 1, 1), locktime(Blocks(3)), routing(ByteString.EMPTY)))
alice2bob.forward(bob, update_add_htlc(43, 300000000, sha256_hash(2, 2, 2, 2), locktime(Blocks(3)), routing(ByteString.EMPTY)))
alice2bob.forward(bob, update_add_htlc(44, 500000000, sha256_hash(3, 3, 3, 3), locktime(Blocks(3)), routing(ByteString.EMPTY)))
bob2alice.expectMsgType[error]
awaitCond(bob.stateName == CLOSING)
bob2blockchain.expectMsg(Publish(tx))
bob2blockchain.expectMsgType[WatchConfirmed]
}
}
test("recv CMD_SIGN") { case (alice, bob, alice2bob, bob2alice, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val r = sha256_hash(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
alice2bob.expectMsgType[update_add_htlc]
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
alice2bob.expectMsgType[update_commit]
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.theirNextCommitInfo.isLeft)
}
}
/*test("recv CMD_SIGN (no changes)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
val sender = TestProbe()
sender.send(alice, CMD_SIGN)
sender.expectMsg("cannot sign when there are no changes")
}
}*/
test("recv CMD_SIGN (while waiting for update_revocation)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
val sender = TestProbe()
val r = sha256_hash(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
alice2bob.expectMsgType[update_add_htlc]
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.theirNextCommitInfo.isRight)
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
alice2bob.expectMsgType[update_commit]
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.theirNextCommitInfo.isLeft)
sender.send(alice, CMD_SIGN)
sender.expectMsg("cannot sign until next revocation hash is received")
}
}
test("recv update_commit") { case (alice, bob, alice2bob, bob2alice, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val r = sha256_hash(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[update_add_htlc]
alice2bob.forward(bob)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == htlc :: Nil)
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
alice2bob.expectMsgType[update_commit]
alice2bob.forward(bob)
bob2alice.expectMsgType[update_revocation]
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.spec.htlcs.exists(h => h.id == htlc.id && h.direction == IN))
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.spec.amount_us_msat == initialState.commitments.ourCommit.spec.amount_us_msat)
}
}
/*test("recv update_commit (no changes)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain) =>
within(30 seconds) {
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
val sender = TestProbe()
// signature is invalid but it doesn't matter
sender.send(bob, update_commit(signature(0, 0, 0, 0, 0, 0, 0, 0)))
bob2alice.expectMsgType[error]
awaitCond(bob.stateName == CLOSING)
bob2blockchain.expectMsg(Publish(tx))
bob2blockchain.expectMsgType[WatchConfirmed]
}
}*/
test("recv update_commit (invalid signature)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain) =>
within(30 seconds) {
val sender = TestProbe()
val r = sha256_hash(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[update_add_htlc]
alice2bob.forward(bob)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == htlc :: Nil)
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
sender.send(bob, update_commit(signature(0, 0, 0, 0, 0, 0, 0, 0)))
bob2alice.expectMsgType[error]
awaitCond(bob.stateName == CLOSING)
bob2blockchain.expectMsg(Publish(tx))
bob2blockchain.expectMsgType[WatchConfirmed]
}
}
test("recv update_revocation") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain) =>
within(30 seconds) {
val sender = TestProbe()
val r = sha256_hash(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[update_add_htlc]
alice2bob.forward(bob)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == htlc :: Nil)
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
alice2bob.expectMsgType[update_commit]
alice2bob.forward(bob)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.theirNextCommitInfo.isLeft)
bob2alice.expectMsgType[update_revocation]
bob2alice.forward(alice)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.theirNextCommitInfo.isRight)
}
}
test("recv update_revocation (invalid preimage") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
val sender = TestProbe()
val r = sha256_hash(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[update_add_htlc]
alice2bob.forward(bob)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == htlc :: Nil)
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
alice2bob.expectMsgType[update_commit]
alice2bob.forward(bob)
bob2alice.expectMsgType[update_revocation]
sender.send(alice, update_revocation(sha256_hash(0, 0, 0, 0), sha256_hash(1, 1, 1, 1)))
alice2bob.expectMsgType[error]
awaitCond(alice.stateName == CLOSING)
alice2blockchain.expectMsg(Publish(tx))
alice2blockchain.expectMsgType[WatchConfirmed]
}
}
test("recv update_revocation (unexpectedly") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
val sender = TestProbe()
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.theirNextCommitInfo.isRight)
sender.send(alice, update_revocation(sha256_hash(0, 0, 0, 0), sha256_hash(1, 1, 1, 1)))
alice2bob.expectMsgType[error]
awaitCond(alice.stateName == CLOSING)
alice2blockchain.expectMsg(Publish(tx))
alice2blockchain.expectMsgType[WatchConfirmed]
}
}
/*test("recv CMD_FULFILL_HTLC") { case (alice, bob, alice2bob, bob2alice, _) =>
within(30 seconds) {
val sender = TestProbe()
val r = sha256_hash(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[update_add_htlc]
alice2bob.forward(bob)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == htlc :: Nil)
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
sender.send(bob, CMD_FULFILL_HTLC(htlc.id, r))
sender.expectMsg("ok")
val fulfill = bob2alice.expectMsgType[update_fulfill_htlc]
awaitCond(bob.stateData == initialState.copy(commitments = initialState.commitments.copy(ourChanges = initialState.commitments.ourChanges.copy(initialState.commitments.ourChanges.proposed :+ fulfill))))
}
}*/
}

View file

@ -110,6 +110,7 @@ class SynchronizationPipe(latch: CountDownLatch) extends Actor with ActorLogging
import scala.io.Source
val script = Source.fromFile(file).getLines().filterNot(_.startsWith("#")).toList
exec(script, a, b)
case "ok" => {}
case msg if sender() == a =>
log.info(s"a -> b $msg")
b forward msg
@ -120,11 +121,14 @@ class SynchronizationPipe(latch: CountDownLatch) extends Actor with ActorLogging
}
def wait(a: ActorRef, b: ActorRef, script: List[String]): Receive = {
case "ok" => {}
case msg if sender() == a && script.head.startsWith("B:recv") =>
log.info(s"a -> b $msg")
b forward msg
unstashAll()
exec(script.drop(1), a, b)
case msg if sender() == b && script.head.startsWith("A:recv") =>
log.info(s"b -> a $msg")
a forward msg
unstashAll()
exec(script.drop(1), a, b)