Skip to content

Commit 94e557c

Browse files
committed
Upgrade input info class to allow spending from taproot transactions
Our InputInfo class contains a tx output and the matching redeem script, which is enough to spend segwit v0 transactions. For taproot transactions, instead of a redeem script, we need a script tree instead, and the appropriate internal pubkey.
1 parent 1b01d99 commit 94e557c

File tree

14 files changed

+93
-41
lines changed

14 files changed

+93
-41
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1143,7 +1143,7 @@ case class Commitments(params: ChannelParams,
11431143
val localFundingKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, commitment.fundingTxIndex).publicKey
11441144
val remoteFundingKey = commitment.remoteFundingPubKey
11451145
val fundingScript = Script.write(Scripts.multiSig2of2(localFundingKey, remoteFundingKey))
1146-
commitment.commitInput.redeemScript == fundingScript
1146+
commitment.commitInput.redeemScriptOrScriptTree == Left(fundingScript)
11471147
}
11481148
}
11491149

eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,12 +358,16 @@ private class ReplaceableTxFunder(nodeParams: NodeParams,
358358
import fr.acinq.bitcoin.scalacompat.KotlinUtils._
359359

360360
// We create a PSBT with the non-wallet input already signed:
361+
val witnessScript = locallySignedTx.txInfo.input.redeemScriptOrScriptTree match {
362+
case Left(redeemScript) => fr.acinq.bitcoin.Script.parse(redeemScript)
363+
case _ => null
364+
}
361365
val psbt = new Psbt(locallySignedTx.txInfo.tx)
362366
.updateWitnessInput(
363367
locallySignedTx.txInfo.input.outPoint,
364368
locallySignedTx.txInfo.input.txOut,
365369
null,
366-
fr.acinq.bitcoin.Script.parse(locallySignedTx.txInfo.input.redeemScript),
370+
witnessScript,
367371
fr.acinq.bitcoin.SigHash.SIGHASH_ALL,
368372
java.util.Map.of(),
369373
null,

eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, ByteVector6
2323
import fr.acinq.eclair.crypto.Generators
2424
import fr.acinq.eclair.crypto.Monitoring.{Metrics, Tags}
2525
import fr.acinq.eclair.router.Announcements
26-
import fr.acinq.eclair.transactions.Transactions
2726
import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, TransactionWithInputInfo, TxOwner}
2827
import fr.acinq.eclair.{KamonExt, randomLong}
2928
import grizzled.slf4j.Logging

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

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616

1717
package fr.acinq.eclair.transactions
1818

19-
import fr.acinq.bitcoin.ScriptFlags
19+
import fr.acinq.bitcoin.{ScriptFlags, ScriptTree}
2020
import fr.acinq.bitcoin.SigHash._
2121
import fr.acinq.bitcoin.SigVersion._
22-
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, ripemd160}
22+
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, XonlyPublicKey, ripemd160}
2323
import fr.acinq.bitcoin.scalacompat.Script._
2424
import fr.acinq.bitcoin.scalacompat._
2525
import fr.acinq.eclair._
@@ -94,9 +94,22 @@ object Transactions {
9494

9595
// @formatter:off
9696
case class OutputInfo(index: Long, amount: Satoshi, publicKeyScript: ByteVector)
97-
case class InputInfo(outPoint: OutPoint, txOut: TxOut, redeemScript: ByteVector)
97+
98+
/**
99+
* to spend the output of a taproot transactions, we need to know the script tree and internal key used to build this output
100+
*/
101+
case class ScriptTreeAndInternalKey(scriptTree: ScriptTree, internalKey: XonlyPublicKey) {
102+
val publicKeyScript: ByteVector = Script.write(Script.pay2tr(internalKey, Some(scriptTree)))
103+
}
104+
105+
case class InputInfo(outPoint: OutPoint, txOut: TxOut, redeemScriptOrScriptTree: Either[ByteVector, ScriptTreeAndInternalKey]) {
106+
val redeemScriptOrEmptyScript: ByteVector = redeemScriptOrScriptTree.swap.getOrElse(ByteVector.empty) // TODO: use the actual script tree for taproot transactions, once we implement them
107+
}
108+
98109
object InputInfo {
99-
def apply(outPoint: OutPoint, txOut: TxOut, redeemScript: Seq[ScriptElt]) = new InputInfo(outPoint, txOut, Script.write(redeemScript))
110+
def apply(outPoint: OutPoint, txOut: TxOut, redeemScript: ByteVector) = new InputInfo(outPoint, txOut, Left(redeemScript))
111+
def apply(outPoint: OutPoint, txOut: TxOut, redeemScript: Seq[ScriptElt]) = new InputInfo(outPoint, txOut, Left(Script.write(redeemScript)))
112+
def apply(outPoint: OutPoint, txOut: TxOut, scriptTree: ScriptTreeAndInternalKey) = new InputInfo(outPoint, txOut, Right(scriptTree))
100113
}
101114

102115
/** Owner of a given transaction (local/remote). */
@@ -125,12 +138,12 @@ object Transactions {
125138
// NB: the tx may have multiple inputs, we will only sign the one provided in txinfo.input. Bear in mind that the
126139
// signature will be invalidated if other inputs are added *afterwards* and sighashType was SIGHASH_ALL.
127140
val inputIndex = tx.txIn.zipWithIndex.find(_._1.outPoint == input.outPoint).get._2
128-
Transactions.sign(tx, input.redeemScript, input.txOut.amount, key, sighashType, inputIndex)
141+
Transactions.sign(tx, input.redeemScriptOrEmptyScript, input.txOut.amount, key, sighashType, inputIndex)
129142
}
130143

131144
def checkSig(sig: ByteVector64, pubKey: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): Boolean = {
132145
val sighash = this.sighash(txOwner, commitmentFormat)
133-
val data = Transaction.hashForSigning(tx, inputIndex = 0, input.redeemScript, sighash, input.txOut.amount, SIGVERSION_WITNESS_V0)
146+
val data = Transaction.hashForSigning(tx, inputIndex = 0, input.redeemScriptOrEmptyScript, sighash, input.txOut.amount, SIGVERSION_WITNESS_V0)
134147
Crypto.verifySignature(data, sig, pubKey)
135148
}
136149
}
@@ -872,7 +885,7 @@ object Transactions {
872885
// NB: the tx may have multiple inputs, we will only sign the one provided in txinfo.input. Bear in mind that the
873886
// signature will be invalidated if other inputs are added *afterwards* and sighashType was SIGHASH_ALL.
874887
val inputIndex = txinfo.tx.txIn.zipWithIndex.find(_._1.outPoint == txinfo.input.outPoint).get._2
875-
sign(txinfo.tx, txinfo.input.redeemScript, txinfo.input.txOut.amount, key, sighashType, inputIndex)
888+
sign(txinfo.tx, txinfo.input.redeemScriptOrEmptyScript, txinfo.input.txOut.amount, key, sighashType, inputIndex)
876889
}
877890

