Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,51 @@ eclair {
cleanup-frequency = 1 day
}

// We keep track of peer statistics to find the most profitable peers and fund channels with them to optimize our
// payment volume and fees earned.
peer-scoring {
// Set this to true if you want to start collecting data to score peers.
enabled = false
// Frequency at which we run our peer scoring algorithm.
frequency = 1 hour
// Maximum number of peers to select as candidates for liquidity and relay fee updates.
top-peers-count = 10
// A list of node_ids with whom we will try to maintain liquidity.
top-peers-whitelist = []
// We can automatically allocate liquidity to our top peers when necessary.
liquidity {
// If true, we will automatically fund channels.
auto-fund = false
// If true, we will automatically close unused channels to reclaim liquidity.
auto-close = false
// We only fund channels if at least this amount is necessary.
min-funding-amount-satoshis = 1000000 // 0.01 btc
// We never fund channels with more than this amount.
max-funding-amount-satoshis = 50000000 // 0.5 btc
// We stop funding channels if our on-chain balance is below this amount.
min-on-chain-balance-satoshis = 50000000 // 0.5 btc
// We stop funding channels if the on-chain feerate is above this value.
max-feerate-sat-per-byte = 5
// Rate-limit the number of funding transactions we make per day (on average).
max-funding-tx-per-day = 6
}
// We can automatically update our relay fees to our top peers when necessary.
relay-fees {
// If true, we will automatically update our relay fees based on variations in outgoing payment volume.
auto-update = false
// We will not lower our fees below these values.
min-fee-base-msat = 1
min-fee-proportional-millionths = 500
// We will not increase our fees above these values.
max-fee-base-msat = 10000
max-fee-proportional-millionths = 5000
// We only increase fees if the daily outgoing payment volume exceeds this threshold or daily-payment-volume-threshold-percent.
daily-payment-volume-threshold-satoshis = 10000000 // 0.1 btc
// We only increase fees if the daily outgoing payment volume exceeds this percentage of our peer capacity or daily-payment-volume-threshold.
daily-payment-volume-threshold-percent = 0.05
}
}

