Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 95 additions & 2 deletions crates/ae-core/tests/generate-vectors.mjs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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`)
Loading
Loading