1
0
mirror of https://github.com/ACINQ/eclair.git synced 2024-11-19 18:10:42 +01:00

Channels data format migration (#1849)

There are three otherwise unrelated changes, that we group together to only have one migration:
- remove local signatures for local commitments (this PR)
- separate internal channel config from channel features (#1848)
- upfront shutdown script (#1846)

We increase database version number in sqlite and postgres to force a full data migration.

The goal of removing local signatures from the channel data is that even if the node database or
a backup is compromised, the attacker won't be able to force close channels from the outside.
This commit is contained in:
Pierre-Marie Padiou 2021-07-15 16:25:29 +02:00 committed by GitHub
parent 547d7e700f
commit e9df4eece0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 1741 additions and 861 deletions

View File

@ -127,7 +127,7 @@ object CheckBalance {
def computeRemoteCloseBalance(c: Commitments, r: RemoteClose, knownPreimages: Set[(ByteVector32, Long)]): PossiblyPublishedMainAndHtlcBalance = {
import r._
val toLocal = if (c.channelVersion.paysDirectlyToWallet) {
val toLocal = if (c.channelFeatures.paysDirectlyToWallet) {
// If static remote key is enabled, the commit tx directly pays to our wallet
// We use the pubkeyscript to retrieve our output
Transactions.findPubKeyScriptIndex(remoteCommitPublished.commitTx, c.localParams.defaultFinalScriptPubKey) match {

View File

@ -18,8 +18,9 @@ package fr.acinq.eclair.blockchain.fee
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.Satoshi
import fr.acinq.eclair.Features
import fr.acinq.eclair.blockchain.CurrentFeerates
import fr.acinq.eclair.channel.ChannelVersion
import fr.acinq.eclair.channel.ChannelFeatures
trait FeeEstimator {
// @formatter:off
@ -32,13 +33,13 @@ case class FeeTargets(fundingBlockTarget: Int, commitmentBlockTarget: Int, mutua
case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMaxCommitFeerate: FeeratePerKw) {
/**
* @param channelVersion channel version
* @param channelFeatures permanent channel features
* @param networkFeerate reference fee rate (value we estimate from our view of the network)
* @param proposedFeerate fee rate proposed (new proposal through update_fee or previous proposal used in our current commit tx)
* @return true if the difference between proposed and reference fee rates is too high.
*/
def isFeeDiffTooHigh(channelVersion: ChannelVersion, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = {
if (channelVersion.hasAnchorOutputs) {
def isFeeDiffTooHigh(channelFeatures: ChannelFeatures, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = {
if (channelFeatures.hasFeature(Features.AnchorOutputs)) {
proposedFeerate < networkFeerate * ratioLow || anchorOutputMaxCommitFeerate * ratioHigh < proposedFeerate
} else {
proposedFeerate < networkFeerate * ratioLow || networkFeerate * ratioHigh < proposedFeerate
@ -60,15 +61,15 @@ case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, cl
* - otherwise we use a feerate that should get the commit tx confirmed within the configured block target
*
* @param remoteNodeId nodeId of our channel peer
* @param channelVersion channel version
* @param channelFeatures permanent channel features
* @param currentFeerates_opt if provided, will be used to compute the most up-to-date network fee, otherwise we rely on the fee estimator
*/
def getCommitmentFeerate(remoteNodeId: PublicKey, channelVersion: ChannelVersion, channelCapacity: Satoshi, currentFeerates_opt: Option[CurrentFeerates]): FeeratePerKw = {
def getCommitmentFeerate(remoteNodeId: PublicKey, channelFeatures: ChannelFeatures, channelCapacity: Satoshi, currentFeerates_opt: Option[CurrentFeerates]): FeeratePerKw = {
val networkFeerate = currentFeerates_opt match {
case Some(currentFeerates) => currentFeerates.feeratesPerKw.feePerBlock(feeTargets.commitmentBlockTarget)
case None => feeEstimator.getFeeratePerKw(feeTargets.commitmentBlockTarget)
}
if (channelVersion.hasAnchorOutputs) {
if (channelFeatures.hasFeature(Features.AnchorOutputs)) {
networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate)
} else {
networkFeerate

View File

@ -195,12 +195,12 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
startWith(WAIT_FOR_INIT_INTERNAL, Nothing)
when(WAIT_FOR_INIT_INTERNAL)(handleExceptions {
case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, _, localParams, remote, _, channelFlags, channelVersion), Nothing) =>
case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, _, localParams, remote, _, channelFlags, channelConfig, _), Nothing) =>
context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = true, temporaryChannelId, initialFeeratePerKw, Some(fundingTxFeeratePerKw)))
activeConnection = remote
txPublisher ! SetChannelId(remoteNodeId, temporaryChannelId)
val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey
val channelKeyPath = keyManager.keyPath(localParams, channelVersion)
val channelKeyPath = keyManager.keyPath(localParams, channelConfig)
val open = OpenChannel(nodeParams.chainHash,
temporaryChannelId = temporaryChannelId,
fundingSatoshis = fundingSatoshis,
@ -224,7 +224,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.empty)))
goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder, open) sending open
case Event(inputFundee@INPUT_INIT_FUNDEE(_, localParams, remote, _, _), Nothing) if !localParams.isFunder =>
case Event(inputFundee@INPUT_INIT_FUNDEE(_, localParams, remote, _, _, _), Nothing) if !localParams.isFunder =>
activeConnection = remote
txPublisher ! SetChannelId(remoteNodeId, inputFundee.temporaryChannelId)
goto(WAIT_FOR_OPEN_CHANNEL) using DATA_WAIT_FOR_OPEN_CHANNEL(inputFundee)
@ -337,14 +337,14 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
})
when(WAIT_FOR_OPEN_CHANNEL)(handleExceptions {
case Event(open: OpenChannel, d@DATA_WAIT_FOR_OPEN_CHANNEL(INPUT_INIT_FUNDEE(_, localParams, _, remoteInit, channelVersion))) =>
case Event(open: OpenChannel, d@DATA_WAIT_FOR_OPEN_CHANNEL(INPUT_INIT_FUNDEE(_, localParams, _, remoteInit, channelConfig, channelFeatures))) =>
log.info("received OpenChannel={}", open)
Helpers.validateParamsFundee(nodeParams, localParams.features, channelVersion, open, remoteNodeId) match {
Helpers.validateParamsFundee(nodeParams, localParams.initFeatures, channelFeatures, open, remoteNodeId) match {
case Left(t) => handleLocalError(t, d, Some(open))
case _ =>
context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = false, open.temporaryChannelId, open.feeratePerKw, None))
val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey
val channelKeyPath = keyManager.keyPath(localParams, channelVersion)
val channelKeyPath = keyManager.keyPath(localParams, channelConfig)
val minimumDepth = Helpers.minDepthForFunding(nodeParams, open.fundingSatoshis)
val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId,
dustLimitSatoshis = localParams.dustLimit,
@ -376,9 +376,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
paymentBasepoint = open.paymentBasepoint,
delayedPaymentBasepoint = open.delayedPaymentBasepoint,
htlcBasepoint = open.htlcBasepoint,
features = remoteInit.features)
initFeatures = remoteInit.features,
shutdownScript = None)
log.debug("remote params: {}", remoteParams)
goto(WAIT_FOR_FUNDING_CREATED) using DATA_WAIT_FOR_FUNDING_CREATED(open.temporaryChannelId, localParams, remoteParams, open.fundingSatoshis, open.pushMsat, open.feeratePerKw, None, open.firstPerCommitmentPoint, open.channelFlags, channelVersion, accept) sending accept
goto(WAIT_FOR_FUNDING_CREATED) using DATA_WAIT_FOR_FUNDING_CREATED(open.temporaryChannelId, localParams, remoteParams, open.fundingSatoshis, open.pushMsat, open.feeratePerKw, None, open.firstPerCommitmentPoint, open.channelFlags, channelConfig, channelFeatures, accept) sending accept
}
case Event(c: CloseCommand, d) => handleFastClose(c, d.channelId)
@ -389,7 +390,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
})
when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions {
case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, initialRelayFees_opt, localParams, _, remoteInit, _, channelVersion), open)) =>
case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, initialRelayFees_opt, localParams, _, remoteInit, _, channelConfig, channelFeatures), open)) =>
log.info(s"received AcceptChannel=$accept")
Helpers.validateParamsFunder(nodeParams, open, accept) match {
case Left(t) => handleLocalError(t, d, Some(accept))
@ -407,12 +408,13 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
paymentBasepoint = accept.paymentBasepoint,
delayedPaymentBasepoint = accept.delayedPaymentBasepoint,
htlcBasepoint = accept.htlcBasepoint,
features = remoteInit.features)
initFeatures = remoteInit.features,
shutdownScript = None)
log.debug("remote params: {}", remoteParams)
val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath)
val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey)))
wallet.makeFundingTx(fundingPubkeyScript, fundingSatoshis, fundingTxFeeratePerKw).pipeTo(self)
goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, initialRelayFees_opt, accept.firstPerCommitmentPoint, channelVersion, open)
goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, initialRelayFees_opt, accept.firstPerCommitmentPoint, channelConfig, channelFeatures, open)
}
case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) =>
@ -433,13 +435,13 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
})
when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions {
case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), d@DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, initialRelayFees_opt, remoteFirstPerCommitmentPoint, channelVersion, open)) =>
case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), d@DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, initialRelayFees_opt, remoteFirstPerCommitmentPoint, channelConfig, channelFeatures, open)) =>
// let's create the first commitment tx that spends the yet uncommitted funding tx
Funding.makeFirstCommitTxs(keyManager, channelVersion, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match {
Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match {
case Left(ex) => handleLocalError(ex, d, None)
case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) =>
require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!")
val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.fundingKeyPath), TxOwner.Remote, channelVersion.commitmentFormat)
val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.fundingKeyPath), TxOwner.Remote, channelFeatures.commitmentFormat)
// signature of their initial commitment tx that pays remote pushMsat
val fundingCreated = FundingCreated(
temporaryChannelId = temporaryChannelId,
@ -452,7 +454,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
txPublisher ! SetChannelId(remoteNodeId, channelId)
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, initialRelayFees_opt, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), open.channelFlags, channelVersion, fundingCreated) sending fundingCreated
goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, fundingTxFee, initialRelayFees_opt, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), open.channelFlags, channelConfig, channelFeatures, fundingCreated) sending fundingCreated
}
case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) =>
@ -478,19 +480,19 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
})
when(WAIT_FOR_FUNDING_CREATED)(handleExceptions {
case Event(FundingCreated(_, fundingTxHash, fundingTxOutputIndex, remoteSig), d@DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, initialRelayFees_opt, remoteFirstPerCommitmentPoint, channelFlags, channelVersion, _)) =>
case Event(FundingCreated(_, fundingTxHash, fundingTxOutputIndex, remoteSig), d@DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, initialRelayFees_opt, remoteFirstPerCommitmentPoint, channelFlags, channelConfig, channelFeatures, _)) =>
// they fund the channel with their funding tx, so the money is theirs (but we are paid pushMsat)
Funding.makeFirstCommitTxs(keyManager, channelVersion, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match {
Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match {
case Left(ex) => handleLocalError(ex, d, None)
case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) =>
// check remote signature validity
val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath)
val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, channelVersion.commitmentFormat)
val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, channelFeatures.commitmentFormat)
val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig)
Transactions.checkSpendable(signedLocalCommitTx) match {
case Failure(_) => handleLocalError(InvalidCommitmentSignature(temporaryChannelId, signedLocalCommitTx.tx), d, None)
case Success(_) =>
val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, channelVersion.commitmentFormat)
val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, channelFeatures.commitmentFormat)
val channelId = toLongId(fundingTxHash, fundingTxOutputIndex)
// watch the funding tx transaction
val commitInput = localCommitTx.input
@ -498,21 +500,21 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
channelId = channelId,
signature = localSigOfRemoteTx
)
val commitments = Commitments(channelVersion, localParams, remoteParams, channelFlags,
LocalCommit(0, localSpec, PublishableTxs(signedLocalCommitTx, Nil)), RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint),
val commitments = Commitments(channelId, channelConfig, channelFeatures, localParams, remoteParams, channelFlags,
LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, remoteSig), htlcTxsAndRemoteSigs = Nil), RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint),
LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil),
localNextHtlcId = 0L, remoteNextHtlcId = 0L,
originChannels = Map.empty,
remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array,
commitInput, ShaChain.init, channelId = channelId)
commitInput, ShaChain.init)
peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages
txPublisher ! SetChannelId(remoteNodeId, channelId)
context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId))
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments))
// NB: we don't send a ChannelSignatureSent for the first commit
log.info(s"waiting for them to publish the funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}")
val fundingMinDepth = Helpers.minDepthForFunding(nodeParams, fundingAmount)
watchFundingTx(commitments)
val fundingMinDepth = Helpers.minDepthForFunding(nodeParams, fundingAmount)
blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, fundingMinDepth)
goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, None, initialRelayFees_opt, nodeParams.currentBlockHeight, None, Right(fundingSigned)) storing() sending fundingSigned
}
@ -528,10 +530,10 @@ 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, initialRelayFees_opt, localSpec, localCommitTx, remoteCommit, channelFlags, channelVersion, fundingCreated)) =>
case Event(msg@FundingSigned(_, remoteSig), d@DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, fundingTxFee, initialRelayFees_opt, localSpec, localCommitTx, remoteCommit, channelFlags, channelConfig, channelFeatures, fundingCreated)) =>
// we make sure that their sig checks out and that our first commit tx is spendable
val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath)
val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, channelVersion.commitmentFormat)
val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, channelFeatures.commitmentFormat)
val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig)
Transactions.checkSpendable(signedLocalCommitTx) match {
case Failure(cause) =>
@ -541,13 +543,13 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
handleLocalError(InvalidCommitmentSignature(channelId, signedLocalCommitTx.tx), d, Some(msg))
case Success(_) =>
val commitInput = localCommitTx.input
val commitments = Commitments(channelVersion, localParams, remoteParams, channelFlags,
LocalCommit(0, localSpec, PublishableTxs(signedLocalCommitTx, Nil)), remoteCommit,
val commitments = Commitments(channelId, channelConfig, channelFeatures, localParams, remoteParams, channelFlags,
LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, remoteSig), htlcTxsAndRemoteSigs = Nil), remoteCommit,
LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil),
localNextHtlcId = 0L, remoteNextHtlcId = 0L,
originChannels = Map.empty,
remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array
commitInput, ShaChain.init, channelId = channelId)
commitInput, ShaChain.init)
val now = System.currentTimeMillis.milliseconds.toSeconds
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments))
log.info(s"publishing funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}")
@ -606,11 +608,11 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
stay using d.copy(deferred = Some(msg)) // no need to store, they will re-send if we get disconnected
case Event(WatchFundingConfirmedTriggered(blockHeight, txIndex, fundingTx), d@DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, _, initialRelayFees_opt, _, deferred, _)) =>
Try(Transaction.correctlySpends(commitments.localCommit.publishableTxs.commitTx.tx, Seq(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) match {
Try(Transaction.correctlySpends(commitments.fullySignedLocalCommitTx(keyManager).tx, Seq(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) match {
case Success(_) =>
log.info(s"channelId=${commitments.channelId} was confirmed at blockHeight=$blockHeight txIndex=$txIndex")
blockchain ! WatchFundingLost(self, commitments.commitInput.outPoint.txid, nodeParams.minDepthBlocks)
val channelKeyPath = keyManager.keyPath(d.commitments.localParams, commitments.channelVersion)
val channelKeyPath = keyManager.keyPath(d.commitments.localParams, commitments.channelConfig)
val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1)
val fundingLocked = FundingLocked(commitments.channelId, nextPerCommitmentPoint)
deferred.foreach(self ! _)
@ -868,7 +870,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
case Event(c: CMD_CLOSE, d: DATA_NORMAL) =>
val localScriptPubKey = c.scriptPubKey.getOrElse(d.commitments.localParams.defaultFinalScriptPubKey)
val allowAnySegwit = Features.canUseFeature(d.commitments.localParams.features, d.commitments.remoteParams.features, Features.ShutdownAnySegwit)
val allowAnySegwit = Features.canUseFeature(d.commitments.localParams.initFeatures, d.commitments.remoteParams.initFeatures, Features.ShutdownAnySegwit)
if (d.localShutdown.isDefined) {
handleCommandError(ClosingAlreadyInProgress(d.channelId), c)
} else if (Commitments.localHasUnsignedOutgoingHtlcs(d.commitments)) {
@ -898,7 +900,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
// we did not send a shutdown message
// there are pending signed changes => go to SHUTDOWN
// there are no htlcs => go to NEGOTIATING
val allowAnySegwit = Features.canUseFeature(d.commitments.localParams.features, d.commitments.remoteParams.features, Features.ShutdownAnySegwit)
val allowAnySegwit = Features.canUseFeature(d.commitments.localParams.initFeatures, d.commitments.remoteParams.initFeatures, Features.ShutdownAnySegwit)
if (!Closing.isValidFinalScriptPubkey(remoteScriptPubKey, allowAnySegwit)) {
handleLocalError(InvalidFinalScript(d.channelId), d, Some(remoteShutdown))
} else if (Commitments.remoteHasUnsignedOutgoingHtlcs(d.commitments)) {
@ -1504,7 +1506,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
activeConnection = r
val yourLastPerCommitmentSecret = d.commitments.remotePerCommitmentSecrets.lastIndex.flatMap(d.commitments.remotePerCommitmentSecrets.getHash).getOrElse(ByteVector32.Zeroes)
val channelKeyPath = keyManager.keyPath(d.commitments.localParams, d.commitments.channelVersion)
val channelKeyPath = keyManager.keyPath(d.commitments.localParams, d.commitments.channelConfig)
val myCurrentPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, d.commitments.localCommit.index)
val channelReestablish = ChannelReestablish(
@ -1576,14 +1578,14 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_LOCKED) =>
log.debug("re-sending fundingLocked")
val channelKeyPath = keyManager.keyPath(d.commitments.localParams, d.commitments.channelVersion)
val channelKeyPath = keyManager.keyPath(d.commitments.localParams, d.commitments.channelConfig)
val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1)
val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint)
goto(WAIT_FOR_FUNDING_LOCKED) sending fundingLocked
case Event(channelReestablish: ChannelReestablish, d: DATA_NORMAL) =>
var sendQueue = Queue.empty[LightningMessage]
val channelKeyPath = keyManager.keyPath(d.commitments.localParams, d.commitments.channelVersion)
val channelKeyPath = keyManager.keyPath(d.commitments.localParams, d.commitments.channelConfig)
channelReestablish match {
case ChannelReestablish(_, _, nextRemoteRevocationNumber, yourLastPerCommitmentSecret, _) if !Helpers.checkLocalCommit(d, nextRemoteRevocationNumber) =>
// if next_remote_revocation_number is greater than our local commitment index, it means that either we are using an outdated commitment, or they are lying
@ -1664,7 +1666,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
val shutdownInProgress = d.localShutdown.nonEmpty || d.remoteShutdown.nonEmpty
if (d.commitments.localParams.isFunder && !shutdownInProgress) {
val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelVersion, d.commitments.capacity, None)
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelFeatures, d.commitments.capacity, None)
if (nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw)) {
self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true)
}
@ -1820,7 +1822,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
// peer doesn't cancel the timer
case Event(TickChannelOpenTimeout, _) => stay
case Event(WatchFundingSpentTriggered(tx), d: HasCommitments) if tx.txid == d.commitments.localCommit.publishableTxs.commitTx.tx.txid =>
case Event(WatchFundingSpentTriggered(tx), d: HasCommitments) if tx.txid == d.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid =>
log.warning(s"processing local commit spent in catch-all handler")
spendLocalCurrent(d)
}
@ -1936,11 +1938,11 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
}
private def handleCurrentFeerate(c: CurrentFeerates, d: HasCommitments) = {
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelVersion, d.commitments.capacity, Some(c))
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelFeatures, d.commitments.capacity, Some(c))
val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw
val shouldUpdateFee = d.commitments.localParams.isFunder && nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw)
val shouldClose = !d.commitments.localParams.isFunder &&
nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isFeeDiffTooHigh(d.commitments.channelVersion, networkFeeratePerKw, currentFeeratePerKw) &&
nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isFeeDiffTooHigh(d.commitments.channelFeatures, networkFeeratePerKw, currentFeeratePerKw) &&
d.commitments.hasPendingOrProposedHtlcs // we close only if we have HTLCs potentially at risk
if (shouldUpdateFee) {
self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true)
@ -1960,11 +1962,11 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
* @return
*/
private def handleOfflineFeerate(c: CurrentFeerates, d: HasCommitments) = {
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelVersion, d.commitments.capacity, Some(c))
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelFeatures, d.commitments.capacity, Some(c))
val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw
// if the network fees are too high we risk to not be able to confirm our current commitment
val shouldClose = networkFeeratePerKw > currentFeeratePerKw &&
nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isFeeDiffTooHigh(d.commitments.channelVersion, networkFeeratePerKw, currentFeeratePerKw) &&
nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isFeeDiffTooHigh(d.commitments.channelFeatures, networkFeeratePerKw, currentFeeratePerKw) &&
d.commitments.hasPendingOrProposedHtlcs // we close only if we have HTLCs potentially at risk
if (shouldClose) {
if (nodeParams.onChainFeeConf.closeOnOfflineMismatch) {
@ -2017,7 +2019,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
private def watchFundingTx(commitments: Commitments, additionalKnownSpendingTxs: Set[ByteVector32] = Set.empty): Unit = {
// TODO: should we wait for an acknowledgment from the watcher?
val knownSpendingTxs = Set(commitments.localCommit.publishableTxs.commitTx.tx.txid, commitments.remoteCommit.txid) ++ commitments.remoteNextCommitInfo.left.toSeq.map(_.nextRemoteCommit.txid).toSet ++ additionalKnownSpendingTxs
val knownSpendingTxs = Set(commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid, commitments.remoteCommit.txid) ++ commitments.remoteNextCommitInfo.left.toSeq.map(_.nextRemoteCommit.txid).toSet ++ additionalKnownSpendingTxs
blockchain ! WatchFundingSpent(self, commitments.commitInput.outPoint.txid, commitments.commitInput.outPoint.index.toInt, knownSpendingTxs)
// TODO: implement this? (not needed if we use a reasonable min_depth)
//blockchain ! WatchLost(self, commitments.commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_LOST)
@ -2215,7 +2217,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
log.warning("we have an outdated commitment: will not publish our local tx")
stay
} else {
val commitTx = d.commitments.localCommit.publishableTxs.commitTx.tx
val commitTx = d.commitments.fullySignedLocalCommitTx(keyManager).tx
val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)
val nextData = d match {
case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished))
@ -2302,8 +2304,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
private def handleRemoteSpentFuture(commitTx: Transaction, d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) = {
log.warning(s"they published their future commit (because we asked them to) in txid=${commitTx.txid}")
d.commitments.channelVersion match {
case v if v.paysDirectlyToWallet =>
d.commitments.channelFeatures match {
case ct if ct.paysDirectlyToWallet =>
val remoteCommitPublished = RemoteCommitPublished(commitTx, None, Map.empty, List.empty, Map.empty)
val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSinceBlock = nodeParams.currentBlockHeight, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished))
goto(CLOSING) using nextData storing() // we don't need to claim our main output in the remote commit because it already spends to our wallet address
@ -2394,7 +2396,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
val error = Error(d.channelId, exc.getMessage)
// let's try to spend our current local tx
val commitTx = d.commitments.localCommit.publishableTxs.commitTx.tx
val commitTx = d.commitments.fullySignedLocalCommitTx(keyManager).tx
val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)
goto(ERR_INFORMATION_LEAK) calling doPublish(localCommitPublished, d.commitments) sending error
@ -2420,7 +2422,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
} else if (commitments1.localCommit.index == channelReestablish.nextRemoteRevocationNumber + 1) {
// our last revocation got lost, let's resend it
log.debug("re-sending last revocation")
val channelKeyPath = keyManager.keyPath(d.commitments.localParams, d.commitments.channelVersion)
val channelKeyPath = keyManager.keyPath(d.commitments.localParams, d.commitments.channelConfig)
val localPerCommitmentSecret = keyManager.commitmentSecret(channelKeyPath, d.commitments.localCommit.index - 1)
val localNextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, d.commitments.localCommit.index + 1)
val revocation = RevokeAndAck(

View File

@ -0,0 +1,58 @@
/*
* Copyright 2021 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
/**
* Created by t-bast on 24/06/2021.
*/
/**
* Internal configuration option impacting the channel's structure or behavior.
* This must be set when creating the channel and cannot be changed afterwards.
*/
trait ChannelConfigOption {
// @formatter:off
def supportBit: Int
def name: String
// @formatter:on
}
case class ChannelConfig(options: Set[ChannelConfigOption]) {
def hasOption(option: ChannelConfigOption): Boolean = options.contains(option)
}
object ChannelConfig {
val standard: ChannelConfig = ChannelConfig(options = Set(FundingPubKeyBasedChannelKeyPath))
def apply(opts: ChannelConfigOption*): ChannelConfig = ChannelConfig(Set.from(opts))
/**
* If set, the channel's BIP32 key path will be deterministically derived from the funding public key.
* It makes it very easy to retrieve funds when channel data has been lost:
* - connect to your peer and use option_data_loss_protect to get them to publish their remote commit tx
* - retrieve the commit tx from the bitcoin network, extract your funding pubkey from its witness data
* - recompute your channel keys and spend your output
*/
case object FundingPubKeyBasedChannelKeyPath extends ChannelConfigOption {
override val supportBit: Int = 0
override val name: String = "funding_pubkey_based_channel_keypath"
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright 2021 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
import fr.acinq.eclair.Features.{AnchorOutputs, StaticRemoteKey, Wumbo}
import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat}
import fr.acinq.eclair.{Feature, Features}
/**
* Created by t-bast on 24/06/2021.
*/
/**
* Subset of Bolt 9 features used to configure a channel and applicable over the lifetime of that channel.
* Even if one of these features is later disabled at the connection level, it will still apply to the channel until the
* channel is upgraded or closed.
*/
case class ChannelFeatures(activated: Set[Feature]) {
/** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */
val paysDirectlyToWallet: Boolean = {
hasFeature(Features.StaticRemoteKey) && !hasFeature(Features.AnchorOutputs)
}
/** Format of the channel transactions. */
val commitmentFormat: CommitmentFormat = {
if (hasFeature(AnchorOutputs)) {
AnchorOutputsCommitmentFormat
} else {
DefaultCommitmentFormat
}
}
def hasFeature(feature: Feature): Boolean = activated.contains(feature)
override def toString: String = activated.mkString(",")
}
object ChannelFeatures {
def apply(features: Feature*): ChannelFeatures = ChannelFeatures(Set.from(features))
/** Pick the channel features that should be used based on local and remote feature bits. */
def pickChannelFeatures(localFeatures: Features, remoteFeatures: Features): ChannelFeatures = {
// NB: we don't include features that can be safely activated/deactivated without impacting the channel's operation,
// such as option_dataloss_protect or option_shutdown_anysegwit.
val availableFeatures = Set[Feature](
StaticRemoteKey,
Wumbo,
AnchorOutputs,
).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f))
ChannelFeatures(availableFeatures)
}
}

View File

@ -26,7 +26,7 @@ import fr.acinq.eclair.transactions.CommitmentSpec
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc}
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, UInt64}
import scodec.bits.{BitVector, ByteVector}
import scodec.bits.ByteVector
import java.util.UUID
@ -87,8 +87,14 @@ case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32,
remote: ActorRef,
remoteInit: Init,
channelFlags: Byte,
channelVersion: ChannelVersion)
case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32, localParams: LocalParams, remote: ActorRef, remoteInit: Init, channelVersion: ChannelVersion)
channelConfig: ChannelConfig,
channelFeatures: ChannelFeatures)
case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32,
localParams: LocalParams,
remote: ActorRef,
remoteInit: Init,
channelConfig: ChannelConfig,
channelFeatures: ChannelFeatures)
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
case class INPUT_RECONNECTED(remote: ActorRef, localInit: Init, remoteInit: Init)
@ -375,7 +381,8 @@ final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: ByteVector32
initialFeeratePerKw: FeeratePerKw,
initialRelayFees_opt: Option[(MilliSatoshi, Int)],
remoteFirstPerCommitmentPoint: PublicKey,
channelVersion: ChannelVersion,
channelConfig: ChannelConfig,
channelFeatures: ChannelFeatures,
lastSent: OpenChannel) extends Data {
val channelId: ByteVector32 = temporaryChannelId
}
@ -388,7 +395,8 @@ final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: ByteVector32,
initialRelayFees_opt: Option[(MilliSatoshi, Int)],
remoteFirstPerCommitmentPoint: PublicKey,
channelFlags: Byte,
channelVersion: ChannelVersion,
channelConfig: ChannelConfig,
channelFeatures: ChannelFeatures,
lastSent: AcceptChannel) extends Data {
val channelId: ByteVector32 = temporaryChannelId
}
@ -402,7 +410,8 @@ final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: ByteVector32,
localCommitTx: CommitTx,
remoteCommit: RemoteCommit,
channelFlags: Byte,
channelVersion: ChannelVersion,
channelConfig: ChannelConfig,
channelFeatures: ChannelFeatures,
lastSent: FundingCreated) extends Data
final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments,
fundingTx: Option[Transaction],
@ -444,9 +453,9 @@ final case class DATA_CLOSING(commitments: Commitments,
final case class DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(commitments: Commitments, remoteChannelReestablish: ChannelReestablish) extends Data with HasCommitments
/**
* @param features current connection features, or last features used if the channel is disconnected. Note that these
* features are updated at each reconnection and may be different from the ones that were used when the
* channel was created. See [[ChannelVersion]] for permanent features associated to a channel.
* @param initFeatures current connection features, or last features used if the channel is disconnected. Note that these
* features are updated at each reconnection and may be different from the channel permanent features
* (see [[ChannelFeatures]]).
*/
final case class LocalParams(nodeId: PublicKey,
fundingKeyPath: DeterministicWallet.KeyPath,
@ -459,10 +468,10 @@ final case class LocalParams(nodeId: PublicKey,
isFunder: Boolean,
defaultFinalScriptPubKey: ByteVector,
walletStaticPaymentBasepoint: Option[PublicKey],
features: Features)
initFeatures: Features)
/**
* @param features see [[LocalParams.features]]
* @param initFeatures see [[LocalParams.initFeatures]]
*/
final case class RemoteParams(nodeId: PublicKey,
dustLimit: Satoshi,
@ -476,61 +485,11 @@ final case class RemoteParams(nodeId: PublicKey,
paymentBasepoint: PublicKey,
delayedPaymentBasepoint: PublicKey,
htlcBasepoint: PublicKey,
features: Features)
initFeatures: Features,
shutdownScript: Option[ByteVector])
object ChannelFlags {
val AnnounceChannel = 0x01.toByte
val Empty = 0x00.toByte
}
case class ChannelVersion(bits: BitVector) {
import ChannelVersion._
require(bits.size == ChannelVersion.LENGTH_BITS, "channel version takes 4 bytes")
val commitmentFormat: CommitmentFormat = if (hasAnchorOutputs) {
AnchorOutputsCommitmentFormat
} else {
DefaultCommitmentFormat
}
def |(other: ChannelVersion) = ChannelVersion(bits | other.bits)
def &(other: ChannelVersion) = ChannelVersion(bits & other.bits)
def ^(other: ChannelVersion) = ChannelVersion(bits ^ other.bits)
def isSet(bit: Int): Boolean = bits.reverse.get(bit)
def hasPubkeyKeyPath: Boolean = isSet(USE_PUBKEY_KEYPATH_BIT)
def hasStaticRemotekey: Boolean = isSet(USE_STATIC_REMOTEKEY_BIT)
def hasAnchorOutputs: Boolean = isSet(USE_ANCHOR_OUTPUTS_BIT)
/** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */
def paysDirectlyToWallet: Boolean = hasStaticRemotekey && !hasAnchorOutputs
}
object ChannelVersion {
import scodec.bits._
val LENGTH_BITS: Int = 4 * 8
private val USE_PUBKEY_KEYPATH_BIT = 0 // bit numbers start at 0
private val USE_STATIC_REMOTEKEY_BIT = 1
private val USE_ANCHOR_OUTPUTS_BIT = 2
def fromBit(bit: Int): ChannelVersion = ChannelVersion(BitVector.low(LENGTH_BITS).set(bit).reverse)
def pickChannelVersion(localFeatures: Features, remoteFeatures: Features): ChannelVersion = {
if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputs)) {
ANCHOR_OUTPUTS
} else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.StaticRemoteKey)) {
STATIC_REMOTEKEY
} else {
STANDARD
}
}
val ZEROES = ChannelVersion(bin"00000000000000000000000000000000")
val STANDARD = ZEROES | fromBit(USE_PUBKEY_KEYPATH_BIT)
val STATIC_REMOTEKEY = STANDARD | fromBit(USE_STATIC_REMOTEKEY_BIT) // PUBKEY_KEYPATH + STATIC_REMOTEKEY
val ANCHOR_OUTPUTS = STATIC_REMOTEKEY | fromBit(USE_ANCHOR_OUTPUTS_BIT) // PUBKEY_KEYPATH + STATIC_REMOTEKEY + ANCHOR_OUTPUTS
}
// @formatter:on

View File