878891
def addSigs(commitTx: CommitTx, localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: ByteVector64, remoteSig: ByteVector64): CommitTx = {
@@ -881,32 +894,32 @@ object Transactions {
881894
}
882895

883896
def addSigs(mainPenaltyTx: MainPenaltyTx, revocationSig: ByteVector64): MainPenaltyTx = {
884-
val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, mainPenaltyTx.input.redeemScript)
897+
val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, mainPenaltyTx.input.redeemScriptOrEmptyScript)
885898
mainPenaltyTx.copy(tx = mainPenaltyTx.tx.updateWitness(0, witness))
886899
}
887900

888901
def addSigs(htlcPenaltyTx: HtlcPenaltyTx, revocationSig: ByteVector64, revocationPubkey: PublicKey): HtlcPenaltyTx = {
889-
val witness = Scripts.witnessHtlcWithRevocationSig(revocationSig, revocationPubkey, htlcPenaltyTx.input.redeemScript)
902+
val witness = Scripts.witnessHtlcWithRevocationSig(revocationSig, revocationPubkey, htlcPenaltyTx.input.redeemScriptOrEmptyScript)
890903
htlcPenaltyTx.copy(tx = htlcPenaltyTx.tx.updateWitness(0, witness))
891904
}
892905

893906
def addSigs(htlcSuccessTx: HtlcSuccessTx, localSig: ByteVector64, remoteSig: ByteVector64, paymentPreimage: ByteVector32, commitmentFormat: CommitmentFormat): HtlcSuccessTx = {
894-
val witness = witnessHtlcSuccess(localSig, remoteSig, paymentPreimage, htlcSuccessTx.input.redeemScript, commitmentFormat)
907+
val witness = witnessHtlcSuccess(localSig, remoteSig, paymentPreimage, htlcSuccessTx.input.redeemScriptOrEmptyScript, commitmentFormat)
895908
htlcSuccessTx.copy(tx = htlcSuccessTx.tx.updateWitness(0, witness))
896909
}
897910

