// Fifth change of 2026-09-22, after swipepredictbot's fifth review: the zero-reset lower bound, the wash-cycle // detector, and the self-send fix. The original lower-bound fields are covered by lower-bound.test.js and are // asserted unchanged here on the two stored chains. const test = require('node:test'); const assert = require('node:assert'); const fs = require('node:fs'); const path = require('node:path'); const L = require('../ladder'); const OP = 'nano_1xug1q5t7nxoj3ywwzokiea9jz8fq8qfgzp8pbyfr3co3e5xgj755uofu8ue'; const TESTACCT = 'nano_1i3y944esngqw6wb6ia68dotj4yuqctch9kx8ct65twt8ewi4rdcfgax7ggf'; const own = new Set([OP, TESTACCT, 'nano_own']); const N = '1000000000000000000000000000000'; const raw = x => (BigInt(Math.round(x * 1e6)) * BigInt(N) / 1000000n).toString(); const STAKE = raw(0.16); const ENTRANT = 'nano_entrant'; const chain = (self, ops) => { let bal = 0n, h = 0; const blocks = ops.map(([subtype, account, x, link]) => { const amt = BigInt(raw(x)); bal = subtype === 'send' ? bal - amt : bal + amt; h++; return { height: h, subtype, account, amount_raw: amt.toString(), balance_raw: bal.toString(), link: link || 'L' + h, hash: 'H' + h, at: new Date(Date.UTC(2026, 8, h)).toISOString() }; }); return { truncated: false, block_count: blocks.length, checked_at: 'x', blocks }; }; const src = (account, ch, receives = [{ at: '2026-09-15T00:00:00.000Z', amount_raw: raw(0.16), hash: 'R' }]) => ({ account, amount_raw: receives.reduce((a, r) => a + BigInt(r.amount_raw), 0n).toString(), first_at: receives[0].at, last_at: receives[receives.length - 1].at, receives, chain: ch }); const other = () => 'other'; const fixture = name => JSON.parse(fs.readFileSync(path.join(__dirname, 'fixtures', name), 'utf8')); test('the wash: out to any address and straight back collapses the bound to zero, and the cycle detector flags it', () => { const ch = chain('nano_a', [['receive', OP, 0.8], ['send', 'nano_x', 0.8], ['receive', 'nano_x', 0.8], ['send', ENTRANT, 0.8]]); const r = L.lowerBoundFunding([src('nano_a', ch)], own, other, ENTRANT, STAKE, []); const p = r.per_source[0]; assert.strictEqual(p.operator_money_lower_bound_raw, 0n); // sound but powerless, as reviewed assert.deepStrictEqual(p.zero_balance_blocks_in_window, [2]); // the send emptied it assert.strictEqual(p.operator_money_lower_bound_zero_reset_raw, 0n); // the reset does not see through a wash either assert.strictEqual(p.wash_cycle_in_window, true); assert.strictEqual(p.wash_cycle_receipts.length, 1); assert.strictEqual(p.wash_cycle_receipts[0].account, 'nano_x'); assert.strictEqual(p.wash_cycle_receipts[0].earlier_send.height, 2); assert.strictEqual(p.wash_cycle_receipts[0].receive.height, 3); assert.strictEqual(p.wash_cycle_in_transfer_stretch, true); assert.deepStrictEqual([r.shown_at_least_stake, r.shown_at_least_stake_reset, r.wash_cycle_in_any_source], [false, false, true]); }); test('zero reset: dust received before the source emptied cannot be in what it sent afterwards', () => { const ch = chain('nano_a', [['receive', OP, 10], ['receive', 'nano_y', 0.001], ['send', 'nano_z', 10.001], ['receive', OP, 10], ['send', ENTRANT, 0.6], ['send', ENTRANT, 0.2]]); const r = L.lowerBoundFunding([src('nano_a', ch)], own, other, ENTRANT, STAKE, []); const p = r.per_source[0]; assert.strictEqual(p.operator_money_lower_bound_raw, BigInt(raw(0.799))); // original figure, unchanged assert.deepStrictEqual(p.zero_balance_blocks_in_window, [3]); assert.strictEqual(p.operator_money_lower_bound_zero_reset_raw, BigInt(raw(0.8))); assert.strictEqual(p.wash_cycle_in_window, false); }); test('the stretch sum is never below the single-window figure and adds up across a zero point', () => { // stretch 1: 0.3 to the entrant out of 1 operator + 0.5 other -> max(0, 0.3-0-0.5)=0; stretch 2: 0.2 out of 0.2 operator -> 0.2 const ch = chain('nano_a', [['receive', OP, 1], ['receive', 'nano_y', 0.5], ['send', ENTRANT, 0.3], ['send', 'nano_z', 1.2], ['receive', OP, 0.2], ['send', ENTRANT, 0.2]]); const r = L.lowerBoundFunding([src('nano_a', ch)], own, other, ENTRANT, STAKE, []); const p = r.per_source[0]; assert.strictEqual(p.operator_money_lower_bound_raw, 0n); // 0.5 - 0 - 0.5 assert.strictEqual(p.operator_money_lower_bound_zero_reset_raw, BigInt(raw(0.2))); assert.deepStrictEqual([r.shown_at_least_stake, r.shown_at_least_stake_reset], [false, true]); }); test('self-send across a zero point is not a reset: the raw comes back and counts as a receipt', () => { const ch = chain('nano_a', [['receive', 'nano_y', 50], ['receive', OP, 0.16], ['send', 'nano_a', 50.16], ['receive', 'nano_a', 50.16], ['send', ENTRANT, 0.16]]); const r = L.lowerBoundFunding([src('nano_a', ch)], own, other, ENTRANT, STAKE, []); const p = r.per_source[0]; assert.deepStrictEqual(p.zero_balance_blocks_in_window, [3]); assert.strictEqual(p.operator_money_lower_bound_zero_reset_raw, 0n); // 0.16 - 0 - 50.16 < 0 assert.strictEqual(p.operator_money_lower_bound_raw, 0n); // original: 0.16 - 50 - 0 < 0 }); test('self-send pending across the first operator payment counts as a receipt (the one case where the original figure moves)', () => { const ch = chain('nano_a', [['receive', 'nano_y', 1], ['send', 'nano_a', 1], ['receive', OP, 0.16], ['receive', 'nano_a', 1], ['send', ENTRANT, 1]]); const r = L.lowerBoundFunding([src('nano_a', ch)], own, other, ENTRANT, STAKE, []); const p = r.per_source[0]; assert.strictEqual(p.balance_before_first_operator_payment_raw, 0n); assert.strictEqual(p.non_operator_received_before_last_transfer_raw, BigInt(raw(1))); // was 0 under the old rule, which gave lower = 1 assert.strictEqual(p.operator_money_lower_bound_raw, 0n); }); test('a self-send and self-receive inside the window still add no raw and do not count', () => { const ch = chain('nano_a', [['receive', OP, 1], ['send', 'nano_a', 0.5], ['receive', 'nano_a', 0.5], ['send', ENTRANT, 0.16]]); const r = L.lowerBoundFunding([src('nano_a', ch)], own, other, ENTRANT, STAKE, []); assert.strictEqual(r.per_source[0].non_operator_received_before_last_transfer_raw, 0n); assert.strictEqual(r.per_source[0].operator_money_lower_bound_raw, BigInt(raw(0.16))); assert.strictEqual(r.per_source[0].wash_cycle_in_window, false); // self is not a cycle }); test('a receipt from the operator, or from an address never sent to in the window, is not a cycle', () => { const ch = chain('nano_a', [['send', 'nano_x', 0], ['receive', OP, 1], ['receive', 'nano_x', 0.1], ['send', 'nano_own', 0.1], ['receive', 'nano_own', 0.1], ['send', ENTRANT, 0.16]]); const r = L.lowerBoundFunding([src('nano_a', ch)], own, other, ENTRANT, STAKE, []); assert.strictEqual(r.per_source[0].wash_cycle_in_window, false); // the send to nano_x was before the window }); test('stored chain nano_16fgno (round 2): original 0.799 unchanged, zero reset gives 0.8, a cycle before the zero point is flagged but not in the transfer stretch', () => { const ch = fixture('chain-nano_16fgnoqwi.json'); const self = 'nano_16fgnoqwia9haruycrgq38ot71bj8zthpmkutomdm7zb94egb7ucg7uk68cm'; const entrant = ch.blocks.find(b => b.height === 18).account; const r = L.lowerBoundFunding([src(self, ch)], own, other, entrant, STAKE, []); const p = r.per_source[0]; assert.strictEqual(L.nanoStr(p.operator_money_lower_bound_raw), '0.799'); assert.deepStrictEqual(p.zero_balance_blocks_in_window, [16]); assert.strictEqual(L.nanoStr(p.operator_money_lower_bound_zero_reset_raw), '0.8'); assert.strictEqual(p.wash_cycle_in_window, true); // 0.01+0.005x3 out to nano_3khufy3jt at 9-12, 0.001 back at 13 assert.strictEqual(p.wash_cycle_receipts.length, 1); assert.strictEqual(p.wash_cycle_receipts[0].receive.height, 13); assert.strictEqual(p.wash_cycle_receipts[0].earlier_send.height, 12); assert.strictEqual(p.wash_cycle_in_transfer_stretch, false); // it was emptied out at block 16, before the transfers assert.deepStrictEqual([r.shown_at_least_stake, r.shown_at_least_stake_reset], [true, true]); }); test('stored chain nano_3uojbn47b (round 2): original 0.37547 unchanged, no zero point, three cycles (one of them dust) in the transfer stretch', () => { const ch = fixture('chain-nano_3uojbn47b.json'); const self = 'nano_3uojbn47b5xqcbs4yibbasamn8aeyqxgyi1z8peogwtdn6z3kagjanjpz4ss'; const entrant = ch.blocks.find(b => b.height === 48).account; const r = L.lowerBoundFunding([src(self, ch)], own, other, entrant, STAKE, []); const p = r.per_source[0]; assert.strictEqual(L.nanoStr(p.operator_money_lower_bound_raw), '0.375469959999999999999320522752'); assert.deepStrictEqual(p.zero_balance_blocks_in_window, []); assert.strictEqual(p.operator_money_lower_bound_zero_reset_raw, p.operator_money_lower_bound_raw); assert.strictEqual(p.wash_cycle_in_window, true); assert.deepStrictEqual(p.wash_cycle_receipts.map(c => [c.earlier_send.height, c.receive.height]), [[3, 6], [10, 11], [45, 46]]); // nano_3njeurfzg, nano_318agkr46, nano_3e3o8keqr (dust) assert.strictEqual(p.wash_cycle_in_transfer_stretch, true); assert.deepStrictEqual([r.shown_at_least_stake, r.shown_at_least_stake_reset, r.wash_cycle_in_any_source], [true, true, true]); });