diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 679f4a5915..22d61f6c7a 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -79,10 +79,10 @@ true - https://bitcoin.org/bin/bitcoin-core-0.16.3/bitcoin-0.16.3-x86_64-linux-gnu.tar.gz + https://bitcoin.org/bin/bitcoin-core-0.17.1/bitcoin-0.17.1-x86_64-linux-gnu.tar.gz - c371e383f024c6c45fb255d528a6beec - e6d8ab1f7661a6654fd81e236b9b5fd35a3d4dcb + 724043999e2b5ed0c088e8db34f15d43 + 546ee35d4089c7ccc040a01cdff3362599b8bc53 @@ -93,10 +93,10 @@ - https://bitcoin.org/bin/bitcoin-core-0.16.3/bitcoin-0.16.3-osx64.tar.gz + https://bitcoin.org/bin/bitcoin-core-0.17.1/bitcoin-0.17.1-osx64.tar.gz - bacd87d0c3f65a5acd666e33d094a59e - 62cc5bd9ced610bb9e8d4a854396bfe2139e3d0f + b5a792c6142995faa42b768273a493bd + 8bd51c7024d71de07df381055993e9f472013db8 @@ -107,9 +107,9 @@ - https://bitcoin.org/bin/bitcoin-core-0.16.3/bitcoin-0.16.3-win64.zip - bbde9b1206956d19298034319e9f405e - 85e3dc8a9c6f93b1b20cb79fa5850b5ce81da221 + https://bitcoin.org/bin/bitcoin-core-0.17.1/bitcoin-0.17.1-win64.zip + b0e824e9dd02580b5b01f073f3c89858 + 4e17bad7d08c465b444143a93cd6eb1c95076e3f diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index 9ff59b4b00..b14755e275 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -36,12 +36,13 @@ object Features { val CHANNEL_RANGE_QUERIES_BIT_MANDATORY = 6 val CHANNEL_RANGE_QUERIES_BIT_OPTIONAL = 7 + val OPTION_SIMPLIFIED_COMMITMENT_MANDATORY = 8 + val OPTION_SIMPLIFIED_COMMITMENT_OPTIONAL = 9 def hasFeature(features: BitSet, bit: Int): Boolean = features.get(bit) def hasFeature(features: ByteVector, bit: Int): Boolean = hasFeature(BitSet.valueOf(features.reverse.toArray), bit) - /** * Check that the features that we understand are correctly specified, and that there are no mandatory features that * we don't understand (even bits) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 9c94257838..ffe2c095d8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -126,15 +126,13 @@ class Setup(datadir: File, } yield (progress, chainHash, bitcoinVersion, unspentAddresses, blocks, headers) // blocking sanity checks val (progress, chainHash, bitcoinVersion, unspentAddresses, blocks, headers) = await(future, 30 seconds, "bicoind did not respond after 30 seconds") - assert(bitcoinVersion >= 160300, "Eclair requires Bitcoin Core 0.16.3 or higher") + assert(bitcoinVersion >= 170000, "Eclair requires Bitcoin Core 0.17.0 or higher") assert(chainHash == nodeParams.chainHash, s"chainHash mismatch (conf=${nodeParams.chainHash} != bitcoind=$chainHash)") if (chainHash != Block.RegtestGenesisBlock.hash) { assert(unspentAddresses.forall(address => !isPay2PubkeyHash(address)), "Make sure that all your UTXOS are segwit UTXOS and not p2pkh (check out our README for more details)") } assert(progress > 0.999, s"bitcoind should be synchronized (progress=$progress") assert(headers - blocks <= 1, s"bitcoind should be synchronized (headers=$headers blocks=$blocks") - // TODO: add a check on bitcoin version? - Bitcoind(bitcoinClient) case ELECTRUM => val addresses = config.hasPath("electrum") match { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala index 9a7f3bc4d4..008326fa00 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala @@ -19,10 +19,12 @@ package fr.acinq.eclair.blockchain.bitcoind import fr.acinq.bitcoin._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain._ -import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, JsonRPCError} +import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, Error, JsonRPCError} import fr.acinq.eclair.transactions.Transactions import grizzled.slf4j.Logging +import org.json4s.DefaultFormats import org.json4s.JsonAST._ +import org.json4s.jackson.Serialization import scodec.bits.ByteVector import scala.concurrent.{ExecutionContext, Future} @@ -47,9 +49,13 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC def fundTransaction(tx: Transaction, lockUnspents: Boolean, feeRatePerKw: Long): Future[FundTransactionResponse] = fundTransaction(Transaction.write(tx).toHex, lockUnspents, feeRatePerKw) def signTransaction(hex: String): Future[SignTransactionResponse] = - rpcClient.invoke("signrawtransaction", hex).map(json => { + rpcClient.invoke("signrawtransactionwithwallet", hex).map(json => { val JString(hex) = json \ "hex" val JBool(complete) = json \ "complete" + if (!complete) { + val message = (json \ "errors" \\ classOf[JString]).mkString(",") + throw new JsonRPCError(Error(-1, message)) + } SignTransactionResponse(Transaction.read(hex), complete) }) @@ -74,7 +80,7 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC private def signTransactionOrUnlock(tx: Transaction): Future[SignTransactionResponse] = { val f = signTransaction(tx) - // if signature fails (e.g. because wallet is uncrypted) we need to unlock the utxos + // if signature fails (e.g. because wallet is encrypted) we need to unlock the utxos f.recoverWith { case _ => unlockOutpoints(tx.txIn.map(_.outPoint)) .recover { case t: Throwable => logger.warn(s"Cannot unlock failed transaction's UTXOs txid=${tx.txid}", t); t } // no-op, just add a log in case of failure @@ -94,7 +100,7 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC // we ask bitcoin core to add inputs to the funding tx, and use the specified change address FundTransactionResponse(unsignedFundingTx, _, fee) <- fundTransaction(partialFundingTx, lockUnspents = true, feeRatePerKw) // now let's sign the funding tx - SignTransactionResponse(fundingTx, _) <- signTransactionOrUnlock(unsignedFundingTx) + SignTransactionResponse(fundingTx, true) <- signTransactionOrUnlock(unsignedFundingTx) // there will probably be a change output, so we need to find which output is ours outputIndex = Transactions.findPubKeyScriptIndex(fundingTx, pubkeyScript, outputsAlreadyUsed = Set.empty, amount_opt = None) _ = logger.debug(s"created funding txid=${fundingTx.txid} outputIndex=$outputIndex fee=$fee") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 671be23852..b1ea67d3ec 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -339,7 +339,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu // let's create the first commitment tx that spends the yet uncommitted funding tx val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.maxFeerateMismatch) require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) + val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath), SIGHASH_ALL) // signature of their initial commitment tx that pays remote pushMsat val fundingCreated = FundingCreated( temporaryChannelId = temporaryChannelId, @@ -377,12 +377,12 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, temporaryChannelId, localParams, remoteParams, fundingSatoshis: Long, pushMsat, initialFeeratePerKw, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.maxFeerateMismatch) // check remote signature validity - val localSigOfLocalTx = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) + val localSigOfLocalTx = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath), SIGHASH_ALL) val signedLocalCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) Transactions.checkSpendable(signedLocalCommitTx) match { case Failure(cause) => handleLocalError(InvalidCommitmentSignature(temporaryChannelId, signedLocalCommitTx.tx), d, None) case Success(_) => - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) + val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath), SIGHASH_ALL) val channelId = toLongId(fundingTxHash, fundingTxOutputIndex) // watch the funding tx transaction val commitInput = localCommitTx.input @@ -390,13 +390,20 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu channelId = channelId, signature = localSigOfRemoteTx ) + + val commitmentVersion = Helpers.canUseSimplifiedCommitment(localParams, remoteParams) match { + case true => VersionSimplifiedCommitment + case false => VersionCommitmentV1 + } + val commitments = Commitments(localParams, remoteParams, channelFlags, LocalCommit(0, localSpec, PublishableTxs(signedLocalCommitTx, Nil)), RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), localNextHtlcId = 0L, remoteNextHtlcId = 0L, originChannels = Map.empty, remoteNextCommitInfo = Right(randomKey.publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array, - commitInput, ShaChain.init, channelId = channelId) + commitInput, ShaChain.init, channelId = channelId, version = commitmentVersion) + context.parent ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId)) context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) @@ -419,7 +426,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu when(WAIT_FOR_FUNDING_SIGNED)(handleExceptions { case Event(msg@FundingSigned(_, remoteSig), d@DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, channelFlags, fundingCreated)) => // we make sure that their sig checks out and that our first commit tx is spendable - val localSigOfLocalTx = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) + val localSigOfLocalTx = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath), SIGHASH_ALL) val signedLocalCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) Transactions.checkSpendable(signedLocalCommitTx) match { case Failure(cause) => @@ -429,13 +436,18 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu handleLocalError(InvalidCommitmentSignature(channelId, signedLocalCommitTx.tx), d, Some(msg)) case Success(_) => val commitInput = localCommitTx.input + val commitmentVersion = Helpers.canUseSimplifiedCommitment(localParams, remoteParams) match { + case true => VersionSimplifiedCommitment + case false => VersionCommitmentV1 + } + val commitments = Commitments(localParams, remoteParams, channelFlags, LocalCommit(0, localSpec, PublishableTxs(signedLocalCommitTx, Nil)), remoteCommit, LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), localNextHtlcId = 0L, remoteNextHtlcId = 0L, originChannels = Map.empty, remoteNextCommitInfo = Right(randomKey.publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array - commitInput, ShaChain.init, channelId = channelId) + commitInput, ShaChain.init, channelId = channelId, version = commitmentVersion) val now = Platform.currentTime / 1000 context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) log.info(s"publishing funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") @@ -645,7 +657,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu val nextCommitNumber = nextRemoteCommit.index // we persist htlc data in order to be able to claim htlc outputs in case a revoked tx is published by our // counterparty, so only htlcs above remote's dust_limit matter - val trimmedHtlcs = Transactions.trimOfferedHtlcs(Satoshi(d.commitments.remoteParams.dustLimitSatoshis), nextRemoteCommit.spec) ++ Transactions.trimReceivedHtlcs(Satoshi(d.commitments.remoteParams.dustLimitSatoshis), nextRemoteCommit.spec) + val trimmedHtlcs = Transactions.trimOfferedHtlcs(Satoshi(d.commitments.remoteParams.dustLimitSatoshis), nextRemoteCommit.spec, d.commitments.version) ++ Transactions.trimReceivedHtlcs(Satoshi(d.commitments.remoteParams.dustLimitSatoshis), nextRemoteCommit.spec, d.commitments.version) trimmedHtlcs collect { case DirectedHtlc(_, u) => log.info(s"adding paymentHash=${u.paymentHash} cltvExpiry=${u.cltvExpiry} to htlcs db for commitNumber=$nextCommitNumber") @@ -775,8 +787,12 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu // are there pending signed htlcs on either side? we need to have received their last revocation! if (d.commitments.hasNoPendingHtlcs) { // there are no pending signed htlcs, let's go directly to NEGOTIATING - if (d.commitments.localParams.isFunder) { - // we are funder, need to initiate the negotiation by sending the first closing_signed + if (d.commitments.localParams.isFunder && d.commitments.version == VersionCommitmentV1) { + // we are funder and we're using commitmentV1, need to initiate the negotiation by sending the first closing_signed + val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey) + goto(NEGOTIATING) using store(DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None)) sending sendList :+ closingSigned + } else if(!d.commitments.localParams.isFunder && d.commitments.version == VersionSimplifiedCommitment) { + // we are fundee BUT we're using option_simplified_commitment, need to initiate the negotiation by sending the first closing_signed val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey) goto(NEGOTIATING) using store(DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None)) sending sendList :+ closingSigned } else { @@ -1031,8 +1047,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu } if (commitments1.hasNoPendingHtlcs) { log.debug(s"switching to NEGOTIATING spec:\n${Commitments.specs2String(commitments1)}") - if (d.commitments.localParams.isFunder) { - // we are funder, need to initiate the negotiation by sending the first closing_signed + if (d.commitments.localParams.isFunder || commitments1.version == VersionSimplifiedCommitment) { + // we are funder or we're using option_simplified_commitmemt, need to initiate the negotiation by sending the first closing_signed val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey) goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None)) sending closingSigned } else { @@ -1055,7 +1071,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu case Event(c@CurrentFeerates(feerates), d: DATA_SHUTDOWN) => val networkFeeratePerKw = feerates.blocks_2 - d.commitments.localParams.isFunder match { + d.commitments.localParams.isFunder match { // TODO --> "&& !d.commitments.isSimplifiedCommitment" case true if Helpers.shouldUpdateFee(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.updateFeeMinDiffRatio) => self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true) stay @@ -1212,6 +1228,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu case Event(WatchEventConfirmed(BITCOIN_TX_CONFIRMED(tx), blockHeight, _), d: DATA_CLOSING) => log.info(s"txid=${tx.txid} has reached mindepth, updating closing state") + implicit val commitmentVersion = d.commitments.version // first we check if this tx belongs to one of the current local/remote commits and update it val localCommitPublished1 = d.localCommitPublished.map(Closing.updateLocalCommitPublished(_, tx)) val remoteCommitPublished1 = d.remoteCommitPublished.map(Closing.updateRemoteCommitPublished(_, tx)) @@ -1224,9 +1241,9 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu } // we may need to fail some htlcs in case a commitment tx was published and they have reached the timeout threshold val timedoutHtlcs = - Closing.timedoutHtlcs(d.commitments.localCommit, Satoshi(d.commitments.localParams.dustLimitSatoshis), tx) ++ - Closing.timedoutHtlcs(d.commitments.remoteCommit, Satoshi(d.commitments.remoteParams.dustLimitSatoshis), tx) ++ - d.commitments.remoteNextCommitInfo.left.toSeq.flatMap(r => Closing.timedoutHtlcs(r.nextRemoteCommit, Satoshi(d.commitments.remoteParams.dustLimitSatoshis), tx)) + Closing.timedoutHtlcs(d.commitments.localCommit, Satoshi(d.commitments.localParams.dustLimitSatoshis), tx, d.commitments.version) ++ + Closing.timedoutHtlcs(d.commitments.remoteCommit, Satoshi(d.commitments.remoteParams.dustLimitSatoshis), tx, d.commitments.version) ++ + d.commitments.remoteNextCommitInfo.left.toSeq.flatMap(r => Closing.timedoutHtlcs(r.nextRemoteCommit, Satoshi(d.commitments.remoteParams.dustLimitSatoshis), tx, d.commitments.version)) timedoutHtlcs.foreach { add => d.commitments.originChannels.get(add.id) match { case Some(origin) => @@ -1251,13 +1268,13 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu } // then let's see if any of the possible close scenarii can be considered done val mutualCloseDone = d.mutualClosePublished.exists(_.txid == tx.txid) // this case is trivial, in a mutual close scenario we only need to make sure that one of the closing txes is confirmed - val localCommitDone = localCommitPublished1.map(Closing.isLocalCommitDone(_)).getOrElse(false) + val localCommitDone = localCommitPublished1.map(Closing.isLocalCommitDone(_)).getOrElse(false) val remoteCommitDone = remoteCommitPublished1.map(Closing.isRemoteCommitDone(_)).getOrElse(false) val nextRemoteCommitDone = nextRemoteCommitPublished1.map(Closing.isRemoteCommitDone(_)).getOrElse(false) val futureRemoteCommitDone = futureRemoteCommitPublished1.map(Closing.isRemoteCommitDone(_)).getOrElse(false) val revokedCommitDone = revokedCommitPublished1.map(Closing.isRevokedCommitDone(_)).exists(_ == true) // we only need one revoked commit done - // finally, if one of the unilateral closes is done, we move to CLOSED state, otherwise we stay (note that we don't store the state) - val d1 = d.copy(localCommitPublished = localCommitPublished1, remoteCommitPublished = remoteCommitPublished1, nextRemoteCommitPublished = nextRemoteCommitPublished1, futureRemoteCommitPublished = futureRemoteCommitPublished1, revokedCommitPublished = revokedCommitPublished1) + // finally, if one of the unilateral closes is done, we move to CLOSED state, otherwise we stay (note that we don't store the state) + val d1 = d.copy(localCommitPublished = localCommitPublished1, remoteCommitPublished = remoteCommitPublished1, nextRemoteCommitPublished = nextRemoteCommitPublished1, futureRemoteCommitPublished = futureRemoteCommitPublished1, revokedCommitPublished = revokedCommitPublished1) // we also send events related to fee Closing.networkFeePaid(tx, d1) map { case (fee, desc) => feePaid(fee, tx, desc, d.channelId) } val closeType_opt = if (mutualCloseDone) { @@ -1331,14 +1348,17 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu forwarder ! r val yourLastPerCommitmentSecret = d.commitments.remotePerCommitmentSecrets.lastIndex.flatMap(d.commitments.remotePerCommitmentSecrets.getHash).getOrElse(ByteVector32.Zeroes) - val myCurrentPerCommitmentPoint = keyManager.commitmentPoint(d.commitments.localParams.channelKeyPath, d.commitments.localCommit.index) + val myCurrentPerCommitmentPoint = d.commitments.version match { + case VersionCommitmentV1 => Some(keyManager.commitmentPoint(d.commitments.localParams.channelKeyPath, d.commitments.localCommit.index)) + case VersionSimplifiedCommitment => None + } val channelReestablish = ChannelReestablish( channelId = d.channelId, nextLocalCommitmentNumber = d.commitments.localCommit.index + 1, nextRemoteRevocationNumber = d.commitments.remoteCommit.index, yourLastPerCommitmentSecret = Some(Scalar(yourLastPerCommitmentSecret)), - myCurrentPerCommitmentPoint = Some(myCurrentPerCommitmentPoint) + myCurrentPerCommitmentPoint = myCurrentPerCommitmentPoint ) // we update local/remote connection-local global/local features, we don't persist it right now @@ -1467,7 +1487,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu // BOLT 2: A node if it has sent a previous shutdown MUST retransmit shutdown. // negotiation restarts from the beginning, and is initialized by the funder // note: in any case we still need to keep all previously sent closing_signed, because they may publish one of them - if (d.commitments.localParams.isFunder) { + if (d.commitments.localParams.isFunder || d.commitments.version == VersionSimplifiedCommitment) { // we could use the last closing_signed we sent, but network fees may have changed while we were offline so it is better to restart from scratch val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey) val closingTxProposed1 = d.closingTxProposed :+ List(ClosingTxProposed(closingTx.tx, closingSigned)) @@ -1738,6 +1758,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu blockchain ! WatchConfirmed(self, closingTx, nodeParams.minDepthBlocks, BITCOIN_TX_CONFIRMED(closingTx)) } + // TODO: adjust for option_simplified_commitmenmt def spendLocalCurrent(d: HasCommitments) = { val outdatedCommitment = d match { @@ -1808,13 +1829,13 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu def doPublish(localCommitPublished: LocalCommitPublished) = { import localCommitPublished._ - val publishQueue = List(commitTx) ++ claimMainDelayedOutputTx ++ htlcSuccessTxs ++ htlcTimeoutTxs ++ claimHtlcDelayedTxs + val publishQueue = List(commitTx) ++ pushMeTx ++ claimMainDelayedOutputTx ++ htlcSuccessTxs ++ htlcTimeoutTxs ++ claimHtlcDelayedTxs publishIfNeeded(publishQueue, irrevocablySpent) // we watch: // - the commitment tx itself, so that we can handle the case where we don't have any outputs // - 'final txes' that send funds to our wallet and that spend outputs that only us control - val watchConfirmedQueue = List(commitTx) ++ claimMainDelayedOutputTx ++ claimHtlcDelayedTxs + val watchConfirmedQueue = List(commitTx) ++ pushMeTx ++ claimMainDelayedOutputTx ++ claimHtlcDelayedTxs watchConfirmedIfNeeded(watchConfirmedQueue, irrevocablySpent) // we watch outputs of the commitment tx that both parties may spend diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala index 3cf1d713ca..60d58088bb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala @@ -69,6 +69,7 @@ case class InvalidHtlcPreimage (override val channelId: ByteVect case class UnknownHtlcId (override val channelId: ByteVector32, id: Long) extends ChannelException(channelId, s"unknown htlc id=$id") case class CannotExtractSharedSecret (override val channelId: ByteVector32, htlc: UpdateAddHtlc) extends ChannelException(channelId, s"can't extract shared secret: paymentHash=${htlc.paymentHash} onion=${htlc.onionRoutingPacket}") case class FundeeCannotSendUpdateFee (override val channelId: ByteVector32) extends ChannelException(channelId, s"only the funder should send update_fee messages") +case class CannotUpdateFeeWithCommitmentType (override val channelId: ByteVector32) extends ChannelException(channelId, s"can't update fees when option_simplified_commitment is in use") case class CannotAffordFees (override val channelId: ByteVector32, missingSatoshis: Long, reserveSatoshis: Long, feesSatoshis: Long) extends ChannelException(channelId, s"can't pay the fee: missingSatoshis=$missingSatoshis reserveSatoshis=$reserveSatoshis feesSatoshis=$feesSatoshis") case class CannotSignWithoutChanges (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot sign when there are no changes") case class CannotSignBeforeRevocation (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot sign until next revocation hash is received") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala index 544ae9a1f4..2ea6e86fc8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala @@ -142,8 +142,8 @@ sealed trait HasCommitments extends Data { case class ClosingTxProposed(unsignedTx: Transaction, localClosingSigned: ClosingSigned) -case class LocalCommitPublished(commitTx: Transaction, claimMainDelayedOutputTx: Option[Transaction], htlcSuccessTxs: List[Transaction], htlcTimeoutTxs: List[Transaction], claimHtlcDelayedTxs: List[Transaction], irrevocablySpent: Map[OutPoint, ByteVector32]) -case class RemoteCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[Transaction], claimHtlcSuccessTxs: List[Transaction], claimHtlcTimeoutTxs: List[Transaction], irrevocablySpent: Map[OutPoint, ByteVector32]) +case class LocalCommitPublished(commitTx: Transaction, claimMainDelayedOutputTx: Option[Transaction], htlcSuccessTxs: List[Transaction], htlcTimeoutTxs: List[Transaction], claimHtlcDelayedTxs: List[Transaction], pushMeTx: Option[Transaction], irrevocablySpent: Map[OutPoint, ByteVector32]) +case class RemoteCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[Transaction], claimHtlcSuccessTxs: List[Transaction], claimHtlcTimeoutTxs: List[Transaction], pushMeTx: Option[Transaction], irrevocablySpent: Map[OutPoint, ByteVector32]) case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[Transaction], mainPenaltyTx: Option[Transaction], htlcPenaltyTxs: List[Transaction], claimHtlcDelayedPenaltyTxs: List[Transaction], irrevocablySpent: Map[OutPoint, ByteVector32]) final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_FUNDEE) extends Data @@ -171,7 +171,7 @@ final case class DATA_NEGOTIATING(commitments: Commitments, closingTxProposed: List[List[ClosingTxProposed]], // one list for every negotiation (there can be several in case of disconnection) bestUnpublishedClosingTx_opt: Option[Transaction]) extends Data with HasCommitments { require(!closingTxProposed.isEmpty, "there must always be a list for the current negotiation") - require(!commitments.localParams.isFunder || closingTxProposed.forall(!_.isEmpty), "funder must have at least one closing signature for every negotation attempt because it initiates the closing") + require(commitments.version == VersionSimplifiedCommitment || !commitments.localParams.isFunder || closingTxProposed.forall(!_.isEmpty), "funder must have at least one closing signature for every negotation attempt because it initiates the closing") } final case class DATA_CLOSING(commitments: Commitments, mutualCloseProposed: List[Transaction], // all exchanged closing sigs are flattened, we use this only to keep track of what publishable tx they have @@ -187,6 +187,11 @@ final case class DATA_CLOSING(commitments: Commitments, final case class DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(commitments: Commitments, remoteChannelReestablish: ChannelReestablish) extends Data with HasCommitments +trait ParamsWithFeatures { + val globalFeatures: ByteVector + val localFeatures: ByteVector +} + final case class LocalParams(nodeId: PublicKey, channelKeyPath: DeterministicWallet.KeyPath, dustLimitSatoshis: Long, @@ -198,7 +203,7 @@ final case class LocalParams(nodeId: PublicKey, isFunder: Boolean, defaultFinalScriptPubKey: ByteVector, globalFeatures: ByteVector, - localFeatures: ByteVector) + localFeatures: ByteVector) extends ParamsWithFeatures final case class RemoteParams(nodeId: PublicKey, dustLimitSatoshis: Long, @@ -213,7 +218,7 @@ final case class RemoteParams(nodeId: PublicKey, delayedPaymentBasepoint: Point, htlcBasepoint: Point, globalFeatures: ByteVector, - localFeatures: ByteVector) + localFeatures: ByteVector) extends ParamsWithFeatures object ChannelFlags { val AnnounceChannel = 0x01.toByte diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 0da1e760d4..171f7464b7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -17,13 +17,15 @@ package fr.acinq.eclair.channel import akka.event.LoggingAdapter -import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, sha256} +import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, sha256} import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi} import fr.acinq.eclair.crypto.{Generators, KeyManager, ShaChain, Sphinx} import fr.acinq.eclair.payment._ import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire._ +import fr.acinq.eclair.{Features, Globals, UInt64} +import fr.acinq.bitcoin._ import fr.acinq.eclair.{Globals, UInt64} import scodec.bits.ByteVector @@ -51,14 +53,16 @@ case class WaitingForRevocation(nextRemoteCommit: RemoteCommit, sent: CommitSig, * theirNextCommitInfo is their next commit tx. The rest of the time, it is their next per-commitment point */ case class Commitments(localParams: LocalParams, remoteParams: RemoteParams, - channelFlags: Byte, - localCommit: LocalCommit, remoteCommit: RemoteCommit, - localChanges: LocalChanges, remoteChanges: RemoteChanges, - localNextHtlcId: Long, remoteNextHtlcId: Long, - originChannels: Map[Long, Origin], // for outgoing htlcs relayed through us, the id of the previous channel - remoteNextCommitInfo: Either[WaitingForRevocation, Point], - commitInput: InputInfo, - remotePerCommitmentSecrets: ShaChain, channelId: ByteVector32) { + channelFlags: Byte, + localCommit: LocalCommit, remoteCommit: RemoteCommit, + localChanges: LocalChanges, remoteChanges: RemoteChanges, + localNextHtlcId: Long, remoteNextHtlcId: Long, + originChannels: Map[Long, Origin], // for outgoing htlcs relayed through us, the id of the previous channel + remoteNextCommitInfo: Either[WaitingForRevocation, Point], + commitInput: InputInfo, + remotePerCommitmentSecrets: ShaChain, channelId: ByteVector32, + version: CommitmentVersion = VersionCommitmentV1 + ) { def hasNoPendingHtlcs: Boolean = localCommit.spec.htlcs.isEmpty && remoteCommit.spec.htlcs.isEmpty && remoteNextCommitInfo.isRight @@ -73,13 +77,21 @@ case class Commitments(localParams: LocalParams, remoteParams: RemoteParams, def announceChannel: Boolean = (channelFlags & 0x01) != 0 + // TODO subtract the pushMe value from the balance? def availableBalanceForSendMsat: Long = { val reduced = CommitmentSpec.reduce(remoteCommit.spec, remoteChanges.acked, localChanges.proposed) - val feesMsat = if (localParams.isFunder) Transactions.commitTxFee(Satoshi(remoteParams.dustLimitSatoshis), reduced).amount * 1000 else 0 + val feesMsat = if (localParams.isFunder) Transactions.commitTxFee(Satoshi(remoteParams.dustLimitSatoshis), reduced, version).amount * 1000 else 0 reduced.toRemoteMsat - remoteParams.channelReserveSatoshis * 1000 - feesMsat } + } +// @formatter: off +sealed trait CommitmentVersion +object VersionCommitmentV1 extends CommitmentVersion +object VersionSimplifiedCommitment extends CommitmentVersion +// @formatter: on + object Commitments { /** * add a change to our proposed change list @@ -140,7 +152,7 @@ object Commitments { // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee // we look from remote's point of view, so if local is funder remote doesn't pay the fees - val fees = if (commitments1.localParams.isFunder) Transactions.commitTxFee(Satoshi(commitments1.remoteParams.dustLimitSatoshis), reduced).amount else 0 + val fees = if (commitments1.localParams.isFunder) Transactions.commitTxFee(Satoshi(commitments1.remoteParams.dustLimitSatoshis), reduced, commitments.version).amount else 0 val missing = reduced.toRemoteMsat / 1000 - commitments1.remoteParams.channelReserveSatoshis - fees if (missing < 0) { return Left(InsufficientFunds(commitments.channelId, amountMsat = cmd.amountMsat, missingSatoshis = -1 * missing, reserveSatoshis = commitments1.remoteParams.channelReserveSatoshis, feesSatoshis = fees)) @@ -150,6 +162,7 @@ object Commitments { } def receiveAdd(commitments: Commitments, add: UpdateAddHtlc): Commitments = { + if (add.id != commitments.remoteNextHtlcId) { throw UnexpectedHtlcId(commitments.channelId, expected = commitments.remoteNextHtlcId, actual = add.id) } @@ -173,7 +186,7 @@ object Commitments { } // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee - val fees = if (commitments1.localParams.isFunder) 0 else Transactions.commitTxFee(Satoshi(commitments1.localParams.dustLimitSatoshis), reduced).amount + val fees = if (commitments1.localParams.isFunder) 0 else Transactions.commitTxFee(Satoshi(commitments1.localParams.dustLimitSatoshis), reduced, commitments.version).amount val missing = reduced.toRemoteMsat / 1000 - commitments1.localParams.channelReserveSatoshis - fees if (missing < 0) { throw InsufficientFunds(commitments.channelId, amountMsat = add.amountMsat, missingSatoshis = -1 * missing, reserveSatoshis = commitments1.localParams.channelReserveSatoshis, feesSatoshis = fees) @@ -287,6 +300,11 @@ object Commitments { if (!commitments.localParams.isFunder) { throw FundeeCannotSendUpdateFee(commitments.channelId) } + + if(commitments.version == VersionSimplifiedCommitment){ + throw CannotUpdateFeeWithCommitmentType(commitments.channelId) + } + // let's compute the current commitment *as seen by them* with this change taken into account val fee = UpdateFee(commitments.channelId, cmd.feeratePerKw) // update_fee replace each other, so we can remove previous ones @@ -295,7 +313,7 @@ object Commitments { // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee // we look from remote's point of view, so if local is funder remote doesn't pay the fees - val fees = Transactions.commitTxFee(Satoshi(commitments1.remoteParams.dustLimitSatoshis), reduced).amount + val fees = Transactions.commitTxFee(Satoshi(commitments1.remoteParams.dustLimitSatoshis), reduced, commitments.version).amount // we update the fee only in NON simplified commitment val missing = reduced.toRemoteMsat / 1000 - commitments1.remoteParams.channelReserveSatoshis - fees if (missing < 0) { throw CannotAffordFees(commitments.channelId, missingSatoshis = -1 * missing, reserveSatoshis = commitments1.localParams.channelReserveSatoshis, feesSatoshis = fees) @@ -318,6 +336,10 @@ object Commitments { throw FeerateTooDifferent(commitments.channelId, localFeeratePerKw = localFeeratePerKw, remoteFeeratePerKw = fee.feeratePerKw) } + if(commitments.version == VersionSimplifiedCommitment){ + throw CannotUpdateFeeWithCommitmentType(commitments.channelId) + } + // NB: we check that the funder can afford this new fee even if spec allows to do it at next signature // It is easier to do it here because under certain (race) conditions spec allows a lower-than-normal fee to be paid, // and it would be tricky to check if the conditions are met at signing @@ -329,7 +351,7 @@ object Commitments { val reduced = CommitmentSpec.reduce(commitments1.localCommit.spec, commitments1.localChanges.acked, commitments1.remoteChanges.proposed) // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee - val fees = Transactions.commitTxFee(Satoshi(commitments1.remoteParams.dustLimitSatoshis), reduced).amount + val fees = Transactions.commitTxFee(Satoshi(commitments1.remoteParams.dustLimitSatoshis), reduced, commitments.version).amount // we update the fee only in NON simplified val missing = reduced.toRemoteMsat / 1000 - commitments1.localParams.channelReserveSatoshis - fees if (missing < 0) { throw CannotAffordFees(commitments.channelId, missingSatoshis = -1 * missing, reserveSatoshis = commitments1.localParams.channelReserveSatoshis, feesSatoshis = fees) @@ -352,17 +374,22 @@ object Commitments { def sendCommit(commitments: Commitments, keyManager: KeyManager)(implicit log: LoggingAdapter): (Commitments, CommitSig) = { import commitments._ + commitments.remoteNextCommitInfo match { case Right(_) if !localHasChanges(commitments) => throw CannotSignWithoutChanges(commitments.channelId) case Right(remoteNextPerCommitmentPoint) => // remote commitment will includes all local changes + remote acked changes val spec = CommitmentSpec.reduce(remoteCommit.spec, remoteChanges.acked, localChanges.proposed) - val (remoteCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeRemoteTxs(keyManager, remoteCommit.index + 1, localParams, remoteParams, commitInput, remoteNextPerCommitmentPoint, spec) - val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) + val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, commitments.localCommit.index + 1) + val (remoteCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeRemoteTxs(keyManager, remoteCommit.index + 1, localParams, remoteParams, commitInput, remoteNextPerCommitmentPoint, localPerCommitmentPoint, spec, commitments.version) + val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath), SIGHASH_ALL) val sortedHtlcTxs: Seq[TransactionWithInputInfo] = (htlcTimeoutTxs ++ htlcSuccessTxs).sortBy(_.input.outPoint.index) - val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(localParams.channelKeyPath), remoteNextPerCommitmentPoint)) + val htlcSigs = commitments.version match { + case VersionCommitmentV1 => sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(localParams.channelKeyPath), remoteNextPerCommitmentPoint, SIGHASH_ALL)) + case VersionSimplifiedCommitment => sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(localParams.channelKeyPath), remoteNextPerCommitmentPoint, SIGHASH_SINGLE | SIGHASH_ANYONECANPAY)) + } // NB: IN/OUT htlcs are inverted because this is the remote commit log.info(s"built remote commit number=${remoteCommit.index + 1} htlc_in={} htlc_out={} feeratePerKw=${spec.feeratePerKw} txid=${remoteCommitTx.tx.txid} tx={}", spec.htlcs.filter(_.direction == OUT).map(_.add.id).mkString(","), spec.htlcs.filter(_.direction == IN).map(_.add.id).mkString(","), remoteCommitTx.tx) @@ -378,13 +405,14 @@ object Commitments { remoteNextCommitInfo = Left(WaitingForRevocation(RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint), commitSig, commitments.localCommit.index)), localChanges = localChanges.copy(proposed = Nil, signed = localChanges.proposed), remoteChanges = remoteChanges.copy(acked = Nil, signed = remoteChanges.acked)) + (commitments1, commitSig) case Left(_) => throw CannotSignBeforeRevocation(commitments.channelId) - } - } + } } def receiveCommit(commitments: Commitments, commit: CommitSig, keyManager: KeyManager)(implicit log: LoggingAdapter): (Commitments, RevokeAndAck) = { + 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: @@ -404,8 +432,13 @@ object Commitments { val spec = CommitmentSpec.reduce(localCommit.spec, localChanges.acked, remoteChanges.proposed) val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, commitments.localCommit.index + 1) - val (localCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeLocalTxs(keyManager, localCommit.index + 1, localParams, remoteParams, commitInput, localPerCommitmentPoint, spec) - val sig = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) + val remotePerCommitmentPoint = remoteNextCommitInfo match { + case Left(_) => commitments.remoteCommit.remotePerCommitmentPoint + case Right(point) => point + } + + val (localCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeLocalTxs(keyManager, localCommit.index + 1, localParams, remoteParams, commitInput, localPerCommitmentPoint, remotePerCommitmentPoint, spec, commitments.version) + val sig = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath), SIGHASH_ALL) log.info(s"built local commit number=${localCommit.index + 1} htlc_in={} htlc_out={} feeratePerKw=${spec.feeratePerKw} txid=${localCommitTx.tx.txid} tx={}", spec.htlcs.filter(_.direction == IN).map(_.add.id).mkString(","), spec.htlcs.filter(_.direction == OUT).map(_.add.id).mkString(","), localCommitTx.tx) @@ -421,7 +454,10 @@ object Commitments { if (commit.htlcSignatures.size != sortedHtlcTxs.size) { throw new HtlcSigCountMismatch(commitments.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size) } - val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(localParams.channelKeyPath), localPerCommitmentPoint)) + val htlcSigs = commitments.version match { + case VersionCommitmentV1 => sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(localParams.channelKeyPath), localPerCommitmentPoint, SIGHASH_ALL)) + case VersionSimplifiedCommitment => sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(localParams.channelKeyPath), localPerCommitmentPoint, SIGHASH_SINGLE | SIGHASH_ANYONECANPAY)) + } val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint) // combine the sigs to make signed txes val htlcTxsAndSigs = (sortedHtlcTxs, htlcSigs, commit.htlcSignatures).zipped.toList.collect { @@ -430,9 +466,15 @@ object Commitments { throw new InvalidHtlcSignature(commitments.channelId, htlcTx.tx) } HtlcTxAndSigs(htlcTx, localSig, remoteSig) - case (htlcTx: HtlcSuccessTx, localSig, remoteSig) => + case (htlcTx: HtlcSuccessTx, localSig, remoteSig) if commitments.version == VersionCommitmentV1 => // we can't check that htlc-success tx are spendable because we need the payment preimage; thus we only check the remote sig - if (Transactions.checkSig(htlcTx, remoteSig, remoteHtlcPubkey) == false) { + if (Transactions.checkSig(htlcTx, remoteSig, remoteHtlcPubkey, SIGHASH_ALL) == false) { + throw new InvalidHtlcSignature(commitments.channelId, htlcTx.tx) + } + HtlcTxAndSigs(htlcTx, localSig, remoteSig) + case (htlcTx: HtlcSuccessTx, localSig, remoteSig) if commitments.version == VersionSimplifiedCommitment => + // we can't check that htlc-success tx are spendable because we need the payment preimage; thus we only check the remote sig + if (Transactions.checkSig(htlcTx, remoteSig, remoteHtlcPubkey, SIGHASH_SINGLE | SIGHASH_ANYONECANPAY) == false) { throw new InvalidHtlcSignature(commitments.channelId, htlcTx.tx) } HtlcTxAndSigs(htlcTx, localSig, remoteSig) @@ -500,25 +542,34 @@ object Commitments { } } - def makeLocalTxs(keyManager: KeyManager, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, localPerCommitmentPoint: Point, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { + def makeLocalTxs(keyManager: KeyManager, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, localPerCommitmentPoint: Point, remotePerCommitmentPoint: Point, spec: CommitmentSpec, commitmentVersion: CommitmentVersion): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { val localDelayedPaymentPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(localParams.channelKeyPath).publicKey, localPerCommitmentPoint) val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(localParams.channelKeyPath).publicKey, localPerCommitmentPoint) - val remotePaymentPubkey = Generators.derivePubKey(remoteParams.paymentBasepoint, localPerCommitmentPoint) + + val remotePaymentPubkey = commitmentVersion match { + case VersionSimplifiedCommitment => PublicKey(remoteParams.paymentBasepoint) + case VersionCommitmentV1 => Generators.derivePubKey(remoteParams.paymentBasepoint, localPerCommitmentPoint) + } + val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint) val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) - val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, keyManager.paymentPoint(localParams.channelKeyPath).publicKey, remoteParams.paymentBasepoint, localParams.isFunder, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec) - val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec) + val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, keyManager.paymentPoint(localParams.channelKeyPath).publicKey, remoteParams.paymentBasepoint, localParams.isFunder, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, remoteDelayedPaymentPubkey, spec, commitmentVersion) + val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec, commitmentVersion) (commitTx, htlcTimeoutTxs, htlcSuccessTxs) } - def makeRemoteTxs(keyManager: KeyManager, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, remotePerCommitmentPoint: Point, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { - val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) + def makeRemoteTxs(keyManager: KeyManager, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, remotePerCommitmentPoint: Point, localPerCommitmentPoint: Point, spec: CommitmentSpec, commitmentVersion: CommitmentVersion): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { + val localPaymentPubkey = commitmentVersion match { + case VersionSimplifiedCommitment => keyManager.paymentPoint(localParams.channelKeyPath).publicKey + case VersionCommitmentV1 => Generators.derivePubKey(keyManager.paymentPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) + } + val localDelayedPaymentPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(localParams.channelKeyPath).publicKey, localPerCommitmentPoint) val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) - val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, keyManager.paymentPoint(localParams.channelKeyPath).publicKey, !localParams.isFunder, Satoshi(remoteParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec) - val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, Satoshi(remoteParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec) + val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, keyManager.paymentPoint(localParams.channelKeyPath).publicKey, !localParams.isFunder, Satoshi(remoteParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, localDelayedPaymentPubkey, spec, commitmentVersion) + val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, Satoshi(remoteParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec, commitmentVersion) (commitTx, htlcTimeoutTxs, htlcSuccessTxs) } @@ -536,7 +587,7 @@ object Commitments { def changes2String(commitments: Commitments): String = { import commitments._ - s"""commitments: + s"""(${commitments.version}) commitments: | localChanges: | proposed: ${localChanges.proposed.map(msg2String(_)).mkString(" ")} | signed: ${localChanges.signed.map(msg2String(_)).mkString(" ")} @@ -551,7 +602,7 @@ object Commitments { } def specs2String(commitments: Commitments): String = { - s"""specs: + s"""(${commitments.version}) specs: |localcommit: | toLocal: ${commitments.localCommit.spec.toLocalMsat} | toRemote: ${commitments.localCommit.spec.toRemoteMsat} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 0013f3a6ff..6e58b9c68e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -27,6 +27,7 @@ import fr.acinq.eclair.transactions.Scripts._ import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire._ +import fr.acinq.eclair._ import fr.acinq.eclair.{Globals, NodeParams, ShortChannelId, addressToPublicKeyScript} import scodec.bits.ByteVector @@ -66,6 +67,7 @@ object Helpers { val commitments1 = data.commitments.copy( localParams = data.commitments.localParams.copy(globalFeatures = localInit.globalFeatures, localFeatures = localInit.localFeatures), remoteParams = data.commitments.remoteParams.copy(globalFeatures = remoteInit.globalFeatures, localFeatures = remoteInit.localFeatures)) + data match { case d: DATA_WAIT_FOR_FUNDING_CONFIRMED => d.copy(commitments = commitments1) case d: DATA_WAIT_FOR_FUNDING_LOCKED => d.copy(commitments = commitments1) @@ -233,6 +235,12 @@ object Helpers { * @return (localSpec, localTx, remoteSpec, remoteTx, fundingTxOutput) */ def makeFirstCommitTxs(keyManager: KeyManager, temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, fundingTxHash: ByteVector32, fundingTxOutputIndex: Int, remoteFirstPerCommitmentPoint: Point, maxFeerateMismatch: Double): (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx) = { + implicit val commitmentVersion = Helpers.canUseSimplifiedCommitment(localParams, remoteParams) match { + case false => VersionCommitmentV1 + case true => VersionSimplifiedCommitment + } + + // TODO adjust for option_simplified_commitment val toLocalMsat = if (localParams.isFunder) fundingSatoshis * 1000 - pushMsat else pushMsat val toRemoteMsat = if (localParams.isFunder) pushMsat else fundingSatoshis * 1000 - pushMsat @@ -242,7 +250,7 @@ object Helpers { if (!localParams.isFunder) { // they are funder, therefore they pay the fee: we need to make sure they can afford it! val toRemoteMsat = remoteSpec.toLocalMsat - val fees = Transactions.commitTxFee(Satoshi(remoteParams.dustLimitSatoshis), remoteSpec).amount + val fees = Transactions.commitTxFee(Satoshi(remoteParams.dustLimitSatoshis), remoteSpec, commitmentVersion).amount val missing = toRemoteMsat / 1000 - localParams.channelReserveSatoshis - fees if (missing < 0) { throw CannotAffordFees(temporaryChannelId, missingSatoshis = -1 * missing, reserveSatoshis = localParams.channelReserveSatoshis, feesSatoshis = fees) @@ -251,8 +259,8 @@ object Helpers { val commitmentInput = makeFundingInputInfo(fundingTxHash, fundingTxOutputIndex, Satoshi(fundingSatoshis), keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey) val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, 0) - val (localCommitTx, _, _) = Commitments.makeLocalTxs(keyManager, 0, localParams, remoteParams, commitmentInput, localPerCommitmentPoint, localSpec) - val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(keyManager, 0, localParams, remoteParams, commitmentInput, remoteFirstPerCommitmentPoint, remoteSpec) + val (localCommitTx, _, _) = Commitments.makeLocalTxs(keyManager, 0, localParams, remoteParams, commitmentInput, localPerCommitmentPoint, remoteFirstPerCommitmentPoint, localSpec, commitmentVersion) + val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(keyManager, 0, localParams, remoteParams, commitmentInput, remoteFirstPerCommitmentPoint, localPerCommitmentPoint, remoteSpec, commitmentVersion) (localSpec, localCommitTx, remoteSpec, remoteCommitTx) } @@ -333,6 +341,16 @@ object Helpers { } } + def canUseSimplifiedCommitment[T <: ParamsWithFeatures](local: T, remote: T) = { + val localHasOptional = Features.hasFeature(local.localFeatures, Features.OPTION_SIMPLIFIED_COMMITMENT_OPTIONAL) + val localHasMandatory = Features.hasFeature(local.localFeatures, Features.OPTION_SIMPLIFIED_COMMITMENT_MANDATORY) + val remoteHasOptional = Features.hasFeature(remote.localFeatures, Features.OPTION_SIMPLIFIED_COMMITMENT_OPTIONAL) + val remoteHasMandatory = Features.hasFeature(remote.localFeatures, Features.OPTION_SIMPLIFIED_COMMITMENT_MANDATORY) + + (remoteHasMandatory && (localHasMandatory || localHasOptional)) || + (localHasMandatory && (remoteHasMandatory || remoteHasOptional)) || + (localHasOptional && remoteHasOptional) + } object Closing { @@ -362,15 +380,18 @@ object Helpers { } } - def firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector)(implicit log: LoggingAdapter): Satoshi = { - import commitments._ - // this is just to estimate the weight, it depends on size of the pubkey scripts - val dummyClosingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, Satoshi(0), Satoshi(0), localCommit.spec) - val closingWeight = Transaction.weight(Transactions.addSigs(dummyClosingTx, dummyPublicKey, remoteParams.fundingPubKey, ByteVector.fill(71)(0xaa), ByteVector.fill(71)(0xbb)).tx) - // no need to use a very high fee here, so we target 6 blocks; also, we "MUST set fee_satoshis less than or equal to the base fee of the final commitment transaction" - val feeratePerKw = Math.min(Globals.feeratesPerKw.get.blocks_6, commitments.localCommit.spec.feeratePerKw) - log.info(s"using feeratePerKw=$feeratePerKw for initial closing tx") - Transactions.weight2fee(feeratePerKw, closingWeight) + def firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector)(implicit log: LoggingAdapter): Satoshi = commitments.version match { + case VersionCommitmentV1 => + import commitments._ + // this is just to estimate the weight, it depends on size of the pubkey scripts + val dummyClosingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, Satoshi(0), Satoshi(0), localCommit.spec) + val closingWeight = Transaction.weight(Transactions.addSigs(dummyClosingTx, dummyPublicKey, remoteParams.fundingPubKey, ByteVector.fill(71)(0xaa), ByteVector.fill(71)(0xbb)).tx) + // no need to use a very high fee here, so we target 6 blocks; also, we "MUST set fee_satoshis less than or equal to the base fee of the final commitment transaction" + val feeratePerKw = Math.min(Globals.feeratesPerKw.get.blocks_6, commitments.localCommit.spec.feeratePerKw) + log.info(s"using feeratePerKw=$feeratePerKw for initial closing tx") + Transactions.weight2fee(feeratePerKw, closingWeight) + case VersionSimplifiedCommitment => + Satoshi(282) } def nextClosingFee(localClosingFee: Satoshi, remoteClosingFee: Satoshi): Satoshi = ((localClosingFee + remoteClosingFee) / 4) * 2 @@ -388,7 +409,7 @@ object Helpers { // TODO: check that val dustLimitSatoshis = Satoshi(Math.max(localParams.dustLimitSatoshis, remoteParams.dustLimitSatoshis)) val closingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, dustLimitSatoshis, closingFee, localCommit.spec) - val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitments.localParams.channelKeyPath)) + val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitments.localParams.channelKeyPath), SIGHASH_ALL) val closingSigned = ClosingSigned(channelId, closingFee.amount, localClosingSig) log.info(s"signed closing txid=${closingTx.tx.txid} with closingFeeSatoshis=${closingSigned.feeSatoshis}") log.debug(s"closingTxid=${closingTx.tx.txid} closingTx=${closingTx.tx}}") @@ -432,6 +453,7 @@ object Helpers { def claimCurrentLocalCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, tx: Transaction)(implicit log: LoggingAdapter): LocalCommitPublished = { import commitments._ require(localCommit.publishableTxs.commitTx.tx.txid == tx.txid, "txid mismatch, provided tx is not the current local commit tx") + implicit val commitmentVersion = commitments.version val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, commitments.localCommit.index.toInt) val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) @@ -442,11 +464,22 @@ object Helpers { // first we will claim our main output as soon as the delay is over val mainDelayedTx = generateTx("main-delayed-output")(Try { - val claimDelayed = Transactions.makeClaimDelayedOutputTx(tx, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed) - val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(localParams.channelKeyPath), localPerCommitmentPoint) + val claimDelayed = Transactions.makeClaimDelayedOutputTx(tx, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed, commitmentVersion) + val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(localParams.channelKeyPath), localPerCommitmentPoint, SIGHASH_ALL) Transactions.addSigs(claimDelayed, sig) }) + // the push-me trasaction attaches the fees to the commitmentTx + val pushMeTransaction = commitmentVersion match { + case VersionCommitmentV1 => None + case VersionSimplifiedCommitment => + generateTx("push-me-cpfp")(Try { + val pushMeTx = Transactions.makePushMeCPFP(tx, localDelayedPubkey, feeratePerKwDelayed, Satoshi(localParams.dustLimitSatoshis)) + val sig = keyManager.sign(pushMeTx, keyManager.delayedPaymentPoint(localParams.channelKeyPath), SIGHASH_ALL) // TODO use SIGHASH_SINGLE + Transactions.addSigs(pushMeTx, sig) + }) + } + // those are the preimages to existing received htlcs val preimages = commitments.localChanges.all.collect { case u: UpdateFulfillHtlc => u.paymentPreimage } @@ -477,8 +510,8 @@ object Helpers { localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, - localParams.defaultFinalScriptPubKey, feeratePerKwDelayed) - val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(localParams.channelKeyPath), localPerCommitmentPoint) + localParams.defaultFinalScriptPubKey, feeratePerKwDelayed, commitmentVersion) + val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(localParams.channelKeyPath), localPerCommitmentPoint, SIGHASH_ALL) Transactions.addSigs(claimDelayed, sig) }) } @@ -489,6 +522,7 @@ object Helpers { htlcSuccessTxs = htlcTxes.collect { case c: HtlcSuccessTx => c.tx }, htlcTimeoutTxs = htlcTxes.collect { case c: HtlcTimeoutTx => c.tx }, claimHtlcDelayedTxs = htlcDelayedTxes.map(_.tx), + pushMeTx = pushMeTransaction.map(_.tx), irrevocablySpent = Map.empty) } @@ -498,18 +532,19 @@ object Helpers { * * @param commitments our commitment data, which include payment preimages * @param remoteCommit the remote commitment data to use to claim outputs (it can be their current or next commitment) - * @param tx the remote commitment transaction that has just been published + * @param commitTx the remote commitment transaction that has just been published * @return a list of transactions (one per HTLC that we can claim) */ - def claimRemoteCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction)(implicit log: LoggingAdapter): RemoteCommitPublished = { + def claimRemoteCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, remoteCommit: RemoteCommit, commitTx: Transaction)(implicit log: LoggingAdapter): RemoteCommitPublished = { import commitments.{commitInput, localParams, remoteParams} - require(remoteCommit.txid == tx.txid, "txid mismatch, provided tx is not the current remote commit tx") - val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(keyManager, remoteCommit.index, localParams, remoteParams, commitInput, remoteCommit.remotePerCommitmentPoint, remoteCommit.spec) - require(remoteCommitTx.tx.txid == tx.txid, "txid mismatch, cannot recompute the current remote commit tx") + require(remoteCommit.txid == commitTx.txid, "txid mismatch, provided tx is not the current remote commit tx") + + val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, commitments.localCommit.index.toInt) + val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(keyManager, remoteCommit.index, localParams, remoteParams, commitInput, remoteCommit.remotePerCommitmentPoint, localPerCommitmentPoint, remoteCommit.spec, commitments.version) + require(remoteCommitTx.tx.txid == commitTx.txid, "txid mismatch, cannot recompute the current remote commit tx") val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(localParams.channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remoteCommit.remotePerCommitmentPoint) - val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, commitments.localCommit.index.toInt) val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(localParams.channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) // we need to use a rather high fee for htlc-claim because we compete with the counterparty @@ -526,7 +561,7 @@ object Helpers { val preimage = preimages.find(r => sha256(r) == add.paymentHash).get val txinfo = Transactions.makeClaimHtlcSuccessTx(remoteCommitTx.tx, outputsAlreadyUsed, Satoshi(localParams.dustLimitSatoshis), localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc) outputsAlreadyUsed = outputsAlreadyUsed + txinfo.input.outPoint.index.toInt - val sig = keyManager.sign(txinfo, keyManager.htlcPoint(localParams.channelKeyPath), remoteCommit.remotePerCommitmentPoint) + val sig = keyManager.sign(txinfo, keyManager.htlcPoint(localParams.channelKeyPath), remoteCommit.remotePerCommitmentPoint, SIGHASH_ALL) Transactions.addSigs(txinfo, sig, preimage) }) @@ -536,12 +571,12 @@ object Helpers { case DirectedHtlc(IN, add: UpdateAddHtlc) => generateTx("claim-htlc-timeout")(Try { val txinfo = Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, outputsAlreadyUsed, Satoshi(localParams.dustLimitSatoshis), localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc) outputsAlreadyUsed = outputsAlreadyUsed + txinfo.input.outPoint.index.toInt - val sig = keyManager.sign(txinfo, keyManager.htlcPoint(localParams.channelKeyPath), remoteCommit.remotePerCommitmentPoint) + val sig = keyManager.sign(txinfo, keyManager.htlcPoint(localParams.channelKeyPath), remoteCommit.remotePerCommitmentPoint, SIGHASH_ALL) Transactions.addSigs(txinfo, sig) }) }.toSeq.flatten - claimRemoteCommitMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx).copy( + claimRemoteCommitMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, commitTx).copy( claimHtlcSuccessTxs = txes.toList.collect { case c: ClaimHtlcSuccessTx => c.tx }, claimHtlcTimeoutTxs = txes.toList.collect { case c: ClaimHtlcTimeoutTx => c.tx } ) @@ -549,33 +584,46 @@ object Helpers { /** * - * Claim our Main output only + * Claim our Main output * * @param commitments either our current commitment data in case of usual remote uncooperative closing * or our outdated commitment data in case of data loss protection procedure; in any case it is used only * to get some constant parameters, not commitment data * @param remotePerCommitmentPoint the remote perCommitmentPoint corresponding to this commitment - * @param tx the remote commitment transaction that has just been published + * @param commitTx the remote commitment transaction that has just been published * @return a list of transactions (one per HTLC that we can claim) */ - def claimRemoteCommitMainOutput(keyManager: KeyManager, commitments: Commitments, remotePerCommitmentPoint: Point, tx: Transaction)(implicit log: LoggingAdapter): RemoteCommitPublished = { - val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(commitments.localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) + def claimRemoteCommitMainOutput(keyManager: KeyManager, commitments: Commitments, remotePerCommitmentPoint: Point, commitTx: Transaction)(implicit log: LoggingAdapter): RemoteCommitPublished = { // no need to use a high fee rate for our main output (we are the only one who can spend it) val feeratePerKwMain = Globals.feeratesPerKw.get.blocks_6 - val mainTx = generateTx("claim-p2wpkh-output")(Try { - val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, Satoshi(commitments.localParams.dustLimitSatoshis), - localPubkey, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain) - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(commitments.localParams.channelKeyPath), remotePerCommitmentPoint) - Transactions.addSigs(claimMain, localPubkey, sig) - }) + val mainTx = commitments.version match { + case VersionCommitmentV1 => + val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(commitments.localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) + generateTx("claim-p2wpkh-output")(Try { + val claimMain = Transactions.makeClaimP2WPKHOutputTx(commitTx, Satoshi(commitments.localParams.dustLimitSatoshis), + localPubkey, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain, None, commitments.version) + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(commitments.localParams.channelKeyPath), remotePerCommitmentPoint, SIGHASH_ALL) + Transactions.addSigs(claimMain, localPubkey, sig) + }) + + case VersionSimplifiedCommitment => + val localPubkey = keyManager.paymentPoint(commitments.localParams.channelKeyPath).publicKey + generateTx("claim-p2wpkh-output")(Try { + val claimMain = Transactions.makeClaimP2WPKHOutputTx(commitTx, Satoshi(commitments.localParams.dustLimitSatoshis), + localPubkey, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain, Some(commitments.localParams.toSelfDelay), commitments.version) + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(commitments.localParams.channelKeyPath), remotePerCommitmentPoint, SIGHASH_ALL) + Transactions.addSigs(claimMain, localPubkey, sig) + }) + } RemoteCommitPublished( - commitTx = tx, + commitTx = commitTx, claimMainOutputTx = mainTx.map(_.tx), claimHtlcSuccessTxs = Nil, claimHtlcTimeoutTxs = Nil, + pushMeTx = None, irrevocablySpent = Map.empty ) } @@ -590,6 +638,7 @@ object Helpers { * @return a [[RevokedCommitPublished]] object containing penalty transactions if the tx is a revoked commitment */ def claimRevokedRemoteCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, tx: Transaction, db: ChannelsDb)(implicit log: LoggingAdapter): Option[RevokedCommitPublished] = { + import commitments._ require(tx.txIn.size == 1, "commitment tx should have 1 input") val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn(0).sequence, tx.lockTime) @@ -615,15 +664,15 @@ object Helpers { // first we will claim our main output right away val mainTx = generateTx("claim-p2wpkh-output")(Try { - val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, Satoshi(localParams.dustLimitSatoshis), localPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain) - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(localParams.channelKeyPath), remotePerCommitmentPoint) + val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, Satoshi(localParams.dustLimitSatoshis), localPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain, Some(localParams.toSelfDelay), commitments.version) + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(localParams.channelKeyPath), remotePerCommitmentPoint, SIGHASH_ALL) Transactions.addSigs(claimMain, localPubkey, sig) }) // then we punish them by stealing their main output val mainPenaltyTx = generateTx("main-penalty")(Try { - val txinfo = Transactions.makeMainPenaltyTx(tx, Satoshi(localParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, feeratePerKwPenalty) - val sig = keyManager.sign(txinfo, keyManager.revocationPoint(localParams.channelKeyPath), remotePerCommitmentSecret) + val txinfo = Transactions.makeMainPenaltyTx(tx, Satoshi(localParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, feeratePerKwPenalty, commitments.version) + val sig = keyManager.sign(txinfo, keyManager.revocationPoint(localParams.channelKeyPath), remotePerCommitmentSecret, SIGHASH_ALL) Transactions.addSigs(txinfo, sig) }) @@ -644,7 +693,7 @@ object Helpers { generateTx("htlc-penalty")(Try { val htlcPenalty = Transactions.makeHtlcPenaltyTx(tx, outputsAlreadyUsed, htlcRedeemScript, Satoshi(localParams.dustLimitSatoshis), localParams.defaultFinalScriptPubKey, feeratePerKwPenalty) outputsAlreadyUsed = outputsAlreadyUsed + htlcPenalty.input.outPoint.index.toInt - val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(localParams.channelKeyPath), remotePerCommitmentSecret) + val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(localParams.channelKeyPath), remotePerCommitmentSecret, SIGHASH_ALL) Transactions.addSigs(htlcPenalty, sig, remoteRevocationPubkey) }) }.toList.flatten @@ -699,7 +748,7 @@ object Helpers { generateTx("claim-htlc-delayed-penalty")(Try { val htlcDelayedPenalty = Transactions.makeClaimDelayedOutputPenaltyTx(htlcTx, Satoshi(localParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty) - val sig = keyManager.sign(htlcDelayedPenalty, keyManager.revocationPoint(localParams.channelKeyPath), remotePerCommitmentSecret) + val sig = keyManager.sign(htlcDelayedPenalty, keyManager.revocationPoint(localParams.channelKeyPath), remotePerCommitmentSecret, SIGHASH_ALL) val signedTx = Transactions.addSigs(htlcDelayedPenalty, sig) // we need to make sure that the tx is indeed valid Transaction.correctlySpends(signedTx.tx, Seq(htlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) @@ -763,10 +812,10 @@ object Helpers { * @param tx a tx that has reached mindepth * @return a set of htlcs that need to be failed upstream */ - def timedoutHtlcs(localCommit: LocalCommit, localDustLimit: Satoshi, tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = + def timedoutHtlcs(localCommit: LocalCommit, localDustLimit: Satoshi, tx: Transaction, commitmentVersion: CommitmentVersion)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = if (tx.txid == localCommit.publishableTxs.commitTx.tx.txid) { // the tx is a commitment tx, we can immediately fail all dust htlcs (they don't have an output in the tx) - (localCommit.spec.htlcs.filter(_.direction == OUT) -- Transactions.trimOfferedHtlcs(localDustLimit, localCommit.spec)).map(_.add) + (localCommit.spec.htlcs.filter(_.direction == OUT) -- Transactions.trimOfferedHtlcs(localDustLimit, localCommit.spec, commitmentVersion)).map(_.add) } else { // maybe this is a timeout tx, in that case we can resolve and fail the corresponding htlc tx.txIn.map(_.witness match { @@ -787,10 +836,10 @@ object Helpers { * @param tx a tx that has reached mindepth * @return a set of htlcs that need to be failed upstream */ - def timedoutHtlcs(remoteCommit: RemoteCommit, remoteDustLimit: Satoshi, tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = + def timedoutHtlcs(remoteCommit: RemoteCommit, remoteDustLimit: Satoshi, tx: Transaction, commitmentVersion: CommitmentVersion)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = if (tx.txid == remoteCommit.txid) { // the tx is a commitment tx, we can immediately fail all dust htlcs (they don't have an output in the tx) - (remoteCommit.spec.htlcs.filter(_.direction == IN) -- Transactions.trimReceivedHtlcs(remoteDustLimit, remoteCommit.spec)).map(_.add) + (remoteCommit.spec.htlcs.filter(_.direction == IN) -- Transactions.trimReceivedHtlcs(remoteDustLimit, remoteCommit.spec, commitmentVersion)).map(_.add) } else { // maybe this is a timeout tx, in that case we can resolve and fail the corresponding htlc tx.txIn.map(_.witness match { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala index 28a739c868..72799ceb45 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala @@ -46,10 +46,11 @@ trait KeyManager { * * @param tx input transaction * @param publicKey extended public key + * @param sigHash the sigHash type used to sign * @return a signature generated with the private key that matches the input * extended public key */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey): ByteVector + def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, sigHash: Int): ByteVector /** * This method is used to spend funds send to htlc keys/delayed keys @@ -57,10 +58,11 @@ trait KeyManager { * @param tx input transaction * @param publicKey extended public key * @param remotePoint remote point + * @param sigHash the sigHash type used to sign * @return a signature generated with a private key generated from the input keys's matching * private key and the remote point. */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: Point): ByteVector + def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: Point, sigHash: Int): ByteVector /** * Ths method is used to spend revoked transactions, with the corresponding revocation key @@ -68,10 +70,11 @@ trait KeyManager { * @param tx input transaction * @param publicKey extended public key * @param remoteSecret remote secret + * @param sigHash the sigHash type used to sign * @return a signature generated with a private key generated from the input keys's matching * private key and the remote secret. */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: Scalar): ByteVector + def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: Scalar, sigHash: Int): ByteVector def signChannelAnnouncement(channelKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector, ByteVector) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala index 3c8e8e5f44..8bc12987e6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala @@ -98,12 +98,13 @@ class LocalKeyManager(seed: ByteVector, chainHash: ByteVector32) extends KeyMana * * @param tx input transaction * @param publicKey extended public key + * @param sigHash the sigHash type used to sign * @return a signature generated with the private key that matches the input * extended public key */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey): ByteVector = { + def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, sigHash: Int): ByteVector = { val privateKey = privateKeys.get(publicKey.path) - Transactions.sign(tx, privateKey.privateKey) + Transactions.sign(tx, privateKey.privateKey, sigHash) } /** @@ -112,13 +113,14 @@ class LocalKeyManager(seed: ByteVector, chainHash: ByteVector32) extends KeyMana * @param tx input transaction * @param publicKey extended public key * @param remotePoint remote point + * @param sigHash the sigHash type used to sign * @return a signature generated with a private key generated from the input keys's matching * private key and the remote point. */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: Point): ByteVector = { + def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: Point, sigHash: Int): ByteVector = { val privateKey = privateKeys.get(publicKey.path) val currentKey = Generators.derivePrivKey(privateKey.privateKey, remotePoint) - Transactions.sign(tx, currentKey) + Transactions.sign(tx, currentKey, sigHash) } /** @@ -127,13 +129,14 @@ class LocalKeyManager(seed: ByteVector, chainHash: ByteVector32) extends KeyMana * @param tx input transaction * @param publicKey extended public key * @param remoteSecret remote secret + * @param sigHash the sigHash type used to sign * @return a signature generated with a private key generated from the input keys's matching * private key and the remote secret. */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: Scalar): ByteVector = { + def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: Scalar, sigHash: Int): ByteVector = { val privateKey = privateKeys.get(publicKey.path) val currentKey = Generators.revocationPrivKey(privateKey.privateKey, remoteSecret) - Transactions.sign(tx, currentKey) + Transactions.sign(tx, currentKey, sigHash) } override def signChannelAnnouncement(channelKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector, ByteVector) = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala index 0ccc2fb2fa..0b7c6f3f80 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala @@ -21,7 +21,7 @@ import java.sql.Connection import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.channel.HasCommitments import fr.acinq.eclair.db.ChannelsDb -import fr.acinq.eclair.wire.ChannelCodecs.stateDataCodec +import fr.acinq.eclair.wire.ChannelCodecs.genericStateDataCodec import scala.collection.immutable.Queue @@ -42,7 +42,7 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb { } override def addOrUpdateChannel(state: HasCommitments): Unit = { - val data = stateDataCodec.encode(state).require.toByteArray + val data = genericStateDataCodec.encode(state).require.toByteArray using (sqlite.prepareStatement("UPDATE local_channels SET data=? WHERE channel_id=?")) { update => update.setBytes(1, data) update.setBytes(2, state.channelId.toArray) @@ -76,7 +76,7 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb { override def listChannels(): Seq[HasCommitments] = { using(sqlite.createStatement) { statement => val rs = statement.executeQuery("SELECT data FROM local_channels") - codecSequence(rs, stateDataCodec) + codecSequence(rs, genericStateDataCodec) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 66b9a2f4b1..b6e42b19e5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -121,8 +121,15 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor val remoteHasInitialRoutingSync = Features.hasFeature(remoteInit.localFeatures, Features.INITIAL_ROUTING_SYNC_BIT_OPTIONAL) val remoteHasChannelRangeQueriesOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_OPTIONAL) val remoteHasChannelRangeQueriesMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_MANDATORY) - log.info(s"peer is using globalFeatures=${remoteInit.globalFeatures.toBin} and localFeatures=${remoteInit.localFeatures.toBin}") - log.info(s"$remoteNodeId has features: initialRoutingSync=$remoteHasInitialRoutingSync channelRangeQueriesOptional=$remoteHasChannelRangeQueriesOptional channelRangeQueriesMandatory=$remoteHasChannelRangeQueriesMandatory") + val remoteHasSimplifiedCommitmentOptional = Features.hasFeature(remoteInit.localFeatures, Features.OPTION_SIMPLIFIED_COMMITMENT_OPTIONAL) + val remoteHasSimplifiedCommitmentMandatory = Features.hasFeature(remoteInit.localFeatures, Features.OPTION_SIMPLIFIED_COMMITMENT_MANDATORY) + log.info(s"$remoteNodeId has features: " + + s"initialRoutingSync=$remoteHasInitialRoutingSync " + + s"channelRangeQueriesOptional=$remoteHasChannelRangeQueriesOptional " + + s"channelRangeQueriesMandatory=$remoteHasChannelRangeQueriesMandatory" + + s"remoteHasSimplifiedCommitmentOptional=$remoteHasSimplifiedCommitmentOptional" + + s"remoteHasSimplifiedCommitmentMandatory=$remoteHasSimplifiedCommitmentMandatory") + if (Features.areSupported(remoteInit.localFeatures)) { d.origin_opt.foreach(origin => origin ! "connected") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala index 1e23d8205d..19a91dbbe9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala @@ -30,6 +30,7 @@ case object OUT extends Direction { def opposite = IN } case class DirectedHtlc(direction: Direction, add: UpdateAddHtlc) +// TODO consider option_simplified_commitment final case class CommitmentSpec(htlcs: Set[DirectedHtlc], feeratePerKw: Long, toLocalMsat: Long, toRemoteMsat: Long) { val totalFunds = toLocalMsat + toRemoteMsat + htlcs.toSeq.map(_.add.amountMsat).sum } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala index d8ab4c77fb..54bd27b5ce 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.Crypto.{PublicKey, ripemd160} import fr.acinq.bitcoin.Script._ -import fr.acinq.bitcoin.{ByteVector32, LexicographicalOrdering, LockTimeThreshold, OP_0, OP_1, OP_1NEGATE, OP_2, OP_2DROP, OP_ADD, OP_CHECKLOCKTIMEVERIFY, OP_CHECKMULTISIG, OP_CHECKSEQUENCEVERIFY, OP_CHECKSIG, OP_DROP, OP_DUP, OP_ELSE, OP_ENDIF, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_IF, OP_NOTIF, OP_PUSHDATA, OP_SIZE, OP_SWAP, Satoshi, Script, ScriptElt, ScriptWitness, Transaction, TxIn} +import fr.acinq.bitcoin.{ByteVector32, LexicographicalOrdering, LockTimeThreshold, OP_0, OP_1, OP_10, OP_1NEGATE, OP_2, OP_2DROP, OP_ADD, OP_CHECKLOCKTIMEVERIFY, OP_CHECKMULTISIG, OP_CHECKSEQUENCEVERIFY, OP_CHECKSIG, OP_DEPTH, OP_DROP, OP_DUP, OP_ELSE, OP_ENDIF, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_IF, OP_NOTIF, OP_PUSHDATA, OP_SIZE, OP_SWAP, Satoshi, Script, ScriptElt, ScriptWitness, Transaction, TxIn} import scodec.bits.ByteVector /** @@ -185,6 +185,24 @@ object Scripts { // @formatter:on } + /** + * Output script for the to_remote output of the commitment (must use with option_simplified_commitment) + * https://github.com/lightningnetwork/lightning-rfc/pull/513/files#diff-bfcf64bee684b75d7a670873c3ece6f2R117 + * + * @param toSelfDelay the delay that will encumber the output + * @param remotePubkey the recipient's pubkey + * @return + */ + def toRemoteDelayed(remotePubkey: PublicKey, toSelfDelay: Int) = { + // @formatter:off + encodeNumber(toSelfDelay) :: + OP_CHECKSEQUENCEVERIFY :: + OP_DROP :: + OP_PUSHDATA(remotePubkey) :: + OP_CHECKSIG :: Nil + // @formatter:on + } + /** * This witness script spends a [[toLocalDelayed]] output using a local sig after a delay */ @@ -251,6 +269,35 @@ object Scripts { // @formatter:on } + /** + * Use with `option_simplified_commitment` + */ + def pushMeSimplified(pubkey: PublicKey): List[ScriptElt] = { + // @formatter:off + OP_DEPTH :: // Puts the number of stack items onto the stack. + OP_IF :: + OP_PUSHDATA(pubkey) :: OP_CHECKSIG :: + OP_ELSE :: + OP_10 :: OP_CHECKSEQUENCEVERIFY :: + OP_ENDIF :: Nil + // @formatter:on + } + + /** + * The script spending 'pushMeSimplified' outputs, signature based version + * + * @param sig + * @return + */ + def claimPushMeOutputWithKey(sig: ByteVector) = ScriptWitness(sig :: Nil) + + /** + * The script spending 'pushMeSimplified' outputs, 10 block delay - no signature - version. + * + * @return + */ + def claimPushMeOutputDelayed() = ScriptWitness(Seq.empty) + /** * This is the witness script of the 2nd-stage HTLC Timeout transaction (consumes htlcReceived script from commit tx) */ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index f725fb5293..8d9b10f088 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -22,10 +22,12 @@ import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, ripemd160} import fr.acinq.bitcoin.Script._ import fr.acinq.bitcoin.SigVersion._ import fr.acinq.bitcoin.{ByteVector32, Crypto, LexicographicalOrdering, MilliSatoshi, OutPoint, Protocol, SIGHASH_ALL, Satoshi, Script, ScriptElt, ScriptFlags, ScriptWitness, Transaction, TxIn, TxOut, millisatoshi2satoshi} +import fr.acinq.eclair.channel._ import fr.acinq.eclair.transactions.Scripts._ import fr.acinq.eclair.wire.UpdateAddHtlc import scodec.bits.ByteVector +import scala.reflect.ClassTag import scala.util.Try /** @@ -60,6 +62,7 @@ object Transactions { case class MainPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo case class HtlcPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo case class ClosingTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo + case class PushMeTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo sealed trait TxGenerationSkipped extends RuntimeException case object OutputNotFound extends RuntimeException(s"output not found (probably trimmed)") with TxGenerationSkipped @@ -94,8 +97,11 @@ object Transactions { * these values are defined in the RFC */ val commitWeight = 724 + val simplifiedCommitWeight = 1116 val htlcTimeoutWeight = 663 val htlcSuccessWeight = 703 + val simplifiedFeerateKw = 253 + val pushMeValue = Satoshi(1000) // used in option_simplified_commitment /** * these values specific to us and used to estimate fees @@ -117,27 +123,35 @@ object Transactions { */ def fee2rate(fee: Satoshi, weight: Int) = (fee.amount * 1000L) / weight - def trimOfferedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec): Seq[DirectedHtlc] = { - val htlcTimeoutFee = weight2fee(spec.feeratePerKw, htlcTimeoutWeight) + def trimOfferedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec, commitmentVersion: CommitmentVersion): Seq[DirectedHtlc] = { + val htlcTimeoutFee = commitmentVersion match { + case VersionCommitmentV1 => weight2fee(spec.feeratePerKw, htlcTimeoutWeight) + case VersionSimplifiedCommitment => Satoshi(0) + } spec.htlcs .filter(_.direction == OUT) .filter(htlc => MilliSatoshi(htlc.add.amountMsat) >= (dustLimit + htlcTimeoutFee)) .toSeq } - def trimReceivedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec): Seq[DirectedHtlc] = { - val htlcSuccessFee = weight2fee(spec.feeratePerKw, htlcSuccessWeight) + def trimReceivedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec, commitmentVersion: CommitmentVersion): Seq[DirectedHtlc] = { + val htlcSuccessFee = commitmentVersion match { + case VersionCommitmentV1 => weight2fee(spec.feeratePerKw, htlcSuccessWeight) + case VersionSimplifiedCommitment => Satoshi(0) + } spec.htlcs .filter(_.direction == IN) .filter(htlc => MilliSatoshi(htlc.add.amountMsat) >= (dustLimit + htlcSuccessFee)) .toSeq } - def commitTxFee(dustLimit: Satoshi, spec: CommitmentSpec): Satoshi = { - val trimmedOfferedHtlcs = trimOfferedHtlcs(dustLimit, spec) - val trimmedReceivedHtlcs = trimReceivedHtlcs(dustLimit, spec) - val weight = commitWeight + 172 * (trimmedOfferedHtlcs.size + trimmedReceivedHtlcs.size) - weight2fee(spec.feeratePerKw, weight) + def commitTxFee(dustLimit: Satoshi, spec: CommitmentSpec, commitmentVersion: CommitmentVersion): Satoshi = { + val trimmedOfferedHtlcs = trimOfferedHtlcs(dustLimit, spec, commitmentVersion) + val trimmedReceivedHtlcs = trimReceivedHtlcs(dustLimit, spec, commitmentVersion) + commitmentVersion match { + case VersionCommitmentV1 => weight2fee(spec.feeratePerKw , commitWeight + 172 * (trimmedOfferedHtlcs.size + trimmedReceivedHtlcs.size)) + case VersionSimplifiedCommitment => weight2fee(simplifiedFeerateKw , simplifiedCommitWeight + 172 * (trimmedOfferedHtlcs.size + trimmedReceivedHtlcs.size)) // simplified commitment has an hardcoded feerate + } } /** @@ -186,32 +200,65 @@ object Transactions { def decodeTxNumber(sequence: Long, locktime: Long): Long = ((sequence & 0xffffffL) << 24) + (locktime & 0xffffffL) - def makeCommitTx(commitTxInput: InputInfo, commitTxNumber: Long, localPaymentBasePoint: Point, remotePaymentBasePoint: Point, localIsFunder: Boolean, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: Int, localDelayedPaymentPubkey: PublicKey, remotePaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, spec: CommitmentSpec): CommitTx = { - val commitFee = commitTxFee(localDustLimit, spec) + def makeCommitTx(commitTxInput: InputInfo, commitTxNumber: Long, localPaymentBasePoint: Point, remotePaymentBasePoint: Point, localIsFunder: Boolean, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: Int, localDelayedPaymentPubkey: PublicKey, remotePaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, remoteDelayedPaymentPubkey: PublicKey, spec: CommitmentSpec, commitmentVersion: CommitmentVersion): CommitTx = commitmentVersion match { - val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localIsFunder) { - (millisatoshi2satoshi(MilliSatoshi(spec.toLocalMsat)) - commitFee, millisatoshi2satoshi(MilliSatoshi(spec.toRemoteMsat))) - } else { - (millisatoshi2satoshi(MilliSatoshi(spec.toLocalMsat)), millisatoshi2satoshi(MilliSatoshi(spec.toRemoteMsat)) - commitFee) - } // NB: we don't care if values are < 0, they will be trimmed if they are < dust limit anyway + case VersionCommitmentV1 => + val commitFee = commitTxFee(localDustLimit, spec, commitmentVersion) - val toLocalDelayedOutput_opt = if (toLocalAmount >= localDustLimit) Some(TxOut(toLocalAmount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)))) else None - val toRemoteOutput_opt = if (toRemoteAmount >= localDustLimit) Some(TxOut(toRemoteAmount, pay2wpkh(remotePaymentPubkey))) else None + val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localIsFunder) { + (millisatoshi2satoshi(MilliSatoshi(spec.toLocalMsat)) - commitFee, millisatoshi2satoshi(MilliSatoshi(spec.toRemoteMsat))) + } else { + (millisatoshi2satoshi(MilliSatoshi(spec.toLocalMsat)), millisatoshi2satoshi(MilliSatoshi(spec.toRemoteMsat)) - commitFee) + } // NB: we don't care if values are < 0, they will be trimmed if they are < dust limit anyway - val htlcOfferedOutputs = trimOfferedHtlcs(localDustLimit, spec) - .map(htlc => TxOut(MilliSatoshi(htlc.add.amountMsat), pay2wsh(htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes))))) - val htlcReceivedOutputs = trimReceivedHtlcs(localDustLimit, spec) - .map(htlc => TxOut(MilliSatoshi(htlc.add.amountMsat), pay2wsh(htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), htlc.add.cltvExpiry)))) + val toLocalDelayedOutput_opt = if (toLocalAmount >= localDustLimit) Some(TxOut(toLocalAmount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)))) else None + val toRemoteOutput_opt = if (toRemoteAmount >= localDustLimit) Some(TxOut(toRemoteAmount, pay2wpkh(remotePaymentPubkey))) else None - val txnumber = obscuredCommitTxNumber(commitTxNumber, localIsFunder, localPaymentBasePoint, remotePaymentBasePoint) - val (sequence, locktime) = encodeTxNumber(txnumber) + val htlcOfferedOutputs = trimOfferedHtlcs(localDustLimit, spec, commitmentVersion) + .map(htlc => TxOut(MilliSatoshi(htlc.add.amountMsat), pay2wsh(htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes))))) + val htlcReceivedOutputs = trimReceivedHtlcs(localDustLimit, spec, commitmentVersion) + .map(htlc => TxOut(MilliSatoshi(htlc.add.amountMsat), pay2wsh(htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), htlc.add.cltvExpiry)))) - val tx = Transaction( - version = 2, - txIn = TxIn(commitTxInput.outPoint, ByteVector.empty, sequence = sequence) :: Nil, - txOut = toLocalDelayedOutput_opt.toSeq ++ toRemoteOutput_opt.toSeq ++ htlcOfferedOutputs ++ htlcReceivedOutputs, - lockTime = locktime) - CommitTx(commitTxInput, LexicographicalOrdering.sort(tx)) + val txnumber = obscuredCommitTxNumber(commitTxNumber, localIsFunder, localPaymentBasePoint, remotePaymentBasePoint) + val (sequence, locktime) = encodeTxNumber(txnumber) + + val tx = Transaction( + version = 2, + txIn = TxIn(commitTxInput.outPoint, ByteVector.empty, sequence = sequence) :: Nil, + txOut = toLocalDelayedOutput_opt.toSeq ++ toRemoteOutput_opt.toSeq ++ htlcOfferedOutputs ++ htlcReceivedOutputs, + lockTime = locktime) + CommitTx(commitTxInput, LexicographicalOrdering.sort(tx)) + + case VersionSimplifiedCommitment => + val commitFee = commitTxFee(localDustLimit, spec, commitmentVersion) + val pushMeValueTotal = pushMeValue * 2 // funder pays the total amount of pushme outputs + + val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localIsFunder) { + (millisatoshi2satoshi(MilliSatoshi(spec.toLocalMsat)) - commitFee - pushMeValueTotal, millisatoshi2satoshi(MilliSatoshi(spec.toRemoteMsat))) + } else { + (millisatoshi2satoshi(MilliSatoshi(spec.toLocalMsat)), millisatoshi2satoshi(MilliSatoshi(spec.toRemoteMsat)) - commitFee - pushMeValueTotal) + } // NB: we don't care if values are < 0, they will be trimmed if they are < dust limit anyway + + val toLocalDelayedOutput_opt = if (toLocalAmount < localDustLimit) None else Some(TxOut(toLocalAmount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)))) + val toRemoteOutput_opt = if (toRemoteAmount < localDustLimit) None else Some(TxOut(toRemoteAmount, pay2wsh(toRemoteDelayed(remotePaymentPubkey, toLocalDelay)))) + + val htlcOfferedOutputs = trimOfferedHtlcs(localDustLimit, spec, commitmentVersion) + .map(htlc => TxOut(MilliSatoshi(htlc.add.amountMsat), pay2wsh(htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash))))) + val htlcReceivedOutputs = trimReceivedHtlcs(localDustLimit, spec, commitmentVersion) + .map(htlc => TxOut(MilliSatoshi(htlc.add.amountMsat), pay2wsh(htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash), htlc.add.cltvExpiry)))) + + val toLocalPushMe_opt = if (toLocalDelayedOutput_opt.isDefined) Some(TxOut(pushMeValue, pay2wsh(pushMeSimplified(localDelayedPaymentPubkey)))) else None + val toRemotePushMe_opt = if (toRemoteOutput_opt.isDefined) Some(TxOut(pushMeValue, pay2wsh(pushMeSimplified(remoteDelayedPaymentPubkey)))) else None + + val txnumber = obscuredCommitTxNumber(commitTxNumber, localIsFunder, localPaymentBasePoint, remotePaymentBasePoint) + val (sequence, locktime) = encodeTxNumber(txnumber) + + val tx = Transaction( + version = 2, + txIn = TxIn(commitTxInput.outPoint, ByteVector.empty, sequence = sequence) :: Nil, + txOut = toLocalDelayedOutput_opt.toSeq ++ toRemoteOutput_opt.toSeq ++ htlcOfferedOutputs ++ htlcReceivedOutputs ++ toLocalPushMe_opt.toSeq ++ toRemotePushMe_opt.toSeq, + lockTime = locktime) + CommitTx(commitTxInput, LexicographicalOrdering.sort(tx)) } def makeHtlcTimeoutTx(commitTx: Transaction, outputsAlreadyUsed: Set[Int], localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: Int, localDelayedPaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, feeratePerKw: Long, htlc: UpdateAddHtlc): HtlcTimeoutTx = { @@ -248,14 +295,14 @@ object Transactions { lockTime = 0), htlc.paymentHash) } - def makeHtlcTxs(commitTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: Int, localDelayedPaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, spec: CommitmentSpec): (Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { + def makeHtlcTxs(commitTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: Int, localDelayedPaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, spec: CommitmentSpec, commitmentVersion: CommitmentVersion): (Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { var outputsAlreadyUsed = Set.empty[Int] // this is needed to handle cases where we have several identical htlcs - val htlcTimeoutTxs = trimOfferedHtlcs(localDustLimit, spec).map { htlc => + val htlcTimeoutTxs = trimOfferedHtlcs(localDustLimit, spec, commitmentVersion).map { htlc => val htlcTx = makeHtlcTimeoutTx(commitTx, outputsAlreadyUsed, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec.feeratePerKw, htlc.add) outputsAlreadyUsed = outputsAlreadyUsed + htlcTx.input.outPoint.index.toInt htlcTx } - val htlcSuccessTxs = trimReceivedHtlcs(localDustLimit, spec).map { htlc => + val htlcSuccessTxs = trimReceivedHtlcs(localDustLimit, spec, commitmentVersion).map { htlc => val htlcTx = makeHtlcSuccessTx(commitTx, outputsAlreadyUsed, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec.feeratePerKw, htlc.add) outputsAlreadyUsed = outputsAlreadyUsed + htlcTx.input.outPoint.index.toInt htlcTx @@ -311,33 +358,56 @@ object Transactions { ClaimHtlcTimeoutTx(input, tx1) } - def makeClaimP2WPKHOutputTx(delayedOutputTx: Transaction, localDustLimit: Satoshi, localPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: Long): ClaimP2WPKHOutputTx = { - val redeemScript = Script.pay2pkh(localPaymentPubkey) - val pubkeyScript = write(pay2wpkh(localPaymentPubkey)) - val outputIndex = findPubKeyScriptIndex(delayedOutputTx, pubkeyScript, outputsAlreadyUsed = Set.empty, amount_opt = None) - val input = InputInfo(OutPoint(delayedOutputTx, outputIndex), delayedOutputTx.txOut(outputIndex), write(redeemScript)) + def makeClaimP2WPKHOutputTx(delayedOutputTx: Transaction, localDustLimit: Satoshi, localPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: Long, toRemoteDelay: Option[Int], commitmentVersion: CommitmentVersion): ClaimP2WPKHOutputTx = { + + val claimTx = commitmentVersion match { + case VersionCommitmentV1 => + val redeemScript = Script.pay2pkh(localPaymentPubkey) + val pubkeyScript = write(pay2wpkh(localPaymentPubkey)) + val outputIndex = findPubKeyScriptIndex(delayedOutputTx, pubkeyScript, outputsAlreadyUsed = Set.empty, amount_opt = None) + val input = InputInfo(OutPoint(delayedOutputTx, outputIndex), delayedOutputTx.txOut(outputIndex), write(redeemScript)) + + // unsigned tx + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, 0x00000000L) :: Nil, + txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, + lockTime = 0) + + // compute weight with a dummy 73 bytes signature (the largest you can get) and a dummy 33 bytes pubkey + Transactions.addSigs(ClaimP2WPKHOutputTx(input, tx), ByteVector.fill(33)(0), ByteVector.fill(73)(0)) + + // here localPaymentPubkey == localDelayedPaymentPubkey + case VersionSimplifiedCommitment => + val redeemScript = Script.pay2pkh(localPaymentPubkey) + val pubkeyScript = write(toRemoteDelayed(localPaymentPubkey, toRemoteDelay.getOrElse(throw new IllegalArgumentException("Error claiming the main output from remote commit, no 'toRemoteDelay' specified. (option_simplified_commitment)")))) + val outputIndex = findPubKeyScriptIndex(delayedOutputTx, pubkeyScript, outputsAlreadyUsed = Set.empty, amount_opt = None) + val input = InputInfo(OutPoint(delayedOutputTx, outputIndex), delayedOutputTx.txOut(outputIndex), write(redeemScript)) + + // unsigned tx + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, 0x00000000L) :: Nil, + txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, + lockTime = 0) + + // compute weight with a dummy 73 bytes signature (the largest you can get) and a dummy 33 bytes pubkey + Transactions.addSigs(ClaimP2WPKHOutputTx(input, tx), ByteVector.fill(33)(0), ByteVector.fill(73)(0)) + } - // unsigned tx - val tx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, 0x00000000L) :: Nil, - txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, - lockTime = 0) - // compute weight with a dummy 73 bytes signature (the largest you can get) and a dummy 33 bytes pubkey - val weight = Transactions.addSigs(ClaimP2WPKHOutputTx(input, tx), ByteVector.fill(33)(0), ByteVector.fill(73)(0)).tx.weight() - val fee = weight2fee(feeratePerKw, weight) + val fee = weight2fee(feeratePerKw, claimTx.tx.weight()) - val amount = input.txOut.amount - fee + val amount = claimTx.input.txOut.amount - fee if (amount < localDustLimit) { throw AmountBelowDustLimit } - val tx1 = tx.copy(txOut = tx.txOut(0).copy(amount = amount) :: Nil) - ClaimP2WPKHOutputTx(input, tx1) + val tx1 = claimTx.tx.copy(txOut = claimTx.tx.txOut(0).copy(amount = amount) :: Nil) // even in case of simplified commitment the main output is at index 0 + ClaimP2WPKHOutputTx(claimTx.input, tx1) } - def makeClaimDelayedOutputTx(delayedOutputTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: Int, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: Long): ClaimDelayedOutputTx = { + def makeClaimDelayedOutputTx(delayedOutputTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: Int, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: Long, commitmentVersion: CommitmentVersion): ClaimDelayedOutputTx = { val redeemScript = toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) val pubkeyScript = write(pay2wsh(redeemScript)) val outputIndex = findPubKeyScriptIndex(delayedOutputTx, pubkeyScript, outputsAlreadyUsed = Set.empty, amount_opt = None) @@ -350,17 +420,20 @@ object Transactions { txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, lockTime = 0) + val claimTx = ClaimDelayedOutputTx(input, tx) + + // compute weight with a dummy 73 bytes signature (the largest you can get) - val weight = Transactions.addSigs(ClaimDelayedOutputTx(input, tx), ByteVector.fill(73)(0)).tx.weight() + val weight = Transactions.addSigs(claimTx, ByteVector.fill(73)(0)).tx.weight() val fee = weight2fee(feeratePerKw, weight) - val amount = input.txOut.amount - fee + val amount = claimTx.input.txOut.amount - fee if (amount < localDustLimit) { throw AmountBelowDustLimit } - val tx1 = tx.copy(txOut = tx.txOut(0).copy(amount = amount) :: Nil) - ClaimDelayedOutputTx(input, tx1) + val tx1 = claimTx.tx.copy(txOut = claimTx.tx.txOut(0).copy(amount = amount) :: Nil) + ClaimDelayedOutputTx(claimTx.input, tx1) } def makeClaimDelayedOutputPenaltyTx(delayedOutputTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: Int, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: Long): ClaimDelayedOutputPenaltyTx = { @@ -389,35 +462,39 @@ object Transactions { ClaimDelayedOutputPenaltyTx(input, tx1) } - def makeMainPenaltyTx(commitTx: Transaction, localDustLimit: Satoshi, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: ByteVector, toRemoteDelay: Int, remoteDelayedPaymentPubkey: PublicKey, feeratePerKw: Long): MainPenaltyTx = { - val redeemScript = toLocalDelayed(remoteRevocationPubkey, toRemoteDelay, remoteDelayedPaymentPubkey) - val pubkeyScript = write(pay2wsh(redeemScript)) - val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed = Set.empty, amount_opt = None) - val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) + // TODO adjust for option_simplified_commitment -> sweep pushme outputs + def makeMainPenaltyTx(commitTx: Transaction, localDustLimit: Satoshi, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: ByteVector, toRemoteDelay: Int, remoteDelayedPaymentPubkey: PublicKey, feeratePerKw: Long, commitmentVersion: CommitmentVersion): MainPenaltyTx = commitmentVersion match { + case VersionSimplifiedCommitment => throw new NotImplementedError("makeMainPenaltyTx with option_simplified_commitment") + case VersionCommitmentV1 => + val redeemScript = toLocalDelayed(remoteRevocationPubkey, toRemoteDelay, remoteDelayedPaymentPubkey) + val pubkeyScript = write(pay2wsh(redeemScript)) + val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed = Set.empty, amount_opt = None) + val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) - // unsigned transaction - val tx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, 0xffffffffL) :: Nil, - txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, - lockTime = 0) + // unsigned transaction + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, 0xffffffffL) :: Nil, + txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, + lockTime = 0) - // compute weight with a dummy 73 bytes signature (the largest you can get) - val weight = Transactions.addSigs(MainPenaltyTx(input, tx), ByteVector.fill(73)(0)).tx.weight() - val fee = weight2fee(feeratePerKw, weight) + // compute weight with a dummy 73 bytes signature (the largest you can get) + val weight = Transactions.addSigs(MainPenaltyTx(input, tx), ByteVector.fill(73)(0)).tx.weight() + val fee = weight2fee(feeratePerKw, weight) - val amount = input.txOut.amount - fee - if (amount < localDustLimit) { - throw AmountBelowDustLimit - } + val amount = input.txOut.amount - fee + if (amount < localDustLimit) { + throw AmountBelowDustLimit + } - val tx1 = tx.copy(txOut = tx.txOut(0).copy(amount = amount) :: Nil) - MainPenaltyTx(input, tx1) + val tx1 = tx.copy(txOut = tx.txOut(0).copy(amount = amount) :: Nil) + MainPenaltyTx(input, tx1) } /** * We already have the redeemScript, no need to build it */ + // TODO adjust for option_simplified_commitment ? def makeHtlcPenaltyTx(commitTx: Transaction, outputsAlreadyUsed: Set[Int], redeemScript: ByteVector, localDustLimit: Satoshi, localFinalScriptPubKey: ByteVector, feeratePerKw: Long): HtlcPenaltyTx = { val pubkeyScript = write(pay2wsh(redeemScript)) val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = None) @@ -443,6 +520,7 @@ object Transactions { HtlcPenaltyTx(input, tx1) } + // TODO adjust for option_simplified_commitment def makeClosingTx(commitTxInput: InputInfo, localScriptPubKey: ByteVector, remoteScriptPubKey: ByteVector, localIsFunder: Boolean, dustLimit: Satoshi, closingFee: Satoshi, spec: CommitmentSpec): ClosingTx = { require(spec.htlcs.isEmpty, "there shouldn't be any pending htlcs") @@ -463,10 +541,36 @@ object Transactions { ClosingTx(commitTxInput, LexicographicalOrdering.sort(tx)) } + def makePushMeCPFP(commitTx: Transaction, toLocalDelayed: PublicKey, feeratePerKw: Long, localDustLimit: Satoshi): PushMeTx = { + val redeemScript = Scripts.pushMeSimplified(toLocalDelayed) + val pubKeyScript = write(pay2wsh(redeemScript)) + + val outputIndex = findPubKeyScriptIndex(commitTx, pubKeyScript, Set.empty, Some(pushMeValue)) + val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), redeemScript) + + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, 0xffffffffL) :: Nil, + txOut = TxOut(pushMeValue, List.empty) :: Nil, // TODO to what do we spend the pushMe? FIXME + lockTime = 0 + ) + + val weight = Transactions.addSigs(PushMeTx(input, tx), ByteVector.fill(73)(0)).tx.weight() + val fee = weight2fee(feeratePerKw, weight) + + val amount = input.txOut.amount - fee + if (amount < localDustLimit) { + throw AmountBelowDustLimit + } + + val tx1 = tx.copy(txOut = tx.txOut(0).copy(amount = amount) :: Nil) + PushMeTx(input, tx1) + } + def findPubKeyScriptIndex(tx: Transaction, pubkeyScript: ByteVector, outputsAlreadyUsed: Set[Int], amount_opt: Option[Satoshi]): Int = { val outputIndex = tx.txOut .zipWithIndex - .indexWhere { case (txOut, index) => amount_opt.map(_ == txOut.amount).getOrElse(true) && txOut.publicKeyScript == pubkeyScript && !outputsAlreadyUsed.contains(index)} // it's not enough to only resolve on pubkeyScript because we may have duplicates + .indexWhere { case (txOut, index) => amount_opt.map(_ == txOut.amount).getOrElse(true) && txOut.publicKeyScript == pubkeyScript && !outputsAlreadyUsed.contains(index) } // it's not enough to only resolve on pubkeyScript because we may have duplicates if (outputIndex >= 0) { outputIndex } else { @@ -474,14 +578,13 @@ object Transactions { } } - - def sign(tx: Transaction, inputIndex: Int, redeemScript: ByteVector, amount: Satoshi, key: PrivateKey): ByteVector = { - Transaction.signInput(tx, inputIndex, redeemScript, SIGHASH_ALL, amount, SIGVERSION_WITNESS_V0, key) + def sign(tx: Transaction, inputIndex: Int, redeemScript: ByteVector, amount: Satoshi, key: PrivateKey, sigHash: Int): ByteVector = { + Transaction.signInput(tx, inputIndex, redeemScript, sigHash, amount, SIGVERSION_WITNESS_V0, key) } - def sign(txinfo: TransactionWithInputInfo, key: PrivateKey): ByteVector = { + def sign(txinfo: TransactionWithInputInfo, key: PrivateKey, sigHash: Int): ByteVector = { require(txinfo.tx.txIn.lengthCompare(1) == 0, "only one input allowed") - sign(txinfo.tx, inputIndex = 0, txinfo.input.redeemScript, txinfo.input.txOut.amount, key) + sign(txinfo.tx, inputIndex = 0, txinfo.input.redeemScript, txinfo.input.txOut.amount, key, sigHash) } def addSigs(commitTx: CommitTx, localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: ByteVector, remoteSig: ByteVector): CommitTx = { @@ -539,11 +642,24 @@ object Transactions { closingTx.copy(tx = closingTx.tx.updateWitness(0, witness)) } + def addSigs(pushMeTx: PushMeTx, localSig: ByteVector): PushMeTx = { + val witness = Scripts.claimPushMeOutputWithKey(localSig) + pushMeTx.copy(tx = pushMeTx.tx.updateWitness(0, witness)) + } + def checkSpendable(txinfo: TransactionWithInputInfo): Try[Unit] = Try(Transaction.correctlySpends(txinfo.tx, Map(txinfo.tx.txIn.head.outPoint -> txinfo.input.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - def checkSig(txinfo: TransactionWithInputInfo, sig: ByteVector, pubKey: PublicKey): Boolean = { - val data = Transaction.hashForSigning(txinfo.tx, inputIndex = 0, txinfo.input.redeemScript, SIGHASH_ALL, txinfo.input.txOut.amount, SIGVERSION_WITNESS_V0) + /** + * + * @param txinfo the transaction containing the signature to check + * @param sig the signature that is going to be checked against + * @param pubKey pubkey of the signature + * @param sigHash the type used to produce the hash for signing + * @return + */ + def checkSig(txinfo: TransactionWithInputInfo, sig: ByteVector, pubKey: PublicKey, sigHash: Int): Boolean = { + val data = Transaction.hashForSigning(txinfo.tx, inputIndex = 0, txinfo.input.redeemScript, sigHash, txinfo.input.txOut.amount, SIGVERSION_WITNESS_V0) Crypto.verifySignature(data, sig, pubKey) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala index 14b2028087..d955b3f11b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.wire +import fr.acinq.bitcoin.Crypto.Point import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, KeyPath} import fr.acinq.bitcoin.{ByteVector32, OutPoint, Transaction, TxOut} import fr.acinq.eclair.channel._ @@ -27,7 +28,8 @@ import fr.acinq.eclair.wire.LightningMessageCodecs._ import grizzled.slf4j.Logging import scodec.bits.BitVector import scodec.codecs._ -import scodec.{Attempt, Codec} +import scodec.{Attempt, Codec, DecodeResult, Decoder, Encoder, Err, GenCodec, SizeBound, Transformer} +import shapeless.{Generic, HList, HNil} /** * Created by PM on 02/06/2017. @@ -178,22 +180,6 @@ object ChannelCodecs extends Logging { (wire: BitVector) => spentListCodec.decode(wire).map(_.map(_.toMap)) ) - val commitmentsCodec: Codec[Commitments] = ( - ("localParams" | localParamsCodec) :: - ("remoteParams" | remoteParamsCodec) :: - ("channelFlags" | byte) :: - ("localCommit" | localCommitCodec) :: - ("remoteCommit" | remoteCommitCodec) :: - ("localChanges" | localChangesCodec) :: - ("remoteChanges" | remoteChangesCodec) :: - ("localNextHtlcId" | uint64) :: - ("remoteNextHtlcId" | uint64) :: - ("originChannels" | originsMapCodec) :: - ("remoteNextCommitInfo" | either(bool, waitingForRevocationCodec, point)) :: - ("commitInput" | inputInfoCodec) :: - ("remotePerCommitmentSecrets" | ShaChain.shaChainCodec) :: - ("channelId" | bytes32)).as[Commitments] - val closingTxProposedCodec: Codec[ClosingTxProposed] = ( ("unsignedTx" | txCodec) :: ("localClosingSigned" | closingSignedCodec)).as[ClosingTxProposed] @@ -204,6 +190,7 @@ object ChannelCodecs extends Logging { ("htlcSuccessTxs" | listOfN(uint16, txCodec)) :: ("htlcTimeoutTxs" | listOfN(uint16, txCodec)) :: ("claimHtlcDelayedTx" | listOfN(uint16, txCodec)) :: + ("pushMeTx" | optional(bool, txCodec)) :: ("spent" | spentMapCodec)).as[LocalCommitPublished] val remoteCommitPublishedCodec: Codec[RemoteCommitPublished] = ( @@ -211,6 +198,7 @@ object ChannelCodecs extends Logging { ("claimMainOutputTx" | optional(bool, txCodec)) :: ("claimHtlcSuccessTxs" | listOfN(uint16, txCodec)) :: ("claimHtlcTimeoutTxs" | listOfN(uint16, txCodec)) :: + ("pushMeTx" | optional(bool, txCodec)) :: ("spent" | spentMapCodec)).as[RemoteCommitPublished] val revokedCommitPublishedCodec: Codec[RevokedCommitPublished] = ( @@ -221,28 +209,46 @@ object ChannelCodecs extends Logging { ("claimHtlcDelayedPenaltyTxs" | listOfN(uint16, txCodec)) :: ("spent" | spentMapCodec)).as[RevokedCommitPublished] + private def commitmentCodec(commitmentVersion: CommitmentVersion): Codec[Commitments] = { + (("localParams" | localParamsCodec) :: + ("remoteParams" | remoteParamsCodec) :: + ("channelFlags" | byte) :: + ("localCommit" | localCommitCodec) :: + ("remoteCommit" | remoteCommitCodec) :: + ("localChanges" | localChangesCodec) :: + ("remoteChanges" | remoteChangesCodec) :: + ("localNextHtlcId" | uint64) :: + ("remoteNextHtlcId" | uint64) :: + ("originChannels" | originsMapCodec) :: + ("remoteNextCommitInfo" | either(bool, waitingForRevocationCodec, point)) :: + ("commitInput" | inputInfoCodec) :: + ("remotePerCommitmentSecrets" | ShaChain.shaChainCodec) :: + ("channelId" | bytes32) :: + ("version" | provide(commitmentVersion))).as[Commitments] + } + // this is a decode-only codec compatible with versions 997acee and below, with placeholders for new fields - val DATA_WAIT_FOR_FUNDING_CONFIRMED_COMPAT_01_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( - ("commitments" | commitmentsCodec) :: + private def DATA_WAIT_FOR_FUNDING_CONFIRMED_COMPAT_01_Codec(commitmentVersion: CommitmentVersion): Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( + ("commitments" | commitmentCodec(commitmentVersion)) :: ("fundingTx" | provide[Option[Transaction]](None)) :: ("waitingSince" | provide(compat.Platform.currentTime / 1000)) :: ("deferred" | optional(bool, fundingLockedCodec)) :: ("lastSent" | either(bool, fundingCreatedCodec, fundingSignedCodec))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED].decodeOnly - val DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( - ("commitments" | commitmentsCodec) :: + private def DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec(commitmentVersion: CommitmentVersion): Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( + ("commitments" | commitmentCodec(commitmentVersion)) :: ("fundingTx" | optional(bool, txCodec)) :: ("waitingSince" | int64) :: ("deferred" | optional(bool, fundingLockedCodec)) :: ("lastSent" | either(bool, fundingCreatedCodec, fundingSignedCodec))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED] - val DATA_WAIT_FOR_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = ( - ("commitments" | commitmentsCodec) :: + private def DATA_WAIT_FOR_FUNDING_LOCKED_Codec(commitmentVersion: CommitmentVersion): Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = ( + ("commitments" | commitmentCodec(commitmentVersion)) :: ("shortChannelId" | shortchannelid) :: ("lastSent" | fundingLockedCodec)).as[DATA_WAIT_FOR_FUNDING_LOCKED] - val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( - ("commitments" | commitmentsCodec) :: + private def DATA_NORMAL_Codec(commitmentVersion: CommitmentVersion): Codec[DATA_NORMAL] = ( + ("commitments" | commitmentCodec(commitmentVersion)) :: ("shortChannelId" | shortchannelid) :: ("buried" | bool) :: ("channelAnnouncement" | optional(bool, channelAnnouncementCodec)) :: @@ -250,20 +256,20 @@ object ChannelCodecs extends Logging { ("localShutdown" | optional(bool, shutdownCodec)) :: ("remoteShutdown" | optional(bool, shutdownCodec))).as[DATA_NORMAL] - val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = ( - ("commitments" | commitmentsCodec) :: + private def DATA_SHUTDOWN_Codec(commitmentVersion: CommitmentVersion): Codec[DATA_SHUTDOWN] = ( + ("commitments" | commitmentCodec(commitmentVersion)) :: ("localShutdown" | shutdownCodec) :: ("remoteShutdown" | shutdownCodec)).as[DATA_SHUTDOWN] - val DATA_NEGOTIATING_Codec: Codec[DATA_NEGOTIATING] = ( - ("commitments" | commitmentsCodec) :: + private def DATA_NEGOTIATING_Codec(commitmentVersion: CommitmentVersion): Codec[DATA_NEGOTIATING] = ( + ("commitments" | commitmentCodec(commitmentVersion)) :: ("localShutdown" | shutdownCodec) :: ("remoteShutdown" | shutdownCodec) :: ("closingTxProposed" | listOfN(uint16, listOfN(uint16, closingTxProposedCodec))) :: ("bestUnpublishedClosingTx_opt" | optional(bool, txCodec))).as[DATA_NEGOTIATING] - val DATA_CLOSING_Codec: Codec[DATA_CLOSING] = ( - ("commitments" | commitmentsCodec) :: + private def DATA_CLOSING_Codec(commitmentVersion: CommitmentVersion): Codec[DATA_CLOSING] = ( + ("commitments" | commitmentCodec(commitmentVersion)) :: ("mutualCloseProposed" | listOfN(uint16, txCodec)) :: ("mutualClosePublished" | listOfN(uint16, txCodec)) :: ("localCommitPublished" | optional(bool, localCommitPublishedCodec)) :: @@ -272,30 +278,25 @@ object ChannelCodecs extends Logging { ("futureRemoteCommitPublished" | optional(bool, remoteCommitPublishedCodec)) :: ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).as[DATA_CLOSING] - val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( - ("commitments" | commitmentsCodec) :: + private def DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec(commitmentVersion: CommitmentVersion): Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( + ("commitments" | commitmentCodec(commitmentVersion)) :: ("remoteChannelReestablish" | channelReestablishCodec)).as[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] - - /** - * Order matters!! - * - * We use the fact that the discriminated codec encodes using the first suitable codec it finds in the list to handle - * database migration. - * - * For example, a data encoded with type 01 will be decoded using [[DATA_WAIT_FOR_FUNDING_CONFIRMED_COMPAT_01_Codec]] and - * encoded to a type 08 using [[DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec]]. - * - * More info here: https://github.com/scodec/scodec/issues/122 - */ - val stateDataCodec: Codec[HasCommitments] = ("version" | constant(0x00)) ~> discriminated[HasCommitments].by(uint16) - .typecase(0x08, DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) - .typecase(0x01, DATA_WAIT_FOR_FUNDING_CONFIRMED_COMPAT_01_Codec) - .typecase(0x02, DATA_WAIT_FOR_FUNDING_LOCKED_Codec) - .typecase(0x03, DATA_NORMAL_Codec) - .typecase(0x04, DATA_SHUTDOWN_Codec) - .typecase(0x05, DATA_NEGOTIATING_Codec) - .typecase(0x06, DATA_CLOSING_Codec) - .typecase(0x07, DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec) + private def stateDataCodec(commitmentVersion: CommitmentVersion): Codec[HasCommitments] = discriminated[HasCommitments].by(uint16) + .typecase(0x08, DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec(commitmentVersion)) + .typecase(0x01, DATA_WAIT_FOR_FUNDING_CONFIRMED_COMPAT_01_Codec(commitmentVersion)) + .typecase(0x02, DATA_WAIT_FOR_FUNDING_LOCKED_Codec(commitmentVersion)) + .typecase(0x03, DATA_NORMAL_Codec(commitmentVersion)) + .typecase(0x04, DATA_SHUTDOWN_Codec(commitmentVersion)) + .typecase(0x05, DATA_NEGOTIATING_Codec(commitmentVersion)) + .typecase(0x06, DATA_CLOSING_Codec(commitmentVersion)) + .typecase(0x07, DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec(commitmentVersion)) + + val COMMITMENTv1_VERSION_BYTE = 0x00.toByte + val COMMITMENT_SIMPLIFIED_VERSION_BYTE = 0x01.toByte + + val genericStateDataCodec = discriminated[HasCommitments].by(uint8) + .\ (COMMITMENTv1_VERSION_BYTE) { case c if c.commitments.version == VersionCommitmentV1 => c } (stateDataCodec(VersionCommitmentV1)) + .\ (COMMITMENT_SIMPLIFIED_VERSION_BYTE) { case c if c.commitments.version == VersionSimplifiedCommitment => c } (stateDataCodec(VersionSimplifiedCommitment)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala index 880ab0d05d..a50faa12f1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala @@ -47,7 +47,13 @@ case class Init(globalFeatures: ByteVector, localFeatures: ByteVector) extends SetupMessage case class Error(channelId: ByteVector32, - data: ByteVector) extends SetupMessage with HasChannelId + data: ByteVector) extends SetupMessage with HasChannelId { + + override def toString: String = data.decodeAscii match { + case Left(err) => s"Could not decode error msg, err=$err data=$data " + case Right(str) => s"Error(channelId=$channelId, data=$str)" + } +} object Error { def apply(channelId: ByteVector32, msg: String): Error = Error(channelId, ByteVector.view(msg.getBytes(Charsets.US_ASCII))) diff --git a/eclair-core/src/test/resources/integration/bitcoin.conf b/eclair-core/src/test/resources/integration/bitcoin.conf index da4dd59a07..29775744a1 100644 --- a/eclair-core/src/test/resources/integration/bitcoin.conf +++ b/eclair-core/src/test/resources/integration/bitcoin.conf @@ -1,7 +1,7 @@ regtest=1 +noprinttoconsole=1 server=1 port=28333 -rpcport=28332 rpcuser=foo rpcpassword=bar txindex=1 @@ -9,3 +9,5 @@ zmqpubrawblock=tcp://127.0.0.1:28334 zmqpubrawtx=tcp://127.0.0.1:28335 rpcworkqueue=64 addresstype=bech32 +[regtest] +rpcport=28332 diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala index 0f2dc94013..5b826b08b6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -18,8 +18,9 @@ package fr.acinq.eclair import java.nio.ByteOrder -import fr.acinq.bitcoin.Protocol +import fr.acinq.bitcoin.{Protocol} import fr.acinq.eclair.Features._ +import fr.acinq.eclair.channel.{Helpers, LocalParams, ParamsWithFeatures} import org.scalatest.FunSuite import scodec.bits._ @@ -43,6 +44,39 @@ class FeaturesSpec extends FunSuite { assert(areSupported(features) && hasFeature(features, OPTION_DATA_LOSS_PROTECT_OPTIONAL) && hasFeature(features, INITIAL_ROUTING_SYNC_BIT_OPTIONAL)) } + test("'option_simplified_commitment' feature") { + val features = hex"0200" + assert(areSupported(features) && hasFeature(features, OPTION_SIMPLIFIED_COMMITMENT_OPTIONAL)) + } + + test("Helpers should correctly detect if the peers negotiated 'option_simplified_commitment'") { + + val optionalSupport = hex"0200" + val mandatorySupport = hex"0100" + + val channelParamNoSupport = new {} with ParamsWithFeatures { + override val globalFeatures: ByteVector = ByteVector.empty + override val localFeatures: ByteVector = ByteVector.empty + } + + val channelParamOptSupport = new {} with ParamsWithFeatures { + override val globalFeatures: ByteVector = ByteVector.empty + override val localFeatures: ByteVector = optionalSupport + } + + val channelParamMandatorySupport = new {} with ParamsWithFeatures { + override val globalFeatures: ByteVector = ByteVector.empty + override val localFeatures: ByteVector = mandatorySupport + } + + assert(Helpers.canUseSimplifiedCommitment(local = channelParamOptSupport, remote = channelParamOptSupport) == true) + assert(Helpers.canUseSimplifiedCommitment(local = channelParamOptSupport, remote = channelParamNoSupport) == false) + assert(Helpers.canUseSimplifiedCommitment(local = channelParamOptSupport, remote = channelParamMandatorySupport) == true) + assert(Helpers.canUseSimplifiedCommitment(local = channelParamMandatorySupport, remote = channelParamMandatorySupport) == true) + assert(Helpers.canUseSimplifiedCommitment(local = channelParamNoSupport, remote = channelParamMandatorySupport) == false) + } + + test("features compatibility") { assert(areSupported(Protocol.writeUInt64(1l << INITIAL_ROUTING_SYNC_BIT_OPTIONAL, ByteOrder.BIG_ENDIAN))) assert(areSupported(Protocol.writeUInt64(1L << OPTION_DATA_LOSS_PROTECT_MANDATORY, ByteOrder.BIG_ENDIAN))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala index 2f4adea65d..3d0a510292 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala @@ -21,7 +21,7 @@ import akka.actor.Status.Failure import akka.pattern.pipe import akka.testkit.{TestKit, TestProbe} import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.{Block, MilliBtc, Satoshi, Script, Transaction} +import fr.acinq.bitcoin.{ByteVector32, Block, MilliBtc, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet.FundTransactionResponse import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, JsonRPCError} @@ -41,7 +41,14 @@ import scala.util.{Random, Try} class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindService with FunSuiteLike with BeforeAndAfterAll with Logging { - val commonConfig = ConfigFactory.parseMap(Map("eclair.chain" -> "regtest", "eclair.spv" -> false, "eclair.server.public-ips.1" -> "localhost", "eclair.bitcoind.port" -> 28333, "eclair.bitcoind.rpcport" -> 28332, "eclair.bitcoind.zmq" -> "tcp://127.0.0.1:28334", "eclair.router-broadcast-interval" -> "2 second", "eclair.auto-reconnect" -> false)) + val commonConfig = ConfigFactory.parseMap(Map( + "eclair.chain" -> "regtest", + "eclair.spv" -> false, + "eclair.server.public-ips.1" -> "localhost", + "eclair.bitcoind.port" -> 28333, + "eclair.bitcoind.rpcport" -> 28332, + "eclair.router-broadcast-interval" -> "2 second", + "eclair.auto-reconnect" -> false)) val config = ConfigFactory.load(commonConfig).getConfig("eclair") val walletPassword = Random.alphanumeric.take(8).mkString @@ -94,6 +101,39 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe } } + test("handle errors when signing transactions") { + val bitcoinClient = new BasicBitcoinJsonRPCClient( + user = config.getString("bitcoind.rpcuser"), + password = config.getString("bitcoind.rpcpassword"), + host = config.getString("bitcoind.host"), + port = config.getInt("bitcoind.rpcport")) + val wallet = new BitcoinCoreWallet(bitcoinClient) + + val sender = TestProbe() + + // create a transaction that spends UTXOs that don't exist + wallet.getFinalAddress.pipeTo(sender.ref) + val address = sender.expectMsgType[String] + val unknownTxids = Seq( + ByteVector32.fromValidHex("01" * 32), + ByteVector32.fromValidHex("02" * 32), + ByteVector32.fromValidHex("03" * 32) + ) + val unsignedTx = Transaction(version = 2, + txIn = Seq( + TxIn(OutPoint(unknownTxids(0), 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL), + TxIn(OutPoint(unknownTxids(1), 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL), + TxIn(OutPoint(unknownTxids(2), 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) + ), + txOut = TxOut(Satoshi(1000000), addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) :: Nil, + lockTime = 0) + + // signing it should fail, and the error message should contain the txids of the UTXOs that could not be used + wallet.signTransaction(unsignedTx).pipeTo(sender.ref) + val Failure(JsonRPCError(error)) = sender.expectMsgType[Failure] + unknownTxids.foreach(id => assert(error.message.contains(id.toString()))) + } + test("create/commit/rollback funding txes") { val bitcoinClient = new BasicBitcoinJsonRPCClient( user = config.getString("bitcoind.rpcuser"), @@ -141,10 +181,9 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe sender.send(bitcoincli, BitcoinReq("getrawtransaction", fundingTxes(2).txid.toString())) assert(sender.expectMsgType[JString](10 seconds).s === fundingTxes(2).toString()) - // NB: bitcoin core doesn't clear the locks when a tx is published + // NB: from 0.17.0 on bitcoin core will clear locks when a tx is published sender.send(bitcoincli, BitcoinReq("listlockunspent")) - assert(sender.expectMsgType[JValue](10 seconds).children.size === 2) - + assert(sender.expectMsgType[JValue](10 seconds).children.size === 0) } test("encrypt wallet") { @@ -177,7 +216,8 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey))) wallet.makeFundingTx(pubkeyScript, MilliBtc(50), 10000).pipeTo(sender.ref) - assert(sender.expectMsgType[Failure].cause.asInstanceOf[JsonRPCError].error.message.contains("Please enter the wallet passphrase with walletpassphrase first.")) + val error = sender.expectMsgType[Failure].cause.asInstanceOf[JsonRPCError].error + assert(error.message.contains("Please enter the wallet passphrase with walletpassphrase first")) sender.send(bitcoincli, BitcoinReq("listlockunspent")) assert(sender.expectMsgType[JValue](10 seconds).children.size === 0) @@ -212,14 +252,14 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe bitcoinClient.invoke("fundrawtransaction", noinputTx1).pipeTo(sender.ref) val json = sender.expectMsgType[JValue] val JString(unsignedtx1) = json \ "hex" - bitcoinClient.invoke("signrawtransaction", unsignedtx1).pipeTo(sender.ref) + bitcoinClient.invoke("signrawtransactionwithwallet", unsignedtx1).pipeTo(sender.ref) val JString(signedTx1) = sender.expectMsgType[JValue] \ "hex" val tx1 = Transaction.read(signedTx1) // let's then generate another tx that double spends the first one val inputs = tx1.txIn.map(txIn => Map("txid" -> txIn.outPoint.txid.toString, "vout" -> txIn.outPoint.index)).toArray bitcoinClient.invoke("createrawtransaction", inputs, Map(address -> tx1.txOut.map(_.amount.toLong).sum * 1.0 / 1e8)).pipeTo(sender.ref) val JString(unsignedtx2) = sender.expectMsgType[JValue] - bitcoinClient.invoke("signrawtransaction", unsignedtx2).pipeTo(sender.ref) + bitcoinClient.invoke("signrawtransactionwithwallet", unsignedtx2).pipeTo(sender.ref) val JString(signedTx2) = sender.expectMsgType[JValue] \ "hex" val tx2 = Transaction.read(signedTx2) @@ -236,4 +276,4 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe sender.expectMsg(true) } -} \ No newline at end of file +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala index 42f7f949c3..479072f4e9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala @@ -44,7 +44,7 @@ trait BitcoindService extends Logging { val INTEGRATION_TMP_DIR = new File(TestUtils.BUILD_DIRECTORY, s"integration-${UUID.randomUUID()}") logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR") - val PATH_BITCOIND = new File(TestUtils.BUILD_DIRECTORY, "bitcoin-0.16.3/bin/bitcoind") + val PATH_BITCOIND = new File(TestUtils.BUILD_DIRECTORY, "bitcoin-0.17.1/bin/bitcoind") val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin") var bitcoind: Process = null diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala index d730b2bf13..18a08923de 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala @@ -34,7 +34,14 @@ import scala.concurrent.ExecutionContext.Implicits.global class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with BitcoindService with FunSuiteLike with BeforeAndAfterAll with Logging { - val commonConfig = ConfigFactory.parseMap(Map("eclair.chain" -> "regtest", "eclair.spv" -> false, "eclair.server.public-ips.1" -> "localhost", "eclair.bitcoind.port" -> 28333, "eclair.bitcoind.rpcport" -> 28332, "eclair.bitcoind.zmq" -> "tcp://127.0.0.1:28334", "eclair.router-broadcast-interval" -> "2 second", "eclair.auto-reconnect" -> false)) + val commonConfig = ConfigFactory.parseMap(Map( + "eclair.chain" -> "regtest", + "eclair.spv" -> false, + "eclair.server.public-ips.1" -> "localhost", + "eclair.bitcoind.port" -> 28333, + "eclair.bitcoind.rpcport" -> 28332, + "eclair.router-broadcast-interval" -> "2 second", + "eclair.auto-reconnect" -> false)) val config = ConfigFactory.load(commonConfig).getConfig("eclair") implicit val formats = DefaultFormats @@ -67,7 +74,7 @@ class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with Bitcoi val json = sender.expectMsgType[JValue] val JString(unsignedtx) = json \ "hex" val JInt(changePos) = json \ "changepos" - bitcoinClient.invoke("signrawtransaction", unsignedtx).pipeTo(sender.ref) + bitcoinClient.invoke("signrawtransactionwithwallet", unsignedtx).pipeTo(sender.ref) val JString(signedTx) = sender.expectMsgType[JValue] \ "hex" val tx = Transaction.read(signedTx) val txid = tx.txid.toString() @@ -92,7 +99,7 @@ class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with Bitcoi val pos = if (changePos == 0) 1 else 0 bitcoinClient.invoke("createrawtransaction", Array(Map("txid" -> txid, "vout" -> pos)), Map(address -> 5.99999)).pipeTo(sender.ref) val JString(unsignedtx) = sender.expectMsgType[JValue] - bitcoinClient.invoke("signrawtransaction", unsignedtx).pipeTo(sender.ref) + bitcoinClient.invoke("signrawtransactionwithwallet", unsignedtx).pipeTo(sender.ref) val JString(signedTx) = sender.expectMsgType[JValue] \ "hex" signedTx } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala index 82c292166c..7b9126b2ab 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala @@ -38,7 +38,14 @@ import scala.util.Random class BitcoinCoreFeeProviderSpec extends TestKit(ActorSystem("test")) with BitcoindService with FunSuiteLike with BeforeAndAfterAll with Logging { - val commonConfig = ConfigFactory.parseMap(Map("eclair.chain" -> "regtest", "eclair.spv" -> false, "eclair.server.public-ips.1" -> "localhost", "eclair.bitcoind.port" -> 28333, "eclair.bitcoind.rpcport" -> 28332, "eclair.bitcoind.zmq" -> "tcp://127.0.0.1:28334", "eclair.router-broadcast-interval" -> "2 second", "eclair.auto-reconnect" -> false)) + val commonConfig = ConfigFactory.parseMap(Map( + "eclair.chain" -> "regtest", + "eclair.spv" -> false, + "eclair.server.public-ips.1" -> "localhost", + "eclair.bitcoind.port" -> 28333, + "eclair.bitcoind.rpcport" -> 28332, + "eclair.router-broadcast-interval" -> "2 second", + "eclair.auto-reconnect" -> false)) val config = ConfigFactory.load(commonConfig).getConfig("eclair") val walletPassword = Random.alphanumeric.take(8).mkString diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala index efdc8fa73a..0f11f836c3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala @@ -28,6 +28,7 @@ import fr.acinq.eclair.router.Hop import fr.acinq.eclair.wire._ import fr.acinq.eclair.{Globals, NodeParams, TestConstants, randomBytes32} import scodec.bits.ByteVector +import scodec.bits._ /** * Created by PM on 23/08/2016. @@ -70,7 +71,12 @@ trait StateTestsHelperMethods extends TestKitBase { import setup._ val channelFlags = if (tags.contains("channels_public")) ChannelFlags.AnnounceChannel else ChannelFlags.Empty val pushMsat = if (tags.contains("no_push_msat")) 0 else TestConstants.pushMsat - val (aliceParams, bobParams) = (Alice.channelParams, Bob.channelParams) + val (aliceParams, bobParams) = if(tags.contains("simplified_commitment")) { + (Alice.channelParams.copy(localFeatures = hex"0200"), Bob.channelParams.copy(localFeatures = hex"0200")) + } else { + (Alice.channelParams, Bob.channelParams) + } + val aliceInit = Init(aliceParams.globalFeatures, aliceParams.localFeatures) val bobInit = Init(bobParams.globalFeatures, bobParams.localFeatures) // reset global feerates (they may have been changed by previous tests) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala index 9b603223d7..11fdfdccb7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala @@ -26,7 +26,7 @@ import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire._ import fr.acinq.eclair.{TestConstants, TestkitBaseClass} import org.scalatest.{Outcome, Tag} - +import scodec.bits._ import scala.concurrent.duration._ /** @@ -45,11 +45,16 @@ class WaitForFundingCreatedStateSpec extends TestkitBaseClass with StateTestsHel } else { (TestConstants.fundingSatoshis, TestConstants.pushMsat) } - val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) - val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) + + val aliceLocalFeatures = if (test.tags.contains("simplified_commitment")) hex"0200" else Alice.channelParams.localFeatures + val bobLocalFeatures = if (test.tags.contains("simplified_commitment")) hex"0200" else Bob.channelParams.localFeatures + + val aliceInit = Init(Alice.channelParams.globalFeatures, aliceLocalFeatures) + val bobInit = Init(Bob.channelParams.globalFeatures, bobLocalFeatures) + within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty) - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams.copy(localFeatures = aliceLocalFeatures), alice2bob.ref, bobInit, ChannelFlags.Empty) + bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams.copy(localFeatures = bobLocalFeatures), bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) bob2alice.expectMsgType[AcceptChannel] @@ -63,12 +68,29 @@ class WaitForFundingCreatedStateSpec extends TestkitBaseClass with StateTestsHel import f._ alice2bob.expectMsgType[FundingCreated] alice2bob.forward(bob) - awaitCond(bob.stateName == WAIT_FOR_FUNDING_CONFIRMED) + awaitCond({ + bob.stateName == WAIT_FOR_FUNDING_CONFIRMED && + bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.version == VersionCommitmentV1 + }) + bob2alice.expectMsgType[FundingSigned] + bob2blockchain.expectMsgType[WatchSpent] + bob2blockchain.expectMsgType[WatchConfirmed] + } + + test("recv FundingCreated (option_simplified_commitment)", Tag("simplified_commitment")) { f => + import f._ + alice2bob.expectMsgType[FundingCreated] + alice2bob.forward(bob) + awaitCond({ + bob.stateName == WAIT_FOR_FUNDING_CONFIRMED && + bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.version == VersionSimplifiedCommitment + }) bob2alice.expectMsgType[FundingSigned] bob2blockchain.expectMsgType[WatchSpent] bob2blockchain.expectMsgType[WatchConfirmed] } + test("recv FundingCreated (funder can't pay fees)", Tag("funder_below_reserve")) { f => import f._ val fees = Transactions.commitWeight * TestConstants.feeratePerKw / 1000 @@ -92,5 +114,4 @@ class WaitForFundingCreatedStateSpec extends TestkitBaseClass with StateTestsHel bob ! CMD_CLOSE(None) awaitCond(bob.stateName == CLOSED) } - } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index 1d8b7cd530..8e346ca60d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -22,11 +22,13 @@ import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods +import fr.acinq.eclair.transactions.{Scripts, Transactions} import fr.acinq.eclair.wire.{AcceptChannel, Error, FundingCreated, FundingSigned, Init, OpenChannel} import fr.acinq.eclair.{TestConstants, TestkitBaseClass} import org.scalatest.Outcome import scodec.bits.ByteVector - +import org.scalatest.{Outcome, Tag} +import scodec.bits._ import scala.concurrent.duration._ /** @@ -40,11 +42,18 @@ class WaitForFundingSignedStateSpec extends TestkitBaseClass with StateTestsHelp override def withFixture(test: OneArgTest): Outcome = { val setup = init() import setup._ - val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) - val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) + + val (aliceParams, bobParams) = if(test.tags.contains("simplified_commitment")) + (Alice.channelParams.copy(localFeatures = hex"0200"), Bob.channelParams.copy(localFeatures = hex"0200")) + else + (Alice.channelParams, Bob.channelParams) + + val aliceInit = Init(aliceParams.globalFeatures, aliceParams.localFeatures) + val bobInit = Init(bobParams.globalFeatures, bobParams.localFeatures) + within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty) - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty) + bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) bob2alice.expectMsgType[AcceptChannel] @@ -65,6 +74,23 @@ class WaitForFundingSignedStateSpec extends TestkitBaseClass with StateTestsHelp alice2blockchain.expectMsgType[WatchConfirmed] } + test("recv FundingSigned (option_simplified_commitment)", Tag("simplified_commitment")) { f => + import f._ + + bob2alice.expectMsgType[FundingSigned] + bob2alice.forward(alice) + + val aliceStateData = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED] + assert(aliceStateData.commitments.version == VersionSimplifiedCommitment) + + // there should be 2 push-me outputs with amount 1000 sat + val aliceLocalCommitTx = aliceStateData.commitments.localCommit.publishableTxs.commitTx.tx + assert(aliceLocalCommitTx.txOut.count(_.amount == Transactions.pushMeValue) == 2) + + alice2blockchain.expectMsgType[WatchSpent] + alice2blockchain.expectMsgType[WatchConfirmed] + } + test("recv FundingSigned with invalid signature") { f => import f._ // sending an invalid sig diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index d6486a7a86..bdad4f61f0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.channel.states.e import akka.actor.Status import akka.actor.Status.Failure import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.Scalar +import fr.acinq.bitcoin.Crypto.{PublicKey, Scalar} import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi, ScriptFlags, Transaction} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.UInt64.Conversions._ @@ -289,6 +289,17 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { relayerB.expectNoMsg() } + test("recv UpdateAddHtlc (option_simplified_commitment)", Tag("simplified_commitment")) { f => + import f._ + val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 150000, ByteVector32(ByteVector.fill(32)(42)), 400144, defaultOnion) + bob ! htlc + awaitCond(initialData.commitments.version == VersionSimplifiedCommitment) + awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ htlc), remoteNextHtlcId = 1))) + // bob won't forward the add before it is cross-signed + relayerB.expectNoMsg() + } + test("recv UpdateAddHtlc (unexpected id)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx @@ -611,6 +622,34 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteChanges.signed.size == 1) } + test("recv CommitSig (one htlc received, option_simplified_commitment)", Tag("simplified_commitment")) { f => + import f._ + val sender = TestProbe() + + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] + + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + + // actual test begins + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + + bob2alice.expectMsgType[RevokeAndAck] + // bob replies immediately with a signature + bob2alice.expectMsgType[CommitSig] + + awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.htlcs.exists(h => h.add.id == htlc.id && h.direction == IN)) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.version == VersionSimplifiedCommitment) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.version == VersionSimplifiedCommitment) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs.size == 1) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toLocalMsat == initialState.commitments.localCommit.spec.toLocalMsat) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteChanges.acked.size == 0) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteChanges.signed.size == 1) + } + + test("recv CommitSig (one htlc sent)") { f => import f._ val sender = TestProbe() @@ -634,6 +673,31 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toLocalMsat == initialState.commitments.localCommit.spec.toLocalMsat) } + test("recv CommitSig (one htlc sent, option_simplified_commitment)", Tag("simplified_commitment")) { f => + import f._ + val sender = TestProbe() + + val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] + + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + + // actual test begins (note that channel sends a CMD_SIGN to itself when it receives RevokeAndAck and there are changes) + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.htlcs.exists(h => h.add.id == htlc.id && h.direction == OUT)) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.version == VersionSimplifiedCommitment) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.version == VersionSimplifiedCommitment) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs.size == 1) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toLocalMsat == initialState.commitments.localCommit.spec.toLocalMsat) + } + test("recv CommitSig (multiple htlcs in both directions)") { f => import f._ val sender = TestProbe() @@ -783,7 +847,6 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { bob2blockchain.expectMsgType[WatchConfirmed] } - test("recv RevokeAndAck (one htlc sent)") { f => import f._ val sender = TestProbe() @@ -802,6 +865,34 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localChanges.acked.size == 1) } + test("recv RevokeAndAck (simplified_commitment - non rotating remote_pubkey)", Tag("simplified_commitment")) { f => + import f._ + val sender = TestProbe() + + // get the remote pubkey (used in to_remote output) before the payment has been sent + val startingPaymentPubkey = PublicKey(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteParams.paymentBasepoint) + + // now alice offers to bob an htlc + addHtlc(50000000, alice, bob, alice2bob, bob2alice) + + // alice signs it's state and sends a commit to bob + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + + // bob acknowledges and sends back revoke_and_ack + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + + // TEST in alice's view bob remotePaymentPubkey (remote_pubkey) stays the same across commitments + awaitCond({ + alice.stateName == NORMAL && + PublicKey(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteParams.paymentBasepoint) == startingPaymentPubkey + }) + + } + test("recv RevokeAndAck (one htlc received)") { f => import f._ val sender = TestProbe() @@ -1314,6 +1405,16 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(initialState == bob.stateData) } + // TODO review, remove? + test("recv CMD_UPDATE_FEE (when option_simplified_commitment)", Tag("simplified_commitment")) { f => + import f._ + val sender = TestProbe() + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + sender.send(alice, CMD_UPDATE_FEE(20000)) + sender.expectMsg(Failure(CannotUpdateFeeWithCommitmentType(channelId(bob)))) + assert(initialState == alice.stateData) + } + test("recv UpdateFee") { f => import f._ val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] @@ -1324,6 +1425,18 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ fee2), remoteNextHtlcId = 0))) } + test("recv UpdateFee (option_simplified_commitment)", Tag("simplified_commitment")) { f => + import f._ + val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] + assert(initialData.commitments.version == VersionSimplifiedCommitment) + + // Alice sends update_fee to Bob but this shouldn't happen with option_simplified_commitment so Bob will reply Error + val fee1 = UpdateFee(ByteVector32.Zeroes, 12000) + bob ! fee1 + + bob2alice.expectMsgType[Error] // should bob close the channel? Yes + } + test("recv UpdateFee (two in a row)") { f => import f._ val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index 57505a09c2..705ad6f550 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -46,8 +46,9 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods override def withFixture(test: OneArgTest): Outcome = { val setup = init() import setup._ + within(30 seconds) { - reachNormal(setup) + reachNormal(setup, test.tags) val sender = TestProbe() // alice initiates a closing if (test.tags.contains("fee2")) Globals.feeratesPerKw.set(FeeratesPerKw.single(4319)) else Globals.feeratesPerKw.set(FeeratesPerKw.single(10000)) @@ -94,6 +95,26 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods awaitCond(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.map(_.localClosingSigned) == initialState.closingTxProposed.last.map(_.localClosingSigned) :+ aliceCloseSig2) } + test("recv ClosingSigned (option_simplified_commitment)", Tag("simplified_commitment")) { f => + import f._ + + awaitCond(alice.stateName == NEGOTIATING) + alice2bob.forward(bob) + awaitCond(bob.stateName == NEGOTIATING) + + // bob should send the first closing signatures with a hardcoded base fee of 282 sats + val closingSig = bob2alice.expectMsgType[ClosingSigned] + assert(closingSig.feeSatoshis == 282) + bob2alice.forward(alice) + + val closingSig2 = alice2bob.expectMsgType[ClosingSigned] + assert(closingSig2.feeSatoshis == 282) // alice sends back the same fees + alice2bob.forward(bob) + + // bob will now publish the closing tx + bob2blockchain.expectMsgType[PublishAsap] + } + private def testFeeConverge(f: FixtureParam) = { import f._ var aliceCloseFee, bobCloseFee = 0L @@ -121,7 +142,7 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods val sender = TestProbe() val tx = bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localCommit.publishableTxs.commitTx.tx sender.send(bob, aliceCloseSig.copy(feeSatoshis = 99000)) // sig doesn't matter, it is checked later - val error = bob2alice.expectMsgType[Error] + val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray).startsWith("invalid close fee: fee_satoshis=99000")) bob2blockchain.expectMsg(PublishAsap(tx)) bob2blockchain.expectMsgType[PublishAsap] @@ -187,7 +208,6 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods assert(alice.stateName == CLOSING) } - test("recv CMD_CLOSE") { f => import f._ val sender = TestProbe() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index d68b63c48d..96deda1f0a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -26,10 +26,13 @@ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.channel.{Data, State, _} import fr.acinq.eclair.payment.{CommandBuffer, ForwardAdd, ForwardFulfill, Local} import fr.acinq.eclair.transactions.Scripts +import fr.acinq.eclair.transactions.Transactions.HtlcTimeoutTx import fr.acinq.eclair.wire._ import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass, randomBytes32} import org.scalatest.Outcome import scodec.bits.ByteVector +import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass} +import org.scalatest.{Outcome, Tag} import scala.concurrent.duration._ @@ -46,7 +49,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import setup._ within(30 seconds) { - reachNormal(setup) + reachNormal(setup, test.tags) val bobCommitTxes: List[PublishableTxs] = (for (amt <- List(100000000, 200000000, 300000000)) yield { val (r, htlc) = addHtlc(amt, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) @@ -257,6 +260,33 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(alice.stateName == CLOSED) } + // TODO improve watcher adding inputs => PublishAsapAndAddFees + ignore("recv BITCOIN_TX_CONFIRMED (local commit, option_simplified_commitment)", Tag("simplified_commitment")) { f => + import f._ + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[LocalCommitConfirmed]) + // alice sends an htlc to bob + val (ra1, htlca1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + // an error occurs and alice publishes her commit tx + val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + val htlcTimeoutTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs.head.txinfo.tx + alice ! Error(ByteVector32.Zeroes, "oops") + alice2blockchain.expectMsg(PublishAsap(aliceCommitTx)) // commit tx + val pushMeTx = alice2blockchain.expectMsgType[PublishAsap].tx // pushMe tx + assert(alice2blockchain.expectMsgType[PublishAsap].tx.txIn.head.outPoint.txid == aliceCommitTx.txid) // claim-main-delayed + alice2blockchain.expectMsgType[PublishAsap] // htlc-timeout + alice2blockchain.expectMsgType[PublishAsap] // claim-htlc-delayed + + + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(aliceCommitTx)) + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(pushMeTx)) + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(htlcTimeoutTx))// htlc-timeout + alice2blockchain.expectMsgType[WatchConfirmed] // claim-main-delayed + assert(alice2blockchain.expectMsgType[WatchSpent].txId === aliceCommitTx.txid) + alice2blockchain.expectMsgType[WatchConfirmed] // claim-htlc-delayed + } + test("recv BITCOIN_TX_CONFIRMED (local commit with htlcs only signed by local)") { f => import f._ val sender = TestProbe() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelStateSpec.scala index e521424ebf..38194c76e6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelStateSpec.scala @@ -29,6 +29,7 @@ import fr.acinq.eclair.wire.{ChannelCodecs, UpdateAddHtlc} import fr.acinq.eclair.{ShortChannelId, UInt64, randomKey} import org.scalatest.FunSuite import scodec.bits._ +import scodec.bits.BitVector /** * Created by fabrice on 07/02/17. @@ -38,13 +39,38 @@ class ChannelStateSpec extends FunSuite { import ChannelStateSpec._ - test("basic serialization test (NORMAL)") { + test("basic serialization test (NORMAL - CommitmentV1)") { val data = normal - val bin = ChannelCodecs.DATA_NORMAL_Codec.encode(data).require - val check = ChannelCodecs.DATA_NORMAL_Codec.decodeValue(bin).require + val bin = ChannelCodecs.genericStateDataCodec.encode(data).require + val check = ChannelCodecs.genericStateDataCodec.decodeValue(bin).require + + assert(bin.take(8).toByte(signed = false) == ChannelCodecs.COMMITMENTv1_VERSION_BYTE) + assert(data.commitments.localCommit.spec === check.commitments.localCommit.spec) + assert(data === check) + } + + test("basic serialization test (NORMAL - SimplifiedCommitment)") { + val data = normalSimplified + val bin = ChannelCodecs.genericStateDataCodec.encode(data).require + val check = ChannelCodecs.genericStateDataCodec.decodeValue(bin).require + + assert(bin.take(8).toByte(signed = false) == ChannelCodecs.COMMITMENT_SIMPLIFIED_VERSION_BYTE) assert(data.commitments.localCommit.spec === check.commitments.localCommit.spec) assert(data === check) } + + test("backward compatible READING of previously stored commitments") { + val state = ChannelCodecs.genericStateDataCodec.decodeValue(BitVector(rawCommitment)).require + + assert(state.commitments.localParams.nodeId.raw == hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96") + assert(state.commitments.version === VersionCommitmentV1) + + val bin = ChannelCodecs.genericStateDataCodec.encode(state).require + val state1 = ChannelCodecs.genericStateDataCodec.decodeValue(bin).require + + assert(state === state1) + } + } object ChannelStateSpec { @@ -101,14 +127,26 @@ object ChannelStateSpec { val localCommit = LocalCommit(0, CommitmentSpec(htlcs.toSet, 1500, 50000000, 70000000), PublishableTxs(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), Nil)) val remoteCommit = RemoteCommit(0, CommitmentSpec(htlcs.map(htlc => htlc.copy(direction = htlc.direction.opposite)).toSet, 1500, 50000, 700000), ByteVector32(hex"0303030303030303030303030303030303030303030303030303030303030303"), Scalar(ByteVector.fill(32)(4)).toPoint) - val commitments = Commitments(localParams, remoteParams, channelFlags = 0x01.toByte, localCommit, remoteCommit, LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), + val commitmentsV1 = Commitments(localParams, remoteParams, channelFlags = 0x01.toByte, localCommit, remoteCommit, LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), localNextHtlcId = 32L, remoteNextHtlcId = 4L, originChannels = Map(42L -> Local(None), 15000L -> Relayed(ByteVector32(ByteVector.fill(32)(42)), 43, 11000000L, 10000000L)), remoteNextCommitInfo = Right(randomKey.publicKey), - commitInput = commitmentInput, remotePerCommitmentSecrets = ShaChain.init, channelId = ByteVector32.Zeroes) + commitInput = commitmentInput, remotePerCommitmentSecrets = ShaChain.init, channelId = ByteVector32.Zeroes, version = VersionCommitmentV1) + + val simplifiedCommitment = Commitments(localParams, remoteParams, channelFlags = 0x01.toByte, localCommit, remoteCommit, LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), + localNextHtlcId = 32L, + remoteNextHtlcId = 4L, + originChannels = Map(42L -> Local(None), 15000L -> Relayed(ByteVector32(ByteVector.fill(32)(42)), 43, 11000000L, 10000000L)), + remoteNextCommitInfo = Right(randomKey.publicKey), + commitInput = commitmentInput, remotePerCommitmentSecrets = ShaChain.init, channelId = ByteVector32.Zeroes, version = VersionSimplifiedCommitment) val channelUpdate = Announcements.makeChannelUpdate(ByteVector32(ByteVector.fill(32)(1)), randomKey, randomKey.publicKey, ShortChannelId(142553), 42, 15, 575, 53, Channel.MAX_FUNDING_SATOSHIS * 1000L) - val normal = DATA_NORMAL(commitments, ShortChannelId(42), true, None, channelUpdate, None, None) + val normal = DATA_NORMAL(commitmentsV1, ShortChannelId(42), true, None, channelUpdate, None, None) + + val normalSimplified = DATA_NORMAL(simplifiedCommitment, ShortChannelId(42), true, None, channelUpdate, None, None) + + // This is a serialized commitment (CommitmemtV1) taken from a mainnet DB and generated with code (commit: f9ead30) that did not account for other commitments type + val rawCommitment = hex"000003036D65409C41AB7380A43448F257809E7496B52BF92057C09C4F300CBD61C50D96000429A0E801FFB3E3B20083A3F8C548677F0000000000000222000000012A05F20000000000000008FC00000000000000010090001E800BD48A44296C8BE17C66F9F5675400AE1ADFF2BF4C776F4380000000C501C3277812FEF47DAC3ECC48C36735250C344AF72254935FE1B87161B32CBD1FC78000000000000111000000009502F900000000000000047E00000000000000008048000F0180884C8F3711CB0FA6009FD387BB18BDE3AE79C6F99FFBDA245A8868E3D84E4A0152D19626E31E85DCC547D6452BFECEA4A58D6421DC3ED9C31EBB5BE25F5EC81301826DC6CF05233C470A78CD05F10719F58CC6E3F3297A86F29F41AD3EC17CD07B81D2E0F42ECE10F90F47068AD225E39205BE9F62234D8217239254D1B149BC91A881BDDB713BD0D5A69215373CB4DD6C082ACCCCA37973FC3ED56486184B4060A08200000001C0404100800000000000000A000000000A5B0000000006CAC68C00000000000FFC3400122B6BEB76D026C009C138624B84F8F56DAD5A49CF2830984E66D66DCC6731BE87000000000015B8410180000000001100102BC29224A48EBA98852295B2000EE07CBE322503CDAC9E423DFABF5A0D307C790023A9108180884C8F3711CB0FA6009FD387BB18BDE3AE79C6F99FFBDA245A8868E3D84E4A1081B23E89D184700908064F254A89032F9618247C780B06332CA9547A783BBCC52EA95700AD01000000000080AB6BEB76D026C009C138624B84F8F56DAD5A49CF2830984E66D66DCC6731BE870000000000752B8CC00117840000000000000B000A2B55A3F061D2BD8FF8D65643AE2ABBE481B612B32135818000000000110010596BBCBEEA49205A1E44895DBE34810E998C5E3041AF6F154F174B877E08AED0820023982201102713B619608C4602CDCB4820CA2E2D1F18F632E14AD1AFBAA2530B195DB7E4B3811037E26CE7057B4B90C7CC88E0AAFCCA551824239C51DACAEAD6938B56E218264880A418228110804CBDAB73C3F569F5A7D6141639C3A94F923C6D15109F3E7C7019472671261B4201103E15DCB850126C070EF6E513AA9CF6D1B624692F4610DFFC6BBF519754B99DE880A3A9108180884C8F3711CB0FA6009FD387BB18BDE3AE79C6F99FFBDA245A8868E3D84E4A1081B23E89D184700908064F254A89032F9618247C780B06332CA9547A783BBCC52EA95745930A100000000000000000000A000000000A5B00000000000FFC340000000006CAC68C1882C81E7E9B4BAD22DECC021DED07B2A04CF401246F21260608E6D0362BB89981E6F8F85DBFDA1385E5274CBE5B0D21DD45329DE7FB534805C11187456282EE6E000000000000000000000000000000000000000500000000000000000000409D4457D14079D3398F705B263163D4841D8DFB50EF8E8FFFD7D72208E512D884800915B5F5BB68136004E09C3125C27C7AB6D6AD24E794184C27336B36E63398DF4380000000000ADC2080C00000000008800815E1491252475D4C42914AD90007703E5F191281E6D64F211EFD5FAD06983E3C8011D48840C04426479B88E587D3004FE9C3DD8C5EF1D73CE37CCFFDED122D443471EC27250840D91F44E8C2380484032792A5448197CB0C123E3C0583199654AA3D3C1DDE629754AB8000800F00003FFFFFFFFFFC008259C04D32D731B196AED4DC91753BC131B113C3BF619A9D7EB7A3222B3F376B9000F80003FFFFFFFFFFB0020703B9CA4B1B9FB5BA2809C0CF5FB8330E29FB47C27EC40C671E9798BADAAD29580007FFFFFFFFFF62B6BEB76D026C009C138624B84F8F56DAD5A49CF2830984E66D66DCC6731BE8704510980054B800070B821EA2784D9FC32BFEA579D3C6136DF87E6A232947BCC9DF84BC5337794370FF9740C589C29EB6931134B44414343132B149099E5DCA288BFD526B3C4BC888F22EA7DBA63472300DFBF40588AAC14A954A91FA3A7B1E760B9408841FC33B9C2137CC172868DBF6A5B3EA430FF121D3E09DEE03446599B79B33A980BB57721744E873A8774D20F9A26B466939C91BB0C9EA31367A8526F223FC505F5FAE0796C28711C7C0D4FE45184A8A5D284BA86AF7A06C7FBD30717DAAEAE237EBD1E9FD91D93A03FDAE3B23B8A2C360D692276F38C518BA33A18899D9E8C74CFA7F814AA37B0C12942FBD3BC19F24B56C5AB1E72645A06C79737C154F451FA67410A9460000DFC518156DE366E5834D448D5CC7EE9F263D06CBC2B41138D1AC32000000000011442600152E000006DACA81388356E701486891E4AF013CE92D6A57F240AF81389E60197AC38A1B2C070C9DE04BFBD1F6B0FB31230D9CD49430D12BDC89524D7F86E1C586CCB2F47F1E06C8FA274611C02420193C952A240CBE586091F1E02C18CCB2A551E9E0EEF314BA060221323CDC472C3E98027F4E1EEC62F78EB9E71BE67FEF68916A21A38F613929E7E8606E1BCCB0FEBDE1C2692621783F368F36FCB3B4840C7F47FF0EFE011EDCE3F34E089065806CC7E04039166B7EEB7B6B2116CE357A36B7A1DCCD7793B242DFC518156DE366E5834D448D5CC7EE9F263D06CBC2B41138D1AC32000000000011442600152E0000B8FD31EE020001200000000000000002000007D0000000C8000000001B6B0B0000" } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala index f23766b1e3..c670b63015 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala @@ -48,7 +48,7 @@ class SqliteAuditDbSpec extends FunSuite { val e4 = NetworkFeePaid(null, randomKey.publicKey, randomBytes32, Transaction(0, Seq.empty, Seq.empty, 0), Satoshi(42), "mutual") val e5 = PaymentSent(MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32, timestamp = 0) val e6 = PaymentSent(MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32, timestamp = Platform.currentTime * 2) - val e7 = AvailableBalanceChanged(null, randomBytes32, ShortChannelId(500000, 42, 1), 456123000, ChannelStateSpec.commitments) + val e7 = AvailableBalanceChanged(null, randomBytes32, ShortChannelId(500000, 42, 1), 456123000, ChannelStateSpec.commitmentsV1) val e8 = ChannelLifecycleEvent(randomBytes32, randomKey.publicKey, 456123000, true, false, "mutual") db.add(e1) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala index d214f9f2b4..f0d9fd3879 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar} import fr.acinq.bitcoin._ +import fr.acinq.eclair.channel.VersionCommitmentV1 import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.transactions.Transactions.{HtlcSuccessTx, HtlcTimeoutTx, TransactionWithInputInfo} @@ -116,6 +117,7 @@ class TestVectorsSpec extends FunSuite with Logging { val funding_pubkey = funding_privkey.publicKey val payment_privkey = Generators.derivePrivKey(payment_basepoint_secret, Local.per_commitment_point) val per_commitment_point = Point(hex"022c76692fd70814a8d1ed9dedc833318afaaed8188db4d14727e2e99bc619d325") + val delayed_payment_pubkey = Generators.derivePubKey(payment_basepoint, per_commitment_point) } val coinbaseTx = Transaction.read("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff0100f2052a010000001976a9143ca33c2e4446f4a305f23c80df8ad1afdcf652f988ac00000000") @@ -189,15 +191,16 @@ class TestVectorsSpec extends FunSuite with Logging { Local.revocation_pubkey, Local.toSelfDelay, Local.delayed_payment_privkey.publicKey, Remote.payment_privkey.publicKey, Local.payment_privkey.publicKey, Remote.payment_privkey.publicKey, // note: we have payment_key = htlc_key - spec) + Remote.delayed_payment_pubkey, + spec, VersionCommitmentV1) - val local_sig = Transactions.sign(tx, Local.funding_privkey) - val remote_sig = Transactions.sign(tx, Remote.funding_privkey) + val local_sig = Transactions.sign(tx, Local.funding_privkey, SIGHASH_ALL) + val remote_sig = Transactions.sign(tx, Remote.funding_privkey, SIGHASH_ALL) Transactions.addSigs(tx, Local.funding_pubkey, Remote.funding_pubkey, local_sig, remote_sig) } - val baseFee = Transactions.commitTxFee(Local.dustLimit, spec) + val baseFee = Transactions.commitTxFee(Local.dustLimit, spec, VersionCommitmentV1) logger.info(s"# base commitment transaction fee = ${baseFee.toLong}") val actualFee = fundingAmount - commitTx.tx.txOut.map(_.amount).sum logger.info(s"# actual commitment transaction fee = ${actualFee.toLong}") @@ -219,11 +222,12 @@ class TestVectorsSpec extends FunSuite with Logging { Local.revocation_pubkey, Local.toSelfDelay, Local.delayed_payment_privkey.publicKey, Remote.payment_privkey.publicKey, Local.payment_privkey.publicKey, Remote.payment_privkey.publicKey, // note: we have payment_key = htlc_key - spec) + Remote.delayed_payment_pubkey, + spec, VersionCommitmentV1) - val local_sig = Transactions.sign(tx, Local.funding_privkey) + val local_sig = Transactions.sign(tx, Local.funding_privkey, SIGHASH_ALL) logger.info(s"# local_signature = ${local_sig.dropRight(1).toHex}") - val remote_sig = Transactions.sign(tx, Remote.funding_privkey) + val remote_sig = Transactions.sign(tx, Remote.funding_privkey, SIGHASH_ALL) logger.info(s"remote_signature: ${remote_sig.dropRight(1).toHex}") } @@ -237,7 +241,7 @@ class TestVectorsSpec extends FunSuite with Logging { Local.revocation_pubkey, Local.toSelfDelay, Local.delayed_payment_privkey.publicKey, Local.payment_privkey.publicKey, Remote.payment_privkey.publicKey, // note: we have payment_key = htlc_key - spec) + spec, VersionCommitmentV1) logger.info(s"num_htlcs: ${(unsignedHtlcTimeoutTxs ++ unsignedHtlcSuccessTxs).length}") val htlcTxs: Seq[TransactionWithInputInfo] = (unsignedHtlcTimeoutTxs ++ unsignedHtlcSuccessTxs).sortBy(_.input.outPoint.index) @@ -245,12 +249,12 @@ class TestVectorsSpec extends FunSuite with Logging { htlcTxs.collect { case tx: HtlcSuccessTx => - val remoteSig = Transactions.sign(tx, Remote.payment_privkey) + val remoteSig = Transactions.sign(tx, Remote.payment_privkey, SIGHASH_ALL) val htlcIndex = htlcScripts.indexOf(Script.parse(tx.input.redeemScript)) logger.info(s"# signature for output ${tx.input.outPoint.index} (htlc $htlcIndex)") logger.info(s"remote_htlc_signature: ${remoteSig.dropRight(1).toHex}") case tx: HtlcTimeoutTx => - val remoteSig = Transactions.sign(tx, Remote.payment_privkey) + val remoteSig = Transactions.sign(tx, Remote.payment_privkey, SIGHASH_ALL) val htlcIndex = htlcScripts.indexOf(Script.parse(tx.input.redeemScript)) logger.info(s"# signature for output ${tx.input.outPoint.index} (htlc $htlcIndex)") logger.info(s"remote_htlc_signature: ${remoteSig.dropRight(1).toHex}") @@ -259,8 +263,8 @@ class TestVectorsSpec extends FunSuite with Logging { val signedTxs = htlcTxs collect { case tx: HtlcSuccessTx => //val tx = tx0.copy(tx = tx0.tx.copy(txOut = tx0.tx.txOut(0).copy(amount = Satoshi(545)) :: Nil)) - val localSig = Transactions.sign(tx, Local.payment_privkey) - val remoteSig = Transactions.sign(tx, Remote.payment_privkey) + val localSig = Transactions.sign(tx, Local.payment_privkey, SIGHASH_ALL) + val remoteSig = Transactions.sign(tx, Remote.payment_privkey, SIGHASH_ALL) val preimage = paymentPreimages.find(p => Crypto.sha256(p) == tx.paymentHash).get val tx1 = Transactions.addSigs(tx, localSig, remoteSig, preimage) Transaction.correctlySpends(tx1.tx, Seq(commitTx.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) @@ -269,8 +273,8 @@ class TestVectorsSpec extends FunSuite with Logging { logger.info(s"output htlc_success_tx ${htlcIndex}: ${tx1.tx}") tx1 case tx: HtlcTimeoutTx => - val localSig = Transactions.sign(tx, Local.payment_privkey) - val remoteSig = Transactions.sign(tx, Remote.payment_privkey) + val localSig = Transactions.sign(tx, Local.payment_privkey, SIGHASH_ALL) + val remoteSig = Transactions.sign(tx, Remote.payment_privkey, SIGHASH_ALL) val tx1 = Transactions.addSigs(tx, localSig, remoteSig) Transaction.correctlySpends(tx1.tx, Seq(commitTx.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) logger.info(s"# local_signature = ${localSig.dropRight(1).toHex}") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index d81b2c8d1a..10e922fecc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -21,6 +21,7 @@ import java.nio.ByteOrder import fr.acinq.bitcoin.Crypto.{PrivateKey, ripemd160, sha256} import fr.acinq.bitcoin.Script.{pay2wpkh, pay2wsh, write} import fr.acinq.bitcoin._ +import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.transactions.Scripts.{htlcOffered, htlcReceived, toLocalDelayed} import fr.acinq.eclair.transactions.Transactions.{addSigs, _} @@ -62,6 +63,10 @@ class TransactionsSpec extends FunSuite with Logging { } test("compute fees") { + + val expectedSizeSimplifiedCommitment = weight2fee(simplifiedFeerateKw , simplifiedCommitWeight + 172 * 4) + val expectedSizeCommitmentV1 = Satoshi(5340) + // see BOLT #3 specs val htlcs = Set( DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(5000000).amount, ByteVector32.Zeroes, 552, ByteVector.empty)), @@ -70,8 +75,9 @@ class TransactionsSpec extends FunSuite with Logging { DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(800000).amount, ByteVector32.Zeroes, 551, ByteVector.empty)) ) val spec = CommitmentSpec(htlcs, feeratePerKw = 5000, toLocalMsat = 0, toRemoteMsat = 0) - val fee = Transactions.commitTxFee(Satoshi(546), spec) - assert(fee == Satoshi(5340)) + + assert(commitTxFee(Satoshi(546), spec, VersionSimplifiedCommitment) == expectedSizeSimplifiedCommitment) + assert(commitTxFee(Satoshi(546), spec, VersionCommitmentV1) == expectedSizeCommitmentV1) } test("check pre-computed transaction weights") { @@ -91,7 +97,7 @@ class TransactionsSpec extends FunSuite with Logging { // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimP2WPKHOutputTx val pubKeyScript = write(pay2wpkh(localPaymentPriv.publicKey)) val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(20000), pubKeyScript) :: Nil, lockTime = 0) - val claimP2WPKHOutputTx = makeClaimP2WPKHOutputTx(commitTx, localDustLimit, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val claimP2WPKHOutputTx = makeClaimP2WPKHOutputTx(commitTx, localDustLimit, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw, None, VersionCommitmentV1) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(claimP2WPKHOutputTx, localPaymentPriv.publicKey, randomBytes(73)).tx) assert(claimP2WPKHOutputWeight == weight) @@ -103,7 +109,7 @@ class TransactionsSpec extends FunSuite with Logging { // first we create a fake htlcSuccessOrTimeoutTx tx, containing only the output that will be spent by the ClaimDelayedOutputTx val pubKeyScript = write(pay2wsh(toLocalDelayed(localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey))) val htlcSuccessOrTimeoutTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(20000), pubKeyScript) :: Nil, lockTime = 0) - val claimHtlcDelayedTx = makeClaimDelayedOutputTx(htlcSuccessOrTimeoutTx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val claimHtlcDelayedTx = makeClaimDelayedOutputTx(htlcSuccessOrTimeoutTx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw, VersionCommitmentV1) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(claimHtlcDelayedTx, randomBytes(73)).tx) assert(claimHtlcDelayedWeight == weight) @@ -115,7 +121,7 @@ class TransactionsSpec extends FunSuite with Logging { // first we create a fake commitTx tx, containing only the output that will be spent by the MainPenaltyTx val pubKeyScript = write(pay2wsh(toLocalDelayed(localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey))) val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(20000), pubKeyScript) :: Nil, lockTime = 0) - val mainPenaltyTx = makeMainPenaltyTx(commitTx, localDustLimit, localRevocationPriv.publicKey, finalPubKeyScript, toLocalDelay, localPaymentPriv.publicKey, feeratePerKw) + val mainPenaltyTx = makeMainPenaltyTx(commitTx, localDustLimit, localRevocationPriv.publicKey, finalPubKeyScript, toLocalDelay, localPaymentPriv.publicKey, feeratePerKw, VersionCommitmentV1) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(mainPenaltyTx, randomBytes(73)).tx) assert(mainPenaltyWeight == weight) @@ -173,6 +179,7 @@ class TransactionsSpec extends FunSuite with Logging { val localPaymentPriv = PrivateKey(randomBytes32 :+ 1.toByte) val localDelayedPaymentPriv = PrivateKey(randomBytes32 :+ 1.toByte) val remotePaymentPriv = PrivateKey(randomBytes32 :+ 1.toByte) + val remoteDelayedPaymentPriv = PrivateKey(randomBytes32 :+ 1.toByte) val localHtlcPriv = PrivateKey(randomBytes32 :+ 1.toByte) val remoteHtlcPriv = PrivateKey(randomBytes32 :+ 1.toByte) val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32, true).publicKey)) @@ -204,21 +211,26 @@ class TransactionsSpec extends FunSuite with Logging { toRemoteMsat = millibtc2satoshi(MilliBtc(300)).amount * 1000) val commitTxNumber = 0x404142434445L - val commitTx = { - val txinfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.toPoint, remotePaymentPriv.toPoint, true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, spec) - val localSig = Transactions.sign(txinfo, localPaymentPriv) - val remoteSig = Transactions.sign(txinfo, remotePaymentPriv) + def commitTx(commitmentVersion: CommitmentVersion = VersionCommitmentV1) = { + val txinfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.toPoint, remotePaymentPriv.toPoint, true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, remoteDelayedPaymentPriv.publicKey, spec, commitmentVersion) + val localSig = Transactions.sign(txinfo, localPaymentPriv, SIGHASH_ALL) + val remoteSig = Transactions.sign(txinfo, remotePaymentPriv, SIGHASH_ALL) Transactions.addSigs(txinfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) } + def createHtlcTxs(commitmentVersion: CommitmentVersion = VersionCommitmentV1):(Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { + makeHtlcTxs(commitTx(commitmentVersion).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, spec, commitmentVersion) + } + { - assert(getCommitTxNumber(commitTx.tx, true, localPaymentPriv.publicKey, remotePaymentPriv.publicKey) == commitTxNumber) + assert(getCommitTxNumber(commitTx().tx, true, localPaymentPriv.publicKey, remotePaymentPriv.publicKey) == commitTxNumber) val hash = Crypto.sha256(localPaymentPriv.publicKey.toBin ++ remotePaymentPriv.publicKey.toBin) val num = Protocol.uint64(hash.takeRight(8).toArray, ByteOrder.BIG_ENDIAN) & 0xffffffffffffL - val check = ((commitTx.tx.txIn.head.sequence & 0xffffff) << 24) | commitTx.tx.lockTime + val check = ((commitTx().tx.txIn.head.sequence & 0xffffff) << 24) | (commitTx().tx.lockTime & 0xffffff) assert((check ^ num) == commitTxNumber) } - val (htlcTimeoutTxs, htlcSuccessTxs) = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, spec) + + val (htlcTimeoutTxs, htlcSuccessTxs) = createHtlcTxs(VersionCommitmentV1) assert(htlcTimeoutTxs.size == 2) // htlc1 and htlc3 assert(htlcSuccessTxs.size == 2) // htlc2 and htlc4 @@ -226,30 +238,55 @@ class TransactionsSpec extends FunSuite with Logging { { // either party spends local->remote htlc output with htlc timeout tx for (htlcTimeoutTx <- htlcTimeoutTxs) { - val localSig = sign(htlcTimeoutTx, localHtlcPriv) - val remoteSig = sign(htlcTimeoutTx, remoteHtlcPriv) + val localSig = sign(htlcTimeoutTx, localHtlcPriv, SIGHASH_ALL) + val remoteSig = sign(htlcTimeoutTx, remoteHtlcPriv, SIGHASH_ALL) val signed = addSigs(htlcTimeoutTx, localSig, remoteSig) assert(checkSpendable(signed).isSuccess) } } + { + // Simplified commitment + val commitTransaction = commitTx(commitmentVersion = VersionSimplifiedCommitment) + + // local is funder, pays for fees + push_me outputs + val expectedToLocalAmount = Satoshi(spec.toLocalMsat / 1000) - Transactions.pushMeValue * 2 - commitTxFee(localDustLimit, spec, VersionSimplifiedCommitment) + val expectedToRemoteAmount = Satoshi(spec.toRemoteMsat / 1000) + + // there must be 2 push-me outputs + val toLocalPushMe = Script.write(pay2wsh(Scripts.pushMeSimplified(localDelayedPaymentPriv.publicKey))) + val toRemotePushMe = Script.write(pay2wsh(Scripts.pushMeSimplified(remoteDelayedPaymentPriv.publicKey))) + assert(commitTransaction.tx.txOut.exists(_.publicKeyScript == toLocalPushMe)) + assert(commitTransaction.tx.txOut.exists(_.publicKeyScript == toRemotePushMe)) + + // to_remote is delayed with to_self_delay + val toRemoteDelayedScript = Script.write(pay2wsh(Scripts.toRemoteDelayed(remotePaymentPriv.publicKey, toLocalDelay))) + val Some(toRemoteMainOut) = commitTransaction.tx.txOut.find(_.publicKeyScript == toRemoteDelayedScript) + + val toLocalMainScript = Script.write(pay2wsh(Scripts.toLocalDelayed(localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey))) + val Some(toLocalMainOut) = commitTransaction.tx.txOut.find(_.publicKeyScript == toLocalMainScript) + + assert(toLocalMainOut.amount == expectedToLocalAmount) + assert(toRemoteMainOut.amount == expectedToRemoteAmount) + } + { // local spends delayed output of htlc1 timeout tx - val claimHtlcDelayed = makeClaimDelayedOutputTx(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = sign(claimHtlcDelayed, localDelayedPaymentPriv) + val claimHtlcDelayed = makeClaimDelayedOutputTx(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw, VersionCommitmentV1) + val localSig = sign(claimHtlcDelayed, localDelayedPaymentPriv, SIGHASH_ALL) val signedTx = addSigs(claimHtlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit intercept[RuntimeException] { - makeClaimDelayedOutputTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + makeClaimDelayedOutputTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw, VersionCommitmentV1) } } { // remote spends local->remote htlc1/htlc3 output directly in case of success for ((htlc, paymentPreimage) <- (htlc1, paymentPreimage1) :: (htlc3, paymentPreimage3) :: Nil) { - val claimHtlcSuccessTx = makeClaimHtlcSuccessTx(commitTx.tx, outputsAlreadyUsed = Set.empty, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw) - val localSig = sign(claimHtlcSuccessTx, remoteHtlcPriv) + val claimHtlcSuccessTx = makeClaimHtlcSuccessTx(commitTx().tx, outputsAlreadyUsed = Set.empty, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw) + val localSig = sign(claimHtlcSuccessTx, remoteHtlcPriv, SIGHASH_ALL) val signed = addSigs(claimHtlcSuccessTx, localSig, paymentPreimage) assert(checkSpendable(signed).isSuccess) } @@ -258,39 +295,39 @@ class TransactionsSpec extends FunSuite with Logging { { // local spends remote->local htlc2/htlc4 output with htlc success tx using payment preimage for ((htlcSuccessTx, paymentPreimage) <- (htlcSuccessTxs(0), paymentPreimage2) :: (htlcSuccessTxs(1), paymentPreimage4) :: Nil) { - val localSig = sign(htlcSuccessTx, localHtlcPriv) - val remoteSig = sign(htlcSuccessTx, remoteHtlcPriv) + val localSig = sign(htlcSuccessTx, localHtlcPriv, SIGHASH_ALL) + val remoteSig = sign(htlcSuccessTx, remoteHtlcPriv, SIGHASH_ALL) val signedTx = addSigs(htlcSuccessTx, localSig, remoteSig, paymentPreimage) assert(checkSpendable(signedTx).isSuccess) // check remote sig - assert(checkSig(htlcSuccessTx, remoteSig, remoteHtlcPriv.publicKey)) + assert(checkSig(htlcSuccessTx, remoteSig, remoteHtlcPriv.publicKey, SIGHASH_ALL)) } } { // local spends delayed output of htlc2 success tx - val claimHtlcDelayed = makeClaimDelayedOutputTx(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = sign(claimHtlcDelayed, localDelayedPaymentPriv) + val claimHtlcDelayed = makeClaimDelayedOutputTx(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw, VersionCommitmentV1) + val localSig = sign(claimHtlcDelayed, localDelayedPaymentPriv, SIGHASH_ALL) val signedTx = addSigs(claimHtlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc4 timeout tx because it is below the dust limit intercept[RuntimeException] { - makeClaimDelayedOutputTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + makeClaimDelayedOutputTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw, VersionCommitmentV1) } } { // remote spends main output - val claimP2WPKHOutputTx = makeClaimP2WPKHOutputTx(commitTx.tx, localDustLimit, remotePaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = sign(claimP2WPKHOutputTx, remotePaymentPriv) + val claimP2WPKHOutputTx = makeClaimP2WPKHOutputTx(commitTx(VersionCommitmentV1).tx, localDustLimit, remotePaymentPriv.publicKey, finalPubKeyScript, feeratePerKw, None, VersionCommitmentV1) + val localSig = sign(claimP2WPKHOutputTx, remotePaymentPriv, SIGHASH_ALL) val signedTx = addSigs(claimP2WPKHOutputTx, remotePaymentPriv.publicKey, localSig) assert(checkSpendable(signedTx).isSuccess) } { // remote spends remote->local htlc output directly in case of timeout - val claimHtlcTimeoutTx = makeClaimHtlcTimeoutTx(commitTx.tx, outputsAlreadyUsed = Set.empty, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc2, feeratePerKw) - val localSig = sign(claimHtlcTimeoutTx, remoteHtlcPriv) + val claimHtlcTimeoutTx = makeClaimHtlcTimeoutTx(commitTx().tx, outputsAlreadyUsed = Set.empty, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc2, feeratePerKw) + val localSig = sign(claimHtlcTimeoutTx, remoteHtlcPriv, SIGHASH_ALL) val signed = addSigs(claimHtlcTimeoutTx, localSig) assert(checkSpendable(signed).isSuccess) } @@ -298,8 +335,8 @@ class TransactionsSpec extends FunSuite with Logging { { // remote spends offered HTLC output with revocation key val script = Script.write(Scripts.htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc1.paymentHash))) - val htlcPenaltyTx = makeHtlcPenaltyTx(commitTx.tx, outputsAlreadyUsed = Set.empty, script, localDustLimit, finalPubKeyScript, feeratePerKw) - val sig = sign(htlcPenaltyTx, localRevocationPriv) + val htlcPenaltyTx = makeHtlcPenaltyTx(commitTx().tx, outputsAlreadyUsed = Set.empty, script, localDustLimit, finalPubKeyScript, feeratePerKw) + val sig = sign(htlcPenaltyTx, localRevocationPriv, SIGHASH_ALL) val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) assert(checkSpendable(signed).isSuccess) } @@ -307,19 +344,14 @@ class TransactionsSpec extends FunSuite with Logging { { // remote spends received HTLC output with revocation key val script = Script.write(Scripts.htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc2.paymentHash), htlc2.cltvExpiry)) - val htlcPenaltyTx = makeHtlcPenaltyTx(commitTx.tx, outputsAlreadyUsed = Set.empty, script, localDustLimit, finalPubKeyScript, feeratePerKw) - val sig = sign(htlcPenaltyTx, localRevocationPriv) + val htlcPenaltyTx = makeHtlcPenaltyTx(commitTx().tx, outputsAlreadyUsed = Set.empty, script, localDustLimit, finalPubKeyScript, feeratePerKw) + val sig = sign(htlcPenaltyTx, localRevocationPriv, SIGHASH_ALL) val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) assert(checkSpendable(signed).isSuccess) } } - def checkSuccessOrFailTest[T](input: Try[T]) = input match { - case Success(_) => () - case Failure(t) => fail(t) - } - def htlc(direction: Direction, amount: Satoshi): DirectedHtlc = DirectedHtlc(direction, UpdateAddHtlc(ByteVector32.Zeroes, 0, amount.amount * 1000, ByteVector32.Zeroes, 144, ByteVector.empty)) @@ -362,7 +394,7 @@ class TransactionsSpec extends FunSuite with Logging { tests.foreach(test => { logger.info(s"running BOLT 2 test: '${test.name}'") - val fee = commitTxFee(test.dustLimit, test.spec) + val fee = commitTxFee(test.dustLimit, test.spec, VersionCommitmentV1) assert(fee === test.expectedFee) }) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala index 276e6db7a6..c8a42a398f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala @@ -172,15 +172,15 @@ class ChannelCodecsSpec extends FunSuite { // currently version=0 and discriminator type=1 assert(bin_old.startsWith(hex"000001")) // let's decode the old data (this will use the old codec that provides default values for new fields) - val data_new = stateDataCodec.decode(bin_old.toBitVector).require.value + val data_new = genericStateDataCodec.decode(bin_old.toBitVector).require.value assert(data_new.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx === None) assert(Platform.currentTime / 1000 - data_new.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].waitingSince < 3600) // we just set this timestamp to current time // and re-encode it with the new codec - val bin_new = ByteVector(stateDataCodec.encode(data_new).require.toByteVector.toArray) + val bin_new = ByteVector(genericStateDataCodec.encode(data_new).require.toByteVector.toArray) // data should now be encoded under the new format, with version=0 and type=8 assert(bin_new.startsWith(hex"000008")) // now let's decode it again - val data_new2 = stateDataCodec.decode(bin_new.toBitVector).require.value + val data_new2 = genericStateDataCodec.decode(bin_new.toBitVector).require.value // data should match perfectly assert(data_new === data_new2) } diff --git a/travis/builddeps.sh b/travis/builddeps.sh deleted file mode 100755 index bfff183d5f..0000000000 --- a/travis/builddeps.sh +++ /dev/null @@ -1,25 +0,0 @@ -pushd . -# lightning deps -sudo add-apt-repository -y ppa:chris-lea/libsodium -sudo apt-get update -sudo apt-get install -y libsodium-dev libgmp-dev libsqlite3-dev -cd -git clone https://github.com/luke-jr/libbase58.git -cd libbase58 -./autogen.sh && ./configure && make && sudo make install -# lightning -cd -git clone https://github.com/ElementsProject/lightning.git -cd lightning -git checkout fce9ee29e3c37b4291ebb050e6a687cfaa7df95a -git submodule init -git submodule update -make -# bitcoind -cd -wget https://bitcoin.org/bin/bitcoin-core-0.13.0/bitcoin-0.13.0-x86_64-linux-gnu.tar.gz -echo "bcc1e42d61f88621301bbb00512376287f9df4568255f8b98bc10547dced96c8 bitcoin-0.13.0-x86_64-linux-gnu.tar.gz" > sha256sum.asc -sha256sum -c sha256sum.asc -tar xzvf bitcoin-0.13.0-x86_64-linux-gnu.tar.gz -popd -