898911
def addSigs(htlcTimeoutTx: HtlcTimeoutTx, localSig: ByteVector64, remoteSig: ByteVector64, commitmentFormat: CommitmentFormat): HtlcTimeoutTx = {
899-
val witness = witnessHtlcTimeout(localSig, remoteSig, htlcTimeoutTx.input.redeemScript, commitmentFormat)
912+
val witness = witnessHtlcTimeout(localSig, remoteSig, htlcTimeoutTx.input.redeemScriptOrEmptyScript, commitmentFormat)
900913
htlcTimeoutTx.copy(tx = htlcTimeoutTx.tx.updateWitness(0, witness))
901914
}
902915

903916
def addSigs(claimHtlcSuccessTx: ClaimHtlcSuccessTx, localSig: ByteVector64, paymentPreimage: ByteVector32): ClaimHtlcSuccessTx = {
904-
val witness = witnessClaimHtlcSuccessFromCommitTx(localSig, paymentPreimage, claimHtlcSuccessTx.input.redeemScript)
917+
val witness = witnessClaimHtlcSuccessFromCommitTx(localSig, paymentPreimage, claimHtlcSuccessTx.input.redeemScriptOrEmptyScript)
905918
claimHtlcSuccessTx.copy(tx = claimHtlcSuccessTx.tx.updateWitness(0, witness))
906919
}
907920

908921
def addSigs(claimHtlcTimeoutTx: ClaimHtlcTimeoutTx, localSig: ByteVector64): ClaimHtlcTimeoutTx = {
909-
val witness = witnessClaimHtlcTimeoutFromCommitTx(localSig, claimHtlcTimeoutTx.input.redeemScript)
922+
val witness = witnessClaimHtlcTimeoutFromCommitTx(localSig, claimHtlcTimeoutTx.input.redeemScriptOrEmptyScript)
910923
claimHtlcTimeoutTx.copy(tx = claimHtlcTimeoutTx.tx.updateWitness(0, witness))
911924
}
912925

@@ -916,27 +929,27 @@ object Transactions {
916929
}
917930

918931
def addSigs(claimRemoteDelayedOutputTx: ClaimRemoteDelayedOutputTx, localSig: ByteVector64): ClaimRemoteDelayedOutputTx = {
919-
val witness = witnessClaimToRemoteDelayedFromCommitTx(localSig, claimRemoteDelayedOutputTx.input.redeemScript)
932+
val witness = witnessClaimToRemoteDelayedFromCommitTx(localSig, claimRemoteDelayedOutputTx.input.redeemScriptOrEmptyScript)
920933
claimRemoteDelayedOutputTx.copy(tx = claimRemoteDelayedOutputTx.tx.updateWitness(0, witness))
921934
}
922935

