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:
parent
924efeabe3
commit
75d23cf1b3
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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"),
|
||||
|
@ -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]
|
||||
|
@ -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())
|
||||
}))
|
||||
|
@ -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")
|
||||
|
||||
|
@ -54,4 +54,4 @@ trait EclairWallet {
|
||||
|
||||
}
|
||||
|
||||
final case class MakeFundingTxResponse(fundingTx: Transaction, fundingTxOutputIndex: Int)
|
||||
final case class MakeFundingTxResponse(fundingTx: Transaction, fundingTxOutputIndex: Int, fee: Satoshi)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 = {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
48
eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala
Normal file
48
eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala
Normal 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)
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
}
|
@ -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 =>
|
||||
|
@ -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
|
||||
|
@ -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)) =>
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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 = {
|
||||
|
@ -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])
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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}")
|
||||
|
@ -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)
|
||||
))
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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 _ =>
|
||||
|
@ -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, _) =>
|
||||
|
Loading…
Reference in New Issue
Block a user