From 40251c6188a4076ff1be81b9496c73aa4096bd56 Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 31 Jul 2019 14:32:12 +0200 Subject: [PATCH 01/11] Derive channel keys from funding pubkey We now generate a random funding key for each new channel, and use its public key to deterministically derive all channel keys and secrets. This will let us easily recover funds using DLP even if we've lost everything but our seed: we just need to connect to the node we had a channel with, ask them to publish their commit tx, and once we see it on the blockchain we can extract our funding pubkey, recompute channel keys and spend our output. --- .../fr/acinq/eclair/channel/Channel.scala | 75 +++++++----- .../acinq/eclair/channel/ChannelTypes.scala | 23 +++- .../fr/acinq/eclair/channel/Commitments.scala | 38 +++--- .../fr/acinq/eclair/channel/Helpers.scala | 67 ++++++----- .../fr/acinq/eclair/crypto/KeyManager.scala | 40 ++++++- .../acinq/eclair/crypto/LocalKeyManager.scala | 4 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 16 ++- .../fr/acinq/eclair/wire/ChannelCodecs.scala | 12 +- .../channel/states/e/NormalStateSpec.scala | 4 +- .../channel/states/e/OfflineStateSpec.scala | 108 ++++++++++++++++-- .../eclair/transactions/LocalParamsSpec.scala | 35 ++++++ .../acinq/eclair/wire/ChannelCodecsSpec.scala | 18 +-- 12 files changed, 337 insertions(+), 103 deletions(-) create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/transactions/LocalParamsSpec.scala 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 72585ee2fd..4bb0b2aa96 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 @@ -24,7 +24,7 @@ import fr.acinq.bitcoin._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel.Helpers.{Closing, Funding} -import fr.acinq.eclair.crypto.{ShaChain, Sphinx} +import fr.acinq.eclair.crypto.{KeyManager, ShaChain, Sphinx} import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Announcements @@ -148,6 +148,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, remote, _, channelFlags), Nothing) => context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, isFunder = true, temporaryChannelId, initialFeeratePerKw, Some(fundingTxFeeratePerKw))) forwarder ! remote + val fundingPubKey = localParams.fundingPubKey(nodeParams.keyManager) + val channelKeyPath = localParams.channelKeyPath(nodeParams.keyManager) val open = OpenChannel(nodeParams.chainHash, temporaryChannelId = temporaryChannelId, fundingSatoshis = fundingSatoshis, @@ -159,12 +161,12 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId feeratePerKw = initialFeeratePerKw, toSelfDelay = localParams.toSelfDelay, maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, - fundingPubkey = keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, - revocationBasepoint = keyManager.revocationPoint(localParams.channelKeyPath).publicKey, - paymentBasepoint = keyManager.paymentPoint(localParams.channelKeyPath).publicKey, - delayedPaymentBasepoint = keyManager.delayedPaymentPoint(localParams.channelKeyPath).publicKey, - htlcBasepoint = keyManager.htlcPoint(localParams.channelKeyPath).publicKey, - firstPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, 0), + fundingPubkey = fundingPubKey.publicKey, + revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, + paymentBasepoint = keyManager.paymentPoint(channelKeyPath).publicKey, + delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, + htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, + firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), channelFlags = channelFlags) goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder, open) sending open @@ -271,6 +273,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Failure(t) => handleLocalError(t, d, Some(open)) case Success(_) => context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, isFunder = false, open.temporaryChannelId, open.feeratePerKw, None)) + val fundingPubKey = localParams.fundingPubKey(nodeParams.keyManager) + val channelKeyPath = localParams.channelKeyPath(nodeParams.keyManager) // TODO: maybe also check uniqueness of temporary channel id val minimumDepth = nodeParams.minDepthBlocks val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId, @@ -281,12 +285,12 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId htlcMinimumMsat = localParams.htlcMinimumMsat, toSelfDelay = localParams.toSelfDelay, maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, - fundingPubkey = keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, - revocationBasepoint = keyManager.revocationPoint(localParams.channelKeyPath).publicKey, - paymentBasepoint = keyManager.paymentPoint(localParams.channelKeyPath).publicKey, - delayedPaymentBasepoint = keyManager.delayedPaymentPoint(localParams.channelKeyPath).publicKey, - htlcBasepoint = keyManager.htlcPoint(localParams.channelKeyPath).publicKey, - firstPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, 0)) + fundingPubkey = fundingPubKey.publicKey, + revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, + paymentBasepoint = keyManager.paymentPoint(channelKeyPath).publicKey, + delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, + htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, + firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0)) val remoteParams = RemoteParams( nodeId = remoteNodeId, dustLimitSatoshis = open.dustLimitSatoshis, @@ -336,8 +340,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId globalFeatures = remoteInit.globalFeatures, localFeatures = remoteInit.localFeatures) log.debug(s"remote params: $remoteParams") - val localFundingPubkey = keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey - val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey, remoteParams.fundingPubKey))) + val localFundingPubkey = localParams.fundingPubKey(nodeParams.keyManager) + val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey))) wallet.makeFundingTx(fundingPubkeyScript, Satoshi(fundingSatoshis), fundingTxFeeratePerKw).pipeTo(self) goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, accept.firstPerCommitmentPoint, open) } @@ -364,7 +368,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // 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.onChainFeeConf.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, localParams.fundingPubKey(keyManager)) // signature of their initial commitment tx that pays remote pushMsat val fundingCreated = FundingCreated( temporaryChannelId = temporaryChannelId, @@ -406,12 +410,13 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, temporaryChannelId, localParams, remoteParams, fundingSatoshis: Long, pushMsat, initialFeeratePerKw, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.onChainFeeConf.maxFeerateMismatch) // check remote signature validity - val localSigOfLocalTx = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) - val signedLocalCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) + val fundingPubKey = localParams.fundingPubKey(keyManager) + val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey) + val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.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, fundingPubKey) val channelId = toLongId(fundingTxHash, fundingTxOutputIndex) // watch the funding tx transaction val commitInput = localCommitTx.input @@ -447,8 +452,9 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId 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 signedLocalCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) + val fundingPubKey = localParams.fundingPubKey(keyManager) + val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey) + val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) Transactions.checkSpendable(signedLocalCommitTx) match { case Failure(cause) => // we rollback the funding tx, it will never be published @@ -522,7 +528,9 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Success(_) => log.info(s"channelId=${commitments.channelId} was confirmed at blockHeight=$blockHeight txIndex=$txIndex") blockchain ! WatchLost(self, commitments.commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_LOST) - val nextPerCommitmentPoint = keyManager.commitmentPoint(commitments.localParams.channelKeyPath, 1) + val fundingPubKey = d.commitments.localParams.fundingPubKey(keyManager).publicKey + val channelKeyPath = d.commitments.localParams.channelKeyPath(keyManager) + val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) val fundingLocked = FundingLocked(commitments.channelId, nextPerCommitmentPoint) deferred.foreach(self ! _) // this is the temporary channel id that we will use in our channel_update message, the goal is to be able to use our channel @@ -891,7 +899,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId require(d.shortChannelId == remoteAnnSigs.shortChannelId, s"shortChannelId mismatch: local=${d.shortChannelId} remote=${remoteAnnSigs.shortChannelId}") log.info(s"announcing channelId=${d.channelId} on the network with shortId=${d.shortChannelId}") import d.commitments.{localParams, remoteParams} - val channelAnn = Announcements.makeChannelAnnouncement(nodeParams.chainHash, localAnnSigs.shortChannelId, nodeParams.nodeId, remoteParams.nodeId, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, localAnnSigs.nodeSignature, remoteAnnSigs.nodeSignature, localAnnSigs.bitcoinSignature, remoteAnnSigs.bitcoinSignature) + val fundingPubKey = localParams.fundingPubKey(keyManager) + val channelAnn = Announcements.makeChannelAnnouncement(nodeParams.chainHash, localAnnSigs.shortChannelId, nodeParams.nodeId, remoteParams.nodeId, fundingPubKey.publicKey, remoteParams.fundingPubKey, localAnnSigs.nodeSignature, remoteAnnSigs.nodeSignature, localAnnSigs.bitcoinSignature, remoteAnnSigs.bitcoinSignature) // we use GOTO instead of stay because we want to fire transitions goto(NORMAL) using d.copy(channelAnnouncement = Some(channelAnn)) storing() case Some(_) => @@ -1405,7 +1414,9 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId 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 fundingPubKey = d.commitments.localParams.fundingPubKey(keyManager) + val channelKeyPath = d.commitments.localParams.channelKeyPath(keyManager) + val myCurrentPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, d.commitments.localCommit.index) val channelReestablish = ChannelReestablish( channelId = d.channelId, @@ -1462,16 +1473,20 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_LOCKED) => log.debug(s"re-sending fundingLocked") - val nextPerCommitmentPoint = keyManager.commitmentPoint(d.commitments.localParams.channelKeyPath, 1) + val fundingPubKey = d.commitments.localParams.fundingPubKey(keyManager).publicKey + val channelKeyPath = d.commitments.localParams.channelKeyPath(keyManager) + val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint) goto(WAIT_FOR_FUNDING_LOCKED) sending fundingLocked case Event(channelReestablish: ChannelReestablish, d: DATA_NORMAL) => + val fundingPubKey = d.commitments.localParams.fundingPubKey(keyManager) + val channelKeyPath = d.commitments.localParams.channelKeyPath(keyManager) channelReestablish match { case ChannelReestablish(_, _, nextRemoteRevocationNumber, Some(yourLastPerCommitmentSecret), _) if !Helpers.checkLocalCommit(d, nextRemoteRevocationNumber) => // if next_remote_revocation_number is greater than our local commitment index, it means that either we are using an outdated commitment, or they are lying // but first we need to make sure that the last per_commitment_secret that they claim to have received from us is correct for that next_remote_revocation_number minus 1 - if (keyManager.commitmentSecret(d.commitments.localParams.channelKeyPath, nextRemoteRevocationNumber - 1) == yourLastPerCommitmentSecret) { + if (keyManager.commitmentSecret(channelKeyPath, nextRemoteRevocationNumber - 1) == yourLastPerCommitmentSecret) { log.warning(s"counterparty proved that we have an outdated (revoked) local commitment!!! ourCommitmentNumber=${d.commitments.localCommit.index} theirCommitmentNumber=$nextRemoteRevocationNumber") // their data checks out, we indeed seem to be using an old revoked commitment, and must absolutely *NOT* publish it, because that would be a cheating attempt and they // would punish us by taking all the funds in the channel @@ -1496,7 +1511,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId if (channelReestablish.nextLocalCommitmentNumber == 1 && d.commitments.localCommit.index == 0) { // If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node MUST retransmit funding_locked, otherwise it MUST NOT log.debug(s"re-sending fundingLocked") - val nextPerCommitmentPoint = keyManager.commitmentPoint(d.commitments.localParams.channelKeyPath, 1) + val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint) forwarder ! fundingLocked } @@ -2128,8 +2143,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } else if (commitments1.localCommit.index == channelReestablish.nextRemoteRevocationNumber + 1) { // our last revocation got lost, let's resend it log.debug(s"re-sending last revocation") - val localPerCommitmentSecret = keyManager.commitmentSecret(commitments1.localParams.channelKeyPath, d.commitments.localCommit.index - 1) - val localNextPerCommitmentPoint = keyManager.commitmentPoint(commitments1.localParams.channelKeyPath, d.commitments.localCommit.index + 1) + val fundingPubKey = d.commitments.localParams.fundingPubKey(keyManager).publicKey + val channelKeyPath = d.commitments.localParams.channelKeyPath(keyManager) + val localPerCommitmentSecret = keyManager.commitmentSecret(channelKeyPath, d.commitments.localCommit.index - 1) + val localNextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, d.commitments.localCommit.index + 1) val revocation = RevokeAndAck( channelId = commitments1.channelId, perCommitmentSecret = localPerCommitmentSecret, 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 48e6d6b57f..c005e7a436 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 @@ -19,8 +19,9 @@ package fr.acinq.eclair.channel import java.util.UUID import akka.actor.ActorRef -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, Transaction} +import fr.acinq.eclair.crypto.KeyManager import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions.CommitTx import fr.acinq.eclair.wire.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc} @@ -189,8 +190,9 @@ 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 -final case class LocalParams(nodeId: PublicKey, - channelKeyPath: DeterministicWallet.KeyPath, +final case class LocalParams(version: Int, + nodeId: PublicKey, + fundingKeyPath: DeterministicWallet.KeyPath, dustLimitSatoshis: Long, maxHtlcValueInFlightMsat: UInt64, channelReserveSatoshis: Long, @@ -200,7 +202,20 @@ final case class LocalParams(nodeId: PublicKey, isFunder: Boolean, defaultFinalScriptPubKey: ByteVector, globalFeatures: ByteVector, - localFeatures: ByteVector) + localFeatures: ByteVector) { + + def fundingPubKey(keyManager: KeyManager) = keyManager.fundingPublicKey(fundingKeyPath) + + def channelKeyPath(keyManager: KeyManager) = version match { + case 0 => + // legacy mode: we reuse the funding key path as our channel key path + fundingKeyPath + case 1 => + // deterministic mode: use the funding pubkey to compute the channel key path + KeyManager.channelKeyPath(fundingPubKey(keyManager)) + + } +} final case class RemoteParams(nodeId: PublicKey, dustLimitSatoshis: Long, 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 7c1bace272..7844b6b241 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 @@ -377,10 +377,12 @@ object Commitments { // 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 sig = keyManager.sign(remoteCommitTx, localParams.fundingPubKey(keyManager)) val sortedHtlcTxs: Seq[TransactionWithInputInfo] = (htlcTimeoutTxs ++ htlcSuccessTxs).sortBy(_.input.outPoint.index) - val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(localParams.channelKeyPath), remoteNextPerCommitmentPoint)) + val fundingPubKey = localParams.fundingPubKey(keyManager) + val channelKeyPath = localParams.channelKeyPath(keyManager) + val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), remoteNextPerCommitmentPoint)) // 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) @@ -424,16 +426,18 @@ object Commitments { // receiving money i.e its commit tx has one output for them val spec = CommitmentSpec.reduce(localCommit.spec, localChanges.acked, remoteChanges.proposed) - val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, commitments.localCommit.index + 1) + val fundingPubKey = localParams.fundingPubKey(keyManager) + val channelKeyPath = localParams.channelKeyPath(keyManager) + val localPerCommitmentPoint = keyManager.commitmentPoint(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 sig = keyManager.sign(localCommitTx, fundingPubKey) 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) // TODO: should we have optional sig? (original comment: this tx will NOT be signed if our output is empty) // no need to compute htlc sigs if commit sig doesn't check out - val signedCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, sig, commit.signature) + val signedCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteParams.fundingPubKey, sig, commit.signature) if (Transactions.checkSpendable(signedCommitTx).isFailure) { throw InvalidCommitmentSignature(commitments.channelId, signedCommitTx.tx) } @@ -442,7 +446,7 @@ object Commitments { if (commit.htlcSignatures.size != sortedHtlcTxs.size) { throw HtlcSigCountMismatch(commitments.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size) } - val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(localParams.channelKeyPath), localPerCommitmentPoint)) + val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint)) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint) // combine the sigs to make signed txes val htlcTxsAndSigs = (sortedHtlcTxs, htlcSigs, commit.htlcSignatures).zipped.toList.collect { @@ -460,8 +464,8 @@ object Commitments { } // we will send our revocation preimage + our next revocation hash - val localPerCommitmentSecret = keyManager.commitmentSecret(localParams.channelKeyPath, commitments.localCommit.index) - val localNextPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, commitments.localCommit.index + 2) + val localPerCommitmentSecret = keyManager.commitmentSecret(channelKeyPath, commitments.localCommit.index) + val localNextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index + 2) val revocation = RevokeAndAck( channelId = commitments.channelId, perCommitmentSecret = localPerCommitmentSecret, @@ -522,23 +526,27 @@ object Commitments { } def makeLocalTxs(keyManager: KeyManager, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, localPerCommitmentPoint: PublicKey, spec: CommitmentSpec): (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 fundingPubKey = localParams.fundingPubKey(keyManager) + val channelKeyPath = localParams.channelKeyPath(keyManager) + val localDelayedPaymentPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) + val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, localPerCommitmentPoint) val remotePaymentPubkey = Generators.derivePubKey(remoteParams.paymentBasepoint, localPerCommitmentPoint) 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 commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, keyManager.paymentPoint(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) (commitTx, htlcTimeoutTxs, htlcSuccessTxs) } def makeRemoteTxs(keyManager: KeyManager, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, remotePerCommitmentPoint: PublicKey, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { - val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) - val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) + val fundingPubKey = localParams.fundingPubKey(keyManager) + val channelKeyPath = localParams.channelKeyPath(keyManager) + val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(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 remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, keyManager.paymentPoint(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) (commitTx, htlcTimeoutTxs, htlcSuccessTxs) } 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 5cbe075fcb..5155509a65 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 @@ -205,7 +205,8 @@ object Helpers { def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: ShortChannelId) = { val features = ByteVector.empty // empty features for now - val (localNodeSig, localBitcoinSig) = nodeParams.keyManager.signChannelAnnouncement(commitments.localParams.channelKeyPath, nodeParams.chainHash, shortChannelId, commitments.remoteParams.nodeId, commitments.remoteParams.fundingPubKey, features) + val fundingPubKey = commitments.localParams.fundingPubKey(nodeParams.keyManager) + val (localNodeSig, localBitcoinSig) = nodeParams.keyManager.signChannelAnnouncement(fundingPubKey, nodeParams.chainHash, shortChannelId, commitments.remoteParams.nodeId, commitments.remoteParams.fundingPubKey, features) AnnouncementSignatures(commitments.channelId, shortChannelId, localNodeSig, localBitcoinSig) } @@ -269,8 +270,10 @@ object Helpers { } } - val commitmentInput = makeFundingInputInfo(fundingTxHash, fundingTxOutputIndex, Satoshi(fundingSatoshis), keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey) - val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, 0) + val fundingPubKey = localParams.fundingPubKey(keyManager) + val channelKeyPath = localParams.channelKeyPath(keyManager) + val commitmentInput = makeFundingInputInfo(fundingTxHash, fundingTxOutputIndex, Satoshi(fundingSatoshis), fundingPubKey.publicKey, remoteParams.fundingPubKey) + val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0) val (localCommitTx, _, _) = Commitments.makeLocalTxs(keyManager, 0, localParams, remoteParams, commitmentInput, localPerCommitmentPoint, localSpec) val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(keyManager, 0, localParams, remoteParams, commitmentInput, remoteFirstPerCommitmentPoint, remoteSpec) @@ -455,7 +458,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, commitments.localParams.fundingPubKey(keyManager)) 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}}") @@ -470,7 +473,7 @@ object Helpers { throw InvalidCloseFee(commitments.channelId, remoteClosingFee.amount) } val (closingTx, closingSigned) = makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, remoteClosingFee) - val signedClosingTx = Transactions.addSigs(closingTx, keyManager.fundingPublicKey(commitments.localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig) + val signedClosingTx = Transactions.addSigs(closingTx, commitments.localParams.fundingPubKey(keyManager).publicKey, remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig) Transactions.checkSpendable(signedClosingTx).map(x => signedClosingTx.tx).recover { case _ => throw InvalidCloseSignature(commitments.channelId, signedClosingTx.tx) } } @@ -499,17 +502,18 @@ object Helpers { def claimCurrentLocalCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(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") - - val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, commitments.localCommit.index.toInt) + val fundingPubKey = localParams.fundingPubKey(keyManager) + val channelKeyPath = localParams.channelKeyPath(keyManager) + val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index.toInt) val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) - val localDelayedPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(localParams.channelKeyPath).publicKey, localPerCommitmentPoint) + val localDelayedPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) val feeratePerKwDelayed = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) // 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 sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint) Transactions.addSigs(claimDelayed, sig) }) @@ -544,7 +548,7 @@ object Helpers { remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed) - val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(localParams.channelKeyPath), localPerCommitmentPoint) + val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint) Transactions.addSigs(claimDelayed, sig) }) } @@ -572,11 +576,12 @@ object Helpers { 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") - - val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(localParams.channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) + val fundingPubKey = localParams.fundingPubKey(keyManager) + val channelKeyPath = localParams.channelKeyPath(keyManager) + val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(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) + val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index.toInt) + val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) // we need to use a rather high fee for htlc-claim because we compete with the counterparty val feeratePerKwHtlc = feeEstimator.getFeeratePerKw(target = 2) @@ -592,7 +597,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(channelKeyPath), remoteCommit.remotePerCommitmentPoint) Transactions.addSigs(txinfo, sig, preimage) }) @@ -602,7 +607,7 @@ 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(channelKeyPath), remoteCommit.remotePerCommitmentPoint) Transactions.addSigs(txinfo, sig) }) }.toSeq.flatten @@ -625,14 +630,16 @@ object Helpers { * @return a list of transactions (one per HTLC that we can claim) */ def claimRemoteCommitMainOutput(keyManager: KeyManager, commitments: Commitments, remotePerCommitmentPoint: PublicKey, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): RemoteCommitPublished = { - val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(commitments.localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) + val fundingPubKey = commitments.localParams.fundingPubKey(keyManager) + val channelKeyPath = commitments.localParams.channelKeyPath(keyManager) + val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) val feeratePerKwMain = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) 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) + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint) Transactions.addSigs(claimMain, localPubkey, sig) }) @@ -657,9 +664,11 @@ object Helpers { def claimRevokedRemoteCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, tx: Transaction, db: ChannelsDb, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): Option[RevokedCommitPublished] = { import commitments._ require(tx.txIn.size == 1, "commitment tx should have 1 input") + val fundingPubKey = commitments.localParams.fundingPubKey(keyManager) + val channelKeyPath = commitments.localParams.channelKeyPath(keyManager) val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn(0).sequence, tx.lockTime) // this tx has been published by remote, so we need to invert local/remote params - val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, keyManager.paymentPoint(localParams.channelKeyPath).publicKey) + val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, keyManager.paymentPoint(channelKeyPath).publicKey) require(txnumber <= 0xffffffffffffL, "txnumber must be lesser than 48 bits long") log.warning(s"a revoked commit has been published with txnumber=$txnumber") // now we know what commit number this tx is referring to, we can derive the commitment point from the shachain @@ -668,9 +677,9 @@ object Helpers { .map { remotePerCommitmentSecret => val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) - val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) - val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) - val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) + val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) val feeratePerKwMain = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) @@ -680,14 +689,14 @@ 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 sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint) 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 sig = keyManager.sign(txinfo, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret) Transactions.addSigs(txinfo, sig) }) @@ -708,7 +717,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(channelKeyPath), remotePerCommitmentSecret) Transactions.addSigs(htlcPenalty, sig, remoteRevocationPubkey) }) }.toList.flatten @@ -748,22 +757,24 @@ object Helpers { import commitments._ val tx = revokedCommitPublished.commitTx val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn(0).sequence, tx.lockTime) + val fundingPubKey = commitments.localParams.fundingPubKey(keyManager) + val channelKeyPath = commitments.localParams.channelKeyPath(keyManager) // this tx has been published by remote, so we need to invert local/remote params - val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, keyManager.paymentPoint(localParams.channelKeyPath).publicKey) + val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, keyManager.paymentPoint(channelKeyPath).publicKey) // now we know what commit number this tx is referring to, we can derive the commitment point from the shachain remotePerCommitmentSecrets.getHash(0xFFFFFFFFFFFFL - txnumber) .map(d => PrivateKey(d)) .flatMap { remotePerCommitmentSecret => val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) - val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) + val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) // we need to use a high fee here for punishment txes because after a delay they can be spent by the counterparty val feeratePerKwPenalty = feeEstimator.getFeeratePerKw(target = 1) 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(channelKeyPath), remotePerCommitmentSecret) 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) 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 f96cb9d62a..ec82b9e66f 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 @@ -16,9 +16,12 @@ package fr.acinq.eclair.crypto +import java.io.ByteArrayInputStream +import java.nio.ByteOrder + import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.DeterministicWallet.ExtendedPublicKey -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, DeterministicWallet} +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, DeterministicWallet, Protocol} import fr.acinq.eclair.ShortChannelId import fr.acinq.eclair.transactions.Transactions.TransactionWithInputInfo import scodec.bits.ByteVector @@ -28,7 +31,7 @@ trait KeyManager { def nodeId: PublicKey - def fundingPublicKey(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPublicKey + def fundingPublicKey(keyPath: DeterministicWallet.KeyPath): ExtendedPublicKey def revocationPoint(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPublicKey @@ -73,5 +76,36 @@ trait KeyManager { */ def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: PrivateKey): ByteVector64 - def signChannelAnnouncement(channelKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64) + /** + * Sign a channel announcement message + * + * @param fundingKeyPath BIP32 path of the funding public key + * @param chainHash chain hash + * @param shortChannelId short channel id + * @param remoteNodeId remote node id + * @param remoteFundingKey remote funding pubkey + * @param features channel features + * @return a (nodeSig, bitcoinSig) pair. nodeSig is the signature of the channel announcement with our node's + * private key, bitcoinSig is the signature of the channel announcement with our funding private key + */ + def signChannelAnnouncement(fundingKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64) + + def signChannelAnnouncement(fundingKey: DeterministicWallet.ExtendedPublicKey, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64) + = signChannelAnnouncement(fundingKey.path, chainHash, shortChannelId, remoteNodeId, remoteFundingKey, features) } + +object KeyManager { + /** + * Create a BIP32 path from a public key. This path will be used to derive channel keys. + * + * @param fundingPubKey funding public key + * @return a BIP32 path + */ + def channelKeyPath(fundingPubKey: PublicKey) : DeterministicWallet.KeyPath = { + val buffer = fundingPubKey.hash160.take(8) + val bis = new ByteArrayInputStream(buffer.toArray) + DeterministicWallet.KeyPath(Seq(Protocol.uint32(bis, ByteOrder.BIG_ENDIAN), Protocol.uint32(bis, ByteOrder.BIG_ENDIAN), Protocol.uint32(bis, ByteOrder.BIG_ENDIAN), Protocol.uint32(bis, ByteOrder.BIG_ENDIAN))) + } + + def channelKeyPath(fundingPubKey: DeterministicWallet.ExtendedPublicKey) : DeterministicWallet.KeyPath = channelKeyPath(fundingPubKey.publicKey) +} \ No newline at end of file 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 2bf80bccc4..f4aaea07d5 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 @@ -137,9 +137,9 @@ class LocalKeyManager(seed: ByteVector, chainHash: ByteVector32) extends KeyMana Transactions.sign(tx, currentKey) } - override def signChannelAnnouncement(channelKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64) = { + override def signChannelAnnouncement(fundingKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64) = { val localNodeSecret = nodeKey.privateKey - val localFundingPrivKey = fundingPrivateKey(channelKeyPath).privateKey + val localFundingPrivKey = privateKeys.get(fundingKeyPath).privateKey Announcements.signChannelAnnouncement(chainHash, shortChannelId, localNodeSecret, remoteNodeId, localFundingPrivKey, remoteFundingKey, features) } } 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 dbc91f208e..6249d3d521 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 @@ -629,17 +629,23 @@ object Peer { // @formatter:on def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: ByteVector, isFunder: Boolean, fundingSatoshis: Long): LocalParams = { - val entropy = new Array[Byte](16) + val entropy = new Array[Byte](8) secureRandom.nextBytes(entropy) val bis = new ByteArrayInputStream(entropy) - val channelKeyPath = DeterministicWallet.KeyPath(Seq(Protocol.uint32(bis, ByteOrder.BIG_ENDIAN), Protocol.uint32(bis, ByteOrder.BIG_ENDIAN), Protocol.uint32(bis, ByteOrder.BIG_ENDIAN), Protocol.uint32(bis, ByteOrder.BIG_ENDIAN))) - makeChannelParams(nodeParams, defaultFinalScriptPubKey, isFunder, fundingSatoshis, channelKeyPath) + // we make sure that funder and fundee key path end differently + val last = isFunder match { + case false => DeterministicWallet.hardened(0) + case true => DeterministicWallet.hardened(1) + } + val fundingKeyPath = DeterministicWallet.KeyPath(Seq(Protocol.uint32(bis, ByteOrder.BIG_ENDIAN), Protocol.uint32(bis, ByteOrder.BIG_ENDIAN), last)) + makeChannelParams(nodeParams, defaultFinalScriptPubKey, isFunder, fundingSatoshis, fundingKeyPath) } - def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: ByteVector, isFunder: Boolean, fundingSatoshis: Long, channelKeyPath: DeterministicWallet.KeyPath): LocalParams = { + def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: ByteVector, isFunder: Boolean, fundingSatoshis: Long, fundingKeyPath: DeterministicWallet.KeyPath): LocalParams = { LocalParams( + version = 1, nodeParams.nodeId, - channelKeyPath, + fundingKeyPath, dustLimitSatoshis = nodeParams.dustLimitSatoshis, maxHtlcValueInFlightMsat = nodeParams.maxHtlcValueInFlightMsat, channelReserveSatoshis = Math.max((nodeParams.reserveToFundingRatio * fundingSatoshis).toLong, nodeParams.dustLimitSatoshis), // BOLT #2: make sure that our reserve is above our dust limit 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 97937c05de..d427c3a25d 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 @@ -58,8 +58,18 @@ object ChannelCodecs extends Logging { fallback = provide(ChannelVersion.STANDARD) ) + // we use the same trick here: un-versioned LocalParams instances start with a node id so the first byte cannot be 0x01 + val localParamsVersionCodec: Codec[Int] = discriminatorWithDefault[Int]( + discriminator = discriminated[Int].by(byte) + .typecase(0x01, int16) + // NB: 0x02 and 0x03 are *reserved* for backward compatibility reasons + , + fallback = provide(0) + ) + val localParamsCodec: Codec[LocalParams] = ( - ("nodeId" | publicKey) :: + ("version" | localParamsVersionCodec) :: + ("nodeId" | publicKey) :: ("channelPath" | keyPathCodec) :: ("dustLimitSatoshis" | uint64overflow) :: ("maxHtlcValueInFlightMsat" | uint64) :: 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 e327d9cad6..c47641f84e 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 @@ -2218,7 +2218,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42, null)) val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures] import initialState.commitments.{localParams, remoteParams} - val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, Alice.keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) + val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, localParams.fundingPubKey(Alice.keyManager).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) // actual test starts here bob2alice.forward(alice) awaitCond({ @@ -2237,7 +2237,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10, null)) val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures] import initialState.commitments.{localParams, remoteParams} - val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, Alice.keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) + val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, localParams.fundingPubKey(Alice.keyManager).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) bob2alice.forward(alice) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement === Some(channelAnn)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index 866a3346f6..aec61e95df 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -20,21 +20,25 @@ import java.util.UUID import akka.actor.Status import akka.testkit.{TestActorRef, TestProbe} -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{ByteVector32, ScriptFlags, Transaction} +import fr.acinq.bitcoin +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.{ByteVector32, OP_0, OP_2, OP_CHECKMULTISIG, OP_PUSHDATA, OutPoint, Satoshi, Script, ScriptFlags, ScriptWitness, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain.{CurrentBlockCount, PublishAsap, WatchConfirmed, WatchEventSpent} import fr.acinq.eclair.channel.Channel.LocalError import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods +import fr.acinq.eclair.crypto.{Generators, KeyManager} import fr.acinq.eclair.payment.CommandBuffer import fr.acinq.eclair.payment.CommandBuffer.CommandSend import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.transactions.Transactions.HtlcSuccessTx +import fr.acinq.eclair.transactions.{Scripts, Transactions} +import fr.acinq.eclair.transactions.Transactions.{ClaimP2WPKHOutputTx, HtlcSuccessTx, InputInfo, TransactionWithInputInfo} import fr.acinq.eclair.wire._ import fr.acinq.eclair.{TestConstants, TestkitBaseClass, randomBytes32} import org.scalatest.Outcome import scala.concurrent.duration._ +import scala.util.Try /** * Created by PM on 05/07/2016. @@ -85,8 +89,12 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val bobCommitments = bob.stateData.asInstanceOf[HasCommitments].commitments val aliceCommitments = alice.stateData.asInstanceOf[HasCommitments].commitments - val bobCurrentPerCommitmentPoint = TestConstants.Bob.keyManager.commitmentPoint(bobCommitments.localParams.channelKeyPath, bobCommitments.localCommit.index) - val aliceCurrentPerCommitmentPoint = TestConstants.Alice.keyManager.commitmentPoint(aliceCommitments.localParams.channelKeyPath, aliceCommitments.localCommit.index) + val bobCurrentPerCommitmentPoint = TestConstants.Bob.keyManager.commitmentPoint( + bobCommitments.localParams.channelKeyPath(TestConstants.Bob.keyManager), + bobCommitments.localCommit.index) + val aliceCurrentPerCommitmentPoint = TestConstants.Alice.keyManager.commitmentPoint( + aliceCommitments.localParams.channelKeyPath(TestConstants.Alice.keyManager), + aliceCommitments.localCommit.index) // a didn't receive any update or sig @@ -169,8 +177,12 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val bobCommitments = bob.stateData.asInstanceOf[HasCommitments].commitments val aliceCommitments = alice.stateData.asInstanceOf[HasCommitments].commitments - val bobCurrentPerCommitmentPoint = TestConstants.Bob.keyManager.commitmentPoint(bobCommitments.localParams.channelKeyPath, bobCommitments.localCommit.index) - val aliceCurrentPerCommitmentPoint = TestConstants.Alice.keyManager.commitmentPoint(aliceCommitments.localParams.channelKeyPath, aliceCommitments.localCommit.index) + val bobCurrentPerCommitmentPoint = TestConstants.Bob.keyManager.commitmentPoint( + bobCommitments.localParams.channelKeyPath(TestConstants.Bob.keyManager), + bobCommitments.localCommit.index) + val aliceCurrentPerCommitmentPoint = TestConstants.Alice.keyManager.commitmentPoint( + aliceCommitments.localParams.channelKeyPath(TestConstants.Alice.keyManager), + aliceCommitments.localCommit.index) // a didn't receive the sig val ab_reestablish = alice2bob.expectMsg(ChannelReestablish(ab_add_0.channelId, 1, 0, Some(PrivateKey(ByteVector32.Zeroes)), Some(aliceCurrentPerCommitmentPoint))) @@ -459,4 +471,86 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { alice2blockchain.expectNoMsg(250 millis) } + test("use funding pubkeys from publish commitment to spend our output") { f => + import f._ + val sender = TestProbe() + + // we start by storing the current state + val oldStateData = alice.stateData + // then we add an htlc and sign it + addHtlc(250000000, alice, bob, alice2bob, bob2alice) + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + // alice will receive neither the revocation nor the commit sig + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.expectMsgType[CommitSig] + + // we simulate a disconnection + sender.send(alice, INPUT_DISCONNECTED) + sender.send(bob, INPUT_DISCONNECTED) + awaitCond(alice.stateName == OFFLINE) + awaitCond(bob.stateName == OFFLINE) + + // then we manually replace alice's state with an older one + alice.setState(OFFLINE, oldStateData) + + // then we reconnect them + sender.send(alice, INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit)) + sender.send(bob, INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit)) + + // peers exchange channel_reestablish messages + alice2bob.expectMsgType[ChannelReestablish] + val ce = bob2alice.expectMsgType[ChannelReestablish] + + // alice then realizes it has an old state... + bob2alice.forward(alice) + // ... and ask bob to publish its current commitment + val error = alice2bob.expectMsgType[Error] + assert(new String(error.data.toArray) === PleasePublishYourCommitment(channelId(alice)).getMessage) + + // alice now waits for bob to publish its commitment + awaitCond(alice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) + + // bob is nice and publishes its commitment + val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + + // actual tests starts here: let's see what we can do with Bob's commit tx + sender.send(alice, WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx)) + + // from Bob's commit tx we can extract both funding public keys + val OP_2 :: OP_PUSHDATA(pub1, _) :: OP_PUSHDATA(pub2, _) :: OP_2 :: OP_CHECKMULTISIG :: Nil = Script.parse(bobCommitTx.txIn(0).witness.stack.last) + // from Bob's commit tx we can also extract our p2wpkh output + val ourOutput = bobCommitTx.txOut.find(_.publicKeyScript.length == 22).get + + val OP_0 :: OP_PUSHDATA(pubKeyHash, _) :: Nil = Script.parse(ourOutput.publicKeyScript) + + val keyManager = TestConstants.Alice.nodeParams.keyManager + + // find our funding pub key + val fundingPubKey = Seq(PublicKey(pub1), PublicKey(pub2)).find { + pub => + val channelKeyPath = KeyManager.channelKeyPath(pub) + val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, ce.myCurrentPerCommitmentPoint.get) + localPubkey.hash160 == pubKeyHash + } get + + // compute our to-remote pubkey + val channelKeyPath = KeyManager.channelKeyPath(fundingPubKey) + val ourToRemotePubKey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, ce.myCurrentPerCommitmentPoint.get) + + // spend our output + val tx = Transaction(version = 2, + txIn = TxIn(OutPoint(bobCommitTx, bobCommitTx.txOut.indexOf(ourOutput)), sequence = TxIn.SEQUENCE_FINAL, signatureScript = Nil) :: Nil, + txOut = TxOut(Satoshi(1000), Script.pay2pkh(fr.acinq.eclair.randomKey.publicKey)) :: Nil, + lockTime = 0) + + val sig = keyManager.sign( + ClaimP2WPKHOutputTx(InputInfo(OutPoint(bobCommitTx, bobCommitTx.txOut.indexOf(ourOutput)), ourOutput, Script.pay2pkh(ourToRemotePubKey)), tx), + keyManager.paymentPoint(channelKeyPath), + ce.myCurrentPerCommitmentPoint.get) + val tx1 = tx.updateWitness(0, ScriptWitness(Scripts.der(sig) :: ourToRemotePubKey.value :: Nil)) + Transaction.correctlySpends(tx1, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/LocalParamsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/LocalParamsSpec.scala new file mode 100644 index 0000000000..6ec144753c --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/LocalParamsSpec.scala @@ -0,0 +1,35 @@ +/* + * Copyright 2019 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fr.acinq.eclair.transactions + +import fr.acinq.bitcoin.{Block, ByteVector32, DeterministicWallet} +import fr.acinq.eclair.crypto.LocalKeyManager +import fr.acinq.eclair.wire.ChannelCodecs +import org.scalatest.FunSuite +import scodec.bits.{BitVector, ByteVector} + +class LocalParamsSpec extends FunSuite { + test("read legacy data correctly") { + val seed = ByteVector32(ByteVector.fill(32)(2)) + val keyManager = new LocalKeyManager(seed, Block.RegtestGenesisBlock.hash) + // read version 0 local params from data produced with an older version of eclair + val localParams = ChannelCodecs.localParamsCodec.decode(BitVector.fromValidHex("0x03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d00004e3fbd1afb75d37604f7de0755c7e3e01000000000000044c0000000008f0d18000000000000027100000000000000000009000648000000000008000")).require.value + val channelKeyPath = localParams.channelKeyPath(keyManager) + val fundingPubKey = localParams.fundingPubKey(keyManager) + assert(channelKeyPath == DeterministicWallet.KeyPath("m/1677447599'/928855904'/1333649525/1551777281")) + assert(fundingPubKey.toString == "ExtendedPublicKey(ByteVector(33 bytes, 0x02f8eea36e21551d30ce9329538c8652b5ecc1f8dc5b84ab52a4eb5e95759cdbc8),a1e0ccf8ac45652752ebfd578c3b7a6adcbdb693fbc0ff3f1d1797db4af9f8bd,7,m/46'/1'/1677447599'/928855904'/1333649525/1551777281/0',1132704962)") + } +} 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 81f6e6e0d6..7b142c620b 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 @@ -82,8 +82,9 @@ class ChannelCodecsSpec extends FunSuite { test("encode/decode localparams") { val o = LocalParams( + version = 1, nodeId = randomKey.publicKey, - channelKeyPath = DeterministicWallet.KeyPath(Seq(42L)), + fundingKeyPath = DeterministicWallet.KeyPath(Seq(42L)), dustLimitSatoshis = Random.nextInt(Int.MaxValue), maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)), channelReserveSatoshis = Random.nextInt(Int.MaxValue), @@ -301,8 +302,10 @@ class ChannelCodecsSpec extends FunSuite { // this test makes sure that we actually produce the same objects than previous versions of eclair val refs = Map( - hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B134000456E4167E3C0EB8C856C79CA31C97C0AA0000000000000222000000012A05F2000000000000028F5C000000000000000102D0001E000BD48A2402E80B723C42EE3E42938866EC6686ABB7ABF64380000000C501A7F2974C5074E9E10DBB3F0D9B8C40932EC63ABC610FAD7EB6B21C6D081A459B000000000000011E80000001EEFFFE5C00000000000147AE00000000000001F403F000F18146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB20131AD64F76FAF90CD7DE26892F1BDAB82FB9E02EF6538D82FF4204B5348F02AE081A5388E9474769D69C4F60A763AE0CCDB5228A06281DE64408871A927297FDFD8818B6383985ABD4F0AC22E73791CF3A4D63C592FA2648242D34B8334B1539E823381BB1F1404C37D9C2318F5FC6B1BF7ECF5E6835B779E3BE09BADCF6DF1F51DCFBC80000000C0808000000000000EFD80000000007F00000000061A0A4880000001EDE5F3C3801203B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E808000000015FFFFFF800000000011001029DFB814F6502A68D6F83B6049E3D2948A2080084083750626532FDB437169C20023A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A95700AD0100000000008083B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E80800000001961B4C001618F8180000000001100102E648BA30998A28C02C2DFD9DDCD0E0BA064DA199C55186485AFAB296B94E704426FFE00000000000B000A67D9B9FAADB91650E0146B1F742E5C16006708890200239822011026A6925C659D006FEB42D639F1E42DD13224EE49AA34E71B612CF96DB66A8CD4011032C22F653C54CC5E41098227427650644266D80DED45B7387AE0FFC10E529C4680A418228110807CB47D9C1A14CB832FB361C398EA672C9542F34A90BAD4288FA6AC5FC9E9845C01101CF71CAE9252D389135D8C606225DCF1E0333CCDF1FAE84B74FC5D3D440C25F880A3A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A9573D7C531000000000000000000F3180000000007F00000001EDE5F3C380000000061A0A48D64CA627B243AD5915A2E5D0BAD026762028DDF3304992B83A26D6C11735FC5F01ED56D769BDE7F6A068AF1A4BCFDF950321F3A4744B01B1DDC7498677F112AE1A80000000000000000000000000000000000000658000000000000819800040D37301C10C9419287E9A3B704EB6D7F45CC145DD77DCE8A63B0A47C8AB67467D800901DCE3C8B05A891E56F2BAF1B82405ABD8640B759AEEBD939B976D42C311758F40400000000AFFFFFFC00000000008800814EFDC0A7B2815346B7C1DB024F1E94A451040042041BA83132997EDA1B8B4E10011D48840A33BCFBC0833F6825A4ABF0A78E2B11D5B2981CD958EA4C881204247273416D90840D9834A03892A6C59DCA9B990600A5C65882972A8A7AF7E0CE7975C031846AE78D4AB8002000EC0003FFFFFFFF86801076D98A575A4CDFD0E3F44D1BB3CD3BBAF3BD04C38FED439ED90D88DF932A9296801A80007FFFFFFFF4008136A9D5896669E8724C5120FB6B36C241EF3CEF68AE0316161F04A9EE3EAFF36000FC0003FFFFFFFF86780106E4B5CC4155733A2427082907338051A5DA1E7CA6432840A5528ECAFFA3FB628801B80007FFFFFFFF10020CA4E125E9126107745D4354D4187ABCDE323117857A1DCEB7CCF60B2AAFA80C6003A0000FFFFFFFFE1C0080981575FD981A73A848CC0243CB467BF451F6811DAF4D71CAD8CE8B1E96DB190C01000003FFFFFFFF867400814C747E0FD8290BE8A3B8B3F73015A261479A71780CD3A0A9270234E4B394409C00D80003FFFFFFFF90020E1B9C9B10A97F15F5E1BB27FC8AC670DF8DADEAE4EDFAFB23BDD0AC705FDF51600340000FFFFFFFFF0020AD2581F3494A17B0BE3F63516D53F028A204FD3156D8B21AA4E57A8738D2062080007FFFFFFFF0CE83B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E0B8C1E00000B8000FA46CC2C7E9AB4A37C64216CD65C944E6D73998419D1A1AD2827AB6BC85B32280230764E374064EC82A3751E789607E23BEAE93FB0EDDD5E7FA803767079662E80EAEF384E2AFCB68049D9DC246119E77BD2ED4112330760CAB6CD3671CFCE006C584B9C95E0B554261E00154D40806EA694F44751B328A9291BAD124EFD5664280936EC92D27B242737E7E3E83B4704BA367B7DA5108F2F6EDFB1C38EE721A369E77EED71B12090BAEAAAC322C1457E31AB0C4DE5D9351943F10FD747742616A1AABD09F680B37D4105A8872695EE9B97FAB8985FAA9D747D45046229BF265CEEB300A40FE23040C5F335E0515496C58EE47418B72331FCC6F47A31A9B33B8E000008692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002069FCA5D3141D3A78436ECFC366E31024CBB18EAF1843EB5FADAC871B42069166C0726710955E3AD621072FCBDFCB90D79E5B1951A5EE01DB533B72429F84E2562680519DE7DE0419FB412D255F853C71588EAD94C0E6CAC7526440902123939A0B6C806CC1A501C495362CEE54DCC830052E32C414B95453D7BF0673CBAE018C23573C69C694A8F88483050257A7366B838489731E5776B6FA0F02573401176D3E7FAEEF11E95A671420586631255F51A0EC2CF4D4D9F69D587712070FE1FB9316B71868692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002BA11BBBA0202012000000000000007D0000007D0000000C800000007CFFFF83000" -> """{"commitments":{"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","channelKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimitSatoshis":546,"maxHtlcValueInFlightMsat":5000000000,"channelReserveSatoshis":167772,"htlcMinimumMsat":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","globalFeatures":"","localFeatures":"8a"},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimitSatoshis":573,"maxHtlcValueInFlightMsat":16609443000,"channelReserveSatoshis":167772,"htlcMinimumMsat":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","globalFeatures":"","localFeatures":"81"},"channelFlags":1,"localCommit":{"index":7675,"spec":{"htlcs":[],"feeratePerKw":254,"toLocalMsat":204739729,"toRemoteMsat":16572475271},"publishableTxs":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"0200000000010107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce1112040047304402204d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a8022065845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d01483045022100f968fb38342997065f66c38731d4ce592a85e6952175a8511f4d58bf93d308b8022039ee395d24a5a71226bb18c0c44bb9e3c066799be3f5d096e9f8ba7a88184bf101475221028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b642103660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e352ae7af8a620"},"htlcTxsAndSigs":[]}},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"feeratePerKw":254,"toLocalMsat":16572475271,"toRemoteMsat":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null,"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c"},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":"","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3"},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":1560862173,"messageFlags":1,"channelFlags":1,"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000}}""", - hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B1340004D443ECE9D9C43A11A19B554BAAA6AD150000000000000222000000003B9ACA0000000000000249F000000000000000010090001E800BD48A22F4C80A42CC8BB29A764DBAEFC95674931FBE9A4380000000C50134D4A745996002F219B5FDBA1E045374DF589ECA06ABE23CECAE47343E65EDCF800000000000011E80000001BA90824000000000000124F800000000000001F4038500F1810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E2266201E8BFEEEEED725775B8116F6F82CF8E87835A5B45B184E56F272AD70D6078118601E06212B8C8F2E25B73EE7974FDCDF007E389B437BBFE238CCC3F3BF7121B6C5E81AA8589D21E9584B24A11F3ABBA5DAD48D121DD63C57A69CD767119C05DA159CB81A649D8CC0E136EB8DFBD2268B69DCA86F8CE4A604235A03D9D37AE7B07FC563F80000000C080800000000000271C000000000177000000002808B14600000001970039BA00123767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB08800000000015E070F20000000000110010584241B5FB364208F6E64A80D1166DAD866186B10C015ED0283FF1C308C2105A0023A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA95700AD81000000000080B767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB0880000000003E7AEDC0011ABE8A00000000001100101A9CE4B6AEF469590BC7BCC51DCEEAE9C86084055A63CC01E443C733FBE400B9B5B16800000000000B000A5E5700106D1A7097E4DE87EBAF1F8F2773842FA482002418228110805E84989A81F51ABD9D11889AE43E68FAD93659DEC019F1B8C0ADBF15A57B118B81101DCC1256F9306439AD3962C043FC47A5179CAAA001CCB23342BE0E8D92E4022780A4182281108074F306DA3751B84EC5FFB155BDCA7B8E02208BBDBC8D4F3327ABA557BF27CD1701102EF4AC8CC92F469DA9642D4D4162BC545F8B34ADE15B7D6F99808AA22B086B0180A3A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA9576F8099900000000000000000271C00000000017700000001970039BA000000002808B14648CE00AE97051EE10A3C361263F81A98165CE4AA7BA076933D4266E533585F24815C15DEACF0691332B38ECF23EC39982C5C978C748374A01BA9B30D501EE4F26E8000000000000000000000000000000000001224000000000000004B800040A911C460F1467952E3B99BED072F81BFB4454FF389636DCB399FE6A78113C28580091BB3F87A7806AF4FEF920BBF794391A1ECFC7D7632E98245D2BAF3870050558440000000000AF0387900000000000880082C2120DAFD9B21047B732540688B36D6C330C3588600AF68141FF8E18461082D0011D488408570D7C50EB7AB7C042AF13382F8C8DD83E6A7121A5E2DD8B4C73F2C407113310840EF456FD0886E454A6C5CF4F7B0B5D742CC143E47C157EF87E03434BEAB81337ED4AB8001C00F40003FFFFFFFEC7200403248A1D44DFA3AC9EC237D452C936400CAA86E9517CCCF2A8F77B7493CD70B6A00780001FFFFFFFF63A0041826829646B907A97FBD1455EA8673A12B8E7AA6EA790F7802E955CE3B69DE57E006E0001FFFFFFFF640081E51EB1F91218821E680B50E4B22DF8B094385BD33ACAE36BFC9E8C2F5AD2DA5400EC0003FFFFFFFEC7801047C26AD5435658D063EBCF73A5D0EEFE73ED6B73426246E8DFB3A21D1C4C7465001900007FFFFFFFE0040B115AC58BAAA900195893EA3B2AB408D2AD348AD047E3B6CB15E599625E38608006A0001FFFFFFFF7002033C39A21A38BB61F6FB33623771A9356D8885B7C12C939C770C939EF826286C200360000FFFFFFFFB4008104EF4271064A0973B053727C3E67352D00E25CAEED944F50782449CEAE8F50960001FFFFFFFF6390DD9FC3D3C0357A7F7C905DFBCA1C8D0F67E3EBB1974C122E95D79C380282AC222B21FA0007920001295AA1FB77029F7620A90EF7AE6A6CD31E4588B93264A7ADB76152D535C52E90B9E1B7C2376DABA316A6290F1A9730D4E5E44D0B1CB0EE6A795702E6A6BCDFCDA1A4BFEBFC134AB8847A5187ECE761D75D3CCB904274875680F51984800000000AC87E8001E480002E884D2A8080804800000000000001F4000001F40000003200000001BF08EB000" -> """{"commitments":{"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","channelKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimitSatoshis":546,"maxHtlcValueInFlightMsat":1000000000,"channelReserveSatoshis":150000,"htlcMinimumMsat":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","globalFeatures":"","localFeatures":"8a"},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimitSatoshis":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserveSatoshis":150000,"htlcMinimumMsat":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","globalFeatures":"","localFeatures":"81"},"channelFlags":1,"localCommit":{"index":20024,"spec":{"htlcs":[],"feeratePerKw":750,"toLocalMsat":1343316620,"toRemoteMsat":13656683380},"publishableTxs":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"020000000001016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f490400483045022100bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af6231702203b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f01483045022100e9e60db46ea3709d8bff62ab7b94f71c0441177b791a9e664f574aaf7e4f9a2e02205de95919925e8d3b52c85a9a82c578a8bf16695bc2b6fadf330115445610d603014752210215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc42103bd15bf4221b91529b173d3dec2d75d0b3050f91f055fbe1f80d0d2faae04cdfb52aedf013320"},"htlcTxsAndSigs":[]}},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"feeratePerKw":750,"toLocalMsat":13656683380,"toRemoteMsat":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null,"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611"},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":1561369173,"messageFlags":1,"channelFlags":1,"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000}}""" + hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B134000456E4167E3C0EB8C856C79CA31C97C0AA0000000000000222000000012A05F2000000000000028F5C000000000000000102D0001E000BD48A2402E80B723C42EE3E42938866EC6686ABB7ABF64380000000C501A7F2974C5074E9E10DBB3F0D9B8C40932EC63ABC610FAD7EB6B21C6D081A459B000000000000011E80000001EEFFFE5C00000000000147AE00000000000001F403F000F18146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB20131AD64F76FAF90CD7DE26892F1BDAB82FB9E02EF6538D82FF4204B5348F02AE081A5388E9474769D69C4F60A763AE0CCDB5228A06281DE64408871A927297FDFD8818B6383985ABD4F0AC22E73791CF3A4D63C592FA2648242D34B8334B1539E823381BB1F1404C37D9C2318F5FC6B1BF7ECF5E6835B779E3BE09BADCF6DF1F51DCFBC80000000C0808000000000000EFD80000000007F00000000061A0A4880000001EDE5F3C3801203B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E808000000015FFFFFF800000000011001029DFB814F6502A68D6F83B6049E3D2948A2080084083750626532FDB437169C20023A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A95700AD0100000000008083B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E80800000001961B4C001618F8180000000001100102E648BA30998A28C02C2DFD9DDCD0E0BA064DA199C55186485AFAB296B94E704426FFE00000000000B000A67D9B9FAADB91650E0146B1F742E5C16006708890200239822011026A6925C659D006FEB42D639F1E42DD13224EE49AA34E71B612CF96DB66A8CD4011032C22F653C54CC5E41098227427650644266D80DED45B7387AE0FFC10E529C4680A418228110807CB47D9C1A14CB832FB361C398EA672C9542F34A90BAD4288FA6AC5FC9E9845C01101CF71CAE9252D389135D8C606225DCF1E0333CCDF1FAE84B74FC5D3D440C25F880A3A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A9573D7C531000000000000000000F3180000000007F00000001EDE5F3C380000000061A0A48D64CA627B243AD5915A2E5D0BAD026762028DDF3304992B83A26D6C11735FC5F01ED56D769BDE7F6A068AF1A4BCFDF950321F3A4744B01B1DDC7498677F112AE1A80000000000000000000000000000000000000658000000000000819800040D37301C10C9419287E9A3B704EB6D7F45CC145DD77DCE8A63B0A47C8AB67467D800901DCE3C8B05A891E56F2BAF1B82405ABD8640B759AEEBD939B976D42C311758F40400000000AFFFFFFC00000000008800814EFDC0A7B2815346B7C1DB024F1E94A451040042041BA83132997EDA1B8B4E10011D48840A33BCFBC0833F6825A4ABF0A78E2B11D5B2981CD958EA4C881204247273416D90840D9834A03892A6C59DCA9B990600A5C65882972A8A7AF7E0CE7975C031846AE78D4AB8002000EC0003FFFFFFFF86801076D98A575A4CDFD0E3F44D1BB3CD3BBAF3BD04C38FED439ED90D88DF932A9296801A80007FFFFFFFF4008136A9D5896669E8724C5120FB6B36C241EF3CEF68AE0316161F04A9EE3EAFF36000FC0003FFFFFFFF86780106E4B5CC4155733A2427082907338051A5DA1E7CA6432840A5528ECAFFA3FB628801B80007FFFFFFFF10020CA4E125E9126107745D4354D4187ABCDE323117857A1DCEB7CCF60B2AAFA80C6003A0000FFFFFFFFE1C0080981575FD981A73A848CC0243CB467BF451F6811DAF4D71CAD8CE8B1E96DB190C01000003FFFFFFFF867400814C747E0FD8290BE8A3B8B3F73015A261479A71780CD3A0A9270234E4B394409C00D80003FFFFFFFF90020E1B9C9B10A97F15F5E1BB27FC8AC670DF8DADEAE4EDFAFB23BDD0AC705FDF51600340000FFFFFFFFF0020AD2581F3494A17B0BE3F63516D53F028A204FD3156D8B21AA4E57A8738D2062080007FFFFFFFF0CE83B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E0B8C1E00000B8000FA46CC2C7E9AB4A37C64216CD65C944E6D73998419D1A1AD2827AB6BC85B32280230764E374064EC82A3751E789607E23BEAE93FB0EDDD5E7FA803767079662E80EAEF384E2AFCB68049D9DC246119E77BD2ED4112330760CAB6CD3671CFCE006C584B9C95E0B554261E00154D40806EA694F44751B328A9291BAD124EFD5664280936EC92D27B242737E7E3E83B4704BA367B7DA5108F2F6EDFB1C38EE721A369E77EED71B12090BAEAAAC322C1457E31AB0C4DE5D9351943F10FD747742616A1AABD09F680B37D4105A8872695EE9B97FAB8985FAA9D747D45046229BF265CEEB300A40FE23040C5F335E0515496C58EE47418B72331FCC6F47A31A9B33B8E000008692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002069FCA5D3141D3A78436ECFC366E31024CBB18EAF1843EB5FADAC871B42069166C0726710955E3AD621072FCBDFCB90D79E5B1951A5EE01DB533B72429F84E2562680519DE7DE0419FB412D255F853C71588EAD94C0E6CAC7526440902123939A0B6C806CC1A501C495362CEE54DCC830052E32C414B95453D7BF0673CBAE018C23573C69C694A8F88483050257A7366B838489731E5776B6FA0F02573401176D3E7FAEEF11E95A671420586631255F51A0EC2CF4D4D9F69D587712070FE1FB9316B71868692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002BA11BBBA0202012000000000000007D0000007D0000000C800000007CFFFF83000" + -> """{"commitments":{"localParams":{"version":0,"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","channelKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimitSatoshis":546,"maxHtlcValueInFlightMsat":5000000000,"channelReserveSatoshis":167772,"htlcMinimumMsat":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","globalFeatures":"","localFeatures":"8a"},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimitSatoshis":573,"maxHtlcValueInFlightMsat":16609443000,"channelReserveSatoshis":167772,"htlcMinimumMsat":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","globalFeatures":"","localFeatures":"81"},"channelFlags":1,"localCommit":{"index":7675,"spec":{"htlcs":[],"feeratePerKw":254,"toLocalMsat":204739729,"toRemoteMsat":16572475271},"publishableTxs":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"0200000000010107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce1112040047304402204d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a8022065845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d01483045022100f968fb38342997065f66c38731d4ce592a85e6952175a8511f4d58bf93d308b8022039ee395d24a5a71226bb18c0c44bb9e3c066799be3f5d096e9f8ba7a88184bf101475221028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b642103660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e352ae7af8a620"},"htlcTxsAndSigs":[]}},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"feeratePerKw":254,"toLocalMsat":16572475271,"toRemoteMsat":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null,"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c"},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":"","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3"},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":1560862173,"messageFlags":1,"channelFlags":1,"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000}}""", + hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B1340004D443ECE9D9C43A11A19B554BAAA6AD150000000000000222000000003B9ACA0000000000000249F000000000000000010090001E800BD48A22F4C80A42CC8BB29A764DBAEFC95674931FBE9A4380000000C50134D4A745996002F219B5FDBA1E045374DF589ECA06ABE23CECAE47343E65EDCF800000000000011E80000001BA90824000000000000124F800000000000001F4038500F1810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E2266201E8BFEEEEED725775B8116F6F82CF8E87835A5B45B184E56F272AD70D6078118601E06212B8C8F2E25B73EE7974FDCDF007E389B437BBFE238CCC3F3BF7121B6C5E81AA8589D21E9584B24A11F3ABBA5DAD48D121DD63C57A69CD767119C05DA159CB81A649D8CC0E136EB8DFBD2268B69DCA86F8CE4A604235A03D9D37AE7B07FC563F80000000C080800000000000271C000000000177000000002808B14600000001970039BA00123767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB08800000000015E070F20000000000110010584241B5FB364208F6E64A80D1166DAD866186B10C015ED0283FF1C308C2105A0023A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA95700AD81000000000080B767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB0880000000003E7AEDC0011ABE8A00000000001100101A9CE4B6AEF469590BC7BCC51DCEEAE9C86084055A63CC01E443C733FBE400B9B5B16800000000000B000A5E5700106D1A7097E4DE87EBAF1F8F2773842FA482002418228110805E84989A81F51ABD9D11889AE43E68FAD93659DEC019F1B8C0ADBF15A57B118B81101DCC1256F9306439AD3962C043FC47A5179CAAA001CCB23342BE0E8D92E4022780A4182281108074F306DA3751B84EC5FFB155BDCA7B8E02208BBDBC8D4F3327ABA557BF27CD1701102EF4AC8CC92F469DA9642D4D4162BC545F8B34ADE15B7D6F99808AA22B086B0180A3A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA9576F8099900000000000000000271C00000000017700000001970039BA000000002808B14648CE00AE97051EE10A3C361263F81A98165CE4AA7BA076933D4266E533585F24815C15DEACF0691332B38ECF23EC39982C5C978C748374A01BA9B30D501EE4F26E8000000000000000000000000000000000001224000000000000004B800040A911C460F1467952E3B99BED072F81BFB4454FF389636DCB399FE6A78113C28580091BB3F87A7806AF4FEF920BBF794391A1ECFC7D7632E98245D2BAF3870050558440000000000AF0387900000000000880082C2120DAFD9B21047B732540688B36D6C330C3588600AF68141FF8E18461082D0011D488408570D7C50EB7AB7C042AF13382F8C8DD83E6A7121A5E2DD8B4C73F2C407113310840EF456FD0886E454A6C5CF4F7B0B5D742CC143E47C157EF87E03434BEAB81337ED4AB8001C00F40003FFFFFFFEC7200403248A1D44DFA3AC9EC237D452C936400CAA86E9517CCCF2A8F77B7493CD70B6A00780001FFFFFFFF63A0041826829646B907A97FBD1455EA8673A12B8E7AA6EA790F7802E955CE3B69DE57E006E0001FFFFFFFF640081E51EB1F91218821E680B50E4B22DF8B094385BD33ACAE36BFC9E8C2F5AD2DA5400EC0003FFFFFFFEC7801047C26AD5435658D063EBCF73A5D0EEFE73ED6B73426246E8DFB3A21D1C4C7465001900007FFFFFFFE0040B115AC58BAAA900195893EA3B2AB408D2AD348AD047E3B6CB15E599625E38608006A0001FFFFFFFF7002033C39A21A38BB61F6FB33623771A9356D8885B7C12C939C770C939EF826286C200360000FFFFFFFFB4008104EF4271064A0973B053727C3E67352D00E25CAEED944F50782449CEAE8F50960001FFFFFFFF6390DD9FC3D3C0357A7F7C905DFBCA1C8D0F67E3EBB1974C122E95D79C380282AC222B21FA0007920001295AA1FB77029F7620A90EF7AE6A6CD31E4588B93264A7ADB76152D535C52E90B9E1B7C2376DABA316A6290F1A9730D4E5E44D0B1CB0EE6A795702E6A6BCDFCDA1A4BFEBFC134AB8847A5187ECE761D75D3CCB904274875680F51984800000000AC87E8001E480002E884D2A8080804800000000000001F4000001F40000003200000001BF08EB000" + -> """{"commitments":{"localParams":{"version":0,"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","channelKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimitSatoshis":546,"maxHtlcValueInFlightMsat":1000000000,"channelReserveSatoshis":150000,"htlcMinimumMsat":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","globalFeatures":"","localFeatures":"8a"},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimitSatoshis":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserveSatoshis":150000,"htlcMinimumMsat":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","globalFeatures":"","localFeatures":"81"},"channelFlags":1,"localCommit":{"index":20024,"spec":{"htlcs":[],"feeratePerKw":750,"toLocalMsat":1343316620,"toRemoteMsat":13656683380},"publishableTxs":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"020000000001016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f490400483045022100bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af6231702203b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f01483045022100e9e60db46ea3709d8bff62ab7b94f71c0441177b791a9e664f574aaf7e4f9a2e02205de95919925e8d3b52c85a9a82c578a8bf16695bc2b6fadf330115445610d603014752210215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc42103bd15bf4221b91529b173d3dec2d75d0b3050f91f055fbe1f80d0d2faae04cdfb52aedf013320"},"htlcTxsAndSigs":[]}},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"feeratePerKw":750,"toLocalMsat":13656683380,"toRemoteMsat":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null,"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611"},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":1561369173,"messageFlags":1,"channelFlags":1,"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000}}""".stripMargin ) refs.foreach { case (oldbin, refjson) => @@ -313,8 +316,8 @@ class ChannelCodecsSpec extends FunSuite { // and we decode with the new codec val newnormal = stateDataCodec.decode(newbin.bits).require.value // finally we check that the actual data is the same as before (we just remove the new json field) - val oldjson = Serialization.write(oldnormal)(JsonSupport.formats).replace(""","unknownFields":""""", "").replace(""""channelVersion":"00000000000000000000000000000000",""", "") - val newjson = Serialization.write(newnormal)(JsonSupport.formats).replace(""","unknownFields":""""", "").replace(""""channelVersion":"00000000000000000000000000000000",""", "") + val oldjson = Serialization.write(oldnormal)(JsonSupport.formats).replace(""","unknownFields":""""", "").replace(""""channelVersion":"00000000000000000000000000000000",""", "").replace("fundingKeyPath", "channelKeyPath") + val newjson = Serialization.write(newnormal)(JsonSupport.formats).replace(""","unknownFields":""""", "").replace(""""channelVersion":"00000000000000000000000000000000",""", "").replace("fundingKeyPath", "channelKeyPath") assert(oldjson === refjson) assert(newjson === refjson) } @@ -326,8 +329,9 @@ class ChannelCodecsSpec extends FunSuite { object ChannelCodecsSpec { val keyManager = new LocalKeyManager(ByteVector32(ByteVector.fill(32)(1)), Block.RegtestGenesisBlock.hash) val localParams = LocalParams( + version = 1, keyManager.nodeId, - channelKeyPath = DeterministicWallet.KeyPath(Seq(42L)), + fundingKeyPath = DeterministicWallet.KeyPath(Seq(42L)), dustLimitSatoshis = Satoshi(546).toLong, maxHtlcValueInFlightMsat = UInt64(50000000), channelReserveSatoshis = 10000, @@ -373,7 +377,7 @@ object ChannelCodecsSpec { val fundingTx = Transaction.read("0200000001adbb20ea41a8423ea937e76e8151636bf6093b70eaff942930d20576600521fd000000006b48304502210090587b6201e166ad6af0227d3036a9454223d49a1f11839c1a362184340ef0240220577f7cd5cca78719405cbf1de7414ac027f0239ef6e214c90fcaab0454d84b3b012103535b32d5eb0a6ed0982a0479bbadc9868d9836f6ba94dd5a63be16d875069184ffffffff028096980000000000220020c015c4a6be010e21657068fc2e6a9d02b27ebe4d490a25846f7237f104d1a3cd20256d29010000001600143ca33c2e4446f4a305f23c80df8ad1afdcf652f900000000") val fundingAmount = fundingTx.txOut(0).amount - val commitmentInput = Funding.makeFundingInputInfo(fundingTx.hash, 0, fundingAmount, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey) + val commitmentInput = Funding.makeFundingInputInfo(fundingTx.hash, 0, fundingAmount, localParams.fundingPubKey(keyManager).publicKey, remoteParams.fundingPubKey) 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"), PrivateKey(ByteVector.fill(32)(4)).publicKey) From 84c5cc6e65573ca6aa16f4d8fc7869bfa3dd88ea Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 12 Aug 2019 11:17:03 +0200 Subject: [PATCH 02/11] Fix failing test --- .../src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala | 2 ++ 1 file changed, 2 insertions(+) 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 cf3ebbd9df..85a5fe192f 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 @@ -323,6 +323,7 @@ class ChannelCodecsSpec extends FunSuite { .replace(""""toLocal"""", """"toLocalMsat"""") .replace(""""toRemote"""", """"toRemoteMsat"""") .replace("fundingKeyPath", "channelKeyPath") + .replace(""""version":0,""", "") val newjson = Serialization.write(newnormal)(JsonSupport.formats) .replace(""","unknownFields":""""", "") @@ -333,6 +334,7 @@ class ChannelCodecsSpec extends FunSuite { .replace(""""toLocal"""", """"toLocalMsat"""") .replace(""""toRemote"""", """"toRemoteMsat"""") .replace("fundingKeyPath", "channelKeyPath") + .replace(""""version":0,""", "") assert(oldjson === refjson) assert(newjson === refjson) From 5adc4e260937a9bbf1146956632dde12e3af5654 Mon Sep 17 00:00:00 2001 From: sstone Date: Thu, 29 Aug 2019 14:06:25 +0200 Subject: [PATCH 03/11] Add rationale for new channel derivation scheme --- .../main/scala/fr/acinq/eclair/crypto/KeyManager.scala | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 ec82b9e66f..086a9a831b 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 @@ -96,7 +96,11 @@ trait KeyManager { object KeyManager { /** - * Create a BIP32 path from a public key. This path will be used to derive channel keys. + * Create a BIP32 path from a public key. This path will be used to derive channel keys. + * Having channel keys derived from the funding public keys makes it very easy to retrieve your funds when've you've lost your data: + * - connect to your peer and use DLP to get them to publish their remote commit tx + * - retrieve the commit tx from the bitcoin network, extract your funding pubkey from its witness data + * - recompute your channel keys and spend your output * * @param fundingPubKey funding public key * @return a BIP32 path @@ -108,4 +112,4 @@ object KeyManager { } def channelKeyPath(fundingPubKey: DeterministicWallet.ExtendedPublicKey) : DeterministicWallet.KeyPath = channelKeyPath(fundingPubKey.publicKey) -} \ No newline at end of file +} From af4d255290db2209d52b569bfca2dad91c15e67b Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 9 Sep 2019 15:47:29 +0200 Subject: [PATCH 04/11] Address review comments --- .../fr/acinq/eclair/channel/Channel.scala | 26 ++++++++----------- .../acinq/eclair/channel/ChannelTypes.scala | 7 ++--- .../fr/acinq/eclair/channel/Commitments.scala | 10 +++---- .../fr/acinq/eclair/channel/Helpers.scala | 17 ++++++------ .../main/scala/fr/acinq/eclair/io/Peer.scala | 14 +++------- .../channel/states/e/NormalStateSpec.scala | 4 +-- .../eclair/transactions/LocalParamsSpec.scala | 2 +- .../acinq/eclair/wire/ChannelCodecsSpec.scala | 4 +-- 8 files changed, 32 insertions(+), 52 deletions(-) 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 56f441a1d4..2637d9f77e 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 @@ -24,7 +24,7 @@ import fr.acinq.bitcoin.{ByteVector32, OutPoint, Satoshi, Script, ScriptFlags, T import fr.acinq.eclair._ import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel.Helpers.{Closing, Funding} -import fr.acinq.eclair.crypto.{KeyManager, ShaChain, Sphinx} +import fr.acinq.eclair.crypto.{ShaChain, Sphinx} import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Announcements @@ -148,7 +148,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, remote, _, channelFlags, _), Nothing) => context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, isFunder = true, temporaryChannelId, initialFeeratePerKw, Some(fundingTxFeeratePerKw))) forwarder ! remote - val fundingPubKey = localParams.fundingPubKey(nodeParams.keyManager) + val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey val channelKeyPath = localParams.channelKeyPath(nodeParams.keyManager) val open = OpenChannel(nodeParams.chainHash, temporaryChannelId = temporaryChannelId, @@ -161,7 +161,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId feeratePerKw = initialFeeratePerKw, toSelfDelay = localParams.toSelfDelay, maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, - fundingPubkey = fundingPubKey.publicKey, + fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey, revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, paymentBasepoint = keyManager.paymentPoint(channelKeyPath).publicKey, delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, @@ -273,7 +273,6 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Failure(t) => handleLocalError(t, d, Some(open)) case Success(_) => context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, isFunder = false, open.temporaryChannelId, open.feeratePerKw, None)) - val fundingPubKey = localParams.fundingPubKey(nodeParams.keyManager) val channelKeyPath = localParams.channelKeyPath(nodeParams.keyManager) // TODO: maybe also check uniqueness of temporary channel id val minimumDepth = nodeParams.minDepthBlocks @@ -285,7 +284,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId htlcMinimumMsat = localParams.htlcMinimum, toSelfDelay = localParams.toSelfDelay, maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, - fundingPubkey = fundingPubKey.publicKey, + fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey, revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, paymentBasepoint = keyManager.paymentPoint(channelKeyPath).publicKey, delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, @@ -340,7 +339,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId globalFeatures = remoteInit.globalFeatures, localFeatures = remoteInit.localFeatures) log.debug(s"remote params: $remoteParams") - val localFundingPubkey = localParams.fundingPubKey(nodeParams.keyManager) + val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath) val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey))) wallet.makeFundingTx(fundingPubkeyScript, fundingSatoshis, fundingTxFeeratePerKw).pipeTo(self) goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, accept.firstPerCommitmentPoint, channelVersion, open) @@ -368,7 +367,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // 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, fundingAmount, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.onChainFeeConf.maxFeerateMismatch) require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, localParams.fundingPubKey(keyManager)) + val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.fundingKeyPath)) // signature of their initial commitment tx that pays remote pushMsat val fundingCreated = FundingCreated( temporaryChannelId = temporaryChannelId, @@ -410,7 +409,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.onChainFeeConf.maxFeerateMismatch) // check remote signature validity - val fundingPubKey = localParams.fundingPubKey(keyManager) + val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey) val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) Transactions.checkSpendable(signedLocalCommitTx) match { @@ -452,7 +451,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId 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, channelVersion, fundingCreated)) => // we make sure that their sig checks out and that our first commit tx is spendable - val fundingPubKey = localParams.fundingPubKey(keyManager) + val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey) val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) Transactions.checkSpendable(signedLocalCommitTx) match { @@ -530,7 +529,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Success(_) => log.info(s"channelId=${commitments.channelId} was confirmed at blockHeight=$blockHeight txIndex=$txIndex") blockchain ! WatchLost(self, commitments.commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_LOST) - val fundingPubKey = d.commitments.localParams.fundingPubKey(keyManager).publicKey + val fundingPubKey = keyManager.fundingPublicKey(d.commitments.localParams.fundingKeyPath).publicKey val channelKeyPath = d.commitments.localParams.channelKeyPath(keyManager) val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) val fundingLocked = FundingLocked(commitments.channelId, nextPerCommitmentPoint) @@ -901,7 +900,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId require(d.shortChannelId == remoteAnnSigs.shortChannelId, s"shortChannelId mismatch: local=${d.shortChannelId} remote=${remoteAnnSigs.shortChannelId}") log.info(s"announcing channelId=${d.channelId} on the network with shortId=${d.shortChannelId}") import d.commitments.{localParams, remoteParams} - val fundingPubKey = localParams.fundingPubKey(keyManager) + val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) val channelAnn = Announcements.makeChannelAnnouncement(nodeParams.chainHash, localAnnSigs.shortChannelId, nodeParams.nodeId, remoteParams.nodeId, fundingPubKey.publicKey, remoteParams.fundingPubKey, localAnnSigs.nodeSignature, remoteAnnSigs.nodeSignature, localAnnSigs.bitcoinSignature, remoteAnnSigs.bitcoinSignature) // we use GOTO instead of stay because we want to fire transitions goto(NORMAL) using d.copy(channelAnnouncement = Some(channelAnn)) storing() @@ -1408,7 +1407,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId forwarder ! r val yourLastPerCommitmentSecret = d.commitments.remotePerCommitmentSecrets.lastIndex.flatMap(d.commitments.remotePerCommitmentSecrets.getHash).getOrElse(ByteVector32.Zeroes) - val fundingPubKey = d.commitments.localParams.fundingPubKey(keyManager) + val fundingPubKey = keyManager.fundingPublicKey(d.commitments.localParams.fundingKeyPath) val channelKeyPath = d.commitments.localParams.channelKeyPath(keyManager) val myCurrentPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, d.commitments.localCommit.index) @@ -1467,14 +1466,12 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_LOCKED) => log.debug(s"re-sending fundingLocked") - val fundingPubKey = d.commitments.localParams.fundingPubKey(keyManager).publicKey val channelKeyPath = d.commitments.localParams.channelKeyPath(keyManager) val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint) goto(WAIT_FOR_FUNDING_LOCKED) sending fundingLocked case Event(channelReestablish: ChannelReestablish, d: DATA_NORMAL) => - val fundingPubKey = d.commitments.localParams.fundingPubKey(keyManager) val channelKeyPath = d.commitments.localParams.channelKeyPath(keyManager) channelReestablish match { case ChannelReestablish(_, _, nextRemoteRevocationNumber, Some(yourLastPerCommitmentSecret), _) if !Helpers.checkLocalCommit(d, nextRemoteRevocationNumber) => @@ -2129,7 +2126,6 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } else if (commitments1.localCommit.index == channelReestablish.nextRemoteRevocationNumber + 1) { // our last revocation got lost, let's resend it log.debug(s"re-sending last revocation") - val fundingPubKey = d.commitments.localParams.fundingPubKey(keyManager).publicKey val channelKeyPath = d.commitments.localParams.channelKeyPath(keyManager) val localPerCommitmentSecret = keyManager.commitmentSecret(channelKeyPath, d.commitments.localCommit.index - 1) val localNextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, d.commitments.localCommit.index + 1) 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 ccefeae1a4..ab87eb1815 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 @@ -19,10 +19,9 @@ package fr.acinq.eclair.channel import java.util.UUID import akka.actor.ActorRef -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, Transaction} import fr.acinq.eclair.crypto.KeyManager -import fr.acinq.eclair.api.MilliSatoshiSerializer import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions.CommitTx import fr.acinq.eclair.wire.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc} @@ -205,15 +204,13 @@ final case class LocalParams(version: Int, globalFeatures: ByteVector, localFeatures: ByteVector) { - def fundingPubKey(keyManager: KeyManager) = keyManager.fundingPublicKey(fundingKeyPath) - def channelKeyPath(keyManager: KeyManager) = version match { case 0 => // legacy mode: we reuse the funding key path as our channel key path fundingKeyPath case 1 => // deterministic mode: use the funding pubkey to compute the channel key path - KeyManager.channelKeyPath(fundingPubKey(keyManager)) + KeyManager.channelKeyPath(keyManager.fundingPublicKey(fundingKeyPath)) } } 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 80e7332a09..59348be820 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 @@ -378,10 +378,9 @@ object Commitments { // 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, localParams.fundingPubKey(keyManager)) + val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath)) val sortedHtlcTxs: Seq[TransactionWithInputInfo] = (htlcTimeoutTxs ++ htlcSuccessTxs).sortBy(_.input.outPoint.index) - val fundingPubKey = localParams.fundingPubKey(keyManager) val channelKeyPath = localParams.channelKeyPath(keyManager) val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), remoteNextPerCommitmentPoint)) @@ -427,18 +426,17 @@ object Commitments { // receiving money i.e its commit tx has one output for them val spec = CommitmentSpec.reduce(localCommit.spec, localChanges.acked, remoteChanges.proposed) - val fundingPubKey = localParams.fundingPubKey(keyManager) val channelKeyPath = localParams.channelKeyPath(keyManager) val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index + 1) val (localCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeLocalTxs(keyManager, localCommit.index + 1, localParams, remoteParams, commitInput, localPerCommitmentPoint, spec) - val sig = keyManager.sign(localCommitTx, fundingPubKey) + val sig = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath)) 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) // TODO: should we have optional sig? (original comment: this tx will NOT be signed if our output is empty) // no need to compute htlc sigs if commit sig doesn't check out - val signedCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteParams.fundingPubKey, sig, commit.signature) + val signedCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, sig, commit.signature) if (Transactions.checkSpendable(signedCommitTx).isFailure) { throw InvalidCommitmentSignature(commitments.channelId, signedCommitTx.tx) } @@ -527,7 +525,6 @@ object Commitments { } def makeLocalTxs(keyManager: KeyManager, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, localPerCommitmentPoint: PublicKey, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { - val fundingPubKey = localParams.fundingPubKey(keyManager) val channelKeyPath = localParams.channelKeyPath(keyManager) val localDelayedPaymentPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, localPerCommitmentPoint) @@ -540,7 +537,6 @@ object Commitments { } def makeRemoteTxs(keyManager: KeyManager, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, remotePerCommitmentPoint: PublicKey, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { - val fundingPubKey = localParams.fundingPubKey(keyManager) val channelKeyPath = localParams.channelKeyPath(keyManager) val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) 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 54cd087aa8..52fc9d5ce8 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 @@ -204,7 +204,7 @@ object Helpers { def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: ShortChannelId) = { val features = ByteVector.empty // empty features for now - val fundingPubKey = commitments.localParams.fundingPubKey(nodeParams.keyManager) + val fundingPubKey = nodeParams.keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath) val (localNodeSig, localBitcoinSig) = nodeParams.keyManager.signChannelAnnouncement(fundingPubKey, nodeParams.chainHash, shortChannelId, commitments.remoteParams.nodeId, commitments.remoteParams.fundingPubKey, features) AnnouncementSignatures(commitments.channelId, shortChannelId, localNodeSig, localBitcoinSig) } @@ -269,7 +269,7 @@ object Helpers { } } - val fundingPubKey = localParams.fundingPubKey(keyManager) + val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) val channelKeyPath = localParams.channelKeyPath(keyManager) val commitmentInput = makeFundingInputInfo(fundingTxHash, fundingTxOutputIndex, fundingAmount, fundingPubKey.publicKey, remoteParams.fundingPubKey) val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0) @@ -457,7 +457,7 @@ object Helpers { // TODO: check that val dustLimitSatoshis = localParams.dustLimit.max(remoteParams.dustLimit) val closingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, dustLimitSatoshis, closingFee, localCommit.spec) - val localClosingSig = keyManager.sign(closingTx, commitments.localParams.fundingPubKey(keyManager)) + val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath)) val closingSigned = ClosingSigned(channelId, closingFee, localClosingSig) log.info(s"signed closing txid=${closingTx.tx.txid} with closingFeeSatoshis=${closingSigned.feeSatoshis}") log.debug(s"closingTxid=${closingTx.tx.txid} closingTx=${closingTx.tx}}") @@ -472,7 +472,7 @@ object Helpers { throw InvalidCloseFee(commitments.channelId, remoteClosingFee) } val (closingTx, closingSigned) = makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, remoteClosingFee) - val signedClosingTx = Transactions.addSigs(closingTx, commitments.localParams.fundingPubKey(keyManager).publicKey, remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig) + val signedClosingTx = Transactions.addSigs(closingTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig) Transactions.checkSpendable(signedClosingTx).map(x => signedClosingTx.tx).recover { case _ => throw InvalidCloseSignature(commitments.channelId, signedClosingTx.tx) } } @@ -501,7 +501,7 @@ object Helpers { def claimCurrentLocalCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(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") - val fundingPubKey = localParams.fundingPubKey(keyManager) + val fundingPubKey = keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath) val channelKeyPath = localParams.channelKeyPath(keyManager) val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index.toInt) val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) @@ -575,7 +575,7 @@ object Helpers { 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") - val fundingPubKey = localParams.fundingPubKey(keyManager) + //val fundingPubKey = localParams.fundingPubKey(keyManager) val channelKeyPath = localParams.channelKeyPath(keyManager) val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remoteCommit.remotePerCommitmentPoint) @@ -629,7 +629,7 @@ object Helpers { * @return a list of transactions (one per HTLC that we can claim) */ def claimRemoteCommitMainOutput(keyManager: KeyManager, commitments: Commitments, remotePerCommitmentPoint: PublicKey, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): RemoteCommitPublished = { - val fundingPubKey = commitments.localParams.fundingPubKey(keyManager) + //val fundingPubKey = commitments.localParams.fundingPubKey(keyManager) val channelKeyPath = commitments.localParams.channelKeyPath(keyManager) val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) @@ -663,7 +663,7 @@ object Helpers { def claimRevokedRemoteCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, tx: Transaction, db: ChannelsDb, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): Option[RevokedCommitPublished] = { import commitments._ require(tx.txIn.size == 1, "commitment tx should have 1 input") - val fundingPubKey = commitments.localParams.fundingPubKey(keyManager) + //val fundingPubKey = commitments.localParams.fundingPubKey(keyManager) val channelKeyPath = commitments.localParams.channelKeyPath(keyManager) val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn(0).sequence, tx.lockTime) // this tx has been published by remote, so we need to invert local/remote params @@ -756,7 +756,6 @@ object Helpers { import commitments._ val tx = revokedCommitPublished.commitTx val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn(0).sequence, tx.lockTime) - val fundingPubKey = commitments.localParams.fundingPubKey(keyManager) val channelKeyPath = commitments.localParams.channelKeyPath(keyManager) // this tx has been published by remote, so we need to invert local/remote params val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, keyManager.paymentPoint(channelKeyPath).publicKey) 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 e389216295..f8ba34fb41 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 @@ -16,16 +16,14 @@ package fr.acinq.eclair.io -import java.io.ByteArrayInputStream import java.net.InetSocketAddress -import java.nio.ByteOrder import akka.actor.{ActorRef, FSM, OneForOneStrategy, PoisonPill, Props, Status, SupervisorStrategy, Terminated} import akka.event.Logging.MDC import akka.util.Timeout import com.google.common.net.HostAndPort import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Block, ByteVector32, DeterministicWallet, Protocol, Satoshi} +import fr.acinq.bitcoin.{Block, ByteVector32, DeterministicWallet, Satoshi} import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.TransportHandler @@ -664,15 +662,9 @@ object Peer { } def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: ByteVector, isFunder: Boolean, fundingAmount: Satoshi): LocalParams = { - val entropy = new Array[Byte](8) - secureRandom.nextBytes(entropy) - val bis = new ByteArrayInputStream(entropy) // we make sure that funder and fundee key path end differently - val last = isFunder match { - case false => DeterministicWallet.hardened(0) - case true => DeterministicWallet.hardened(1) - } - val fundingKeyPath = DeterministicWallet.KeyPath(Seq(Protocol.uint32(bis, ByteOrder.BIG_ENDIAN), Protocol.uint32(bis, ByteOrder.BIG_ENDIAN), last)) + val last = DeterministicWallet.hardened(if (isFunder) 1 else 0) + val fundingKeyPath = DeterministicWallet.KeyPath(Seq(secureRandom.nextInt() & 0xFFFFFFFFL, secureRandom.nextInt() & 0xFFFFFFFFL, last)) makeChannelParams(nodeParams, defaultFinalScriptPubKey, isFunder, fundingAmount, fundingKeyPath) } 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 8d0b865e7f..c240898615 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 @@ -2229,7 +2229,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42, null)) val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures] import initialState.commitments.{localParams, remoteParams} - val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, localParams.fundingPubKey(Alice.keyManager).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) + val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, Alice.keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) // actual test starts here bob2alice.forward(alice) awaitCond({ @@ -2248,7 +2248,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10, null)) val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures] import initialState.commitments.{localParams, remoteParams} - val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, localParams.fundingPubKey(Alice.keyManager).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) + val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, Alice.keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) bob2alice.forward(alice) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement === Some(channelAnn)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/LocalParamsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/LocalParamsSpec.scala index 6ec144753c..10b87bc6d5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/LocalParamsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/LocalParamsSpec.scala @@ -28,7 +28,7 @@ class LocalParamsSpec extends FunSuite { // read version 0 local params from data produced with an older version of eclair val localParams = ChannelCodecs.localParamsCodec.decode(BitVector.fromValidHex("0x03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d00004e3fbd1afb75d37604f7de0755c7e3e01000000000000044c0000000008f0d18000000000000027100000000000000000009000648000000000008000")).require.value val channelKeyPath = localParams.channelKeyPath(keyManager) - val fundingPubKey = localParams.fundingPubKey(keyManager) + val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) assert(channelKeyPath == DeterministicWallet.KeyPath("m/1677447599'/928855904'/1333649525/1551777281")) assert(fundingPubKey.toString == "ExtendedPublicKey(ByteVector(33 bytes, 0x02f8eea36e21551d30ce9329538c8652b5ecc1f8dc5b84ab52a4eb5e95759cdbc8),a1e0ccf8ac45652752ebfd578c3b7a6adcbdb693fbc0ff3f1d1797db4af9f8bd,7,m/46'/1'/1677447599'/928855904'/1333649525/1551777281/0',1132704962)") } 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 dff4839b5e..5c936918d8 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 @@ -33,9 +33,9 @@ import fr.acinq.eclair.transactions.Transactions.{CommitTx, InputInfo, Transacti import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.ChannelCodecs._ import fr.acinq.eclair.{TestConstants, UInt64, randomBytes, randomBytes32, randomKey, _} -import org.json4s.{CustomKeySerializer, CustomSerializer} import org.json4s.JsonAST._ import org.json4s.jackson.Serialization +import org.json4s.{CustomKeySerializer, CustomSerializer} import org.scalatest.FunSuite import scodec.bits._ import scodec.{Attempt, DecodeResult} @@ -395,7 +395,7 @@ object ChannelCodecsSpec { val fundingTx = Transaction.read("0200000001adbb20ea41a8423ea937e76e8151636bf6093b70eaff942930d20576600521fd000000006b48304502210090587b6201e166ad6af0227d3036a9454223d49a1f11839c1a362184340ef0240220577f7cd5cca78719405cbf1de7414ac027f0239ef6e214c90fcaab0454d84b3b012103535b32d5eb0a6ed0982a0479bbadc9868d9836f6ba94dd5a63be16d875069184ffffffff028096980000000000220020c015c4a6be010e21657068fc2e6a9d02b27ebe4d490a25846f7237f104d1a3cd20256d29010000001600143ca33c2e4446f4a305f23c80df8ad1afdcf652f900000000") val fundingAmount = fundingTx.txOut(0).amount - val commitmentInput = Funding.makeFundingInputInfo(fundingTx.hash, 0, fundingAmount, localParams.fundingPubKey(keyManager).publicKey, remoteParams.fundingPubKey) + val commitmentInput = Funding.makeFundingInputInfo(fundingTx.hash, 0, fundingAmount, keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey) val localCommit = LocalCommit(0, CommitmentSpec(htlcs.toSet, 1500, 50000000 msat, 70000000 msat), 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 msat, 700000 msat), ByteVector32(hex"0303030303030303030303030303030303030303030303030303030303030303"), PrivateKey(ByteVector.fill(32)(4)).publicKey) From 823aaedc1cfa54a3cdcb3e9867272b32e844a04f Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 16 Sep 2019 17:25:40 +0200 Subject: [PATCH 05/11] Add a "funding pubkey path" option to the channel version field This option is checked when we need to compute channel keys. For old channels it won't be set, and we always set it for new ones. --- .../fr/acinq/eclair/channel/Channel.scala | 30 ++++++++-------- .../acinq/eclair/channel/ChannelTypes.scala | 29 +++++++-------- .../fr/acinq/eclair/channel/Commitments.scala | 16 ++++----- .../fr/acinq/eclair/channel/Helpers.scala | 25 ++++++------- .../fr/acinq/eclair/crypto/KeyManager.scala | 9 +++++ .../main/scala/fr/acinq/eclair/io/Peer.scala | 3 +- .../fr/acinq/eclair/wire/ChannelCodecs.scala | 12 +------ .../states/StateTestsHelperMethods.scala | 2 +- .../a/WaitForAcceptChannelStateSpec.scala | 2 +- .../a/WaitForOpenChannelStateSpec.scala | 4 +-- ...itForFundingCreatedInternalStateSpec.scala | 4 +-- .../b/WaitForFundingCreatedStateSpec.scala | 2 +- .../b/WaitForFundingSignedStateSpec.scala | 3 +- .../c/WaitForFundingConfirmedStateSpec.scala | 2 +- .../c/WaitForFundingLockedStateSpec.scala | 2 +- .../channel/states/e/OfflineStateSpec.scala | 8 ++--- .../channel/states/h/ClosingStateSpec.scala | 2 +- .../eclair/transactions/LocalParamsSpec.scala | 35 ------------------- .../acinq/eclair/wire/ChannelCodecsSpec.scala | 2 -- 19 files changed, 73 insertions(+), 119 deletions(-) delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/transactions/LocalParamsSpec.scala 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 700f98a4ef..9e92ead3e1 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 @@ -145,11 +145,11 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId startWith(WAIT_FOR_INIT_INTERNAL, Nothing) when(WAIT_FOR_INIT_INTERNAL)(handleExceptions { - case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, remote, _, channelFlags, _), Nothing) => + case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, remote, _, channelFlags, channelVersion), Nothing) => context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, isFunder = true, temporaryChannelId, initialFeeratePerKw, Some(fundingTxFeeratePerKw))) forwarder ! remote val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey - val channelKeyPath = localParams.channelKeyPath(nodeParams.keyManager) + val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) val open = OpenChannel(nodeParams.chainHash, temporaryChannelId = temporaryChannelId, fundingSatoshis = fundingSatoshis, @@ -161,7 +161,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId feeratePerKw = initialFeeratePerKw, toSelfDelay = localParams.toSelfDelay, maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, - fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey, + fundingPubkey = fundingPubKey, revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, paymentBasepoint = keyManager.paymentPoint(channelKeyPath).publicKey, delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, @@ -273,7 +273,9 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Failure(t) => handleLocalError(t, d, Some(open)) case Success(_) => context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, isFunder = false, open.temporaryChannelId, open.feeratePerKw, None)) - val channelKeyPath = localParams.channelKeyPath(nodeParams.keyManager) + val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey + val channelVersion = ChannelVersion.STANDARD | ChannelVersion.USE_PUBKEY_KEYPATH + val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) // TODO: maybe also check uniqueness of temporary channel id val minimumDepth = nodeParams.minDepthBlocks val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId, @@ -284,7 +286,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId htlcMinimumMsat = localParams.htlcMinimum, toSelfDelay = localParams.toSelfDelay, maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, - fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey, + fundingPubkey = fundingPubkey, revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, paymentBasepoint = keyManager.paymentPoint(channelKeyPath).publicKey, delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, @@ -306,7 +308,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId globalFeatures = remoteInit.globalFeatures, localFeatures = remoteInit.localFeatures) log.debug(s"remote params: $remoteParams") - goto(WAIT_FOR_FUNDING_CREATED) using DATA_WAIT_FOR_FUNDING_CREATED(open.temporaryChannelId, localParams, remoteParams, open.fundingSatoshis, open.pushMsat, open.feeratePerKw, open.firstPerCommitmentPoint, open.channelFlags, ChannelVersion.STANDARD, accept) sending accept + goto(WAIT_FOR_FUNDING_CREATED) using DATA_WAIT_FOR_FUNDING_CREATED(open.temporaryChannelId, localParams, remoteParams, open.fundingSatoshis, open.pushMsat, open.feeratePerKw, open.firstPerCommitmentPoint, open.channelFlags, channelVersion, accept) sending accept } case Event(CMD_CLOSE(_), _) => goto(CLOSED) replying "ok" @@ -365,7 +367,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions { case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, channelVersion, open)) => // 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, fundingAmount, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.onChainFeeConf.maxFeerateMismatch) + val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, channelVersion, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.onChainFeeConf.maxFeerateMismatch) require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.fundingKeyPath)) // signature of their initial commitment tx that pays remote pushMsat @@ -406,7 +408,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId when(WAIT_FOR_FUNDING_CREATED)(handleExceptions { case Event(FundingCreated(_, fundingTxHash, fundingTxOutputIndex, remoteSig), d@DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, channelFlags, channelVersion, _)) => // they fund the channel with their funding tx, so the money is theirs (but we are paid pushMsat) - val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.onChainFeeConf.maxFeerateMismatch) + val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, channelVersion, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.onChainFeeConf.maxFeerateMismatch) // check remote signature validity val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) @@ -529,8 +531,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Success(_) => log.info(s"channelId=${commitments.channelId} was confirmed at blockHeight=$blockHeight txIndex=$txIndex") blockchain ! WatchLost(self, commitments.commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_LOST) - val fundingPubKey = keyManager.fundingPublicKey(d.commitments.localParams.fundingKeyPath).publicKey - val channelKeyPath = d.commitments.localParams.channelKeyPath(keyManager) + val channelKeyPath = keyManager.channelKeyPath(d.commitments.localParams, commitments.channelVersion) val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) val fundingLocked = FundingLocked(commitments.channelId, nextPerCommitmentPoint) deferred.foreach(self ! _) @@ -1407,8 +1408,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId forwarder ! r val yourLastPerCommitmentSecret = d.commitments.remotePerCommitmentSecrets.lastIndex.flatMap(d.commitments.remotePerCommitmentSecrets.getHash).getOrElse(ByteVector32.Zeroes) - val fundingPubKey = keyManager.fundingPublicKey(d.commitments.localParams.fundingKeyPath) - val channelKeyPath = d.commitments.localParams.channelKeyPath(keyManager) + val channelKeyPath = keyManager.channelKeyPath(d.commitments.localParams, d.commitments.channelVersion) val myCurrentPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, d.commitments.localCommit.index) val channelReestablish = ChannelReestablish( @@ -1466,13 +1466,13 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_LOCKED) => log.debug(s"re-sending fundingLocked") - val channelKeyPath = d.commitments.localParams.channelKeyPath(keyManager) + val channelKeyPath = keyManager.channelKeyPath(d.commitments.localParams, d.commitments.channelVersion) val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint) goto(WAIT_FOR_FUNDING_LOCKED) sending fundingLocked case Event(channelReestablish: ChannelReestablish, d: DATA_NORMAL) => - val channelKeyPath = d.commitments.localParams.channelKeyPath(keyManager) + val channelKeyPath = keyManager.channelKeyPath(d.commitments.localParams, d.commitments.channelVersion) channelReestablish match { case ChannelReestablish(_, _, nextRemoteRevocationNumber, Some(yourLastPerCommitmentSecret), _) if !Helpers.checkLocalCommit(d, nextRemoteRevocationNumber) => // if next_remote_revocation_number is greater than our local commitment index, it means that either we are using an outdated commitment, or they are lying @@ -2126,7 +2126,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } else if (commitments1.localCommit.index == channelReestablish.nextRemoteRevocationNumber + 1) { // our last revocation got lost, let's resend it log.debug(s"re-sending last revocation") - val channelKeyPath = d.commitments.localParams.channelKeyPath(keyManager) + val channelKeyPath = keyManager.channelKeyPath(d.commitments.localParams, d.commitments.channelVersion) val localPerCommitmentSecret = keyManager.commitmentSecret(channelKeyPath, d.commitments.localCommit.index - 1) val localNextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, d.commitments.localCommit.index + 1) val revocation = RevokeAndAck( 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 ab87eb1815..dd898cb1c0 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 @@ -21,7 +21,6 @@ import java.util.UUID import akka.actor.ActorRef import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, Transaction} -import fr.acinq.eclair.crypto.KeyManager import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions.CommitTx import fr.acinq.eclair.wire.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc} @@ -190,8 +189,7 @@ 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 -final case class LocalParams(version: Int, - nodeId: PublicKey, +final case class LocalParams(nodeId: PublicKey, fundingKeyPath: DeterministicWallet.KeyPath, dustLimit: Satoshi, maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi @@ -202,18 +200,7 @@ final case class LocalParams(version: Int, isFunder: Boolean, defaultFinalScriptPubKey: ByteVector, globalFeatures: ByteVector, - localFeatures: ByteVector) { - - def channelKeyPath(keyManager: KeyManager) = version match { - case 0 => - // legacy mode: we reuse the funding key path as our channel key path - fundingKeyPath - case 1 => - // deterministic mode: use the funding pubkey to compute the channel key path - KeyManager.channelKeyPath(keyManager.fundingPublicKey(fundingKeyPath)) - - } -} + localFeatures: ByteVector) final case class RemoteParams(nodeId: PublicKey, dustLimit: Satoshi, @@ -237,9 +224,19 @@ object ChannelFlags { case class ChannelVersion(bits: BitVector) { require(bits.size == ChannelVersion.LENGTH_BITS, "channel version takes 4 bytes") + + def |(other: ChannelVersion) = ChannelVersion(bits | other.bits) + def &(other: ChannelVersion) = ChannelVersion(bits & other.bits) + def ^(other: ChannelVersion) = ChannelVersion(bits ^ other.bits) + def isSet(bit: Int) = bits.reverse.get(bit) } + object ChannelVersion { + import scodec.bits._ val LENGTH_BITS = 4 * 8 - val STANDARD = ChannelVersion(BitVector.fill(LENGTH_BITS)(false)) + val ZEROES = ChannelVersion(bin"00000000000000000000000000000000") + val STANDARD = ZEROES + val USE_PUBKEY_KEYPATH_BIT = 0 // bit numbers start at 0 + val USE_PUBKEY_KEYPATH = STANDARD | ChannelVersion(bin"00000000000000000000000000000001") } // @formatter:on 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 619c6975dc..e16af2418c 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 @@ -376,11 +376,11 @@ object Commitments { 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 (remoteCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeRemoteTxs(keyManager, channelVersion, remoteCommit.index + 1, localParams, remoteParams, commitInput, remoteNextPerCommitmentPoint, spec) val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath)) val sortedHtlcTxs: Seq[TransactionWithInputInfo] = (htlcTimeoutTxs ++ htlcSuccessTxs).sortBy(_.input.outPoint.index) - val channelKeyPath = localParams.channelKeyPath(keyManager) + val channelKeyPath = keyManager.channelKeyPath(commitments.localParams, commitments.channelVersion) val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), remoteNextPerCommitmentPoint)) // NB: IN/OUT htlcs are inverted because this is the remote commit @@ -425,9 +425,9 @@ object Commitments { // receiving money i.e its commit tx has one output for them val spec = CommitmentSpec.reduce(localCommit.spec, localChanges.acked, remoteChanges.proposed) - val channelKeyPath = localParams.channelKeyPath(keyManager) + val channelKeyPath = keyManager.channelKeyPath(commitments.localParams, commitments.channelVersion) val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index + 1) - val (localCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeLocalTxs(keyManager, localCommit.index + 1, localParams, remoteParams, commitInput, localPerCommitmentPoint, spec) + val (localCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeLocalTxs(keyManager, channelVersion, localCommit.index + 1, localParams, remoteParams, commitInput, localPerCommitmentPoint, spec) val sig = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath)) 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) @@ -523,8 +523,8 @@ object Commitments { } } - def makeLocalTxs(keyManager: KeyManager, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, localPerCommitmentPoint: PublicKey, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { - val channelKeyPath = localParams.channelKeyPath(keyManager) + def makeLocalTxs(keyManager: KeyManager, channelVersion: ChannelVersion, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, localPerCommitmentPoint: PublicKey, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { + val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) val localDelayedPaymentPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, localPerCommitmentPoint) val remotePaymentPubkey = Generators.derivePubKey(remoteParams.paymentBasepoint, localPerCommitmentPoint) @@ -535,8 +535,8 @@ object Commitments { (commitTx, htlcTimeoutTxs, htlcSuccessTxs) } - def makeRemoteTxs(keyManager: KeyManager, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, remotePerCommitmentPoint: PublicKey, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { - val channelKeyPath = localParams.channelKeyPath(keyManager) + def makeRemoteTxs(keyManager: KeyManager, channelVersion: ChannelVersion, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, remotePerCommitmentPoint: PublicKey, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { + val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) 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 52fc9d5ce8..9a5147134d 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 @@ -252,7 +252,7 @@ object Helpers { * @param remoteFirstPerCommitmentPoint * @return (localSpec, localTx, remoteSpec, remoteTx, fundingTxOutput) */ - def makeFirstCommitTxs(keyManager: KeyManager, temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingAmount: Satoshi, pushMsat: MilliSatoshi, initialFeeratePerKw: Long, fundingTxHash: ByteVector32, fundingTxOutputIndex: Int, remoteFirstPerCommitmentPoint: PublicKey, maxFeerateMismatch: Double): (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx) = { + def makeFirstCommitTxs(keyManager: KeyManager, channelVersion: ChannelVersion, temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingAmount: Satoshi, pushMsat: MilliSatoshi, initialFeeratePerKw: Long, fundingTxHash: ByteVector32, fundingTxOutputIndex: Int, remoteFirstPerCommitmentPoint: PublicKey, maxFeerateMismatch: Double): (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx) = { val toLocalMsat = if (localParams.isFunder) fundingAmount.toMilliSatoshi - pushMsat else pushMsat val toRemoteMsat = if (localParams.isFunder) pushMsat else fundingAmount.toMilliSatoshi - pushMsat @@ -270,11 +270,11 @@ object Helpers { } val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) - val channelKeyPath = localParams.channelKeyPath(keyManager) + val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) val commitmentInput = makeFundingInputInfo(fundingTxHash, fundingTxOutputIndex, fundingAmount, fundingPubKey.publicKey, remoteParams.fundingPubKey) val localPerCommitmentPoint = keyManager.commitmentPoint(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, channelVersion, 0, localParams, remoteParams, commitmentInput, localPerCommitmentPoint, localSpec) + val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(keyManager, channelVersion, 0, localParams, remoteParams, commitmentInput, remoteFirstPerCommitmentPoint, remoteSpec) (localSpec, localCommitTx, remoteSpec, remoteCommitTx) } @@ -501,8 +501,7 @@ object Helpers { def claimCurrentLocalCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(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") - val fundingPubKey = keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath) - val channelKeyPath = localParams.channelKeyPath(keyManager) + val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index.toInt) val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) val localDelayedPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) @@ -571,12 +570,11 @@ object Helpers { * @return a list of transactions (one per HTLC that we can claim) */ def claimRemoteCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): RemoteCommitPublished = { - import commitments.{commitInput, localParams, remoteParams} + import commitments.{channelVersion, 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) + val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(keyManager, channelVersion, remoteCommit.index, localParams, remoteParams, commitInput, remoteCommit.remotePerCommitmentPoint, remoteCommit.spec) require(remoteCommitTx.tx.txid == tx.txid, "txid mismatch, cannot recompute the current remote commit tx") - //val fundingPubKey = localParams.fundingPubKey(keyManager) - val channelKeyPath = localParams.channelKeyPath(keyManager) + val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remoteCommit.remotePerCommitmentPoint) val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index.toInt) @@ -629,8 +627,7 @@ object Helpers { * @return a list of transactions (one per HTLC that we can claim) */ def claimRemoteCommitMainOutput(keyManager: KeyManager, commitments: Commitments, remotePerCommitmentPoint: PublicKey, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): RemoteCommitPublished = { - //val fundingPubKey = commitments.localParams.fundingPubKey(keyManager) - val channelKeyPath = commitments.localParams.channelKeyPath(keyManager) + val channelKeyPath = keyManager.channelKeyPath(commitments.localParams, commitments.channelVersion) val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) val feeratePerKwMain = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) @@ -664,7 +661,7 @@ object Helpers { import commitments._ require(tx.txIn.size == 1, "commitment tx should have 1 input") //val fundingPubKey = commitments.localParams.fundingPubKey(keyManager) - val channelKeyPath = commitments.localParams.channelKeyPath(keyManager) + val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn(0).sequence, tx.lockTime) // this tx has been published by remote, so we need to invert local/remote params val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, keyManager.paymentPoint(channelKeyPath).publicKey) @@ -756,7 +753,7 @@ object Helpers { import commitments._ val tx = revokedCommitPublished.commitTx val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn(0).sequence, tx.lockTime) - val channelKeyPath = commitments.localParams.channelKeyPath(keyManager) + val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) // this tx has been published by remote, so we need to invert local/remote params val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, keyManager.paymentPoint(channelKeyPath).publicKey) // now we know what commit number this tx is referring to, we can derive the commitment point from the shachain 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 086a9a831b..1fb5d5aa82 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 @@ -23,6 +23,7 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.DeterministicWallet.ExtendedPublicKey import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, DeterministicWallet, Protocol} import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.channel.{ChannelVersion, LocalParams} import fr.acinq.eclair.transactions.Transactions.TransactionWithInputInfo import scodec.bits.ByteVector @@ -45,6 +46,14 @@ trait KeyManager { def commitmentPoint(channelKeyPath: DeterministicWallet.KeyPath, index: Long): Crypto.PublicKey + def channelKeyPath(localParams: LocalParams, channelVersion: ChannelVersion): DeterministicWallet.KeyPath = if (channelVersion.isSet(ChannelVersion.USE_PUBKEY_KEYPATH_BIT)) { + // deterministic mode: use the funding pubkey to compute the channel key path + KeyManager.channelKeyPath(fundingPublicKey(localParams.fundingKeyPath)) + } else { + // legacy mode: we reuse the funding key path as our channel key path + localParams.fundingKeyPath + } + /** * * @param tx input transaction 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 f8ba34fb41..7a06acc4a3 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 @@ -297,7 +297,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A val channelFeeratePerKw = nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget) val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) log.info(s"requesting a new channel with fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt} temporaryChannelId=$temporaryChannelId localParams=$localParams") - channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.transport, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags), ChannelVersion.STANDARD) + channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.transport, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags), ChannelVersion.STANDARD | ChannelVersion.USE_PUBKEY_KEYPATH) stay using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) case Event(msg: wire.OpenChannel, d: ConnectedData) => @@ -670,7 +670,6 @@ object Peer { def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: ByteVector, isFunder: Boolean, fundingAmount: Satoshi, fundingKeyPath: DeterministicWallet.KeyPath): LocalParams = { LocalParams( - version = 1, nodeParams.nodeId, fundingKeyPath, dustLimit = nodeParams.dustLimit, 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 08547078f2..536023b513 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 @@ -58,18 +58,8 @@ object ChannelCodecs extends Logging { fallback = provide(ChannelVersion.STANDARD) ) - // we use the same trick here: un-versioned LocalParams instances start with a node id so the first byte cannot be 0x01 - val localParamsVersionCodec: Codec[Int] = discriminatorWithDefault[Int]( - discriminator = discriminated[Int].by(byte) - .typecase(0x01, int16) - // NB: 0x02 and 0x03 are *reserved* for backward compatibility reasons - , - fallback = provide(0) - ) - val localParamsCodec: Codec[LocalParams] = ( - ("version" | localParamsVersionCodec) :: - ("nodeId" | publicKey) :: + ("nodeId" | publicKey) :: ("channelPath" | keyPathCodec) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: 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 9df32f77c7..94c50aa80c 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 @@ -70,7 +70,7 @@ trait StateTestsHelperMethods extends TestKitBase with fixture.TestSuite with Pa def reachNormal(setup: SetupFixture, tags: Set[String] = Set.empty): Unit = { import setup._ - val channelVersion = ChannelVersion.STANDARD + val channelVersion = ChannelVersion.STANDARD | ChannelVersion.USE_PUBKEY_KEYPATH val channelFlags = if (tags.contains("channels_public")) ChannelFlags.AnnounceChannel else ChannelFlags.Empty val pushMsat = if (tags.contains("no_push_msat")) 0.msat else TestConstants.pushMsat val (aliceParams, bobParams) = (Alice.channelParams, Bob.channelParams) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index 1b102e748d..8758aa6b5e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -49,7 +49,7 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp init(wallet = noopWallet) } import setup._ - val channelVersion = ChannelVersion.STANDARD + val channelVersion = ChannelVersion.STANDARD | ChannelVersion.USE_PUBKEY_KEYPATH val (aliceParams, bobParams) = (Alice.channelParams, Bob.channelParams) val aliceInit = Init(aliceParams.globalFeatures, aliceParams.localFeatures) val bobInit = Init(bobParams.globalFeatures, bobParams.localFeatures) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index da61ba884c..a3568ce9c6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -22,7 +22,7 @@ import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.wire.{Error, Init, OpenChannel} -import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, TestConstants, TestkitBaseClass, ToMilliSatoshiConversion} +import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, TestConstants, TestkitBaseClass, ToMilliSatoshiConversion} import org.scalatest.Outcome import scala.concurrent.duration._ @@ -38,7 +38,7 @@ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelper override def withFixture(test: OneArgTest): Outcome = { val setup = init() import setup._ - val channelVersion = ChannelVersion.STANDARD + val channelVersion = ChannelVersion.STANDARD | ChannelVersion.USE_PUBKEY_KEYPATH val (aliceParams, bobParams) = (Alice.channelParams, Bob.channelParams) val aliceInit = Init(aliceParams.globalFeatures, aliceParams.localFeatures) val bobInit = Init(bobParams.globalFeatures, bobParams.localFeatures) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala index 0c5facf221..64a94aba5c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala @@ -27,8 +27,8 @@ import fr.acinq.eclair.{TestConstants, TestkitBaseClass} import org.scalatest.Outcome import scodec.bits.ByteVector -import scala.concurrent.{Future, Promise} import scala.concurrent.duration._ +import scala.concurrent.{Future, Promise} /** * Created by PM on 05/07/2016. @@ -47,7 +47,7 @@ class WaitForFundingCreatedInternalStateSpec extends TestkitBaseClass with State val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.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, ChannelVersion.STANDARD) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD | ChannelVersion.USE_PUBKEY_KEYPATH) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) 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 93be60649f..8f721262fd 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 @@ -48,7 +48,7 @@ class WaitForFundingCreatedStateSpec extends TestkitBaseClass with StateTestsHel val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD | ChannelVersion.USE_PUBKEY_KEYPATH) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) 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 4afe51eee6..6b0fefa539 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 @@ -26,7 +26,6 @@ import fr.acinq.eclair.channel.states.StateTestsHelperMethods 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 scala.concurrent.duration._ @@ -44,7 +43,7 @@ class WaitForFundingSignedStateSpec extends TestkitBaseClass with StateTestsHelp val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.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, ChannelVersion.STANDARD) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD | ChannelVersion.USE_PUBKEY_KEYPATH) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index 5c54affbf6..8379e7923d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -44,7 +44,7 @@ class WaitForFundingConfirmedStateSpec extends TestkitBaseClass with StateTestsH val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.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, ChannelVersion.STANDARD) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD | ChannelVersion.USE_PUBKEY_KEYPATH) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala index 9068f0dec1..53b6918e61 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala @@ -43,7 +43,7 @@ class WaitForFundingLockedStateSpec extends TestkitBaseClass with StateTestsHelp val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.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, ChannelVersion.STANDARD) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD | ChannelVersion.USE_PUBKEY_KEYPATH) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index bb87841b7a..810c972973 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -88,10 +88,10 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val aliceCommitments = alice.stateData.asInstanceOf[HasCommitments].commitments val bobCurrentPerCommitmentPoint = TestConstants.Bob.keyManager.commitmentPoint( - bobCommitments.localParams.channelKeyPath(TestConstants.Bob.keyManager), + TestConstants.Bob.keyManager.channelKeyPath(bobCommitments.localParams, bobCommitments.channelVersion), bobCommitments.localCommit.index) val aliceCurrentPerCommitmentPoint = TestConstants.Alice.keyManager.commitmentPoint( - aliceCommitments.localParams.channelKeyPath(TestConstants.Alice.keyManager), + TestConstants.Alice.keyManager.channelKeyPath(aliceCommitments.localParams, aliceCommitments.channelVersion), aliceCommitments.localCommit.index) @@ -176,10 +176,10 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val aliceCommitments = alice.stateData.asInstanceOf[HasCommitments].commitments val bobCurrentPerCommitmentPoint = TestConstants.Bob.keyManager.commitmentPoint( - bobCommitments.localParams.channelKeyPath(TestConstants.Bob.keyManager), + TestConstants.Bob.keyManager.channelKeyPath(bobCommitments.localParams, bobCommitments.channelVersion), bobCommitments.localCommit.index) val aliceCurrentPerCommitmentPoint = TestConstants.Alice.keyManager.commitmentPoint( - aliceCommitments.localParams.channelKeyPath(TestConstants.Alice.keyManager), + TestConstants.Alice.keyManager.channelKeyPath(aliceCommitments.localParams, aliceCommitments.channelVersion), aliceCommitments.localCommit.index) // a didn't receive the sig 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 c9b3628452..cd595638e4 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 @@ -65,7 +65,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { within(30 seconds) { val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD | ChannelVersion.USE_PUBKEY_KEYPATH) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/LocalParamsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/LocalParamsSpec.scala deleted file mode 100644 index 10b87bc6d5..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/LocalParamsSpec.scala +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package fr.acinq.eclair.transactions - -import fr.acinq.bitcoin.{Block, ByteVector32, DeterministicWallet} -import fr.acinq.eclair.crypto.LocalKeyManager -import fr.acinq.eclair.wire.ChannelCodecs -import org.scalatest.FunSuite -import scodec.bits.{BitVector, ByteVector} - -class LocalParamsSpec extends FunSuite { - test("read legacy data correctly") { - val seed = ByteVector32(ByteVector.fill(32)(2)) - val keyManager = new LocalKeyManager(seed, Block.RegtestGenesisBlock.hash) - // read version 0 local params from data produced with an older version of eclair - val localParams = ChannelCodecs.localParamsCodec.decode(BitVector.fromValidHex("0x03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d00004e3fbd1afb75d37604f7de0755c7e3e01000000000000044c0000000008f0d18000000000000027100000000000000000009000648000000000008000")).require.value - val channelKeyPath = localParams.channelKeyPath(keyManager) - val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) - assert(channelKeyPath == DeterministicWallet.KeyPath("m/1677447599'/928855904'/1333649525/1551777281")) - assert(fundingPubKey.toString == "ExtendedPublicKey(ByteVector(33 bytes, 0x02f8eea36e21551d30ce9329538c8652b5ecc1f8dc5b84ab52a4eb5e95759cdbc8),a1e0ccf8ac45652752ebfd578c3b7a6adcbdb693fbc0ff3f1d1797db4af9f8bd,7,m/46'/1'/1677447599'/928855904'/1333649525/1551777281/0',1132704962)") - } -} 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 0d234fd2f4..bc5787342d 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 @@ -84,7 +84,6 @@ class ChannelCodecsSpec extends FunSuite { test("encode/decode localparams") { val o = LocalParams( - version = 1, nodeId = randomKey.publicKey, fundingKeyPath = DeterministicWallet.KeyPath(Seq(42L)), dustLimit = Satoshi(Random.nextInt(Int.MaxValue)), @@ -347,7 +346,6 @@ class ChannelCodecsSpec extends FunSuite { object ChannelCodecsSpec { val keyManager = new LocalKeyManager(ByteVector32(ByteVector.fill(32)(1)), Block.RegtestGenesisBlock.hash) val localParams = LocalParams( - version = 1, keyManager.nodeId, fundingKeyPath = DeterministicWallet.KeyPath(Seq(42L)), dustLimit = Satoshi(546), From 96507f57bcc5c80890232b381a5a5f82b560336d Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 16 Sep 2019 17:43:01 +0200 Subject: [PATCH 06/11] Minor fixup (remove redundant signing method) --- .../src/main/scala/fr/acinq/eclair/channel/Helpers.scala | 3 +-- .../src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) 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 9a5147134d..2218085ac0 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 @@ -204,8 +204,7 @@ object Helpers { def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: ShortChannelId) = { val features = ByteVector.empty // empty features for now - val fundingPubKey = nodeParams.keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath) - val (localNodeSig, localBitcoinSig) = nodeParams.keyManager.signChannelAnnouncement(fundingPubKey, nodeParams.chainHash, shortChannelId, commitments.remoteParams.nodeId, commitments.remoteParams.fundingPubKey, features) + val (localNodeSig, localBitcoinSig) = nodeParams.keyManager.signChannelAnnouncement(commitments.localParams.fundingKeyPath, nodeParams.chainHash, shortChannelId, commitments.remoteParams.nodeId, commitments.remoteParams.fundingPubKey, features) AnnouncementSignatures(commitments.channelId, shortChannelId, localNodeSig, localBitcoinSig) } 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 1fb5d5aa82..6d80d46b1f 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 @@ -98,9 +98,6 @@ trait KeyManager { * private key, bitcoinSig is the signature of the channel announcement with our funding private key */ def signChannelAnnouncement(fundingKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64) - - def signChannelAnnouncement(fundingKey: DeterministicWallet.ExtendedPublicKey, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64) - = signChannelAnnouncement(fundingKey.path, chainHash, shortChannelId, remoteNodeId, remoteFundingKey, features) } object KeyManager { From e2c36d4ad41611c4af07bade74d365a654678e4f Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 16 Sep 2019 18:55:29 +0200 Subject: [PATCH 07/11] Revert "Minor fixup (remove redundant signing method)" This reverts commit 96507f57bcc5c80890232b381a5a5f82b560336d. --- .../src/main/scala/fr/acinq/eclair/channel/Helpers.scala | 3 ++- .../src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) 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 2218085ac0..9a5147134d 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 @@ -204,7 +204,8 @@ object Helpers { def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: ShortChannelId) = { val features = ByteVector.empty // empty features for now - val (localNodeSig, localBitcoinSig) = nodeParams.keyManager.signChannelAnnouncement(commitments.localParams.fundingKeyPath, nodeParams.chainHash, shortChannelId, commitments.remoteParams.nodeId, commitments.remoteParams.fundingPubKey, features) + val fundingPubKey = nodeParams.keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath) + val (localNodeSig, localBitcoinSig) = nodeParams.keyManager.signChannelAnnouncement(fundingPubKey, nodeParams.chainHash, shortChannelId, commitments.remoteParams.nodeId, commitments.remoteParams.fundingPubKey, features) AnnouncementSignatures(commitments.channelId, shortChannelId, localNodeSig, localBitcoinSig) } 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 6d80d46b1f..1fb5d5aa82 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 @@ -98,6 +98,9 @@ trait KeyManager { * private key, bitcoinSig is the signature of the channel announcement with our funding private key */ def signChannelAnnouncement(fundingKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64) + + def signChannelAnnouncement(fundingKey: DeterministicWallet.ExtendedPublicKey, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64) + = signChannelAnnouncement(fundingKey.path, chainHash, shortChannelId, remoteNodeId, remoteFundingKey, features) } object KeyManager { From 2b297f491242c3ee3b8adfed712e1812bb203d89 Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 17 Sep 2019 14:19:51 +0200 Subject: [PATCH 08/11] Minor fixup (remove redundant signing method) --- .../src/main/scala/fr/acinq/eclair/channel/Helpers.scala | 2 +- .../src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) 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 9a5147134d..297ab06bd9 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 @@ -205,7 +205,7 @@ object Helpers { def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: ShortChannelId) = { val features = ByteVector.empty // empty features for now val fundingPubKey = nodeParams.keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath) - val (localNodeSig, localBitcoinSig) = nodeParams.keyManager.signChannelAnnouncement(fundingPubKey, nodeParams.chainHash, shortChannelId, commitments.remoteParams.nodeId, commitments.remoteParams.fundingPubKey, features) + val (localNodeSig, localBitcoinSig) = nodeParams.keyManager.signChannelAnnouncement(fundingPubKey.path, nodeParams.chainHash, shortChannelId, commitments.remoteParams.nodeId, commitments.remoteParams.fundingPubKey, features) AnnouncementSignatures(commitments.channelId, shortChannelId, localNodeSig, localBitcoinSig) } 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 1fb5d5aa82..6d80d46b1f 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 @@ -98,9 +98,6 @@ trait KeyManager { * private key, bitcoinSig is the signature of the channel announcement with our funding private key */ def signChannelAnnouncement(fundingKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64) - - def signChannelAnnouncement(fundingKey: DeterministicWallet.ExtendedPublicKey, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64) - = signChannelAnnouncement(fundingKey.path, chainHash, shortChannelId, remoteNodeId, remoteFundingKey, features) } object KeyManager { From 06fdbdf9545b02c0e71fb5bd7fc0d7d62a1c6f6a Mon Sep 17 00:00:00 2001 From: sstone Date: Fri, 20 Sep 2019 18:14:53 +0200 Subject: [PATCH 09/11] ChannelVersion: make sure that all bits are set to 0 for legacy channels --- .../src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 536023b513..258ca597da 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 @@ -55,7 +55,8 @@ object ChannelCodecs extends Logging { .typecase(0x01, bits(ChannelVersion.LENGTH_BITS).as[ChannelVersion]) // NB: 0x02 and 0x03 are *reserved* for backward compatibility reasons , - fallback = provide(ChannelVersion.STANDARD) + fallback = provide(ChannelVersion.ZEROES) // README: DO NOT CHANGE THIS !! old channels don't have a channel version + // field and don't support additional features which is why all bits are set to 0. ) val localParamsCodec: Codec[LocalParams] = ( From 0377fb9a69d7863b7db0f8928fd5f5b99bffea90 Mon Sep 17 00:00:00 2001 From: sstone Date: Fri, 20 Sep 2019 19:33:55 +0200 Subject: [PATCH 10/11] ChannelVersion: USE_PUBKEY_KEYPATH is set by default --- .../main/scala/fr/acinq/eclair/channel/Channel.scala | 2 +- .../scala/fr/acinq/eclair/channel/ChannelTypes.scala | 8 ++++++-- .../src/main/scala/fr/acinq/eclair/io/Peer.scala | 2 +- .../fr/acinq/eclair/channel/ChannelTypesSpec.scala | 10 ++++++++++ .../channel/states/StateTestsHelperMethods.scala | 2 +- .../states/a/WaitForAcceptChannelStateSpec.scala | 2 +- .../channel/states/a/WaitForOpenChannelStateSpec.scala | 2 +- .../b/WaitForFundingCreatedInternalStateSpec.scala | 2 +- .../states/b/WaitForFundingCreatedStateSpec.scala | 2 +- .../states/b/WaitForFundingSignedStateSpec.scala | 2 +- .../states/c/WaitForFundingConfirmedStateSpec.scala | 2 +- .../states/c/WaitForFundingLockedStateSpec.scala | 2 +- .../eclair/channel/states/h/ClosingStateSpec.scala | 2 +- .../scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala | 10 +++++----- 14 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala 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 35356aa8f4..ad95f9774d 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 @@ -274,7 +274,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Success(_) => context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, isFunder = false, open.temporaryChannelId, open.feeratePerKw, None)) val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey - val channelVersion = ChannelVersion.STANDARD | ChannelVersion.USE_PUBKEY_KEYPATH + val channelVersion = ChannelVersion.STANDARD val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) // TODO: maybe also check uniqueness of temporary channel id val minimumDepth = nodeParams.minDepthBlocks 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 0a13ae4f50..c23af9ea06 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 @@ -235,8 +235,12 @@ object ChannelVersion { import scodec.bits._ val LENGTH_BITS = 4 * 8 val ZEROES = ChannelVersion(bin"00000000000000000000000000000000") - val STANDARD = ZEROES val USE_PUBKEY_KEYPATH_BIT = 0 // bit numbers start at 0 - val USE_PUBKEY_KEYPATH = STANDARD | ChannelVersion(BitVector.low(LENGTH_BITS).set(USE_PUBKEY_KEYPATH_BIT).reverse) + + def fromBit(bit: Int) = ChannelVersion(BitVector.low(LENGTH_BITS).set(bit).reverse) + + val USE_PUBKEY_KEYPATH = fromBit(USE_PUBKEY_KEYPATH_BIT) + + val STANDARD = ZEROES | USE_PUBKEY_KEYPATH } // @formatter:on 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 7a06acc4a3..af9a021728 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 @@ -297,7 +297,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A val channelFeeratePerKw = nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget) val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) log.info(s"requesting a new channel with fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt} temporaryChannelId=$temporaryChannelId localParams=$localParams") - channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.transport, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags), ChannelVersion.STANDARD | ChannelVersion.USE_PUBKEY_KEYPATH) + channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.transport, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags), ChannelVersion.STANDARD) stay using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) case Event(msg: wire.OpenChannel, d: ConnectedData) => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala new file mode 100644 index 0000000000..e9905b9ed7 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala @@ -0,0 +1,10 @@ +package fr.acinq.eclair.channel + +import org.scalatest.{FunSuite, ParallelTestExecution} + +class ChannelTypesSpec extends FunSuite with ParallelTestExecution { + test("standard channel features include deterministic channel key path") { + assert(ChannelVersion.STANDARD.isSet(ChannelVersion.USE_PUBKEY_KEYPATH_BIT)) + assert(!ChannelVersion.ZEROES.isSet(ChannelVersion.USE_PUBKEY_KEYPATH_BIT)) + } +} 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 94c50aa80c..9df32f77c7 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 @@ -70,7 +70,7 @@ trait StateTestsHelperMethods extends TestKitBase with fixture.TestSuite with Pa def reachNormal(setup: SetupFixture, tags: Set[String] = Set.empty): Unit = { import setup._ - val channelVersion = ChannelVersion.STANDARD | ChannelVersion.USE_PUBKEY_KEYPATH + val channelVersion = ChannelVersion.STANDARD val channelFlags = if (tags.contains("channels_public")) ChannelFlags.AnnounceChannel else ChannelFlags.Empty val pushMsat = if (tags.contains("no_push_msat")) 0.msat else TestConstants.pushMsat val (aliceParams, bobParams) = (Alice.channelParams, Bob.channelParams) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index 8758aa6b5e..1b102e748d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -49,7 +49,7 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp init(wallet = noopWallet) } import setup._ - val channelVersion = ChannelVersion.STANDARD | ChannelVersion.USE_PUBKEY_KEYPATH + val channelVersion = ChannelVersion.STANDARD val (aliceParams, bobParams) = (Alice.channelParams, Bob.channelParams) val aliceInit = Init(aliceParams.globalFeatures, aliceParams.localFeatures) val bobInit = Init(bobParams.globalFeatures, bobParams.localFeatures) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index a3568ce9c6..7e3f85896e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -38,7 +38,7 @@ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelper override def withFixture(test: OneArgTest): Outcome = { val setup = init() import setup._ - val channelVersion = ChannelVersion.STANDARD | ChannelVersion.USE_PUBKEY_KEYPATH + val channelVersion = ChannelVersion.STANDARD val (aliceParams, bobParams) = (Alice.channelParams, Bob.channelParams) val aliceInit = Init(aliceParams.globalFeatures, aliceParams.localFeatures) val bobInit = Init(bobParams.globalFeatures, bobParams.localFeatures) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala index 64a94aba5c..3537d27ef8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala @@ -47,7 +47,7 @@ class WaitForFundingCreatedInternalStateSpec extends TestkitBaseClass with State val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.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, ChannelVersion.STANDARD | ChannelVersion.USE_PUBKEY_KEYPATH) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) 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 8f721262fd..93be60649f 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 @@ -48,7 +48,7 @@ class WaitForFundingCreatedStateSpec extends TestkitBaseClass with StateTestsHel val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD | ChannelVersion.USE_PUBKEY_KEYPATH) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) 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 6b0fefa539..fbe248ef21 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 @@ -43,7 +43,7 @@ class WaitForFundingSignedStateSpec extends TestkitBaseClass with StateTestsHelp val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.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, ChannelVersion.STANDARD | ChannelVersion.USE_PUBKEY_KEYPATH) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index 8379e7923d..5c54affbf6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -44,7 +44,7 @@ class WaitForFundingConfirmedStateSpec extends TestkitBaseClass with StateTestsH val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.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, ChannelVersion.STANDARD | ChannelVersion.USE_PUBKEY_KEYPATH) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala index 53b6918e61..9068f0dec1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala @@ -43,7 +43,7 @@ class WaitForFundingLockedStateSpec extends TestkitBaseClass with StateTestsHelp val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.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, ChannelVersion.STANDARD | ChannelVersion.USE_PUBKEY_KEYPATH) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) 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 cd595638e4..c9b3628452 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 @@ -65,7 +65,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { within(30 seconds) { val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD | ChannelVersion.USE_PUBKEY_KEYPATH) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) 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 bc5787342d..6a668347ad 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 @@ -71,15 +71,15 @@ class ChannelCodecsSpec extends FunSuite { // before we had commitment version, public keys were stored first (they started with 0x02 and 0x03) val legacy02 = hex"02a06ea3081f0f7a8ce31eb4f0822d10d2da120d5a1b1451f0727f51c7372f0f9b" val legacy03 = hex"03d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" - val current02 = hex"010000000002a06ea3081f0f7a8ce31eb4f0822d10d2da120d5a1b1451f0727f51c7372f0f9b" - val current03 = hex"010000000003d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" + val current02 = hex"010000000102a06ea3081f0f7a8ce31eb4f0822d10d2da120d5a1b1451f0727f51c7372f0f9b" + val current03 = hex"010000000103d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" - assert(channelVersionCodec.decode(legacy02.bits) === Attempt.successful(DecodeResult(ChannelVersion.STANDARD, legacy02.bits))) - assert(channelVersionCodec.decode(legacy03.bits) === Attempt.successful(DecodeResult(ChannelVersion.STANDARD, legacy03.bits))) + assert(channelVersionCodec.decode(legacy02.bits) === Attempt.successful(DecodeResult(ChannelVersion.ZEROES, legacy02.bits))) + assert(channelVersionCodec.decode(legacy03.bits) === Attempt.successful(DecodeResult(ChannelVersion.ZEROES, legacy03.bits))) assert(channelVersionCodec.decode(current02.bits) === Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current02.drop(5).bits))) assert(channelVersionCodec.decode(current03.bits) === Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current03.drop(5).bits))) - assert(channelVersionCodec.encode(ChannelVersion.STANDARD) === Attempt.successful(hex"0100000000".bits)) + assert(channelVersionCodec.encode(ChannelVersion.STANDARD) === Attempt.successful(hex"0100000001".bits)) } test("encode/decode localparams") { From 07ff4dcfa838b7427ebcb1eea1253d1f2fec60fa Mon Sep 17 00:00:00 2001 From: sstone Date: Fri, 20 Sep 2019 19:48:49 +0200 Subject: [PATCH 11/11] Move recovery test out of OfflineStateSpec --- .../eclair/channel/ChannelTypesSpec.scala | 4 +- .../acinq/eclair/channel/RecoverySpec.scala | 122 ++++++++++++++++++ .../channel/states/e/OfflineStateSpec.scala | 92 +------------ 3 files changed, 126 insertions(+), 92 deletions(-) create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/channel/RecoverySpec.scala diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala index e9905b9ed7..56131fbb31 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala @@ -1,8 +1,8 @@ package fr.acinq.eclair.channel -import org.scalatest.{FunSuite, ParallelTestExecution} +import org.scalatest.FunSuite -class ChannelTypesSpec extends FunSuite with ParallelTestExecution { +class ChannelTypesSpec extends FunSuite { test("standard channel features include deterministic channel key path") { assert(ChannelVersion.STANDARD.isSet(ChannelVersion.USE_PUBKEY_KEYPATH_BIT)) assert(!ChannelVersion.ZEROES.isSet(ChannelVersion.USE_PUBKEY_KEYPATH_BIT)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RecoverySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RecoverySpec.scala new file mode 100644 index 0000000000..d10a18a266 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RecoverySpec.scala @@ -0,0 +1,122 @@ +package fr.acinq.eclair.channel + +import akka.testkit.TestProbe +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin._ +import fr.acinq.eclair.TestConstants.Alice +import fr.acinq.eclair.blockchain.WatchEventSpent +import fr.acinq.eclair.channel.states.StateTestsHelperMethods +import fr.acinq.eclair.crypto.{Generators, KeyManager} +import fr.acinq.eclair.transactions.Scripts +import fr.acinq.eclair.transactions.Transactions.{ClaimP2WPKHOutputTx, InputInfo} +import fr.acinq.eclair.wire.{ChannelReestablish, CommitSig, Error, Init, RevokeAndAck} +import fr.acinq.eclair.{TestConstants, TestkitBaseClass, _} +import org.scalatest.Outcome + +import scala.concurrent.duration._ + +class RecoverySpec extends TestkitBaseClass with StateTestsHelperMethods { + + type FixtureParam = SetupFixture + + override def withFixture(test: OneArgTest): Outcome = { + val setup = test.tags.contains("disable-offline-mismatch") match { + case false => init() + case true => init(nodeParamsA = Alice.nodeParams.copy(onChainFeeConf = Alice.nodeParams.onChainFeeConf.copy(closeOnOfflineMismatch = false))) + } + import setup._ + within(30 seconds) { + reachNormal(setup) + awaitCond(alice.stateName == NORMAL) + awaitCond(bob.stateName == NORMAL) + withFixture(test.toNoArgTest(setup)) + } + } + + def aliceInit = Init(TestConstants.Alice.nodeParams.globalFeatures, TestConstants.Alice.nodeParams.localFeatures) + + def bobInit = Init(TestConstants.Bob.nodeParams.globalFeatures, TestConstants.Bob.nodeParams.localFeatures) + + test("use funding pubkeys from publish commitment to spend our output") { f => + import f._ + val sender = TestProbe() + + // we start by storing the current state + val oldStateData = alice.stateData + // then we add an htlc and sign it + addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + // alice will receive neither the revocation nor the commit sig + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.expectMsgType[CommitSig] + + // we simulate a disconnection + sender.send(alice, INPUT_DISCONNECTED) + sender.send(bob, INPUT_DISCONNECTED) + awaitCond(alice.stateName == OFFLINE) + awaitCond(bob.stateName == OFFLINE) + + // then we manually replace alice's state with an older one + alice.setState(OFFLINE, oldStateData) + + // then we reconnect them + sender.send(alice, INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit)) + sender.send(bob, INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit)) + + // peers exchange channel_reestablish messages + alice2bob.expectMsgType[ChannelReestablish] + val ce = bob2alice.expectMsgType[ChannelReestablish] + + // alice then realizes it has an old state... + bob2alice.forward(alice) + // ... and ask bob to publish its current commitment + val error = alice2bob.expectMsgType[Error] + assert(new String(error.data.toArray) === PleasePublishYourCommitment(channelId(alice)).getMessage) + + // alice now waits for bob to publish its commitment + awaitCond(alice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) + + // bob is nice and publishes its commitment + val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + + // actual tests starts here: let's see what we can do with Bob's commit tx + sender.send(alice, WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx)) + + // from Bob's commit tx we can extract both funding public keys + val OP_2 :: OP_PUSHDATA(pub1, _) :: OP_PUSHDATA(pub2, _) :: OP_2 :: OP_CHECKMULTISIG :: Nil = Script.parse(bobCommitTx.txIn(0).witness.stack.last) + // from Bob's commit tx we can also extract our p2wpkh output + val ourOutput = bobCommitTx.txOut.find(_.publicKeyScript.length == 22).get + + val OP_0 :: OP_PUSHDATA(pubKeyHash, _) :: Nil = Script.parse(ourOutput.publicKeyScript) + + val keyManager = TestConstants.Alice.nodeParams.keyManager + + // find our funding pub key + val fundingPubKey = Seq(PublicKey(pub1), PublicKey(pub2)).find { + pub => + val channelKeyPath = KeyManager.channelKeyPath(pub) + val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, ce.myCurrentPerCommitmentPoint.get) + localPubkey.hash160 == pubKeyHash + } get + + // compute our to-remote pubkey + val channelKeyPath = KeyManager.channelKeyPath(fundingPubKey) + val ourToRemotePubKey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, ce.myCurrentPerCommitmentPoint.get) + + // spend our output + val tx = Transaction(version = 2, + txIn = TxIn(OutPoint(bobCommitTx, bobCommitTx.txOut.indexOf(ourOutput)), sequence = TxIn.SEQUENCE_FINAL, signatureScript = Nil) :: Nil, + txOut = TxOut(Satoshi(1000), Script.pay2pkh(fr.acinq.eclair.randomKey.publicKey)) :: Nil, + lockTime = 0) + + val sig = keyManager.sign( + ClaimP2WPKHOutputTx(InputInfo(OutPoint(bobCommitTx, bobCommitTx.txOut.indexOf(ourOutput)), ourOutput, Script.pay2pkh(ourToRemotePubKey)), tx), + keyManager.paymentPoint(channelKeyPath), + ce.myCurrentPerCommitmentPoint.get) + val tx1 = tx.updateWitness(0, ScriptWitness(Scripts.der(sig) :: ourToRemotePubKey.value :: Nil)) + Transaction.correctlySpends(tx1, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index bc9909f677..528e384104 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -24,19 +24,14 @@ import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{ByteVector32, ScriptFlags, Transaction} import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair.blockchain.fee.FeeratesPerKw -import fr.acinq.eclair.blockchain.{CurrentBlockCount, CurrentFeerates, PublishAsap, WatchConfirmed, WatchEventSpent} -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, OP_0, OP_2, OP_CHECKMULTISIG, OP_PUSHDATA, OutPoint, Satoshi, Script, ScriptFlags, ScriptWitness, Transaction, TxIn, TxOut} -import fr.acinq.eclair.blockchain.{CurrentBlockCount, PublishAsap, WatchConfirmed, WatchEventSpent} +import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel.Channel.LocalError import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods -import fr.acinq.eclair.crypto.{Generators, KeyManager} import fr.acinq.eclair.payment.CommandBuffer import fr.acinq.eclair.payment.CommandBuffer.CommandSend import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.transactions.Scripts -import fr.acinq.eclair.transactions.Transactions.{ClaimP2WPKHOutputTx, HtlcSuccessTx, InputInfo} +import fr.acinq.eclair.transactions.Transactions.HtlcSuccessTx import fr.acinq.eclair.wire._ import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, TestConstants, TestkitBaseClass, randomBytes32} import org.scalatest.{Outcome, Tag} @@ -543,87 +538,4 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.send(bob, CurrentFeerates(highFeerate)) bob2blockchain.expectMsg(PublishAsap(bobCommitTx)) } - - test("use funding pubkeys from publish commitment to spend our output") { f => - import f._ - val sender = TestProbe() - - // we start by storing the current state - val oldStateData = alice.stateData - // then we add an htlc and sign it - addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) - sender.send(alice, CMD_SIGN) - sender.expectMsg("ok") - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) - // alice will receive neither the revocation nor the commit sig - bob2alice.expectMsgType[RevokeAndAck] - bob2alice.expectMsgType[CommitSig] - - // we simulate a disconnection - sender.send(alice, INPUT_DISCONNECTED) - sender.send(bob, INPUT_DISCONNECTED) - awaitCond(alice.stateName == OFFLINE) - awaitCond(bob.stateName == OFFLINE) - - // then we manually replace alice's state with an older one - alice.setState(OFFLINE, oldStateData) - - // then we reconnect them - sender.send(alice, INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit)) - sender.send(bob, INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit)) - - // peers exchange channel_reestablish messages - alice2bob.expectMsgType[ChannelReestablish] - val ce = bob2alice.expectMsgType[ChannelReestablish] - - // alice then realizes it has an old state... - bob2alice.forward(alice) - // ... and ask bob to publish its current commitment - val error = alice2bob.expectMsgType[Error] - assert(new String(error.data.toArray) === PleasePublishYourCommitment(channelId(alice)).getMessage) - - // alice now waits for bob to publish its commitment - awaitCond(alice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) - - // bob is nice and publishes its commitment - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - - // actual tests starts here: let's see what we can do with Bob's commit tx - sender.send(alice, WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx)) - - // from Bob's commit tx we can extract both funding public keys - val OP_2 :: OP_PUSHDATA(pub1, _) :: OP_PUSHDATA(pub2, _) :: OP_2 :: OP_CHECKMULTISIG :: Nil = Script.parse(bobCommitTx.txIn(0).witness.stack.last) - // from Bob's commit tx we can also extract our p2wpkh output - val ourOutput = bobCommitTx.txOut.find(_.publicKeyScript.length == 22).get - - val OP_0 :: OP_PUSHDATA(pubKeyHash, _) :: Nil = Script.parse(ourOutput.publicKeyScript) - - val keyManager = TestConstants.Alice.nodeParams.keyManager - - // find our funding pub key - val fundingPubKey = Seq(PublicKey(pub1), PublicKey(pub2)).find { - pub => - val channelKeyPath = KeyManager.channelKeyPath(pub) - val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, ce.myCurrentPerCommitmentPoint.get) - localPubkey.hash160 == pubKeyHash - } get - - // compute our to-remote pubkey - val channelKeyPath = KeyManager.channelKeyPath(fundingPubKey) - val ourToRemotePubKey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, ce.myCurrentPerCommitmentPoint.get) - - // spend our output - val tx = Transaction(version = 2, - txIn = TxIn(OutPoint(bobCommitTx, bobCommitTx.txOut.indexOf(ourOutput)), sequence = TxIn.SEQUENCE_FINAL, signatureScript = Nil) :: Nil, - txOut = TxOut(Satoshi(1000), Script.pay2pkh(fr.acinq.eclair.randomKey.publicKey)) :: Nil, - lockTime = 0) - - val sig = keyManager.sign( - ClaimP2WPKHOutputTx(InputInfo(OutPoint(bobCommitTx, bobCommitTx.txOut.indexOf(ourOutput)), ourOutput, Script.pay2pkh(ourToRemotePubKey)), tx), - keyManager.paymentPoint(channelKeyPath), - ce.myCurrentPerCommitmentPoint.get) - val tx1 = tx.updateWitness(0, ScriptWitness(Scripts.der(sig) :: ourToRemotePubKey.value :: Nil)) - Transaction.correctlySpends(tx1, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - } }