diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index 71232305a9..c44049e359 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -78,11 +78,11 @@ case class Features(activated: Set[ActivatedFeature], unknown: Set[UnknownFeatur } /** - * Eclair-mobile thinks feature bit 15 (payment_secret) is gossip_queries_ex which creates issues, so we mask - * off basic_mpp and payment_secret. As long as they're provided in the invoice it's not an issue. - * We use a long enough mask to account for future features. - * TODO: remove that once eclair-mobile is patched. - */ + * Eclair-mobile thinks feature bit 15 (payment_secret) is gossip_queries_ex which creates issues, so we mask + * off basic_mpp and payment_secret. As long as they're provided in the invoice it's not an issue. + * We use a long enough mask to account for future features. + * TODO: remove that once eclair-mobile is patched. + */ def maskFeaturesForEclairMobile(): Features = { Features( activated = activated.filter { @@ -190,6 +190,11 @@ object Features { val mandatory = 18 } + case object AnchorOutputs extends Feature { + val rfcName = "option_anchor_outputs" + val mandatory = 20 + } + // TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605) // We're not advertising these bits yet in our announcements, clients have to assume support. // This is why we haven't added them yet to `areSupported`. @@ -214,6 +219,7 @@ object Features { Wumbo, TrampolinePayment, StaticRemoteKey, + AnchorOutputs, KeySend ) @@ -234,6 +240,7 @@ object Features { // invoices in their payment history. We choose to treat such invoices as valid; this is a harmless spec violation. // PaymentSecret -> (VariableLengthOnion :: Nil), BasicMultiPartPayment -> (PaymentSecret :: Nil), + AnchorOutputs -> (StaticRemoteKey :: Nil), TrampolinePayment -> (PaymentSecret :: Nil), KeySend -> (VariableLengthOnion :: Nil) ) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 29f9768710..d8d02e8d19 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -21,7 +21,6 @@ import akka.event.Logging.MDC import akka.pattern.pipe import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, OutPoint, Satoshi, Script, ScriptFlags, Transaction} -import fr.acinq.eclair.Features.StaticRemoteKey import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair._ import fr.acinq.eclair.blockchain._ @@ -33,6 +32,7 @@ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.relay.{Origin, Relayer} import fr.acinq.eclair.router.Announcements +import fr.acinq.eclair.transactions.Transactions.TxOwner import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire._ import scodec.bits.ByteVector @@ -292,10 +292,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Success(_) => context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = false, open.temporaryChannelId, open.feeratePerKw, None)) val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey - val channelVersion = Features.canUseFeature(localParams.features, remoteInit.features, StaticRemoteKey) match { - case false => ChannelVersion.STANDARD - case true => ChannelVersion.STATIC_REMOTEKEY - } + val channelVersion = ChannelVersion.pickChannelVersion(localParams.features, remoteInit.features) val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) // TODO: maybe also check uniqueness of temporary channel id val minimumDepth = Helpers.minDepthForFunding(nodeParams, open.fundingSatoshis) @@ -391,7 +388,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // let's create the first commitment tx that spends the yet uncommitted funding tx val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, channelVersion, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.fundingKeyPath)) + val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.fundingKeyPath), TxOwner.Remote, channelVersion.commitmentFormat) // signature of their initial commitment tx that pays remote pushMsat val fundingCreated = FundingCreated( temporaryChannelId = temporaryChannelId, @@ -434,12 +431,12 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // check remote signature validity val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) - val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey) + val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, channelVersion.commitmentFormat) val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) Transactions.checkSpendable(signedLocalCommitTx) match { - case Failure(cause) => handleLocalError(InvalidCommitmentSignature(temporaryChannelId, signedLocalCommitTx.tx), d, None) + case Failure(_) => handleLocalError(InvalidCommitmentSignature(temporaryChannelId, signedLocalCommitTx.tx), d, None) case Success(_) => - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey) + val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, channelVersion.commitmentFormat) val channelId = toLongId(fundingTxHash, fundingTxOutputIndex) // watch the funding tx transaction val commitInput = localCommitTx.input @@ -477,7 +474,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(msg@FundingSigned(_, remoteSig), d@DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, channelFlags, channelVersion, 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) + val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, channelVersion.commitmentFormat) val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) Transactions.checkSpendable(signedLocalCommitTx) match { case Failure(cause) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala index 8f10631ac0..52471274ee 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala @@ -22,7 +22,7 @@ import akka.actor.ActorRef import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, Transaction} import fr.acinq.eclair.transactions.CommitmentSpec -import fr.acinq.eclair.transactions.Transactions.{CommitTx, CommitmentFormat, DefaultCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitTx, CommitmentFormat, DefaultCommitmentFormat} import fr.acinq.eclair.wire.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc} import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, UInt64} import scodec.bits.{BitVector, ByteVector} @@ -257,7 +257,11 @@ case class ChannelVersion(bits: BitVector) { require(bits.size == ChannelVersion.LENGTH_BITS, "channel version takes 4 bytes") - val commitmentFormat: CommitmentFormat = DefaultCommitmentFormat + val commitmentFormat: CommitmentFormat = if (hasAnchorOutputs) { + AnchorOutputsCommitmentFormat + } else { + DefaultCommitmentFormat + } def |(other: ChannelVersion) = ChannelVersion(bits | other.bits) def &(other: ChannelVersion) = ChannelVersion(bits & other.bits) @@ -267,19 +271,33 @@ case class ChannelVersion(bits: BitVector) { def hasPubkeyKeyPath: Boolean = isSet(USE_PUBKEY_KEYPATH_BIT) def hasStaticRemotekey: Boolean = isSet(USE_STATIC_REMOTEKEY_BIT) + def hasAnchorOutputs: Boolean = isSet(USE_ANCHOR_OUTPUTS_BIT) } 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 private def setBit(bit: Int) = 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 | setBit(USE_PUBKEY_KEYPATH_BIT) val STATIC_REMOTEKEY = STANDARD | setBit(USE_STATIC_REMOTEKEY_BIT) // PUBKEY_KEYPATH + STATIC_REMOTEKEY + val ANCHOR_OUTPUTS = STATIC_REMOTEKEY | setBit(USE_ANCHOR_OUTPUTS_BIT) // PUBKEY_KEYPATH + STATIC_REMOTEKEY + ANCHOR_OUTPUTS } // @formatter:on diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 12fb953819..1cd520084b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -466,23 +466,20 @@ object Commitments { // remote commitment will includes all local changes + remote acked changes val spec = CommitmentSpec.reduce(remoteCommit.spec, remoteChanges.acked, localChanges.proposed) val (remoteCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeRemoteTxs(keyManager, channelVersion, remoteCommit.index + 1, localParams, remoteParams, commitInput, remoteNextPerCommitmentPoint, spec) - val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath)) + val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath), TxOwner.Remote, commitmentFormat) val sortedHtlcTxs: Seq[TransactionWithInputInfo] = (htlcTimeoutTxs ++ htlcSuccessTxs).sortBy(_.input.outPoint.index) val channelKeyPath = keyManager.channelKeyPath(commitments.localParams, commitments.channelVersion) - val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), remoteNextPerCommitmentPoint)) + 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 log.info(s"built remote commit number=${remoteCommit.index + 1} toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.feeratePerKw} txid=${remoteCommitTx.tx.txid} tx={}", spec.htlcs.collect(outgoing).map(_.id).mkString(","), spec.htlcs.collect(incoming).map(_.id).mkString(","), remoteCommitTx.tx) Metrics.recordHtlcsInFlight(spec, remoteCommit.spec) - // don't sign if they don't get paid val commitSig = CommitSig( channelId = commitments.channelId, signature = sig, - htlcSignatures = htlcSigs.toList - ) - + htlcSignatures = htlcSigs.toList) val commitments1 = commitments.copy( remoteNextCommitInfo = Left(WaitingForRevocation(RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint), commitSig, commitments.localCommit.index)), localChanges = localChanges.copy(proposed = Nil, signed = localChanges.proposed), @@ -511,20 +508,14 @@ object Commitments { log.warning("received a commit sig with no changes (probably coming from lnd)") } - // check that their signature is valid - // signatures are now optional in the commit message, and will be sent only if the other party is actually - // receiving money i.e its commit tx has one output for them - val spec = CommitmentSpec.reduce(localCommit.spec, localChanges.acked, remoteChanges.proposed) val channelKeyPath = keyManager.channelKeyPath(commitments.localParams, commitments.channelVersion) val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index + 1) val (localCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeLocalTxs(keyManager, channelVersion, localCommit.index + 1, localParams, remoteParams, commitInput, localPerCommitmentPoint, spec) - val sig = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath)) + val sig = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath), TxOwner.Local, commitmentFormat) 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) - // TODO: should we have optional sig? (original comment: this tx will NOT be signed if our output is empty) - // no need to compute htlc sigs if commit sig doesn't check out val signedCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, sig, commit.signature) if (Transactions.checkSpendable(signedCommitTx).isFailure) { @@ -535,7 +526,7 @@ object Commitments { if (commit.htlcSignatures.size != sortedHtlcTxs.size) { throw HtlcSigCountMismatch(commitments.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size) } - val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint)) + 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 { @@ -546,7 +537,8 @@ object Commitments { 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 - if (!Transactions.checkSig(htlcTx, remoteSig, remoteHtlcPubkey)) { + // we verify the signature from their point of view, where it is a remote tx + if (!Transactions.checkSig(htlcTx, remoteSig, remoteHtlcPubkey, TxOwner.Remote, commitmentFormat)) { throw InvalidHtlcSignature(commitments.channelId, htlcTx.tx) } HtlcTxAndSigs(htlcTx, localSig, remoteSig) @@ -705,4 +697,5 @@ object Commitments { | htlcs: |${commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.cltvExpiry}").mkString("\n")).getOrElse("N/A")}""".stripMargin } + } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 25f80818a4..e61012b5bb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -114,7 +114,7 @@ object Helpers { // BOLT #2: The receiving node MUST fail the channel if: max_accepted_htlcs is greater than 483. if (open.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) throw InvalidMaxAcceptedHtlcs(open.temporaryChannelId, open.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS) - // BOLT #2: The receiving node MUST fail the channel if: push_msat is greater than funding_satoshis * 1000. + // BOLT #2: The receiving node MUST fail the channel if: it considers feerate_per_kw too small for timely processing. if (isFeeTooSmall(open.feeratePerKw)) throw FeerateTooSmall(open.temporaryChannelId, open.feeratePerKw) // BOLT #2: The receiving node MUST fail the channel if: dust_limit_satoshis is greater than channel_reserve_satoshis. @@ -127,6 +127,7 @@ object Helpers { throw ChannelReserveNotMet(open.temporaryChannelId, toLocalMsat, toRemoteMsat, open.channelReserveSatoshis) } + // 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.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget) if (isFeeDiffTooHigh(localFeeratePerKw, open.feeratePerKw, nodeParams.onChainFeeConf.maxFeerateMismatch)) throw FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw) // only enforce dust limit check on mainnet @@ -221,7 +222,6 @@ object Helpers { /** * This indicates whether our side of the channel is above the reserve requested by our counterparty. In other words, * this tells if we can use the channel to make a payment. - * */ def aboveReserve(commitments: Commitments)(implicit log: LoggingAdapter): Boolean = { val remoteCommit = commitments.remoteNextCommitInfo match { @@ -235,6 +235,7 @@ object Helpers { result } + /** NB: this is a blocking call, use carefully! */ def getFinalScriptPubKey(wallet: EclairWallet, chainHash: ByteVector32): ByteVector = { import scala.concurrent.duration._ val finalAddress = Await.result(wallet.getReceiveAddress, 40 seconds) @@ -242,6 +243,7 @@ object Helpers { Script.write(addressToPublicKeyScript(finalAddress, chainHash)) } + /** NB: this is a blocking call, use carefully! */ def getWalletPaymentBasepoint(wallet: EclairWallet): PublicKey = { Await.result(wallet.getReceivePubkey(), 40 seconds) } @@ -369,7 +371,7 @@ object Helpers { /** * As soon as a tx spending the funding tx has reached min_depth, we know what the closing type will be, before - * the whole closing process finishes(e.g. there may still be delayed or unconfirmed child transactions). It can + * the whole closing process finishes (e.g. there may still be delayed or unconfirmed child transactions). It can * save us from attempting to publish some transactions. * * Note that we can't tell for mutual close before it is already final, because only one tx needs to be confirmed. @@ -473,7 +475,7 @@ object Helpers { log.debug("making closing tx with closingFee={} and commitments:\n{}", closingFee, Commitments.specs2String(commitments)) val dustLimitSatoshis = localParams.dustLimit.max(remoteParams.dustLimit) val closingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, dustLimitSatoshis, closingFee, localCommit.spec) - val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath)) + val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath), TxOwner.Local, commitmentFormat) val closingSigned = ClosingSigned(channelId, closingFee, localClosingSig) log.info(s"signed closing txid=${closingTx.tx.txid} with closingFeeSatoshis=${closingSigned.feeSatoshis}") log.debug(s"closingTxid=${closingTx.tx.txid} closingTx=${closingTx.tx}}") @@ -532,7 +534,7 @@ object Helpers { // first we will claim our main output as soon as the delay is over val mainDelayedTx = generateTx("main-delayed-output") { Transactions.makeClaimDelayedOutputTx(tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed).right.map(claimDelayed => { - val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint) + val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitmentFormat) Transactions.addSigs(claimDelayed, sig) }) } @@ -562,7 +564,7 @@ object Helpers { txinfo: TransactionWithInputInfo => generateTx("claim-htlc-delayed") { Transactions.makeClaimDelayedOutputTx(txinfo.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed).right.map(claimDelayed => { - val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint) + val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitmentFormat) Transactions.addSigs(claimDelayed, sig) }) } @@ -613,7 +615,7 @@ object Helpers { case OutgoingHtlc(add: UpdateAddHtlc) if preimages.exists(r => sha256(r) == add.paymentHash) => generateTx("claim-htlc-success") { val preimage = preimages.find(r => sha256(r) == add.paymentHash).get Transactions.makeClaimHtlcSuccessTx(remoteCommitTx.tx, outputs, localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc, commitments.commitmentFormat).right.map(txinfo => { - val sig = keyManager.sign(txinfo, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint) + val sig = keyManager.sign(txinfo, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint, TxOwner.Local, commitments.commitmentFormat) Transactions.addSigs(txinfo, sig, preimage) }) } @@ -623,7 +625,7 @@ object Helpers { // outgoing htlc: they may or may not have the preimage, the only thing to do is try to get back our funds after timeout case IncomingHtlc(add: UpdateAddHtlc) => generateTx("claim-htlc-timeout") { Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, outputs, localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc, commitments.commitmentFormat).right.map(txinfo => { - val sig = keyManager.sign(txinfo, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint) + val sig = keyManager.sign(txinfo, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint, TxOwner.Local, commitments.commitmentFormat) Transactions.addSigs(txinfo, sig) }) } @@ -663,7 +665,7 @@ object Helpers { val mainTx = generateTx("claim-p2wpkh-output") { Transactions.makeClaimP2WPKHOutputTx(tx, commitments.localParams.dustLimit, localPubkey, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain).right.map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint) + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitments.commitmentFormat) Transactions.addSigs(claimMain, localPubkey, sig) }) } @@ -718,7 +720,7 @@ object Helpers { None case _ => generateTx("claim-p2wpkh-output") { Transactions.makeClaimP2WPKHOutputTx(tx, localParams.dustLimit, localPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain).right.map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint) + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitmentFormat) Transactions.addSigs(claimMain, localPaymentPubkey, sig) }) } @@ -727,7 +729,7 @@ object Helpers { // then we punish them by stealing their main output val mainPenaltyTx = generateTx("main-penalty") { Transactions.makeMainPenaltyTx(tx, localParams.dustLimit, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, feeratePerKwPenalty).right.map(txinfo => { - val sig = keyManager.sign(txinfo, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret) + val sig = keyManager.sign(txinfo, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) Transactions.addSigs(txinfo, sig) }) } @@ -747,7 +749,7 @@ object Helpers { val htlcRedeemScript = htlcsRedeemScripts(txOut.publicKeyScript) generateTx("htlc-penalty") { Transactions.makeHtlcPenaltyTx(tx, outputIndex, htlcRedeemScript, localParams.dustLimit, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty).right.map(htlcPenalty => { - val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret) + val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) Transactions.addSigs(htlcPenalty, sig, remoteRevocationPubkey) }) } @@ -798,7 +800,7 @@ object Helpers { generateTx("claim-htlc-delayed-penalty") { Transactions.makeClaimDelayedOutputPenaltyTx(htlcTx, localParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty).right.map(htlcDelayedPenalty => { - val sig = keyManager.sign(htlcDelayedPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret) + val sig = keyManager.sign(htlcDelayedPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) val signedTx = Transactions.addSigs(htlcDelayedPenalty, sig) // we need to make sure that the tx is indeed valid Transaction.correctlySpends(signedTx.tx, Seq(htlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) @@ -1025,7 +1027,7 @@ object Helpers { val relevantOutpoints = tx.txIn.map(_.outPoint).filter { outPoint => // is this the commit tx itself ? (we could do this outside of the loop...) val isCommitTx = remoteCommitPublished.commitTx.txid == tx.txid - // does the tx spend an output of the local commitment tx? + // does the tx spend an output of the remote commitment tx? val spendsTheCommitTx = remoteCommitPublished.commitTx.txid == outPoint.txid isCommitTx || spendsTheCommitTx } @@ -1049,7 +1051,7 @@ object Helpers { val relevantOutpoints = tx.txIn.map(_.outPoint).filter { outPoint => // is this the commit tx itself ? (we could do this outside of the loop...) val isCommitTx = revokedCommitPublished.commitTx.txid == tx.txid - // does the tx spend an output of the local commitment tx? + // does the tx spend an output of the remote commitment tx? val spendsTheCommitTx = revokedCommitPublished.commitTx.txid == outPoint.txid // is the tx one of our 3rd stage delayed txes? (a 3rd stage tx is a tx spending the output of an htlc tx, which // is itself spending the output of the commitment tx) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala index 45f97d09fd..fc56d6e874 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala @@ -22,9 +22,9 @@ import java.nio.ByteOrder import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.DeterministicWallet.ExtendedPublicKey import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, DeterministicWallet, Protocol} -import fr.acinq.eclair.{Features, ShortChannelId} import fr.acinq.eclair.channel.{ChannelVersion, LocalParams} -import fr.acinq.eclair.transactions.Transactions.TransactionWithInputInfo +import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, TransactionWithInputInfo, TxOwner} +import fr.acinq.eclair.{Features, ShortChannelId} trait KeyManager { def nodeKey: DeterministicWallet.ExtendedPrivateKey @@ -54,77 +54,83 @@ trait KeyManager { } /** - * * @param isFunder true if we're funding this channel * @return a partial key path for a new funding public key. This key path will be extended: * - with a specific "chain" prefix * - with a specific "funding pubkey" suffix */ - def newFundingKeyPath(isFunder: Boolean) : DeterministicWallet.KeyPath + def newFundingKeyPath(isFunder: Boolean): DeterministicWallet.KeyPath /** - * - * @param tx input transaction - * @param publicKey extended public key - * @return a signature generated with the private key that matches the input - * extended public key - */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey): ByteVector64 + * @param tx input transaction + * @param publicKey extended public key + * @param txOwner owner of the transaction (local/remote) + * @param commitmentFormat format of the commitment tx + * @return a signature generated with the private key that matches the input + * extended public key + */ + def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 /** - * This method is used to spend funds send to htlc keys/delayed keys - * - * @param tx input transaction - * @param publicKey extended public key - * @param remotePoint remote point - * @return a signature generated with a private key generated from the input keys's matching - * private key and the remote point. - */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey): ByteVector64 + * This method is used to spend funds sent to htlc keys/delayed keys + * + * @param tx input transaction + * @param publicKey extended public key + * @param remotePoint remote point + * @param txOwner owner of the transaction (local/remote) + * @param commitmentFormat format of the commitment tx + * @return a signature generated with a private key generated from the input keys's matching + * private key and the remote point. + */ + def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 /** - * Ths method is used to spend revoked transactions, with the corresponding revocation key - * - * @param tx input transaction - * @param publicKey extended public key - * @param remoteSecret remote secret - * @return a signature generated with a private key generated from the input keys's matching - * private key and the remote secret. - */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: PrivateKey): ByteVector64 + * Ths method is used to spend revoked transactions, with the corresponding revocation key + * + * @param tx input transaction + * @param publicKey extended public key + * @param remoteSecret remote secret + * @param txOwner owner of the transaction (local/remote) + * @param commitmentFormat format of the commitment tx + * @return a signature generated with a private key generated from the input keys's matching + * private key and the remote secret. + */ + def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 /** - * Sign a channel announcement message - * - * @param fundingKeyPath BIP32 path of the funding public key - * @param chainHash chain hash - * @param shortChannelId short channel id - * @param remoteNodeId remote node id - * @param remoteFundingKey remote funding pubkey - * @param features channel features - * @return a (nodeSig, bitcoinSig) pair. nodeSig is the signature of the channel announcement with our node's - * private key, bitcoinSig is the signature of the channel announcement with our funding private key - */ + * Sign a channel announcement message + * + * @param fundingKeyPath BIP32 path of the funding public key + * @param chainHash chain hash + * @param shortChannelId short channel id + * @param remoteNodeId remote node id + * @param remoteFundingKey remote funding pubkey + * @param features channel features + * @return a (nodeSig, bitcoinSig) pair. nodeSig is the signature of the channel announcement with our node's + * private key, bitcoinSig is the signature of the channel announcement with our funding private key + */ def signChannelAnnouncement(fundingKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: Features): (ByteVector64, ByteVector64) } object KeyManager { /** - * Create a BIP32 path from a public key. This path will be used to derive channel keys. - * Having channel keys derived from the funding public keys makes it very easy to retrieve your funds when've you've lost your data: - * - connect to your peer and use DLP 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 - * - * @param fundingPubKey funding public key - * @return a BIP32 path - */ - def channelKeyPath(fundingPubKey: PublicKey) : DeterministicWallet.KeyPath = { + * Create a BIP32 path from a public key. This path will be used to derive channel keys. + * Having channel keys derived from the funding public keys makes it very easy to retrieve your funds when've you've lost your data: + * - connect to your peer and use DLP 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 + * + * @param fundingPubKey funding public key + * @return a BIP32 path + */ + def channelKeyPath(fundingPubKey: PublicKey): DeterministicWallet.KeyPath = { val buffer = Crypto.sha256(fundingPubKey.value) val bis = new ByteArrayInputStream(buffer.toArray) + def next() = Protocol.uint32(bis, ByteOrder.BIG_ENDIAN) + DeterministicWallet.KeyPath(Seq(next(), next(), next(), next(), next(), next(), next(), next())) } - def channelKeyPath(fundingPubKey: DeterministicWallet.ExtendedPublicKey) : DeterministicWallet.KeyPath = channelKeyPath(fundingPubKey.publicKey) + def channelKeyPath(fundingPubKey: DeterministicWallet.ExtendedPublicKey): DeterministicWallet.KeyPath = channelKeyPath(fundingPubKey.publicKey) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala index 9f514905b6..d40b060aff 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala @@ -22,7 +22,7 @@ import fr.acinq.bitcoin.DeterministicWallet.{derivePrivateKey, _} import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet} import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.transactions.Transactions.TransactionWithInputInfo +import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, TransactionWithInputInfo, TxOwner} import fr.acinq.eclair.{Features, ShortChannelId, secureRandom} import scodec.bits.ByteVector @@ -32,7 +32,6 @@ object LocalKeyManager { case Block.LivenetGenesisBlock.hash => DeterministicWallet.hardened(47) :: DeterministicWallet.hardened(1) :: Nil } - // WARNING: if you change this path, you will change your node id even if the seed remains the same!!! // Note that the node path and the above channel path are on different branches so even if the // node key is compromised there is no way to retrieve the wallet keys @@ -43,11 +42,11 @@ object LocalKeyManager { } /** - * This class manages secrets and private keys. - * It exports points and public keys, and provides signing methods - * - * @param seed seed from which keys will be derived - */ + * This class manages secrets and private keys. + * It exports points and public keys, and provides signing methods + * + * @param seed seed from which keys will be derived + */ class LocalKeyManager(seed: ByteVector, chainHash: ByteVector32) extends KeyManager { private val master = DeterministicWallet.generate(seed) @@ -57,14 +56,14 @@ class LocalKeyManager(seed: ByteVector, chainHash: ByteVector32) extends KeyMana private val privateKeys: LoadingCache[KeyPath, ExtendedPrivateKey] = CacheBuilder.newBuilder() .maximumSize(6 * 200) // 6 keys per channel * 200 channels .build[KeyPath, ExtendedPrivateKey](new CacheLoader[KeyPath, ExtendedPrivateKey] { - override def load(keyPath: KeyPath): ExtendedPrivateKey = derivePrivateKey(master, keyPath) - }) + override def load(keyPath: KeyPath): ExtendedPrivateKey = derivePrivateKey(master, keyPath) + }) private val publicKeys: LoadingCache[KeyPath, ExtendedPublicKey] = CacheBuilder.newBuilder() .maximumSize(6 * 200) // 6 keys per channel * 200 channels .build[KeyPath, ExtendedPublicKey](new CacheLoader[KeyPath, ExtendedPublicKey] { - override def load(keyPath: KeyPath): ExtendedPublicKey = publicKey(privateKeys.get(keyPath)) - }) + override def load(keyPath: KeyPath): ExtendedPublicKey = publicKey(privateKeys.get(keyPath)) + }) private def internalKeyPath(channelKeyPath: DeterministicWallet.KeyPath, index: Long): List[Long] = (LocalKeyManager.channelKeyBasePath(chainHash) ++ channelKeyPath.path) :+ index @@ -82,7 +81,9 @@ class LocalKeyManager(seed: ByteVector, chainHash: ByteVector32) extends KeyMana override def newFundingKeyPath(isFunder: Boolean): KeyPath = { val last = DeterministicWallet.hardened(if (isFunder) 1 else 0) + def next() = secureRandom.nextInt() & 0xFFFFFFFFL + DeterministicWallet.KeyPath(Seq(next(), next(), next(), next(), next(), next(), next(), next(), last)) } @@ -101,46 +102,50 @@ class LocalKeyManager(seed: ByteVector, chainHash: ByteVector32) extends KeyMana override def commitmentPoint(channelKeyPath: DeterministicWallet.KeyPath, index: Long) = Generators.perCommitPoint(shaSeed(channelKeyPath), index) /** - * - * @param tx input transaction - * @param publicKey extended public key - * @return a signature generated with the private key that matches the input - * extended public key - */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey): ByteVector64 = { + * @param tx input transaction + * @param publicKey extended public key + * @param txOwner owner of the transaction (local/remote) + * @param commitmentFormat format of the commitment tx + * @return a signature generated with the private key that matches the input + * extended public key + */ + override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = { val privateKey = privateKeys.get(publicKey.path) - Transactions.sign(tx, privateKey.privateKey) + Transactions.sign(tx, privateKey.privateKey, txOwner, commitmentFormat) } /** - * This method is used to spend funds send to htlc keys/delayed keys - * - * @param tx input transaction - * @param publicKey extended public key - * @param remotePoint remote point - * @return a signature generated with a private key generated from the input keys's matching - * private key and the remote point. - */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey): ByteVector64 = { + * This method is used to spend funds sent to htlc keys/delayed keys + * + * @param tx input transaction + * @param publicKey extended public key + * @param remotePoint remote point + * @param txOwner owner of the transaction (local/remote) + * @param commitmentFormat format of the commitment tx + * @return a signature generated with a private key generated from the input keys's matching + * private key and the remote point. + */ + override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = { val privateKey = privateKeys.get(publicKey.path) val currentKey = Generators.derivePrivKey(privateKey.privateKey, remotePoint) - Transactions.sign(tx, currentKey) + Transactions.sign(tx, currentKey, txOwner, commitmentFormat) } - /** - * Ths method is used to spend revoked transactions, with the corresponding revocation key - * - * @param tx input transaction - * @param publicKey extended public key - * @param remoteSecret remote secret - * @return a signature generated with a private key generated from the input keys's matching - * private key and the remote secret. - */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: PrivateKey): ByteVector64 = { + * Ths method is used to spend revoked transactions, with the corresponding revocation key + * + * @param tx input transaction + * @param publicKey extended public key + * @param remoteSecret remote secret + * @param txOwner owner of the transaction (local/remote) + * @param commitmentFormat format of the commitment tx + * @return a signature generated with a private key generated from the input keys's matching + * private key and the remote secret. + */ + override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = { val privateKey = privateKeys.get(publicKey.path) val currentKey = Generators.revocationPrivKey(privateKey.privateKey, remoteSecret) - Transactions.sign(tx, currentKey) + Transactions.sign(tx, currentKey, txOwner, commitmentFormat) } override def signChannelAnnouncement(fundingKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: Features): (ByteVector64, ByteVector64) = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 4200568a2e..727360edf7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -25,12 +25,10 @@ import akka.util.Timeout import com.google.common.net.HostAndPort import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, Satoshi, Script} -import fr.acinq.eclair.FeatureSupport.Optional -import fr.acinq.eclair.Features.{StaticRemoteKey, Wumbo, canUseFeature} +import fr.acinq.eclair.Features.Wumbo import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.channel._ -import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Monitoring.Metrics import fr.acinq.eclair.wire._ import fr.acinq.eclair.{wire, _} @@ -121,11 +119,8 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, watcher: ActorRe 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 = canUseFeature(d.localInit.features, d.remoteInit.features, StaticRemoteKey) match { - case false => ChannelVersion.STANDARD - case true => ChannelVersion.STATIC_REMOTEKEY - } - val (channel, localParams) = createNewChannel(nodeParams, funder = true, c.fundingSatoshis, origin_opt = Some(sender), channelVersion) + val channelVersion = ChannelVersion.pickChannelVersion(d.localInit.features, d.remoteInit.features) + val (channel, localParams) = createNewChannel(nodeParams, funder = true, c.fundingSatoshis, origin_opt = Some(sender), channelVersion) c.timeout_opt.map(openTimeout => context.system.scheduler.scheduleOnce(openTimeout.duration, channel, Channel.TickChannelOpenTimeout)(context.dispatcher)) val temporaryChannelId = randomBytes32 val channelFeeratePerKw = nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget) @@ -138,10 +133,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, watcher: ActorRe case Event(msg: wire.OpenChannel, d: ConnectedData) => d.channels.get(TemporaryChannelId(msg.temporaryChannelId)) match { case None => - val channelVersion = canUseFeature(d.localInit.features, d.remoteInit.features, StaticRemoteKey) match { - case false => ChannelVersion.STANDARD - case true => ChannelVersion.STATIC_REMOTEKEY - } + val channelVersion = ChannelVersion.pickChannelVersion(d.localInit.features, d.remoteInit.features) val (channel, localParams) = createNewChannel(nodeParams, funder = false, fundingAmount = msg.fundingSatoshis, origin_opt = None, channelVersion) val temporaryChannelId = msg.temporaryChannelId log.info(s"accepting a new channel with temporaryChannelId=$temporaryChannelId localParams=$localParams") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/package.scala b/eclair-core/src/main/scala/fr/acinq/eclair/package.scala index 99dac1ddbb..b26d4350c7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/package.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/package.scala @@ -47,7 +47,6 @@ package object eclair { def toLongId(fundingTxHash: ByteVector32, fundingOutputIndex: Int): ByteVector32 = { require(fundingOutputIndex < 65536, "fundingOutputIndex must not be greater than FFFF") - require(fundingTxHash.size == 32, "fundingTxHash must be of length 32B") val channelId = ByteVector32(fundingTxHash.take(30) :+ (fundingTxHash(30) ^ (fundingOutputIndex >> 8)).toByte :+ (fundingTxHash(31) ^ fundingOutputIndex).toByte) channelId } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala index e27280890a..a82bd60acc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala @@ -31,12 +31,12 @@ object Scripts { /** * Convert a raw ECDSA signature (r,s) to a der-encoded signature that can be used in bitcoin scripts. * - * @param sig raw ECDSA signature (r,s) - * @param sighash sighash flags + * @param sig raw ECDSA signature (r,s) + * @param sighashType sighash flags */ - def der(sig: ByteVector64, sighash: Int = SIGHASH_ALL): ByteVector = Crypto.compact2der(sig) :+ sighash.toByte + def der(sig: ByteVector64, sighashType: Int = SIGHASH_ALL): ByteVector = Crypto.compact2der(sig) :+ sighashType.toByte - def htlcRemoteSighash(commitmentFormat: CommitmentFormat): Int = commitmentFormat match { + private def htlcRemoteSighash(commitmentFormat: CommitmentFormat): Int = commitmentFormat match { case DefaultCommitmentFormat => SIGHASH_ALL case AnchorOutputsCommitmentFormat => SIGHASH_SINGLE | SIGHASH_ANYONECANPAY } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index b4306f2f03..7fe07c8991 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -72,6 +72,13 @@ object Transactions { def apply(outPoint: OutPoint, txOut: TxOut, redeemScript: Seq[ScriptElt]) = new InputInfo(outPoint, txOut, Script.write(redeemScript)) } + /** Owner of a given transaction (local/remote). */ + sealed trait TxOwner + object TxOwner { + case object Local extends TxOwner + case object Remote extends TxOwner + } + sealed trait TransactionWithInputInfo { def input: InputInfo def tx: Transaction @@ -80,11 +87,22 @@ object Transactions { val vsize = (tx.weight() + 3) / 4 Satoshi(fr.acinq.eclair.MinimumRelayFeeRate * vsize / 1000) } + /** Sighash flags to use when signing the transaction. */ + def sighash(txOwner: TxOwner, commitmentFormat: CommitmentFormat): Int = SIGHASH_ALL } case class CommitTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo - case class HtlcSuccessTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32) extends TransactionWithInputInfo - case class HtlcTimeoutTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo + sealed trait HtlcTx extends TransactionWithInputInfo { + override def sighash(txOwner: TxOwner, commitmentFormat: CommitmentFormat): Int = commitmentFormat match { + case DefaultCommitmentFormat => SIGHASH_ALL + case AnchorOutputsCommitmentFormat => txOwner match { + case TxOwner.Local => SIGHASH_ALL + case TxOwner.Remote => SIGHASH_SINGLE | SIGHASH_ANYONECANPAY + } + } + } + case class HtlcSuccessTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32) extends HtlcTx + case class HtlcTimeoutTx(input: InputInfo, tx: Transaction) extends HtlcTx case class ClaimHtlcSuccessTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo case class ClaimHtlcTimeoutTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo case class ClaimAnchorOutputTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo @@ -715,11 +733,13 @@ object Transactions { sig64 } - def sign(txinfo: TransactionWithInputInfo, key: PrivateKey, sighashType: Int = SIGHASH_ALL): ByteVector64 = { + def sign(txinfo: TransactionWithInputInfo, key: PrivateKey, sighashType: Int): ByteVector64 = { require(txinfo.tx.txIn.lengthCompare(1) == 0, "only one input allowed") sign(txinfo.tx, txinfo.input.redeemScript, txinfo.input.txOut.amount, key, sighashType) } + def sign(txinfo: TransactionWithInputInfo, key: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = sign(txinfo, key, txinfo.sighash(txOwner, commitmentFormat)) + def addSigs(commitTx: CommitTx, localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: ByteVector64, remoteSig: ByteVector64): CommitTx = { val witness = Scripts.witness2of2(localSig, remoteSig, localFundingPubkey, remoteFundingPubkey) commitTx.copy(tx = commitTx.tx.updateWitness(0, witness)) @@ -788,8 +808,9 @@ object Transactions { def checkSpendable(txinfo: TransactionWithInputInfo): Try[Unit] = Try(Transaction.correctlySpends(txinfo.tx, Map(txinfo.tx.txIn.head.outPoint -> txinfo.input.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - def checkSig(txinfo: TransactionWithInputInfo, sig: ByteVector64, pubKey: PublicKey): Boolean = { - val data = Transaction.hashForSigning(txinfo.tx, inputIndex = 0, txinfo.input.redeemScript, SIGHASH_ALL, txinfo.input.txOut.amount, SIGVERSION_WITNESS_V0) + def checkSig(txinfo: TransactionWithInputInfo, sig: ByteVector64, pubKey: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): Boolean = { + val sighash = txinfo.sighash(txOwner, commitmentFormat) + val data = Transaction.hashForSigning(txinfo.tx, inputIndex = 0, txinfo.input.redeemScript, sighash, txinfo.input.txOut.amount, SIGVERSION_WITNESS_V0) Crypto.verifySignature(data, sig, pubKey) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala index e90eb23edd..084a053582 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -61,7 +61,6 @@ class FeaturesSpec extends AnyFunSuite { assert(Features(hex"2000").hasFeature(StaticRemoteKey, Some(Optional))) } - test("features dependencies") { val testCases = Map( bin" " -> true, @@ -82,7 +81,14 @@ class FeaturesSpec extends AnyFunSuite { bin"000000101000000000000000" -> true, // we allow not setting var_onion_optin bin"000000011000000000000000" -> true, // we allow not setting var_onion_optin bin"000000011000001000000000" -> true, - bin"000000100100000100000000" -> true + bin"000000100100000100000000" -> true, + // option_anchor_outputs depends on option_static_remotekey + bin"001000000000000000000000" -> false, + bin"000100000000000000000000" -> false, + bin"001000000010000000000000" -> true, + bin"001000000001000000000000" -> true, + bin"000100000010000000000000" -> true, + bin"000100000001000000000000" -> true ) for ((testCase, valid) <- testCases) { @@ -112,6 +118,8 @@ class FeaturesSpec extends AnyFunSuite { assert(areSupported(Features(Set(ActivatedFeature(BasicMultiPartPayment, Optional))))) assert(areSupported(Features(Set(ActivatedFeature(Wumbo, Mandatory))))) assert(areSupported(Features(Set(ActivatedFeature(Wumbo, Optional))))) + assert(!areSupported(Features(Set(ActivatedFeature(AnchorOutputs, Mandatory))))) // NB: we're not ready to fully support anchor outputs + assert(areSupported(Features(Set(ActivatedFeature(AnchorOutputs, Optional))))) val testCases = Map( bin" 00000000000000001011" -> true, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala index 0273bfde9e..62e03cdfca 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala @@ -1,12 +1,48 @@ package fr.acinq.eclair.channel +import fr.acinq.eclair.transactions.Transactions import org.scalatest.funsuite.AnyFunSuite class ChannelTypesSpec extends AnyFunSuite { + 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("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") { + import fr.acinq.eclair.FeatureSupport._ + import fr.acinq.eclair.Features._ + import fr.acinq.eclair.{ActivatedFeature, Features} + + case class TestCase(localFeatures: Features, remoteFeatures: Features, expectedChannelVersion: ChannelVersion) + val testCases = Seq( + TestCase(Features.empty, Features.empty, ChannelVersion.STANDARD), + TestCase(Features(Set(ActivatedFeature(StaticRemoteKey, Optional))), Features.empty, ChannelVersion.STANDARD), + TestCase(Features.empty, Features(Set(ActivatedFeature(StaticRemoteKey, Optional))), ChannelVersion.STANDARD), + TestCase(Features(Set(ActivatedFeature(StaticRemoteKey, Optional))), Features(Set(ActivatedFeature(StaticRemoteKey, Optional))), ChannelVersion.STATIC_REMOTEKEY), + TestCase(Features(Set(ActivatedFeature(StaticRemoteKey, Optional))), Features(Set(ActivatedFeature(StaticRemoteKey, Mandatory))), ChannelVersion.STATIC_REMOTEKEY), + TestCase(Features(Set(ActivatedFeature(StaticRemoteKey, Optional), ActivatedFeature(AnchorOutputs, Optional))), Features(Set(ActivatedFeature(StaticRemoteKey, Optional))), ChannelVersion.STATIC_REMOTEKEY), + TestCase(Features(Set(ActivatedFeature(StaticRemoteKey, Mandatory), ActivatedFeature(AnchorOutputs, Optional))), Features(Set(ActivatedFeature(StaticRemoteKey, Optional), ActivatedFeature(AnchorOutputs, Optional))), ChannelVersion.ANCHOR_OUTPUTS) + ) + + for (testCase <- testCases) { + assert(ChannelVersion.pickChannelVersion(testCase.localFeatures, testCase.remoteFeatures) === testCase.expectedChannelVersion) + } + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RecoverySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RecoverySpec.scala index fbf17743fa..2b06697e37 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RecoverySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RecoverySpec.scala @@ -8,7 +8,7 @@ import fr.acinq.eclair.blockchain.WatchEventSpent import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.crypto.{Generators, KeyManager} import fr.acinq.eclair.transactions.Scripts -import fr.acinq.eclair.transactions.Transactions.{ClaimP2WPKHOutputTx, InputInfo} +import fr.acinq.eclair.transactions.Transactions.{ClaimP2WPKHOutputTx, DefaultCommitmentFormat, InputInfo, TxOwner} import fr.acinq.eclair.wire.{ChannelReestablish, CommitSig, Error, Init, RevokeAndAck} import fr.acinq.eclair.{TestConstants, TestKitBaseClass, _} import org.scalatest.Outcome @@ -116,7 +116,9 @@ class RecoverySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Sta val sig = keyManager.sign( ClaimP2WPKHOutputTx(InputInfo(OutPoint(bobCommitTx, bobCommitTx.txOut.indexOf(ourOutput)), ourOutput, Script.pay2pkh(ourToRemotePubKey)), tx), keyManager.paymentPoint(channelKeyPath), - ce.myCurrentPerCommitmentPoint) + ce.myCurrentPerCommitmentPoint, + TxOwner.Local, + DefaultCommitmentFormat) val tx1 = tx.updateWitness(0, ScriptWitness(Scripts.der(sig) :: ourToRemotePubKey.value :: Nil)) Transaction.correctlySpends(tx1, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala index 608e88fbf6..f4ec331b21 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala @@ -21,8 +21,6 @@ import java.util.UUID import akka.testkit.{TestFSMRef, TestKitBase, TestProbe} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, Crypto, ScriptFlags, Transaction} -import fr.acinq.eclair.FeatureSupport.Optional -import fr.acinq.eclair.Features.StaticRemoteKey import fr.acinq.eclair.TestConstants.{Alice, Bob, TestFeeEstimator} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeTargets @@ -32,9 +30,8 @@ import fr.acinq.eclair.payment.OutgoingPacket import fr.acinq.eclair.router.Router.ChannelHop import fr.acinq.eclair.wire.Onion.FinalLegacyPayload import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{NodeParams, TestConstants, randomBytes32, _} +import fr.acinq.eclair.{FeatureSupport, Features, NodeParams, TestConstants, randomBytes32, _} import org.scalatest.{FixtureTestSuite, ParallelTestExecution} -import scodec.bits._ import scala.concurrent.duration._ @@ -77,17 +74,22 @@ trait StateTestsHelperMethods extends TestKitBase with FixtureTestSuite with Par SetupFixture(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, relayerA, relayerB, channelUpdateListener, wallet) } - def reachNormal(setup: SetupFixture, - tags: Set[String] = Set.empty): Unit = { + def reachNormal(setup: SetupFixture, tags: Set[String] = Set.empty): Unit = { import setup._ - val channelVersion = if(tags.contains("static_remotekey")) ChannelVersion.STATIC_REMOTEKEY else ChannelVersion.STANDARD val channelFlags = if (tags.contains("channels_public")) ChannelFlags.AnnounceChannel else ChannelFlags.Empty val pushMsat = if (tags.contains("no_push_msat")) 0.msat else TestConstants.pushMsat - val (aliceParams, bobParams) = if(tags.contains("static_remotekey")) { - (Alice.channelParams.copy(features = Features(Set(ActivatedFeature(StaticRemoteKey, Optional))), staticPaymentBasepoint = Some(Helpers.getWalletPaymentBasepoint(wallet))), - Bob.channelParams.copy(features = Features(Set(ActivatedFeature(StaticRemoteKey, Optional))), staticPaymentBasepoint = Some(Helpers.getWalletPaymentBasepoint(wallet)))) + val (aliceParams, bobParams, channelVersion) = if (tags.contains("anchor_outputs")) { + val features = Features(Set(ActivatedFeature(Features.StaticRemoteKey, FeatureSupport.Mandatory), ActivatedFeature(Features.AnchorOutputs, FeatureSupport.Optional))) + val aliceParams = Alice.channelParams.copy(features = features, staticPaymentBasepoint = Some(Helpers.getWalletPaymentBasepoint(wallet))) + val bobParams = Bob.channelParams.copy(features = features, staticPaymentBasepoint = Some(Helpers.getWalletPaymentBasepoint(wallet))) + (aliceParams, bobParams, ChannelVersion.ANCHOR_OUTPUTS) + } else if (tags.contains("static_remotekey")) { + val features = Features(Set(ActivatedFeature(Features.StaticRemoteKey, FeatureSupport.Optional))) + val aliceParams = Alice.channelParams.copy(features = features, staticPaymentBasepoint = Some(Helpers.getWalletPaymentBasepoint(wallet))) + val bobParams = Bob.channelParams.copy(features = features, staticPaymentBasepoint = Some(Helpers.getWalletPaymentBasepoint(wallet))) + (aliceParams, bobParams, ChannelVersion.STATIC_REMOTEKEY) } else { - (Alice.channelParams, Bob.channelParams) + (Alice.channelParams, Bob.channelParams, ChannelVersion.STANDARD) } val aliceInit = Init(aliceParams.features) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index ac27b950f0..f7acaabc73 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -29,7 +29,7 @@ import fr.acinq.eclair.wire.{AcceptChannel, ChannelTlv, Error, Init, OpenChannel import fr.acinq.eclair.{ActivatedFeature, CltvExpiryDelta, Features, LongToBtcAmount, TestConstants, TestKitBaseClass} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} -import scodec.bits.{ByteVector, HexStringSyntax} +import scodec.bits.ByteVector import scala.concurrent.duration._ import scala.concurrent.{Future, Promise} @@ -66,7 +66,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val aliceInit = Init(aliceParams.features) val bobInit = Init(bobParams.features) within(30 seconds) { - val fundingAmount = if(test.tags.contains("wumbo")) Btc(5).toSatoshi else TestConstants.fundingSatoshis + val fundingAmount = if (test.tags.contains("wumbo")) Btc(5).toSatoshi else TestConstants.fundingSatoshis alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingAmount, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelVersion) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] @@ -160,7 +160,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS awaitCond(alice.stateName == CLOSED) } - test("recv AcceptChannel (wumbo size channel)", Tag("wumbo"), Tag("high-max-funding-size")) { f => + test("recv AcceptChannel (wumbo size channel)", Tag("wumbo"), Tag("high-max-funding-size")) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] assert(accept.minimumDepth == 13) // with wumbo tag we use fundingSatoshis=5BTC diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 3e3bcab059..6820da521c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -211,6 +211,20 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectNoMsg(200 millis) } + test("recv CMD_ADD_HTLC (insufficient funds) (anchor outputs)", Tag("anchor_outputs")) { f => + import f._ + 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) + val add = CMD_ADD_HTLC(initialState.commitments.availableBalanceForSend + 1.msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, Upstream.Local(UUID.randomUUID())) + sender.send(alice, add) + + val error = InsufficientFunds(channelId(alice), amount = add.amount, missing = 0 sat, reserve = 20000 sat, fees = 13620 sat) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Origin.Local(add.upstream.asInstanceOf[Upstream.Local].id, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + alice2bob.expectNoMsg(200 millis) + } + test("recv CMD_ADD_HTLC (insufficient funds, missing 1 msat)") { f => import f._ val sender = TestProbe() @@ -220,7 +234,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val error = InsufficientFunds(channelId(alice), amount = add.amount, missing = 0 sat, reserve = 10000 sat, fees = 0 sat) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Origin.Local(add.upstream.asInstanceOf[Upstream.Local].id, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) - alice2bob.expectNoMsg(200 millis) + bob2alice.expectNoMsg(200 millis) } test("recv CMD_ADD_HTLC (HTLC dips into remote funder fee reserve)") { f => @@ -467,6 +481,22 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2blockchain.expectMsgType[WatchConfirmed] } + test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs) (anchor outputs)", Tag("anchor_outputs")) { f => + import f._ + val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.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)) + val error = bob2alice.expectMsgType[Error] + assert(new String(error.data.toArray) === InsufficientFunds(channelId(bob), amount = 100000000 msat, missing = 37060 sat, reserve = 20000 sat, fees = 17060 sat).getMessage) + awaitCond(bob.stateName == CLOSING) + // channel should be advertised as down + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + bob2blockchain.expectMsg(PublishAsap(tx)) + bob2blockchain.expectMsgType[PublishAsap] + bob2blockchain.expectMsgType[WatchConfirmed] + } + 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 @@ -874,7 +904,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv CommitSig (invalid signature)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) + addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx // actual test begins @@ -1128,14 +1158,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(forward.htlc === htlc) } - test("recv RevokeAndAck (one htlc sent, static_remotekey)", Tag("static_remotekey")) { f => + def testRevokeAndAckHtlcStaticRemoteKey(f: FixtureParam): Unit = { import f._ val sender = TestProbe() assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.features.hasFeature(StaticRemoteKey)) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.features.hasFeature(StaticRemoteKey)) - def aliceToRemoteScript() = { + 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) toRemoteOut.publicKeyScript @@ -1165,6 +1195,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(initialToRemoteScript == aliceToRemoteScript()) } + test("recv RevokeAndAck (one htlc sent, static_remotekey)", Tag("static_remotekey")) { + testRevokeAndAckHtlcStaticRemoteKey _ + } + + test("recv RevokeAndAck (one htlc sent, anchor outputs)", Tag("anchor_outputs")) { + testRevokeAndAckHtlcStaticRemoteKey _ + } + test("recv RevocationTimeout") { f => import f._ val sender = TestProbe() @@ -1207,6 +1245,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testReceiveCmdFulfillHtlc _ } + test("recv CMD_FULFILL_HTLC (anchor_outputs)", Tag("anchor_outputs")) { + testReceiveCmdFulfillHtlc _ + } + test("recv CMD_FULFILL_HTLC (unknown htlc id)") { f => import f._ val sender = TestProbe() @@ -1241,7 +1283,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.underlyingActor.nodeParams.db.pendingRelay.listPendingRelay(initialState.channelId).isEmpty) } - private def testUpdateFulfillHtlc(f: FixtureParam) = { + private def testUpdateFulfillHtlc(f: FixtureParam): Unit = { import f._ val sender = TestProbe() val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) @@ -1269,6 +1311,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testUpdateFulfillHtlc _ } + test("recv UpdateFulfillHtlc (anchor_outputs)", Tag("anchor_outputs")) { + testUpdateFulfillHtlc _ + } + test("recv UpdateFulfillHtlc (sender has not signed htlc)") { f => import f._ val sender = TestProbe() @@ -1324,7 +1370,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectMsgType[WatchConfirmed] } - private def testCmdFailHtlc(f: FixtureParam) = { + private def testCmdFailHtlc(f: FixtureParam): Unit = { import f._ val sender = TestProbe() val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) @@ -1338,7 +1384,6 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateData == initialState.copy( commitments = initialState.commitments.copy( localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fail)))) - } test("recv CMD_FAIL_HTLC") { @@ -1349,6 +1394,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testCmdFailHtlc _ } + test("recv CMD_FAIL_HTLC (anchor_outputs)", Tag("anchor_outputs")) { + testCmdFailHtlc _ + } + test("recv CMD_FAIL_HTLC (unknown htlc id)") { f => import f._ val sender = TestProbe() @@ -1415,7 +1464,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.underlyingActor.nodeParams.db.pendingRelay.listPendingRelay(initialState.channelId).isEmpty) } - private def testUpdateFailHtlc(f: FixtureParam) = { + private def testUpdateFailHtlc(f: FixtureParam): Unit = { import f._ val sender = TestProbe() val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) @@ -1436,10 +1485,15 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv UpdateFailHtlc") { testUpdateFailHtlc _ } + test("recv UpdateFailHtlc (static_remotekey)", Tag("static_remotekey")) { testUpdateFailHtlc _ } + test("recv UpdateFailHtlc (anchor_outputs)", Tag("anchor_outputs")) { + testUpdateFailHtlc _ + } + test("recv UpdateFailMalformedHtlc") { f => import f._ val sender = TestProbe() @@ -1537,7 +1591,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(fail.reason.length === Sphinx.FailurePacket.PacketLength) } - test("recv CMD_UPDATE_FEE") { f => + private def testCmdUpdateFee(f: FixtureParam): Unit = { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] @@ -1549,6 +1603,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fee)))) } + test("recv CMD_UPDATE_FEE") { + testCmdUpdateFee _ + } + + test("recv CMD_UPDATE_FEE (anchor outputs)", Tag("anchor_outputs")) { + testCmdUpdateFee _ + } + test("recv CMD_UPDATE_FEE (two in a row)") { f => import f._ val sender = TestProbe() @@ -1581,6 +1643,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ fee), remoteNextHtlcId = 0))) } + test("recv UpdateFee (anchor outputs)", Tag("anchor_outputs")) { f => + import f._ + val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] + val fee = UpdateFee(ByteVector32.Zeroes, 8000) + bob ! fee + awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ fee), remoteNextHtlcId = 0))) + } + test("recv UpdateFee (two in a row)") { f => import f._ val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] @@ -1623,6 +1693,25 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2blockchain.expectMsgType[WatchConfirmed] } + test("recv UpdateFee (sender can't afford it) (anchor outputs)", Tag("anchor_outputs")) { f => + import f._ + val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx + val sender = TestProbe() + // This feerate is just above the threshold: (800000 (alice balance) - 20000 (reserve) - 660 (anchors)) / 1124 (commit tx weight) = 693363 + val fee = UpdateFee(ByteVector32.Zeroes, 693364) + // we first update the feerates so that we don't trigger a 'fee too different' error + bob.feeEstimator.setFeerate(FeeratesPerKw.single(fee.feeratePerKw)) + sender.send(bob, fee) + val error = bob2alice.expectMsgType[Error] + assert(new String(error.data.toArray) === CannotAffordFees(channelId(bob), missing = 1 sat, reserve = 20000 sat, fees = 780001 sat).getMessage) + awaitCond(bob.stateName == CLOSING) + // channel should be advertised as down + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + bob2blockchain.expectMsg(PublishAsap(tx)) // commit tx + //bob2blockchain.expectMsgType[PublishAsap] // main delayed (removed because of the high fees) + bob2blockchain.expectMsgType[WatchConfirmed] + } + test("recv UpdateFee (local/remote feerates are too different)") { f => import f._ @@ -1679,7 +1768,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with relayerA.expectNoMsg(1 seconds) } - test("recv CMD_CLOSE (no pending htlcs)") { f => + def testCmdClose(f: FixtureParam): Unit = { import f._ val sender = TestProbe() awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isEmpty) @@ -1690,6 +1779,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined) } + test("recv CMD_CLOSE (no pending htlcs)") { + testCmdClose _ + } + + test("recv CMD_CLOSE (no pending htlcs) (anchor outputs)", Tag("anchor_outputs")) { + testCmdClose _ + } + test("recv CMD_CLOSE (with unacked sent htlcs)") { f => import f._ val sender = TestProbe() @@ -1744,7 +1841,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == NORMAL) } - test("recv Shutdown (no pending htlcs)") { f => + def testShutdown(f: FixtureParam): Unit = { import f._ val sender = TestProbe() sender.send(alice, Shutdown(ByteVector32.Zeroes, Bob.channelParams.defaultFinalScriptPubKey)) @@ -1755,6 +1852,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_NEGOTIATING].channelId) } + test("recv Shutdown (no pending htlcs)") { + testShutdown _ + } + + test("recv Shutdown (no pending htlcs) (anchor outputs)", Tag("anchor_outputs")) { + testShutdown _ + } + test("recv Shutdown (with unacked sent htlcs)") { f => import f._ val sender = TestProbe() @@ -1816,7 +1921,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateName == CLOSING) } - test("recv Shutdown (with signed htlcs)") { f => + def testShutdownWithHtlcs(f: FixtureParam): Unit = { import f._ val sender = TestProbe() val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) @@ -1828,6 +1933,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateName == SHUTDOWN) } + test("recv Shutdown (with signed htlcs)") { + testShutdownWithHtlcs _ + } + + test("recv Shutdown (with signed htlcs) (anchor outputs)", Tag("anchor_outputs")) { + testShutdownWithHtlcs _ + } + test("recv Shutdown (while waiting for a RevokeAndAck)") { f => import f._ val sender = TestProbe() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index d60ada1385..d74883bbd9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -50,7 +50,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val setup = init() import setup._ within(30 seconds) { - reachNormal(setup) + reachNormal(setup, test.tags) val sender = TestProbe() // alice initiates a closing if (test.tags.contains("fee2")) { @@ -93,7 +93,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike alice2bob.expectNoMsg(200 millis) } - test("recv ClosingSigned (theirCloseFee != ourCloseFee)") { f => + def testClosingSigned(f: FixtureParam): Unit = { import f._ // alice initiates the negotiation val aliceCloseSig1 = alice2bob.expectMsgType[ClosingSigned] @@ -108,6 +108,16 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // BOLT 2: If the receiver [doesn't agree with the fee] it SHOULD propose a value strictly between the received fee-satoshis and its previously-sent fee-satoshis assert(aliceCloseSig2.feeSatoshis < aliceCloseSig1.feeSatoshis && aliceCloseSig2.feeSatoshis > bobCloseSig1.feeSatoshis) awaitCond(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.map(_.localClosingSigned) == initialState.closingTxProposed.last.map(_.localClosingSigned) :+ aliceCloseSig2) + val Some(closingTx) = alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt + assert(closingTx.txOut.length === 2) // NB: in the anchor outputs case, anchors are removed from the closing tx + } + + test("recv ClosingSigned (theirCloseFee != ourCloseFee)") { + testClosingSigned _ + } + + test("recv ClosingSigned (anchor outputs)", Tag("anchor_outputs")) { + testClosingSigned _ } private def testFeeConverge(f: FixtureParam) = { @@ -203,7 +213,6 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(alice.stateName == CLOSING) } - test("recv CMD_CLOSE") { f => import f._ val sender = TestProbe() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index ff8b08c9d3..b9595899c2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -50,11 +50,11 @@ import fr.acinq.eclair.router.Graph.WeightRatios import fr.acinq.eclair.router.RouteCalculation.ROUTE_MAX_LENGTH import fr.acinq.eclair.router.Router.{GossipDecision, MultiPartParams, PublicChannel, RouteParams, NORMAL => _, State => _} import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec, Router} -import fr.acinq.eclair.transactions.Transactions +import fr.acinq.eclair.transactions.{Scripts, Transactions} import fr.acinq.eclair.wire._ import fr.acinq.eclair.{CltvExpiryDelta, Kit, LongToBtcAmount, MilliSatoshi, Setup, ShortChannelId, TestKitBaseClass, randomBytes32} import grizzled.slf4j.Logging -import org.json4s.DefaultFormats +import org.json4s.{DefaultFormats, Formats} import org.json4s.JsonAST.{JString, JValue} import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike @@ -125,8 +125,11 @@ class IntegrationSpec extends TestKitBaseClass with BitcoindService with AnyFunS s"eclair.features.${StaticRemoteKey.rfcName}" -> "optional" ).asJava)) + val withAnchorOutputs = withStaticRemoteKey.withFallback(ConfigFactory.parseMap(Map( + s"eclair.features.${AnchorOutputs.rfcName}" -> "optional" + ).asJava)) - implicit val formats = DefaultFormats + implicit val formats: Formats = DefaultFormats override def beforeAll: Unit = { startBitcoind() @@ -172,15 +175,15 @@ class IntegrationSpec extends TestKitBaseClass with BitcoindService with AnyFunS test("starting eclair nodes") { instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 130, "eclair.server.port" -> 29730, "eclair.api.port" -> 28080, "eclair.channel-flags" -> 0).asJava).withFallback(commonFeatures).withFallback(commonConfig)) // A's channels are private instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.expiry-delta-blocks" -> 131, "eclair.server.port" -> 29731, "eclair.api.port" -> 28081, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonFeatures).withFallback(commonConfig)) - instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 132, "eclair.server.port" -> 29732, "eclair.api.port" -> 28082, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withStaticRemoteKey).withFallback(withWumbo).withFallback(commonConfig)) + instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 132, "eclair.server.port" -> 29732, "eclair.api.port" -> 28082, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withAnchorOutputs).withFallback(withWumbo).withFallback(commonConfig)) instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.expiry-delta-blocks" -> 133, "eclair.server.port" -> 29733, "eclair.api.port" -> 28083, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonFeatures).withFallback(commonConfig)) - instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.node-alias" -> "E", "eclair.expiry-delta-blocks" -> 134, "eclair.server.port" -> 29734, "eclair.api.port" -> 28084).asJava).withFallback(commonConfig)) + instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.node-alias" -> "E", "eclair.expiry-delta-blocks" -> 134, "eclair.server.port" -> 29734, "eclair.api.port" -> 28084).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig)) instantiateEclairNode("F1", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F1", "eclair.expiry-delta-blocks" -> 135, "eclair.server.port" -> 29735, "eclair.api.port" -> 28085).asJava).withFallback(withWumbo).withFallback(commonConfig)) instantiateEclairNode("F2", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F2", "eclair.expiry-delta-blocks" -> 136, "eclair.server.port" -> 29736, "eclair.api.port" -> 28086).asJava).withFallback(commonConfig)) instantiateEclairNode("F3", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F3", "eclair.expiry-delta-blocks" -> 137, "eclair.server.port" -> 29737, "eclair.api.port" -> 28087, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonFeatures).withFallback(commonConfig)) instantiateEclairNode("F4", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F4", "eclair.expiry-delta-blocks" -> 138, "eclair.server.port" -> 29738, "eclair.api.port" -> 28088).asJava).withFallback(commonConfig)) instantiateEclairNode("F5", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F5", "eclair.expiry-delta-blocks" -> 139, "eclair.server.port" -> 29739, "eclair.api.port" -> 28089).asJava).withFallback(commonConfig)) - instantiateEclairNode("F6", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F6", "eclair.expiry-delta-blocks" -> 140, "eclair.server.port" -> 29740, "eclair.api.port" -> 28090).asJava).withFallback(withStaticRemoteKey).withFallback(commonConfig)) // supports optional option_static_remotekey + instantiateEclairNode("F6", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F6", "eclair.expiry-delta-blocks" -> 140, "eclair.server.port" -> 29740, "eclair.api.port" -> 28090).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig)) instantiateEclairNode("G", ConfigFactory.parseMap(Map("eclair.node-alias" -> "G", "eclair.expiry-delta-blocks" -> 141, "eclair.server.port" -> 29741, "eclair.api.port" -> 28091, "eclair.fee-base-msat" -> 1010, "eclair.fee-proportional-millionths" -> 102, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonConfig)) // by default C has a normal payment handler, but this can be overridden in tests @@ -860,10 +863,10 @@ class IntegrationSpec extends TestKitBaseClass with BitcoindService with AnyFunS assert(outgoingPayments.forall(p => p.status.isInstanceOf[OutgoingPaymentStatus.Failed]), outgoingPayments) } - test("send payments and close the channel C -> F6 with option_static_remotekey") { + test("send payments and close the channel C -> F6 with option_anchor_outputs/option_static_remotekey") { // initially all the balance is on C side and F6 doesn't have an output val sender = TestProbe() - sender.send(nodes("F6").register, 'channelsTo) + sender.send(nodes("F6").register, Symbol("channelsTo")) // retrieve the channelId of C <--> F6 val Some(channelId) = sender.expectMsgType[Map[ByteVector32, PublicKey]].find(_._2 == nodes("C").nodeParams.nodeId).map(_._1) @@ -871,8 +874,8 @@ class IntegrationSpec extends TestKitBaseClass with BitcoindService with AnyFunS val initialStateDataF6 = sender.expectMsgType[DATA_NORMAL] val initialCommitmentIndex = initialStateDataF6.commitments.localCommit.index - // the 'to remote' address is a simple P2WPKH spending to the remote payment basepoint - val toRemoteAddress = Script.pay2wpkh(initialStateDataF6.commitments.remoteParams.paymentBasepoint) + // the 'to remote' address is a simple script spending to the remote payment basepoint with a 1-block CSV delay + val toRemoteAddress = Script.pay2wsh(Scripts.toRemoteDelayed(initialStateDataF6.commitments.remoteParams.paymentBasepoint)) // toRemote output of C as seen by F6 val Some(toRemoteOutC) = initialStateDataF6.commitments.localCommit.publishableTxs.commitTx.tx.txOut.find(_.publicKeyScript == Script.write(toRemoteAddress)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala index f5103e7012..a069587ab4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala @@ -382,9 +382,7 @@ class PaymentRequestSpec extends AnyFunSuite { } test("supported payment request features") { - case class Result(allowMultiPart: Boolean, requirePaymentSecret: Boolean, areSupported: Boolean) // "supported" is based on the "it's okay to be odd" rule" - val featureBits = Map( PaymentRequestFeatures(bin" 00000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true), PaymentRequestFeatures(bin" 00011000001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true), @@ -395,9 +393,9 @@ class PaymentRequestSpec extends AnyFunSuite { PaymentRequestFeatures(bin" 01000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true), PaymentRequestFeatures(bin" 0000010000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true), PaymentRequestFeatures(bin" 0000011000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true), - PaymentRequestFeatures(bin" 0000110000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false), + PaymentRequestFeatures(bin" 0000110000001000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false), // those are useful for nonreg testing of the areSupported method (which needs to be updated with every new supported mandatory bit) - PaymentRequestFeatures(bin" 0000100000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false), + PaymentRequestFeatures(bin" 0000100000001000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false), PaymentRequestFeatures(bin" 0010000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false), PaymentRequestFeatures(bin" 000001000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false), PaymentRequestFeatures(bin" 000100000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala index eaf4580a66..a4eab42a5d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, Crypto, SIGHASH_ANYONECANPAY, SIGHASH_SINGLE, Script, ScriptFlags, Transaction} +import fr.acinq.bitcoin.{ByteVector32, Crypto, Script, ScriptFlags, Transaction} import fr.acinq.eclair.channel.ChannelVersion import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.crypto.Generators @@ -200,9 +200,9 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { remotePaymentBasePoint = Remote.payment_basepoint, localIsFunder = true, outputs = outputs) - val local_sig = Transactions.sign(tx, Local.funding_privkey) + val local_sig = Transactions.sign(tx, Local.funding_privkey, TxOwner.Local, commitmentFormat) logger.info(s"# local_signature = ${Scripts.der(local_sig).dropRight(1).toHex}") - val remote_sig = Transactions.sign(tx, Remote.funding_privkey) + val remote_sig = Transactions.sign(tx, Remote.funding_privkey, TxOwner.Remote, commitmentFormat) logger.info(s"remote_signature: ${Scripts.der(remote_sig).dropRight(1).toHex}") Transactions.addSigs(tx, Local.funding_pubkey, Remote.funding_pubkey, local_sig, remote_sig) } @@ -239,12 +239,12 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { htlcTxs.collect { case tx: HtlcSuccessTx => - val remoteSig = Transactions.sign(tx, Remote.htlc_privkey, Scripts.htlcRemoteSighash(commitmentFormat)) + val remoteSig = Transactions.sign(tx, Remote.htlc_privkey, TxOwner.Remote, commitmentFormat) val htlcIndex = htlcScripts.indexOf(Script.parse(tx.input.redeemScript)) logger.info(s"# signature for output ${tx.input.outPoint.index} (htlc $htlcIndex)") logger.info(s"remote_htlc_signature: ${Scripts.der(remoteSig).dropRight(1).toHex}") case tx: HtlcTimeoutTx => - val remoteSig = Transactions.sign(tx, Remote.htlc_privkey, Scripts.htlcRemoteSighash(commitmentFormat)) + val remoteSig = Transactions.sign(tx, Remote.htlc_privkey, TxOwner.Remote, commitmentFormat) val htlcIndex = htlcScripts.indexOf(Script.parse(tx.input.redeemScript)) logger.info(s"# signature for output ${tx.input.outPoint.index} (htlc $htlcIndex)") logger.info(s"remote_htlc_signature: ${Scripts.der(remoteSig).dropRight(1).toHex}") @@ -252,8 +252,8 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { val signedTxs = htlcTxs collect { case tx: HtlcSuccessTx => - val localSig = Transactions.sign(tx, Local.htlc_privkey) - val remoteSig = Transactions.sign(tx, Remote.htlc_privkey, Scripts.htlcRemoteSighash(commitmentFormat)) + val localSig = Transactions.sign(tx, Local.htlc_privkey, TxOwner.Local, commitmentFormat) + val remoteSig = Transactions.sign(tx, Remote.htlc_privkey, TxOwner.Remote, commitmentFormat) val preimage = paymentPreimages.find(p => Crypto.sha256(p) == tx.paymentHash).get val tx1 = Transactions.addSigs(tx, localSig, remoteSig, preimage, commitmentFormat) Transaction.correctlySpends(tx1.tx, Seq(commitTx.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) @@ -262,8 +262,8 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { logger.info(s"output htlc_success_tx $htlcIndex: ${tx1.tx}") tx1 case tx: HtlcTimeoutTx => - val localSig = Transactions.sign(tx, Local.htlc_privkey) - val remoteSig = Transactions.sign(tx, Remote.htlc_privkey, Scripts.htlcRemoteSighash(commitmentFormat)) + val localSig = Transactions.sign(tx, Local.htlc_privkey, TxOwner.Local, commitmentFormat) + val remoteSig = Transactions.sign(tx, Remote.htlc_privkey, TxOwner.Remote, commitmentFormat) val tx1 = Transactions.addSigs(tx, localSig, remoteSig, commitmentFormat) Transaction.correctlySpends(tx1.tx, Seq(commitTx.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) logger.info(s"# local_signature = ${Scripts.der(localSig).dropRight(1).toHex}") @@ -425,7 +425,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.STATIC_REMOTEKEY + override def channelVersion: ChannelVersion = ChannelVersion.ANCHOR_OUTPUTS override def commitmentFormat: CommitmentFormat = AnchorOutputsCommitmentFormat // @formatter:on } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index 9c4f4110a0..55b6ae5c0e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -20,7 +20,7 @@ import java.nio.ByteOrder import fr.acinq.bitcoin.Crypto.{PrivateKey, ripemd160, sha256} import fr.acinq.bitcoin.Script.{pay2wpkh, pay2wsh, write} -import fr.acinq.bitcoin.{Btc, ByteVector32, Crypto, MilliBtc, Protocol, Satoshi, Script, Transaction, TxOut, millibtc2satoshi} +import fr.acinq.bitcoin.{Btc, ByteVector32, Crypto, MilliBtc, Protocol, SIGHASH_ALL, SIGHASH_ANYONECANPAY, SIGHASH_NONE, SIGHASH_SINGLE, Satoshi, Script, Transaction, TxOut, millibtc2satoshi} import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.transactions.CommitmentOutput.{InHtlc, OutHtlc} import fr.acinq.eclair.transactions.Scripts.{anchor, htlcOffered, htlcReceived, toLocalDelayed} @@ -256,8 +256,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { val commitTxNumber = 0x404142434445L val commitTx = { val txinfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsFunder = true, outputs) - val localSig = Transactions.sign(txinfo, localPaymentPriv) - val remoteSig = Transactions.sign(txinfo, remotePaymentPriv) + val localSig = Transactions.sign(txinfo, localPaymentPriv, TxOwner.Local, DefaultCommitmentFormat) + val remoteSig = Transactions.sign(txinfo, remotePaymentPriv, TxOwner.Remote, DefaultCommitmentFormat) Transactions.addSigs(txinfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) } @@ -276,8 +276,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // either party spends local->remote htlc output with htlc timeout tx for (htlcTimeoutTx <- htlcTimeoutTxs) { - val localSig = sign(htlcTimeoutTx, localHtlcPriv) - val remoteSig = sign(htlcTimeoutTx, remoteHtlcPriv) + val localSig = sign(htlcTimeoutTx, localHtlcPriv, TxOwner.Local, DefaultCommitmentFormat) + val remoteSig = sign(htlcTimeoutTx, remoteHtlcPriv, TxOwner.Remote, DefaultCommitmentFormat) val signed = addSigs(htlcTimeoutTx, localSig, remoteSig, DefaultCommitmentFormat) assert(checkSpendable(signed).isSuccess) } @@ -285,7 +285,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends delayed output of htlc1 timeout tx val Right(claimHtlcDelayed) = makeClaimDelayedOutputTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = sign(claimHtlcDelayed, localDelayedPaymentPriv) + val localSig = sign(claimHtlcDelayed, localDelayedPaymentPriv, TxOwner.Local, DefaultCommitmentFormat) val signedTx = addSigs(claimHtlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit @@ -296,7 +296,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { // remote spends local->remote htlc1/htlc3 output directly in case of success for ((htlc, paymentPreimage) <- (htlc1, paymentPreimage1) :: (htlc3, paymentPreimage3) :: Nil) { val Right(claimHtlcSuccessTx) = makeClaimHtlcSuccessTx(commitTx.tx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, DefaultCommitmentFormat) - val localSig = sign(claimHtlcSuccessTx, remoteHtlcPriv) + val localSig = sign(claimHtlcSuccessTx, remoteHtlcPriv, TxOwner.Local, DefaultCommitmentFormat) val signed = addSigs(claimHtlcSuccessTx, localSig, paymentPreimage) assert(checkSpendable(signed).isSuccess) } @@ -304,18 +304,18 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends remote->local htlc2/htlc4 output with htlc success tx using payment preimage for ((htlcSuccessTx, paymentPreimage) <- (htlcSuccessTxs(1), paymentPreimage2) :: (htlcSuccessTxs(0), paymentPreimage4) :: Nil) { - val localSig = sign(htlcSuccessTx, localHtlcPriv) - val remoteSig = sign(htlcSuccessTx, remoteHtlcPriv) + val localSig = sign(htlcSuccessTx, localHtlcPriv, TxOwner.Local, DefaultCommitmentFormat) + val remoteSig = sign(htlcSuccessTx, remoteHtlcPriv, TxOwner.Remote, DefaultCommitmentFormat) val signedTx = addSigs(htlcSuccessTx, localSig, remoteSig, paymentPreimage, DefaultCommitmentFormat) assert(checkSpendable(signedTx).isSuccess) // check remote sig - assert(checkSig(htlcSuccessTx, remoteSig, remoteHtlcPriv.publicKey)) + assert(checkSig(htlcSuccessTx, remoteSig, remoteHtlcPriv.publicKey, TxOwner.Remote, DefaultCommitmentFormat)) } } { // local spends delayed output of htlc2 success tx val Right(claimHtlcDelayed) = makeClaimDelayedOutputTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = sign(claimHtlcDelayed, localDelayedPaymentPriv) + val localSig = sign(claimHtlcDelayed, localDelayedPaymentPriv, TxOwner.Local, DefaultCommitmentFormat) val signedTx = addSigs(claimHtlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc4 success tx because it is below the dust limit @@ -325,35 +325,35 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends main delayed output val Right(claimMainOutputTx) = makeClaimDelayedOutputTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = sign(claimMainOutputTx, localDelayedPaymentPriv) + val localSig = sign(claimMainOutputTx, localDelayedPaymentPriv, TxOwner.Local, DefaultCommitmentFormat) val signedTx = addSigs(claimMainOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) } { // remote spends main output val Right(claimP2WPKHOutputTx) = makeClaimP2WPKHOutputTx(commitTx.tx, localDustLimit, remotePaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = sign(claimP2WPKHOutputTx, remotePaymentPriv) + val localSig = sign(claimP2WPKHOutputTx, remotePaymentPriv, TxOwner.Local, DefaultCommitmentFormat) val signedTx = addSigs(claimP2WPKHOutputTx, remotePaymentPriv.publicKey, localSig) assert(checkSpendable(signedTx).isSuccess) } { // remote spends remote->local htlc output directly in case of timeout val Right(claimHtlcTimeoutTx) = makeClaimHtlcTimeoutTx(commitTx.tx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc2, feeratePerKw, DefaultCommitmentFormat) - val remoteSig = sign(claimHtlcTimeoutTx, remoteHtlcPriv) - val signed = addSigs(claimHtlcTimeoutTx, remoteSig) + val localSig = sign(claimHtlcTimeoutTx, remoteHtlcPriv, TxOwner.Local, DefaultCommitmentFormat) + val signed = addSigs(claimHtlcTimeoutTx, localSig) assert(checkSpendable(signed).isSuccess) } { // remote spends local main delayed output with revocation key val Right(mainPenaltyTx) = makeMainPenaltyTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, finalPubKeyScript, toLocalDelay, localDelayedPaymentPriv.publicKey, feeratePerKw) - val sig = sign(mainPenaltyTx, localRevocationPriv) + val sig = sign(mainPenaltyTx, localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat) val signed = addSigs(mainPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) } { // remote spends htlc1's htlc-timeout tx with revocation key val Right(claimHtlcDelayedPenaltyTx) = makeClaimDelayedOutputPenaltyTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val sig = sign(claimHtlcDelayedPenaltyTx, localRevocationPriv) + val sig = sign(claimHtlcDelayedPenaltyTx, localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat) val signed = addSigs(claimHtlcDelayedPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) // remote can't claim revoked output of htlc3's htlc-timeout tx because it is below the dust limit @@ -368,14 +368,14 @@ class TransactionsSpec extends AnyFunSuite with Logging { case _ => false }.map(_._2) val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) - val sig = sign(htlcPenaltyTx, localRevocationPriv) + val sig = sign(htlcPenaltyTx, localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat) val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) assert(checkSpendable(signed).isSuccess) } { // remote spends htlc2's htlc-success tx with revocation key val Right(claimHtlcDelayedPenaltyTx) = makeClaimDelayedOutputPenaltyTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val sig = sign(claimHtlcDelayedPenaltyTx, localRevocationPriv) + val sig = sign(claimHtlcDelayedPenaltyTx, localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat) val signed = addSigs(claimHtlcDelayedPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) // remote can't claim revoked output of htlc4's htlc-success tx because it is below the dust limit @@ -390,7 +390,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { case _ => false }.map(_._2) val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) - val sig = sign(htlcPenaltyTx, localRevocationPriv) + val sig = sign(htlcPenaltyTx, localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat) val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) assert(checkSpendable(signed).isSuccess) } @@ -481,8 +481,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { val commitTxNumber = 0x404142434445L val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, AnchorOutputsCommitmentFormat) val txinfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsFunder = true, outputs) - val localSig = Transactions.sign(txinfo, localPaymentPriv) - val remoteSig = Transactions.sign(txinfo, remotePaymentPriv) + val localSig = Transactions.sign(txinfo, localPaymentPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) + val remoteSig = Transactions.sign(txinfo, remotePaymentPriv, TxOwner.Remote, AnchorOutputsCommitmentFormat) val commitTx = Transactions.addSigs(txinfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) val (htlcTimeoutTxs, htlcSuccessTxs) = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.feeratePerKw, outputs, AnchorOutputsCommitmentFormat) assert(htlcTimeoutTxs.size == 2) // htlc1 and htlc3 @@ -493,7 +493,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends main delayed output val Right(claimMainOutputTx) = makeClaimDelayedOutputTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = sign(claimMainOutputTx, localDelayedPaymentPriv) + val localSig = sign(claimMainOutputTx, localDelayedPaymentPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) val signedTx = addSigs(claimMainOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) } @@ -505,7 +505,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // remote spends main delayed output val Right(claimRemoteDelayedOutputTx) = makeClaimRemoteDelayedOutputTx(commitTx.tx, localDustLimit, remotePaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = sign(claimRemoteDelayedOutputTx, remotePaymentPriv) + val localSig = sign(claimRemoteDelayedOutputTx, remotePaymentPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) val signedTx = addSigs(claimRemoteDelayedOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) } @@ -513,7 +513,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { // local spends local anchor val Right(claimAnchorOutputTx) = makeClaimAnchorOutputTx(commitTx.tx, localFundingPriv.publicKey) assert(checkSpendable(claimAnchorOutputTx).isFailure) - val localSig = sign(claimAnchorOutputTx, localFundingPriv) + val localSig = sign(claimAnchorOutputTx, localFundingPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) val signedTx = addSigs(claimAnchorOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) } @@ -521,30 +521,37 @@ class TransactionsSpec extends AnyFunSuite with Logging { // remote spends remote anchor val Right(claimAnchorOutputTx) = makeClaimAnchorOutputTx(commitTx.tx, remoteFundingPriv.publicKey) assert(checkSpendable(claimAnchorOutputTx).isFailure) - val localSig = sign(claimAnchorOutputTx, remoteFundingPriv) + val localSig = sign(claimAnchorOutputTx, remoteFundingPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) val signedTx = addSigs(claimAnchorOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) } { // remote spends local main delayed output with revocation key val Right(mainPenaltyTx) = makeMainPenaltyTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, finalPubKeyScript, toLocalDelay, localDelayedPaymentPriv.publicKey, feeratePerKw) - val sig = sign(mainPenaltyTx, localRevocationPriv) + val sig = sign(mainPenaltyTx, localRevocationPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) val signed = addSigs(mainPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) } { // local spends received htlc with HTLC-timeout tx for (htlcTimeoutTx <- htlcTimeoutTxs) { - val localSig = sign(htlcTimeoutTx, localHtlcPriv) - val remoteSig = sign(htlcTimeoutTx, remoteHtlcPriv, Scripts.htlcRemoteSighash(AnchorOutputsCommitmentFormat)) + val localSig = sign(htlcTimeoutTx, localHtlcPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) + val remoteSig = sign(htlcTimeoutTx, remoteHtlcPriv, TxOwner.Remote, AnchorOutputsCommitmentFormat) val signedTx = addSigs(htlcTimeoutTx, localSig, remoteSig, AnchorOutputsCommitmentFormat) assert(checkSpendable(signedTx).isSuccess) + // local detects when remote doesn't use the right sighash flags + val invalidSighash = Seq(SIGHASH_ALL, SIGHASH_ALL | SIGHASH_ANYONECANPAY, SIGHASH_SINGLE, SIGHASH_NONE) + for (sighash <- invalidSighash) { + val invalidRemoteSig = sign(htlcTimeoutTx, remoteHtlcPriv, sighash) + val invalidTx = addSigs(htlcTimeoutTx, localSig, invalidRemoteSig, AnchorOutputsCommitmentFormat) + assert(checkSpendable(invalidTx).isFailure) + } } } { // local spends delayed output of htlc1 timeout tx val Right(claimHtlcDelayed) = makeClaimDelayedOutputTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = sign(claimHtlcDelayed, localDelayedPaymentPriv) + val localSig = sign(claimHtlcDelayed, localDelayedPaymentPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) val signedTx = addSigs(claimHtlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit @@ -554,10 +561,20 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends offered htlc with HTLC-success tx for ((htlcSuccessTx, paymentPreimage) <- (htlcSuccessTxs(0), paymentPreimage4) :: (htlcSuccessTxs(1), paymentPreimage2) :: (htlcSuccessTxs(2), paymentPreimage2) :: Nil) { - val localSig = sign(htlcSuccessTx, localHtlcPriv) - val remoteSig = sign(htlcSuccessTx, remoteHtlcPriv, Scripts.htlcRemoteSighash(AnchorOutputsCommitmentFormat)) + val localSig = sign(htlcSuccessTx, localHtlcPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) + val remoteSig = sign(htlcSuccessTx, remoteHtlcPriv, TxOwner.Remote, AnchorOutputsCommitmentFormat) val signedTx = addSigs(htlcSuccessTx, localSig, remoteSig, paymentPreimage, AnchorOutputsCommitmentFormat) assert(checkSpendable(signedTx).isSuccess) + // check remote sig + assert(checkSig(htlcSuccessTx, remoteSig, remoteHtlcPriv.publicKey, TxOwner.Remote, AnchorOutputsCommitmentFormat)) + // local detects when remote doesn't use the right sighash flags + val invalidSighash = Seq(SIGHASH_ALL, SIGHASH_ALL | SIGHASH_ANYONECANPAY, SIGHASH_SINGLE, SIGHASH_NONE) + for (sighash <- invalidSighash) { + val invalidRemoteSig = sign(htlcSuccessTx, remoteHtlcPriv, sighash) + val invalidTx = addSigs(htlcSuccessTx, localSig, invalidRemoteSig, paymentPreimage, AnchorOutputsCommitmentFormat) + assert(checkSpendable(invalidTx).isFailure) + assert(!checkSig(invalidTx, invalidRemoteSig, remoteHtlcPriv.publicKey, TxOwner.Remote, AnchorOutputsCommitmentFormat)) + } } } { @@ -565,7 +582,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val Right(claimHtlcDelayedA) = makeClaimDelayedOutputTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) val Right(claimHtlcDelayedB) = makeClaimDelayedOutputTx(htlcSuccessTxs(2).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) for (claimHtlcDelayed <- Seq(claimHtlcDelayedA, claimHtlcDelayedB)) { - val localSig = sign(claimHtlcDelayed, localDelayedPaymentPriv) + val localSig = sign(claimHtlcDelayed, localDelayedPaymentPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) val signedTx = addSigs(claimHtlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) } @@ -577,7 +594,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { // remote spends local->remote htlc outputs directly in case of success for ((htlc, paymentPreimage) <- (htlc1, paymentPreimage1) :: (htlc3, paymentPreimage3) :: Nil) { val Right(claimHtlcSuccessTx) = makeClaimHtlcSuccessTx(commitTx.tx, commitTxOutputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, AnchorOutputsCommitmentFormat) - val localSig = sign(claimHtlcSuccessTx, remoteHtlcPriv) + val localSig = sign(claimHtlcSuccessTx, remoteHtlcPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) val signed = addSigs(claimHtlcSuccessTx, localSig, paymentPreimage) assert(checkSpendable(signed).isSuccess) } @@ -585,7 +602,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // remote spends htlc1's htlc-timeout tx with revocation key val Right(claimHtlcDelayedPenaltyTx) = makeClaimDelayedOutputPenaltyTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val sig = sign(claimHtlcDelayedPenaltyTx, localRevocationPriv) + val sig = sign(claimHtlcDelayedPenaltyTx, localRevocationPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) val signed = addSigs(claimHtlcDelayedPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) // remote can't claim revoked output of htlc3's htlc-timeout tx because it is below the dust limit @@ -596,8 +613,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { // remote spends remote->local htlc output directly in case of timeout for (htlc <- Seq(htlc2a, htlc2b)) { val Right(claimHtlcTimeoutTx) = makeClaimHtlcTimeoutTx(commitTx.tx, commitTxOutputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, AnchorOutputsCommitmentFormat) - val remoteSig = sign(claimHtlcTimeoutTx, remoteHtlcPriv) - val signed = addSigs(claimHtlcTimeoutTx, remoteSig) + val localSig = sign(claimHtlcTimeoutTx, remoteHtlcPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) + val signed = addSigs(claimHtlcTimeoutTx, localSig) assert(checkSpendable(signed).isSuccess) } } @@ -606,7 +623,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val Right(claimHtlcDelayedPenaltyTxA) = makeClaimDelayedOutputPenaltyTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) val Right(claimHtlcDelayedPenaltyTxB) = makeClaimDelayedOutputPenaltyTx(htlcSuccessTxs(2).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) for (claimHtlcSuccessPenaltyTx <- Seq(claimHtlcDelayedPenaltyTxA, claimHtlcDelayedPenaltyTxB)) { - val sig = sign(claimHtlcSuccessPenaltyTx, localRevocationPriv) + val sig = sign(claimHtlcSuccessPenaltyTx, localRevocationPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) val signed = addSigs(claimHtlcSuccessPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) } @@ -622,7 +639,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { case _ => false }.map(_._2) val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) - val sig = sign(htlcPenaltyTx, localRevocationPriv) + val sig = sign(htlcPenaltyTx, localRevocationPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) assert(checkSpendable(signed).isSuccess) } @@ -635,7 +652,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { case _ => false }.map(_._2) val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) - val sig = sign(htlcPenaltyTx, localRevocationPriv) + val sig = sign(htlcPenaltyTx, localRevocationPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) assert(checkSpendable(signed).isSuccess) } @@ -681,8 +698,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { val (commitTx, outputs) = { val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) val txinfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsFunder = true, outputs) - val localSig = Transactions.sign(txinfo, localPaymentPriv) - val remoteSig = Transactions.sign(txinfo, remotePaymentPriv) + val localSig = Transactions.sign(txinfo, localPaymentPriv, TxOwner.Local, DefaultCommitmentFormat) + val remoteSig = Transactions.sign(txinfo, remotePaymentPriv, TxOwner.Remote, DefaultCommitmentFormat) (Transactions.addSigs(txinfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig), outputs) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala index 0dfa475123..51726f1aed 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala @@ -37,7 +37,7 @@ import fr.acinq.eclair.wire.CommonCodecs.setCodec import fr.acinq.eclair.{TestConstants, UInt64, randomBytes32, randomKey, _} import org.json4s.JsonAST._ import org.json4s.jackson.Serialization -import org.json4s.{CustomKeySerializer, CustomSerializer} +import org.json4s.{CustomKeySerializer, CustomSerializer, Formats} import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ import scodec.{Attempt, Codec, DecodeResult} @@ -92,13 +92,16 @@ class ChannelCodecsSpec extends AnyFunSuite { val current02 = hex"0000000102a06ea3081f0f7a8ce31eb4f0822d10d2da120d5a1b1451f0727f51c7372f0f9b" val current03 = hex"0000000103d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" val current04 = hex"0000000303d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" + val current05 = hex"0000000703d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" assert(channelVersionCodec.decode(current02.bits) === Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current02.drop(4).bits))) assert(channelVersionCodec.decode(current03.bits) === Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current03.drop(4).bits))) assert(channelVersionCodec.decode(current04.bits) === Attempt.successful(DecodeResult(ChannelVersion.STATIC_REMOTEKEY, current04.drop(4).bits))) + assert(channelVersionCodec.decode(current05.bits) === Attempt.successful(DecodeResult(ChannelVersion.ANCHOR_OUTPUTS, current05.drop(4).bits))) assert(channelVersionCodec.encode(ChannelVersion.STANDARD) === Attempt.successful(hex"00000001".bits)) assert(channelVersionCodec.encode(ChannelVersion.STATIC_REMOTEKEY) === Attempt.successful(hex"00000003".bits)) + assert(channelVersionCodec.encode(ChannelVersion.ANCHOR_OUTPUTS) === Attempt.successful(hex"00000007".bits)) } test("encode/decode localparams") { @@ -125,6 +128,7 @@ class ChannelCodecsSpec extends AnyFunSuite { roundtrip(o, localParamsCodec(ChannelVersion.ZEROES)) roundtrip(o1, localParamsCodec(ChannelVersion.STATIC_REMOTEKEY)) + roundtrip(o1, localParamsCodec(ChannelVersion.ANCHOR_OUTPUTS)) } test("backward compatibility local params with global features") { @@ -240,7 +244,6 @@ class ChannelCodecsSpec extends AnyFunSuite { test("encode/decode origin") { val id = UUID.randomUUID() assert(originCodec.decodeValue(originCodec.encode(Local(id, Some(ActorSystem("test").deadLetters))).require).require === Local(id, None)) - val ZERO_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000") val relayed = Relayed(randomBytes32, 4324, 12000000 msat, 11000000 msat) assert(originCodec.decodeValue(originCodec.encode(relayed).require).require === relayed) val trampolineRelayed = TrampolineRelayed((randomBytes32, 1L) :: (randomBytes32, 1L) :: (randomBytes32, 2L) :: Nil, None) @@ -418,6 +421,7 @@ class ChannelCodecsSpec extends AnyFunSuite { assert(newjson === refjson) } } + } object ChannelCodecsSpec { @@ -491,91 +495,91 @@ object ChannelCodecsSpec { object JsonSupport { - class ByteVectorSerializer extends CustomSerializer[ByteVector](format => ( { + class ByteVectorSerializer extends CustomSerializer[ByteVector](_ => ( { null }, { case x: ByteVector => JString(x.toHex) })) - class ByteVector32Serializer extends CustomSerializer[ByteVector32](format => ( { + class ByteVector32Serializer extends CustomSerializer[ByteVector32](_ => ( { null }, { case x: ByteVector32 => JString(x.toHex) })) - class ByteVector64Serializer extends CustomSerializer[ByteVector64](format => ( { + class ByteVector64Serializer extends CustomSerializer[ByteVector64](_ => ( { null }, { case x: ByteVector64 => JString(x.toHex) })) - class UInt64Serializer extends CustomSerializer[UInt64](format => ( { + class UInt64Serializer extends CustomSerializer[UInt64](_ => ( { null }, { case x: UInt64 => JInt(x.toBigInt) })) - class SatoshiSerializer extends CustomSerializer[Satoshi](format => ( { + class SatoshiSerializer extends CustomSerializer[Satoshi](_ => ( { null }, { case x: Satoshi => JInt(x.toLong) })) - class MilliSatoshiSerializer extends CustomSerializer[MilliSatoshi](format => ( { + class MilliSatoshiSerializer extends CustomSerializer[MilliSatoshi](_ => ( { null }, { case x: MilliSatoshi => JInt(x.toLong) })) - class CltvExpirySerializer extends CustomSerializer[CltvExpiry](format => ( { + class CltvExpirySerializer extends CustomSerializer[CltvExpiry](_ => ( { null }, { case x: CltvExpiry => JLong(x.toLong) })) - class CltvExpiryDeltaSerializer extends CustomSerializer[CltvExpiryDelta](format => ( { + class CltvExpiryDeltaSerializer extends CustomSerializer[CltvExpiryDelta](_ => ( { null }, { case x: CltvExpiryDelta => JInt(x.toInt) })) - class ShortChannelIdSerializer extends CustomSerializer[ShortChannelId](format => ( { + class ShortChannelIdSerializer extends CustomSerializer[ShortChannelId](_ => ( { null }, { case x: ShortChannelId => JString(x.toString()) })) - class StateSerializer extends CustomSerializer[State](format => ( { + class StateSerializer extends CustomSerializer[State](_ => ( { null }, { case x: State => JString(x.toString()) })) - class ShaChainSerializer extends CustomSerializer[ShaChain](format => ( { + class ShaChainSerializer extends CustomSerializer[ShaChain](_ => ( { null }, { - case x: ShaChain => JNull + case _: ShaChain => JNull })) - class PublicKeySerializer extends CustomSerializer[PublicKey](format => ( { + class PublicKeySerializer extends CustomSerializer[PublicKey](_ => ( { null }, { case x: PublicKey => JString(x.toString()) })) - class PrivateKeySerializer extends CustomSerializer[PrivateKey](format => ( { + class PrivateKeySerializer extends CustomSerializer[PrivateKey](_ => ( { null }, { - case x: PrivateKey => JString("XXX") + case _: PrivateKey => JString("XXX") })) - class ChannelVersionSerializer extends CustomSerializer[ChannelVersion](format => ( { + class ChannelVersionSerializer extends CustomSerializer[ChannelVersion](_ => ( { null }, { case x: ChannelVersion => JString(x.bits.toBin) })) - class TransactionSerializer extends CustomSerializer[TransactionWithInputInfo](ser = format => ( { + class TransactionSerializer extends CustomSerializer[TransactionWithInputInfo](_ => ( { null }, { case x: Transaction => JObject(List( @@ -584,7 +588,7 @@ object ChannelCodecsSpec { )) })) - class TransactionWithInputInfoSerializer extends CustomSerializer[TransactionWithInputInfo](ser = format => ( { + class TransactionWithInputInfoSerializer extends CustomSerializer[TransactionWithInputInfo](_ => ( { null }, { case x: TransactionWithInputInfo => JObject(List( @@ -593,31 +597,31 @@ object ChannelCodecsSpec { )) })) - class InetSocketAddressSerializer extends CustomSerializer[InetSocketAddress](format => ( { + class InetSocketAddressSerializer extends CustomSerializer[InetSocketAddress](_ => ( { null }, { case address: InetSocketAddress => JString(HostAndPort.fromParts(address.getHostString, address.getPort).toString) })) - class OutPointSerializer extends CustomSerializer[OutPoint](format => ( { + class OutPointSerializer extends CustomSerializer[OutPoint](_ => ( { null }, { case x: OutPoint => JString(s"${x.txid}:${x.index}") })) - class OutPointKeySerializer extends CustomKeySerializer[OutPoint](format => ( { + class OutPointKeySerializer extends CustomKeySerializer[OutPoint](_ => ( { null }, { case x: OutPoint => s"${x.txid}:${x.index}" })) - class InputInfoSerializer extends CustomSerializer[InputInfo](format => ( { + class InputInfoSerializer extends CustomSerializer[InputInfo](_ => ( { null }, { case x: InputInfo => JObject(("outPoint", JString(s"${x.outPoint.txid}:${x.outPoint.index}")), ("amountSatoshis", JInt(x.txOut.amount.toLong))) })) - implicit val formats = org.json4s.DefaultFormats + + implicit val formats: Formats = org.json4s.DefaultFormats + new ByteVectorSerializer + new ByteVector32Serializer + new ByteVector64Serializer +