forked from yvv/decision
v3 : relecture de 7ffa4fd — un refus du nœud lâche le bulletin, le reçu ne dit vérifié que ce que scelle l'empreinte
polkadot-api lève InvalidTxError (JSON indenté) quand le nœud refuse la transaction : elle est désormais lue comme « la chaîne a dit non » (rien n'est parti, le bulletin est lâché), au lieu de rester en attente 24 h. La page /bulletins ne présente comme vérifiés que la version, la révision et le choix ; l'article, le titre et le bloc du reçu sont dits hors du sceau. ADR 0008 : trois affirmations corrigées (indexeur sans filtre, débit de la tâche de fond, lectures qui n'utilisent pas encore shown_tally). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -117,8 +117,10 @@ le dernier bloc définitif traité. Relevé le 2026-10-31 sur indexer.duniter.or
|
|||||||
3 407 342 contre 3 407 345…), les 2 à 3 blocs de la finalité de Duniter v2. Seul un commentaire
|
3 407 342 contre 3 407 345…), les 2 à 3 blocs de la finalité de Duniter v2. Seul un commentaire
|
||||||
d'un bloc ≤ `height` compte, ou refuse ; au-dessus, le bulletin attend.
|
d'un bloc ≤ `height` compte, ou refuse ; au-dessus, le bulletin attend.
|
||||||
|
|
||||||
Le moteur (`ld_engine::ballot::judge`) juge à nouveau ce qui revient — un indexeur qui ignore le
|
Le moteur (`ld_engine::ballot::judge`) juge à nouveau ce qui revient. Un indexeur qui ignorerait
|
||||||
filtre ne change que le nombre de pages. Il garde le premier commentaire, dans l'ordre de la
|
le filtre ne change d'abord que le nombre de pages, mais au-delà de 1 000 copies nues placées
|
||||||
|
avant le vrai commentaire (20 pages de 50), celui-ci n'est plus lu et le bulletin reste en attente
|
||||||
|
(relecture de `7ffa4fd`) : cas théorique, un PostGraphile sans ce filtre répond par une erreur. Il garde le premier commentaire, dans l'ordre de la
|
||||||
chaîne, qui n'est pas une copie : pour un bulletin lié, un commentaire d'un autre compte est une
|
chaîne, qui n'est pas une copie : pour un bulletin lié, un commentaire d'un autre compte est une
|
||||||
copie ; pour un bulletin Ğecko, **un commentaire sans le virement est une copie** (une remarque
|
copie ; pour un bulletin Ğecko, **un commentaire sans le virement est une copie** (une remarque
|
||||||
seule ne coûte rien à envoyer). Puis il exige :
|
seule ne coûte rien à envoyer). Puis il exige :
|
||||||
@@ -138,8 +140,9 @@ autre session ne voit jamais ce qu'a voté la même identité ailleurs.
|
|||||||
**La tâche de fond** passe toutes les 10 secondes et lit **50 bulletins en attente au plus, le
|
**La tâche de fond** passe toutes les 10 secondes et lit **50 bulletins en attente au plus, le
|
||||||
moins récemment lu d'abord** (jamais lu, puis la plus ancienne lecture) : chaque bulletin en
|
moins récemment lu d'abord** (jamais lu, puis la plus ancienne lecture) : chaque bulletin en
|
||||||
attente est lu au moins toutes les ⌈n / 50⌉ passes, que la page l'attende ou non (un code Ğecko,
|
attente est lu au moins toutes les ⌈n / 50⌉ passes, que la page l'attende ou non (un code Ğecko,
|
||||||
une fenêtre fermée). Il faudrait plus de 430 000 bulletins en attente pour qu'un seul ne soit pas
|
une fenêtre fermée). Une lecture de l'indexeur prend 0,5 à 2,1 s (mesuré le 31/10), donc une passe
|
||||||
lu dans ses 24 heures. La page, elle, fait lire son bulletin au plus toutes les 3 secondes.
|
de 50 bulletins dure 25 à 100 s : il faudrait de l'ordre de 40 000 à 170 000 bulletins en attente
|
||||||
|
pour qu'un seul ne soit pas lu dans ses 24 heures. La page, elle, fait lire son bulletin au plus toutes les 3 secondes.
|
||||||
|
|
||||||
Un bulletin non vu en **24 heures** est **lu une dernière fois à son échéance**, puis lâché ;
|
Un bulletin non vu en **24 heures** est **lu une dernière fois à son échéance**, puis lâché ;
|
||||||
s'aucun indexeur ne répond, ou si son commentaire attend un bloc définitif, il reste en attente
|
s'aucun indexeur ne répond, ou si son commentaire attend un bloc définitif, il reste en attente
|
||||||
@@ -201,7 +204,9 @@ Recoupée avec les totaux affichés en direct, la chaîne peut laisser deviner u
|
|||||||
qui bouge juste après un vote) : une décision d'Yvv attend (secret par paliers, ou totaux en
|
qui bouge juste après un vote) : une décision d'Yvv attend (secret par paliers, ou totaux en
|
||||||
direct). En attendant, les totaux affichés passent par une seule fonction,
|
direct). En attendant, les totaux affichés passent par une seule fonction,
|
||||||
`DocState::shown_tally` (et `shown_evaluation`), où une publication par paliers se branchera ;
|
`DocState::shown_tally` (et `shown_evaluation`), où une publication par paliers se branchera ;
|
||||||
l'adoption lit `tally`, et les textes publiés (`publish.rs`) suivraient `shown_tally`.
|
l'adoption lit `tally`. Deux lectures n'y passent pas encore et lisent les totaux en direct :
|
||||||
|
l'aperçu des propositions bien cotées (`docs.rs`, `unit_states` et `proposal_states`) et les
|
||||||
|
textes publiés (`publish.rs`). Elles s'y brancheront avec la décision d'Yvv.
|
||||||
|
|
||||||
### Ce que sait le serveur
|
### Ce que sait le serveur
|
||||||
|
|
||||||
|
|||||||
@@ -126,7 +126,10 @@ test('C33 — le reçu de ton vote : téléchargé, retrouvé dans tes bulletins
|
|||||||
// Checked here, in the browser, without the server.
|
// Checked here, in the browser, without the server.
|
||||||
await page.getByLabel('Fichier du reçu').setInputFiles(file);
|
await page.getByLabel('Fichier du reçu').setInputFiles(file);
|
||||||
await expect(page.getByText('Le reçu tient : ses votes et son sel donnent bien ce commentaire.')).toBeVisible();
|
await expect(page.getByText('Le reçu tient : ses votes et son sel donnent bien ce commentaire.')).toBeVisible();
|
||||||
await expect(page.getByText(`${NEVER_ALONE} : contre`)).toBeVisible();
|
// Only what the seal covers is said verified: the version, its revision and the choice.
|
||||||
|
await expect(page.locator('ul.lines li').first()).toContainText(`Version ${receipt.lines[0].proposalId}, révision ${receipt.lines[0].revision} : contre`);
|
||||||
|
// The article and its title are written in the receipt, outside the seal (relecture of 7ffa4fd).
|
||||||
|
await expect(page.getByText(`${NEVER_ALONE} — noté dans le reçu, hors du sceau`, { exact: false })).toBeVisible();
|
||||||
// One choice changed: it no longer holds.
|
// One choice changed: it no longer holds.
|
||||||
const changed = { ...receipt, lines: receipt.lines.map((l: object) => ({ ...l, choice: 'for' })) };
|
const changed = { ...receipt, lines: receipt.lines.map((l: object) => ({ ...l, choice: 'for' })) };
|
||||||
await page.getByLabel('Ou colle le reçu').fill(JSON.stringify(changed));
|
await page.getByLabel('Ou colle le reçu').fill(JSON.stringify(changed));
|
||||||
|
|||||||
@@ -298,9 +298,10 @@ export const en: Messages = {
|
|||||||
'ballots.verify.paste': 'Or paste the receipt',
|
'ballots.verify.paste': 'Or paste the receipt',
|
||||||
'ballots.verify.go': 'Check',
|
'ballots.verify.go': 'Check',
|
||||||
'ballots.verify.ok': 'The receipt holds: its votes and its salt give this very comment.',
|
'ballots.verify.ok': 'The receipt holds: its votes and its salt give this very comment.',
|
||||||
'ballots.verify.line': '{article} · {title}: {choice}',
|
'ballots.verify.line': 'Version {version}, revision {revision}: {choice}',
|
||||||
|
'ballots.verify.line.label': '{label} — written in the receipt, outside the seal',
|
||||||
'ballots.verify.where': 'Look for this comment in the history of your wallet: it is your vote, written on the Ğ1 chain.',
|
'ballots.verify.where': 'Look for this comment in the history of your wallet: it is your vote, written on the Ğ1 chain.',
|
||||||
'ballots.verify.where.block': 'Look for this comment in the history of your wallet, or on the Ğ1 chain at block {block}: it is your vote.',
|
'ballots.verify.where.block': 'Look for this comment in the history of your wallet, or on the Ğ1 chain (the receipt says block {block}, outside the seal): if it is there, signed by your account, it is your vote.',
|
||||||
'ballots.verify.bad': 'The receipt does not hold: its votes and its salt do not give its comment.',
|
'ballots.verify.bad': 'The receipt does not hold: its votes and its salt do not give its comment.',
|
||||||
'ballots.verify.computed': 'Recomputed:',
|
'ballots.verify.computed': 'Recomputed:',
|
||||||
'ballots.verify.expected': 'In the receipt:',
|
'ballots.verify.expected': 'In the receipt:',
|
||||||
|
|||||||
@@ -298,9 +298,10 @@ export const es: Messages = {
|
|||||||
'ballots.verify.paste': 'O pega el recibo',
|
'ballots.verify.paste': 'O pega el recibo',
|
||||||
'ballots.verify.go': 'Comprobar',
|
'ballots.verify.go': 'Comprobar',
|
||||||
'ballots.verify.ok': 'El recibo es válido: sus votos y su sal dan exactamente este comentario.',
|
'ballots.verify.ok': 'El recibo es válido: sus votos y su sal dan exactamente este comentario.',
|
||||||
'ballots.verify.line': '{article} · {title}: {choice}',
|
'ballots.verify.line': 'Versión {version}, revisión {revision}: {choice}',
|
||||||
|
'ballots.verify.line.label': '{label} — anotado en el recibo, fuera del sello',
|
||||||
'ballots.verify.where': 'Busca este comentario en el historial de tu monedero: es tu voto, inscrito en la cadena Ğ1.',
|
'ballots.verify.where': 'Busca este comentario en el historial de tu monedero: es tu voto, inscrito en la cadena Ğ1.',
|
||||||
'ballots.verify.where.block': 'Busca este comentario en el historial de tu monedero, o en la cadena Ğ1 en el bloque {block}: es tu voto.',
|
'ballots.verify.where.block': 'Busca este comentario en el historial de tu monedero, o en la cadena Ğ1 (el recibo indica el bloque {block}, fuera del sello): si figura allí, firmado por tu cuenta, es tu voto.',
|
||||||
'ballots.verify.bad': 'El recibo no es válido: sus votos y su sal no dan su comentario.',
|
'ballots.verify.bad': 'El recibo no es válido: sus votos y su sal no dan su comentario.',
|
||||||
'ballots.verify.computed': 'Recalculado:',
|
'ballots.verify.computed': 'Recalculado:',
|
||||||
'ballots.verify.expected': 'En el recibo:',
|
'ballots.verify.expected': 'En el recibo:',
|
||||||
|
|||||||
@@ -301,9 +301,10 @@ export const fr = {
|
|||||||
'ballots.verify.paste': 'Ou colle le reçu',
|
'ballots.verify.paste': 'Ou colle le reçu',
|
||||||
'ballots.verify.go': 'Vérifier',
|
'ballots.verify.go': 'Vérifier',
|
||||||
'ballots.verify.ok': 'Le reçu tient : ses votes et son sel donnent bien ce commentaire.',
|
'ballots.verify.ok': 'Le reçu tient : ses votes et son sel donnent bien ce commentaire.',
|
||||||
'ballots.verify.line': '{article} · {title} : {choice}',
|
'ballots.verify.line': 'Version {version}, révision {revision} : {choice}',
|
||||||
|
'ballots.verify.line.label': '{label} — noté dans le reçu, hors du sceau',
|
||||||
'ballots.verify.where': "Cherche ce commentaire dans l'historique de ton portefeuille : c'est ton vote, inscrit sur la chaîne Ğ1.",
|
'ballots.verify.where': "Cherche ce commentaire dans l'historique de ton portefeuille : c'est ton vote, inscrit sur la chaîne Ğ1.",
|
||||||
'ballots.verify.where.block': "Cherche ce commentaire dans l'historique de ton portefeuille, ou sur la chaîne Ğ1 au bloc {block} : c'est ton vote.",
|
'ballots.verify.where.block': "Cherche ce commentaire dans l'historique de ton portefeuille, ou sur la chaîne Ğ1 (le reçu indique le bloc {block}, hors du sceau) : s'il y figure, signé de ton compte, c'est ton vote.",
|
||||||
'ballots.verify.bad': 'Le reçu ne tient pas : ses votes et son sel ne donnent pas son commentaire.',
|
'ballots.verify.bad': 'Le reçu ne tient pas : ses votes et son sel ne donnent pas son commentaire.',
|
||||||
'ballots.verify.computed': 'Recalculé :',
|
'ballots.verify.computed': 'Recalculé :',
|
||||||
'ballots.verify.expected': 'Dans le reçu :',
|
'ballots.verify.expected': 'Dans le reçu :',
|
||||||
|
|||||||
@@ -73,6 +73,12 @@ export function injectedNames(page: Page): string[] {
|
|||||||
/** What went wrong, in one of a few words the interface translates. */
|
/** What went wrong, in one of a few words the interface translates. */
|
||||||
export function failure(e: unknown): Failure {
|
export function failure(e: unknown): Failure {
|
||||||
if (e instanceof WalletError) return e.code;
|
if (e instanceof WalletError) return e.code;
|
||||||
|
// polkadot-api's refusal by the node's pool (stale, bad proof, too late…): never sent.
|
||||||
|
// Its message is indented JSON, so the reason is read from the error itself.
|
||||||
|
if (e instanceof Error && e.name === 'InvalidTxError') {
|
||||||
|
const reason = JSON.stringify((e as Error & { error?: unknown }).error ?? e.message).toLowerCase();
|
||||||
|
return /payment|funds|balance|existential/.test(reason) ? 'funds' : 'chain';
|
||||||
|
}
|
||||||
let text: string;
|
let text: string;
|
||||||
if (e instanceof Error) text = e.message;
|
if (e instanceof Error) text = e.message;
|
||||||
else {
|
else {
|
||||||
@@ -86,7 +92,7 @@ export function failure(e: unknown): Failure {
|
|||||||
if (/cancel|reject|denied|declined/.test(text)) return 'cancelled';
|
if (/cancel|reject|denied|declined/.test(text)) return 'cancelled';
|
||||||
if (/payment|funds|balance|existential|pay some fees/.test(text)) return 'funds';
|
if (/payment|funds|balance|existential|pay some fees/.test(text)) return 'funds';
|
||||||
// Refused by the node's pool (stale, bad proof…): it will never be included.
|
// Refused by the node's pool (stale, bad proof…): it will never be included.
|
||||||
if (/"type":"invalid"|invalid transaction/.test(text)) return 'chain';
|
if (/"type":\s*"invalid"|invalid transaction/.test(text)) return 'chain';
|
||||||
return 'wallet';
|
return 'wallet';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,10 @@
|
|||||||
<code class="comment">{check.comment}</code>
|
<code class="comment">{check.comment}</code>
|
||||||
<ul class="lines">
|
<ul class="lines">
|
||||||
{#each check.lines as l (l.proposalId)}
|
{#each check.lines as l (l.proposalId)}
|
||||||
<li>{t('ballots.verify.line', { article: l.article ?? '', title: l.title ?? l.proposalId, choice: t(`vote.choice.${l.choice}`) })}</li>
|
<li>
|
||||||
|
{t('ballots.verify.line', { choice: t(`vote.choice.${l.choice}`), version: l.proposalId, revision: i18n.number(l.revision) })}
|
||||||
|
{#if l.article || l.title}<span class="faint small unsealed">{t('ballots.verify.line.label', { label: [l.article, l.title].filter(Boolean).join(' · ') })}</span>{/if}
|
||||||
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
<p class="faint small">
|
<p class="faint small">
|
||||||
@@ -196,6 +199,9 @@
|
|||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
.unsealed {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
.ok {
|
.ok {
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: var(--moss);
|
color: var(--moss);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { InvalidTxError } from 'polkadot-api';
|
||||||
import { amountText, geckoLink } from '../../src/lib/wallet/gecko';
|
import { amountText, geckoLink } from '../../src/lib/wallet/gecko';
|
||||||
import { failure, injectedNames, mayLetGo, testExtrinsic } from '../../src/lib/wallet/injected';
|
import { failure, injectedNames, mayLetGo, testExtrinsic } from '../../src/lib/wallet/injected';
|
||||||
|
|
||||||
@@ -35,6 +36,17 @@ describe('wallets of the page', () => {
|
|||||||
expect(failure({ code: 1010, message: 'Invalid Transaction' })).toBe('chain');
|
expect(failure({ code: 1010, message: 'Invalid Transaction' })).toBe('chain');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('a refusal of the node, as polkadot-api raises it, is the chain saying no (relecture of 7ffa4fd)', () => {
|
||||||
|
// The real class: its message is indented JSON (`"type": "Invalid"`, with a space).
|
||||||
|
for (const reason of ['Stale', 'AncientBirthBlock', 'BadProof', 'ExhaustsResources', 'Future']) {
|
||||||
|
const e = new InvalidTxError({ type: 'Invalid', value: { type: reason } });
|
||||||
|
expect(failure(e), reason).toBe('chain');
|
||||||
|
expect(mayLetGo(failure(e), true), reason).toBe(true);
|
||||||
|
}
|
||||||
|
expect(failure(new InvalidTxError({ type: 'Invalid', value: { type: 'Payment' } }))).toBe('funds');
|
||||||
|
expect(failure(new InvalidTxError({ type: 'Unknown', value: { type: 'CannotLookup' } }))).toBe('chain');
|
||||||
|
});
|
||||||
|
|
||||||
it('a ballot is let go when nothing left the page, or when the chain said no — kept when it may still land', () => {
|
it('a ballot is let go when nothing left the page, or when the chain said no — kept when it may still land', () => {
|
||||||
// Before the signature (the node down before sending, a refusal, the window closed): nothing left.
|
// Before the signature (the node down before sending, a refusal, the window closed): nothing left.
|
||||||
for (const f of ['node', 'wallet', 'cancelled', 'funds', 'account', 'chain'] as const) expect(mayLetGo(f, false), f).toBe(true);
|
for (const f of ['node', 'wallet', 'cancelled', 'funds', 'account', 'chain'] as const) expect(mayLetGo(f, false), f).toBe(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user