offers {
// Minimum length of an offer blinded path when hiding our real node id
message-path-min-length = 2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ case class MilliSatoshi(private val underlying: Long) extends Ordered[MilliSatos
def *(m: Long) = MilliSatoshi(underlying * m)
def *(m: Double) = MilliSatoshi((underlying * m).toLong)
def /(d: Long) = MilliSatoshi(underlying / d)
def /(d: Double) = MilliSatoshi((underlying / d).toLong)
def unary_- = MilliSatoshi(-underlying)

override def compare(other: MilliSatoshi): Int = underlying.compareTo(other.underlying)
Expand Down
30 changes: 30 additions & 0 deletions eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import fr.acinq.eclair.message.OnionMessages.OnionMessageConfig
import fr.acinq.eclair.payment.offer.OffersConfig
import fr.acinq.eclair.payment.relay.OnTheFlyFunding
import fr.acinq.eclair.payment.relay.Relayer.{AsyncPaymentsParams, RelayFees, RelayParams}
import fr.acinq.eclair.profit.PeerScorer
import fr.acinq.eclair.reputation.Reputation
import fr.acinq.eclair.router.Announcements.AddressException
import fr.acinq.eclair.router.Graph.HeuristicsConstants
Expand Down Expand Up @@ -95,6 +96,7 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
peerWakeUpConfig: PeerReadyNotifier.WakeUpConfig,
onTheFlyFundingConfig: OnTheFlyFunding.Config,
peerStorageConfig: PeerStorageConfig,
peerScoringConfig: PeerScorer.Config,
offersConfig: OffersConfig) {
val privateKey: Crypto.PrivateKey = nodeKeyManager.nodeKey.privateKey

Expand Down Expand Up @@ -714,6 +716,34 @@ object NodeParams extends Logging {
removalDelay = FiniteDuration(config.getDuration("peer-storage.removal-delay").getSeconds, TimeUnit.SECONDS),
cleanUpFrequency = FiniteDuration(config.getDuration("peer-storage.cleanup-frequency").getSeconds, TimeUnit.SECONDS),
),
peerScoringConfig = PeerScorer.Config(
enabled = config.getBoolean("peer-scoring.enabled"),
scoringFrequency = FiniteDuration(config.getDuration("peer-scoring.frequency").getSeconds, TimeUnit.SECONDS),
topPeersCount = config.getInt("peer-scoring.top-peers-count"),
topPeersWhitelist = config.getStringList("peer-scoring.top-peers-whitelist").asScala.map(s => PublicKey(ByteVector.fromValidHex(s))).toSet,
liquidity = PeerScorer.LiquidityConfig(
autoFund = config.getBoolean("peer-scoring.liquidity.auto-fund"),
autoClose = config.getBoolean("peer-scoring.liquidity.auto-close"),
minFundingAmount = config.getLong("peer-scoring.liquidity.min-funding-amount-satoshis").sat,
maxFundingAmount = config.getLong("peer-scoring.liquidity.max-funding-amount-satoshis").sat,
maxFundingTxPerDay = config.getInt("peer-scoring.liquidity.max-funding-tx-per-day"),
minOnChainBalance = config.getLong("peer-scoring.liquidity.min-on-chain-balance-satoshis").sat,
maxFeerate = FeeratePerByte(config.getLong("peer-scoring.liquidity.max-feerate-sat-per-byte").sat).perKw,
),
relayFees = PeerScorer.RelayFeesConfig(
autoUpdate = config.getBoolean("peer-scoring.relay-fees.auto-update"),
minRelayFees = RelayFees(
feeBase = config.getLong("peer-scoring.relay-fees.min-fee-base-msat").msat,
feeProportionalMillionths = config.getLong("peer-scoring.relay-fees.min-fee-proportional-millionths"),
),
maxRelayFees = RelayFees(
feeBase = config.getLong("peer-scoring.relay-fees.max-fee-base-msat").msat,
feeProportionalMillionths = config.getLong("peer-scoring.relay-fees.max-fee-proportional-millionths"),
),
dailyPaymentVolumeThreshold = config.getLong("peer-scoring.relay-fees.daily-payment-volume-threshold-satoshis").sat,
dailyPaymentVolumeThresholdPercent = config.getDouble("peer-scoring.relay-fees.daily-payment-volume-threshold-percent"),
)
),
offersConfig = OffersConfig(
messagePathMinLength = config.getInt("offers.message-path-min-length"),
paymentPathCount = config.getInt("offers.payment-path-count"),
Expand Down
6 changes: 5 additions & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import fr.acinq.eclair.payment.offer.{DefaultOfferHandler, OfferManager}
import fr.acinq.eclair.payment.receive.PaymentHandler
import fr.acinq.eclair.payment.relay.{AsyncPaymentTriggerer, PostRestartHtlcCleaner, Relayer}
import fr.acinq.eclair.payment.send.{Autoprobe, PaymentInitiator}
import fr.acinq.eclair.profit.{PeerScorer, PeerStatsTracker}
import fr.acinq.eclair.reputation.ReputationRecorder
import fr.acinq.eclair.router._
import fr.acinq.eclair.tor.{Controller, TorProtocolHandler}
Expand Down Expand Up @@ -400,8 +401,11 @@ class Setup(val datadir: File,
_ = for (i <- 0 until config.getInt("autoprobe-count")) yield system.actorOf(SimpleSupervisor.props(Autoprobe.props(nodeParams, router, paymentInitiator), s"payment-autoprobe-$i", SupervisorStrategy.Restart))

balanceActor = system.spawn(BalanceActor(bitcoinClient, nodeParams.channelConf.minDepth, channelsListener, nodeParams.balanceCheckInterval), name = "balance-actor")

postman = system.spawn(Behaviors.supervise(Postman(nodeParams, switchboard, router.toTyped, register, offerManager)).onFailure(typed.SupervisorStrategy.restart), name = "postman")
_ = if (nodeParams.peerScoringConfig.enabled) {
val statsTracker = system.spawn(Behaviors.supervise(PeerStatsTracker(nodeParams.db.audit, channels)).onFailure(typed.SupervisorStrategy.restart), name = "peer-stats-tracker")
system.spawn(Behaviors.supervise(PeerScorer(nodeParams, bitcoinClient, statsTracker, register)).onFailure(typed.SupervisorStrategy.restart), name = "peer-scorer")
}

kit = Kit(
nodeParams = nodeParams,
Expand Down
1 change: 1 addition & 0 deletions eclair-core/src/main/scala/fr/acinq/eclair/Timestamp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ case class TimestampMilli(private val underlying: Long) extends Ordered[Timestam
require(underlying >= 0 && underlying <= 253402300799L * 1000, "invalid timestamp value")
// @formatter:off
def toLong: Long = underlying
def toTimestampSecond: TimestampSecond = TimestampSecond(underlying / 1000)
def toSqlTimestamp: sql.Timestamp = sql.Timestamp.from(Instant.ofEpochMilli(underlying))
override def toString: String = s"$underlying unixms"
override def compare(that: TimestampMilli): Int = underlying.compareTo(that.underlying)
Expand Down
Loading