1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-23 14:40:34 +01:00

Improve key management (#372)

* generate all channel keys and secrets from the node key and channel number

* use a key manager
the key manager does not export private keys or secrets.
It exports public keys, and points, and provide methods to sign
transaction.
There is just one exception: it does export revocation secrets, since
we need to send them back when we receive a commitment signature.

* key management: cache private keys and public keys

* add key manager to node parameters

* create an interface for key manager
and an implementation which keeps private keys locally

* generate a new BIP32 key path for each new channel

When we create a new channel we generate a new random BIP32 key path
with 128 bits of entropy
This commit is contained in:
Fabrice Drouin 2018-03-01 20:37:39 +01:00 committed by Pierre-Marie Padiou
parent 42fb9a90c1
commit 24dadff625
18 changed files with 435 additions and 250 deletions

View file

@ -7,11 +7,10 @@ import java.sql.DriverManager
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import com.typesafe.config.{Config, ConfigFactory} import com.typesafe.config.{Config, ConfigFactory}
import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{BinaryData, Block}
import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey
import fr.acinq.bitcoin.{BinaryData, Block, DeterministicWallet}
import fr.acinq.eclair.NodeParams.WatcherType import fr.acinq.eclair.NodeParams.WatcherType
import fr.acinq.eclair.channel.Channel import fr.acinq.eclair.channel.Channel
import fr.acinq.eclair.crypto.KeyManager
import fr.acinq.eclair.db._ import fr.acinq.eclair.db._
import fr.acinq.eclair.db.sqlite._ import fr.acinq.eclair.db.sqlite._
import fr.acinq.eclair.wire.Color import fr.acinq.eclair.wire.Color
@ -22,8 +21,7 @@ import scala.concurrent.duration.FiniteDuration
/** /**
* Created by PM on 26/02/2017. * Created by PM on 26/02/2017.
*/ */
case class NodeParams(extendedPrivateKey: ExtendedPrivateKey, case class NodeParams(keyManager: KeyManager,
privateKey: PrivateKey,
alias: String, alias: String,
color: Color, color: Color,
publicAddresses: List[InetSocketAddress], publicAddresses: List[InetSocketAddress],
@ -58,7 +56,8 @@ case class NodeParams(extendedPrivateKey: ExtendedPrivateKey,
watcherType: WatcherType, watcherType: WatcherType,
paymentRequestExpiry: FiniteDuration, paymentRequestExpiry: FiniteDuration,
maxPendingPaymentRequests: Int) { maxPendingPaymentRequests: Int) {
val nodeId = privateKey.publicKey val privateKey = keyManager.nodeKey.privateKey
val nodeId = keyManager.nodeId
} }
object NodeParams { object NodeParams {
@ -82,25 +81,22 @@ object NodeParams {
.withFallback(overrideDefaults) .withFallback(overrideDefaults)
.withFallback(ConfigFactory.load()).getConfig("eclair") .withFallback(ConfigFactory.load()).getConfig("eclair")
def makeNodeParams(datadir: File, config: Config, seed_opt: Option[BinaryData] = None): NodeParams = { def getSeed(datadir: File): BinaryData = {
val seedPath = new File(datadir, "seed.dat")
seedPath.exists() match {
case true => Files.readAllBytes(seedPath.toPath)
case false =>
datadir.mkdirs()
val seed = randomKey.toBin
Files.write(seedPath.toPath, seed)
seed
}
}
def makeNodeParams(datadir: File, config: Config, keyManager: KeyManager): NodeParams = {
datadir.mkdirs() datadir.mkdirs()
val seed: BinaryData = seed_opt match {
case Some(s) => s
case None =>
val seedPath = new File(datadir, "seed.dat")
seedPath.exists() match {
case true => Files.readAllBytes(seedPath.toPath)
case false =>
val seed = randomKey.toBin
Files.write(seedPath.toPath, seed)
seed
}
}
val master = DeterministicWallet.generate(seed)
val extendedPrivateKey = DeterministicWallet.derivePrivateKey(master, DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(0) :: Nil)
val chain = config.getString("chain") val chain = config.getString("chain")
val chainHash = chain match { val chainHash = chain match {
case "test" => Block.TestnetGenesisBlock.hash case "test" => Block.TestnetGenesisBlock.hash
@ -134,8 +130,7 @@ object NodeParams {
require(maxAcceptedHtlcs <= Channel.MAX_ACCEPTED_HTLCS, s"max-accepted-htlcs must be lower than ${Channel.MAX_ACCEPTED_HTLCS}") require(maxAcceptedHtlcs <= Channel.MAX_ACCEPTED_HTLCS, s"max-accepted-htlcs must be lower than ${Channel.MAX_ACCEPTED_HTLCS}")
NodeParams( NodeParams(
extendedPrivateKey = extendedPrivateKey, keyManager = keyManager,
privateKey = extendedPrivateKey.privateKey,
alias = config.getString("node-alias").take(32), alias = config.getString("node-alias").take(32),
color = Color(color.data(0), color.data(1), color.data(2)), color = Color(color.data(0), color.data(1), color.data(2)),
publicAddresses = config.getStringList("server.public-ips").toList.map(ip => new InetSocketAddress(ip, config.getInt("server.port"))), publicAddresses = config.getStringList("server.public-ips").toList.map(ip => new InetSocketAddress(ip, config.getInt("server.port"))),

View file

@ -19,6 +19,7 @@ import fr.acinq.eclair.blockchain.electrum.{ElectrumClient, ElectrumEclairWallet
import fr.acinq.eclair.blockchain.fee.{ConstantFeeProvider, _} import fr.acinq.eclair.blockchain.fee.{ConstantFeeProvider, _}
import fr.acinq.eclair.blockchain.{EclairWallet, _} import fr.acinq.eclair.blockchain.{EclairWallet, _}
import fr.acinq.eclair.channel.Register import fr.acinq.eclair.channel.Register
import fr.acinq.eclair.crypto.LocalKeyManager
import fr.acinq.eclair.io.{Authenticator, Server, Switchboard} import fr.acinq.eclair.io.{Authenticator, Server, Switchboard}
import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment._
import fr.acinq.eclair.router._ import fr.acinq.eclair.router._
@ -30,7 +31,7 @@ import scala.concurrent.{Await, ExecutionContext, Future, Promise}
/** /**
* Setup eclair from a datadir. * Setup eclair from a datadir.
* <p> *
* Created by PM on 25/01/2016. * Created by PM on 25/01/2016.
* *
* @param datadir directory where eclair-core will write/read its data * @param datadir directory where eclair-core will write/read its data
@ -44,9 +45,11 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
logger.info(s"version=${getClass.getPackage.getImplementationVersion} commit=${getClass.getPackage.getSpecificationVersion}") logger.info(s"version=${getClass.getPackage.getImplementationVersion} commit=${getClass.getPackage.getSpecificationVersion}")
logger.info(s"datadir=${datadir.getCanonicalPath}") logger.info(s"datadir=${datadir.getCanonicalPath}")
val config: Config = NodeParams.loadConfiguration(datadir, overrideDefaults) val config = NodeParams.loadConfiguration(datadir, overrideDefaults)
val nodeParams: NodeParams = NodeParams.makeNodeParams(datadir, config, seed_opt) val seed = seed_opt.getOrElse(NodeParams.getSeed(datadir))
val chain: String = config.getString("chain") val keyManager = new LocalKeyManager(seed)
val nodeParams = NodeParams.makeNodeParams(datadir, config, keyManager)
val chain = config.getString("chain")
// early checks // early checks
DBCompatChecker.checkDBCompatibility(nodeParams) DBCompatChecker.checkDBCompatibility(nodeParams)

View file

@ -10,7 +10,7 @@ import fr.acinq.bitcoin._
import fr.acinq.eclair._ import fr.acinq.eclair._
import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel.Helpers.{Closing, Funding} import fr.acinq.eclair.channel.Helpers.{Closing, Funding}
import fr.acinq.eclair.crypto.{Generators, ShaChain, Sphinx} import fr.acinq.eclair.crypto.{Generators, LocalKeyManager, ShaChain, Sphinx}
import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment._
import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions._ import fr.acinq.eclair.transactions._
@ -52,6 +52,7 @@ object Channel {
class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: PublicKey, blockchain: ActorRef, router: ActorRef, relayer: ActorRef, origin_opt: Option[ActorRef] = None)(implicit ec: ExecutionContext = ExecutionContext.Implicits.global) extends FSM[State, Data] with FSMDiagnosticActorLogging[State, Data] { class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: PublicKey, blockchain: ActorRef, router: ActorRef, relayer: ActorRef, origin_opt: Option[ActorRef] = None)(implicit ec: ExecutionContext = ExecutionContext.Implicits.global) extends FSM[State, Data] with FSMDiagnosticActorLogging[State, Data] {
import Channel._ import Channel._
import nodeParams.keyManager
// we pass these to helpers classes so that they have the logging context // we pass these to helpers classes so that they have the logging context
implicit def implicitLog = log implicit def implicitLog = log
@ -106,7 +107,6 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, remote, remoteInit, channelFlags), Nothing) => case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, remote, remoteInit, channelFlags), Nothing) =>
context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, true, temporaryChannelId)) context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, true, temporaryChannelId))
forwarder ! remote forwarder ! remote
val firstPerCommitmentPoint = Generators.perCommitPoint(localParams.shaSeed, 0)
val open = OpenChannel(nodeParams.chainHash, val open = OpenChannel(nodeParams.chainHash,
temporaryChannelId = temporaryChannelId, temporaryChannelId = temporaryChannelId,
fundingSatoshis = fundingSatoshis, fundingSatoshis = fundingSatoshis,
@ -118,12 +118,12 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
feeratePerKw = initialFeeratePerKw, feeratePerKw = initialFeeratePerKw,
toSelfDelay = localParams.toSelfDelay, toSelfDelay = localParams.toSelfDelay,
maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, maxAcceptedHtlcs = localParams.maxAcceptedHtlcs,
fundingPubkey = localParams.fundingPrivKey.publicKey, fundingPubkey = keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey,
revocationBasepoint = localParams.revocationBasepoint, revocationBasepoint = keyManager.revocationPoint(localParams.channelKeyPath).publicKey,
paymentBasepoint = localParams.paymentBasepoint, paymentBasepoint = keyManager.paymentPoint(localParams.channelKeyPath).publicKey,
delayedPaymentBasepoint = localParams.delayedPaymentBasepoint, delayedPaymentBasepoint = keyManager.delayedPaymentPoint(localParams.channelKeyPath).publicKey,
htlcBasepoint = localParams.htlcBasepoint, htlcBasepoint = keyManager.htlcPoint(localParams.channelKeyPath).publicKey,
firstPerCommitmentPoint = firstPerCommitmentPoint, firstPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, 0),
channelFlags = channelFlags) channelFlags = channelFlags)
goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder, open) sending open goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder, open) sending open
@ -187,7 +187,6 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, false, open.temporaryChannelId)) context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, false, open.temporaryChannelId))
// TODO: maybe also check uniqueness of temporary channel id // TODO: maybe also check uniqueness of temporary channel id
val minimumDepth = nodeParams.minDepthBlocks val minimumDepth = nodeParams.minDepthBlocks
val firstPerCommitmentPoint = Generators.perCommitPoint(localParams.shaSeed, 0)
val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId, val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId,
dustLimitSatoshis = localParams.dustLimitSatoshis, dustLimitSatoshis = localParams.dustLimitSatoshis,
maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat,
@ -196,12 +195,12 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
htlcMinimumMsat = localParams.htlcMinimumMsat, htlcMinimumMsat = localParams.htlcMinimumMsat,
toSelfDelay = localParams.toSelfDelay, toSelfDelay = localParams.toSelfDelay,
maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, maxAcceptedHtlcs = localParams.maxAcceptedHtlcs,
fundingPubkey = localParams.fundingPrivKey.publicKey, fundingPubkey = keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey,
revocationBasepoint = localParams.revocationBasepoint, revocationBasepoint = keyManager.revocationPoint(localParams.channelKeyPath).publicKey,
paymentBasepoint = localParams.paymentBasepoint, paymentBasepoint = keyManager.paymentPoint(localParams.channelKeyPath).publicKey,
delayedPaymentBasepoint = localParams.delayedPaymentBasepoint, delayedPaymentBasepoint = keyManager.delayedPaymentPoint(localParams.channelKeyPath).publicKey,
htlcBasepoint = localParams.htlcBasepoint, htlcBasepoint = keyManager.htlcPoint(localParams.channelKeyPath).publicKey,
firstPerCommitmentPoint = firstPerCommitmentPoint) firstPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, 0))
val remoteParams = RemoteParams( val remoteParams = RemoteParams(
nodeId = remoteNodeId, nodeId = remoteNodeId,
dustLimitSatoshis = open.dustLimitSatoshis, dustLimitSatoshis = open.dustLimitSatoshis,
@ -254,7 +253,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
globalFeatures = remoteInit.globalFeatures, globalFeatures = remoteInit.globalFeatures,
localFeatures = remoteInit.localFeatures) localFeatures = remoteInit.localFeatures)
log.debug(s"remote params: $remoteParams") log.debug(s"remote params: $remoteParams")
val localFundingPubkey = localParams.fundingPrivKey.publicKey val localFundingPubkey = keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey
val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey, remoteParams.fundingPubKey))) val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey, remoteParams.fundingPubKey)))
wallet.makeFundingTx(fundingPubkeyScript, Satoshi(fundingSatoshis), fundingTxFeeratePerKw).pipeTo(self) 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) goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, accept.firstPerCommitmentPoint, open)
@ -276,9 +275,9 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions { when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions {
case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex), data@DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, open)) => case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex), data@DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, open)) =>
// let's create the first commitment tx that spends the yet uncommitted funding tx // let's create the first commitment tx that spends the yet uncommitted funding tx
val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.maxFeerateMismatch) val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.maxFeerateMismatch)
require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!")
val localSigOfRemoteTx = Transactions.sign(remoteCommitTx, localParams.fundingPrivKey) val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath))
// signature of their initial commitment tx that pays remote pushMsat // signature of their initial commitment tx that pays remote pushMsat
val fundingCreated = FundingCreated( val fundingCreated = FundingCreated(
temporaryChannelId = temporaryChannelId, temporaryChannelId = temporaryChannelId,
@ -315,11 +314,11 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
when(WAIT_FOR_FUNDING_CREATED)(handleExceptions { when(WAIT_FOR_FUNDING_CREATED)(handleExceptions {
case Event(FundingCreated(_, fundingTxHash, fundingTxOutputIndex, remoteSig), DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, channelFlags, _)) => case Event(FundingCreated(_, fundingTxHash, fundingTxOutputIndex, remoteSig), DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, channelFlags, _)) =>
// they fund the channel with their funding tx, so the money is theirs (but we are paid pushMsat) // 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(temporaryChannelId, localParams, remoteParams, fundingSatoshis: Long, pushMsat, initialFeeratePerKw, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.maxFeerateMismatch) val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, temporaryChannelId, localParams, remoteParams, fundingSatoshis: Long, pushMsat, initialFeeratePerKw, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.maxFeerateMismatch)
// check remote signature validity // check remote signature validity
val localSigOfLocalTx = Transactions.sign(localCommitTx, localParams.fundingPrivKey) val localSigOfLocalTx = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath))
val signedLocalCommitTx = Transactions.addSigs(localCommitTx, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) val signedLocalCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig)
Transactions.checkSpendable(signedLocalCommitTx) match { Transactions.checkSpendable(signedLocalCommitTx) match {
case Failure(cause) => case Failure(cause) =>
log.error(cause, "their FundingCreated message contains an invalid signature") log.error(cause, "their FundingCreated message contains an invalid signature")
@ -328,7 +327,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
// we haven't anything at stake yet, we can just stop // we haven't anything at stake yet, we can just stop
goto(CLOSED) sending error goto(CLOSED) sending error
case Success(_) => case Success(_) =>
val localSigOfRemoteTx = Transactions.sign(remoteCommitTx, localParams.fundingPrivKey) val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath))
val channelId = toLongId(fundingTxHash, fundingTxOutputIndex) val channelId = toLongId(fundingTxHash, fundingTxOutputIndex)
// watch the funding tx transaction // watch the funding tx transaction
val commitInput = localCommitTx.input val commitInput = localCommitTx.input
@ -363,8 +362,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
when(WAIT_FOR_FUNDING_SIGNED)(handleExceptions { when(WAIT_FOR_FUNDING_SIGNED)(handleExceptions {
case Event(FundingSigned(_, remoteSig), DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, localSpec, localCommitTx, remoteCommit, channelFlags, fundingCreated)) => case Event(FundingSigned(_, remoteSig), DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, localSpec, localCommitTx, remoteCommit, channelFlags, fundingCreated)) =>
// we make sure that their sig checks out and that our first commit tx is spendable // we make sure that their sig checks out and that our first commit tx is spendable
val localSigOfLocalTx = Transactions.sign(localCommitTx, localParams.fundingPrivKey) val localSigOfLocalTx = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath))
val signedLocalCommitTx = Transactions.addSigs(localCommitTx, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) val signedLocalCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig)
Transactions.checkSpendable(signedLocalCommitTx) match { Transactions.checkSpendable(signedLocalCommitTx) match {
case Failure(cause) => case Failure(cause) =>
log.error(cause, "their FundingSigned message contains an invalid signature") log.error(cause, "their FundingSigned message contains an invalid signature")
@ -425,7 +424,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Event(WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, blockHeight, txIndex), DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, deferred, _)) => case Event(WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, blockHeight, txIndex), DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, deferred, _)) =>
log.info(s"channelId=${commitments.channelId} was confirmed at blockHeight=$blockHeight txIndex=$txIndex") 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) blockchain ! WatchLost(self, commitments.commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_LOST)
val nextPerCommitmentPoint = Generators.perCommitPoint(commitments.localParams.shaSeed, 1) val nextPerCommitmentPoint = keyManager.commitmentPoint(commitments.localParams.channelKeyPath, 1)
val fundingLocked = FundingLocked(commitments.channelId, nextPerCommitmentPoint) val fundingLocked = FundingLocked(commitments.channelId, nextPerCommitmentPoint)
deferred.map(self ! _) deferred.map(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 // 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
@ -590,7 +589,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
log.debug("ignoring CMD_SIGN (nothing to sign)") log.debug("ignoring CMD_SIGN (nothing to sign)")
stay stay
case Right(_) => case Right(_) =>
Try(Commitments.sendCommit(d.commitments)) match { Try(Commitments.sendCommit(d.commitments, keyManager)) match {
case Success((commitments1, commit)) => case Success((commitments1, commit)) =>
log.debug(s"sending a new sig, spec:\n${Commitments.specs2String(commitments1)}") log.debug(s"sending a new sig, spec:\n${Commitments.specs2String(commitments1)}")
commitments1.localChanges.signed.collect { commitments1.localChanges.signed.collect {
@ -609,7 +608,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
} }
case Event(commit: CommitSig, d: DATA_NORMAL) => case Event(commit: CommitSig, d: DATA_NORMAL) =>
Try(Commitments.receiveCommit(d.commitments, commit)) match { Try(Commitments.receiveCommit(d.commitments, commit, keyManager)) match {
case Success((commitments1, revocation)) => case Success((commitments1, revocation)) =>
log.debug(s"received a new sig, spec:\n${Commitments.specs2String(commitments1)}") log.debug(s"received a new sig, spec:\n${Commitments.specs2String(commitments1)}")
if (Commitments.localHasChanges(commitments1)) { if (Commitments.localHasChanges(commitments1)) {
@ -713,7 +712,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
// there are no pending signed htlcs, let's go directly to NEGOTIATING // there are no pending signed htlcs, let's go directly to NEGOTIATING
if (d.commitments.localParams.isFunder) { if (d.commitments.localParams.isFunder) {
// we are funder, need to initiate the negotiation by sending the first closing_signed // we are funder, need to initiate the negotiation by sending the first closing_signed
val (closingTx, closingSigned) = Closing.makeFirstClosingTx(d.commitments, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey) val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey)
goto(NEGOTIATING) using store(DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None)) sending sendList :+ closingSigned goto(NEGOTIATING) using store(DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None)) sending sendList :+ closingSigned
} else { } else {
// we are fundee, will wait for their closing_signed // we are fundee, will wait for their closing_signed
@ -769,7 +768,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
require(d.shortChannelId == remoteAnnSigs.shortChannelId, s"shortChannelId mismatch: local=${d.shortChannelId.toHexString} remote=${remoteAnnSigs.shortChannelId.toHexString}") require(d.shortChannelId == remoteAnnSigs.shortChannelId, s"shortChannelId mismatch: local=${d.shortChannelId.toHexString} remote=${remoteAnnSigs.shortChannelId.toHexString}")
log.info(s"announcing channelId=${d.channelId} on the network with shortId=${d.shortChannelId.toHexString}") log.info(s"announcing channelId=${d.channelId} on the network with shortId=${d.shortChannelId.toHexString}")
import d.commitments.{localParams, remoteParams} import d.commitments.{localParams, remoteParams}
val channelAnn = Announcements.makeChannelAnnouncement(nodeParams.chainHash, localAnnSigs.shortChannelId, localParams.nodeId, remoteParams.nodeId, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, localAnnSigs.nodeSignature, remoteAnnSigs.nodeSignature, localAnnSigs.bitcoinSignature, remoteAnnSigs.bitcoinSignature) 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)
// we use GOTO instead of stay because we want to fire transitions // we use GOTO instead of stay because we want to fire transitions
goto(NORMAL) using store(d.copy(channelAnnouncement = Some(channelAnn))) goto(NORMAL) using store(d.copy(channelAnnouncement = Some(channelAnn)))
case Some(_) => case Some(_) =>
@ -899,7 +898,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
log.debug("ignoring CMD_SIGN (nothing to sign)") log.debug("ignoring CMD_SIGN (nothing to sign)")
stay stay
case Right(_) => case Right(_) =>
Try(Commitments.sendCommit(d.commitments)) match { Try(Commitments.sendCommit(d.commitments, keyManager)) match {
case Success((commitments1, commit)) => case Success((commitments1, commit)) =>
log.debug(s"sending a new sig, spec:\n${Commitments.specs2String(commitments1)}") log.debug(s"sending a new sig, spec:\n${Commitments.specs2String(commitments1)}")
commitments1.localChanges.signed.collect { commitments1.localChanges.signed.collect {
@ -917,7 +916,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
} }
case Event(commit: CommitSig, d@DATA_SHUTDOWN(_, localShutdown, remoteShutdown)) => case Event(commit: CommitSig, d@DATA_SHUTDOWN(_, localShutdown, remoteShutdown)) =>
Try(Commitments.receiveCommit(d.commitments, commit)) map { Try(Commitments.receiveCommit(d.commitments, commit, keyManager)) map {
case (commitments1, revocation) => case (commitments1, revocation) =>
// we always reply with a revocation // we always reply with a revocation
log.debug(s"received a new sig:\n${Commitments.specs2String(commitments1)}") log.debug(s"received a new sig:\n${Commitments.specs2String(commitments1)}")
@ -927,7 +926,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Success((commitments1, revocation)) if commitments1.hasNoPendingHtlcs => case Success((commitments1, revocation)) if commitments1.hasNoPendingHtlcs =>
if (d.commitments.localParams.isFunder) { if (d.commitments.localParams.isFunder) {
// we are funder, need to initiate the negotiation by sending the first closing_signed // we are funder, need to initiate the negotiation by sending the first closing_signed
val (closingTx, closingSigned) = Closing.makeFirstClosingTx(commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey) val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey)
goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None)) sending revocation :: closingSigned :: Nil goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None)) sending revocation :: closingSigned :: Nil
} else { } else {
// we are fundee, will wait for their closing_signed // we are fundee, will wait for their closing_signed
@ -950,7 +949,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
log.debug(s"received a new rev, switching to NEGOTIATING spec:\n${Commitments.specs2String(commitments1)}") log.debug(s"received a new rev, switching to NEGOTIATING spec:\n${Commitments.specs2String(commitments1)}")
if (d.commitments.localParams.isFunder) { if (d.commitments.localParams.isFunder) {
// we are funder, need to initiate the negotiation by sending the first closing_signed // we are funder, need to initiate the negotiation by sending the first closing_signed
val (closingTx, closingSigned) = Closing.makeFirstClosingTx(commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey) val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey)
goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None)) sending closingSigned goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None)) sending closingSigned
} else { } else {
// we are fundee, will wait for their closing_signed // we are fundee, will wait for their closing_signed
@ -1000,7 +999,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
when(NEGOTIATING)(handleExceptions { when(NEGOTIATING)(handleExceptions {
case Event(c@ClosingSigned(_, remoteClosingFee, remoteSig), d: DATA_NEGOTIATING) => case Event(c@ClosingSigned(_, remoteClosingFee, remoteSig), d: DATA_NEGOTIATING) =>
log.info(s"received closingFeeSatoshis=$remoteClosingFee") log.info(s"received closingFeeSatoshis=$remoteClosingFee")
Closing.checkClosingSignature(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, Satoshi(remoteClosingFee), remoteSig) match { Closing.checkClosingSignature(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, Satoshi(remoteClosingFee), remoteSig) match {
case Success(signedClosingTx) if Some(remoteClosingFee) == d.closingTxProposed.last.lastOption.map(_.localClosingSigned.feeSatoshis) || d.closingTxProposed.flatten.size >= MAX_NEGOTIATION_ITERATIONS => case Success(signedClosingTx) if Some(remoteClosingFee) == d.closingTxProposed.last.lastOption.map(_.localClosingSigned.feeSatoshis) || d.closingTxProposed.flatten.size >= MAX_NEGOTIATION_ITERATIONS =>
// we close when we converge or when there were too many iterations // we close when we converge or when there were too many iterations
handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx))))
@ -1010,7 +1009,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
val nextClosingFee = Closing.nextClosingFee( val nextClosingFee = Closing.nextClosingFee(
localClosingFee = lastLocalClosingFee.getOrElse(Closing.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey)), localClosingFee = lastLocalClosingFee.getOrElse(Closing.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey)),
remoteClosingFee = Satoshi(remoteClosingFee)) remoteClosingFee = Satoshi(remoteClosingFee))
val (closingTx, closingSigned) = Closing.makeClosingTx(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nextClosingFee) val (closingTx, closingSigned) = Closing.makeClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nextClosingFee)
if (Some(nextClosingFee) == lastLocalClosingFee) { if (Some(nextClosingFee) == lastLocalClosingFee) {
// next computed fee is the same than the one we previously sent (probably because of rounding), let's close now // next computed fee is the same than the one we previously sent (probably because of rounding), let's close now
handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx))))
@ -1053,19 +1052,19 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
log.info(s"got valid payment preimage, recalculating transactions to redeem the corresponding htlc on-chain") log.info(s"got valid payment preimage, recalculating transactions to redeem the corresponding htlc on-chain")
val localCommitPublished1 = d.localCommitPublished.map { val localCommitPublished1 = d.localCommitPublished.map {
case localCommitPublished => case localCommitPublished =>
val localCommitPublished1 = Helpers.Closing.claimCurrentLocalCommitTxOutputs(commitments1, localCommitPublished.commitTx) val localCommitPublished1 = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, commitments1, localCommitPublished.commitTx)
doPublish(localCommitPublished1) doPublish(localCommitPublished1)
localCommitPublished1 localCommitPublished1
} }
val remoteCommitPublished1 = d.remoteCommitPublished.map { val remoteCommitPublished1 = d.remoteCommitPublished.map {
case remoteCommitPublished => case remoteCommitPublished =>
val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx) val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx)
doPublish(remoteCommitPublished1) doPublish(remoteCommitPublished1)
remoteCommitPublished1 remoteCommitPublished1
} }
val nextRemoteCommitPublished1 = d.nextRemoteCommitPublished.map { val nextRemoteCommitPublished1 = d.nextRemoteCommitPublished.map {
case remoteCommitPublished => case remoteCommitPublished =>
val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx) val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx)
doPublish(remoteCommitPublished1) doPublish(remoteCommitPublished1)
remoteCommitPublished1 remoteCommitPublished1
} }
@ -1136,7 +1135,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
} }
// then let's see if any of the possible close scenarii can be considered done // then let's see if any of the possible close scenarii can be considered done
val mutualCloseDone = d.mutualClosePublished.exists(_.txid == tx.txid) // this case is trivial, in a mutual close scenario we only need to make sure that one of the closing txes is confirmed val mutualCloseDone = d.mutualClosePublished.exists(_.txid == tx.txid) // this case is trivial, in a mutual close scenario we only need to make sure that one of the closing txes is confirmed
val localCommitDone = localCommitPublished1.map(Closing.isLocalCommitDone(_)).getOrElse(false) val localCommitDone = localCommitPublished1.map(Closing.isLocalCommitDone(_)).getOrElse(false)
val remoteCommitDone = remoteCommitPublished1.map(Closing.isRemoteCommitDone(_)).getOrElse(false) val remoteCommitDone = remoteCommitPublished1.map(Closing.isRemoteCommitDone(_)).getOrElse(false)
val nextRemoteCommitDone = nextRemoteCommitPublished1.map(Closing.isRemoteCommitDone(_)).getOrElse(false) val nextRemoteCommitDone = nextRemoteCommitPublished1.map(Closing.isRemoteCommitDone(_)).getOrElse(false)
val futureRemoteCommitDone = futureRemoteCommitPublished1.map(Closing.isRemoteCommitDone(_)).getOrElse(false) val futureRemoteCommitDone = futureRemoteCommitPublished1.map(Closing.isRemoteCommitDone(_)).getOrElse(false)
@ -1212,7 +1211,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
forwarder ! r forwarder ! r
val yourLastPerCommitmentSecret = d.commitments.remotePerCommitmentSecrets.lastIndex.flatMap(d.commitments.remotePerCommitmentSecrets.getHash).getOrElse(Sphinx zeroes 32) val yourLastPerCommitmentSecret = d.commitments.remotePerCommitmentSecrets.lastIndex.flatMap(d.commitments.remotePerCommitmentSecrets.getHash).getOrElse(Sphinx zeroes 32)
val myCurrentPerCommitmentPoint = Generators.perCommitPoint(d.commitments.localParams.shaSeed, d.commitments.localCommit.index) val myCurrentPerCommitmentPoint = keyManager.commitmentPoint(d.commitments.localParams.channelKeyPath, d.commitments.localCommit.index)
val channelReestablish = ChannelReestablish( val channelReestablish = ChannelReestablish(
channelId = d.channelId, channelId = d.channelId,
@ -1252,7 +1251,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_LOCKED) => case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_LOCKED) =>
log.debug(s"re-sending fundingLocked") log.debug(s"re-sending fundingLocked")
val nextPerCommitmentPoint = Generators.perCommitPoint(d.commitments.localParams.shaSeed, 1) val nextPerCommitmentPoint = keyManager.commitmentPoint(d.commitments.localParams.channelKeyPath, 1)
val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint) val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint)
goto(WAIT_FOR_FUNDING_LOCKED) sending fundingLocked goto(WAIT_FOR_FUNDING_LOCKED) sending fundingLocked
@ -1260,7 +1259,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
if d.commitments.localCommit.index < nextRemoteRevocationNumber => if d.commitments.localCommit.index < 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 // 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 // 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 (Generators.perCommitSecret(d.commitments.localParams.shaSeed, nextRemoteRevocationNumber - 1) == yourLastPerCommitmentSecret) { if (keyManager.commitmentSecret(d.commitments.localParams.channelKeyPath, nextRemoteRevocationNumber - 1) == yourLastPerCommitmentSecret) {
log.warning(s"counterparty proved that we have an outdated (revoked) local commitment!!! ourCommitmentNumber=${d.commitments.localCommit.index} theirCommitmentNumber=${nextRemoteRevocationNumber}") 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 // 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 // would punish us by taking all the funds in the channel
@ -1276,7 +1275,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
if (channelReestablish.nextLocalCommitmentNumber == 1 && d.commitments.localCommit.index == 0) { 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 // 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") log.debug(s"re-sending fundingLocked")
val nextPerCommitmentPoint = Generators.perCommitPoint(d.commitments.localParams.shaSeed, 1) val nextPerCommitmentPoint = keyManager.commitmentPoint(d.commitments.localParams.channelKeyPath, 1)
val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint) val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint)
forwarder ! fundingLocked forwarder ! fundingLocked
} }
@ -1325,7 +1324,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
// note: in any case we still need to keep all previously sent closing_signed, because they may publish one of them // note: in any case we still need to keep all previously sent closing_signed, because they may publish one of them
if (d.commitments.localParams.isFunder) { if (d.commitments.localParams.isFunder) {
// we could use the last closing_signed we sent, but network fees may have changed while we were offline so it is better to restart from scratch // we could use the last closing_signed we sent, but network fees may have changed while we were offline so it is better to restart from scratch
val (closingTx, closingSigned) = Closing.makeFirstClosingTx(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey) val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey)
val closingTxProposed1 = d.closingTxProposed :+ List(ClosingTxProposed(closingTx.tx, closingSigned)) val closingTxProposed1 = d.closingTxProposed :+ List(ClosingTxProposed(closingTx.tx, closingSigned))
goto(NEGOTIATING) using store(d.copy(closingTxProposed = closingTxProposed1)) sending d.localShutdown :: closingSigned :: Nil goto(NEGOTIATING) using store(d.copy(closingTxProposed = closingTxProposed1)) sending d.localShutdown :: closingSigned :: Nil
} else { } else {
@ -1543,7 +1542,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
} else { } else {
val commitTx = d.commitments.localCommit.publishableTxs.commitTx.tx val commitTx = d.commitments.localCommit.publishableTxs.commitTx.tx
val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(d.commitments, commitTx) val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, d.commitments, commitTx)
doPublish(localCommitPublished) doPublish(localCommitPublished)
val nextData = d match { val nextData = d match {
@ -1614,7 +1613,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
log.warning(s"they published their current commit in txid=${commitTx.txid}") log.warning(s"they published their current commit in txid=${commitTx.txid}")
require(commitTx.txid == d.commitments.remoteCommit.txid, "txid mismatch") require(commitTx.txid == d.commitments.remoteCommit.txid, "txid mismatch")
val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(d.commitments, d.commitments.remoteCommit, commitTx) val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, d.commitments, d.commitments.remoteCommit, commitTx)
doPublish(remoteCommitPublished) doPublish(remoteCommitPublished)
val nextData = d match { val nextData = d match {
@ -1630,7 +1629,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
log.warning(s"they published their future commit (because we asked them to) in txid=${commitTx.txid}") log.warning(s"they published their future commit (because we asked them to) in txid=${commitTx.txid}")
// if we are in this state, then this field is defined // if we are in this state, then this field is defined
val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint.get val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint.get
val remoteCommitPublished = Helpers.Closing.claimRemoteCommitMainOutput(d.commitments, remotePerCommitmentPoint, commitTx) val remoteCommitPublished = Helpers.Closing.claimRemoteCommitMainOutput(keyManager, d.commitments, remotePerCommitmentPoint, commitTx)
val nextData = DATA_CLOSING(d.commitments, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) val nextData = DATA_CLOSING(d.commitments, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished))
doPublish(remoteCommitPublished) doPublish(remoteCommitPublished)
@ -1643,7 +1642,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
val remoteCommit = d.commitments.remoteNextCommitInfo.left.get.nextRemoteCommit val remoteCommit = d.commitments.remoteNextCommitInfo.left.get.nextRemoteCommit
require(commitTx.txid == remoteCommit.txid, "txid mismatch") require(commitTx.txid == remoteCommit.txid, "txid mismatch")
val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(d.commitments, remoteCommit, commitTx) val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, d.commitments, remoteCommit, commitTx)
doPublish(remoteCommitPublished) doPublish(remoteCommitPublished)
val nextData = d match { val nextData = d match {
@ -1675,7 +1674,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
def handleRemoteSpentOther(tx: Transaction, d: HasCommitments) = { def handleRemoteSpentOther(tx: Transaction, d: HasCommitments) = {
log.warning(s"funding tx spent in txid=${tx.txid}") log.warning(s"funding tx spent in txid=${tx.txid}")
Helpers.Closing.claimRevokedRemoteCommitTxOutputs(d.commitments, tx) match { Helpers.Closing.claimRevokedRemoteCommitTxOutputs(keyManager, d.commitments, tx) match {
case Some(revokedCommitPublished) => case Some(revokedCommitPublished) =>
log.warning(s"txid=${tx.txid} was a revoked commitment, publishing the penalty tx") log.warning(s"txid=${tx.txid} was a revoked commitment, publishing the penalty tx")
val exc = FundingTxSpent(d.channelId, tx) val exc = FundingTxSpent(d.channelId, tx)
@ -1721,7 +1720,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
// let's try to spend our current local tx // let's try to spend our current local tx
val commitTx = d.commitments.localCommit.publishableTxs.commitTx.tx val commitTx = d.commitments.localCommit.publishableTxs.commitTx.tx
val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(d.commitments, commitTx) val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, d.commitments, commitTx)
doPublish(localCommitPublished) doPublish(localCommitPublished)
goto(ERR_INFORMATION_LEAK) sending error goto(ERR_INFORMATION_LEAK) sending error
@ -1746,8 +1745,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
} else if (commitments1.localCommit.index == channelReestablish.nextRemoteRevocationNumber + 1) { } else if (commitments1.localCommit.index == channelReestablish.nextRemoteRevocationNumber + 1) {
// our last revocation got lost, let's resend it // our last revocation got lost, let's resend it
log.debug(s"re-sending last revocation") log.debug(s"re-sending last revocation")
val localPerCommitmentSecret = Generators.perCommitSecret(commitments1.localParams.shaSeed, d.commitments.localCommit.index - 1) val localPerCommitmentSecret = keyManager.commitmentSecret(commitments1.localParams.channelKeyPath, d.commitments.localCommit.index - 1)
val localNextPerCommitmentPoint = Generators.perCommitPoint(commitments1.localParams.shaSeed, d.commitments.localCommit.index + 1) val localNextPerCommitmentPoint = keyManager.commitmentPoint(commitments1.localParams.channelKeyPath, d.commitments.localCommit.index + 1)
val revocation = RevokeAndAck( val revocation = RevokeAndAck(
channelId = commitments1.channelId, channelId = commitments1.channelId,
perCommitmentSecret = localPerCommitmentSecret, perCommitmentSecret = localPerCommitmentSecret,

View file

@ -2,10 +2,10 @@ package fr.acinq.eclair.channel
import akka.actor.ActorRef import akka.actor.ActorRef
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar} import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar}
import fr.acinq.bitcoin.{BinaryData, OutPoint, Transaction} import fr.acinq.bitcoin.{BinaryData, DeterministicWallet, OutPoint, Transaction}
import fr.acinq.eclair.UInt64 import fr.acinq.eclair.UInt64
import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.crypto.{Sphinx}
import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.{CommitmentSpec, Transactions}
import fr.acinq.eclair.transactions.Transactions.CommitTx import fr.acinq.eclair.transactions.Transactions.CommitTx
import fr.acinq.eclair.wire.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel, Shutdown, UpdateAddHtlc} import fr.acinq.eclair.wire.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel, Shutdown, UpdateAddHtlc}
@ -168,28 +168,17 @@ 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 DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(commitments: Commitments, remoteChannelReestablish: ChannelReestablish) extends Data with HasCommitments
final case class LocalParams(nodeId: PublicKey, final case class LocalParams(nodeId: PublicKey,
channelKeyPath: DeterministicWallet.KeyPath,
dustLimitSatoshis: Long, dustLimitSatoshis: Long,
maxHtlcValueInFlightMsat: UInt64, maxHtlcValueInFlightMsat: UInt64,
channelReserveSatoshis: Long, channelReserveSatoshis: Long,
htlcMinimumMsat: Long, htlcMinimumMsat: Long,
toSelfDelay: Int, toSelfDelay: Int,
maxAcceptedHtlcs: Int, maxAcceptedHtlcs: Int,
fundingPrivKey: PrivateKey,
revocationSecret: Scalar,
paymentKey: Scalar,
delayedPaymentKey: Scalar,
htlcKey: Scalar,
defaultFinalScriptPubKey: BinaryData,
shaSeed: BinaryData,
isFunder: Boolean, isFunder: Boolean,
defaultFinalScriptPubKey: BinaryData,
globalFeatures: BinaryData, globalFeatures: BinaryData,
localFeatures: BinaryData) { localFeatures: BinaryData)
// precomputed for performance reasons
val paymentBasepoint = paymentKey.toPoint
val delayedPaymentBasepoint = delayedPaymentKey.toPoint
val revocationBasepoint = revocationSecret.toPoint
val htlcBasepoint = htlcKey.toPoint
}
final case class RemoteParams(nodeId: PublicKey, final case class RemoteParams(nodeId: PublicKey,
dustLimitSatoshis: Long, dustLimitSatoshis: Long,

View file

@ -3,8 +3,8 @@ package fr.acinq.eclair.channel
import akka.event.LoggingAdapter import akka.event.LoggingAdapter
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, sha256} import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, sha256}
import fr.acinq.bitcoin.{BinaryData, Crypto, Satoshi, Transaction} import fr.acinq.bitcoin.{BinaryData, Crypto, Satoshi, Transaction}
import fr.acinq.eclair.crypto.{Generators, ShaChain, Sphinx} import fr.acinq.eclair.crypto.{Generators, KeyManager, ShaChain, Sphinx}
import fr.acinq.eclair.payment.{Origin, PaymentLifecycle} import fr.acinq.eclair.payment.Origin
import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions._ import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire._ import fr.acinq.eclair.wire._
@ -346,7 +346,7 @@ object Commitments {
def revocationHash(seed: BinaryData, index: Long): BinaryData = Crypto.sha256(revocationPreimage(seed, index)) def revocationHash(seed: BinaryData, index: Long): BinaryData = Crypto.sha256(revocationPreimage(seed, index))
def sendCommit(commitments: Commitments): (Commitments, CommitSig) = { def sendCommit(commitments: Commitments, keyManager: KeyManager): (Commitments, CommitSig) = {
import commitments._ import commitments._
commitments.remoteNextCommitInfo match { commitments.remoteNextCommitInfo match {
case Right(_) if !localHasChanges(commitments) => case Right(_) if !localHasChanges(commitments) =>
@ -354,12 +354,11 @@ object Commitments {
case Right(remoteNextPerCommitmentPoint) => case Right(remoteNextPerCommitmentPoint) =>
// remote commitment will includes all local changes + remote acked changes // remote commitment will includes all local changes + remote acked changes
val spec = CommitmentSpec.reduce(remoteCommit.spec, remoteChanges.acked, localChanges.proposed) val spec = CommitmentSpec.reduce(remoteCommit.spec, remoteChanges.acked, localChanges.proposed)
val (remoteCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeRemoteTxs(remoteCommit.index + 1, localParams, remoteParams, commitInput, remoteNextPerCommitmentPoint, spec) val (remoteCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeRemoteTxs(keyManager, remoteCommit.index + 1, localParams, remoteParams, commitInput, remoteNextPerCommitmentPoint, spec)
val sig = Transactions.sign(remoteCommitTx, localParams.fundingPrivKey) val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath))
val sortedHtlcTxs: Seq[TransactionWithInputInfo] = (htlcTimeoutTxs ++ htlcSuccessTxs).sortBy(_.input.outPoint.index) val sortedHtlcTxs: Seq[TransactionWithInputInfo] = (htlcTimeoutTxs ++ htlcSuccessTxs).sortBy(_.input.outPoint.index)
val htlcKey = Generators.derivePrivKey(localParams.htlcKey, remoteNextPerCommitmentPoint) val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(localParams.channelKeyPath), remoteNextPerCommitmentPoint))
val htlcSigs = sortedHtlcTxs.map(Transactions.sign(_, htlcKey))
// don't sign if they don't get paid // don't sign if they don't get paid
val commitSig = CommitSig( val commitSig = CommitSig(
@ -378,7 +377,7 @@ object Commitments {
} }
} }
def receiveCommit(commitments: Commitments, commit: CommitSig)(implicit log: LoggingAdapter): (Commitments, RevokeAndAck) = { def receiveCommit(commitments: Commitments, commit: CommitSig, keyManager: KeyManager)(implicit log: LoggingAdapter): (Commitments, RevokeAndAck) = {
import commitments._ import commitments._
// they sent us a signature for *their* view of *our* next commit tx // they sent us a signature for *their* view of *our* next commit tx
// so in terms of rev.hashes and indexes we have: // so in terms of rev.hashes and indexes we have:
@ -397,14 +396,14 @@ object Commitments {
// receiving money i.e its commit tx has one output for them // receiving money i.e its commit tx has one output for them
val spec = CommitmentSpec.reduce(localCommit.spec, localChanges.acked, remoteChanges.proposed) val spec = CommitmentSpec.reduce(localCommit.spec, localChanges.acked, remoteChanges.proposed)
val localPerCommitmentPoint = Generators.perCommitPoint(localParams.shaSeed, commitments.localCommit.index + 1) val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, commitments.localCommit.index + 1)
val (localCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeLocalTxs(localCommit.index + 1, localParams, remoteParams, commitInput, localPerCommitmentPoint, spec) val (localCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeLocalTxs(keyManager, localCommit.index + 1, localParams, remoteParams, commitInput, localPerCommitmentPoint, spec)
val sig = Transactions.sign(localCommitTx, localParams.fundingPrivKey) val sig = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath))
// TODO: should we have optional sig? (original comment: this tx will NOT be signed if our output is empty) // 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 // no need to compute htlc sigs if commit sig doesn't check out
val signedCommitTx = Transactions.addSigs(localCommitTx, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, sig, commit.signature) val signedCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, sig, commit.signature)
if (Transactions.checkSpendable(signedCommitTx).isFailure) { if (Transactions.checkSpendable(signedCommitTx).isFailure) {
throw InvalidCommitmentSignature(commitments.channelId, signedCommitTx.tx) throw InvalidCommitmentSignature(commitments.channelId, signedCommitTx.tx)
} }
@ -413,8 +412,7 @@ object Commitments {
if (commit.htlcSignatures.size != sortedHtlcTxs.size) { if (commit.htlcSignatures.size != sortedHtlcTxs.size) {
throw new HtlcSigCountMismatch(commitments.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size) throw new HtlcSigCountMismatch(commitments.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size)
} }
val localHtlcKey = Generators.derivePrivKey(localParams.htlcKey, localPerCommitmentPoint) val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(localParams.channelKeyPath), localPerCommitmentPoint))
val htlcSigs = sortedHtlcTxs.map(Transactions.sign(_, localHtlcKey))
val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint)
// combine the sigs to make signed txes // combine the sigs to make signed txes
val htlcTxsAndSigs = (sortedHtlcTxs, htlcSigs, commit.htlcSignatures).zipped.toList.collect { val htlcTxsAndSigs = (sortedHtlcTxs, htlcSigs, commit.htlcSignatures).zipped.toList.collect {
@ -432,8 +430,8 @@ object Commitments {
} }
// we will send our revocation preimage + our next revocation hash // we will send our revocation preimage + our next revocation hash
val localPerCommitmentSecret = Generators.perCommitSecret(localParams.shaSeed, commitments.localCommit.index) val localPerCommitmentSecret = keyManager.commitmentSecret(localParams.channelKeyPath, commitments.localCommit.index)
val localNextPerCommitmentPoint = Generators.perCommitPoint(localParams.shaSeed, commitments.localCommit.index + 2) val localNextPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, commitments.localCommit.index + 2)
val revocation = RevokeAndAck( val revocation = RevokeAndAck(
channelId = commitments.channelId, channelId = commitments.channelId,
perCommitmentSecret = localPerCommitmentSecret, perCommitmentSecret = localPerCommitmentSecret,
@ -478,26 +476,26 @@ object Commitments {
} }
} }
def makeLocalTxs(commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, localPerCommitmentPoint: Point, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { def makeLocalTxs(keyManager: KeyManager, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, localPerCommitmentPoint: Point, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = {
val localPaymentPubkey = Generators.derivePubKey(localParams.paymentBasepoint, localPerCommitmentPoint) val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(localParams.channelKeyPath).publicKey, localPerCommitmentPoint)
val localDelayedPaymentPubkey = Generators.derivePubKey(localParams.delayedPaymentBasepoint, localPerCommitmentPoint) val localDelayedPaymentPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(localParams.channelKeyPath).publicKey, localPerCommitmentPoint)
val localHtlcPubkey = Generators.derivePubKey(localParams.htlcBasepoint, localPerCommitmentPoint) val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(localParams.channelKeyPath).publicKey, localPerCommitmentPoint)
val remotePaymentPubkey = Generators.derivePubKey(remoteParams.paymentBasepoint, localPerCommitmentPoint) val remotePaymentPubkey = Generators.derivePubKey(remoteParams.paymentBasepoint, localPerCommitmentPoint)
val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint)
val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint)
val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, localParams.paymentBasepoint, remoteParams.paymentBasepoint, localParams.isFunder, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec) val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, keyManager.paymentPoint(localParams.channelKeyPath).publicKey, remoteParams.paymentBasepoint, localParams.isFunder, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec)
val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec) val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec)
(commitTx, htlcTimeoutTxs, htlcSuccessTxs) (commitTx, htlcTimeoutTxs, htlcSuccessTxs)
} }
def makeRemoteTxs(commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, remotePerCommitmentPoint: Point, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { def makeRemoteTxs(keyManager: KeyManager, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, remotePerCommitmentPoint: Point, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = {
val localPaymentPubkey = Generators.derivePubKey(localParams.paymentBasepoint, remotePerCommitmentPoint) val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint)
val localHtlcPubkey = Generators.derivePubKey(localParams.htlcBasepoint, remotePerCommitmentPoint) val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint)
val remotePaymentPubkey = Generators.derivePubKey(remoteParams.paymentBasepoint, remotePerCommitmentPoint) val remotePaymentPubkey = Generators.derivePubKey(remoteParams.paymentBasepoint, remotePerCommitmentPoint)
val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint)
val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint)
val remoteRevocationPubkey = Generators.revocationPubKey(localParams.revocationBasepoint, remotePerCommitmentPoint) val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint)
val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, localParams.paymentBasepoint, !localParams.isFunder, Satoshi(remoteParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec) val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, keyManager.paymentPoint(localParams.channelKeyPath).publicKey, !localParams.isFunder, Satoshi(remoteParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec)
val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, Satoshi(remoteParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec) val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, Satoshi(remoteParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec)
(commitTx, htlcTimeoutTxs, htlcSuccessTxs) (commitTx, htlcTimeoutTxs, htlcSuccessTxs)
} }

View file

@ -1,12 +1,11 @@
package fr.acinq.eclair.channel package fr.acinq.eclair.channel
import akka.event.LoggingAdapter import akka.event.LoggingAdapter
import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar, ripemd160, sha256} import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar, ripemd160, sha256}
import fr.acinq.bitcoin.Script._ import fr.acinq.bitcoin.Script._
import fr.acinq.bitcoin.{OutPoint, _} import fr.acinq.bitcoin.{OutPoint, _}
import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.blockchain.EclairWallet
import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.crypto.{Generators, KeyManager}
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions.Scripts._ import fr.acinq.eclair.transactions.Scripts._
import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions._ import fr.acinq.eclair.transactions._
@ -97,7 +96,7 @@ object Helpers {
def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: Long) = { def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: Long) = {
// TODO: empty features // TODO: empty features
val features = BinaryData("") val features = BinaryData("")
val (localNodeSig, localBitcoinSig) = Announcements.signChannelAnnouncement(nodeParams.chainHash, shortChannelId, nodeParams.privateKey, commitments.remoteParams.nodeId, commitments.localParams.fundingPrivKey, commitments.remoteParams.fundingPubKey, features) val (localNodeSig, localBitcoinSig) = nodeParams.keyManager.signChannelAnnouncement(commitments.localParams.channelKeyPath, nodeParams.chainHash, shortChannelId, commitments.remoteParams.nodeId, commitments.remoteParams.fundingPubKey, features)
AnnouncementSignatures(commitments.channelId, shortChannelId, localNodeSig, localBitcoinSig) AnnouncementSignatures(commitments.channelId, shortChannelId, localNodeSig, localBitcoinSig)
} }
@ -130,7 +129,7 @@ object Helpers {
* @param remoteFirstPerCommitmentPoint * @param remoteFirstPerCommitmentPoint
* @return (localSpec, localTx, remoteSpec, remoteTx, fundingTxOutput) * @return (localSpec, localTx, remoteSpec, remoteTx, fundingTxOutput)
*/ */
def makeFirstCommitTxs(temporaryChannelId: BinaryData, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, fundingTxHash: BinaryData, fundingTxOutputIndex: Int, remoteFirstPerCommitmentPoint: Point, maxFeerateMismatch: Double): (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx) = { def makeFirstCommitTxs(keyManager: KeyManager, temporaryChannelId: BinaryData, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, fundingTxHash: BinaryData, fundingTxOutputIndex: Int, remoteFirstPerCommitmentPoint: Point, maxFeerateMismatch: Double): (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx) = {
val toLocalMsat = if (localParams.isFunder) fundingSatoshis * 1000 - pushMsat else pushMsat val toLocalMsat = if (localParams.isFunder) fundingSatoshis * 1000 - pushMsat else pushMsat
val toRemoteMsat = if (localParams.isFunder) pushMsat else fundingSatoshis * 1000 - pushMsat val toRemoteMsat = if (localParams.isFunder) pushMsat else fundingSatoshis * 1000 - pushMsat
@ -147,10 +146,10 @@ object Helpers {
} }
} }
val commitmentInput = makeFundingInputInfo(fundingTxHash, fundingTxOutputIndex, Satoshi(fundingSatoshis), localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey) val commitmentInput = makeFundingInputInfo(fundingTxHash, fundingTxOutputIndex, Satoshi(fundingSatoshis), keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey)
val localPerCommitmentPoint = Generators.perCommitPoint(localParams.shaSeed, 0) val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, 0)
val (localCommitTx, _, _) = Commitments.makeLocalTxs(0, localParams, remoteParams, commitmentInput, localPerCommitmentPoint, localSpec) val (localCommitTx, _, _) = Commitments.makeLocalTxs(keyManager, 0, localParams, remoteParams, commitmentInput, localPerCommitmentPoint, localSpec)
val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(0, localParams, remoteParams, commitmentInput, remoteFirstPerCommitmentPoint, remoteSpec) val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(keyManager, 0, localParams, remoteParams, commitmentInput, remoteFirstPerCommitmentPoint, remoteSpec)
(localSpec, localCommitTx, remoteSpec, remoteCommitTx) (localSpec, localCommitTx, remoteSpec, remoteCommitTx)
} }
@ -159,6 +158,9 @@ object Helpers {
object Closing { object Closing {
// used only to compute tx weights and estimate fees
lazy val dummyPublicKey = PrivateKey(BinaryData("01" * 32), true).publicKey
def isValidFinalScriptPubkey(scriptPubKey: BinaryData): Boolean = { def isValidFinalScriptPubkey(scriptPubKey: BinaryData): Boolean = {
Try(Script.parse(scriptPubKey)) match { Try(Script.parse(scriptPubKey)) match {
case Success(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubkeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) if pubkeyHash.size == 20 => true case Success(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubkeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) if pubkeyHash.size == 20 => true
@ -173,7 +175,7 @@ object Helpers {
import commitments._ import commitments._
// this is just to estimate the weight, it depends on size of the pubkey scripts // this is just to estimate the weight, it depends on size of the pubkey scripts
val dummyClosingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, Satoshi(0), Satoshi(0), localCommit.spec) val dummyClosingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, Satoshi(0), Satoshi(0), localCommit.spec)
val closingWeight = Transaction.weight(Transactions.addSigs(dummyClosingTx, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, "aa" * 71, "bb" * 71).tx) val closingWeight = Transaction.weight(Transactions.addSigs(dummyClosingTx, dummyPublicKey, remoteParams.fundingPubKey, "aa" * 71, "bb" * 71).tx)
// no need to use a very high fee here, so we target 6 blocks; also, we "MUST set fee_satoshis less than or equal to the base fee of the final commitment transaction" // no need to use a very high fee here, so we target 6 blocks; also, we "MUST set fee_satoshis less than or equal to the base fee of the final commitment transaction"
val feeratePerKw = Math.min(Globals.feeratesPerKw.get.blocks_6, commitments.localCommit.spec.feeratePerKw) val feeratePerKw = Math.min(Globals.feeratesPerKw.get.blocks_6, commitments.localCommit.spec.feeratePerKw)
log.info(s"using feeratePerKw=$feeratePerKw for initial closing tx") log.info(s"using feeratePerKw=$feeratePerKw for initial closing tx")
@ -182,12 +184,12 @@ object Helpers {
def nextClosingFee(localClosingFee: Satoshi, remoteClosingFee: Satoshi): Satoshi = ((localClosingFee + remoteClosingFee) / 4) * 2 def nextClosingFee(localClosingFee: Satoshi, remoteClosingFee: Satoshi): Satoshi = ((localClosingFee + remoteClosingFee) / 4) * 2
def makeFirstClosingTx(commitments: Commitments, localScriptPubkey: BinaryData, remoteScriptPubkey: BinaryData)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { def makeFirstClosingTx(keyManager: KeyManager, commitments: Commitments, localScriptPubkey: BinaryData, remoteScriptPubkey: BinaryData)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = {
val closingFee = firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey) val closingFee = firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey)
makeClosingTx(commitments, localScriptPubkey, remoteScriptPubkey, closingFee) makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, closingFee)
} }
def makeClosingTx(commitments: Commitments, localScriptPubkey: BinaryData, remoteScriptPubkey: BinaryData, closingFee: Satoshi)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { def makeClosingTx(keyManager: KeyManager, commitments: Commitments, localScriptPubkey: BinaryData, remoteScriptPubkey: BinaryData, closingFee: Satoshi)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = {
import commitments._ import commitments._
require(isValidFinalScriptPubkey(localScriptPubkey), "invalid localScriptPubkey") require(isValidFinalScriptPubkey(localScriptPubkey), "invalid localScriptPubkey")
require(isValidFinalScriptPubkey(remoteScriptPubkey), "invalid remoteScriptPubkey") require(isValidFinalScriptPubkey(remoteScriptPubkey), "invalid remoteScriptPubkey")
@ -195,22 +197,22 @@ object Helpers {
// TODO: check that // TODO: check that
val dustLimitSatoshis = Satoshi(Math.max(localParams.dustLimitSatoshis, remoteParams.dustLimitSatoshis)) val dustLimitSatoshis = Satoshi(Math.max(localParams.dustLimitSatoshis, remoteParams.dustLimitSatoshis))
val closingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, dustLimitSatoshis, closingFee, localCommit.spec) val closingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, dustLimitSatoshis, closingFee, localCommit.spec)
val localClosingSig = Transactions.sign(closingTx, commitments.localParams.fundingPrivKey) val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitments.localParams.channelKeyPath))
val closingSigned = ClosingSigned(channelId, closingFee.amount, localClosingSig) val closingSigned = ClosingSigned(channelId, closingFee.amount, localClosingSig)
log.info(s"signed closing txid=${closingTx.tx.txid} with closingFeeSatoshis=${closingSigned.feeSatoshis}") log.info(s"signed closing txid=${closingTx.tx.txid} with closingFeeSatoshis=${closingSigned.feeSatoshis}")
log.debug(s"closingTxid=${closingTx.tx.txid} closingTx=${closingTx.tx}}") log.debug(s"closingTxid=${closingTx.tx.txid} closingTx=${closingTx.tx}}")
(closingTx, closingSigned) (closingTx, closingSigned)
} }
def checkClosingSignature(commitments: Commitments, localScriptPubkey: BinaryData, remoteScriptPubkey: BinaryData, remoteClosingFee: Satoshi, remoteClosingSig: BinaryData)(implicit log: LoggingAdapter): Try[Transaction] = { def checkClosingSignature(keyManager: KeyManager, commitments: Commitments, localScriptPubkey: BinaryData, remoteScriptPubkey: BinaryData, remoteClosingFee: Satoshi, remoteClosingSig: BinaryData)(implicit log: LoggingAdapter): Try[Transaction] = {
import commitments._ import commitments._
val lastCommitFeeSatoshi = commitments.commitInput.txOut.amount.amount - commitments.localCommit.publishableTxs.commitTx.tx.txOut.map(_.amount.amount).sum val lastCommitFeeSatoshi = commitments.commitInput.txOut.amount.amount - commitments.localCommit.publishableTxs.commitTx.tx.txOut.map(_.amount.amount).sum
if (remoteClosingFee.amount > lastCommitFeeSatoshi) { if (remoteClosingFee.amount > lastCommitFeeSatoshi) {
log.error(s"remote proposed a commit fee higher than the last commitment fee: remoteClosingFeeSatoshi=${remoteClosingFee.amount} lastCommitFeeSatoshi=$lastCommitFeeSatoshi") log.error(s"remote proposed a commit fee higher than the last commitment fee: remoteClosingFeeSatoshi=${remoteClosingFee.amount} lastCommitFeeSatoshi=$lastCommitFeeSatoshi")
throw new InvalidCloseFee(commitments.channelId, remoteClosingFee.amount) throw new InvalidCloseFee(commitments.channelId, remoteClosingFee.amount)
} }
val (closingTx, closingSigned) = makeClosingTx(commitments, localScriptPubkey, remoteScriptPubkey, remoteClosingFee) val (closingTx, closingSigned) = makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, remoteClosingFee)
val signedClosingTx = Transactions.addSigs(closingTx, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig) val signedClosingTx = Transactions.addSigs(closingTx, keyManager.fundingPublicKey(commitments.localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig)
Transactions.checkSpendable(signedClosingTx).map(x => signedClosingTx.tx).recover { case _ => throw InvalidCloseSignature(commitments.channelId, signedClosingTx.tx) } Transactions.checkSpendable(signedClosingTx).map(x => signedClosingTx.tx).recover { case _ => throw InvalidCloseSignature(commitments.channelId, signedClosingTx.tx) }
} }
@ -236,21 +238,21 @@ object Helpers {
* @param commitments our commitment data, which include payment preimages * @param commitments our commitment data, which include payment preimages
* @return a list of transactions (one per HTLC that we can claim) * @return a list of transactions (one per HTLC that we can claim)
*/ */
def claimCurrentLocalCommitTxOutputs(commitments: Commitments, tx: Transaction)(implicit log: LoggingAdapter): LocalCommitPublished = { def claimCurrentLocalCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, tx: Transaction)(implicit log: LoggingAdapter): LocalCommitPublished = {
import commitments._ import commitments._
require(localCommit.publishableTxs.commitTx.tx.txid == tx.txid, "txid mismatch, provided tx is not the current local commit tx") require(localCommit.publishableTxs.commitTx.tx.txid == tx.txid, "txid mismatch, provided tx is not the current local commit tx")
val localPerCommitmentPoint = Generators.perCommitPoint(localParams.shaSeed, commitments.localCommit.index.toInt) val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, commitments.localCommit.index.toInt)
val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint)
val localDelayedPrivkey = Generators.derivePrivKey(localParams.delayedPaymentKey, localPerCommitmentPoint) val localDelayedPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(localParams.channelKeyPath).publicKey, localPerCommitmentPoint)
// no need to use a high fee rate for delayed transactions (we are the only one who can spend them) // no need to use a high fee rate for delayed transactions (we are the only one who can spend them)
val feeratePerKwDelayed = Globals.feeratesPerKw.get.blocks_6 val feeratePerKwDelayed = Globals.feeratesPerKw.get.blocks_6
// first we will claim our main output as soon as the delay is over // first we will claim our main output as soon as the delay is over
val mainDelayedTx = generateTx("main-delayed-output")(Try { val mainDelayedTx = generateTx("main-delayed-output")(Try {
val claimDelayed = Transactions.makeClaimDelayedOutputTx(tx, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPrivkey.publicKey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed) val claimDelayed = Transactions.makeClaimDelayedOutputTx(tx, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed)
val sig = Transactions.sign(claimDelayed, localDelayedPrivkey) val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(localParams.channelKeyPath), localPerCommitmentPoint)
Transactions.addSigs(claimDelayed, sig) Transactions.addSigs(claimDelayed, sig)
}) })
@ -278,8 +280,14 @@ object Helpers {
val htlcDelayedTxes = htlcTxes.flatMap { val htlcDelayedTxes = htlcTxes.flatMap {
txinfo: TransactionWithInputInfo => generateTx("claim-delayed-output")(Try { txinfo: TransactionWithInputInfo => generateTx("claim-delayed-output")(Try {
// TODO: we should use the current fee rate, not the initial fee rate that we get from localParams // TODO: we should use the current fee rate, not the initial fee rate that we get from localParams
val claimDelayed = Transactions.makeClaimDelayedOutputTx(txinfo.tx, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPrivkey.publicKey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed) val claimDelayed = Transactions.makeClaimDelayedOutputTx(
val sig = Transactions.sign(claimDelayed, localDelayedPrivkey) txinfo.tx,
Satoshi(localParams.dustLimitSatoshis),
localRevocationPubkey,
remoteParams.toSelfDelay,
localDelayedPubkey,
localParams.defaultFinalScriptPubKey, feeratePerKwDelayed)
val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(localParams.channelKeyPath), localPerCommitmentPoint)
Transactions.addSigs(claimDelayed, sig) Transactions.addSigs(claimDelayed, sig)
}) })
} }
@ -306,15 +314,18 @@ object Helpers {
* @param tx the remote commitment transaction that has just been published * @param tx the remote commitment transaction that has just been published
* @return a list of transactions (one per HTLC that we can claim) * @return a list of transactions (one per HTLC that we can claim)
*/ */
def claimRemoteCommitTxOutputs(commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction)(implicit log: LoggingAdapter): RemoteCommitPublished = { def claimRemoteCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction)(implicit log: LoggingAdapter): RemoteCommitPublished = {
import commitments.{commitInput, localParams, remoteParams} import commitments.{commitInput, localParams, remoteParams}
require(remoteCommit.txid == tx.txid, "txid mismatch, provided tx is not the current remote commit tx") require(remoteCommit.txid == tx.txid, "txid mismatch, provided tx is not the current remote commit tx")
val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(remoteCommit.index, localParams, remoteParams, commitInput, remoteCommit.remotePerCommitmentPoint, remoteCommit.spec) val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(keyManager, remoteCommit.index, localParams, remoteParams, commitInput, remoteCommit.remotePerCommitmentPoint, remoteCommit.spec)
require(remoteCommitTx.tx.txid == tx.txid, "txid mismatch, cannot recompute the current remote commit tx") require(remoteCommitTx.tx.txid == tx.txid, "txid mismatch, cannot recompute the current remote commit tx")
val localHtlcPrivkey = Generators.derivePrivKey(localParams.htlcKey, remoteCommit.remotePerCommitmentPoint) val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(localParams.channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint)
val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(localParams.channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint)
val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remoteCommit.remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remoteCommit.remotePerCommitmentPoint)
val remoteRevocationPubkey = Generators.revocationPubKey(localParams.revocationBasepoint, remoteCommit.remotePerCommitmentPoint) val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, commitments.localCommit.index.toInt)
val localRevocationPubKey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint)
val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(localParams.channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint)
// we need to use a rather high fee for htlc-claim because we compete with the counterparty // we need to use a rather high fee for htlc-claim because we compete with the counterparty
val feeratePerKwHtlc = Globals.feeratesPerKw.get.block_1 val feeratePerKwHtlc = Globals.feeratesPerKw.get.block_1
@ -327,8 +338,8 @@ object Helpers {
// incoming htlc for which we have the preimage: we spend it directly // incoming htlc for which we have the preimage: we spend it directly
case DirectedHtlc(OUT, add: UpdateAddHtlc) if preimages.exists(r => sha256(r) == add.paymentHash) => generateTx("claim-htlc-success")(Try { case DirectedHtlc(OUT, add: UpdateAddHtlc) if preimages.exists(r => sha256(r) == add.paymentHash) => generateTx("claim-htlc-success")(Try {
val preimage = preimages.find(r => sha256(r) == add.paymentHash).get val preimage = preimages.find(r => sha256(r) == add.paymentHash).get
val tx = Transactions.makeClaimHtlcSuccessTx(remoteCommitTx.tx, Satoshi(localParams.dustLimitSatoshis), localHtlcPrivkey.publicKey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc) val tx = Transactions.makeClaimHtlcSuccessTx(remoteCommitTx.tx, Satoshi(localParams.dustLimitSatoshis), localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc)
val sig = Transactions.sign(tx, localHtlcPrivkey) val sig = keyManager.sign(tx, keyManager.htlcPoint(localParams.channelKeyPath), remoteCommit.remotePerCommitmentPoint)
Transactions.addSigs(tx, sig, preimage) Transactions.addSigs(tx, sig, preimage)
}) })
@ -336,8 +347,8 @@ object Helpers {
// outgoing htlc: they may or may not have the preimage, the only thing to do is try to get back our funds after timeout // outgoing htlc: they may or may not have the preimage, the only thing to do is try to get back our funds after timeout
case DirectedHtlc(IN, add: UpdateAddHtlc) => generateTx("claim-htlc-timeout")(Try { case DirectedHtlc(IN, add: UpdateAddHtlc) => generateTx("claim-htlc-timeout")(Try {
val tx = Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, Satoshi(localParams.dustLimitSatoshis), localHtlcPrivkey.publicKey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc) val tx = Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, Satoshi(localParams.dustLimitSatoshis), localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc)
val sig = Transactions.sign(tx, localHtlcPrivkey) val sig = keyManager.sign(tx, keyManager.htlcPoint(localParams.channelKeyPath), remoteCommit.remotePerCommitmentPoint)
Transactions.addSigs(tx, sig) Transactions.addSigs(tx, sig)
}) })
}.toSeq.flatten }.toSeq.flatten
@ -345,7 +356,7 @@ object Helpers {
// OPTIONAL: let's check transactions are actually spendable // OPTIONAL: let's check transactions are actually spendable
//require(txes.forall(Transactions.checkSpendable(_).isSuccess), "the tx we produced are not spendable!") //require(txes.forall(Transactions.checkSpendable(_).isSuccess), "the tx we produced are not spendable!")
claimRemoteCommitMainOutput(commitments, remoteCommit.remotePerCommitmentPoint, tx).copy( claimRemoteCommitMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx).copy(
claimHtlcSuccessTxs = txes.toList.collect { case c: ClaimHtlcSuccessTx => c.tx }, claimHtlcSuccessTxs = txes.toList.collect { case c: ClaimHtlcSuccessTx => c.tx },
claimHtlcTimeoutTxs = txes.toList.collect { case c: ClaimHtlcTimeoutTx => c.tx } claimHtlcTimeoutTxs = txes.toList.collect { case c: ClaimHtlcTimeoutTx => c.tx }
) )
@ -362,17 +373,17 @@ object Helpers {
* @param tx the remote commitment transaction that has just been published * @param tx the remote commitment transaction that has just been published
* @return a list of transactions (one per HTLC that we can claim) * @return a list of transactions (one per HTLC that we can claim)
*/ */
def claimRemoteCommitMainOutput(commitments: Commitments, remotePerCommitmentPoint: Point, tx: Transaction)(implicit log: LoggingAdapter): RemoteCommitPublished = { def claimRemoteCommitMainOutput(keyManager: KeyManager, commitments: Commitments, remotePerCommitmentPoint: Point, tx: Transaction)(implicit log: LoggingAdapter): RemoteCommitPublished = {
val localPaymentPrivkey = Generators.derivePrivKey(commitments.localParams.paymentKey, remotePerCommitmentPoint) val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(commitments.localParams.channelKeyPath).publicKey, remotePerCommitmentPoint)
// no need to use a high fee rate for our main output (we are the only one who can spend it) // no need to use a high fee rate for our main output (we are the only one who can spend it)
val feeratePerKwMain = Globals.feeratesPerKw.get.blocks_6 val feeratePerKwMain = Globals.feeratesPerKw.get.blocks_6
val mainTx = generateTx("claim-p2wpkh-output")(Try { val mainTx = generateTx("claim-p2wpkh-output")(Try {
val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, Satoshi(commitments.localParams.dustLimitSatoshis), val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, Satoshi(commitments.localParams.dustLimitSatoshis),
localPaymentPrivkey.publicKey, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain) localPubkey, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain)
val sig = Transactions.sign(claimMain, localPaymentPrivkey) val sig = keyManager.sign(claimMain, keyManager.paymentPoint(commitments.localParams.channelKeyPath), remotePerCommitmentPoint)
Transactions.addSigs(claimMain, localPaymentPrivkey.publicKey, sig) Transactions.addSigs(claimMain, localPubkey, sig)
}) })
RemoteCommitPublished( RemoteCommitPublished(
@ -393,12 +404,12 @@ object Helpers {
* *
* @return a [[RevokedCommitPublished]] object containing penalty transactions if the tx is a revoked commitment * @return a [[RevokedCommitPublished]] object containing penalty transactions if the tx is a revoked commitment
*/ */
def claimRevokedRemoteCommitTxOutputs(commitments: Commitments, tx: Transaction)(implicit log: LoggingAdapter): Option[RevokedCommitPublished] = { def claimRevokedRemoteCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, tx: Transaction)(implicit log: LoggingAdapter): Option[RevokedCommitPublished] = {
import commitments._ import commitments._
require(tx.txIn.size == 1, "commitment tx should have 1 input") require(tx.txIn.size == 1, "commitment tx should have 1 input")
val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn(0).sequence, tx.lockTime) 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 // this tx has been published by remote, so we need to invert local/remote params
val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, localParams.paymentBasepoint) val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, keyManager.paymentPoint(localParams.channelKeyPath).publicKey)
require(txnumber <= 0xffffffffffffL, "txnumber must be lesser than 48 bits long") require(txnumber <= 0xffffffffffffL, "txnumber must be lesser than 48 bits long")
log.warning(s"counterparty has published revoked commit txnumber=$txnumber") log.warning(s"counterparty has published revoked commit txnumber=$txnumber")
// now we know what commit number this tx is referring to, we can derive the commitment point from the shachain // now we know what commit number this tx is referring to, we can derive the commitment point from the shachain
@ -408,8 +419,8 @@ object Helpers {
val remotePerCommitmentPoint = remotePerCommitmentSecret.toPoint val remotePerCommitmentPoint = remotePerCommitmentSecret.toPoint
val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint)
val remoteRevocationPrivkey = Generators.revocationPrivKey(localParams.revocationSecret, remotePerCommitmentSecret) val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentSecret.toPoint)
val localPrivkey = Generators.derivePrivKey(localParams.paymentKey, remotePerCommitmentPoint) val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint)
// no need to use a high fee rate for our main output (we are the only one who can spend it) // no need to use a high fee rate for our main output (we are the only one who can spend it)
val feeratePerKwMain = Globals.feeratesPerKw.get.blocks_6 val feeratePerKwMain = Globals.feeratesPerKw.get.blocks_6
@ -418,16 +429,16 @@ object Helpers {
// first we will claim our main output right away // first we will claim our main output right away
val mainTx = generateTx("claim-p2wpkh-output")(Try { val mainTx = generateTx("claim-p2wpkh-output")(Try {
val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, Satoshi(localParams.dustLimitSatoshis), localPrivkey.publicKey, localParams.defaultFinalScriptPubKey, feeratePerKwMain) val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, Satoshi(localParams.dustLimitSatoshis), localPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain)
val sig = Transactions.sign(claimMain, localPrivkey) val sig = keyManager.sign(claimMain, keyManager.paymentPoint(localParams.channelKeyPath), remotePerCommitmentPoint)
Transactions.addSigs(claimMain, localPrivkey.publicKey, sig) Transactions.addSigs(claimMain, localPubkey, sig)
}) })
// then we punish them by stealing their main output // then we punish them by stealing their main output
val mainPenaltyTx = generateTx("main-penalty")(Try { val mainPenaltyTx = generateTx("main-penalty")(Try {
// TODO: we should use the current fee rate, not the initial fee rate that we get from localParams // TODO: we should use the current fee rate, not the initial fee rate that we get from localParams
val txinfo = Transactions.makeMainPenaltyTx(tx, Satoshi(localParams.dustLimitSatoshis), remoteRevocationPrivkey.publicKey, localParams.defaultFinalScriptPubKey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, feeratePerKwPenalty) val txinfo = Transactions.makeMainPenaltyTx(tx, Satoshi(localParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, feeratePerKwPenalty)
val sig = Transactions.sign(txinfo, remoteRevocationPrivkey) val sig = keyManager.sign(txinfo, keyManager.revocationPoint(localParams.channelKeyPath), remotePerCommitmentSecret)
Transactions.addSigs(txinfo, sig) Transactions.addSigs(txinfo, sig)
}) })

View file

@ -0,0 +1,59 @@
package fr.acinq.eclair.crypto
import fr.acinq.bitcoin.{BinaryData, Crypto, DeterministicWallet}
import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar}
import fr.acinq.bitcoin.DeterministicWallet.ExtendedPublicKey
import fr.acinq.eclair.transactions.Transactions.TransactionWithInputInfo
trait KeyManager {
def nodeKey: DeterministicWallet.ExtendedPrivateKey
def nodeId: PublicKey
def fundingPublicKey(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPublicKey
def revocationPoint(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPublicKey
def paymentPoint(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPublicKey
def delayedPaymentPoint(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPublicKey
def htlcPoint(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPublicKey
def commitmentSecret(channelKeyPath: DeterministicWallet.KeyPath, index: Long): Crypto.Scalar
def commitmentPoint(channelKeyPath: DeterministicWallet.KeyPath, index: Long): Crypto.Point
/**
*
* @param tx input transaction
* @param publicKey extended public key
* @return a signature generated with the private key that matches the input
* extended public key
*/
def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey): BinaryData
/**
* This method is used to spend funds send to htlc keys/delayed keys
*
* @param tx input transaction
* @param publicKey extended public key
* @param remotePoint remote point
* @return a signature generated with a private key generated from the input keys's matching
* private key and the remote point.
*/
def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: Point): BinaryData
/**
* Ths method is used to spend revoked transactions, with the corresponding revocation key
*
* @param tx input transaction
* @param publicKey extended public key
* @param remoteSecret remote secret
* @return a signature generated with a private key generated from the input keys's matching
* private key and the remote secret.
*/
def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: Scalar): BinaryData
def signChannelAnnouncement(channelKeyPath: DeterministicWallet.KeyPath, chainHash: BinaryData, shortChannelId: Long, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: BinaryData): (BinaryData, BinaryData)
}

View file

@ -0,0 +1,120 @@
package fr.acinq.eclair.crypto
import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache}
import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar}
import fr.acinq.bitcoin.DeterministicWallet.{derivePrivateKey, _}
import fr.acinq.bitcoin.{BinaryData, Crypto, DeterministicWallet}
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.transactions.Transactions.TransactionWithInputInfo
object LocalKeyManager {
val channelKeyBasePath = DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(0) :: Nil
val nodeKeyBasePath = DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(1) :: Nil
}
/**
* This class manages secrets and private keys.
* It exports points and public keys, and provides signing methods
*
* @param seed seed from which keys will be derived
*/
class LocalKeyManager(seed: BinaryData) extends KeyManager {
private val master = DeterministicWallet.generate(seed)
override val nodeKey = DeterministicWallet.derivePrivateKey(master, LocalKeyManager.nodeKeyBasePath)
override val nodeId = nodeKey.publicKey
private val privateKeys: LoadingCache[KeyPath, ExtendedPrivateKey] = CacheBuilder.newBuilder()
.maximumSize(6 * 200) // 6 keys per channel * 200 channels
.build[KeyPath, ExtendedPrivateKey](new CacheLoader[KeyPath, ExtendedPrivateKey] {
override def load(keyPath: KeyPath): ExtendedPrivateKey = derivePrivateKey(master, keyPath)
})
private val publicKeys: LoadingCache[KeyPath, ExtendedPublicKey] = CacheBuilder.newBuilder()
.maximumSize(6 * 200) // 6 keys per channel * 200 channels
.build[KeyPath, ExtendedPublicKey](new CacheLoader[KeyPath, ExtendedPublicKey] {
override def load(keyPath: KeyPath): ExtendedPublicKey = publicKey(privateKeys.get(keyPath))
})
private def internalKeyPath(channelKeyPath: DeterministicWallet.KeyPath, index: Long): List[Long] = (LocalKeyManager.channelKeyBasePath ++ channelKeyPath.path) :+ index
private def fundingPrivateKey(channelKeyPath: DeterministicWallet.KeyPath) = privateKeys.get(internalKeyPath(channelKeyPath, hardened(0)))
private def revocationSecret(channelKeyPath: DeterministicWallet.KeyPath) = privateKeys.get(internalKeyPath(channelKeyPath, hardened(1)))
private def paymentSecret(channelKeyPath: DeterministicWallet.KeyPath) = privateKeys.get(internalKeyPath(channelKeyPath, hardened(2)))
private def delayedPaymentSecret(channelKeyPath: DeterministicWallet.KeyPath) = privateKeys.get(internalKeyPath(channelKeyPath, hardened(3)))
private def htlcSecret(channelKeyPath: DeterministicWallet.KeyPath) = privateKeys.get(internalKeyPath(channelKeyPath, hardened(4)))
private def shaSeed(channelKeyPath: DeterministicWallet.KeyPath) = Crypto.sha256(privateKeys.get(internalKeyPath(channelKeyPath, hardened(5))).privateKey.toBin)
override def fundingPublicKey(channelKeyPath: DeterministicWallet.KeyPath) = publicKeys.get(internalKeyPath(channelKeyPath, hardened(0)))
override def revocationPoint(channelKeyPath: DeterministicWallet.KeyPath) = publicKeys.get(internalKeyPath(channelKeyPath, hardened(1)))
override def paymentPoint(channelKeyPath: DeterministicWallet.KeyPath) = publicKeys.get(internalKeyPath(channelKeyPath, hardened(2)))
override def delayedPaymentPoint(channelKeyPath: DeterministicWallet.KeyPath) = publicKeys.get(internalKeyPath(channelKeyPath, hardened(3)))
override def htlcPoint(channelKeyPath: DeterministicWallet.KeyPath) = publicKeys.get(internalKeyPath(channelKeyPath, hardened(4)))
override def commitmentSecret(channelKeyPath: DeterministicWallet.KeyPath, index: Long) = Generators.perCommitSecret(shaSeed(channelKeyPath), index)
override def commitmentPoint(channelKeyPath: DeterministicWallet.KeyPath, index: Long) = Generators.perCommitPoint(shaSeed(channelKeyPath), index)
/**
*
* @param tx input transaction
* @param publicKey extended public key
* @return a signature generated with the private key that matches the input
* extended public key
*/
def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey): BinaryData = {
val privateKey = privateKeys.get(publicKey.path)
Transactions.sign(tx, privateKey.privateKey)
}
/**
* This method is used to spend funds send to htlc keys/delayed keys
*
* @param tx input transaction
* @param publicKey extended public key
* @param remotePoint remote point
* @return a signature generated with a private key generated from the input keys's matching
* private key and the remote point.
*/
def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: Point): BinaryData = {
val privateKey = privateKeys.get(publicKey.path)
val currentKey = Generators.derivePrivKey(privateKey.privateKey, remotePoint)
Transactions.sign(tx, currentKey)
}
/**
* Ths method is used to spend revoked transactions, with the corresponding revocation key
*
* @param tx input transaction
* @param publicKey extended public key
* @param remoteSecret remote secret
* @return a signature generated with a private key generated from the input keys's matching
* private key and the remote secret.
*/
def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: Scalar): BinaryData = {
val privateKey = privateKeys.get(publicKey.path)
val currentKey = Generators.revocationPrivKey(privateKey.privateKey, remoteSecret)
Transactions.sign(tx, currentKey)
}
override def signChannelAnnouncement(channelKeyPath: DeterministicWallet.KeyPath, chainHash: BinaryData, shortChannelId: Long, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: BinaryData): (BinaryData, BinaryData) = {
val witness = if (Announcements.isNode1(nodeId.toBin, remoteNodeId.toBin)) {
Announcements.channelAnnouncementWitnessEncode(chainHash, shortChannelId, nodeId, remoteNodeId, fundingPublicKey(channelKeyPath).publicKey, remoteFundingKey, features)
} else {
Announcements.channelAnnouncementWitnessEncode(chainHash, shortChannelId, remoteNodeId, nodeId, remoteFundingKey, fundingPublicKey(channelKeyPath).publicKey, features)
}
val nodeSig = Crypto.encodeSignature(Crypto.sign(witness, nodeKey.privateKey)) :+ 1.toByte
val bitcoinSig = Crypto.encodeSignature(Crypto.sign(witness, fundingPrivateKey(channelKeyPath).privateKey)) :+ 1.toByte
(nodeSig, bitcoinSig)
}
}

