test: pin exact lovelace at every money call site, not just in the helper
apexToLovelace's own unit tests prove the helper is exact, but nothing proved any of its 5 call sites actually invoke it - reverting buildSendApex's call site to Math.floor(amountApex * 1_000_000) left the full build suite at 26/26 green, because every existing amountApex/ apexAmount/stakeApex fixture is a whole integer. Adds one call-site pin each for buildSendApex, buildSendTokens' apexAmount path (both build.test.ts), and buildEndorse (gov-build.test.ts, the simpler of the two stake fixtures), each using a fractional amount confirmed via node -e to actually lose a lovelace under the old Math.floor path, decoding the built tx and asserting the recipient/script output's coin is the exact expected lovelace. buildSendTokens needed its own defect value (1.005 AP3X undershoots this output's min-UTxO once it carries a native asset, an unrelated ledger rule) - reused buildEndorse's verified 8.03 AP3X instead. buildCritique and buildProposalLock's call sites are structurally identical to buildEndorse's and already covered by apexToLovelace's own exhaustive unit tests; one representative call-site pin plus the two build.ts sites is the load-bearing set, not all five. Proved each pin load-bearing by reverting its call site to Math.floor one at a time: each mutation failed exactly the matching test (naming the lost lovelace: 1004999 vs 1005000, 8029999 vs 8030000, 8029999 vs 8030000) and no other test, then was reverted byte-identical (confirmed via git diff). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -133,6 +133,24 @@ describe('buildSendApex', () => {
|
||||
await assert.rejects(() => buildSendApex(lucid, { recipientAddress: FOREIGN_ADDRESS, amountApex: 0 }), /positive/);
|
||||
await assert.rejects(() => buildSendApex(lucid, { recipientAddress: FOREIGN_ADDRESS, amountApex: -5 }), /positive/);
|
||||
});
|
||||
|
||||
test('pins exact lovelace at the call site for a fractional amount float math gets wrong', async () => {
|
||||
// 1.005 * 1_000_000 === 1004999.9999999999 in IEEE-754 (node -e
|
||||
// "console.log(Math.floor(1.005*1e6))" -> 1004999, one lovelace short of
|
||||
// the correct 1005000; see packages/shared/test/tx.test.ts for the full
|
||||
// empirical proof). apexToLovelace itself is already proven exact there -
|
||||
// this test instead pins that buildSendApex's call site actually invokes
|
||||
// it, so a future edit reverting to Math.floor(amountApex * 1_000_000)
|
||||
// fails HERE, not just in an untouched helper's own unit tests.
|
||||
const r = await buildSendApex(lucid, { recipientAddress: FOREIGN_ADDRESS, amountApex: 1.005 });
|
||||
const outs = decodeOutputs(r.txCbor);
|
||||
const toRecipient = outs.filter((o) => o.address === FOREIGN_ADDRESS);
|
||||
assert.equal(toRecipient.length, 1);
|
||||
assert.equal(
|
||||
toRecipient[0].lovelace, 1_005_000n,
|
||||
'lost a lovelace - buildSendApex must call apexToLovelace, not Math.floor(amountApex * 1_000_000)',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildSendTokens', () => {
|
||||
@@ -181,4 +199,37 @@ describe('buildSendTokens', () => {
|
||||
/positive/,
|
||||
);
|
||||
});
|
||||
|
||||
test('pins exact lovelace at the call site on the apexAmount path for a fractional amount float math gets wrong', async () => {
|
||||
// Same defect class as buildSendApex's pin above, but 1.005 AP3X
|
||||
// (1,005,000 lovelace) turned out to be BELOW this output's actual
|
||||
// min-UTxO once it carries a native asset - Lucid silently bumped it to
|
||||
// 1,043,020 during .complete(), which would have made this test measure
|
||||
// ledger dust-floor behavior instead of the float-conversion bug it
|
||||
// exists to pin (confirmed empirically: that bump reproduces regardless
|
||||
// of amountApex's exactness, so it is not this defect). Reused the
|
||||
// endorse test's already-verified defect value instead - comfortably
|
||||
// above min-UTxO here too, since the existing 2,000,000n default (see
|
||||
// 'moves the token...' above) is already sufficient for this same
|
||||
// single-token output shape:
|
||||
// 8.03 * 1_000_000 === 8029999.999999999 in IEEE-754 (node -e
|
||||
// "console.log(Math.floor(8.03*1e6))" -> 8029999, one lovelace short of
|
||||
// the correct 8030000). This is the apexAmount branch's OWN call site
|
||||
// (build.ts's buildSendTokens, not buildSendApex), so it needs its own
|
||||
// pin; a shared helper test cannot prove either caller wires it up.
|
||||
const r = await buildSendTokens(lucid, {
|
||||
recipientAddress: FOREIGN_ADDRESS,
|
||||
policyId: FIXTURE_TOKEN_UNIT.slice(0, 56),
|
||||
assetName: FIXTURE_TOKEN_UNIT.slice(56),
|
||||
amount: '50',
|
||||
apexAmount: 8.03,
|
||||
});
|
||||
const outs = decodeOutputs(r.txCbor);
|
||||
const toRecipient = outs.find((o) => o.address === FOREIGN_ADDRESS);
|
||||
assert.ok(toRecipient, 'no output to recipient');
|
||||
assert.equal(
|
||||
toRecipient.lovelace, 8_030_000n,
|
||||
'lost a lovelace - buildSendTokens apexAmount path must call apexToLovelace, not Math.floor(apexAmount * 1_000_000)',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -326,6 +326,31 @@ describe('buildEndorse (offline, FixtureProvider)', () => {
|
||||
assert.equal(endorseOut!.amount().coin(), 7_000_000n, 'exactly-minimum stake (5 AP3X) must still build: 5 + 2 AP3X');
|
||||
});
|
||||
|
||||
test('pins exact lovelace at the call site for a fractional stake float math gets wrong', async () => {
|
||||
// 10.005 and 5.005 (the "obvious" values at each family's minimum) both
|
||||
// happen to be bit-exact under Math.floor(x * 1e6) on this Node/V8 build
|
||||
// (verified: node -e "console.log(Math.floor(10.005*1e6), Math.floor(5.005*1e6))"
|
||||
// -> 10005000 5005000, both correct - neither reproduces the defect).
|
||||
// Scanned upward from the 5 AP3X endorse minimum
|
||||
// for a real loser instead: 8.03 * 1_000_000 === 8029999.999999999 in
|
||||
// IEEE-754 (node -e "console.log(Math.floor(8.03*1e6))" -> 8029999, one
|
||||
// lovelace short of the correct 8030000). This pins buildEndorse's OWN
|
||||
// call site - apexToLovelace itself is already proven exact in
|
||||
// packages/shared/test/tx.test.ts; this test instead catches a future
|
||||
// revert of THIS call site back to Math.floor(stakeApex * 1_000_000).
|
||||
const r = await buildEndorse(lucid, {
|
||||
agentDid: AGENT_DID, proposalTxHash: PROPOSAL_TX, proposalOutputIndex: 3, stakeApex: 8.03,
|
||||
});
|
||||
assert.equal(r.stakeLovelace, '8030000');
|
||||
const tx = decodeTx(r.txCbor);
|
||||
const endorseOut = findOutputTo(tx, r.scriptAddress);
|
||||
assert.ok(endorseOut, 'no output to the endorsement address');
|
||||
assert.equal(
|
||||
endorseOut!.amount().coin(), 10_030_000n,
|
||||
'lost a lovelace - buildEndorse must call apexToLovelace, not Math.floor(stakeApex * 1_000_000): stake (8.03 AP3X = 8,030,000 lovelace) + 2 AP3X rider (2,000,000) = 10,030,000',
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects a stake below the minimum', async () => {
|
||||
await assert.rejects(
|
||||
buildEndorse(lucid, {
|
||||
|
||||
Reference in New Issue
Block a user