Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Segregate the node seed from the channel seed #1584

Merged
merged 13 commits into from
Nov 5, 2020
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,10 @@ eclair-node-<version>-<commit_id>/bin/eclair-node.sh -Dlogback.configurationFile

The files that you need to backup are located in your data directory. You must backup:

* your seed (`seed.dat`)
* your seeds (`nodeSeed.dat` and `channelSeed.dat`)
* your channel database (`eclair.sqlite.bak` under directory `mainnet`, `testnet` or `regtest` depending on which chain you're running on)

Your seed never changes once it has been created, but your channels will change whenever you receive or send payments. Eclair will
Your seeds never change once they have been created, but your channels will change whenever you receive or send payments. Eclair will
create and maintain a snapshot of its database, named `eclair.sqlite.bak`, in your data directory, and update it when needed. This file is
always consistent and safe to use even when Eclair is running, and this is what you should backup regularly.

Expand Down
2 changes: 1 addition & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ class EclairImpl(appKit: Kit) extends Eclair {

override def signMessage(message: ByteVector): SignedMessage = {
val bytesToSign = SignedMessage.signedBytes(message)
val (signature, recoveryId) = appKit.nodeParams.keyManager.signDigest(bytesToSign)
val (signature, recoveryId) = appKit.nodeParams.nodeKeyManager.signDigest(bytesToSign)
SignedMessage(appKit.nodeParams.nodeId, message.toBase64, (recoveryId + 31).toByte +: signature)
}

Expand Down
72 changes: 54 additions & 18 deletions eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,19 @@ import java.util.concurrent.atomic.AtomicLong

import com.typesafe.config.{Config, ConfigFactory, ConfigValueType}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi}
import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, Satoshi}
import fr.acinq.eclair.NodeParams.WatcherType
import fr.acinq.eclair.Setup.Seeds
import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeerateTolerance, OnChainFeeConf}
import fr.acinq.eclair.channel.Channel
import fr.acinq.eclair.crypto.KeyManager
import fr.acinq.eclair.crypto.Noise.KeyPair
import fr.acinq.eclair.crypto.{ChannelKeyManager, NodeKeyManager}
import fr.acinq.eclair.db._
import fr.acinq.eclair.io.PeerConnection
import fr.acinq.eclair.router.Router.RouterConf
import fr.acinq.eclair.tor.Socks5ProxyParams
import fr.acinq.eclair.wire.{Color, EncodingType, NodeAddress}
import grizzled.slf4j.Logging
import scodec.bits.ByteVector