View file

@ -1,18 +1,19 @@
package fr.acinq.eclair.io package fr.acinq.eclair.io
import java.io.ByteArrayInputStream
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.nio.ByteOrder
import akka.actor.{ActorRef, FSM, OneForOneStrategy, PoisonPill, Props, Status, SupervisorStrategy, Terminated} import akka.actor.{ActorRef, FSM, OneForOneStrategy, PoisonPill, Props, Status, SupervisorStrategy, Terminated}
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{BinaryData, Crypto, DeterministicWallet, MilliSatoshi, Satoshi} import fr.acinq.bitcoin.{BinaryData, DeterministicWallet, MilliSatoshi, Protocol, Satoshi}
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.blockchain.EclairWallet
import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.crypto.TransportHandler
import fr.acinq.eclair.crypto.TransportHandler.Listener import fr.acinq.eclair.crypto.TransportHandler.Listener
import fr.acinq.eclair.router._ import fr.acinq.eclair.router._
import fr.acinq.eclair.wire import fr.acinq.eclair.{wire, _}
import fr.acinq.eclair.wire.LightningMessage import fr.acinq.eclair.wire._
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.util.Random import scala.util.Random
@ -358,31 +359,27 @@ object Peer {
// @formatter:on // @formatter:on
def generateKey(nodeParams: NodeParams, keyPath: Seq[Long]): PrivateKey = DeterministicWallet.derivePrivateKey(nodeParams.extendedPrivateKey, keyPath).privateKey
def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: BinaryData, isFunder: Boolean, fundingSatoshis: Long): LocalParams = { def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: BinaryData, isFunder: Boolean, fundingSatoshis: Long): LocalParams = {
// all secrets are generated from the main seed val entropy = new Array[Byte](16)
// TODO: check this secureRandom.nextBytes(entropy)
val keyIndex = secureRandom.nextInt(1000).toLong 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)
}
def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: BinaryData, isFunder: Boolean, fundingSatoshis: Long, channelKeyPath: DeterministicWallet.KeyPath): LocalParams = {
LocalParams( LocalParams(
nodeId = nodeParams.privateKey.publicKey, nodeParams.nodeId,
channelKeyPath,
dustLimitSatoshis = nodeParams.dustLimitSatoshis, dustLimitSatoshis = nodeParams.dustLimitSatoshis,
maxHtlcValueInFlightMsat = nodeParams.maxHtlcValueInFlightMsat, maxHtlcValueInFlightMsat = nodeParams.maxHtlcValueInFlightMsat,
channelReserveSatoshis = (nodeParams.reserveToFundingRatio * fundingSatoshis).toLong, channelReserveSatoshis = (nodeParams.reserveToFundingRatio * fundingSatoshis).toLong,
htlcMinimumMsat = nodeParams.htlcMinimumMsat, htlcMinimumMsat = nodeParams.htlcMinimumMsat,
toSelfDelay = nodeParams.delayBlocks, toSelfDelay = nodeParams.delayBlocks,
maxAcceptedHtlcs = nodeParams.maxAcceptedHtlcs, maxAcceptedHtlcs = nodeParams.maxAcceptedHtlcs,
fundingPrivKey = generateKey(nodeParams, keyIndex :: 0L :: Nil),
revocationSecret = generateKey(nodeParams, keyIndex :: 1L :: Nil),
paymentKey = generateKey(nodeParams, keyIndex :: 2L :: Nil),
delayedPaymentKey = generateKey(nodeParams, keyIndex :: 3L :: Nil),
htlcKey = generateKey(nodeParams, keyIndex :: 4L :: Nil),
defaultFinalScriptPubKey = defaultFinalScriptPubKey, defaultFinalScriptPubKey = defaultFinalScriptPubKey,
shaSeed = Crypto.sha256(generateKey(nodeParams, keyIndex :: 5L :: Nil).toBin), // TODO: check that
isFunder = isFunder, isFunder = isFunder,
globalFeatures = nodeParams.globalFeatures, globalFeatures = nodeParams.globalFeatures,
localFeatures = nodeParams.localFeatures) localFeatures = nodeParams.localFeatures)
} }
} }

