Skip to content

Commit b78ed5a

Browse files
committed
Add commitment format to interactive-tx shared input
And remove the unused sealed trait, the commitment format contains the information we need so we don't need multiple child classes. We still use a `discriminated` at the codec level though to allow changing the format in the future if needed.
1 parent 9d1a651 commit b78ed5a

File tree

9 files changed

+53
-43
lines changed

9 files changed

+53
-43
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,7 +1111,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
11111111
isInitiator = false,
11121112
localContribution = spliceAck.fundingContribution,
11131113
remoteContribution = msg.fundingContribution,
1114-
sharedInput_opt = Some(Multisig2of2Input(channelKeys, parentCommitment)),
1114+
sharedInput_opt = Some(SharedFundingInput(channelKeys, parentCommitment)),
11151115
remoteFundingPubKey = msg.fundingPubKey,
11161116
localOutputs = Nil,
11171117
commitmentFormat = commitmentFormat,
@@ -1159,7 +1159,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
11591159
isInitiator = true,
11601160
localContribution = spliceInit.fundingContribution,
11611161
remoteContribution = msg.fundingContribution,
1162-
sharedInput_opt = Some(Multisig2of2Input(channelKeys, parentCommitment)),
1162+
sharedInput_opt = Some(SharedFundingInput(channelKeys, parentCommitment)),
11631163
remoteFundingPubKey = msg.fundingPubKey,
11641164
localOutputs = cmd.spliceOutputs,
11651165
commitmentFormat = commitmentFormat,
@@ -1238,7 +1238,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
12381238
isInitiator = false,
12391239
localContribution = fundingContribution,
12401240
remoteContribution = msg.fundingContribution,
1241-
sharedInput_opt = Some(Multisig2of2Input(channelKeys, rbf.parentCommitment)),
1241+
sharedInput_opt = Some(SharedFundingInput(channelKeys, rbf.parentCommitment)),
12421242
remoteFundingPubKey = rbf.latestFundingTx.fundingParams.remoteFundingPubKey,
12431243
localOutputs = rbf.latestFundingTx.fundingParams.localOutputs,
12441244
commitmentFormat = rbf.latestFundingTx.fundingParams.commitmentFormat,
@@ -1295,7 +1295,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
12951295
isInitiator = true,
12961296
localContribution = txInitRbf.fundingContribution,
12971297
remoteContribution = msg.fundingContribution,
1298-
sharedInput_opt = Some(Multisig2of2Input(channelKeys, rbf.parentCommitment)),
1298+
sharedInput_opt = Some(SharedFundingInput(channelKeys, rbf.parentCommitment)),
12991299
remoteFundingPubKey = rbf.latestFundingTx.fundingParams.remoteFundingPubKey,
13001300
localOutputs = rbf.latestFundingTx.fundingParams.localOutputs,
13011301
commitmentFormat = rbf.latestFundingTx.fundingParams.commitmentFormat,
@@ -3368,7 +3368,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
33683368
val targetFeerate = nodeParams.onChainFeeConf.getFundingFeerate(nodeParams.currentFeeratesForFundingClosing)
33693369
val fundingContribution = InteractiveTxFunder.computeSpliceContribution(
33703370
isInitiator = true,
3371-
sharedInput = Multisig2of2Input(channelKeys, parentCommitment),
3371+
sharedInput = SharedFundingInput(channelKeys, parentCommitment),
33723372
spliceInAmount = cmd.additionalLocalFunding,
33733373
spliceOut = cmd.spliceOutputs,
33743374
targetFeerate = targetFeerate)

eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -101,27 +101,21 @@ object InteractiveTxBuilder {
101101
case class RequireConfirmedInputs(forLocal: Boolean, forRemote: Boolean)
102102

103103
/** An input that is already shared between participants (e.g. the current funding output when doing a splice). */
104-
sealed trait SharedFundingInput {
105-
// @formatter:off
106-
def info: InputInfo
107-
def weight: Int
108-
// @formatter:on
109-
}
110-
111-
case class Multisig2of2Input(info: InputInfo, fundingTxIndex: Long, remoteFundingPubkey: PublicKey) extends SharedFundingInput {
112-
override val weight: Int = 388
104+
case class SharedFundingInput(info: InputInfo, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, commitmentFormat: CommitmentFormat) {
105+
val weight: Int = commitmentFormat.fundingInputWeight
113106

114107
def sign(channelKeys: ChannelKeys, tx: Transaction, spentUtxos: Map[OutPoint, TxOut]): ChannelSpendSignature.IndividualSignature = {
115108
val localFundingKey = channelKeys.fundingKey(fundingTxIndex)
116109
Transactions.SpliceTx(info, tx).sign(localFundingKey, remoteFundingPubkey, spentUtxos)
117110
}
118111
}
119112

120-
object Multisig2of2Input {
121-
def apply(channelKeys: ChannelKeys, commitment: Commitment): Multisig2of2Input = Multisig2of2Input(
113+
object SharedFundingInput {
114+
def apply(channelKeys: ChannelKeys, commitment: Commitment): SharedFundingInput = SharedFundingInput(
122115
info = commitment.commitInput(channelKeys),
123116
fundingTxIndex = commitment.fundingTxIndex,
124-
remoteFundingPubkey = commitment.remoteFundingPubKey
117+
remoteFundingPubkey = commitment.remoteFundingPubKey,
118+
commitmentFormat = commitment.commitmentFormat,
125119
)
126120
}
127121

@@ -928,9 +922,10 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
928922
import fr.acinq.bitcoin.scalacompat.KotlinUtils._
929923

930924
val tx = unsignedTx.buildUnsignedTx()
931-
val sharedSig_opt = fundingParams.sharedInput_opt.collect {
932-
case i: Multisig2of2Input => i.sign(channelKeys, tx, unsignedTx.inputDetails).sig
933-
}
925+
val sharedSig_opt = fundingParams.sharedInput_opt.map(i => i.commitmentFormat match {
926+
case _: SegwitV0CommitmentFormat => i.sign(channelKeys, tx, unsignedTx.inputDetails).sig
927+
case _: SimpleTaprootChannelCommitmentFormat => ???
928+
})
934929
if (unsignedTx.localInputs.isEmpty) {
935930
context.self ! SignTransactionResult(PartiallySignedSharedTransaction(unsignedTx, TxSignatures(fundingParams.channelId, tx, Nil, sharedSig_opt)))
936931
} else {
@@ -1054,18 +1049,19 @@ object InteractiveTxSigningSession {
10541049
log.info("invalid tx_signatures: witness count mismatch (expected={}, got={})", partiallySignedTx.tx.remoteInputs.length, remoteSigs.witnesses.length)
10551050
return Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId)))
10561051
}
1057-
val sharedSigs_opt = fundingParams.sharedInput_opt match {
1058-
case Some(sharedInput: Multisig2of2Input) =>
1059-
(partiallySignedTx.localSigs.previousFundingTxSig_opt, remoteSigs.previousFundingTxSig_opt) match {
1052+
val sharedSigs_opt = fundingParams.sharedInput_opt.map(sharedInput => {
1053+
sharedInput.commitmentFormat match {
1054+
case _: SegwitV0CommitmentFormat => (partiallySignedTx.localSigs.previousFundingTxSig_opt, remoteSigs.previousFundingTxSig_opt) match {
10601055
case (Some(localSig), Some(remoteSig)) =>
10611056
val localFundingPubkey = channelKeys.fundingKey(sharedInput.fundingTxIndex).publicKey
1062-
Some(Scripts.witness2of2(localSig, remoteSig, localFundingPubkey, sharedInput.remoteFundingPubkey))
1057+
Scripts.witness2of2(localSig, remoteSig, localFundingPubkey, sharedInput.remoteFundingPubkey)
10631058
case _ =>
10641059
log.info("invalid tx_signatures: missing shared input signatures")
10651060
return Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId)))
10661061
}
1067-
case None => None
1068-
}
1062+
case _: SimpleTaprootChannelCommitmentFormat => ???
1063+
}
1064+
})
10691065
val txWithSigs = FullySignedSharedTransaction(partiallySignedTx.tx, partiallySignedTx.localSigs, remoteSigs, sharedSigs_opt)
10701066
if (remoteSigs.txId != txWithSigs.signedTx.txid) {
10711067
log.info("invalid tx_signatures: txId mismatch (expected={}, got={})", txWithSigs.signedTx.txid, remoteSigs.txId)

eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ object Transactions {
5858

5959
sealed trait CommitmentFormat {
6060
// @formatter:off
61+
/** Weight of a fully signed channel output, when spent by a [[ChannelSpendTransaction]]. */
62+
def fundingInputWeight: Int
6163
/** Weight of a fully signed [[CommitTx]] transaction without any HTLCs. */
6264
def commitWeight: Int
6365
/** Weight of a fully signed [[ClaimLocalAnchorTx]] or [[ClaimRemoteAnchorTx]] input. */
@@ -93,7 +95,9 @@ object Transactions {
9395
// @formatter:on
9496
}
9597

96-
sealed trait SegwitV0CommitmentFormat extends CommitmentFormat
98+
sealed trait SegwitV0CommitmentFormat extends CommitmentFormat {
99+
override val fundingInputWeight = 384
100+
}
97101

98102
/**
99103
* Commitment format as defined in the v1.0 specification (https://github.com/lightningnetwork/lightning-rfc/tree/v1.0).
@@ -168,6 +172,7 @@ object Transactions {
168172
sealed trait SimpleTaprootChannelCommitmentFormat extends TaprootCommitmentFormat {
169173
// weights for taproot transactions are deterministic since signatures are encoded as 64 bytes and
170174
// not in variable length DER format (around 72 bytes)
175+
override val fundingInputWeight = 230
171176
override val commitWeight = 960
172177
override val anchorInputWeight = 230
173178
override val htlcOutputWeight = 172

eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,12 +277,12 @@ private[channel] object ChannelCodecs4 {
277277

278278
val spentMapCodec: Codec[Map[OutPoint, Transaction]] = mapCodec(outPointCodec, txCodec)
279279

280-
private val multisig2of2InputCodec: Codec[InteractiveTxBuilder.Multisig2of2Input] = (
280+
private val multisig2of2InputCodec: Codec[ChannelTypes4.Multisig2of2Input] = (
281281
("info" | inputInfoCodec) ::
282282
("fundingTxIndex" | uint32) ::
283-
("remoteFundingPubkey" | publicKey)).as[InteractiveTxBuilder.Multisig2of2Input]
283+
("remoteFundingPubkey" | publicKey)).as[ChannelTypes4.Multisig2of2Input]
284284

285-
private val sharedFundingInputCodec: Codec[InteractiveTxBuilder.SharedFundingInput] = discriminated[InteractiveTxBuilder.SharedFundingInput].by(uint16)
285+
private val sharedFundingInputCodec: Codec[ChannelTypes4.Multisig2of2Input] = discriminated[ChannelTypes4.Multisig2of2Input].by(uint16)
286286
.typecase(0x01, multisig2of2InputCodec)
287287

288288
private val requireConfirmedInputsCodec: Codec[InteractiveTxBuilder.RequireConfirmedInputs] = (("forLocal" | bool8) :: ("forRemote" | bool8)).as[InteractiveTxBuilder.RequireConfirmedInputs]

eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelTypes4.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,16 @@ private[channel] object ChannelTypes4 {
7272
def remoteCommitParams(): channel.CommitParams = channel.CommitParams(remoteParams.dustLimit, remoteParams.htlcMinimum, remoteParams.maxHtlcValueInFlightMsat, remoteParams.maxAcceptedHtlcs, localParams.toSelfDelay)
7373
}
7474

75+
case class Multisig2of2Input(info: InputInfo, fundingTxIndex: Long, remoteFundingPubkey: PublicKey) {
76+
def migrate(commitmentFormat: CommitmentFormat): InteractiveTxBuilder.SharedFundingInput = InteractiveTxBuilder.SharedFundingInput(info, fundingTxIndex, remoteFundingPubkey, commitmentFormat)
77+
}
78+
7579
// We added the commitment format when moving to channel codecs v5.
7680
case class InteractiveTxParams(channelId: ByteVector32,
7781
isInitiator: Boolean,
7882
localContribution: Satoshi,
7983
remoteContribution: Satoshi,
80-
sharedInput_opt: Option[InteractiveTxBuilder.SharedFundingInput],
84+
sharedInput_opt: Option[Multisig2of2Input],
8185
remoteFundingPubKey: PublicKey,
8286
localOutputs: List[TxOut],
8387
lockTime: Long,
@@ -89,7 +93,7 @@ private[channel] object ChannelTypes4 {
8993
isInitiator = isInitiator,
9094
localContribution = localContribution,
9195
remoteContribution = remoteContribution,
92-
sharedInput_opt = sharedInput_opt,
96+
sharedInput_opt = sharedInput_opt.map(_.migrate(commitmentFormat)),
9397
remoteFundingPubKey = remoteFundingPubKey,
9498
localOutputs = localOutputs,
9599
commitmentFormat = commitmentFormat,

eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,14 @@ private[channel] object ChannelCodecs5 {
119119
("maxAcceptedHtlcs" | uint16) ::
120120
("toSelfDelay" | cltvExpiryDelta)).as[CommitParams]
121121

122-
private val multisig2of2InputCodec: Codec[InteractiveTxBuilder.Multisig2of2Input] = (
122+
private val interactiveTxSharedFundingInputCodec: Codec[InteractiveTxBuilder.SharedFundingInput] = (
123123
("info" | inputInfoCodec) ::
124124
("fundingTxIndex" | uint32) ::
125-
("remoteFundingPubkey" | publicKey)).as[InteractiveTxBuilder.Multisig2of2Input]
125+
("remoteFundingPubkey" | publicKey) ::
126+
("commitmentFormat" | commitmentFormatCodec)).as[InteractiveTxBuilder.SharedFundingInput]
126127

127128
private val sharedFundingInputCodec: Codec[InteractiveTxBuilder.SharedFundingInput] = discriminated[InteractiveTxBuilder.SharedFundingInput].by(uint16)
128-
.typecase(0x01, multisig2of2InputCodec)
129+
.typecase(0x01, interactiveTxSharedFundingInputCodec)
129130

130131
private val requireConfirmedInputsCodec: Codec[InteractiveTxBuilder.RequireConfirmedInputs] = (("forLocal" | bool8) :: ("forRemote" | bool8)).as[InteractiveTxBuilder.RequireConfirmedInputs]
131132

eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,18 +105,18 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit
105105
val fundingPubkeyScript: ByteVector = Script.write(Script.pay2wsh(Scripts.multiSig2of2(fundingParamsB.remoteFundingPubKey, fundingParamsA.remoteFundingPubKey)))
106106

107107
def sharedInputs(commitmentA: Commitment, commitmentB: Commitment): (SharedFundingInput, SharedFundingInput) = {
108-
val sharedInputA = Multisig2of2Input(channelKeysA, commitmentA)
109-
val sharedInputB = Multisig2of2Input(channelKeysB, commitmentB)
108+
val sharedInputA = SharedFundingInput(channelKeysA, commitmentA)
109+
val sharedInputB = SharedFundingInput(channelKeysB, commitmentB)
110110
(sharedInputA, sharedInputB)
111111
}
112112

113113
def dummySharedInputB(amount: Satoshi): SharedFundingInput = {
114114
val inputInfo = InputInfo(OutPoint(randomTxId(), 3), TxOut(amount, fundingPubkeyScript))
115115
val fundingTxIndex = fundingParamsA.sharedInput_opt match {
116-
case Some(input: Multisig2of2Input) => input.fundingTxIndex + 1
116+
case Some(input) => input.fundingTxIndex + 1
117117
case _ => 0
118118
}
119-
Multisig2of2Input(inputInfo, fundingTxIndex, fundingParamsA.remoteFundingPubKey)
119+
SharedFundingInput(inputInfo, fundingTxIndex, fundingParamsA.remoteFundingPubKey, fundingParamsA.commitmentFormat)
120120
}
121121

122122
def createSpliceFixtureParams(fundingTxIndex: Long, fundingAmountA: Satoshi, fundingAmountB: Satoshi, targetFeerate: FeeratePerKw, dustLimit: Satoshi, lockTime: Long, sharedInputA: SharedFundingInput, sharedInputB: SharedFundingInput, spliceOutputsA: List[TxOut] = Nil, spliceOutputsB: List[TxOut] = Nil, requireConfirmedInputs: RequireConfirmedInputs = RequireConfirmedInputs(forLocal = false, forRemote = false)): FixtureParams = {
@@ -2606,7 +2606,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit
26062606
val wallet = new SingleKeyOnChainWallet()
26072607
val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0)
26082608
val previousCommitment = CommitmentsSpec.makeCommitments(25_000_000 msat, 50_000_000 msat).active.head
2609-
val fundingParams = params.fundingParamsB.copy(sharedInput_opt = Some(Multisig2of2Input(previousCommitment.commitInput(params.channelKeysB), 0, randomKey().publicKey)))
2609+
val fundingParams = params.fundingParamsB.copy(sharedInput_opt = Some(SharedFundingInput(previousCommitment.commitInput(params.channelKeysB), 0, randomKey().publicKey, previousCommitment.commitmentFormat)))
26102610
val bob = params.spawnTxBuilderSpliceBob(fundingParams, previousCommitment, wallet)
26112611
bob ! Start(probe.ref)
26122612
// Alice --- tx_add_input --> Bob
@@ -2622,7 +2622,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit
26222622
val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0)
26232623
val previousCommitment = CommitmentsSpec.makeCommitments(25_000_000 msat, 50_000_000 msat).active.head
26242624
val fundingTx = Transaction(2, Nil, Seq(TxOut(50_000 sat, Script.pay2wpkh(randomKey().publicKey)), TxOut(20_000 sat, Script.pay2wpkh(randomKey().publicKey))), 0)
2625-
val sharedInput = Multisig2of2Input(InputInfo(OutPoint(fundingTx, 0), fundingTx.txOut.head), 0, randomKey().publicKey)
2625+
val sharedInput = SharedFundingInput(InputInfo(OutPoint(fundingTx, 0), fundingTx.txOut.head), 0, randomKey().publicKey, previousCommitment.commitmentFormat)
26262626
val bob = params.spawnTxBuilderSpliceBob(params.fundingParamsB.copy(sharedInput_opt = Some(sharedInput)), previousCommitment, wallet)
26272627
bob ! Start(probe.ref)
26282628
// Alice --- tx_add_input --> Bob

eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,9 @@ class TransactionsSpec extends AnyFunSuite with Logging {
315315
commitTx
316316
}
317317
commitTx.correctlySpends(Seq(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
318+
// We check the expected weight of the commit input:
319+
val commitInputWeight = commitTx.copy(txIn = Seq(commitTx.txIn.head, commitTx.txIn.head)).weight() - commitTx.weight()
320+
checkExpectedWeight(commitInputWeight, commitmentFormat.fundingInputWeight, commitmentFormat)
318321
val htlcTxs = makeHtlcTxs(commitTx, outputs, commitmentFormat)
319322
val expiries = htlcTxs.map(tx => tx.htlcId -> tx.htlcExpiry.toLong).toMap
320323
val htlcSuccessTxs = htlcTxs.collect { case tx: UnsignedHtlcSuccessTx => tx }

eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,11 @@ class ChannelCodecs4Spec extends AnyFunSuite {
101101
),
102102
createdAt = BlockHeight(1000),
103103
fundingParams = InteractiveTxParams(channelId = channelId, isInitiator = true, localContribution = 100.sat, remoteContribution = 200.sat,
104-
sharedInput_opt = Some(InteractiveTxBuilder.Multisig2of2Input(
104+
sharedInput_opt = Some(InteractiveTxBuilder.SharedFundingInput(
105105
InputInfo(OutPoint(TxId(ByteVector32.Zeroes), 0), TxOut(1000.sat, Script.pay2wsh(script))),
106106
0,
107-
PrivateKey(ByteVector.fromValidHex("02" * 32)).publicKey
107+
PrivateKey(ByteVector.fromValidHex("02" * 32)).publicKey,
108+
ZeroFeeHtlcTxAnchorOutputsCommitmentFormat,
108109
)),
109110
remoteFundingPubKey = PrivateKey(ByteVector.fromValidHex("01" * 32)).publicKey,
110111
localOutputs = Nil,

0 commit comments

Comments
 (0)