mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 09:05:06 -07:00
Add payout modes and production switchover schema
This commit is contained in:
@@ -73,6 +73,11 @@ interface ComputedAddress {
|
||||
includedInCoinbase: boolean;
|
||||
}
|
||||
|
||||
interface AllocationTarget<T> {
|
||||
item: T;
|
||||
weight: number;
|
||||
}
|
||||
|
||||
export function buildPayoutDistribution(input: PayoutDistributionInput): PayoutDistributionResult {
|
||||
const coinbaseValueSats = Math.max(0, Math.floor(input.coinbaseValueSats));
|
||||
const minOutputSats = Math.max(DUST_LIMIT_SATS, Math.floor(input.minOutputSats));
|
||||
@@ -120,6 +125,15 @@ export function buildPayoutDistribution(input: PayoutDistributionInput): PayoutD
|
||||
};
|
||||
}
|
||||
|
||||
const fairShareSats = allocateIntegerSats(
|
||||
[...workByAddress.entries()].map(([address, work]) => ({
|
||||
item: address,
|
||||
weight: work.creditedDifficulty,
|
||||
})),
|
||||
minerRewardSats,
|
||||
totalCreditedDifficulty,
|
||||
);
|
||||
|
||||
const computations = new Map<string, ComputedAddress>();
|
||||
const considered = new Set<string>([...workByAddress.keys(), ...balanceByAddress.keys()]);
|
||||
|
||||
@@ -128,7 +142,7 @@ export function buildPayoutDistribution(input: PayoutDistributionInput): PayoutD
|
||||
const creditedDifficulty = work?.creditedDifficulty ?? 0;
|
||||
const acceptedShareCount = work?.acceptedShareCount ?? 0;
|
||||
const payoutWeight = creditedDifficulty > 0 ? creditedDifficulty / totalCreditedDifficulty : 0;
|
||||
const rawFairSats = Math.floor(payoutWeight * minerRewardSats);
|
||||
const rawFairSats = fairShareSats.get(address) ?? 0;
|
||||
const balanceBeforeSats = balanceByAddress.get(address) ?? 0;
|
||||
const targetSats = rawFairSats + balanceBeforeSats;
|
||||
computations.set(address, {
|
||||
@@ -148,47 +162,31 @@ export function buildPayoutDistribution(input: PayoutDistributionInput): PayoutD
|
||||
const feeWeight = feeSats > 0 ? outputWeightForAddress(feeAddress) : 0;
|
||||
let usedWeight = COINBASE_BASE_WEIGHT + COINBASE_WITNESS_COMMITMENT_WEIGHT + feeWeight;
|
||||
const effectiveBudget = coinbaseWeightBudget - BUDGET_SAFETY_MARGIN_WEIGHT;
|
||||
allocatePayoutTargets(computations, minerRewardSats, totalCreditedDifficulty);
|
||||
|
||||
const eligible = [...computations.values()]
|
||||
.filter(row => row.targetSats >= minOutputSats)
|
||||
.sort((a, b) => b.targetSats - a.targetSats || a.address.localeCompare(b.address));
|
||||
.filter(row => row.payoutSats > 0 && (row.creditedDifficulty > 0 || row.payoutSats >= minOutputSats))
|
||||
.sort((a, b) => b.payoutSats - a.payoutSats || b.targetSats - a.targetSats || a.address.localeCompare(b.address));
|
||||
|
||||
for (const row of eligible) {
|
||||
const outputWeight = outputWeightForAddress(row.address);
|
||||
if (usedWeight + outputWeight > effectiveBudget) {
|
||||
row.balanceAfterSats = row.targetSats;
|
||||
continue;
|
||||
}
|
||||
row.includedInCoinbase = true;
|
||||
row.payoutSats = row.targetSats;
|
||||
row.balanceAfterSats = 0;
|
||||
row.balanceAfterSats = row.targetSats - row.payoutSats;
|
||||
usedWeight += outputWeight;
|
||||
}
|
||||
|
||||
let distributedToMiners = 0;
|
||||
for (const row of computations.values()) {
|
||||
distributedToMiners += row.payoutSats;
|
||||
}
|
||||
|
||||
const remainder = minerRewardSats - distributedToMiners;
|
||||
if (remainder > 0) {
|
||||
const keptActive = [...computations.values()]
|
||||
.filter(row => row.includedInCoinbase && row.creditedDifficulty > 0)
|
||||
.sort((a, b) => b.creditedDifficulty - a.creditedDifficulty || a.address.localeCompare(b.address));
|
||||
if (keptActive.length > 0) {
|
||||
let assigned = 0;
|
||||
for (const row of keptActive) {
|
||||
const bonus = Math.floor((remainder * row.creditedDifficulty) / totalCreditedDifficulty);
|
||||
row.payoutSats += bonus;
|
||||
row.balanceAfterSats -= bonus;
|
||||
assigned += bonus;
|
||||
}
|
||||
const residual = remainder - assigned;
|
||||
if (residual > 0) {
|
||||
keptActive[0].payoutSats += residual;
|
||||
keptActive[0].balanceAfterSats -= residual;
|
||||
}
|
||||
if (!row.includedInCoinbase && row.payoutSats > 0) {
|
||||
row.payoutSats = 0;
|
||||
row.balanceAfterSats = row.targetSats;
|
||||
}
|
||||
}
|
||||
|
||||
redistributeUnfitMinerPayouts(computations, minerRewardSats);
|
||||
|
||||
const feeEntry = feeSats > 0
|
||||
? {
|
||||
address: feeAddress,
|
||||
@@ -243,6 +241,134 @@ export function buildPayoutDistribution(input: PayoutDistributionInput): PayoutD
|
||||
};
|
||||
}
|
||||
|
||||
function allocatePayoutTargets(
|
||||
computations: Map<string, ComputedAddress>,
|
||||
minerRewardSats: number,
|
||||
totalCreditedDifficulty: number,
|
||||
): void {
|
||||
const positiveTargets = [...computations.values()]
|
||||
.filter(row => row.targetSats > 0);
|
||||
const totalPositiveTarget = positiveTargets.reduce((sum, row) => sum + row.targetSats, 0);
|
||||
|
||||
if (totalPositiveTarget <= 0) {
|
||||
const activeRows = [...computations.values()]
|
||||
.filter(row => row.creditedDifficulty > 0);
|
||||
const activeAllocations = allocateIntegerSats(
|
||||
activeRows.map(row => ({ item: row.address, weight: row.creditedDifficulty })),
|
||||
minerRewardSats,
|
||||
totalCreditedDifficulty,
|
||||
);
|
||||
for (const row of activeRows) {
|
||||
row.payoutSats = activeAllocations.get(row.address) ?? 0;
|
||||
row.balanceAfterSats = row.targetSats - row.payoutSats;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (totalPositiveTarget <= minerRewardSats) {
|
||||
for (const row of positiveTargets) {
|
||||
row.payoutSats = row.targetSats;
|
||||
row.balanceAfterSats = 0;
|
||||
}
|
||||
const surplus = minerRewardSats - totalPositiveTarget;
|
||||
if (surplus <= 0) {
|
||||
return;
|
||||
}
|
||||
const activeRows = positiveTargets.filter(row => row.creditedDifficulty > 0);
|
||||
const activeWeight = activeRows.reduce((sum, row) => sum + row.creditedDifficulty, 0);
|
||||
const surplusAllocations = allocateIntegerSats(
|
||||
activeRows.map(row => ({ item: row.address, weight: row.creditedDifficulty })),
|
||||
surplus,
|
||||
activeWeight,
|
||||
);
|
||||
for (const row of activeRows) {
|
||||
const extra = surplusAllocations.get(row.address) ?? 0;
|
||||
row.payoutSats += extra;
|
||||
row.balanceAfterSats -= extra;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const cappedAllocations = allocateIntegerSats(
|
||||
positiveTargets.map(row => ({ item: row.address, weight: row.targetSats })),
|
||||
minerRewardSats,
|
||||
totalPositiveTarget,
|
||||
);
|
||||
for (const row of positiveTargets) {
|
||||
row.payoutSats = cappedAllocations.get(row.address) ?? 0;
|
||||
row.balanceAfterSats = row.targetSats - row.payoutSats;
|
||||
}
|
||||
}
|
||||
|
||||
function redistributeUnfitMinerPayouts(
|
||||
computations: Map<string, ComputedAddress>,
|
||||
minerRewardSats: number,
|
||||
): void {
|
||||
const distributedToMiners = [...computations.values()]
|
||||
.filter(row => row.includedInCoinbase)
|
||||
.reduce((sum, row) => sum + row.payoutSats, 0);
|
||||
const remainder = minerRewardSats - distributedToMiners;
|
||||
if (remainder <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keptActive = [...computations.values()]
|
||||
.filter(row => row.includedInCoinbase && row.creditedDifficulty > 0);
|
||||
const keptDifficulty = keptActive.reduce((sum, row) => sum + row.creditedDifficulty, 0);
|
||||
if (keptDifficulty <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bonusAllocations = allocateIntegerSats(
|
||||
keptActive.map(row => ({ item: row.address, weight: row.creditedDifficulty })),
|
||||
remainder,
|
||||
keptDifficulty,
|
||||
);
|
||||
for (const row of keptActive) {
|
||||
const bonus = bonusAllocations.get(row.address) ?? 0;
|
||||
row.payoutSats += bonus;
|
||||
row.balanceAfterSats -= bonus;
|
||||
}
|
||||
}
|
||||
|
||||
function allocateIntegerSats<T extends string>(
|
||||
targets: AllocationTarget<T>[],
|
||||
amountSats: number,
|
||||
totalWeight: number,
|
||||
): Map<T, number> {
|
||||
const result = new Map<T, number>();
|
||||
if (amountSats <= 0 || totalWeight <= 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
let assigned = 0;
|
||||
const fractions = targets
|
||||
.filter(target => target.weight > 0)
|
||||
.map(target => {
|
||||
const exact = (target.weight * amountSats) / totalWeight;
|
||||
const whole = Math.floor(exact);
|
||||
assigned += whole;
|
||||
result.set(target.item, whole);
|
||||
return {
|
||||
item: target.item,
|
||||
weight: target.weight,
|
||||
fraction: exact - whole,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.fraction - a.fraction || b.weight - a.weight || a.item.localeCompare(b.item));
|
||||
|
||||
let residual = amountSats - assigned;
|
||||
for (const target of fractions) {
|
||||
if (residual <= 0) {
|
||||
break;
|
||||
}
|
||||
result.set(target.item, (result.get(target.item) ?? 0) + 1);
|
||||
residual--;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function mergeFeeEntry(
|
||||
feeEntry: PayoutDistributionEntry,
|
||||
minerEntries: Omit<PayoutDistributionEntry, 'rank'>[],
|
||||
|
||||
Reference in New Issue
Block a user