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:
parent
6d2ee58158
commit
d07a201353
5 changed files with 346 additions and 473 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue