Load Balancing
MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
@@ -83,7 +83,10 @@ public partial class Arena : PeriodicRunner
await CreateRoundsAsync(cancel).ConfigureAwait(false);
// RoundStates have to contain all states. Do not change stateId=0.
- RoundStates = Rounds.OrderBy(x => x.InputCount).Select(r => RoundState.FromRound(r, stateId: 0)).ToImmutableArray();
+ RoundStates = Rounds
+ .OrderBy(x => x.Parameters.MaxSuggestedAmount)
+ .ThenBy(x => x.InputCount)
+ .Select(r => RoundState.FromRound(r, stateId: 0)).ToImmutableArray();
}
}
@@ -388,28 +391,35 @@ public partial class Arena : PeriodicRunner
{
FeeRate? feeRate = null;
- // Create rounds to split the volume around minimum input counts if load balance is required.
- if (Config.WW200CompatibleRoundParallelization > 1 // Only do things if the load balancer compatibility is configured to at least 2.
- && (DateTimeOffset.UtcNow - LastFirstOrderedRoundCreated) > TimeSpan.FromMinutes(1) // Run this expensive algorithm every minute at best.
- && Rounds.Count(x => // We can at max create the configured number of parallel rounds.
- x.Phase == Phase.InputRegistration
- && x is not BlameRound
- && !x.IsInputRegistrationEnded(Config.MaxInputCountByRound)) < Config.WW200CompatibleRoundParallelization // It gets computationally expensive getting in here quickly.
- && !Rounds.Any(x => // If there's round in input registration that didn't yet reach the minimum, then skip.
+ // Have rounds to split the volume around minimum input counts if load balance is required.
+ // Only do things if the load balancer compatibility is configured.
+ if (Config.WW200CompatibleLoadBalancing)
+ {
+ // Destroy the round when it reaches this input count and create 2 new ones instead.
+ var roundDestroyerInputCount = Config.MinInputCountByRound * 2 + Config.MinInputCountByRound / 2;
+
+ foreach (var round in Rounds.Where(x =>
x.Phase == Phase.InputRegistration
&& x is not BlameRound
&& !x.IsInputRegistrationEnded(Config.MaxInputCountByRound)
- && x.InputCount < Config.MinInputCountByRound)
- && Rounds.Any(x => // If there isn't any input reg round, then let the software create a normal round.
- x.Phase == Phase.InputRegistration
- && x is not BlameRound
- && !x.IsInputRegistrationEnded(Config.MaxInputCountByRound)))
+ && x.InputCount >= roundDestroyerInputCount).ToArray())
{
feeRate = (await Rpc.EstimateSmartFeeAsync((int)Config.ConfirmationTarget, EstimateSmartFeeMode.Conservative, simulateIfRegTest: true, cancellationToken).ConfigureAwait(false)).FeeRate;
- RoundParameters parameters = RoundParameterFactory.CreateRoundParameter(feeRate, MaxSuggestedAmountProvider.MaxSuggestedAmount);
+ var allInputs = round.Alices.Select(y => y.Coin.Amount).OrderBy(x => x).ToArray();
+ round.EndRound(EndRoundState.AbortedLoadBalancing);
+ Logger.LogInfo($"Destroyed round with {allInputs.Length} inputs. Threshold: {roundDestroyerInputCount}");
+
+ // 0.75 to bias towards larger numbers as larger input owners often have many smaller inputs too.
+ var smallSuggestion = allInputs.Skip((int)(allInputs.Length * 0.75)).First();
+ var largeSuggestion = MaxSuggestedAmountProvider.AbsoluteMaximumInput;
+
+ RoundParameters parameters = RoundParameterFactory.CreateRoundParameter(feeRate, largeSuggestion);
TryCreateFirstOrderedRound(parameters);
- LastFirstOrderedRoundCreated = DateTimeOffset.UtcNow;
+
+ parameters = RoundParameterFactory.CreateRoundParameter(feeRate, smallSuggestion);
+ TryCreateFirstOrderedRound(parameters);
+ }
}
// Add more rounds if not enough.
@@ -444,7 +454,8 @@ public partial class Arena : PeriodicRunner
roundsCopy.Add(r);
orderedRounds = roundsCopy
.Where(x => x.Phase == Phase.InputRegistration && x is not BlameRound && !x.IsInputRegistrationEnded(Config.MaxInputCountByRound))
- .OrderBy(x => x.InputCount);
+ .OrderBy(x => x.Parameters.MaxSuggestedAmount)
+ .ThenBy(x => x.InputCount);
times++;
}
while (times <= maxCycleTimes && orderedRounds.ToImmutableDictionary(x => x.Id, x => x).First().Key != r.Id);

MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/MaxSuggestedAmountProvider.cs
WalletWasabi/WabiSabi/Backend/Rounds/MaxSuggestedAmountProvider.cs
@@ -17,7 +17,7 @@ public class MaxSuggestedAmountProvider
private WabiSabiConfig Config { get; init; }
private int Counter { get; set; }
public Money MaxSuggestedAmount { get; private set; }
- private Money AbsoluteMaximumInput { get; } = Money.Satoshis(ProtocolConstants.MaxAmountPerAlice);
+ public Money AbsoluteMaximumInput { get; } = Money.Satoshis(ProtocolConstants.MaxAmountPerAlice);
private void CheckOrGenerateRoundCounterDividerAndMaxAmounts()
{

MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/Round.cs
WalletWasabi/WabiSabi/Backend/Rounds/Round.cs
@@ -22,7 +22,8 @@ public enum EndRoundState
TransactionBroadcasted,
NotAllAlicesSign,
AbortedNotEnoughAlicesSigned,
- AbortedNotAllAlicesConfirmed
+ AbortedNotAllAlicesConfirmed,
+ AbortedLoadBalancing
}
public class Round

MODIFY
WalletWasabi/WabiSabi/Backend/WabiSabiConfig.cs
WalletWasabi/WabiSabi/Backend/WabiSabiConfig.cs
@@ -112,12 +112,9 @@ public class WabiSabiConfig : ConfigBase
[JsonProperty(PropertyName = "RoundParallelization", DefaultValueHandling = DefaultValueHandling.Populate)]
public int RoundParallelization { get; set; } = 1;
- /// <summary>
- /// Try to keep this below 6. The compatibility hack is computationally expensive.
- /// </summary>
- [DefaultValue(1)]
- [JsonProperty(PropertyName = "WW200CompatibleRoundParallelization", DefaultValueHandling = DefaultValueHandling.Populate)]
- public int WW200CompatibleRoundParallelization { get; set; } = 1;
+ [DefaultValue(false)]
+ [JsonProperty(PropertyName = "WW200CompatibleLoadBalancing", DefaultValueHandling = DefaultValueHandling.Populate)]
+ public bool WW200CompatibleLoadBalancing { get; set; } = false;
public Script GetNextCleanCoordinatorScript() => DeriveCoordinatorScript(CoordinatorExtPubKeyCurrentDepth);

Load Balancing