1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-22 06:21:42 +01:00

added handling of remote current commit

This commit is contained in:
pm47 2017-01-16 10:26:02 +01:00
parent 26767054fb
commit af30b268ee
10 changed files with 170 additions and 235 deletions

View file

@ -6,6 +6,7 @@ import fr.acinq.eclair._
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel.Helpers.{Closing, Funding}
import fr.acinq.eclair.crypto.{Generators, ShaChain}
import fr.acinq.eclair.transactions.Transactions.{ClaimHtlcSuccessTx, ClaimHtlcTimeoutTx}
import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire._
@ -300,13 +301,17 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
case Event(cmd: CMD_CLOSE, d: DATA_WAIT_FOR_FUNDING_LOCKED_INTERNAL) =>
blockchain ! Publish(d.commitments.localCommit.publishableTxs._1.tx)
blockchain ! WatchConfirmed(self, d.commitments.localCommit.publishableTxs._1.tx.txid, d.params.minimumDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.localCommit.publishableTxs._1.tx))
// there can't be htlcs at this stage
val localCommitPublished = LocalCommitPublished(d.commitments.localCommit.publishableTxs._1.tx, Nil, Nil, Nil)
goto(CLOSING) using DATA_CLOSING(d.commitments, localCommitPublished = Some(localCommitPublished))
case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_LOCKED_INTERNAL) =>
log.error(s"peer sent $e, closing connection") // see bolt #2: A node MUST fail the connection if it receives an err message
blockchain ! Publish(d.commitments.localCommit.publishableTxs._1.tx)
blockchain ! WatchConfirmed(self, d.commitments.localCommit.publishableTxs._1.tx.txid, d.params.minimumDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.localCommit.publishableTxs._1.tx))
// there can't be htlcs at this stage
val localCommitPublished = LocalCommitPublished(d.commitments.localCommit.publishableTxs._1.tx, Nil, Nil, Nil)
goto(CLOSING) using DATA_CLOSING(d.commitments, localCommitPublished = Some(localCommitPublished))
})
when(WAIT_FOR_FUNDING_LOCKED)(handleExceptions {
@ -322,7 +327,9 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
case Event(cmd: CMD_CLOSE, d: DATA_NORMAL) =>
blockchain ! Publish(d.commitments.localCommit.publishableTxs._1.tx)
blockchain ! WatchConfirmed(self, d.commitments.localCommit.publishableTxs._1.tx.txid, d.params.minimumDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.localCommit.publishableTxs._1.tx))
// there can't be htlcs at this stage
val localCommitPublished = LocalCommitPublished(d.commitments.localCommit.publishableTxs._1.tx, Nil, Nil, Nil)
goto(CLOSING) using DATA_CLOSING(d.commitments, localCommitPublished = Some(localCommitPublished))
case Event(e: Error, d: DATA_NORMAL) => handleRemoteError(e, d)
})
@ -636,11 +643,11 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
case Event(BITCOIN_CLOSE_DONE, d: DATA_CLOSING) if d.mutualClosePublished.isDefined => goto(CLOSED)
case Event(BITCOIN_SPEND_OURS_DONE, d: DATA_CLOSING) if d.ourCommitPublished.isDefined => goto(CLOSED)
case Event(BITCOIN_SPEND_OURS_DONE, d: DATA_CLOSING) if d.localCommitPublished.isDefined => goto(CLOSED)
case Event(BITCOIN_SPEND_THEIRS_DONE, d: DATA_CLOSING) if d.theirCommitPublished.isDefined => goto(CLOSED)
case Event(BITCOIN_SPEND_THEIRS_DONE, d: DATA_CLOSING) if d.remoteCommitPublished.isDefined => goto(CLOSED)
case Event(BITCOIN_STEAL_DONE, d: DATA_CLOSING) if d.revokedPublished.size > 0 => goto(CLOSED)
case Event(BITCOIN_STEAL_DONE, d: DATA_CLOSING) if d.revokedCommitPublished.size > 0 => goto(CLOSED)
case Event(e: Error, d: DATA_CLOSING) => stay // nothing to do, there is already a spending tx published
}
@ -797,53 +804,54 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
val txs = txs1 ++ txs2
txs.map(tx => blockchain ! PublishAsap(tx))*/
// TODO: remove Nils
val localCommitPublished = LocalCommitPublished(tx, Nil, Nil, Nil)
val nextData = d match {
case closing: DATA_CLOSING => closing.copy(ourCommitPublished = Some(tx))
case _ => DATA_CLOSING(d.commitments, ourCommitPublished = Some(tx))
case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished))
case _ => DATA_CLOSING(d.commitments, localCommitPublished = Some(localCommitPublished))
}
goto(CLOSING) using nextData
}
def handleRemoteSpentCurrent(tx: Transaction, d: HasCommitments) = {
log.warning(s"they published their current commit in txid=${
tx.txid
}")
assert(tx.txid == d.commitments.remoteCommit.txid)
log.warning(s"they published their current commit in txid=${tx.txid}")
require(tx.txid == d.commitments.remoteCommit.txid, "txid mismatch")
blockchain ! WatchConfirmed(self, tx.txid, 3, BITCOIN_SPEND_THEIRS_DONE) // TODO hardcoded mindepth
val txs1: Seq[Transaction] = ???
//claimReceivedHtlcs(tx, Commitments.makeRemoteTxTemplate(d.commitments), d.commitments)
val txs2: Seq[Transaction] = ???
//claimSentHtlcs(tx, Commitments.makeRemoteTxTemplate(d.commitments), d.commitments)
val txs = txs1 ++ txs2
txs.map(tx => blockchain ! PublishAsap(tx))
val claimTxs = Helpers.Closing.claimRemoteCommitTxOutputs(d.commitments, tx)
claimTxs.map(txinfo => blockchain ! PublishAsap(txinfo.tx))
val remoteCommitPublished = RemoteCommitPublished(
commitTx = tx,
claimHtlcSuccessTxs = claimTxs.collect { case c: ClaimHtlcSuccessTx => c.tx },
claimHtlcTimeoutTxs = claimTxs.collect { case c: ClaimHtlcTimeoutTx => c.tx }
)
val nextData = d match {
case closing: DATA_CLOSING => closing.copy(theirCommitPublished = Some(tx))
case _ => DATA_CLOSING(d.commitments, theirCommitPublished = Some(tx))
case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished))
case _ => DATA_CLOSING(d.commitments, remoteCommitPublished = Some(remoteCommitPublished))
}
goto(CLOSING) using nextData
}
def handleRemoteSpentOther(tx: Transaction, d: HasCommitments) = {
log.warning(s"funding tx spent in txid=${
tx.txid
}")
log.warning(s"funding tx spent in txid=${tx.txid}")
d.commitments.txDb.get(tx.txid) match {
case Some(spendingTx) =>
log.warning(s"txid=${
tx.txid
} was a revoked commitment, publishing the punishment tx")
log.warning(s"txid=${tx.txid} was a revoked commitment, publishing the punishment tx")
them ! Error(0, "Anchor has been spent".getBytes)
blockchain ! Publish(spendingTx)
blockchain ! WatchConfirmed(self, spendingTx.txid, 3, BITCOIN_STEAL_DONE)
// TODO hardcoded mindepth
blockchain ! WatchConfirmed(self, spendingTx.txid, 3, BITCOIN_STEAL_DONE) // TODO hardcoded mindepth
val remoteCommitPublished = RevokedCommitPublished(tx)
val nextData = d match {
case closing: DATA_CLOSING => closing.copy(revokedPublished = closing.revokedPublished :+ tx)
case _ => DATA_CLOSING(d.commitments, revokedPublished = Seq(tx))
case closing: DATA_CLOSING => closing.copy(revokedCommitPublished = closing.revokedCommitPublished :+ remoteCommitPublished)
case _ => DATA_CLOSING(d.commitments, revokedCommitPublished = remoteCommitPublished :: Nil)
}
goto(CLOSING) using nextData
case None =>

View file

@ -121,6 +121,10 @@ trait HasCommitments extends Data {
def commitments: Commitments
}
case class LocalCommitPublished(commitTx: Transaction, htlcSuccessTxs: Seq[Transaction], htlcTimeoutTxs: Seq[Transaction], claimHtlcDelayedTx: Seq[Transaction])
case class RemoteCommitPublished(commitTx: Transaction, claimHtlcSuccessTxs: Seq[Transaction], claimHtlcTimeoutTxs: Seq[Transaction])
case class RevokedCommitPublished(commitTxs: Transaction)
final case class DATA_WAIT_FOR_OPEN_CHANNEL(localParams: LocalParams, autoSignInterval: Option[FiniteDuration]) extends Data
final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(temporaryChannelId: Long, localParams: LocalParams, fundingSatoshis: Long, pushMsat: Long, autoSignInterval: Option[FiniteDuration]) extends Data
final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: Long, params: ChannelParams, pushMsat: Long, remoteFirstPerCommitmentPoint: BinaryData) extends Data
@ -136,10 +140,10 @@ final case class DATA_NEGOTIATING(channelId: Long, params: ChannelParams, commit
final case class DATA_CLOSING(commitments: Commitments,
ourSignature: Option[ClosingSigned] = None,
mutualClosePublished: Option[Transaction] = None,
ourCommitPublished: Option[Transaction] = None,
theirCommitPublished: Option[Transaction] = None,
revokedPublished: Seq[Transaction] = Seq()) extends Data with HasCommitments {
assert(mutualClosePublished.isDefined || ourCommitPublished.isDefined || theirCommitPublished.isDefined || revokedPublished.size > 0, "there should be at least one tx published in this state")
localCommitPublished: Option[LocalCommitPublished] = None,
remoteCommitPublished: Option[RemoteCommitPublished] = None,
revokedCommitPublished: Seq[RevokedCommitPublished] = Nil) extends Data with HasCommitments {
require(mutualClosePublished.isDefined || localCommitPublished.isDefined || remoteCommitPublished.isDefined || revokedCommitPublished.size > 0, "there should be at least one tx published in this state")
}
final case class ChannelParams(localParams: LocalParams,

View file

@ -1,7 +1,7 @@
package fr.acinq.eclair.channel
import fr.acinq.bitcoin.Crypto.{Point, Scalar}
import fr.acinq.bitcoin.{BinaryData, Crypto, Satoshi, Transaction}
import fr.acinq.bitcoin.{BinaryData, Crypto, Satoshi}
import fr.acinq.eclair.crypto.LightningCrypto.sha256
import fr.acinq.eclair.crypto.{Generators, ShaChain}
import fr.acinq.eclair.transactions.Transactions._
@ -86,7 +86,7 @@ object Commitments {
}
def sendFulfill(commitments: Commitments, cmd: CMD_FULFILL_HTLC): (Commitments, UpdateFulfillHtlc) = {
commitments.localCommit.spec.htlcs.collectFirst { case u: Htlc if u.add.id == cmd.id => u.add } match {
commitments.localCommit.spec.htlcs.collectFirst { case u: Htlc if u.direction == IN && u.add.id == cmd.id => u.add } match {
case Some(htlc) if htlc.paymentHash == sha256(cmd.r) =>
val fulfill = UpdateFulfillHtlc(commitments.channelId, cmd.id, cmd.r)
val commitments1 = addLocalProposal(commitments, fulfill)
@ -97,7 +97,7 @@ object Commitments {
}
def receiveFulfill(commitments: Commitments, fulfill: UpdateFulfillHtlc): (Commitments, UpdateAddHtlc) = {
commitments.remoteCommit.spec.htlcs.collectFirst { case u: Htlc if u.add.id == fulfill.id => u.add } match {
commitments.remoteCommit.spec.htlcs.collectFirst { case u: Htlc if u.direction == IN && u.add.id == fulfill.id => u.add } match {
case Some(htlc) if htlc.paymentHash == sha256(fulfill.paymentPreimage) => (addRemoteProposal(commitments, fulfill), htlc)
case Some(htlc) => throw new RuntimeException(s"invalid htlc preimage for htlc id=${fulfill.id}")
case None => throw new RuntimeException(s"unknown htlc id=${fulfill.id}") // TODO: we should fail the channel
@ -220,7 +220,7 @@ object Commitments {
// TODO: Long or Int??
val revocation = RevokeAndAck(
channelId = commitments.channelId,
perCommitmentSecret = localPerCommitmentSecret,
perCommitmentSecret = localPerCommitmentSecret.toBin.take(32),
nextPerCommitmentPoint = localNextPerCommitmentPoint,
htlcTimeoutSignatures = timeoutHtlcSigs.toList
)
@ -238,7 +238,7 @@ object Commitments {
import commitments._
// we receive a revocation because we just sent them a sig for their next commit tx
remoteNextCommitInfo match {
case Left(_) if Scalar(revocation.perCommitmentSecret).toPoint != remoteCommit.remotePerCommitmentPoint =>
case Left(_) if Scalar(revocation.perCommitmentSecret :+ 1.toByte).toPoint != remoteCommit.remotePerCommitmentPoint =>
throw new RuntimeException("invalid preimage")
case Left(theirNextCommit) =>
// we rebuild the transactions a 2nd time but we are just interested in HTLC-timeout txs because we need to check their sig

View file

@ -3,10 +3,11 @@ package fr.acinq.eclair.channel
import fr.acinq.bitcoin.Script._
import fr.acinq.bitcoin.{OutPoint, _}
import fr.acinq.eclair.crypto.Generators
import fr.acinq.eclair.crypto.LightningCrypto.sha256
import fr.acinq.eclair.transactions.Scripts._
import fr.acinq.eclair.transactions.Transactions.{ClosingTx, CommitTx, InputInfo}
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire.ClosingSigned
import fr.acinq.eclair.wire.{ClosingSigned, UpdateAddHtlc, UpdateFulfillHtlc}
import scala.util.Try
@ -94,131 +95,49 @@ object Helpers {
}
/**
* Claim a revoked commit tx using the matching revocation preimage, which allows us to claim all its inputs without a
* delay
*
* @param theirTxTemplate revoked commit tx template
* @param revocationPreimage revocation preimage (which must match this specific commit tx)
* @param privateKey private key to send the claimed funds to (the returned tx will include a single P2WPKH output)
* @return a signed transaction that spends the revoked commit tx
*/
def claimRevokedCommitTx(theirTxTemplate: CommitTx, revocationPreimage: BinaryData, privateKey: BinaryData): Transaction = ???
/*{
val theirTx = theirTxTemplate.makeTx
val outputs = collection.mutable.ListBuffer.empty[TxOut]
// first, find out how much we can claim
val outputsToClaim = (theirTxTemplate.localOutput.toSeq ++ theirTxTemplate.htlcReceivedOutputs ++ theirTxTemplate.htlcOfferedOutputs).filter(o => theirTx.txOut.indexOf(o.txOut) != -1)
val totalAmount = outputsToClaim.map(_.amount).sum // TODO: substract a small network fee
// create a tx that sends everything to our private key
val tx = Transaction(version = 2,
txIn = Seq.empty[TxIn],
txOut = TxOut(totalAmount, pay2wpkh(Crypto.publicKeyFromPrivateKey(privateKey))) :: Nil,
lockTime = 0)
// create tx inputs that spend each output that we can spend
val inputs = outputsToClaim.map(outputTemplate => {
val index = theirTx.txOut.indexOf(outputTemplate.txOut)
TxIn(OutPoint(theirTx, index), signatureScript = BinaryData.empty, sequence = 0xffffffffL)
})
assert(inputs.length == outputsToClaim.length)
// and sign them
val tx1 = tx.copy(txIn = inputs)
val witnesses = for (i <- 0 until tx1.txIn.length) yield {
val sig = Transaction.signInput(tx1, i, outputsToClaim(i).redeemScript, SIGHASH_ALL, outputsToClaim(i).amount, 1, privateKey)
val witness = ScriptWitness(sig :: revocationPreimage :: outputsToClaim(i).redeemScript :: Nil)
witness
}
tx1.updateWitnesses(witnesses)
}*/
/**
* claim an HTLC that we received using its payment preimage. This is used only when the other party publishes its
* current commit tx which contains pending HTLCs.
*
* @param tx commit tx published by the other party
* @param htlcTemplate HTLC template for an HTLC in the commit tx for which we have the preimage
* @param paymentPreimage HTLC preimage
* @param privateKey private key which matches the pubkey that the HTLC was sent to
* @return a signed transaction that spends the HTLC in their published commit tx.
* This tx is not spendable right away: it has both an absolute CLTV time-out and a relative CSV time-out
* before which it can be published
*/
//def claimReceivedHtlc(tx: Transaction, htlcTemplate: ReceivedHTLC, paymentPreimage: BinaryData, privateKey: BinaryData): Transaction = ???
/*{
require(htlcTemplate.htlc.add.paymentHash == BinaryData(Crypto.sha256(paymentPreimage)), "invalid payment preimage")
// find its index in their tx
val index = tx.txOut.indexOf(htlcTemplate.txOut)
val tx1 = Transaction(version = 2,
txIn = TxIn(OutPoint(tx, index), BinaryData.empty, sequence = Common.toSelfDelay2csv(htlcTemplate.delay)) :: Nil,
txOut = TxOut(htlcTemplate.amount, Common.pay2pkh(Crypto.publicKeyFromPrivateKey(privateKey))) :: Nil,
lockTime = ??? /*Scripts.locktime2long_cltv(htlcTemplate.htlc.add.expiry)*/)
val sig = Transaction.signInput(tx1, 0, htlcTemplate.redeemScript, SIGHASH_ALL, htlcTemplate.amount, 1, privateKey)
val witness = ScriptWitness(sig :: paymentPreimage :: htlcTemplate.redeemScript :: Nil)
val tx2 = tx1.updateWitness(0, witness)
tx2
}*/
/**
* claim all the HTLCs that we've received from their current commit tx
*
* @param txTemplate commit tx published by the other party
* @param commitments our commitment data, which include payment preimages
* @return a list of transactions (one per HTLC that we can claim)
*/
//def claimReceivedHtlcs(tx: Transaction, txTemplate: CommitTxTemplate, commitments: Commitments): Seq[Transaction] = ???
/*{
val preImages = commitments.localChanges.all.collect { case UpdateFulfillHtlc(_, id, paymentPreimage) => paymentPreimage }
// TODO: FIXME !!!
//val htlcTemplates = txTemplate.htlcSent
val htlcTemplates = txTemplate.htlcReceived ++ txTemplate.htlcSent
def claimRemoteCommitTxOutputs(commitments: Commitments, tx: Transaction): Seq[TransactionWithInputInfo] = {
import commitments._
require(remoteCommit.txid == tx.txid, "txid mismatch, provided tx is not the current remote commit tx")
val (remoteCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = Commitments.makeRemoteTxs(remoteCommit.index, localParams, remoteParams, commitInput, remoteCommit.remotePerCommitmentPoint, remoteCommit.spec)
require(remoteCommitTx.tx.txid == tx.txid, "txid mismatch, cannot recompute the current remote commit tx")
//@tailrec
def loop(htlcs: Seq[HTLCTemplate], acc: Seq[Transaction] = Seq.empty[Transaction]): Seq[Transaction] = Nil
val localPubkey = Generators.derivePubKey(localParams.paymentKey.toPoint, remoteCommit.remotePerCommitmentPoint)
val remotePubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remoteCommit.remotePerCommitmentPoint)
val localPrivkey = Generators.derivePrivKey(localParams.paymentKey, remoteCommit.remotePerCommitmentPoint)
/*{
htlcs.headOption match {
case Some(head) =>
preImages.find(preImage => head.htlc.add.rHash == bin2sha256(Crypto.sha256(preImage))) match {
case Some(preImage) => loop(htlcs.tail, claimReceivedHtlc(tx, head, preImage, commitments.ourParams.finalPrivKey) +: acc)
case None => loop(htlcs.tail, acc)
}
case None => acc
// those are the preimages to existing received htlcs
val preimages = commitments.localChanges.all.collect { case u: UpdateFulfillHtlc => u.paymentPreimage }
// TODO: final key is the payment pubkey so that it matches the main outputs, is that the best option?
// remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa
val txes = commitments.remoteCommit.spec.htlcs.collect {
// incoming htlc for which we have the preimage: we spend it directly
case Htlc(OUT, add: UpdateAddHtlc, _) if preimages.exists(r => sha256(r) == add.paymentHash) =>
val preimage = preimages.find(r => sha256(r) == add.paymentHash).get
val tx = Transactions.makeClaimHtlcSuccessTx(remoteCommitTx.tx, localPubkey, remotePubkey, localPubkey, add)
val sig = Transactions.sign(tx, localPrivkey)
Transactions.addSigs(tx, sig, preimage)
// NB: incoming htlc for which we don't have the preimage: nothing to do, it will timeout eventually and they will get their funds back
// outgoing htlc: they may or may not have the preimage, the only thing to do is try to get back our funds after timeout
case Htlc(IN, add: UpdateAddHtlc, _) =>
val tx = Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, localPubkey, remotePubkey, localPubkey, add)
val sig = Transactions.sign(tx, localPrivkey)
Transactions.addSigs(tx, sig)
}
}*/
loop(htlcTemplates)
}*/
//def claimSentHtlc(tx: Transaction, htlcTemplate: OfferedHTLCOutputTemplate, privateKey: BinaryData): Transaction = ???
/*{
val index = tx.txOut.indexOf(htlcTemplate.txOut)
val tx1 = Transaction(
version = 2,
txIn = TxIn(OutPoint(tx, index), Array.emptyByteArray, sequence = Common.toSelfDelay2csv(htlcTemplate.delay)) :: Nil,
txOut = TxOut(htlcTemplate.amount, Common.pay2pkh(Crypto.publicKeyFromPrivateKey(privateKey))) :: Nil,
lockTime = ??? /*Scripts.locktime2long_cltv(htlcTemplate.htlc.add.expiry)*/)
// OPTIONAL: let's check transactions are actually spendable
require(txes.forall(Transactions.checkSpendable(_).isSuccess), "the tx we produced are not spendable!")
val sig = Transaction.signInput(tx1, 0, htlcTemplate.redeemScript, SIGHASH_ALL, htlcTemplate.amount, 1, privateKey)
val witness = ScriptWitness(sig :: Hash.Zeroes :: htlcTemplate.redeemScript :: Nil)
tx1.updateWitness(0, witness)
}*/
txes.toSeq
}
// TODO: fix this!
//def claimSentHtlcs(tx: Transaction, txTemplate: CommitTxTemplate, commitments: Commitments): Seq[Transaction] = Nil
/*{
// txTemplate could be our template (we published our commit tx) or their template (they published their commit tx)
val htlcs1 = txTemplate.htlcSent.filter(_.ourKey == commitments.ourParams.finalPubKey)
val htlcs2 = txTemplate.htlcReceived.filter(_.theirKey == commitments.ourParams.finalPubKey)
val htlcs = htlcs1 ++ htlcs2
htlcs.map(htlcTemplate => claimSentHtlc(tx, htlcTemplate, commitments.ourParams.finalPrivKey))
}*/
}
}

View file

@ -13,7 +13,7 @@ object Generators {
case length if length < 32 => Array.fill(32 - length)(0.toByte) ++ data
}
def perCommitSecret(seed: BinaryData, index: Int): Scalar = Scalar(ShaChain.shaChainFromSeed(seed, index) :+ 1.toByte)
def perCommitSecret(seed: BinaryData, index: Int): Scalar = Scalar(ShaChain.shaChainFromSeed(seed, 0xFFFFFFFFFFFFFFFFL - index) :+ 1.toByte)
def perCommitPoint(seed: BinaryData, index: Int): Point = perCommitSecret(seed, index).toPoint

View file

@ -20,6 +20,7 @@ object Transactions {
def input: InputInfo
def tx: Transaction
}
case class CommitTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo
case class HtlcSuccessTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo
case class HtlcTimeoutTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo
@ -29,6 +30,18 @@ object Transactions {
case class ClosingTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo
// @formatter:on
/**
* When *local* *current* [[CommitTx]] is published:
* - [[HtlcSuccessTx]] spends htlc-received outputs of [[CommitTx]] for which we have the preimage
* - [[ClaimHtlcDelayedTx]] spends [[HtlcSuccessTx]] after a delay
* - [[HtlcTimeoutTx]] spends htlc-sent outputs of [[CommitTx]] after a timeout
* - [[ClaimHtlcDelayedTx]] spends [[HtlcTimeoutTx]] after a delay
*
* When *remote* *current* [[CommitTx]] is published:
* - [[ClaimHtlcSuccessTx]] spends htlc-received outputs of [[CommitTx]] for which we have the preimage
* - [[ClaimHtlcTimeoutTx]] spends htlc-received outputs of [[CommitTx]] for which we have the preimage
*/
val commitWeight = 724
val htlcTimeoutWeight = 634
val htlcSuccessWeight = 671
@ -114,6 +127,7 @@ object Transactions {
.filter(htlc => (MilliSatoshi(htlc.add.amountMsat) - htlcSuccessFee).compare(localDustLimit) > 0)
.map(htlc => TxOut(MilliSatoshi(htlc.add.amountMsat), pay2wsh(htlcReceived(localPubkey, remotePubkey, ripemd160(htlc.add.paymentHash), htlc.add.expiry))))
// TODO: txnumber can't be > 2^48
val txnumber = obscuredCommitTxNumber(commitTxNumber, localPaymentBasePoint, remotePaymentBasePoint)
val tx = Transaction(

View file

@ -45,6 +45,7 @@ trait StateTestsHelperMethods extends TestKitBase {
r2s.expectMsgType[RevokeAndAck]
r2s.forward(s)
awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.localCommit.index == rCommitIndex + 1)
awaitCond(s.stateData.asInstanceOf[HasCommitments].commitments.remoteCommit.index == rCommitIndex + 1)
}
}

View file

@ -261,19 +261,19 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
within(30 seconds) {
val sender = TestProbe()
val (r1, htlc1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
// a->b (regular)
val (r2, htlc2) = addHtlc(8000000, alice, bob, alice2bob, bob2alice)
// a->b (regular)
val (r3, htlc3) = addHtlc(300000, bob, alice, bob2alice, alice2bob)
// b->a (dust)
val (r4, htlc4) = addHtlc(1000000, alice, bob, alice2bob, bob2alice)
// a->b (regular)
val (r5, htlc5) = addHtlc(50000000, bob, alice, bob2alice, alice2bob)
// b->a (regular)
val (r6, htlc6) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
// a->b (dust)
val (r7, htlc7) = addHtlc(4000000, bob, alice, bob2alice, alice2bob) // b->a (regular)
val (r1, htlc1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) // a->b (regular)
val (r2, htlc2) = addHtlc(8000000, alice, bob, alice2bob, bob2alice) // a->b (regular)
val (r3, htlc3) = addHtlc(300000, bob, alice, bob2alice, alice2bob) // b->a (dust)
val (r4, htlc4) = addHtlc(1000000, alice, bob, alice2bob, bob2alice) // a->b (regular)
val (r5, htlc5) = addHtlc(50000000, bob, alice, bob2alice, alice2bob) // b->a (regular)
val (r6, htlc6) = addHtlc(500000, alice, bob, alice2bob, bob2alice) // a->b (dust)
val (r7, htlc7) = addHtlc(4000000, bob, alice, bob2alice, alice2bob) // b->a (regular)
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
@ -288,6 +288,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
bob2alice.expectMsgType[CommitSig]
bob2alice.forward(alice)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 1)
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs._2.size == 3)
}
}
@ -395,19 +396,19 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
test("recv RevokeAndAck (multiple htlcs in both directions)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain) =>
within(30 seconds) {
val sender = TestProbe()
val (r1, htlc1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
// a->b (regular)
val (r2, htlc2) = addHtlc(8000000, alice, bob, alice2bob, bob2alice)
// a->b (regular)
val (r3, htlc3) = addHtlc(300000, bob, alice, bob2alice, alice2bob)
// b->a (dust)
val (r4, htlc4) = addHtlc(1000000, alice, bob, alice2bob, bob2alice)
// a->b (regular)
val (r5, htlc5) = addHtlc(50000000, bob, alice, bob2alice, alice2bob)
// b->a (regular)
val (r6, htlc6) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
// a->b (dust)
val (r7, htlc7) = addHtlc(4000000, bob, alice, bob2alice, alice2bob) // b->a (regular)
val (r1, htlc1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) // a->b (regular)
val (r2, htlc2) = addHtlc(8000000, alice, bob, alice2bob, bob2alice) // a->b (regular)
val (r3, htlc3) = addHtlc(300000, bob, alice, bob2alice, alice2bob) // b->a (dust)
val (r4, htlc4) = addHtlc(1000000, alice, bob, alice2bob, bob2alice) // a->b (regular)
val (r5, htlc5) = addHtlc(50000000, bob, alice, bob2alice, alice2bob) // b->a (regular)
val (r6, htlc6) = addHtlc(500000, alice, bob, alice2bob, bob2alice) // a->b (dust)
val (r7, htlc7) = addHtlc(4000000, bob, alice, bob2alice, alice2bob) // b->a (regular)
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
@ -425,7 +426,10 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
// actual test begins
alice2bob.expectMsgType[RevokeAndAck]
alice2bob.forward(bob)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight)
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteCommit.index == 1)
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteCommit.spec.htlcs.size == 7)
}
}
@ -782,32 +786,25 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
within(30 seconds) {
val sender = TestProbe()
val (r1, htlc1) = addHtlc(300000000, alice, bob, alice2bob, bob2alice)
// id 1
val (r2, htlc2) = addHtlc(200000000, alice, bob, alice2bob, bob2alice)
// id 2
val (r3, htlc3) = addHtlc(100000000, alice, bob, alice2bob, bob2alice) // id 3
val (ra1, htlca1) = addHtlc(250000000, alice, bob, alice2bob, bob2alice)
val (ra2, htlca2) = addHtlc(100000000, alice, bob, alice2bob, bob2alice)
val (rb1, htlcb1) = addHtlc(50000000, bob, alice, bob2alice, alice2bob)
val (rb2, htlcb2) = addHtlc(55000000, bob, alice, bob2alice, alice2bob)
sign(alice, bob, alice2bob, bob2alice)
fulfillHtlc(1, r1, bob, alice, bob2alice, alice2bob)
sign(bob, alice, bob2alice, alice2bob)
sign(alice, bob, alice2bob, bob2alice)
val (r4, htlc4) = addHtlc(150000000, bob, alice, bob2alice, alice2bob)
// id 1
val (r5, htlc5) = addHtlc(120000000, bob, alice, bob2alice, alice2bob) // id 2
sign(bob, alice, bob2alice, alice2bob)
sign(alice, bob, alice2bob, bob2alice)
fulfillHtlc(2, r2, bob, alice, bob2alice, alice2bob)
fulfillHtlc(1, r4, alice, bob, alice2bob, bob2alice)
fulfillHtlc(2, ra2, bob, alice, bob2alice, alice2bob)
fulfillHtlc(1, rb1, alice, bob, alice2bob, bob2alice)
// at this point here is the situation from alice pov and what she should do :
// balances :
// alice's balance : 400 000 000 => nothing to do
// bob's balance : 30 000 000 => nothing to do
// alice's balance : 450 000 000 => nothing to do
// bob's balance : 95 000 000 => nothing to do
// htlcs :
// alice -> bob : 200 000 000 (bob has the r) => if bob does not use the r, wait for the timeout and spend
// alice -> bob : 100 000 000 (bob does not have the r) => wait for the timeout and spend
// bob -> alice : 150 000 000 (alice has the r) => spend immediately using the r
// bob -> alice : 120 000 000 (alice does not have the r) => nothing to do, bob will get his money back after the timeout
// alice -> bob : 250 000 000 (bob does not have the preimage) => wait for the timeout and spend
// alice -> bob : 100 000 000 (bob has the preimage) => if bob does not use the preimage, wait for the timeout and spend
// bob -> alice : 50 000 000 (alice has the preimage) => spend immediately using the preimage
// bob -> alice : 55 000 000 (alice does not have the preimage) => nothing to do, bob will get his money back after the timeout
// bob publishes his current commit tx
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs._1.tx
@ -816,8 +813,8 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid
// alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage
val amountClaimed = (for (i <- 0 until 3) yield {
// alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the htlc
val claimHtlcTx = alice2blockchain.expectMsgType[PublishAsap].tx
assert(claimHtlcTx.txIn.size == 1)
val previousOutputs = Map(claimHtlcTx.txIn(0).outPoint -> bobCommitTx.txOut(claimHtlcTx.txIn(0).outPoint.index.toInt))
@ -825,10 +822,12 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
assert(claimHtlcTx.txOut.size == 1)
claimHtlcTx.txOut(0).amount
}).sum
assert(amountClaimed == Satoshi(450000))
assert(amountClaimed == Satoshi(400000))
awaitCond(alice.stateName == CLOSING)
assert(alice.stateData.asInstanceOf[DATA_CLOSING].theirCommitPublished == Some(bobCommitTx))
assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined)
assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimHtlcSuccessTxs.size == 1)
assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimHtlcTimeoutTxs.size == 2)
}
}
@ -836,25 +835,12 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
within(30 seconds) {
val sender = TestProbe()
// alice sends 300 000 sat and bob fulfills
// we reuse the same r (it doesn't matter here)
val (r, htlc) = addHtlc(300000000, alice, bob, alice2bob, bob2alice)
sign(alice, bob, alice2bob, bob2alice)
sender.send(bob, CMD_FULFILL_HTLC(1, r))
sender.expectMsg("ok")
val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc]
bob2alice.forward(alice)
sign(bob, alice, bob2alice, alice2bob)
// at this point we have :
// alice = 700 000
// bob = 300 000
// initally we have :
// alice = 800 000
// bob = 200 000
def send(): Transaction = {
// alice sends 1 000 sat
// we reuse the same r (it doesn't matter here)
val (r, htlc) = addHtlc(1000000, alice, bob, alice2bob, bob2alice)
// alice sends 10 000 sat
val (r, htlc) = addHtlc(10000000, alice, bob, alice2bob, bob2alice)
sign(alice, bob, alice2bob, bob2alice)
bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs._1.tx
@ -866,9 +852,12 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
// let's say that bob published this tx
val revokedTx = txs(3)
// channel state for this revoked tx is as follows:
// alice = 696 000
// bob = 300 000
// a->b = 4 000
// alice = 760 000
// bob = 200 000
// a->b = 10 000
// a->b = 10 000
// a->b = 10 000
// a->b = 10 000
alice ! (BITCOIN_FUNDING_SPENT, revokedTx)
alice2bob.expectMsgType[Error]
val punishTx = alice2blockchain.expectMsgType[Publish].tx
@ -932,7 +921,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
assert(amountClaimed == Satoshi(450000))
awaitCond(alice.stateName == CLOSING)
assert(alice.stateData.asInstanceOf[DATA_CLOSING].ourCommitPublished == Some(aliceCommitTx))
assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished == Some(aliceCommitTx))
}
}