View file

@ -1,7 +1,8 @@
package fr.acinq.eclair.wire package fr.acinq.eclair.wire
import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, KeyPath}
import fr.acinq.bitcoin.{BinaryData, OutPoint, Transaction, TxOut} import fr.acinq.bitcoin.{BinaryData, OutPoint, Transaction, TxOut}
import fr.acinq.eclair.channel.{DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT, _} import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.payment.{Local, Origin, Relayed} import fr.acinq.eclair.payment.{Local, Origin, Relayed}
import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions.Transactions._
@ -10,29 +11,33 @@ import fr.acinq.eclair.wire.LightningMessageCodecs._
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
import scodec.bits.{BitVector, ByteVector} import scodec.bits.{BitVector, ByteVector}
import scodec.codecs._ import scodec.codecs._
import scodec.{Attempt, Codec, DecodeResult} import scodec.{Attempt, Codec}
/** /**
* Created by PM on 02/06/2017. * Created by PM on 02/06/2017.
*/ */
object ChannelCodecs extends Logging { object ChannelCodecs extends Logging {
val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => new KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath]
val extendedPrivateKeyCodec: Codec[ExtendedPrivateKey] = (
("secretkeybytes" | binarydata(32)) ::
("chaincode" | binarydata(32)) ::
("depth" | uint16) ::
("path" | keyPathCodec) ::
("parent" | int64)).as[ExtendedPrivateKey]
val localParamsCodec: Codec[LocalParams] = ( val localParamsCodec: Codec[LocalParams] = (
("nodeId" | publicKey) :: ("nodeId" | publicKey) ::
("channelPath" | keyPathCodec) ::
("dustLimitSatoshis" | uint64) :: ("dustLimitSatoshis" | uint64) ::
("maxHtlcValueInFlightMsat" | uint64ex) :: ("maxHtlcValueInFlightMsat" | uint64ex) ::
("channelReserveSatoshis" | uint64) :: ("channelReserveSatoshis" | uint64) ::
("htlcMinimumMsat" | uint64) :: ("htlcMinimumMsat" | uint64) ::
("toSelfDelay" | uint16) :: ("toSelfDelay" | uint16) ::
("maxAcceptedHtlcs" | uint16) :: ("maxAcceptedHtlcs" | uint16) ::
("fundingPrivKey" | privateKey) ::
("revocationSecret" | scalar) ::
("paymentKey" | scalar) ::
("delayedPaymentKey" | scalar) ::
("htlcKey" | scalar) ::
("defaultFinalScriptPubKey" | varsizebinarydata) ::
("shaSeed" | varsizebinarydata) ::
("isFunder" | bool) :: ("isFunder" | bool) ::
("defaultFinalScriptPubKey" | varsizebinarydata) ::
("globalFeatures" | varsizebinarydata) :: ("globalFeatures" | varsizebinarydata) ::
("localFeatures" | varsizebinarydata)).as[LocalParams] ("localFeatures" | varsizebinarydata)).as[LocalParams]

View file

@ -4,8 +4,9 @@ import java.net.InetSocketAddress
import java.sql.DriverManager import java.sql.DriverManager
import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.{BinaryData, Block, DeterministicWallet, Script} import fr.acinq.bitcoin.{BinaryData, Block, Script}
import fr.acinq.eclair.NodeParams.BITCOIND import fr.acinq.eclair.NodeParams.BITCOIND
import fr.acinq.eclair.crypto.LocalKeyManager
import fr.acinq.eclair.db.sqlite._ import fr.acinq.eclair.db.sqlite._
import fr.acinq.eclair.io.Peer import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.wire.Color import fr.acinq.eclair.wire.Color
@ -22,15 +23,13 @@ object TestConstants {
object Alice { object Alice {
val seed = BinaryData("01" * 32) val seed = BinaryData("01" * 32)
val master = DeterministicWallet.generate(seed) val keyManager = new LocalKeyManager(seed)
val extendedPrivateKey = DeterministicWallet.derivePrivateKey(master, DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(0) :: Nil)
def sqlite = DriverManager.getConnection("jdbc:sqlite::memory:") def sqlite = DriverManager.getConnection("jdbc:sqlite::memory:")
// This is a function, and not a val! When called will return a new NodeParams // This is a function, and not a val! When called will return a new NodeParams
def nodeParams = NodeParams( def nodeParams = NodeParams(
extendedPrivateKey = extendedPrivateKey, keyManager = keyManager,
privateKey = extendedPrivateKey.privateKey,
alias = "alice", alias = "alice",
color = Color(1, 2, 3), color = Color(1, 2, 3),
publicAddresses = new InetSocketAddress("localhost", 9731) :: Nil, publicAddresses = new InetSocketAddress("localhost", 9731) :: Nil,
@ -79,14 +78,12 @@ object TestConstants {
object Bob { object Bob {
val seed = BinaryData("02" * 32) val seed = BinaryData("02" * 32)
val master = DeterministicWallet.generate(seed) val keyManager = new LocalKeyManager(seed)
val extendedPrivateKey = DeterministicWallet.derivePrivateKey(master, DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(0) :: Nil)
def sqlite = DriverManager.getConnection("jdbc:sqlite::memory:") def sqlite = DriverManager.getConnection("jdbc:sqlite::memory:")
def nodeParams = NodeParams( def nodeParams = NodeParams(
extendedPrivateKey = extendedPrivateKey, keyManager = keyManager,
privateKey = extendedPrivateKey.privateKey,
alias = "bob", alias = "bob",
color = Color(4, 5, 6), color = Color(4, 5, 6),
publicAddresses = new InetSocketAddress("localhost", 9732) :: Nil, publicAddresses = new InetSocketAddress("localhost", 9732) :: Nil,

View file

@ -1871,7 +1871,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures] val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures]
import initialState.commitments.localParams import initialState.commitments.localParams
import initialState.commitments.remoteParams import initialState.commitments.remoteParams
val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, localParams.nodeId, remoteParams.nodeId, localParams.fundingPrivKey.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.channelKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature)
val channelUpdate = Announcements.makeChannelUpdate(Alice.nodeParams.chainHash, Alice.nodeParams.privateKey, remoteParams.nodeId, annSigsA.shortChannelId, Alice.nodeParams.expiryDeltaBlocks, Bob.nodeParams.htlcMinimumMsat, Alice.nodeParams.feeBaseMsat, Alice.nodeParams.feeProportionalMillionth) val channelUpdate = Announcements.makeChannelUpdate(Alice.nodeParams.chainHash, Alice.nodeParams.privateKey, remoteParams.nodeId, annSigsA.shortChannelId, Alice.nodeParams.expiryDeltaBlocks, Bob.nodeParams.htlcMinimumMsat, Alice.nodeParams.feeBaseMsat, Alice.nodeParams.feeProportionalMillionth)
// actual test starts here // actual test starts here
bob2alice.forward(alice) bob2alice.forward(alice)
@ -1890,7 +1890,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures] val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures]
import initialState.commitments.localParams import initialState.commitments.localParams
import initialState.commitments.remoteParams import initialState.commitments.remoteParams
val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, localParams.nodeId, remoteParams.nodeId, localParams.fundingPrivKey.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.channelKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature)
bob2alice.forward(alice) bob2alice.forward(alice)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement === Some(channelAnn)) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement === Some(channelAnn))

View file

@ -1,14 +1,14 @@
package fr.acinq.eclair.channel.states.e package fr.acinq.eclair.channel.states.e
import akka.testkit.{TestFSMRef, TestProbe} import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.{BinaryData, ScriptFlags, Transaction}
import fr.acinq.bitcoin.Crypto.Scalar import fr.acinq.bitcoin.Crypto.Scalar
import fr.acinq.eclair.TestkitBaseClass import fr.acinq.bitcoin.{BinaryData, ScriptFlags, Transaction}
import fr.acinq.eclair.blockchain.{PublishAsap, WatchEventSpent} import fr.acinq.eclair.blockchain.{PublishAsap, WatchEventSpent}
import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.channel.{Data, State, _} import fr.acinq.eclair.channel.{Data, State, _}
import fr.acinq.eclair.crypto.{Generators, Sphinx} import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.wire._ import fr.acinq.eclair.wire._
import fr.acinq.eclair.{TestConstants, TestkitBaseClass}
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner import org.scalatest.junit.JUnitRunner
@ -58,8 +58,8 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
val bobCommitments = bob.stateData.asInstanceOf[HasCommitments].commitments val bobCommitments = bob.stateData.asInstanceOf[HasCommitments].commitments
val aliceCommitments = alice.stateData.asInstanceOf[HasCommitments].commitments val aliceCommitments = alice.stateData.asInstanceOf[HasCommitments].commitments
val bobCurrentPerCommitmentPoint = Generators.perCommitPoint(bobCommitments.localParams.shaSeed, bobCommitments.localCommit.index) val bobCurrentPerCommitmentPoint = TestConstants.Bob.keyManager.commitmentPoint(bobCommitments.localParams.channelKeyPath, bobCommitments.localCommit.index)
val aliceCurrentPerCommitmentPoint = Generators.perCommitPoint(aliceCommitments.localParams.shaSeed, aliceCommitments.localCommit.index) val aliceCurrentPerCommitmentPoint = TestConstants.Alice.keyManager.commitmentPoint(aliceCommitments.localParams.channelKeyPath, aliceCommitments.localCommit.index)
// a didn't receive any update or sig // a didn't receive any update or sig
@ -141,8 +141,8 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
val bobCommitments = bob.stateData.asInstanceOf[HasCommitments].commitments val bobCommitments = bob.stateData.asInstanceOf[HasCommitments].commitments
val aliceCommitments = alice.stateData.asInstanceOf[HasCommitments].commitments val aliceCommitments = alice.stateData.asInstanceOf[HasCommitments].commitments
val bobCurrentPerCommitmentPoint = Generators.perCommitPoint(bobCommitments.localParams.shaSeed, bobCommitments.localCommit.index) val bobCurrentPerCommitmentPoint = TestConstants.Bob.keyManager.commitmentPoint(bobCommitments.localParams.channelKeyPath, bobCommitments.localCommit.index)
val aliceCurrentPerCommitmentPoint = Generators.perCommitPoint(aliceCommitments.localParams.shaSeed, aliceCommitments.localCommit.index) val aliceCurrentPerCommitmentPoint = TestConstants.Alice.keyManager.commitmentPoint(aliceCommitments.localParams.channelKeyPath, aliceCommitments.localCommit.index)
// a didn't receive the sig // a didn't receive the sig
val ab_reestablish = alice2bob.expectMsg(ChannelReestablish(ab_add_0.channelId, 1, 0, Some(Scalar(Sphinx zeroes 32)), Some(aliceCurrentPerCommitmentPoint))) val ab_reestablish = alice2bob.expectMsg(ChannelReestablish(ab_add_0.channelId, 1, 0, Some(Scalar(Sphinx zeroes 32)), Some(aliceCurrentPerCommitmentPoint)))

View file

@ -3,6 +3,7 @@ package fr.acinq.eclair.channel.states.g
import akka.actor.Status.Failure import akka.actor.Status.Failure
import akka.testkit.{TestFSMRef, TestProbe} import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.Satoshi import fr.acinq.bitcoin.Satoshi
import fr.acinq.eclair.TestConstants.Bob
import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.fee.FeeratesPerKw import fr.acinq.eclair.blockchain.fee.FeeratesPerKw
import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.Helpers.Closing
@ -172,7 +173,7 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods
// at this point alice and bob have not yet converged on closing fees, but bob decides to publish a mutual close with one of the previous sigs // at this point alice and bob have not yet converged on closing fees, but bob decides to publish a mutual close with one of the previous sigs
val d = bob.stateData.asInstanceOf[DATA_NEGOTIATING] val d = bob.stateData.asInstanceOf[DATA_NEGOTIATING]
implicit val log = bob.underlyingActor.implicitLog implicit val log = bob.underlyingActor.implicitLog
val Success(bobClosingTx) = Closing.checkClosingSignature(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, Satoshi(aliceClose1.feeSatoshis), aliceClose1.signature) val Success(bobClosingTx) = Closing.checkClosingSignature(Bob.keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, Satoshi(aliceClose1.feeSatoshis), aliceClose1.signature)
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobClosingTx) alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobClosingTx)
alice2blockchain.expectMsgType[PublishAsap] alice2blockchain.expectMsgType[PublishAsap]

View file

@ -1,16 +1,16 @@
package fr.acinq.eclair.db package fr.acinq.eclair.db
import fr.acinq.bitcoin.Crypto.{PrivateKey, Scalar} import fr.acinq.bitcoin.Crypto.{PrivateKey, Scalar}
import fr.acinq.bitcoin.{BinaryData, Crypto, MilliSatoshi, Satoshi, Transaction} import fr.acinq.bitcoin.{BinaryData, Crypto, DeterministicWallet, MilliSatoshi, Satoshi, Transaction}
import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel.Helpers.Funding
import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.{ShaChain, Sphinx} import fr.acinq.eclair.crypto.{LocalKeyManager, ShaChain, Sphinx}
import fr.acinq.eclair.payment.{Local, Relayed} import fr.acinq.eclair.payment.{Local, Relayed}
import fr.acinq.eclair.{UInt64, randomKey}
import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions.Transactions.CommitTx import fr.acinq.eclair.transactions.Transactions.CommitTx
import fr.acinq.eclair.transactions._ import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire.{ChannelCodecs, ChannelUpdate, UpdateAddHtlc} import fr.acinq.eclair.wire.{ChannelCodecs, UpdateAddHtlc}
import fr.acinq.eclair.{UInt64, randomKey}
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.scalatest.FunSuite import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner import org.scalatest.junit.JUnitRunner
@ -33,21 +33,17 @@ class ChannelStateSpec extends FunSuite {
} }
object ChannelStateSpec { object ChannelStateSpec {
val keyManager = new LocalKeyManager("01" * 32)
val localParams = LocalParams( val localParams = LocalParams(
nodeId = randomKey.publicKey, keyManager.nodeId,
channelKeyPath = DeterministicWallet.KeyPath(Seq(42)),
dustLimitSatoshis = Satoshi(546).toLong, dustLimitSatoshis = Satoshi(546).toLong,
maxHtlcValueInFlightMsat = UInt64(50), maxHtlcValueInFlightMsat = UInt64(50),
channelReserveSatoshis = 10000, channelReserveSatoshis = 10000,
htlcMinimumMsat = 50000, htlcMinimumMsat = 50000,
toSelfDelay = 144, toSelfDelay = 144,
maxAcceptedHtlcs = 50, maxAcceptedHtlcs = 50,
fundingPrivKey = PrivateKey(BinaryData("01" * 32) :+ 1.toByte),
revocationSecret = Scalar(BinaryData("02" * 32)),
paymentKey = PrivateKey(BinaryData("03" * 32) :+ 1.toByte),
delayedPaymentKey = Scalar(BinaryData("04" * 32)),
htlcKey = PrivateKey(BinaryData("06" * 32) :+ 1.toByte),
defaultFinalScriptPubKey = Nil, defaultFinalScriptPubKey = Nil,
shaSeed = BinaryData("05" * 32),
isFunder = true, isFunder = true,
globalFeatures = "foo".getBytes(), globalFeatures = "foo".getBytes(),
localFeatures = "bar".getBytes()) localFeatures = "bar".getBytes())
@ -86,7 +82,7 @@ object ChannelStateSpec {
val fundingTx = Transaction.read("0200000001adbb20ea41a8423ea937e76e8151636bf6093b70eaff942930d20576600521fd000000006b48304502210090587b6201e166ad6af0227d3036a9454223d49a1f11839c1a362184340ef0240220577f7cd5cca78719405cbf1de7414ac027f0239ef6e214c90fcaab0454d84b3b012103535b32d5eb0a6ed0982a0479bbadc9868d9836f6ba94dd5a63be16d875069184ffffffff028096980000000000220020c015c4a6be010e21657068fc2e6a9d02b27ebe4d490a25846f7237f104d1a3cd20256d29010000001600143ca33c2e4446f4a305f23c80df8ad1afdcf652f900000000") val fundingTx = Transaction.read("0200000001adbb20ea41a8423ea937e76e8151636bf6093b70eaff942930d20576600521fd000000006b48304502210090587b6201e166ad6af0227d3036a9454223d49a1f11839c1a362184340ef0240220577f7cd5cca78719405cbf1de7414ac027f0239ef6e214c90fcaab0454d84b3b012103535b32d5eb0a6ed0982a0479bbadc9868d9836f6ba94dd5a63be16d875069184ffffffff028096980000000000220020c015c4a6be010e21657068fc2e6a9d02b27ebe4d490a25846f7237f104d1a3cd20256d29010000001600143ca33c2e4446f4a305f23c80df8ad1afdcf652f900000000")
val fundingAmount = fundingTx.txOut(0).amount val fundingAmount = fundingTx.txOut(0).amount
val commitmentInput = Funding.makeFundingInputInfo(fundingTx.hash, 0, fundingAmount, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey) val commitmentInput = Funding.makeFundingInputInfo(fundingTx.hash, 0, fundingAmount, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey)
val localCommit = LocalCommit(0, CommitmentSpec(htlcs.toSet, 1500, 50000, 700000), PublishableTxs(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), Nil)) val localCommit = LocalCommit(0, CommitmentSpec(htlcs.toSet, 1500, 50000, 700000), PublishableTxs(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), Nil))
val remoteCommit = RemoteCommit(0, CommitmentSpec(htlcs.toSet, 1500, 50000, 700000), BinaryData("0303030303030303030303030303030303030303030303030303030303030303"), Scalar(BinaryData("04" * 32)).toPoint) val remoteCommit = RemoteCommit(0, CommitmentSpec(htlcs.toSet, 1500, 50000, 700000), BinaryData("0303030303030303030303030303030303030303030303030303030303030303"), Scalar(BinaryData("04" * 32)).toPoint)

View file

@ -1,13 +1,14 @@
package fr.acinq.eclair.payment package fr.acinq.eclair.payment
import fr.acinq.bitcoin.{BinaryData, Block, Crypto} import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey
import fr.acinq.bitcoin.{BinaryData, Block, Crypto, DeterministicWallet}
import fr.acinq.eclair.channel.Channel import fr.acinq.eclair.channel.Channel
import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.crypto.Sphinx.{PacketAndSecrets, ParsedPacket} import fr.acinq.eclair.crypto.Sphinx.{PacketAndSecrets, ParsedPacket}
import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.payment.PaymentLifecycle._
import fr.acinq.eclair.{randomKey, nodeFee}
import fr.acinq.eclair.router.Hop import fr.acinq.eclair.router.Hop
import fr.acinq.eclair.wire.{ChannelUpdate, LightningMessageCodecs, PerHopPayload} import fr.acinq.eclair.wire.{ChannelUpdate, LightningMessageCodecs, PerHopPayload}
import fr.acinq.eclair.{TestConstants, nodeFee, randomBytes}
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.scalatest.FunSuite import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner import org.scalatest.junit.JUnitRunner
@ -54,25 +55,25 @@ class HtlcGenerationSpec extends FunSuite {
assert(packet_b.serialize.size === Sphinx.PacketLength) assert(packet_b.serialize.size === Sphinx.PacketLength)
// let's peel the onion // let's peel the onion
val Success(ParsedPacket(bin_b, packet_c, _)) = Sphinx.parsePacket(priv_b, paymentHash, packet_b.serialize) val Success(ParsedPacket(bin_b, packet_c, _)) = Sphinx.parsePacket(priv_b.privateKey, paymentHash, packet_b.serialize)
val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_b.data)).require.value val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_b.data)).require.value
assert(packet_c.serialize.size === Sphinx.PacketLength) assert(packet_c.serialize.size === Sphinx.PacketLength)
assert(payload_b.amtToForward === amount_bc) assert(payload_b.amtToForward === amount_bc)
assert(payload_b.outgoingCltvValue === expiry_bc) assert(payload_b.outgoingCltvValue === expiry_bc)
val Success(ParsedPacket(bin_c, packet_d, _)) = Sphinx.parsePacket(priv_c, paymentHash, packet_c.serialize) val Success(ParsedPacket(bin_c, packet_d, _)) = Sphinx.parsePacket(priv_c.privateKey, paymentHash, packet_c.serialize)
val payload_c = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_c.data)).require.value val payload_c = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_c.data)).require.value
assert(packet_d.serialize.size === Sphinx.PacketLength) assert(packet_d.serialize.size === Sphinx.PacketLength)
assert(payload_c.amtToForward === amount_cd) assert(payload_c.amtToForward === amount_cd)
assert(payload_c.outgoingCltvValue === expiry_cd) assert(payload_c.outgoingCltvValue === expiry_cd)
val Success(ParsedPacket(bin_d, packet_e, _)) = Sphinx.parsePacket(priv_d, paymentHash, packet_d.serialize) val Success(ParsedPacket(bin_d, packet_e, _)) = Sphinx.parsePacket(priv_d.privateKey, paymentHash, packet_d.serialize)
val payload_d = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_d.data)).require.value val payload_d = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_d.data)).require.value
assert(packet_e.serialize.size === Sphinx.PacketLength) assert(packet_e.serialize.size === Sphinx.PacketLength)
assert(payload_d.amtToForward === amount_de) assert(payload_d.amtToForward === amount_de)
assert(payload_d.outgoingCltvValue === expiry_de) assert(payload_d.outgoingCltvValue === expiry_de)
val Success(ParsedPacket(bin_e, packet_random, _)) = Sphinx.parsePacket(priv_e, paymentHash, packet_e.serialize) val Success(ParsedPacket(bin_e, packet_random, _)) = Sphinx.parsePacket(priv_e.privateKey, paymentHash, packet_e.serialize)
val payload_e = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_e.data)).require.value val payload_e = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_e.data)).require.value
assert(packet_random.serialize.size === Sphinx.PacketLength) assert(packet_random.serialize.size === Sphinx.PacketLength)
assert(payload_e.amtToForward === finalAmountMsat) assert(payload_e.amtToForward === finalAmountMsat)
@ -89,25 +90,25 @@ class HtlcGenerationSpec extends FunSuite {
assert(add.onion.length === Sphinx.PacketLength) assert(add.onion.length === Sphinx.PacketLength)
// let's peel the onion // let's peel the onion
val Success(ParsedPacket(bin_b, packet_c, _)) = Sphinx.parsePacket(priv_b, paymentHash, add.onion) val Success(ParsedPacket(bin_b, packet_c, _)) = Sphinx.parsePacket(priv_b.privateKey, paymentHash, add.onion)
val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_b.data)).require.value val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_b.data)).require.value
assert(packet_c.serialize.size === Sphinx.PacketLength) assert(packet_c.serialize.size === Sphinx.PacketLength)
assert(payload_b.amtToForward === amount_bc) assert(payload_b.amtToForward === amount_bc)
assert(payload_b.outgoingCltvValue === expiry_bc) assert(payload_b.outgoingCltvValue === expiry_bc)
val Success(ParsedPacket(bin_c, packet_d, _)) = Sphinx.parsePacket(priv_c, paymentHash, packet_c.serialize) val Success(ParsedPacket(bin_c, packet_d, _)) = Sphinx.parsePacket(priv_c.privateKey, paymentHash, packet_c.serialize)
val payload_c = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_c.data)).require.value val payload_c = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_c.data)).require.value
assert(packet_d.serialize.size === Sphinx.PacketLength) assert(packet_d.serialize.size === Sphinx.PacketLength)
assert(payload_c.amtToForward === amount_cd) assert(payload_c.amtToForward === amount_cd)
assert(payload_c.outgoingCltvValue === expiry_cd) assert(payload_c.outgoingCltvValue === expiry_cd)
val Success(ParsedPacket(bin_d, packet_e, _)) = Sphinx.parsePacket(priv_d, paymentHash, packet_d.serialize) val Success(ParsedPacket(bin_d, packet_e, _)) = Sphinx.parsePacket(priv_d.privateKey, paymentHash, packet_d.serialize)
val payload_d = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_d.data)).require.value val payload_d = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_d.data)).require.value
assert(packet_e.serialize.size === Sphinx.PacketLength) assert(packet_e.serialize.size === Sphinx.PacketLength)
assert(payload_d.amtToForward === amount_de) assert(payload_d.amtToForward === amount_de)
assert(payload_d.outgoingCltvValue === expiry_de) assert(payload_d.outgoingCltvValue === expiry_de)
val Success(ParsedPacket(bin_e, packet_random, _)) = Sphinx.parsePacket(priv_e, paymentHash, packet_e.serialize) val Success(ParsedPacket(bin_e, packet_random, _)) = Sphinx.parsePacket(priv_e.privateKey, paymentHash, packet_e.serialize)
val payload_e = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_e.data)).require.value val payload_e = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_e.data)).require.value
assert(packet_random.serialize.size === Sphinx.PacketLength) assert(packet_random.serialize.size === Sphinx.PacketLength)
assert(payload_e.amtToForward === finalAmountMsat) assert(payload_e.amtToForward === finalAmountMsat)
@ -123,7 +124,7 @@ class HtlcGenerationSpec extends FunSuite {
assert(add.onion.size === Sphinx.PacketLength) assert(add.onion.size === Sphinx.PacketLength)
// let's peel the onion // let's peel the onion
val Success(ParsedPacket(bin_b, packet_random, _)) = Sphinx.parsePacket(priv_b, paymentHash, add.onion) val Success(ParsedPacket(bin_b, packet_random, _)) = Sphinx.parsePacket(priv_b.privateKey, paymentHash, add.onion)
val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_b.data)).require.value val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_b.data)).require.value
assert(packet_random.serialize.size === Sphinx.PacketLength) assert(packet_random.serialize.size === Sphinx.PacketLength)
assert(payload_b.amtToForward === finalAmountMsat) assert(payload_b.amtToForward === finalAmountMsat)
@ -133,9 +134,12 @@ class HtlcGenerationSpec extends FunSuite {
} }
object HtlcGenerationSpec { object HtlcGenerationSpec {
val (priv_a, priv_b, priv_c, priv_d, priv_e) = (randomKey, randomKey, randomKey, randomKey, randomKey)
def randomExtendedPrivateKey: ExtendedPrivateKey = DeterministicWallet.generate(randomBytes(32))
val (priv_a, priv_b, priv_c, priv_d, priv_e) = (TestConstants.Alice.keyManager.nodeKey, TestConstants.Bob.keyManager.nodeKey, randomExtendedPrivateKey, randomExtendedPrivateKey, randomExtendedPrivateKey)
val (a, b, c, d, e) = (priv_a.publicKey, priv_b.publicKey, priv_c.publicKey, priv_d.publicKey, priv_e.publicKey) val (a, b, c, d, e) = (priv_a.publicKey, priv_b.publicKey, priv_c.publicKey, priv_d.publicKey, priv_e.publicKey)
val sig = Crypto.encodeSignature(Crypto.sign(Crypto.sha256(BinaryData.empty), priv_a)) :+ 1.toByte val sig = Crypto.encodeSignature(Crypto.sign(Crypto.sha256(BinaryData.empty), priv_a.privateKey)) :+ 1.toByte
val defaultChannelUpdate = ChannelUpdate(sig, Block.RegtestGenesisBlock.hash, 0, 0, "0000", 0, 42000, 0, 0) val defaultChannelUpdate = ChannelUpdate(sig, Block.RegtestGenesisBlock.hash, 0, 0, "0000", 0, 42000, 0, 0)
val channelUpdate_ab = defaultChannelUpdate.copy(shortChannelId = 1, cltvExpiryDelta = 4, feeBaseMsat = 642000, feeProportionalMillionths = 7) val channelUpdate_ab = defaultChannelUpdate.copy(shortChannelId = 1, cltvExpiryDelta = 4, feeBaseMsat = 642000, feeProportionalMillionths = 7)
val channelUpdate_bc = defaultChannelUpdate.copy(shortChannelId = 2, cltvExpiryDelta = 5, feeBaseMsat = 153000, feeProportionalMillionths = 4) val channelUpdate_bc = defaultChannelUpdate.copy(shortChannelId = 2, cltvExpiryDelta = 5, feeBaseMsat = 153000, feeProportionalMillionths = 4)

View file

@ -34,7 +34,8 @@ class RelayerSpec extends TestkitBaseClass {
val register = TestProbe() val register = TestProbe()
val paymentHandler = TestProbe() val paymentHandler = TestProbe()
// we are node B in the route A -> B -> C -> .... // we are node B in the route A -> B -> C -> ....
val relayer = system.actorOf(Relayer.props(TestConstants.Bob.nodeParams.copy(privateKey = priv_b), register.ref, paymentHandler.ref)) //val relayer = system.actorOf(Relayer.props(TestConstants.Bob.nodeParams.copy(nodeKey = priv_b), register.ref, paymentHandler.ref))
val relayer = system.actorOf(Relayer.props(TestConstants.Bob.nodeParams, register.ref, paymentHandler.ref))
test((relayer, register, paymentHandler)) test((relayer, register, paymentHandler))
} }
} }