import scala.concurrent.duration._
Expand All @@ -44,7 +46,8 @@ import scala.jdk.CollectionConverters._
/**
* Created by PM on 26/02/2017.
*/
case class NodeParams(keyManager: KeyManager,
case class NodeParams(nodeKeyManager: NodeKeyManager,
channelKeyManager: ChannelKeyManager,
instanceId: UUID, // a unique instance ID regenerated after each restart
private val blockCount: AtomicLong,
alias: String,
Expand Down Expand Up @@ -87,8 +90,8 @@ case class NodeParams(keyManager: KeyManager,
socksProxy_opt: Option[Socks5ProxyParams],
maxPaymentAttempts: Int,
enableTrampolinePayment: Boolean) {
val privateKey = keyManager.nodeKey.privateKey
val nodeId = keyManager.nodeId
val privateKey: Crypto.PrivateKey = nodeKeyManager.nodeKey.privateKey
val nodeId: PublicKey = nodeKeyManager.nodeId
val keyPair = KeyPair(nodeId.value, privateKey.value)

val pluginMessageTags: Set[Int] = pluginParams.flatMap(_.tags).toSet
Expand All @@ -98,7 +101,7 @@ case class NodeParams(keyManager: KeyManager,
def featuresFor(nodeId: PublicKey) = overrideFeatures.getOrElse(nodeId, features)
}

object NodeParams {
object NodeParams extends Logging {

sealed trait WatcherType

Expand All @@ -118,18 +121,49 @@ object NodeParams {
.withFallback(ConfigFactory.parseFile(new File(datadir, "eclair.conf")))
.withFallback(ConfigFactory.load())

def getSeed(datadir: File): ByteVector = {
val seedPath = new File(datadir, "seed.dat")
if (seedPath.exists()) {
ByteVector(Files.readAllBytes(seedPath.toPath))
} else {
datadir.mkdirs()
val seed = randomBytes32
Files.write(seedPath.toPath, seed.toArray)
seed
private def readSeedFromFile(seedPath: File): ByteVector = {
logger.info(s"use seed file: ${seedPath.getCanonicalPath}")
ByteVector(Files.readAllBytes(seedPath.toPath))
}

private def writeSeedToFile(path: File, seed: ByteVector): Unit = {
Files.write(path.toPath, seed.toArray)
logger.info(s"create new seed file: ${path.getCanonicalPath}")
}

private def migrateSeedFile(old: File, `new`: File): Unit = {
t-bast marked this conversation as resolved.
Show resolved Hide resolved
dariuskramer marked this conversation as resolved.
Show resolved Hide resolved
if (old.exists() && !`new`.exists()) {
Files.copy(old.toPath, `new`.toPath)
logger.info(s"migrate seed file: ${old.getCanonicalPath} → ${`new`.getCanonicalPath}")
}
}

def getSeeds(datadir: File): Seeds = {
// Previously we used one seed file ("seed.dat") to generate the node and the channel private keys
// Now we use two separate files and thus we need to migrate the old seed file if necessary
val oldSeedPath = new File(datadir, "seed.dat")
val nodeSeedFilename: String = "nodeSeed.dat"
val channelSeedFilename: String = "channelSeed.dat"
dariuskramer marked this conversation as resolved.
Show resolved Hide resolved

def getSeed(filename: String): ByteVector = {
val seedPath = new File(datadir, filename)
if (seedPath.exists()) {
readSeedFromFile(seedPath)
} else if (oldSeedPath.exists()) {
migrateSeedFile(oldSeedPath, seedPath)
readSeedFromFile(seedPath)
} else {
val randomSeed = randomBytes32
writeSeedToFile(seedPath, randomSeed)
randomSeed.bytes
}
}

val nodeSeed = getSeed(nodeSeedFilename)
val channelSeed = getSeed(channelSeedFilename)
Seeds(nodeSeed, channelSeed)
}

private val chain2Hash: Map[String, ByteVector32] = Map(
"regtest" -> Block.RegtestGenesisBlock.hash,
"testnet" -> Block.TestnetGenesisBlock.hash,
Expand All @@ -140,8 +174,9 @@ object NodeParams {

def chainFromHash(chainHash: ByteVector32): String = chain2Hash.map(_.swap).getOrElse(chainHash, throw new RuntimeException(s"invalid chainHash '$chainHash'"))

def makeNodeParams(config: Config, instanceId: UUID, keyManager: KeyManager, torAddress_opt: Option[NodeAddress], database: Databases,
blockCount: AtomicLong, feeEstimator: FeeEstimator, pluginParams: Seq[PluginParams] = Nil): NodeParams = {
def makeNodeParams(config: Config, instanceId: UUID, nodeKeyManager: NodeKeyManager, channelKeyManager: ChannelKeyManager,
torAddress_opt: Option[NodeAddress], database: Databases, blockCount: AtomicLong, feeEstimator: FeeEstimator,
pluginParams: Seq[PluginParams] = Nil): NodeParams = {
// check configuration for keys that have been renamed
val deprecatedKeyPaths = Map(
// v0.3.2
Expand Down Expand Up @@ -263,7 +298,8 @@ object NodeParams {
}

NodeParams(
keyManager = keyManager,
nodeKeyManager = nodeKeyManager,
channelKeyManager = channelKeyManager,
instanceId = instanceId,
blockCount = blockCount,
alias = nodeAlias,
Expand Down
24 changes: 15 additions & 9 deletions eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import akka.util.Timeout
import com.softwaremill.sttp.okhttp.OkHttpFutureBackend
import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi}
import fr.acinq.eclair.NodeParams.{BITCOIND, ELECTRUM}
import fr.acinq.eclair.Setup.Seeds
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BatchingBitcoinJsonRPCClient, ExtendedBitcoinClient}
import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor
import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, ZmqWatcher}
Expand All @@ -40,7 +41,7 @@ import fr.acinq.eclair.blockchain.electrum.db.sqlite.SqliteWalletDb
import fr.acinq.eclair.blockchain.fee.{ConstantFeeProvider, _}
import fr.acinq.eclair.blockchain.{EclairWallet, _}
import fr.acinq.eclair.channel.Register
import fr.acinq.eclair.crypto.LocalKeyManager
import fr.acinq.eclair.crypto.{LocalChannelKeyManager, LocalNodeKeyManager}
import fr.acinq.eclair.db.Databases.FileBackup
import fr.acinq.eclair.db.{Databases, FileBackupHandler}
import fr.acinq.eclair.io.{ClientSpawner, Server, Switchboard}
Expand All @@ -65,13 +66,13 @@ import scala.util.{Failure, Success}
*
* Created by PM on 25/01/2016.
*
* @param datadir directory where eclair-core will write/read its data.
* @param seed_opt optional seed, if set eclair will use it instead of generating one and won't create a seed.dat file.
* @param db optional databases to use, if not set eclair will create the necessary databases
* @param datadir directory where eclair-core will write/read its data.
* @param seeds_opt optional seeds, if set eclair will use them instead of generating them and won't create a nodeSeed.dat and channelSeed.dat files.
* @param db optional databases to use, if not set eclair will create the necessary databases
*/
class Setup(datadir: File,
pluginParams: Seq[PluginParams],
seed_opt: Option[ByteVector] = None,
seeds_opt: Option[Seeds] = None,
db: Option[Databases] = None)(implicit system: ActorSystem) extends Logging {

implicit val timeout = Timeout(30 seconds)
Expand All @@ -88,10 +89,11 @@ class Setup(datadir: File,

datadir.mkdirs()
val config = system.settings.config.getConfig("eclair")
val seed = seed_opt.getOrElse(NodeParams.getSeed(datadir))
val Seeds(nodeSeed, channelSeed) = seeds_opt.getOrElse(NodeParams.getSeeds(datadir))
val chain = config.getString("chain")
val chaindir = new File(datadir, chain)
val keyManager = new LocalKeyManager(seed, NodeParams.hashFromChain(chain))
val nodeKeyManager = new LocalNodeKeyManager(nodeSeed, NodeParams.hashFromChain(chain))
val channelKeyManager = new LocalChannelKeyManager(channelSeed, NodeParams.hashFromChain(chain))
val instanceId = UUID.randomUUID()

logger.info(s"instanceid=$instanceId")
Expand Down Expand Up @@ -124,7 +126,7 @@ class Setup(datadir: File,
// @formatter:on
}

val nodeParams = NodeParams.makeNodeParams(config, instanceId, keyManager, initTor(), databases, blockCount, feeEstimator, pluginParams)
val nodeParams = NodeParams.makeNodeParams(config, instanceId, nodeKeyManager, channelKeyManager, initTor(), databases, blockCount, feeEstimator, pluginParams)
pluginParams.foreach(param => logger.info(param.toString))

val serverBindingAddress = new InetSocketAddress(
Expand Down Expand Up @@ -279,7 +281,7 @@ class Setup(datadir: File,
case Electrum(electrumClient) =>
val sqlite = DriverManager.getConnection(s"jdbc:sqlite:${new File(chaindir, "wallet.sqlite")}")
val walletDb = new SqliteWalletDb(sqlite)
val electrumWallet = system.actorOf(ElectrumWallet.props(seed, electrumClient, ElectrumWallet.WalletParameters(nodeParams.chainHash, walletDb)), "electrum-wallet")
val electrumWallet = system.actorOf(ElectrumWallet.props(channelSeed, electrumClient, ElectrumWallet.WalletParameters(nodeParams.chainHash, walletDb)), "electrum-wallet")
new ElectrumEclairWallet(electrumWallet, nodeParams.chainHash)
}
_ = wallet.getReceiveAddress.map(address => logger.info(s"initial wallet address=$address"))
Expand Down Expand Up @@ -373,6 +375,10 @@ class Setup(datadir: File,

}

object Setup {
final case class Seeds(nodeSeed: ByteVector, channelSeed: ByteVector)
}

// @formatter:off
sealed trait Bitcoin
case class Bitcoind(bitcoinClient: BasicBitcoinJsonRPCClient) extends Bitcoin
Expand Down
21 changes: 11 additions & 10 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ import fr.acinq.eclair._
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel.Helpers.{Closing, Funding}
import fr.acinq.eclair.channel.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.crypto.{ChannelKeyManager, ShaChain}
import fr.acinq.eclair.db.PendingRelayDb
import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.PaymentSettlingOnChain
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions.Transactions.TxOwner
import fr.acinq.eclair.transactions._
Expand Down Expand Up @@ -102,7 +102,8 @@ object Channel {
class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId: PublicKey, blockchain: 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 nodeParams.keyManager

private val keyManager: ChannelKeyManager = nodeParams.channelKeyManager

// we pass these to helpers classes so that they have the logging context
implicit def implicitLog: akka.event.DiagnosticLoggingAdapter = diagLog
Expand Down Expand Up @@ -163,7 +164,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = true, temporaryChannelId, initialFeeratePerKw, Some(fundingTxFeeratePerKw)))
activeConnection = remote
val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey
val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion)
val channelKeyPath = keyManager.keyPath(localParams, channelVersion)
val open = OpenChannel(nodeParams.chainHash,
temporaryChannelId = temporaryChannelId,
fundingSatoshis = fundingSatoshis,
Expand Down Expand Up @@ -294,7 +295,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
case Success(_) =>
context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = false, open.temporaryChannelId, open.feeratePerKw, None))
val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey
val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion)
val channelKeyPath = keyManager.keyPath(localParams, channelVersion)
// TODO: maybe also check uniqueness of temporary channel id
val minimumDepth = Helpers.minDepthForFunding(nodeParams, open.fundingSatoshis)
val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId,
Expand Down Expand Up @@ -558,7 +559,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
case Success(_) =>
log.info(s"channelId=${commitments.channelId} was confirmed at blockHeight=$blockHeight txIndex=$txIndex")
blockchain ! WatchLost(self, commitments.commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_LOST)
val channelKeyPath = keyManager.channelKeyPath(d.commitments.localParams, commitments.channelVersion)
val channelKeyPath = keyManager.keyPath(d.commitments.localParams, commitments.channelVersion)
val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1)
val fundingLocked = FundingLocked(commitments.channelId, nextPerCommitmentPoint)
deferred.foreach(self ! _)
Expand Down Expand Up @@ -1418,7 +1419,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
activeConnection = r

val yourLastPerCommitmentSecret = d.commitments.remotePerCommitmentSecrets.lastIndex.flatMap(d.commitments.remotePerCommitmentSecrets.getHash).getOrElse(ByteVector32.Zeroes)
val channelKeyPath = keyManager.channelKeyPath(d.commitments.localParams, d.commitments.channelVersion)
val channelKeyPath = keyManager.keyPath(d.commitments.localParams, d.commitments.channelVersion)
val myCurrentPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, d.commitments.localCommit.index)

val channelReestablish = ChannelReestablish(
Expand Down Expand Up @@ -1487,14 +1488,14 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId

case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_LOCKED) =>
log.debug("re-sending fundingLocked")
val channelKeyPath = keyManager.channelKeyPath(d.commitments.localParams, d.commitments.channelVersion)
val channelKeyPath = keyManager.keyPath(d.commitments.localParams, d.commitments.channelVersion)
val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1)
val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint)
goto(WAIT_FOR_FUNDING_LOCKED) sending fundingLocked

case Event(channelReestablish: ChannelReestablish, d: DATA_NORMAL) =>
var sendQueue = Queue.empty[LightningMessage]
val channelKeyPath = keyManager.channelKeyPath(d.commitments.localParams, d.commitments.channelVersion)
val channelKeyPath = keyManager.keyPath(d.commitments.localParams, d.commitments.channelVersion)
channelReestablish match {
case ChannelReestablish(_, _, nextRemoteRevocationNumber, yourLastPerCommitmentSecret, _) if !Helpers.checkLocalCommit(d, nextRemoteRevocationNumber) =>
// if next_remote_revocation_number is greater than our local commitment index, it means that either we are using an outdated commitment, or they are lying
Expand Down Expand Up @@ -2280,7 +2281,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
} else if (commitments1.localCommit.index == channelReestablish.nextRemoteRevocationNumber + 1) {
// our last revocation got lost, let's resend it
log.debug("re-sending last revocation")
val channelKeyPath = keyManager.channelKeyPath(d.commitments.localParams, d.commitments.channelVersion)
val channelKeyPath = keyManager.keyPath(d.commitments.localParams, d.commitments.channelVersion)
val localPerCommitmentSecret = keyManager.commitmentSecret(channelKeyPath, d.commitments.localCommit.index - 1)
val localNextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, d.commitments.localCommit.index + 1)
val revocation = RevokeAndAck(
Expand Down
Loading