Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package fr.acinq.eclair.blockchain

import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{Satoshi, Transaction}
import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, Transaction}
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import scodec.bits.ByteVector

Expand All @@ -30,10 +30,22 @@ import scala.concurrent.{ExecutionContext, Future}
/** This trait lets users fund lightning channels. */
trait OnChainChannelFunder {

import OnChainWallet.MakeFundingTxResponse
import OnChainWallet._

/** Create a channel funding transaction with the provided pubkeyScript. */
def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse]
/** Fund the provided transaction by adding inputs (and a change output if necessary). */
def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, lockUtxos: Boolean)(implicit ec: ExecutionContext): Future[FundTransactionResponse]

/** Sign the wallet inputs of the provided transaction. */
def signTransaction(tx: Transaction, allowIncomplete: Boolean)(implicit ec: ExecutionContext): Future[SignTransactionResponse]

/**
* Publish a transaction on the bitcoin network.
* This method must be idempotent: if the tx was already published, it must return a success.
*/
def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[ByteVector32]

/** Create a fully signed channel funding transaction with the provided pubkeyScript. */
def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRate: FeeratePerKw)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse]

/**
* Committing *must* include publishing the transaction on the network.
Expand All @@ -47,9 +59,10 @@ trait OnChainChannelFunder {
*/
def commit(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean]

/**
* Rollback a transaction that we failed to commit: this probably translates to "release locks on utxos".
*/
/** Return the transaction if it exists, either in the blockchain or in the mempool. */
def getTransaction(txId: ByteVector32)(implicit ec: ExecutionContext): Future[Transaction]

/** Rollback a transaction that we failed to commit: this probably translates to "release locks on utxos". */
def rollback(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean]

/**
Expand Down Expand Up @@ -97,4 +110,10 @@ object OnChainWallet {

final case class MakeFundingTxResponse(fundingTx: Transaction, fundingTxOutputIndex: Int, fee: Satoshi)

final case class FundTransactionResponse(tx: Transaction, fee: Satoshi, changePosition: Option[Int]) {
val amountIn: Satoshi = fee + tx.txOut.map(_.amount).sum
}

final case class SignTransactionResponse(tx: Transaction, complete: Boolean)

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import fr.acinq.bitcoin.scalacompat._
import fr.acinq.bitcoin.{Bech32, Block}
import fr.acinq.eclair.ShortChannelId.coordinates
import fr.acinq.eclair.blockchain.OnChainWallet
import fr.acinq.eclair.blockchain.OnChainWallet.{MakeFundingTxResponse, OnChainBalance}
import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, SignTransactionResponse}
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{GetTxWithMetaResponse, UtxoStatus, ValidateResult}
import fr.acinq.eclair.blockchain.fee.{FeeratePerKB, FeeratePerKw}
import fr.acinq.eclair.transactions.Transactions
Expand Down Expand Up @@ -220,6 +220,10 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall
})
}

def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, lockUtxos: Boolean)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = {
fundTransaction(tx, FundTransactionOptions(feeRate, replaceable, lockUtxos))
}

def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, targetFeerate: FeeratePerKw)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = {
val partialFundingTx = Transaction(
version = 2,
Expand Down Expand Up @@ -255,11 +259,12 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall

def signTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[SignTransactionResponse] = signTransaction(tx, Nil)

def signTransaction(tx: Transaction, allowIncomplete: Boolean)(implicit ec: ExecutionContext): Future[SignTransactionResponse] = signTransaction(tx, Nil, allowIncomplete)

def signTransaction(tx: Transaction, previousTxs: Seq[PreviousTx], allowIncomplete: Boolean = false)(implicit ec: ExecutionContext): Future[SignTransactionResponse] = {
rpcClient.invoke("signrawtransactionwithwallet", tx.toString(), previousTxs).map(json => {
val JString(hex) = json \ "hex"
val JBool(complete) = json \ "complete"
// TODO: remove allowIncomplete once https://github.com/bitcoin/bitcoin/issues/21151 is fixed
if (!complete && !allowIncomplete) {
val JArray(errors) = json \ "errors"
val message = errors.map(error => {
Expand Down Expand Up @@ -336,7 +341,6 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall
case Failure(JsonRPCError(error)) if error.message.contains("expected locked output") =>
Future.successful(true) // we consider that the outpoint was successfully unlocked (since it was not locked to begin with)
case Failure(t) =>
logger.warn(s"cannot unlock utxo=$utxo:", t)
Future.successful(false)
})
val future = Future.sequence(futures)
Expand Down Expand Up @@ -473,10 +477,6 @@ object BitcoinCoreClient {
}
}

case class FundTransactionResponse(tx: Transaction, fee: Satoshi, changePosition: Option[Int]) {
val amountIn: Satoshi = fee + tx.txOut.map(_.amount).sum
}

case class PreviousTx(txid: ByteVector32, vout: Long, scriptPubKey: String, redeemScript: String, witnessScript: String, amount: BigDecimal)

object PreviousTx {
Expand All @@ -490,8 +490,6 @@ object BitcoinCoreClient {
)
}

case class SignTransactionResponse(tx: Transaction, complete: Boolean)

/**
* Information about a transaction currently in the mempool.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@

package fr.acinq.eclair.channel

import akka.actor.typed
import akka.actor.{ActorRef, PossiblyHarmful}
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, Transaction}
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.channel.InteractiveTxBuilder.{InteractiveTxParams, SignedSharedTransaction}
import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream
import fr.acinq.eclair.transactions.CommitmentSpec
import fr.acinq.eclair.transactions.Transactions._
Expand Down Expand Up @@ -60,7 +62,8 @@ case object WAIT_FOR_CHANNEL_READY extends ChannelState
case object WAIT_FOR_INIT_DUAL_FUNDED_CHANNEL extends ChannelState
case object WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL extends ChannelState
case object WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL extends ChannelState
case object WAIT_FOR_DUAL_FUNDING_INTERNAL extends ChannelState
case object WAIT_FOR_DUAL_FUNDING_CREATED extends ChannelState
case object WAIT_FOR_DUAL_FUNDING_PLACEHOLDER extends ChannelState
// Channel opened:
case object NORMAL extends ChannelState
case object SHUTDOWN extends ChannelState
Expand Down Expand Up @@ -397,6 +400,9 @@ object RealScidStatus {
*/
case class ShortIds(real: RealScidStatus, localAlias: Alias, remoteAlias_opt: Option[Alias])

