1
0
mirror of https://github.com/ACINQ/eclair.git synced 2024-11-19 09:54:02 +01:00

Audit: Keep track of sent/received/relayed payments and relay/network fees (#654)

Added a new `AuditDb` which keeps tracks of:
- every single payment (received/sent/relayed)
- every single network fee paid to the miners (funding, closing, and all commit/htlc transactions)

Note that network fees are considered paid when the corresponding tx has reached `min_depth`, it makes sense and allows us to compute the fee in one single place in the `CLOSING` handler. There is an exception for the funding tx, for which we consider the fee paid when the tx has successfully been published to the network. It simplifies the implementation and the tradeoff seems acceptable.

Three new functions have been added to the json-rpc api:
- `audit`: returns all individual payments, with optional filtering on timestamp.
- `networkfees`: returns every single fee paid to the miners, by type (`funding`, `mutual`, `revoked-commit`, etc.) and by channel, with optional filtering on timestamp.
- `channelstats`: maybe the most useful method; it returns a number of information per channel, including the `relayFee` (earned) and the `networkFee` (paid).

The `channels` method now returns details information about channels. It makes it far easier to compute aggregate information about channels using the command line.

Also added a new `ChannelFailed` event that allows e.g. the mobile app to know why a channel got closed.
This commit is contained in:
Pierre-Marie Padiou 2018-07-25 16:47:38 +02:00 committed by GitHub
parent 924efeabe3
commit 75d23cf1b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 591 additions and 67 deletions

View File

@ -128,11 +128,12 @@ java -Declair.datadir=/tmp/node1 -jar eclair-node-gui-<version>-<commit_id>.jar
connect | nodeId, host, port | open a secure connection to a lightning node
connect | uri | open a secure connection to a lightning node
open | nodeId, fundingSatoshis, pushMsat = 0, feerateSatPerByte = ?, channelFlags = 0x01 | open a channel with another lightning node, by default push = 0, feerate for the funding tx targets 6 blocks, and channel is announced
updaterelayfee | channelId, feeBaseMsat, feeProportionalMillionths | updates the outgoing fee on this channel
updaterelayfee | channelId, feeBaseMsat, feeProportionalMillionths | update relay fee for payments going through this channel
peers | | list existing local peers
channels | | list existing local channels
channels | nodeId | list existing local channels opened with a particular nodeId
channel | channelId | retrieve detailed information about a given channel
channelstats | | retrieves statistics about channel usage (fees, number and average amount of payments)
allnodes | | list all known nodes
allchannels | | list all known channels
allupdates | | list all channels updates
@ -150,6 +151,10 @@ java -Declair.datadir=/tmp/node1 -jar eclair-node-gui-<version>-<commit_id>.jar
close | channelId | close a channel
close | channelId, scriptPubKey | close a channel and send the funds to the given scriptPubKey
forceclose | channelId | force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable)"
audit | | list all send/received/relayed payments
audit | from, to | list send/received/relayed payments in that interval (from <= timestamp < to)
networkfees | | list all network fees paid to the miners, by transaction
networkfees |from, to | list network fees paid to the miners, by transaction, in that interval (from <= timestamp < to)
help | | display available methods
## Docker

View File

@ -82,6 +82,8 @@ case ${METHOD}_${#} in
"channel_"*) call ${METHOD} "'${PARAMS}'" "if .error != null then .error.message else .result | { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceMsat: .data.commitments.localCommit.spec.toLocalMsat, capacitySat: .data.commitments.commitInput.txOut.amount.amount } end" ;;
"channels_"*) call ${METHOD} "'${PARAMS}'" "if .error != null then .error.message else .result | map( { nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceMsat: .data.commitments.localCommit.spec.toLocalMsat, capacitySat: .data.commitments.commitInput.txOut.amount.amount } ) end" ;;
"send_3") call ${METHOD} "'$(printf '[%s,"%s","%s"]' "${1}" "${2}" "${3}")'" ;; # ${1} is numeric (amount of the payment)
"send_2") call ${METHOD} "'$(printf '["%s",%s]' "${1}" "${2}")'" ;; # ${2} is numeric (amount overriding the payment request)

View File

