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/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/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..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) @@ -477,10 +499,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 +513,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 +568,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 +630,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 + ); +}