Files
salvium-rs/test/dynamic-block-size.test.js
T
Matt Hess dfc4651657 ● Fix fee estimation to match Salvium C++ source
Six bugs fixed in tx size/weight estimation: bp_base clawback formula,
  input key offset size, BP range proof size with +3 bytes, padded log
  starting at 2, estimateTransactionFee switched to per-byte weight,
  priority 0 default mapped to Normal. Added getDynamicBaseFee() and
  getDynamicBaseFee2021Scaling() 4-tier fee model. Added chain reorg
  handling: AlternativeChainManager, wallet reorg detection/rollback,
  storage rollback methods. 30 new tests.
2026-01-30 02:56:15 +00:00

164 lines
5.6 KiB
JavaScript

/**
* Dynamic Block Size Scaling Tests
*
* Tests the long-term/short-term block weight median system
* that governs Salvium's dynamic block size limits.
*
* Reference: Salvium blockchain.cpp:5518-5608
*/
import { describe, test, expect } from 'bun:test';
import {
getNextLongTermBlockWeight,
getEffectiveMedianBlockWeight,
getMedianBlockWeight,
ChainState,
CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5,
CRYPTONOTE_SHORT_TERM_BLOCK_WEIGHT_SURGE_FACTOR
} from '../src/consensus.js';
const FRZ = CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5; // 300000
describe('getNextLongTermBlockWeight', () => {
test('weight at median stays unchanged', () => {
expect(getNextLongTermBlockWeight(FRZ, FRZ)).toBe(FRZ);
});
test('weight within ±70% stays unchanged', () => {
const median = 500000;
// 500000 * 10/17 = 294117, 500000 * 17/10 = 850000
expect(getNextLongTermBlockWeight(400000, median)).toBe(400000);
expect(getNextLongTermBlockWeight(700000, median)).toBe(700000);
});
test('weight below lower bound gets clamped up', () => {
const median = 500000;
const lowerBound = Math.floor(median * 10 / 17); // 294117
expect(getNextLongTermBlockWeight(100000, median)).toBe(lowerBound);
});
test('weight above upper bound gets clamped down', () => {
const median = 500000;
const upperBound = median + Math.floor(median * 7 / 10); // 850000
expect(getNextLongTermBlockWeight(1000000, median)).toBe(upperBound);
});
test('uses full reward zone as minimum median', () => {
// Even if longTermMedian is 0, effectiveMedian = max(FRZ, 0) = FRZ
const result = getNextLongTermBlockWeight(FRZ, 0);
expect(result).toBe(FRZ);
});
test('small block gets clamped to lower bound', () => {
const result = getNextLongTermBlockWeight(1000, FRZ);
const lowerBound = Math.floor(FRZ * 10 / 17);
expect(result).toBe(lowerBound);
});
});
describe('getEffectiveMedianBlockWeight', () => {
test('empty weights use full reward zone', () => {
const result = getEffectiveMedianBlockWeight([], [], 2);
expect(result.longTermEffectiveMedian).toBe(FRZ);
expect(result.effectiveMedian).toBe(FRZ);
expect(result.blockLimit).toBe(FRZ * 2);
});
test('all weights at FRZ gives standard limit', () => {
const weights = Array(100).fill(FRZ);
const result = getEffectiveMedianBlockWeight(weights, weights, 2);
expect(result.effectiveMedian).toBe(FRZ);
expect(result.blockLimit).toBe(FRZ * 2);
});
test('large short-term weights increase effective median', () => {
const longTerm = Array(100).fill(FRZ);
const shortTerm = Array(100).fill(FRZ * 2);
const result = getEffectiveMedianBlockWeight(longTerm, shortTerm, 2);
// Short-term median (600000) > long-term effective (300000)
// But clamped to 50 * longTermEffective = 15000000
expect(result.effectiveMedian).toBe(FRZ * 2);
expect(result.blockLimit).toBe(FRZ * 4);
});
test('surge factor caps at 50x', () => {
const longTerm = Array(100).fill(FRZ);
const shortTerm = Array(100).fill(FRZ * 100); // Way above surge limit
const result = getEffectiveMedianBlockWeight(longTerm, shortTerm, 2);
// Capped at 50 * FRZ
expect(result.effectiveMedian).toBe(
CRYPTONOTE_SHORT_TERM_BLOCK_WEIGHT_SURGE_FACTOR * FRZ
);
});
test('short-term below long-term uses long-term', () => {
const longTerm = Array(100).fill(FRZ * 2);
const shortTerm = Array(100).fill(FRZ);
const result = getEffectiveMedianBlockWeight(longTerm, shortTerm, 2);
// Long-term median = 600000 > short-term 300000
expect(result.effectiveMedian).toBe(FRZ * 2);
});
test('full reward zone acts as floor', () => {
const longTerm = Array(100).fill(1000); // Way below FRZ
const shortTerm = Array(100).fill(1000);
const result = getEffectiveMedianBlockWeight(longTerm, shortTerm, 2);
expect(result.longTermEffectiveMedian).toBe(FRZ);
expect(result.effectiveMedian).toBe(FRZ);
});
});
describe('ChainState dynamic block size integration', () => {
test('growing blocks increase block limit', () => {
const cs = new ChainState();
// Add 150 blocks with increasing weights
for (let i = 0; i < 150; i++) {
cs.addBlock(1000 + i * 120, 100n, FRZ + i * 1000);
}
const { blockLimit } = cs.getBlockWeightLimit(2);
// With growing weights, limit should be above minimum
expect(blockLimit).toBeGreaterThanOrEqual(FRZ * 2);
});
test('consistent blocks give stable limit', () => {
const cs = new ChainState();
for (let i = 0; i < 200; i++) {
cs.addBlock(1000 + i * 120, 100n, FRZ);
}
const { blockLimit, effectiveMedian } = cs.getBlockWeightLimit(2);
expect(effectiveMedian).toBe(FRZ);
expect(blockLimit).toBe(FRZ * 2);
});
test('spike in block weight is limited by surge factor', () => {
const cs = new ChainState();
// 100 normal blocks
for (let i = 0; i < 100; i++) {
cs.addBlock(1000 + i * 120, 100n, FRZ);
}
// 50 huge blocks
for (let i = 0; i < 50; i++) {
cs.addBlock(13000 + i * 120, 100n, FRZ * 200);
}
const { effectiveMedian } = cs.getBlockWeightLimit(2);
// Should be capped by surge factor (50x long-term effective median)
expect(effectiveMedian).toBeLessThanOrEqual(
CRYPTONOTE_SHORT_TERM_BLOCK_WEIGHT_SURGE_FACTOR * FRZ * 2
);
});
});
describe('getMedianBlockWeight edge cases', () => {
test('two elements', () => {
expect(getMedianBlockWeight([10, 20])).toBe(15);
});
test('large spread', () => {
expect(getMedianBlockWeight([1, 1000000])).toBe(500000);
});
test('duplicate values', () => {
expect(getMedianBlockWeight([5, 5, 5, 5, 5])).toBe(5);
});
});