diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/EclairWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/EclairWallet.scala index a2e2973284..550edc4daa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/EclairWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/EclairWallet.scala @@ -30,7 +30,9 @@ trait EclairWallet { def getFinalAddress: Future[String] - def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] + def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long, lockUnspent: Boolean = true): Future[MakeFundingTxResponse] + + def signTransactionComplete(tx: Transaction): Future[Transaction] /** * Committing *must* include publishing the transaction on the network. @@ -67,3 +69,4 @@ trait EclairWallet { } final case class MakeFundingTxResponse(fundingTx: Transaction, fundingTxOutputIndex: Int, fee: Satoshi) +final case class SignFundingTxResponse(fundingTx: Transaction, fundingTxOutputIndex: Int, fee: Satoshi) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala index f32856491e..2d3bf12f96 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala @@ -62,6 +62,11 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC def signTransaction(tx: Transaction): Future[SignTransactionResponse] = signTransaction(Transaction.write(tx).toHex) + override def signTransactionComplete(tx: Transaction): Future[Transaction] = signTransaction(tx).map { + case SignTransactionResponse(signedTx, true) => signedTx + case _ => throw new IllegalStateException("Signed transaction is not complete") + } + def getTransaction(txid: ByteVector32): Future[Transaction] = rpcClient.invoke("getrawtransaction", txid.toString()) collect { case JString(hex) => Transaction.read(hex) } def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] = publishTransaction(Transaction.write(tx).toHex) @@ -89,7 +94,7 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC } } - override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = { + override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long, lockUnspent: Boolean = true): Future[MakeFundingTxResponse] = { // partial funding tx val partialFundingTx = Transaction( version = 2, @@ -98,7 +103,7 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC lockTime = 0) for { // we ask bitcoin core to add inputs to the funding tx, and use the specified change address - FundTransactionResponse(unsignedFundingTx, _, fee) <- fundTransaction(partialFundingTx, lockUnspents = true, feeRatePerKw) + FundTransactionResponse(unsignedFundingTx, _, fee) <- fundTransaction(partialFundingTx, lockUnspents = lockUnspent, feeRatePerKw) // now let's sign the funding tx SignTransactionResponse(fundingTx, true) <- signTransactionOrUnlock(unsignedFundingTx) // there will probably be a change output, so we need to find which output is ours diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumEclairWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumEclairWallet.scala index ed8dea4e01..fcef53d3be 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumEclairWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumEclairWallet.scala @@ -36,7 +36,11 @@ class ElectrumEclairWallet(val wallet: ActorRef, chainHash: ByteVector32)(implic def getXpub: Future[GetXpubResponse] = (wallet ? GetXpub).mapTo[GetXpubResponse] - override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = { + override def signTransactionComplete(tx: Transaction): Future[Transaction] = { + (wallet ? SignTransaction(tx)).mapTo[SignTransactionResponse].map(_.tx) + } + + override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long, lockUnspent: Boolean = true): Future[MakeFundingTxResponse] = { val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(amount, pubkeyScript) :: Nil, lockTime = 0) (wallet ? CompleteTransaction(tx, feeRatePerKw)).mapTo[CompleteTransactionResponse].map(response => response match { case CompleteTransactionResponse(tx1, fee1, None) => MakeFundingTxResponse(tx1, 0, fee1) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWallet.scala index 428623f9e3..f39da4dbb6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWallet.scala @@ -445,6 +445,11 @@ class ElectrumWallet(seed: ByteVector, client: ActorRef, params: ElectrumWallet. log.info(s"cancelling txid=${tx.txid}") stay using persistAndNotify(data.cancelTransaction(tx)) replying CancelTransactionResponse(tx) + case Event(SignTransaction(tx), data) => + log.info(s"signing txid=${tx.txid}") + val signed = data.signTransaction(tx) + stay replying SignTransactionResponse(signed) + case Event(bc@ElectrumClient.BroadcastTransaction(tx), _) => log.info(s"broadcasting txid=${tx.txid}") client forward bc @@ -543,6 +548,9 @@ object ElectrumWallet { case class IsDoubleSpent(tx: Transaction) extends Request case class IsDoubleSpentResponse(tx: Transaction, isDoubleSpent: Boolean) extends Response + case class SignTransaction(tx: Transaction) extends Request + case class SignTransactionResponse(tx: Transaction) extends Response + sealed trait WalletEvent /** * diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 3199abcbe4..97c450d4da 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -24,16 +24,17 @@ import fr.acinq.bitcoin._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel.Helpers.{Closing, Funding} -import fr.acinq.eclair.crypto.{ShaChain, Sphinx} +import fr.acinq.eclair.crypto.{KeyManager, LocalKeyManager, ShaChain, Sphinx} import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.{ChannelReestablish, _} - +import scodec.bits.ByteVector +import scodec.bits._ import scala.compat.Platform -import scala.concurrent.ExecutionContext import scala.concurrent.duration._ +import scala.concurrent.{Await, ExecutionContext} import scala.util.{Failure, Success, Try} @@ -90,6 +91,57 @@ object Channel { case class RemoteError(e: Error) extends ChannelError // @formatter:on + def makeFunderChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: ByteVector, fundingSatoshis: Long, fundingInput: TxIn): LocalParams = { + val channelKeyPath = LocalKeyManager.makeChannelKeyPathFunder(fundingInput.outPoint.hash) + makeChannelParams(nodeParams, defaultFinalScriptPubKey, fundingSatoshis, Left(channelKeyPath)) + } + + def makeFundeeChannelParams(nodeParams: NodeParams, open: OpenChannel, defaultFinalScriptPubKey: ByteVector, fundingSatoshis: Long): LocalParams = { + val blockHeight = Globals.blockCount.get + val counter = nodeParams.db.channels.getCounterFor(blockHeight) + val fundingKeyPath = LocalKeyManager.makeChannelKeyPathFundeePubkey(blockHeight, counter) + val localFundingPubkey = nodeParams.keyManager.fundingPublicKey(fundingKeyPath).publicKey + val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey, open.fundingPubkey))) + val channelKeyPath = LocalKeyManager.makeChannelKeyPathFundee(fundingPubkeyScript) + val channelKeyPaths = KeyPathFundee(fundingKeyPath, channelKeyPath) + makeChannelParams(nodeParams, defaultFinalScriptPubKey, fundingSatoshis, Right(channelKeyPaths)) + } + + def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: ByteVector, fundingSatoshis: Long, channelKeyPath: Either[DeterministicWallet.KeyPath, KeyPathFundee]): LocalParams = { + LocalParams( + nodeParams.nodeId, + channelKeyPath, + dustLimitSatoshis = nodeParams.dustLimitSatoshis, + maxHtlcValueInFlightMsat = nodeParams.maxHtlcValueInFlightMsat, + channelReserveSatoshis = Math.max((nodeParams.reserveToFundingRatio * fundingSatoshis).toLong, nodeParams.dustLimitSatoshis), // BOLT #2: make sure that our reserve is above our dust limit + htlcMinimumMsat = nodeParams.htlcMinimumMsat, + toSelfDelay = nodeParams.toRemoteDelayBlocks, // we choose their delay + maxAcceptedHtlcs = nodeParams.maxAcceptedHtlcs, + defaultFinalScriptPubKey = defaultFinalScriptPubKey, + globalFeatures = nodeParams.globalFeatures, + localFeatures = nodeParams.localFeatures) + } + + def stripSignaturesFromTx(funding: MakeFundingTxResponse) = funding.copy(fundingTx = funding.fundingTx.copy( + txIn = funding.fundingTx.txIn.map(_.copy( + signatureScript = ByteVector.empty, + witness = ScriptWitness.empty + )) + )) + + // P2WSH of 2-2 multisig + val dummyMultisigScriptPubkey = hex"0020992b70c4600f066c3b63146c06e651cae903275761f1a2920d966bcb05a0c9ba" + + def fundingKeyPath(localParams: LocalParams) = localParams.channelKeyPath match { + case Left(funderKeyPath) => funderKeyPath + case Right(fundeeKeyPath) => fundeeKeyPath.fundingKeyPath + } + + def keyPath(localParams: LocalParams) = localParams.channelKeyPath match { + case Left(funderKeyPath) => funderKeyPath + case Right(fundeeKeyPath) => fundeeKeyPath.pointsKeyPath + } + } class Channel(val nodeParams: NodeParams, val 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] { @@ -145,28 +197,13 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId startWith(WAIT_FOR_INIT_INTERNAL, Nothing) when(WAIT_FOR_INIT_INTERNAL)(handleExceptions { - case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, _, localParams, remote, _, channelFlags), Nothing) => - context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, true, temporaryChannelId)) + case Event(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, remote, remoteInit, channelFlags), Nothing) => forwarder ! remote - val open = OpenChannel(nodeParams.chainHash, - temporaryChannelId = temporaryChannelId, - fundingSatoshis = fundingSatoshis, - pushMsat = pushMsat, - dustLimitSatoshis = localParams.dustLimitSatoshis, - maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, - channelReserveSatoshis = localParams.channelReserveSatoshis, - htlcMinimumMsat = localParams.htlcMinimumMsat, - feeratePerKw = initialFeeratePerKw, - toSelfDelay = localParams.toSelfDelay, - maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, - fundingPubkey = keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, - revocationBasepoint = keyManager.revocationPoint(localParams.channelKeyPath).publicKey, - paymentBasepoint = keyManager.paymentPoint(localParams.channelKeyPath).publicKey, - delayedPaymentBasepoint = keyManager.delayedPaymentPoint(localParams.channelKeyPath).publicKey, - htlcBasepoint = keyManager.htlcPoint(localParams.channelKeyPath).publicKey, - firstPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, 0), - channelFlags = channelFlags) - goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder, open) sending open + + // the resulting funding tx response will NOT have input script signature + wallet.makeFundingTx(pubkeyScript = dummyMultisigScriptPubkey, Satoshi(fundingSatoshis), fundingTxFeeratePerKw).map(stripSignaturesFromTx).pipeTo(self) + + goto(WAIT_FOR_FUNDING_INTERNAL_CREATED) using DATA_WAIT_FOR_FUNDING_INTERNAL_CREATED(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, remote, remoteInit, channelFlags) case Event(inputFundee@INPUT_INIT_FUNDEE(_, localParams, remote, _), Nothing) if !localParams.isFunder => forwarder ! remote @@ -271,6 +308,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Failure(t) => handleLocalError(t, d, Some(open)) case Success(_) => context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, false, open.temporaryChannelId)) + // TODO: maybe also check uniqueness of temporary channel id val minimumDepth = nodeParams.minDepthBlocks val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId, @@ -281,12 +319,12 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId htlcMinimumMsat = localParams.htlcMinimumMsat, toSelfDelay = localParams.toSelfDelay, maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, - fundingPubkey = keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, - revocationBasepoint = keyManager.revocationPoint(localParams.channelKeyPath).publicKey, - paymentBasepoint = keyManager.paymentPoint(localParams.channelKeyPath).publicKey, - delayedPaymentBasepoint = keyManager.delayedPaymentPoint(localParams.channelKeyPath).publicKey, - htlcBasepoint = keyManager.htlcPoint(localParams.channelKeyPath).publicKey, - firstPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, 0)) + fundingPubkey = keyManager.fundingPublicKey(fundingKeyPath(localParams)).publicKey, + revocationBasepoint = keyManager.revocationPoint(keyPath(localParams)).publicKey, + paymentBasepoint = keyManager.paymentPoint(keyPath(localParams)).publicKey, + delayedPaymentBasepoint = keyManager.delayedPaymentPoint(keyPath(localParams)).publicKey, + htlcBasepoint = keyManager.htlcPoint(keyPath(localParams)).publicKey, + firstPerCommitmentPoint = keyManager.commitmentPoint(keyPath(localParams), 0)) val remoteParams = RemoteParams( nodeId = remoteNodeId, dustLimitSatoshis = open.dustLimitSatoshis, @@ -313,8 +351,61 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(INPUT_DISCONNECTED, _) => goto(CLOSED) }) + when(WAIT_FOR_FUNDING_INTERNAL_CREATED)(handleExceptions{ + case Event(funding:MakeFundingTxResponse, DATA_WAIT_FOR_FUNDING_INTERNAL_CREATED(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, remote, remoteInit, channelFlags)) => + context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, true, temporaryChannelId)) + + val defaultFinalScriptPubKey = Helpers.getFinalScriptPubKey(wallet, nodeParams.chainHash) + val localParams = makeFunderChannelParams(nodeParams, defaultFinalScriptPubKey, fundingSatoshis, funding.fundingTx.txIn.head) + log.info(s"using localParams=$localParams") + + val open = OpenChannel(nodeParams.chainHash, + temporaryChannelId = temporaryChannelId, + fundingSatoshis = fundingSatoshis, + pushMsat = pushMsat, + dustLimitSatoshis = localParams.dustLimitSatoshis, + maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, + channelReserveSatoshis = localParams.channelReserveSatoshis, + htlcMinimumMsat = localParams.htlcMinimumMsat, + feeratePerKw = initialFeeratePerKw, + toSelfDelay = localParams.toSelfDelay, + maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, + fundingPubkey = keyManager.fundingPublicKey(fundingKeyPath(localParams)).publicKey, + revocationBasepoint = keyManager.revocationPoint(keyPath(localParams)).publicKey, + paymentBasepoint = keyManager.paymentPoint(keyPath(localParams)).publicKey, + delayedPaymentBasepoint = keyManager.delayedPaymentPoint(keyPath(localParams)).publicKey, + htlcBasepoint = keyManager.htlcPoint(keyPath(localParams)).publicKey, + firstPerCommitmentPoint = keyManager.commitmentPoint(keyPath(localParams), 0), + channelFlags = channelFlags) + + val nextStateData = INPUT_INIT_FUNDER_WITH_PARAMS(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, remote, remoteInit, channelFlags) + + goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(nextStateData, open, funding) sending open + + case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL_CREATED) => + log.error(t, s"wallet returned error: ") + replyToUser(Left(LocalError(t))) + handleLocalError(ChannelFundingError(d.temporaryChannelId), d, None) // we use a generic exception and don't send the internal error to the peer + + case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_INTERNAL_CREATED) => + replyToUser(Left(RemoteError(e))) + handleRemoteError(e, d) + + case Event(CMD_CLOSE(_), _) => + replyToUser(Right("closed")) + goto(CLOSED) replying "ok" + + case Event(INPUT_DISCONNECTED, _) => + replyToUser(Left(LocalError(new RuntimeException("disconnected")))) + goto(CLOSED) + + case Event(TickChannelOpenTimeout, _) => + replyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) + goto(CLOSED) + }) + when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions { - case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, _, remoteInit, _), open)) => + case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER_WITH_PARAMS(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, _, localParams, _, remoteInit, _), open, funding)) => log.info(s"received AcceptChannel=$accept") Try(Helpers.validateParamsFunder(nodeParams, open, accept)) match { case Failure(t) => handleLocalError(t, d, Some(accept)) @@ -336,35 +427,48 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId globalFeatures = remoteInit.globalFeatures, localFeatures = remoteInit.localFeatures) log.debug(s"remote params: $remoteParams") - val localFundingPubkey = keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey + + val localFundingPubkey = keyManager.fundingPublicKey(fundingKeyPath(localParams)).publicKey val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey, remoteParams.fundingPubKey))) - wallet.makeFundingTx(fundingPubkeyScript, Satoshi(fundingSatoshis), fundingTxFeeratePerKw).pipeTo(self) - goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, accept.firstPerCommitmentPoint, open) + + // update the funding TX with the new scriptPubkey + val finalFundingTx = funding.fundingTx.copy ( + txOut = funding.fundingTx.txOut.updated(funding.fundingTxOutputIndex, TxOut(Satoshi(fundingSatoshis), fundingPubkeyScript)) + ) + + // add our signature to the funding transaction + wallet.signTransactionComplete(finalFundingTx).map(SignFundingTxResponse(_, funding.fundingTxOutputIndex, funding.fee)).pipeTo(self) + + goto(WAIT_FOR_FUNDING_INTERNAL_SIGNED) using DATA_WAIT_FOR_FUNDING_INTERNAL_SIGNED(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, accept.firstPerCommitmentPoint, open, finalFundingTx) } - case Event(CMD_CLOSE(_), _) => + case Event(CMD_CLOSE(_) | CMD_FORCECLOSE, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => replyToUser(Right("closed")) + wallet.rollback(d.unsignedFundingTx.fundingTx) goto(CLOSED) replying "ok" case Event(e: Error, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => replyToUser(Left(RemoteError(e))) + wallet.rollback(d.unsignedFundingTx.fundingTx) handleRemoteError(e, d) - case Event(INPUT_DISCONNECTED, _) => + case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => replyToUser(Left(LocalError(new RuntimeException("disconnected")))) + wallet.rollback(d.unsignedFundingTx.fundingTx) goto(CLOSED) - case Event(TickChannelOpenTimeout, _) => + case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => replyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) + wallet.rollback(d.unsignedFundingTx.fundingTx) goto(CLOSED) }) - when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions { - case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, open)) => + when(WAIT_FOR_FUNDING_INTERNAL_SIGNED)(handleExceptions { + case Event(SignFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), DATA_WAIT_FOR_FUNDING_INTERNAL_SIGNED(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, open, fundingTxResponse)) => // let's create the first commitment tx that spends the yet uncommitted funding tx val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.maxFeerateMismatch) require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) + val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(fundingKeyPath(localParams))) // signature of their initial commitment tx that pays remote pushMsat val fundingCreated = FundingCreated( temporaryChannelId = temporaryChannelId, @@ -372,31 +476,36 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId fundingOutputIndex = fundingTxOutputIndex, signature = localSigOfRemoteTx ) + val channelId = toLongId(fundingTx.hash, fundingTxOutputIndex) context.parent ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId)) // NB: we don't send a ChannelSignatureSent for the first commit goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, fundingTxFee, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), open.channelFlags, fundingCreated) sending fundingCreated - case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL_SIGNED) => log.error(t, s"wallet returned error: ") replyToUser(Left(LocalError(t))) handleLocalError(ChannelFundingError(d.temporaryChannelId), d, None) // we use a generic exception and don't send the internal error to the peer - case Event(CMD_CLOSE(_), _) => + case Event(CMD_CLOSE(_) | CMD_FORCECLOSE, d: DATA_WAIT_FOR_FUNDING_INTERNAL_SIGNED) => replyToUser(Right("closed")) + wallet.rollback(d.unsignedFundingTx) goto(CLOSED) replying "ok" - case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_INTERNAL_SIGNED) => replyToUser(Left(RemoteError(e))) + wallet.rollback(d.unsignedFundingTx) handleRemoteError(e, d) - case Event(INPUT_DISCONNECTED, _) => + case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_FUNDING_INTERNAL_SIGNED) => replyToUser(Left(LocalError(new RuntimeException("disconnected")))) + wallet.rollback(d.unsignedFundingTx) goto(CLOSED) - case Event(TickChannelOpenTimeout, _) => + case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_FUNDING_INTERNAL_SIGNED) => replyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) + wallet.rollback(d.unsignedFundingTx) goto(CLOSED) }) @@ -406,12 +515,12 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, temporaryChannelId, localParams, remoteParams, fundingSatoshis: Long, pushMsat, initialFeeratePerKw, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.maxFeerateMismatch) // check remote signature validity - val localSigOfLocalTx = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) - val signedLocalCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) + val localSigOfLocalTx = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(fundingKeyPath(localParams))) + val signedLocalCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(fundingKeyPath(localParams))publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) Transactions.checkSpendable(signedLocalCommitTx) match { case Failure(cause) => handleLocalError(InvalidCommitmentSignature(temporaryChannelId, signedLocalCommitTx.tx), d, None) case Success(_) => - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) + val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(fundingKeyPath(localParams))) val channelId = toLongId(fundingTxHash, fundingTxOutputIndex) // watch the funding tx transaction val commitInput = localCommitTx.input @@ -447,8 +556,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId when(WAIT_FOR_FUNDING_SIGNED)(handleExceptions { case Event(msg@FundingSigned(_, remoteSig), d@DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, channelFlags, fundingCreated)) => // we make sure that their sig checks out and that our first commit tx is spendable - val localSigOfLocalTx = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) - val signedLocalCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) + val localSigOfLocalTx = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(fundingKeyPath(localParams))) + val signedLocalCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(fundingKeyPath(localParams)).publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) Transactions.checkSpendable(signedLocalCommitTx) match { case Failure(cause) => // we rollback the funding tx, it will never be published @@ -522,7 +631,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 nextPerCommitmentPoint = keyManager.commitmentPoint(commitments.localParams.channelKeyPath, 1) + val nextPerCommitmentPoint = keyManager.commitmentPoint(keyPath(commitments.localParams), 1) val fundingLocked = FundingLocked(commitments.channelId, nextPerCommitmentPoint) deferred.foreach(self ! _) // this is the temporary channel id that we will use in our channel_update message, the goal is to be able to use our channel @@ -892,7 +1001,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId require(d.shortChannelId == remoteAnnSigs.shortChannelId, s"shortChannelId mismatch: local=${d.shortChannelId} remote=${remoteAnnSigs.shortChannelId}") log.info(s"announcing channelId=${d.channelId} on the network with shortId=${d.shortChannelId}") import d.commitments.{localParams, remoteParams} - val channelAnn = Announcements.makeChannelAnnouncement(nodeParams.chainHash, localAnnSigs.shortChannelId, nodeParams.nodeId, remoteParams.nodeId, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, localAnnSigs.nodeSignature, remoteAnnSigs.nodeSignature, localAnnSigs.bitcoinSignature, remoteAnnSigs.bitcoinSignature) + val channelAnn = Announcements.makeChannelAnnouncement(nodeParams.chainHash, localAnnSigs.shortChannelId, nodeParams.nodeId, remoteParams.nodeId, keyManager.fundingPublicKey(fundingKeyPath(localParams)).publicKey, remoteParams.fundingPubKey, localAnnSigs.nodeSignature, remoteAnnSigs.nodeSignature, localAnnSigs.bitcoinSignature, remoteAnnSigs.bitcoinSignature) // we use GOTO instead of stay because we want to fire transitions goto(NORMAL) using store(d.copy(channelAnnouncement = Some(channelAnn))) case Some(_) => @@ -1384,6 +1493,12 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId log.info("shutting down") stop(FSM.Normal) + // this may happen if connection is lost, or remote sends an error while we were waiting for the funding tx to be created by our wallet + // in that case we rollback the tx + case Event(SignFundingTxResponse(fundingTx, _, _), _) => + wallet.rollback(fundingTx) + stay + case Event(MakeFundingTxResponse(fundingTx, _, _), _) => // this may happen if connection is lost, or remote sends an error while we were waiting for the funding tx to be created by our wallet // in that case we rollback the tx @@ -1407,7 +1522,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId forwarder ! r val yourLastPerCommitmentSecret = d.commitments.remotePerCommitmentSecrets.lastIndex.flatMap(d.commitments.remotePerCommitmentSecrets.getHash).getOrElse(ByteVector32.Zeroes) - val myCurrentPerCommitmentPoint = keyManager.commitmentPoint(d.commitments.localParams.channelKeyPath, d.commitments.localCommit.index) + val myCurrentPerCommitmentPoint = keyManager.commitmentPoint(keyPath(d.commitments.localParams), d.commitments.localCommit.index) val channelReestablish = ChannelReestablish( channelId = d.channelId, @@ -1465,7 +1580,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_LOCKED) => log.debug(s"re-sending fundingLocked") - val nextPerCommitmentPoint = keyManager.commitmentPoint(d.commitments.localParams.channelKeyPath, 1) + val nextPerCommitmentPoint = keyManager.commitmentPoint(keyPath(d.commitments.localParams), 1) val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint) goto(WAIT_FOR_FUNDING_LOCKED) sending fundingLocked @@ -1474,7 +1589,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case ChannelReestablish(_, _, nextRemoteRevocationNumber, Some(yourLastPerCommitmentSecret), _) if !Helpers.checkLocalCommit(d, nextRemoteRevocationNumber) => // if next_remote_revocation_number is greater than our local commitment index, it means that either we are using an outdated commitment, or they are lying // but first we need to make sure that the last per_commitment_secret that they claim to have received from us is correct for that next_remote_revocation_number minus 1 - if (keyManager.commitmentSecret(d.commitments.localParams.channelKeyPath, nextRemoteRevocationNumber - 1) == yourLastPerCommitmentSecret) { + if (keyManager.commitmentSecret(keyPath(d.commitments.localParams), nextRemoteRevocationNumber - 1) == yourLastPerCommitmentSecret) { log.warning(s"counterparty proved that we have an outdated (revoked) local commitment!!! ourCommitmentNumber=${d.commitments.localCommit.index} theirCommitmentNumber=${nextRemoteRevocationNumber}") // their data checks out, we indeed seem to be using an old revoked commitment, and must absolutely *NOT* publish it, because that would be a cheating attempt and they // would punish us by taking all the funds in the channel @@ -1499,7 +1614,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId if (channelReestablish.nextLocalCommitmentNumber == 1 && d.commitments.localCommit.index == 0) { // If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node MUST retransmit funding_locked, otherwise it MUST NOT log.debug(s"re-sending fundingLocked") - val nextPerCommitmentPoint = keyManager.commitmentPoint(d.commitments.localParams.channelKeyPath, 1) + val nextPerCommitmentPoint = keyManager.commitmentPoint(keyPath(d.commitments.localParams), 1) val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint) forwarder ! fundingLocked } @@ -2116,8 +2231,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } else if (commitments1.localCommit.index == channelReestablish.nextRemoteRevocationNumber + 1) { // our last revocation got lost, let's resend it log.debug(s"re-sending last revocation") - val localPerCommitmentSecret = keyManager.commitmentSecret(commitments1.localParams.channelKeyPath, d.commitments.localCommit.index - 1) - val localNextPerCommitmentPoint = keyManager.commitmentPoint(commitments1.localParams.channelKeyPath, d.commitments.localCommit.index + 1) + val localPerCommitmentSecret = keyManager.commitmentSecret(keyPath(commitments1.localParams), d.commitments.localCommit.index - 1) + val localNextPerCommitmentPoint = keyManager.commitmentPoint(keyPath(commitments1.localParams), d.commitments.localCommit.index + 1) val revocation = RevokeAndAck( channelId = commitments1.channelId, perCommitmentSecret = localPerCommitmentSecret, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala index 4ce00a554e..b3d2beaf26 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala @@ -20,7 +20,9 @@ import java.util.UUID import akka.actor.ActorRef import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.DeterministicWallet.KeyPath import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, Transaction} +import fr.acinq.eclair.blockchain.MakeFundingTxResponse import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions.CommitTx @@ -49,7 +51,8 @@ sealed trait State case object WAIT_FOR_INIT_INTERNAL extends State case object WAIT_FOR_OPEN_CHANNEL extends State case object WAIT_FOR_ACCEPT_CHANNEL extends State -case object WAIT_FOR_FUNDING_INTERNAL extends State +case object WAIT_FOR_FUNDING_INTERNAL_SIGNED extends State +case object WAIT_FOR_FUNDING_INTERNAL_CREATED extends State case object WAIT_FOR_FUNDING_CREATED extends State case object WAIT_FOR_FUNDING_SIGNED extends State case object WAIT_FOR_FUNDING_CONFIRMED extends State @@ -76,7 +79,8 @@ case object ERR_INFORMATION_LEAK extends State 8888888888 Y8P 8888888888 888 Y888 888 "Y8888P" */ -case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, fundingTxFeeratePerKw: Long, localParams: LocalParams, remote: ActorRef, remoteInit: Init, channelFlags: Byte) +case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, fundingTxFeeratePerKw: Long, remote: ActorRef, remoteInit: Init, channelFlags: Byte) +case class INPUT_INIT_FUNDER_WITH_PARAMS(temporaryChannelId: ByteVector32, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, fundingTxFeeratePerKw: Long, localParams: LocalParams, remote: ActorRef, remoteInit: Init, channelFlags: Byte) case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32, localParams: LocalParams, remote: ActorRef, remoteInit: Init) case object INPUT_CLOSE_COMPLETE_TIMEOUT // when requesting a mutual close, we wait for as much as this timeout, then unilateral close case object INPUT_DISCONNECTED @@ -148,8 +152,9 @@ case class RemoteCommitPublished(commitTx: Transaction, claimMainOutputTx: Optio case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[Transaction], mainPenaltyTx: Option[Transaction], htlcPenaltyTxs: List[Transaction], claimHtlcDelayedPenaltyTxs: List[Transaction], irrevocablySpent: Map[OutPoint, ByteVector32]) final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_FUNDEE) extends Data -final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_FUNDER, lastSent: OpenChannel) extends Data -final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, remoteFirstPerCommitmentPoint: PublicKey, lastSent: OpenChannel) extends Data +final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_FUNDER_WITH_PARAMS, lastSent: OpenChannel, unsignedFundingTx: MakeFundingTxResponse) extends Data +final case class DATA_WAIT_FOR_FUNDING_INTERNAL_CREATED(temporaryChannelId: ByteVector32, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, fundingTxFeeratePerKw: Long, remote: ActorRef, remoteInit: Init, channelFlags: Byte) extends Data +final case class DATA_WAIT_FOR_FUNDING_INTERNAL_SIGNED(temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, remoteFirstPerCommitmentPoint: PublicKey, lastSent: OpenChannel, unsignedFundingTx: Transaction) extends Data final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, remoteFirstPerCommitmentPoint: PublicKey, channelFlags: Byte, lastSent: AcceptChannel) extends Data final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingTx: Transaction, fundingTxFee: Satoshi, localSpec: CommitmentSpec, localCommitTx: CommitTx, remoteCommit: RemoteCommit, channelFlags: Byte, lastSent: FundingCreated) extends Data final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments, @@ -191,17 +196,21 @@ final case class DATA_CLOSING(commitments: Commitments, final case class DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(commitments: Commitments, remoteChannelReestablish: ChannelReestablish) extends Data with HasCommitments final case class LocalParams(nodeId: PublicKey, - channelKeyPath: DeterministicWallet.KeyPath, + channelKeyPath: Either[DeterministicWallet.KeyPath, KeyPathFundee], dustLimitSatoshis: Long, maxHtlcValueInFlightMsat: UInt64, channelReserveSatoshis: Long, htlcMinimumMsat: Long, toSelfDelay: Int, maxAcceptedHtlcs: Int, - isFunder: Boolean, defaultFinalScriptPubKey: ByteVector, globalFeatures: ByteVector, - localFeatures: ByteVector) + localFeatures: ByteVector) { + + def isFunder = channelKeyPath.isLeft +} + +case class KeyPathFundee(fundingKeyPath: KeyPath, pointsKeyPath: KeyPath) final case class RemoteParams(nodeId: PublicKey, dustLimitSatoshis: Long, @@ -229,5 +238,6 @@ case class ChannelVersion(bits: BitVector) { object ChannelVersion { val LENGTH_BITS = 4 * 8 val STANDARD = ChannelVersion(BitVector.fill(LENGTH_BITS)(false)) + val DETERMINISTIC_KEYPATH = ChannelVersion(BitVector.one ++ BitVector.fill(LENGTH_BITS -1)(false)) } // @formatter:on diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 579f8bac9f..02d1a0d30e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.channel import akka.event.LoggingAdapter import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi} +import fr.acinq.eclair.channel.Channel._ import fr.acinq.eclair.crypto.{Generators, KeyManager, ShaChain, Sphinx} import fr.acinq.eclair.payment._ import fr.acinq.eclair.transactions.Transactions._ @@ -365,10 +366,10 @@ object Commitments { // remote commitment will includes all local changes + remote acked changes val spec = CommitmentSpec.reduce(remoteCommit.spec, remoteChanges.acked, localChanges.proposed) val (remoteCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeRemoteTxs(keyManager, remoteCommit.index + 1, localParams, remoteParams, commitInput, remoteNextPerCommitmentPoint, spec) - val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) + val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(fundingKeyPath(localParams))) val sortedHtlcTxs: Seq[TransactionWithInputInfo] = (htlcTimeoutTxs ++ htlcSuccessTxs).sortBy(_.input.outPoint.index) - val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(localParams.channelKeyPath), remoteNextPerCommitmentPoint)) + val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(keyPath(localParams)), remoteNextPerCommitmentPoint)) // NB: IN/OUT htlcs are inverted because this is the remote commit log.info(s"built remote commit number=${remoteCommit.index + 1} htlc_in={} htlc_out={} feeratePerKw=${spec.feeratePerKw} txid=${remoteCommitTx.tx.txid} tx={}", spec.htlcs.filter(_.direction == OUT).map(_.add.id).mkString(","), spec.htlcs.filter(_.direction == IN).map(_.add.id).mkString(","), remoteCommitTx.tx) @@ -412,16 +413,16 @@ object Commitments { // receiving money i.e its commit tx has one output for them val spec = CommitmentSpec.reduce(localCommit.spec, localChanges.acked, remoteChanges.proposed) - val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, commitments.localCommit.index + 1) + val localPerCommitmentPoint = keyManager.commitmentPoint(keyPath(localParams), commitments.localCommit.index + 1) val (localCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeLocalTxs(keyManager, localCommit.index + 1, localParams, remoteParams, commitInput, localPerCommitmentPoint, spec) - val sig = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) + val sig = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(fundingKeyPath(localParams))) log.info(s"built local commit number=${localCommit.index + 1} htlc_in={} htlc_out={} feeratePerKw=${spec.feeratePerKw} txid=${localCommitTx.tx.txid} tx={}", spec.htlcs.filter(_.direction == IN).map(_.add.id).mkString(","), spec.htlcs.filter(_.direction == OUT).map(_.add.id).mkString(","), localCommitTx.tx) // TODO: should we have optional sig? (original comment: this tx will NOT be signed if our output is empty) // no need to compute htlc sigs if commit sig doesn't check out - val signedCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, sig, commit.signature) + val signedCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(fundingKeyPath(localParams)).publicKey, remoteParams.fundingPubKey, sig, commit.signature) if (Transactions.checkSpendable(signedCommitTx).isFailure) { throw InvalidCommitmentSignature(commitments.channelId, signedCommitTx.tx) } @@ -430,7 +431,7 @@ object Commitments { if (commit.htlcSignatures.size != sortedHtlcTxs.size) { throw new HtlcSigCountMismatch(commitments.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size) } - val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(localParams.channelKeyPath), localPerCommitmentPoint)) + val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(keyPath(localParams)), localPerCommitmentPoint)) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint) // combine the sigs to make signed txes val htlcTxsAndSigs = (sortedHtlcTxs, htlcSigs, commit.htlcSignatures).zipped.toList.collect { @@ -448,8 +449,8 @@ object Commitments { } // we will send our revocation preimage + our next revocation hash - val localPerCommitmentSecret = keyManager.commitmentSecret(localParams.channelKeyPath, commitments.localCommit.index) - val localNextPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, commitments.localCommit.index + 2) + val localPerCommitmentSecret = keyManager.commitmentSecret(keyPath(localParams), commitments.localCommit.index) + val localNextPerCommitmentPoint = keyManager.commitmentPoint(keyPath(localParams), commitments.localCommit.index + 2) val revocation = RevokeAndAck( channelId = commitments.channelId, perCommitmentSecret = localPerCommitmentSecret, @@ -510,23 +511,23 @@ object Commitments { } def makeLocalTxs(keyManager: KeyManager, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, localPerCommitmentPoint: PublicKey, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { - val localDelayedPaymentPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(localParams.channelKeyPath).publicKey, localPerCommitmentPoint) - val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(localParams.channelKeyPath).publicKey, localPerCommitmentPoint) + val localDelayedPaymentPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(keyPath(localParams)).publicKey, localPerCommitmentPoint) + val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(keyPath(localParams)).publicKey, localPerCommitmentPoint) val remotePaymentPubkey = Generators.derivePubKey(remoteParams.paymentBasepoint, localPerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint) val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) - val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, keyManager.paymentPoint(localParams.channelKeyPath).publicKey, remoteParams.paymentBasepoint, localParams.isFunder, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec) + val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, keyManager.paymentPoint(keyPath(localParams)).publicKey, remoteParams.paymentBasepoint, localParams.isFunder, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec) val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec) (commitTx, htlcTimeoutTxs, htlcSuccessTxs) } def makeRemoteTxs(keyManager: KeyManager, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, remotePerCommitmentPoint: PublicKey, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { - val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) - val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) + val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(keyPath(localParams)).publicKey, remotePerCommitmentPoint) + val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(keyPath(localParams)).publicKey, remotePerCommitmentPoint) val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) - val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) - val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, keyManager.paymentPoint(localParams.channelKeyPath).publicKey, !localParams.isFunder, Satoshi(remoteParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec) + val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(keyPath(localParams)).publicKey, remotePerCommitmentPoint) + val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, keyManager.paymentPoint(keyPath(localParams)).publicKey, !localParams.isFunder, Satoshi(remoteParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec) val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, Satoshi(remoteParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec) (commitTx, htlcTimeoutTxs, htlcSuccessTxs) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index f4609344f5..e4fa24dd73 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -21,7 +21,7 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, ripemd160, sha256} import fr.acinq.bitcoin.Script._ import fr.acinq.bitcoin.{OutPoint, _} import fr.acinq.eclair.blockchain.EclairWallet -import fr.acinq.eclair.channel.Channel.REFRESH_CHANNEL_UPDATE_INTERVAL +import fr.acinq.eclair.channel.Channel.{REFRESH_CHANNEL_UPDATE_INTERVAL, fundingKeyPath, keyPath} import fr.acinq.eclair.crypto.{Generators, KeyManager} import fr.acinq.eclair.db.ChannelsDb import fr.acinq.eclair.payment.{Local, Origin} @@ -53,7 +53,8 @@ object Helpers { case Nothing => ByteVector32.Zeroes case d: DATA_WAIT_FOR_OPEN_CHANNEL => d.initFundee.temporaryChannelId case d: DATA_WAIT_FOR_ACCEPT_CHANNEL => d.initFunder.temporaryChannelId - case d: DATA_WAIT_FOR_FUNDING_INTERNAL => d.temporaryChannelId + case d: DATA_WAIT_FOR_FUNDING_INTERNAL_SIGNED => d.temporaryChannelId + case d: DATA_WAIT_FOR_FUNDING_INTERNAL_CREATED => d.temporaryChannelId case d: DATA_WAIT_FOR_FUNDING_CREATED => d.temporaryChannelId case d: DATA_WAIT_FOR_FUNDING_SIGNED => d.channelId case d: HasCommitments => d.channelId @@ -204,7 +205,7 @@ object Helpers { def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: ShortChannelId) = { val features = ByteVector.empty // empty features for now - val (localNodeSig, localBitcoinSig) = nodeParams.keyManager.signChannelAnnouncement(commitments.localParams.channelKeyPath, nodeParams.chainHash, shortChannelId, commitments.remoteParams.nodeId, commitments.remoteParams.fundingPubKey, features) + val (localNodeSig, localBitcoinSig) = nodeParams.keyManager.signChannelAnnouncement(fundingKeyPath(commitments.localParams), nodeParams.chainHash, shortChannelId, commitments.remoteParams.nodeId, commitments.remoteParams.fundingPubKey, features) AnnouncementSignatures(commitments.channelId, shortChannelId, localNodeSig, localBitcoinSig) } @@ -268,8 +269,8 @@ object Helpers { } } - val commitmentInput = makeFundingInputInfo(fundingTxHash, fundingTxOutputIndex, Satoshi(fundingSatoshis), keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey) - val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, 0) + val commitmentInput = makeFundingInputInfo(fundingTxHash, fundingTxOutputIndex, Satoshi(fundingSatoshis), keyManager.fundingPublicKey(fundingKeyPath(localParams)).publicKey, remoteParams.fundingPubKey) + val localPerCommitmentPoint = keyManager.commitmentPoint(keyPath(localParams), 0) val (localCommitTx, _, _) = Commitments.makeLocalTxs(keyManager, 0, localParams, remoteParams, commitmentInput, localPerCommitmentPoint, localSpec) val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(keyManager, 0, localParams, remoteParams, commitmentInput, remoteFirstPerCommitmentPoint, remoteSpec) @@ -449,7 +450,7 @@ object Helpers { // TODO: check that val dustLimitSatoshis = Satoshi(Math.max(localParams.dustLimitSatoshis, remoteParams.dustLimitSatoshis)) val closingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, dustLimitSatoshis, closingFee, localCommit.spec) - val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitments.localParams.channelKeyPath)) + val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(fundingKeyPath(commitments.localParams))) val closingSigned = ClosingSigned(channelId, closingFee.amount, localClosingSig) log.info(s"signed closing txid=${closingTx.tx.txid} with closingFeeSatoshis=${closingSigned.feeSatoshis}") log.debug(s"closingTxid=${closingTx.tx.txid} closingTx=${closingTx.tx}}") @@ -464,7 +465,7 @@ object Helpers { throw InvalidCloseFee(commitments.channelId, remoteClosingFee.amount) } val (closingTx, closingSigned) = makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, remoteClosingFee) - val signedClosingTx = Transactions.addSigs(closingTx, keyManager.fundingPublicKey(commitments.localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig) + val signedClosingTx = Transactions.addSigs(closingTx, keyManager.fundingPublicKey(fundingKeyPath(commitments.localParams)).publicKey, remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig) Transactions.checkSpendable(signedClosingTx).map(x => signedClosingTx.tx).recover { case _ => throw InvalidCloseSignature(commitments.channelId, signedClosingTx.tx) } } @@ -494,9 +495,9 @@ object Helpers { import commitments._ require(localCommit.publishableTxs.commitTx.tx.txid == tx.txid, "txid mismatch, provided tx is not the current local commit tx") - val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, commitments.localCommit.index.toInt) + val localPerCommitmentPoint = keyManager.commitmentPoint(keyPath(localParams), commitments.localCommit.index.toInt) val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) - val localDelayedPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(localParams.channelKeyPath).publicKey, localPerCommitmentPoint) + val localDelayedPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(keyPath(localParams)).publicKey, localPerCommitmentPoint) // 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 @@ -504,7 +505,7 @@ object Helpers { // first we will claim our main output as soon as the delay is over val mainDelayedTx = generateTx("main-delayed-output")(Try { val claimDelayed = Transactions.makeClaimDelayedOutputTx(tx, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed) - val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(localParams.channelKeyPath), localPerCommitmentPoint) + val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(keyPath(localParams)), localPerCommitmentPoint) Transactions.addSigs(claimDelayed, sig) }) @@ -539,7 +540,7 @@ object Helpers { remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed) - val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(localParams.channelKeyPath), localPerCommitmentPoint) + val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(keyPath(localParams)), localPerCommitmentPoint) Transactions.addSigs(claimDelayed, sig) }) } @@ -568,10 +569,10 @@ object Helpers { val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(keyManager, remoteCommit.index, localParams, remoteParams, commitInput, remoteCommit.remotePerCommitmentPoint, remoteCommit.spec) require(remoteCommitTx.tx.txid == tx.txid, "txid mismatch, cannot recompute the current remote commit tx") - val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(localParams.channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) + val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(keyPath(localParams)).publicKey, remoteCommit.remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remoteCommit.remotePerCommitmentPoint) - val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, commitments.localCommit.index.toInt) - val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(localParams.channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) + val localPerCommitmentPoint = keyManager.commitmentPoint(keyPath(localParams), commitments.localCommit.index.toInt) + val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(keyPath(localParams)).publicKey, remoteCommit.remotePerCommitmentPoint) // we need to use a rather high fee for htlc-claim because we compete with the counterparty val feeratePerKwHtlc = Globals.feeratesPerKw.get.blocks_2 @@ -587,7 +588,7 @@ object Helpers { val preimage = preimages.find(r => sha256(r) == add.paymentHash).get val txinfo = Transactions.makeClaimHtlcSuccessTx(remoteCommitTx.tx, outputsAlreadyUsed, Satoshi(localParams.dustLimitSatoshis), localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc) outputsAlreadyUsed = outputsAlreadyUsed + txinfo.input.outPoint.index.toInt - val sig = keyManager.sign(txinfo, keyManager.htlcPoint(localParams.channelKeyPath), remoteCommit.remotePerCommitmentPoint) + val sig = keyManager.sign(txinfo, keyManager.htlcPoint(keyPath(localParams)), remoteCommit.remotePerCommitmentPoint) Transactions.addSigs(txinfo, sig, preimage) }) @@ -597,7 +598,7 @@ object Helpers { case DirectedHtlc(IN, add: UpdateAddHtlc) => generateTx("claim-htlc-timeout")(Try { val txinfo = Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, outputsAlreadyUsed, Satoshi(localParams.dustLimitSatoshis), localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc) outputsAlreadyUsed = outputsAlreadyUsed + txinfo.input.outPoint.index.toInt - val sig = keyManager.sign(txinfo, keyManager.htlcPoint(localParams.channelKeyPath), remoteCommit.remotePerCommitmentPoint) + val sig = keyManager.sign(txinfo, keyManager.htlcPoint(keyPath(localParams)), remoteCommit.remotePerCommitmentPoint) Transactions.addSigs(txinfo, sig) }) }.toSeq.flatten @@ -620,7 +621,7 @@ object Helpers { * @return a list of transactions (one per HTLC that we can claim) */ def claimRemoteCommitMainOutput(keyManager: KeyManager, commitments: Commitments, remotePerCommitmentPoint: PublicKey, tx: Transaction)(implicit log: LoggingAdapter): RemoteCommitPublished = { - val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(commitments.localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) + val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(keyPath(commitments.localParams)).publicKey, remotePerCommitmentPoint) // 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 @@ -628,7 +629,7 @@ object Helpers { val mainTx = generateTx("claim-p2wpkh-output")(Try { val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, Satoshi(commitments.localParams.dustLimitSatoshis), localPubkey, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain) - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(commitments.localParams.channelKeyPath), remotePerCommitmentPoint) + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(keyPath(commitments.localParams)), remotePerCommitmentPoint) Transactions.addSigs(claimMain, localPubkey, sig) }) @@ -655,7 +656,7 @@ object Helpers { require(tx.txIn.size == 1, "commitment tx should have 1 input") val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn(0).sequence, tx.lockTime) // this tx has been published by remote, so we need to invert local/remote params - val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, keyManager.paymentPoint(localParams.channelKeyPath).publicKey) + val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, keyManager.paymentPoint(keyPath(localParams)).publicKey) require(txnumber <= 0xffffffffffffL, "txnumber must be lesser than 48 bits long") log.warning(s"a revoked commit has been published with txnumber=$txnumber") // now we know what commit number this tx is referring to, we can derive the commitment point from the shachain @@ -664,9 +665,9 @@ object Helpers { .map { remotePerCommitmentSecret => val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) - val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) - val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) - val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) + val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(keyPath(localParams)).publicKey, remotePerCommitmentPoint) + val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(keyPath(localParams)).publicKey, remotePerCommitmentPoint) + val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(keyPath(localParams)).publicKey, remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) // no need to use a high fee rate for our main output (we are the only one who can spend it) @@ -677,14 +678,14 @@ object Helpers { // first we will claim our main output right away val mainTx = generateTx("claim-p2wpkh-output")(Try { val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, Satoshi(localParams.dustLimitSatoshis), localPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain) - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(localParams.channelKeyPath), remotePerCommitmentPoint) + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(keyPath(localParams)), remotePerCommitmentPoint) Transactions.addSigs(claimMain, localPubkey, sig) }) // then we punish them by stealing their main output val mainPenaltyTx = generateTx("main-penalty")(Try { val txinfo = Transactions.makeMainPenaltyTx(tx, Satoshi(localParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, feeratePerKwPenalty) - val sig = keyManager.sign(txinfo, keyManager.revocationPoint(localParams.channelKeyPath), remotePerCommitmentSecret) + val sig = keyManager.sign(txinfo, keyManager.revocationPoint(keyPath(localParams)), remotePerCommitmentSecret) Transactions.addSigs(txinfo, sig) }) @@ -705,7 +706,7 @@ object Helpers { generateTx("htlc-penalty")(Try { val htlcPenalty = Transactions.makeHtlcPenaltyTx(tx, outputsAlreadyUsed, htlcRedeemScript, Satoshi(localParams.dustLimitSatoshis), localParams.defaultFinalScriptPubKey, feeratePerKwPenalty) outputsAlreadyUsed = outputsAlreadyUsed + htlcPenalty.input.outPoint.index.toInt - val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(localParams.channelKeyPath), remotePerCommitmentSecret) + val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(keyPath(localParams)), remotePerCommitmentSecret) Transactions.addSigs(htlcPenalty, sig, remoteRevocationPubkey) }) }.toList.flatten @@ -746,21 +747,21 @@ object Helpers { val tx = revokedCommitPublished.commitTx val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn(0).sequence, tx.lockTime) // this tx has been published by remote, so we need to invert local/remote params - val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, keyManager.paymentPoint(localParams.channelKeyPath).publicKey) + val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, keyManager.paymentPoint(keyPath(localParams)).publicKey) // now we know what commit number this tx is referring to, we can derive the commitment point from the shachain remotePerCommitmentSecrets.getHash(0xFFFFFFFFFFFFL - txnumber) .map(d => PrivateKey(d)) .flatMap { remotePerCommitmentSecret => val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) - val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) + val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(keyPath(localParams)).publicKey, remotePerCommitmentPoint) // we need to use a high fee here for punishment txes because after a delay they can be spent by the counterparty val feeratePerKwPenalty = Globals.feeratesPerKw.get.block_1 generateTx("claim-htlc-delayed-penalty")(Try { val htlcDelayedPenalty = Transactions.makeClaimDelayedOutputPenaltyTx(htlcTx, Satoshi(localParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty) - val sig = keyManager.sign(htlcDelayedPenalty, keyManager.revocationPoint(localParams.channelKeyPath), remotePerCommitmentSecret) + val sig = keyManager.sign(htlcDelayedPenalty, keyManager.revocationPoint(keyPath(localParams)), remotePerCommitmentSecret) val signedTx = Transactions.addSigs(htlcDelayedPenalty, sig) // we need to make sure that the tx is indeed valid Transaction.correctlySpends(signedTx.tx, Seq(htlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala index 2bf80bccc4..37e2fa21ae 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala @@ -24,7 +24,7 @@ import fr.acinq.eclair.ShortChannelId import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions.TransactionWithInputInfo -import scodec.bits.ByteVector +import scodec.bits.{ByteOrdering, ByteVector} object LocalKeyManager { def channelKeyBasePath(chainHash: ByteVector32) = chainHash match { @@ -40,6 +40,20 @@ object LocalKeyManager { case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash => DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(0) :: Nil case Block.LivenetGenesisBlock.hash => DeterministicWallet.hardened(47) :: DeterministicWallet.hardened(0) :: Nil } + + // split the SHA(input) into 8 groups of 4 bytes and convert to uint32 + def fourByteGroupsFromSha(input: ByteVector): List[Long] = Crypto.sha256(input).toArray.grouped(4).map(ByteVector(_).toLong(signed = false)).toList + + def makeChannelKeyPathFunder(entropy: ByteVector) = KeyPath(fourByteGroupsFromSha(entropy) :+ hardened(0)) + def makeChannelKeyPathFundee(entropy: ByteVector) = KeyPath(fourByteGroupsFromSha(entropy) :+ hardened(1)) + def makeChannelKeyPathFundeePubkey(entropy: ByteVector) = KeyPath(fourByteGroupsFromSha(entropy) :+ hardened(2)) + + def makeChannelKeyPathFundeePubkey(blockHeight: Long, counter: Long): KeyPath = { + val blockHeightBytes = ByteVector.fromLong(blockHeight, size = 4, ordering = ByteOrdering.LittleEndian) + val counterBytes = ByteVector.fromLong(counter, size = 4, ordering = ByteOrdering.LittleEndian) + + makeChannelKeyPathFundeePubkey(blockHeightBytes ++ counterBytes) + } } /** @@ -80,6 +94,9 @@ class LocalKeyManager(seed: ByteVector, chainHash: ByteVector32) extends KeyMana private def shaSeed(channelKeyPath: DeterministicWallet.KeyPath) = Crypto.sha256(privateKeys.get(internalKeyPath(channelKeyPath, hardened(5))).privateKey.value :+ 1.toByte) + // used only in test + def shaSeedPub(channelKeyPath: DeterministicWallet.KeyPath) = publicKeys.get(internalKeyPath(channelKeyPath, hardened(5))) + override def fundingPublicKey(channelKeyPath: DeterministicWallet.KeyPath) = publicKeys.get(internalKeyPath(channelKeyPath, hardened(0))) override def revocationPoint(channelKeyPath: DeterministicWallet.KeyPath) = publicKeys.get(internalKeyPath(channelKeyPath, hardened(1))) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala index dd9fb9e120..ea58fbdabb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala @@ -21,6 +21,8 @@ import fr.acinq.eclair.channel.HasCommitments trait ChannelsDb { + def getCounterFor(blockHeight: Long): Long + def addOrUpdateChannel(state: HasCommitments) def removeChannel(channelId: ByteVector32) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala index d747a3b9dd..be1d21eb55 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.db.sqlite -import java.sql.{Connection, Statement} +import java.sql.{Connection, SQLException, Statement} import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.channel.HasCommitments @@ -36,6 +36,7 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb with Logging { private def migration12(statement: Statement) = { statement.executeUpdate("ALTER TABLE local_channels ADD COLUMN is_closed BOOLEAN NOT NULL DEFAULT 0") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS block_key_counter (block_height INTEGER NOT NULL PRIMARY KEY, counter INTEGER NOT NULL)") } using(sqlite.createStatement()) { statement => @@ -49,11 +50,40 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb with Logging { statement.executeUpdate("CREATE TABLE IF NOT EXISTS local_channels (channel_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL, is_closed BOOLEAN NOT NULL DEFAULT 0)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS htlc_infos (channel_id BLOB NOT NULL, commitment_number BLOB NOT NULL, payment_hash BLOB NOT NULL, cltv_expiry INTEGER NOT NULL, FOREIGN KEY(channel_id) REFERENCES local_channels(channel_id))") statement.executeUpdate("CREATE INDEX IF NOT EXISTS htlc_infos_idx ON htlc_infos(channel_id, commitment_number)") - + statement.executeUpdate("CREATE TABLE IF NOT EXISTS block_key_counter (block_height INTEGER NOT NULL PRIMARY KEY, counter INTEGER NOT NULL)") case unknownVersion => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") } } + override def getCounterFor(blockHeight: Long): Long = synchronized { + val counterOld = getCounter(blockHeight) + setCounter(blockHeight, counterOld + 1) + counterOld + } + + private def getCounter(blockHeight: Long): Long = { + using(sqlite.prepareStatement("SELECT counter FROM block_key_counter WHERE block_height=?")) { statement => + statement.setLong(1, blockHeight) + val rs = statement.executeQuery() + if(rs.next()) rs.getLong("counter") else 0 + } + } + + private def setCounter(blockHeight: Long, counter: Long) = { + using(sqlite.prepareStatement("UPDATE block_key_counter SET counter=? WHERE block_height=?")) { statement => + statement.setLong(1, counter) + statement.setLong(2, blockHeight) + if(statement.executeUpdate() != 1){ + using(sqlite.prepareStatement("INSERT INTO block_key_counter VALUES(?, ?)")) { insert => + insert.setLong(1, blockHeight) + insert.setLong(2, counter) + insert.executeUpdate() + } + } + } + } + + override def addOrUpdateChannel(state: HasCommitments): Unit = { val data = stateDataCodec.encode(state).require.toByteArray using (sqlite.prepareStatement("UPDATE local_channels SET data=? WHERE channel_id=?")) { update => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 08969aed8d..4994dfca42 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -25,7 +25,8 @@ import akka.event.Logging.MDC import akka.util.Timeout import com.google.common.net.HostAndPort import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, MilliSatoshi, Protocol, Satoshi} +import fr.acinq.bitcoin.DeterministicWallet.KeyPath +import fr.acinq.bitcoin.{ByteVector32, Crypto, DeterministicWallet, MilliSatoshi, Protocol, Satoshi, TxIn} import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.TransportHandler @@ -46,6 +47,8 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A import Peer._ + implicit val ec = context.system.dispatcher + startWith(INSTANTIATING, Nothing()) when(INSTANTIATING) { @@ -280,22 +283,26 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A stay case Event(c: Peer.OpenChannel, d: ConnectedData) => - val (channel, localParams) = createNewChannel(nodeParams, funder = true, c.fundingSatoshis.toLong, origin_opt = Some(sender)) + val channel = spawnChannel(nodeParams, origin_opt = Some(sender)) c.timeout_opt.map(openTimeout => context.system.scheduler.scheduleOnce(openTimeout.duration, channel, Channel.TickChannelOpenTimeout)(context.dispatcher)) val temporaryChannelId = randomBytes32 val channelFeeratePerKw = Globals.feeratesPerKw.get.blocks_2 val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(Globals.feeratesPerKw.get.blocks_6) - log.info(s"requesting a new channel with fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt} temporaryChannelId=$temporaryChannelId localParams=$localParams") - channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis.amount, c.pushMsat.amount, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.transport, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags)) + log.info(s"requesting a new channel with fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt} temporaryChannelId=$temporaryChannelId") + channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis.amount, c.pushMsat.amount, channelFeeratePerKw, fundingTxFeeratePerKw, d.transport, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags)) stay using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) case Event(msg: wire.OpenChannel, d: ConnectedData) => d.transport ! TransportHandler.ReadAck(msg) d.channels.get(TemporaryChannelId(msg.temporaryChannelId)) match { case None => - val (channel, localParams) = createNewChannel(nodeParams, funder = false, fundingSatoshis = msg.fundingSatoshis, origin_opt = None) + + val channel = spawnChannel(nodeParams, origin_opt = None) + val defaultFinalScriptPubkey = Helpers.getFinalScriptPubKey(wallet, nodeParams.chainHash) + val localParams = Channel.makeFundeeChannelParams(nodeParams, msg, defaultFinalScriptPubkey, msg.fundingSatoshis) val temporaryChannelId = msg.temporaryChannelId - log.info(s"accepting a new channel to $remoteNodeId temporaryChannelId=$temporaryChannelId localParams=$localParams") + log.info(s"accepting a new channel to $remoteNodeId temporaryChannelId=$temporaryChannelId") + channel ! INPUT_INIT_FUNDEE(temporaryChannelId, localParams, d.transport, d.remoteInit) channel ! msg stay using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) @@ -523,13 +530,6 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A case DISCONNECTED -> _ if nodeParams.autoReconnect => cancelTimer(RECONNECT_TIMER) } - def createNewChannel(nodeParams: NodeParams, funder: Boolean, fundingSatoshis: Long, origin_opt: Option[ActorRef]): (ActorRef, LocalParams) = { - val defaultFinalScriptPubKey = Helpers.getFinalScriptPubKey(wallet, nodeParams.chainHash) - val localParams = makeChannelParams(nodeParams, defaultFinalScriptPubKey, funder, fundingSatoshis) - val channel = spawnChannel(nodeParams, origin_opt) - (channel, localParams) - } - def spawnChannel(nodeParams: NodeParams, origin_opt: Option[ActorRef]): ActorRef = { val channel = context.actorOf(Channel.props(nodeParams, wallet, remoteNodeId, watcher, router, relayer, origin_opt)) context watch channel @@ -628,30 +628,6 @@ object Peer { // @formatter:on - def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: ByteVector, isFunder: Boolean, fundingSatoshis: Long): LocalParams = { - val entropy = new Array[Byte](16) - secureRandom.nextBytes(entropy) - val bis = new ByteArrayInputStream(entropy) - val channelKeyPath = DeterministicWallet.KeyPath(Seq(Protocol.uint32(bis, ByteOrder.BIG_ENDIAN), Protocol.uint32(bis, ByteOrder.BIG_ENDIAN), Protocol.uint32(bis, ByteOrder.BIG_ENDIAN), Protocol.uint32(bis, ByteOrder.BIG_ENDIAN))) - makeChannelParams(nodeParams, defaultFinalScriptPubKey, isFunder, fundingSatoshis, channelKeyPath) - } - - def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: ByteVector, isFunder: Boolean, fundingSatoshis: Long, channelKeyPath: DeterministicWallet.KeyPath): LocalParams = { - LocalParams( - nodeParams.nodeId, - channelKeyPath, - dustLimitSatoshis = nodeParams.dustLimitSatoshis, - maxHtlcValueInFlightMsat = nodeParams.maxHtlcValueInFlightMsat, - channelReserveSatoshis = Math.max((nodeParams.reserveToFundingRatio * fundingSatoshis).toLong, nodeParams.dustLimitSatoshis), // BOLT #2: make sure that our reserve is above our dust limit - htlcMinimumMsat = nodeParams.htlcMinimumMsat, - toSelfDelay = nodeParams.toRemoteDelayBlocks, // we choose their delay - maxAcceptedHtlcs = nodeParams.maxAcceptedHtlcs, - defaultFinalScriptPubKey = defaultFinalScriptPubKey, - isFunder = isFunder, - globalFeatures = nodeParams.globalFeatures, - localFeatures = nodeParams.localFeatures) - } - /** * Peer may want to filter announcements based on timestamp * diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala index 97937c05de..37572db296 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala @@ -19,8 +19,11 @@ package fr.acinq.eclair.wire import java.util.UUID import akka.actor.ActorRef +import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, KeyPath} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, OutPoint, Transaction, TxOut} +import fr.acinq.eclair.UInt64 +import fr.acinq.eclair.channel.ChannelVersion.{DETERMINISTIC_KEYPATH, STANDARD} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.payment.{Local, Origin, Relayed} @@ -29,9 +32,10 @@ import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.CommonCodecs._ import fr.acinq.eclair.wire.LightningMessageCodecs._ import grizzled.slf4j.Logging -import scodec.bits.BitVector +import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ -import scodec.{Attempt, Codec} +import scodec.{Attempt, Codec, Decoder, Encoder} +import shapeless.{HList, HNil} import scala.compat.Platform import scala.concurrent.duration._ @@ -42,6 +46,12 @@ import scala.concurrent.duration._ 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 keyPathFundeeCodec: Codec[KeyPathFundee] = ( + ("fundingKeyPath" | keyPathCodec) :: + ("pointsKeyPath" | keyPathCodec) + ).as[KeyPathFundee] + + val channelKeyPathCodec = either(bool, keyPathCodec, keyPathFundeeCodec) val extendedPrivateKeyCodec: Codec[ExtendedPrivateKey] = ( ("secretkeybytes" | bytes32) :: @@ -58,7 +68,8 @@ object ChannelCodecs extends Logging { fallback = provide(ChannelVersion.STANDARD) ) - val localParamsCodec: Codec[LocalParams] = ( + import shapeless._ + val localParamsCodecPreDKP: Codec[LocalParams] = ( ("nodeId" | publicKey) :: ("channelPath" | keyPathCodec) :: ("dustLimitSatoshis" | uint64overflow) :: @@ -70,8 +81,65 @@ object ChannelCodecs extends Logging { ("isFunder" | bool) :: ("defaultFinalScriptPubKey" | varsizebinarydata) :: ("globalFeatures" | varsizebinarydata) :: + ("localFeatures" | varsizebinarydata)).xmap({ + case nodeId :: + keyPath :: + dustLimitSatoshis :: + maxHtlcValueInFlightMsat :: + channelReserveSatoshis :: + htlcMinimumMsat :: + toSelfDelay :: + maxAcceptedHtlcs :: + isFunder :: + defaultFinalScriptPubKey :: + globalFeatures :: localFeatures :: HNil => + + LocalParams( + nodeId = nodeId, + // old versions of "LocalParams" use a single keypath + channelKeyPath = if(isFunder) Left(keyPath) else Right(KeyPathFundee(keyPath, keyPath)), + dustLimitSatoshis = dustLimitSatoshis, + maxHtlcValueInFlightMsat = maxHtlcValueInFlightMsat, + channelReserveSatoshis = channelReserveSatoshis, + htlcMinimumMsat = htlcMinimumMsat, + toSelfDelay = toSelfDelay, + maxAcceptedHtlcs = maxAcceptedHtlcs, + defaultFinalScriptPubKey = defaultFinalScriptPubKey, + globalFeatures = globalFeatures, + localFeatures = localFeatures) + },{ localParams => + + localParams.nodeId :: + localParams.channelKeyPath.fold(kp => kp, fKp => fKp.fundingKeyPath) :: // when encoding an old version of local params 'KeyPathFundee' holds 2 identical keypaths + localParams.dustLimitSatoshis :: + localParams.maxHtlcValueInFlightMsat :: + localParams.channelReserveSatoshis :: + localParams.htlcMinimumMsat :: + localParams.toSelfDelay :: + localParams.maxAcceptedHtlcs :: + localParams.isFunder :: + localParams.defaultFinalScriptPubKey :: + localParams.globalFeatures :: localParams.localFeatures :: HNil + }) + + val localParamsCodecCurrent: Codec[LocalParams] = ( + ("nodeId" | publicKey) :: + ("channelPath" | channelKeyPathCodec) :: + ("dustLimitSatoshis" | uint64overflow) :: + ("maxHtlcValueInFlightMsat" | uint64) :: + ("channelReserveSatoshis" | uint64overflow) :: + ("htlcMinimumMsat" | uint64overflow) :: + ("toSelfDelay" | uint16) :: + ("maxAcceptedHtlcs" | uint16) :: + ("defaultFinalScriptPubKey" | varsizebinarydata) :: + ("globalFeatures" | varsizebinarydata) :: ("localFeatures" | varsizebinarydata)).as[LocalParams] + def localParamsCodec(version: ChannelVersion): Codec[LocalParams] = version match { + case STANDARD => localParamsCodecPreDKP + case DETERMINISTIC_KEYPATH => localParamsCodecCurrent + } + val remoteParamsCodec: Codec[RemoteParams] = ( ("nodeId" | publicKey) :: ("dustLimitSatoshis" | uint64overflow) :: @@ -211,22 +279,23 @@ object ChannelCodecs extends Logging { (wire: BitVector) => spentListCodec.decode(wire).map(_.map(_.toMap)) ) - val commitmentsCodec: Codec[Commitments] = ( - ("channelVersion" | channelVersionCodec) :: - ("localParams" | localParamsCodec) :: - ("remoteParams" | remoteParamsCodec) :: - ("channelFlags" | byte) :: - ("localCommit" | localCommitCodec) :: - ("remoteCommit" | remoteCommitCodec) :: - ("localChanges" | localChangesCodec) :: - ("remoteChanges" | remoteChangesCodec) :: - ("localNextHtlcId" | uint64overflow) :: - ("remoteNextHtlcId" | uint64overflow) :: - ("originChannels" | originsMapCodec) :: - ("remoteNextCommitInfo" | either(bool, waitingForRevocationCodec, publicKey)) :: - ("commitInput" | inputInfoCodec) :: - ("remotePerCommitmentSecrets" | ShaChain.shaChainCodec) :: - ("channelId" | bytes32)).as[Commitments] + val commitmentsCodec: Codec[Commitments] = + (("channelVersion" | channelVersionCodec) >>:~ { version => + ("localParams" | localParamsCodec(version)) :: + ("remoteParams" | remoteParamsCodec) :: + ("channelFlags" | byte) :: + ("localCommit" | localCommitCodec) :: + ("remoteCommit" | remoteCommitCodec) :: + ("localChanges" | localChangesCodec) :: + ("remoteChanges" | remoteChangesCodec) :: + ("localNextHtlcId" | uint64overflow) :: + ("remoteNextHtlcId" | uint64overflow) :: + ("originChannels" | originsMapCodec) :: + ("remoteNextCommitInfo" | either(bool, waitingForRevocationCodec, publicKey)) :: + ("commitInput" | inputInfoCodec) :: + ("remotePerCommitmentSecrets" | ShaChain.shaChainCodec) :: + ("channelId" | bytes32) + }).as[Commitments] val closingTxProposedCodec: Codec[ClosingTxProposed] = ( ("unsignedTx" | txCodec) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala index 0e4aef640a..fb323345a7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala @@ -50,6 +50,8 @@ case class Init(globalFeatures: ByteVector, case class Error(channelId: ByteVector32, data: ByteVector) extends SetupMessage with HasChannelId { def toAscii: String = if (fr.acinq.eclair.isAsciiPrintable(data)) new String(data.toArray, StandardCharsets.US_ASCII) else "n/a" + + override def toString: String = toAscii } object Error { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 79770b5833..f1ff0d39eb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -19,8 +19,10 @@ package fr.acinq.eclair import java.sql.{Connection, DriverManager} import fr.acinq.bitcoin.Crypto.PrivateKey +import fr.acinq.bitcoin.DeterministicWallet.KeyPath import fr.acinq.bitcoin.{Block, ByteVector32, Script} import fr.acinq.eclair.NodeParams.BITCOIND +import fr.acinq.eclair.channel.{Channel, KeyPathFundee} import fr.acinq.eclair.crypto.LocalKeyManager import fr.acinq.eclair.db._ import fr.acinq.eclair.db.sqlite._ @@ -28,6 +30,7 @@ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.router.RouterConf import fr.acinq.eclair.wire.{Color, NodeAddress} import scodec.bits.ByteVector + import scala.concurrent.duration._ /** @@ -101,11 +104,12 @@ object TestConstants { maxPaymentAttempts = 5 ) - def channelParams = Peer.makeChannelParams( + def channelParams = Channel.makeChannelParams( nodeParams = nodeParams, defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32).publicKey)), - isFunder = true, - fundingSatoshis).copy( + fundingSatoshis, + Left(KeyPath(Seq(1, 2, 3, 4L))) + ).copy( channelReserveSatoshis = 10000 // Bob will need to keep that much satoshis as direct payment ) } @@ -167,11 +171,12 @@ object TestConstants { maxPaymentAttempts = 5 ) - def channelParams = Peer.makeChannelParams( + def channelParams = Channel.makeChannelParams( nodeParams = nodeParams, defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32).publicKey)), - isFunder = false, - fundingSatoshis).copy( + fundingSatoshis, + Right(KeyPathFundee(KeyPath(Seq(1, 2, 3, 4L)), KeyPath(Seq(1, 2, 3, 4L)))) + ).copy( channelReserveSatoshis = 20000 // Alice will need to keep that much satoshis as direct payment ) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/TestWallet.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/TestWallet.scala index 825a609efa..9bf06dfdce 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/TestWallet.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/TestWallet.scala @@ -16,7 +16,8 @@ package fr.acinq.eclair.blockchain -import fr.acinq.bitcoin.{ByteVector32, Crypto, OP_PUSHDATA, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.{ByteVector32, Crypto, OP_PUSHDATA, OutPoint, Satoshi, Script, ScriptWitness, Transaction, TxIn, TxOut} +import fr.acinq.eclair.randomBytes import scodec.bits.ByteVector import scala.concurrent.Future @@ -31,11 +32,15 @@ class TestWallet extends EclairWallet { override def getBalance: Future[Satoshi] = ??? - override def getFinalAddress: Future[String] = Future.successful("2MsRZ1asG6k94m6GYUufDGaZJMoJ4EV5JKs") + override def getFinalAddress: Future[String] = Future.successful("bcrt1q82l6tngfd7stp2amhd8w2crn7dfy3qyelzywtn") - override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = + override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long, lockUnspent: Boolean = true): Future[MakeFundingTxResponse] = Future.successful(TestWallet.makeDummyFundingTx(pubkeyScript, amount, feeRatePerKw)) + override def signTransactionComplete(tx: Transaction): Future[Transaction] = Future.successful { + tx.updateWitness(0, ScriptWitness(Seq(randomBytes(73)))) + } + override def commit(tx: Transaction): Future[Boolean] = Future.successful(true) override def rollback(tx: Transaction): Future[Boolean] = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala index 4771d98fe4..19e0c124c4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala @@ -24,13 +24,16 @@ import akka.actor.{ActorRef, ActorSystem, Props} import akka.testkit.{TestKit, TestProbe} import com.whisk.docker.DockerReadyChecker import fr.acinq.bitcoin.{Block, Btc, ByteVector32, DeterministicWallet, MnemonicCode, Satoshi, Transaction, TxOut} -import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet.{FundTransactionResponse, SignTransactionResponse} +import fr.acinq.eclair.blockchain.MakeFundingTxResponse +import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet.FundTransactionResponse import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, BitcoindService} import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{BroadcastTransaction, BroadcastTransactionResponse, SSL} import fr.acinq.eclair.blockchain.electrum.ElectrumClientPool.ElectrumServerAddress +import fr.acinq.eclair.blockchain.electrum.ElectrumWallet._ import fr.acinq.eclair.blockchain.electrum.db.sqlite.SqliteWalletDb +import fr.acinq.eclair.channel.Channel import grizzled.slf4j.Logging -import org.json4s.JsonAST.{JDecimal, JString, JValue} +import org.json4s.JsonAST.{JArray, JDecimal, JString, JValue} import org.scalatest.{BeforeAndAfterAll, FunSuiteLike} import scodec.bits.ByteVector @@ -38,11 +41,8 @@ import scala.concurrent.Await import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ - class ElectrumWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BitcoindService with ElectrumxService with BeforeAndAfterAll with Logging { - import ElectrumWallet._ - val entropy = ByteVector32(ByteVector.fill(32)(1)) val mnemonics = MnemonicCode.toMnemonics(entropy) val seed = MnemonicCode.toSeed(mnemonics, "") @@ -160,7 +160,7 @@ class ElectrumWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val btcWallet = new BitcoinCoreWallet(bitcoinrpcclient) val future = for { FundTransactionResponse(tx1, pos, fee) <- btcWallet.fundTransaction(tx, false, 10000) - SignTransactionResponse(tx2, true) <- btcWallet.signTransaction(tx1) + BitcoinCoreWallet.SignTransactionResponse(tx2, true) <- btcWallet.signTransaction(tx1) txid <- btcWallet.publishTransaction(tx2) } yield txid val txid = Await.result(future, 10 seconds) @@ -341,4 +341,80 @@ class ElectrumWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLike probe.send(wallet, IsDoubleSpent(tx2)) probe.expectMsg(IsDoubleSpentResponse(tx2, true)) } + + test("fund, sign and broadcast a transaction") { + val probe = TestProbe() + + val GetBalanceResponse(confirmed, unconfirmed) = getBalance(probe) + logger.info(s" balance: $confirmed $unconfirmed") + + // send money to our wallet + val GetCurrentReceiveAddressResponse(address) = getCurrentAddress(probe) + + logger.info(s"sending 1 btc to $address") + probe.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0)) + probe.expectMsgType[JValue] + + awaitCond({ + val GetBalanceResponse(_, unconfirmed1) = getBalance(probe) + unconfirmed1 == unconfirmed + Satoshi(100000000L) + }, max = 30 seconds, interval = 1 second) + + // confirm our tx + probe.send(bitcoincli, BitcoinReq("generate", 1)) + probe.expectMsgType[JValue] + + awaitCond({ + val GetBalanceResponse(confirmed1, _) = getBalance(probe) + confirmed1 == confirmed + Satoshi(100000000L) + }, max = 30 seconds, interval = 1 second) + + val amountToSend = Satoshi(50000000L) // 0.5 BTC + + // create a tx that sends money to Bitcoin Core's address + probe.send(bitcoincli, BitcoinReq("getnewaddress")) + val JString(destinationAddress) = probe.expectMsgType[JValue] + + // raw transaction that sends the funds to a hardcoded address + val hardcodedOutput = TxOut(amountToSend, fr.acinq.eclair.addressToPublicKeyScript("2N2JczfZK7tDJ9yuH3eQ9S64fL3dMp5eNCr", Block.RegtestGenesisBlock.hash)) + val tx = Transaction(version = 2, txIn = Nil, txOut = hardcodedOutput :: Nil, lockTime = 0L) + + // this will ask the electrum wallet to attach an input (fund the tx) + probe.send(wallet, CompleteTransaction(tx, 20000)) + val CompleteTransactionResponse(tx1, _, None) = probe.expectMsgType[CompleteTransactionResponse] + + // we strip the signatures from the tx + val tx2 = Channel.stripSignaturesFromTx(MakeFundingTxResponse(tx1, 0, Satoshi(0))).fundingTx + + // assert there are no signatures + assert(tx2.txIn.forall(_.signatureScript.isEmpty)) + assert(tx2.txIn.forall(!_.hasWitness)) + + // update the transaction with the actual output we want + val tx3 = tx2.copy(txOut = tx2.txOut.updated( + tx2.txOut.indexOf(hardcodedOutput), + TxOut(amountToSend, fr.acinq.eclair.addressToPublicKeyScript(destinationAddress, Block.RegtestGenesisBlock.hash)) + )) + + // ask the electrum wallet to sign the transaction again + probe.send(wallet, SignTransaction(tx3)) + val ElectrumWallet.SignTransactionResponse(tx4) = probe.expectMsgType[SignTransactionResponse] + + // broadcast the transaction + logger.info(s"sending 0.5 btc to $address with tx ${tx4.txid}") + probe.send(wallet, BroadcastTransaction(tx4)) + val BroadcastTransactionResponse(_, None) = probe.expectMsgType[BroadcastTransactionResponse] + + // mine a block + probe.send(bitcoincli, BitcoinReq("generate", 1)) + val JArray(List(JString(blockHash))) = probe.expectMsgType[JValue] + + // get the block + probe.send(bitcoincli, BitcoinReq("getblock", blockHash, 0)) + val JString(serializedBlock) = probe.expectMsgType[JValue] + + // assert the block contains our transaction + assert(Block.read(serializedBlock).tx.exists(_.txid == tx4.txid)) + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index d44d98dc94..707df15fb7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -67,7 +67,7 @@ class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Loggi relayerA ! alice relayerB ! bob // no announcements - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, pipe, bobInit, channelFlags = 0x00.toByte) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, pipe, bobInit, channelFlags = 0x00.toByte) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit) pipe ! (alice, bob) alice2blockchain.expectMsgType[WatchSpent] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala index 18aff515ef..981187c56a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala @@ -71,7 +71,7 @@ class ThroughputSpec extends FunSuite { val bob = system.actorOf(Channel.props(Bob.nodeParams, wallet, Alice.nodeParams.nodeId, blockchain, ???, relayerB, None), "b") val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, pipe, bobInit, ChannelFlags.Empty) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, pipe, bobInit, ChannelFlags.Empty) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit) val latch = new CountDownLatch(2) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala index 8ced14a635..650a3c04de 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala @@ -77,7 +77,7 @@ trait StateTestsHelperMethods extends TestKitBase { val bobInit = Init(bobParams.globalFeatures, bobParams.localFeatures) // reset global feerates (they may have been changed by previous tests) Globals.feeratesPerKw.set(FeeratesPerKw.single(TestConstants.feeratePerKw)) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, alice2bob.ref, bobInit, channelFlags) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index b71fd9de45..6e966acc31 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -22,12 +22,10 @@ import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.{MakeFundingTxResponse, TestWallet} import fr.acinq.eclair.channel.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel.states.StateTestsHelperMethods -import fr.acinq.eclair.channel.{WAIT_FOR_FUNDING_INTERNAL, _} +import fr.acinq.eclair.channel.{WAIT_FOR_FUNDING_INTERNAL_SIGNED, _} import fr.acinq.eclair.wire.{AcceptChannel, Error, Init, OpenChannel} import fr.acinq.eclair.{TestConstants, TestkitBaseClass} import org.scalatest.{Outcome, Tag} -import scodec.bits.ByteVector - import scala.concurrent.{Future, Promise} import scala.concurrent.duration._ @@ -37,39 +35,40 @@ import scala.concurrent.duration._ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelperMethods { - case class FixtureParam(alice: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe) + case class FixtureParam(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe) override def withFixture(test: OneArgTest): Outcome = { - val noopWallet = new TestWallet { - override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = Promise[MakeFundingTxResponse].future // will never be completed - } val setup = if (test.tags.contains("mainnet")) { - init(TestConstants.Alice.nodeParams.copy(chainHash = Block.LivenetGenesisBlock.hash), TestConstants.Bob.nodeParams.copy(chainHash = Block.LivenetGenesisBlock.hash), wallet = noopWallet) + val mainnetWallet = new TestWallet { + override def getFinalAddress: Future[String] = Future.successful("3LcWzTnuZGPkGkPyX7tfKsktdvMoz4VabR") + } + init(TestConstants.Alice.nodeParams.copy(chainHash = Block.LivenetGenesisBlock.hash), TestConstants.Bob.nodeParams.copy(chainHash = Block.LivenetGenesisBlock.hash), wallet = mainnetWallet) } else { - init(wallet = noopWallet) + init() } import setup._ val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, alice2bob.ref, bobInit, ChannelFlags.Empty) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] - alice2bob.forward(bob) awaitCond(alice.stateName == WAIT_FOR_ACCEPT_CHANNEL) - withFixture(test.toNoArgTest(FixtureParam(alice, alice2bob, bob2alice, alice2blockchain))) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain))) } } test("recv AcceptChannel") { f => import f._ + alice2bob.forward(bob) bob2alice.expectMsgType[AcceptChannel] bob2alice.forward(alice) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL_SIGNED) } test("recv AcceptChannel (invalid max accepted htlcs)") { f => import f._ + alice2bob.forward(bob) val accept = bob2alice.expectMsgType[AcceptChannel] // spec says max = 483 val invalidMaxAcceptedHtlcs = 484 @@ -81,6 +80,7 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp test("recv AcceptChannel (invalid dust limit)", Tag("mainnet")) { f => import f._ + alice2bob.forward(bob) val accept = bob2alice.expectMsgType[AcceptChannel] // we don't want their dust limit to be below 546 val lowDustLimitSatoshis = 545 @@ -92,6 +92,7 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp test("recv AcceptChannel (to_self_delay too high)") { f => import f._ + alice2bob.forward(bob) val accept = bob2alice.expectMsgType[AcceptChannel] val delayTooHigh = 10000 alice ! accept.copy(toSelfDelay = delayTooHigh) @@ -102,6 +103,7 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp test("recv AcceptChannel (reserve too high)") { f => import f._ + alice2bob.forward(bob) val accept = bob2alice.expectMsgType[AcceptChannel] // 30% is huge, recommended ratio is 1% val reserveTooHigh = (0.3 * TestConstants.fundingSatoshis).toLong @@ -113,6 +115,7 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp test("recv AcceptChannel (reserve below dust limit)") { f => import f._ + alice2bob.forward(bob) val accept = bob2alice.expectMsgType[AcceptChannel] val reserveTooSmall = accept.dustLimitSatoshis - 1 alice ! accept.copy(channelReserveSatoshis = reserveTooSmall) @@ -123,6 +126,7 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp test("recv AcceptChannel (reserve below our dust limit)") { f => import f._ + alice2bob.forward(bob) val accept = bob2alice.expectMsgType[AcceptChannel] val open = alice.stateData.asInstanceOf[DATA_WAIT_FOR_ACCEPT_CHANNEL].lastSent val reserveTooSmall = open.dustLimitSatoshis - 1 @@ -134,6 +138,7 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp test("recv AcceptChannel (dust limit above our reserve)") { f => import f._ + alice2bob.forward(bob) val accept = bob2alice.expectMsgType[AcceptChannel] val open = alice.stateData.asInstanceOf[DATA_WAIT_FOR_ACCEPT_CHANNEL].lastSent val dustTooBig = open.channelReserveSatoshis + 1 @@ -145,19 +150,37 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp test("recv Error") { f => import f._ + alice2bob.forward(bob) + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_ACCEPT_CHANNEL].unsignedFundingTx.fundingTx + assert(alice.underlyingActor.wallet.asInstanceOf[TestWallet].rolledback.isEmpty) + alice ! Error(ByteVector32.Zeroes, "oops") + + assert(alice.underlyingActor.wallet.asInstanceOf[TestWallet].rolledback.contains(fundingTx)) awaitCond(alice.stateName == CLOSED) } test("recv CMD_CLOSE") { f => import f._ + alice2bob.forward(bob) + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_ACCEPT_CHANNEL].unsignedFundingTx.fundingTx + assert(alice.underlyingActor.wallet.asInstanceOf[TestWallet].rolledback.isEmpty) + alice ! CMD_CLOSE(None) + + assert(alice.underlyingActor.wallet.asInstanceOf[TestWallet].rolledback.contains(fundingTx)) awaitCond(alice.stateName == CLOSED) } test("recv TickChannelOpenTimeout") { f => import f._ + alice2bob.forward(bob) + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_ACCEPT_CHANNEL].unsignedFundingTx.fundingTx + assert(alice.underlyingActor.wallet.asInstanceOf[TestWallet].rolledback.isEmpty) + alice ! TickChannelOpenTimeout + + assert(alice.underlyingActor.wallet.asInstanceOf[TestWallet].rolledback.contains(fundingTx)) awaitCond(alice.stateName == CLOSED) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index 6b74dfd654..d7f89523fb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -41,7 +41,7 @@ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelper val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, alice2bob.ref, bobInit, ChannelFlags.Empty) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) awaitCond(bob.stateName == WAIT_FOR_OPEN_CHANNEL) withFixture(test.toNoArgTest(FixtureParam(bob, alice2bob, bob2alice, bob2blockchain))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala index 2e3edfe5fa..b71bb94306 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala @@ -16,18 +16,17 @@ package fr.acinq.eclair.channel.states.b +import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition} import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.{ByteVector32, Satoshi} +import fr.acinq.bitcoin.{ByteVector32, Satoshi, Transaction} import fr.acinq.eclair.TestConstants.{Alice, Bob} -import fr.acinq.eclair.blockchain.{MakeFundingTxResponse, TestWallet} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods +import fr.acinq.eclair.payment.PaymentLifecycle.WAITING_FOR_REQUEST import fr.acinq.eclair.wire._ import fr.acinq.eclair.{TestConstants, TestkitBaseClass} import org.scalatest.Outcome -import scodec.bits.ByteVector -import scala.concurrent.{Future, Promise} import scala.concurrent.duration._ /** @@ -39,21 +38,23 @@ class WaitForFundingCreatedInternalStateSpec extends TestkitBaseClass with State case class FixtureParam(alice: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe) override def withFixture(test: OneArgTest): Outcome = { - val noopWallet = new TestWallet { - override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = Promise[MakeFundingTxResponse].future // will never be completed - } - val setup = init(wallet = noopWallet) + val setup = init() import setup._ val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty) + val monitor = TestProbe() + alice ! SubscribeTransitionCallBack(monitor.ref) + val CurrentState(_, WAIT_FOR_INIT_INTERNAL) = monitor.expectMsgClass(classOf[CurrentState[_]]) + + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, alice2bob.ref, bobInit, ChannelFlags.Empty) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) + + val Transition(_, WAIT_FOR_INIT_INTERNAL, WAIT_FOR_FUNDING_INTERNAL_CREATED) = monitor.expectMsgClass(classOf[Transition[_]]) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) bob2alice.expectMsgType[AcceptChannel] bob2alice.forward(alice) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) withFixture(test.toNoArgTest(FixtureParam(alice, alice2bob, bob2alice, alice2blockchain))) } } @@ -66,6 +67,7 @@ class WaitForFundingCreatedInternalStateSpec extends TestkitBaseClass with State test("recv CMD_CLOSE") { f => import f._ + alice ! CMD_CLOSE(None) awaitCond(alice.stateName == CLOSED) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala index 3bc38c87cf..d390fc9dfb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala @@ -48,7 +48,7 @@ class WaitForFundingCreatedStateSpec extends TestkitBaseClass with StateTestsHel val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, alice2bob.ref, bobInit, ChannelFlags.Empty) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedInternalStateSpec.scala new file mode 100644 index 0000000000..a3552f344e --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedInternalStateSpec.scala @@ -0,0 +1,97 @@ +/* + * Copyright 2019 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.channel.states.b + +import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition} + +import scala.concurrent.duration._ +import akka.testkit.{TestFSMRef, TestProbe} +import fr.acinq.bitcoin.{Block, ByteVector32, Script, Transaction} +import fr.acinq.eclair.TestConstants.{Alice, Bob} +import fr.acinq.eclair.blockchain.TestWallet +import fr.acinq.eclair.{TestConstants, TestkitBaseClass} +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.states.StateTestsHelperMethods +import fr.acinq.eclair.wire.{AcceptChannel, Error, Init, OpenChannel} +import org.scalatest.Outcome + +import scala.concurrent.{Await, ExecutionContext, Future, Promise} + +class WaitForFundingSignedInternalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { + + case class FixtureParam(alice: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe) + + override def withFixture(test: OneArgTest): Outcome = { + + val nonReturningWallet = new TestWallet { + override def signTransactionComplete(tx: Transaction): Future[Transaction] = Promise[Transaction]().future + } + + val setup = init(wallet = nonReturningWallet) + import setup._ + val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) + val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) + within(30 seconds) { + val monitor = TestProbe() + alice ! SubscribeTransitionCallBack(monitor.ref) + val CurrentState(_, WAIT_FOR_INIT_INTERNAL) = monitor.expectMsgClass(classOf[CurrentState[_]]) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, alice2bob.ref, bobInit, ChannelFlags.Empty) + bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) + val Transition(_, WAIT_FOR_INIT_INTERNAL, WAIT_FOR_FUNDING_INTERNAL_CREATED) = monitor.expectMsgClass(classOf[Transition[_]]) + alice2bob.expectMsgType[OpenChannel] + alice2bob.forward(bob) + bob2alice.expectMsgType[AcceptChannel] + bob2alice.forward(alice) + val Transition(_, WAIT_FOR_FUNDING_INTERNAL_CREATED, WAIT_FOR_ACCEPT_CHANNEL) = monitor.expectMsgClass(classOf[Transition[_]]) + val Transition(_, WAIT_FOR_ACCEPT_CHANNEL, WAIT_FOR_FUNDING_INTERNAL_SIGNED) = monitor.expectMsgClass(classOf[Transition[_]]) + withFixture(test.toNoArgTest(FixtureParam(alice, alice2bob, bob2alice, alice2blockchain))) + } + } + + test("recv Error") { f => + import f._ + + assert(alice.stateName == WAIT_FOR_FUNDING_INTERNAL_SIGNED) + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL_SIGNED].unsignedFundingTx + assert(alice.underlyingActor.wallet.asInstanceOf[TestWallet].rolledback.isEmpty) + + alice ! Error(ByteVector32.Zeroes, "oops") + + awaitCond({ + val rolledBackTx = alice.underlyingActor.wallet.asInstanceOf[TestWallet].rolledback.head + rolledBackTx.txOut == fundingTx.txOut && rolledBackTx.txIn.map(_.outPoint) == fundingTx.txIn.map(_.outPoint) + }) + awaitCond(alice.stateName == CLOSED) + } + + test("recv CMD_CLOSE") { f => + import f._ + + assert(alice.stateName == WAIT_FOR_FUNDING_INTERNAL_SIGNED) + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL_SIGNED].unsignedFundingTx + assert(alice.underlyingActor.wallet.asInstanceOf[TestWallet].rolledback.isEmpty) + + alice ! CMD_CLOSE(None) + + awaitCond({ + val rolledBackTx = alice.underlyingActor.wallet.asInstanceOf[TestWallet].rolledback.head + rolledBackTx.txOut == fundingTx.txOut && rolledBackTx.txIn.map(_.outPoint) == fundingTx.txIn.map(_.outPoint) + }) + awaitCond(alice.stateName == CLOSED) + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index 12722b2e1b..647e9c4ab9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -44,7 +44,7 @@ class WaitForFundingSignedStateSpec extends TestkitBaseClass with StateTestsHelp val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, alice2bob.ref, bobInit, ChannelFlags.Empty) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index d3ef649aab..6c7c6b2c89 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -22,12 +22,13 @@ import fr.acinq.bitcoin.{ByteVector32, Satoshi, Script, Transaction} import fr.acinq.eclair.randomKey import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain._ -import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.{WAIT_FOR_FUNDING_CONFIRMED, _} import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.transactions.Scripts.multiSig2of2 -import fr.acinq.eclair.wire.{AcceptChannel, Error, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel} +import fr.acinq.eclair.wire.{AcceptChannel, ChannelCodecs, Error, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel} import fr.acinq.eclair.{TestConstants, TestkitBaseClass} import org.scalatest.Outcome +import scodec.bits._ import scala.concurrent.duration._ @@ -45,7 +46,7 @@ class WaitForFundingConfirmedStateSpec extends TestkitBaseClass with StateTestsH val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, alice2bob.ref, bobInit, ChannelFlags.Empty) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) @@ -82,6 +83,19 @@ class WaitForFundingConfirmedStateSpec extends TestkitBaseClass with StateTestsH alice2bob.expectMsgType[FundingLocked] } + // this asserts we can move WAIT_FOR_FUNDING_CONFIRMED -> WAIT_FOR_FUNDING_LOCKED after upgrading from master 316ba02f + test("recv BITCOIN_FUNDING_DEPTHOK using a legacy DATA_WAIT_FOR_FUNDING_CONFIRMED") { f => + import f._ + + val legacyStateData = ChannelCodecs.stateDataCodec.decode(hex"00000803af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d00004087224bcb3e8633fe2886e6f3c18b703000000000000044c0000000008f0d1800000000000002710000000000000000000900064800b000a3c62de7dc8e62bd93b66cf47420e5790edef339a800000008001cee07058e92c82f227efb7c744baad2f10cb42c20685e0e94699847cb1451ac280000000000001f47fffffffffffffff800000000000271000000000000001f40048000f01ed9187296e4c54b479ae345140aa1391ccc9faf267f6c5944dfe664caa63bc87819a259d51b0799ae40ffe378da636a9295f0f1ca729fe7288e3fb726321ddacb9012a9a22a5fe0f279fd7a14a2470128271d76ef0d4f7e2e6a89d40372966b3c60d81d57eefaafa9c0d29da3771e8bfb57df89bf9222a6201a512dffb3002070c2bb101e68a0233838a37442cc5ac9367c5e33036d86c59da6504bf4b640c513dcc8f44000000000000000000000000000000000013880000000017d784000000000005f5e100001256455401b07c5bf408c59267c5c7f9ba4a3d95e328450deb5a7a3296d83ecab2800000000015a021078000000000110010646319c0c17e834063062d25ad955159489aea968e1b14d6cc58cb99be89c7838023a910813d7b72631b3de37925f4d91934a35e9f5178ff78c7deaba3b49ba5a94cff776b1081ed9187296e4c54b479ae345140aa1391ccc9faf267f6c5944dfe664caa63bc87a95700ad01000000000080d6455401b07c5bf408c59267c5c7f9ba4a3d95e328450deb5a7a3296d83ecab28000000000316633400120068180000000000b000a08f63f057773606da741e58e2f9d7a536d9d91dd5c0c0600000000001100105be873e3d5ba9979d72da3bbd5720a416b1d970bf2bc00702f1676655c18d65b82002418228110805db5dae6348bc53e64f001e567412275e64af83e5b66d71e6c0d693d833d8c3181102f27d53bd13e6e7581e9bc55cc101cb844a50f2094ef24a4b0f65fe6fbe6f67880a39822011020340f177c84953a0321b6c2cde893fa4d3629bd1f9b5164308fb5766196850d811010d91e80ba0f02a3ed8a31a84f897338f4549e6dab14b109b591d25a44da8b4000a3a910813d7b72631b3de37925f4d91934a35e9f5178ff78c7deaba3b49ba5a94cff776b1081ed9187296e4c54b479ae345140aa1391ccc9faf267f6c5944dfe664caa63bc87a9570382a890000000000000000000000000000013880000000005f5e1000000000017d784000eb072f0b13aeae10a66802ce65c477f1eb2a44be91c4f178a044479bf442060016197621911deea00ecdd7d789e91690661608651f7b5e7c6b7f30582b4dd6d2100000000000000000000000000000000000000000000000000000000000040c0c42642c86958a4e78c48f1078e03ccf00165e9483691069a1e70f59a15c1b580092b22aa00d83e2dfa0462c933e2e3fcdd251ecaf1942286f5ad3d194b6c1f655940000000000ad01083c00000000008800832318ce060bf41a031831692d6caa8aca44d754b470d8a6b662c65ccdf44e3c1c011d488409ebdb9318d9ef1bc92fa6c8c9a51af4fa8bc7fbc63ef55d1da4dd2d4a67fbbb58840f6c8c394b7262a5a3cd71a28a05509c8e664fd7933fb62ca26ff33265531de43d4ab8000159155006c1f16fd02316499f171fe6e928f6578ca11437ad69e8ca5b60fb2acb005e020000000101010101010101010101010101010101010101010101010101010101010101012a00000000ffffffff0140420f0000000000220020c8c6338182fd0680c60c5a4b5b2aa2b29135d52d1c3629ad98b197337d138f0700000000000000005d10c71400000000000000000000000000000000000000000000000000000000000000002b22aa00d83e2dfa0462c933e2e3fcdd251ecaf1942286f5ad3d194b6c1f655940003726337f1c37ac74d004a8836ac4fc05eb3dbb6f4c99301a2a4d079bc6fa9391d00e30f4a4a1aa0bb119b1528ca822195da27202992398f79c820cb0569ba4910".bits).require.value + alice.setState(WAIT_FOR_FUNDING_CONFIRMED, stateData = legacyStateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED]) + alice ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, 42000, 42, legacyStateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_LOCKED) + alice2blockchain.expectMsgType[WatchLost] + alice2bob.expectMsgType[FundingLocked] + } + + test("recv BITCOIN_FUNDING_DEPTHOK (bad funding pubkey script)") { f => import f._ val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala index de4fa0ea27..f4225a552e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala @@ -26,7 +26,7 @@ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.wire._ import fr.acinq.eclair.{TestConstants, TestkitBaseClass} import org.scalatest.Outcome - +import scodec.bits._ import scala.concurrent.duration._ /** @@ -43,7 +43,7 @@ class WaitForFundingLockedStateSpec extends TestkitBaseClass with StateTestsHelp val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, alice2bob.ref, bobInit, ChannelFlags.Empty) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) @@ -78,6 +78,18 @@ class WaitForFundingLockedStateSpec extends TestkitBaseClass with StateTestsHelp bob2alice.expectNoMsg(200 millis) } + // this asserts we can move WAIT_FOR_FUNDING_LOCKED -> NORMAL after upgrading from master 316ba02f + test("recv FundingLocked using a legacy DATA_WAIT_FOR_FUNDING_LOCKED") { f => + import f._ + + val legacyStateData = ChannelCodecs.stateDataCodec.decode(hex"00000203af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0000419bcfa4263dd8d3687392dafc9912ee2000000000000044c0000000008f0d1800000000000002710000000000000000000900064800b000a011bd50be544b3e72531e4bdcda249a7035bfe8a800000008001cee07058e92c82f227efb7c744baad2f10cb42c20685e0e94699847cb1451ac280000000000001f47fffffffffffffff800000000000271000000000000001f40048000f011b7b3af58e62bd1afb758ec699a9de5c024bb9ffa3a13eedff8cab2f42fadf8e811c3a0116ed5f9b6e70131b4a563f14887ad2c74da13b0153b7eeb674d6cc408c01536569e85f9f905ffa5517ddc1f95e88d0350df9ccf58760fa315521c3917e0b81ebd3e53a68001ba7bbe2e387b4de858147f0f3facbc7460251438d931177e89981e15184fa6a7d0750233dbfdfdf023c5ef72ae88d67c8e64f499e020fe4d0723f800000000000000000000000000000000013880000000017d784000000000005f5e10000121b6b4371be5edfc7bc25e0c9201d867b1869a30a84928bcb30f3aee423b28f14800000000015a0210780000000001100102b64496b535cc694f07c8ca04832eabc04df4910a9707df0d1d5478d5d3862bd0023a910811b7b3af58e62bd1afb758ec699a9de5c024bb9ffa3a13eedff8cab2f42fadf8e9081fc58d19c6c0c664fb329dd212a43e043a7094202c5f4410b04e964353b1770c8a95700ac810000000000809b6b4371be5edfc7bc25e0c9201d867b1869a30a84928bcb30f3aee423b28f1480000000000cbf3dc00120068180000000000b000a1ddff3dac3bc63f50b7f548c71c3c67189fac725dc0c06000000000011001012b70e548015620a96a8b89ff50d877830f87a49edfb7baf3cba2944eb6a3c58020023982201100be011964fa2b937fc410107f1196e12fc512be2f7495f09a954f8afe53d2a0a011019210ba8ef98698a7fb84c0598757e9c38fc3d81cf919bfb19d3b6676baff85500a39822011003ebbeb478fa8233d82d86402dbf5e5e3ab92a79c5093fb3b1a3792c1e51dc78811026e1122d3fd2ed2a14d61f3496679838c57cedcdb9e35a02145f34db5da701b880a3a910811b7b3af58e62bd1afb758ec699a9de5c024bb9ffa3a13eedff8cab2f42fadf8e9081fc58d19c6c0c664fb329dd212a43e043a7094202c5f4410b04e964353b1770c8a95702ba2d90000000000000000000000000000013880000000005f5e1000000000017d7840060a5b5bbab14ce247fdbfcef08972f3c4cbecb63bb5c7832b24db0150ab37da8013fb54c36f195bef6fd3034d4105a75b04d688b8e08d035e19df55b3a06623e2000000000000000000000000000000000000000000000000000000000000040e3559e01ef0db325dae812ad6a4f85fdfc4539ae02d2ced0e06cacc8b4cbf0f800090db5a1b8df2f6fe3de12f064900ec33d8c34d185424945e59879d77211d9478a40000000000ad01083c00000000008800815b224b5a9ae634a783e46502419755e026fa48854b83ef868eaa3c6ae9c315e8011d488408dbd9d7ac7315e8d7dbac7634cd4ef2e0125dcffd1d09f76ffc65597a17d6fc74840fe2c68ce36063327d994ee909521f021d384a10162fa20858274b21a9d8bb86454ab800006dad0dc6f97b7f1ef0978324807619ec61a68c2a124a2f2cc3cebb908eca3c520c350000005400006dad0dc6f97b7f1ef0978324807619ec61a68c2a124a2f2cc3cebb908eca3c52044526ddfd95ae01708239a4c3f48a04a762a4586607066e782569afa115590a840".bits).require.value + alice.setState(WAIT_FOR_FUNDING_LOCKED, stateData = legacyStateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED]) + bob2alice.expectMsgType[FundingLocked] + bob2alice.forward(alice) + awaitCond(alice.stateName == NORMAL) + bob2alice.expectNoMsg(200 millis) + } + test("recv BITCOIN_FUNDING_SPENT (remote commit)") { f => import f._ // bob publishes his commitment tx diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index b37eb9418b..3d84082a37 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -27,7 +27,7 @@ import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.UInt64.Conversions._ import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw -import fr.acinq.eclair.channel.Channel.{BroadcastChannelUpdate, PeriodicRefresh, Reconnected, RevocationTimeout} +import fr.acinq.eclair.channel.Channel._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.io.Peer @@ -2089,7 +2089,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42, null)) val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures] import initialState.commitments.{localParams, remoteParams} - val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, Alice.keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) + val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, Alice.keyManager.fundingPublicKey(fundingKeyPath(localParams)).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) // actual test starts here bob2alice.forward(alice) awaitCond({ @@ -2108,7 +2108,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10, null)) val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures] import initialState.commitments.{localParams, remoteParams} - val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, Alice.keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) + val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, Alice.keyManager.fundingPublicKey(fundingKeyPath(localParams)).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) bob2alice.forward(alice) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement === Some(channelAnn)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index e3a2c2dfcc..ab640efef7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.channel.states.e import akka.actor.Status import java.util.UUID - +import fr.acinq.eclair.channel.Channel._ import akka.testkit.TestProbe import fr.acinq.bitcoin.Crypto.{PrivateKey} import fr.acinq.bitcoin.{ByteVector32, ScriptFlags, Transaction} @@ -80,8 +80,8 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val bobCommitments = bob.stateData.asInstanceOf[HasCommitments].commitments val aliceCommitments = alice.stateData.asInstanceOf[HasCommitments].commitments - val bobCurrentPerCommitmentPoint = TestConstants.Bob.keyManager.commitmentPoint(bobCommitments.localParams.channelKeyPath, bobCommitments.localCommit.index) - val aliceCurrentPerCommitmentPoint = TestConstants.Alice.keyManager.commitmentPoint(aliceCommitments.localParams.channelKeyPath, aliceCommitments.localCommit.index) + val bobCurrentPerCommitmentPoint = TestConstants.Bob.keyManager.commitmentPoint(keyPath(bobCommitments.localParams), bobCommitments.localCommit.index) + val aliceCurrentPerCommitmentPoint = TestConstants.Alice.keyManager.commitmentPoint(keyPath(aliceCommitments.localParams), aliceCommitments.localCommit.index) // a didn't receive any update or sig @@ -164,8 +164,8 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val bobCommitments = bob.stateData.asInstanceOf[HasCommitments].commitments val aliceCommitments = alice.stateData.asInstanceOf[HasCommitments].commitments - val bobCurrentPerCommitmentPoint = TestConstants.Bob.keyManager.commitmentPoint(bobCommitments.localParams.channelKeyPath, bobCommitments.localCommit.index) - val aliceCurrentPerCommitmentPoint = TestConstants.Alice.keyManager.commitmentPoint(aliceCommitments.localParams.channelKeyPath, aliceCommitments.localCommit.index) + val bobCurrentPerCommitmentPoint = TestConstants.Bob.keyManager.commitmentPoint(keyPath(bobCommitments.localParams), bobCommitments.localCommit.index) + val aliceCurrentPerCommitmentPoint = TestConstants.Alice.keyManager.commitmentPoint(keyPath(aliceCommitments.localParams), aliceCommitments.localCommit.index) // a didn't receive the sig val ab_reestablish = alice2bob.expectMsg(ChannelReestablish(ab_add_0.channelId, 1, 0, Some(PrivateKey(ByteVector32.Zeroes)), Some(aliceCurrentPerCommitmentPoint))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index ec19d818e7..c23393d212 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -64,7 +64,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { within(30 seconds) { val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, alice2bob.ref, bobInit, ChannelFlags.Empty) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/LocalKeyManagerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/LocalKeyManagerSpec.scala index 37a8dc367b..4336cc530e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/LocalKeyManagerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/LocalKeyManagerSpec.scala @@ -19,10 +19,14 @@ package fr.acinq.eclair.crypto import fr.acinq.bitcoin.{Block, ByteVector32, DeterministicWallet} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.DeterministicWallet.KeyPath +import fr.acinq.bitcoin.{Block, Script} +import fr.acinq.eclair.TestConstants +import fr.acinq.eclair.channel.Channel.{fundingKeyPath, keyPath} +import fr.acinq.eclair.channel.LocalParams +import fr.acinq.eclair.transactions.Scripts import org.scalatest.FunSuite import scodec.bits._ - class LocalKeyManagerSpec extends FunSuite { test("generate the same node id from the same seed") { // if this test breaks it means that we will generate a different node id from @@ -33,7 +37,7 @@ class LocalKeyManagerSpec extends FunSuite { } test("generate the same secrets from the same seed") { - // data was generated with eclair 0.3 + // data was generated with eclair 0.3 val seed = hex"17b086b228025fa8f4416324b6ba2ec36e68570ae2fc3d392520969f2a9d0c1501" val keyManager = new LocalKeyManager(seed, Block.TestnetGenesisBlock.hash) assert(keyManager.nodeId == PublicKey(hex"02a051267759c3a149e3e72372f4e0c4054ba597ebfd0eda78a2273023667205ee")) @@ -57,4 +61,144 @@ class LocalKeyManagerSpec extends FunSuite { assert(keyManager1.fundingPublicKey(keyPath) != keyManager2.fundingPublicKey(keyPath)) assert(keyManager1.commitmentPoint(keyPath, 1) != keyManager2.commitmentPoint(keyPath, 1)) } -} + + /** + * TESTNET funder funding public key from extended private key: 039abec2238484d9a0680a8ffce6ad920930285fea1cbc30b8f53fad862f6e9157 + * TESTNET funder payment point from extended private key: 03edbfb2cd187e5738f3d4bb5b4d14dad16ee50e3deb1043155d6832d7816aa5f7 + * TESTNET funder revocation point from extended private key: 023c73ac122df051f8cabbefd9d1a06845aab01f0e4fed85475530ff99b73a4bfd + * TESTNET funder delayed payment point from extended private key: 02168b4c51ba573a7cf2158da5338c6e852f5a62960ae2561dcc6f9e5ed6932ba5 + * TESTNET funder htlc point from extended private key: 03d82c30596db587d9e32ebcc68e451727c841ed27ffed441c7eb05e00bb16305d + * + * MAINNET funder funding public key from extended private key: 0353b7dfdb0cbae349146795bd6406ff6ac5cd93bfb31bbdfc5df01b0d4da171b4 + * MAINNET funder payment point from extended private key: 0254036505ad9aa8d8e03ed3727825eaa72ce49f5ff9f2fd286e6d20a2b88caa12 + * + * TESTNET fundee funding public key from extended private key #34273 #0: 02bfe2d6e9ec07b68f588c1bcbf6afdb1e1e068ab11e82b3648eb5032007e9bb48 + * TESTNET fundee htlc public point from extended private key #34273 #0: 037223be02f02a117c9ad3ee243c1664d5763a57911317257536e6f355acb32418 + * TESTNET fundee payment point from extended private key #34273 #0: 02ce4468352b0b722e7e066a3603ed9081dcc6a93137b45e1d9acbe35caad08c2e + * + * MAINNET fundee funding public key from extended private key #34273 #0: 03b8c35b3171dcec2d1e4a2b08244b4b4ce284bd8a7dbef6da467cb6db5abdc30d + * MAINNET fundee htlc public point from extended private key #34273 #0: 0258173e7db0f833691e72b259a9bf5349d9548dd9dcfccf4556a238d9d76193bf + * + * MAINNET fundee revocation point from extended private key #34273 #500: 0213718574b4169ef093286fbc6459c81c7a672cfae62391e405aa04f82fa318e2 + * MAINNET fundee payment point from extended private key #34273 #500: 026a5d2792fb7fec674e75e34a981c74bb07b7439aca8db72d800a5b7f106648be + * MAINNET fundee delayed payment point from extended private key #34273 #500: 03b289f71d00e6b363adc08cdd7eec113441ab1c6c8208621d71e70789874f48d0 + * MAINNET fundee htlc point from extended private key #34273 #500: 039fd510225bd7c70a5f8f1661247c19cab9942088d7c868bad1c47bf0fad9b73d + * MAINNET fundee shachain public point from extended private key #34273 #500: 0200742b2176552d4e29d01455f7de2c16094c77ba644310d5d58385306c09ddc9 + * + */ + + test("test vectors derivation paths (funder TESTNET)") { + + val inputOutpoint = hex"1d12dcab62f3d509db16b8dcb69782ea6358a7060b579675561c4fc2e3294f41" + val seed = hex"0101010102020202AABBCCDD030303030404040405050505060606060707070701" + val keyManager = new LocalKeyManager(seed, Block.TestnetGenesisBlock.hash) + + val funderChannelKeyPath = LocalKeyManager.makeChannelKeyPathFunder(inputOutpoint) + + // TESTNET funder funding public key + assert(keyManager.fundingPublicKey(funderChannelKeyPath).publicKey.value === hex"039abec2238484d9a0680a8ffce6ad920930285fea1cbc30b8f53fad862f6e9157") + // TESTNET funder payment point + assert(keyManager.paymentPoint(funderChannelKeyPath).publicKey.value === hex"03edbfb2cd187e5738f3d4bb5b4d14dad16ee50e3deb1043155d6832d7816aa5f7") + // TESTNET funder revocation point + assert(keyManager.revocationPoint(funderChannelKeyPath).publicKey.value === hex"023c73ac122df051f8cabbefd9d1a06845aab01f0e4fed85475530ff99b73a4bfd") + // TESTNET funder delayed payment point + assert(keyManager.delayedPaymentPoint(funderChannelKeyPath).publicKey.value === hex"02168b4c51ba573a7cf2158da5338c6e852f5a62960ae2561dcc6f9e5ed6932ba5") + // TESTNET funder htlc point + assert(keyManager.htlcPoint(funderChannelKeyPath).publicKey.value === hex"03d82c30596db587d9e32ebcc68e451727c841ed27ffed441c7eb05e00bb16305d") + } + + test("test vectors derivation paths (funder MAINNET)") { + + val inputOutpoint = hex"1d12dcab62f3d509db16b8dcb69782ea6358a7060b579675561c4fc2e3294f41" + val seed = hex"0101010102020202AABBCCDD030303030404040405050505060606060707070701" + val keyManager = new LocalKeyManager(seed, Block.LivenetGenesisBlock.hash) + + val funderChannelKeyPath = LocalKeyManager.makeChannelKeyPathFunder(inputOutpoint) + + // MAINNET funder funding public key from extended private key + assert(keyManager.fundingPublicKey(funderChannelKeyPath).publicKey.value === hex"0353b7dfdb0cbae349146795bd6406ff6ac5cd93bfb31bbdfc5df01b0d4da171b4") + // MAINNET funder payment point from extended private key + assert(keyManager.paymentPoint(funderChannelKeyPath).publicKey.value === hex"0254036505ad9aa8d8e03ed3727825eaa72ce49f5ff9f2fd286e6d20a2b88caa12") + } + + test("test vectors derivation paths (fundee TESTNET)") { + + val remoteNodePubkey = PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f") + val seed = hex"0101010102020202AABBCCDD030303030404040405050505060606060707070701" + val keyManager = new LocalKeyManager(seed, Block.TestnetGenesisBlock.hash) + + val fundeePubkeyKeyPath = LocalKeyManager.makeChannelKeyPathFundeePubkey(34273, 0) + // TESTNET fundee funding public key from extended private key + assert(keyManager.fundingPublicKey(fundeePubkeyKeyPath).publicKey.value === hex"02bfe2d6e9ec07b68f588c1bcbf6afdb1e1e068ab11e82b3648eb5032007e9bb48") + + val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(keyManager.fundingPublicKey(fundeePubkeyKeyPath).publicKey, remoteNodePubkey))) + val fundeeChannelKeyPath = LocalKeyManager.makeChannelKeyPathFundee(fundingPubkeyScript) + + // TESTNET fundee htlc public point from extended private key + assert(keyManager.htlcPoint(fundeeChannelKeyPath).publicKey.value === hex"037223be02f02a117c9ad3ee243c1664d5763a57911317257536e6f355acb32418") + // TESTNET fundee htlc public point from extended private key + assert(keyManager.paymentPoint(fundeeChannelKeyPath).publicKey.value === hex"02ce4468352b0b722e7e066a3603ed9081dcc6a93137b45e1d9acbe35caad08c2e") + } + + test("test vectors derivation paths (fundee MAINNET)") { + + val remoteNodePubkey = PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f") + val seed = hex"0101010102020202AABBCCDD030303030404040405050505060606060707070701" + val keyManager = new LocalKeyManager(seed, Block.LivenetGenesisBlock.hash) + + val fundeePubkeyKeyPath = LocalKeyManager.makeChannelKeyPathFundeePubkey(34273, 0) + // MAINNET fundee funding public key from extended private key + assert(keyManager.fundingPublicKey(fundeePubkeyKeyPath).publicKey.value === hex"03b8c35b3171dcec2d1e4a2b08244b4b4ce284bd8a7dbef6da467cb6db5abdc30d") + + val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(keyManager.fundingPublicKey(fundeePubkeyKeyPath).publicKey, remoteNodePubkey))) + val fundeeChannelKeyPath = LocalKeyManager.makeChannelKeyPathFundee(fundingPubkeyScript) + + // MAINNET fundee htlc public point from extended private key + assert(keyManager.htlcPoint(fundeeChannelKeyPath).publicKey.value === hex"0258173e7db0f833691e72b259a9bf5349d9548dd9dcfccf4556a238d9d76193bf") + + // with different counter + val fundeePubkeyKeyPath1 = LocalKeyManager.makeChannelKeyPathFundeePubkey(34273, 500) + val fundingPubkeyScript1 = Script.write(Script.pay2wsh(Scripts.multiSig2of2(keyManager.fundingPublicKey(fundeePubkeyKeyPath1).publicKey, remoteNodePubkey))) + val fundeeChannelKeyPath1 = LocalKeyManager.makeChannelKeyPathFundee(fundingPubkeyScript1) + + // MAINNET fundee revocation point + assert(keyManager.revocationPoint(fundeeChannelKeyPath1).publicKey.value === hex"0213718574b4169ef093286fbc6459c81c7a672cfae62391e405aa04f82fa318e2") + // MAINNET fundee payment point + assert(keyManager.paymentPoint(fundeeChannelKeyPath1).publicKey.value === hex"026a5d2792fb7fec674e75e34a981c74bb07b7439aca8db72d800a5b7f106648be") + // MAINNET fundee delayed payment point + assert(keyManager.delayedPaymentPoint(fundeeChannelKeyPath1).publicKey.value === hex"03b289f71d00e6b363adc08cdd7eec113441ab1c6c8208621d71e70789874f48d0") + // MAINNET fundee htlc point + assert(keyManager.htlcPoint(fundeeChannelKeyPath1).publicKey.value === hex"039fd510225bd7c70a5f8f1661247c19cab9942088d7c868bad1c47bf0fad9b73d") + // MAINNED fundee shaSeed point + assert(keyManager.shaSeedPub(fundeeChannelKeyPath1).publicKey.value === hex"0200742b2176552d4e29d01455f7de2c16094c77ba644310d5d58385306c09ddc9") + } + + test("use correct keypath to compute keys") { + val seed = hex"0101010102020202AABBCCDD030303030404040405050505060606060707070701" + val keyManager = new LocalKeyManager(seed, Block.LivenetGenesisBlock.hash) + + // FUNDER + val funderParams = TestConstants.Alice.channelParams + val funderKeyPath = funderParams.channelKeyPath.left.get + + assert(keyManager.fundingPublicKey(fundingKeyPath(funderParams)).publicKey == keyManager.fundingPublicKey(funderKeyPath).publicKey) + assert(keyManager.revocationPoint(keyPath(funderParams)) == keyManager.revocationPoint(funderKeyPath)) + assert(keyManager.paymentPoint(keyPath(funderParams)).publicKey == keyManager.paymentPoint(funderKeyPath).publicKey) + assert(keyManager.delayedPaymentPoint(keyPath(funderParams)).publicKey == keyManager.delayedPaymentPoint(funderKeyPath).publicKey) + assert(keyManager.htlcPoint(keyPath(funderParams)).publicKey == keyManager.htlcPoint(funderKeyPath).publicKey) + assert(keyManager.commitmentPoint(keyPath(funderParams), 0) == keyManager.commitmentPoint(funderKeyPath, 0)) + + // FUNDEE + val fundeeParams = TestConstants.Bob.channelParams + val fundeeFundingKeyPath = fundeeParams.channelKeyPath.right.get.fundingKeyPath + val fundeeKeyPath = fundeeParams.channelKeyPath.right.get.pointsKeyPath + + assert(keyManager.fundingPublicKey(fundingKeyPath(fundeeParams)).publicKey == keyManager.fundingPublicKey(fundeeFundingKeyPath).publicKey) + assert(keyManager.revocationPoint(keyPath(fundeeParams)) == keyManager.revocationPoint(fundeeKeyPath)) + assert(keyManager.paymentPoint(keyPath(fundeeParams)).publicKey == keyManager.paymentPoint(fundeeKeyPath).publicKey) + assert(keyManager.delayedPaymentPoint(keyPath(fundeeParams)).publicKey == keyManager.delayedPaymentPoint(fundeeKeyPath).publicKey) + assert(keyManager.htlcPoint(keyPath(fundeeParams)).publicKey == keyManager.htlcPoint(fundeeKeyPath).publicKey) + assert(keyManager.commitmentPoint(keyPath(fundeeParams), 0) == keyManager.commitmentPoint(fundeeKeyPath, 0)) + } + +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala index 88052c3158..db637f5ddd 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala @@ -93,5 +93,20 @@ class SqliteChannelsDbSpec extends FunSuite { assert(getVersion(statement, "channels", 1) == 2) // version changed from 1 -> 2 } assert(db.listLocalChannels() === List(channel)) + assert(db.getCounterFor(123) === 0) } + + test("channel keypath counter should get and increment") { + + val sqlite = TestConstants.sqliteInMemory() + val channelDb = new SqliteChannelsDb(sqlite) + + assert(channelDb.getCounterFor(123) == 0) + assert(channelDb.getCounterFor(123) == 1) + assert(channelDb.getCounterFor(123) == 2) + assert(channelDb.getCounterFor(124) == 0) + assert(channelDb.getCounterFor(125) == 0) + assert(channelDb.getCounterFor(124) == 1) + } + } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala index 2e15d4ce78..9c7743a988 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala @@ -59,7 +59,7 @@ class RustyTestsSpec extends TestKit(ActorSystem("test")) with Matchers with fix val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) // alice and bob will both have 1 000 000 sat Globals.feeratesPerKw.set(FeeratesPerKw.single(10000)) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, 2000000, 1000000000, Globals.feeratesPerKw.get.blocks_2, Globals.feeratesPerKw.get.blocks_6, Alice.channelParams, pipe, bobInit, ChannelFlags.Empty) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, 2000000, 1000000000, Globals.feeratesPerKw.get.blocks_2, Globals.feeratesPerKw.get.blocks_6, pipe, bobInit, ChannelFlags.Empty) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit) pipe ! (alice, bob) within(30 seconds) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala index c5ba94558b..4b3324e821 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala @@ -21,9 +21,11 @@ import java.util.UUID import akka.actor.ActorSystem import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.DeterministicWallet.KeyPath +import fr.acinq.bitcoin.{Crypto, DeterministicWallet, OutPoint} import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, DeterministicWallet, MilliSatoshi, OutPoint, Satoshi, Transaction} import fr.acinq.eclair._ import fr.acinq.eclair.api.JsonSupport +import fr.acinq.eclair.channel.ChannelVersion.STANDARD import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.{LocalKeyManager, ShaChain, Sphinx} @@ -80,9 +82,9 @@ class ChannelCodecsSpec extends FunSuite { } test("encode/decode localparams") { - val o = LocalParams( + val localParamFundee = LocalParams( nodeId = randomKey.publicKey, - channelKeyPath = DeterministicWallet.KeyPath(Seq(42L)), + channelKeyPath = Right(KeyPathFundee(KeyPath(Seq(4, 3, 2, 1L)), KeyPath(Seq(1, 2, 3, 4L)))), dustLimitSatoshis = Random.nextInt(Int.MaxValue), maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)), channelReserveSatoshis = Random.nextInt(Int.MaxValue), @@ -90,12 +92,29 @@ class ChannelCodecsSpec extends FunSuite { toSelfDelay = Random.nextInt(Short.MaxValue), maxAcceptedHtlcs = Random.nextInt(Short.MaxValue), defaultFinalScriptPubKey = randomBytes(10 + Random.nextInt(200)), - isFunder = Random.nextBoolean(), globalFeatures = randomBytes(256), localFeatures = randomBytes(256)) - val encoded = localParamsCodec.encode(o).require - val decoded = localParamsCodec.decode(encoded).require - assert(o === decoded.value) + + val encoded = localParamsCodec(ChannelVersion.DETERMINISTIC_KEYPATH).encode(localParamFundee).require + val decoded = localParamsCodec(ChannelVersion.DETERMINISTIC_KEYPATH).decode(encoded).require + assert(localParamFundee === decoded.value) + + val localParamFunder = LocalParams( + nodeId = randomKey.publicKey, + channelKeyPath = Left(KeyPath(Seq(4, 3, 2, 1L))), + dustLimitSatoshis = Random.nextInt(Int.MaxValue), + maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)), + channelReserveSatoshis = Random.nextInt(Int.MaxValue), + htlcMinimumMsat = Random.nextInt(Int.MaxValue), + toSelfDelay = Random.nextInt(Short.MaxValue), + maxAcceptedHtlcs = Random.nextInt(Short.MaxValue), + defaultFinalScriptPubKey = randomBytes(10 + Random.nextInt(200)), + globalFeatures = randomBytes(256), + localFeatures = randomBytes(256)) + + val encoded1 = localParamsCodec(ChannelVersion.DETERMINISTIC_KEYPATH).encode(localParamFunder).require + val decoded1 = localParamsCodec(ChannelVersion.DETERMINISTIC_KEYPATH).decode(encoded1).require + assert(localParamFunder === decoded1.value) } test("encode/decode remoteparams") { @@ -200,6 +219,8 @@ class ChannelCodecsSpec extends FunSuite { test("basic serialization test (NORMAL)") { val data = normal val bin = ChannelCodecs.DATA_NORMAL_Codec.encode(data).require + assert(data.commitments.channelVersion == STANDARD) + assert(data.commitments.localParams.channelKeyPath.isLeft) val check = ChannelCodecs.DATA_NORMAL_Codec.decodeValue(bin).require assert(data.commitments.localCommit.spec === check.commitments.localCommit.spec) assert(data === check) @@ -311,11 +332,11 @@ class ChannelCodecsSpec extends FunSuite { val newbin = stateDataCodec.encode(oldnormal).require.bytes // and we decode with the new codec val newnormal = stateDataCodec.decode(newbin.bits).require.value - // finally we check that the actual data is the same as before (we just remove the new json field) - val oldjson = Serialization.write(oldnormal)(JsonSupport.formats).replace(""","unknownFields":""""", "").replace(""""channelVersion":"00000000000000000000000000000000",""", "") - val newjson = Serialization.write(newnormal)(JsonSupport.formats).replace(""","unknownFields":""""", "").replace(""""channelVersion":"00000000000000000000000000000000",""", "") - assert(oldjson === refjson) - assert(newjson === refjson) + // finally we check that the actual data is the same as before + val oldjson = Serialization.write(oldnormal)(JsonSupport.formats) + val newjson = Serialization.write(newnormal)(JsonSupport.formats) + + assert(oldjson === newjson) } } @@ -326,7 +347,7 @@ object ChannelCodecsSpec { val keyManager = new LocalKeyManager(ByteVector32(ByteVector.fill(32)(1)), Block.RegtestGenesisBlock.hash) val localParams = LocalParams( keyManager.nodeId, - channelKeyPath = DeterministicWallet.KeyPath(Seq(42L)), + channelKeyPath = Left(DeterministicWallet.KeyPath(Seq(42L))), dustLimitSatoshis = Satoshi(546).toLong, maxHtlcValueInFlightMsat = UInt64(50000000), channelReserveSatoshis = 10000, @@ -334,7 +355,6 @@ object ChannelCodecsSpec { toSelfDelay = 144, maxAcceptedHtlcs = 50, defaultFinalScriptPubKey = ByteVector.empty, - isFunder = true, globalFeatures = hex"dead", localFeatures = hex"beef") @@ -372,7 +392,7 @@ object ChannelCodecsSpec { val fundingTx = Transaction.read("0200000001adbb20ea41a8423ea937e76e8151636bf6093b70eaff942930d20576600521fd000000006b48304502210090587b6201e166ad6af0227d3036a9454223d49a1f11839c1a362184340ef0240220577f7cd5cca78719405cbf1de7414ac027f0239ef6e214c90fcaab0454d84b3b012103535b32d5eb0a6ed0982a0479bbadc9868d9836f6ba94dd5a63be16d875069184ffffffff028096980000000000220020c015c4a6be010e21657068fc2e6a9d02b27ebe4d490a25846f7237f104d1a3cd20256d29010000001600143ca33c2e4446f4a305f23c80df8ad1afdcf652f900000000") val fundingAmount = fundingTx.txOut(0).amount - val commitmentInput = Funding.makeFundingInputInfo(fundingTx.hash, 0, fundingAmount, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey) + val commitmentInput = Funding.makeFundingInputInfo(fundingTx.hash, 0, fundingAmount, keyManager.fundingPublicKey(localParams.channelKeyPath.left.get).publicKey, remoteParams.fundingPubKey) val localCommit = LocalCommit(0, CommitmentSpec(htlcs.toSet, 1500, 50000000, 70000000), PublishableTxs(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), Nil)) val remoteCommit = RemoteCommit(0, CommitmentSpec(htlcs.map(htlc => htlc.copy(direction = htlc.direction.opposite)).toSet, 1500, 50000, 700000), ByteVector32(hex"0303030303030303030303030303030303030303030303030303030303030303"), PrivateKey(ByteVector.fill(32)(4)).publicKey) diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala index d63969c9b0..d5916dfb4e 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala @@ -41,7 +41,7 @@ import scala.collection.JavaConversions._ */ class GUIUpdater(mainController: MainController) extends Actor with ActorLogging { - val STATE_MUTUAL_CLOSE = Set(WAIT_FOR_INIT_INTERNAL, WAIT_FOR_OPEN_CHANNEL, WAIT_FOR_ACCEPT_CHANNEL, WAIT_FOR_FUNDING_INTERNAL, WAIT_FOR_FUNDING_CREATED, WAIT_FOR_FUNDING_SIGNED, NORMAL) + val STATE_MUTUAL_CLOSE = Set(WAIT_FOR_INIT_INTERNAL, WAIT_FOR_OPEN_CHANNEL, WAIT_FOR_ACCEPT_CHANNEL, WAIT_FOR_FUNDING_INTERNAL_SIGNED, WAIT_FOR_FUNDING_CREATED, WAIT_FOR_FUNDING_SIGNED, NORMAL) val STATE_FORCE_CLOSE = Set(WAIT_FOR_FUNDING_CONFIRMED, WAIT_FOR_FUNDING_LOCKED, NORMAL, SHUTDOWN, NEGOTIATING, OFFLINE, SYNCING) /**