923936
def addSigs(claimDelayedOutputTx: ClaimLocalDelayedOutputTx, localSig: ByteVector64): ClaimLocalDelayedOutputTx = {
924-
val witness = witnessToLocalDelayedAfterDelay(localSig, claimDelayedOutputTx.input.redeemScript)
937+
val witness = witnessToLocalDelayedAfterDelay(localSig, claimDelayedOutputTx.input.redeemScriptOrEmptyScript)
925938
claimDelayedOutputTx.copy(tx = claimDelayedOutputTx.tx.updateWitness(0, witness))
926939
}
927940

928941
def addSigs(htlcDelayedTx: HtlcDelayedTx, localSig: ByteVector64): HtlcDelayedTx = {
929-
val witness = witnessToLocalDelayedAfterDelay(localSig, htlcDelayedTx.input.redeemScript)
942+
val witness = witnessToLocalDelayedAfterDelay(localSig, htlcDelayedTx.input.redeemScriptOrEmptyScript)
930943
htlcDelayedTx.copy(tx = htlcDelayedTx.tx.updateWitness(0, witness))
931944
}
932945

933946
def addSigs(claimAnchorOutputTx: ClaimLocalAnchorOutputTx, localSig: ByteVector64): ClaimLocalAnchorOutputTx = {
934-
val witness = witnessAnchor(localSig, claimAnchorOutputTx.input.redeemScript)
947+
val witness = witnessAnchor(localSig, claimAnchorOutputTx.input.redeemScriptOrEmptyScript)
935948
claimAnchorOutputTx.copy(tx = claimAnchorOutputTx.tx.updateWitness(0, witness))
936949
}
937950

938951
def addSigs(claimHtlcDelayedPenalty: ClaimHtlcDelayedOutputPenaltyTx, revocationSig: ByteVector64): ClaimHtlcDelayedOutputPenaltyTx = {
939-
val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, claimHtlcDelayedPenalty.input.redeemScript)
952+
val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, claimHtlcDelayedPenalty.input.redeemScriptOrEmptyScript)
940953
claimHtlcDelayedPenalty.copy(tx = claimHtlcDelayedPenalty.tx.updateWitness(0, witness))
941954
}
942955

eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,14 @@ private[channel] object ChannelCodecs0 {
125125
closingTx => closingTx.tx
126126
)
127127

128-
val inputInfoCodec: Codec[InputInfo] = (
128+
private case class InputInfoLegacy(outPoint: OutPoint, txOut: TxOut, redeemScript: ByteVector)
129+
130+
private val inputInfoLegacyCodec: Codec[InputInfoLegacy] = (
129131
("outPoint" | outPointCodec) ::
130132
("txOut" | txOutCodec) ::
131-
("redeemScript" | varsizebinarydata)).as[InputInfo].decodeOnly
133+
("redeemScript" | varsizebinarydata)).as[InputInfoLegacy]
134+
135+
val inputInfoCodec: Codec[InputInfo] = inputInfoLegacyCodec.xmap[InputInfo](legacy => InputInfo(legacy.outPoint, legacy.txOut, Left(legacy.redeemScript)), _ => ???).decodeOnly
132136

133137
private val defaultConfirmationTarget: Codec[ConfirmationTarget.Absolute] = provide(ConfirmationTarget.Absolute(BlockHeight(0)))
134138

eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ private[channel] object ChannelTypes0 {
4848
// modified: we don't use the InputInfo in closing business logic, so we don't need to fill everything (this part
4949
// assumes that we only have standard channels, no anchor output channels - which was the case before version2).
5050
val input = childTx.txIn.head.outPoint
51-
InputInfo(input, parentTx.txOut(input.index.toInt), Nil)
51+
InputInfo(input, parentTx.txOut(input.index.toInt), ByteVector.fromValidHex("deadbeef"))
5252
}
5353

