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:
parent
547d7e700f
commit
e9df4eece0
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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"
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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()))
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
)
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
}
|
||||
|
@ -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) ::
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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) ::
|
||||
|
@ -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) ::
|
||||
|
@ -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)
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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))
|
||||
|
@ -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]]
|
||||
|
@ -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])
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)))
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 =>
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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 =>
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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)))
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
@ -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") {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -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)
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user