/** Once a dual funding tx has been signed, we must remember the associated commitments. */
case class DualFundingTx(fundingTx: SignedSharedTransaction, commitments: Commitments)

sealed trait ChannelData extends PossiblyHarmful {
def channelId: ByteVector32
}
Expand Down Expand Up @@ -470,10 +476,19 @@ final case class DATA_WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL(init: INPUT_INIT_CHANNEL
final case class DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL(init: INPUT_INIT_CHANNEL_INITIATOR, lastSent: OpenDualFundedChannel) extends TransientChannelData {
val channelId: ByteVector32 = lastSent.temporaryChannelId
}
final case class DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL(channelId: ByteVector32,
localParams: LocalParams,
remoteParams: RemoteParams,
channelFeatures: ChannelFeatures) extends TransientChannelData
final case class DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId: ByteVector32,
txBuilder: typed.ActorRef[InteractiveTxBuilder.Command],
deferred: Option[ChannelReady]) extends TransientChannelData
final case class DATA_WAIT_FOR_DUAL_FUNDING_PLACEHOLDER(commitments: Commitments,
Comment thread
pm47 marked this conversation as resolved.
fundingTx: SignedSharedTransaction,
fundingParams: InteractiveTxParams,
previousFundingTxs: Seq[DualFundingTx],
waitingSince: BlockHeight, // how long have we been waiting for a funding tx to confirm
lastChecked: BlockHeight, // last time we checked if the channel was double-spent
rbfAttempt: Option[typed.ActorRef[InteractiveTxBuilder.Command]],
deferred: Option[ChannelReady]) extends TransientChannelData {
val channelId: ByteVector32 = commitments.channelId
}

final case class DATA_NORMAL(commitments: Commitments,
shortIds: ShortIds,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package fr.acinq.eclair.channel
import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey
import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, Transaction}
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, Error, UpdateAddHtlc}
import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, Error, InteractiveTxMessage, UpdateAddHtlc}
import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshi, UInt64}

