From 894100edee87ce516cf8385b4a8baeca39755edf Mon Sep 17 00:00:00 2001 From: Badi Ifaoui Date: Thu, 20 Aug 2026 04:51:51 +0000 Subject: [PATCH 1/2] Make the corpus carry the postable marking the definition already asserts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The parity-green sentence was scoped to the postable set, and the corpus had no such set. A README describing a marking the artifact does not have is exactly the drift this crate exists to prevent, so the marking now lives in the corpus next to the vector it describes, where the drift check protects it too. Each of the three refused vectors carries its `error_code`, the chain rule that refuses it, and the sibling that gives its tag an acceptance result. Two have one; `ChannelForceProgressTx` has none and is a named exception, pinned in both directions — removing the exception without giving the tag a postable vector fails, and giving it one without removing the exception fails too. A silent pass needs someone to delete a line, which is the point. `channel create, with account delegates` is new: the only sibling the corpus was actually missing. The `ba_` pointer sibling for the version 2 name update was already committed, so nothing was added for it. The scoring moved into `matrix.rs`, beside the definition rather than inside the script that posts. A non-postable vector leaves the builder half too, for the reason it left the acceptance half: the node has already said it will not take that transaction, so asking whether it builds the same bytes scores a disagreement already recorded as the refusal. `node-exercise.mjs` now fails in both directions. A postable vector the decoder refuses fails the clause; a non-postable vector the node accepts fails as a stale marking, because an exclusion nobody has re-earned is a vector quietly removed from the measurement. Two fixes found by using the thing. `regenerate.mjs --write` had never been run: it reused one field for a path and its contents, so it tried to open a whole corpus as a filename — the check path never noticed because it only compares. And both generators' headers documented an invocation that cannot work, since Node resolves a bare specifier from the importing module's directory; they now point at the harness that copies them next to the install. --- crates/ae-core/tests/generate-vectors.mjs | 97 ++++++++- .../ae-core/tests/vectors/transactions.json | 202 +++++++++++++---- crates/ae-fate/tests/vectors/generate.mjs | 18 +- crates/ae-parity/MATRIX.md | 64 +++--- crates/ae-parity/matrix.json | 93 +++++++- crates/ae-parity/node-exercise.mjs | 42 ++++ crates/ae-parity/regenerate.mjs | 16 +- crates/ae-parity/src/corpus.rs | 43 ++++ crates/ae-parity/src/main.rs | 2 +- crates/ae-parity/src/matrix.rs | 204 ++++++++++++++++++ crates/ae-parity/src/render.rs | 103 ++++++++- crates/ae-parity/tests/gate.rs | 107 +++++++++ 12 files changed, 901 insertions(+), 90 deletions(-) diff --git a/crates/ae-core/tests/generate-vectors.mjs b/crates/ae-core/tests/generate-vectors.mjs index 2c19066..99cde78 100644 --- a/crates/ae-core/tests/generate-vectors.mjs +++ b/crates/ae-core/tests/generate-vectors.mjs @@ -1,7 +1,15 @@ // Generates the transaction vector corpus in `vectors/` from the reference -// JavaScript SDK. Run it from a directory where `@aeternity/aepp-sdk` resolves: +// JavaScript SDK. Do not run it directly — use the harness: // -// node generate-vectors.mjs > vectors/transactions.json +// node ../../ae-parity/regenerate.mjs # check it still reproduces +// node ../../ae-parity/regenerate.mjs --write # take new bytes, review the diff +// +// Node resolves a bare specifier from the importing *module's* directory rather +// than the working directory, so this file cannot see `@aeternity/aepp-sdk` +// wherever it is installed and running it in place always fails. The harness +// installs the version the corpus records for itself, copies this file next to +// that install, runs it, and diffs — which is also what keeps the committed +// bytes honest. // // The corpus is committed so the Rust tests are offline and bisectable, and so // that regenerating it on an SDK bump produces a reviewable diff. Every address @@ -471,6 +479,27 @@ const cases = [ nonce: uint(15), }, ], + [ + // The postable sibling of the case above: same shape, delegates that are + // accounts. A node refuses a contract or an oracle as a delegate, so without + // this the tag would have no accepted vector at all. + 'channel create, with account delegates', + Tag.ChannelCreateTx, + null, + { + initiator: enc(ak1), + initiatorAmount: uint('1000'), + responder: enc(ak2), + responderAmount: uint('2000'), + channelReserve: uint('10'), + lockPeriod: uint(3), + ttl: uint(600), + initiatorDelegateIds: list([enc(ak1)]), + responderDelegateIds: list([enc(ak2)]), + stateHash: enc(st), + nonce: uint(15), + }, + ], [ 'channel deposit', Tag.ChannelDepositTx, @@ -641,11 +670,57 @@ function pinFee(tag, version, params) { return { ...params, fee: uint(minFee) } } +/** + * Vectors whose bytes are correct and whose *transaction* a node will not take. + * + * A committed vector is an assertion about bytes; an accepted transaction is an + * assertion about content, and the two part company here. Each of these was + * reproduced with this same reference sdk both building and wrapping the + * transaction, so none of them is a defect in the Rust core — they are the blind + * spot an offline byte-diff has, and deleting them would delete the evidence. + * + * These are the only chain facts in this file, and they were not measured by the + * sdk. They were measured against an `ae_uat` node by the parity harness, they + * are recorded case by case in `crates/ae-parity/TESTNET.md`, and + * `crates/ae-parity/node-exercise.mjs` re-measures every one of them on each run + * and fails if reality has moved in either direction. A marking that nothing + * re-checks is a marking that rots. + * + * `sibling` names the vector that gives the tag its acceptance result. `null` + * means the tag has none, which is a finding against the tag rather than a hole + * in the corpus, and is carried as a named exception by the harness. + */ +const nonPostable = { + 'name update v2, id pointer': { + errorCode: 'broken_tx', + rule: + 'the node accepts serialised version 2 only when a pointer needs it — a ' + + 'raw ba_ blob — and version 1 when none does. One encoding per content, ' + + 'enforced at decode. The reference sdk serialises whichever version the ' + + 'caller names.', + sibling: 'name update v2, raw pointer', + }, + 'channel create, with delegates': { + errorCode: 'broken_tx', + rule: 'a channel delegate must be an account; this vector uses a contract and an oracle.', + sibling: 'channel create, with account delegates', + }, + 'channel force progress': { + errorCode: 'broken_tx', + rule: + 'refused whatever it contains. Seven variants, crossing payload ' + + 'signedness, update-entry validity and off-chain-trees validity, were ' + + 'refused identically, so the cause is none of those fields.', + sibling: null, + }, +} + const out = { note: 'Generated by generate-vectors.mjs. Do not hand-edit.', sdkVersion, cases: cases.map(([name, tag, version, rawParams]) => { const params = pinFee(tag, version, rawParams) + const refused = nonPostable[name] return { name, tag, @@ -656,8 +731,26 @@ const out = { ...(version != null ? { version } : {}), ...plain(params), }), + postable: refused === undefined, + ...(refused === undefined ? {} : { refusedBy: refused }), } }), } +// A marking naming a vector that does not exist is worse than no marking: it +// reads as covered and asserts nothing. +for (const [name, entry] of Object.entries(nonPostable)) { + if (!cases.some(([caseName]) => caseName === name)) { + throw new Error(`non-postable marking names no such vector: ${name}`) + } + if ( + entry.sibling != null && + !cases.some(([caseName]) => caseName === entry.sibling) + ) { + throw new Error( + `non-postable sibling names no such vector: ${entry.sibling}`, + ) + } +} + process.stdout.write(`${JSON.stringify(out, null, 2)}\n`) diff --git a/crates/ae-core/tests/vectors/transactions.json b/crates/ae-core/tests/vectors/transactions.json index 5e2678e..d60e601 100644 --- a/crates/ae-core/tests/vectors/transactions.json +++ b/crates/ae-core/tests/vectors/transactions.json @@ -28,7 +28,8 @@ "v": "16660000000000" } }, - "tx": "tx_+FEMAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQGhAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAIYPJvVhyAAAAYBOKPYa" + "tx": "tx_+FEMAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQGhAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAIYPJvVhyAAAAYBOKPYa", + "postable": true }, { "name": "spend, every field set", @@ -64,7 +65,8 @@ "v": "16960000000000" } }, - "tx": "tx_+GAMAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQGhAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICiA3gtrOnZAAAhg9szsaAAIIB9CqFaGVsbG9sFwgP" + "tx": "tx_+GAMAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQGhAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICiA3gtrOnZAAAhg9szsaAAIIB9CqFaGVsbG9sFwgP", + "postable": true }, { "name": "spend, to a contract", @@ -92,7 +94,8 @@ "v": "16660000000000" } }, - "tx": "tx_+FEMAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQGhBQMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAYYPJvVhyAAAAYD0Dl2b" + "tx": "tx_+FEMAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQGhBQMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAYYPJvVhyAAAAYD0Dl2b", + "postable": true }, { "name": "spend, to a name", @@ -120,7 +123,8 @@ "v": "16660000000000" } }, - "tx": "tx_+FEMAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQGhAlCYygTo5mrGvATbkThUR6EXecvfzfsug8dFPZFieKO3AYYPJvVhyAAAAYBNyxTk" + "tx": "tx_+FEMAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQGhAlCYygTo5mrGvATbkThUR6EXecvfzfsug8dFPZFieKO3AYYPJvVhyAAAAYBNyxTk", + "postable": true }, { "name": "signed tx", @@ -141,7 +145,8 @@ "v": "tx_+FEMAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQGhAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAYYPJvVhyAAAAYCsIXtD" } }, - "tx": "tx_+JsLAfhCuECrq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6uruFP4UQwBoQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAaEBAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIBhg8m9WHIAAABgMi/ZoY=" + "tx": "tx_+JsLAfhCuECrq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6uruFP4UQwBoQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAaEBAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIBhg8m9WHIAAABgMi/ZoY=", + "postable": true }, { "name": "signed tx, two signatures", @@ -166,7 +171,8 @@ "v": "tx_+FEMAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQGhAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAYYPJvVhyAAAAYCsIXtD" } }, - "tx": "tx_+N0LAfiEuECrq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6uruEDNzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3NuFP4UQwBoQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAaEBAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIBhg8m9WHIAAABgEfrw1U=" + "tx": "tx_+N0LAfiEuECrq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6uruEDNzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3NuFP4UQwBoQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAaEBAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIBhg8m9WHIAAABgEfrw1U=", + "postable": true }, { "name": "name preclaim", @@ -190,7 +196,8 @@ "v": "16620000000000" } }, - "tx": "tx_+E8hAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEDoQMGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBoYPHaUyOAAAWMy6kw==" + "tx": "tx_+E8hAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEDoQMGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBoYPHaUyOAAAWMy6kw==", + "postable": true }, { "name": "name claim", @@ -222,7 +229,8 @@ "v": "16420000000000" } }, - "tx": "tx_+EUgAqEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEEinRlc3QuY2hhaW6CMDmJCteOvFrGIAAAhg7vFERoAACWy7VM" + "tx": "tx_+EUgAqEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEEinRlc3QuY2hhaW6CMDmJCteOvFrGIAAAhg7vFERoAACWy7VM", + "postable": true }, { "name": "name claim, default salt and minimum name fee", @@ -246,7 +254,8 @@ "v": "16660000000000" } }, - "tx": "tx_+FEgAqEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEEmmF2ZXJ5bG9uZ25hbWV0aGF0aXNjLmNoYWluAIfYty1DTIAAhg8m9WHIAADkgPXa" + "tx": "tx_+FEgAqEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEEmmF2ZXJ5bG9uZ25hbWV0aGF0aXNjLmNoYWluAIfYty1DTIAAhg8m9WHIAADkgPXa", + "postable": true }, { "name": "name update v1", @@ -279,7 +288,8 @@ "v": "17780000000000" } }, - "tx": "tx_+IkiAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEFoQJQmMoE6OZqxrwE25E4VEehF3nL3837LoPHRT2RYnijt4MCvyDy8Y5hY2NvdW50X3B1YmtleaEBAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgKCDhCGECu6lYgAAFAPm4A=" + "tx": "tx_+IkiAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEFoQJQmMoE6OZqxrwE25E4VEehF3nL3837LoPHRT2RYnijt4MCvyDy8Y5hY2NvdW50X3B1YmtleaEBAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgKCDhCGECu6lYgAAFAPm4A=", + "postable": true }, { "name": "name update v1, explicit ttls and several pointers", @@ -332,7 +342,8 @@ "v": "19780000000000" } }, - "tx": "tx_+O0iAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEFoQJQmMoE6OZqxrwE25E4VEehF3nL3837LoPHRT2RYnijt4ID6PiW8Y5hY2NvdW50X3B1YmtleaEBAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLwjW9yYWNsZV9wdWJrZXmhBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE8o9jb250cmFjdF9wdWJrZXmhBQMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDeIYR/WPfqACCAry4mwfV" + "tx": "tx_+O0iAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEFoQJQmMoE6OZqxrwE25E4VEehF3nL3837LoPHRT2RYnijt4ID6PiW8Y5hY2NvdW50X3B1YmtleaEBAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLwjW9yYWNsZV9wdWJrZXmhBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE8o9jb250cmFjdF9wdWJrZXmhBQMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDeIYR/WPfqACCAry4mwfV", + "postable": true }, { "name": "name update v1, no pointers", @@ -360,7 +371,8 @@ "v": "16780000000000" } }, - "tx": "tx_+FciAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEFoQJQmMoE6OZqxrwE25E4VEehF3nL3837LoPHRT2RYnijt4MCvyDAgg4Qhg9C5fB4AACt97DW" + "tx": "tx_+FciAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEFoQJQmMoE6OZqxrwE25E4VEehF3nL3837LoPHRT2RYnijt4MCvyDAgg4Qhg9C5fB4AACt97DW", + "postable": true }, { "name": "name update v2, id pointer", @@ -393,7 +405,13 @@ "v": "17800000000000" } }, - "tx": "tx_+IoiAqEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEGoQJQmMoE6OZqxrwE25E4VEehF3nL3837LoPHRT2RYnijt4MCvyDz8o5hY2NvdW50X3B1YmtleaIBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICgg4QhhAwYq1QAACmjBiH" + "tx": "tx_+IoiAqEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEGoQJQmMoE6OZqxrwE25E4VEehF3nL3837LoPHRT2RYnijt4MCvyDz8o5hY2NvdW50X3B1YmtleaIBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICgg4QhhAwYq1QAACmjBiH", + "postable": false, + "refusedBy": { + "errorCode": "broken_tx", + "rule": "the node accepts serialised version 2 only when a pointer needs it — a raw ba_ blob — and version 1 when none does. One encoding per content, enforced at decode. The reference sdk serialises whichever version the caller names.", + "sibling": "name update v2, raw pointer" + } }, { "name": "name update v2, raw pointer", @@ -426,7 +444,8 @@ "v": "17020000000000" } }, - "tx": "tx_+GMiAqEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEGoQJQmMoE6OZqxrwE25E4VEehF3nL3837LoPHRT2RYnijt4MCvyDMy4NyYXeGAmhlbGxvgg4Qhg96xw3YAAD5up17" + "tx": "tx_+GMiAqEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEGoQJQmMoE6OZqxrwE25E4VEehF3nL3837LoPHRT2RYnijt4MCvyDMy4NyYXeGAmhlbGxvgg4Qhg96xw3YAAD5up17", + "postable": true }, { "name": "name transfer", @@ -454,7 +473,8 @@ "v": "17300000000000" } }, - "tx": "tx_+HEkAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEHoQJQmMoE6OZqxrwE25E4VEehF3nL3837LoPHRT2RYnijt6EBAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgKGD7v4WsgAALo/hkg=" + "tx": "tx_+HEkAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEHoQJQmMoE6OZqxrwE25E4VEehF3nL3837LoPHRT2RYnijt6EBAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgKGD7v4WsgAALo/hkg=", + "postable": true }, { "name": "name revoke", @@ -478,7 +498,8 @@ "v": "16620000000000" } }, - "tx": "tx_+E8jAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEIoQJQmMoE6OZqxrwE25E4VEehF3nL3837LoPHRT2RYnijt4YPHaUyOAAA1e0zpQ==" + "tx": "tx_+E8jAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEIoQJQmMoE6OZqxrwE25E4VEehF3nL3837LoPHRT2RYnijt4YPHaUyOAAA1e0zpQ==", + "postable": true }, { "name": "contract create", @@ -529,7 +550,8 @@ "v": "76300000000000" } }, - "tx": "tx_+D8qAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEJgsr+gwgAA4ZFZPpleAAAAABMhDuaygCCyv6fqx39" + "tx": "tx_+D8qAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEJgsr+gwgAA4ZFZPpleAAAAABMhDuaygCCyv6fqx39", + "postable": true }, { "name": "contract create, protocol default ctVersion", @@ -569,7 +591,8 @@ "v": "76300000000000" } }, - "tx": "tx_+D8qAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEJgsr+gwgAA4ZFZPpleAAAAAFMhDuaygCCyv4WYRvw" + "tx": "tx_+D8qAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEJgsr+gwgAA4ZFZPpleAAAAAFMhDuaygCCyv4WYRvw", + "postable": true }, { "name": "contract call", @@ -613,7 +636,8 @@ "v": "181880000000000" } }, - "tx": "tx_+FwrAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEKoQUDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwOGpWs8yrAAAACCYaiEO5rKAILK/nBFiVg=" + "tx": "tx_+FwrAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEKoQUDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwOGpWs8yrAAAACCYaiEO5rKAILK/nBFiVg=", + "postable": true }, { "name": "contract call, to a name", @@ -653,7 +677,8 @@ "v": "181880000000000" } }, - "tx": "tx_+FwrAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEKoQJQmMoE6OZqxrwE25E4VEehF3nL3837LoPHRT2RYnijtwOGpWs8yrAAAAeCYaiEO5rKAILK/sU8diI=" + "tx": "tx_+FwrAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEKoQJQmMoE6OZqxrwE25E4VEehF3nL3837LoPHRT2RYnijtwOGpWs8yrAAAAeCYaiEO5rKAILK/sU8diI=", + "postable": true }, { "name": "oracle register", @@ -685,7 +710,8 @@ "v": "16432000000000" } }, - "tx": "tx_+EEWAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQELhnN0cmluZ4ZzdHJpbmcAAIIB9IYO8d+F4AAAAPGisjA=" + "tx": "tx_+EEWAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQELhnN0cmluZ4ZzdHJpbmcAAIIB9IYO8d+F4AAAAPGisjA=", + "postable": true }, { "name": "oracle register, block ttl and fate abi", @@ -733,7 +759,8 @@ "v": "16666000000000" } }, - "tx": "tx_+D8WAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQELg2ludINpbnSCA+gBggfQhg8oWwKEAIIDhAORHb+6" + "tx": "tx_+D8WAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQELg2ludINpbnSCA+gBggfQhg8oWwKEAIIDhAORHb+6", + "postable": true }, { "name": "oracle extend", @@ -753,7 +780,8 @@ "v": "16092000000000" } }, - "tx": "tx_8RkBoQQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAwAggH0hg6itfGYAAAavkyU" + "tx": "tx_8RkBoQQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAwAggH0hg6itfGYAAAavkyU", + "postable": true }, { "name": "oracle query", @@ -785,7 +813,8 @@ "v": "16862000000000" } }, - "tx": "tx_+FsXAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQENoQQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBIRob3c/ggPoAAoACoYPVf2FLAAAlucZPg==" + "tx": "tx_+FsXAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQENoQQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBIRob3c/ggPoAAoACoYPVf2FLAAAlucZPg==", + "postable": true }, { "name": "oracle query, every ttl explicit", @@ -837,7 +866,8 @@ "v": "16919000000000" } }, - "tx": "tx_+F0XAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQENoQQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBIRob3c/ggPoAWQAMoYPY0L8JgCCAyD7YFam" + "tx": "tx_+F0XAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQENoQQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBIRob3c/ggPoAWQAMoYPY0L8JgCCAyD7YFam", + "postable": true }, { "name": "oracle respond", @@ -865,7 +895,8 @@ "v": "16722000000000" } }, - "tx": "tx_+FQYAaEEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQOoAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIg3llcwAKhg81ZN60AAB0wzS3" + "tx": "tx_+FQYAaEEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQOoAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIg3llcwAKhg81ZN60AAB0wzS3", + "postable": true }, { "name": "channel create", @@ -917,7 +948,8 @@ "v": "17480000000000" } }, - "tx": "tx_+HoyAqEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQGCA+ihAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICggPoCgMAhg/l4TDQAMDAoAcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHD5nllGQ=" + "tx": "tx_+HoyAqEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQGCA+ihAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICggPoCgMAhg/l4TDQAMDAoAcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHD5nllGQ=", + "postable": true }, { "name": "channel create, with delegates", @@ -987,7 +1019,80 @@ "v": "15" } }, - "tx": "tx_+OMyAqEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQGCA+ihAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICggfQCgOCAliGEc7S8dgA4qEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH4RKEFAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwOhBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEoAcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHD7VWtfk=" + "tx": "tx_+OMyAqEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQGCA+ihAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICggfQCgOCAliGEc7S8dgA4qEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH4RKEFAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwOhBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEoAcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHD7VWtfk=", + "postable": false, + "refusedBy": { + "errorCode": "broken_tx", + "rule": "a channel delegate must be an account; this vector uses a contract and an oracle.", + "sibling": "channel create, with account delegates" + } + }, + { + "name": "channel create, with account delegates", + "tag": 50, + "version": null, + "params": { + "initiator": { + "t": "enc", + "v": "ak_SeLqn3UAUoRymWmwW7axrzJK7JfNaBR2cHCryA6cFsgFkHEF" + }, + "initiatorAmount": { + "t": "uint", + "v": "1000" + }, + "responder": { + "t": "enc", + "v": "ak_tHggZ5wKxbrxY2Yt1EAviybdDcKk9Mq4DZQiwKCDWkEaXE8U" + }, + "responderAmount": { + "t": "uint", + "v": "2000" + }, + "channelReserve": { + "t": "uint", + "v": "10" + }, + "lockPeriod": { + "t": "uint", + "v": "3" + }, + "ttl": { + "t": "uint", + "v": "600" + }, + "initiatorDelegateIds": { + "t": "list", + "v": [ + { + "t": "enc", + "v": "ak_SeLqn3UAUoRymWmwW7axrzJK7JfNaBR2cHCryA6cFsgFkHEF" + } + ] + }, + "responderDelegateIds": { + "t": "list", + "v": [ + { + "t": "enc", + "v": "ak_tHggZ5wKxbrxY2Yt1EAviybdDcKk9Mq4DZQiwKCDWkEaXE8U" + } + ] + }, + "stateHash": { + "t": "enc", + "v": "st_BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwfrYL3u" + }, + "nonce": { + "t": "uint", + "v": "15" + }, + "fee": { + "t": "uint", + "v": "18880000000000" + } + }, + "tx": "tx_+MAyAqEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQGCA+ihAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICggfQCgOCAliGESvXsYAA4qEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQHioQECAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAqAHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBw90zpta", + "postable": true }, { "name": "channel deposit", @@ -1023,7 +1128,8 @@ "v": "17360000000000" } }, - "tx": "tx_+HQzAaEGBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQWhAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBggH0AIYPyfCiIACgBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcEEJeF6wg=" + "tx": "tx_+HQzAaEGBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQWhAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBggH0AIYPyfCiIACgBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcEEJeF6wg=", + "postable": true }, { "name": "channel withdraw", @@ -1059,7 +1165,8 @@ "v": "17360000000000" } }, - "tx": "tx_+HQ0AaEGBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQWhAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBggH0AIYPyfCiIACgBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcFEVU9vMw=" + "tx": "tx_+HQ0AaEGBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQWhAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBggH0AIYPyfCiIACgBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcFEVU9vMw=", + "postable": true }, { "name": "channel close mutual", @@ -1091,7 +1198,8 @@ "v": "16740000000000" } }, - "tx": "tx_+FU1AaEGBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQWhAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBggOEggRMAIYPOZXA6AAS80VL4g==" + "tx": "tx_+FU1AaEGBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQWhAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBggOEggRMAIYPOZXA6AAS80VL4g==", + "postable": true }, { "name": "channel close solo", @@ -1123,7 +1231,8 @@ "v": "18300000000000" } }, - "tx": "tx_+KM2AaEGBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQWhAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBuEj4RjkCoQYFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQKgBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBweJyDwBwMDAwMDAAIYQpMz/2AATLUm/4w==" + "tx": "tx_+KM2AaEGBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQWhAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBuEj4RjkCoQYFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQKgBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBweJyDwBwMDAwMDAAIYQpMz/2AATLUm/4w==", + "postable": true }, { "name": "channel slash", @@ -1155,7 +1264,8 @@ "v": "18300000000000" } }, - "tx": "tx_+KM3AaEGBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQWhAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBuEj4RjkCoQYFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQKgBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBweJyDwBwMDAwMDAAIYQpMz/2AAUmb2jtg==" + "tx": "tx_+KM3AaEGBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQWhAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBuEj4RjkCoQYFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQKgBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBweJyDwBwMDAwMDAAIYQpMz/2AAUmb2jtg==", + "postable": true }, { "name": "channel settle", @@ -1187,7 +1297,8 @@ "v": "16740000000000" } }, - "tx": "tx_+FU4AaEGBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQWhAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBggOEggRMAIYPOZXA6AAV5R9i8w==" + "tx": "tx_+FU4AaEGBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQWhAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBggOEggRMAIYPOZXA6AAV5R9i8w==", + "postable": true }, { "name": "channel snapshot solo", @@ -1215,7 +1326,8 @@ "v": "18100000000000" } }, - "tx": "tx_+Jk7AaEGBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQWhAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBuEj4RjkCoQYFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQKgBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcAhhB2PBIIABZ1KJbV" + "tx": "tx_+Jk7AaEGBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQWhAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBuEj4RjkCoQYFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQKgBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcAhhB2PBIIABZ1KJbV", + "postable": true }, { "name": "channel force progress", @@ -1259,7 +1371,13 @@ "v": "453960000000000" } }, - "tx": "tx_+MSCAgkBoQYFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQG4SPhGOQKhBgUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFAqAHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwaCyv6gBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBweCAQIAhwGc38yK0AAXRKoaSA==" + "tx": "tx_+MSCAgkBoQYFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQG4SPhGOQKhBgUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFAqAHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwaCyv6gBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBweCAQIAhwGc38yK0AAXRKoaSA==", + "postable": false, + "refusedBy": { + "errorCode": "broken_tx", + "rule": "refused whatever it contains. Seven variants, crossing payload signedness, update-entry validity and off-chain-trees validity, were refused identically, so the cause is none of those fields.", + "sibling": null + } }, { "name": "channel off-chain", @@ -1279,7 +1397,8 @@ "v": "st_BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwfrYL3u" } }, - "tx": "tx_+EY5AqEGBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUCoAcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcH55xIMw==" + "tx": "tx_+EY5AqEGBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUCoAcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcH55xIMw==", + "postable": true }, { "name": "ga attach", @@ -1326,7 +1445,8 @@ "v": "cb_yv6aT3/a" } }, - "tx": "tx_+GBQAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBgsr+oAkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJgwgAA4ZHZTSdaAAAggPohDuaygCCyv4KLH+D" + "tx": "tx_+GBQAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBgsr+oAkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJgwgAA4ZHZTSdaAAAggPohDuaygCCyv4KLH+D", + "postable": true }, { "name": "ga meta", @@ -1362,7 +1482,8 @@ "v": "77660000000000" } }, - "tx": "tx_+NZRAqEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQGCyv4DhkahoLaYAIITiIQ7msoAuJ34mwsB+EK4QKurq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6u4U/hRDAGhAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBoQECAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgGGDyb1YcgAAAGAltf1RQ==" + "tx": "tx_+NZRAqEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQGCyv4DhkahoLaYAIITiIQ7msoAuJ34mwsB+EK4QKurq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6u4U/hRDAGhAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBoQECAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgGGDyb1YcgAAAGAltf1RQ==", + "postable": true }, { "name": "paying for", @@ -1386,7 +1507,8 @@ "v": "tx_+JsLAfhCuECrq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6uruFP4UQwBoQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAaEBAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIBhg8m9WHIAAABgMi/ZoY=" } }, - "tx": "tx_+MtSAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEYhhGW8dR4ALid+JsLAfhCuECrq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6uruFP4UQwBoQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAaEBAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIBhg8m9WHIAAABgBwcAlw=" + "tx": "tx_+MtSAaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEYhhGW8dR4ALid+JsLAfhCuECrq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6uruFP4UQwBoQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAaEBAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIBhg8m9WHIAAABgBwcAlw=", + "postable": true } ] } diff --git a/crates/ae-fate/tests/vectors/generate.mjs b/crates/ae-fate/tests/vectors/generate.mjs index 5aca17e..72aa891 100644 --- a/crates/ae-fate/tests/vectors/generate.mjs +++ b/crates/ae-fate/tests/vectors/generate.mjs @@ -2,17 +2,25 @@ // `@aeternity/aepp-calldata` (ISC), the JavaScript implementation the aeternity // SDK ships. // -// npm install @aeternity/aepp-calldata@1.9.1 -// node generate.mjs > aepp-calldata-1.9.1.json +// Do not run it directly — use the harness: +// +// node ../../../ae-parity/regenerate.mjs # check it still reproduces +// node ../../../ae-parity/regenerate.mjs --write # take new bytes, review the diff +// +// Node resolves a bare specifier from the importing *module's* directory rather +// than the working directory, so this file cannot see `@aeternity/aepp-calldata` +// wherever it is installed and running it in place always fails. The harness +// installs the version the corpus records for itself, copies this file next to +// that install, runs it, and diffs. // // Every case here has a hand-written twin in `tests/vectors.rs` that builds the // same value through this crate. The test asserts the bytes match and that they // decode back to the value, so the corpus is a cross-implementation check, not // a snapshot of ourselves. // -// This is not the differential harness: that one runs both implementations over -// generated inputs on every change. This is a fixed corpus, regenerated by hand -// when the reference version moves. +// This is the fixed-corpus half of the differential harness — see +// `crates/ae-parity` for the coverage matrix and the on-node exercise built on +// top of it. // The package's `exports` map publishes only its high level encoders, so the // serialiser and the data classes are reached through the resolved entry point diff --git a/crates/ae-parity/MATRIX.md b/crates/ae-parity/MATRIX.md index 752f08b..0f4c425 100644 --- a/crates/ae-parity/MATRIX.md +++ b/crates/ae-parity/MATRIX.md @@ -19,35 +19,41 @@ References: `@aeternity/aepp-sdk` 14.1.1, `@aeternity/aepp-calldata` 1.9.1. `reach` is how a consumer gets here: a named action, or only the generic builder that takes any tag. `origin` is who serialises it in the flow that runs — a node-built tag is proved by the round trip, not by the build. -| Tag | v | Reach | Origin | Vectors | Build | Round trip | Fields not set | Fee fixed point | -|---|---|---|---|---|---|---|---|---| -| `SignedTx` | 1 | action | client | 2 | 2/2 | 2/2 | — | — | -| `SpendTx` | 1 | action | client | 4 | 4/4 | 4/4 | — | **not exercised** | -| `NamePreclaimTx` | 1 | action | client | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | -| `NameClaimTx` | 2 | action | client | 2 | 2/2 | 2/2 | 1 (ttl (Ttl)) | **not exercised** | -| `NameUpdateTx` | 1 | action | client | 3 | 3/3 | 3/3 | — | **not exercised** | -| `NameUpdateTx` | 2 | action | client | 2 | 2/2 | 2/2 | 3 (nameTtl (NameTtl), clientTtl (ShortUIntDefault), ttl (Ttl)) | **not exercised** | -| `NameTransferTx` | 1 | action | client | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | -| `NameRevokeTx` | 1 | action | client | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | -| `ContractCreateTx` | 1 | action | client | 2 | 2/2 | 2/2 | 1 (ttl (Ttl)) | **not exercised** | -| `ContractCallTx` | 1 | action | client | 2 | 2/2 | 2/2 | 1 (ttl (Ttl)) | **not exercised** | -| `OracleRegisterTx` | 1 | action | client | 2 | 2/2 | 2/2 | — | **not exercised** | -| `OracleExtendTx` | 1 | action | client | 1 | 1/1 | 1/1 | 3 (oracleTtlType (OracleTtlType), oracleTtlValue (ShortUIntDefault), ttl (Ttl)) | **not exercised** | -| `OracleQueryTx` | 1 | action | client | 2 | 2/2 | 2/2 | — | **not exercised** | -| `OracleRespondTx` | 1 | action | client | 1 | 1/1 | 1/1 | 3 (responseTtlType (OracleTtlType), responseTtlValue (ShortUIntDefault), ttl (Ttl)) | **not exercised** | -| `ChannelCreateTx` | 2 | action | node | 2 | 2/2 | 2/2 | — | **not exercised** | -| `ChannelCloseMutualTx` | 1 | action | node | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | -| `ChannelCloseSoloTx` | 1 | generic builder only | node | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | -| `ChannelSlashTx` | 1 | generic builder only | node | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | -| `ChannelDepositTx` | 1 | action | node | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | -| `ChannelWithdrawTx` | 1 | action | node | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | -| `ChannelSettleTx` | 1 | generic builder only | node | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | -| `ChannelForceProgressTx` | 1 | generic builder only | node | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | -| `ChannelOffChainTx` | 2 | action | node | 1 | 1/1 | 1/1 | — | — | -| `ChannelSnapshotSoloTx` | 1 | generic builder only | node | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | -| `GaAttachTx` | 1 | action | client | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | -| `GaMetaTx` | 2 | action | client | 1 | 1/1 | 1/1 | — | **not exercised** | -| `PayingForTx` | 1 | action | client | 1 | 1/1 | 1/1 | — | **not exercised** | +| Tag | v | Reach | Origin | Vectors | Postable | Build | Round trip | Fields not set | Fee fixed point | +|---|---|---|---|---|---|---|---|---|---| +| `SignedTx` | 1 | action | client | 2 | 2 | 2/2 | 2/2 | — | — | +| `SpendTx` | 1 | action | client | 4 | 4 | 4/4 | 4/4 | — | **not exercised** | +| `NamePreclaimTx` | 1 | action | client | 1 | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | +| `NameClaimTx` | 2 | action | client | 2 | 2 | 2/2 | 2/2 | 1 (ttl (Ttl)) | **not exercised** | +| `NameUpdateTx` | 1 | action | client | 3 | 3 | 3/3 | 3/3 | — | **not exercised** | +| `NameUpdateTx` | 2 | action | client | 2 | **1/2** | 2/2 | 2/2 | 3 (nameTtl (NameTtl), clientTtl (ShortUIntDefault), ttl (Ttl)) | **not exercised** | +| `NameTransferTx` | 1 | action | client | 1 | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | +| `NameRevokeTx` | 1 | action | client | 1 | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | +| `ContractCreateTx` | 1 | action | client | 2 | 2 | 2/2 | 2/2 | 1 (ttl (Ttl)) | **not exercised** | +| `ContractCallTx` | 1 | action | client | 2 | 2 | 2/2 | 2/2 | 1 (ttl (Ttl)) | **not exercised** | +| `OracleRegisterTx` | 1 | action | client | 2 | 2 | 2/2 | 2/2 | — | **not exercised** | +| `OracleExtendTx` | 1 | action | client | 1 | 1 | 1/1 | 1/1 | 3 (oracleTtlType (OracleTtlType), oracleTtlValue (ShortUIntDefault), ttl (Ttl)) | **not exercised** | +| `OracleQueryTx` | 1 | action | client | 2 | 2 | 2/2 | 2/2 | — | **not exercised** | +| `OracleRespondTx` | 1 | action | client | 1 | 1 | 1/1 | 1/1 | 3 (responseTtlType (OracleTtlType), responseTtlValue (ShortUIntDefault), ttl (Ttl)) | **not exercised** | +| `ChannelCreateTx` | 2 | action | node | 3 | **2/3** | 3/3 | 3/3 | — | **not exercised** | +| `ChannelCloseMutualTx` | 1 | action | node | 1 | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | +| `ChannelCloseSoloTx` | 1 | generic builder only | node | 1 | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | +| `ChannelSlashTx` | 1 | generic builder only | node | 1 | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | +| `ChannelDepositTx` | 1 | action | node | 1 | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | +| `ChannelWithdrawTx` | 1 | action | node | 1 | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | +| `ChannelSettleTx` | 1 | generic builder only | node | 1 | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | +| `ChannelForceProgressTx` | 1 | generic builder only | node | 1 | **0/1** | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | +| `ChannelOffChainTx` | 2 | action | node | 1 | 1 | 1/1 | 1/1 | — | — | +| `ChannelSnapshotSoloTx` | 1 | generic builder only | node | 1 | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | +| `GaAttachTx` | 1 | action | client | 1 | 1 | 1/1 | 1/1 | 1 (ttl (Ttl)) | **not exercised** | +| `GaMetaTx` | 2 | action | client | 1 | 1 | 1/1 | 1/1 | — | **not exercised** | +| `PayingForTx` | 1 | action | client | 1 | 1 | 1/1 | 1/1 | — | **not exercised** | + +**Non-postable vectors.** Correct bytes, content a node refuses. Kept as encoding tests, excluded from the on-node clause, and re-measured on every on-node run — a marking nothing re-checks is a marking that rots. + +- `name update v2, id pointer` — `broken_tx`, postable sibling `name update v2, raw pointer`. the node accepts serialised version 2 only when a pointer needs it — a raw ba_ blob — and version 1 when none does. One encoding per content, enforced at decode. The reference sdk serialises whichever version the caller names. +- `channel create, with delegates` — `broken_tx`, postable sibling `channel create, with account delegates`. a channel delegate must be an account; this vector uses a contract and an oracle. +- `channel force progress` — `broken_tx`, **no postable sibling — named exception**. refused whatever it contains. Seven variants, crossing payload signedness, update-entry validity and off-chain-trees validity, were refused identically, so the cause is none of those fields. No transaction vector diverges from the reference. diff --git a/crates/ae-parity/matrix.json b/crates/ae-parity/matrix.json index c003562..9d4449d 100644 --- a/crates/ae-parity/matrix.json +++ b/crates/ae-parity/matrix.json @@ -38,7 +38,11 @@ ], "vectors": 85 }, + "named_exceptions": [ + "ChannelForceProgressTx" + ], "node": null, + "node_clause": null, "references": { "aepp-calldata": "1.9.1", "aepp-sdk": "14.1.1" @@ -230,13 +234,21 @@ "fields_exercised": 176, "fields_total": 200, "green": 27, + "non_postable_vectors": [ + "name update v2, id pointer", + "channel create, with delegates", + "channel force progress" + ], + "postable_vectors": 38, "rows": [ { "build_pass": 2, "failures": [], "fee_fixed_point_unexercised": false, "origin": "ClientBuilt", + "postable_vectors": 2, "reach": "NamedAction", + "refusals": [], "roundtrip_pass": 2, "tag": "SignedTx", "tag_value": 11, @@ -249,7 +261,9 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "ClientBuilt", + "postable_vectors": 4, "reach": "NamedAction", + "refusals": [], "roundtrip_pass": 4, "tag": "SpendTx", "tag_value": 12, @@ -262,7 +276,9 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "ClientBuilt", + "postable_vectors": 1, "reach": "NamedAction", + "refusals": [], "roundtrip_pass": 1, "tag": "NamePreclaimTx", "tag_value": 33, @@ -277,7 +293,9 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "ClientBuilt", + "postable_vectors": 2, "reach": "NamedAction", + "refusals": [], "roundtrip_pass": 2, "tag": "NameClaimTx", "tag_value": 32, @@ -292,7 +310,9 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "ClientBuilt", + "postable_vectors": 3, "reach": "NamedAction", + "refusals": [], "roundtrip_pass": 3, "tag": "NameUpdateTx", "tag_value": 34, @@ -305,7 +325,16 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "ClientBuilt", + "postable_vectors": 1, "reach": "NamedAction", + "refusals": [ + { + "error_code": "broken_tx", + "rule": "the node accepts serialised version 2 only when a pointer needs it — a raw ba_ blob — and version 1 when none does. One encoding per content, enforced at decode. The reference sdk serialises whichever version the caller names.", + "sibling": "name update v2, raw pointer", + "vector": "name update v2, id pointer" + } + ], "roundtrip_pass": 2, "tag": "NameUpdateTx", "tag_value": 34, @@ -322,7 +351,9 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "ClientBuilt", + "postable_vectors": 1, "reach": "NamedAction", + "refusals": [], "roundtrip_pass": 1, "tag": "NameTransferTx", "tag_value": 36, @@ -337,7 +368,9 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "ClientBuilt", + "postable_vectors": 1, "reach": "NamedAction", + "refusals": [], "roundtrip_pass": 1, "tag": "NameRevokeTx", "tag_value": 35, @@ -352,7 +385,9 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "ClientBuilt", + "postable_vectors": 2, "reach": "NamedAction", + "refusals": [], "roundtrip_pass": 2, "tag": "ContractCreateTx", "tag_value": 42, @@ -367,7 +402,9 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "ClientBuilt", + "postable_vectors": 2, "reach": "NamedAction", + "refusals": [], "roundtrip_pass": 2, "tag": "ContractCallTx", "tag_value": 43, @@ -382,7 +419,9 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "ClientBuilt", + "postable_vectors": 2, "reach": "NamedAction", + "refusals": [], "roundtrip_pass": 2, "tag": "OracleRegisterTx", "tag_value": 22, @@ -395,7 +434,9 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "ClientBuilt", + "postable_vectors": 1, "reach": "NamedAction", + "refusals": [], "roundtrip_pass": 1, "tag": "OracleExtendTx", "tag_value": 25, @@ -412,7 +453,9 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "ClientBuilt", + "postable_vectors": 2, "reach": "NamedAction", + "refusals": [], "roundtrip_pass": 2, "tag": "OracleQueryTx", "tag_value": 23, @@ -425,7 +468,9 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "ClientBuilt", + "postable_vectors": 1, "reach": "NamedAction", + "refusals": [], "roundtrip_pass": 1, "tag": "OracleRespondTx", "tag_value": 24, @@ -438,16 +483,25 @@ "version": 1 }, { - "build_pass": 2, + "build_pass": 3, "failures": [], "fee_fixed_point_unexercised": true, "origin": "NodeBuilt", + "postable_vectors": 2, "reach": "NamedAction", - "roundtrip_pass": 2, + "refusals": [ + { + "error_code": "broken_tx", + "rule": "a channel delegate must be an account; this vector uses a contract and an oracle.", + "sibling": "channel create, with account delegates", + "vector": "channel create, with delegates" + } + ], + "roundtrip_pass": 3, "tag": "ChannelCreateTx", "tag_value": 50, "unexercised_fields": [], - "vectors": 2, + "vectors": 3, "version": 2 }, { @@ -455,7 +509,9 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "NodeBuilt", + "postable_vectors": 1, "reach": "NamedAction", + "refusals": [], "roundtrip_pass": 1, "tag": "ChannelCloseMutualTx", "tag_value": 53, @@ -470,7 +526,9 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "NodeBuilt", + "postable_vectors": 1, "reach": "PublicSurfaceOnly", + "refusals": [], "roundtrip_pass": 1, "tag": "ChannelCloseSoloTx", "tag_value": 54, @@ -485,7 +543,9 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "NodeBuilt", + "postable_vectors": 1, "reach": "PublicSurfaceOnly", + "refusals": [], "roundtrip_pass": 1, "tag": "ChannelSlashTx", "tag_value": 55, @@ -500,7 +560,9 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "NodeBuilt", + "postable_vectors": 1, "reach": "NamedAction", + "refusals": [], "roundtrip_pass": 1, "tag": "ChannelDepositTx", "tag_value": 51, @@ -515,7 +577,9 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "NodeBuilt", + "postable_vectors": 1, "reach": "NamedAction", + "refusals": [], "roundtrip_pass": 1, "tag": "ChannelWithdrawTx", "tag_value": 52, @@ -530,7 +594,9 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "NodeBuilt", + "postable_vectors": 1, "reach": "PublicSurfaceOnly", + "refusals": [], "roundtrip_pass": 1, "tag": "ChannelSettleTx", "tag_value": 56, @@ -545,7 +611,16 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "NodeBuilt", + "postable_vectors": 0, "reach": "PublicSurfaceOnly", + "refusals": [ + { + "error_code": "broken_tx", + "rule": "refused whatever it contains. Seven variants, crossing payload signedness, update-entry validity and off-chain-trees validity, were refused identically, so the cause is none of those fields.", + "sibling": null, + "vector": "channel force progress" + } + ], "roundtrip_pass": 1, "tag": "ChannelForceProgressTx", "tag_value": 521, @@ -560,7 +635,9 @@ "failures": [], "fee_fixed_point_unexercised": false, "origin": "NodeBuilt", + "postable_vectors": 1, "reach": "NamedAction", + "refusals": [], "roundtrip_pass": 1, "tag": "ChannelOffChainTx", "tag_value": 57, @@ -573,7 +650,9 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "NodeBuilt", + "postable_vectors": 1, "reach": "PublicSurfaceOnly", + "refusals": [], "roundtrip_pass": 1, "tag": "ChannelSnapshotSoloTx", "tag_value": 59, @@ -588,7 +667,9 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "ClientBuilt", + "postable_vectors": 1, "reach": "NamedAction", + "refusals": [], "roundtrip_pass": 1, "tag": "GaAttachTx", "tag_value": 80, @@ -603,7 +684,9 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "ClientBuilt", + "postable_vectors": 1, "reach": "NamedAction", + "refusals": [], "roundtrip_pass": 1, "tag": "GaMetaTx", "tag_value": 81, @@ -616,7 +699,9 @@ "failures": [], "fee_fixed_point_unexercised": true, "origin": "ClientBuilt", + "postable_vectors": 1, "reach": "NamedAction", + "refusals": [], "roundtrip_pass": 1, "tag": "PayingForTx", "tag_value": 82, @@ -627,6 +712,6 @@ ], "schema_entries": 27, "unexercised_codecs": [], - "vectors": 40 + "vectors": 41 } } diff --git a/crates/ae-parity/node-exercise.mjs b/crates/ae-parity/node-exercise.mjs index 207d5ff..d5e5ed3 100644 --- a/crates/ae-parity/node-exercise.mjs +++ b/crates/ae-parity/node-exercise.mjs @@ -477,10 +477,12 @@ async function post(tx) { async function measureAcceptance() { const rows = [] for (const entry of signed.cases) { + const refusal = refusedBy.get(entry.name) ?? null if (typeof entry.signed !== 'string') { rows.push({ name: entry.name, tag: entry.tag, + postable: refusal === null, verdict: 'not-built', code: entry.build_error, }) @@ -489,6 +491,8 @@ async function measureAcceptance() { rows.push({ name: entry.name, tag: entry.tag, + postable: refusal === null, + ...(refusal === null ? {} : { marked: refusal }), ...(await post(entry.signed)), }) } @@ -542,9 +546,31 @@ const built = await measureBuilders() const accepted = await measureAcceptance() const controls = await measureControls() +const postableRejected = accepted + .filter((row) => row.postable && row.verdict === 'decoder-rejected') + .map((row) => `${row.name}: ${row.code}`) +// The other direction, and the one a marking makes easy to forget: a vector +// excused from the clause that the chain has since started accepting. The +// exclusion is then unearned, and an unearned exclusion is a vector quietly +// removed from the measurement. +const staleMarkings = accepted + .filter((row) => row.postable === false && row.verdict === 'decoder-accepted') + .map( + (row) => `${row.name}: marked ${row.marked?.errorCode}, node accepted it`, + ) + const summary = { built: tally(built, 'verdict'), accepted: tally(accepted, 'verdict'), + postable_accepted: accepted.filter( + (row) => row.postable && row.verdict === 'decoder-accepted', + ).length, + postable_total: accepted.filter((row) => row.postable).length, + postable_rejected: postableRejected, + non_postable_excluded: accepted + .filter((row) => row.postable === false) + .map((row) => `${row.name}: ${row.code}`), + stale_markings: staleMarkings, controls_all_rejected: controls.every( (row) => row.verdict === 'decoder-rejected', ), @@ -582,6 +608,22 @@ if (!summary.controls_all_rejected) { ) process.exit(1) } +if (summary.postable_rejected.length > 0) { + stderr( + 'CLAUSE 6 FAILED — a postable vector was refused by the decoder:', + `\n ${summary.postable_rejected.join('\n ')}`, + ) + process.exit(1) +} +if (summary.stale_markings.length > 0) { + stderr( + 'STALE MARKING — a vector marked non-postable was accepted by the node. ' + + 'The exclusion is no longer earned; re-measure and drop the marking, or ' + + 'correct the rule it names:', + `\n ${summary.stale_markings.join('\n ')}`, + ) + process.exit(1) +} if (summary.accepted['entered-mempool']) { stderr('A transaction entered the mempool. That must never happen here.') process.exit(1) diff --git a/crates/ae-parity/regenerate.mjs b/crates/ae-parity/regenerate.mjs index d0a10fd..afbbd02 100644 --- a/crates/ae-parity/regenerate.mjs +++ b/crates/ae-parity/regenerate.mjs @@ -87,9 +87,12 @@ try { `${JSON.stringify({ name: 'parity-regenerate', private: true, type: 'module' }, null, 2)}\n`, ) + // `path` and `text` stay separate fields. An earlier revision reused one name + // for both, which the check path never noticed because it only ever compares — + // and `--write` then tried to open the whole corpus as a filename. const pins = corpora.map((corpus) => { - const committed = readFileSync(corpus.committed, 'utf8') - const version = corpus.version(JSON.parse(committed)) + const text = readFileSync(corpus.committed, 'utf8') + const version = corpus.version(JSON.parse(text)) if (!version) { throw new Error( `${corpus.committed} does not record its reference version`, @@ -97,7 +100,8 @@ try { } return { ...corpus, - committed, + path: corpus.committed, + text, pinned: `${corpus.package}@${version}`, version, } @@ -129,13 +133,13 @@ try { maxBuffer: 64 * 1024 * 1024, }) - if (regenerated === pin.committed) { + if (regenerated === pin.text) { console.log(`${pin.label}: reproducible from ${pin.pinned}`) continue } if (write) { - writeFileSync(pin.committed, regenerated) + writeFileSync(pin.path, regenerated) console.log( `${pin.label}: rewritten from ${pin.pinned} — review the diff`, ) @@ -146,7 +150,7 @@ try { stderr( `${pin.label}: DRIFT — the committed corpus is not what ${pin.pinned} produces`, ) - stderr(describeDifference(pin.committed, regenerated)) + stderr(describeDifference(pin.text, regenerated)) stderr( 'Re-run with --write to take the new bytes, then review the diff and the ' + 'parity matrix before committing.', diff --git a/crates/ae-parity/src/corpus.rs b/crates/ae-parity/src/corpus.rs index ac98e5f..af057ad 100644 --- a/crates/ae-parity/src/corpus.rs +++ b/crates/ae-parity/src/corpus.rs @@ -38,6 +38,27 @@ pub struct TxCase { /// fixed-point iteration is erased by the act of committing the corpus. A /// vector with no fee is one whose tag has no fee field at all. pub fee_present: bool, + /// Why a node's decoder refuses this vector, when one does. + /// + /// `None` is the ordinary case: the bytes are correct *and* the transaction + /// is one a node will take. `Some` marks a vector whose bytes are correct and + /// whose content the chain rejects — a valid encoding test that must never + /// count towards the on-node clause of parity green. + pub refused_by: Option, +} + +/// A chain rule that refuses a vector, and the vector that stands in for it. +#[derive(Debug, Clone)] +pub struct Refusal { + /// The node's `error_code` for it, measured rather than assumed. + pub error_code: String, + /// The rule, in a sentence a reviewer can check against a node. + pub rule: String, + /// The vector that gives this tag its acceptance result, if it has one. + /// + /// `None` is a **named exception**: the tag has no accepted vector at all, + /// which is a finding against the tag rather than a hole in the corpus. + pub sibling: Option, } /// One FATE vector: a name and the bytes the reference produced. @@ -136,11 +157,33 @@ fn read_tx_case(case: &Json) -> TxCase { pinned_version, tx: case["tx"].as_str().expect("case has a tx").to_string(), fee_present: param_names.iter().any(|name| name == "fee"), + refused_by: read_refusal(case), param_names, params, } } +fn read_refusal(case: &Json) -> Option { + // Absent means postable. A vector that is silently missing the field would + // otherwise read as non-postable and drop itself out of the on-node clause, + // which is the one direction this must never fail in. + if case["postable"].as_bool().unwrap_or(true) { + return None; + } + let refused = &case["refusedBy"]; + Some(Refusal { + error_code: refused["errorCode"] + .as_str() + .expect("a non-postable vector names the error code") + .to_string(), + rule: refused["rule"] + .as_str() + .expect("a non-postable vector names the rule that refuses it") + .to_string(), + sibling: refused["sibling"].as_str().map(ToString::to_string), + }) +} + fn read_value(json: &Json) -> Value { let kind = json["t"].as_str().expect("typed value"); let raw = &json["v"]; diff --git a/crates/ae-parity/src/main.rs b/crates/ae-parity/src/main.rs index 5e737e2..b3e03a6 100644 --- a/crates/ae-parity/src/main.rs +++ b/crates/ae-parity/src/main.rs @@ -47,7 +47,7 @@ fn run_matrix(arguments: &[String]) -> ExitCode { .map_err(|error| error.to_string()) .and_then(|text| serde_json::from_str(&text).map_err(|error| error.to_string())) { - Ok(node) => computed.node = Some(node), + Ok(node) => computed.merge_node_run(node), Err(error) => { eprintln!("could not read {path}: {error}"); return ExitCode::FAILURE; diff --git a/crates/ae-parity/src/matrix.rs b/crates/ae-parity/src/matrix.rs index df708fb..669c1ce 100644 --- a/crates/ae-parity/src/matrix.rs +++ b/crates/ae-parity/src/matrix.rs @@ -56,6 +56,24 @@ pub struct TxRow { pub unexercised_fields: Vec, /// Whether the entry has a fee field whose derivation no vector exercises. pub fee_fixed_point_unexercised: bool, + /// Vectors on this entry a node's decoder will take. + pub postable_vectors: usize, + /// Vectors on this entry whose bytes are correct and whose content the chain + /// refuses, with the rule that refuses each. + pub refusals: Vec, +} + +/// One vector the chain refuses, carried into the matrix from the corpus. +#[derive(Debug, Clone)] +pub struct VectorRefusal { + /// The vector's name in the generator's table. + pub vector: String, + /// The node's `error_code` for it. + pub error_code: String, + /// The rule that refuses it. + pub rule: String, + /// The vector that gives this tag its acceptance result, if it has one. + pub sibling: Option, } impl TxRow { @@ -115,8 +133,43 @@ pub struct Matrix { pub fields_total: usize, /// Fields some vector sets. pub fields_exercised: usize, + /// Tags with a refused vector and no postable sibling — a finding against the + /// tag, carried as a named exception rather than counted as a pass. + pub named_exceptions: Vec, /// The on-node exercise, when one has been merged in. pub node: Option, + /// Clause 6 of parity green, evaluated over the postable set. `None` until an + /// on-node run is merged. + pub node_clause: Option, +} + +/// Clause 6, scored. Every count here is over the **postable** set: a vector the +/// corpus marks non-postable is excluded by construction, because scoring it +/// would make the clause unachievable by any corpus that documents a chain rule. +#[derive(Debug, Clone, Default)] +pub struct NodeClause { + /// Postable vectors the node's decoder accepted. + pub postable_accepted: usize, + /// Postable vectors posted. + pub postable_total: usize, + /// Postable vectors the decoder refused. Any entry here fails clause 6. + pub postable_rejected: Vec, + /// Non-postable vectors excluded from the count, named so the exclusion is + /// visible rather than silent. + pub excluded: Vec, + /// Non-postable vectors the node **accepted**. The marking is then stale, and + /// a stale marking is an exclusion nobody has re-earned. + pub stale_markings: Vec, + /// Tags carried as named exceptions, restated here so the clause never reads + /// as green over a tag with no acceptance result at all. + pub exceptions: Vec, + /// Whether the corrupted controls were all rejected. Without this the + /// acceptance counts above are not evidence of anything. + pub controls_rejected: bool, + /// Vectors whose bytes matched the node's own builder. + pub builder_identical: usize, + /// Vectors whose bytes differed from the node's own builder. + pub builder_differs: Vec, } /// Compute the offline half of the matrix. No network, no working directory. @@ -200,6 +253,21 @@ pub fn compute() -> Matrix { failures, unexercised_fields, fee_fixed_point_unexercised, + postable_vectors: cases + .iter() + .filter(|case| case.refused_by.is_none()) + .count(), + refusals: cases + .iter() + .filter_map(|case| { + case.refused_by.as_ref().map(|refusal| VectorRefusal { + vector: case.name.clone(), + error_code: refusal.error_code.clone(), + rule: refusal.rule.clone(), + sibling: refusal.sibling.clone(), + }) + }) + .collect(), }); } @@ -231,15 +299,31 @@ pub fn compute() -> Matrix { let unexercised_codecs: Vec = all_codecs.difference(&exercised_codecs).cloned().collect(); + // A tag whose only word from the chain is a refusal. Not a pass and not a + // hole in the corpus: a finding against the tag, which stays named so that it + // cannot become a pass without someone deleting this line. + let mut named_exceptions: Vec = transactions + .iter() + .filter(|row| { + row.refusals.iter().any(|refusal| refusal.sibling.is_none()) + && row.postable_vectors == 0 + }) + .map(|row| format!("{:?}", row.tag)) + .collect(); + named_exceptions.sort(); + named_exceptions.dedup(); + Matrix { references: corpus::references(), transactions, entries: entry_rows(), fate: fate_surface(), unexercised_codecs, + named_exceptions, fields_total, fields_exercised, node: None, + node_clause: None, } } @@ -546,6 +630,13 @@ impl Matrix { "fields_total": self.fields_total, "fields_exercised": self.fields_exercised, "unexercised_codecs": self.unexercised_codecs, + "postable_vectors": self.transactions.iter().map(|row| row.postable_vectors).sum::(), + "non_postable_vectors": self + .transactions + .iter() + .flat_map(|row| row.refusals.iter()) + .map(|refusal| refusal.vector.clone()) + .collect::>(), "fee_fixed_point_unexercised": self .transactions .iter() @@ -564,6 +655,13 @@ impl Matrix { "failures": row.failures, "unexercised_fields": row.unexercised_fields, "fee_fixed_point_unexercised": row.fee_fixed_point_unexercised, + "postable_vectors": row.postable_vectors, + "refusals": row.refusals.iter().map(|refusal| json!({ + "vector": refusal.vector, + "error_code": refusal.error_code, + "rule": refusal.rule, + "sibling": refusal.sibling, + })).collect::>(), })).collect::>(), }, "state_tree_entries": { @@ -588,11 +686,117 @@ impl Matrix { "type_variants_covered": self.fate.type_variants_covered, "type_variants_uncovered": self.fate.type_variants_uncovered, }, + "named_exceptions": self.named_exceptions, "node": self.node, + "node_clause": self.node_clause.as_ref().map(|clause| json!({ + "satisfied": clause.is_satisfied(), + "postable_accepted": clause.postable_accepted, + "postable_total": clause.postable_total, + "postable_rejected": clause.postable_rejected, + "excluded": clause.excluded, + "stale_markings": clause.stale_markings, + "exceptions": clause.exceptions, + "controls_rejected": clause.controls_rejected, + "builder_identical": clause.builder_identical, + "builder_differs": clause.builder_differs, + })), }) } } +impl Matrix { + /// Merge an on-node run and score clause 6 over the postable set. + /// + /// The scoring lives here rather than in the script that does the posting, + /// for the same reason the corpus carries the marking rather than the + /// harness: the thing that decides whether a run satisfies the clause has to + /// be readable next to the definition of the clause. + pub fn merge_node_run(&mut self, run: Json) { + let mut clause = NodeClause { + controls_rejected: run["summary"]["controls_all_rejected"] + .as_bool() + .unwrap_or(false), + exceptions: self.named_exceptions.clone(), + ..NodeClause::default() + }; + + let refusals: BTreeMap<&str, &VectorRefusal> = self + .transactions + .iter() + .flat_map(|row| row.refusals.iter()) + .map(|refusal| (refusal.vector.as_str(), refusal)) + .collect(); + + for row in run["accepted"].as_array().into_iter().flatten() { + let Some(name) = row["name"].as_str() else { + continue; + }; + let accepted = row["verdict"].as_str() == Some("decoder-accepted"); + let code = row["code"].as_str().unwrap_or("no code"); + match refusals.get(name) { + // Non-postable. Excluded from the count either way — but if the + // node now takes it, the exclusion is unearned and says so. + Some(refusal) => { + if accepted { + clause.stale_markings.push(format!( + "{name}: marked non-postable ({}) but the node accepted it — \ + re-measure and drop the marking, or correct the rule: {}", + refusal.error_code, refusal.rule + )); + } else { + clause + .excluded + .push(format!("{name}: {} — {}", refusal.error_code, refusal.rule)); + } + } + None => { + clause.postable_total += 1; + if accepted { + clause.postable_accepted += 1; + } else { + clause.postable_rejected.push(format!("{name}: {code}")); + } + } + } + } + + for row in run["built"].as_array().into_iter().flatten() { + let name = row["name"].as_str().unwrap_or("unnamed"); + // Non-postable vectors leave the builder half too, and for the same + // reason they left the acceptance half: the node has already said it + // will not take this transaction, so asking whether it builds the + // same bytes for it scores a disagreement we have already recorded. + if refusals.contains_key(name) { + continue; + } + match row["verdict"].as_str() { + Some("identical") => clause.builder_identical += 1, + Some("differs") => clause.builder_differs.push(name.to_string()), + _ => {} + } + } + + self.node = Some(run); + self.node_clause = Some(clause); + } +} + +impl NodeClause { + /// Whether this run satisfies clause 6. + /// + /// A named exception does not fail it — the exception is the honest record of + /// a tag with no acceptance result, and it is carried in the report rather + /// than scored. A stale marking does fail it: an exclusion nobody has + /// re-earned is a vector quietly removed from the measurement. + pub fn is_satisfied(&self) -> bool { + self.controls_rejected + && self.postable_total > 0 + && self.postable_rejected.is_empty() + && self.stale_markings.is_empty() + && self.builder_differs.is_empty() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/ae-parity/src/render.rs b/crates/ae-parity/src/render.rs index 32fa4f5..b8258c7 100644 --- a/crates/ae-parity/src/render.rs +++ b/crates/ae-parity/src/render.rs @@ -87,9 +87,9 @@ pub fn markdown(matrix: &Matrix) -> String { let _ = writeln!(out); let _ = writeln!( out, - "| Tag | v | Reach | Origin | Vectors | Build | Round trip | Fields not set | Fee fixed point |" + "| Tag | v | Reach | Origin | Vectors | Postable | Build | Round trip | Fields not set | Fee fixed point |" ); - let _ = writeln!(out, "|---|---|---|---|---|---|---|---|---|"); + let _ = writeln!(out, "|---|---|---|---|---|---|---|---|---|---|"); for row in &matrix.transactions { let reach = match row.reach { Reach::NamedAction => "action", @@ -115,9 +115,14 @@ pub fn markdown(matrix: &Matrix) -> String { } else { "—" }; + let postable = if row.refusals.is_empty() { + format!("{}", row.postable_vectors) + } else { + format!("**{}/{}**", row.postable_vectors, row.vectors) + }; let _ = writeln!( out, - "| `{:?}` | {} | {reach} | {origin} | {} | {build} | {roundtrip} | {unset} | {fee} |", + "| `{:?}` | {} | {reach} | {origin} | {} | {postable} | {build} | {roundtrip} | {unset} | {fee} |", row.tag, row.version, row.vectors ); } @@ -137,6 +142,34 @@ pub fn markdown(matrix: &Matrix) -> String { let _ = writeln!(out); } + let refusals: Vec<&crate::matrix::VectorRefusal> = matrix + .transactions + .iter() + .flat_map(|row| row.refusals.iter()) + .collect(); + if !refusals.is_empty() { + let _ = writeln!( + out, + "**Non-postable vectors.** Correct bytes, content a node refuses. Kept \ + as encoding tests, excluded from the on-node clause, and re-measured \ + on every on-node run — a marking nothing re-checks is a marking that \ + rots." + ); + let _ = writeln!(out); + for refusal in &refusals { + let sibling = match &refusal.sibling { + Some(sibling) => format!("postable sibling `{sibling}`"), + None => "**no postable sibling — named exception**".to_string(), + }; + let _ = writeln!( + out, + "- `{}` — `{}`, {sibling}. {}", + refusal.vector, refusal.error_code, refusal.rule + ); + } + let _ = writeln!(out); + } + let failures: Vec<&String> = matrix .transactions .iter() @@ -226,6 +259,68 @@ pub fn markdown(matrix: &Matrix) -> String { ); } Some(node) => { + if let Some(clause) = &matrix.node_clause { + let _ = writeln!( + out, + "Clause 6, scored over the **postable** set. A non-postable \ + vector is excluded by construction — scoring it would make \ + the clause unachievable by any corpus that documents a chain \ + rule — and a named exception is carried rather than counted." + ); + let _ = writeln!(out); + let _ = writeln!( + out, + "**Clause 6: {}.**", + if clause.is_satisfied() { + "satisfied" + } else { + "NOT satisfied" + } + ); + let _ = writeln!(out); + let _ = writeln!( + out, + "- postable vectors accepted by the decoder: {} of {}", + clause.postable_accepted, clause.postable_total + ); + let _ = writeln!( + out, + "- postable vectors refused: {}", + list_or_none(&clause.postable_rejected) + ); + let _ = writeln!( + out, + "- stale markings — non-postable but the node took them: {}", + list_or_none(&clause.stale_markings) + ); + let _ = writeln!( + out, + "- named exceptions, carried not counted: {}", + list_or_none(&clause.exceptions) + ); + let _ = writeln!( + out, + "- node builder: {} identical, differs on {}", + clause.builder_identical, + list_or_none(&clause.builder_differs) + ); + let _ = writeln!( + out, + "- corrupted controls all rejected: {}", + clause.controls_rejected + ); + if !clause.excluded.is_empty() { + let _ = writeln!(out); + let _ = writeln!(out, "Excluded, each naming its rule:"); + let _ = writeln!(out); + for excluded in &clause.excluded { + let _ = writeln!(out, "- {excluded}"); + } + } + let _ = writeln!(out); + } + let _ = writeln!(out, "
Raw run"); + let _ = writeln!(out); let _ = writeln!(out, "```json"); let _ = writeln!( out, @@ -233,6 +328,8 @@ pub fn markdown(matrix: &Matrix) -> String { serde_json::to_string_pretty(node).unwrap_or_default() ); let _ = writeln!(out, "```"); + let _ = writeln!(out); + let _ = writeln!(out, "
"); } } diff --git a/crates/ae-parity/tests/gate.rs b/crates/ae-parity/tests/gate.rs index 757a7d6..d054859 100644 --- a/crates/ae-parity/tests/gate.rs +++ b/crates/ae-parity/tests/gate.rs @@ -151,3 +151,110 @@ fn the_recorded_gaps_are_exactly_what_was_reported() { matrix.unexercised_codecs ); } + +/// The non-postable set, pinned exactly. +/// +/// This is the offline half of clause 6: the corpus's own claim about which +/// vectors a node refuses and why. It cannot check that claim against a node — +/// that is `node-exercise.mjs`, which re-measures every one of these on each run +/// and fails in either direction. What it does check is that the claim is +/// well-formed: every marking names a rule and an error code, and every marking +/// either names a postable sibling that exists or is a declared exception. +#[test] +fn every_non_postable_vector_names_its_rule_and_its_sibling() { + let matrix = matrix::compute(); + let corpus = ae_parity::corpus::transactions(); + let names: Vec<&str> = corpus.iter().map(|case| case.name.as_str()).collect(); + + let refusals: Vec<&matrix::VectorRefusal> = matrix + .transactions + .iter() + .flat_map(|row| row.refusals.iter()) + .collect(); + + assert_eq!( + refusals + .iter() + .map(|refusal| refusal.vector.as_str()) + .collect::>(), + vec![ + "name update v2, id pointer", + "channel create, with delegates", + "channel force progress", + ], + "the non-postable set changed; re-run the on-node exercise before editing this" + ); + + for refusal in &refusals { + assert!( + !refusal.error_code.is_empty() && !refusal.rule.is_empty(), + "{} is marked non-postable without naming a code and a rule", + refusal.vector + ); + if let Some(sibling) = &refusal.sibling { + assert!( + names.contains(&sibling.as_str()), + "{} names sibling {sibling}, which is not in the corpus", + refusal.vector + ); + } + } +} + +/// `ChannelForceProgressTx` has no accepted vector at all, and that is a named +/// exception rather than a pass. +/// +/// Seven variants — crossing payload signedness, update-entry validity and +/// off-chain-trees validity — were refused identically. The tag is reachable +/// through the generic builder today, so this is a finding against the tag. +/// +/// The assertion is deliberately two-sided. Removing the exception without +/// giving the tag a postable vector fails here, and giving it one without +/// removing the exception fails here too — which is what stops it becoming a +/// silent pass in either direction. +#[test] +fn the_named_exception_is_exactly_channel_force_progress() { + let matrix = matrix::compute(); + assert_eq!( + matrix.named_exceptions, + vec!["ChannelForceProgressTx".to_string()] + ); + + for row in &matrix.transactions { + let declared = matrix.named_exceptions.contains(&format!("{:?}", row.tag)); + assert_eq!( + declared, + row.postable_vectors == 0, + "{:?} v{}: declared exception = {declared}, but it has {} postable vectors", + row.tag, + row.version, + row.postable_vectors + ); + } +} + +/// Both tags that lost a vector to a chain rule kept an acceptance result. +#[test] +fn every_other_refused_tag_still_has_a_postable_vector() { + let matrix = matrix::compute(); + for row in &matrix.transactions { + if row.refusals.is_empty() { + continue; + } + let excepted = matrix.named_exceptions.contains(&format!("{:?}", row.tag)); + assert!( + excepted || row.postable_vectors > 0, + "{:?} v{} has a refused vector, no postable vector, and no exception", + row.tag, + row.version + ); + } + assert_eq!( + matrix + .transactions + .iter() + .map(|row| row.postable_vectors) + .sum::(), + 38 + ); +} From 749de911f2e148db8f5e2f957777d215ed541efc Mon Sep 17 00:00:00 2001 From: Badi Ifaoui Date: Thu, 20 Aug 2026 04:55:09 +0000 Subject: [PATCH 2/2] Re-measure the markings against a node, and record the run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The exclusion the corpus claims is now re-earned on every on-node run instead of assumed: the non-postable vectors are posted too, and the run fails if the node takes one. Verified against a deliberately false marking rather than by reading the branch — marking a plainly acceptable vector non-postable exits non-zero with the stale-marking message, and the control was reverted afterwards. 38 of 38 postable vectors accepted, no stale markings, controls all rejected. The new sibling is accepted, which is what gives `ChannelCreateTx` an acceptance result again. Clause 6 still scores **not satisfied**, for one row and one reason. Excluding non-postable vectors from the builder half takes the version-2 name update out of it, and what remains is the node HTTP endpoint that emits a pointer list reversed. Our order is the reference sdk's and the node's own decoder accepts it, so it is an endpoint quirk rather than a wire rule — but the clause asks for byte-identity from every tag the node builds, and whether a quirk belongs inside that half is not this harness's call to make. --- crates/ae-parity/TESTNET.md | 61 ++++++++++++++++++++++-------- crates/ae-parity/node-exercise.mjs | 22 +++++++++++ 2 files changed, 67 insertions(+), 16 deletions(-) diff --git a/crates/ae-parity/TESTNET.md b/crates/ae-parity/TESTNET.md index bd669d4..495e744 100644 --- a/crates/ae-parity/TESTNET.md +++ b/crates/ae-parity/TESTNET.md @@ -6,10 +6,10 @@ below are what a later run is compared against. | | | |---|---| | Node | `https://testnet.aeternity.io/v3`, `7.3.0-rc8`, network `ae_uat` | -| Height | 1283399 | +| Height | 1283414 | | Recorded | 2026-08-20, UTC | | References | `@aeternity/aepp-sdk` 14.1.1, `@aeternity/aepp-calldata` 1.9.1 | -| Corpus | 40 vectors over 26 tags | +| Corpus | 41 vectors over 26 tags — 38 postable, 3 not | Nothing was spent. The signer was generated in-process, never persisted, and has never held a balance on any network; every posted transaction was rejected before @@ -31,12 +31,21 @@ A single flipped byte turns `signature_check_failed` into `broken_tx`. That is the whole basis for reading `signature_check_failed` as "the decoder took our bytes". -## Decoder acceptance — 37 of 40 vectors, 23 of 26 tags +## Decoder acceptance — 38 of 38 postable vectors, every tag but one -Every vector rebuilt through `ae-core`, wrapped and signed, posted. Three were -refused by the decoder, and **none of the three is a defect in this core**: each -was reproduced byte-for-byte with `@aeternity/aepp-sdk` 14.1.1 building *and* -wrapping the transaction, independently of anything written here. +Every vector rebuilt through `ae-core`, wrapped and signed, posted. Every one of +the 38 postable vectors was accepted by the decoder. Three vectors are marked +non-postable and are excluded from that count, and **none of the three is a +defect in this core**: each was reproduced byte-for-byte with +`@aeternity/aepp-sdk` 14.1.1 building *and* wrapping the transaction, +independently of anything written here. + +The exclusion is re-earned on every run rather than assumed. `node-exercise.mjs` +posts the non-postable vectors too and fails if the node now takes one, because +an exclusion nobody has re-checked is a vector quietly removed from the +measurement. Verified against a deliberately false marking: marking `spend, +minimal` non-postable makes the run exit non-zero with +`STALE MARKING — a vector marked non-postable was accepted by the node`. What they are instead is the failure mode the on-node half exists to catch — a committed vector both implementations agree on that the chain will not take. An @@ -74,7 +83,9 @@ parameters it returns a **version 1** transaction. Two of the three implementati agree, and the odd one out is the reference sdk. The committed vector `name update v2, id pointer` is therefore a valid *encoding* -test and an invalid *transaction*. +test and an invalid *transaction*. Its postable sibling is +`name update v2, raw pointer`, which was already in the corpus — a version 2 +name update the node does accept, because its pointer needs version 2. ### 2. `ChannelCreateTx` — delegates must be accounts @@ -87,7 +98,9 @@ test and an invalid *transaction*. The committed vector `channel create, with delegates` uses a contract and an oracle as responder delegates. Same shape of finding as above: the bytes are -agreed, the transaction is not acceptable. +agreed, the transaction is not acceptable. Its postable sibling +`channel create, with account delegates` was added for it and the node accepts +it — this was the only sibling the corpus was actually missing. ### 3. `ChannelForceProgressTx` — refused whatever it contains @@ -101,6 +114,11 @@ tag has no on-node evidence available by this route; diagnosing why is the node' business, not this harness's. It is the one tag in the corpus with no acceptance result, and it is reachable today through `buildTransaction()`. +It is therefore a **named exception**, not a pass. `gate.rs` pins it in both +directions: removing the exception without giving the tag a postable vector +fails, and giving it one without removing the exception fails too. Turning it +into a silent pass takes deleting a line. + ## Node-builder byte agreement — 18 identical, 0 disagreements The node exposes its own Erlang transaction builders behind `/v3/debug/…`. Same @@ -113,21 +131,32 @@ opinion from the same family. | node declined to build (needs chain state, or rejects a corpus value) | 12 | | no builder on the node's HTTP surface | 6 | | not comparable (different transaction version) | 2 | -| differs | 2 | +| excluded, non-postable | 2 | +| differs | 1 | **Eighteen byte-for-byte agreements across `SpendTx`, four name tags, three oracle tags, `ContractCreateTx` and five channel tags.** No vector where all three implementations built the same transaction produced three different answers. -The two `differs` rows are both `NameUpdateTx`, and neither is an encoding -disagreement in this core: +A non-postable vector leaves this half too, for the reason it left the +acceptance half: the node has already said it will not take that transaction, so +asking whether it builds the same bytes scores a disagreement already recorded as +the refusal. That removes `name update v2, id pointer`, where the node builds at +version 1 per the rule above. + +One `differs` row remains, and it is not an encoding disagreement in this core: - `name update v1, explicit ttls and several pointers` — the node's HTTP builder emits the pointer list **reversed**. Ours is in the order given, byte-identical to the reference sdk, and the node's own decoder accepts it. A quirk of that endpoint, not a wire rule. -- `name update v2, id pointer` — the node builds at version 1, per the rule above. + +It is the single reason clause 6 scores **not satisfied** on this run: the +acceptance half is 38 of 38, and the builder half asks for byte-identity from +every tag the node builds. Whether a node-endpoint quirk belongs inside that half +is the same shape of call as the one that scoped the acceptance half, and it is +not this harness's to make. The twelve declines are all state lookups or value rejections, not serialisation: `Contract code … not found`, `Oracle address … not found`, `Invalid hash: name` @@ -147,7 +176,7 @@ different transactions. | `SignedTx` | no builder ×2 | accepted ×2 | | `NamePreclaimTx` | identical | accepted | | `NameClaimTx` | declined ×2 | accepted ×2 | -| `NameUpdateTx` | identical ×2, differs ×2, declined | accepted ×4, **rejected ×1** | +| `NameUpdateTx` | identical ×2, differs ×1, declined, excluded ×1 | accepted ×4, **non-postable ×1** | | `NameTransferTx` | identical | accepted | | `NameRevokeTx` | identical | accepted | | `ContractCreateTx` | identical ×2 | accepted ×2 | @@ -156,7 +185,7 @@ different transactions. | `OracleQueryTx` | declined ×2 | accepted ×2 | | `OracleRespondTx` | declined | accepted | | `OracleExtendTx` | identical | accepted | -| `ChannelCreateTx` | not comparable ×2 | accepted, **rejected ×1** | +| `ChannelCreateTx` | not comparable ×2, excluded ×1 | accepted ×2, **non-postable ×1** | | `ChannelDepositTx` | identical | accepted | | `ChannelWithdrawTx` | identical | accepted | | `ChannelCloseMutualTx` | identical | accepted | @@ -165,7 +194,7 @@ different transactions. | `ChannelSettleTx` | identical | accepted | | `ChannelSnapshotSoloTx` | declined | accepted (`invalid_at_protocol`) | | `ChannelOffChainTx` | no builder | accepted | -| `ChannelForceProgressTx` | no builder | **rejected** | +| `ChannelForceProgressTx` | no builder | **non-postable, named exception** | | `GaAttachTx` | no builder | accepted | | `GaMetaTx` | no builder | accepted | | `PayingForTx` | declined | accepted | diff --git a/crates/ae-parity/node-exercise.mjs b/crates/ae-parity/node-exercise.mjs index d5e5ed3..dd3d89c 100644 --- a/crates/ae-parity/node-exercise.mjs +++ b/crates/ae-parity/node-exercise.mjs @@ -59,6 +59,21 @@ const corpus = JSON.parse( ) const signed = JSON.parse(readFileSync(signedPath, 'utf8')) +/** + * The corpus's own claim about which vectors a node refuses, and why. + * + * Everything below re-measures it. A marking that nothing re-checks is a marking + * that rots: it silently removes a vector from the on-node clause and nobody + * finds out when the chain rule it names stops applying. So this script fails in + * **both** directions — a postable vector the node refuses, and a non-postable + * vector the node takes. + */ +const refusedBy = new Map( + corpus.cases + .filter((entry) => entry.postable === false) + .map((entry) => [entry.name, entry.refusedBy]), +) + /** base64check, the envelope every `tx_`-family string uses. */ function encodeCheck(prefix, bytes) { const first = createHash('sha256').update(bytes).digest() @@ -417,6 +432,13 @@ async function measureBuilders() { rows.push({ name, tag, verdict: 'no-node-builder' }) continue } + // A vector the node has already said it will not take leaves the builder + // comparison too. Asking whether it builds the same bytes for a transaction + // it refuses scores a disagreement already recorded as the refusal. + if (refusedBy.has(name)) { + rows.push({ name, tag, verdict: 'excluded-non-postable' }) + continue + } let body try { body = builder.map(entry.params)