@ -39,9 +39,9 @@ case class RemoteChanges(proposed: List[UpdateMessage], acked: List[UpdateMessag
def all: List[UpdateMessage] = proposed ++ signed ++ acked
}
case class Changes(ourChanges: LocalChanges, theirChanges: RemoteChanges)
case class HtlcTxAndSigs(txinfo: HtlcTx, localSig: ByteVector64, remoteSig: ByteVector64)
case class PublishableTxs(commitTx: CommitTx, htlcTxsAndSigs: List[HtlcTxAndSigs])
case class LocalCommit(index: Long, spec: CommitmentSpec, publishableTxs: PublishableTxs)
case class HtlcTxAndRemoteSig(htlcTx: HtlcTx, remoteSig: ByteVector64)
case class CommitTxAndRemoteSig(commitTx: CommitTx, remoteSig: ByteVector64)
case class LocalCommit(index: Long, spec: CommitmentSpec, commitTxAndRemoteSig: CommitTxAndRemoteSig, htlcTxsAndRemoteSigs: List[HtlcTxAndRemoteSig])
case class RemoteCommit(index: Long, spec: CommitmentSpec, txid: ByteVector32, remotePerCommitmentPoint: PublicKey)
case class WaitingForRevocation(nextRemoteCommit: RemoteCommit, sent: CommitSig, sentAfterLocalCommitIndex: Long, reSignAsap: Boolean = false)
// @formatter:on
@ -69,7 +69,9 @@ trait AbstractCommitments {
* So, when we've signed and sent a commit message and are waiting for their revocation message,
* theirNextCommitInfo is their next commit tx. The rest of the time, it is their next per-commitment point
*/
case class Commitments(channelVersion: ChannelVersion,
case class Commitments(channelId: ByteVector32,
channelConfig: ChannelConfig,
channelFeatures: ChannelFeatures,
localParams: LocalParams, remoteParams: RemoteParams,
channelFlags: Byte,
localCommit: LocalCommit, remoteCommit: RemoteCommit,
@ -78,9 +80,9 @@ case class Commitments(channelVersion: ChannelVersion,
originChannels: Map[Long, Origin], // for outgoing htlcs relayed through us, details about the corresponding incoming htlcs
remoteNextCommitInfo: Either[WaitingForRevocation, PublicKey],
commitInput: InputInfo,
remotePerCommitmentSecrets: ShaChain, channelId: ByteVector32) extends AbstractCommitments {
remotePerCommitmentSecrets: ShaChain) extends AbstractCommitments {
require(channelVersion.paysDirectlyToWallet == localParams.walletStaticPaymentBasepoint.isDefined, s"localParams.walletStaticPaymentBasepoint must be defined only for commitments that pay directly to our wallet (version=$channelVersion)")
require(channelFeatures.paysDirectlyToWallet == localParams.walletStaticPaymentBasepoint.isDefined, s"localParams.walletStaticPaymentBasepoint must be defined only for commitments that pay directly to our wallet (channel features: $channelFeatures")
def hasNoPendingHtlcs: Boolean = localCommit.spec.htlcs.isEmpty && remoteCommit.spec.htlcs.isEmpty && remoteNextCommitInfo.isRight
@ -144,11 +146,20 @@ case class Commitments(channelVersion: ChannelVersion,
localCommit.spec.htlcs.collect(incoming).filter(nearlyExpired)
}
def addLocalProposal(proposal: UpdateMessage): Commitments = Commitments.addLocalProposal(this, proposal)
/**
* Return a fully signed commit tx, that can be published as-is.
*/
def fullySignedLocalCommitTx(keyManager: ChannelKeyManager): CommitTx = {
val unsignedCommitTx = localCommit.commitTxAndRemoteSig.commitTx
val localSig = keyManager.sign(unsignedCommitTx, keyManager.fundingPublicKey(localParams.fundingKeyPath), TxOwner.Local, commitmentFormat)
val remoteSig = localCommit.commitTxAndRemoteSig.remoteSig
val commitTx = Transactions.addSigs(unsignedCommitTx, keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, localSig, remoteSig)
// We verify the remote signature when receiving their commit_sig, so this check should always pass.
require(Transactions.checkSpendable(commitTx).isSuccess, "commit signatures are invalid")
commitTx
}
def addRemoteProposal(proposal: UpdateMessage): Commitments = Commitments.addRemoteProposal(this, proposal)
val commitmentFormat: CommitmentFormat = channelVersion.commitmentFormat
val commitmentFormat: CommitmentFormat = channelFeatures.commitmentFormat
val localNodeId: PublicKey = localParams.nodeId
@ -288,9 +299,9 @@ object Commitments {
// we allowed mismatches between our feerates and our remote's as long as commitments didn't contain any HTLC at risk
// we need to verify that we're not disagreeing on feerates anymore before offering new HTLCs
// NB: there may be a pending update_fee that hasn't been applied yet that needs to be taken into account
val localFeeratePerKw = feeConf.getCommitmentFeerate(commitments.remoteNodeId, commitments.channelVersion, commitments.capacity, None)
val localFeeratePerKw = feeConf.getCommitmentFeerate(commitments.remoteNodeId, commitments.channelFeatures, commitments.capacity, None)
val remoteFeeratePerKw = commitments.localCommit.spec.feeratePerKw +: commitments.remoteChanges.all.collect { case f: UpdateFee => f.feeratePerKw }
remoteFeeratePerKw.find(feerate => feeConf.feerateToleranceFor(commitments.remoteNodeId).isFeeDiffTooHigh(commitments.channelVersion, localFeeratePerKw, feerate)) match {
remoteFeeratePerKw.find(feerate => feeConf.feerateToleranceFor(commitments.remoteNodeId).isFeeDiffTooHigh(commitments.channelFeatures, localFeeratePerKw, feerate)) match {
case Some(feerate) => return Left(FeerateTooDifferent(commitments.channelId, localFeeratePerKw = localFeeratePerKw, remoteFeeratePerKw = feerate))
case None =>
}
@ -352,9 +363,9 @@ object Commitments {
// we allowed mismatches between our feerates and our remote's as long as commitments didn't contain any HTLC at risk
// we need to verify that we're not disagreeing on feerates anymore before accepting new HTLCs
// NB: there may be a pending update_fee that hasn't been applied yet that needs to be taken into account
val localFeeratePerKw = feeConf.getCommitmentFeerate(commitments.remoteNodeId, commitments.channelVersion, commitments.capacity, None)
val localFeeratePerKw = feeConf.getCommitmentFeerate(commitments.remoteNodeId, commitments.channelFeatures, commitments.capacity, None)
val remoteFeeratePerKw = commitments.localCommit.spec.feeratePerKw +: commitments.remoteChanges.all.collect { case f: UpdateFee => f.feeratePerKw }
remoteFeeratePerKw.find(feerate => feeConf.feerateToleranceFor(commitments.remoteNodeId).isFeeDiffTooHigh(commitments.channelVersion, localFeeratePerKw, feerate)) match {
remoteFeeratePerKw.find(feerate => feeConf.feerateToleranceFor(commitments.remoteNodeId).isFeeDiffTooHigh(commitments.channelFeatures, localFeeratePerKw, feerate)) match {
case Some(feerate) => return Left(FeerateTooDifferent(commitments.channelId, localFeeratePerKw = localFeeratePerKw, remoteFeeratePerKw = feerate))
case None =>
}
@ -501,9 +512,9 @@ object Commitments {
Left(FeerateTooSmall(commitments.channelId, remoteFeeratePerKw = fee.feeratePerKw))
} else {
Metrics.RemoteFeeratePerKw.withoutTags().record(fee.feeratePerKw.toLong)
val localFeeratePerKw = feeConf.getCommitmentFeerate(commitments.remoteNodeId, commitments.channelVersion, commitments.capacity, None)
val localFeeratePerKw = feeConf.getCommitmentFeerate(commitments.remoteNodeId, commitments.channelFeatures, commitments.capacity, None)
log.info("remote feeratePerKw={}, local feeratePerKw={}, ratio={}", fee.feeratePerKw, localFeeratePerKw, fee.feeratePerKw.toLong.toDouble / localFeeratePerKw.toLong)
if (feeConf.feerateToleranceFor(commitments.remoteNodeId).isFeeDiffTooHigh(commitments.channelVersion, localFeeratePerKw, fee.feeratePerKw) && commitments.hasPendingOrProposedHtlcs) {
if (feeConf.feerateToleranceFor(commitments.remoteNodeId).isFeeDiffTooHigh(commitments.channelFeatures, localFeeratePerKw, fee.feeratePerKw) && commitments.hasPendingOrProposedHtlcs) {
Left(FeerateTooDifferent(commitments.channelId, localFeeratePerKw = localFeeratePerKw, remoteFeeratePerKw = fee.feeratePerKw))
} else {
// NB: we check that the funder can afford this new fee even if spec allows to do it at next signature
@ -552,11 +563,11 @@ object Commitments {
case Right(remoteNextPerCommitmentPoint) =>
// remote commitment will includes all local changes + remote acked changes
val spec = CommitmentSpec.reduce(remoteCommit.spec, remoteChanges.acked, localChanges.proposed)
val (remoteCommitTx, htlcTxs) = makeRemoteTxs(keyManager, channelVersion, remoteCommit.index + 1, localParams, remoteParams, commitInput, remoteNextPerCommitmentPoint, spec)
val (remoteCommitTx, htlcTxs) = makeRemoteTxs(keyManager, channelConfig, channelFeatures, remoteCommit.index + 1, localParams, remoteParams, commitInput, remoteNextPerCommitmentPoint, spec)
val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath), TxOwner.Remote, commitmentFormat)
val sortedHtlcTxs: Seq[TransactionWithInputInfo] = htlcTxs.sortBy(_.input.outPoint.index)
val channelKeyPath = keyManager.keyPath(commitments.localParams, commitments.channelVersion)
val channelKeyPath = keyManager.keyPath(commitments.localParams, channelConfig)
val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), remoteNextPerCommitmentPoint, TxOwner.Remote, commitmentFormat))
// NB: IN/OUT htlcs are inverted because this is the remote commit
@ -595,39 +606,28 @@ object Commitments {
}
val spec = CommitmentSpec.reduce(localCommit.spec, localChanges.acked, remoteChanges.proposed)
val channelKeyPath = keyManager.keyPath(commitments.localParams, commitments.channelVersion)
val channelKeyPath = keyManager.keyPath(localParams, channelConfig)
val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index + 1)
val (localCommitTx, htlcTxs) = makeLocalTxs(keyManager, channelVersion, localCommit.index + 1, localParams, remoteParams, commitInput, localPerCommitmentPoint, spec)
val sig = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath), TxOwner.Local, commitmentFormat)
val (localCommitTx, htlcTxs) = makeLocalTxs(keyManager, channelConfig, channelFeatures, localCommit.index + 1, localParams, remoteParams, commitInput, localPerCommitmentPoint, spec)
log.info(s"built local commit number=${localCommit.index + 1} toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.feeratePerKw} txid=${localCommitTx.tx.txid} tx={}", spec.htlcs.collect(incoming).map(_.id).mkString(","), spec.htlcs.collect(outgoing).map(_.id).mkString(","), localCommitTx.tx)
// no need to compute htlc sigs if commit sig doesn't check out
val signedCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, sig, commit.signature)
if (Transactions.checkSpendable(signedCommitTx).isFailure) {
return Left(InvalidCommitmentSignature(commitments.channelId, signedCommitTx.tx))
if (!Transactions.checkSig(localCommitTx, commit.signature, remoteParams.fundingPubKey, TxOwner.Remote, commitmentFormat)) {
return Left(InvalidCommitmentSignature(commitments.channelId, localCommitTx.tx))
}
val sortedHtlcTxs: Seq[TransactionWithInputInfo] = htlcTxs.sortBy(_.input.outPoint.index)
val sortedHtlcTxs: Seq[HtlcTx] = htlcTxs.sortBy(_.input.outPoint.index)
if (commit.htlcSignatures.size != sortedHtlcTxs.size) {
return Left(HtlcSigCountMismatch(commitments.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size))
}
val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitmentFormat))
val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint)
// combine the sigs to make signed txes
val htlcTxsAndSigs = (sortedHtlcTxs, htlcSigs, commit.htlcSignatures).zipped.toList.collect {
case (htlcTx: HtlcTimeoutTx, localSig, remoteSig) =>
if (Transactions.checkSpendable(Transactions.addSigs(htlcTx, localSig, remoteSig, commitmentFormat)).isFailure) {
return Left(InvalidHtlcSignature(commitments.channelId, htlcTx.tx))
}
HtlcTxAndSigs(htlcTx, localSig, remoteSig)
case (htlcTx: HtlcSuccessTx, localSig, remoteSig) =>
// we can't check that htlc-success tx are spendable because we need the payment preimage; thus we only check the remote sig
// we verify the signature from their point of view, where it is a remote tx
val htlcTxsAndRemoteSigs = sortedHtlcTxs.zip(commit.htlcSignatures).toList.map {
case (htlcTx: HtlcTx, remoteSig) =>
if (!Transactions.checkSig(htlcTx, remoteSig, remoteHtlcPubkey, TxOwner.Remote, commitmentFormat)) {
return Left(InvalidHtlcSignature(commitments.channelId, htlcTx.tx))
}
HtlcTxAndSigs(htlcTx, localSig, remoteSig)
HtlcTxAndRemoteSig(htlcTx, remoteSig)
}
// we will send our revocation preimage + our next revocation hash
@ -643,7 +643,8 @@ object Commitments {
val localCommit1 = LocalCommit(
index = localCommit.index + 1,
spec,
publishableTxs = PublishableTxs(signedCommitTx, htlcTxsAndSigs))
commitTxAndRemoteSig = CommitTxAndRemoteSig(localCommitTx, commit.signature),
htlcTxsAndRemoteSigs = htlcTxsAndRemoteSigs)
val ourChanges1 = localChanges.copy(acked = Nil)
val theirChanges1 = remoteChanges.copy(proposed = Nil, acked = remoteChanges.acked ++ remoteChanges.proposed)
val commitments1 = commitments.copy(localCommit = localCommit1, localChanges = ourChanges1, remoteChanges = theirChanges1)
@ -693,46 +694,56 @@ object Commitments {
}
def makeLocalTxs(keyManager: ChannelKeyManager,
channelVersion: ChannelVersion,
channelConfig: ChannelConfig,
channelFeatures: ChannelFeatures,
commitTxNumber: Long,
localParams: LocalParams,
remoteParams: RemoteParams,
commitmentInput: InputInfo,
localPerCommitmentPoint: PublicKey,
spec: CommitmentSpec): (CommitTx, Seq[HtlcTx]) = {
val channelKeyPath = keyManager.keyPath(localParams, channelVersion)
val channelKeyPath = keyManager.keyPath(localParams, channelConfig)
val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey
val localDelayedPaymentPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint)
val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, localPerCommitmentPoint)
val remotePaymentPubkey = if (channelVersion.hasStaticRemotekey) remoteParams.paymentBasepoint else Generators.derivePubKey(remoteParams.paymentBasepoint, localPerCommitmentPoint)
val remotePaymentPubkey = if (channelFeatures.hasFeature(Features.StaticRemoteKey)) {
remoteParams.paymentBasepoint
} else {
Generators.derivePubKey(remoteParams.paymentBasepoint, localPerCommitmentPoint)
}
val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint)
val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint)
val localPaymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey)
val outputs = makeCommitTxOutputs(localParams.isFunder, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, localFundingPubkey, remoteParams.fundingPubKey, spec, channelVersion.commitmentFormat)
val outputs = makeCommitTxOutputs(localParams.isFunder, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, localFundingPubkey, remoteParams.fundingPubKey, spec, channelFeatures.commitmentFormat)
val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, localPaymentBasepoint, remoteParams.paymentBasepoint, localParams.isFunder, outputs)
val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, spec.feeratePerKw, outputs, channelVersion.commitmentFormat)
val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, spec.feeratePerKw, outputs, channelFeatures.commitmentFormat)
(commitTx, htlcTxs)
}
def makeRemoteTxs(keyManager: ChannelKeyManager,
channelVersion: ChannelVersion,
channelConfig: ChannelConfig,
channelFeatures: ChannelFeatures,
commitTxNumber: Long,
localParams: LocalParams,
remoteParams: RemoteParams,
commitmentInput: InputInfo,
remotePerCommitmentPoint: PublicKey,
spec: CommitmentSpec): (CommitTx, Seq[HtlcTx]) = {
val channelKeyPath = keyManager.keyPath(localParams, channelVersion)
val channelKeyPath = keyManager.keyPath(localParams, channelConfig)
val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey
val localPaymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey)
val localPaymentPubkey = if (channelVersion.hasStaticRemotekey) localPaymentBasepoint else Generators.derivePubKey(localPaymentBasepoint, remotePerCommitmentPoint)
val localPaymentPubkey = if (channelFeatures.hasFeature(Features.StaticRemoteKey)) {
localPaymentBasepoint
} else {
Generators.derivePubKey(localPaymentBasepoint, remotePerCommitmentPoint)
}
val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint)
val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint)
val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint)
val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint)
val outputs = makeCommitTxOutputs(!localParams.isFunder, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, remoteParams.fundingPubKey, localFundingPubkey, spec, channelVersion.commitmentFormat)
val outputs = makeCommitTxOutputs(!localParams.isFunder, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, remoteParams.fundingPubKey, localFundingPubkey, spec, channelFeatures.commitmentFormat)
val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, localPaymentBasepoint, !localParams.isFunder, outputs)
val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, spec.feeratePerKw, outputs, channelVersion.commitmentFormat)
val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, spec.feeratePerKw, outputs, channelFeatures.commitmentFormat)
(commitTx, htlcTxs)
}

View File