@ -62,6 +62,7 @@ case class NodeParams(keyManager: KeyManager,
networkDb: NetworkDb,
pendingRelayDb: PendingRelayDb,
paymentsDb: PaymentsDb,
auditDb: AuditDb,
routerBroadcastInterval: FiniteDuration,
pingInterval: FiniteDuration,
maxFeerateMismatch: Double,
@ -140,6 +141,9 @@ object NodeParams {
val sqliteNetwork = DriverManager.getConnection(s"jdbc:sqlite:${new File(chaindir, "network.sqlite")}")
val networkDb = new SqliteNetworkDb(sqliteNetwork)
val sqliteAudit = DriverManager.getConnection(s"jdbc:sqlite:${new File(chaindir, "audit.sqlite")}")
val auditDb = new SqliteAuditDb(sqliteAudit)
val color = BinaryData(config.getString("node-color"))
require(color.size == 3, "color should be a 3-bytes hex buffer")
@ -181,6 +185,7 @@ object NodeParams {
networkDb = networkDb,
pendingRelayDb = pendingRelayDb,
paymentsDb = paymentsDb,
auditDb = auditDb,
routerBroadcastInterval = FiniteDuration(config.getDuration("router-broadcast-interval").getSeconds, TimeUnit.SECONDS),
pingInterval = FiniteDuration(config.getDuration("ping-interval").getSeconds, TimeUnit.SECONDS),
maxFeerateMismatch = config.getDouble("max-feerate-mismatch"),

View File

@ -180,6 +180,7 @@ class Setup(datadir: File,
case address => logger.info(s"initial wallet address=$address")
}
audit = system.actorOf(SimpleSupervisor.props(Auditor.props(nodeParams), "auditor", SupervisorStrategy.Resume))
paymentHandler = system.actorOf(SimpleSupervisor.props(config.getString("payment-handler") match {
case "local" => LocalPaymentHandler.props(nodeParams)
case "noop" => Props[NoopPaymentHandler]

View File

@ -20,7 +20,7 @@ import java.net.InetSocketAddress
import com.google.common.net.HostAndPort
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar}
import fr.acinq.bitcoin.{BinaryData, OutPoint, Transaction}
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, OutPoint, Transaction}
import fr.acinq.eclair.channel.State
import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.router.RouteResponse
@ -43,6 +43,10 @@ class UInt64Serializer extends CustomSerializer[UInt64](format => ({ null }, {
case x: UInt64 => JInt(x.toBigInt)
}))
class MilliSatoshiSerializer extends CustomSerializer[MilliSatoshi](format => ({ null }, {
case x: MilliSatoshi => JInt(x.amount)
}))
class ShortChannelIdSerializer extends CustomSerializer[ShortChannelId](format => ({ null }, {
case x: ShortChannelId => JString(x.toString())
}))

View File

@ -39,7 +39,7 @@ import fr.acinq.eclair.channel._
import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo}
import fr.acinq.eclair.io.{NodeURI, Peer}
import fr.acinq.eclair.payment.PaymentLifecycle._
import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentReceived, PaymentRequest}
import fr.acinq.eclair.payment._
import fr.acinq.eclair.router.{ChannelDesc, RouteRequest, RouteResponse}
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement}
import fr.acinq.eclair.{Kit, ShortChannelId, feerateByte2Kw}
@ -57,7 +57,7 @@ case class Error(code: Int, message: String)
case class JsonRPCRes(result: AnyRef, error: Option[Error], id: String)
case class Status(node_id: String)
case class GetInfoResponse(nodeId: PublicKey, alias: String, port: Int, chainHash: BinaryData, blockHeight: Int)
case class LocalChannelInfo(nodeId: BinaryData, channelId: BinaryData, state: String)
case class AuditResponse(sent: Seq[PaymentSent], received: Seq[PaymentReceived], relayed: Seq[PaymentRelayed])
trait RPCRejection extends Rejection {
def requestId: String
}
@ -74,7 +74,7 @@ trait Service extends Logging {
def scheduler: Scheduler
implicit val serialization = jackson.Serialization
implicit val formats = org.json4s.DefaultFormats + new BinaryDataSerializer + new UInt64Serializer + new ShortChannelIdSerializer + new StateSerializer + new ShaChainSerializer + new PublicKeySerializer + new PrivateKeySerializer + new ScalarSerializer + new PointSerializer + new TransactionSerializer + new TransactionWithInputInfoSerializer + new InetSocketAddressSerializer + new OutPointSerializer + new OutPointKeySerializer + new InputInfoSerializer + new ColorSerializer + new RouteResponseSerializer + new ThrowableSerializer + new FailureMessageSerializer + new NodeAddressSerializer + new DirectionSerializer
implicit val formats = org.json4s.DefaultFormats + new BinaryDataSerializer + new UInt64Serializer + new MilliSatoshiSerializer + new ShortChannelIdSerializer + new StateSerializer + new ShaChainSerializer + new PublicKeySerializer + new PrivateKeySerializer + new ScalarSerializer + new PointSerializer + new TransactionSerializer + new TransactionWithInputInfoSerializer + new InetSocketAddressSerializer + new OutPointSerializer + new OutPointKeySerializer + new InputInfoSerializer + new ColorSerializer + new RouteResponseSerializer + new ThrowableSerializer + new FailureMessageSerializer + new NodeAddressSerializer + new DirectionSerializer
implicit val timeout = Timeout(60 seconds)
implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True
@ -191,16 +191,14 @@ trait Service extends Logging {
case Nil =>
val f = for {
channels_id <- (register ? 'channels).mapTo[Map[BinaryData, ActorRef]].map(_.keys)
channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toString(), CMD_GETINFO).mapTo[RES_GETINFO]
.map(gi => LocalChannelInfo(gi.nodeId, gi.channelId, gi.state.toString))))
channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toString(), CMD_GETINFO).mapTo[RES_GETINFO]))
} yield channels
completeRpcFuture(req.id, f)
case JString(remoteNodeId) :: Nil => Try(PublicKey(remoteNodeId)) match {
case Success(pk) =>
val f = for {
channels_id <- (register ? 'channelsTo).mapTo[Map[BinaryData, PublicKey]].map(_.filter(_._2 == pk).keys)
channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toString(), CMD_GETINFO).mapTo[RES_GETINFO]
.map(gi => LocalChannelInfo(gi.nodeId, gi.channelId, gi.state.toString))))
channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toString(), CMD_GETINFO).mapTo[RES_GETINFO]))
} yield channels
completeRpcFuture(req.id, f)
case Failure(_) => reject(RpcValidationRejection(req.id, s"invalid remote node id '$remoteNodeId'"))
@ -308,6 +306,29 @@ trait Service extends Logging {
case _ => reject(UnknownParamsRejection(req.id, "[paymentHash] or [paymentRequest]"))
}
// retrieve audit events
case "audit" =>
val (from, to) = req.params match {
case JInt(from) :: JInt(to) :: Nil => (from.toLong, to.toLong)
case _ => (0L, Long.MaxValue)
}
completeRpcFuture(req.id, Future(AuditResponse(
sent = nodeParams.auditDb.listSent(from, to),
received = nodeParams.auditDb.listReceived(from, to),
relayed = nodeParams.auditDb.listRelayed(from, to))
))
case "networkfees" =>
val (from, to) = req.params match {
case JInt(from) :: JInt(to) :: Nil => (from.toLong, to.toLong)
case _ => (0L, Long.MaxValue)
}
completeRpcFuture(req.id, Future(nodeParams.auditDb.listNetworkFees(from, to)))
// retrieve fee stats
case "channelstats" => completeRpcFuture(req.id, Future(nodeParams.auditDb.stats))
// method name was not found
case _ => reject(UnknownMethodRejection(req.id))
}
@ -345,11 +366,12 @@ trait Service extends Logging {
"connect (uri): open a secure connection to a lightning node",
"connect (nodeId, host, port): open a secure connection to a lightning node",
"open (nodeId, fundingSatoshis, pushMsat = 0, feerateSatPerByte = ?, channelFlags = 0x01): open a channel with another lightning node, by default push = 0, feerate for the funding tx targets 6 blocks, and channel is announced",
"updaterelayfee (channelId, feeBaseMsat, feeProportionalMillionths)",
"updaterelayfee (channelId, feeBaseMsat, feeProportionalMillionths): update relay fee for payments going through this channel",
"peers: list existing local peers",
"channels: list existing local channels",
"channels (nodeId): list existing local channels to a particular nodeId",
"channel (channelId): retrieve detailed information about a given channel",
"channelstats: retrieves statistics about channel usage (fees, number and average amount of payments)",
"allnodes: list all known nodes",
"allchannels: list all known channels",
"allupdates: list all channels updates",
@ -366,6 +388,10 @@ trait Service extends Logging {
"forceclose (channelId): force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable)",
"checkpayment (paymentHash): returns true if the payment has been received, false otherwise",
"checkpayment (paymentRequest): returns true if the payment has been received, false otherwise",
"audit: list all send/received/relayed payments",
"audit (from, to): list send/received/relayed payments in that interval (from <= timestamp < to)",
"networkfees: list all network fees paid to the miners, by transaction",
"networkfees (from, to): list network fees paid to the miners, by transaction, in that interval (from <= timestamp < to)",
"getinfo: returns info about the blockchain and this node",
"help: display this message")

View File

@ -54,4 +54,4 @@ trait EclairWallet {
}
final case class MakeFundingTxResponse(fundingTx: Transaction, fundingTxOutputIndex: Int)
final case class MakeFundingTxResponse(fundingTx: Transaction, fundingTxOutputIndex: Int, fee: Satoshi)

View File

@ -89,13 +89,13 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit system: ActorS
lockTime = 0)
for {
// we ask bitcoin core to add inputs to the funding tx, and use the specified change address
FundTransactionResponse(unsignedFundingTx, changepos, fee) <- fundTransaction(partialFundingTx, lockUnspents = true)
FundTransactionResponse(unsignedFundingTx, _, fee) <- fundTransaction(partialFundingTx, lockUnspents = true)
// now let's sign the funding tx
SignTransactionResponse(fundingTx, _) <- signTransactionOrUnlock(unsignedFundingTx)
// there will probably be a change output, so we need to find which output is ours
outputIndex = Transactions.findPubKeyScriptIndex(fundingTx, pubkeyScript, outputsAlreadyUsed = Set.empty, amount_opt = None)
_ = logger.debug(s"created funding txid=${fundingTx.txid} outputIndex=$outputIndex fee=$fee")
} yield MakeFundingTxResponse(fundingTx, outputIndex)
} yield MakeFundingTxResponse(fundingTx, outputIndex, fee)
}
override def commit(tx: Transaction): Future[Boolean] = publishTransaction(tx)

View File