5454
case class LocalCommitPublished(commitTx: Transaction, claimMainDelayedOutputTx: Option[Transaction], htlcSuccessTxs: List[Transaction], htlcTimeoutTxs: List[Transaction], claimHtlcDelayedTxs: List[Transaction], irrevocablySpent: Map[OutPoint, TxId]) {
@@ -97,7 +97,7 @@ private[channel] object ChannelTypes0 {
9797
val htlcPenaltyTxsNew = htlcPenaltyTxs.map(tx => HtlcPenaltyTx(getPartialInputInfo(commitTx, tx), tx))
9898
val claimHtlcDelayedPenaltyTxsNew = claimHtlcDelayedPenaltyTxs.map(tx => {
9999
// We don't have all the `InputInfo` data, but it's ok: we only use the tx that is fully signed.
100-
ClaimHtlcDelayedOutputPenaltyTx(InputInfo(tx.txIn.head.outPoint, TxOut(Satoshi(0), Nil), Nil), tx)
100+
ClaimHtlcDelayedOutputPenaltyTx(InputInfo(tx.txIn.head.outPoint, TxOut(Satoshi(0), Nil), ByteVector.fromValidHex("deadbeef")), tx) // FIXME: use proper value when we upgrade InputInfo to use `Either`
101101
})
102102
channel.RevokedCommitPublished(commitTx, claimMainOutputTxNew, mainPenaltyTxNew, htlcPenaltyTxsNew, claimHtlcDelayedPenaltyTxsNew, irrevocablySpentNew)
103103
}
@@ -108,7 +108,7 @@ private[channel] object ChannelTypes0 {
108108
* the raw transaction. It provides more information for auditing but is not used for business logic, so we can safely
109109
* put dummy values in the migration.
110110
*/
111-
def migrateClosingTx(tx: Transaction): ClosingTx = ClosingTx(InputInfo(tx.txIn.head.outPoint, TxOut(Satoshi(0), Nil), Nil), tx, None)
111+
def migrateClosingTx(tx: Transaction): ClosingTx = ClosingTx(InputInfo(tx.txIn.head.outPoint, TxOut(Satoshi(0), Nil), ByteVector.fromValidHex("deadbeef")), tx, None)
112112

113113
case class HtlcTxAndSigs(txinfo: HtlcTx, localSig: ByteVector64, remoteSig: ByteVector64)
114114

eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,14 @@ private[channel] object ChannelCodecs1 {
9797
closingTx => closingTx.tx
9898
)
9999

100-
val inputInfoCodec: Codec[InputInfo] = (
100+
private case class InputInfoLegacy(outPoint: OutPoint, txOut: TxOut, redeemScript: ByteVector)
101+
102+
private val inputInfoLegacyCodec: Codec[InputInfoLegacy] = (
101103
("outPoint" | outPointCodec) ::
102104
("txOut" | txOutCodec) ::
103-
("redeemScript" | lengthDelimited(bytes))).as[InputInfo]
105+
("redeemScript" | lengthDelimited(bytes))).as[InputInfoLegacy]
106+
107+
val inputInfoCodec: Codec[InputInfo] = inputInfoLegacyCodec.xmap[InputInfo](legacy => InputInfo(legacy.outPoint, legacy.txOut, Left(legacy.redeemScript)), _ => ???).decodeOnly
104108

105109
private val defaultConfirmationTarget: Codec[ConfirmationTarget.Absolute] = provide(ConfirmationTarget.Absolute(BlockHeight(0)))
106110

eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,14 @@ private[channel] object ChannelCodecs2 {
101101

102102
val txCodec: Codec[Transaction] = lengthDelimited(bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d)))
103103

104-
val inputInfoCodec: Codec[InputInfo] = (
104+
private case class InputInfoLegacy(outPoint: OutPoint, txOut: TxOut, redeemScript: ByteVector)
105+
106+
private val inputInfoLegacyCodec: Codec[InputInfoLegacy] = (
105107
("outPoint" | outPointCodec) ::
106108
("txOut" | txOutCodec) ::
107-
("redeemScript" | lengthDelimited(bytes))).as[InputInfo]
109+
("redeemScript" | lengthDelimited(bytes))).as[InputInfoLegacy]
110+
111+
val inputInfoCodec: Codec[InputInfo] = inputInfoLegacyCodec.xmap[InputInfo](legacy => InputInfo(legacy.outPoint, legacy.txOut, Left(legacy.redeemScript)), _ => ???).decodeOnly
108112

109113
val outputInfoCodec: Codec[OutputInfo] = (
110114
("index" | uint32) ::

eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,14 @@ private[channel] object ChannelCodecs3 {
113113

114114
val txCodec: Codec[Transaction] = lengthDelimited(bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d)))
115115

116-
val inputInfoCodec: Codec[InputInfo] = (
116+
private case class InputInfoLegacy(outPoint: OutPoint, txOut: TxOut, redeemScript: ByteVector)
117+
118+
private val inputInfoLegacyCodec: Codec[InputInfoLegacy] = (
117119
("outPoint" | outPointCodec) ::
118120
("txOut" | txOutCodec) ::
119-
("redeemScript" | lengthDelimited(bytes))).as[InputInfo]
121+
("redeemScript" | lengthDelimited(bytes))).as[InputInfoLegacy]
122+
123+
val inputInfoCodec: Codec[InputInfo] = inputInfoLegacyCodec.xmap[InputInfo](legacy => InputInfo(legacy.outPoint, legacy.txOut, Left(legacy.redeemScript)), _ => ???).decodeOnly
120124

121125
val outputInfoCodec: Codec[OutputInfo] = (
122126
("index" | uint32) ::

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package fr.acinq.eclair.wire.internal.channel.version4
22

3+
import fr.acinq.bitcoin.ScriptTree
4+
import fr.acinq.bitcoin.io.ByteArrayInput
35
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
46
import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath
57
import fr.acinq.bitcoin.scalacompat.{OutPoint, ScriptWitness, Transaction, TxOut}
@@ -109,10 +111,26 @@ private[channel] object ChannelCodecs4 {
109111

110112
val txCodec: Codec[Transaction] = lengthDelimited(bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d)))
111113

112-
val inputInfoCodec: Codec[InputInfo] = (
114+
val scriptTreeCodec: Codec[ScriptTree] = lengthDelimited(bytes.xmap(d => ScriptTree.read(new ByteArrayInput(d.toArray)), d => ByteVector.view(d.write())))
115+
116+
val scriptTreeAndInternalKey: Codec[ScriptTreeAndInternalKey] = (scriptTreeCodec :: xonlyPublicKey).as[ScriptTreeAndInternalKey]
117+
118+
private case class InputInfoEx(outPoint: OutPoint, txOut: TxOut, redeemScript: ByteVector, redeemScriptOrScriptTree: Either[ByteVector, ScriptTreeAndInternalKey], dummy: Boolean)
119+
120+
// To support the change from redeemScript to "either redeem script or script tree" while remaining backwards-compatible with the previous version 4 codec, we use
121+
// the redeem script itself as a left/write indicator: empty -> right, not empty -> left
122+
private val inputInfoExCodec: Codec[InputInfoEx] = (
113123
("outPoint" | outPointCodec) ::
114124
("txOut" | txOutCodec) ::
115-
("redeemScript" | lengthDelimited(bytes))).as[InputInfo]
125+
(("redeemScript" | lengthDelimited(bytes)) >>:~ { redeemScript =>
126+
("redeemScriptOrScriptTree" | either(provide(redeemScript.isEmpty), provide(redeemScript), scriptTreeAndInternalKey)) :: ("dummy" | provide(false))
127+
})
128+
).as[InputInfoEx]
129+
130+
val inputInfoCodec: Codec[InputInfo] = inputInfoExCodec.xmap(
131+
iex => InputInfo(iex.outPoint, iex.txOut, iex.redeemScriptOrScriptTree),
132+
i => InputInfoEx(i.outPoint, i.txOut, i.redeemScriptOrScriptTree.swap.toOption.getOrElse(ByteVector.empty), i.redeemScriptOrScriptTree, false)
133+
)
116134

117135
val outputInfoCodec: Codec[OutputInfo] = (
118136
("index" | uint32) ::

0 commit comments

Comments
 (0)