mirror of
https://github.com/ACINQ/eclair.git
synced 2024-11-19 09:54:02 +01:00
Wip uniclose txdb (#15)
* implement a basic tx db that stores how to spend revoked transactions it's a simple db that stores spending tx indexed by their parent id * bumped akka version to 2.4.8 * fixed wrong data type in CLEARING and NEGOTIATING handlers * added a few common handlers * moved sendAdd/receiveAdd logic to Commitments.scala * moved htlc index to Commitments class * cleaned up sig handlers * removed unused variable * storing their commit's txid so that we can identify spending tx * added a log to unimplemented function 'handleTheirSpentCurrent' * changed ascii art helpers->handlers * fix misleading comments and code in Channel * removed unused imports * added test showing bug when two htlcs share the same r * added a failing test showing that punish tx output isn't p2wpkh * fix failing test * use P2WPKH instead of P2PKH in our "punishment" tx * fix failing test * added helper methods in test * fixed test
This commit is contained in:
parent
90811d1d5a
commit
9d419b9d8a
@ -1,11 +1,11 @@
|
||||
package fr.acinq.eclair.channel
|
||||
|
||||
import akka.actor.{ActorRef, FSM, LoggingFSM, Props}
|
||||
import com.google.protobuf.ByteString
|
||||
import fr.acinq.bitcoin.{OutPoint, _}
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.blockchain._
|
||||
import fr.acinq.eclair.channel.Helpers._
|
||||
import fr.acinq.eclair.channel.TypeDefs.Change
|
||||
import fr.acinq.eclair.crypto.ShaChain
|
||||
import lightning._
|
||||
import lightning.open_channel.anchor_offer.{WILL_CREATE_ANCHOR, WONT_CREATE_ANCHOR}
|
||||
@ -45,6 +45,32 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
|
||||
888 888 Y8888 888 888
|
||||
8888888 888 Y888 8888888 888
|
||||
*/
|
||||
|
||||
/*
|
||||
FUNDER FUNDEE
|
||||
| open_channel open_channel |
|
||||
|--------------- ---------------|
|
||||
OPEN_WAIT_FOR_OPEN_WITHANCHOR| \/ |OPEN_WAIT_FOR_OPEN_NOANCHOR
|
||||
| /\ |
|
||||
|<-------------- -------------->|
|
||||
| |OPEN_WAIT_FOR_ANCHOR
|
||||
| open_anchor |
|
||||
|--------------- |
|
||||
OPEN_WAIT_FOR_COMMIT_SIG| \ |
|
||||
| --------------->|
|
||||
| open_commit_sig |
|
||||
| ----------------|
|
||||
| / |OPEN_WAITING_THEIRANCHOR
|
||||
|<-------------- |
|
||||
OPEN_WAITING_OURANCHOR| |
|
||||
| |
|
||||
| open_complete openu_complete |
|
||||
|--------------- ---------------|
|
||||
OPEN_WAIT_FOR_COMPLETE_OURANCHOR| \/ |OPEN_WAIT_FOR_COMPLETE_THEIRANCHOR
|
||||
| /\ |
|
||||
|<-------------- -------------->|
|
||||
NORMAL| |NORMAL
|
||||
*/
|
||||
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)
|
||||
@ -69,7 +95,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
|
||||
val amount = anchorTx.txOut(anchorOutputIndex).amount.toLong
|
||||
val theirSpec = CommitmentSpec(Set.empty[Htlc], feeRate = theirParams.initialFeeRate, initial_amount_us_msat = 0, initial_amount_them_msat = amount * 1000, amount_us_msat = 0, amount_them_msat = amount * 1000)
|
||||
them ! open_anchor(anchorTx.hash, anchorOutputIndex, amount)
|
||||
goto(OPEN_WAIT_FOR_COMMIT_SIG) using DATA_OPEN_WAIT_FOR_COMMIT_SIG(ourParams, theirParams, anchorTx, anchorOutputIndex, TheirCommit(0, theirSpec, theirRevocationHash), theirNextRevocationHash)
|
||||
goto(OPEN_WAIT_FOR_COMMIT_SIG) using DATA_OPEN_WAIT_FOR_COMMIT_SIG(ourParams, theirParams, anchorTx, anchorOutputIndex, TheirCommit(0, theirSpec, BinaryData(""), theirRevocationHash), theirNextRevocationHash)
|
||||
|
||||
case Event(CMD_CLOSE(_), _) => goto(CLOSED)
|
||||
|
||||
@ -88,20 +114,24 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
|
||||
val ourSpec = CommitmentSpec(Set.empty[Htlc], feeRate = ourParams.initialFeeRate, initial_amount_them_msat = anchorAmount * 1000, initial_amount_us_msat = 0, amount_them_msat = anchorAmount * 1000, amount_us_msat = 0)
|
||||
val theirSpec = CommitmentSpec(Set.empty[Htlc], feeRate = theirParams.initialFeeRate, initial_amount_them_msat = 0, initial_amount_us_msat = anchorAmount * 1000, amount_them_msat = 0, amount_us_msat = anchorAmount * 1000)
|
||||
|
||||
// we build our commitment tx, sign it and check that it is spendable using the counterparty's sig
|
||||
val ourRevocationHash = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, 0))
|
||||
val ourTx = makeOurTx(ourParams, theirParams, TxIn(OutPoint(anchorTxHash, anchorOutputIndex), Array.emptyByteArray, 0xffffffffL) :: Nil, ourRevocationHash, ourSpec)
|
||||
// build and sign their commit tx
|
||||
val theirTx = makeTheirTx(ourParams, theirParams, TxIn(OutPoint(anchorTxHash, anchorOutputIndex), Array.emptyByteArray, 0xffffffffL) :: Nil, theirRevocationHash, theirSpec)
|
||||
log.info(s"signing their tx: $theirTx")
|
||||
val ourSigForThem = sign(ourParams, theirParams, Satoshi(anchorAmount), theirTx)
|
||||
them ! open_commit_sig(ourSigForThem)
|
||||
|
||||
// watch the anchor
|
||||
blockchain ! WatchConfirmed(self, anchorTxid, ourParams.minDepth, BITCOIN_ANCHOR_DEPTHOK)
|
||||
blockchain ! WatchSpent(self, anchorTxid, anchorOutputIndex, 0, BITCOIN_ANCHOR_SPENT)
|
||||
|
||||
// FIXME: ourTx is not signed by them and cannot be published. We won't lose money since they are funding the chanel
|
||||
val ourRevocationHash = Helpers.revocationHash(ourParams.shaSeed, 0)
|
||||
val ourTx = makeOurTx(ourParams, theirParams, TxIn(OutPoint(anchorTxHash, anchorOutputIndex), Array.emptyByteArray, 0xffffffffL) :: Nil, ourRevocationHash, ourSpec)
|
||||
|
||||
val commitments = Commitments(ourParams, theirParams,
|
||||
OurCommit(0, ourSpec, ourTx), TheirCommit(0, theirSpec, theirRevocationHash),
|
||||
OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil),
|
||||
Right(theirNextRevocationHash), anchorOutput, ShaChain.init)
|
||||
OurCommit(0, ourSpec, ourTx), TheirCommit(0, theirSpec, theirTx.txid, theirRevocationHash),
|
||||
OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil), 0L,
|
||||
Right(theirNextRevocationHash), anchorOutput, ShaChain.init, new BasicTxDb)
|
||||
goto(OPEN_WAITING_THEIRANCHOR) using DATA_OPEN_WAITING(commitments, None)
|
||||
|
||||
case Event(CMD_CLOSE(_), _) => goto(CLOSED)
|
||||
@ -135,8 +165,8 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
|
||||
blockchain ! Publish(anchorTx)
|
||||
val commitments = Commitments(ourParams, theirParams,
|
||||
OurCommit(0, ourSpec, signedTx), theirCommitment,
|
||||
OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil),
|
||||
Right(theirNextRevocationHash), anchorOutput, ShaChain.init)
|
||||
OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil), 0L,
|
||||
Right(theirNextRevocationHash), anchorOutput, ShaChain.init, new BasicTxDb)
|
||||
goto(OPEN_WAITING_OURANCHOR) using DATA_OPEN_WAITING(commitments, None)
|
||||
}
|
||||
|
||||
@ -157,18 +187,20 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
|
||||
them ! open_complete(None)
|
||||
deferred.map(self ! _)
|
||||
//TODO htlcIdx should not be 0 when resuming connection
|
||||
goto(OPEN_WAIT_FOR_COMPLETE_THEIRANCHOR) using DATA_NORMAL(commitments, 0, None)
|
||||
goto(OPEN_WAIT_FOR_COMPLETE_THEIRANCHOR) using DATA_NORMAL(commitments, None)
|
||||
|
||||
case Event(BITCOIN_ANCHOR_TIMEOUT, _) =>
|
||||
them ! error(Some("Anchor timed out"))
|
||||
goto(CLOSED)
|
||||
|
||||
case Event((BITCOIN_ANCHOR_SPENT, _), d: DATA_OPEN_WAITING) =>
|
||||
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_OPEN_WAITING) if tx.txid == d.commitments.theirCommit.txid =>
|
||||
// they are funding the anchor, we have nothing at stake
|
||||
log.warning(s"their anchor ${d.commitments.anchorId} was spent, sending error and closing")
|
||||
them ! error(Some(s"your anchor ${d.commitments.anchorId} was spent"))
|
||||
goto(CLOSED)
|
||||
|
||||
case Event((BITCOIN_ANCHOR_SPENT, _), d: HasCommitments) => handleInformationLeak(d)
|
||||
|
||||
case Event(CMD_CLOSE(_), _) => goto(CLOSED)
|
||||
|
||||
case Event(e@error(problem), _) =>
|
||||
@ -186,18 +218,13 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
|
||||
them ! open_complete(None)
|
||||
deferred.map(self ! _)
|
||||
//TODO htlcIdx should not be 0 when resuming connection
|
||||
goto(OPEN_WAIT_FOR_COMPLETE_OURANCHOR) using DATA_NORMAL(commitments, 0, None)
|
||||
goto(OPEN_WAIT_FOR_COMPLETE_OURANCHOR) using DATA_NORMAL(commitments, None)
|
||||
|
||||
case Event(BITCOIN_ANCHOR_TIMEOUT, _) =>
|
||||
them ! error(Some("Anchor timed out"))
|
||||
goto(CLOSED)
|
||||
|
||||
case Event((BITCOIN_ANCHOR_SPENT, _), d: DATA_OPEN_WAITING) =>
|
||||
// this is never supposed to happen !!
|
||||
log.error(s"our anchor ${d.commitments.anchorId} was spent !!")
|
||||
them ! error(Some("Anchor has been spent"))
|
||||
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
|
||||
goto(ERR_INFORMATION_LEAK)
|
||||
case Event((BITCOIN_ANCHOR_SPENT, _), d: DATA_OPEN_WAITING) => handleInformationLeak(d)
|
||||
|
||||
case Event(cmd: CMD_CLOSE, d: DATA_OPEN_WAITING) =>
|
||||
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
|
||||
@ -216,7 +243,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
|
||||
Register.create_alias(theirNodeId, d.commitments.anchorId)
|
||||
goto(NORMAL)
|
||||
|
||||
case Event((BITCOIN_ANCHOR_SPENT, _), d: DATA_NORMAL) =>
|
||||
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_NORMAL) if tx.txid == d.commitments.theirCommit.txid =>
|
||||
// they are funding the anchor, we have nothing at stake
|
||||
log.warning(s"their anchor ${d.commitments.anchorId} was spent, sending error and closing")
|
||||
them ! error(Some(s"your anchor ${d.commitments.anchorId} was spent"))
|
||||
@ -234,23 +261,14 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
|
||||
Register.create_alias(theirNodeId, d.commitments.anchorId)
|
||||
goto(NORMAL)
|
||||
|
||||
case Event((BITCOIN_ANCHOR_SPENT, _), d: DATA_NORMAL) =>
|
||||
// this is never supposed to happen !!
|
||||
log.error(s"our anchor ${d.commitments.anchorId} was spent while we were waiting for their open_complete message !!")
|
||||
them ! error(Some("Anchor has been spent"))
|
||||
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
|
||||
goto(ERR_INFORMATION_LEAK)
|
||||
case Event((BITCOIN_ANCHOR_SPENT, _), d: DATA_NORMAL) => handleInformationLeak(d)
|
||||
|
||||
case Event(cmd: CMD_CLOSE, d: DATA_NORMAL) =>
|
||||
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(e@error(problem), d: DATA_NORMAL) =>
|
||||
log.error(s"peer sent $e, closing connection") // see bolt #2: A node MUST fail the connection if it receives an err message
|
||||
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(e@error(problem), d: DATA_NORMAL) => handleError(e, d)
|
||||
})
|
||||
|
||||
|
||||
@ -266,112 +284,46 @@ 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 : 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 steps = route(route_step(0, next = route_step.Next.End(true)) :: Nil)
|
||||
val htlc = update_add_htlc(id, amount, rHash, expiry, routing(ByteString.copyFrom(steps.toByteArray)))
|
||||
them ! htlc
|
||||
sender ! "ok"
|
||||
stay using d.copy(htlcIdx = htlc.id, commitments = commitments.addOurProposal(htlc))
|
||||
case Event(c@CMD_ADD_HTLC(amount, rHash, expiry, nodeIds, origin, id_opt), d@DATA_NORMAL(commitments, _)) =>
|
||||
Try(Commitments.sendAdd(commitments, c)) match {
|
||||
case Success((commitments1, add)) => handleCommandSuccess(sender, add, d.copy(commitments = commitments1))
|
||||
case Failure(cause) => handleCommandError(sender, cause)
|
||||
}
|
||||
|
||||
case Event(htlc@update_add_htlc(htlcId, amount, rHash, expiry, nodeIds), d@DATA_NORMAL(commitments, _, _)) =>
|
||||
// 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(add@update_add_htlc(htlcId, amount, rHash, expiry, nodeIds), d@DATA_NORMAL(commitments, _)) =>
|
||||
Try(Commitments.receiveAdd(commitments, add)) match {
|
||||
case Success(commitments1) => stay using d.copy(commitments = commitments1)
|
||||
case Failure(cause) => handleUnicloseError(cause, d)
|
||||
}
|
||||
|
||||
case Event(CMD_FULFILL_HTLC(id, r), d: DATA_NORMAL) =>
|
||||
Try(Commitments.sendFulfill(d.commitments, CMD_FULFILL_HTLC(id, r))) match {
|
||||
case Success((commitments1, fullfill)) =>
|
||||
them ! fullfill
|
||||
sender ! "ok"
|
||||
stay using d.copy(commitments = commitments1)
|
||||
case Failure(cause) =>
|
||||
log.error(cause, "")
|
||||
sender ! cause.getMessage
|
||||
stay
|
||||
case Event(c@CMD_FULFILL_HTLC(id, r), d: DATA_NORMAL) =>
|
||||
Try(Commitments.sendFulfill(d.commitments, c)) match {
|
||||
case Success((commitments1, fulfill)) => handleCommandSuccess(sender, fulfill, d.copy(commitments = commitments1))
|
||||
case Failure(cause) => handleCommandError(sender, cause)
|
||||
}
|
||||
|
||||
case Event(fulfill@update_fulfill_htlc(id, r), d: DATA_NORMAL) =>
|
||||
Try(Commitments.receiveFulfill(d.commitments, fulfill)) match {
|
||||
case Success(commitments1) =>
|
||||
stay using d.copy(commitments = commitments1)
|
||||
case Failure(cause) =>
|
||||
log.error(cause, "")
|
||||
them ! error(Some(cause.getMessage))
|
||||
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 Success(commitments1) => stay using d.copy(commitments = commitments1)
|
||||
case Failure(cause) => handleUnicloseError(cause, d)
|
||||
}
|
||||
|
||||
case Event(CMD_FAIL_HTLC(id, reason), d: DATA_NORMAL) =>
|
||||
Try(Commitments.sendFail(d.commitments, CMD_FAIL_HTLC(id, reason))) match {
|
||||
case Success((commitments1, fail)) =>
|
||||
them ! fail
|
||||
sender ! "ok"
|
||||
stay using d.copy(commitments = commitments1)
|
||||
case Failure(cause) =>
|
||||
log.error(cause, "")
|
||||
sender ! cause.getMessage
|
||||
stay
|
||||
case Event(c@CMD_FAIL_HTLC(id, reason), d: DATA_NORMAL) =>
|
||||
Try(Commitments.sendFail(d.commitments, c)) match {
|
||||
case Success((commitments1, fail)) => handleCommandSuccess(sender, fail, d.copy(commitments = commitments1))
|
||||
case Failure(cause) => handleCommandError(sender, cause)
|
||||
}
|
||||
|
||||
case Event(fail@update_fail_htlc(id, reason), d: DATA_NORMAL) =>
|
||||
Try(Commitments.receiveFail(d.commitments, fail)) match {
|
||||
case Success(commitments1) =>
|
||||
stay using d.copy(commitments = commitments1)
|
||||
case Failure(cause) =>
|
||||
log.error(cause, "")
|
||||
them ! error(Some(cause.getMessage))
|
||||
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 Success(commitments1) => stay using d.copy(commitments = commitments1)
|
||||
case Failure(cause) => handleUnicloseError(cause, d)
|
||||
}
|
||||
|
||||
case Event(CMD_SIGN, d: DATA_NORMAL) =>
|
||||
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(cause.getMessage))
|
||||
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))
|
||||
}
|
||||
Try(Commitments.sendCommit(d.commitments)) match {
|
||||
case Success((commitments1, commit)) => handleCommandSuccess(sender, commit, d.copy(commitments = commitments1))
|
||||
case Failure(cause) => handleCommandError(sender, cause)
|
||||
}
|
||||
|
||||
case Event(msg@update_commit(theirSig), d: DATA_NORMAL) =>
|
||||
@ -379,12 +331,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
|
||||
case Success((commitments1, revocation)) =>
|
||||
them ! revocation
|
||||
stay using d.copy(commitments = commitments1)
|
||||
case Failure(cause) =>
|
||||
log.error(cause, "")
|
||||
them ! error(Some(cause.getMessage))
|
||||
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 Failure(cause) => handleUnicloseError(cause, d)
|
||||
}
|
||||
|
||||
case Event(msg@update_revocation(revocationPreimage, nextRevocationHash), d: DATA_NORMAL) =>
|
||||
@ -393,12 +340,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
|
||||
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(cause.getMessage))
|
||||
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 Failure(cause) => handleUnicloseError(cause, d)
|
||||
}
|
||||
|
||||
case Event(CMD_CLOSE(scriptPubKeyOpt), d: DATA_NORMAL) =>
|
||||
@ -415,7 +357,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
|
||||
stay using d.copy(ourClearing = Some(ourCloseClearing))
|
||||
}
|
||||
|
||||
case Event(theirClearing@close_clearing(theirScriptPubKey), d@DATA_NORMAL(commitments, _, ourClearingOpt)) =>
|
||||
case Event(theirClearing@close_clearing(theirScriptPubKey), d@DATA_NORMAL(commitments, ourClearingOpt)) =>
|
||||
val ourClearing: close_clearing = ourClearingOpt.getOrElse {
|
||||
val ourScriptPubKey: BinaryData = Script.write(Scripts.pay2pkh(commitments.ourParams.finalPubKey))
|
||||
log.info(s"our final tx can be redeemed with ${Base58Check.encode(Base58.Prefix.SecretKeyTestnet, d.commitments.ourParams.finalPrivKey)}")
|
||||
@ -424,31 +366,18 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
|
||||
c
|
||||
}
|
||||
if (commitments.hasNoPendingHtlcs) {
|
||||
val (finalTx, ourCloseSig) = makeFinalTx(commitments, ourClearing.scriptPubkey, theirScriptPubKey)
|
||||
val (_, ourCloseSig) = makeFinalTx(commitments, ourClearing.scriptPubkey, theirScriptPubKey)
|
||||
them ! ourCloseSig
|
||||
goto(NEGOTIATING) using DATA_NEGOTIATING(commitments, d.htlcIdx, ourClearing, theirClearing, ourCloseSig)
|
||||
goto(NEGOTIATING) using DATA_NEGOTIATING(commitments, ourClearing, theirClearing, ourCloseSig)
|
||||
} else {
|
||||
goto(CLEARING) using DATA_CLEARING(commitments, d.htlcIdx, ourClearing, theirClearing)
|
||||
goto(CLEARING) using DATA_CLEARING(commitments, ourClearing, theirClearing)
|
||||
}
|
||||
|
||||
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_NORMAL) =>
|
||||
log.warning(s"anchor spent in ${tx.txid}")
|
||||
them ! error(Some("Anchor has been spent"))
|
||||
Helpers.claimTheirRevokedCommit(tx, d.commitments) match {
|
||||
case Some(spendingTx) =>
|
||||
blockchain ! Publish(spendingTx)
|
||||
blockchain ! WatchConfirmed(self, spendingTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
|
||||
goto(CLOSING) using DATA_CLOSING(d.commitments, revokedPublished = Seq(tx))
|
||||
case None =>
|
||||
// TODO: this is definitely not right!
|
||||
stay()
|
||||
}
|
||||
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_NORMAL) if tx.txid == d.commitments.theirCommit.txid => handleTheirSpentCurrent(tx, d)
|
||||
|
||||
case Event(e@error(problem), d: DATA_NORMAL) =>
|
||||
log.error(s"peer sent $e, closing connection") // see bolt #2: A node MUST fail the connection if it receives an err message
|
||||
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((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_NORMAL) => handleTheirSpentOther(tx, d)
|
||||
|
||||
case Event(e@error(problem), d: DATA_NORMAL) => handleError(e, d)
|
||||
|
||||
}
|
||||
|
||||
@ -464,124 +393,67 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
|
||||
*/
|
||||
|
||||
when(CLEARING) {
|
||||
case Event(CMD_FULFILL_HTLC(id, r), d: DATA_CLEARING) =>
|
||||
Try(Commitments.sendFulfill(d.commitments, CMD_FULFILL_HTLC(id, r))) match {
|
||||
case Success((commitments1, fullfill)) =>
|
||||
them ! fullfill
|
||||
sender ! "ok"
|
||||
stay using d.copy(commitments = commitments1)
|
||||
case Failure(cause) =>
|
||||
log.error(cause, "")
|
||||
sender ! cause.getMessage
|
||||
stay
|
||||
case Event(c@CMD_FULFILL_HTLC(id, r), d: DATA_CLEARING) =>
|
||||
Try(Commitments.sendFulfill(d.commitments, c)) match {
|
||||
case Success((commitments1, fulfill)) => handleCommandSuccess(sender, fulfill, d.copy(commitments = commitments1))
|
||||
case Failure(cause) => handleCommandError(sender, cause)
|
||||
}
|
||||
|
||||
case Event(fulfill@update_fulfill_htlc(id, r), d: DATA_CLEARING) =>
|
||||
Try(Commitments.receiveFulfill(d.commitments, fulfill)) match {
|
||||
case Success(commitments1) =>
|
||||
stay using d.copy(commitments = commitments1)
|
||||
case Failure(cause) =>
|
||||
log.error(cause, "")
|
||||
them ! error(Some(cause.getMessage))
|
||||
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 Success(commitments1) => stay using d.copy(commitments = commitments1)
|
||||
case Failure(cause) => handleUnicloseError(cause, d)
|
||||
}
|
||||
|
||||
case Event(CMD_FAIL_HTLC(id, reason), d: DATA_CLEARING) =>
|
||||
Try(Commitments.sendFail(d.commitments, CMD_FAIL_HTLC(id, reason))) match {
|
||||
case Success((commitments1, fail)) =>
|
||||
them ! fail
|
||||
sender ! "ok"
|
||||
stay using d.copy(commitments = commitments1)
|
||||
case Failure(cause) =>
|
||||
log.error(cause, "")
|
||||
sender ! cause.getMessage
|
||||
stay
|
||||
case Event(c@CMD_FAIL_HTLC(id, reason), d: DATA_CLEARING) =>
|
||||
Try(Commitments.sendFail(d.commitments, c)) match {
|
||||
case Success((commitments1, fail)) => handleCommandSuccess(sender, fail, d.copy(commitments = commitments1))
|
||||
case Failure(cause) => handleCommandError(sender, cause)
|
||||
}
|
||||
|
||||
case Event(fail@update_fail_htlc(id, reason), d: DATA_CLEARING) =>
|
||||
Try(Commitments.receiveFail(d.commitments, fail)) match {
|
||||
case Success(commitments1) =>
|
||||
stay using d.copy(commitments = commitments1)
|
||||
case Failure(cause) =>
|
||||
log.error(cause, "")
|
||||
them ! error(Some(cause.getMessage))
|
||||
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 Success(commitments1) => stay using d.copy(commitments = commitments1)
|
||||
case Failure(cause) => handleUnicloseError(cause, d)
|
||||
}
|
||||
|
||||
case Event(CMD_SIGN, d: DATA_CLEARING) =>
|
||||
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(cause.getMessage))
|
||||
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))
|
||||
}
|
||||
Try(Commitments.sendCommit(d.commitments)) match {
|
||||
case Success((commitments1, commit)) => handleCommandSuccess(sender, commit, d.copy(commitments = commitments1))
|
||||
case Failure(cause) => handleCommandError(sender, cause)
|
||||
}
|
||||
|
||||
case Event(msg@update_commit(theirSig), d@DATA_CLEARING(commitments, _, ourClearing, theirClearing)) =>
|
||||
case Event(msg@update_commit(theirSig), d@DATA_CLEARING(commitments, ourClearing, theirClearing)) =>
|
||||
Try(Commitments.receiveCommit(d.commitments, msg)) match {
|
||||
case Success((commitments1, revocation)) if commitments1.hasNoPendingHtlcs =>
|
||||
them ! revocation
|
||||
val (_, ourCloseSig) = makeFinalTx(commitments1, ourClearing.scriptPubkey, theirClearing.scriptPubkey)
|
||||
them ! ourCloseSig
|
||||
goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, ourClearing, theirClearing, ourCloseSig)
|
||||
case Success((commitments1, revocation)) =>
|
||||
them ! revocation
|
||||
if (commitments1.hasNoPendingHtlcs) {
|
||||
val (finalTx, ourCloseSig) = makeFinalTx(commitments1, ourClearing.scriptPubkey, theirClearing.scriptPubkey)
|
||||
them ! ourCloseSig
|
||||
goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, d.htlcIdx, ourClearing, theirClearing, ourCloseSig)
|
||||
} else {
|
||||
stay using d.copy(commitments = commitments1)
|
||||
}
|
||||
case Failure(cause) =>
|
||||
log.error(cause, "")
|
||||
them ! error(Some(cause.getMessage))
|
||||
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))
|
||||
stay using d.copy(commitments = commitments1)
|
||||
case Failure(cause) => handleUnicloseError(cause, d)
|
||||
}
|
||||
|
||||
case Event(msg@update_revocation(revocationPreimage, nextRevocationHash), d@DATA_CLEARING(commitments, _, ourClearing, theirClearing)) =>
|
||||
case Event(msg@update_revocation(revocationPreimage, nextRevocationHash), d@DATA_CLEARING(commitments, ourClearing, theirClearing)) =>
|
||||
// we received a revocation because we sent a signature
|
||||
// => all our changes have been acked
|
||||
Try(Commitments.receiveRevocation(d.commitments, msg)) match {
|
||||
case Success(commitments1) if commitments1.hasNoPendingHtlcs =>
|
||||
val (finalTx, ourCloseSig) = makeFinalTx(commitments1, ourClearing.scriptPubkey, theirClearing.scriptPubkey)
|
||||
them ! ourCloseSig
|
||||
goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, d.htlcIdx, ourClearing, theirClearing, ourCloseSig)
|
||||
goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, ourClearing, theirClearing, ourCloseSig)
|
||||
case Success(commitments1) =>
|
||||
stay using d.copy(commitments = commitments1)
|
||||
case Failure(cause) =>
|
||||
log.error(cause, "")
|
||||
them ! error(Some(cause.getMessage))
|
||||
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 Failure(cause) => handleUnicloseError(cause, d)
|
||||
}
|
||||
|
||||
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_NORMAL) =>
|
||||
// TODO : not implemented
|
||||
stay
|
||||
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_CLEARING) if tx.txid == d.commitments.theirCommit.txid => handleTheirSpentCurrent(tx, d)
|
||||
|
||||
case Event(e@error(problem), d: DATA_CLEARING) =>
|
||||
log.error(s"peer sent $e, closing connection") // see bolt #2: A node MUST fail the connection if it receives an err message
|
||||
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((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_CLEARING) => handleTheirSpentOther(tx, d)
|
||||
|
||||
case Event(e@error(problem), d: DATA_CLEARING) => handleError(e, d)
|
||||
}
|
||||
|
||||
when(NEGOTIATING) {
|
||||
@ -618,16 +490,14 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
|
||||
throw new RuntimeException("cannot verify their close signature", cause)
|
||||
}
|
||||
|
||||
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_NORMAL) =>
|
||||
// TODO : not implemented
|
||||
// seing the anchor being spent here could be normal
|
||||
stay
|
||||
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_NEGOTIATING) if tx.txid == d.commitments.theirCommit.txid =>
|
||||
// TODO : this may be normal
|
||||
handleTheirSpentCurrent(tx, d)
|
||||
|
||||
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_NEGOTIATING) => handleTheirSpentOther(tx, d)
|
||||
|
||||
case Event(e@error(problem), d: DATA_NEGOTIATING) => handleError(e, d)
|
||||
|
||||
case Event(e@error(problem), d: DATA_NEGOTIATING) =>
|
||||
log.error(s"peer sent $e, closing connection") // see bolt #2: A node MUST fail the connection if it receives an err message
|
||||
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(CLOSING) {
|
||||
@ -681,6 +551,74 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
888 888 d8888 888b 888 8888888b. 888 8888888888 8888888b. .d8888b.
|
||||
888 888 d88888 8888b 888 888 "Y88b 888 888 888 Y88b d88P Y88b
|
||||
888 888 d88P888 88888b 888 888 888 888 888 888 888 Y88b.
|
||||
8888888888 d88P 888 888Y88b 888 888 888 888 8888888 888 d88P "Y888b.
|
||||
888 888 d88P 888 888 Y88b888 888 888 888 888 8888888P" "Y88b.
|
||||
888 888 d88P 888 888 Y88888 888 888 888 888 888 T88b "888
|
||||
888 888 d8888888888 888 Y8888 888 .d88P 888 888 888 T88b Y88b d88P
|
||||
888 888 d88P 888 888 Y888 8888888P" 88888888 8888888888 888 T88b "Y8888P"
|
||||
*/
|
||||
|
||||
def handleCommandSuccess(sender: ActorRef, change: Change, newData: Data) = {
|
||||
them ! change
|
||||
sender ! "ok"
|
||||
stay using newData
|
||||
}
|
||||
|
||||
def handleCommandError(sender: ActorRef, cause: Throwable) = {
|
||||
log.error(cause, "")
|
||||
sender ! cause.getMessage
|
||||
stay
|
||||
}
|
||||
|
||||
def handleUnicloseError(cause: Throwable, d: HasCommitments) = {
|
||||
log.error(cause, "")
|
||||
them ! error(Some(cause.getMessage))
|
||||
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))
|
||||
}
|
||||
|
||||
def handleTheirSpentCurrent(tx: Transaction, d: HasCommitments) = {
|
||||
log.warning(s"they published their current commit in txid=${tx.txid}")
|
||||
// TODO
|
||||
???
|
||||
}
|
||||
|
||||
def handleTheirSpentOther(tx: Transaction, d: HasCommitments) = {
|
||||
log.warning(s"anchor spent in txid=${tx.txid}")
|
||||
d.commitments.txDb.get(tx.txid) match {
|
||||
case Some(spendingTx) =>
|
||||
log.warning(s"txid=${tx.txid} was a revoked commitment, publishing the punishment tx")
|
||||
them ! error(Some("Anchor has been spent"))
|
||||
blockchain ! Publish(spendingTx)
|
||||
blockchain ! WatchConfirmed(self, spendingTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
|
||||
goto(CLOSING) using DATA_CLOSING(d.commitments, revokedPublished = Seq(tx))
|
||||
case None =>
|
||||
// the published tx was neither their current commitment nor a revoked one
|
||||
log.error(s"couldn't identify txid=${tx.txid}!")
|
||||
goto(ERR_INFORMATION_LEAK)
|
||||
}
|
||||
}
|
||||
|
||||
def handleError(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
|
||||
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))
|
||||
}
|
||||
|
||||
def handleInformationLeak(d: HasCommitments) = {
|
||||
// this is never supposed to happen !!
|
||||
log.error(s"our anchor ${d.commitments.anchorId} was spent !!")
|
||||
them ! error(Some("Anchor has been spent"))
|
||||
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
|
||||
goto(ERR_INFORMATION_LEAK)
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper function runs the state's default event handlers, and react to exceptions by unilaterally closing the channel
|
||||
*/
|
||||
|
@ -1,8 +1,6 @@
|
||||
package fr.acinq.eclair.channel
|
||||
|
||||
import com.trueaccord.scalapb.GeneratedMessage
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto, Transaction, TxOut}
|
||||
import fr.acinq.eclair.crypto.ShaChain
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto, Transaction}
|
||||
import lightning._
|
||||
|
||||
/**
|
||||
@ -131,16 +129,6 @@ final case class CommitmentSpec(htlcs: Set[Htlc], feeRate: Long, initial_amount_
|
||||
val totalFunds = amount_us_msat + amount_them_msat + htlcs.toSeq.map(_.amountMsat).sum
|
||||
}
|
||||
|
||||
object TypeDefs {
|
||||
type Change = GeneratedMessage
|
||||
}
|
||||
import TypeDefs._
|
||||
case class OurChanges(proposed: List[Change], signed: List[Change], acked: List[Change])
|
||||
case class TheirChanges(proposed: List[Change], acked: List[Change])
|
||||
case class Changes(ourChanges: OurChanges, theirChanges: TheirChanges)
|
||||
case class OurCommit(index: Long, spec: CommitmentSpec, publishableTx: Transaction)
|
||||
case class TheirCommit(index: Long, spec: CommitmentSpec, theirRevocationHash: sha256_hash)
|
||||
|
||||
final case class ClosingData(ourScriptPubKey: BinaryData, theirScriptPubKey: Option[BinaryData])
|
||||
|
||||
trait HasCommitments {
|
||||
@ -152,11 +140,11 @@ final case class DATA_OPEN_WITH_ANCHOR_WAIT_FOR_ANCHOR(ourParams: OurChannelPara
|
||||
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 with HasCommitments
|
||||
final case class DATA_NORMAL (commitments: Commitments, htlcIdx: Long,
|
||||
final case class DATA_NORMAL (commitments: Commitments,
|
||||
ourClearing: Option[close_clearing]) extends Data with HasCommitments
|
||||
final case class DATA_CLEARING (commitments: Commitments, htlcIdx: Long,
|
||||
final case class DATA_CLEARING (commitments: Commitments,
|
||||
ourClearing: close_clearing, theirClearing: close_clearing) extends Data with HasCommitments
|
||||
final case class DATA_NEGOTIATING (commitments: Commitments, htlcIdx: Long,
|
||||
final case class DATA_NEGOTIATING (commitments: Commitments,
|
||||
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,
|
||||
|
@ -1,12 +1,43 @@
|
||||
package fr.acinq.eclair.channel
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto, Satoshi, Transaction, TxOut}
|
||||
import com.trueaccord.scalapb.GeneratedMessage
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto, ScriptFlags, Transaction, TxOut}
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.channel.Scripts.TxTemplate
|
||||
import fr.acinq.eclair.channel.TypeDefs.Change
|
||||
import fr.acinq.eclair.crypto.ShaChain
|
||||
import lightning._
|
||||
|
||||
trait TxDb {
|
||||
def add(parentId: BinaryData, spending: Transaction): Unit
|
||||
|
||||
def get(parentId: BinaryData): Option[Transaction]
|
||||
}
|
||||
|
||||
class BasicTxDb extends TxDb {
|
||||
val db = collection.mutable.HashMap.empty[BinaryData, Transaction]
|
||||
|
||||
override def add(parentId: BinaryData, spending: Transaction): Unit = {
|
||||
db += parentId -> spending
|
||||
}
|
||||
|
||||
override def get(parentId: BinaryData): Option[Transaction] = db.get(parentId)
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
|
||||
object TypeDefs {
|
||||
type Change = GeneratedMessage
|
||||
}
|
||||
case class OurChanges(proposed: List[Change], signed: List[Change], acked: List[Change])
|
||||
case class TheirChanges(proposed: List[Change], acked: List[Change])
|
||||
case class Changes(ourChanges: OurChanges, theirChanges: TheirChanges)
|
||||
case class OurCommit(index: Long, spec: CommitmentSpec, publishableTx: Transaction)
|
||||
case class TheirCommit(index: Long, spec: CommitmentSpec, txid: BinaryData, theirRevocationHash: sha256_hash)
|
||||
|
||||
// @formatter:on
|
||||
|
||||
/**
|
||||
* about theirNextCommitInfo:
|
||||
* we either:
|
||||
@ -18,8 +49,9 @@ import lightning._
|
||||
case class Commitments(ourParams: OurChannelParams, theirParams: TheirChannelParams,
|
||||
ourCommit: OurCommit, theirCommit: TheirCommit,
|
||||
ourChanges: OurChanges, theirChanges: TheirChanges,
|
||||
ourCurrentHtlcId: Long,
|
||||
theirNextCommitInfo: Either[TheirCommit, BinaryData],
|
||||
anchorOutput: TxOut, theirPreimages: ShaChain) {
|
||||
anchorOutput: TxOut, theirPreimages: ShaChain, txDb: TxDb) {
|
||||
def anchorId: BinaryData = {
|
||||
assert(ourCommit.publishableTx.txIn.size == 1, "commitment tx should only have one input")
|
||||
ourCommit.publishableTx.txIn(0).outPoint.hash
|
||||
@ -40,12 +72,42 @@ object Commitments {
|
||||
* @param proposal
|
||||
* @return an updated commitment instance
|
||||
*/
|
||||
def addOurProposal(commitments: Commitments, proposal: Change): Commitments =
|
||||
commitments.copy(ourChanges = commitments.ourChanges.copy(proposed = commitments.ourChanges.proposed :+ proposal))
|
||||
private def addOurProposal(commitments: Commitments, proposal: Change): Commitments =
|
||||
commitments.copy(ourChanges = commitments.ourChanges.copy(proposed = commitments.ourChanges.proposed :+ proposal))
|
||||
|
||||
def addTheirProposal(commitments: Commitments, proposal: Change): Commitments =
|
||||
private def addTheirProposal(commitments: Commitments, proposal: Change): Commitments =
|
||||
commitments.copy(theirChanges = commitments.theirChanges.copy(proposed = commitments.theirChanges.proposed :+ proposal))
|
||||
|
||||
def sendAdd(commitments: Commitments, cmd: CMD_ADD_HTLC): (Commitments, update_add_htlc) = {
|
||||
// our available funds as seen by them, including all pending changes
|
||||
val reduced = Helpers.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 (cmd.amountMsat > available) {
|
||||
throw new RuntimeException(s"insufficient funds (available=$available msat)")
|
||||
} else {
|
||||
// TODO: nodeIds are ignored
|
||||
val id = cmd.id.getOrElse(commitments.ourCurrentHtlcId + 1)
|
||||
val steps = route(route_step(0, next = route_step.Next.End(true)) :: Nil)
|
||||
val add = update_add_htlc(id, cmd.amountMsat, cmd.rHash, cmd.expiry, routing(ByteString.copyFrom(steps.toByteArray)))
|
||||
val commitments1 = addOurProposal(commitments, add).copy(ourCurrentHtlcId = id)
|
||||
(commitments1, add)
|
||||
}
|
||||
}
|
||||
|
||||
def receiveAdd(commitments: Commitments, add: update_add_htlc): Commitments = {
|
||||
// their available funds as seen by us, including all pending changes
|
||||
val reduced = Helpers.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 (add.amountMsat > available) {
|
||||
throw new RuntimeException("Insufficient funds")
|
||||
} else {
|
||||
// TODO: nodeIds are ignored
|
||||
addTheirProposal(commitments, add)
|
||||
}
|
||||
}
|
||||
|
||||
def sendFulfill(commitments: Commitments, cmd: CMD_FULFILL_HTLC): (Commitments, update_fulfill_htlc) = {
|
||||
commitments.theirChanges.acked.collectFirst { case u: update_add_htlc if u.id == cmd.id => u } match {
|
||||
case Some(htlc) if htlc.rHash == bin2sha256(Crypto.sha256(cmd.r)) =>
|
||||
@ -84,21 +146,23 @@ object Commitments {
|
||||
}
|
||||
|
||||
def sendCommit(commitments: Commitments): (Commitments, update_commit) = {
|
||||
// TODO : check empty changes
|
||||
import commitments._
|
||||
commitments.theirNextCommitInfo match {
|
||||
case Right(theirNextRevocationHash) =>
|
||||
// sign all our proposals + their acked proposals
|
||||
// their commitment now includes all our changes + their acked changes
|
||||
val spec = Helpers.reduce(theirCommit.spec, theirChanges.acked, ourChanges.acked ++ ourChanges.signed ++ ourChanges.proposed)
|
||||
val theirTx = Helpers.makeTheirTx(ourParams, theirParams, ourCommit.publishableTx.txIn, theirNextRevocationHash, spec)
|
||||
val theirTxTemplate = Helpers.makeTheirTxTemplate(ourParams, theirParams, ourCommit.publishableTx.txIn, theirNextRevocationHash, spec)
|
||||
val theirTx = theirTxTemplate.makeTx
|
||||
val ourSig = Helpers.sign(ourParams, theirParams, anchorOutput.amount, theirTx)
|
||||
val commit = update_commit(ourSig)
|
||||
val commitments1 = commitments.copy(
|
||||
theirNextCommitInfo = Left(TheirCommit(theirCommit.index + 1, spec, theirNextRevocationHash)),
|
||||
theirNextCommitInfo = Left(TheirCommit(theirCommit.index + 1, spec, theirTx.txid, theirNextRevocationHash)),
|
||||
ourChanges = ourChanges.copy(proposed = Nil, signed = ourChanges.signed ++ ourChanges.proposed))
|
||||
(commitments1, commit)
|
||||
case Left(theirNextCommit) =>
|
||||
throw new RuntimeException("attempting to sign twice waiting for the first revocation message")
|
||||
throw new RuntimeException("cannot sign until next revocation hash is received")
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,6 +209,13 @@ object Commitments {
|
||||
case Left(theirNextCommit) if BinaryData(Crypto.sha256(revocation.revocationPreimage)) != BinaryData(theirCommit.theirRevocationHash) =>
|
||||
throw new RuntimeException("invalid preimage")
|
||||
case Left(theirNextCommit) =>
|
||||
// this is their revoked commit tx
|
||||
val theirTxTemplate = Helpers.makeTheirTxTemplate(ourParams, theirParams, ourCommit.publishableTx.txIn, theirCommit.theirRevocationHash, theirCommit.spec)
|
||||
val theirTx = theirTxTemplate.makeTx
|
||||
val punishTx = Helpers.claimRevokedCommitTx(theirTxTemplate, revocation.revocationPreimage, ourParams.finalPrivKey)
|
||||
Transaction.correctlySpends(punishTx, Seq(theirTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
txDb.add(theirTx.txid, punishTx)
|
||||
|
||||
commitments.copy(
|
||||
ourChanges = ourChanges.copy(signed = Nil, acked = ourChanges.acked ++ ourChanges.signed),
|
||||
theirCommit = theirNextCommit,
|
||||
@ -154,6 +225,17 @@ object Commitments {
|
||||
throw new RuntimeException("received unexpected update_revocation message")
|
||||
}
|
||||
}
|
||||
|
||||
def makeTheirTxTemplate(commitments: Commitments): TxTemplate = {
|
||||
commitments.theirNextCommitInfo match {
|
||||
case Left(theirNextCommit) =>
|
||||
Helpers.makeTheirTxTemplate(commitments.ourParams, commitments.theirParams, commitments.ourCommit.publishableTx.txIn,
|
||||
theirNextCommit.theirRevocationHash, theirNextCommit.spec)
|
||||
case Right(revocationHash) =>
|
||||
Helpers.makeTheirTxTemplate(commitments.ourParams, commitments.theirParams, commitments.ourCommit.publishableTx.txIn,
|
||||
commitments.theirCommit.theirRevocationHash, commitments.theirCommit.spec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -71,8 +71,11 @@ object Helpers {
|
||||
def makeOurTx(ourParams: OurChannelParams, theirParams: TheirChannelParams, inputs: Seq[TxIn], ourRevocationHash: sha256_hash, spec: CommitmentSpec): Transaction =
|
||||
makeCommitTx(inputs, ourParams.finalPubKey, theirParams.finalPubKey, ourParams.delay, ourRevocationHash, spec)
|
||||
|
||||
def makeTheirTxTemplate(ourParams: OurChannelParams, theirParams: TheirChannelParams, inputs: Seq[TxIn], theirRevocationHash: sha256_hash, spec: CommitmentSpec): TxTemplate =
|
||||
makeCommitTxTemplate(inputs, theirParams.finalPubKey, ourParams.finalPubKey, theirParams.delay, theirRevocationHash, spec)
|
||||
|
||||
def makeTheirTx(ourParams: OurChannelParams, theirParams: TheirChannelParams, inputs: Seq[TxIn], theirRevocationHash: sha256_hash, spec: CommitmentSpec): Transaction =
|
||||
makeCommitTx(inputs, theirParams.finalPubKey, ourParams.finalPubKey, theirParams.delay, theirRevocationHash, spec)
|
||||
makeTheirTxTemplate(ourParams, theirParams, inputs, theirRevocationHash, spec).makeTx
|
||||
|
||||
def sign(ourParams: OurChannelParams, theirParams: TheirChannelParams, anchorAmount: Satoshi, tx: Transaction): signature =
|
||||
bin2signature(Transaction.signInput(tx, 0, multiSig2of2(ourParams.commitPubKey, theirParams.commitPubKey), SIGHASH_ALL, anchorAmount, 1, ourParams.commitPrivKey))
|
||||
@ -209,4 +212,51 @@ object Helpers {
|
||||
tx1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* claim a revoked commit tx
|
||||
* @param theirTxTemplate revoked commit tx template
|
||||
* @param revocationPreimage revocation preimage
|
||||
* @param privateKey private key to send the claimed funds to
|
||||
* @return a signed transaction that spends the revoked commit tx
|
||||
*/
|
||||
def claimRevokedCommitTx(theirTxTemplate: TxTemplate, revocationPreimage: BinaryData, privateKey: BinaryData): Transaction = {
|
||||
val theirTx = theirTxTemplate.makeTx
|
||||
val outputs = collection.mutable.ListBuffer.empty[TxOut]
|
||||
|
||||
def findOutputIndex(output: TxOut): Option[Int] = {
|
||||
for (i <- 0 until theirTx.txOut.length) {
|
||||
if (theirTx.txOut(i) == output) return Some(i)
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// first, find out how much we can claim
|
||||
val outputsToClaim = (theirTxTemplate.ourOutput.toSeq ++ theirTxTemplate.htlcReceived.toSeq ++ theirTxTemplate.htlcSent.toSeq).filter(o => findOutputIndex(o.txOut).isDefined)
|
||||
val totalAmount = outputsToClaim.map(_.amount).sum
|
||||
|
||||
// create a tx that sends everything to our private key
|
||||
val tx = Transaction(version = 2,
|
||||
txIn = Seq.empty[TxIn],
|
||||
txOut = TxOut(totalAmount, pay2wpkh(Crypto.publicKeyFromPrivateKey(privateKey))) :: Nil,
|
||||
witness = Seq.empty[ScriptWitness],
|
||||
lockTime = 0)
|
||||
|
||||
// create tx inputs that spend each output that we can spend
|
||||
val inputs = outputsToClaim.map(outputTemplate => {
|
||||
val index = findOutputIndex(outputTemplate.txOut).get
|
||||
TxIn(OutPoint(theirTx, index), signatureScript = BinaryData.empty, sequence = 0xffffffffL)
|
||||
})
|
||||
assert(inputs.length == outputsToClaim.length)
|
||||
|
||||
// and sign them
|
||||
val tx1 = tx.copy(txIn = inputs)
|
||||
val witnesses = for(i <- 0 until tx1.txIn.length) yield {
|
||||
val sig = Transaction.signInput(tx1, i, outputsToClaim(i).redeemScript, SIGHASH_ALL, outputsToClaim(i).amount, 1, privateKey)
|
||||
val witness = ScriptWitness(sig :: revocationPreimage :: outputsToClaim(i).redeemScript :: Nil)
|
||||
witness
|
||||
}
|
||||
|
||||
tx1.copy(witness = witnesses)
|
||||
}
|
||||
}
|
||||
|
@ -166,9 +166,45 @@ object Scripts {
|
||||
(amount_us1, amount_them1)
|
||||
}
|
||||
|
||||
def makeCommitTx(inputs: Seq[TxIn], ourFinalKey: BinaryData, theirFinalKey: BinaryData, theirDelay: locktime, revocationHash: BinaryData, commitmentSpec: CommitmentSpec): Transaction = {
|
||||
sealed trait OutputTemplate {
|
||||
def amount: Satoshi
|
||||
|
||||
def txOut: TxOut
|
||||
|
||||
// this is the actual script that must be used to claim this output
|
||||
def redeemScript: BinaryData
|
||||
}
|
||||
|
||||
case class P2WSH(amount: Satoshi, script: BinaryData) extends OutputTemplate {
|
||||
override def txOut: TxOut = TxOut(amount, pay2wsh(script))
|
||||
override def redeemScript = script
|
||||
}
|
||||
|
||||
object P2WSH {
|
||||
def apply(amount: Satoshi, script: Seq[ScriptElt]): P2WSH = new P2WSH(amount, Script.write(script))
|
||||
}
|
||||
|
||||
case class P2WPKH(amount: Satoshi, publicKey: BinaryData) extends OutputTemplate {
|
||||
override def txOut: TxOut = TxOut(amount, pay2wpkh(publicKey))
|
||||
override def redeemScript = Script.write(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(Crypto.hash160(publicKey)) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil)
|
||||
}
|
||||
|
||||
case class TxTemplate(inputs: Seq[TxIn], ourOutput: Option[OutputTemplate], theirOutput: Option[OutputTemplate], htlcSent: Seq[OutputTemplate], htlcReceived: Seq[OutputTemplate]) {
|
||||
def makeTx: Transaction = {
|
||||
val outputs = ourOutput.toSeq ++ theirOutput.toSeq ++ htlcSent ++ htlcReceived
|
||||
val tx = Transaction(
|
||||
version = 2,
|
||||
txIn = inputs,
|
||||
txOut = outputs.map(_.txOut),
|
||||
lockTime = 0
|
||||
)
|
||||
permuteOutputs(tx)
|
||||
}
|
||||
}
|
||||
|
||||
def makeCommitTxTemplate(inputs: Seq[TxIn], ourFinalKey: BinaryData, theirFinalKey: BinaryData, theirDelay: locktime, revocationHash: BinaryData, commitmentSpec: CommitmentSpec): TxTemplate = {
|
||||
val redeemScript = redeemSecretOrDelay(ourFinalKey, locktime2long_csv(theirDelay), theirFinalKey, revocationHash: BinaryData)
|
||||
val htlcs = commitmentSpec.htlcs.filter(_.amountMsat >= 546000)
|
||||
val htlcs = commitmentSpec.htlcs.filter(_.amountMsat >= 546000).toSeq
|
||||
val fee_msat = computeFee(commitmentSpec.feeRate, htlcs.size) * 1000
|
||||
val (amount_us_msat: Long, amount_them_msat: Long) = (commitmentSpec.amount_us_msat, commitmentSpec.amount_them_msat) match {
|
||||
case (us, them) if us >= fee_msat / 2 && them >= fee_msat / 2 => (us - fee_msat / 2, them - fee_msat / 2)
|
||||
@ -176,26 +212,27 @@ object Scripts {
|
||||
case (us, them) if them < fee_msat / 2 => (Math.max(us - fee_msat + them, 0L), 0L)
|
||||
}
|
||||
|
||||
val outputs = Seq(
|
||||
// TODO : is that the correct way to handle sub-satoshi balances ?
|
||||
TxOut(amount = Satoshi(amount_us_msat / 1000), publicKeyScript = pay2wsh(redeemScript)),
|
||||
TxOut(amount = Satoshi(amount_them_msat / 1000), publicKeyScript = pay2wpkh(theirFinalKey))
|
||||
).filterNot(_.amount.toLong < 546) // do not add dust
|
||||
// our output is a pay2wsh output than can be claimed by them if they know the preimage, or by us after a delay
|
||||
// when * they * publish a revoked commit tx, we use the preimage that they sent us to claim it
|
||||
val ourOutput = if (amount_us_msat >= 546000) Some(P2WSH(Satoshi(amount_us_msat / 1000), redeemScript)) else None
|
||||
|
||||
val tx = Transaction(
|
||||
version = 2,
|
||||
txIn = inputs,
|
||||
txOut = outputs,
|
||||
lockTime = 0)
|
||||
// their output is a simple pay2pkh output that sends money to their final key and can only be claimed by them
|
||||
// when * they * publish a revoked commit tx we don't have anything special to do about it
|
||||
val theirOutput = if (amount_them_msat >= 546000) Some(P2WPKH(Satoshi(amount_them_msat / 1000), theirFinalKey)) else None
|
||||
|
||||
val sendOuts = htlcs.filter(_.direction == OUT).map(htlc =>
|
||||
TxOut(Satoshi(htlc.amountMsat / 1000), pay2wsh(scriptPubKeyHtlcSend(ourFinalKey, theirFinalKey, locktime2long_cltv(htlc.expiry), locktime2long_csv(theirDelay), htlc.rHash, revocationHash)))
|
||||
)
|
||||
val receiveOuts = htlcs.filter(_.direction == IN).map(htlc =>
|
||||
TxOut(Satoshi(htlc.amountMsat / 1000), pay2wsh(scriptPubKeyHtlcReceive(ourFinalKey, theirFinalKey, locktime2long_cltv(htlc.expiry), locktime2long_csv(theirDelay), htlc.rHash, revocationHash)))
|
||||
)
|
||||
val tx1 = tx.copy(txOut = tx.txOut ++ sendOuts ++ receiveOuts)
|
||||
permuteOutputs(tx1)
|
||||
val sendOuts: Seq[OutputTemplate] = htlcs.filter(_.direction == OUT).map(htlc => {
|
||||
P2WSH(Satoshi(htlc.amountMsat / 1000), scriptPubKeyHtlcSend(ourFinalKey, theirFinalKey, locktime2long_cltv(htlc.expiry), locktime2long_csv(theirDelay), htlc.rHash, revocationHash))
|
||||
})
|
||||
val receiveOuts: Seq[OutputTemplate] = htlcs.filter(_.direction == IN).map(htlc => {
|
||||
P2WSH(Satoshi(htlc.amountMsat / 1000), scriptPubKeyHtlcReceive(ourFinalKey, theirFinalKey, locktime2long_cltv(htlc.expiry), locktime2long_csv(theirDelay), htlc.rHash, revocationHash))
|
||||
})
|
||||
TxTemplate(inputs, ourOutput, theirOutput, sendOuts, receiveOuts)
|
||||
}
|
||||
|
||||
def makeCommitTx(inputs: Seq[TxIn], ourFinalKey: BinaryData, theirFinalKey: BinaryData, theirDelay: locktime, revocationHash: BinaryData, commitmentSpec: CommitmentSpec): Transaction = {
|
||||
val txTemplate = makeCommitTxTemplate(inputs, ourFinalKey, theirFinalKey, theirDelay, revocationHash, commitmentSpec)
|
||||
val tx = txTemplate.makeTx
|
||||
tx
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,6 +1,6 @@
|
||||
package fr.acinq.eclair
|
||||
|
||||
import fr.acinq.bitcoin.{Base58, Base58Check, Crypto, Hash, OutPoint, Satoshi, TxIn, TxOut}
|
||||
import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, Crypto, Hash, OutPoint, Satoshi, TxIn, TxOut}
|
||||
import fr.acinq.eclair.channel.{TheirChanges, _}
|
||||
import fr.acinq.eclair.crypto.ShaChain
|
||||
import lightning.locktime
|
||||
@ -33,9 +33,9 @@ object TestConstants {
|
||||
val commitments = Commitments(
|
||||
Alice.channelParams,
|
||||
TheirChannelParams(Bob.channelParams),
|
||||
OurCommit(0, ourSpec, ourTx), TheirCommit(0, theirSpec, Bob.revocationHash(0)),
|
||||
OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil),
|
||||
Right(Bob.revocationHash(1)), anchorOutput, ShaChain.init)
|
||||
OurCommit(0, ourSpec, ourTx), TheirCommit(0, theirSpec, BinaryData(""), Bob.revocationHash(0)),
|
||||
OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil), 0L,
|
||||
Right(Bob.revocationHash(1)), anchorOutput, ShaChain.init, new BasicTxDb)
|
||||
|
||||
}
|
||||
|
||||
@ -56,9 +56,9 @@ object TestConstants {
|
||||
val commitments = Commitments(
|
||||
Bob.channelParams,
|
||||
TheirChannelParams(Alice.channelParams),
|
||||
OurCommit(0, ourSpec, ourTx), TheirCommit(0, theirSpec, Alice.revocationHash(0)),
|
||||
OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil),
|
||||
Right(Alice.revocationHash(1)), anchorOutput, ShaChain.init)
|
||||
OurCommit(0, ourSpec, ourTx), TheirCommit(0, theirSpec, BinaryData(""), Alice.revocationHash(0)),
|
||||
OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil), 0L,
|
||||
Right(Alice.revocationHash(1)), anchorOutput, ShaChain.init, new BasicTxDb)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ class StealRevokedCommitmentSpec extends FunSuite {
|
||||
}
|
||||
|
||||
def addHtlc(sender: Commitments, receiver: Commitments, htlc: update_add_htlc): (Commitments, Commitments) = {
|
||||
(Commitments.addOurProposal(sender, htlc), Commitments.addTheirProposal(receiver, htlc))
|
||||
(Commitments.sendAdd(sender, CMD_ADD_HTLC(id = Some(htlc.id), amountMsat = htlc.amountMsat, rHash = htlc.rHash, expiry = htlc.expiry))._1, Commitments.receiveAdd(receiver, htlc))
|
||||
}
|
||||
|
||||
def fulfillHtlc(sender: Commitments, receiver: Commitments, id: Long, paymentPreimage: BinaryData): (Commitments, Commitments) = {
|
||||
@ -44,13 +44,22 @@ class StealRevokedCommitmentSpec extends FunSuite {
|
||||
val (bob4, alice4) = fulfillHtlc(bob3, alice3, 1, R)
|
||||
val (bob5, alice5) = signAndReceiveRevocation(bob4, alice4)
|
||||
|
||||
|
||||
val theirTxTemplate = Commitments.makeTheirTxTemplate(bob3)
|
||||
val theirTx = theirTxTemplate.makeTx
|
||||
assert(theirTx.txIn == alice3.ourCommit.publishableTx.txIn && theirTx.txOut == alice3.ourCommit.publishableTx.txOut)
|
||||
val preimage = bob5.theirPreimages.getHash(0xFFFFFFFFFFFFFFFFL - bob3.theirCommit.index).get
|
||||
val punishTx = Helpers.claimRevokedCommitTx(theirTxTemplate, preimage, bob3.ourParams.finalPrivKey)
|
||||
Transaction.correctlySpends(punishTx, Seq(alice3.ourCommit.publishableTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
|
||||
|
||||
// now what if Alice published a revoked commit tx ?
|
||||
Seq(alice1, alice2, alice3, alice4).map(alice => {
|
||||
val stealTx = Helpers.claimTheirRevokedCommit(alice.ourCommit.publishableTx, bob5)
|
||||
val stealTx = bob5.txDb.get(alice.ourCommit.publishableTx.txid)
|
||||
Transaction.correctlySpends(stealTx.get, Seq(alice.ourCommit.publishableTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
})
|
||||
|
||||
// but we cannot steal Alice's current commit tx
|
||||
assert(Helpers.claimTheirRevokedCommit(alice5.ourCommit.publishableTx, bob5) == None)
|
||||
assert(bob5.txDb.get(alice5.ourCommit.publishableTx.txid) == None)
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ import scala.concurrent.duration._
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class OpenWaitingTheirAnchorStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteLike with BeforeAndAfterAll {
|
||||
|
||||
type FixtureParam = Tuple4[TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe]
|
||||
type FixtureParam = Tuple5[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe]
|
||||
|
||||
override def withFixture(test: OneArgTest) = {
|
||||
val alice2bob = TestProbe()
|
||||
@ -39,14 +39,14 @@ class OpenWaitingTheirAnchorStateSpec extends TestKit(ActorSystem("test")) with
|
||||
bob2blockchain.expectMsgType[WatchConfirmed]
|
||||
bob2blockchain.expectMsgType[WatchSpent]
|
||||
awaitCond(bob.stateName == OPEN_WAITING_THEIRANCHOR)
|
||||
test((bob, alice2bob, bob2alice, bob2blockchain))
|
||||
test((alice, bob, alice2bob, bob2alice, bob2blockchain))
|
||||
}
|
||||
|
||||
override def afterAll {
|
||||
TestKit.shutdownActorSystem(system)
|
||||
}
|
||||
|
||||
test("recv open_complete") { case (bob, alice2bob, bob2alice, bob2blockchain) =>
|
||||
test("recv open_complete") { case (_, bob, alice2bob, bob2alice, bob2blockchain) =>
|
||||
within(30 seconds) {
|
||||
val msg = alice2bob.expectMsgType[open_complete]
|
||||
alice2bob.forward(bob)
|
||||
@ -55,7 +55,7 @@ class OpenWaitingTheirAnchorStateSpec extends TestKit(ActorSystem("test")) with
|
||||
}
|
||||
}
|
||||
|
||||
test("recv BITCOIN_ANCHOR_DEPTHOK") { case (bob, alice2bob, bob2alice, bob2blockchain) =>
|
||||
test("recv BITCOIN_ANCHOR_DEPTHOK") { case (_, bob, alice2bob, bob2alice, bob2blockchain) =>
|
||||
within(30 seconds) {
|
||||
bob ! BITCOIN_ANCHOR_DEPTHOK
|
||||
awaitCond(bob.stateName == OPEN_WAIT_FOR_COMPLETE_THEIRANCHOR)
|
||||
@ -64,7 +64,7 @@ class OpenWaitingTheirAnchorStateSpec extends TestKit(ActorSystem("test")) with
|
||||
}
|
||||
}
|
||||
|
||||
test("recv BITCOIN_ANCHOR_TIMEOUT") { case (bob, alice2bob, bob2alice, bob2blockchain) =>
|
||||
test("recv BITCOIN_ANCHOR_TIMEOUT") { case (_, bob, alice2bob, bob2alice, bob2blockchain) =>
|
||||
within(30 seconds) {
|
||||
bob ! BITCOIN_ANCHOR_TIMEOUT
|
||||
bob2alice.expectMsgType[error]
|
||||
@ -72,23 +72,25 @@ class OpenWaitingTheirAnchorStateSpec extends TestKit(ActorSystem("test")) with
|
||||
}
|
||||
}
|
||||
|
||||
test("recv BITCOIN_ANCHOR_SPENT") { case (bob, alice2bob, bob2alice, bob2blockchain) =>
|
||||
test("recv BITCOIN_ANCHOR_SPENT") { case (alice, bob, alice2bob, bob2alice, bob2blockchain) =>
|
||||
within(30 seconds) {
|
||||
// this is the fully signed tx that alice could decide to publish
|
||||
val tx = alice.stateData.asInstanceOf[DATA_OPEN_WAITING].commitments.ourCommit.publishableTx
|
||||
// we have nothing at stake so we don't do anything with the tx
|
||||
bob ! (BITCOIN_ANCHOR_SPENT, null)
|
||||
bob ! (BITCOIN_ANCHOR_SPENT, tx)
|
||||
bob2alice.expectMsgType[error]
|
||||
awaitCond(bob.stateName == CLOSED)
|
||||
}
|
||||
}
|
||||
|
||||
test("recv error") { case (bob, alice2bob, bob2alice, bob2blockchain) =>
|
||||
test("recv error") { case (_, bob, alice2bob, bob2alice, bob2blockchain) =>
|
||||
within(30 seconds) {
|
||||
bob ! error(Some("oops"))
|
||||
awaitCond(bob.stateName == CLOSED)
|
||||
}
|
||||
}
|
||||
|
||||
test("recv CMD_CLOSE") { case (bob, alice2bob, bob2alice, bob2blockchain) =>
|
||||
test("recv CMD_CLOSE") { case (_, bob, alice2bob, bob2alice, bob2blockchain) =>
|
||||
within(30 seconds) {
|
||||
bob ! CMD_CLOSE(None)
|
||||
awaitCond(bob.stateName == CLOSED)
|
||||
|
@ -19,7 +19,7 @@ import scala.concurrent.duration._
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class OpenWaitForCompleteTheirAnchorStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteLike with BeforeAndAfterAll {
|
||||
|
||||
type FixtureParam = Tuple4[TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe]
|
||||
type FixtureParam = Tuple5[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe]
|
||||
|
||||
override def withFixture(test: OneArgTest) = {
|
||||
val alice2bob = TestProbe()
|
||||
@ -43,14 +43,14 @@ class OpenWaitForCompleteTheirAnchorStateSpec extends TestKit(ActorSystem("test"
|
||||
bob2alice.expectMsgType[open_complete]
|
||||
bob2alice.forward(alice)
|
||||
awaitCond(bob.stateName == OPEN_WAIT_FOR_COMPLETE_THEIRANCHOR)
|
||||
test((bob, alice2bob, bob2alice, bob2blockchain))
|
||||
test((alice, bob, alice2bob, bob2alice, bob2blockchain))
|
||||
}
|
||||
|
||||
override def afterAll {
|
||||
TestKit.shutdownActorSystem(system)
|
||||
}
|
||||
|
||||
test("recv open_complete") { case (bob, alice2bob, bob2alice, bob2blockchain) =>
|
||||
test("recv open_complete") { case (_, bob, alice2bob, bob2alice, bob2blockchain) =>
|
||||
within(30 seconds) {
|
||||
alice2bob.expectMsgType[open_complete]
|
||||
alice2bob.forward(bob)
|
||||
@ -58,23 +58,25 @@ class OpenWaitForCompleteTheirAnchorStateSpec extends TestKit(ActorSystem("test"
|
||||
}
|
||||
}
|
||||
|
||||
test("recv BITCOIN_ANCHOR_SPENT") { case (bob, alice2bob, bob2alice, bob2blockchain) =>
|
||||
test("recv BITCOIN_ANCHOR_SPENT") { case (alice, bob, alice2bob, bob2alice, bob2blockchain) =>
|
||||
within(30 seconds) {
|
||||
// this is the fully signed tx that alice could decide to publish
|
||||
val tx = alice.stateData.asInstanceOf[DATA_OPEN_WAITING].commitments.ourCommit.publishableTx
|
||||
// we have nothing at stake so we don't do anything with the tx
|
||||
bob ! (BITCOIN_ANCHOR_SPENT, null)
|
||||
bob ! (BITCOIN_ANCHOR_SPENT, tx)
|
||||
bob2alice.expectMsgType[error]
|
||||
awaitCond(bob.stateName == CLOSED)
|
||||
}
|
||||
}
|
||||
|
||||
test("recv CMD_CLOSE") { case (bob, alice2bob, bob2alice, bob2blockchain) =>
|
||||
test("recv CMD_CLOSE") { case (_, bob, alice2bob, bob2alice, bob2blockchain) =>
|
||||
within(30 seconds) {
|
||||
bob ! CMD_CLOSE(None)
|
||||
awaitCond(bob.stateName == CLOSED)
|
||||
}
|
||||
}
|
||||
|
||||
test("recv error") { case (bob, alice2bob, bob2alice, bob2blockchain) =>
|
||||
test("recv error") { case (_, bob, alice2bob, bob2alice, bob2blockchain) =>
|
||||
within(30 seconds) {
|
||||
bob ! error(Some("oops"))
|
||||
awaitCond(bob.stateName == CLOSED)
|
||||
|
@ -3,12 +3,12 @@ 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.bitcoin.{Crypto, Satoshi, Script, ScriptFlags, Transaction, TxOut}
|
||||
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 fr.acinq.eclair.channel.{BITCOIN_ANCHOR_DEPTHOK, Data, State, _}
|
||||
import lightning._
|
||||
import lightning.locktime.Locktime.Blocks
|
||||
import org.junit.runner.RunWith
|
||||
@ -16,6 +16,7 @@ import org.scalatest.junit.JUnitRunner
|
||||
import org.scalatest.{BeforeAndAfterAll, fixture}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Random
|
||||
|
||||
/**
|
||||
* Created by PM on 05/07/2016.
|
||||
@ -69,13 +70,38 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
|
||||
TestKit.shutdownActorSystem(system)
|
||||
}
|
||||
|
||||
def addHtlc(amountMsat: Int, s: TestFSMRef[State, Data, Channel], r: TestFSMRef[State, Data, Channel], s2r: TestProbe, r2s: TestProbe): (rval, update_add_htlc) = {
|
||||
val rand = new Random()
|
||||
val R = rval(rand.nextInt(), rand.nextInt(), rand.nextInt(), rand.nextInt())
|
||||
val H: sha256_hash = Crypto.sha256(R)
|
||||
val sender = TestProbe()
|
||||
sender.send(s, CMD_ADD_HTLC(amountMsat, H, locktime(Blocks(3))))
|
||||
sender.expectMsg("ok")
|
||||
val htlc = s2r.expectMsgType[update_add_htlc]
|
||||
s2r.forward(r)
|
||||
awaitCond(r.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed.contains(htlc))
|
||||
(R, htlc)
|
||||
}
|
||||
|
||||
def sign(s: TestFSMRef[State, Data, Channel], r: TestFSMRef[State, Data, Channel], s2r: TestProbe, r2s: TestProbe) = {
|
||||
val sender = TestProbe()
|
||||
val rCommitIndex = r.stateData.asInstanceOf[HasCommitments].commitments.ourCommit.index
|
||||
sender.send(s, CMD_SIGN)
|
||||
sender.expectMsg("ok")
|
||||
s2r.expectMsgType[update_commit]
|
||||
s2r.forward(r)
|
||||
r2s.expectMsgType[update_revocation]
|
||||
r2s.forward(s)
|
||||
awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.ourCommit.index == rCommitIndex + 1)
|
||||
}
|
||||
|
||||
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)
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCurrentHtlcId == 1 && alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.proposed == htlc :: Nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,11 +187,7 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
|
||||
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]
|
||||
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
|
||||
sender.send(alice, CMD_SIGN)
|
||||
sender.expectMsg("ok")
|
||||
alice2bob.expectMsgType[update_commit]
|
||||
@ -185,11 +207,7 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
|
||||
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]
|
||||
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.theirNextCommitInfo.isRight)
|
||||
sender.send(alice, CMD_SIGN)
|
||||
sender.expectMsg("ok")
|
||||
@ -203,14 +221,8 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
|
||||
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 (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
|
||||
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
|
||||
|
||||
sender.send(alice, CMD_SIGN)
|
||||
@ -225,6 +237,33 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
|
||||
}
|
||||
}
|
||||
|
||||
test("recv update_commit (two htlcs with same r)") { 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(5000000, h, locktime(Blocks(3))))
|
||||
sender.expectMsg("ok")
|
||||
val htlc1 = alice2bob.expectMsgType[update_add_htlc]
|
||||
alice2bob.forward(bob)
|
||||
|
||||
sender.send(alice, CMD_ADD_HTLC(5000000, h, locktime(Blocks(3))))
|
||||
sender.expectMsg("ok")
|
||||
val htlc2 = alice2bob.expectMsgType[update_add_htlc]
|
||||
alice2bob.forward(bob)
|
||||
|
||||
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == htlc1 :: htlc2 :: Nil)
|
||||
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
|
||||
|
||||
sign(alice, bob, alice2bob, bob2alice)
|
||||
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.spec.htlcs.exists(h => h.id == htlc1.id && h.direction == IN))
|
||||
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.spec.htlcs.exists(h => h.id == htlc2.id && h.direction == IN))
|
||||
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.spec.amount_us_msat == initialState.commitments.ourCommit.spec.amount_us_msat)
|
||||
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx.txOut.count(_.amount == Satoshi(5000)) == 2)
|
||||
}
|
||||
}
|
||||
|
||||
ignore("recv update_commit (no changes)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain) =>
|
||||
within(30 seconds) {
|
||||
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
|
||||
@ -241,14 +280,7 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
|
||||
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 (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
|
||||
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
|
||||
|
||||
// actual test begins
|
||||
@ -263,14 +295,7 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
|
||||
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 (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
|
||||
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
|
||||
|
||||
sender.send(alice, CMD_SIGN)
|
||||
@ -290,14 +315,7 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
|
||||
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)
|
||||
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
|
||||
|
||||
sender.send(alice, CMD_SIGN)
|
||||
sender.expectMsg("ok")
|
||||
@ -330,22 +348,8 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
|
||||
test("recv CMD_FULFILL_HTLC") { case (alice, bob, alice2bob, bob2alice, _, _) =>
|
||||
within(30 seconds) {
|
||||
val sender = TestProbe()
|
||||
val r: rval = rval(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]
|
||||
bob2alice.forward(alice)
|
||||
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == Nil && bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.acked == htlc :: Nil)
|
||||
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
|
||||
sign(alice, bob, alice2bob, bob2alice)
|
||||
|
||||
// actual test begins
|
||||
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
|
||||
@ -371,22 +375,8 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
|
||||
test("recv CMD_FULFILL_HTLC (invalid preimage)") { case (alice, bob, alice2bob, bob2alice, _, _) =>
|
||||
within(30 seconds) {
|
||||
val sender = TestProbe()
|
||||
val r: rval = rval(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]
|
||||
bob2alice.forward(alice)
|
||||
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == Nil && bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.acked == htlc :: Nil)
|
||||
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
|
||||
sign(alice, bob, alice2bob, bob2alice)
|
||||
|
||||
// actual test begins
|
||||
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
|
||||
@ -399,22 +389,8 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
|
||||
test("recv update_fulfill_htlc") { case (alice, bob, alice2bob, bob2alice, _, _) =>
|
||||
within(30 seconds) {
|
||||
val sender = TestProbe()
|
||||
val r: rval = rval(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(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.proposed == htlc :: Nil)
|
||||
|
||||
sender.send(alice, CMD_SIGN)
|
||||
sender.expectMsg("ok")
|
||||
alice2bob.expectMsgType[update_commit]
|
||||
alice2bob.forward(bob)
|
||||
bob2alice.expectMsgType[update_revocation]
|
||||
bob2alice.forward(alice)
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.proposed == Nil && alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.acked == htlc :: Nil)
|
||||
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
|
||||
sign(alice, bob, alice2bob, bob2alice)
|
||||
|
||||
sender.send(bob, CMD_FULFILL_HTLC(htlc.id, r))
|
||||
sender.expectMsg("ok")
|
||||
@ -443,22 +419,8 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
|
||||
within(30 seconds) {
|
||||
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
|
||||
val sender = TestProbe()
|
||||
val r: rval = rval(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(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.proposed == htlc :: Nil)
|
||||
|
||||
sender.send(alice, CMD_SIGN)
|
||||
sender.expectMsg("ok")
|
||||
alice2bob.expectMsgType[update_commit]
|
||||
alice2bob.forward(bob)
|
||||
bob2alice.expectMsgType[update_revocation]
|
||||
bob2alice.forward(alice)
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.proposed == Nil && alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.acked == htlc :: Nil)
|
||||
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
|
||||
sign(alice, bob, alice2bob, bob2alice)
|
||||
|
||||
// actual test begins
|
||||
sender.send(alice, update_fulfill_htlc(42, rval(0, 0, 0, 0)))
|
||||
@ -472,22 +434,8 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
|
||||
test("recv CMD_FAIL_HTLC") { case (alice, bob, alice2bob, bob2alice, _, _) =>
|
||||
within(30 seconds) {
|
||||
val sender = TestProbe()
|
||||
val r: rval = rval(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]
|
||||
bob2alice.forward(alice)
|
||||
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == Nil && bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.acked == htlc :: Nil)
|
||||
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
|
||||
sign(alice, bob, alice2bob, bob2alice)
|
||||
|
||||
// actual test begins
|
||||
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
|
||||
@ -513,22 +461,8 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
|
||||
test("recv update_fail_htlc") { case (alice, bob, alice2bob, bob2alice, _, _) =>
|
||||
within(30 seconds) {
|
||||
val sender = TestProbe()
|
||||
val r: rval = rval(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(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.proposed == htlc :: Nil)
|
||||
|
||||
sender.send(alice, CMD_SIGN)
|
||||
sender.expectMsg("ok")
|
||||
alice2bob.expectMsgType[update_commit]
|
||||
alice2bob.forward(bob)
|
||||
bob2alice.expectMsgType[update_revocation]
|
||||
bob2alice.forward(alice)
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.proposed == Nil && alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.acked == htlc :: Nil)
|
||||
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
|
||||
sign(alice, bob, alice2bob, bob2alice)
|
||||
|
||||
sender.send(bob, CMD_FAIL_HTLC(htlc.id, "some reason"))
|
||||
sender.expectMsg("ok")
|
||||
@ -587,14 +521,10 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
|
||||
}
|
||||
}
|
||||
|
||||
ignore("recv close_clearing (with unacked received htlcs)") { case (alice, _, alice2bob, _, alice2blockchain, _) =>
|
||||
ignore("recv close_clearing (with unacked received htlcs)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
|
||||
within(30 seconds) {
|
||||
val sender = TestProbe()
|
||||
val r: rval = rval(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]
|
||||
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
|
||||
// actual test begins
|
||||
sender.send(alice, close_clearing(ByteString.EMPTY))
|
||||
alice2bob.expectMsgType[close_clearing]
|
||||
@ -602,14 +532,10 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
|
||||
}
|
||||
}
|
||||
|
||||
ignore("recv close_clearing (with unacked sent htlcs)") { case (alice, _, alice2bob, _, alice2blockchain, _) =>
|
||||
ignore("recv close_clearing (with unacked sent htlcs)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
|
||||
within(30 seconds) {
|
||||
val sender = TestProbe()
|
||||
val r: rval = rval(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]
|
||||
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
|
||||
// actual test begins
|
||||
sender.send(alice, close_clearing(ByteString.EMPTY))
|
||||
alice2bob.expectMsgType[close_clearing]
|
||||
@ -620,21 +546,8 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
|
||||
test("recv close_clearing (with signed htlcs)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
|
||||
within(30 seconds) {
|
||||
val sender = TestProbe()
|
||||
val r: rval = rval(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(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.proposed == htlc :: Nil)
|
||||
|
||||
sender.send(alice, CMD_SIGN)
|
||||
sender.expectMsg("ok")
|
||||
alice2bob.expectMsgType[update_commit]
|
||||
alice2bob.forward(bob)
|
||||
bob2alice.expectMsgType[update_revocation]
|
||||
bob2alice.forward(alice)
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.proposed == Nil && alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.acked == htlc :: Nil)
|
||||
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
|
||||
sign(alice, bob, alice2bob, bob2alice)
|
||||
|
||||
// actual test begins
|
||||
sender.send(alice, close_clearing(ByteString.EMPTY))
|
||||
@ -643,12 +556,54 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
|
||||
}
|
||||
}
|
||||
|
||||
ignore("recv BITCOIN_ANCHOR_SPENT") { case (alice, _, alice2bob, bob2alice, alice2blockchain, _) =>
|
||||
test("recv BITCOIN_ANCHOR_SPENT (revoked commit)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
|
||||
within(30 seconds) {
|
||||
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
|
||||
alice ! (BITCOIN_ANCHOR_SPENT, null)
|
||||
val sender = TestProbe()
|
||||
|
||||
// alice sends 300 000 sat and bob fulfills
|
||||
// we reuse the same r (it doesn't matter here)
|
||||
val (r, htlc) = addHtlc(300000000, alice, bob, alice2bob, bob2alice)
|
||||
sign(alice, bob, alice2bob, bob2alice)
|
||||
|
||||
sender.send(bob, CMD_FULFILL_HTLC(1, r))
|
||||
sender.expectMsg("ok")
|
||||
val fulfill = bob2alice.expectMsgType[update_fulfill_htlc]
|
||||
bob2alice.forward(alice)
|
||||
|
||||
sign(bob, alice, bob2alice, alice2bob)
|
||||
|
||||
// at this point we have :
|
||||
// alice = 700 000
|
||||
// bob = 300 000
|
||||
def send(): Transaction = {
|
||||
// alice sends 1 000 sat
|
||||
// we reuse the same r (it doesn't matter here)
|
||||
val (r, htlc) = addHtlc(1000000, alice, bob, alice2bob, bob2alice)
|
||||
sign(alice, bob, alice2bob, bob2alice)
|
||||
|
||||
bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
|
||||
}
|
||||
val txs = for (i <- 0 until 10) yield send()
|
||||
// bob now has 10 spendable tx, 9 of them being revoked
|
||||
|
||||
// let's say that bob published this tx
|
||||
val revokedTx = txs(3)
|
||||
// channel state for this revoked tx is as follows:
|
||||
// alice = 696 000
|
||||
// bob = 300 000
|
||||
// a->b = 4 000
|
||||
alice ! (BITCOIN_ANCHOR_SPENT, revokedTx)
|
||||
alice2bob.expectMsgType[error]
|
||||
// TODO
|
||||
val punishTx = alice2blockchain.expectMsgType[Publish].tx
|
||||
alice2blockchain.expectMsgType[WatchConfirmed]
|
||||
awaitCond(alice.stateName == CLOSING)
|
||||
Transaction.correctlySpends(punishTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
// two main outputs + 4 htlc
|
||||
assert(revokedTx.txOut.size == 6)
|
||||
// the punishment tx consumes all output but ours (which already goes to our final key)
|
||||
assert(punishTx.txIn.size == 5)
|
||||
// TODO : when changefee is implemented we should set fee = 0 and check against 304 000
|
||||
assert(punishTx.txOut == Seq(TxOut(Satoshi(301670), Script.write(Scripts.pay2wpkh(Alice.finalPubKey)))))
|
||||
}
|
||||
}
|
||||
|
||||
|
2
pom.xml
2
pom.xml
@ -45,7 +45,7 @@
|
||||
<maven.compiler.target>1.7</maven.compiler.target>
|
||||
<scala.version>2.11.8</scala.version>
|
||||
<scala.version.short>2.11</scala.version.short>
|
||||
<akka.version>2.4.6</akka.version>
|
||||
<akka.version>2.4.8</akka.version>
|
||||
<bitcoinlib.version>0.9.6-RC2</bitcoinlib.version>
|
||||
<acinqtools.version>1.2</acinqtools.version>
|
||||
</properties>
|
||||
|
Loading…
Reference in New Issue
Block a user