@ -38,8 +38,8 @@ class ElectrumEclairWallet(val wallet: ActorRef, chainHash: BinaryData)(implicit
override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long) = {
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(amount, pubkeyScript) :: Nil, lockTime = 0)
(wallet ? CompleteTransaction(tx, feeRatePerKw)).mapTo[CompleteTransactionResponse].map(response => response match {
case CompleteTransactionResponse(tx1, None) => MakeFundingTxResponse(tx1, 0)
case CompleteTransactionResponse(_, Some(error)) => throw error
case CompleteTransactionResponse(tx1, fee1, None) => MakeFundingTxResponse(tx1, 0, fee1) // TODO: output index is always 0?
case CompleteTransactionResponse(_, _, Some(error)) => throw error
})
}
@ -71,11 +71,11 @@ class ElectrumEclairWallet(val wallet: ActorRef, chainHash: BinaryData)(implicit
(wallet ? CompleteTransaction(tx, feeRatePerKw))
.mapTo[CompleteTransactionResponse]
.flatMap {
case CompleteTransactionResponse(tx, None) => commit(tx).map {
case CompleteTransactionResponse(tx, _, None) => commit(tx).map {
case true => tx.txid.toString()
case false => throw new RuntimeException(s"could not commit tx=$tx")
}
case CompleteTransactionResponse(_, Some(error)) => throw error
case CompleteTransactionResponse(_, _, Some(error)) => throw error
}
}

View File

@ -223,8 +223,8 @@ class ElectrumWallet(seed: BinaryData, client: ActorRef, params: ElectrumWallet.
case Event(CompleteTransaction(tx, feeRatePerKw), data) =>
Try(data.completeTransaction(tx, feeRatePerKw, minimumFee, dustLimit, allowSpendUnconfirmed)) match {
case Success((data1, tx1)) => stay using data1 replying CompleteTransactionResponse(tx1, None)
case Failure(t) => stay replying CompleteTransactionResponse(tx, Some(t))
case Success((data1, tx1, fee1)) => stay using data1 replying CompleteTransactionResponse(tx1, fee1, None)
case Failure(t) => stay replying CompleteTransactionResponse(tx, Satoshi(0), Some(t))
}
case Event(SendAll(publicKeyScript, feeRatePerKw), data) =>
@ -309,7 +309,7 @@ object ElectrumWallet {
case class GetDataResponse(state: Data) extends Response
case class CompleteTransaction(tx: Transaction, feeRatePerKw: Long) extends Request
case class CompleteTransactionResponse(tx: Transaction, error: Option[Throwable]) extends Response
case class CompleteTransactionResponse(tx: Transaction, fee: Satoshi, error: Option[Throwable]) extends Response
case class SendAll(publicKeyScript: BinaryData, feeRatePerKw: Long) extends Request
case class SendAllResponse(tx: Transaction, fee: Satoshi) extends Response
@ -715,12 +715,12 @@ object ElectrumWallet {
* @param feeRatePerKw fee rate per kiloweight
* @param minimumFee minimum fee
* @param dustLimit dust limit
* @return a (state, tx) tuple where state has been updated and tx is a complete,
* @return a (state, tx, fee) tuple where state has been updated and tx is a complete,
* fully signed transaction that can be broadcast.
* our utxos spent by this tx are locked and won't be available for spending
* until the tx has been cancelled. If the tx is committed, they will be removed
*/
def completeTransaction(tx: Transaction, feeRatePerKw: Long, minimumFee: Satoshi, dustLimit: Satoshi, allowSpendUnconfirmed: Boolean): (Data, Transaction) = {
def completeTransaction(tx: Transaction, feeRatePerKw: Long, minimumFee: Satoshi, dustLimit: Satoshi, allowSpendUnconfirmed: Boolean): (Data, Transaction, Satoshi) = {
require(tx.txIn.isEmpty, "cannot complete a tx that already has inputs")
require(feeRatePerKw >= 0, "fee rate cannot be negative")
val amount = tx.txOut.map(_.amount).sum
@ -752,8 +752,9 @@ object ElectrumWallet {
// and add the completed tx to the lokcs
val data1 = this.copy(locks = this.locks + tx3)
val fee3 = spent - tx3.txOut.map(_.amount).sum
(data1, tx3)
(data1, tx3, fee3)
}
def signTransaction(tx: Transaction) : Transaction = {

View File

@ -289,7 +289,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
})
when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions {
case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex), data@DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, open)) =>
case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, open)) =>
// let's create the first commitment tx that spends the yet uncommitted funding tx
val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.maxFeerateMismatch)
require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!")
@ -305,7 +305,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
context.parent ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages
context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId))
// NB: we don't send a ChannelSignatureSent for the first commit
goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), open.channelFlags, fundingCreated) sending fundingCreated
goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, fundingTxFee, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), open.channelFlags, fundingCreated) sending fundingCreated
case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) =>
log.error(t, s"wallet returned error: ")
@ -369,7 +369,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
})
when(WAIT_FOR_FUNDING_SIGNED)(handleExceptions {
case Event(msg@FundingSigned(_, remoteSig), d@DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, localSpec, localCommitTx, remoteCommit, channelFlags, fundingCreated)) =>
case Event(msg@FundingSigned(_, remoteSig), d@DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, channelFlags, fundingCreated)) =>
// we make sure that their sig checks out and that our first commit tx is spendable
val localSigOfLocalTx = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath))
val signedLocalCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig)
@ -397,6 +397,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
log.info(s"committing txid=${fundingTx.txid}")
wallet.commit(fundingTx).onComplete {
case Success(true) =>
// NB: funding tx isn't confirmed at this point, so technically we didn't really pay the network fee yet, so this is a (fair) approximation
feePaid(fundingTxFee, fundingTx, "funding", commitments.channelId)
replyToUser(Right(s"created channel $channelId"))
case Success(false) =>
replyToUser(Left(LocalError(new RuntimeException("couldn't publish funding tx"))))
@ -1147,7 +1149,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Event(WatchEventConfirmed(BITCOIN_TX_CONFIRMED(tx), _, _), d: DATA_CLOSING) =>
log.info(s"txid=${tx.txid} has reached mindepth, updating closing state")
// first we check if this tx belongs to a one of the current local/remote commits and update it
// first we check if this tx belongs to one of the current local/remote commits and update it
val localCommitPublished1 = d.localCommitPublished.map(Closing.updateLocalCommitPublished(_, tx))
val remoteCommitPublished1 = d.remoteCommitPublished.map(Closing.updateRemoteCommitPublished(_, tx))
val nextRemoteCommitPublished1 = d.nextRemoteCommitPublished.map(Closing.updateRemoteCommitPublished(_, tx))
@ -1172,6 +1174,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
val revokedCommitDone = revokedCommitPublished1.map(Closing.isRevokedCommitDone(_)).exists(_ == true) // we only need one revoked commit done
// finally, if one of the unilateral closes is done, we move to CLOSED state, otherwise we stay (note that we don't store the state)
val d1 = d.copy(localCommitPublished = localCommitPublished1, remoteCommitPublished = remoteCommitPublished1, nextRemoteCommitPublished = nextRemoteCommitPublished1, futureRemoteCommitPublished = futureRemoteCommitPublished1, revokedCommitPublished = revokedCommitPublished1)
// we also send events related to fee
Closing.networkFeePaid(tx, d1) map { case (fee, desc) => feePaid(fee, tx, desc, d.channelId) }
val closeType_opt = if (mutualCloseDone) {
Some("mutual")
} else if (localCommitDone) {
@ -1219,7 +1223,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
log.info("shutting down")
stop(FSM.Normal)
case Event(MakeFundingTxResponse(fundingTx, _), _) =>
case Event(MakeFundingTxResponse(fundingTx, _, _), _) =>
// this may happen if connection is lost, or remote sends an error while we were waiting for the funding tx to be created by our wallet
// in that case we rollback the tx
wallet.rollback(fundingTx)
@ -1419,7 +1423,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Event(c@CMD_FORCECLOSE, d) =>
d match {
case data: HasCommitments => handleLocalError(ForcedLocalCommit(data.channelId, "forced local commit"), data, Some(c)) replying "ok"
case data: HasCommitments => handleLocalError(ForcedLocalCommit(data.channelId), data, Some(c)) replying "ok"
case _ => handleCommandError(CannotCloseInThisState(Helpers.getChannelId(d), stateName), c)
}
@ -1854,6 +1858,11 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Some(u) => Relayed(u.channelId, u.id, u.amountMsat, c.amountMsat)
}
def feePaid(fee: Satoshi, tx: Transaction, desc: String, channelId: BinaryData) = {
log.info(s"paid feeSatoshi=${fee.amount} for txid=${tx.txid} desc=$desc")
context.system.eventStream.publish(NetworkFeePaid(self, remoteNodeId, channelId, tx, fee, desc))
}
def store[T](d: T)(implicit tp: T <:< HasCommitments): T = {
log.debug(s"updating database record for channelId=${d.channelId}")
nodeParams.channelsDb.addOrUpdateChannel(d)

View File

@ -17,8 +17,8 @@
package fr.acinq.eclair.channel
import akka.actor.ActorRef
import fr.acinq.bitcoin.BinaryData
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{BinaryData, Satoshi, Transaction}
import fr.acinq.eclair.ShortChannelId
import fr.acinq.eclair.channel.Channel.ChannelError
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate}
@ -47,7 +47,9 @@ case class ChannelSignatureSent(channel: ActorRef, commitments: Commitments) ext
case class ChannelSignatureReceived(channel: ActorRef, commitments: Commitments) extends ChannelEvent
case class ChannelFailed(channel: ActorRef, channelId: BinaryData, remoteNodeId: PublicKey, data: Data, error: ChannelError) extends ChannelEvent
case class NetworkFeePaid(channel: ActorRef, remoteNodeId: PublicKey, channelId: BinaryData, tx: Transaction, fee: Satoshi, txType: String) extends ChannelEvent
// NB: this event is only sent when the channel is available
case class AvailableBalanceChanged(channel: ActorRef, channelId: BinaryData, shortChannelId: ShortChannelId, localBalanceMsat: Long) extends ChannelEvent
case class ChannelFailed(channel: ActorRef, channelId: BinaryData, remoteNodeId: PublicKey, data: Data, error: ChannelError) extends ChannelEvent

View File

@ -56,7 +56,7 @@ case class InvalidHtlcSignature (override val channelId: BinaryDa
case class InvalidCloseSignature (override val channelId: BinaryData, tx: Transaction) extends ChannelException(channelId, s"invalid close signature: tx=$tx")
case class InvalidCloseFee (override val channelId: BinaryData, feeSatoshi: Long) extends ChannelException(channelId, s"invalid close fee: fee_satoshis=$feeSatoshi")
case class HtlcSigCountMismatch (override val channelId: BinaryData, expected: Int, actual: Int) extends ChannelException(channelId, s"htlc sig count mismatch: expected=$expected actual: $actual")
case class ForcedLocalCommit (override val channelId: BinaryData, reason: String) extends ChannelException(channelId, s"forced local commit: reason")
case class ForcedLocalCommit (override val channelId: BinaryData) extends ChannelException(channelId, s"forced local commit")
case class UnexpectedHtlcId (override val channelId: BinaryData, expected: Long, actual: Long) extends ChannelException(channelId, s"unexpected htlc id: expected=$expected actual=$actual")
case class InvalidPaymentHash (override val channelId: BinaryData) extends ChannelException(channelId, "invalid payment hash")
case class ExpiryTooSmall (override val channelId: BinaryData, minimum: Long, actual: Long, blockCount: Long) extends ChannelException(channelId, s"expiry too small: minimum=$minimum actual=$actual blockCount=$blockCount")

View File

@ -18,7 +18,7 @@ package fr.acinq.eclair.channel
import akka.actor.ActorRef
import fr.acinq.bitcoin.Crypto.{Point, PublicKey}
import fr.acinq.bitcoin.{BinaryData, DeterministicWallet, OutPoint, Transaction}
import fr.acinq.bitcoin.{BinaryData, DeterministicWallet, OutPoint, Satoshi, Transaction}
import fr.acinq.eclair.{ShortChannelId, UInt64}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.transactions.CommitmentSpec
@ -149,7 +149,7 @@ final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_FUNDEE) exten
final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_FUNDER, lastSent: OpenChannel) extends Data
final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: BinaryData, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, remoteFirstPerCommitmentPoint: Point, lastSent: OpenChannel) extends Data
final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: BinaryData, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, remoteFirstPerCommitmentPoint: Point, channelFlags: Byte, lastSent: AcceptChannel) extends Data
final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: BinaryData, localParams: LocalParams, remoteParams: RemoteParams, fundingTx: Transaction, localSpec: CommitmentSpec, localCommitTx: CommitTx, remoteCommit: RemoteCommit, channelFlags: Byte, lastSent: FundingCreated) extends Data
final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: BinaryData, localParams: LocalParams, remoteParams: RemoteParams, fundingTx: Transaction, fundingTxFee: Satoshi, localSpec: CommitmentSpec, localCommitTx: CommitTx, remoteCommit: RemoteCommit, channelFlags: Byte, lastSent: FundingCreated) extends Data
final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments, deferred: Option[FundingLocked], lastSent: Either[FundingCreated, FundingSigned]) extends Data with HasCommitments
final case class DATA_WAIT_FOR_FUNDING_LOCKED(commitments: Commitments, shortChannelId: ShortChannelId, lastSent: FundingLocked) extends Data with HasCommitments
final case class DATA_NORMAL(commitments: Commitments,

View File

@ -511,7 +511,7 @@ object Helpers {
// and finally we steal the htlc outputs
var outputsAlreadyUsed = Set.empty[Int] // this is needed to handle cases where we have several identical htlcs
val htlcPenaltyTxs = tx.txOut.collect { case txOut if htlcsRedeemScripts.contains(txOut.publicKeyScript) =>
val htlcPenaltyTxs = tx.txOut.collect { case txOut if htlcsRedeemScripts.contains(txOut.publicKeyScript) =>
val htlcRedeemScript = htlcsRedeemScripts(txOut.publicKeyScript)
generateTx("htlc-penalty")(Try {
val htlcPenalty = Transactions.makeHtlcPenaltyTx(tx, outputsAlreadyUsed, htlcRedeemScript, Satoshi(localParams.dustLimitSatoshis), localParams.defaultFinalScriptPubKey, feeratePerKwPenalty)
@ -833,6 +833,62 @@ object Helpers {
irrevocablySpent.contains(outPoint)
}
/**
* This helper function returns the fee paid by the given transaction.
*
* It relies on the current channel data to find the parent tx and compute the fee, and also provides a description.
*
* @param tx a tx for which we want to compute the fee
* @param d current channel data
* @return if the parent tx is found, a tuple (fee, description)
*/
def networkFeePaid(tx: Transaction, d: DATA_CLOSING): Option[(Satoshi, String)] = {
// only funder pays the fee
if (d.commitments.localParams.isFunder) {
// we build a map with all known txes (that's not particularly efficient, but it doesn't really matter)
val txes: Map[BinaryData, (Transaction, String)] = (
d.mutualClosePublished.map(_ -> "mutual") ++
d.localCommitPublished.map(_.commitTx).map(_ -> "local-commit").toSeq ++
d.localCommitPublished.flatMap(_.claimMainDelayedOutputTx).map(_ -> "local-main-delayed") ++
d.localCommitPublished.toSeq.flatMap(_.htlcSuccessTxs).map(_ -> "local-htlc-success") ++
d.localCommitPublished.toSeq.flatMap(_.htlcTimeoutTxs).map(_ -> "local-htlc-timeout") ++
d.localCommitPublished.toSeq.flatMap(_.claimHtlcDelayedTxs).map(_ -> "local-htlc-delayed") ++
d.remoteCommitPublished.map(_.commitTx).map(_ -> "remote-commit") ++
d.remoteCommitPublished.toSeq.flatMap(_.claimMainOutputTx).map(_ -> "remote-main") ++
d.remoteCommitPublished.toSeq.flatMap(_.claimHtlcSuccessTxs).map(_ -> "remote-htlc-success") ++
d.remoteCommitPublished.toSeq.flatMap(_.claimHtlcTimeoutTxs).map(_ -> "remote-htlc-timeout") ++
d.nextRemoteCommitPublished.map(_.commitTx).map(_ -> "remote-commit") ++
d.nextRemoteCommitPublished.toSeq.flatMap(_.claimMainOutputTx).map(_ -> "remote-main") ++
d.nextRemoteCommitPublished.toSeq.flatMap(_.claimHtlcSuccessTxs).map(_ -> "remote-htlc-success") ++
d.nextRemoteCommitPublished.toSeq.flatMap(_.claimHtlcTimeoutTxs).map(_ -> "remote-htlc-timeout") ++
d.revokedCommitPublished.map(_.commitTx).map(_ -> "revoked-commit") ++
d.revokedCommitPublished.flatMap(_.claimMainOutputTx).map(_ -> "revoked-main") ++
d.revokedCommitPublished.flatMap(_.mainPenaltyTx).map(_ -> "revoked-main-penalty") ++
d.revokedCommitPublished.flatMap(_.htlcPenaltyTxs).map(_ -> "revoked-htlc-penalty") ++
d.revokedCommitPublished.flatMap(_.claimHtlcDelayedPenaltyTxs).map(_ -> "revoked-htlc-penalty-delayed")
)
.map { case (tx, desc) => tx.txid -> (tx, desc) } // will allow easy lookup of parent transaction
.toMap
def fee(child: Transaction): Option[Satoshi] = {
require(child.txIn.size == 1, "transaction must have exactly one input")
val outPoint = child.txIn.head.outPoint
val parentTxOut_opt = if (outPoint == d.commitments.commitInput.outPoint) {
Some(d.commitments.commitInput.txOut)
}
else {
txes.get(outPoint.txid) map { case (parent, _) => parent.txOut(outPoint.index.toInt) }
}
parentTxOut_opt map {
case parentTxOut => parentTxOut.amount - child.txOut.map(_.amount).sum
}
}
txes.get(tx.txid) flatMap {
case (_, desc) => fee(tx).map(_ -> desc)
}
} else None
}
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright 2018 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.db
import fr.acinq.bitcoin.BinaryData
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.channel.NetworkFeePaid
import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent}
trait AuditDb {
def add(paymentSent: PaymentSent)
def add(paymentReceived: PaymentReceived)
def add(paymentRelayed: PaymentRelayed)
def add(networkFeePaid: NetworkFeePaid)
def listSent(from: Long, to: Long): Seq[PaymentSent]
def listReceived(from: Long, to: Long): Seq[PaymentReceived]
def listRelayed(from: Long, to: Long): Seq[PaymentRelayed]
def listNetworkFees(from: Long, to: Long): Seq[NetworkFee]
def stats: Seq[Stats]
}
case class NetworkFee(remoteNodeId: PublicKey, channelId: BinaryData, txId: BinaryData, feeSat: Long, txType: String, timestamp: Long)
case class Stats(channelId: BinaryData, avgPaymentAmountSatoshi: Long, paymentCount: Int, relayFeeSatoshi: Long, networkFeeSatoshi: Long)

View File

@ -0,0 +1,204 @@
/*
* Copyright 2018 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.db.sqlite
import java.sql.Connection
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, Satoshi}
import fr.acinq.eclair.channel.NetworkFeePaid
import fr.acinq.eclair.db.{AuditDb, NetworkFee, Stats}
import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent}
import scala.collection.immutable.Queue
import scala.compat.Platform
class SqliteAuditDb(sqlite: Connection) extends AuditDb {
import SqliteUtils._
val DB_NAME = "audit"
val CURRENT_VERSION = 1
using(sqlite.createStatement()) { statement =>
require(getVersion(statement, DB_NAME, CURRENT_VERSION) == CURRENT_VERSION) // there is only one version currently deployed
statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent (amount_msat INTEGER NOT NULL, fees_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, payment_preimage BLOB NOT NULL, to_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)")
statement.executeUpdate("CREATE TABLE IF NOT EXISTS received (amount_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, from_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)")
statement.executeUpdate("CREATE TABLE IF NOT EXISTS relayed (amount_in_msat INTEGER NOT NULL, amount_out_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, from_channel_id BLOB NOT NULL, to_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)")
statement.executeUpdate("CREATE TABLE IF NOT EXISTS network_fees (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, tx_id BLOB NOT NULL, fee_sat INTEGER NOT NULL, tx_type TEXT NOT NULL, timestamp INTEGER NOT NULL)")
statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_timestamp_idx ON sent(timestamp)")
statement.executeUpdate("CREATE INDEX IF NOT EXISTS received_timestamp_idx ON received(timestamp)")
statement.executeUpdate("CREATE INDEX IF NOT EXISTS relayed_timestamp_idx ON relayed(timestamp)")
statement.executeUpdate("CREATE INDEX IF NOT EXISTS network_fees_timestamp_idx ON network_fees(timestamp)")
}
override def add(e: PaymentSent): Unit =
using(sqlite.prepareStatement("INSERT INTO sent VALUES (?, ?, ?, ?, ?, ?)")) { statement =>
statement.setLong(1, e.amount.toLong)
statement.setLong(2, e.feesPaid.toLong)
statement.setBytes(3, e.paymentHash)
statement.setBytes(4, e.paymentPreimage)
statement.setBytes(5, e.toChannelId)
statement.setLong(6, e.timestamp)
statement.executeUpdate()
}
override def add(e: PaymentReceived): Unit =
using(sqlite.prepareStatement("INSERT INTO received VALUES (?, ?, ?, ?)")) { statement =>
statement.setLong(1, e.amount.toLong)
statement.setBytes(2, e.paymentHash)
statement.setBytes(3, e.fromChannelId)
statement.setLong(4, e.timestamp)
statement.executeUpdate()
}
override def add(e: PaymentRelayed): Unit =
using(sqlite.prepareStatement("INSERT INTO relayed VALUES (?, ?, ?, ?, ?, ?)")) { statement =>
statement.setLong(1, e.amountIn.toLong)
statement.setLong(2, e.amountOut.toLong)
statement.setBytes(3, e.paymentHash)
statement.setBytes(4, e.fromChannelId)
statement.setBytes(5, e.toChannelId)
statement.setLong(6, e.timestamp)
statement.executeUpdate()
}
override def add(e: NetworkFeePaid): Unit =
using(sqlite.prepareStatement("INSERT INTO network_fees VALUES (?, ?, ?, ?, ?, ?)")) { statement =>
statement.setBytes(1, e.channelId)
statement.setBytes(2, e.remoteNodeId.toBin)
statement.setBytes(3, e.tx.txid)
statement.setLong(4, e.fee.toLong)
statement.setString(5, e.txType)
statement.setLong(6, Platform.currentTime)
statement.executeUpdate()
}
override def listSent(from: Long, to: Long): Seq[PaymentSent] =
using(sqlite.prepareStatement("SELECT * FROM sent WHERE timestamp >= ? AND timestamp < ?")) { statement =>
statement.setLong(1, from)
statement.setLong(2, to)
val rs = statement.executeQuery()
var q: Queue[PaymentSent] = Queue()
while (rs.next()) {
q = q :+ PaymentSent(
amount = MilliSatoshi(rs.getLong("amount_msat")),
feesPaid = MilliSatoshi(rs.getLong("fees_msat")),
paymentHash = BinaryData(rs.getBytes("payment_hash")),
paymentPreimage = BinaryData(rs.getBytes("payment_preimage")),
toChannelId = BinaryData(rs.getBytes("to_channel_id")),
timestamp = rs.getLong("timestamp"))
}
q
}
override def listReceived(from: Long, to: Long): Seq[PaymentReceived] =
using(sqlite.prepareStatement("SELECT * FROM received WHERE timestamp >= ? AND timestamp < ?")) { statement =>
statement.setLong(1, from)
statement.setLong(2, to)
val rs = statement.executeQuery()
var q: Queue[PaymentReceived] = Queue()
while (rs.next()) {
q = q :+ PaymentReceived(
amount = MilliSatoshi(rs.getLong("amount_msat")),
paymentHash = BinaryData(rs.getBytes("payment_hash")),
fromChannelId = BinaryData(rs.getBytes("from_channel_id")),
timestamp = rs.getLong("timestamp"))
}
q
}
override def listRelayed(from: Long, to: Long): Seq[PaymentRelayed] =
using(sqlite.prepareStatement("SELECT * FROM relayed WHERE timestamp >= ? AND timestamp < ?")) { statement =>
statement.setLong(1, from)
statement.setLong(2, to)
val rs = statement.executeQuery()
var q: Queue[PaymentRelayed] = Queue()
while (rs.next()) {
q = q :+ PaymentRelayed(
amountIn = MilliSatoshi(rs.getLong("amount_in_msat")),
amountOut = MilliSatoshi(rs.getLong("amount_out_msat")),
paymentHash = BinaryData(rs.getBytes("payment_hash")),
fromChannelId = BinaryData(rs.getBytes("from_channel_id")),
toChannelId = BinaryData(rs.getBytes("to_channel_id")),
timestamp = rs.getLong("timestamp"))
}
q
}
override def listNetworkFees(from: Long, to: Long): Seq[NetworkFee] =
using(sqlite.prepareStatement("SELECT * FROM network_fees WHERE timestamp >= ? AND timestamp < ?")) { statement =>
statement.setLong(1, from)
statement.setLong(2, to)
val rs = statement.executeQuery()
var q: Queue[NetworkFee] = Queue()
while (rs.next()) {
q = q :+ NetworkFee(
remoteNodeId = PublicKey(rs.getBytes("node_id")),
channelId = BinaryData(rs.getBytes("channel_id")),
txId = BinaryData(rs.getBytes("tx_id")),
feeSat = rs.getLong("fee_sat"),
txType = rs.getString("tx_type"),
timestamp = rs.getLong("timestamp"))
}
q
}
override def stats: Seq[Stats] =
using(sqlite.createStatement()) { statement =>
val rs = statement.executeQuery(
"""
|SELECT
| channel_id,
| sum(avg_payment_amount_sat) AS avg_payment_amount_sat,
| sum(payment_count) AS payment_count,
| sum(relay_fee_sat) AS relay_fee_sat,
| sum(network_fee_sat) AS network_fee_sat
|FROM (
| SELECT
| to_channel_id AS channel_id,
| avg(amount_out_msat) / 1000 AS avg_payment_amount_sat,
| count(*) AS payment_count,
| sum(amount_in_msat - amount_out_msat) / 1000 AS relay_fee_sat,
| 0 AS network_fee_sat
| FROM relayed
| GROUP BY 1
| UNION
| SELECT
| channel_id,
| 0 AS avg_payment_amount_sat,
| 0 AS payment_count,
| 0 AS relay_fee_sat,
| sum(fee_sat) AS network_fee_sat
| FROM network_fees
| GROUP BY 1
|)
|GROUP BY 1
""".stripMargin)
var q: Queue[Stats] = Queue()
while (rs.next()) {
q = q :+ Stats(
channelId = BinaryData(rs.getBytes("channel_id")),
avgPaymentAmountSatoshi = rs.getLong("avg_payment_amount_sat"),
paymentCount = rs.getInt("payment_count"),
relayFeeSatoshi = rs.getLong("relay_fee_sat"),
networkFeeSatoshi = rs.getLong("network_fee_sat"))
}
q
}
}

View File

@ -22,7 +22,6 @@ import fr.acinq.bitcoin.BinaryData
import fr.acinq.eclair.channel.HasCommitments
import fr.acinq.eclair.db.ChannelsDb
import fr.acinq.eclair.wire.ChannelCodecs.stateDataCodec
import scodec.bits.BitVector
import scala.collection.immutable.Queue

View File

@ -63,7 +63,7 @@ package object eclair {
/**
*
* @param feeratesPerKw fee rate in satoshiper-kw
* @param feeratesPerKw fee rate in satoshi-per-kw
* @return fee rate in satoshi-per-byte
*/
def feerateKw2Byte(feeratesPerKw: Long): Long = feeratesPerKw / 250

View File

@ -0,0 +1,49 @@
/*
* Copyright 2018 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.payment
import akka.actor.{Actor, ActorLogging, Props}
import fr.acinq.eclair.NodeParams
import fr.acinq.eclair.channel.NetworkFeePaid
class Auditor(nodeParams: NodeParams) extends Actor with ActorLogging {
val db = nodeParams.auditDb
context.system.eventStream.subscribe(self, classOf[PaymentEvent])
context.system.eventStream.subscribe(self, classOf[NetworkFeePaid])
override def receive: Receive = {
case e: PaymentSent => db.add(e)
case e: PaymentReceived => db.add(e)
case e: PaymentRelayed => db.add(e)
case e: NetworkFeePaid => db.add(e)
}
override def unhandled(message: Any): Unit = log.warning(s"unhandled msg=$message")
}
object Auditor {
def props(nodeParams: NodeParams) = Props(classOf[Auditor], nodeParams)
}

View File

@ -89,7 +89,7 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin
// amount is correct or was not specified in the payment request
nodeParams.paymentsDb.addPayment(Payment(htlc.paymentHash, htlc.amountMsat, Platform.currentTime / 1000))
sender ! CMD_FULFILL_HTLC(htlc.id, paymentPreimage, commit = true)
context.system.eventStream.publish(PaymentReceived(MilliSatoshi(htlc.amountMsat), htlc.paymentHash))
context.system.eventStream.publish(PaymentReceived(MilliSatoshi(htlc.amountMsat), htlc.paymentHash, htlc.channelId))
context.become(run(hash2preimage - htlc.paymentHash))
}
case None =>

View File

@ -18,6 +18,8 @@ package fr.acinq.eclair.payment
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi}
import scala.compat.Platform
/**
* Created by PM on 01/02/2017.
*/
@ -25,8 +27,8 @@ sealed trait PaymentEvent {
val paymentHash: BinaryData
}
case class PaymentSent(amount: MilliSatoshi, feesPaid: MilliSatoshi, paymentHash: BinaryData, paymentPreimage: BinaryData) extends PaymentEvent
case class PaymentSent(amount: MilliSatoshi, feesPaid: MilliSatoshi, paymentHash: BinaryData, paymentPreimage: BinaryData, toChannelId: BinaryData, timestamp: Long = Platform.currentTime) extends PaymentEvent
case class PaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: BinaryData) extends PaymentEvent
case class PaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: BinaryData, fromChannelId: BinaryData, toChannelId: BinaryData, timestamp: Long = Platform.currentTime) extends PaymentEvent
case class PaymentReceived(amount: MilliSatoshi, paymentHash: BinaryData) extends PaymentEvent
case class PaymentReceived(amount: MilliSatoshi, paymentHash: BinaryData, fromChannelId: BinaryData, timestamp: Long = Platform.currentTime) extends PaymentEvent

View File

@ -74,7 +74,7 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto
case Event(fulfill: UpdateFulfillHtlc, WaitingForComplete(s, c, cmd, _, _, _, _, hops)) =>
reply(s, PaymentSucceeded(cmd.amountMsat, c.paymentHash, fulfill.paymentPreimage, hops))
context.system.eventStream.publish(PaymentSent(MilliSatoshi(c.amountMsat), MilliSatoshi(cmd.amountMsat - c.amountMsat), cmd.paymentHash, fulfill.paymentPreimage))
context.system.eventStream.publish(PaymentSent(MilliSatoshi(c.amountMsat), MilliSatoshi(cmd.amountMsat - c.amountMsat), cmd.paymentHash, fulfill.paymentPreimage, fulfill.channelId))
stop(FSM.Normal)
case Event(fail: UpdateFailHtlc, WaitingForComplete(s, c, _, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops)) =>

View File

@ -148,16 +148,18 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR
}
case ForwardFulfill(fulfill, Local(None), add) =>
// we sent the payment, but we probably restarted and the reference to the original sender was lost, we just publish the failure on the event stream
context.system.eventStream.publish(PaymentSucceeded(add.amountMsat, add.paymentHash, fulfill.paymentPreimage, Nil))
val feesPaid = MilliSatoshi(0)
context.system.eventStream.publish(PaymentSent(MilliSatoshi(add.amountMsat), feesPaid, add.paymentHash, fulfill.paymentPreimage, fulfill.channelId))
// we sent the payment, but we probably restarted and the reference to the original sender was lost, we just publish the success on the event stream
context.system.eventStream.publish(PaymentSucceeded(add.amountMsat, add.paymentHash, fulfill.paymentPreimage, Nil)) //
case ForwardFulfill(fulfill, Local(Some(sender)), _) =>
sender ! fulfill
case ForwardFulfill(fulfill, Relayed(originChannelId, originHtlcId, amountMsatIn, amountMsatOut), _) =>
case ForwardFulfill(fulfill, Relayed(originChannelId, originHtlcId, amountMsatIn, amountMsatOut), add) =>
val cmd = CMD_FULFILL_HTLC(originHtlcId, fulfill.paymentPreimage, commit = true)
commandBuffer ! CommandBuffer.CommandSend(originChannelId, originHtlcId, cmd)
context.system.eventStream.publish(PaymentRelayed(MilliSatoshi(amountMsatIn), MilliSatoshi(amountMsatOut), Crypto.sha256(fulfill.paymentPreimage)))
context.system.eventStream.publish(PaymentRelayed(MilliSatoshi(amountMsatIn), MilliSatoshi(amountMsatOut), add.paymentHash, fromChannelId = originChannelId, toChannelId = fulfill.channelId))
case ForwardFail(_, Local(None), add) =>
// we sent the payment, but we probably restarted and the reference to the original sender was lost, we just publish the failure on the event stream

View File

@ -22,6 +22,7 @@ import java.sql.DriverManager
import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.{BinaryData, Block, Script}
import fr.acinq.eclair.NodeParams.BITCOIND
import fr.acinq.eclair.TestConstants.Alice.sqlite
import fr.acinq.eclair.crypto.LocalKeyManager
import fr.acinq.eclair.db.sqlite._
import fr.acinq.eclair.io.Peer
@ -69,6 +70,7 @@ object TestConstants {
networkDb = new SqliteNetworkDb(sqlite),
pendingRelayDb = new SqlitePendingRelayDb(sqlite),
paymentsDb = new SqlitePaymentsDb(sqlite),
auditDb = new SqliteAuditDb(sqlite),
routerBroadcastInterval = 60 seconds,
pingInterval = 30 seconds,
maxFeerateMismatch = 1.5,
@ -123,6 +125,7 @@ object TestConstants {
networkDb = new SqliteNetworkDb(sqlite),
pendingRelayDb = new SqlitePendingRelayDb(sqlite),
paymentsDb = new SqlitePaymentsDb(sqlite),
auditDb = new SqliteAuditDb(sqlite),
routerBroadcastInterval = 60 seconds,
pingInterval = 30 seconds,
maxFeerateMismatch = 1.0,

View File

@ -45,7 +45,7 @@ object TestWallet {
txIn = TxIn(OutPoint("42" * 32, 42), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil,
txOut = TxOut(amount, pubkeyScript) :: Nil,
lockTime = 0)
MakeFundingTxResponse(fundingTx, 0)
MakeFundingTxResponse(fundingTx, 0, Satoshi(420))
}
def malleateTx(tx: Transaction): Transaction = {

View File

@ -117,7 +117,7 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe
val fundingTxes = for (i <- 0 to 3) yield {
val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey)))
wallet.makeFundingTx(pubkeyScript, MilliBtc(50), 10000).pipeTo(sender.ref)
val MakeFundingTxResponse(fundingTx, _) = sender.expectMsgType[MakeFundingTxResponse]
val MakeFundingTxResponse(fundingTx, _, _) = sender.expectMsgType[MakeFundingTxResponse]
fundingTx
}
@ -187,7 +187,7 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe
sender.expectMsgType[JValue]
wallet.makeFundingTx(pubkeyScript, MilliBtc(50), 10000).pipeTo(sender.ref)
val MakeFundingTxResponse(fundingTx, _) = sender.expectMsgType[MakeFundingTxResponse]
val MakeFundingTxResponse(fundingTx, _, _) = sender.expectMsgType[MakeFundingTxResponse]
wallet.commit(fundingTx).pipeTo(sender.ref)
assert(sender.expectMsgType[Boolean])

View File

@ -97,8 +97,9 @@ class ElectrumWalletBasicSpec extends FunSuite {
val pub = PrivateKey(BinaryData("01" * 32), compressed = true).publicKey
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(0.5 btc, Script.pay2pkh(pub)) :: Nil, lockTime = 0)
val (state2, tx1) = state1.completeTransaction(tx, feeRatePerKw, minimumFee, dustLimit, false)
val (state2, tx1, fee1) = state1.completeTransaction(tx, feeRatePerKw, minimumFee, dustLimit, false)
val Some((_, _, Some(fee))) = state2.computeTransactionDelta(tx1)
assert(fee == fee1)
val actualFeeRate = Transactions.fee2rate(fee, tx1.weight())
val state3 = state2.cancelTransaction(tx1)
@ -121,9 +122,10 @@ class ElectrumWalletBasicSpec extends FunSuite {
test("compute the effect of tx") {
val state1 = addFunds(state, state.accountKeys.head, 1 btc)
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(0.5 btc, Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0)
val (state2, tx1) = state1.completeTransaction(tx, feeRatePerKw, minimumFee, dustLimit, false)
val (state2, tx1, fee1) = state1.completeTransaction(tx, feeRatePerKw, minimumFee, dustLimit, false)
val Some((received, sent, Some(fee))) = state1.computeTransactionDelta(tx1)
assert(fee == fee1)
assert(sent - received - fee == btc2satoshi(0.5 btc))
}
@ -132,31 +134,35 @@ class ElectrumWalletBasicSpec extends FunSuite {
{
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Satoshi(5000000), Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0)
val (state3, tx1) = state1.completeTransaction(tx, feeRatePerKw, minimumFee, dustLimit, true)
val (state3, tx1, fee1) = state1.completeTransaction(tx, feeRatePerKw, minimumFee, dustLimit, true)
val Some((_, _, Some(fee))) = state3.computeTransactionDelta(tx1)
assert(fee == fee1)
val actualFeeRate = Transactions.fee2rate(fee, tx1.weight())
assert(isFeerateOk(actualFeeRate, feeRatePerKw))
}
{
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Satoshi(5000000) - dustLimit, Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0)
val (state3, tx1) = state1.completeTransaction(tx, feeRatePerKw, minimumFee, dustLimit, true)
val (state3, tx1, fee1) = state1.completeTransaction(tx, feeRatePerKw, minimumFee, dustLimit, true)
val Some((_, _, Some(fee))) = state3.computeTransactionDelta(tx1)
assert(fee == fee1)
val actualFeeRate = Transactions.fee2rate(fee, tx1.weight())
assert(isFeerateOk(actualFeeRate, feeRatePerKw))
}
{
// with a huge fee rate that will force us to use an additional input when we complete our tx
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Satoshi(3000000), Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0)
val (state3, tx1) = state1.completeTransaction(tx, 100 * feeRatePerKw, minimumFee, dustLimit, true)
val (state3, tx1, fee1) = state1.completeTransaction(tx, 100 * feeRatePerKw, minimumFee, dustLimit, true)
val Some((_, _, Some(fee))) = state3.computeTransactionDelta(tx1)
assert(fee == fee1)
val actualFeeRate = Transactions.fee2rate(fee, tx1.weight())
assert(isFeerateOk(actualFeeRate, 100 * feeRatePerKw))
}
{
// with a tiny fee rate that will force us to use an additional input when we complete our tx
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Btc(0.09), Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0)
val (state3, tx1) = state1.completeTransaction(tx, feeRatePerKw / 10, minimumFee / 10, dustLimit, true)
val (state3, tx1, fee1) = state1.completeTransaction(tx, feeRatePerKw / 10, minimumFee / 10, dustLimit, true)
val Some((_, _, Some(fee))) = state3.computeTransactionDelta(tx1)
assert(fee == fee1)
val actualFeeRate = Transactions.fee2rate(fee, tx1.weight())
assert(isFeerateOk(actualFeeRate, feeRatePerKw / 10))
}
@ -189,7 +195,7 @@ class ElectrumWalletBasicSpec extends FunSuite {
val amount = dustLimit + Satoshi(random.nextInt(10000000))
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(amount, Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0)
Try(state1.completeTransaction(tx, feeRatePerKw, minimumFee, dustLimit, true)) match {
case Success((state2, tx1)) => ()
case Success((state2, tx1, fee1)) => ()
case Failure(cause) if cause.getMessage != null && cause.getMessage.contains("insufficient funds") => ()
case Failure(cause) => println(s"unexpected $cause")
}

View File

@ -27,7 +27,7 @@ import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet.{FundTransactionRes
import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, BitcoindService}
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{BroadcastTransaction, BroadcastTransactionResponse}
import grizzled.slf4j.Logging
import org.json4s.JsonAST.{JDecimal, JString, JValue}
import org.json4s.JsonAST.{JDecimal, JDouble, JString, JValue}
import org.junit.experimental.categories.Category
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
@ -227,7 +227,7 @@ class ElectrumWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLike
val JString(address) = probe.expectMsgType[JValue]
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Btc(1), fr.acinq.eclair.addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) :: Nil, lockTime = 0L)
probe.send(wallet, CompleteTransaction(tx, 20000))
val CompleteTransactionResponse(tx1, None) = probe.expectMsgType[CompleteTransactionResponse]
val CompleteTransactionResponse(tx1, fee1, None) = probe.expectMsgType[CompleteTransactionResponse]
// send it ourselves
logger.info(s"sending 1 btc to $address with tx ${tx1.txid}")
@ -260,7 +260,7 @@ class ElectrumWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLike
val JString(address) = probe.expectMsgType[JValue]
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Btc(1), fr.acinq.eclair.addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) :: Nil, lockTime = 0L)
probe.send(wallet, CompleteTransaction(tx, 20000))
val CompleteTransactionResponse(tx1, None) = probe.expectMsgType[CompleteTransactionResponse]
val CompleteTransactionResponse(tx1, fee1, None) = probe.expectMsgType[CompleteTransactionResponse]
// send it ourselves
logger.info(s"sending 1 btc to $address with tx ${tx1.txid}")

