Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1f707f7
feat: add per-channel automated rebalancing opt-in
markettes Aug 20, 2026
5f7a954
fix: update routing state model to support managed node association
markettes Aug 21, 2026
36e770f
refactor: update channel fee and routing state repositories to includ…
markettes Aug 21, 2026
d4f43e6
refactor: update TargetRatioReevaluationJob to simplify node reevalua…
markettes Aug 21, 2026
8e409e2
fix: update fee state repository verification to use UpsertByChannelA…
markettes Aug 21, 2026
4192fcb
test: add RoutingEngineCollection for sequential routing-engine job t…
markettes Aug 25, 2026
339c0fa
refactor: replace channel ownership check with helper method for clarity
markettes Aug 25, 2026
982e355
feat: add IsAutoRebalanceEnabled property to new channels configuration
markettes Aug 25, 2026
96c91c2
feat: add GetLocalOutboundFeeRatePpmAsync method to the interface
markettes Aug 25, 2026
3e547da
feat: add WorstCaseFeeSats method for calculating worst-case fee rese…
markettes Aug 25, 2026
802352e
feat: add routing engine actuator job configurations and parameters
markettes Aug 25, 2026
5a0bc6b
refactor: remove GetChannelsByOpenAndDynamicFeeEnabled method and upd…
markettes Aug 27, 2026
0829320
feat: rebalancing algorithm
markettes Aug 27, 2026
2c8675c
refactor: rebalance alorithm
markettes Aug 27, 2026
c0b83b2
Validate human PSBT approvals against template (#557)
Jossec101 Aug 20, 2026
355050f
E2E tests: containerized end-to-end suite for rebalance + dynamic fee…
markettes Aug 20, 2026
cb598fb
Scope coin-selection ignore list to queried wallet (#575)
Jossec101 Aug 24, 2026
a03a323
E2E: coin selection with an ignore list too long for the request line…
Jossec101 Aug 24, 2026
21252c0
Fix sweep node wallets job to return errors without clear reason
imclvr Aug 26, 2026
600f4b3
feat: implement max HTLC sync functionality and related tests (#578)
markettes Aug 26, 2026
a57008e
Merge pull request #580 from Elenpay/fix/fix-sweep-node-wallets-error…
imclvr Aug 27, 2026
c79b596
Merge branch 'main' into rebalance-algorithm
markettes Aug 28, 2026
1da2796
Merge branch 'rebalances-engine/fix-owned-channels-fee-state' into re…
markettes Aug 28, 2026
be1d2d8
feat: enhance routing job scheduling with dynamic interval configuration
markettes Aug 28, 2026
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
6 changes: 6 additions & 0 deletions src/Data/Models/Rebalance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ public class Rebalance : Entity
[NotMapped]
public Money FeePaid => new Money(FeePaidSats ?? 0, MoneyUnit.Satoshi);

/// <summary>
/// Worst-case fee a not-yet-settled rebalance can still consume: the whole amount at its fee cap.
/// </summary>
public static long WorstCaseFeeSats(long amountSats, double maxFeePct)
=> (long)(amountSats * maxFeePct / 100.0);

public int? SourceChannelId { get; set; }
public Channel? SourceChannel { get; set; }

Expand Down
9 changes: 0 additions & 9 deletions src/Data/Repositories/ChannelRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,6 @@ public async Task<List<Channel>> GetOpenChannels()
.ToListAsync();
}

public async Task<List<Channel>> GetChannelsByOpenAndDynamicFeeEnabled()
{
await using var applicationDbContext = await _dbContextFactory.CreateDbContextAsync();

return await applicationDbContext.Channels
.Where(c => c.Status == Channel.ChannelStatus.Open && c.ChanId != 0 && c.IsDynamicFeeEnabled)
.ToListAsync();
}

public async Task<(bool, string?)> AddAsync(Channel type)
{
await using var applicationDbContext = await _dbContextFactory.CreateDbContextAsync();
Expand Down
2 changes: 0 additions & 2 deletions src/Data/Repositories/Interfaces/IChannelRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ public interface IChannelRepository

Task<List<Channel>> GetOpenChannels();

Task <List<Channel>> GetChannelsByOpenAndDynamicFeeEnabled();

Task<(bool, string?)> AddAsync(Channel type);

Task<(bool, string?)> AddRangeAsync(List<Channel> type);
Expand Down
5 changes: 2 additions & 3 deletions src/Data/Repositories/RebalanceRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,8 @@ public async Task<long> GetConsumedFeesSince(int nodeId, DateTimeOffset since)
foreach (var r in rows)
{
var feePaid = r.FeePaidSats ?? 0;
// Conservative worst-case reservation for a still-in-flight rebalance: RequestedAmountSats ×
// (MaxFeePct/100).
var reserved = (long)(r.RequestedAmountSats * r.MaxFeePct / 100.0);
// Conservative worst-case reservation for a still-in-flight rebalance.
var reserved = Rebalance.WorstCaseFeeSats(r.RequestedAmountSats, r.MaxFeePct);
total += r.Status == RebalanceStatus.Succeeded
? feePaid
: reserved;
Expand Down
53 changes: 50 additions & 3 deletions src/Helpers/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -273,11 +273,23 @@ public class Constants
public static int ROUTING_ENGINE_CATEGORY_FLIP_HYSTERESIS_CYCLES = 3;

/// <summary>
/// Cadence of TargetRatioReevaluationJob and ChannelFeeOptimizerJob in prod, in minutes. Default 30. In dev
/// Cadence of TargetRatioReevaluationJob and RoutingEngineActuatorJob in prod, in minutes. Default 30. In dev
/// (IS_DEV_ENVIRONMENT) the job runs every 5 minutes regardless.
/// </summary>
public static int ROUTING_ENGINE_JOB_INTERVAL_MINUTES = 30;

/// <summary>
/// How long after TargetRatioReevaluationJob the RoutingEngineActuatorJob first fires,
/// in minutes (prod only).
/// </summary>
public static int ROUTING_ENGINE_ACTUATOR_OFFSET_MINUTES = 5;

/// <summary>
/// Cadence of AutoRebalanceJob in prod, in minutes. Independent of
/// ROUTING_ENGINE_JOB_INTERVAL_MINUTES. In dev it is 1 minute.
/// </summary>
public static int ROUTING_ENGINE_REBALANCE_JOB_INTERVAL_MINUTES = 10;

// Both fees use integral control: each cycle the applied value is nudged by gain·deviation·baseline
// off its previous value, so a persistent deviation keeps driving the fee until the channel balances.
public static double ROUTING_ENGINE_FEE_OUTBOUND_INTEGRAL_GAIN = 0.8;
Expand All @@ -288,7 +300,7 @@ public class Constants
public static double ROUTING_ENGINE_FEE_DEADBAND = 0.03;

// The rebalancer's imbalance deadband is a separate, more aggressive threshold for triggering rebalances.
public static double ROUTING_ENGINE_REBALANCE_TRIGGER = 0.15;
public static double ROUTING_ENGINE_REBALANCE_DEADBAND = 0.15;

// Max outbound ppm change applied in a single cycle (rate limiter / anti-jump).
public static uint ROUTING_ENGINE_FEE_MAX_STEP_PPM = 50;
Expand Down Expand Up @@ -335,6 +347,27 @@ public class Constants
/// </summary>
public static double MAX_HTLC_CAPACITY_RATIO = 0.99;

// Routing Engine: automated rebalancer

/// Upper clamp on a single automated rebalance amount (sats)
public static long ROUTING_ENGINE_REBALANCE_MAX_AMOUNT_SATS = 10_000_000;

/// Max rebalances the routing-engine actuator initiates per node per run
public static int ROUTING_ENGINE_REBALANCE_MAX_INITIATIONS_PER_RUN = 5;

/// Fallback profitability margin when a node leaves MaxRebalanceCostToEarnRatio unset. The
/// gate caps rebalance cost at ratio × (capacity-weighted-avg outbound ppm of the refilled
/// peer). 0.5 = spend at most half the destination's earn rate.
public static double ROUTING_ENGINE_REBALANCE_DEFAULT_COST_TO_EARN_RATIO = 0.5;

/// Fallback max concurrent (Pending/InFlight) rebalances when a node leaves
/// MaxRebalancesInFlight unset.
public static int ROUTING_ENGINE_REBALANCE_DEFAULT_MAX_IN_FLIGHT = 5;

/// Fallback rebalance-budget refresh window (hours) when a node leaves
/// RebalanceBudgetRefreshInterval unset.
public static int ROUTING_ENGINE_REBALANCE_DEFAULT_BUDGET_REFRESH_HOURS = 24;

public const string IsFrozenTag = "frozen";
public const string IsManuallyFrozenTag = "manually_frozen";

Expand Down Expand Up @@ -606,6 +639,10 @@ static Constants()

var reJobInterval = Environment.GetEnvironmentVariable("ROUTING_ENGINE_JOB_INTERVAL_MINUTES");
if (reJobInterval != null) ROUTING_ENGINE_JOB_INTERVAL_MINUTES = int.Parse(reJobInterval);
var reActuatorOffset = Environment.GetEnvironmentVariable("ROUTING_ENGINE_ACTUATOR_OFFSET_MINUTES");
if (reActuatorOffset != null) ROUTING_ENGINE_ACTUATOR_OFFSET_MINUTES = int.Parse(reActuatorOffset);
var reRebalanceInterval = Environment.GetEnvironmentVariable("ROUTING_ENGINE_REBALANCE_JOB_INTERVAL_MINUTES");
if (reRebalanceInterval != null) ROUTING_ENGINE_REBALANCE_JOB_INTERVAL_MINUTES = int.Parse(reRebalanceInterval);

// Routing Engine
var feeOutboundIntegralGain = Environment.GetEnvironmentVariable("ROUTING_ENGINE_FEE_OUTBOUND_INTEGRAL_GAIN");
Expand All @@ -618,7 +655,7 @@ static Constants()
if (feeDeadband != null) ROUTING_ENGINE_FEE_DEADBAND = double.Parse(feeDeadband, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture);

var rebalanceDeadband = Environment.GetEnvironmentVariable("ROUTING_ENGINE_REBALANCE_DEADBAND");
if (rebalanceDeadband != null) ROUTING_ENGINE_REBALANCE_TRIGGER = double.Parse(rebalanceDeadband, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture);
if (rebalanceDeadband != null) ROUTING_ENGINE_REBALANCE_DEADBAND = double.Parse(rebalanceDeadband, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture);

var feeMaxStep = Environment.GetEnvironmentVariable("ROUTING_ENGINE_FEE_MAX_STEP_PPM");
if (feeMaxStep != null) ROUTING_ENGINE_FEE_MAX_STEP_PPM = uint.Parse(feeMaxStep);
Expand Down Expand Up @@ -665,6 +702,16 @@ static Constants()
if (parsedRatio > 0 && parsedRatio <= 1) MAX_HTLC_CAPACITY_RATIO = parsedRatio;
else throw new ArgumentOutOfRangeException(nameof(MAX_HTLC_CAPACITY_RATIO), parsedRatio, "MAX_HTLC_CAPACITY_RATIO must be in (0, 1]");
}
var reRebalanceMaxAmount = Environment.GetEnvironmentVariable("ROUTING_ENGINE_REBALANCE_MAX_AMOUNT_SATS");
if (reRebalanceMaxAmount != null) ROUTING_ENGINE_REBALANCE_MAX_AMOUNT_SATS = long.Parse(reRebalanceMaxAmount);
var reRebalanceMaxInit = Environment.GetEnvironmentVariable("ROUTING_ENGINE_REBALANCE_MAX_INITIATIONS_PER_RUN");
if (reRebalanceMaxInit != null) ROUTING_ENGINE_REBALANCE_MAX_INITIATIONS_PER_RUN = int.Parse(reRebalanceMaxInit);
var reRebalanceDefaultRatio = Environment.GetEnvironmentVariable("ROUTING_ENGINE_REBALANCE_DEFAULT_COST_TO_EARN_RATIO");
if (reRebalanceDefaultRatio != null) ROUTING_ENGINE_REBALANCE_DEFAULT_COST_TO_EARN_RATIO = double.Parse(reRebalanceDefaultRatio, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture);
var reRebalanceDefaultMaxInFlight = Environment.GetEnvironmentVariable("ROUTING_ENGINE_REBALANCE_DEFAULT_MAX_IN_FLIGHT");
if (reRebalanceDefaultMaxInFlight != null) ROUTING_ENGINE_REBALANCE_DEFAULT_MAX_IN_FLIGHT = int.Parse(reRebalanceDefaultMaxInFlight);
var reRebalanceDefaultRefreshHours = Environment.GetEnvironmentVariable("ROUTING_ENGINE_REBALANCE_DEFAULT_BUDGET_REFRESH_HOURS");
if (reRebalanceDefaultRefreshHours != null) ROUTING_ENGINE_REBALANCE_DEFAULT_BUDGET_REFRESH_HOURS = int.Parse(reRebalanceDefaultRefreshHours);

// DB Initialization
ALICE_PUBKEY = Environment.GetEnvironmentVariable("ALICE_PUBKEY") ?? ALICE_PUBKEY;
Expand Down
Loading
Loading