/**
Expand Down Expand Up @@ -50,6 +50,26 @@ case class ChannelReserveTooHigh (override val channelId: Byte
case class ChannelReserveBelowOurDustLimit (override val channelId: ByteVector32, channelReserve: Satoshi, dustLimit: Satoshi) extends ChannelException(channelId, s"their channelReserve=$channelReserve is below our dustLimit=$dustLimit")
case class ChannelReserveNotMet (override val channelId: ByteVector32, toLocal: MilliSatoshi, toRemote: MilliSatoshi, reserve: Satoshi) extends ChannelException(channelId, s"channel reserve is not met toLocal=$toLocal toRemote=$toRemote reserve=$reserve")
case class ChannelFundingError (override val channelId: ByteVector32) extends ChannelException(channelId, "channel funding error")
case class InvalidSerialId (override val channelId: ByteVector32, serialId: UInt64) extends ChannelException(channelId, s"invalid serial_id=${serialId.toByteVector.toHex}")
case class DuplicateSerialId (override val channelId: ByteVector32, serialId: UInt64) extends ChannelException(channelId, s"duplicate serial_id=${serialId.toByteVector.toHex}")
case class UnknownSerialId (override val channelId: ByteVector32, serialId: UInt64) extends ChannelException(channelId, s"unknown serial_id=${serialId.toByteVector.toHex}")
case class DuplicateInput (override val channelId: ByteVector32, serialId: UInt64, previousTxId: ByteVector32, previousTxOutput: Long) extends ChannelException(channelId, s"duplicate input $previousTxId:$previousTxOutput (serial_id=${serialId.toByteVector.toHex})")
case class InputOutOfBounds (override val channelId: ByteVector32, serialId: UInt64, previousTxId: ByteVector32, previousTxOutput: Long) extends ChannelException(channelId, s"invalid input $previousTxId:$previousTxOutput (serial_id=${serialId.toByteVector.toHex})")
case class NonSegwitInput (override val channelId: ByteVector32, serialId: UInt64, previousTxId: ByteVector32, previousTxOutput: Long) extends ChannelException(channelId, s"$previousTxId:$previousTxOutput is not a native segwit input (serial_id=${serialId.toByteVector.toHex})")
case class OutputBelowDust (override val channelId: ByteVector32, serialId: UInt64, amount: Satoshi, dustLimit: Satoshi) extends ChannelException(channelId, s"invalid output amount=$amount below dust=$dustLimit (serial_id=${serialId.toByteVector.toHex})")
case class NonSegwitOutput (override val channelId: ByteVector32, serialId: UInt64) extends ChannelException(channelId, s"output with serial_id=${serialId.toByteVector.toHex} is not a native segwit output")
case class InvalidCompleteInteractiveTx (override val channelId: ByteVector32) extends ChannelException(channelId, "the completed interactive tx is invalid")
case class TooManyInteractiveTxRounds (override val channelId: ByteVector32) extends ChannelException(channelId, "too many messages exchanged during interactive tx construction")
case class DualFundingAborted (override val channelId: ByteVector32) extends ChannelException(channelId, "dual funding aborted")
case class UnexpectedInteractiveTxMessage (override val channelId: ByteVector32, msg: InteractiveTxMessage) extends ChannelException(channelId, s"unexpected interactive-tx message (${msg.getClass.getSimpleName})")
case class UnexpectedCommitSig (override val channelId: ByteVector32) extends ChannelException(channelId, "unexpected commitment signatures (commit_sig)")
case class UnexpectedFundingSignatures (override val channelId: ByteVector32) extends ChannelException(channelId, "unexpected funding signatures (tx_signatures)")
case class InvalidFundingFeerate (override val channelId: ByteVector32, targetFeerate: FeeratePerKw, actualFeerate: FeeratePerKw) extends ChannelException(channelId, s"invalid funding feerate: target=$targetFeerate actual=$actualFeerate")
case class InvalidFundingSignature (override val channelId: ByteVector32, tx_opt: Option[Transaction]) extends ChannelException(channelId, s"invalid funding signature: tx=${tx_opt.map(_.toString()).getOrElse("n/a")}")
case class InvalidRbfFeerate (override val channelId: ByteVector32, proposed: FeeratePerKw, expected: FeeratePerKw) extends ChannelException(channelId, s"invalid rbf attempt: the feerate must be at least $expected, you proposed $proposed")
case class InvalidRbfAlreadyInProgress (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid rbf attempt: the current rbf attempt must be completed or aborted first")
case class InvalidRbfTxConfirmed (override val channelId: ByteVector32) extends ChannelException(channelId, "no need to rbf, transaction is already confirmed")
case class InvalidRbfAttempt (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid rbf attempt")
case class NoMoreHtlcsClosingInProgress (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot send new htlcs, closing in progress")
case class NoMoreFeeUpdateClosingInProgress (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot send new update_fee, closing in progress")
case class ClosingAlreadyInProgress (override val channelId: ByteVector32) extends ChannelException(channelId, "closing already in progress")
Expand All @@ -59,6 +79,7 @@ case class ChannelUnavailable (override val channelId: Byte
case class InvalidFinalScript (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid final script")
case class MissingUpfrontShutdownScript (override val channelId: ByteVector32) extends ChannelException(channelId, "missing upfront shutdown script")
case class FundingTxTimedout (override val channelId: ByteVector32) extends ChannelException(channelId, "funding tx timed out")
case class FundingTxDoubleSpent (override val channelId: ByteVector32) extends ChannelException(channelId, "funding tx double spent")
case class FundingTxSpent (override val channelId: ByteVector32, spendingTx: Transaction) extends ChannelException(channelId, s"funding tx has been spent by txid=${spendingTx.txid}")
case class HtlcsTimedoutDownstream (override val channelId: ByteVector32, htlcs: Set[UpdateAddHtlc]) extends ChannelException(channelId, s"one or more htlcs timed out downstream: ids=${htlcs.take(10).map(_.id).mkString(",")}") // we only display the first 10 ids
case class HtlcsWillTimeoutUpstream (override val channelId: ByteVector32, htlcs: Set[UpdateAddHtlc]) extends ChannelException(channelId, s"one or more htlcs that should be fulfilled are close to timing out upstream: ids=${htlcs.take(10).map(_.id).mkString}") // we only display the first 10 ids
Expand Down
Loading