View File

@ -0,0 +1,97 @@
/*
* Copyright 2018 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.db
import java.sql.DriverManager
import fr.acinq.bitcoin.{MilliSatoshi, Satoshi, Transaction}
import fr.acinq.eclair.channel.NetworkFeePaid
import fr.acinq.eclair.db.sqlite.SqliteAuditDb
import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent}
import fr.acinq.eclair.{randomBytes, randomKey}
import org.junit.runner.RunWith
import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner
import scala.compat.Platform
@RunWith(classOf[JUnitRunner])
class SqliteAuditDbSpec extends FunSuite {
def inmem = DriverManager.getConnection("jdbc:sqlite::memory:")
test("init sqlite 2 times in a row") {
val sqlite = inmem
val db1 = new SqliteAuditDb(sqlite)
val db2 = new SqliteAuditDb(sqlite)
}
test("add/list events") {
val sqlite = inmem
val db = new SqliteAuditDb(sqlite)
val e1 = PaymentSent(MilliSatoshi(42000), MilliSatoshi(1000), randomBytes(32), randomBytes(32), randomBytes(32))
val e2 = PaymentReceived(MilliSatoshi(42000), randomBytes(32), randomBytes(32))
val e3 = PaymentRelayed(MilliSatoshi(42000), MilliSatoshi(1000), randomBytes(32), randomBytes(32), randomBytes(32))
val e4 = NetworkFeePaid(null, randomKey.publicKey, randomBytes(32), Transaction(0, Seq.empty, Seq.empty, 0), Satoshi(42), "mutual")
val e5 = PaymentSent(MilliSatoshi(42000), MilliSatoshi(1000), randomBytes(32), randomBytes(32), randomBytes(32), timestamp = 0)
val e6 = PaymentSent(MilliSatoshi(42000), MilliSatoshi(1000), randomBytes(32), randomBytes(32), randomBytes(32), timestamp = Platform.currentTime * 2)
db.add(e1)
db.add(e2)
db.add(e3)
db.add(e4)
db.add(e5)
db.add(e6)
assert(db.listSent(from = 0L, to = Long.MaxValue).toSet === Set(e1, e5, e6))
assert(db.listSent(from = 100000L, to = Platform.currentTime).toList === List(e1))
assert(db.listReceived(from = 0L, to = Long.MaxValue).toList === List(e2))
assert(db.listRelayed(from = 0L, to = Long.MaxValue).toList === List(e3))
assert(db.listNetworkFees(from = 0L, to = Long.MaxValue).size === 1)
assert(db.listNetworkFees(from = 0L, to = Long.MaxValue).head.txType === "mutual")
}
test("stats") {
val sqlite = inmem
val db = new SqliteAuditDb(sqlite)
val n1 = randomKey.publicKey
val n2 = randomKey.publicKey
val n3 = randomKey.publicKey
val c1 = randomBytes(32)
val c2 = randomBytes(32)
val c3 = randomBytes(32)
db.add(PaymentRelayed(MilliSatoshi(46000), MilliSatoshi(44000), randomBytes(32), randomBytes(32), c1))
db.add(PaymentRelayed(MilliSatoshi(41000), MilliSatoshi(40000), randomBytes(32), randomBytes(32), c1))
db.add(PaymentRelayed(MilliSatoshi(43000), MilliSatoshi(42000), randomBytes(32), randomBytes(32), c1))
db.add(PaymentRelayed(MilliSatoshi(42000), MilliSatoshi(40000), randomBytes(32), randomBytes(32), c2))
db.add(NetworkFeePaid(null, n1, c1, Transaction(0, Seq.empty, Seq.empty, 0), Satoshi(100), "funding"))
db.add(NetworkFeePaid(null, n2, c2, Transaction(0, Seq.empty, Seq.empty, 0), Satoshi(200), "funding"))
db.add(NetworkFeePaid(null, n2, c2, Transaction(0, Seq.empty, Seq.empty, 0), Satoshi(300), "mutual"))
db.add(NetworkFeePaid(null, n3, c3, Transaction(0, Seq.empty, Seq.empty, 0), Satoshi(400), "funding"))
assert(db.stats.toSet === Set(
Stats(channelId = c1, avgPaymentAmountSatoshi = 42, paymentCount = 3, relayFeeSatoshi = 4, networkFeeSatoshi = 100),
Stats(channelId = c2, avgPaymentAmountSatoshi = 40, paymentCount = 1, relayFeeSatoshi = 2, networkFeeSatoshi = 500),
Stats(channelId = c3, avgPaymentAmountSatoshi = 0, paymentCount = 0, relayFeeSatoshi = 0, networkFeeSatoshi = 400)
))
}
}

View File

@ -56,7 +56,8 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike
val add = UpdateAddHtlc("11" * 32, 0, amountMsat.amount, pr.paymentHash, expiry, "")
sender.send(handler, add)
sender.expectMsgType[CMD_FULFILL_HTLC]
eventListener.expectMsg(PaymentReceived(amountMsat, add.paymentHash))
val paymentRelayed = eventListener.expectMsgType[PaymentReceived]
assert(paymentRelayed.copy(timestamp = 0) === PaymentReceived(amountMsat,add.paymentHash, add.channelId, timestamp = 0))
sender.send(handler, CheckPayment(pr.paymentHash))
assert(sender.expectMsgType[Boolean] === true)
}
@ -69,7 +70,8 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike
val add = UpdateAddHtlc("11" * 32, 0, amountMsat.amount, pr.paymentHash, expiry, "")
sender.send(handler, add)
sender.expectMsgType[CMD_FULFILL_HTLC]
eventListener.expectMsg(PaymentReceived(amountMsat, add.paymentHash))
val paymentRelayed = eventListener.expectMsgType[PaymentReceived]
assert(paymentRelayed.copy(timestamp = 0) === PaymentReceived(amountMsat,add.paymentHash, add.channelId, timestamp = 0))
sender.send(handler, CheckPayment(pr.paymentHash))
assert(sender.expectMsgType[Boolean] === true)
}

View File

@ -39,7 +39,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val initialBlockCount = 420000
Globals.blockCount.set(initialBlockCount)
val defaultAmountMsat = 142000000L
val defaultPaymentHash = BinaryData("42" * 32)
@ -251,9 +251,9 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
sender.send(paymentFSM, UpdateFulfillHtlc("00" * 32, 0, defaultPaymentHash))
val paymentOK = sender.expectMsgType[PaymentSucceeded]
assert(paymentOK.amountMsat > request.amountMsat)
val PaymentSent(MilliSatoshi(request.amountMsat), feesPaid, request.paymentHash, paymentOK.paymentPreimage) = eventListener.expectMsgType[PaymentSent]
assert(feesPaid.amount > 0)
val PaymentSent(MilliSatoshi(request.amountMsat), fee, request.paymentHash, paymentOK.paymentPreimage, _, _) = eventListener.expectMsgType[PaymentSent]
assert(fee > MilliSatoshi(0))
assert(fee === MilliSatoshi(paymentOK.amountMsat - request.amountMsat))
}
test("filter errors properly") { case _ =>

View File

@ -338,7 +338,8 @@ class RelayerSpec extends TestkitBaseClass {
assert(fwd.channelId === origin.originChannelId)
assert(fwd.message.id === origin.originHtlcId)
eventListener.expectMsg(PaymentRelayed(MilliSatoshi(origin.amountMsatIn), MilliSatoshi(origin.amountMsatOut), Crypto.sha256(fulfill_ba.paymentPreimage)))
val paymentRelayed = eventListener.expectMsgType[PaymentRelayed]
assert(paymentRelayed.copy(timestamp = 0) === PaymentRelayed(MilliSatoshi(origin.amountMsatIn), MilliSatoshi(origin.amountMsatOut), add_bc.paymentHash, channelId_ab, channelId_bc, timestamp = 0))
}
test("relay an htlc-fail") { case (relayer, register, _) =>