View file

@ -1,6 +1,7 @@
package fr.acinq.eclair.wire package fr.acinq.eclair.wire
import fr.acinq.bitcoin.{BinaryData, OutPoint} import fr.acinq.bitcoin.DeterministicWallet.KeyPath
import fr.acinq.bitcoin.{BinaryData, DeterministicWallet, OutPoint}
import fr.acinq.eclair.channel.{LocalParams, RemoteParams} import fr.acinq.eclair.channel.{LocalParams, RemoteParams}
import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.{Local, Relayed} import fr.acinq.eclair.payment.{Local, Relayed}
@ -25,22 +26,31 @@ class ChannelCodecsSpec extends FunSuite {
bin bin
} }
test("encode/decode key paths (all 0s)") {
val keyPath = KeyPath(Seq(0, 0, 0, 0))
val encoded = keyPathCodec.encode(keyPath).require
val decoded = keyPathCodec.decode(encoded).require
assert(keyPath === decoded.value)
}
test("encode/decode key paths (all 1s)") {
val keyPath = KeyPath(Seq(0xffffffffL, 0xffffffffL, 0xffffffffL, 0xffffffffL))
val encoded = keyPathCodec.encode(keyPath).require
val decoded = keyPathCodec.decode(encoded).require
assert(keyPath === decoded.value)
}
test("encode/decode localparams") { test("encode/decode localparams") {
val o = LocalParams( val o = LocalParams(
nodeId = randomKey.publicKey, nodeId = randomKey.publicKey,
channelKeyPath = DeterministicWallet.KeyPath(Seq(42)),
dustLimitSatoshis = Random.nextInt(Int.MaxValue), dustLimitSatoshis = Random.nextInt(Int.MaxValue),
maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)), maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)),
channelReserveSatoshis = Random.nextInt(Int.MaxValue), channelReserveSatoshis = Random.nextInt(Int.MaxValue),
htlcMinimumMsat = Random.nextInt(Int.MaxValue), htlcMinimumMsat = Random.nextInt(Int.MaxValue),
toSelfDelay = Random.nextInt(Short.MaxValue), toSelfDelay = Random.nextInt(Short.MaxValue),
maxAcceptedHtlcs = Random.nextInt(Short.MaxValue), maxAcceptedHtlcs = Random.nextInt(Short.MaxValue),
fundingPrivKey = randomKey,
revocationSecret = randomKey.value,
paymentKey = randomKey,
delayedPaymentKey = randomKey.value,
htlcKey = randomKey,
defaultFinalScriptPubKey = randomBytes(10 + Random.nextInt(200)), defaultFinalScriptPubKey = randomBytes(10 + Random.nextInt(200)),
shaSeed = randomBytes(32),
isFunder = Random.nextBoolean(), isFunder = Random.nextBoolean(),
globalFeatures = randomBytes(256), globalFeatures = randomBytes(256),
localFeatures = randomBytes(256)) localFeatures = randomBytes(256))