@ -49,8 +49,8 @@ object Helpers {
*/
def updateFeatures(data: HasCommitments, localInit: Init, remoteInit: Init): HasCommitments = {
val commitments1 = data.commitments.copy(
localParams = data.commitments.localParams.copy(features = localInit.features),
remoteParams = data.commitments.remoteParams.copy(features = remoteInit.features))
localParams = data.commitments.localParams.copy(initFeatures = localInit.features),
remoteParams = data.commitments.remoteParams.copy(initFeatures = remoteInit.features))
data match {
case d: DATA_WAIT_FOR_FUNDING_CONFIRMED => d.copy(commitments = commitments1)
case d: DATA_WAIT_FOR_FUNDING_LOCKED => d.copy(commitments = commitments1)
@ -81,7 +81,7 @@ object Helpers {
/**
* Called by the fundee
*/
def validateParamsFundee(nodeParams: NodeParams, features: Features, channelVersion: ChannelVersion, open: OpenChannel, remoteNodeId: PublicKey): Either[ChannelException, Unit] = {
def validateParamsFundee(nodeParams: NodeParams, initFeatures: Features, channelFeatures: ChannelFeatures, open: OpenChannel, remoteNodeId: PublicKey): Either[ChannelException, Unit] = {
// BOLT #2: if the chain_hash value, within the open_channel, message is set to a hash of a chain that is unknown to the receiver:
// MUST reject the channel.
if (nodeParams.chainHash != open.chainHash) return Left(InvalidChainHash(open.temporaryChannelId, local = nodeParams.chainHash, remote = open.chainHash))
@ -89,7 +89,7 @@ object Helpers {
if (open.fundingSatoshis < nodeParams.minFundingSatoshis || open.fundingSatoshis > nodeParams.maxFundingSatoshis) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, nodeParams.maxFundingSatoshis))
// BOLT #2: Channel funding limits
if (open.fundingSatoshis >= Channel.MAX_FUNDING && !features.hasFeature(Features.Wumbo)) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, Channel.MAX_FUNDING))
if (open.fundingSatoshis >= Channel.MAX_FUNDING && !initFeatures.hasFeature(Features.Wumbo)) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, Channel.MAX_FUNDING))
// BOLT #2: The receiving node MUST fail the channel if: push_msat is greater than funding_satoshis * 1000.
if (open.pushMsat > open.fundingSatoshis) return Left(InvalidPushAmount(open.temporaryChannelId, open.pushMsat, open.fundingSatoshis.toMilliSatoshi))
@ -116,8 +116,8 @@ object Helpers {
}
// BOLT #2: The receiving node MUST fail the channel if: it considers feerate_per_kw too small for timely processing or unreasonably large.
val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, channelVersion, open.fundingSatoshis, None)
if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isFeeDiffTooHigh(channelVersion, localFeeratePerKw, open.feeratePerKw)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw))
val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, channelFeatures, open.fundingSatoshis, None)
if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isFeeDiffTooHigh(channelFeatures, localFeeratePerKw, open.feeratePerKw)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw))
// only enforce dust limit check on mainnet
if (nodeParams.chainHash == Block.LivenetGenesisBlock.hash) {
if (open.dustLimitSatoshis < Channel.MIN_DUSTLIMIT) return Left(DustLimitTooSmall(open.temporaryChannelId, open.dustLimitSatoshis, Channel.MIN_DUSTLIMIT))
@ -249,7 +249,7 @@ object Helpers {
*
* @return (localSpec, localTx, remoteSpec, remoteTx, fundingTxOutput)
*/
def makeFirstCommitTxs(keyManager: ChannelKeyManager, channelVersion: ChannelVersion, temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingAmount: Satoshi, pushMsat: MilliSatoshi, initialFeeratePerKw: FeeratePerKw, fundingTxHash: ByteVector32, fundingTxOutputIndex: Int, remoteFirstPerCommitmentPoint: PublicKey): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx)] = {
def makeFirstCommitTxs(keyManager: ChannelKeyManager, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingAmount: Satoshi, pushMsat: MilliSatoshi, initialFeeratePerKw: FeeratePerKw, fundingTxHash: ByteVector32, fundingTxOutputIndex: Int, remoteFirstPerCommitmentPoint: PublicKey): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx)] = {
val toLocalMsat = if (localParams.isFunder) fundingAmount.toMilliSatoshi - pushMsat else pushMsat
val toRemoteMsat = if (localParams.isFunder) pushMsat else fundingAmount.toMilliSatoshi - pushMsat
@ -259,7 +259,7 @@ object Helpers {
if (!localParams.isFunder) {
// they are funder, therefore they pay the fee: we need to make sure they can afford it!
val toRemoteMsat = remoteSpec.toLocal
val fees = commitTxTotalCost(remoteParams.dustLimit, remoteSpec, channelVersion.commitmentFormat)
val fees = commitTxTotalCost(remoteParams.dustLimit, remoteSpec, channelFeatures.commitmentFormat)
val missing = toRemoteMsat.truncateToSatoshi - localParams.channelReserve - fees
if (missing < Satoshi(0)) {
return Left(CannotAffordFees(temporaryChannelId, missing = -missing, reserve = localParams.channelReserve, fees = fees))
@ -267,11 +267,11 @@ object Helpers {
}
val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath)
val channelKeyPath = keyManager.keyPath(localParams, channelVersion)
val channelKeyPath = keyManager.keyPath(localParams, channelConfig)
val commitmentInput = makeFundingInputInfo(fundingTxHash, fundingTxOutputIndex, fundingAmount, fundingPubKey.publicKey, remoteParams.fundingPubKey)
val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0)
val (localCommitTx, _) = Commitments.makeLocalTxs(keyManager, channelVersion, 0, localParams, remoteParams, commitmentInput, localPerCommitmentPoint, localSpec)
val (remoteCommitTx, _) = Commitments.makeRemoteTxs(keyManager, channelVersion, 0, localParams, remoteParams, commitmentInput, remoteFirstPerCommitmentPoint, remoteSpec)
val (localCommitTx, _) = Commitments.makeLocalTxs(keyManager, channelConfig, channelFeatures, 0, localParams, remoteParams, commitmentInput, localPerCommitmentPoint, localSpec)
val (remoteCommitTx, _) = Commitments.makeRemoteTxs(keyManager, channelConfig, channelFeatures, 0, localParams, remoteParams, commitmentInput, remoteFirstPerCommitmentPoint, remoteSpec)
Right(localSpec, localCommitTx, remoteSpec, remoteCommitTx)
}
@ -432,7 +432,7 @@ object Helpers {
def firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): Satoshi = {
val requestedFeerate = feeEstimator.getFeeratePerKw(feeTargets.mutualCloseBlockTarget)
val feeratePerKw = if (commitments.channelVersion.hasAnchorOutputs) {
val feeratePerKw = if (commitments.channelFeatures.hasFeature(Features.AnchorOutputs)) {
requestedFeerate
} else {
// we "MUST set fee_satoshis less than or equal to the base fee of the final commitment transaction"
@ -450,7 +450,7 @@ object Helpers {
def makeClosingTx(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingFee: Satoshi)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = {
import commitments._
val allowAnySegwit = Features.canUseFeature(commitments.localParams.features, commitments.remoteParams.features, Features.ShutdownAnySegwit)
val allowAnySegwit = Features.canUseFeature(commitments.localParams.initFeatures, commitments.remoteParams.initFeatures, Features.ShutdownAnySegwit)
require(isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit), "invalid localScriptPubkey")
require(isValidFinalScriptPubkey(remoteScriptPubkey, allowAnySegwit), "invalid remoteScriptPubkey")
log.debug("making closing tx with closingFee={} and commitments:\n{}", closingFee, Commitments.specs2String(commitments))
@ -465,8 +465,8 @@ object Helpers {
def checkClosingSignature(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, remoteClosingFee: Satoshi, remoteClosingSig: ByteVector64)(implicit log: LoggingAdapter): Either[ChannelException, ClosingTx] = {
import commitments._
val lastCommitFeeSatoshi = commitments.commitInput.txOut.amount - commitments.localCommit.publishableTxs.commitTx.tx.txOut.map(_.amount).sum
if (remoteClosingFee > lastCommitFeeSatoshi && !commitments.channelVersion.hasAnchorOutputs) {
val lastCommitFeeSatoshi = commitments.commitInput.txOut.amount - commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.map(_.amount).sum
if (remoteClosingFee > lastCommitFeeSatoshi && !commitments.channelFeatures.hasFeature(Features.AnchorOutputs)) {
log.error(s"remote proposed a commit fee higher than the last commitment fee: remoteClosingFeeSatoshi=${remoteClosingFee.toLong} lastCommitFeeSatoshi=$lastCommitFeeSatoshi")
Left(InvalidCloseFee(commitments.channelId, remoteClosingFee))
} else {
@ -504,8 +504,8 @@ object Helpers {
*/
def claimCurrentLocalCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): LocalCommitPublished = {
import commitments._
require(localCommit.publishableTxs.commitTx.tx.txid == tx.txid, "txid mismatch, provided tx is not the current local commit tx")
val channelKeyPath = keyManager.keyPath(localParams, channelVersion)
require(localCommit.commitTxAndRemoteSig.commitTx.tx.txid == tx.txid, "txid mismatch, provided tx is not the current local commit tx")
val channelKeyPath = keyManager.keyPath(localParams, channelConfig)
val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index.toInt)
val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint)
val localDelayedPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint)
@ -523,11 +523,12 @@ object Helpers {
// those are the preimages to existing received htlcs
val preimages = commitments.localChanges.all.collect { case u: UpdateFulfillHtlc => u.paymentPreimage }.map(r => Crypto.sha256(r) -> r).toMap
val htlcTxs: Map[OutPoint, Option[HtlcTx]] = localCommit.publishableTxs.htlcTxsAndSigs.collect {
case HtlcTxAndSigs(txInfo@HtlcSuccessTx(_, _, paymentHash, _), localSig, remoteSig) =>
val htlcTxs: Map[OutPoint, Option[HtlcTx]] = localCommit.htlcTxsAndRemoteSigs.collect {
case HtlcTxAndRemoteSig(txInfo@HtlcSuccessTx(_, _, paymentHash, _), remoteSig) =>
if (preimages.contains(paymentHash)) {
// incoming htlc for which we have the preimage: we can spend it immediately
txInfo.input.outPoint -> generateTx("htlc-success") {
val localSig = keyManager.sign(txInfo, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitmentFormat)
Right(Transactions.addSigs(txInfo, localSig, remoteSig, preimages(paymentHash), commitmentFormat))
}
} else {
@ -535,9 +536,10 @@ object Helpers {
// preimage later, otherwise it will eventually timeout and they will get their funds back
txInfo.input.outPoint -> None
}
case HtlcTxAndSigs(txInfo: HtlcTimeoutTx, localSig, remoteSig) =>
case HtlcTxAndRemoteSig(txInfo: HtlcTimeoutTx, remoteSig) =>
// outgoing htlc: they may or may not have the preimage, the only thing to do is try to get back our funds after timeout
txInfo.input.outPoint -> generateTx("htlc-timeout") {
val localSig = keyManager.sign(txInfo, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitmentFormat)
Right(Transactions.addSigs(txInfo, localSig, remoteSig, commitmentFormat))
}
}.toMap
@ -570,7 +572,7 @@ object Helpers {
import commitments._
if (isHtlcSuccess(tx, localCommitPublished) || isHtlcTimeout(tx, localCommitPublished)) {
val feeratePerKwDelayed = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget)
val channelKeyPath = keyManager.keyPath(localParams, channelVersion)
val channelKeyPath = keyManager.keyPath(localParams, channelConfig)
val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index.toInt)
val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint)
val localDelayedPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint)
@ -597,11 +599,11 @@ object Helpers {
* @return a list of transactions (one per output of the commit tx that we can claim)
*/
def claimRemoteCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): RemoteCommitPublished = {
import commitments.{channelVersion, commitInput, localParams, remoteParams}
import commitments.{channelConfig, channelFeatures, commitInput, localParams, remoteParams}
require(remoteCommit.txid == tx.txid, "txid mismatch, provided tx is not the current remote commit tx")
val (remoteCommitTx, _) = Commitments.makeRemoteTxs(keyManager, channelVersion, remoteCommit.index, localParams, remoteParams, commitInput, remoteCommit.remotePerCommitmentPoint, remoteCommit.spec)
val (remoteCommitTx, _) = Commitments.makeRemoteTxs(keyManager, channelConfig, channelFeatures, 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 channelKeyPath = keyManager.keyPath(localParams, channelVersion)
val channelKeyPath = keyManager.keyPath(localParams, channelConfig)
val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey
val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint)
val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remoteCommit.remotePerCommitmentPoint)
@ -655,20 +657,19 @@ object Helpers {
}
).flatten
channelVersion match {
case v if v.paysDirectlyToWallet =>
RemoteCommitPublished(
commitTx = tx,
claimMainOutputTx = None,
claimHtlcTxs = htlcTxs,
claimAnchorTxs = claimAnchorTxs,
irrevocablySpent = Map.empty
)
case _ =>
claimRemoteCommitMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx, feeEstimator, feeTargets).copy(
claimHtlcTxs = htlcTxs,
claimAnchorTxs = claimAnchorTxs,
)
if (channelFeatures.paysDirectlyToWallet) {
RemoteCommitPublished(
commitTx = tx,
claimMainOutputTx = None,
claimHtlcTxs = htlcTxs,
claimAnchorTxs = claimAnchorTxs,
irrevocablySpent = Map.empty
)
} else {
claimRemoteCommitMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx, feeEstimator, feeTargets).copy(
claimHtlcTxs = htlcTxs,
claimAnchorTxs = claimAnchorTxs,
)
}
}
@ -683,7 +684,7 @@ object Helpers {
* @return a transaction claiming our main output
*/
def claimRemoteCommitMainOutput(keyManager: ChannelKeyManager, commitments: Commitments, remotePerCommitmentPoint: PublicKey, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): RemoteCommitPublished = {
val channelKeyPath = keyManager.keyPath(commitments.localParams, commitments.channelVersion)
val channelKeyPath = keyManager.keyPath(commitments.localParams, commitments.channelConfig)
val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint)
val localPaymentPoint = keyManager.paymentPoint(channelKeyPath).publicKey
val feeratePerKwMain = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget)
@ -724,7 +725,7 @@ object Helpers {
def claimRevokedRemoteCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, commitTx: Transaction, db: ChannelsDb, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): Option[RevokedCommitPublished] = {
import commitments._
require(commitTx.txIn.size == 1, "commitment tx should have 1 input")
val channelKeyPath = keyManager.keyPath(localParams, channelVersion)
val channelKeyPath = keyManager.keyPath(localParams, channelConfig)
val obscuredTxNumber = Transactions.decodeTxNumber(commitTx.txIn.head.sequence, commitTx.lockTime)
val localPaymentPoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey)
// this tx has been published by remote, so we need to invert local/remote params
@ -747,11 +748,11 @@ object Helpers {
val feeratePerKwPenalty = feeEstimator.getFeeratePerKw(target = 2)
// first we will claim our main output right away
val mainTx = channelVersion match {
case v if v.paysDirectlyToWallet =>
val mainTx = channelFeatures match {
case ct if ct.paysDirectlyToWallet =>
log.info(s"channel uses option_static_remotekey to pay directly to our wallet, there is nothing to do")
None
case v if v.hasAnchorOutputs => generateTx("remote-main-delayed") {
case ct if ct.hasFeature(Features.AnchorOutputs) => generateTx("remote-main-delayed") {
Transactions.makeClaimRemoteDelayedOutputTx(commitTx, localParams.dustLimit, localPaymentPoint, localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => {
val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitmentFormat)
Transactions.addSigs(claimMain, sig)
@ -827,7 +828,7 @@ object Helpers {
import commitments._
val commitTx = revokedCommitPublished.commitTx
val obscuredTxNumber = Transactions.decodeTxNumber(commitTx.txIn.head.sequence, commitTx.lockTime)
val channelKeyPath = keyManager.keyPath(localParams, channelVersion)
val channelKeyPath = keyManager.keyPath(localParams, channelConfig)
val localPaymentPoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey)
// this tx has been published by remote, so we need to invert local/remote params
val txNumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, localPaymentPoint)
@ -956,7 +957,7 @@ object Helpers {
*/
def timedOutHtlcs(commitmentFormat: CommitmentFormat, localCommit: LocalCommit, localCommitPublished: LocalCommitPublished, localDustLimit: Satoshi, tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = {
val untrimmedHtlcs = Transactions.trimOfferedHtlcs(localDustLimit, localCommit.spec, commitmentFormat).map(_.add)
if (tx.txid == localCommit.publishableTxs.commitTx.tx.txid) {
if (tx.txid == localCommit.commitTxAndRemoteSig.commitTx.tx.txid) {
// the tx is a commitment tx, we can immediately fail all dust htlcs (they don't have an output in the tx)
localCommit.spec.htlcs.collect(outgoing) -- untrimmedHtlcs
} else {
@ -1036,7 +1037,7 @@ object Helpers {
* @param tx a transaction that is sufficiently buried in the blockchain
*/
def onChainOutgoingHtlcs(localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: Option[RemoteCommit], tx: Transaction): Set[UpdateAddHtlc] = {
if (localCommit.publishableTxs.commitTx.tx.txid == tx.txid) {
if (localCommit.commitTxAndRemoteSig.commitTx.tx.txid == tx.txid) {
localCommit.spec.htlcs.collect(outgoing)
} else if (remoteCommit.txid == tx.txid) {
remoteCommit.spec.htlcs.collect(incoming)
@ -1055,7 +1056,7 @@ object Helpers {
val localCommit = d.commitments.localCommit
val remoteCommit = d.commitments.remoteCommit
val nextRemoteCommit_opt = d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit)
if (localCommit.publishableTxs.commitTx.tx.txid == tx.txid) {
if (localCommit.commitTxAndRemoteSig.commitTx.tx.txid == tx.txid) {
// our commit got confirmed, so any htlc that is in their commitment but not in ours will never reach the chain
val htlcsInRemoteCommit = remoteCommit.spec.htlcs ++ nextRemoteCommit_opt.map(_.spec.htlcs).getOrElse(Set.empty)
// NB: from the p.o.v of remote, their incoming htlcs are our outgoing htlcs

View File

@ -26,7 +26,7 @@ import fr.acinq.eclair.blockchain.bitcoind.rpc.ExtendedBitcoinClient.FundTransac
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.channel.publish.TxPublisher.TxPublishLogContext
import fr.acinq.eclair.channel.publish.TxTimeLocksMonitor.CheckTx
import fr.acinq.eclair.channel.{Commitments, HtlcTxAndSigs}
import fr.acinq.eclair.channel.{Commitments, HtlcTxAndRemoteSig}
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.wire.protocol.UpdateFulfillHtlc
@ -145,13 +145,13 @@ object ReplaceableTxPublisher {
commitments.localChanges.all.collectFirst {
case u: UpdateFulfillHtlc if Crypto.sha256(u.paymentPreimage) == tx.paymentHash => u.paymentPreimage
}.flatMap(preimage => {
commitments.localCommit.publishableTxs.htlcTxsAndSigs.collectFirst {
case HtlcTxAndSigs(HtlcSuccessTx(input, _, _, _), _, remoteSig) if input.outPoint == tx.input.outPoint => HtlcSuccess(tx, remoteSig, preimage)
commitments.localCommit.htlcTxsAndRemoteSigs.collectFirst {
case HtlcTxAndRemoteSig(HtlcSuccessTx(input, _, _, _), remoteSig) if input.outPoint == tx.input.outPoint => HtlcSuccess(tx, remoteSig, preimage)
}
})
case tx: HtlcTimeoutTx =>
commitments.localCommit.publishableTxs.htlcTxsAndSigs.collectFirst {
case HtlcTxAndSigs(HtlcTimeoutTx(input, _, _), _, remoteSig) if input.outPoint == tx.input.outPoint => HtlcTimeout(tx, remoteSig)
commitments.localCommit.htlcTxsAndRemoteSigs.collectFirst {
case HtlcTxAndRemoteSig(HtlcTimeoutTx(input, _, _), remoteSig) if input.outPoint == tx.input.outPoint => HtlcTimeout(tx, remoteSig)
}
}
}
@ -206,7 +206,7 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams,
// - our commit is not confirmed (if it is, no need to claim our anchor)
// - their commit is not confirmed (if it is, no need to claim our anchor either)
// - our commit tx is in the mempool (otherwise we can't claim our anchor)
val commitTx = cmd.commitments.localCommit.publishableTxs.commitTx.tx
val commitTx = cmd.commitments.fullySignedLocalCommitTx(nodeParams.channelKeyManager).tx
val fundingOutpoint = cmd.commitments.commitInput.outPoint
context.pipeToSelf(bitcoinClient.isTransactionOutputSpendable(fundingOutpoint.txid, fundingOutpoint.index.toInt, includeMempool = false).flatMap {
case false => Future.failed(CommitTxAlreadyConfirmed)
@ -243,7 +243,7 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams,
// been confirmed (we don't need to check again here).
HtlcTxAndWitnessData(htlcTx, cmd.commitments) match {
case Some(txWithWitnessData) if targetFeerate <= commitFeerate =>
val channelKeyPath = keyManager.keyPath(cmd.commitments.localParams, cmd.commitments.channelVersion)
val channelKeyPath = keyManager.keyPath(cmd.commitments.localParams, cmd.commitments.channelConfig)
val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, cmd.commitments.localCommit.index)
val localHtlcBasepoint = keyManager.htlcPoint(channelKeyPath)
val localSig = keyManager.sign(htlcTx, localHtlcBasepoint, localPerCommitmentPoint, TxOwner.Local, cmd.commitments.commitmentFormat)
@ -296,7 +296,7 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams,
// NB: we've already checked witness data in the precondition phase. Witness data extraction should be done
// earlier by the channel to remove this duplication.
val txWithWitnessData = HtlcTxAndWitnessData(htlcTx, cmd.commitments).get
val channelKeyPath = keyManager.keyPath(cmd.commitments.localParams, cmd.commitments.channelVersion)
val channelKeyPath = keyManager.keyPath(cmd.commitments.localParams, cmd.commitments.channelConfig)
val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, cmd.commitments.localCommit.index)
val localHtlcBasepoint = keyManager.htlcPoint(channelKeyPath)
val localSig = keyManager.sign(htlcTx, localHtlcBasepoint, localPerCommitmentPoint, TxOwner.Local, cmd.commitments.commitmentFormat)
@ -368,7 +368,7 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams,
private def addInputs(txInfo: ClaimLocalAnchorOutputTx, targetFeerate: FeeratePerKw, commitments: Commitments): Future[ClaimLocalAnchorOutputTx] = {
val dustLimit = commitments.localParams.dustLimit
val commitFeerate = commitments.localCommit.spec.feeratePerKw
val commitTx = commitments.localCommit.publishableTxs.commitTx.tx
val commitTx = commitments.fullySignedLocalCommitTx(nodeParams.channelKeyManager).tx
// We want the feerate of the package (commit tx + tx spending anchor) to equal targetFeerate.
// Thus we have: anchorFeerate = targetFeerate + (weight-commit-tx / weight-anchor-tx) * (targetFeerate - commitTxFeerate)
// If we use the smallest weight possible for the anchor tx, the feerate we use will thus be greater than what we want,

View File

@ -59,7 +59,7 @@ object WeakEntropyPool {
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelPaymentRelayed](e => WrappedPaymentRelayed(e.paymentHash, e.timestamp)))
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[PeerConnected](e => WrappedPeerConnected(e.nodeId)))
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[NodeUpdated](e => WrappedNodeUpdated(e.ann.signature)))
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelSignatureReceived](e => WrappedChannelSignature(e.commitments.localCommit.publishableTxs.commitTx.tx.wtxid)))
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelSignatureReceived](e => WrappedChannelSignature(e.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.wtxid)))
Behaviors.withTimers { timers =>
timers.startTimerWithFixedDelay(FlushEntropy, 30 seconds)
collecting(collector, None)

View File

@ -19,7 +19,7 @@ package fr.acinq.eclair.crypto.keymanager
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.DeterministicWallet.ExtendedPublicKey
import fr.acinq.bitcoin.{ByteVector64, Crypto, DeterministicWallet, Protocol}
import fr.acinq.eclair.channel.{ChannelVersion, LocalParams}
import fr.acinq.eclair.channel.{ChannelConfig, LocalParams}
import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, TransactionWithInputInfo, TxOwner}
import scodec.bits.ByteVector
@ -41,12 +41,14 @@ trait ChannelKeyManager {
def commitmentPoint(channelKeyPath: DeterministicWallet.KeyPath, index: Long): Crypto.PublicKey
def keyPath(localParams: LocalParams, channelVersion: ChannelVersion): DeterministicWallet.KeyPath = if (channelVersion.hasPubkeyKeyPath) {
// deterministic mode: use the funding pubkey to compute the channel key path
ChannelKeyManager.keyPath(fundingPublicKey(localParams.fundingKeyPath))
} else {
// legacy mode: we reuse the funding key path as our channel key path
localParams.fundingKeyPath
def keyPath(localParams: LocalParams, channelConfig: ChannelConfig): DeterministicWallet.KeyPath = {
if (channelConfig.hasOption(ChannelConfig.FundingPubKeyBasedChannelKeyPath)) {
// deterministic mode: use the funding pubkey to compute the channel key path
ChannelKeyManager.keyPath(fundingPublicKey(localParams.fundingKeyPath))
} else {
// legacy mode: we reuse the funding key path as our channel key path
localParams.fundingKeyPath
}
}
/**
@ -114,7 +116,7 @@ object ChannelKeyManager {
val buffer = Crypto.sha256(fundingPubKey.value)
val bis = new ByteArrayInputStream(buffer.toArray)
def next() = Protocol.uint32(bis, ByteOrder.BIG_ENDIAN)
def next(): Long = Protocol.uint32(bis, ByteOrder.BIG_ENDIAN)
DeterministicWallet.KeyPath(Seq(next(), next(), next(), next(), next(), next(), next(), next()))
}

View File

@ -41,7 +41,7 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit
import lock._
val DB_NAME = "channels"
val CURRENT_VERSION = 6
val CURRENT_VERSION = 7
inTransaction { pg =>
using(pg.createStatement()) { statement =>
@ -79,6 +79,23 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit
statement.executeUpdate("ALTER TABLE htlc_infos SET SCHEMA local")
}
def migration67(): Unit = {
migrateTable(pg, pg,
"local.channels",
s"UPDATE local.channels SET data=?, json=?::JSONB WHERE channel_id=?",
(rs, statement) => {
// This forces a re-serialization of the channel data with latest codecs, because as of codecs v3 we don't
// store local commitment signatures anymore, and we want to clean up existing data
val state = stateDataCodec.decode(BitVector(rs.getBytes("data"))).require.value
val data = stateDataCodec.encode(state).require.toByteArray
val json = serialization.writePretty(state)
statement.setBytes(1, data)
statement.setString(2, json)
statement.setString(3, state.channelId.toHex)
}
)(logger)
}
getVersion(statement, DB_NAME) match {
case None =>
statement.executeUpdate("CREATE SCHEMA IF NOT EXISTS local")
@ -95,18 +112,25 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit
migration34(statement)
migration45(statement)
migration56(statement)
migration67()
case Some(v@3) =>
logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION")
migration34(statement)
migration45(statement)
migration56(statement)
migration67()
case Some(v@4) =>
logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION")
migration45(statement)
migration56(statement)
migration67()
case Some(v@5) =>
logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION")
migration56(statement)
migration67()
case Some(v@6) =>
logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION")
migration67()
case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do
case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion")
}

View File

@ -25,6 +25,7 @@ import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics
import fr.acinq.eclair.db.Monitoring.Tags.DbBackends
import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.stateDataCodec
import grizzled.slf4j.Logging
import scodec.bits.BitVector
import java.sql.{Connection, Statement}
@ -34,7 +35,7 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb with Logging {
import SqliteUtils._
val DB_NAME = "channels"
val CURRENT_VERSION = 3
val CURRENT_VERSION = 4
/**
* The SQLite documentation states that "It is not possible to enable or disable foreign key constraints in the middle
@ -59,6 +60,21 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb with Logging {
statement.executeUpdate("ALTER TABLE local_channels ADD COLUMN closed_timestamp INTEGER")
}
def migration34(): Unit = {
migrateTable(sqlite, sqlite,
"local_channels",
s"UPDATE local_channels SET data=? WHERE channel_id=?",
(rs, statement) => {
// This forces a re-serialization of the channel data with latest codecs, because as of codecs v3 we don't
// store local commitment signatures anymore, and we want to clean up existing data
val state = stateDataCodec.decode(BitVector(rs.getBytes("data"))).require.value
val data = stateDataCodec.encode(state).require.toByteArray
statement.setBytes(1, data)
statement.setBytes(2, state.channelId.toArray)
}
)(logger)
}
getVersion(statement, DB_NAME) match {
case None =>
statement.executeUpdate("CREATE TABLE local_channels (channel_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL, is_closed BOOLEAN NOT NULL DEFAULT 0, created_timestamp INTEGER, last_payment_sent_timestamp INTEGER, last_payment_received_timestamp INTEGER, last_connected_timestamp INTEGER, closed_timestamp INTEGER)")
@ -68,9 +84,14 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb with Logging {
logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION")
migration12(statement)
migration23(statement)
migration34()
case Some(v@2) =>
logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION")
migration23(statement)
migration34()
case Some(v@3) =>
logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION")
migration34()
case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do
case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion")
}

View File

@ -136,25 +136,27 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: EclairWa
sender ! Status.Failure(new RuntimeException(s"fundingSatoshis=${c.fundingSatoshis} is too big for the current settings, increase 'eclair.max-funding-satoshis' (see eclair.conf)"))
stay
} else {
val channelVersion = ChannelVersion.pickChannelVersion(d.localFeatures, d.remoteFeatures)
val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, funder = true, c.fundingSatoshis, origin_opt = Some(sender), channelVersion)
val channelConfig = ChannelConfig.standard
val channelFeatures = ChannelFeatures.pickChannelFeatures(d.localFeatures, d.remoteFeatures)
val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, channelFeatures, funder = true, c.fundingSatoshis, 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 = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, channelVersion, c.fundingSatoshis, None)
val channelFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, channelFeatures, c.fundingSatoshis, None)
val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget))
log.info(s"requesting a new channel with fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt} temporaryChannelId=$temporaryChannelId localParams=$localParams")
channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, c.initialRelayFees_opt, localParams, d.peerConnection, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags), channelVersion)
channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, c.initialRelayFees_opt, localParams, d.peerConnection, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags), channelConfig, channelFeatures)
stay using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel))
}
case Event(msg: protocol.OpenChannel, d: ConnectedData) =>
d.channels.get(TemporaryChannelId(msg.temporaryChannelId)) match {
case None =>
val channelVersion = ChannelVersion.pickChannelVersion(d.localFeatures, d.remoteFeatures)
val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, funder = false, fundingAmount = msg.fundingSatoshis, origin_opt = None, channelVersion)
val channelConfig = ChannelConfig.standard
val channelFeatures = ChannelFeatures.pickChannelFeatures(d.localFeatures, d.remoteFeatures)
val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, channelFeatures, funder = false, fundingAmount = msg.fundingSatoshis, origin_opt = None)
val temporaryChannelId = msg.temporaryChannelId
log.info(s"accepting a new channel with temporaryChannelId=$temporaryChannelId localParams=$localParams")
channel ! INPUT_INIT_FUNDEE(temporaryChannelId, localParams, d.peerConnection, d.remoteInit, channelVersion)
channel ! INPUT_INIT_FUNDEE(temporaryChannelId, localParams, d.peerConnection, d.remoteInit, channelConfig, channelFeatures)
channel ! msg
stay using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel))
case Some(_) =>
@ -298,15 +300,14 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: EclairWa
s(e)
}
def createNewChannel(nodeParams: NodeParams, features: Features, funder: Boolean, fundingAmount: Satoshi, origin_opt: Option[ActorRef], channelVersion: ChannelVersion): (ActorRef, LocalParams) = {
val (finalScript, walletStaticPaymentBasepoint) = channelVersion match {
case v if v.paysDirectlyToWallet =>
val walletKey = Helpers.getWalletPaymentBasepoint(wallet)
(Script.write(Script.pay2wpkh(walletKey)), Some(walletKey))
case _ =>
(Helpers.getFinalScriptPubKey(wallet, nodeParams.chainHash), None)
def createNewChannel(nodeParams: NodeParams, initFeatures: Features, channelFeatures: ChannelFeatures, funder: Boolean, fundingAmount: Satoshi, origin_opt: Option[ActorRef]): (ActorRef, LocalParams) = {
val (finalScript, walletStaticPaymentBasepoint) = if (channelFeatures.paysDirectlyToWallet) {
val walletKey = Helpers.getWalletPaymentBasepoint(wallet)
(Script.write(Script.pay2wpkh(walletKey)), Some(walletKey))
} else {
(Helpers.getFinalScriptPubKey(wallet, nodeParams.chainHash), None)
}
val localParams = makeChannelParams(nodeParams, features, finalScript, walletStaticPaymentBasepoint, funder, fundingAmount)
val localParams = makeChannelParams(nodeParams, initFeatures, finalScript, walletStaticPaymentBasepoint, funder, fundingAmount)
val channel = spawnChannel(origin_opt)
(channel, localParams)
}
@ -429,16 +430,10 @@ object Peer {
// @formatter:on
def makeChannelParams(nodeParams: NodeParams, features: Features, defaultFinalScriptPubkey: ByteVector, walletStaticPaymentBasepoint: Option[PublicKey], isFunder: Boolean, fundingAmount: Satoshi): LocalParams = {
// we make sure that funder and fundee key path end differently
val fundingKeyPath = nodeParams.channelKeyManager.newFundingKeyPath(isFunder)
makeChannelParams(nodeParams, features, defaultFinalScriptPubkey, walletStaticPaymentBasepoint, isFunder, fundingAmount, fundingKeyPath)
}
def makeChannelParams(nodeParams: NodeParams, features: Features, defaultFinalScriptPubkey: ByteVector, walletStaticPaymentBasepoint: Option[PublicKey], isFunder: Boolean, fundingAmount: Satoshi, fundingKeyPath: DeterministicWallet.KeyPath): LocalParams = {
def makeChannelParams(nodeParams: NodeParams, initFeatures: Features, defaultFinalScriptPubkey: ByteVector, walletStaticPaymentBasepoint: Option[PublicKey], isFunder: Boolean, fundingAmount: Satoshi): LocalParams = {
LocalParams(
nodeParams.nodeId,
fundingKeyPath,
nodeParams.channelKeyManager.newFundingKeyPath(isFunder), // we make sure that funder and fundee key path end differently
dustLimit = nodeParams.dustLimit,
maxHtlcValueInFlightMsat = nodeParams.maxHtlcValueInFlightMsat,
channelReserve = (fundingAmount * nodeParams.reserveToFundingRatio).max(nodeParams.dustLimit), // BOLT #2: make sure that our reserve is above our dust limit
@ -448,6 +443,6 @@ object Peer {
isFunder = isFunder,
defaultFinalScriptPubKey = defaultFinalScriptPubkey,
walletStaticPaymentBasepoint = walletStaticPaymentBasepoint,
features = features)
initFeatures = initFeatures)
}
}

View File

@ -139,8 +139,12 @@ class PrivateKeySerializer extends CustomSerializerOnly[PrivateKey](_ => {
case _: PrivateKey => JString("XXX")
})
class ChannelVersionSerializer extends CustomSerializerOnly[ChannelVersion](_ => {
case x: ChannelVersion => JString(x.bits.toBin)
class ChannelConfigSerializer extends CustomSerializerOnly[ChannelConfig](_ => {
case x: ChannelConfig => JArray(x.options.toList.map(o => JString(o.name)))
})
class ChannelFeaturesSerializer extends CustomSerializerOnly[ChannelFeatures](_ => {
case channelFeatures: ChannelFeatures => JArray(channelFeatures.activated.map(f => JString(f.rfcName)).toList)
})
class ChannelOpenResponseSerializer extends CustomSerializerOnly[ChannelOpenResponse](_ => {
@ -417,7 +421,8 @@ object JsonSerializers {
new InetSocketAddressSerializer +
new OutPointSerializer +
new OutPointKeySerializer +
new ChannelVersionSerializer +
new ChannelConfigSerializer +
new ChannelFeaturesSerializer +
new ChannelOpenResponseSerializer +
new CommandResponseSerializer +
new InputInfoSerializer +
@ -442,9 +447,7 @@ object JsonSerializers {
JField("activated", JObject(features.activated.map { case (feature, support) =>
feature.rfcName -> JString(support.toString)
}.toList)),
JField("unknown", JArray(features.unknown.map { i =>
JObject(JField("featureBit", JInt(i.bitIndex)))
}.toList))
JField("unknown", JArray(features.unknown.map(u => JInt(u.bitIndex)).toList))
)
}

View File

@ -56,8 +56,8 @@ object IncomingPacket {
case class DecodedOnionPacket[T <: Onion.PacketType](payload: T, next: OnionRoutingPacket)
private def decryptOnion[T <: Onion.PacketType : ClassTag](add: UpdateAddHtlc, privateKey: PrivateKey)(packet: OnionRoutingPacket, packetType: Sphinx.OnionRoutingPacket[T])(implicit log: LoggingAdapter): Either[FailureMessage, DecodedOnionPacket[T]] =
packetType.peel(privateKey, add.paymentHash, packet) match {
private[payment] def decryptOnion[T <: Onion.PacketType : ClassTag](paymentHash: ByteVector32, privateKey: PrivateKey)(packet: OnionRoutingPacket, packetType: Sphinx.OnionRoutingPacket[T])(implicit log: LoggingAdapter): Either[FailureMessage, DecodedOnionPacket[T]] =
packetType.peel(privateKey, paymentHash, packet) match {
case Right(p@Sphinx.DecryptedPacket(payload, nextPacket, _)) =>
OnionCodecs.perHopPayloadCodecByPacketType(packetType, p.isLastPacket).decode(payload.bits) match {
case Attempt.Successful(DecodeResult(perHopPayload: T, _)) => Right(DecodedOnionPacket(perHopPayload, nextPacket))
@ -81,12 +81,12 @@ object IncomingPacket {
* @return whether the payment is to be relayed or if our node is the final recipient (or an error).
*/
def decrypt(add: UpdateAddHtlc, privateKey: PrivateKey)(implicit log: LoggingAdapter): Either[FailureMessage, IncomingPacket] = {
decryptOnion(add, privateKey)(add.onionRoutingPacket, Sphinx.PaymentPacket) match {
decryptOnion(add.paymentHash, privateKey)(add.onionRoutingPacket, Sphinx.PaymentPacket) match {
case Left(failure) => Left(failure)
// NB: we don't validate the ChannelRelayPacket here because its fees and cltv depend on what channel we'll choose to use.
case Right(DecodedOnionPacket(payload: Onion.ChannelRelayPayload, next)) => Right(ChannelRelayPacket(add, payload, next))
case Right(DecodedOnionPacket(payload: Onion.FinalTlvPayload, _)) => payload.records.get[OnionTlv.TrampolineOnion] match {
case Some(OnionTlv.TrampolineOnion(trampolinePacket)) => decryptOnion(add, privateKey)(trampolinePacket, Sphinx.TrampolinePacket) match {
case Some(OnionTlv.TrampolineOnion(trampolinePacket)) => decryptOnion(add.paymentHash, privateKey)(trampolinePacket, Sphinx.TrampolinePacket) match {
case Left(failure) => Left(failure)
case Right(DecodedOnionPacket(innerPayload: Onion.NodeRelayPayload, next)) => validateNodeRelay(add, payload, innerPayload, next)
case Right(DecodedOnionPacket(innerPayload: Onion.FinalPayload, _)) => validateFinal(add, payload, innerPayload)
@ -244,15 +244,21 @@ object OutgoingPacket {
CMD_ADD_HTLC(replyTo, firstAmount, paymentHash, firstExpiry, onion.packet, Origin.Hot(replyTo, upstream), commit = true) -> onion.sharedSecrets
}
def buildHtlcFailure(nodeSecret: PrivateKey, cmd: CMD_FAIL_HTLC, add: UpdateAddHtlc): Either[CannotExtractSharedSecret, UpdateFailHtlc] = {
def buildHtlcFailure(nodeSecret: PrivateKey, reason: Either[ByteVector, FailureMessage], add: UpdateAddHtlc): Either[CannotExtractSharedSecret, ByteVector] = {
Sphinx.PaymentPacket.peel(nodeSecret, add.paymentHash, add.onionRoutingPacket) match {
case Right(Sphinx.DecryptedPacket(_, _, sharedSecret)) =>
val reason = cmd.reason match {
val encryptedReason = reason match {
case Left(forwarded) => Sphinx.FailurePacket.wrap(forwarded, sharedSecret)
case Right(failure) => Sphinx.FailurePacket.create(sharedSecret, failure)
}
Right(UpdateFailHtlc(add.channelId, cmd.id, reason))
Right(encryptedReason)
case Left(_) => Left(CannotExtractSharedSecret(add.channelId, add))
}
}
def buildHtlcFailure(nodeSecret: PrivateKey, cmd: CMD_FAIL_HTLC, add: UpdateAddHtlc): Either[CannotExtractSharedSecret, UpdateFailHtlc] = {
buildHtlcFailure(nodeSecret, cmd.reason, add) map {
encryptedReason => UpdateFailHtlc(add.channelId, cmd.id, encryptedReason)
}
}
}

View File

@ -20,6 +20,7 @@ import fr.acinq.eclair.channel.HasCommitments
import fr.acinq.eclair.wire.internal.channel.version0.ChannelCodecs0
import fr.acinq.eclair.wire.internal.channel.version1.ChannelCodecs1
import fr.acinq.eclair.wire.internal.channel.version2.ChannelCodecs2
import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3
import grizzled.slf4j.Logging
import scodec.Codec
import scodec.codecs.{byte, discriminated}
@ -47,7 +48,7 @@ import scodec.codecs.{byte, discriminated}
val stateDataCodec: Codec[HasCommitments] = ...
* }}}
*
* Notice that the outer class has a visibility restricted to package [[channel]], while the inner class has a
* Notice that the outer class has a visibility restricted to package [[fr.acinq.eclair.wire.internal.channel]], while the inner class has a
* visibility restricted to package [[version0]]. This guarantees that we strictly segregate each codec version,
* while still allowing unitary testing.
*
@ -65,8 +66,9 @@ object ChannelCodecs extends Logging {
* More info here: https://github.com/scodec/scodec/issues/122
*/
val stateDataCodec: Codec[HasCommitments] = discriminated[HasCommitments].by(byte)
.typecase(2, ChannelCodecs2.stateDataCodec)
.typecase(1, ChannelCodecs1.stateDataCodec)
.typecase(0, ChannelCodecs0.stateDataCodec)
.typecase(3, ChannelCodecs3.stateDataCodec)
.typecase(2, ChannelCodecs2.stateDataCodec.decodeOnly)
.typecase(1, ChannelCodecs1.stateDataCodec.decodeOnly)
.typecase(0, ChannelCodecs0.stateDataCodec.decodeOnly)
}

View File

@ -23,10 +23,11 @@ import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSigs, PublishableTxs}
import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._
import fr.acinq.eclair.wire.protocol.UpdateMessage
import scodec.bits.BitVector
import scodec.bits.{BitVector, ByteVector}
import scodec.codecs._
import scodec.{Attempt, Codec}
@ -51,16 +52,16 @@ private[channel] object ChannelCodecs0 {
("path" | keyPathCodec) ::
("parent" | int64)).as[ExtendedPrivateKey].decodeOnly
val channelVersionCodec: Codec[ChannelVersion] = discriminatorWithDefault[ChannelVersion](
discriminator = discriminated[ChannelVersion].by(byte)
.typecase(0x01, bits(ChannelVersion.LENGTH_BITS).as[ChannelVersion])
val channelVersionCodec: Codec[ChannelTypes0.ChannelVersion] = discriminatorWithDefault[ChannelTypes0.ChannelVersion](
discriminator = discriminated[ChannelTypes0.ChannelVersion].by(byte)
.typecase(0x01, bits(ChannelTypes0.ChannelVersion.LENGTH_BITS).as[ChannelTypes0.ChannelVersion])
// NB: 0x02 and 0x03 are *reserved* for backward compatibility reasons
,
fallback = provide(ChannelVersion.ZEROES) // README: DO NOT CHANGE THIS !! old channels don't have a channel version
fallback = provide(ChannelTypes0.ChannelVersion.ZEROES) // README: DO NOT CHANGE THIS !! old channels don't have a channel version
// field and don't support additional features which is why all bits are set to 0.
)
def localParamsCodec(channelVersion: ChannelVersion): Codec[LocalParams] = (
def localParamsCodec(channelVersion: ChannelTypes0.ChannelVersion): Codec[LocalParams] = (
("nodeId" | publicKey) ::
("channelPath" | keyPathCodec) ::
("dustLimit" | satoshi) ::
@ -87,7 +88,8 @@ private[channel] object ChannelCodecs0 {
("paymentBasepoint" | publicKey) ::
("delayedPaymentBasepoint" | publicKey) ::
("htlcBasepoint" | publicKey) ::
("features" | combinedFeaturesCodec)).as[RemoteParams].decodeOnly
("features" | combinedFeaturesCodec) ::
("shutdownScript" | provide[Option[ByteVector]](None))).as[RemoteParams].decodeOnly
val htlcCodec: Codec[DirectedHtlc] = discriminated[DirectedHtlc].by(bool)
.typecase(true, updateAddHtlcCodec.as[IncomingHtlc])
@ -152,10 +154,10 @@ private[channel] object ChannelCodecs0 {
("commitTx" | (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx]) ::
("htlcTxsAndSigs" | listOfN(uint16, htlcTxAndSigsCodec))).as[PublishableTxs].decodeOnly
val localCommitCodec: Codec[LocalCommit] = (
val localCommitCodec: Codec[ChannelTypes0.LocalCommit] = (
("index" | uint64overflow) ::
("spec" | commitmentSpecCodec) ::
("publishableTxs" | publishableTxsCodec)).as[LocalCommit].decodeOnly
("publishableTxs" | publishableTxsCodec)).as[ChannelTypes0.LocalCommit].decodeOnly
val remoteCommitCodec: Codec[RemoteCommit] = (
("index" | uint64overflow) ::
@ -236,7 +238,7 @@ private[channel] object ChannelCodecs0 {
("commitInput" | inputInfoCodec) ::
("remotePerCommitmentSecrets" | ShaChain.shaChainCodec) ::
("channelId" | bytes32)
}).as[Commitments].decodeOnly
}).as[ChannelTypes0.Commitments].decodeOnly.map[Commitments](_.migrate()).decodeOnly
val closingTxProposedCodec: Codec[ClosingTxProposed] = (
("unsignedTx" | closingTxCodec) ::

View File

@ -16,9 +16,15 @@
package fr.acinq.eclair.wire.internal.channel.version0
import fr.acinq.bitcoin.{ByteVector32, OutPoint, Satoshi, Transaction, TxOut}
import fr.acinq.eclair.channel
import com.softwaremill.quicklens._
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, OP_CHECKMULTISIG, OP_PUSHDATA, OutPoint, Satoshi, Script, ScriptWitness, Transaction, TxOut}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.transactions.CommitmentSpec
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.{Feature, FeatureSupport, Features, channel}
import scodec.bits.BitVector
private[channel] object ChannelTypes0 {
@ -102,4 +108,116 @@ private[channel] object ChannelTypes0 {
*/
def migrateClosingTx(tx: Transaction): ClosingTx = ClosingTx(InputInfo(tx.txIn.head.outPoint, TxOut(Satoshi(0), Nil), Nil), tx, None)
case class HtlcTxAndSigs(txinfo: HtlcTx, localSig: ByteVector64, remoteSig: ByteVector64)
case class PublishableTxs(commitTx: CommitTx, htlcTxsAndSigs: List[HtlcTxAndSigs])
// Before version3, we stored fully signed local transactions (commit tx and htlc txs). It meant that someone gaining
// access to the database could publish revoked commit txs, so we changed that to only store unsigned txs and remote
// signatures.
case class LocalCommit(index: Long, spec: CommitmentSpec, publishableTxs: PublishableTxs) {
def migrate(remoteFundingPubKey: PublicKey): channel.LocalCommit = {
val remoteSig = extractRemoteSig(publishableTxs.commitTx, remoteFundingPubKey)
val unsignedCommitTx = publishableTxs.commitTx.modify(_.tx.txIn.each.witness).setTo(ScriptWitness.empty)
val commitTxAndRemoteSig = CommitTxAndRemoteSig(unsignedCommitTx, remoteSig)
val htlcTxsAndRemoteSigs = publishableTxs.htlcTxsAndSigs map {
case HtlcTxAndSigs(htlcTx: HtlcSuccessTx, _, remoteSig) =>
val unsignedHtlcTx = htlcTx.modify(_.tx.txIn.each.witness).setTo(ScriptWitness.empty)
HtlcTxAndRemoteSig(unsignedHtlcTx, remoteSig)
case HtlcTxAndSigs(htlcTx: HtlcTimeoutTx, _, remoteSig) =>
val unsignedHtlcTx = htlcTx.modify(_.tx.txIn.each.witness).setTo(ScriptWitness.empty)
HtlcTxAndRemoteSig(unsignedHtlcTx, remoteSig)
}
channel.LocalCommit(index, spec, commitTxAndRemoteSig, htlcTxsAndRemoteSigs)
}
private def extractRemoteSig(commitTx: CommitTx, remoteFundingPubKey: PublicKey): ByteVector64 = {
require(commitTx.tx.txIn.size == 1, s"commit tx must have exactly one input, found ${commitTx.tx.txIn.size}")
val ScriptWitness(Seq(_, sig1, sig2, redeemScript)) = commitTx.tx.txIn.head.witness
val _ :: OP_PUSHDATA(pub1, _) :: OP_PUSHDATA(pub2, _) :: _ :: OP_CHECKMULTISIG :: Nil = Script.parse(redeemScript)
require(pub1 == remoteFundingPubKey.value || pub2 == remoteFundingPubKey.value, "unrecognized funding pubkey")
if (pub1 == remoteFundingPubKey.value) {
Crypto.der2compact(sig1)
} else {
Crypto.der2compact(sig2)
}
}
}
// Before version3, we had a ChannelVersion field describing what channel features were activated. It was mixing
// official features (static_remotekey, anchor_outputs) and internal features (channel key derivation scheme).
// We separated this into two separate fields in version3:
// - a channel type field containing the channel Bolt 9 features
// - an internal channel configuration field
case class ChannelVersion(bits: BitVector) {
// @formatter:off
def isSet(bit: Int): Boolean = bits.reverse.get(bit)
def |(other: ChannelVersion): ChannelVersion = ChannelVersion(bits | other.bits)
def hasPubkeyKeyPath: Boolean = isSet(ChannelVersion.USE_PUBKEY_KEYPATH_BIT)
def hasStaticRemotekey: Boolean = isSet(ChannelVersion.USE_STATIC_REMOTEKEY_BIT)
def hasAnchorOutputs: Boolean = isSet(ChannelVersion.USE_ANCHOR_OUTPUTS_BIT)
def paysDirectlyToWallet: Boolean = hasStaticRemotekey && !hasAnchorOutputs
// @formatter:on
}
object ChannelVersion {
import scodec.bits._
val LENGTH_BITS: Int = 4 * 8
private val USE_PUBKEY_KEYPATH_BIT = 0 // bit numbers start at 0
private val USE_STATIC_REMOTEKEY_BIT = 1
private val USE_ANCHOR_OUTPUTS_BIT = 2
def fromBit(bit: Int): ChannelVersion = ChannelVersion(BitVector.low(LENGTH_BITS).set(bit).reverse)
val ZEROES = ChannelVersion(bin"00000000000000000000000000000000")
val STANDARD = ZEROES | fromBit(USE_PUBKEY_KEYPATH_BIT)
val STATIC_REMOTEKEY = STANDARD | fromBit(USE_STATIC_REMOTEKEY_BIT) // PUBKEY_KEYPATH + STATIC_REMOTEKEY
val ANCHOR_OUTPUTS = STATIC_REMOTEKEY | fromBit(USE_ANCHOR_OUTPUTS_BIT) // PUBKEY_KEYPATH + STATIC_REMOTEKEY + ANCHOR_OUTPUTS
}
case class Commitments(channelVersion: ChannelVersion,
localParams: LocalParams, remoteParams: RemoteParams,
channelFlags: Byte,
localCommit: LocalCommit, remoteCommit: RemoteCommit,
localChanges: LocalChanges, remoteChanges: RemoteChanges,
localNextHtlcId: Long, remoteNextHtlcId: Long,
originChannels: Map[Long, Origin],
remoteNextCommitInfo: Either[WaitingForRevocation, PublicKey],
commitInput: InputInfo,
remotePerCommitmentSecrets: ShaChain, channelId: ByteVector32) {
def migrate(): channel.Commitments = {
val channelConfig = if (channelVersion.hasPubkeyKeyPath) {
ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath)
} else {
ChannelConfig()
}
val isWumboChannel = commitInput.txOut.amount > Satoshi(16777215)
val baseChannelFeatures: Set[Feature] = if (isWumboChannel) Set(Features.Wumbo) else Set.empty
val commitmentFeatures: Set[Feature] = if (channelVersion.hasAnchorOutputs) {
Set(Features.StaticRemoteKey, Features.AnchorOutputs)
} else if (channelVersion.hasStaticRemotekey) {
Set(Features.StaticRemoteKey)
} else {
Set.empty
}
val channelFeatures = ChannelFeatures(baseChannelFeatures ++ commitmentFeatures)
channel.Commitments(
channelId,
channelConfig, channelFeatures,
localParams, remoteParams,
channelFlags,
localCommit.migrate(remoteParams.fundingPubKey), remoteCommit,
localChanges, remoteChanges,
localNextHtlcId, remoteNextHtlcId,
originChannels,
remoteNextCommitInfo,
commitInput,
remotePerCommitmentSecrets)
}
}
}

View File

@ -23,10 +23,12 @@ import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, OutgoingHtlc}
import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0
import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSigs, PublishableTxs}
import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._
import fr.acinq.eclair.wire.protocol.UpdateMessage
import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0
import scodec.bits.ByteVector
import scodec.codecs._
import scodec.{Attempt, Codec}
@ -43,9 +45,9 @@ private[channel] object ChannelCodecs1 {
("path" | keyPathCodec) ::
("parent" | int64)).as[ExtendedPrivateKey]
val channelVersionCodec: Codec[ChannelVersion] = bits(ChannelVersion.LENGTH_BITS).as[ChannelVersion]
val channelVersionCodec: Codec[ChannelTypes0.ChannelVersion] = bits(ChannelTypes0.ChannelVersion.LENGTH_BITS).as[ChannelTypes0.ChannelVersion]
def localParamsCodec(channelVersion: ChannelVersion): Codec[LocalParams] = (
def localParamsCodec(channelVersion: ChannelTypes0.ChannelVersion): Codec[LocalParams] = (
("nodeId" | publicKey) ::
("channelPath" | keyPathCodec) ::
("dustLimit" | satoshi) ::
@ -72,7 +74,8 @@ private[channel] object ChannelCodecs1 {
("paymentBasepoint" | publicKey) ::
("delayedPaymentBasepoint" | publicKey) ::
("htlcBasepoint" | publicKey) ::
("features" | combinedFeaturesCodec)).as[RemoteParams]
("features" | combinedFeaturesCodec) ::
("shutdownScript" | provide[Option[ByteVector]](None))).as[RemoteParams]
def setCodec[T](codec: Codec[T]): Codec[Set[T]] = listOfN(uint16, codec).xmap(_.toSet, _.toList)
@ -125,10 +128,10 @@ private[channel] object ChannelCodecs1 {
("commitTx" | (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx]) ::
("htlcTxsAndSigs" | listOfN(uint16, htlcTxAndSigsCodec))).as[PublishableTxs]
val localCommitCodec: Codec[LocalCommit] = (
val localCommitCodec: Codec[ChannelTypes0.LocalCommit] = (
("index" | uint64overflow) ::
("spec" | commitmentSpecCodec) ::
("publishableTxs" | publishableTxsCodec)).as[LocalCommit]
("publishableTxs" | publishableTxsCodec)).as[ChannelTypes0.LocalCommit].decodeOnly
val remoteCommitCodec: Codec[RemoteCommit] = (
("index" | uint64overflow) ::
@ -197,7 +200,7 @@ private[channel] object ChannelCodecs1 {
("commitInput" | inputInfoCodec) ::
("remotePerCommitmentSecrets" | byteAligned(ShaChain.shaChainCodec)) ::
("channelId" | bytes32)
}).as[Commitments]
}).as[ChannelTypes0.Commitments].decodeOnly.map[Commitments](_.migrate()).decodeOnly
val closingTxProposedCodec: Codec[ClosingTxProposed] = (
("unsignedTx" | closingTxCodec) ::

View File

@ -23,9 +23,12 @@ import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, OutgoingHtlc}
import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0
import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSigs, PublishableTxs}
import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._
import fr.acinq.eclair.wire.protocol.UpdateMessage
import scodec.bits.ByteVector
import scodec.codecs._
import scodec.{Attempt, Codec}
@ -42,9 +45,9 @@ private[channel] object ChannelCodecs2 {
("path" | keyPathCodec) ::
("parent" | int64)).as[ExtendedPrivateKey]
val channelVersionCodec: Codec[ChannelVersion] = bits(ChannelVersion.LENGTH_BITS).as[ChannelVersion]
val channelVersionCodec: Codec[ChannelTypes0.ChannelVersion] = bits(ChannelTypes0.ChannelVersion.LENGTH_BITS).as[ChannelTypes0.ChannelVersion]
def localParamsCodec(channelVersion: ChannelVersion): Codec[LocalParams] = (
def localParamsCodec(channelVersion: ChannelTypes0.ChannelVersion): Codec[LocalParams] = (
("nodeId" | publicKey) ::
("channelPath" | keyPathCodec) ::
("dustLimit" | satoshi) ::
@ -71,7 +74,8 @@ private[channel] object ChannelCodecs2 {
("paymentBasepoint" | publicKey) ::
("delayedPaymentBasepoint" | publicKey) ::
("htlcBasepoint" | publicKey) ::
("features" | combinedFeaturesCodec)).as[RemoteParams]
("features" | combinedFeaturesCodec) ::
("shutdownScript" | provide[Option[ByteVector]](None))).as[RemoteParams]
def setCodec[T](codec: Codec[T]): Codec[Set[T]] = listOfN(uint16, codec).xmap(_.toSet, _.toList)
@ -159,10 +163,10 @@ private[channel] object ChannelCodecs2 {
("commitTx" | (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx]) ::
("htlcTxsAndSigs" | listOfN(uint16, htlcTxAndSigsCodec))).as[PublishableTxs]
val localCommitCodec: Codec[LocalCommit] = (
val localCommitCodec: Codec[ChannelTypes0.LocalCommit] = (
("index" | uint64overflow) ::
("spec" | commitmentSpecCodec) ::
("publishableTxs" | publishableTxsCodec)).as[LocalCommit]
("publishableTxs" | publishableTxsCodec)).as[ChannelTypes0.LocalCommit].decodeOnly
val remoteCommitCodec: Codec[RemoteCommit] = (
("index" | uint64overflow) ::
@ -231,7 +235,7 @@ private[channel] object ChannelCodecs2 {
("commitInput" | inputInfoCodec) ::
("remotePerCommitmentSecrets" | byteAligned(ShaChain.shaChainCodec)) ::
("channelId" | bytes32)
}).as[Commitments]
}).as[ChannelTypes0.Commitments].decodeOnly.map[Commitments](_.migrate()).decodeOnly
val closingTxProposedCodec: Codec[ClosingTxProposed] = (
("unsignedTx" | closingTxCodec) ::

View File

@ -0,0 +1,348 @@
/*
* Copyright 2021 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.wire.internal.channel.version3
import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, KeyPath}
import fr.acinq.bitcoin.{OutPoint, Transaction, TxOut}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, OutgoingHtlc}
import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._
import fr.acinq.eclair.wire.protocol.UpdateMessage
import fr.acinq.eclair.{FeatureSupport, Features, MilliSatoshi}
import scodec.bits.{BitVector, ByteVector}
import scodec.codecs._
import scodec.{Attempt, Codec}
private[channel] object ChannelCodecs3 {
private[version3] object Codecs {
val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => new KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath]
val extendedPrivateKeyCodec: Codec[ExtendedPrivateKey] = (
("secretkeybytes" | bytes32) ::
("chaincode" | bytes32) ::
("depth" | uint16) ::
("path" | keyPathCodec) ::
("parent" | int64)).as[ExtendedPrivateKey]
val channelConfigCodec: Codec[ChannelConfig] = lengthDelimited(bytes).xmap(b => {
val activated: Set[ChannelConfigOption] = b.bits.toIndexedSeq.reverse.zipWithIndex.collect {
case (true, 0) => ChannelConfig.FundingPubKeyBasedChannelKeyPath
}.toSet
ChannelConfig(activated)
}, cfg => {
val indices = cfg.options.map(_.supportBit)
if (indices.isEmpty) {
ByteVector.empty
} else {
// NB: when converting from BitVector to ByteVector, scodec pads right instead of left, so we make sure we pad to bytes *before* setting bits.
var buffer = BitVector.fill(indices.max + 1)(high = false).bytes.bits
indices.foreach(i => buffer = buffer.set(i))
buffer.reverse.bytes
}
})
/** We use the same encoding as init features, even if we don't need the distinction between mandatory and optional */
val channelFeaturesCodec: Codec[ChannelFeatures] = lengthDelimited(bytes).xmap(
(b: ByteVector) => ChannelFeatures(Features(b).activated.keySet), // we make no difference between mandatory/optional, both are considered activated
(cf: ChannelFeatures) => Features(cf.activated.map(f => f -> FeatureSupport.Mandatory).toMap).toByteVector // we encode features as mandatory, by convention
)
def localParamsCodec(channelFeatures: ChannelFeatures): Codec[LocalParams] = (
("nodeId" | publicKey) ::
("channelPath" | keyPathCodec) ::
("dustLimit" | satoshi) ::
("maxHtlcValueInFlightMsat" | uint64) ::
("channelReserve" | satoshi) ::
("htlcMinimum" | millisatoshi) ::
("toSelfDelay" | cltvExpiryDelta) ::
("maxAcceptedHtlcs" | uint16) ::
("isFunder" | bool8) ::
("defaultFinalScriptPubKey" | lengthDelimited(bytes)) ::
("walletStaticPaymentBasepoint" | optional(provide(channelFeatures.paysDirectlyToWallet), publicKey)) ::
("features" | combinedFeaturesCodec)).as[LocalParams]
val remoteParamsCodec: Codec[RemoteParams] = (
("nodeId" | publicKey) ::
("dustLimit" | satoshi) ::
("maxHtlcValueInFlightMsat" | uint64) ::
("channelReserve" | satoshi) ::
("htlcMinimum" | millisatoshi) ::
("toSelfDelay" | cltvExpiryDelta) ::
("maxAcceptedHtlcs" | uint16) ::
("fundingPubKey" | publicKey) ::
("revocationBasepoint" | publicKey) ::
("paymentBasepoint" | publicKey) ::
("delayedPaymentBasepoint" | publicKey) ::
("htlcBasepoint" | publicKey) ::
("features" | combinedFeaturesCodec) ::
("shutdownScript" | optional(bool8, bytes))).as[RemoteParams]
def setCodec[T](codec: Codec[T]): Codec[Set[T]] = listOfN(uint16, codec).xmap(_.toSet, _.toList)
val htlcCodec: Codec[DirectedHtlc] = discriminated[DirectedHtlc].by(bool8)
.typecase(true, lengthDelimited(updateAddHtlcCodec).as[IncomingHtlc])
.typecase(false, lengthDelimited(updateAddHtlcCodec).as[OutgoingHtlc])
val commitmentSpecCodec: Codec[CommitmentSpec] = (
("htlcs" | setCodec(htlcCodec)) ::
("feeratePerKw" | feeratePerKw) ::
("toLocal" | millisatoshi) ::
("toRemote" | millisatoshi)).as[CommitmentSpec]
val outPointCodec: Codec[OutPoint] = lengthDelimited(bytes.xmap(d => OutPoint.read(d.toArray), d => OutPoint.write(d)))
val txOutCodec: Codec[TxOut] = lengthDelimited(bytes.xmap(d => TxOut.read(d.toArray), d => TxOut.write(d)))
val txCodec: Codec[Transaction] = lengthDelimited(bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d)))
val inputInfoCodec: Codec[InputInfo] = (
("outPoint" | outPointCodec) ::
("txOut" | txOutCodec) ::
("redeemScript" | lengthDelimited(bytes))).as[InputInfo]
val outputInfoCodec: Codec[OutputInfo] = (
("index" | uint32) ::
("amount" | satoshi) ::
("scriptPubKey" | lengthDelimited(bytes))).as[OutputInfo]
val commitTxCodec: Codec[CommitTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx]
val htlcSuccessTxCodec: Codec[HtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | uint64overflow)).as[HtlcSuccessTx]
val htlcTimeoutTxCodec: Codec[HtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow)).as[HtlcTimeoutTx]
val htlcDelayedTxCodec: Codec[HtlcDelayedTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcDelayedTx]
val claimHtlcSuccessTxCodec: Codec[ClaimHtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow)).as[ClaimHtlcSuccessTx]
val claimHtlcTimeoutTxCodec: Codec[ClaimHtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow)).as[ClaimHtlcTimeoutTx]
val claimLocalDelayedOutputTxCodec: Codec[ClaimLocalDelayedOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimLocalDelayedOutputTx]
val claimP2WPKHOutputTxCodec: Codec[ClaimP2WPKHOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimP2WPKHOutputTx]
val claimRemoteDelayedOutputTxCodec: Codec[ClaimRemoteDelayedOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimRemoteDelayedOutputTx]
val mainPenaltyTxCodec: Codec[MainPenaltyTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[MainPenaltyTx]
val htlcPenaltyTxCodec: Codec[HtlcPenaltyTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcPenaltyTx]
val claimHtlcDelayedOutputPenaltyTxCodec: Codec[ClaimHtlcDelayedOutputPenaltyTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimHtlcDelayedOutputPenaltyTx]
val claimLocalAnchorOutputTxCodec: Codec[ClaimLocalAnchorOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimLocalAnchorOutputTx]
val claimRemoteAnchorOutputTxCodec: Codec[ClaimRemoteAnchorOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimRemoteAnchorOutputTx]
val closingTxCodec: Codec[ClosingTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("outputIndex" | optional(bool8, outputInfoCodec))).as[ClosingTx]
val txWithInputInfoCodec: Codec[TransactionWithInputInfo] = discriminated[TransactionWithInputInfo].by(uint16)
.typecase(0x01, commitTxCodec)
.typecase(0x02, htlcSuccessTxCodec)
.typecase(0x03, htlcTimeoutTxCodec)
.typecase(0x04, claimHtlcSuccessTxCodec)
.typecase(0x05, claimHtlcTimeoutTxCodec)
.typecase(0x06, claimP2WPKHOutputTxCodec)
.typecase(0x07, claimLocalDelayedOutputTxCodec)
.typecase(0x08, mainPenaltyTxCodec)
.typecase(0x09, htlcPenaltyTxCodec)
.typecase(0x10, closingTxCodec)
.typecase(0x11, claimLocalAnchorOutputTxCodec)
.typecase(0x12, claimRemoteAnchorOutputTxCodec)
.typecase(0x13, claimRemoteDelayedOutputTxCodec)
.typecase(0x14, claimHtlcDelayedOutputPenaltyTxCodec)
.typecase(0x15, htlcDelayedTxCodec)
val claimRemoteCommitMainOutputTxCodec: Codec[ClaimRemoteCommitMainOutputTx] = discriminated[ClaimRemoteCommitMainOutputTx].by(uint8)
.typecase(0x01, claimP2WPKHOutputTxCodec)
.typecase(0x02, claimRemoteDelayedOutputTxCodec)
val claimAnchorOutputTxCodec: Codec[ClaimAnchorOutputTx] = discriminated[ClaimAnchorOutputTx].by(uint8)
.typecase(0x01, claimLocalAnchorOutputTxCodec)
.typecase(0x02, claimRemoteAnchorOutputTxCodec)
val htlcTxCodec: Codec[HtlcTx] = discriminated[HtlcTx].by(uint8)
.typecase(0x01, htlcSuccessTxCodec)
.typecase(0x02, htlcTimeoutTxCodec)
val claimHtlcTxCodec: Codec[ClaimHtlcTx] = discriminated[ClaimHtlcTx].by(uint8)
.typecase(0x01, claimHtlcSuccessTxCodec)
.typecase(0x02, claimHtlcTimeoutTxCodec)
val htlcTxsAndRemoteSigsCodec: Codec[HtlcTxAndRemoteSig] = (
("txinfo" | htlcTxCodec) ::
("remoteSig" | bytes64)).as[HtlcTxAndRemoteSig]
val commitTxAndRemoteSigCodec: Codec[CommitTxAndRemoteSig] = (
("commitTx" | commitTxCodec) ::
("remoteSig" | bytes64)).as[CommitTxAndRemoteSig]
val localCommitCodec: Codec[LocalCommit] = (
("index" | uint64overflow) ::
("spec" | commitmentSpecCodec) ::
("commitTxAndRemoteSig" | commitTxAndRemoteSigCodec) ::
("htlcTxsAndRemoteSigs" | listOfN(uint16, htlcTxsAndRemoteSigsCodec))).as[LocalCommit]
val remoteCommitCodec: Codec[RemoteCommit] = (
("index" | uint64overflow) ::
("spec" | commitmentSpecCodec) ::
("txid" | bytes32) ::
("remotePerCommitmentPoint" | publicKey)).as[RemoteCommit]
val updateMessageCodec: Codec[UpdateMessage] = lengthDelimited(lightningMessageCodec.narrow[UpdateMessage](f => Attempt.successful(f.asInstanceOf[UpdateMessage]), g => g))
val localChangesCodec: Codec[LocalChanges] = (
("proposed" | listOfN(uint16, updateMessageCodec)) ::
("signed" | listOfN(uint16, updateMessageCodec)) ::
("acked" | listOfN(uint16, updateMessageCodec))).as[LocalChanges]
val remoteChangesCodec: Codec[RemoteChanges] = (
("proposed" | listOfN(uint16, updateMessageCodec)) ::
("acked" | listOfN(uint16, updateMessageCodec)) ::
("signed" | listOfN(uint16, updateMessageCodec))).as[RemoteChanges]
val waitingForRevocationCodec: Codec[WaitingForRevocation] = (
("nextRemoteCommit" | remoteCommitCodec) ::
("sent" | lengthDelimited(commitSigCodec)) ::
("sentAfterLocalCommitIndex" | uint64overflow) ::
("reSignAsap" | bool8)).as[WaitingForRevocation]
val localColdCodec: Codec[Origin.LocalCold] = ("id" | uuid).as[Origin.LocalCold]
val localCodec: Codec[Origin.Local] = localColdCodec.xmap[Origin.Local](o => o: Origin.Local, o => Origin.LocalCold(o.id))
val relayedColdCodec: Codec[Origin.ChannelRelayedCold] = (
("originChannelId" | bytes32) ::
("originHtlcId" | int64) ::
("amountIn" | millisatoshi) ::
("amountOut" | millisatoshi)).as[Origin.ChannelRelayedCold]
val relayedCodec: Codec[Origin.ChannelRelayed] = relayedColdCodec.xmap[Origin.ChannelRelayed](o => o: Origin.ChannelRelayed, o => Origin.ChannelRelayedCold(o.originChannelId, o.originHtlcId, o.amountIn, o.amountOut))
val trampolineRelayedColdCodec: Codec[Origin.TrampolineRelayedCold] = listOfN(uint16, bytes32 ~ int64).as[Origin.TrampolineRelayedCold]
val trampolineRelayedCodec: Codec[Origin.TrampolineRelayed] = trampolineRelayedColdCodec.xmap[Origin.TrampolineRelayed](o => o: Origin.TrampolineRelayed, o => Origin.TrampolineRelayedCold(o.htlcs))
val originCodec: Codec[Origin] = discriminated[Origin].by(uint16)
.typecase(0x02, relayedCodec)
.typecase(0x03, localCodec)
.typecase(0x04, trampolineRelayedCodec)
def mapCodec[K, V](keyCodec: Codec[K], valueCodec: Codec[V]): Codec[Map[K, V]] = listOfN(uint16, keyCodec ~ valueCodec).xmap(_.toMap, _.toList)
val originsMapCodec: Codec[Map[Long, Origin]] = mapCodec(int64, originCodec)
val spentMapCodec: Codec[Map[OutPoint, Transaction]] = mapCodec(outPointCodec, txCodec)
val commitmentsCodec: Codec[Commitments] = (
("channelId" | bytes32) ::
("channelConfig" | channelConfigCodec) ::
(("channelFeatures" | channelFeaturesCodec) >>:~ { channelFeatures =>
("localParams" | localParamsCodec(channelFeatures)) ::
("remoteParams" | remoteParamsCodec) ::
("channelFlags" | byte) ::
("localCommit" | localCommitCodec) ::
("remoteCommit" | remoteCommitCodec) ::
("localChanges" | localChangesCodec) ::
("remoteChanges" | remoteChangesCodec) ::
("localNextHtlcId" | uint64overflow) ::
("remoteNextHtlcId" | uint64overflow) ::
("originChannels" | originsMapCodec) ::
("remoteNextCommitInfo" | either(bool8, waitingForRevocationCodec, publicKey)) ::
("commitInput" | inputInfoCodec) ::
("remotePerCommitmentSecrets" | byteAligned(ShaChain.shaChainCodec))
})).as[Commitments]
val closingTxProposedCodec: Codec[ClosingTxProposed] = (
("unsignedTx" | closingTxCodec) ::
("localClosingSigned" | lengthDelimited(closingSignedCodec))).as[ClosingTxProposed]
val localCommitPublishedCodec: Codec[LocalCommitPublished] = (
("commitTx" | txCodec) ::
("claimMainDelayedOutputTx" | optional(bool8, claimLocalDelayedOutputTxCodec)) ::
("htlcTxs" | mapCodec(outPointCodec, optional(bool8, htlcTxCodec))) ::
("claimHtlcDelayedTx" | listOfN(uint16, htlcDelayedTxCodec)) ::
("claimAnchorTxs" | listOfN(uint16, claimAnchorOutputTxCodec)) ::
("spent" | spentMapCodec)).as[LocalCommitPublished]
val remoteCommitPublishedCodec: Codec[RemoteCommitPublished] = (
("commitTx" | txCodec) ::
("claimMainOutputTx" | optional(bool8, claimRemoteCommitMainOutputTxCodec)) ::
("claimHtlcTxs" | mapCodec(outPointCodec, optional(bool8, claimHtlcTxCodec))) ::
("claimAnchorTxs" | listOfN(uint16, claimAnchorOutputTxCodec)) ::
("spent" | spentMapCodec)).as[RemoteCommitPublished]
val revokedCommitPublishedCodec: Codec[RevokedCommitPublished] = (
("commitTx" | txCodec) ::
("claimMainOutputTx" | optional(bool8, claimRemoteCommitMainOutputTxCodec)) ::
("mainPenaltyTx" | optional(bool8, mainPenaltyTxCodec)) ::
("htlcPenaltyTxs" | listOfN(uint16, htlcPenaltyTxCodec)) ::
("claimHtlcDelayedPenaltyTxs" | listOfN(uint16, claimHtlcDelayedOutputPenaltyTxCodec)) ::
("spent" | spentMapCodec)).as[RevokedCommitPublished]
val DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = (
("commitments" | commitmentsCodec) ::
("fundingTx" | optional(bool8, txCodec)) ::
("initialRelayFees" | provide(Option.empty[(MilliSatoshi, Int)])) ::
("waitingSince" | int64) ::
("deferred" | optional(bool8, lengthDelimited(fundingLockedCodec))) ::
("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED]
val DATA_WAIT_FOR_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = (
("commitments" | commitmentsCodec) ::
("shortChannelId" | shortchannelid) ::
("lastSent" | lengthDelimited(fundingLockedCodec)) ::
("initialRelayFees" | provide(Option.empty[(MilliSatoshi, Int)]))).as[DATA_WAIT_FOR_FUNDING_LOCKED]
val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = (
("commitments" | commitmentsCodec) ::
("shortChannelId" | shortchannelid) ::
("buried" | bool8) ::
("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) ::
("channelUpdate" | lengthDelimited(channelUpdateCodec)) ::
("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) ::
("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec)))).as[DATA_NORMAL]
val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = (
("commitments" | commitmentsCodec) ::
("localShutdown" | lengthDelimited(shutdownCodec)) ::
("remoteShutdown" | lengthDelimited(shutdownCodec))).as[DATA_SHUTDOWN]
val DATA_NEGOTIATING_Codec: Codec[DATA_NEGOTIATING] = (
("commitments" | commitmentsCodec) ::
("localShutdown" | lengthDelimited(shutdownCodec)) ::
("remoteShutdown" | lengthDelimited(shutdownCodec)) ::
("closingTxProposed" | listOfN(uint16, listOfN(uint16, lengthDelimited(closingTxProposedCodec)))) ::
("bestUnpublishedClosingTx_opt" | optional(bool8, closingTxCodec))).as[DATA_NEGOTIATING]
val DATA_CLOSING_Codec: Codec[DATA_CLOSING] = (
("commitments" | commitmentsCodec) ::
("fundingTx" | optional(bool8, txCodec)) ::
("waitingSince" | int64) ::
("mutualCloseProposed" | listOfN(uint16, closingTxCodec)) ::
("mutualClosePublished" | listOfN(uint16, closingTxCodec)) ::
("localCommitPublished" | optional(bool8, localCommitPublishedCodec)) ::
("remoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) ::
("nextRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) ::
("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) ::
("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).as[DATA_CLOSING]
val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = (
("commitments" | commitmentsCodec) ::
("remoteChannelReestablish" | channelReestablishCodec)).as[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT]
}
val stateDataCodec: Codec[HasCommitments] = discriminated[HasCommitments].by(uint16)
.typecase(0x00, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec)
.typecase(0x01, Codecs.DATA_WAIT_FOR_FUNDING_LOCKED_Codec)
.typecase(0x02, Codecs.DATA_NORMAL_Codec)
.typecase(0x03, Codecs.DATA_SHUTDOWN_Codec)
.typecase(0x04, Codecs.DATA_NEGOTIATING_Codec)
.typecase(0x05, Codecs.DATA_CLOSING_Codec)
.typecase(0x06, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec)
}

View File

@ -329,16 +329,27 @@ class FeaturesSpec extends AnyFunSuite {
}
test("'knownFeatures' contains all our known features (reflection test)") {
import scala.reflect.ClassTag
import scala.reflect.runtime.universe._
import scala.reflect.runtime.{universe => runtime}
val mirror = runtime.runtimeMirror(ClassLoader.getSystemClassLoader)
val subclasses = typeOf[Feature].typeSymbol.asClass.knownDirectSubclasses
val knownFeatures = subclasses.map({ desc =>
val mod = mirror.staticModule(desc.asClass.fullName)
mirror.reflectModule(mod).instance.asInstanceOf[Feature]
})
assert((knownFeatures -- Features.knownFeatures).isEmpty)
def extract[T: TypeTag](container: T)(implicit c: ClassTag[T]): Set[Feature] = {
typeOf[T].decls.filter(_.isPublic).flatMap(symbol => {
if (symbol.isTerm && symbol.isModule) {
mirror.reflectModule(symbol.asModule).instance match {
case f: Feature => Some(f)
case _ => None
}
} else {
None
}
}).toSet
}
val declaredFeatures = extract(Features)
assert(declaredFeatures.nonEmpty)
assert(declaredFeatures.removedAll(knownFeatures).isEmpty)
}
}

View File

@ -20,7 +20,7 @@ import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi, SatoshiLong, Script}
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
import fr.acinq.eclair.Features._
import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratesPerKw, OnChainFeeConf, _}
import fr.acinq.eclair.channel.LocalParams
import fr.acinq.eclair.channel.{ChannelFeatures, LocalParams}
import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager}
import fr.acinq.eclair.io.{Peer, PeerConnection}
import fr.acinq.eclair.router.Router.RouterConf

View File

@ -19,8 +19,8 @@ package fr.acinq.eclair.blockchain.fee
import fr.acinq.bitcoin.SatoshiLong
import fr.acinq.eclair.TestConstants.TestFeeEstimator
import fr.acinq.eclair.blockchain.CurrentFeerates
import fr.acinq.eclair.channel.ChannelVersion
import fr.acinq.eclair.randomKey
import fr.acinq.eclair.channel.ChannelFeatures
import fr.acinq.eclair.{FeatureSupport, Features, randomKey}
import org.scalatest.funsuite.AnyFunSuite
class FeeEstimatorSpec extends AnyFunSuite {
@ -36,19 +36,19 @@ class FeeEstimatorSpec extends AnyFunSuite {
test("get commitment feerate") {
val feeEstimator = new TestFeeEstimator()
val channelVersion = ChannelVersion.STANDARD
val channelFeatures = ChannelFeatures()
val feeConf = OnChainFeeConf(FeeTargets(1, 2, 1, 1), feeEstimator, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, FeerateTolerance(0.5, 2.0, FeeratePerKw(2500 sat)), Map.empty)
feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = FeeratePerKw(5000 sat)))
assert(feeConf.getCommitmentFeerate(randomKey().publicKey, channelVersion, 100000 sat, None) === FeeratePerKw(5000 sat))
assert(feeConf.getCommitmentFeerate(randomKey().publicKey, channelFeatures, 100000 sat, None) === FeeratePerKw(5000 sat))
val currentFeerates = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = FeeratePerKw(4000 sat)))
assert(feeConf.getCommitmentFeerate(randomKey().publicKey, channelVersion, 100000 sat, Some(currentFeerates)) === FeeratePerKw(4000 sat))
assert(feeConf.getCommitmentFeerate(randomKey().publicKey, channelFeatures, 100000 sat, Some(currentFeerates)) === FeeratePerKw(4000 sat))
}
test("get commitment feerate (anchor outputs)") {
val feeEstimator = new TestFeeEstimator()
val channelVersion = ChannelVersion.ANCHOR_OUTPUTS
val channelFeatures = ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)
val defaultNodeId = randomKey().publicKey
val defaultMaxCommitFeerate = FeeratePerKw(2500 sat)
val overrideNodeId = randomKey().publicKey
@ -56,23 +56,23 @@ class FeeEstimatorSpec extends AnyFunSuite {
val feeConf = OnChainFeeConf(FeeTargets(1, 2, 1, 1), feeEstimator, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, FeerateTolerance(0.5, 2.0, defaultMaxCommitFeerate), Map(overrideNodeId -> FeerateTolerance(0.5, 2.0, overrideMaxCommitFeerate)))
feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2))
assert(feeConf.getCommitmentFeerate(defaultNodeId, channelVersion, 100000 sat, None) === defaultMaxCommitFeerate / 2)
assert(feeConf.getCommitmentFeerate(defaultNodeId, channelFeatures, 100000 sat, None) === defaultMaxCommitFeerate / 2)
feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate * 2))
assert(feeConf.getCommitmentFeerate(defaultNodeId, channelVersion, 100000 sat, None) === defaultMaxCommitFeerate)
assert(feeConf.getCommitmentFeerate(overrideNodeId, channelVersion, 100000 sat, None) === overrideMaxCommitFeerate)
assert(feeConf.getCommitmentFeerate(defaultNodeId, channelFeatures, 100000 sat, None) === defaultMaxCommitFeerate)
assert(feeConf.getCommitmentFeerate(overrideNodeId, channelFeatures, 100000 sat, None) === overrideMaxCommitFeerate)
val currentFeerates1 = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2))
assert(feeConf.getCommitmentFeerate(defaultNodeId, channelVersion, 100000 sat, Some(currentFeerates1)) === defaultMaxCommitFeerate / 2)
assert(feeConf.getCommitmentFeerate(defaultNodeId, channelFeatures, 100000 sat, Some(currentFeerates1)) === defaultMaxCommitFeerate / 2)
val currentFeerates2 = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate * 1.5))
feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2))
assert(feeConf.getCommitmentFeerate(defaultNodeId, channelVersion, 100000 sat, Some(currentFeerates2)) === defaultMaxCommitFeerate)
assert(feeConf.getCommitmentFeerate(defaultNodeId, channelFeatures, 100000 sat, Some(currentFeerates2)) === defaultMaxCommitFeerate)
}
test("fee difference too high") {
val tolerance = FeerateTolerance(ratioLow = 0.5, ratioHigh = 4.0, anchorOutputMaxCommitFeerate = FeeratePerKw(2500 sat))
val channelVersion = ChannelVersion.STANDARD
val channelFeatures = ChannelFeatures()
val testCases = Seq(
(FeeratePerKw(500 sat), FeeratePerKw(500 sat), false),
(FeeratePerKw(500 sat), FeeratePerKw(250 sat), false),
@ -85,13 +85,13 @@ class FeeEstimatorSpec extends AnyFunSuite {
(FeeratePerKw(250 sat), FeeratePerKw(1500 sat), true),
)
testCases.foreach { case (networkFeerate, proposedFeerate, expected) =>
assert(tolerance.isFeeDiffTooHigh(channelVersion, networkFeerate, proposedFeerate) === expected)
assert(tolerance.isFeeDiffTooHigh(channelFeatures, networkFeerate, proposedFeerate) === expected)
}
}
test("fee difference too high (anchor outputs)") {
val tolerance = FeerateTolerance(ratioLow = 0.5, ratioHigh = 4.0, anchorOutputMaxCommitFeerate = FeeratePerKw(2500 sat))
val channelVersion = ChannelVersion.ANCHOR_OUTPUTS
val channelFeatures = ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)
val testCases = Seq(
(FeeratePerKw(500 sat), FeeratePerKw(500 sat), false),
(FeeratePerKw(500 sat), FeeratePerKw(2500 sat), false),
@ -106,7 +106,7 @@ class FeeEstimatorSpec extends AnyFunSuite {
(FeeratePerKw(1000 sat), FeeratePerKw(499 sat), true),
)
testCases.foreach { case (networkFeerate, proposedFeerate, expected) =>
assert(tolerance.isFeeDiffTooHigh(channelVersion, networkFeerate, proposedFeerate) === expected)
assert(tolerance.isFeeDiffTooHigh(channelFeatures, networkFeerate, proposedFeerate) === expected)
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright 2021 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
import fr.acinq.eclair.channel.ChannelConfig._
import org.scalatest.funsuite.AnyFunSuiteLike
class ChannelConfigSpec extends AnyFunSuiteLike {
test("channel key path based on funding public key") {
assert(!ChannelConfig(Set.empty[ChannelConfigOption]).hasOption(FundingPubKeyBasedChannelKeyPath))
assert(ChannelConfig.standard.hasOption(FundingPubKeyBasedChannelKeyPath))
assert(ChannelConfig(FundingPubKeyBasedChannelKeyPath).hasOption(FundingPubKeyBasedChannelKeyPath))
}
}

View File

@ -1,3 +1,19 @@
/*
* 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
import akka.testkit.{TestFSMRef, TestProbe}
@ -8,7 +24,7 @@ import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.wire.protocol.{CommitSig, RevokeAndAck, UpdateAddHtlc}
import fr.acinq.eclair.{MilliSatoshiLong, TestKitBaseClass}
import fr.acinq.eclair.{Features, MilliSatoshiLong, TestKitBaseClass}
import org.scalatest.funsuite.AnyFunSuiteLike
import scodec.bits.ByteVector
@ -16,43 +32,47 @@ class ChannelTypesSpec extends TestKitBaseClass with AnyFunSuiteLike with StateT
implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging
test("standard channel features include deterministic channel key path") {
assert(!ChannelVersion.ZEROES.hasPubkeyKeyPath)
assert(ChannelVersion.STANDARD.hasPubkeyKeyPath)
assert(ChannelVersion.STATIC_REMOTEKEY.hasStaticRemotekey)
assert(ChannelVersion.STATIC_REMOTEKEY.hasPubkeyKeyPath)
test("channel features determines commitment format") {
val standardChannel = ChannelFeatures()
val staticRemoteKeyChannel = ChannelFeatures(Features.StaticRemoteKey)
val anchorOutputsChannel = ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)
assert(!standardChannel.hasFeature(Features.StaticRemoteKey))
assert(!standardChannel.hasFeature(Features.AnchorOutputs))
assert(standardChannel.commitmentFormat === Transactions.DefaultCommitmentFormat)
assert(!standardChannel.paysDirectlyToWallet)
assert(staticRemoteKeyChannel.hasFeature(Features.StaticRemoteKey))
assert(!staticRemoteKeyChannel.hasFeature(Features.AnchorOutputs))
assert(staticRemoteKeyChannel.commitmentFormat === Transactions.DefaultCommitmentFormat)
assert(staticRemoteKeyChannel.paysDirectlyToWallet)
assert(anchorOutputsChannel.hasFeature(Features.StaticRemoteKey))
assert(anchorOutputsChannel.hasFeature(Features.AnchorOutputs))
assert(anchorOutputsChannel.commitmentFormat === Transactions.AnchorOutputsCommitmentFormat)
assert(!anchorOutputsChannel.paysDirectlyToWallet)
}
test("anchor outputs includes static remote key") {
assert(ChannelVersion.ANCHOR_OUTPUTS.hasPubkeyKeyPath)
assert(ChannelVersion.ANCHOR_OUTPUTS.hasStaticRemotekey)
}
test("channel version determines commitment format") {
assert(ChannelVersion.ZEROES.commitmentFormat === Transactions.DefaultCommitmentFormat)
assert(ChannelVersion.STANDARD.commitmentFormat === Transactions.DefaultCommitmentFormat)
assert(ChannelVersion.STATIC_REMOTEKEY.commitmentFormat === Transactions.DefaultCommitmentFormat)
assert(ChannelVersion.ANCHOR_OUTPUTS.commitmentFormat === Transactions.AnchorOutputsCommitmentFormat)
}
test("pick channel version based on local and remote features") {
test("pick channel features based on local and remote features") {
import fr.acinq.eclair.FeatureSupport._
import fr.acinq.eclair.Features
import fr.acinq.eclair.Features._
case class TestCase(localFeatures: Features, remoteFeatures: Features, expectedChannelVersion: ChannelVersion)
case class TestCase(localFeatures: Features, remoteFeatures: Features, expectedChannelFeatures: ChannelFeatures)
val testCases = Seq(
TestCase(Features.empty, Features.empty, ChannelVersion.STANDARD),
TestCase(Features(StaticRemoteKey -> Optional), Features.empty, ChannelVersion.STANDARD),
TestCase(Features.empty, Features(StaticRemoteKey -> Optional), ChannelVersion.STANDARD),
TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Optional), ChannelVersion.STATIC_REMOTEKEY),
TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Mandatory), ChannelVersion.STATIC_REMOTEKEY),
TestCase(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional), ChannelVersion.STATIC_REMOTEKEY),
TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), ChannelVersion.ANCHOR_OUTPUTS)
TestCase(Features.empty, Features.empty, ChannelFeatures()),
TestCase(Features(StaticRemoteKey -> Optional), Features.empty, ChannelFeatures()),
TestCase(Features.empty, Features(StaticRemoteKey -> Optional), ChannelFeatures()),
TestCase(Features.empty, Features(StaticRemoteKey -> Mandatory), ChannelFeatures()),
TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Mandatory), Features(Wumbo -> Mandatory), ChannelFeatures(Wumbo)),
TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Optional), ChannelFeatures(StaticRemoteKey)),
TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Mandatory), ChannelFeatures(StaticRemoteKey)),
TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Optional), Features(StaticRemoteKey -> Mandatory, Wumbo -> Mandatory), ChannelFeatures(StaticRemoteKey, Wumbo)),
TestCase(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional), ChannelFeatures(StaticRemoteKey)),
TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), ChannelFeatures(StaticRemoteKey, AnchorOutputs)),
)
for (testCase <- testCases) {
assert(ChannelVersion.pickChannelVersion(testCase.localFeatures, testCase.remoteFeatures) === testCase.expectedChannelVersion)
assert(ChannelFeatures.pickChannelFeatures(testCase.localFeatures, testCase.remoteFeatures) === testCase.expectedChannelFeatures)
}
}
@ -479,7 +499,7 @@ class ChannelTypesSpec extends TestKitBaseClass with AnyFunSuiteLike with StateT
addHtlc(18_000_000 msat, bob, alice, bob2alice, alice2bob)
addHtlc(400_000 msat, bob, alice, bob2alice, alice2bob) // below dust
crossSign(bob, alice, bob2alice, alice2bob)
val revokedCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val revokedCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
fulfillHtlc(htlca1.id, ra1, bob, alice, bob2alice, alice2bob)
crossSign(bob, alice, bob2alice, alice2bob)

View File

@ -17,7 +17,7 @@
package fr.acinq.eclair.channel
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{DeterministicWallet, Satoshi, SatoshiLong, Transaction}
import fr.acinq.bitcoin.{ByteVector64, DeterministicWallet, Satoshi, SatoshiLong, Transaction}
import fr.acinq.eclair.TestConstants.TestFeeEstimator
import fr.acinq.eclair.blockchain.fee.{FeeTargets, FeeratePerKw, FeerateTolerance, OnChainFeeConf}
import fr.acinq.eclair.channel.Commitments._
@ -466,14 +466,16 @@ object CommitmentsSpec {
def makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, feeRatePerKw: FeeratePerKw = FeeratePerKw(0 sat), dustLimit: Satoshi = 0 sat, isFunder: Boolean = true, announceChannel: Boolean = true): Commitments = {
val localParams = LocalParams(randomKey().publicKey, DeterministicWallet.KeyPath(Seq(42L)), dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isFunder, ByteVector.empty, None, Features.empty)
val remoteParams = RemoteParams(randomKey().publicKey, dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty)
val remoteParams = RemoteParams(randomKey().publicKey, dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty, None)
val commitmentInput = Funding.makeFundingInputInfo(randomBytes32(), 0, (toLocal + toRemote).truncateToSatoshi, randomKey().publicKey, remoteParams.fundingPubKey)
Commitments(
ChannelVersion.STANDARD,
channelId = randomBytes32(),
ChannelConfig.standard,
ChannelFeatures(),
localParams,
remoteParams,
channelFlags = if (announceChannel) ChannelFlags.AnnounceChannel else ChannelFlags.Empty,
LocalCommit(0, CommitmentSpec(Set.empty, feeRatePerKw, toLocal, toRemote), PublishableTxs(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), Nil)),
LocalCommit(0, CommitmentSpec(Set.empty, feeRatePerKw, toLocal, toRemote), CommitTxAndRemoteSig(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), ByteVector64.Zeroes), Nil),
RemoteCommit(0, CommitmentSpec(Set.empty, feeRatePerKw, toRemote, toLocal), randomBytes32(), randomKey().publicKey),
LocalChanges(Nil, Nil, Nil),
RemoteChanges(Nil, Nil, Nil),
@ -482,20 +484,21 @@ object CommitmentsSpec {
originChannels = Map.empty,
remoteNextCommitInfo = Right(randomKey().publicKey),
commitInput = commitmentInput,
remotePerCommitmentSecrets = ShaChain.init,
channelId = randomBytes32())
remotePerCommitmentSecrets = ShaChain.init)
}
def makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, localNodeId: PublicKey, remoteNodeId: PublicKey, announceChannel: Boolean): Commitments = {
val localParams = LocalParams(localNodeId, DeterministicWallet.KeyPath(Seq(42L)), 0 sat, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isFunder = true, ByteVector.empty, None, Features.empty)
val remoteParams = RemoteParams(remoteNodeId, 0 sat, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty)
val remoteParams = RemoteParams(remoteNodeId, 0 sat, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty, None)
val commitmentInput = Funding.makeFundingInputInfo(randomBytes32(), 0, (toLocal + toRemote).truncateToSatoshi, randomKey().publicKey, remoteParams.fundingPubKey)
Commitments(
ChannelVersion.STANDARD,
channelId = randomBytes32(),
ChannelConfig.standard,
ChannelFeatures(),
localParams,
remoteParams,
channelFlags = if (announceChannel) ChannelFlags.AnnounceChannel else ChannelFlags.Empty,
LocalCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(0 sat), toLocal, toRemote), PublishableTxs(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), Nil)),
LocalCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(0 sat), toLocal, toRemote), CommitTxAndRemoteSig(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), ByteVector64.Zeroes), Nil),
RemoteCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(0 sat), toRemote, toLocal), randomBytes32(), randomKey().publicKey),
LocalChanges(Nil, Nil, Nil),
RemoteChanges(Nil, Nil, Nil),
@ -504,8 +507,7 @@ object CommitmentsSpec {
originChannels = Map.empty,
remoteNextCommitInfo = Right(randomKey().publicKey),
commitInput = commitmentInput,
remotePerCommitmentSecrets = ShaChain.init,
channelId = randomBytes32())
remotePerCommitmentSecrets = ShaChain.init)
}
}

View File

@ -20,7 +20,6 @@ import akka.actor.{Actor, ActorLogging, ActorRef, Stash}
import fr.acinq.eclair.Features
import fr.acinq.eclair.channel.Commitments.msg2String
import fr.acinq.eclair.wire.protocol.{Init, LightningMessage}
import scodec.bits.ByteVector
import scala.concurrent.duration._
import scala.util.Random

View File

@ -74,14 +74,14 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateT
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(aliceParams, wallet, bobParams.nodeId, alice2blockchain.ref, relayerA, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref)
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bobParams, wallet, aliceParams.nodeId, bob2blockchain.ref, relayerB, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref)
within(30 seconds) {
val aliceInit = Init(Alice.channelParams.features)
val bobInit = Init(Bob.channelParams.features)
val aliceInit = Init(Alice.channelParams.initFeatures)
val bobInit = Init(Bob.channelParams.initFeatures)
registerA ! alice
registerB ! bob
// no announcements
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, Alice.channelParams, pipe, bobInit, channelFlags = 0x00.toByte, ChannelVersion.STANDARD)
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, Alice.channelParams, pipe, bobInit, channelFlags = 0x00.toByte, ChannelConfig.standard, ChannelFeatures())
alice2blockchain.expectMsgType[TxPublisher.SetChannelId]
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit, ChannelVersion.STANDARD)
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit, ChannelConfig.standard, ChannelFeatures())
bob2blockchain.expectMsgType[TxPublisher.SetChannelId]
pipe ! (alice, bob)
alice2blockchain.expectMsgType[TxPublisher.SetChannelId]

View File

@ -81,7 +81,7 @@ class RecoverySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Sta
awaitCond(alice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT)
// bob is nice and publishes its commitment
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager).tx
// actual tests starts here: let's see what we can do with Bob's commit tx
sender.send(alice, WatchFundingSpentTriggered(bobCommitTx))

View File

@ -129,7 +129,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
def closeChannelWithoutHtlcs(f: Fixture): (PublishRawTx, PublishReplaceableTx) = {
import f._
val commitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx
val commitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(alice.underlyingActor.nodeParams.channelKeyManager)
probe.send(alice, CMD_FORCECLOSE(probe.ref))
probe.expectMsgType[CommandSuccess[CMD_FORCECLOSE]]
@ -177,7 +177,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
withFixture(Seq(500 millibtc), f => {
import f._
val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx
val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager)
val (_, anchorTx) = closeChannelWithoutHtlcs(f)
walletClient.publishTransaction(remoteCommit.tx).pipeTo(probe.ref)
probe.expectMsg(remoteCommit.tx.txid)
@ -194,7 +194,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
withFixture(Seq(500 millibtc), f => {
import f._
val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx
val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager)
val (_, anchorTx) = closeChannelWithoutHtlcs(f)
walletClient.publishTransaction(remoteCommit.tx).pipeTo(probe.ref)
probe.expectMsg(remoteCommit.tx.txid)
@ -411,7 +411,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]]
// Force-close channel and verify txs sent to watcher.
val commitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx
val commitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(alice.underlyingActor.nodeParams.channelKeyManager)
assert(commitTx.tx.txOut.size === 6)
probe.send(alice, CMD_FORCECLOSE(probe.ref))
probe.expectMsgType[CommandSuccess[CMD_FORCECLOSE]]

View File

@ -19,6 +19,7 @@ package fr.acinq.eclair.channel.states
import akka.actor.typed.scaladsl.adapter.actorRefAdapter
import akka.actor.{ActorContext, ActorRef}
import akka.testkit.{TestFSMRef, TestKitBase, TestProbe}
import com.softwaremill.quicklens.ModifyPimp
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{ByteVector32, Crypto, SatoshiLong, ScriptFlags, Transaction}
import fr.acinq.eclair.TestConstants.{Alice, Bob, TestFeeEstimator}
@ -109,34 +110,43 @@ trait StateTestsHelperMethods extends TestKitBase {
SetupFixture(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, relayerA, relayerB, channelUpdateListener, wallet)
}
def setChannelFeatures(defaultChannelParams: LocalParams, tags: Set[String]): LocalParams = {
import com.softwaremill.quicklens._
def computeFeatures(setup: SetupFixture, tags: Set[String]): (LocalParams, ChannelFeatures, LocalParams, ChannelFeatures) = {
import setup._
defaultChannelParams
.modify(_.features.activated).usingIf(tags.contains(StateTestsTags.Wumbo))(_.updated(Features.Wumbo, FeatureSupport.Optional))
.modify(_.features.activated).usingIf(tags.contains(StateTestsTags.StaticRemoteKey))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional))
.modify(_.features.activated).usingIf(tags.contains(StateTestsTags.AnchorOutputs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Mandatory).updated(Features.AnchorOutputs, FeatureSupport.Optional))
.modify(_.features.activated).usingIf(tags.contains(StateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional))
val aliceInitFeatures = Alice.nodeParams.features
.modify(_.activated).usingIf(tags.contains(StateTestsTags.Wumbo))(_.updated(Features.Wumbo, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(StateTestsTags.StaticRemoteKey))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(StateTestsTags.AnchorOutputs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(StateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional))
val bobInitFeatures = Bob.nodeParams.features
.modify(_.activated).usingIf(tags.contains(StateTestsTags.Wumbo))(_.updated(Features.Wumbo, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(StateTestsTags.StaticRemoteKey))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(StateTestsTags.AnchorOutputs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(StateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional))
val aliceChannelFeatures = ChannelFeatures.pickChannelFeatures(aliceInitFeatures, bobInitFeatures)
val bobChannelFeatures = ChannelFeatures.pickChannelFeatures(bobInitFeatures, aliceInitFeatures)
val aliceParams = Alice.channelParams
.modify(_.initFeatures).setTo(aliceInitFeatures)
.modify(_.walletStaticPaymentBasepoint).setToIf(aliceChannelFeatures.paysDirectlyToWallet)(Some(Helpers.getWalletPaymentBasepoint(wallet)))
.modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(StateTestsTags.NoMaxHtlcValueInFlight))(UInt64.MaxValue)
.modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(StateTestsTags.AliceLowMaxHtlcValueInFlight))(UInt64(150000000))
val bobParams = Bob.channelParams
.modify(_.initFeatures).setTo(bobInitFeatures)
.modify(_.walletStaticPaymentBasepoint).setToIf(bobChannelFeatures.paysDirectlyToWallet)(Some(Helpers.getWalletPaymentBasepoint(wallet)))
.modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(StateTestsTags.NoMaxHtlcValueInFlight))(UInt64.MaxValue)
(aliceParams, aliceChannelFeatures, bobParams, bobChannelFeatures)
}
def reachNormal(setup: SetupFixture, tags: Set[String] = Set.empty): Unit = {
import com.softwaremill.quicklens._
import setup._
val channelVersion = List(
ChannelVersion.STANDARD,
if (tags.contains(StateTestsTags.AnchorOutputs)) ChannelVersion.ANCHOR_OUTPUTS else ChannelVersion.ZEROES,
if (tags.contains(StateTestsTags.StaticRemoteKey)) ChannelVersion.STATIC_REMOTEKEY else ChannelVersion.ZEROES,
).reduce(_ | _)
val channelConfig = ChannelConfig.standard
val (aliceParams, aliceChannelFeatures, bobParams, bobChannelFeatures) = computeFeatures(setup, tags)
val channelFlags = if (tags.contains(StateTestsTags.ChannelsPublic)) ChannelFlags.AnnounceChannel else ChannelFlags.Empty
val aliceParams = setChannelFeatures(Alice.channelParams, tags)
.modify(_.walletStaticPaymentBasepoint).setToIf(channelVersion.paysDirectlyToWallet)(Some(Helpers.getWalletPaymentBasepoint(wallet)))
.modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(StateTestsTags.NoMaxHtlcValueInFlight))(UInt64.MaxValue)
.modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(StateTestsTags.AliceLowMaxHtlcValueInFlight))(UInt64(150000000))
val bobParams = setChannelFeatures(Bob.channelParams, tags)
.modify(_.walletStaticPaymentBasepoint).setToIf(channelVersion.paysDirectlyToWallet)(Some(Helpers.getWalletPaymentBasepoint(wallet)))
.modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(StateTestsTags.NoMaxHtlcValueInFlight))(UInt64.MaxValue)
val initialFeeratePerKw = if (tags.contains(StateTestsTags.AnchorOutputs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw
val (fundingSatoshis, pushMsat) = if (tags.contains(StateTestsTags.NoPushMsat)) {
(TestConstants.fundingSatoshis, 0.msat)
@ -144,11 +154,11 @@ trait StateTestsHelperMethods extends TestKitBase {
(TestConstants.fundingSatoshis, TestConstants.pushMsat)
}
val aliceInit = Init(aliceParams.features)
val bobInit = Init(bobParams.features)
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, initialFeeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelVersion)
val aliceInit = Init(aliceParams.initFeatures)
val bobInit = Init(bobParams.initFeatures)
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, initialFeeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, aliceChannelFeatures)
assert(alice2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId === ByteVector32.Zeroes)
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelVersion)
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures)
assert(bob2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId === ByteVector32.Zeroes)
alice2bob.expectMsgType[OpenChannel]
alice2bob.forward(bob)
@ -281,14 +291,22 @@ trait StateTestsHelperMethods extends TestKitBase {
def localClose(s: TestFSMRef[State, Data, Channel], s2blockchain: TestProbe): LocalCommitPublished = {
// an error occurs and s publishes its commit tx
val commitTx = s.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val localCommit = s.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit
// check that we store the local txs without sigs
localCommit.commitTxAndRemoteSig.commitTx.tx.txIn.foreach(txIn => assert(txIn.witness.isNull))
localCommit.htlcTxsAndRemoteSigs.foreach(_.htlcTx.tx.txIn.foreach(txIn => assert(txIn.witness.isNull)))
val commitTx = localCommit.commitTxAndRemoteSig.commitTx.tx
s ! Error(ByteVector32.Zeroes, "oops")
awaitCond(s.stateName == CLOSING)
val closingState = s.stateData.asInstanceOf[DATA_CLOSING]
assert(closingState.localCommitPublished.isDefined)
val localCommitPublished = closingState.localCommitPublished.get
assert(s2blockchain.expectMsgType[TxPublisher.PublishRawTx].tx == commitTx)
val publishedLocalCommitTx = s2blockchain.expectMsgType[TxPublisher.PublishRawTx].tx
assert(publishedLocalCommitTx.txid == commitTx.txid)
val commitInput = closingState.commitments.commitInput
Transaction.correctlySpends(publishedLocalCommitTx, Map(commitInput.outPoint -> commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
if (closingState.commitments.commitmentFormat == Transactions.AnchorOutputsCommitmentFormat) {
assert(s2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx])
}

View File

@ -51,23 +51,22 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS
.modify(_.chainHash).setToIf(test.tags.contains("mainnet"))(Block.LivenetGenesisBlock.hash)
.modify(_.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-size"))(Btc(100))
.modify(_.maxRemoteDustLimit).setToIf(test.tags.contains("high-remote-dust-limit"))(15000 sat)
val aliceParams = setChannelFeatures(Alice.channelParams, test.tags)
val bobNodeParams = Bob.nodeParams
.modify(_.chainHash).setToIf(test.tags.contains("mainnet"))(Block.LivenetGenesisBlock.hash)
.modify(_.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-size"))(Btc(100))
val bobParams = setChannelFeatures(Bob.channelParams, test.tags)
val setup = init(aliceNodeParams, bobNodeParams, wallet = noopWallet)
import setup._
val channelVersion = ChannelVersion.STANDARD
val aliceInit = Init(aliceParams.features)
val bobInit = Init(bobParams.features)
val channelConfig = ChannelConfig.standard
val (aliceParams, aliceChannelFeatures, bobParams, bobChannelFeatures) = computeFeatures(setup, test.tags)
val aliceInit = Init(aliceParams.initFeatures)
val bobInit = Init(bobParams.initFeatures)
within(30 seconds) {
val fundingAmount = if (test.tags.contains(StateTestsTags.Wumbo)) Btc(5).toSatoshi else TestConstants.fundingSatoshis
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingAmount, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelVersion)
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelVersion)
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingAmount, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures)
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures)
alice2bob.expectMsgType[OpenChannel]
alice2bob.forward(bob)
awaitCond(alice.stateName == WAIT_FOR_ACCEPT_CHANNEL)

View File

@ -39,21 +39,22 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui
case class FixtureParam(bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, bob2blockchain: TestProbe)
override def withFixture(test: OneArgTest): Outcome = {
import com.softwaremill.quicklens._
val aliceParams = Alice.channelParams
val bobNodeParams = Bob.nodeParams.modify(_.maxFundingSatoshis).setToIf(test.tags.contains("max-funding-satoshis"))(Btc(1))
val bobParams = setChannelFeatures(Bob.channelParams, test.tags)
import com.softwaremill.quicklens._
val bobNodeParams = Bob.nodeParams
.modify(_.maxFundingSatoshis).setToIf(test.tags.contains("max-funding-satoshis"))(Btc(1))
val setup = init(nodeParamsB = bobNodeParams)
import setup._
val channelVersion = ChannelVersion.STANDARD
val aliceInit = Init(aliceParams.features)
val bobInit = Init(bobParams.features)
val channelConfig = ChannelConfig.standard
val (aliceParams, aliceChannelFeatures, bobParams, bobChannelFeatures) = computeFeatures(setup, test.tags)
val aliceInit = Init(aliceParams.initFeatures)
val bobInit = Init(bobParams.initFeatures)
within(30 seconds) {
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelVersion)
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelVersion)
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures)
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures)
awaitCond(bob.stateName == WAIT_FOR_OPEN_CHANNEL)
withFixture(test.toNoArgTest(FixtureParam(bob, alice2bob, bob2alice, bob2blockchain)))
}

View File

@ -18,7 +18,6 @@ package fr.acinq.eclair.channel.states.b
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.{ByteVector32, Satoshi}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.blockchain.{MakeFundingTxResponse, TestWallet}
import fr.acinq.eclair.channel._
@ -46,12 +45,13 @@ class WaitForFundingCreatedInternalStateSpec extends TestKitBaseClass with Fixtu
}
val setup = init(wallet = noopWallet)
import setup._
val channelVersion = ChannelVersion.STANDARD
val aliceInit = Init(Alice.channelParams.features)
val bobInit = Init(Bob.channelParams.features)
val channelConfig = ChannelConfig.standard
val (aliceParams, aliceChannelFeatures, bobParams, bobChannelFeatures) = computeFeatures(setup, test.tags)
val aliceInit = Init(aliceParams.initFeatures)
val bobInit = Init(bobParams.initFeatures)
within(30 seconds) {
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelVersion)
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit, channelVersion)
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures)
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures)
alice2bob.expectMsgType[OpenChannel]
alice2bob.forward(bob)
bob2alice.expectMsgType[AcceptChannel]

View File

@ -42,10 +42,10 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun
override def withFixture(test: OneArgTest): Outcome = {
import com.softwaremill.quicklens._
val aliceNodeParams = Alice.nodeParams.modify(_.maxFundingSatoshis).setToIf(test.tags.contains(StateTestsTags.Wumbo))(Btc(100))
val aliceParams = setChannelFeatures(Alice.channelParams, test.tags)
val bobNodeParams = Bob.nodeParams.modify(_.maxFundingSatoshis).setToIf(test.tags.contains(StateTestsTags.Wumbo))(Btc(100))
val bobParams = setChannelFeatures(Bob.channelParams, test.tags)
val aliceNodeParams = Alice.nodeParams
.modify(_.maxFundingSatoshis).setToIf(test.tags.contains(StateTestsTags.Wumbo))(Btc(100))
val bobNodeParams = Bob.nodeParams
.modify(_.maxFundingSatoshis).setToIf(test.tags.contains(StateTestsTags.Wumbo))(Btc(100))
val (fundingSatoshis, pushMsat) = if (test.tags.contains("funder_below_reserve")) {
(1000100 sat, (1000000 sat).toMilliSatoshi) // toLocal = 100 satoshis
@ -58,13 +58,14 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun
val setup = init(aliceNodeParams, bobNodeParams)
import setup._
val channelVersion = ChannelVersion.STANDARD
val aliceInit = Init(aliceParams.features)
val bobInit = Init(bobParams.features)
val channelConfig = ChannelConfig.standard
val (aliceParams, aliceChannelFeatures, bobParams, bobChannelFeatures) = computeFeatures(setup, test.tags)
val aliceInit = Init(aliceParams.initFeatures)
val bobInit = Init(bobParams.initFeatures)
within(30 seconds) {
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelVersion)
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures)
alice2blockchain.expectMsgType[TxPublisher.SetChannelId]
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelVersion)
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures)
bob2blockchain.expectMsgType[TxPublisher.SetChannelId]
alice2bob.expectMsgType[OpenChannel]
alice2bob.forward(bob)
@ -103,7 +104,8 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun
test("recv FundingCreated (funder can't pay fees)", Tag("funder_below_reserve")) { f =>
import f._
val fees = Transactions.weight2fee(TestConstants.feeratePerKw, Transactions.DefaultCommitmentFormat.commitWeight)
val reserve = Bob.channelParams.channelReserve
val bobParams = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].localParams
val reserve = bobParams.channelReserve
val missing = 100.sat - fees - reserve
val fundingCreated = alice2bob.expectMsgType[FundingCreated]
alice2bob.forward(bob)

View File

@ -42,10 +42,10 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS
override def withFixture(test: OneArgTest): Outcome = {
import com.softwaremill.quicklens._
val aliceNodeParams = Alice.nodeParams.modify(_.maxFundingSatoshis).setToIf(test.tags.contains(StateTestsTags.Wumbo))(Btc(100))
val aliceParams = setChannelFeatures(Alice.channelParams, test.tags)
val bobNodeParams = Bob.nodeParams.modify(_.maxFundingSatoshis).setToIf(test.tags.contains(StateTestsTags.Wumbo))(Btc(100))
val bobParams = setChannelFeatures(Bob.channelParams, test.tags)
val aliceNodeParams = Alice.nodeParams
.modify(_.maxFundingSatoshis).setToIf(test.tags.contains(StateTestsTags.Wumbo))(Btc(100))
val bobNodeParams = Bob.nodeParams
.modify(_.maxFundingSatoshis).setToIf(test.tags.contains(StateTestsTags.Wumbo))(Btc(100))
val (fundingSatoshis, pushMsat) = if (test.tags.contains(StateTestsTags.Wumbo)) {
(Btc(5).toSatoshi, TestConstants.pushMsat)
@ -56,13 +56,14 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS
val setup = init(aliceNodeParams, bobNodeParams)
import setup._
val channelVersion = ChannelVersion.STANDARD
val aliceInit = Init(aliceParams.features)
val bobInit = Init(bobParams.features)
val channelConfig = ChannelConfig.standard
val (aliceParams, aliceChannelFeatures, bobParams, bobChannelFeatures) = computeFeatures(setup, test.tags)
val aliceInit = Init(aliceParams.initFeatures)
val bobInit = Init(bobParams.initFeatures)
within(30 seconds) {
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelVersion)
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures)
alice2blockchain.expectMsgType[TxPublisher.SetChannelId]
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelVersion)
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures)
bob2blockchain.expectMsgType[TxPublisher.SetChannelId]
alice2bob.expectMsgType[OpenChannel]
alice2bob.forward(bob)

View File

@ -18,7 +18,6 @@ package fr.acinq.eclair.channel.states.c
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.{ByteVector32, SatoshiLong, Script, Transaction}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain.CurrentBlockCount
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
import fr.acinq.eclair.channel.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT}
@ -42,15 +41,18 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF
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 setup = init()
import setup._
val channelVersion = ChannelVersion.STANDARD
val aliceInit = Init(Alice.channelParams.features)
val bobInit = Init(Bob.channelParams.features)
val channelConfig = ChannelConfig.standard
val (aliceParams, aliceChannelFeatures, bobParams, bobChannelFeatures) = computeFeatures(setup, test.tags)
val aliceInit = Init(aliceParams.initFeatures)
val bobInit = Init(bobParams.initFeatures)
within(30 seconds) {
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelVersion)
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures)
alice2blockchain.expectMsgType[TxPublisher.SetChannelId]
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit, channelVersion)
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures)
bob2blockchain.expectMsgType[TxPublisher.SetChannelId]
alice2bob.expectMsgType[OpenChannel]
alice2bob.forward(bob)
@ -164,7 +166,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF
test("recv WatchFundingSpentTriggered (remote commit)") { f =>
import f._
// bob publishes his commitment tx
val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! WatchFundingSpentTriggered(tx)
alice2blockchain.expectMsgType[TxPublisher.PublishTx]
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid)
@ -173,19 +175,19 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF
test("recv WatchFundingSpentTriggered (other commit)") { f =>
import f._
val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0))
alice2bob.expectMsgType[Error]
assert(alice2blockchain.expectMsgType[TxPublisher.PublishRawTx].tx === tx)
assert(alice2blockchain.expectMsgType[TxPublisher.PublishRawTx].tx.txid === tx.txid)
awaitCond(alice.stateName == ERR_INFORMATION_LEAK)
}
test("recv Error") { f =>
import f._
val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! Error(ByteVector32.Zeroes, "oops")
awaitCond(alice.stateName == CLOSING)
assert(alice2blockchain.expectMsgType[TxPublisher.PublishRawTx].tx === tx)
assert(alice2blockchain.expectMsgType[TxPublisher.PublishRawTx].tx.txid === tx.txid)
alice2blockchain.expectMsgType[TxPublisher.PublishTx] // claim-main-delayed
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid)
}
@ -201,10 +203,10 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF
test("recv CMD_FORCECLOSE") { f =>
import f._
val sender = TestProbe()
val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! CMD_FORCECLOSE(sender.ref)
awaitCond(alice.stateName == CLOSING)
assert(alice2blockchain.expectMsgType[TxPublisher.PublishRawTx].tx === tx)
assert(alice2blockchain.expectMsgType[TxPublisher.PublishRawTx].tx.txid === tx.txid)
alice2blockchain.expectMsgType[TxPublisher.PublishTx] // claim-main-delayed
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid)
}

View File

@ -18,7 +18,6 @@ package fr.acinq.eclair.channel.states.c
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.{ByteVector32, Transaction}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.publish.TxPublisher
@ -43,13 +42,14 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS
override def withFixture(test: OneArgTest): Outcome = {
val setup = init()
import setup._
val channelVersion = ChannelVersion.STANDARD
val aliceInit = Init(Alice.channelParams.features)
val bobInit = Init(Bob.channelParams.features)
val channelConfig = ChannelConfig.standard
val (aliceParams, aliceChannelFeatures, bobParams, bobChannelFeatures) = computeFeatures(setup, test.tags)
val aliceInit = Init(aliceParams.initFeatures)
val bobInit = Init(bobParams.initFeatures)
within(30 seconds) {
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Some(initialRelayFees), Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelVersion)
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Some(initialRelayFees), aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures)
alice2blockchain.expectMsgType[TxPublisher.SetChannelId]
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit, channelVersion)
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures)
bob2blockchain.expectMsgType[TxPublisher.SetChannelId]
alice2bob.expectMsgType[OpenChannel]
alice2bob.forward(bob)
@ -92,7 +92,7 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS
test("recv WatchFundingSpentTriggered (remote commit)") { f =>
import f._
// bob publishes his commitment tx
val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! WatchFundingSpentTriggered(tx)
alice2blockchain.expectMsgType[TxPublisher.PublishTx]
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid)
@ -101,20 +101,20 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS
test("recv WatchFundingSpentTriggered (other commit)") { f =>
import f._
val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0))
alice2bob.expectMsgType[Error]
assert(alice2blockchain.expectMsgType[TxPublisher.PublishRawTx].tx === tx)
assert(alice2blockchain.expectMsgType[TxPublisher.PublishRawTx].tx.txid === tx.txid)
alice2blockchain.expectMsgType[TxPublisher.PublishTx]
awaitCond(alice.stateName == ERR_INFORMATION_LEAK)
}
test("recv Error") { f =>
import f._
val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! Error(ByteVector32.Zeroes, "oops")
awaitCond(alice.stateName == CLOSING)
assert(alice2blockchain.expectMsgType[TxPublisher.PublishRawTx].tx === tx)
assert(alice2blockchain.expectMsgType[TxPublisher.PublishRawTx].tx.txid === tx.txid)
alice2blockchain.expectMsgType[TxPublisher.PublishTx]
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid)
}
@ -130,10 +130,10 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS
test("recv CMD_FORCECLOSE") { f =>
import f._
val sender = TestProbe()
val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! CMD_FORCECLOSE(sender.ref)
awaitCond(alice.stateName == CLOSING)
assert(alice2blockchain.expectMsgType[TxPublisher.PublishRawTx].tx === tx)
assert(alice2blockchain.expectMsgType[TxPublisher.PublishRawTx].tx.txid === tx.txid)
alice2blockchain.expectMsgType[TxPublisher.PublishTx]
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid)
}

View File

@ -20,13 +20,12 @@ import akka.actor.ActorRef
import akka.testkit.TestProbe
import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, SatoshiLong, ScriptFlags, Transaction}
import fr.acinq.eclair.balance.CheckBalance.PossiblyPublishedMainAndHtlcBalance
import fr.acinq.eclair.Features.StaticRemoteKey
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.UInt64.Conversions._
import fr.acinq.eclair._
import fr.acinq.eclair.balance.CheckBalance
import fr.acinq.eclair.blockchain.{CurrentBlockCount, CurrentFeerates}
import fr.acinq.eclair.balance.CheckBalance.PossiblyPublishedMainAndHtlcBalance
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw, FeeratesPerKw}
import fr.acinq.eclair.blockchain.{CurrentBlockCount, CurrentFeerates}
@ -220,7 +219,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val sender = TestProbe()
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
// The anchor outputs commitment format costs more fees for the funder (bigger commit tx + cost of anchor outputs)
assert(initialState.commitments.availableBalanceForSend < initialState.commitments.copy(channelVersion = ChannelVersion.STANDARD).availableBalanceForSend)
assert(initialState.commitments.availableBalanceForSend < initialState.commitments.copy(channelFeatures = ChannelFeatures()).availableBalanceForSend)
val add = CMD_ADD_HTLC(sender.ref, initialState.commitments.availableBalanceForSend + 1.msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
alice ! add
@ -474,7 +473,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
test("recv UpdateAddHtlc (unexpected id)") { f =>
import f._
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 42, 150000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)
bob ! htlc.copy(id = 0)
bob ! htlc.copy(id = 1)
@ -484,14 +483,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val error = bob2alice.expectMsgType[Error]
assert(new String(error.data.toArray) === UnexpectedHtlcId(channelId(bob), expected = 4, actual = 42).getMessage)
awaitCond(bob.stateName == CLOSING)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
bob2blockchain.expectMsgType[PublishTx]
bob2blockchain.expectMsgType[WatchTxConfirmed]
}
test("recv UpdateAddHtlc (value too small)") { f =>
import f._
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 150 msat, randomBytes32(), cltvExpiry = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)
alice2bob.forward(bob, htlc)
val error = bob2alice.expectMsgType[Error]
@ -499,14 +498,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
bob2blockchain.expectMsgType[PublishTx]
bob2blockchain.expectMsgType[WatchTxConfirmed]
}
test("recv UpdateAddHtlc (insufficient funds)") { f =>
import f._
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(Long.MaxValue), randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)
alice2bob.forward(bob, htlc)
val error = bob2alice.expectMsgType[Error]
@ -514,14 +513,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
bob2blockchain.expectMsgType[PublishTx]
bob2blockchain.expectMsgType[WatchTxConfirmed]
}
test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs) (anchor outputs)", Tag(StateTestsTags.AnchorOutputs)) { f =>
import f._
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 400000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket))
alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 300000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket))
alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 100000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket))
@ -530,12 +529,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
}
test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs 1/2)") { f =>
import f._
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 400000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket))
alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 200000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket))
alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 167600000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket))
@ -545,14 +544,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
bob2blockchain.expectMsgType[PublishTx]
bob2blockchain.expectMsgType[WatchTxConfirmed]
}
test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs 2/2)") { f =>
import f._
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 300000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket))
alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 300000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket))
alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 500000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket))
@ -561,28 +560,28 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
bob2blockchain.expectMsgType[PublishTx]
bob2blockchain.expectMsgType[WatchTxConfirmed]
}
test("recv UpdateAddHtlc (over max inflight htlc value)", Tag(StateTestsTags.AliceLowMaxHtlcValueInFlight)) { f =>
import f._
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice2bob.forward(alice, UpdateAddHtlc(ByteVector32.Zeroes, 0, 151000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket))
val error = alice2bob.expectMsgType[Error]
assert(new String(error.data.toArray) === HtlcValueTooHighInFlight(channelId(alice), maximum = 150000000, actual = 151000000 msat).getMessage)
awaitCond(alice.stateName == CLOSING)
// channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
alice2blockchain.expectMsgType[PublishTx]
alice2blockchain.expectMsgType[WatchTxConfirmed]
}
test("recv UpdateAddHtlc (over max accepted htlcs)") { f =>
import f._
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
// Bob accepts a maximum of 30 htlcs
for (i <- 0 until 30) {
alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, i, 1000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket))
@ -593,7 +592,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
bob2blockchain.expectMsgType[PublishTx]
bob2blockchain.expectMsgType[WatchTxConfirmed]
}
@ -702,9 +701,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val commitSig = alice2bob.expectMsgType[CommitSig]
assert(commitSig.htlcSignatures.toSet.size == htlcCount)
alice2bob.forward(bob)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs.size == htlcCount)
val htlcTxs = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs
val amounts = htlcTxs.map(_.txinfo.tx.txOut.head.amount.toLong)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.htlcTxsAndRemoteSigs.size == htlcCount)
val htlcTxs = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.htlcTxsAndRemoteSigs
val amounts = htlcTxs.map(_.htlcTx.tx.txOut.head.amount.toLong)
assert(amounts === amounts.sorted)
}
@ -802,7 +801,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
bob2alice.expectMsgType[CommitSig]
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.htlcs.collect(incoming).exists(_.id == htlc.id))
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs.size == 1)
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.htlcTxsAndRemoteSigs.size == 1)
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toLocal == initialState.commitments.localCommit.spec.toLocal)
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteChanges.acked.size == 0)
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteChanges.signed.size == 1)
@ -825,7 +824,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
bob2alice.forward(alice)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.htlcs.collect(outgoing).exists(_.id == htlc.id))
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs.size == 1)
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.htlcTxsAndRemoteSigs.size == 1)
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toLocal == initialState.commitments.localCommit.spec.toLocal)
}
@ -851,7 +850,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
bob2alice.forward(alice)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 1)
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs.size == 3)
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.htlcTxsAndRemoteSigs.size == 3)
}
test("recv CommitSig (only fee update)") { f =>
@ -892,14 +891,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
crossSign(alice, bob, alice2bob, bob2alice)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.htlcs.collect(incoming).exists(_.id == htlc1.id))
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs.size == 2)
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.htlcTxsAndRemoteSigs.size == 2)
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toLocal == initialState.commitments.localCommit.spec.toLocal)
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx.txOut.count(_.amount == 50000.sat) == 2)
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.count(_.amount == 50000.sat) == 2)
}
ignore("recv CommitSig (no changes)") { f =>
import f._
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
// signature is invalid but it doesn't matter
bob ! CommitSig(ByteVector32.Zeroes, ByteVector64.Zeroes, Nil)
val error = bob2alice.expectMsgType[Error]
@ -907,7 +906,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
bob2blockchain.expectMsgType[PublishTx]
bob2blockchain.expectMsgType[WatchTxConfirmed]
}
@ -915,14 +914,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
test("recv CommitSig (invalid signature)") { f =>
import f._
addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice)
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
// actual test begins
bob ! CommitSig(ByteVector32.Zeroes, ByteVector64.Zeroes, Nil)
val error = bob2alice.expectMsgType[Error]
assert(new String(error.data.toArray).startsWith("invalid commitment signature"))
awaitCond(bob.stateName == CLOSING)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
bob2blockchain.expectMsgType[PublishTx]
bob2blockchain.expectMsgType[WatchTxConfirmed]
}
@ -931,7 +930,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
import f._
addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice)
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! CMD_SIGN()
val commitSig = alice2bob.expectMsgType[CommitSig]
@ -941,7 +940,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
bob ! badCommitSig
val error = bob2alice.expectMsgType[Error]
assert(new String(error.data.toArray) === HtlcSigCountMismatch(channelId(bob), expected = 1, actual = 2).getMessage)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
bob2blockchain.expectMsgType[PublishTx]
bob2blockchain.expectMsgType[WatchTxConfirmed]
}
@ -950,7 +949,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
import f._
addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice)
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! CMD_SIGN()
val commitSig = alice2bob.expectMsgType[CommitSig]
@ -960,7 +959,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
bob ! badCommitSig
val error = bob2alice.expectMsgType[Error]
assert(new String(error.data.toArray).startsWith("invalid htlc signature"))
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
bob2blockchain.expectMsgType[PublishTx]
bob2blockchain.expectMsgType[WatchTxConfirmed]
}
@ -1058,7 +1057,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
test("recv RevokeAndAck (invalid preimage)") { f =>
import f._
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice)
alice ! CMD_SIGN()
@ -1072,21 +1071,21 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(alice.stateName == CLOSING)
// channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
alice2blockchain.expectMsgType[PublishTx]
alice2blockchain.expectMsgType[WatchTxConfirmed]
}
test("recv RevokeAndAck (unexpectedly)") { f =>
import f._
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight)
alice ! RevokeAndAck(ByteVector32.Zeroes, PrivateKey(randomBytes32()), PrivateKey(randomBytes32()).publicKey)
alice2bob.expectMsgType[Error]
awaitCond(alice.stateName == CLOSING)
// channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
alice2blockchain.expectMsgType[PublishTx]
alice2blockchain.expectMsgType[WatchTxConfirmed]
}
@ -1146,12 +1145,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
def testRevokeAndAckHtlcStaticRemoteKey(f: FixtureParam): Unit = {
import f._
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.features.hasFeature(StaticRemoteKey))
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.features.hasFeature(StaticRemoteKey))
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.initFeatures.hasFeature(StaticRemoteKey))
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.initFeatures.hasFeature(StaticRemoteKey))
def aliceToRemoteScript(): ByteVector = {
val toRemoteAmount = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toRemote
val Some(toRemoteOut) = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx.txOut.find(_.amount == toRemoteAmount.truncateToSatoshi)
val Some(toRemoteOut) = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.find(_.amount == toRemoteAmount.truncateToSatoshi)
toRemoteOut.publicKeyScript
}
@ -1320,26 +1319,26 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
alice2bob.expectMsgType[CommitSig]
// actual test begins
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! UpdateFulfillHtlc(ByteVector32.Zeroes, htlc.id, r)
alice2bob.expectMsgType[Error]
awaitCond(alice.stateName == CLOSING)
// channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
alice2blockchain.expectMsgType[PublishTx]
alice2blockchain.expectMsgType[WatchTxConfirmed]
}
test("recv UpdateFulfillHtlc (unknown htlc id)") { f =>
import f._
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! UpdateFulfillHtlc(ByteVector32.Zeroes, 42, ByteVector32.Zeroes)
alice2bob.expectMsgType[Error]
awaitCond(alice.stateName == CLOSING)
// channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
alice2blockchain.expectMsgType[PublishTx]
alice2blockchain.expectMsgType[WatchTxConfirmed]
}
@ -1349,7 +1348,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
relayerB.expectMsgType[RelayForward]
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
// actual test begins
alice ! UpdateFulfillHtlc(ByteVector32.Zeroes, htlc.id, ByteVector32.Zeroes)
@ -1357,7 +1356,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(alice.stateName == CLOSING)
// channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
alice2blockchain.expectMsgType[PublishTx] // main delayed
alice2blockchain.expectMsgType[PublishTx] // htlc timeout
alice2blockchain.expectMsgType[WatchTxConfirmed]
@ -1540,13 +1539,13 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
crossSign(alice, bob, alice2bob, bob2alice)
// actual test begins
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
val fail = UpdateFailMalformedHtlc(ByteVector32.Zeroes, htlc.id, Sphinx.PaymentPacket.hash(htlc.onionRoutingPacket), 42)
alice ! fail
val error = alice2bob.expectMsgType[Error]
assert(new String(error.data.toArray) === InvalidFailureCode(ByteVector32.Zeroes).getMessage)
awaitCond(alice.stateName == CLOSING)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === tx) // commit tx
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid) // commit tx
alice2blockchain.expectMsgType[PublishTx] // main delayed
alice2blockchain.expectMsgType[PublishTx] // htlc timeout
alice2blockchain.expectMsgType[WatchTxConfirmed]
@ -1559,26 +1558,26 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
alice2bob.expectMsgType[CommitSig]
// actual test begins
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! UpdateFailHtlc(ByteVector32.Zeroes, htlc.id, ByteVector.fill(152)(0))
alice2bob.expectMsgType[Error]
awaitCond(alice.stateName == CLOSING)
// channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
alice2blockchain.expectMsgType[PublishTx]
alice2blockchain.expectMsgType[WatchTxConfirmed]
}
test("recv UpdateFailHtlc (unknown htlc id)") { f =>
import f._
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! UpdateFailHtlc(ByteVector32.Zeroes, 42, ByteVector.fill(152)(0))
alice2bob.expectMsgType[Error]
awaitCond(alice.stateName == CLOSING)
// channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
alice2blockchain.expectMsgType[PublishTx]
alice2blockchain.expectMsgType[WatchTxConfirmed]
}
@ -1664,20 +1663,20 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
test("recv UpdateFee (when sender is not funder)") { f =>
import f._
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(12000 sat))
alice2bob.expectMsgType[Error]
awaitCond(alice.stateName == CLOSING)
// channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
alice2blockchain.expectMsgType[PublishTx]
alice2blockchain.expectMsgType[WatchTxConfirmed]
}
test("recv UpdateFee (sender can't afford it)") { f =>
import f._
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
val fee = UpdateFee(ByteVector32.Zeroes, FeeratePerKw(100000000 sat))
// we first update the feerates so that we don't trigger a 'fee too different' error
bob.feeEstimator.setFeerate(FeeratesPerKw.single(fee.feeratePerKw))
@ -1687,14 +1686,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx) // commit tx
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid) // commit tx
//bob2blockchain.expectMsgType[PublishTx] // main delayed (removed because of the high fees)
bob2blockchain.expectMsgType[WatchTxConfirmed]
}
test("recv UpdateFee (sender can't afford it, anchor outputs)", Tag(StateTestsTags.AnchorOutputs)) { f =>
import f._
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
// This feerate is just above the threshold: (800000 (alice balance) - 20000 (reserve) - 660 (anchors)) / 1124 (commit tx weight) = 693363
bob ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(693364 sat))
val error = bob2alice.expectMsgType[Error]
@ -1702,14 +1701,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx) // commit tx
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid) // commit tx
}
test("recv UpdateFee (local/remote feerates are too different)") { f =>
import f._
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
val commitTx = initialState.commitments.localCommit.publishableTxs.commitTx.tx
val commitTx = initialState.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
assert(initialState.commitments.localCommit.spec.feeratePerKw === TestConstants.feeratePerKw)
alice2bob.send(bob, UpdateFee(ByteVector32.Zeroes, TestConstants.feeratePerKw * 3))
bob2alice.expectNoMsg(250 millis) // we don't close because the commitment doesn't contain any HTLC
@ -1721,7 +1720,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === commitTx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === commitTx.txid)
bob2blockchain.expectMsgType[PublishTx]
bob2blockchain.expectMsgType[WatchTxConfirmed]
}
@ -1730,7 +1729,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
import f._
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
val commitTx = initialState.commitments.localCommit.publishableTxs.commitTx.tx
val commitTx = initialState.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
assert(initialState.commitments.localCommit.spec.feeratePerKw === TestConstants.anchorOutputsFeeratePerKw)
alice2bob.send(bob, UpdateFee(initialState.channelId, TestConstants.anchorOutputsFeeratePerKw * 3))
bob2alice.expectNoMsg(250 millis) // we don't close because the commitment doesn't contain any HTLC
@ -1742,14 +1741,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === commitTx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === commitTx.txid)
}
test("recv UpdateFee (remote feerate is too small, anchor outputs)", Tag(StateTestsTags.AnchorOutputs)) { f =>
import f._
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
val commitTx = initialState.commitments.localCommit.publishableTxs.commitTx.tx
val commitTx = initialState.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
assert(initialState.commitments.localCommit.spec.feeratePerKw === TestConstants.anchorOutputsFeeratePerKw)
alice2bob.send(bob, UpdateFee(initialState.channelId, FeeratePerKw(FeeratePerByte(2 sat))))
bob2alice.expectNoMsg(250 millis) // we don't close because the commitment doesn't contain any HTLC
@ -1761,13 +1760,13 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === commitTx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === commitTx.txid)
}
test("recv UpdateFee (remote feerate is too small)") { f =>
import f._
val bobCommitments = bob.stateData.asInstanceOf[DATA_NORMAL].commitments
val tx = bobCommitments.localCommit.publishableTxs.commitTx.tx
val tx = bobCommitments.localCommit.commitTxAndRemoteSig.commitTx.tx
val expectedFeeratePerKw = bob.feeEstimator.getFeeratePerKw(bob.feeTargets.commitmentBlockTarget)
assert(bobCommitments.localCommit.spec.feeratePerKw == expectedFeeratePerKw)
bob ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(252 sat))
@ -1776,7 +1775,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
bob2blockchain.expectMsgType[PublishTx]
bob2blockchain.expectMsgType[WatchTxConfirmed]
}
@ -1921,7 +1920,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
def testShutdown(f: FixtureParam, script_opt: Option[ByteVector]): Unit = {
import f._
alice ! Shutdown(ByteVector32.Zeroes, script_opt.getOrElse(Bob.channelParams.defaultFinalScriptPubKey))
val bobParams = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams
alice ! Shutdown(ByteVector32.Zeroes, script_opt.getOrElse(bobParams.defaultFinalScriptPubKey))
alice2bob.expectMsgType[Shutdown]
alice2bob.expectMsgType[ClosingSigned]
awaitCond(alice.stateName == NEGOTIATING)
@ -1962,7 +1962,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
import f._
addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice)
// actual test begins
bob ! Shutdown(ByteVector32.Zeroes, TestConstants.Alice.channelParams.defaultFinalScriptPubKey)
val aliceParams = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams
bob ! Shutdown(ByteVector32.Zeroes, aliceParams.defaultFinalScriptPubKey)
bob2alice.expectMsgType[Error]
bob2blockchain.expectMsgType[PublishTx]
bob2blockchain.expectMsgType[PublishTx]
@ -2042,7 +2043,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
crossSign(alice, bob, alice2bob, bob2alice)
// actual test begins
bob ! Shutdown(ByteVector32.Zeroes, TestConstants.Alice.channelParams.defaultFinalScriptPubKey)
val bobParams = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams
bob ! Shutdown(ByteVector32.Zeroes, bobParams.defaultFinalScriptPubKey)
bob2alice.expectMsgType[Shutdown]
awaitCond(bob.stateName == SHUTDOWN)
}
@ -2128,9 +2130,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// actual test begins
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
val aliceCommitTx = initialState.commitments.localCommit.publishableTxs.commitTx.tx
val aliceCommitTx = initialState.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! CurrentBlockCount(400145)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === aliceCommitTx)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === aliceCommitTx.txid)
alice2blockchain.expectMsgType[PublishTx] // main delayed
alice2blockchain.expectMsgType[PublishTx] // htlc timeout
@ -2151,8 +2153,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// * When the HTLC timeout on Alice side is near, Bob needs to close the channel to avoid an on-chain race
// condition between his HTLC-success and Alice's HTLC-timeout
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
val initialCommitTx = initialState.commitments.localCommit.publishableTxs.commitTx.tx
val HtlcSuccessTx(_, htlcSuccessTx, _, _) = initialState.commitments.localCommit.publishableTxs.htlcTxsAndSigs.head.txinfo
val initialCommitTx = initialState.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
val HtlcSuccessTx(_, htlcSuccessTx, _, _) = initialState.commitments.localCommit.htlcTxsAndRemoteSigs.head.htlcTx
bob ! CMD_FULFILL_HTLC(htlc.id, r, commit = true)
bob2alice.expectMsgType[UpdateFulfillHtlc]
@ -2163,7 +2165,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(isFatal)
assert(err.isInstanceOf[HtlcsWillTimeoutUpstream])
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === initialCommitTx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === initialCommitTx.txid)
bob2blockchain.expectMsgType[PublishTx] // main delayed
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txOut === htlcSuccessTx.txOut)
assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === initialCommitTx.txid)
@ -2184,8 +2186,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// * When the HTLC timeout on Alice side is near, Bob needs to close the channel to avoid an on-chain race
// condition between his HTLC-success and Alice's HTLC-timeout
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
val initialCommitTx = initialState.commitments.localCommit.publishableTxs.commitTx.tx
val HtlcSuccessTx(_, htlcSuccessTx, _, _) = initialState.commitments.localCommit.publishableTxs.htlcTxsAndSigs.head.txinfo
val initialCommitTx = initialState.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
val HtlcSuccessTx(_, htlcSuccessTx, _, _) = initialState.commitments.localCommit.htlcTxsAndRemoteSigs.head.htlcTx
bob ! CMD_FULFILL_HTLC(htlc.id, r, commit = false)
bob2alice.expectMsgType[UpdateFulfillHtlc]
@ -2196,7 +2198,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(isFatal)
assert(err.isInstanceOf[HtlcsWillTimeoutUpstream])
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === initialCommitTx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === initialCommitTx.txid)
bob2blockchain.expectMsgType[PublishTx] // main delayed
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txOut === htlcSuccessTx.txOut)
assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === initialCommitTx.txid)
@ -2217,8 +2219,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// * When the HTLC timeout on Alice side is near, Bob needs to close the channel to avoid an on-chain race
// condition between his HTLC-success and Alice's HTLC-timeout
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
val initialCommitTx = initialState.commitments.localCommit.publishableTxs.commitTx.tx
val HtlcSuccessTx(_, htlcSuccessTx, _, _) = initialState.commitments.localCommit.publishableTxs.htlcTxsAndSigs.head.txinfo
val initialCommitTx = initialState.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
val HtlcSuccessTx(_, htlcSuccessTx, _, _) = initialState.commitments.localCommit.htlcTxsAndRemoteSigs.head.htlcTx
bob ! CMD_FULFILL_HTLC(htlc.id, r, commit = true)
bob2alice.expectMsgType[UpdateFulfillHtlc]
@ -2233,7 +2235,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(isFatal)
assert(err.isInstanceOf[HtlcsWillTimeoutUpstream])
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === initialCommitTx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === initialCommitTx.txid)
bob2blockchain.expectMsgType[PublishTx] // main delayed
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txOut === htlcSuccessTx.txOut)
assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === initialCommitTx.txid)
@ -2357,7 +2359,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// bob -> alice : 55 000 000 (alice does not have the preimage) => nothing to do, bob will get his money back after the timeout
// bob publishes his current commit tx
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
assert(bobCommitTx.txOut.size == 6) // two main outputs and 4 pending htlcs
alice ! WatchFundingSpentTriggered(bobCommitTx)
@ -2445,7 +2447,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// bob -> alice : 55 000 000 (alice does not have the preimage) => nothing to do, bob will get his money back after the timeout
// bob publishes his current commit tx
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
assert(bobCommitTx.txOut.size == 5) // two main outputs and 3 pending htlcs
alice ! WatchFundingSpentTriggered(bobCommitTx)
@ -2503,7 +2505,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
addHtlc(10000000 msat, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
}
val txs = for (_ <- 0 until 10) yield send()
@ -2570,7 +2572,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
crossSign(alice, bob, alice2bob, bob2alice)
// bob will publish this tx after it is revoked
val revokedTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val revokedTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! add
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
@ -2626,15 +2628,15 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// bob -> alice : 55 000 000 (alice does not have the preimage) => nothing to do, bob will get his money back after the timeout
// an error occurs and alice publishes her commit tx
val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! Error(ByteVector32.Zeroes, "oops")
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === aliceCommitTx)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === aliceCommitTx.txid)
assert(aliceCommitTx.txOut.size == 6) // two main outputs and 4 pending htlcs
awaitCond(alice.stateName == CLOSING)
assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined)
val commitments = alice.stateData.asInstanceOf[DATA_CLOSING].commitments
val localCommitPublished = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get
assert(localCommitPublished.commitTx == aliceCommitTx)
assert(localCommitPublished.commitTx.txid == aliceCommitTx.txid)
assert(localCommitPublished.htlcTxs.size === 4)
assert(getHtlcSuccessTxs(localCommitPublished).length === 1)
assert(getHtlcTimeoutTxs(localCommitPublished).length === 2)
@ -2696,16 +2698,16 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// have lost its data and need assistance
// an error occurs and alice publishes her commit tx
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
bob ! Error(ByteVector32.Zeroes, "oops")
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === bobCommitTx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === bobCommitTx.txid)
assert(bobCommitTx.txOut.size == 1) // only one main output
alice2blockchain.expectNoMsg(1 second)
awaitCond(bob.stateName == CLOSING)
assert(bob.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined)
val localCommitPublished = bob.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get
assert(localCommitPublished.commitTx == bobCommitTx)
assert(localCommitPublished.commitTx.txid == bobCommitTx.txid)
}
test("recv WatchFundingDeeplyBuriedTriggered", Tag(StateTestsTags.ChannelsPublic)) { f =>

View File

@ -271,7 +271,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(alice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT)
// bob is nice and publishes its commitment
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! WatchFundingSpentTriggered(bobCommitTx)
// alice is able to claim its main output
@ -316,7 +316,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(alice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT)
// bob is nice and publishes its commitment
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! WatchFundingSpentTriggered(bobCommitTx)
// alice is able to claim its main output
@ -326,7 +326,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
test("counterparty lies about having a more recent commitment") { f =>
import f._
val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
// we simulate a disconnection followed by a reconnection
disconnect(alice, bob)
@ -340,7 +340,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// alice then finds out bob is lying
bob2alice.send(alice, invalidReestablish)
val error = alice2bob.expectMsgType[Error]
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === aliceCommitTx)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === aliceCommitTx.txid)
val claimMainOutput = alice2blockchain.expectMsgType[PublishRawTx].tx
Transaction.correctlySpends(claimMainOutput, aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
assert(error === Error(channelId(alice), InvalidRevokedCommitProof(channelId(alice), 0, 42, invalidReestablish.yourLastPerCommitmentSecret).getMessage))
@ -470,8 +470,8 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred])
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
val initialCommitTx = initialState.commitments.localCommit.publishableTxs.commitTx.tx
val HtlcSuccessTx(_, htlcSuccessTx, _, _) = initialState.commitments.localCommit.publishableTxs.htlcTxsAndSigs.head.txinfo
val initialCommitTx = initialState.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
val HtlcSuccessTx(_, htlcSuccessTx, _, _) = initialState.commitments.localCommit.htlcTxsAndRemoteSigs.head.htlcTx
disconnect(alice, bob)
@ -484,13 +484,13 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(isFatal)
assert(err.isInstanceOf[HtlcsWillTimeoutUpstream])
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === initialCommitTx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === initialCommitTx.txid)
bob2blockchain.expectMsgType[PublishTx] // main delayed
assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === initialCommitTx.txid)
bob2blockchain.expectMsgType[WatchTxConfirmed] // main delayed
bob2blockchain.expectMsgType[WatchOutputSpent] // htlc
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === initialCommitTx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === initialCommitTx.txid)
bob2blockchain.expectMsgType[PublishTx] // main delayed
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txOut === htlcSuccessTx.txOut)
assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === initialCommitTx.txid)
@ -536,7 +536,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
disconnect(alice, bob)
val aliceStateData = alice.stateData.asInstanceOf[DATA_NORMAL]
val aliceCommitTx = aliceStateData.commitments.localCommit.publishableTxs.commitTx.tx
val aliceCommitTx = aliceStateData.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
val currentFeeratePerKw = aliceStateData.commitments.localCommit.spec.feeratePerKw
// we receive a feerate update that makes our current feerate too low compared to the network's (we multiply by 1.1
@ -547,7 +547,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// alice is funder
alice ! CurrentFeerates(networkFeerate)
if (shouldClose) {
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === aliceCommitTx)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === aliceCommitTx.txid)
} else {
alice2blockchain.expectNoMsg()
}
@ -645,7 +645,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
disconnect(alice, bob)
val bobStateData = bob.stateData.asInstanceOf[DATA_NORMAL]
val bobCommitTx = bobStateData.commitments.localCommit.publishableTxs.commitTx.tx
val bobCommitTx = bobStateData.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
val currentFeeratePerKw = bobStateData.commitments.localCommit.spec.feeratePerKw
// we receive a feerate update that makes our current feerate too low compared to the network's (we multiply by 1.1
@ -656,7 +656,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// bob is fundee
bob ! CurrentFeerates(networkFeerate)
if (shouldClose) {
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === bobCommitTx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === bobCommitTx.txid)
} else {
bob2blockchain.expectNoMsg()
}
@ -729,12 +729,12 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val aliceCommitments = alice.stateData.asInstanceOf[HasCommitments].commitments
val aliceCurrentPerCommitmentPoint = TestConstants.Alice.channelKeyManager.commitmentPoint(
TestConstants.Alice.channelKeyManager.keyPath(aliceCommitments.localParams, aliceCommitments.channelVersion),
TestConstants.Alice.channelKeyManager.keyPath(aliceCommitments.localParams, aliceCommitments.channelConfig),
aliceCommitments.localCommit.index)
val bobCommitments = bob.stateData.asInstanceOf[HasCommitments].commitments
val bobCurrentPerCommitmentPoint = TestConstants.Bob.channelKeyManager.commitmentPoint(
TestConstants.Bob.channelKeyManager.keyPath(bobCommitments.localParams, bobCommitments.channelVersion),
TestConstants.Bob.channelKeyManager.keyPath(bobCommitments.localParams, bobCommitments.channelConfig),
bobCommitments.localCommit.index)
(aliceCurrentPerCommitmentPoint, bobCurrentPerCommitmentPoint)

View File

@ -179,12 +179,12 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
test("recv UpdateFulfillHtlc (unknown htlc id)") { f =>
import f._
val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
val fulfill = UpdateFulfillHtlc(ByteVector32.Zeroes, 42, ByteVector32.Zeroes)
alice ! fulfill
alice2bob.expectMsgType[Error]
awaitCond(alice.stateName == CLOSING)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === tx) // commit tx
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid) // commit tx
alice2blockchain.expectMsgType[PublishTx] // main delayed
alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1
alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2
@ -193,11 +193,11 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
test("recv UpdateFulfillHtlc (invalid preimage)") { f =>
import f._
val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! UpdateFulfillHtlc(ByteVector32.Zeroes, 42, ByteVector32.Zeroes)
alice2bob.expectMsgType[Error]
awaitCond(alice.stateName == CLOSING)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === tx) // commit tx
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid) // commit tx
alice2blockchain.expectMsgType[PublishTx] // main delayed
alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1
alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2
@ -284,11 +284,11 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
test("recv UpdateFailHtlc (unknown htlc id)") { f =>
import f._
val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! UpdateFailHtlc(ByteVector32.Zeroes, 42, ByteVector.fill(152)(0))
alice2bob.expectMsgType[Error]
awaitCond(alice.stateName == CLOSING)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === tx) // commit tx
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid) // commit tx
alice2blockchain.expectMsgType[PublishTx] // main delayed
alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1
alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2
@ -305,13 +305,13 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
test("recv UpdateFailMalformedHtlc (invalid failure_code)") { f =>
import f._
val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
val fail = UpdateFailMalformedHtlc(ByteVector32.Zeroes, 1, Crypto.sha256(ByteVector.empty), 42)
alice ! fail
val error = alice2bob.expectMsgType[Error]
assert(new String(error.data.toArray) === InvalidFailureCode(ByteVector32.Zeroes).getMessage)
awaitCond(alice.stateName == CLOSING)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === tx) // commit tx
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid) // commit tx
alice2blockchain.expectMsgType[PublishTx] // main delayed
alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1
alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2
@ -374,23 +374,23 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
test("recv CommitSig (no changes)") { f =>
import f._
val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
// signature is invalid but it doesn't matter
bob ! CommitSig(ByteVector32.Zeroes, ByteVector64.Zeroes, Nil)
bob2alice.expectMsgType[Error]
awaitCond(bob.stateName == CLOSING)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx) // commit tx
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid) // commit tx
bob2blockchain.expectMsgType[PublishTx] // main delayed
bob2blockchain.expectMsgType[WatchTxConfirmed]
}
test("recv CommitSig (invalid signature)") { f =>
import f._
val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
bob ! CommitSig(ByteVector32.Zeroes, ByteVector64.Zeroes, Nil)
bob2alice.expectMsgType[Error]
awaitCond(bob.stateName == CLOSING)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx) // commit tx
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid) // commit tx
bob2blockchain.expectMsgType[PublishTx] // main delayed
bob2blockchain.expectMsgType[WatchTxConfirmed]
}
@ -434,7 +434,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
test("recv RevokeAndAck (invalid preimage)") { f =>
import f._
val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
bob ! CMD_FULFILL_HTLC(0, r1)
bob2alice.expectMsgType[UpdateFulfillHtlc]
bob2alice.forward(alice)
@ -446,7 +446,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
bob ! RevokeAndAck(ByteVector32.Zeroes, PrivateKey(randomBytes32()), PrivateKey(randomBytes32()).publicKey)
bob2alice.expectMsgType[Error]
awaitCond(bob.stateName == CLOSING)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx) // commit tx
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid) // commit tx
bob2blockchain.expectMsgType[PublishTx] // main delayed
bob2blockchain.expectMsgType[PublishTx] // htlc success
bob2blockchain.expectMsgType[WatchTxConfirmed]
@ -454,12 +454,12 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
test("recv RevokeAndAck (unexpectedly)") { f =>
import f._
val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo.isRight)
alice ! RevokeAndAck(ByteVector32.Zeroes, PrivateKey(randomBytes32()), PrivateKey(randomBytes32()).publicKey)
alice2bob.expectMsgType[Error]
awaitCond(alice.stateName == CLOSING)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === tx) // commit tx
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid) // commit tx
alice2blockchain.expectMsgType[PublishTx] // main delayed
alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1
alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2
@ -543,11 +543,11 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
test("recv UpdateFee (when sender is not funder)") { f =>
import f._
val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(12000 sat))
alice2bob.expectMsgType[Error]
awaitCond(alice.stateName == CLOSING)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === tx) // commit tx
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid) // commit tx
alice2blockchain.expectMsgType[PublishTx] // main delayed
alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1
alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2
@ -556,7 +556,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
test("recv UpdateFee (sender can't afford it)") { f =>
import f._
val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
val fee = UpdateFee(ByteVector32.Zeroes, FeeratePerKw(100000000 sat))
// we first update the feerates so that we don't trigger a 'fee too different' error
bob.feeEstimator.setFeerate(FeeratesPerKw.single(fee.feeratePerKw))
@ -564,31 +564,31 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
val error = bob2alice.expectMsgType[Error]
assert(new String(error.data.toArray) === CannotAffordFees(channelId(bob), missing = 72120000L sat, reserve = 20000L sat, fees = 72400000L sat).getMessage)
awaitCond(bob.stateName == CLOSING)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx) // commit tx
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid) // commit tx
//bob2blockchain.expectMsgType[PublishTx] // main delayed (removed because of the high fees)
bob2blockchain.expectMsgType[WatchTxConfirmed]
}
test("recv UpdateFee (local/remote feerates are too different)") { f =>
import f._
val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
bob ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(65000 sat))
val error = bob2alice.expectMsgType[Error]
assert(new String(error.data.toArray) === "local/remote feerates are too different: remoteFeeratePerKw=65000 localFeeratePerKw=10000")
awaitCond(bob.stateName == CLOSING)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx) // commit tx
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid) // commit tx
bob2blockchain.expectMsgType[PublishTx] // main delayed
bob2blockchain.expectMsgType[WatchTxConfirmed]
}
test("recv UpdateFee (remote feerate is too small)") { f =>
import f._
val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
bob ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(252 sat))
val error = bob2alice.expectMsgType[Error]
assert(new String(error.data.toArray) === "remote fee rate is too small: remoteFeeratePerKw=252")
awaitCond(bob.stateName == CLOSING)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx) // commit tx
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid) // commit tx
bob2blockchain.expectMsgType[PublishTx] // main delayed
bob2blockchain.expectMsgType[WatchTxConfirmed]
}
@ -613,9 +613,9 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
test("recv CurrentBlockCount (an htlc timed out)") { f =>
import f._
val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN]
val aliceCommitTx = initialState.commitments.localCommit.publishableTxs.commitTx.tx
val aliceCommitTx = initialState.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! CurrentBlockCount(400145)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === aliceCommitTx) // commit tx
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === aliceCommitTx.txid) // commit tx
alice2blockchain.expectMsgType[PublishTx] // main delayed
alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1
alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2
@ -674,7 +674,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
test("recv WatchFundingSpentTriggered (their commit)") { f =>
import f._
// bob publishes his current commit tx, which contains two pending htlcs alice->bob
val bobCommitTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
val bobCommitTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
assert(bobCommitTx.txOut.size == 4) // two main outputs and 2 pending htlcs
alice ! WatchFundingSpentTriggered(bobCommitTx)
@ -721,7 +721,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
// as far as alice knows, bob currently has two valid unrevoked commitment transactions
// bob publishes his current commit tx, which contains one pending htlc alice->bob
val bobCommitTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
val bobCommitTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
assert(bobCommitTx.txOut.size == 3) // two main outputs and 1 pending htlc
alice ! WatchFundingSpentTriggered(bobCommitTx)
@ -752,7 +752,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
test("recv WatchFundingSpentTriggered (revoked tx)") { f =>
import f._
val revokedTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
val revokedTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
// two main outputs + 2 htlc
assert(revokedTx.txOut.size == 4)
@ -794,13 +794,13 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
test("recv WatchFundingSpentTriggered (revoked tx with updated commitment)") { f =>
import f._
val initialCommitTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
val initialCommitTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
assert(initialCommitTx.txOut.size === 4) // two main outputs + 2 htlc
// bob fulfills one of the pending htlc (commitment update while in shutdown state)
fulfillHtlc(0, r1, bob, alice, bob2alice, alice2bob)
crossSign(bob, alice, bob2alice, alice2bob)
val revokedTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
val revokedTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
assert(revokedTx.txOut.size === 3) // two main outputs + 1 htlc
// bob fulfills the second pending htlc (and revokes the previous commitment)
@ -844,13 +844,13 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
test("recv CMD_FORCECLOSE") { f =>
import f._
val aliceCommitTx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
val aliceCommitTx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
assert(aliceCommitTx.txOut.size == 4) // two main outputs and two htlcs
val sender = TestProbe()
alice ! CMD_FORCECLOSE(sender.ref)
sender.expectMsgType[RES_SUCCESS[CMD_FORCECLOSE]]
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === aliceCommitTx)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === aliceCommitTx.txid)
awaitCond(alice.stateName == CLOSING)
assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined)
val lcp = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get
@ -882,9 +882,9 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
test("recv Error") { f =>
import f._
val aliceCommitTx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
val aliceCommitTx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! Error(ByteVector32.Zeroes, "oops")
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === aliceCommitTx)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === aliceCommitTx.txid)
assert(aliceCommitTx.txOut.size == 4) // two main outputs and two htlcs
awaitCond(alice.stateName == CLOSING)
assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined)

View File

@ -159,11 +159,11 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
import f._
val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned]
val sender = TestProbe()
val tx = bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
sender.send(bob, aliceCloseSig.copy(feeSatoshis = 99000 sat)) // sig doesn't matter, it is checked later
val error = bob2alice.expectMsgType[Error]
assert(new String(error.data.toArray).startsWith("invalid close fee: fee_satoshis=99000 sat"))
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
bob2blockchain.expectMsgType[PublishTx]
bob2blockchain.expectMsgType[WatchTxConfirmed]
}
@ -171,11 +171,11 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
test("recv ClosingSigned (invalid sig)") { f =>
import f._
val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned]
val tx = bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localCommit.publishableTxs.commitTx.tx
val tx = bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
bob ! aliceCloseSig.copy(signature = ByteVector64.Zeroes)
val error = bob2alice.expectMsgType[Error]
assert(new String(error.data.toArray).startsWith("invalid close signature"))
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
bob2blockchain.expectMsgType[PublishTx]
bob2blockchain.expectMsgType[WatchTxConfirmed]
}
@ -235,10 +235,10 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
test("recv Error") { f =>
import f._
val tx = alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localCommit.publishableTxs.commitTx.tx
val tx = alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
alice ! Error(ByteVector32.Zeroes, "oops")
awaitCond(alice.stateName == CLOSING)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === tx)
assert(alice2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid)
alice2blockchain.expectMsgType[PublishTx]
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid)
}

View File

@ -18,8 +18,7 @@ package fr.acinq.eclair.channel.states.h
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.{ByteVector32, Crypto, OutPoint, SatoshiLong, Script, ScriptFlags, Transaction, TxIn, TxOut}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, OutPoint, SatoshiLong, Script, ScriptFlags, Transaction, TxIn, TxOut}
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw}
import fr.acinq.eclair.channel.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT}
@ -32,7 +31,7 @@ import fr.acinq.eclair.payment.relay.Relayer._
import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, HtlcSuccessTx, HtlcTimeoutTx}
import fr.acinq.eclair.transactions.{Scripts, Transactions}
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiry, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32, randomKey}
import fr.acinq.eclair.{CltvExpiry, Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32, randomKey}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag}
import scodec.bits.ByteVector
@ -45,7 +44,7 @@ import scala.concurrent.duration._
class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateTestsBase {
case class FixtureParam(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, relayerA: TestProbe, relayerB: TestProbe, channelUpdateListener: TestProbe, bobCommitTxs: List[PublishableTxs])
case class FixtureParam(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, relayerA: TestProbe, relayerB: TestProbe, channelUpdateListener: TestProbe, bobCommitTxs: List[CommitTxAndRemoteSig])
override def withFixture(test: OneArgTest): Outcome = {
val setup = init()
@ -64,12 +63,13 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
if (unconfirmedFundingTx) {
within(30 seconds) {
val channelVersion = ChannelVersion.STANDARD
val aliceInit = Init(Alice.channelParams.features)
val bobInit = Init(Bob.channelParams.features)
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelVersion)
val channelConfig = ChannelConfig.standard
val (aliceParams, aliceChannelFeatures, bobParams, bobChannelFeatures) = computeFeatures(setup, test.tags)
val aliceInit = Init(aliceParams.initFeatures)
val bobInit = Init(bobParams.initFeatures)
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures)
alice2blockchain.expectMsgType[SetChannelId]
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit, channelVersion)
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures)
bob2blockchain.expectMsgType[SetChannelId]
alice2bob.expectMsgType[OpenChannel]
alice2bob.forward(bob)
@ -92,18 +92,18 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
} else {
within(30 seconds) {
reachNormal(setup, test.tags)
val bobCommitTxs: List[PublishableTxs] = (for (amt <- List(100000000 msat, 200000000 msat, 300000000 msat)) yield {
val bobCommitTxs: List[CommitTxAndRemoteSig] = (for (amt <- List(100000000 msat, 200000000 msat, 300000000 msat)) yield {
val (r, htlc) = addHtlc(amt, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
relayerB.expectMsgType[RelayForward]
val bobCommitTx1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs
val bobCommitTx1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig
fulfillHtlc(htlc.id, r, bob, alice, bob2alice, alice2bob)
// alice forwards the fulfill upstream
relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.Fulfill]]
crossSign(bob, alice, bob2alice, alice2bob)
// bob confirms that it has forwarded the fulfill to alice
awaitCond(bob.underlyingActor.nodeParams.db.pendingCommands.listSettlementCommands(htlc.channelId).isEmpty)
val bobCommitTx2 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs
val bobCommitTx2 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig
bobCommitTx1 :: bobCommitTx2 :: Nil
}).flatten
@ -284,10 +284,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// NB: nominal case is tested in IntegrationSpec
}
def testMutualCloseBeforeConverge(f: FixtureParam, channelVersion: ChannelVersion): Unit = {
def testMutualCloseBeforeConverge(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = {
import f._
val sender = TestProbe()
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion === channelVersion)
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures)
// alice initiates a closing
alice ! CMD_CLOSE(sender.ref, None)
alice2bob.expectMsgType[Shutdown]
@ -316,11 +316,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
}
test("recv WatchFundingSpentTriggered (mutual close before converging)") { f =>
testMutualCloseBeforeConverge(f, ChannelVersion.STANDARD)
testMutualCloseBeforeConverge(f, ChannelFeatures())
}
test("recv WatchFundingSpentTriggered (mutual close before converging, anchor outputs)", Tag(StateTestsTags.AnchorOutputs)) { f =>
testMutualCloseBeforeConverge(f, ChannelVersion.ANCHOR_OUTPUTS)
testMutualCloseBeforeConverge(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs))
}
test("recv WatchTxConfirmedTriggered (mutual close)") { f =>
@ -336,7 +336,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
test("recv WatchFundingSpentTriggered (local commit)") { f =>
import f._
// an error occurs and alice publishes her commit tx
val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
localClose(alice, alice2blockchain)
val initialState = alice.stateData.asInstanceOf[DATA_CLOSING]
assert(initialState.localCommitPublished.isDefined)
@ -377,10 +377,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(alice.stateData == initialState) // this was a no-op
}
def testLocalCommitTxConfirmed(f: FixtureParam, channelVersion: ChannelVersion): Unit = {
def testLocalCommitTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = {
import f._
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion === channelVersion)
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures)
val listener = TestProbe()
system.eventStream.subscribe(listener.ref, classOf[LocalCommitConfirmed])
@ -402,7 +402,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val htlcTimeoutTx = getHtlcTimeoutTxs(closingState).head.tx
assert(closingState.claimHtlcDelayedTxs.length === 0)
alice ! WatchTxConfirmedTriggered(42, 0, closingState.commitTx)
assert(listener.expectMsgType[LocalCommitConfirmed].refundAtBlock == 42 + TestConstants.Bob.channelParams.toSelfDelay.toInt)
assert(listener.expectMsgType[LocalCommitConfirmed].refundAtBlock == 42 + bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.toSelfDelay.toInt)
assert(listener.expectMsgType[PaymentSettlingOnChain].paymentHash == htlca1.paymentHash)
// htlcs below dust will never reach the chain, once the commit tx is confirmed we can consider them failed
assert(relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc === htlca2)
@ -423,11 +423,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
}
test("recv WatchTxConfirmedTriggered (local commit)") { f =>
testLocalCommitTxConfirmed(f, ChannelVersion.STANDARD)
testLocalCommitTxConfirmed(f, ChannelFeatures())
}
test("recv WatchTxConfirmedTriggered (local commit, anchor outputs)", Tag(StateTestsTags.AnchorOutputs)) { f =>
testLocalCommitTxConfirmed(f, ChannelVersion.ANCHOR_OUTPUTS)
testLocalCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs))
}
test("recv WatchTxConfirmedTriggered (local commit with multiple htlcs for the same payment)") { f =>
@ -488,7 +488,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
import f._
val listener = TestProbe()
system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain])
val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
// alice sends an htlc
val (_, htlc) = addHtlc(4200000 msat, alice, bob, alice2bob, bob2alice)
// and signs it (but bob doesn't sign it)
@ -517,7 +517,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val (r, htlc) = addHtlc(110000000 msat, bob, alice, bob2alice, alice2bob)
crossSign(bob, alice, bob2alice, alice2bob)
relayerA.expectMsgType[RelayForward]
val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
assert(aliceCommitTx.txOut.size === 3) // 2 main outputs + 1 htlc
// alice fulfills the HTLC but bob doesn't receive the signature
@ -528,7 +528,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// note that bob doesn't receive the new sig!
// then we make alice unilaterally close the channel
val closingState = localClose(alice, alice2blockchain)
assert(closingState.commitTx === aliceCommitTx)
assert(closingState.commitTx.txid === aliceCommitTx.txid)
assert(getHtlcTimeoutTxs(closingState).isEmpty)
assert(getHtlcSuccessTxs(closingState).length === 1)
}
@ -618,7 +618,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
import f._
val listener = TestProbe()
system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain])
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
// alice sends an htlc
val (_, htlc) = addHtlc(4200000 msat, alice, bob, alice2bob, bob2alice)
// and signs it (but bob doesn't sign it)
@ -656,7 +656,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
import f._
mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)
val initialState = alice.stateData.asInstanceOf[DATA_CLOSING]
assert(initialState.commitments.channelVersion === ChannelVersion.STANDARD)
assert(initialState.commitments.channelFeatures === ChannelFeatures())
// bob publishes his last current commit tx, the one it had when entering NEGOTIATING state
val bobCommitTx = bobCommitTxs.last.commitTx.tx
assert(bobCommitTx.txOut.size == 2) // two main outputs
@ -674,7 +674,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
test("recv WatchTxConfirmedTriggered (remote commit, option_static_remotekey)", Tag(StateTestsTags.StaticRemoteKey)) { f =>
import f._
mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)
assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.channelVersion === ChannelVersion.STATIC_REMOTEKEY)
assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.channelFeatures === ChannelFeatures(Features.StaticRemoteKey))
// bob publishes his last current commit tx, the one it had when entering NEGOTIATING state
val bobCommitTx = bobCommitTxs.last.commitTx.tx
assert(bobCommitTx.txOut.size == 2) // two main outputs
@ -692,7 +692,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
import f._
mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)
val initialState = alice.stateData.asInstanceOf[DATA_CLOSING]
assert(initialState.commitments.channelVersion === ChannelVersion.ANCHOR_OUTPUTS)
assert(initialState.commitments.channelFeatures === ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs))
// bob publishes his last current commit tx, the one it had when entering NEGOTIATING state
val bobCommitTx = bobCommitTxs.last.commitTx.tx
assert(bobCommitTx.txOut.size == 4) // two main outputs + two anchors
@ -707,10 +707,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(alice.stateName == CLOSED)
}
def testRemoteCommitTxWithHtlcsConfirmed(f: FixtureParam, channelVersion: ChannelVersion): Unit = {
def testRemoteCommitTxWithHtlcsConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = {
import f._
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion === channelVersion)
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures)
// alice sends a first htlc to bob
val (ra1, htlca1) = addHtlc(15000000 msat, alice, bob, alice2bob, bob2alice)
@ -722,8 +722,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
crossSign(alice, bob, alice2bob, bob2alice)
// Bob publishes the latest commit tx.
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
if (channelVersion.hasAnchorOutputs) {
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
if (channelFeatures.hasFeature(Features.AnchorOutputs)) {
assert(bobCommitTx.txOut.length === 7) // two main outputs + two anchors + 3 HTLCs
} else {
assert(bobCommitTx.txOut.length === 5) // two main outputs + 3 HTLCs
@ -750,11 +750,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
}
test("recv WatchTxConfirmedTriggered (remote commit with multiple htlcs for the same payment)") { f =>
testRemoteCommitTxWithHtlcsConfirmed(f, ChannelVersion.STANDARD)
testRemoteCommitTxWithHtlcsConfirmed(f, ChannelFeatures())
}
test("recv WatchTxConfirmedTriggered (remote commit with multiple htlcs for the same payment, anchor outputs)", Tag(StateTestsTags.AnchorOutputs)) { f =>
testRemoteCommitTxWithHtlcsConfirmed(f, ChannelVersion.ANCHOR_OUTPUTS)
testRemoteCommitTxWithHtlcsConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs))
}
test("recv WatchTxConfirmedTriggered (remote commit) followed by CMD_FULFILL_HTLC") { f =>
@ -770,7 +770,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
alice2bob.expectMsgType[CommitSig] // We stop here: Alice sent her CommitSig, but doesn't hear back from Bob.
// Now Bob publishes the first commit tx (force-close).
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
assert(bobCommitTx.txOut.length === 3) // two main outputs + 1 HTLC
val closingState = remoteClose(bobCommitTx, alice, alice2blockchain)
assert(closingState.claimMainOutputTx.nonEmpty)
@ -810,7 +810,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// alice sends an htlc to bob
addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
val closingState = remoteClose(bobCommitTx, alice, alice2blockchain)
val htlcTimeoutTx = getClaimHtlcTimeoutTxs(closingState).head
alice ! WatchTxConfirmedTriggered(0, 0, bobCommitTx)
@ -829,10 +829,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex === htlcTimeoutTx.input.outPoint.index)
}
private def testNextRemoteCommitTxConfirmed(f: FixtureParam, channelVersion: ChannelVersion): (Transaction, RemoteCommitPublished, Set[UpdateAddHtlc]) = {
private def testNextRemoteCommitTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): (Transaction, RemoteCommitPublished, Set[UpdateAddHtlc]) = {
import f._
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion === channelVersion)
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures)
// alice sends a first htlc to bob
val (ra1, htlca1) = addHtlc(15000000 msat, alice, bob, alice2bob, bob2alice)
@ -850,8 +850,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
bob2alice.expectMsgType[CommitSig] // not forwarded to Alice (malicious Bob)
// Bob publishes the next commit tx.
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
if (channelVersion.hasAnchorOutputs) {
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
if (channelFeatures.hasFeature(Features.AnchorOutputs)) {
assert(bobCommitTx.txOut.length === 7) // two main outputs + two anchors + 3 HTLCs
} else {
assert(bobCommitTx.txOut.length === 5) // two main outputs + 3 HTLCs
@ -863,7 +863,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
test("recv WatchTxConfirmedTriggered (next remote commit)") { f =>
import f._
val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelVersion.STANDARD)
val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures())
val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState).map(_.tx)
alice ! WatchTxConfirmedTriggered(42, 0, bobCommitTx)
alice ! WatchTxConfirmedTriggered(45, 0, closingState.claimMainOutputTx.get.tx)
@ -883,7 +883,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
test("recv WatchTxConfirmedTriggered (next remote commit, static_remotekey)", Tag(StateTestsTags.StaticRemoteKey)) { f =>
import f._
val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelVersion.STATIC_REMOTEKEY)
val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey))
val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState).map(_.tx)
alice ! WatchTxConfirmedTriggered(42, 0, bobCommitTx)
assert(closingState.claimMainOutputTx.isEmpty) // with static_remotekey we don't claim out main output
@ -903,7 +903,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
test("recv WatchTxConfirmedTriggered (next remote commit, anchor outputs)", Tag(StateTestsTags.AnchorOutputs)) { f =>
import f._
val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelVersion.ANCHOR_OUTPUTS)
val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs))
val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState).map(_.tx)
alice ! WatchTxConfirmedTriggered(42, 0, bobCommitTx)
alice ! WatchTxConfirmedTriggered(45, 0, closingState.claimMainOutputTx.get.tx)
@ -937,7 +937,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
bob2alice.expectMsgType[CommitSig] // not forwarded to Alice (malicious Bob)
// Now Bob publishes the next commit tx (force-close).
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
assert(bobCommitTx.txOut.length === 4) // two main outputs + 2 HTLCs
val closingState = remoteClose(bobCommitTx, alice, alice2blockchain)
assert(closingState.claimMainOutputTx.nonEmpty)
@ -974,7 +974,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
test("recv INPUT_RESTORED (next remote commit, anchor outputs)", Tag(StateTestsTags.AnchorOutputs)) { f =>
import f._
val (bobCommitTx, closingState, _) = testNextRemoteCommitTxConfirmed(f, ChannelVersion.ANCHOR_OUTPUTS)
val (bobCommitTx, closingState, _) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs))
val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState)
// simulate a node restart
@ -994,10 +994,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
claimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex === claimHtlcTimeout.input.outPoint.index))
}
private def testFutureRemoteCommitTxConfirmed(f: FixtureParam, channelVersion: ChannelVersion): Transaction = {
private def testFutureRemoteCommitTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): Transaction = {
import f._
val oldStateData = alice.stateData
assert(oldStateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion === channelVersion)
assert(oldStateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures)
// This HTLC will be fulfilled.
val (ra1, htlca1) = addHtlc(25000000 msat, alice, bob, alice2bob, bob2alice)
// These 2 HTLCs should timeout on-chain, but since alice lost data, she won't be able to claim them.
@ -1030,8 +1030,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// alice now waits for bob to publish its commitment
awaitCond(alice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT)
// bob is nice and publishes its commitment
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
if (channelVersion.hasAnchorOutputs) {
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
if (channelFeatures.hasFeature(Features.AnchorOutputs)) {
assert(bobCommitTx.txOut.length === 6) // two main outputs + two anchors + 2 HTLCs
} else {
assert(bobCommitTx.txOut.length === 4) // two main outputs + 2 HTLCs
@ -1042,7 +1042,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
test("recv WatchTxConfirmedTriggered (future remote commit)") { f =>
import f._
val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ChannelVersion.STANDARD)
val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ChannelFeatures())
// alice is able to claim its main output
val claimMainTx = alice2blockchain.expectMsgType[PublishRawTx].tx
Transaction.correctlySpends(claimMainTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
@ -1059,7 +1059,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
test("recv WatchTxConfirmedTriggered (future remote commit, option_static_remotekey)", Tag(StateTestsTags.StaticRemoteKey)) { f =>
import f._
val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ChannelVersion.STATIC_REMOTEKEY)
val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey))
// using option_static_remotekey alice doesn't need to sweep her output
awaitCond(alice.stateName == CLOSING, 10 seconds)
alice ! WatchTxConfirmedTriggered(0, 0, bobCommitTx)
@ -1069,7 +1069,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
test("recv WatchTxConfirmedTriggered (future remote commit, anchor outputs)", Tag(StateTestsTags.AnchorOutputs)) { f =>
import f._
val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ChannelVersion.ANCHOR_OUTPUTS)
val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs))
// alice is able to claim its main output
val claimMainTx = alice2blockchain.expectMsgType[PublishRawTx].tx
Transaction.correctlySpends(claimMainTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
@ -1087,7 +1087,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
test("recv INPUT_RESTORED (future remote commit)") { f =>
import f._
val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ChannelVersion.STANDARD)
val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ChannelFeatures())
// simulate a node restart
val beforeRestart = alice.stateData.asInstanceOf[DATA_CLOSING]
@ -1102,86 +1102,86 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMainTx.txid)
}
case class RevokedCloseFixture(bobRevokedTxs: Seq[PublishableTxs], htlcsAlice: Seq[(UpdateAddHtlc, ByteVector32)], htlcsBob: Seq[(UpdateAddHtlc, ByteVector32)])
case class RevokedCloseFixture(bobRevokedTxs: Seq[LocalCommit], htlcsAlice: Seq[(UpdateAddHtlc, ByteVector32)], htlcsBob: Seq[(UpdateAddHtlc, ByteVector32)])
private def prepareRevokedClose(f: FixtureParam, channelVersion: ChannelVersion): RevokedCloseFixture = {
private def prepareRevokedClose(f: FixtureParam, channelFeatures: ChannelFeatures): RevokedCloseFixture = {
import f._
// Bob's first commit tx doesn't contain any htlc
val commitTx1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs
if (channelVersion.hasAnchorOutputs) {
assert(commitTx1.commitTx.tx.txOut.size === 4) // 2 main outputs + 2 anchors
val localCommit1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit
if (channelFeatures.hasFeature(Features.AnchorOutputs)) {
assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size === 4) // 2 main outputs + 2 anchors
} else {
assert(commitTx1.commitTx.tx.txOut.size === 2) // 2 main outputs
assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size === 2) // 2 main outputs
}
// Bob's second commit tx contains 1 incoming htlc and 1 outgoing htlc
val (commitTx2, htlcAlice1, htlcBob1) = {
val (localCommit2, htlcAlice1, htlcBob1) = {
val (ra, htlcAlice) = addHtlc(35000000 msat, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
val (rb, htlcBob) = addHtlc(20000000 msat, bob, alice, bob2alice, alice2bob)
crossSign(bob, alice, bob2alice, alice2bob)
val commitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs
(commitTx, (htlcAlice, ra), (htlcBob, rb))
val localCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit
(localCommit, (htlcAlice, ra), (htlcBob, rb))
}
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx.txOut.size == commitTx2.commitTx.tx.txOut.size)
if (channelVersion.hasAnchorOutputs) {
assert(commitTx2.commitTx.tx.txOut.size === 6)
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size)
if (channelFeatures.hasFeature(Features.AnchorOutputs)) {
assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size === 6)
} else {
assert(commitTx2.commitTx.tx.txOut.size === 4)
assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size === 4)
}
// Bob's third commit tx contains 2 incoming htlcs and 2 outgoing htlcs
val (commitTx3, htlcAlice2, htlcBob2) = {
val (localCommit3, htlcAlice2, htlcBob2) = {
val (ra, htlcAlice) = addHtlc(25000000 msat, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
val (rb, htlcBob) = addHtlc(18000000 msat, bob, alice, bob2alice, alice2bob)
crossSign(bob, alice, bob2alice, alice2bob)
val commitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs
(commitTx, (htlcAlice, ra), (htlcBob, rb))
val localCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit
(localCommit, (htlcAlice, ra), (htlcBob, rb))
}
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx.txOut.size == commitTx3.commitTx.tx.txOut.size)
if (channelVersion.hasAnchorOutputs) {
assert(commitTx3.commitTx.tx.txOut.size === 8)
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size)
if (channelFeatures.hasFeature(Features.AnchorOutputs)) {
assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size === 8)
} else {
assert(commitTx3.commitTx.tx.txOut.size === 6)
assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size === 6)
}
// Bob's fourth commit tx doesn't contain any htlc
val commitTx4 = {
val localCommit4 = {
Seq(htlcAlice1, htlcAlice2).foreach { case (htlcAlice, _) => failHtlc(htlcAlice.id, bob, alice, bob2alice, alice2bob) }
Seq(htlcBob1, htlcBob2).foreach { case (htlcBob, _) => failHtlc(htlcBob.id, alice, bob, alice2bob, bob2alice) }
crossSign(alice, bob, alice2bob, bob2alice)
bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs
bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit
}
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx.txOut.size == commitTx4.commitTx.tx.txOut.size)
if (channelVersion.hasAnchorOutputs) {
assert(commitTx4.commitTx.tx.txOut.size === 4)
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size)
if (channelFeatures.hasFeature(Features.AnchorOutputs)) {
assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size === 4)
} else {
assert(commitTx4.commitTx.tx.txOut.size === 2)
assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size === 2)
}
RevokedCloseFixture(Seq(commitTx1, commitTx2, commitTx3, commitTx4), Seq(htlcAlice1, htlcAlice2), Seq(htlcBob1, htlcBob2))
RevokedCloseFixture(Seq(localCommit1, localCommit2, localCommit3, localCommit4), Seq(htlcAlice1, htlcAlice2), Seq(htlcBob1, htlcBob2))
}
private def setupFundingSpentRevokedTx(f: FixtureParam, channelVersion: ChannelVersion): (Transaction, RevokedCommitPublished) = {
private def setupFundingSpentRevokedTx(f: FixtureParam, channelFeatures: ChannelFeatures): (Transaction, RevokedCommitPublished) = {
import f._
val revokedCloseFixture = prepareRevokedClose(f, channelVersion)
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion === channelVersion)
val revokedCloseFixture = prepareRevokedClose(f, channelFeatures)
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures)
// bob publishes one of his revoked txs
val bobRevokedTx = revokedCloseFixture.bobRevokedTxs(1).commitTx.tx
val bobRevokedTx = revokedCloseFixture.bobRevokedTxs(1).commitTxAndRemoteSig.commitTx.tx
alice ! WatchFundingSpentTriggered(bobRevokedTx)
awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING])
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1)
val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head
assert(rvk.commitTx === bobRevokedTx)
if (!channelVersion.paysDirectlyToWallet) {
if (!channelFeatures.paysDirectlyToWallet) {
assert(rvk.claimMainOutputTx.nonEmpty)
}
assert(rvk.mainPenaltyTx.nonEmpty)
@ -1190,7 +1190,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val penaltyTxs = rvk.claimMainOutputTx.toList ++ rvk.mainPenaltyTx.toList ++ rvk.htlcPenaltyTxs
// alice publishes the penalty txs
if (!channelVersion.paysDirectlyToWallet) {
if (!channelFeatures.paysDirectlyToWallet) {
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === rvk.claimMainOutputTx.get.tx)
}
assert(alice2blockchain.expectMsgType[PublishRawTx].tx === rvk.mainPenaltyTx.get.tx)
@ -1202,10 +1202,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// alice spends all outpoints of the revoked tx, except her main output when it goes directly to our wallet
val spentOutpoints = penaltyTxs.flatMap(_.tx.txIn.map(_.outPoint)).toSet
assert(spentOutpoints.forall(_.txid === bobRevokedTx.txid))
if (channelVersion.hasAnchorOutputs) {
if (channelFeatures.hasFeature(Features.AnchorOutputs)) {
assert(spentOutpoints.size === bobRevokedTx.txOut.size - 2) // we don't claim the anchors
}
else if (channelVersion.paysDirectlyToWallet) {
else if (channelFeatures.paysDirectlyToWallet) {
assert(spentOutpoints.size === bobRevokedTx.txOut.size - 1) // we don't claim our main output, it directly goes to our wallet
} else {
assert(spentOutpoints.size === bobRevokedTx.txOut.size)
@ -1213,7 +1213,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// alice watches confirmation for the outputs only her can claim
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobRevokedTx.txid)
if (!channelVersion.paysDirectlyToWallet) {
if (!channelFeatures.paysDirectlyToWallet) {
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === rvk.claimMainOutputTx.get.tx.txid)
}
@ -1225,15 +1225,15 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
(bobRevokedTx, rvk)
}
private def testFundingSpentRevokedTx(f: FixtureParam, channelVersion: ChannelVersion): Unit = {
private def testFundingSpentRevokedTx(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = {
import f._
val (bobRevokedTx, rvk) = setupFundingSpentRevokedTx(f, channelVersion)
val (bobRevokedTx, rvk) = setupFundingSpentRevokedTx(f, channelFeatures)
// once all txs are confirmed, alice can move to the closed state
alice ! WatchTxConfirmedTriggered(100, 3, bobRevokedTx)
alice ! WatchTxConfirmedTriggered(110, 1, rvk.mainPenaltyTx.get.tx)
if (!channelVersion.paysDirectlyToWallet) {
if (!channelFeatures.paysDirectlyToWallet) {
alice ! WatchTxConfirmedTriggered(110, 2, rvk.claimMainOutputTx.get.tx)
}
alice ! WatchTxConfirmedTriggered(115, 0, rvk.htlcPenaltyTxs(0).tx)
@ -1243,21 +1243,21 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
}
test("recv WatchFundingSpentTriggered (one revoked tx)") { f =>
testFundingSpentRevokedTx(f, ChannelVersion.STANDARD)
testFundingSpentRevokedTx(f, ChannelFeatures())
}
test("recv WatchFundingSpentTriggered (one revoked tx, option_static_remotekey)", Tag(StateTestsTags.StaticRemoteKey)) { f =>
testFundingSpentRevokedTx(f, ChannelVersion.STATIC_REMOTEKEY)
testFundingSpentRevokedTx(f, ChannelFeatures(Features.StaticRemoteKey))
}
test("recv WatchFundingSpentTriggered (one revoked tx, anchor outputs)", Tag(StateTestsTags.AnchorOutputs)) { f =>
testFundingSpentRevokedTx(f, ChannelVersion.ANCHOR_OUTPUTS)
testFundingSpentRevokedTx(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs))
}
test("recv WatchFundingSpentTriggered (multiple revoked tx)") { f =>
import f._
val revokedCloseFixture = prepareRevokedClose(f, ChannelVersion.STANDARD)
assert(revokedCloseFixture.bobRevokedTxs.map(_.commitTx.tx.txid).toSet.size === revokedCloseFixture.bobRevokedTxs.size) // all commit txs are distinct
val revokedCloseFixture = prepareRevokedClose(f, ChannelFeatures())
assert(revokedCloseFixture.bobRevokedTxs.map(_.commitTxAndRemoteSig.commitTx.tx.txid).toSet.size === revokedCloseFixture.bobRevokedTxs.size) // all commit txs are distinct
def broadcastBobRevokedTx(revokedTx: Transaction, htlcCount: Int, revokedCount: Int): RevokedCommitPublished = {
alice ! WatchFundingSpentTriggered(revokedTx)
@ -1285,11 +1285,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
}
// bob publishes a first revoked tx (no htlc in that commitment)
broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs.head.commitTx.tx, 0, 1)
broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs.head.commitTxAndRemoteSig.commitTx.tx, 0, 1)
// bob publishes a second revoked tx
val rvk2 = broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs(1).commitTx.tx, 2, 2)
val rvk2 = broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs(1).commitTxAndRemoteSig.commitTx.tx, 2, 2)
// bob publishes a third revoked tx
broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs(2).commitTx.tx, 4, 3)
broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs(2).commitTxAndRemoteSig.commitTx.tx, 4, 3)
// bob's second revoked tx confirms: once all penalty txs are confirmed, alice can move to the closed state
// NB: if multiple txs confirm in the same block, we may receive the events in any order
@ -1302,10 +1302,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(alice.stateName === CLOSED)
}
def testInputRestoredRevokedTx(f: FixtureParam, channelVersion: ChannelVersion): Unit = {
def testInputRestoredRevokedTx(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = {
import f._
val (bobRevokedTx, rvk) = setupFundingSpentRevokedTx(f, channelVersion)
val (bobRevokedTx, rvk) = setupFundingSpentRevokedTx(f, channelFeatures)
// simulate a node restart
val beforeRestart = alice.stateData.asInstanceOf[DATA_CLOSING]
@ -1327,28 +1327,28 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
}
test("recv INPUT_RESTORED (one revoked tx)") { f =>
testInputRestoredRevokedTx(f, ChannelVersion.STANDARD)
testInputRestoredRevokedTx(f, ChannelFeatures())
}
test("recv INPUT_RESTORED (one revoked tx, anchor outputs)", Tag(StateTestsTags.AnchorOutputs)) { f =>
testInputRestoredRevokedTx(f, ChannelVersion.ANCHOR_OUTPUTS)
testInputRestoredRevokedTx(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs))
}
def testOutputSpentRevokedTx(f: FixtureParam, channelVersion: ChannelVersion): Unit = {
def testOutputSpentRevokedTx(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = {
import f._
val revokedCloseFixture = prepareRevokedClose(f, channelVersion)
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion === channelVersion)
val revokedCloseFixture = prepareRevokedClose(f, channelFeatures)
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures)
val commitmentFormat = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.commitmentFormat
// bob publishes one of his revoked txs
val bobRevokedTxs = revokedCloseFixture.bobRevokedTxs(2)
alice ! WatchFundingSpentTriggered(bobRevokedTxs.commitTx.tx)
val bobRevokedCommit = revokedCloseFixture.bobRevokedTxs(2)
alice ! WatchFundingSpentTriggered(bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx)
awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING])
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1)
val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head
assert(rvk.commitTx === bobRevokedTxs.commitTx.tx)
if (channelVersion.paysDirectlyToWallet) {
assert(rvk.commitTx === bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx)
if (channelFeatures.paysDirectlyToWallet) {
assert(rvk.claimMainOutputTx.isEmpty)
} else {
assert(rvk.claimMainOutputTx.nonEmpty)
@ -1358,10 +1358,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty)
// alice publishes the penalty txs and watches outputs
val claimTxsCount = if (channelVersion.paysDirectlyToWallet) 5 else 6 // 2 main outputs and 4 htlcs
val claimTxsCount = if (channelFeatures.paysDirectlyToWallet) 5 else 6 // 2 main outputs and 4 htlcs
(1 to claimTxsCount).foreach(_ => alice2blockchain.expectMsgType[PublishTx])
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === rvk.commitTx.txid)
if (!channelVersion.paysDirectlyToWallet) {
if (!channelFeatures.paysDirectlyToWallet) {
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === rvk.claimMainOutputTx.get.tx.txid)
}
(1 to 5).foreach(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) // main output penalty and 4 htlc penalties
@ -1370,14 +1370,14 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// bob manages to claim 2 htlc outputs before alice can penalize him: 1 htlc-success and 1 htlc-timeout.
val (fulfilledHtlc, preimage) = revokedCloseFixture.htlcsAlice.head
val (failedHtlc, _) = revokedCloseFixture.htlcsBob.last
val bobHtlcSuccessTx1 = bobRevokedTxs.htlcTxsAndSigs.collectFirst {
case HtlcTxAndSigs(txInfo: HtlcSuccessTx, localSig, remoteSig) if txInfo.htlcId == fulfilledHtlc.id =>
val bobHtlcSuccessTx1 = bobRevokedCommit.htlcTxsAndRemoteSigs.collectFirst {
case HtlcTxAndRemoteSig(txInfo: HtlcSuccessTx, _) if txInfo.htlcId == fulfilledHtlc.id =>
assert(fulfilledHtlc.paymentHash === txInfo.paymentHash)
Transactions.addSigs(txInfo, localSig, remoteSig, preimage, commitmentFormat)
Transactions.addSigs(txInfo, ByteVector64.Zeroes, ByteVector64.Zeroes, preimage, commitmentFormat)
}.get
val bobHtlcTimeoutTx = bobRevokedTxs.htlcTxsAndSigs.collectFirst {
case HtlcTxAndSigs(txInfo: HtlcTimeoutTx, localSig, remoteSig) if txInfo.htlcId == failedHtlc.id =>
Transactions.addSigs(txInfo, localSig, remoteSig, commitmentFormat)
val bobHtlcTimeoutTx = bobRevokedCommit.htlcTxsAndRemoteSigs.collectFirst {
case HtlcTxAndRemoteSig(txInfo: HtlcTimeoutTx, _) if txInfo.htlcId == failedHtlc.id =>
Transactions.addSigs(txInfo, ByteVector64.Zeroes, ByteVector64.Zeroes, commitmentFormat)
}.get
val bobOutpoints = Seq(bobHtlcSuccessTx1, bobHtlcTimeoutTx).map(_.input.outPoint).toSet
assert(bobOutpoints.size === 2)
@ -1425,7 +1425,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(remainingHtlcPenaltyTxs.size === 2)
alice ! WatchTxConfirmedTriggered(100, 3, rvk.commitTx)
alice ! WatchTxConfirmedTriggered(110, 0, rvk.mainPenaltyTx.get.tx)
if (!channelVersion.paysDirectlyToWallet) {
if (!channelFeatures.paysDirectlyToWallet) {
alice ! WatchTxConfirmedTriggered(110, 1, rvk.claimMainOutputTx.get.tx)
}
alice ! WatchTxConfirmedTriggered(110, 2, remainingHtlcPenaltyTxs.head.tx)
@ -1440,29 +1440,30 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
}
test("recv WatchOutputSpentTriggered (one revoked tx, counterparty published htlc-success tx)") { f =>
testOutputSpentRevokedTx(f, ChannelVersion.STANDARD)
testOutputSpentRevokedTx(f, ChannelFeatures())
}
test("recv WatchOutputSpentTriggered (one revoked tx, counterparty published htlc-success tx, option_static_remotekey)", Tag(StateTestsTags.StaticRemoteKey)) { f =>
testOutputSpentRevokedTx(f, ChannelVersion.STATIC_REMOTEKEY)
testOutputSpentRevokedTx(f, ChannelFeatures(Features.StaticRemoteKey))
}
test("recv WatchOutputSpentTriggered (one revoked tx, counterparty published htlc-success tx, anchor outputs)", Tag(StateTestsTags.AnchorOutputs)) { f =>
testOutputSpentRevokedTx(f, ChannelVersion.ANCHOR_OUTPUTS)
testOutputSpentRevokedTx(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs))
}
test("recv WatchOutputSpentTriggered (one revoked tx, counterparty published aggregated htlc tx)", Tag(StateTestsTags.AnchorOutputs)) { f =>
import f._
// bob publishes one of his revoked txs
val revokedCloseFixture = prepareRevokedClose(f, ChannelVersion.ANCHOR_OUTPUTS)
val bobRevokedTxs = revokedCloseFixture.bobRevokedTxs(2)
alice ! WatchFundingSpentTriggered(bobRevokedTxs.commitTx.tx)
val revokedCloseFixture = prepareRevokedClose(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs))
val bobRevokedCommit = revokedCloseFixture.bobRevokedTxs(2)
val commitmentFormat = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.commitmentFormat
alice ! WatchFundingSpentTriggered(bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx)
awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING])
assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.commitmentFormat === AnchorOutputsCommitmentFormat)
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1)
val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head
assert(rvk.commitTx === bobRevokedTxs.commitTx.tx)
assert(rvk.commitTx === bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx)
assert(rvk.htlcPenaltyTxs.size === 4)
assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty)
@ -1475,13 +1476,13 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// bob claims multiple htlc outputs in a single transaction (this is possible with anchor outputs because signatures
// use sighash_single | sighash_anyonecanpay)
val bobHtlcTxs = bobRevokedTxs.htlcTxsAndSigs.collect {
case HtlcTxAndSigs(txInfo: HtlcSuccessTx, localSig, remoteSig) =>
val bobHtlcTxs = bobRevokedCommit.htlcTxsAndRemoteSigs.collect {
case HtlcTxAndRemoteSig(txInfo: HtlcSuccessTx, _) =>
val preimage = revokedCloseFixture.htlcsAlice.collectFirst { case (add, preimage) if add.id == txInfo.htlcId => preimage }.get
assert(Crypto.sha256(preimage) === txInfo.paymentHash)
Transactions.addSigs(txInfo, localSig, remoteSig, preimage, AnchorOutputsCommitmentFormat)
case HtlcTxAndSigs(txInfo: HtlcTimeoutTx, localSig, remoteSig) =>
Transactions.addSigs(txInfo, localSig, remoteSig, AnchorOutputsCommitmentFormat)
Transactions.addSigs(txInfo, ByteVector64.Zeroes, ByteVector64.Zeroes, preimage, commitmentFormat)
case HtlcTxAndRemoteSig(txInfo: HtlcTimeoutTx, _) =>
Transactions.addSigs(txInfo, ByteVector64.Zeroes, ByteVector64.Zeroes, commitmentFormat)
}
assert(bobHtlcTxs.map(_.input.outPoint).size === 4)
val bobHtlcTx = Transaction(
@ -1528,18 +1529,18 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
alice2blockchain.expectNoMsg(1 second)
}
private def testRevokedTxConfirmed(f: FixtureParam, channelVersion: ChannelVersion): Unit = {
private def testRevokedTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = {
import f._
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion === channelVersion)
val initOutputCount = if (channelVersion.hasAnchorOutputs) 4 else 2
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx.txOut.size === initOutputCount)
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures)
val initOutputCount = if (channelFeatures.hasFeature(Features.AnchorOutputs)) 4 else 2
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size === initOutputCount)
// bob's second commit tx contains 2 incoming htlcs
val (bobRevokedTx, htlcs1) = {
val (_, htlc1) = addHtlc(35000000 msat, alice, bob, alice2bob, bob2alice)
val (_, htlc2) = addHtlc(20000000 msat, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
assert(bobCommitTx.txOut.size === initOutputCount + 2)
(bobCommitTx, Seq(htlc1, htlc2))
}
@ -1550,7 +1551,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val (_, htlc4) = addHtlc(18000000 msat, alice, bob, alice2bob, bob2alice)
failHtlc(htlcs1.head.id, bob, alice, bob2alice, alice2bob)
crossSign(bob, alice, bob2alice, alice2bob)
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx.txOut.size === initOutputCount + 3)
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size === initOutputCount + 3)
Seq(htlc3, htlc4)
}
@ -1574,11 +1575,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
}
test("recv WatchTxConfirmedTriggered (one revoked tx, pending htlcs)") { f =>
testRevokedTxConfirmed(f, ChannelVersion.STANDARD)
testRevokedTxConfirmed(f, ChannelFeatures())
}
test("recv WatchTxConfirmedTriggered (one revoked tx, pending htlcs, anchor outputs)", Tag(StateTestsTags.AnchorOutputs)) { f =>
testRevokedTxConfirmed(f, ChannelVersion.ANCHOR_OUTPUTS)
testRevokedTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs))
}
test("recv ChannelReestablish") { f =>
@ -1587,7 +1588,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val initialState = alice.stateData.asInstanceOf[DATA_CLOSING]
val bobCommitments = bob.stateData.asInstanceOf[HasCommitments].commitments
val bobCurrentPerCommitmentPoint = TestConstants.Bob.channelKeyManager.commitmentPoint(
TestConstants.Bob.channelKeyManager.keyPath(bobCommitments.localParams, bobCommitments.channelVersion),
TestConstants.Bob.channelKeyManager.keyPath(bobCommitments.localParams, bobCommitments.channelConfig),
bobCommitments.localCommit.index)
alice ! ChannelReestablish(channelId(bob), 42, 42, PrivateKey(ByteVector32.Zeroes), bobCurrentPerCommitmentPoint)

View File

@ -18,12 +18,11 @@ package fr.acinq.eclair.crypto.keymanager
import java.io.File
import java.nio.file.Files
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.DeterministicWallet.KeyPath
import fr.acinq.bitcoin.{Block, ByteVector32, DeterministicWallet}
import fr.acinq.eclair.Setup.Seeds
import fr.acinq.eclair.channel.ChannelVersion
import fr.acinq.eclair.channel.ChannelConfig
import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.{NodeParams, TestConstants, TestUtils}
import org.scalatest.funsuite.AnyFunSuite
@ -69,7 +68,7 @@ class LocalChannelKeyManagerSpec extends AnyFunSuite {
val fundingPub = channelKeyManager.fundingPublicKey(fundingKeyPath)
val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath)
val channelKeyPath = channelKeyManager.keyPath(localParams, ChannelVersion.STANDARD)
val channelKeyPath = channelKeyManager.keyPath(localParams, ChannelConfig.standard)
assert(fundingPub.publicKey == PrivateKey(hex"216414970b4216b197a1040367419ad6922f80e8b73ced083e9afe5e6ddd8e4c").publicKey)
assert(channelKeyManager.revocationPoint(channelKeyPath).publicKey == PrivateKey(hex"a4e7ab3c54752a3487b3c474467843843f28d3bb9113e65e92056ad45d1e318e").publicKey)
@ -86,7 +85,7 @@ class LocalChannelKeyManagerSpec extends AnyFunSuite {
val fundingPub = channelKeyManager.fundingPublicKey(fundingKeyPath)
val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath)
val channelKeyPath = channelKeyManager.keyPath(localParams, ChannelVersion.STANDARD)
val channelKeyPath = channelKeyManager.keyPath(localParams, ChannelConfig.standard)
assert(fundingPub.publicKey == PrivateKey(hex"7bb8019c99fcba1c6bd0cc7f3c635c14c658d26751232d6a6350d8b6127d53c3").publicKey)
assert(channelKeyManager.revocationPoint(channelKeyPath).publicKey == PrivateKey(hex"26510db99546c9b08418fe9df2da710a92afa6cc4e5681141610dfb8019052e6").publicKey)
@ -103,7 +102,7 @@ class LocalChannelKeyManagerSpec extends AnyFunSuite {
val fundingPub = channelKeyManager.fundingPublicKey(fundingKeyPath)
val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath)
val channelKeyPath = channelKeyManager.keyPath(localParams, ChannelVersion.STANDARD)
val channelKeyPath = channelKeyManager.keyPath(localParams, ChannelConfig.standard)
assert(fundingPub.publicKey == PrivateKey(hex"b97c04796850e9d74a06c9d7230d85e2ecca3598b162ddf902895ece820c8f09").publicKey)
assert(channelKeyManager.revocationPoint(channelKeyPath).publicKey == PrivateKey(hex"ee13db7f2d7e672f21395111ee169af8462c6e8d1a6a78d808f7447b27155ffb").publicKey)
@ -120,7 +119,7 @@ class LocalChannelKeyManagerSpec extends AnyFunSuite {
val fundingPub = channelKeyManager.fundingPublicKey(fundingKeyPath)
val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath)
val channelKeyPath = channelKeyManager.keyPath(localParams, ChannelVersion.STANDARD)
val channelKeyPath = channelKeyManager.keyPath(localParams, ChannelConfig.standard)
assert(fundingPub.publicKey == PrivateKey(hex"46a4e818615a48a99ce9f6bd73eea07d5822dcfcdff18081ea781d4e5e6c036c").publicKey)
assert(channelKeyManager.revocationPoint(channelKeyPath).publicKey == PrivateKey(hex"c2cd9e2f9f8203f16b1751bd252285bb2e7fc4688857d620467b99645ebdfbe6").publicKey)

View File

@ -151,7 +151,7 @@ class ChannelsDbSpec extends AnyFunSuite {
}
}
test("migrate channel database v1 -> v3 (sqlite)") {
test("migrate channel database v1 -> v4 (sqlite)") {
forAllDbs {
case _: TestPgDatabases => // no migration
case dbs: TestSqliteDatabases =>
@ -187,7 +187,7 @@ class ChannelsDbSpec extends AnyFunSuite {
// check that db migration works
val db = new SqliteChannelsDb(sqlite)
using(sqlite.createStatement()) { statement =>
assert(getVersion(statement, "channels").contains(3))
assert(getVersion(statement, "channels").contains(4))
}
assert(db.listLocalChannels().size === testCases.size)
for (testCase <- testCases) {
@ -199,7 +199,7 @@ class ChannelsDbSpec extends AnyFunSuite {
}
}
test("migrate channel database v2 -> v3/v6") {
test("migrate channel database v2 -> v4/v7") {
def postCheck(channelsDb: ChannelsDb): Unit = {
assert(channelsDb.listLocalChannels().size === testCases.filterNot(_.isClosed).size)
for (testCase <- testCases.filterNot(_.isClosed)) {
@ -242,7 +242,7 @@ class ChannelsDbSpec extends AnyFunSuite {
}
},
dbName = "channels",
targetVersion = 6,
targetVersion = 7,
postCheck = _ => postCheck(dbs.channels)
)
case dbs: TestSqliteDatabases =>
@ -277,7 +277,7 @@ class ChannelsDbSpec extends AnyFunSuite {
}
},
dbName = "channels",
targetVersion = 3,
targetVersion = 4,
postCheck = _ => postCheck(dbs.channels)
)
}
@ -312,7 +312,7 @@ class ChannelsDbSpec extends AnyFunSuite {
}
},
dbName = "channels",
targetVersion = 6,
targetVersion = 7,
postCheck = connection => {
assert(dbs.channels.listLocalChannels().size === testCases.filterNot(_.isClosed).size)
testCases.foreach { testCase =>

View File

@ -34,6 +34,7 @@ import fr.acinq.eclair.payment.receive.{ForwardHandler, PaymentHandler}
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPayment
import fr.acinq.eclair.router.Router
import fr.acinq.eclair.transactions.Transactions.TxOwner
import fr.acinq.eclair.transactions.{Scripts, Transactions}
import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, PermanentChannelFailure, UpdateAddHtlc}
import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong, randomBytes32}
@ -393,13 +394,13 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec {
sigListener.expectNoMsg(1 second)
assert(commitmentsF.commitmentFormat === commitmentFormat)
// in this commitment, both parties should have a main output, there are four pending htlcs and anchor outputs if applicable
val localCommitF = commitmentsF.localCommit.publishableTxs
val localCommitF = commitmentsF.localCommit
commitmentFormat match {
case Transactions.DefaultCommitmentFormat => assert(localCommitF.commitTx.tx.txOut.size === 6)
case Transactions.AnchorOutputsCommitmentFormat => assert(localCommitF.commitTx.tx.txOut.size === 8)
case Transactions.DefaultCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size === 6)
case Transactions.AnchorOutputsCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size === 8)
}
val htlcTimeoutTxs = localCommitF.htlcTxsAndSigs.collect { case h@HtlcTxAndSigs(_: Transactions.HtlcTimeoutTx, _, _) => h }
val htlcSuccessTxs = localCommitF.htlcTxsAndSigs.collect { case h@HtlcTxAndSigs(_: Transactions.HtlcSuccessTx, _, _) => h }
val htlcTimeoutTxs = localCommitF.htlcTxsAndRemoteSigs.collect { case h@HtlcTxAndRemoteSig(_: Transactions.HtlcTimeoutTx, _) => h }
val htlcSuccessTxs = localCommitF.htlcTxsAndRemoteSigs.collect { case h@HtlcTxAndRemoteSig(_: Transactions.HtlcSuccessTx, _) => h }
assert(htlcTimeoutTxs.size === 2)
assert(htlcSuccessTxs.size === 2)
// we fulfill htlcs to get the preimages
@ -429,9 +430,23 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec {
sender.send(nodes("C").register, Register.Forward(sender.ref, commitmentsF.channelId, CMD_GETSTATEDATA(ActorRef.noSender)))
val finalAddressC = scriptPubKeyToAddress(sender.expectMsgType[RES_GETSTATEDATA[DATA_NORMAL]].data.commitments.localParams.defaultFinalScriptPubKey)
// we prepare the revoked transactions F will publish
val revokedCommitTx = localCommitF.commitTx.tx
val htlcSuccess = htlcSuccessTxs.zip(Seq(preimage1, preimage2)).map { case (htlcTxAndSigs, preimage) => Transactions.addSigs(htlcTxAndSigs.txinfo.asInstanceOf[Transactions.HtlcSuccessTx], htlcTxAndSigs.localSig, htlcTxAndSigs.remoteSig, preimage, commitmentsF.commitmentFormat).tx }
val htlcTimeout = htlcTimeoutTxs.map(htlcTxAndSigs => Transactions.addSigs(htlcTxAndSigs.txinfo.asInstanceOf[Transactions.HtlcTimeoutTx], htlcTxAndSigs.localSig, htlcTxAndSigs.remoteSig, commitmentsF.commitmentFormat).tx)
val keyManagerF = nodes("F").nodeParams.channelKeyManager
val channelKeyPathF = keyManagerF.keyPath(commitmentsF.localParams, commitmentsF.channelConfig)
val localPerCommitmentPointF = keyManagerF.commitmentPoint(channelKeyPathF, commitmentsF.localCommit.index.toInt)
val revokedCommitTx = {
val commitTx = localCommitF.commitTxAndRemoteSig.commitTx
val localSig = keyManagerF.sign(commitTx, keyManagerF.fundingPublicKey(commitmentsF.localParams.fundingKeyPath), TxOwner.Local, commitmentFormat)
Transactions.addSigs(commitTx, keyManagerF.fundingPublicKey(commitmentsF.localParams.fundingKeyPath).publicKey, commitmentsF.remoteParams.fundingPubKey, localSig, localCommitF.commitTxAndRemoteSig.remoteSig).tx
}
val htlcSuccess = htlcSuccessTxs.zip(Seq(preimage1, preimage2)).map {
case (htlcTxAndSigs, preimage) =>
val localSig = keyManagerF.sign(htlcTxAndSigs.htlcTx, keyManagerF.htlcPoint(channelKeyPathF), localPerCommitmentPointF, TxOwner.Local, commitmentFormat)
Transactions.addSigs(htlcTxAndSigs.htlcTx.asInstanceOf[Transactions.HtlcSuccessTx], localSig, htlcTxAndSigs.remoteSig, preimage, commitmentsF.commitmentFormat).tx
}
val htlcTimeout = htlcTimeoutTxs.map { htlcTxAndSigs =>
val localSig = keyManagerF.sign(htlcTxAndSigs.htlcTx, keyManagerF.htlcPoint(channelKeyPathF), localPerCommitmentPointF, TxOwner.Local, commitmentFormat)
Transactions.addSigs(htlcTxAndSigs.htlcTx.asInstanceOf[Transactions.HtlcTimeoutTx], localSig, htlcTxAndSigs.remoteSig, commitmentsF.commitmentFormat).tx
}
htlcSuccess.foreach(tx => Transaction.correctlySpends(tx, Seq(revokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))
htlcTimeout.foreach(tx => Transaction.correctlySpends(tx, Seq(revokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))
RevokedCommitFixture(sender, stateListenerC, revokedCommitTx, htlcSuccess, htlcTimeout, finalAddressC)
@ -658,7 +673,7 @@ class AnchorOutputChannelIntegrationSpec extends ChannelIntegrationSpec {
val toRemoteAddress = Script.pay2wsh(Scripts.toRemoteDelayed(initialStateDataF.commitments.remoteParams.paymentBasepoint))
// toRemote output of C as seen by F
val Some(toRemoteOutC) = initialStateDataF.commitments.localCommit.publishableTxs.commitTx.tx.txOut.find(_.publicKeyScript == Script.write(toRemoteAddress))
val Some(toRemoteOutC) = initialStateDataF.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.find(_.publicKeyScript == Script.write(toRemoteAddress))
// let's make a payment to advance the commit index
val amountMsat = 4200000.msat
@ -683,7 +698,7 @@ class AnchorOutputChannelIntegrationSpec extends ChannelIntegrationSpec {
sender.send(nodes("F").register, Register.Forward(sender.ref, channelId, CMD_GETSTATEDATA(ActorRef.noSender)))
val stateDataF = sender.expectMsgType[RES_GETSTATEDATA[DATA_NORMAL]].data
val commitmentIndex = stateDataF.commitments.localCommit.index
val commitTx = stateDataF.commitments.localCommit.publishableTxs.commitTx.tx
val commitTx = stateDataF.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
val Some(toRemoteOutCNew) = commitTx.txOut.find(_.publicKeyScript == Script.write(toRemoteAddress))
// there is a new commitment index in the channel state

View File

@ -29,7 +29,7 @@ import fr.acinq.eclair.channel.publish.TxPublisher
import fr.acinq.eclair.channel.states.StateTestsHelperMethods.FakeTxPublisherFactory
import fr.acinq.eclair.payment.receive.{ForwardHandler, PaymentHandler}
import fr.acinq.eclair.wire.protocol.Init
import fr.acinq.eclair.{MilliSatoshiLong, TestKitBaseClass, TestUtils}
import fr.acinq.eclair.{Features, MilliSatoshiLong, TestKitBaseClass, TestUtils}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.matchers.should.Matchers
import org.scalatest.{BeforeAndAfterAll, Outcome}
@ -66,16 +66,17 @@ class RustyTestsSpec extends TestKitBaseClass with Matchers with FixtureAnyFunSu
val feeEstimator = new TestFeeEstimator
val aliceNodeParams = Alice.nodeParams.copy(blockCount = blockCount, onChainFeeConf = Alice.nodeParams.onChainFeeConf.copy(feeEstimator = feeEstimator))
val bobNodeParams = Bob.nodeParams.copy(blockCount = blockCount, onChainFeeConf = Bob.nodeParams.onChainFeeConf.copy(feeEstimator = feeEstimator))
val channelVersion = ChannelVersion.STANDARD
val channelConfig = ChannelConfig.standard
val channelFeatures = ChannelFeatures()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(aliceNodeParams, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, relayer, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref)
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bobNodeParams, wallet, Alice.nodeParams.nodeId, bob2blockchain.ref, relayer, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref)
val aliceInit = Init(Alice.channelParams.features)
val bobInit = Init(Bob.channelParams.features)
val aliceInit = Init(Alice.channelParams.initFeatures)
val bobInit = Init(Bob.channelParams.initFeatures)
// alice and bob will both have 1 000 000 sat
feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)))
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, 2000000 sat, 1000000000 msat, feeEstimator.getFeeratePerKw(target = 2), feeEstimator.getFeeratePerKw(target = 6), None, Alice.channelParams, pipe, bobInit, ChannelFlags.Empty, channelVersion)
alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, 2000000 sat, 1000000000 msat, feeEstimator.getFeeratePerKw(target = 2), feeEstimator.getFeeratePerKw(target = 6), None, Alice.channelParams, pipe, bobInit, ChannelFlags.Empty, channelConfig, channelFeatures)
alice2blockchain.expectMsgType[TxPublisher.SetChannelId]
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit, channelVersion)
bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit, channelConfig, channelFeatures)
bob2blockchain.expectMsgType[TxPublisher.SetChannelId]
pipe ! (alice, bob)
within(30 seconds) {

View File

@ -22,7 +22,7 @@ import akka.testkit.{TestFSMRef, TestProbe}
import com.google.common.net.HostAndPort
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{Block, Btc, SatoshiLong, Script}
import fr.acinq.eclair.FeatureSupport.Optional
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
import fr.acinq.eclair.Features.{AnchorOutputs, StaticRemoteKey, Wumbo}
import fr.acinq.eclair.TestConstants._
import fr.acinq.eclair._
@ -313,7 +313,8 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle
val relayFees = Some(100 msat, 1000)
probe.send(peer, Peer.OpenChannel(remoteNodeId, 12300 sat, 0 msat, None, relayFees, None, None))
val init = channel.expectMsgType[INPUT_INIT_FUNDER]
assert(init.channelVersion === ChannelVersion.STANDARD)
assert(init.channelConfig === ChannelConfig.standard)
assert(init.channelFeatures === ChannelFeatures())
assert(init.fundingAmount === 12300.sat)
assert(init.initialRelayFees_opt === relayFees)
awaitCond(peer.stateData.channels.nonEmpty)
@ -331,7 +332,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle
feeEstimator.setFeerate(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2))
probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, None, None, None, None))
val init = channel.expectMsgType[INPUT_INIT_FUNDER]
assert(init.channelVersion.hasAnchorOutputs)
assert(init.channelFeatures === ChannelFeatures(StaticRemoteKey, AnchorOutputs))
assert(init.fundingAmount === 15000.sat)
assert(init.initialRelayFees_opt === None)
assert(init.initialFeeratePerKw === TestConstants.anchorOutputsFeeratePerKw)
@ -342,10 +343,10 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle
import f._
val probe = TestProbe()
connect(remoteNodeId, peer, peerConnection, remoteInit = protocol.Init(Features(StaticRemoteKey -> Optional)))
connect(remoteNodeId, peer, peerConnection, remoteInit = protocol.Init(Features(StaticRemoteKey -> Mandatory)))
probe.send(peer, Peer.OpenChannel(remoteNodeId, 24000 sat, 0 msat, None, None, None, None))
val init = channel.expectMsgType[INPUT_INIT_FUNDER]
assert(init.channelVersion.hasStaticRemotekey)
assert(init.channelFeatures === ChannelFeatures(StaticRemoteKey))
assert(init.localParams.walletStaticPaymentBasepoint.isDefined)
assert(init.localParams.defaultFinalScriptPubKey === Script.write(Script.pay2wpkh(init.localParams.walletStaticPaymentBasepoint.get)))
}

View File

@ -28,7 +28,7 @@ import fr.acinq.eclair.payment.OutgoingPacket._
import fr.acinq.eclair.payment.PaymentRequest.PaymentRequestFeatures
import fr.acinq.eclair.router.Router.{ChannelHop, NodeHop}
import fr.acinq.eclair.transactions.Transactions.InputInfo
import fr.acinq.eclair.wire.protocol.Onion.{ChannelRelayTlvPayload, FinalTlvPayload, RelayLegacyPayload}
import fr.acinq.eclair.wire.protocol.Onion.{ChannelRelayTlvPayload, FinalTlvPayload}
import fr.acinq.eclair.wire.protocol.OnionTlv.{AmountToForward, OutgoingCltv, PaymentData}
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, nodeFee, randomBytes32, randomKey}
@ -365,9 +365,9 @@ object PaymentPacketSpec {
def makeCommitments(channelId: ByteVector32, testAvailableBalanceForSend: MilliSatoshi = 50000000 msat, testAvailableBalanceForReceive: MilliSatoshi = 50000000 msat, testCapacity: Satoshi = 100000 sat): Commitments = {
val params = LocalParams(null, null, null, null, null, null, null, 0, isFunder = true, null, None, null)
val remoteParams = RemoteParams(randomKey().publicKey, null, null, null, null, null, maxAcceptedHtlcs = 0, null, null, null, null, null, null)
val remoteParams = RemoteParams(randomKey().publicKey, null, null, null, null, null, maxAcceptedHtlcs = 0, null, null, null, null, null, null, None)
val commitInput = InputInfo(OutPoint(randomBytes32(), 1), TxOut(testCapacity, Nil), Nil)
new Commitments(ChannelVersion.STANDARD, params, remoteParams, 0.toByte, null, null, null, null, 0, 0, Map.empty, null, commitInput, null, channelId) {
new Commitments(channelId, ChannelConfig.standard, ChannelFeatures(), params, remoteParams, 0.toByte, null, null, null, null, 0, 0, Map.empty, null, commitInput, null) {
override lazy val availableBalanceForSend: MilliSatoshi = testAvailableBalanceForSend.max(0 msat)
override lazy val availableBalanceForReceive: MilliSatoshi = testAvailableBalanceForReceive.max(0 msat)
}

View File

@ -19,12 +19,12 @@ package fr.acinq.eclair.transactions
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{ByteVector32, Crypto, SatoshiLong, Script, ScriptFlags, Transaction}
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.channel.ChannelVersion
import fr.acinq.eclair.channel.ChannelFeatures
import fr.acinq.eclair.channel.Helpers.Funding
import fr.acinq.eclair.crypto.Generators
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.wire.protocol.UpdateAddHtlc
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, MilliSatoshiLong, TestConstants}
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, TestConstants}
import grizzled.slf4j.Logging
import org.scalatest.funsuite.AnyFunSuite
import scodec.bits._
@ -35,7 +35,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging {
// @formatter:off
def filename: String
def channelVersion: ChannelVersion
def channelFeatures: ChannelFeatures
def commitmentFormat: CommitmentFormat
// @formatter:on
@ -95,7 +95,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging {
val funding_pubkey = funding_privkey.publicKey
val per_commitment_point = PublicKey(hex"025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce1486")
val htlc_privkey = Generators.derivePrivKey(payment_basepoint_secret, per_commitment_point)
val payment_privkey = if (channelVersion.hasStaticRemotekey) payment_basepoint_secret else htlc_privkey
val payment_privkey = if (channelFeatures.hasFeature(Features.StaticRemoteKey)) payment_basepoint_secret else htlc_privkey
val delayed_payment_privkey = Generators.derivePrivKey(delayed_payment_basepoint_secret, per_commitment_point)
val revocation_pubkey = PublicKey(hex"0212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b19")
val feerate_per_kw = 15000
@ -112,7 +112,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging {
val funding_privkey = PrivateKey(hex"1552dfba4f6cf29a62a0af13c8d6981d36d0ef8d61ba10fb0fe90da7634d7e1301")
val funding_pubkey = funding_privkey.publicKey
val htlc_privkey = Generators.derivePrivKey(payment_basepoint_secret, Local.per_commitment_point)
val payment_privkey = if (channelVersion.hasStaticRemotekey) payment_basepoint_secret else htlc_privkey
val payment_privkey = if (channelFeatures.hasFeature(Features.StaticRemoteKey)) payment_basepoint_secret else htlc_privkey
}
val coinbaseTx = Transaction.read("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff0100f2052a010000001976a9143ca33c2e4446f4a305f23c80df8ad1afdcf652f988ac00000000")
@ -399,7 +399,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging {
class DefaultCommitmentTestVectorSpec extends TestVectorsSpec {
// @formatter:off
override def filename: String = "/bolt3-tx-test-vectors-default-commitment-format.txt"
override def channelVersion: ChannelVersion = ChannelVersion.STANDARD
override def channelFeatures: ChannelFeatures = ChannelFeatures()
override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat
// @formatter:on
}
@ -407,7 +407,7 @@ class DefaultCommitmentTestVectorSpec extends TestVectorsSpec {
class StaticRemoteKeyTestVectorSpec extends TestVectorsSpec {
// @formatter:off
override def filename: String = "/bolt3-tx-test-vectors-static-remotekey-format.txt"
override def channelVersion: ChannelVersion = ChannelVersion.STATIC_REMOTEKEY
override def channelFeatures: ChannelFeatures = ChannelFeatures(Features.StaticRemoteKey)
override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat
// @formatter:on
}
@ -415,7 +415,7 @@ class StaticRemoteKeyTestVectorSpec extends TestVectorsSpec {
class AnchorOutputsTestVectorSpec extends TestVectorsSpec {
// @formatter:off
override def filename: String = "/bolt3-tx-test-vectors-anchor-outputs-format.txt"
override def channelVersion: ChannelVersion = ChannelVersion.ANCHOR_OUTPUTS
override def channelFeatures: ChannelFeatures = ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)
override def commitmentFormat: CommitmentFormat = AnchorOutputsCommitmentFormat
// @formatter:on
}

File diff suppressed because one or more lines are too long

View File

@ -1,9 +1,9 @@
package fr.acinq.eclair.wire.internal.channel.version0
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.eclair.channel.ChannelVersion
import fr.acinq.eclair.transactions.{IncomingHtlc, OutgoingHtlc}
import fr.acinq.eclair.wire.internal.channel.version0.ChannelCodecs0.Codecs._
import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.ChannelVersion
import fr.acinq.eclair.wire.protocol.{OnionRoutingPacket, UpdateAddHtlc}
import fr.acinq.eclair.{CltvExpiry, MilliSatoshiLong}
import org.scalatest.funsuite.AnyFunSuite
@ -36,7 +36,7 @@ class ChannelCodecs0Spec extends AnyFunSuite {
// Backwards-compatibility: decode localparams with global features.
val withGlobalFeatures = hex"033b1d42aa7c6a1a3502cbcfe4d2787e9f96237465cd1ba675f50cadf0be17092500010000002a0000000026cb536b00000000568a2768000000004f182e8d0000000040dd1d3d10e3040d00422f82d368b09056d1dcb2d67c4e8cae516abbbc8932f2b7d8f93b3be8e8cc6b64bb164563d567189bad0e07e24e821795aaef2dcbb9e5c1ad579961680202b38de5dd5426c524c7523b1fcdcf8c600d47f4b96a6dd48516b8e0006e81c83464b2800db0f3f63ceeb23a81511d159bae9ad07d10c0d144ba2da6f0cff30e7154eb48c908e9000101000001044500"
val withGlobalFeaturesDecoded = localParamsCodec(ChannelVersion.STANDARD).decode(withGlobalFeatures.bits).require.value
assert(withGlobalFeaturesDecoded.features.toByteVector === hex"0a8a")
assert(withGlobalFeaturesDecoded.initFeatures.toByteVector === hex"0a8a")
}
test("backward compatibility of htlc codec") {

View File

@ -6,9 +6,9 @@ import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.DeterministicWallet.KeyPath
import fr.acinq.bitcoin.{DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Script}
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.channel.{ChannelVersion, LocalParams, Origin, RemoteParams}
import fr.acinq.eclair.channel.{LocalParams, Origin, RemoteParams}
import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, OutgoingHtlc}
import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec.normal
import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.ChannelVersion
import fr.acinq.eclair.wire.internal.channel.version1.ChannelCodecs1.Codecs._
import fr.acinq.eclair.wire.protocol.UpdateAddHtlc
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, TestConstants, UInt64, randomBytes, randomBytes32, randomKey}
@ -70,7 +70,7 @@ class ChannelCodecs1Spec extends AnyFunSuite {
defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32()).publicKey)),
walletStaticPaymentBasepoint = None,
isFunder = Random.nextBoolean(),
features = Features(randomBytes(256)))
initFeatures = Features(randomBytes(256)))
val o1 = o.copy(walletStaticPaymentBasepoint = Some(PrivateKey(randomBytes32()).publicKey))
roundtrip(o, localParamsCodec(ChannelVersion.ZEROES))
@ -78,7 +78,6 @@ class ChannelCodecs1Spec extends AnyFunSuite {
roundtrip(o, localParamsCodec(ChannelVersion.ANCHOR_OUTPUTS))
}
test("encode/decode remoteparams") {
val o = RemoteParams(
nodeId = randomKey().publicKey,
@ -93,7 +92,8 @@ class ChannelCodecs1Spec extends AnyFunSuite {
paymentBasepoint = randomKey().publicKey,
delayedPaymentBasepoint = randomKey().publicKey,
htlcBasepoint = randomKey().publicKey,
features = TestConstants.Alice.nodeParams.features)
initFeatures = TestConstants.Alice.nodeParams.features,
shutdownScript = None)
val encoded = remoteParamsCodec.encode(o).require
val decoded = remoteParamsCodec.decodeValue(encoded).require
assert(o === decoded)
@ -101,7 +101,7 @@ class ChannelCodecs1Spec extends AnyFunSuite {
// Backwards-compatibility: decode remoteparams with global features.
val withGlobalFeatures = hex"03c70c3b813815a8b79f41622b6f2c343fa24d94fb35fa7110bbb3d4d59cd9612e0000000059844cbc000000001b1524ea000000001503cbac000000006b75d3272e38777e029fa4e94066163024177311de7ba1befec2e48b473c387bbcee1484bf276a54460215e3dfb8e6f262222c5f343f5e38c5c9a43d2594c7f06dd7ac1a4326c665dd050347aba4d56d7007a7dcf03594423dccba9ed700d11e665d261594e1154203df31020d457ee336ba6eeb328d00f1b8bd8bfefb8a4dcd5af6db4c438b7ec5106c7edc0380df17e1beb0f238e51a39122ac4c6fb57f3c4f5b7bc9432f991b1ef4a8af3570002020000018a"
val withGlobalFeaturesDecoded = remoteParamsCodec.decode(withGlobalFeatures.bits).require.value
assert(withGlobalFeaturesDecoded.features.toByteVector === hex"028a")
assert(withGlobalFeaturesDecoded.initFeatures.toByteVector === hex"028a")
}
test("encode/decode htlc") {
@ -197,12 +197,4 @@ class ChannelCodecs1Spec extends AnyFunSuite {
assert(spentMapCodec.decodeValue(spentMapCodec.encode(map).require).require === map)
}
test("basic serialization test (NORMAL)") {
val data = normal
val bin = DATA_NORMAL_Codec.encode(data).require
val check = DATA_NORMAL_Codec.decodeValue(bin).require
assert(data.commitments.localCommit.spec === check.commitments.localCommit.spec)
assert(data === check)
}
}

View File

@ -0,0 +1,58 @@
package fr.acinq.eclair.wire.internal.channel.version3
import fr.acinq.eclair.channel.{ChannelConfigOption, ChannelConfig}
import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec.normal
import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3.Codecs.{DATA_NORMAL_Codec, channelConfigCodec}
import org.scalatest.funsuite.AnyFunSuite
import scodec.bits.HexStringSyntax
class ChannelCodecs3Spec extends AnyFunSuite {
test("basic serialization test (NORMAL)") {
val data = normal
val bin = DATA_NORMAL_Codec.encode(data).require
val check = DATA_NORMAL_Codec.decodeValue(bin).require
assert(data.commitments.localCommit.spec === check.commitments.localCommit.spec)
assert(data === check)
}
test("encode/decode channel configuration options") {
assert(channelConfigCodec.encode(ChannelConfig(Set.empty[ChannelConfigOption])).require.bytes === hex"00")
assert(channelConfigCodec.decode(hex"00".bits).require.value === ChannelConfig(Set.empty[ChannelConfigOption]))
assert(channelConfigCodec.decode(hex"01f0".bits).require.value === ChannelConfig(Set.empty[ChannelConfigOption]))
assert(channelConfigCodec.decode(hex"020000".bits).require.value === ChannelConfig(Set.empty[ChannelConfigOption]))
assert(channelConfigCodec.encode(ChannelConfig.standard).require.bytes === hex"0101")
assert(channelConfigCodec.encode(ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath)).require.bytes === hex"0101")
assert(channelConfigCodec.decode(hex"0101".bits).require.value === ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath))
assert(channelConfigCodec.decode(hex"01ff".bits).require.value === ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath))
assert(channelConfigCodec.decode(hex"020001".bits).require.value === ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath))
}
test("decode all known channel configuration options") {
import scala.reflect.ClassTag
import scala.reflect.runtime.universe._
import scala.reflect.runtime.{universe => runtime}
val mirror = runtime.runtimeMirror(ClassLoader.getSystemClassLoader)
def extract[T: TypeTag](container: T)(implicit c: ClassTag[T]): Set[ChannelConfigOption] = {
typeOf[T].decls.filter(_.isPublic).flatMap(symbol => {
if (symbol.isTerm && symbol.isModule) {
mirror.reflectModule(symbol.asModule).instance match {
case f: ChannelConfigOption => Some(f)
case _ => None
}
} else {
None
}
}).toSet
}
val declaredOptions = extract(ChannelConfig)
assert(declaredOptions.nonEmpty)
val encoded = channelConfigCodec.encode(ChannelConfig(declaredOptions)).require
val decoded = channelConfigCodec.decode(encoded).require.value
assert(decoded.options === declaredOptions)
}
}