1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-03-27 02:37:06 +01:00

create a specific data stucture for commitment data

* create a specific data stucture for commitment data

* remove lenses

* move makeFinalTx to Helpers

* remove unused type, clean up CLOSING data

* code cleanup

* check revocation preimage

* use a "either" type to represent their next commit information

We either have their next revocation hash (most of the time), or their next commit
tx (when we've signed and are waiting for their revocation message)
This commit is contained in:
Fabrice Drouin 2016-06-07 14:50:10 +02:00 committed by Pierre-Marie Padiou
parent 6d2ee58158
commit d07a201353
5 changed files with 346 additions and 473 deletions

View file

@ -73,7 +73,8 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
when(OPEN_WAIT_FOR_ANCHOR) {
case Event(open_anchor(anchorTxHash, anchorOutputIndex, anchorAmount), DATA_OPEN_WAIT_FOR_ANCHOR(ourParams, theirParams, theirRevocationHash, theirNextRevocationHash)) =>
val anchorTxid = anchorTxHash.reverse //see https://github.com/ElementsProject/lightning/issues/17
val anchorOutput = TxOut(Satoshi(anchorAmount), publicKeyScript = Scripts.anchorPubkeyScript(ourParams.commitPubKey, theirParams.commitPubKey))
val anchorOutput = TxOut(Satoshi(anchorAmount), publicKeyScript = Scripts.anchorPubkeyScript(ourParams.commitPubKey, theirParams.commitPubKey))
// they fund the channel with their anchor tx, so the money is theirs
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)
@ -89,7 +90,11 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
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
goto(OPEN_WAITING_THEIRANCHOR) using DATA_OPEN_WAITING(ourParams, theirParams, ShaChain.init, OurCommit(0, ourSpec, ourTx), TheirCommit(0, theirSpec, theirRevocationHash), theirNextRevocationHash, None, anchorOutput)
val commitments = Commitments(ourParams, theirParams,
OurCommit(0, ourSpec, ourTx), TheirCommit(0, theirSpec, theirRevocationHash),
OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil),
Right(theirNextRevocationHash), anchorOutput)
goto(OPEN_WAITING_THEIRANCHOR) using DATA_OPEN_WAITING(commitments, ShaChain.init, None)
case Event(CMD_CLOSE(_), _) => goto(CLOSED)
}
@ -115,19 +120,23 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
blockchain ! WatchConfirmed(self, anchorTx.txid, ourParams.minDepth, BITCOIN_ANCHOR_DEPTHOK)
blockchain ! WatchSpent(self, anchorTx.txid, anchorOutputIndex, 0, BITCOIN_ANCHOR_SPENT)
blockchain ! Publish(anchorTx)
goto(OPEN_WAITING_OURANCHOR) using DATA_OPEN_WAITING(ourParams, theirParams, ShaChain.init, OurCommit(0, ourSpec, signedTx), theirCommitment, theirNextRevocationHash, None, anchorOutput)
val commitments = Commitments(ourParams, theirParams,
OurCommit(0, ourSpec, signedTx), theirCommitment,
OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil),
Right(theirNextRevocationHash), anchorOutput)
goto(OPEN_WAITING_OURANCHOR) using DATA_OPEN_WAITING(commitments, ShaChain.init, None)
}
case Event(CMD_CLOSE(_), _) => goto(CLOSED)
}
when(OPEN_WAITING_THEIRANCHOR) {
case Event(BITCOIN_ANCHOR_DEPTHOK, d@DATA_OPEN_WAITING(ourParams, theirParams, shaChain, ourCommit, theirCommit, theirNextRevocationHash, deferred, anchorOutput)) =>
blockchain ! WatchLost(self, d.asInstanceOf[CurrentCommitment].anchorId, ourParams.minDepth, BITCOIN_ANCHOR_LOST)
case Event(BITCOIN_ANCHOR_DEPTHOK, d@DATA_OPEN_WAITING(commitments, shaChain, deferred)) =>
blockchain ! WatchLost(self, commitments.anchorId, commitments.ourParams.minDepth, BITCOIN_ANCHOR_LOST)
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(ourParams, theirParams, shaChain, 0, ourCommit, theirCommit, OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil), Some(theirNextRevocationHash), anchorOutput, None)
goto(OPEN_WAIT_FOR_COMPLETE_THEIRANCHOR) using DATA_NORMAL(commitments, shaChain, 0, None)
case Event(msg@open_complete(blockId_opt), d: DATA_OPEN_WAITING) =>
log.info(s"received their open_complete, deferring message")
@ -147,25 +156,25 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
them ! handle_cmd_close(cmd, d.ourParams, d.theirParams, d.commitment)
goto(WAIT_FOR_CLOSE_COMPLETE)*/
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: CurrentCommitment) if (isTheirCommit(tx, d.ourParams, d.theirParams, d.theirCommit)) =>
them ! handle_theircommit(tx, d.ourParams, d.theirParams, d.shaChain, d.theirCommit)
goto(CLOSING) using DATA_CLOSING(d.ourParams, d.theirParams, d.shaChain, d.ourCommit, d.theirCommit, theirCommitPublished = Some(tx))
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_OPEN_WAITING) if (isTheirCommit(tx, d.commitments.ourParams, d.commitments.theirParams, d.commitments.theirCommit)) =>
them ! handle_theircommit(tx, d.commitments.ourParams, d.commitments.theirParams, d.shaChain, d.commitments.theirCommit)
goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, theirCommitPublished = Some(tx))
case Event(BITCOIN_ANCHOR_SPENT, _) =>
goto(ERR_INFORMATION_LEAK)
case Event(pkt: error, d: CurrentCommitment) =>
publish_ourcommit(d.ourCommit)
goto(CLOSING) using DATA_CLOSING(d.ourParams, d.theirParams, d.shaChain, d.ourCommit, d.theirCommit, ourCommitPublished = Some(d.ourCommit.publishableTx))
case Event(pkt: error, d@DATA_OPEN_WAITING(commitments, _, _)) =>
publish_ourcommit(commitments.ourCommit)
goto(CLOSING) using DATA_CLOSING(commitments, d.shaChain, ourCommitPublished = Some(commitments.ourCommit.publishableTx))
}
when(OPEN_WAITING_OURANCHOR) {
case Event(BITCOIN_ANCHOR_DEPTHOK, d@DATA_OPEN_WAITING(ourParams, theirParams, shaChain, ourCommit, theirCommit, theirNextRevocationHash, deferred, anchorOutput)) =>
blockchain ! WatchLost(self, d.asInstanceOf[CurrentCommitment].anchorId, ourParams.minDepth, BITCOIN_ANCHOR_LOST)
case Event(BITCOIN_ANCHOR_DEPTHOK, d@DATA_OPEN_WAITING(commitments, shaChain, deferred)) =>
blockchain ! WatchLost(self, commitments.anchorId, commitments.ourParams.minDepth, BITCOIN_ANCHOR_LOST)
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(ourParams, theirParams, shaChain, 0, ourCommit, theirCommit, OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil), Some(theirNextRevocationHash), anchorOutput, None)
goto(OPEN_WAIT_FOR_COMPLETE_OURANCHOR) using DATA_NORMAL(commitments, shaChain, 0, None)
case Event(msg@open_complete(blockId_opt), d: DATA_OPEN_WAITING) =>
log.info(s"received their open_complete, deferring message")
@ -181,21 +190,21 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
them ! handle_cmd_close(cmd, d.ourParams, d.theirParams, d.commitment)
goto(WAIT_FOR_CLOSE_COMPLETE)*/
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: CurrentCommitment) if (isTheirCommit(tx, d.ourParams, d.theirParams, d.theirCommit)) =>
them ! handle_theircommit(tx, d.ourParams, d.theirParams, d.shaChain, d.theirCommit)
goto(CLOSING) using DATA_CLOSING(d.ourParams, d.theirParams, d.shaChain, d.ourCommit, d.theirCommit, theirCommitPublished = Some(tx))
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_OPEN_WAITING) if (isTheirCommit(tx, d.commitments.ourParams, d.commitments.theirParams, d.commitments.theirCommit)) =>
them ! handle_theircommit(tx, d.commitments.ourParams, d.commitments.theirParams, d.shaChain, d.commitments.theirCommit)
goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, theirCommitPublished = Some(tx))
case Event((BITCOIN_ANCHOR_SPENT, _), _) =>
goto(ERR_INFORMATION_LEAK)
case Event(pkt: error, d: CurrentCommitment) =>
publish_ourcommit(d.ourCommit)
goto(CLOSING) using DATA_CLOSING(d.ourParams, d.theirParams, d.shaChain, d.ourCommit, d.theirCommit, ourCommitPublished = Some(d.ourCommit.publishableTx))
case Event(pkt: error, d: DATA_OPEN_WAITING) =>
publish_ourcommit(d.commitments.ourCommit)
goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
}
when(OPEN_WAIT_FOR_COMPLETE_THEIRANCHOR) {
case Event(open_complete(blockid_opt), d: CurrentCommitment) =>
Register.create_alias(theirNodeId, d.anchorId)
case Event(open_complete(blockid_opt), d: DATA_NORMAL) =>
Register.create_alias(theirNodeId, d.commitments.anchorId)
goto(NORMAL)
/*case Event(pkt: close_channel, d: CurrentCommitment) =>
@ -208,22 +217,22 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
them ! handle_cmd_close(cmd, d.ourParams, d.theirParams, d.commitment)
goto(WAIT_FOR_CLOSE_COMPLETE)*/
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: CurrentCommitment) if (isTheirCommit(tx, d.ourParams, d.theirParams, d.theirCommit)) =>
them ! handle_theircommit(tx, d.ourParams, d.theirParams, d.shaChain, d.theirCommit)
goto(CLOSING) using DATA_CLOSING(d.ourParams, d.theirParams, d.shaChain, d.ourCommit, d.theirCommit, theirCommitPublished = Some(tx))
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_NORMAL) if (isTheirCommit(tx, d.commitments.ourParams, d.commitments.theirParams, d.commitments.theirCommit)) =>
them ! handle_theircommit(tx, d.commitments.ourParams, d.commitments.theirParams, d.shaChain, d.commitments.theirCommit)
goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, theirCommitPublished = Some(tx))
case Event((BITCOIN_ANCHOR_SPENT, _), _) =>
goto(ERR_INFORMATION_LEAK)
case Event(pkt: error, d: CurrentCommitment) =>
publish_ourcommit(d.ourCommit)
goto(CLOSING) using DATA_CLOSING(d.ourParams, d.theirParams, d.shaChain, d.ourCommit, d.theirCommit, ourCommitPublished = Some(d.ourCommit.publishableTx))
case Event(pkt: error, d: DATA_NORMAL) =>
publish_ourcommit(d.commitments.ourCommit)
goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
}
when(OPEN_WAIT_FOR_COMPLETE_OURANCHOR) {
case Event(open_complete(blockid_opt), d: CurrentCommitment) =>
Register.create_alias(theirNodeId, d.anchorId)
case Event(open_complete(blockid_opt), d: DATA_NORMAL) =>
Register.create_alias(theirNodeId, d.commitments.anchorId)
goto(NORMAL)
/*case Event(pkt: close_channel, d: CurrentCommitment) =>
@ -236,16 +245,16 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
them ! handle_cmd_close(cmd, d.ourParams, d.theirParams, d.commitment)
goto(WAIT_FOR_CLOSE_COMPLETE)*/
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: CurrentCommitment) if (isTheirCommit(tx, d.ourParams, d.theirParams, d.theirCommit)) =>
them ! handle_theircommit(tx, d.ourParams, d.theirParams, d.shaChain, d.theirCommit)
goto(CLOSING) using DATA_CLOSING(d.ourParams, d.theirParams, d.shaChain, d.ourCommit, d.theirCommit, theirCommitPublished = Some(tx))
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_NORMAL) if (isTheirCommit(tx, d.commitments.ourParams, d.commitments.theirParams, d.commitments.theirCommit)) =>
them ! handle_theircommit(tx, d.commitments.ourParams, d.commitments.theirParams, d.shaChain, d.commitments.theirCommit)
goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, theirCommitPublished = Some(tx))
case Event((BITCOIN_ANCHOR_SPENT, _), _) =>
goto(ERR_INFORMATION_LEAK)
case Event(pkt: error, d: CurrentCommitment) =>
publish_ourcommit(d.ourCommit)
goto(CLOSING) using DATA_CLOSING(d.ourParams, d.theirParams, d.shaChain, d.ourCommit, d.theirCommit, ourCommitPublished = Some(d.ourCommit.publishableTx))
case Event(pkt: error, d: DATA_NORMAL) =>
publish_ourcommit(d.commitments.ourCommit)
goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
}
@ -262,122 +271,78 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
when(NORMAL) {
case Event(CMD_ADD_HTLC(amount, rHash, expiry, nodeIds, origin), d@DATA_NORMAL(_, _, _, htlcIdx, _, _, ourChanges, _, _, _, _)) =>
case Event(CMD_ADD_HTLC(amount, rHash, expiry, nodeIds, origin), d@DATA_NORMAL(commitments, _, htlcIdx, _)) =>
// TODO: should we take pending htlcs into account?
// TODO: assert(commitment.state.commit_changes(staged).us.pay_msat >= amount, "insufficient funds!")
// TODO: nodeIds are ignored
val htlc = update_add_htlc(htlcIdx + 1, amount, rHash, expiry, routing(ByteString.EMPTY))
them ! htlc
stay using d.copy(htlcIdx = htlc.id, ourChanges = ourChanges.copy(proposed = ourChanges.proposed :+ htlc))
stay using d.copy(htlcIdx = htlc.id, commitments = commitments.addOurProposal(htlc))
case Event(htlc@update_add_htlc(htlcId, amount, rHash, expiry, nodeIds), d@DATA_NORMAL(_, _, _, _, _, _, _, theirChanges, _, _, _)) =>
case Event(htlc@update_add_htlc(htlcId, amount, rHash, expiry, nodeIds), d@DATA_NORMAL(commitments, _, _, _)) =>
// TODO: should we take pending htlcs into account?
// assert(commitment.state.commit_changes(staged).them.pay_msat >= amount, "insufficient funds!") // TODO : we should fail the channel
// TODO: nodeIds are ignored
stay using d.copy(theirChanges = theirChanges.copy(proposed = theirChanges.proposed :+ htlc))
stay using d.copy(commitments = commitments.addTheirProposal(htlc))
case Event(CMD_FULFILL_HTLC(id, r), d: DATA_NORMAL) =>
d.theirChanges.acked.collectFirst { case u: update_add_htlc if u.id == id => u } match {
case Some(htlc) if htlc.rHash == bin2sha256(Crypto.sha256(r)) =>
val fulfill = update_fulfill_htlc(id, r)
them ! fulfill
stay using d.copy(ourChanges = d.ourChanges.copy(proposed = d.ourChanges.proposed :+ fulfill))
case Some(htlc) => throw new RuntimeException(s"invalid htlc preimage for htlc $id")
case None => throw new RuntimeException(s"unknown htlc id=$id")
}
val (commitments1, fullfill) = Commitments.sendFulfill(d.commitments, CMD_FULFILL_HTLC(id, r))
them ! fullfill
stay using d.copy(commitments = commitments1)
case Event(fulfill@update_fulfill_htlc(id, r), d: DATA_NORMAL) =>
d.ourChanges.acked.collectFirst { case u: update_add_htlc if u.id == id => u } match {
case Some(htlc) if htlc.rHash == bin2sha256(Crypto.sha256(r)) =>
stay using d.copy(theirChanges = d.theirChanges.copy(proposed = d.theirChanges.proposed :+ fulfill))
case Some(htlc) => throw new RuntimeException(s"invalid htlc preimage for htlc $id")
case None => throw new RuntimeException(s"unknown htlc id=$id") // TODO : we should fail the channel
}
stay using d.copy(commitments = Commitments.receiveFulfill(d.commitments, fulfill))
case Event(CMD_FAIL_HTLC(id, reason), d@DATA_NORMAL(_, _, _, _, _, theirCommit, ourChanges, theirChanges, _, _, _)) =>
theirChanges.acked.collectFirst { case u: update_add_htlc if u.id == id => u } match {
case Some(htlc) =>
val fail = update_fail_htlc(id, fail_reason(ByteString.copyFromUtf8(reason)))
them ! fail
stay using d.copy(ourChanges = ourChanges.copy(proposed = ourChanges.proposed :+ fail))
case None => throw new RuntimeException(s"unknown htlc id=$id")
}
case Event(CMD_FAIL_HTLC(id, reason), d: DATA_NORMAL) =>
val (commitments1, fail) = Commitments.sendFail(d.commitments, CMD_FAIL_HTLC(id, reason))
them ! fail
stay using d.copy(commitments = commitments1)
case Event(fail@update_fail_htlc(id, reason), d@DATA_NORMAL(_, _, _, _, ourCommit, _, ourChanges, theirChanges, _, _, _)) =>
ourChanges.acked.collectFirst { case u: update_add_htlc if u.id == id => u } match {
case Some(htlc) =>
stay using d.copy(theirChanges = theirChanges.copy(proposed = theirChanges.proposed :+ fail))
case None => throw new RuntimeException(s"unknown htlc id=$id") // TODO : we should fail the channel
}
case Event(fail@update_fail_htlc(id, reason), d: DATA_NORMAL) =>
stay using d.copy(commitments = Commitments.receiveFail(d.commitments, fail))
case Event(CMD_SIGN, d: DATA_NORMAL) if d.theirNextRevocationHash.isEmpty =>
throw new RuntimeException(s"cannot send two update_commit in a row (must wait for revocation)")
case Event(CMD_SIGN, d: DATA_NORMAL) =>
val (commitments1, commit) = Commitments.sendCommit(d.commitments)
them ! commit
stay using d.copy(commitments = commitments1)
case Event(CMD_SIGN, d@DATA_NORMAL(ourParams, theirParams, _, _, ourCommit, theirCommit, ourChanges, theirChanges, Some(theirNextRevocationHash), anchorOutput, _)) =>
// sign all our proposals + their acked proposals
// their commitment now includes all our changes + their acked changes
val spec = reduce(theirCommit.spec, theirChanges.acked, ourChanges.acked ++ ourChanges.signed ++ ourChanges.proposed)
val theirTx = makeTheirTx(ourParams, theirParams, ourCommit.publishableTx.txIn, theirNextRevocationHash, spec)
val ourSig = sign(ourParams, theirParams, anchorOutput.amount.toLong, theirTx)
them ! update_commit(ourSig)
stay using d.copy(theirCommit = TheirCommit(theirCommit.index + 1, spec, theirNextRevocationHash), ourChanges = ourChanges.copy(proposed = Nil, signed = ourChanges.signed ++ ourChanges.proposed), theirNextRevocationHash = None)
case Event(msg@update_commit(theirSig), d@DATA_NORMAL(ourParams, theirParams, shaChain, _, ourCommit, theirCommit, ourChanges, theirChanges, _, anchorOutput, _)) =>
// we've received a signature
// ack all their changes
// our commitment now includes all theirs changes + our acked changes
val spec = reduce(ourCommit.spec, ourChanges.acked, theirChanges.acked ++ theirChanges.proposed)
val ourNextRevocationHash = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, ourCommit.index + 1))
val ourTx = makeOurTx(ourParams, theirParams, ourCommit.publishableTx.txIn, ourNextRevocationHash, spec)
val ourSig = sign(ourParams, theirParams, anchorOutput.amount.toLong, ourTx)
val signedTx = addSigs(ourParams, theirParams, anchorOutput.amount.toLong, ourTx, ourSig, theirSig)
checksig(ourParams, theirParams, anchorOutput, signedTx) match {
case Event(msg@update_commit(theirSig), d: DATA_NORMAL) =>
Try(Commitments.receiveCommit(d.commitments, msg)) match {
case Success((commitments1, revocation)) =>
them ! revocation
stay using d.copy(commitments = commitments1)
case Failure(cause) =>
log.error(cause, "received a bad signature")
them ! error(Some("Bad signature"))
publish_ourcommit(ourCommit)
goto(CLOSING) using DATA_CLOSING(ourParams, theirParams, shaChain, ourCommit, theirCommit, ourCommitPublished = Some(ourCommit.publishableTx))
case Success(_) =>
val ourRevocationPreimage = ShaChain.shaChainFromSeed(ourParams.shaSeed, ourCommit.index)
val ourRevocationHash = Crypto.sha256(ourRevocationPreimage)
val ourNextRevocationHash = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, ourCommit.index + 2))
them ! update_revocation(ourRevocationPreimage, ourNextRevocationHash)
val ourCommit1 = ourCommit.copy(index = ourCommit.index + 1, spec, publishableTx = signedTx)
val theirChanges1 = theirChanges.copy(proposed = Nil, acked = theirChanges.acked ++ theirChanges.proposed)
stay using d.copy(ourCommit = ourCommit1, theirChanges = theirChanges1)
publish_ourcommit(d.commitments.ourCommit)
goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
}
case Event(msg@update_revocation(revocationPreimage, nextRevocationHash), d@DATA_NORMAL(ourParams, theirParams, shaChain, _, ourCommit, theirCommit, ourChanges, theirChanges, _, _, _)) =>
case Event(msg@update_revocation(revocationPreimage, nextRevocationHash), d: DATA_NORMAL) =>
// we received a revocation because we sent a signature
// => all our changes have been acked
//TODO : check rev pre image is valid
stay using d.copy(ourChanges = ourChanges.copy(signed = Nil, acked = ourChanges.acked ++ ourChanges.signed), theirNextRevocationHash = Some(nextRevocationHash))
// TODO: check preimage
stay using d.copy(commitments = Commitments.receiveRevocation(d.commitments, msg))
case Event(theirClearing@close_clearing(theirScriptPubKey), d@DATA_NORMAL(ourParams, theirParams, shaChain, _, ourCommit, theirCommit, ourChanges, theirChanges, _, anchorOutput, 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(ourParams.finalPubKey))
log.info(s"our final tx can be redeemed with ${Base58Check.encode(Base58.Prefix.SecretKeyTestnet, d.ourParams.finalPrivKey)}")
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)}")
them ! close_clearing(ourScriptPubKey)
close_clearing(ourScriptPubKey)
}
if (ourCommit.spec.htlcs.isEmpty && theirCommit.spec.htlcs.isEmpty) {
val commitFee = d.anchorOutput.amount.toLong - d.ourCommit.publishableTx.txOut.map(_.amount.toLong).sum
val closeFee = Satoshi(2 * (commitFee / 4))
val amount_us = Satoshi(ourCommit.spec.amount_us_msat / 1000)
val amount_them = Satoshi(theirCommit.spec.amount_us_msat / 1000)
val finalTx = Scripts.makeFinalTx(ourCommit.publishableTx.txIn, ourClearing.scriptPubkey, theirScriptPubKey, amount_us, amount_them, closeFee)
val ourSig = sign(ourParams, theirParams, anchorOutput.amount.toLong, finalTx)
val ourCloseSig = close_signature(closeFee.toLong, ourSig)
if (commitments.hasNoPendingHtlcs) {
val (finalTx, ourCloseSig) = makeFinalTx(commitments, ourClearing.scriptPubkey, theirScriptPubKey)
them ! ourCloseSig
goto(NEGOCIATING) using DATA_NEGOCIATING(ourParams, theirParams, shaChain, d.htlcIdx, ourCommit, theirCommit, ourChanges, theirChanges, d.theirNextRevocationHash, anchorOutput, ourClearing, theirClearing, ourCloseSig)
goto(NEGOCIATING) using DATA_NEGOCIATING(commitments, d.shaChain, d.htlcIdx, ourClearing, theirClearing, ourCloseSig)
} else {
goto(CLEARING) using DATA_CLEARING(ourParams, theirParams, shaChain, d.htlcIdx, ourCommit, theirCommit, ourChanges, theirChanges, d.theirNextRevocationHash, anchorOutput, ourClearing, theirClearing)
goto(CLEARING) using DATA_CLEARING(commitments, d.shaChain, d.htlcIdx, ourClearing, theirClearing)
}
case Event(CMD_CLOSE(scriptPubKeyOpt), d@DATA_NORMAL(ourParams, _, _, _, _, _, _, _, _, _, None)) =>
case Event(CMD_CLOSE(scriptPubKeyOpt), d: DATA_NORMAL) =>
val ourScriptPubKey: BinaryData = scriptPubKeyOpt.getOrElse {
log.info(s"our final tx can be redeemed with ${Base58Check.encode(Base58.Prefix.SecretKeyTestnet, d.ourParams.finalPrivKey)}")
Script.write(Scripts.pay2pkh(ourParams.finalPubKey))
log.info(s"our final tx can be redeemed with ${Base58Check.encode(Base58.Prefix.SecretKeyTestnet, d.commitments.ourParams.finalPrivKey)}")
Script.write(Scripts.pay2pkh(d.commitments.ourParams.finalPubKey))
}
val ourCloseClearing = close_clearing(ourScriptPubKey)
them ! ourCloseClearing
@ -408,105 +373,53 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
when(CLEARING) {
case Event(CMD_FULFILL_HTLC(id, r), d: DATA_CLEARING) =>
d.theirChanges.acked.collectFirst { case u: update_add_htlc if u.id == id => u } match {
case Some(htlc) if htlc.rHash == bin2sha256(Crypto.sha256(r)) =>
val fulfill = update_fulfill_htlc(id, r)
them ! fulfill
stay using d.copy(ourChanges = d.ourChanges.copy(proposed = d.ourChanges.proposed :+ fulfill))
case Some(htlc) => throw new RuntimeException(s"invalid htlc preimage for htlc $id")
case None => throw new RuntimeException(s"unknown htlc id=$id")
}
val (commitments1, fullfill) = Commitments.sendFulfill(d.commitments, CMD_FULFILL_HTLC(id, r))
them ! fullfill
stay using d.copy(commitments = commitments1)
case Event(fulfill@update_fulfill_htlc(id, r), d: DATA_CLEARING) =>
d.ourChanges.acked.collectFirst { case u: update_add_htlc if u.id == id => u } match {
case Some(htlc) if htlc.rHash == bin2sha256(Crypto.sha256(r)) =>
stay using d.copy(theirChanges = d.theirChanges.copy(proposed = d.theirChanges.proposed :+ fulfill))
case Some(htlc) => throw new RuntimeException(s"invalid htlc preimage for htlc $id")
case None => throw new RuntimeException(s"unknown htlc id=$id") // TODO : we should fail the channel
}
stay using d.copy(commitments = Commitments.receiveFulfill(d.commitments, fulfill))
case Event(CMD_FAIL_HTLC(id, reason), d@DATA_CLEARING(_, _, _, _, _, theirCommit, ourChanges, theirChanges, _, _, _, _)) =>
theirChanges.acked.collectFirst { case u: update_add_htlc if u.id == id => u } match {
case Some(htlc) =>
val fail = update_fail_htlc(id, fail_reason(ByteString.copyFromUtf8(reason)))
them ! fail
stay using d.copy(ourChanges = ourChanges.copy(proposed = ourChanges.proposed :+ fail))
case None => throw new RuntimeException(s"unknown htlc id=$id")
}
case Event(CMD_FAIL_HTLC(id, reason), d: DATA_CLEARING) =>
val (commitments1, fail) = Commitments.sendFail(d.commitments, CMD_FAIL_HTLC(id, reason))
them ! fail
stay using d.copy(commitments = commitments1)
case Event(fail@update_fail_htlc(id, reason), d@DATA_CLEARING(_, _, _, _, ourCommit, _, ourChanges, theirChanges, _, _, _, _)) =>
ourChanges.acked.collectFirst { case u: update_add_htlc if u.id == id => u } match {
case Some(htlc) =>
stay using d.copy(theirChanges = theirChanges.copy(proposed = theirChanges.proposed :+ fail))
case None => throw new RuntimeException(s"unknown htlc id=$id") // TODO : we should fail the channel
}
case Event(fail@update_fail_htlc(id, reason), d: DATA_CLEARING) =>
stay using d.copy(commitments = Commitments.receiveFail(d.commitments, fail))
case Event(CMD_SIGN, d: DATA_CLEARING) if d.theirNextRevocationHash.isEmpty =>
throw new RuntimeException(s"cannot send two update_commit in a row (must wait for revocation)")
case Event(CMD_SIGN, d: DATA_CLEARING) =>
val (commitments1, commit) = Commitments.sendCommit(d.commitments)
them ! commit
stay using d.copy(commitments = commitments1)
case Event(CMD_SIGN, d@DATA_CLEARING(ourParams, theirParams, _, _, ourCommit, theirCommit, ourChanges, theirChanges, Some(theirNextRevocationHash), anchorOutput, _, _)) =>
// sign all our proposals + their acked proposals
// their commitment now includes all our changes + their acked changes
val spec = reduce(theirCommit.spec, theirChanges.acked, ourChanges.acked ++ ourChanges.signed ++ ourChanges.proposed)
val theirTx = makeTheirTx(ourParams, theirParams, ourCommit.publishableTx.txIn, theirNextRevocationHash, spec)
val ourSig = sign(ourParams, theirParams, anchorOutput.amount.toLong, theirTx)
them ! update_commit(ourSig)
stay using d.copy(theirCommit = TheirCommit(theirCommit.index + 1, spec, theirNextRevocationHash), ourChanges = ourChanges.copy(proposed = Nil, signed = ourChanges.signed ++ ourChanges.proposed), theirNextRevocationHash = None)
case Event(msg@update_commit(theirSig), d@DATA_CLEARING(ourParams, theirParams, shaChain, _, ourCommit, theirCommit, ourChanges, theirChanges, _, anchorOutput, _, _)) =>
// we've received a signature
// ack all their changes
// our commitment now includes all theirs changes + our acked changes
val spec = reduce(ourCommit.spec, ourChanges.acked, theirChanges.acked ++ theirChanges.proposed)
val ourNextRevocationHash = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, ourCommit.index + 1))
val ourTx = makeOurTx(ourParams, theirParams, ourCommit.publishableTx.txIn, ourNextRevocationHash, spec)
val ourSig = sign(ourParams, theirParams, anchorOutput.amount.toLong, ourTx)
val signedTx = addSigs(ourParams, theirParams, anchorOutput.amount.toLong, ourTx, ourSig, theirSig)
checksig(ourParams, theirParams, anchorOutput, signedTx) match {
case Event(msg@update_commit(theirSig), d@DATA_CLEARING(commitments, _, _, ourClearing, theirClearing)) =>
Try(Commitments.receiveCommit(d.commitments, msg)) match {
case Success((commitments1, revocation)) =>
them ! revocation
if (commitments1.hasNoPendingHtlcs) {
val (finalTx, ourCloseSig) = makeFinalTx(commitments1, ourClearing.scriptPubkey, theirClearing.scriptPubkey)
them ! ourCloseSig
goto(NEGOCIATING) using DATA_NEGOCIATING(commitments1, d.shaChain, d.htlcIdx, ourClearing, theirClearing, ourCloseSig)
} else {
stay using d.copy(commitments = commitments1)
}
case Failure(cause) =>
log.error(cause, "received a bad signature")
them ! error(Some("Bad signature"))
publish_ourcommit(ourCommit)
goto(CLOSING) using DATA_CLOSING(ourParams, theirParams, shaChain, ourCommit, theirCommit, ourCommitPublished = Some(ourCommit.publishableTx))
case Success(_) =>
val ourRevocationPreimage = ShaChain.shaChainFromSeed(ourParams.shaSeed, ourCommit.index)
val ourRevocationHash = Crypto.sha256(ourRevocationPreimage)
val ourNextRevocationHash = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, ourCommit.index + 2))
them ! update_revocation(ourRevocationPreimage, ourNextRevocationHash)
val ourCommit1 = ourCommit.copy(index = ourCommit.index + 1, spec = spec, publishableTx = signedTx)
val theirChanges1 = theirChanges.copy(proposed = Nil, acked = theirChanges.acked ++ theirChanges.proposed)
if (ourCommit1.spec.htlcs.isEmpty && theirCommit.spec.htlcs.isEmpty) {
val commitFee = d.anchorOutput.amount.toLong - d.ourCommit.publishableTx.txOut.map(_.amount.toLong).sum
val closeFee = Satoshi(2 * (commitFee / 4))
val amount_us = Satoshi(ourCommit1.spec.amount_us_msat / 1000)
val amount_them = Satoshi(theirCommit.spec.amount_us_msat / 1000)
val finalTx = Scripts.makeFinalTx(ourCommit.publishableTx.txIn, d.ourClearing.scriptPubkey, d.theirClearing.scriptPubkey, amount_us, amount_them, closeFee)
val ourSig = sign(ourParams, theirParams, anchorOutput.amount.toLong, finalTx)
val ourCloseSig = close_signature(closeFee.toLong, ourSig)
them ! ourCloseSig
goto(NEGOCIATING) using DATA_NEGOCIATING(ourParams, theirParams, shaChain, d.htlcIdx, ourCommit1, theirCommit, ourChanges, theirChanges1, d.theirNextRevocationHash, anchorOutput, d.ourClearing, d.theirClearing, ourCloseSig)
} else {
stay using d.copy(ourCommit = ourCommit1, theirChanges = theirChanges1)
}
publish_ourcommit(d.commitments.ourCommit)
goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
}
case Event(msg@update_revocation(revocationPreimage, nextRevocationHash), d@DATA_CLEARING(ourParams, theirParams, shaChain, _, ourCommit, theirCommit, ourChanges, theirChanges, _, _, _, _)) =>
// we received a revocation because we sent a signature
// => all our changes have been acked
//TODO : check rev pre image is valid
val ourChanges1 = ourChanges.copy(signed = Nil, acked = ourChanges.acked ++ ourChanges.signed)
if (ourCommit.spec.htlcs.isEmpty && theirCommit.spec.htlcs.isEmpty) {
val commitFee = d.anchorOutput.amount.toLong - d.ourCommit.publishableTx.txOut.map(_.amount.toLong).sum
val closeFee = Satoshi(2 * (commitFee / 4))
val amount_us = Satoshi(ourCommit.spec.amount_us_msat / 1000)
val amount_them = Satoshi(theirCommit.spec.amount_us_msat / 1000)
val finalTx = Scripts.makeFinalTx(ourCommit.publishableTx.txIn, d.ourClearing.scriptPubkey, d.theirClearing.scriptPubkey, amount_us, amount_them, closeFee)
val ourSig = sign(ourParams, theirParams, d.anchorOutput.amount.toLong, finalTx)
val ourCloseSig = close_signature(closeFee.toLong, ourSig)
case Event(msg@update_revocation(revocationPreimage, nextRevocationHash), d@DATA_CLEARING(commitments, _, _, ourClearing, theirClearing)) =>
val commitments1 = Commitments.receiveRevocation(commitments, msg)
if (commitments1.hasNoPendingHtlcs) {
val (finalTx, ourCloseSig) = makeFinalTx(commitments1, ourClearing.scriptPubkey, theirClearing.scriptPubkey)
them ! ourCloseSig
goto(NEGOCIATING) using DATA_NEGOCIATING(ourParams, theirParams, shaChain, d.htlcIdx, ourCommit, theirCommit, ourChanges1, theirChanges, Some(nextRevocationHash), d.anchorOutput, d.ourClearing, d.theirClearing, ourCloseSig)
goto(NEGOCIATING) using DATA_NEGOCIATING(commitments1, d.shaChain, d.htlcIdx, ourClearing, theirClearing, ourCloseSig)
} else {
stay using d.copy(ourChanges = ourChanges1, theirNextRevocationHash = Some(nextRevocationHash))
stay using d.copy(commitments = commitments1)
}
}
@ -515,8 +428,8 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
checkCloseSignature(theirSig, Satoshi(theirCloseFee), d) match {
case Success(signedTx) =>
blockchain ! Publish(signedTx)
blockchain ! WatchConfirmed(self, signedTx.txid, d.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.ourParams, d.theirParams, d.shaChain, d.ourCommit, d.theirCommit, mutualClosePublished = Some(signedTx))
blockchain ! WatchConfirmed(self, signedTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, mutualClosePublished = Some(signedTx))
case Failure(cause) =>
log.error(cause, "cannot verify their close signature")
throw new RuntimeException("cannot verify their close signature", cause)
@ -529,17 +442,13 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
case value if value == d.ourSignature.closeFee => value + 2
case value => value
}
val amount_us = Satoshi(d.ourCommit.spec.amount_us_msat / 1000)
val amount_them = Satoshi(d.theirCommit.spec.amount_us_msat / 1000)
val finalTx = Scripts.makeFinalTx(d.ourCommit.publishableTx.txIn, d.ourClearing.scriptPubkey, d.theirClearing.scriptPubkey, amount_us, amount_them, Satoshi(closeFee))
val ourSig = sign(d.ourParams, d.theirParams, d.anchorOutput.amount.toLong, finalTx)
val ourCloseSig = close_signature(closeFee, ourSig)
val (finalTx, ourCloseSig) = makeFinalTx(d.commitments, d.ourClearing.scriptPubkey, d.theirClearing.scriptPubkey, Satoshi(closeFee))
them ! ourCloseSig
if (closeFee == theirCloseFee) {
val signedTx = addSigs(d.ourParams, d.theirParams, d.anchorOutput.amount.toLong, finalTx, ourSig, theirSig)
val signedTx = addSigs(d.commitments.ourParams, d.commitments.theirParams, d.commitments.anchorOutput.amount.toLong, finalTx, ourCloseSig.sig, theirSig)
blockchain ! Publish(signedTx)
blockchain ! WatchConfirmed(self, signedTx.txid, d.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.ourParams, d.theirParams, d.shaChain, d.ourCommit, d.theirCommit, mutualClosePublished = Some(signedTx))
blockchain ! WatchConfirmed(self, signedTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, mutualClosePublished = Some(signedTx))
} else {
stay using d.copy(ourSignature = ourCloseSig)
}
@ -560,186 +469,17 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
*/
when(CLOSING) {
case Event(close_signature(theirCloseFee, theirSig), d: DATA_NEGOCIATING) if theirCloseFee == d.ourSignature.closeFee =>
case Event(close_signature(theirCloseFee, theirSig), d: DATA_CLOSING) if d.ourSignature.map(_.closeFee) == Some(theirCloseFee) =>
stay()
case Event(close_signature(theirCloseFee, theirSig), d: DATA_NEGOCIATING) =>
throw new RuntimeException(s"unexpected closing fee: $theirCloseFee ours is ${d.ourSignature.closeFee}")
case Event(close_signature(theirCloseFee, theirSig), d: DATA_CLOSING) =>
throw new RuntimeException(s"unexpected closing fee: $theirCloseFee ours is ${d.ourSignature.map(_.closeFee)}")
case Event(BITCOIN_CLOSE_DONE, _) => goto(CLOSED)
}
/*def clearing_handler: StateFunction = {
case Event(htlc@update_add_htlc(htlcId, amount, rHash, expiry, nodeIds), d@DATA_CLEARING(ack_in, _, _, _, _, staged, commitment, _, _)) =>
// TODO : should we take pending htlcs into account?
assert(commitment.state.commit_changes(staged).them.pay_msat >= amount, "insufficient funds!") // TODO : we should fail the channel
// TODO nodeIds are ignored
stay using d.copy(ack_in = ack_in + 1, staged = staged :+ Change(IN, ack_in + 1, htlc))
case Event(fulfill@update_fulfill_htlc(id, r), d@DATA_CLEARING(ack_in, _, _, _, _, staged, commitment, _, _)) =>
assert(commitment.state.commit_changes(staged).them.htlcs_received.exists(_.id == id), s"unknown htlc id=$id") // TODO : we should fail the channel
stay using d.copy(ack_in = ack_in + 1, staged = staged :+ Change(IN, ack_in + 1, fulfill))
case Event(fail@update_fail_htlc(id, reason), d@DATA_CLEARING(ack_in, _, _, _, _, staged, commitment, _, _)) =>
assert(commitment.state.commit_changes(staged).them.htlcs_received.exists(_.id == id), s"unknown htlc id=$id") // TODO : we should fail the channel
stay using d.copy(ack_in = ack_in + 1, staged = staged :+ Change(IN, ack_in + 1, fail))
case Event(CMD_FULFILL_HTLC(id, r), d@DATA_CLEARING(_, ack_out, _, _, _, staged, commitment, _, _)) =>
assert(commitment.state.commit_changes(staged).us.htlcs_received.exists(_.id == id), s"unknown htlc id=$id") // TODO : we should fail the channel
val fulfill = update_fulfill_htlc(id, r)
them ! fulfill
stay using d.copy(ack_out = ack_out + 1, staged = staged :+ Change(OUT, ack_out + 1, fulfill))
case Event(fulfill@update_fulfill_htlc(id, r), d@DATA_CLEARING(ack_in, _, _, _, _, staged, commitment, _, _)) =>
assert(commitment.state.commit_changes(staged).them.htlcs_received.exists(_.id == id), s"unknown htlc id=$id") // TODO : we should fail the channel
stay using d.copy(ack_in = ack_in + 1, staged = staged :+ Change(IN, ack_in + 1, fulfill))
case Event(CMD_FAIL_HTLC(id, reason), d@DATA_CLEARING(_, ack_out, _, _, _, staged, commitment, _, _)) =>
assert(commitment.state.commit_changes(staged).us.htlcs_received.exists(_.id == id), s"unknown htlc id=$id") // TODO : we should fail the channel
val fail = update_fail_htlc(id, fail_reason(ByteString.copyFromUtf8(reason)))
them ! fail
stay using d.copy(ack_out = ack_out + 1, staged = staged :+ Change(OUT, ack_out + 1, fail))
case Event(fail@update_fail_htlc(id, reason), d@DATA_CLEARING(ack_in, _, _, _, _, staged, commitment, _, _)) =>
assert(commitment.state.commit_changes(staged).them.htlcs_received.exists(_.id == id), s"unknown htlc id=$id") // TODO : we should fail the channel
stay using d.copy(ack_in = ack_in + 1, staged = staged :+ Change(IN, ack_in + 1, fail))
case Event(clearing@close_clearing(theirScriptPubKey), d@DATA_CLEARING(ack_in, ack_out, ourParams, theirParams, shaChain, staged, commitment, _, ClosingData(_, None))) =>
val closing = d.closing.copy(theirScriptPubKey = Some(theirScriptPubKey))
if (commitment.state.them.htlcs_received.size == 0 && commitment.state.us.htlcs_received.size == 0) {
val finalTx = makeFinalTx(commitment.tx.txIn, ourParams.finalPubKey, theirParams.finalPubKey, commitment.state) //TODO ADJUST FEES
val ourSig = bin2signature(Transaction.signInput(finalTx, 0, multiSig2of2(ourParams.commitPubKey, theirParams.commitPubKey), SIGHASH_ALL, ourParams.commitPrivKey))
them ! close_signature(closeFee, ourSig)
goto(CLOSE_NEGOTIATING) using DATA_NEGOTIATING(ack_in + 1, ack_out + 1, ourParams, theirParams, shaChain, commitment, closing)
} else {
stay using d.copy(ack_in = ack_in + 1, closing = closing)
}
}
when(CLOSE_CLEARING)(clearing_handler orElse {
case Event(CMD_SIGN, d@DATA_CLEARING(ack_in, ack_out, ourParams, theirParams, shaChain, staged, previousCommitment, ReadyForSig(theirNextRevocationHash), _)) =>
val proposal = UpdateProposal(previousCommitment.index + 1, previousCommitment.state.commit_changes(staged), theirNextRevocationHash)
val ourRevocationHash = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, proposal.index))
val (ourCommitTx, ourSigForThem) = sign_their_commitment_tx(ourParams, theirParams, previousCommitment.tx.txIn, proposal.state, ourRevocationHash, theirNextRevocationHash)
them ! update_commit(ourSigForThem, ack_in)
goto(CLOSE_CLEARING_WAIT_FOR_REV) using d.copy(ack_out = ack_out + 1, staged = Nil, next = WaitForRev(proposal))
case Event(msg@update_commit(theirSig, theirAck), d@DATA_CLEARING(ack_in, ack_out, ourParams, theirParams, shaChain, staged, previousCommitment, ReadyForSig(theirNextRevocationHash), _)) =>
// counterparty initiated a new commitment
val committed_changes = staged.filter(c => c.direction == IN || c.ack <= theirAck)
val uncommitted_changes = staged.filterNot(committed_changes.contains(_))
// TODO : we should check that this is the correct state (see acknowledge discussion)
val proposal = UpdateProposal(previousCommitment.index + 1, previousCommitment.state.commit_changes(committed_changes), theirNextRevocationHash)
val ourRevocationHash = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, proposal.index))
val (ourCommitTx, ourSigForThem) = sign_their_commitment_tx(ourParams, theirParams, previousCommitment.tx.txIn, proposal.state, ourRevocationHash, proposal.theirRevocationHash)
val signedCommitTx = sign_our_commitment_tx(ourParams, theirParams, ourCommitTx, theirSig)
val ok = Try(Transaction.correctlySpends(signedCommitTx, Map(previousCommitment.tx.txIn(0).outPoint -> anchorPubkeyScript(ourCommitPubKey, theirParams.commitPubKey)), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)).isSuccess
ok match {
case false =>
them ! error(Some("Bad signature"))
publish_ourcommit(previousCommitment)
goto(CLOSING) using DATA_CLOSING(ack_in = ack_in + 1, ack_out = ack_out + 1, ourParams, theirParams, shaChain, previousCommitment, ourCommitPublished = Some(previousCommitment.tx))
case true =>
val preimage = ShaChain.shaChainFromSeed(ourParams.shaSeed, previousCommitment.index)
val ourNextRevocationHash = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, proposal.index + 1))
them ! update_revocation(preimage, ourNextRevocationHash, ack_in + 1)
them ! update_commit(ourSigForThem, ack_in + 1)
goto(CLOSE_CLEARING_WAIT_FOR_REV_THEIRSIG) using d.copy(ack_in = ack_in + 1, ack_out = ack_out + 2, staged = uncommitted_changes, next = WaitForRevTheirSig(Commitment(proposal.index, signedCommitTx, proposal.state, proposal.theirRevocationHash)))
}
})
when(CLOSE_CLEARING_WAIT_FOR_REV)(clearing_handler orElse {
case Event(update_revocation(theirRevocationPreimage, theirNextRevocationHash, theirAck), d@DATA_CLEARING(ack_in, ack_out, ourParams, theirParams, shaChain, staged, previousCommitment, WaitForRev(proposal), closing)) =>
// counterparty replied with the signature for its new commitment tx, and revocationPreimage
val revocationHashCheck = new BinaryData(previousCommitment.theirRevocationHash) == new BinaryData(Crypto.sha256(theirRevocationPreimage))
if (revocationHashCheck) {
goto(CLOSE_CLEARING_WAIT_FOR_SIG) using d.copy(ack_in = ack_in + 1, next = WaitForSig(proposal, theirNextRevocationHash))
} else {
log.warning(s"the revocation preimage they gave us is wrong! hash=${previousCommitment.theirRevocationHash} preimage=$theirRevocationPreimage")
them ! error(Some("Wrong preimage"))
publish_ourcommit(previousCommitment)
goto(CLOSING) using DATA_CLOSING(ack_in = ack_in + 1, ack_out = ack_out + 1, ourParams, theirParams, shaChain, previousCommitment, ourCommitPublished = Some(previousCommitment.tx))
}
case Event(msg@update_commit(theirSig, theirAck), DATA_CLEARING(ack_in, ack_out, ourParams, theirParams, shaChain, staged, previousCommitment, WaitForRev(proposal), closing)) =>
// TODO : IGNORED FOR NOW
log.warning(s"ignored $msg")
stay
})
when(CLOSE_CLEARING_WAIT_FOR_REV_THEIRSIG)(clearing_handler orElse {
case Event(update_revocation(theirRevocationPreimage, theirNextRevocationHash, theirAck), d@DATA_CLEARING(ack_in, ack_out, ourParams, theirParams, shaChain, staged, previousCommitment, WaitForRevTheirSig(nextCommitment), _)) =>
// counterparty replied with the signature for its new commitment tx, and revocationPreimage
val revocationHashCheck = new BinaryData(previousCommitment.theirRevocationHash) == new BinaryData(Crypto.sha256(theirRevocationPreimage))
if (revocationHashCheck) {
if (nextCommitment.state.them.htlcs_received.size == 0 && nextCommitment.state.us.htlcs_received.size == 0) {
val finalTx = makeFinalTx(nextCommitment.tx.txIn, ourParams.finalPubKey, theirParams.finalPubKey, nextCommitment.state) //TODO ADJUST FEES
val ourSig = bin2signature(Transaction.signInput(finalTx, 0, multiSig2of2(ourParams.commitPubKey, theirParams.commitPubKey), SIGHASH_ALL, ourParams.commitPrivKey))
them ! close_signature(closeFee, ourSig)
goto(CLOSE_NEGOTIATING) using d.copy(ack_in = ack_in + 1, ack_out = ack_out + 1, commitment = nextCommitment, next = ReadyForSig(theirNextRevocationHash))
} else {
goto(CLOSE_CLEARING) using d.copy(ack_in = ack_in + 1, commitment = nextCommitment, next = ReadyForSig(theirNextRevocationHash))
}
} else {
log.warning(s"the revocation preimage they gave us is wrong! hash=${previousCommitment.theirRevocationHash} preimage=$theirRevocationPreimage")
them ! error(Some("Wrong preimage"))
publish_ourcommit(previousCommitment)
goto(CLOSING) using DATA_CLOSING(ack_in = ack_in + 1, ack_out = ack_out + 1, ourParams, theirParams, shaChain, previousCommitment, ourCommitPublished = Some(previousCommitment.tx))
}
})
when(CLOSE_CLEARING_WAIT_FOR_SIG)(clearing_handler orElse {
case Event(update_commit(theirSig, theirAck), d@DATA_CLEARING(ack_in, ack_out, ourParams, theirParams, shaChain, staged, previousCommitment, WaitForSig(proposal, theirNextRevocationHash), _)) =>
// counterparty replied with the signature for the new commitment tx
val ourRevocationHash = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, proposal.index))
val (ourCommitTx, ourSigForThem) = sign_their_commitment_tx(ourParams, theirParams, previousCommitment.tx.txIn, proposal.state, ourRevocationHash, proposal.theirRevocationHash)
val signedCommitTx = sign_our_commitment_tx(ourParams, theirParams, ourCommitTx, theirSig)
val ok = Try(Transaction.correctlySpends(signedCommitTx, Map(previousCommitment.tx.txIn(0).outPoint -> anchorPubkeyScript(ourCommitPubKey, theirParams.commitPubKey)), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)).isSuccess
ok match {
case false =>
them ! error(Some("Bad signature"))
publish_ourcommit(previousCommitment)
goto(CLOSING) using DATA_CLOSING(ack_in = ack_in + 1, ack_out = ack_out + 1, ourParams, theirParams, shaChain, previousCommitment, ourCommitPublished = Some(previousCommitment.tx))
case true =>
val preimage = ShaChain.shaChainFromSeed(ourParams.shaSeed, previousCommitment.index)
val ourNextRevocationHash = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, proposal.index + 1))
them ! update_revocation(preimage, ourNextRevocationHash, ack_in + 1)
val nextCommitment = Commitment(proposal.index, signedCommitTx, proposal.state, proposal.theirRevocationHash)
if (nextCommitment.state.them.htlcs_received.size == 0 && nextCommitment.state.us.htlcs_received.size == 0) {
val finalTx = makeFinalTx(nextCommitment.tx.txIn, ourParams.finalPubKey, theirParams.finalPubKey, nextCommitment.state) //TODO ADJUST FEES
val ourSig = bin2signature(Transaction.signInput(finalTx, 0, multiSig2of2(ourParams.commitPubKey, theirParams.commitPubKey), SIGHASH_ALL, ourParams.commitPrivKey))
them ! close_signature(closeFee, ourSig)
goto(CLOSE_NEGOTIATING) using d.copy(ack_in = ack_in + 1, ack_out = ack_out + 1, commitment = nextCommitment, next = ReadyForSig(theirNextRevocationHash))
} else {
goto(CLOSE_CLEARING) using d.copy(ack_in = ack_in + 1, ack_out = ack_out + 1, commitment = nextCommitment, next = ReadyForSig(theirNextRevocationHash))
}
}
})
when(CLOSE_NEGOTIATING) {
case Event(close_signature(closeFee, sig), DATA_NEGOTIATING(ack_in, ack_out, ourParams, theirParams, shaChain, commitment, closing)) =>
// TODO: we should actually negotiate
// TODO: publish tx
val mutualTx: Transaction = null // TODO
goto(CLOSING) using DATA_CLOSING(ack_in, ack_out, ourParams, theirParams, shaChain, commitment, Some(mutualTx), None, None, Nil)
}
/*
/*when(WAIT_FOR_CLOSE_COMPLETE) {
case Event(close_channel_complete(theirSig), d: CurrentCommitment) =>
//TODO we should use the closing fee in pkts
val closingState = d.commitment.state.adjust_fees(Globals.closing_fee * 1000, d.ourParams.anchorAmount.isDefined)
val finalTx = makeFinalTx(d.commitment.tx.txIn, ourFinalPubKey, d.theirParams.finalPubKey, closingState)
val signedFinalTx = sign_our_commitment_tx(d.ourParams, d.theirParams, finalTx, theirSig)
val ok = Try(Transaction.correctlySpends(signedFinalTx, Map(signedFinalTx.txIn(0).outPoint -> anchorPubkeyScript(ourCommitPubKey, d.theirParams.commitPubKey)), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)).isSuccess
ok match {
case false =>
them ! error(Some("Bad signature"))
publish_ourcommit(d.commitment)
goto(CLOSING) using DATA_CLOSING(d.ourParams, d.theirParams, d.shaChain, d.commitment, ourCommitPublished = Some(d.commitment.tx))
case true =>
them ! close_channel_ack()
blockchain ! Publish(signedFinalTx)
goto(CLOSING) using DATA_CLOSING(d.ourParams, d.theirParams, d.shaChain, d.commitment, mutualClosePublished = Some(signedFinalTx))
}
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: CurrentCommitment) if (isMutualClose(tx, d.ourParams, d.theirParams, d.commitment)) =>
// it is possible that we received this before the close_channel_complete, we may still receive the latter
@ -863,7 +603,11 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
case Event(CMD_GETINFO, _) =>
sender ! RES_GETINFO(theirNodeId, stateData match {
case c: CurrentCommitment => c.anchorId
case c: DATA_NORMAL => c.commitments.anchorId
case c: DATA_OPEN_WAITING => c.commitments.anchorId
case c: DATA_CLEARING => c.commitments.anchorId
case c: DATA_NEGOCIATING => c.commitments.anchorId
case c: DATA_CLOSING => c.commitments.anchorId
case _ => Hash.Zeroes
}, stateName, stateData)
stay
@ -928,13 +672,9 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
}
def checkCloseSignature(closeSig: BinaryData, closeFee: Satoshi, d: DATA_NEGOCIATING): Try[Transaction] = {
val amount_us = Satoshi(d.ourCommit.spec.amount_us_msat / 1000)
val amount_them = Satoshi(d.theirCommit.spec.amount_us_msat / 1000)
val finalTx = Scripts.makeFinalTx(d.ourCommit.publishableTx.txIn, d.ourClearing.scriptPubkey, d.theirClearing.scriptPubkey, amount_us, amount_them, closeFee)
val ourSig = sign(d.ourParams, d.theirParams, d.anchorOutput.amount.toLong, finalTx)
val ourCloseSig = close_signature(d.ourSignature.closeFee, ourSig)
val signedTx = addSigs(d.ourParams, d.theirParams, d.anchorOutput.amount.toLong, finalTx, ourSig, closeSig)
checksig(d.ourParams, d.theirParams, d.anchorOutput, signedTx).map(_ => signedTx)
val (finalTx, ourCloseSig) = Helpers.makeFinalTx(d.commitments, d.ourClearing.scriptPubkey, d.theirClearing.scriptPubkey, closeFee)
val signedTx = addSigs(d.commitments.ourParams, d.commitments.theirParams, d.commitments.anchorOutput.amount.toLong, finalTx, ourCloseSig.sig, closeSig)
checksig(d.commitments.ourParams, d.commitments.theirParams, d.commitments.anchorOutput, signedTx).map(_ => signedTx)
}
}

View file

@ -112,53 +112,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
}
trait CurrentCommitment {
def ourParams: OurChannelParams
def theirParams: TheirChannelParams
def shaChain: ShaChain
def ourCommit: OurCommit
def theirCommit: TheirCommit
def anchorId: BinaryData = {
assert(ourCommit.publishableTx.txIn.size == 1, "commitment tx should only have one input")
ourCommit.publishableTx.txIn(0).outPoint.hash
}
}
final case class ClosingData(ourScriptPubKey: BinaryData, theirScriptPubKey: Option[BinaryData])
final case class DATA_OPEN_WAIT_FOR_OPEN (ourParams: OurChannelParams) extends Data
final case class DATA_OPEN_WITH_ANCHOR_WAIT_FOR_ANCHOR(ourParams: OurChannelParams, theirParams: TheirChannelParams, theirRevocationHash: BinaryData, theirNextRevocationHash: sha256_hash) extends Data
final case class DATA_OPEN_WAIT_FOR_ANCHOR (ourParams: OurChannelParams, theirParams: TheirChannelParams, theirRevocationHash: sha256_hash, theirNextRevocationHash: sha256_hash) extends Data
final case class DATA_OPEN_WAIT_FOR_COMMIT_SIG (ourParams: OurChannelParams, theirParams: TheirChannelParams, anchorTx: Transaction, anchorOutputIndex: Int, initialCommitment: TheirCommit, theirNextRevocationHash: sha256_hash) extends Data
final case class DATA_OPEN_WAITING (ourParams: OurChannelParams, theirParams: TheirChannelParams, shaChain: ShaChain, ourCommit: OurCommit, theirCommit: TheirCommit, theirNextRevocationHash: sha256_hash, deferred: Option[open_complete], anchorOutput: TxOut) extends Data with CurrentCommitment
final case class DATA_NORMAL (ourParams: OurChannelParams, theirParams: TheirChannelParams, shaChain: ShaChain, htlcIdx: Long,
ourCommit: OurCommit,
theirCommit: TheirCommit,
ourChanges: OurChanges,
theirChanges: TheirChanges,
theirNextRevocationHash: Option[sha256_hash],
anchorOutput: TxOut,
ourClearing: Option[close_clearing]) extends Data with CurrentCommitment
final case class DATA_CLEARING (ourParams: OurChannelParams, theirParams: TheirChannelParams, shaChain: ShaChain, htlcIdx: Long,
ourCommit: OurCommit,
theirCommit: TheirCommit,
ourChanges: OurChanges,
theirChanges: TheirChanges,
theirNextRevocationHash: Option[sha256_hash],
anchorOutput: TxOut,
ourClearing: close_clearing,
theirClearing: close_clearing) extends Data with CurrentCommitment
final case class DATA_NEGOCIATING (ourParams: OurChannelParams, theirParams: TheirChannelParams, shaChain: ShaChain, htlcIdx: Long,
ourCommit: OurCommit,
theirCommit: TheirCommit,
ourChanges: OurChanges,
theirChanges: TheirChanges,
theirNextRevocationHash: Option[sha256_hash],
anchorOutput: TxOut,
ourClearing: close_clearing,
theirClearing: close_clearing,
ourSignature: close_signature) extends Data with CurrentCommitment
object TypeDefs {
type Change = GeneratedMessage
}
@ -169,11 +122,25 @@ 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 DATA_CLEARING (ack_in: Long, ack_out: Long, ourParams: OurChannelParams, theirParams: TheirChannelParams, shaChain: ShaChain, staged: List[Change], commitment: Commitment, next: NextCommitment, closing: ClosingData) extends Data with CurrentCommitment
final case class DATA_NEGOTIATING (ack_in: Long, ack_out: Long, ourParams: OurChannelParams, theirParams: TheirChannelParams, shaChain: ShaChain, commitment: Commitment, closing: ClosingData) extends Data with CurrentCommitment
*/
final case class DATA_CLOSING (ourParams: OurChannelParams, theirParams: TheirChannelParams, shaChain: ShaChain, ourCommit: OurCommit, theirCommit: TheirCommit,
mutualClosePublished: Option[Transaction] = None, ourCommitPublished: Option[Transaction] = None, theirCommitPublished: Option[Transaction] = None, revokedPublished: Seq[Transaction] = Seq()) extends Data with CurrentCommitment {
final case class ClosingData(ourScriptPubKey: BinaryData, theirScriptPubKey: Option[BinaryData])
final case class DATA_OPEN_WAIT_FOR_OPEN (ourParams: OurChannelParams) extends Data
final case class DATA_OPEN_WITH_ANCHOR_WAIT_FOR_ANCHOR(ourParams: OurChannelParams, theirParams: TheirChannelParams, theirRevocationHash: BinaryData, theirNextRevocationHash: sha256_hash) extends Data
final case class DATA_OPEN_WAIT_FOR_ANCHOR (ourParams: OurChannelParams, theirParams: TheirChannelParams, theirRevocationHash: sha256_hash, theirNextRevocationHash: sha256_hash) extends Data
final case class DATA_OPEN_WAIT_FOR_COMMIT_SIG (ourParams: OurChannelParams, theirParams: TheirChannelParams, anchorTx: Transaction, anchorOutputIndex: Int, initialCommitment: TheirCommit, theirNextRevocationHash: sha256_hash) extends Data
final case class DATA_OPEN_WAITING (commitments: Commitments, shaChain: ShaChain, deferred: Option[open_complete]) extends Data
final case class DATA_NORMAL (commitments: Commitments, shaChain: ShaChain, htlcIdx: Long,
ourClearing: Option[close_clearing]) extends Data
final case class DATA_CLEARING (commitments: Commitments, shaChain: ShaChain, htlcIdx: Long,
ourClearing: close_clearing, theirClearing: close_clearing) extends Data
final case class DATA_NEGOCIATING (commitments: Commitments, shaChain: ShaChain, htlcIdx: Long,
ourClearing: close_clearing, theirClearing: close_clearing, ourSignature: close_signature) extends Data
final case class DATA_CLOSING (commitments: Commitments, shaChain: ShaChain,
ourSignature: Option[close_signature] = None,
mutualClosePublished: Option[Transaction] = None,
ourCommitPublished: Option[Transaction] = None,
theirCommitPublished: Option[Transaction] = None,
revokedPublished: Seq[Transaction] = Seq()) extends Data {
assert(mutualClosePublished.isDefined || ourCommitPublished.isDefined || theirCommitPublished.isDefined || revokedPublished.size > 0, "there should be at least one tx published in this state")
}

View file

@ -0,0 +1,153 @@
package fr.acinq.eclair.channel
import com.google.protobuf.ByteString
import fr.acinq.bitcoin.{BinaryData, Crypto, Satoshi, Transaction, TxOut}
import fr.acinq.eclair._
import fr.acinq.eclair.channel.TypeDefs.Change
import fr.acinq.eclair.crypto.ShaChain
import lightning._
/**
* about theirNextCommitInfo:
* we either:
* - have built and sign their next commit tx with their next revocation hash which can now be discarded
* - have their next revocation hash
* So, when we've signed and sent a commit message and are waiting for their revocation message,
* theirNextCommitInfo is their next commit tx. The rest of the time, it is their next revocation hash
*/
case class Commitments(ourParams: OurChannelParams, theirParams: TheirChannelParams,
ourCommit: OurCommit, theirCommit: TheirCommit,
ourChanges: OurChanges, theirChanges: TheirChanges,
theirNextCommitInfo: Either[TheirCommit, BinaryData],
anchorOutput: TxOut) {
def anchorId: BinaryData = {
assert(ourCommit.publishableTx.txIn.size == 1, "commitment tx should only have one input")
ourCommit.publishableTx.txIn(0).outPoint.hash
}
def hasNoPendingHtlcs: Boolean = ourCommit.spec.htlcs.isEmpty && theirCommit.spec.htlcs.isEmpty
def addOurProposal(proposal: Change): Commitments = Commitments.addOurProposal(this, proposal)
def addTheirProposal(proposal: Change): Commitments = Commitments.addTheirProposal(this, proposal)
}
object Commitments {
/**
* add a change to our proposed change list
*
* @param 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))
def addTheirProposal(commitments: Commitments, proposal: Change): Commitments =
commitments.copy(theirChanges = commitments.theirChanges.copy(proposed = commitments.theirChanges.proposed :+ proposal))
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)) =>
val fulfill = update_fulfill_htlc(cmd.id, cmd.r)
val commitments1 = addOurProposal(commitments, fulfill)
(commitments1, fulfill)
case Some(htlc) => throw new RuntimeException(s"invalid htlc preimage for htlc ${cmd.id}")
case None => throw new RuntimeException(s"unknown htlc id=${cmd.id}")
}
}
def receiveFulfill(commitments: Commitments, fulfill: update_fulfill_htlc): Commitments = {
commitments.ourChanges.acked.collectFirst { case u: update_add_htlc if u.id == fulfill.id => u } match {
case Some(htlc) if htlc.rHash == bin2sha256(Crypto.sha256(fulfill.r)) => addTheirProposal(commitments, fulfill)
case Some(htlc) => throw new RuntimeException(s"invalid htlc preimage for htlc ${fulfill.id}")
case None => throw new RuntimeException(s"unknown htlc id=${fulfill.id}") // TODO : we should fail the channel
}
}
def sendFail(commitments: Commitments, cmd: CMD_FAIL_HTLC): (Commitments, update_fail_htlc) = {
commitments.theirChanges.acked.collectFirst { case u: update_add_htlc if u.id == cmd.id => u } match {
case Some(htlc) =>
val fail = update_fail_htlc(cmd.id, fail_reason(ByteString.copyFromUtf8(cmd.reason)))
val commitments1 = addOurProposal(commitments, fail)
(commitments1, fail)
case None => throw new RuntimeException(s"unknown htlc id=${cmd.id}")
}
}
def receiveFail(commitments: Commitments, fail: update_fail_htlc): Commitments = {
commitments.ourChanges.acked.collectFirst { case u: update_add_htlc if u.id == fail.id => u } match {
case Some(htlc) =>
addTheirProposal(commitments, fail)
case None => throw new RuntimeException(s"unknown htlc id=${fail.id}") // TODO : we should fail the channel
}
}
def sendCommit(commitments: Commitments): (Commitments, update_commit) = {
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 ourSig = Helpers.sign(ourParams, theirParams, anchorOutput.amount.toLong, theirTx)
val commit = update_commit(ourSig)
val commitments1 = commitments.copy(
theirNextCommitInfo = Left(TheirCommit(theirCommit.index + 1, spec, 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")
}
}
def receiveCommit(commitments: Commitments, commit: update_commit): (Commitments, update_revocation) = {
import commitments._
// they sent us a signature for *their* view of *our* next commit tx
// so in terms of rev.hashes and indexes we have:
// ourCommit.index -> our current revocation hash, which is about to become our old revocation hash
// ourCommit.index + 1 -> our next revocation hash, used by * them * to build the sig we've just received, and which
// is about to become our current revocation hash
// ourCommit.index + 2 -> which is about to become our next revocation hash
// we will reply to this sig with our old revocation hash preimage (at index) and our next revocation hash (at index + 1)
// and will increment our index
// check that their signature is valid
val spec = Helpers.reduce(ourCommit.spec, ourChanges.acked, theirChanges.acked ++ theirChanges.proposed)
val ourNextRevocationHash = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, ourCommit.index + 1))
val ourTx = Helpers.makeOurTx(ourParams, theirParams, ourCommit.publishableTx.txIn, ourNextRevocationHash, spec)
val ourSig = Helpers.sign(ourParams, theirParams, anchorOutput.amount.toLong, ourTx)
val signedTx = Helpers.addSigs(ourParams, theirParams, anchorOutput.amount.toLong, ourTx, ourSig, commit.sig)
Helpers.checksig(ourParams, theirParams, anchorOutput, signedTx).get
// we will send our revocation preimage+ our next revocation hash
val ourRevocationPreimage = ShaChain.shaChainFromSeed(ourParams.shaSeed, ourCommit.index)
val ourNextRevocationHash1 = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, ourCommit.index + 2))
val revocation = update_revocation(ourRevocationPreimage, ourNextRevocationHash1)
// update our commitment data
val ourCommit1 = ourCommit.copy(index = ourCommit.index + 1, spec, publishableTx = signedTx)
val theirChanges1 = theirChanges.copy(proposed = Nil, acked = theirChanges.acked ++ theirChanges.proposed)
val commitments1 = commitments.copy(ourCommit = ourCommit1, theirChanges = theirChanges1)
(commitments1, revocation)
}
def receiveRevocation(commitments: Commitments, revocation: update_revocation): Commitments = {
import commitments._
// we receive a revocation because we just sent them a sig for their next commit tx
theirNextCommitInfo match {
case Left(theirNextCommit) =>
assert(BinaryData(Crypto.sha256(revocation.revocationPreimage)) == BinaryData(theirCommit.theirRevocationHash), "invalid preimage")
commitments.copy(
ourChanges = ourChanges.copy(signed = Nil, acked = ourChanges.acked ++ ourChanges.signed),
theirCommit = theirNextCommit,
theirNextCommitInfo = Right(revocation.nextRevocationHash))
case Right(_) =>
throw new RuntimeException("received unexpected update_revocation message")
}
}
}

View file

@ -113,20 +113,33 @@ object Helpers {
true
}
/*def handle_cmd_close(cmd: CMD_CLOSE, ourParams: OurChannelParams, theirParams: TheirChannelParams, commitment: Commitment): close_channel = {
val closingState = commitment.state.adjust_fees(cmd.fee * 1000, ourParams.anchorAmount.isDefined)
val finalTx = makeFinalTx(commitment.tx.txIn, ourParams.finalPubKey, theirParams.finalPubKey, closingState)
val ourSig = bin2signature(Transaction.signInput(finalTx, 0, multiSig2of2(ourParams.commitPubKey, theirParams.commitPubKey), SIGHASH_ALL, ourParams.commitPrivKey))
val anchorTxId = commitment.tx.txIn(0).outPoint.txid // commit tx only has 1 input, which is the anchor
close_channel(ourSig, cmd.fee)
}*/
/*def handle_pkt_close(pkt: close_channel, ourParams: OurChannelParams, theirParams: TheirChannelParams, commitment: Commitment): (Transaction, close_channel_complete) = {
val closingState = commitment.state.adjust_fees(pkt.closeFee * 1000, ourParams.anchorAmount.isDefined)
val finalTx = makeFinalTx(commitment.tx.txIn, ourParams.finalPubKey, theirParams.finalPubKey, closingState)
val ourSig = Transaction.signInput(finalTx, 0, multiSig2of2(ourParams.commitPubKey, theirParams.commitPubKey), SIGHASH_ALL, ourParams.commitPrivKey)
val signedFinalTx = finalTx.updateSigScript(0, sigScript2of2(pkt.sig, ourSig, theirParams.commitPubKey, ourParams.commitPubKey))
(signedFinalTx, close_channel_complete(ourSig))
}*/
/**
*
* @param commitments
* @param ourScriptPubKey
* @param theirScriptPubKey
* @param closeFee bitcoin fee for the final tx
* @return a (final tx, our signature) tuple. The tx is not signed.
*/
def makeFinalTx(commitments: Commitments, ourScriptPubKey: BinaryData, theirScriptPubKey: BinaryData, closeFee: Satoshi): (Transaction, close_signature) = {
val amount_us = Satoshi(commitments.ourCommit.spec.amount_us_msat / 1000)
val amount_them = Satoshi(commitments.theirCommit.spec.amount_us_msat / 1000)
val finalTx = Scripts.makeFinalTx(commitments.ourCommit.publishableTx.txIn, ourScriptPubKey, theirScriptPubKey, amount_us, amount_them, closeFee)
val ourSig = Helpers.sign(commitments.ourParams, commitments.theirParams, commitments.anchorOutput.amount.toLong, finalTx)
(finalTx, close_signature(closeFee.toLong, ourSig))
}
/**
*
* @param commitments
* @param ourScriptPubKey
* @param theirScriptPubKey
* @return a (final tx, our signature) tuple. The tx is not signed. Bitcoin fees will be copied from our
* last commit tx
*/
def makeFinalTx(commitments: Commitments, ourScriptPubKey: BinaryData, theirScriptPubKey: BinaryData): (Transaction, close_signature) = {
val commitFee = commitments.anchorOutput.amount.toLong - commitments.ourCommit.publishableTx.txOut.map(_.amount.toLong).sum
val closeFee = Satoshi(2 * (commitFee / 4))
makeFinalTx(commitments, ourScriptPubKey, theirScriptPubKey, closeFee)
}
}

View file

@ -60,12 +60,12 @@ class NominalChannelSpec extends BaseChannelTestClass {
alice.stateData match {
case d: DATA_NORMAL =>
val List(update_add_htlc(_, _, h, _, _)) = d.ourChanges.proposed
val List(update_add_htlc(_, _, h, _, _)) = d.commitments.ourChanges.proposed
assert(h == bin2sha256(H))
}
bob.stateData match {
case d: DATA_NORMAL =>
val List(update_add_htlc(_, _, h, _, _)) = d.theirChanges.proposed
val List(update_add_htlc(_, _, h, _, _)) = d.commitments.theirChanges.proposed
assert(h == bin2sha256(H))
}
@ -74,12 +74,12 @@ class NominalChannelSpec extends BaseChannelTestClass {
alice.stateData match {
case d: DATA_NORMAL =>
val htlc = d.theirCommit.spec.htlcs.head
val htlc = d.commitments.theirCommit.spec.htlcs.head
assert(htlc.rHash == bin2sha256(H))
}
bob.stateData match {
case d: DATA_NORMAL =>
val htlc = d.ourCommit.spec.htlcs.head
val htlc = d.commitments.ourCommit.spec.htlcs.head
assert(htlc.rHash == bin2sha256(H))
}
@ -91,15 +91,15 @@ class NominalChannelSpec extends BaseChannelTestClass {
alice.stateData match {
case d: DATA_NORMAL =>
assert(d.ourCommit.spec.htlcs.isEmpty)
assert(d.ourCommit.spec.amount_us_msat == d.ourCommit.spec.initial_amount_us_msat - 60000000)
assert(d.ourCommit.spec.amount_them_msat == d.ourCommit.spec.initial_amount_them_msat + 60000000)
assert(d.commitments.ourCommit.spec.htlcs.isEmpty)
assert(d.commitments.ourCommit.spec.amount_us_msat == d.commitments.ourCommit.spec.initial_amount_us_msat - 60000000)
assert(d.commitments.ourCommit.spec.amount_them_msat == d.commitments.ourCommit.spec.initial_amount_them_msat + 60000000)
}
bob.stateData match {
case d: DATA_NORMAL =>
assert(d.ourCommit.spec.htlcs.isEmpty)
assert(d.ourCommit.spec.amount_us_msat == d.ourCommit.spec.initial_amount_us_msat + 60000000)
assert(d.ourCommit.spec.amount_them_msat == d.ourCommit.spec.initial_amount_them_msat - 60000000)
assert(d.commitments.ourCommit.spec.htlcs.isEmpty)
assert(d.commitments.ourCommit.spec.amount_us_msat == d.commitments.ourCommit.spec.initial_amount_us_msat + 60000000)
assert(d.commitments.ourCommit.spec.amount_them_msat == d.commitments.ourCommit.spec.initial_amount_them_msat - 60000000)
}
}
}