Skip to content

Commit

Permalink
Upgrade input info class to allow spending from taproot transactions
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sstone committed Sep 9, 2024
1 parent a8148e8 commit cf583ea
Show file tree
Hide file tree
Showing 18 changed files with 133 additions and 60 deletions.
15 changes: 15 additions & 0 deletions eclair-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,21 @@
<version>4.1.94.Final</version>
</dependency>
<!-- BITCOIN -->
<dependency>
<groupId>fr.acinq.bitcoin</groupId>
<artifactId>bitcoin-kmp-jvm</artifactId>
<version>0.20.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>fr.acinq.secp256k1</groupId>
<artifactId>secp256k1-kmp-jvm</artifactId>
<version>0.15.0</version>
</dependency>
<dependency>
<groupId>fr.acinq.secp256k1</groupId>
<artifactId>secp256k1-kmp-jni-jvm</artifactId>
<version>0.15.0</version>
</dependency>
<dependency>
<groupId>fr.acinq</groupId>
<artifactId>bitcoin-lib_${scala.version.short}</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1143,7 +1143,7 @@ case class Commitments(params: ChannelParams,
val localFundingKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, commitment.fundingTxIndex).publicKey
val remoteFundingKey = commitment.remoteFundingPubKey
val fundingScript = Script.write(Scripts.multiSig2of2(localFundingKey, remoteFundingKey))
commitment.commitInput.redeemScript == fundingScript
commitment.commitInput.redeemScriptOrScriptTree == Left(fundingScript)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,17 @@ private class ReplaceableTxFunder(nodeParams: NodeParams,
import fr.acinq.bitcoin.scalacompat.KotlinUtils._

// We create a PSBT with the non-wallet input already signed:
val witnessScript = locallySignedTx.txInfo.input.redeemScriptOrScriptTree match {
case Left(redeemScript) => fr.acinq.bitcoin.Script.parse(redeemScript)
case _ => null
}
val psbt = new Psbt(locallySignedTx.txInfo.tx)
.updateWitnessInput(locallySignedTx.txInfo.input.outPoint, locallySignedTx.txInfo.input.txOut, null, fr.acinq.bitcoin.Script.parse(locallySignedTx.txInfo.input.redeemScript), fr.acinq.bitcoin.SigHash.SIGHASH_ALL, java.util.Map.of())
.updateWitnessInput(
locallySignedTx.txInfo.input.outPoint, locallySignedTx.txInfo.input.txOut,
null,
witnessScript,
fr.acinq.bitcoin.SigHash.SIGHASH_ALL,
java.util.Map.of(), null, null, java.util.Map.of())
.flatMap(_.finalizeWitnessInput(0, locallySignedTx.txInfo.tx.txIn.head.witness))
psbt match {
case Left(failure) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ class LocalOnChainKeyManager(override val walletName: String, seed: ByteVector,
require(kmp2scala(input.getWitnessUtxo.publicKeyScript) == expectedScript, s"script mismatch (expected=$expectedScript, actual=${input.getWitnessUtxo.publicKeyScript}): bitcoin core may be malicious")

// Update the input with the right script for a p2wpkh input, which is a *p2pkh* script, then sign and finalize.
val updated: Either[UpdateFailure, Psbt] = psbt.updateWitnessInput(psbt.global.tx.txIn.get(pos).outPoint, input.getWitnessUtxo, null, Script.pay2pkh(pub), SigHash.SIGHASH_ALL, input.getDerivationPaths)
val updated: Either[UpdateFailure, Psbt] = psbt.updateWitnessInput(psbt.global.tx.txIn.get(pos).outPoint, input.getWitnessUtxo, null, Script.pay2pkh(pub), SigHash.SIGHASH_ALL, input.getDerivationPaths, null, null, java.util.Map.of())
val signed = updated.flatMap(_.sign(priv, pos))
val finalized = signed.flatMap(s => {
require(s.getSig.last.toInt == SigHash.SIGHASH_ALL, "signature must end with SIGHASH_ALL")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@

package fr.acinq.eclair.transactions

import fr.acinq.bitcoin.ScriptFlags
import fr.acinq.bitcoin.{ScriptFlags, ScriptTree}
import fr.acinq.bitcoin.SigHash._
import fr.acinq.bitcoin.SigVersion._
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, ripemd160}
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, XonlyPublicKey, ripemd160}
import fr.acinq.bitcoin.scalacompat.Script._
import fr.acinq.bitcoin.scalacompat._
import fr.acinq.eclair._
Expand Down Expand Up @@ -94,9 +94,22 @@ object Transactions {

// @formatter:off
case class OutputInfo(index: Long, amount: Satoshi, publicKeyScript: ByteVector)
case class InputInfo(outPoint: OutPoint, txOut: TxOut, redeemScript: ByteVector)

/**
* to spend the output of a taproot transactions, we need to know the script tree and internal key used to build this output
*/
case class ScriptTreeAndInternalKey(scriptTree: ScriptTree, internalKey: XonlyPublicKey) {
val publicKeyScript: ByteVector = Script.write(Script.pay2tr(internalKey, Some(scriptTree)))
}

case class InputInfo(outPoint: OutPoint, txOut: TxOut, redeemScriptOrScriptTree: Either[ByteVector, ScriptTreeAndInternalKey]) {
val redeemScriptOrEmptyScript: ByteVector = redeemScriptOrScriptTree.swap.getOrElse(ByteVector.empty) // TODO: use the actual script tree for taproot transactions, once we implement them
}

object InputInfo {
def apply(outPoint: OutPoint, txOut: TxOut, redeemScript: Seq[ScriptElt]) = new InputInfo(outPoint, txOut, Script.write(redeemScript))
def apply(outPoint: OutPoint, txOut: TxOut, redeemScript: ByteVector) = new InputInfo(outPoint, txOut, Left(redeemScript))
def apply(outPoint: OutPoint, txOut: TxOut, redeemScript: Seq[ScriptElt]) = new InputInfo(outPoint, txOut, Left(Script.write(redeemScript)))
def apply(outPoint: OutPoint, txOut: TxOut, scriptTree: ScriptTreeAndInternalKey) = new InputInfo(outPoint, txOut, Right(scriptTree))
}

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

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

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

def addSigs(mainPenaltyTx: MainPenaltyTx, revocationSig: ByteVector64): MainPenaltyTx = {
val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, mainPenaltyTx.input.redeemScript)
val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, mainPenaltyTx.input.redeemScriptOrEmptyScript)
mainPenaltyTx.copy(tx = mainPenaltyTx.tx.updateWitness(0, witness))
}

def addSigs(htlcPenaltyTx: HtlcPenaltyTx, revocationSig: ByteVector64, revocationPubkey: PublicKey): HtlcPenaltyTx = {
val witness = Scripts.witnessHtlcWithRevocationSig(revocationSig, revocationPubkey, htlcPenaltyTx.input.redeemScript)
val witness = Scripts.witnessHtlcWithRevocationSig(revocationSig, revocationPubkey, htlcPenaltyTx.input.redeemScriptOrEmptyScript)
htlcPenaltyTx.copy(tx = htlcPenaltyTx.tx.updateWitness(0, witness))
}

def addSigs(htlcSuccessTx: HtlcSuccessTx, localSig: ByteVector64, remoteSig: ByteVector64, paymentPreimage: ByteVector32, commitmentFormat: CommitmentFormat): HtlcSuccessTx = {
val witness = witnessHtlcSuccess(localSig, remoteSig, paymentPreimage, htlcSuccessTx.input.redeemScript, commitmentFormat)
val witness = witnessHtlcSuccess(localSig, remoteSig, paymentPreimage, htlcSuccessTx.input.redeemScriptOrEmptyScript, commitmentFormat)
htlcSuccessTx.copy(tx = htlcSuccessTx.tx.updateWitness(0, witness))
}

def addSigs(htlcTimeoutTx: HtlcTimeoutTx, localSig: ByteVector64, remoteSig: ByteVector64, commitmentFormat: CommitmentFormat): HtlcTimeoutTx = {
val witness = witnessHtlcTimeout(localSig, remoteSig, htlcTimeoutTx.input.redeemScript, commitmentFormat)
val witness = witnessHtlcTimeout(localSig, remoteSig, htlcTimeoutTx.input.redeemScriptOrEmptyScript, commitmentFormat)
htlcTimeoutTx.copy(tx = htlcTimeoutTx.tx.updateWitness(0, witness))
}

def addSigs(claimHtlcSuccessTx: ClaimHtlcSuccessTx, localSig: ByteVector64, paymentPreimage: ByteVector32): ClaimHtlcSuccessTx = {
val witness = witnessClaimHtlcSuccessFromCommitTx(localSig, paymentPreimage, claimHtlcSuccessTx.input.redeemScript)
val witness = witnessClaimHtlcSuccessFromCommitTx(localSig, paymentPreimage, claimHtlcSuccessTx.input.redeemScriptOrEmptyScript)
claimHtlcSuccessTx.copy(tx = claimHtlcSuccessTx.tx.updateWitness(0, witness))
}

def addSigs(claimHtlcTimeoutTx: ClaimHtlcTimeoutTx, localSig: ByteVector64): ClaimHtlcTimeoutTx = {
val witness = witnessClaimHtlcTimeoutFromCommitTx(localSig, claimHtlcTimeoutTx.input.redeemScript)
val witness = witnessClaimHtlcTimeoutFromCommitTx(localSig, claimHtlcTimeoutTx.input.redeemScriptOrEmptyScript)
claimHtlcTimeoutTx.copy(tx = claimHtlcTimeoutTx.tx.updateWitness(0, witness))
}

Expand All @@ -916,27 +929,27 @@ object Transactions {
}

def addSigs(claimRemoteDelayedOutputTx: ClaimRemoteDelayedOutputTx, localSig: ByteVector64): ClaimRemoteDelayedOutputTx = {
val witness = witnessClaimToRemoteDelayedFromCommitTx(localSig, claimRemoteDelayedOutputTx.input.redeemScript)
val witness = witnessClaimToRemoteDelayedFromCommitTx(localSig, claimRemoteDelayedOutputTx.input.redeemScriptOrEmptyScript)
claimRemoteDelayedOutputTx.copy(tx = claimRemoteDelayedOutputTx.tx.updateWitness(0, witness))
}

def addSigs(claimDelayedOutputTx: ClaimLocalDelayedOutputTx, localSig: ByteVector64): ClaimLocalDelayedOutputTx = {
val witness = witnessToLocalDelayedAfterDelay(localSig, claimDelayedOutputTx.input.redeemScript)
val witness = witnessToLocalDelayedAfterDelay(localSig, claimDelayedOutputTx.input.redeemScriptOrEmptyScript)
claimDelayedOutputTx.copy(tx = claimDelayedOutputTx.tx.updateWitness(0, witness))
}

def addSigs(htlcDelayedTx: HtlcDelayedTx, localSig: ByteVector64): HtlcDelayedTx = {
val witness = witnessToLocalDelayedAfterDelay(localSig, htlcDelayedTx.input.redeemScript)
val witness = witnessToLocalDelayedAfterDelay(localSig, htlcDelayedTx.input.redeemScriptOrEmptyScript)
htlcDelayedTx.copy(tx = htlcDelayedTx.tx.updateWitness(0, witness))
}

def addSigs(claimAnchorOutputTx: ClaimLocalAnchorOutputTx, localSig: ByteVector64): ClaimLocalAnchorOutputTx = {
val witness = witnessAnchor(localSig, claimAnchorOutputTx.input.redeemScript)
val witness = witnessAnchor(localSig, claimAnchorOutputTx.input.redeemScriptOrEmptyScript)
claimAnchorOutputTx.copy(tx = claimAnchorOutputTx.tx.updateWitness(0, witness))
}

def addSigs(claimHtlcDelayedPenalty: ClaimHtlcDelayedOutputPenaltyTx, revocationSig: ByteVector64): ClaimHtlcDelayedOutputPenaltyTx = {
val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, claimHtlcDelayedPenalty.input.redeemScript)
val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, claimHtlcDelayedPenalty.input.redeemScriptOrEmptyScript)
claimHtlcDelayedPenalty.copy(tx = claimHtlcDelayedPenalty.tx.updateWitness(0, witness))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,14 @@ private[channel] object ChannelCodecs0 {
closingTx => closingTx.tx
)

val inputInfoCodec: Codec[InputInfo] = (
private case class InputInfoLegacy(outPoint: OutPoint, txOut: TxOut, redeemScript: ByteVector)

private val inputInfoLegacyCodec: Codec[InputInfoLegacy] = (
("outPoint" | outPointCodec) ::
("txOut" | txOutCodec) ::
("redeemScript" | varsizebinarydata)).as[InputInfo].decodeOnly
("redeemScript" | varsizebinarydata)).as[InputInfoLegacy]

val inputInfoCodec: Codec[InputInfo] = inputInfoLegacyCodec.xmap[InputInfo](legacy => InputInfo(legacy.outPoint, legacy.txOut, Left(legacy.redeemScript)), _ => ???).decodeOnly

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

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

case class LocalCommitPublished(commitTx: Transaction, claimMainDelayedOutputTx: Option[Transaction], htlcSuccessTxs: List[Transaction], htlcTimeoutTxs: List[Transaction], claimHtlcDelayedTxs: List[Transaction], irrevocablySpent: Map[OutPoint, TxId]) {
Expand Down Expand Up @@ -97,7 +97,7 @@ private[channel] object ChannelTypes0 {
val htlcPenaltyTxsNew = htlcPenaltyTxs.map(tx => HtlcPenaltyTx(getPartialInputInfo(commitTx, tx), tx))
val claimHtlcDelayedPenaltyTxsNew = claimHtlcDelayedPenaltyTxs.map(tx => {
// We don't have all the `InputInfo` data, but it's ok: we only use the tx that is fully signed.
ClaimHtlcDelayedOutputPenaltyTx(InputInfo(tx.txIn.head.outPoint, TxOut(Satoshi(0), Nil), Nil), tx)
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`
})
channel.RevokedCommitPublished(commitTx, claimMainOutputTxNew, mainPenaltyTxNew, htlcPenaltyTxsNew, claimHtlcDelayedPenaltyTxsNew, irrevocablySpentNew)
}
Expand All @@ -108,7 +108,7 @@ private[channel] object ChannelTypes0 {
* the raw transaction. It provides more information for auditing but is not used for business logic, so we can safely
* put dummy values in the migration.
*/
def migrateClosingTx(tx: Transaction): ClosingTx = ClosingTx(InputInfo(tx.txIn.head.outPoint, TxOut(Satoshi(0), Nil), Nil), tx, None)
def migrateClosingTx(tx: Transaction): ClosingTx = ClosingTx(InputInfo(tx.txIn.head.outPoint, TxOut(Satoshi(0), Nil), ByteVector.fromValidHex("deadbeef")), tx, None)

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,14 @@ private[channel] object ChannelCodecs1 {
closingTx => closingTx.tx
)

val inputInfoCodec: Codec[InputInfo] = (
private case class InputInfoLegacy(outPoint: OutPoint, txOut: TxOut, redeemScript: ByteVector)

private val inputInfoLegacyCodec: Codec[InputInfoLegacy] = (
("outPoint" | outPointCodec) ::
("txOut" | txOutCodec) ::
("redeemScript" | lengthDelimited(bytes))).as[InputInfo]
("redeemScript" | lengthDelimited(bytes))).as[InputInfoLegacy]

val inputInfoCodec: Codec[InputInfo] = inputInfoLegacyCodec.xmap[InputInfo](legacy => InputInfo(legacy.outPoint, legacy.txOut, Left(legacy.redeemScript)), _ => ???).decodeOnly

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,14 @@ private[channel] object ChannelCodecs2 {

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

val inputInfoCodec: Codec[InputInfo] = (
private case class InputInfoLegacy(outPoint: OutPoint, txOut: TxOut, redeemScript: ByteVector)

private val inputInfoLegacyCodec: Codec[InputInfoLegacy] = (
("outPoint" | outPointCodec) ::
("txOut" | txOutCodec) ::
("redeemScript" | lengthDelimited(bytes))).as[InputInfo]
("redeemScript" | lengthDelimited(bytes))).as[InputInfoLegacy]

val inputInfoCodec: Codec[InputInfo] = inputInfoLegacyCodec.xmap[InputInfo](legacy => InputInfo(legacy.outPoint, legacy.txOut, Left(legacy.redeemScript)), _ => ???).decodeOnly

val outputInfoCodec: Codec[OutputInfo] = (
("index" | uint32) ::
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,14 @@ private[channel] object ChannelCodecs3 {

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

val inputInfoCodec: Codec[InputInfo] = (
private case class InputInfoLegacy(outPoint: OutPoint, txOut: TxOut, redeemScript: ByteVector)

private val inputInfoLegacyCodec: Codec[InputInfoLegacy] = (
("outPoint" | outPointCodec) ::
("txOut" | txOutCodec) ::
("redeemScript" | lengthDelimited(bytes))).as[InputInfo]
("redeemScript" | lengthDelimited(bytes))).as[InputInfoLegacy]

val inputInfoCodec: Codec[InputInfo] = inputInfoLegacyCodec.xmap[InputInfo](legacy => InputInfo(legacy.outPoint, legacy.txOut, Left(legacy.redeemScript)), _ => ???).decodeOnly

val outputInfoCodec: Codec[OutputInfo] = (
("index" | uint32) ::
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package fr.acinq.eclair.wire.internal.channel.version4

import fr.acinq.bitcoin.ScriptTree
import fr.acinq.bitcoin.io.ByteArrayInput
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath
import fr.acinq.bitcoin.scalacompat.{OutPoint, ScriptWitness, Transaction, TxOut}
Expand Down Expand Up @@ -109,10 +111,26 @@ private[channel] object ChannelCodecs4 {

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

val inputInfoCodec: Codec[InputInfo] = (
val scriptTreeCodec: Codec[ScriptTree] = lengthDelimited(bytes.xmap(d => ScriptTree.read(new ByteArrayInput(d.toArray)), d => ByteVector.view(d.write())))

val scriptTreeAndInternalKey: Codec[ScriptTreeAndInternalKey] = (scriptTreeCodec :: xonlyPublicKey).as[ScriptTreeAndInternalKey]

private case class InputInfoEx(outPoint: OutPoint, txOut: TxOut, redeemScript: ByteVector, redeemScriptOrScriptTree: Either[ByteVector, ScriptTreeAndInternalKey], dummy: Boolean)

// 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
// the redeem script itself as a left/write indicator: empty -> right, not empty -> left
private val inputInfoExCodec: Codec[InputInfoEx] = (
("outPoint" | outPointCodec) ::
("txOut" | txOutCodec) ::
("redeemScript" | lengthDelimited(bytes))).as[InputInfo]
(("redeemScript" | lengthDelimited(bytes)) >>:~ { redeemScript =>
("redeemScriptOrScriptTree" | either(provide(redeemScript.isEmpty), provide(redeemScript), scriptTreeAndInternalKey)) :: ("dummy" | provide(false))
})
).as[InputInfoEx]

val inputInfoCodec: Codec[InputInfo] = inputInfoExCodec.xmap(
iex => InputInfo(iex.outPoint, iex.txOut, iex.redeemScriptOrScriptTree),
i => InputInfoEx(i.outPoint, i.txOut, i.redeemScriptOrScriptTree.swap.toOption.getOrElse(ByteVector.empty), i.redeemScriptOrScriptTree, false)
)

val outputInfoCodec: Codec[OutputInfo] = (
("index" | uint32) ::
Expand Down
Loading

0 comments on commit cf583ea

Please sign in to comment.