View file

@ -398,7 +398,7 @@ class ShutdownStateSpec extends StateSpecBaseClass with StateTestsHelperMethods
assert(amountClaimed == Satoshi(500000))
awaitCond(alice.stateName == CLOSING)
assert(alice.stateData.asInstanceOf[DATA_CLOSING].theirCommitPublished == Some(bobCommitTx))
assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished == Some(bobCommitTx))
}
}
@ -439,7 +439,7 @@ class ShutdownStateSpec extends StateSpecBaseClass with StateTestsHelperMethods
alice2blockchain.expectNoMsg()
awaitCond(alice.stateName == CLOSING)
assert(alice.stateData.asInstanceOf[DATA_CLOSING].ourCommitPublished == Some(aliceCommitTx))
assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished == Some(aliceCommitTx))
}
}

View file

@ -154,7 +154,7 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
alice2blockchain.expectMsgType[WatchConfirmed].txId == aliceCommitTx.txid
awaitCond(alice.stateName == CLOSING)
val initialState = alice.stateData.asInstanceOf[DATA_CLOSING]
assert(initialState.ourCommitPublished == Some(aliceCommitTx))
assert(initialState.localCommitPublished == Some(aliceCommitTx))
// actual test starts here
alice ! (BITCOIN_FUNDING_SPENT, aliceCommitTx)
@ -211,7 +211,7 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
alice2blockchain.expectMsg(Publish(aliceCommitTx))
alice2blockchain.expectMsgType[WatchConfirmed].txId == aliceCommitTx.txid
awaitCond(alice.stateName == CLOSING)
assert(alice.stateData.asInstanceOf[DATA_CLOSING].ourCommitPublished == Some(aliceCommitTx))
assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished == Some(aliceCommitTx))
// actual test starts here
alice ! BITCOIN_SPEND_OURS_DONE
@ -229,7 +229,7 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(theirCommitPublished = Some(bobCommitTx)))
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(remoteCommitPublished = Some(RemoteCommitPublished(bobCommitTx, Nil, Nil))))
}
}
@ -241,7 +241,7 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
assert(bobCommitTx.txOut.size == 2) // two main outputs
alice ! (BITCOIN_FUNDING_SPENT, bobCommitTx)
alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(theirCommitPublished = Some(bobCommitTx)))
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(remoteCommitPublished = Some(RemoteCommitPublished(bobCommitTx, Nil, Nil))))
// actual test starts here
alice ! BITCOIN_SPEND_THEIRS_DONE
@ -260,7 +260,7 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
alice2blockchain.expectMsgType[Publish]
alice2blockchain.expectMsgType[WatchConfirmed]
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(revokedPublished = Seq(bobRevokedTx)))
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(revokedCommitPublished = Seq(RevokedCommitPublished(bobRevokedTx))))
}
}
@ -273,9 +273,9 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
// alice publishes and watches the stealing tx
alice2blockchain.expectMsgType[Publish]
alice2blockchain.expectMsgType[WatchConfirmed]
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(revokedPublished = initialState.revokedPublished :+ bobRevokedTx))
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(revokedCommitPublished = initialState.revokedCommitPublished :+ RevokedCommitPublished(bobRevokedTx)))
}
assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedPublished.size == bobCommitTxes.size - 1)
assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == bobCommitTxes.size - 1)
}
}
@ -288,7 +288,7 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
// alice publishes and watches the stealing tx
alice2blockchain.expectMsgType[Publish]
alice2blockchain.expectMsgType[WatchConfirmed]
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(revokedPublished = Seq(bobRevokedTx)))
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(revokedCommitPublished = Seq(RevokedCommitPublished(bobRevokedTx))))
// actual test starts here
alice ! BITCOIN_STEAL_DONE