diff --git a/.gitattributes b/.gitattributes index 27c567fd..68e57a0c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,3 +13,4 @@ docs/qualification/evidence/worker-attempt-quiescence/** -text docs/qualification/evidence/action-attempt-worker-20261023/** -text tests/fixtures/native-stream-v1/* -text tests/fixtures/wire-rpc-block-33.json -text +tests/fixtures/antelope-worker/* text eol=lf diff --git a/tests/fixtures/antelope-worker/LICENSE.leap b/tests/fixtures/antelope-worker/LICENSE.leap new file mode 100644 index 00000000..9cf8ca77 --- /dev/null +++ b/tests/fixtures/antelope-worker/LICENSE.leap @@ -0,0 +1,26 @@ +AntelopeIO/leap +Copyright (c) 2021-2022 EOS Network Foundation (ENF) and its contributors. All rights reserved. +This ENF software is based upon: + +EOSIO/eos +Copyright (c) 2017-2021 block.one and its contributors. All rights reserved. + +The MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/tests/fixtures/antelope-worker/README.md b/tests/fixtures/antelope-worker/README.md new file mode 100644 index 00000000..c162d039 --- /dev/null +++ b/tests/fixtures/antelope-worker/README.md @@ -0,0 +1,52 @@ +# Constructed Antelope worker source + +This fixture qualifies the builtin Antelope codec and original emitted callbacks under +History API #321. It is a deterministic engineering input, not a live-chain capture or +a consensus-valid signed block. It does not establish reversible state, capacity, +complete history replay, or release support. + +The handshake is the JSON literal from +[Leap v5.0.3 libraries/state_history/abi.cpp](https://github.com/AntelopeIO/leap/blob/d133c6413ce8ce2e96096a0513ec25b4a8dbe837/libraries/state_history/abi.cpp), +commit `d133c6413ce8ce2e96096a0513ec25b4a8dbe837`, +Git blob `d778d0c7fc40c96a070b860400f9d9244f1cba1b`. +The literal is extracted without modifying its definitions and saved with one terminal +newline. Its checksum is in source.json. Upstream licensing is retained in LICENSE.leap. + +generate.mjs writes source binary fields independently of the codecs under test. The +block uses the standard header layout and a zero K1 signature with valid ABI encoding; +that signature is deliberately unsuitable for consensus verification. Trace, receipt, +transaction, parent, producer and ordinal inputs are explicit. Separate frames preserve +the distinction between encoded-empty traces and unavailable traces. The counter contract +has an explicitly constructed deployment at block 7 / ABI sequence 3; a controlled +historical ABI search returns those exact bytes. The input and expected uint64 values +are specified independently of the worker's inventory derivation. + +After a normal npm installation and build, regeneration is explicit: + +~~~sh +node tests/fixtures/antelope-worker/generate.mjs +~~~ + +Tests read the committed bytes rather than regenerating them. The generator, handshake, +contract ABI and frames have checksums. Native runs additionally report the actual Node +executable, producer revision/configuration, emitted dependency/artifact digest, fixture +and worker-source digests. The independent expected action checks include contract data, +authorization, exact sequence strings, ABI/code counters, return value, transaction and +block identity. + +The emitted reader verifies the chain through an ABI-correct status response and retains +original ingress before MainDS. The selected MainDS or DS callback uses native parent/worker +IPC and its real managed lifecycle. The other callback stages and external ES/RabbitMQ/ +cache/RPC transports are explicitly controlled. Production retained stores and their +two-copy write receipts, native contract ABI loading, original semantic membership, +fresh-result validators and actual master adjudicator remain active. + +The handoff counterexamples reject stale configuration, target UUID and canonical block +identity, including configuration replacement during an awaited target read. A reversible +RPC observation without an admitted source selection remains unresolved. This conservative +result is a coverage boundary, not completed Antelope reversible-source support. The full +master/coordinator source-revocation and pending-worker cases remain owned by #321. + +The three modes are handoff, empty and missing. Broker settlement remains unknown even +after a private source-checked association. No reader credit, capacity grant, public +activation, worker architecture or release qualification follows from these cases. diff --git a/tests/fixtures/antelope-worker/generate.mjs b/tests/fixtures/antelope-worker/generate.mjs new file mode 100644 index 00000000..ac069e7d --- /dev/null +++ b/tests/fixtures/antelope-worker/generate.mjs @@ -0,0 +1,90 @@ +/** Constructed Antelope source, written independently of the codecs under test. + * Field order follows the pinned Leap SHiP ABI; no Wire input or decoder is used. + * Regeneration is explicit, not part of tests. This is not a signed live-chain capture. + */ +import {createHash} from 'node:crypto'; +import {readFileSync, writeFileSync} from 'node:fs'; +import {createAntelopeProvider} from '../../../build/indexer/protocol/antelope.js'; + +const hash = value => createHash('sha256').update(value).digest('hex'); +const abiBytes = readFileSync(new URL('./ship.abi.json', import.meta.url)); +const {provider} = await createAntelopeProvider(); +class Bytes { + parts = []; + raw(value) {this.parts.push(Buffer.from(value)); return this;} + unsigned(value, size) { + let n = BigInt(value); const bytes = Buffer.alloc(size); + for (let i = 0; i < size; i++) {bytes[i] = Number(n & 255n); n >>= 8n;} + if (n !== 0n) throw new Error('Integer overflow'); return this.raw(bytes); + } + varuint(value) { + let n = BigInt(value); + do {const byte = Number(n & 127n); n >>= 7n; this.unsigned(byte | (n ? 128 : 0), 1);} while (n); + return this; + } + name(value) { + const alphabet = '.12345abcdefghijklmnopqrstuvwxyz'; let n = 0n; + if (value.length > 12) throw new Error('Fixture names are at most twelve characters'); + for (let i = 0; i < 12; i++) { + const digit = i < value.length ? alphabet.indexOf(value[i]) : 0; + if (digit < 0) throw new Error('Invalid name'); + n = (n << 5n) | BigInt(digit); + } + return this.unsigned(n << 4n, 8); + } + hex(value) {return this.raw(Buffer.from(value, 'hex'));} + vector(bytes) {return this.varuint(bytes.length).raw(bytes);} + done() {return Buffer.concat(this.parts);} +} +const chainId = hash('constructed-antelope-worker-source-v1'); +const previousBlockId = '0000000b' + 'a'.repeat(56); +const timestamp = '2026-10-25T00:00:00.000', blockNum = 12; +const header = new Bytes().unsigned((Date.parse(timestamp + 'Z') - Date.UTC(2000, 0, 1)) / 500, 4) + .name('producer').unsigned(1, 2).hex(previousBlockId).hex('b'.repeat(64)).hex('c'.repeat(64)) + .unsigned(1, 4).unsigned(0, 1).varuint(0).done(); +const idBytes = Buffer.from(hash(header), 'hex'); idBytes.writeUInt32BE(blockNum); +const blockId = idBytes.toString('hex'); +// K1 type tag and 65 zero signature bytes are ABI-valid, explicitly not consensus-valid. +const blockBytes = new Bytes().raw(header).unsigned(0, 1).raw(Buffer.alloc(65)).varuint(0).varuint(0).done(); +const transactionId = '3'.repeat(64), actionHex = '0100000000000080'; +const contractAbi = {version: 'eosio::abi/1.2', types: [], structs: [{name: 'setvalue', base: '', + fields: [{name: 'value', type: 'uint64'}]}], actions: [{name: 'setvalue', type: 'setvalue', ricardian_contract: ''}], + tables: [], ricardian_clauses: [], error_messages: [], abi_extensions: [], variants: [], action_results: []}; +const contractAbiHex = Buffer.from(provider.contractCodec.encodeAbi(contractAbi)).toString('hex'); +// transaction_trace_v0 with one action_trace_v1 and one action_receipt_v0. +const traces = new Bytes().varuint(1).varuint(0).hex(transactionId).unsigned(0, 1).unsigned(15, 4) + .varuint(2).unsigned(7, 8).unsigned(16, 8).unsigned(0, 1).varuint(1) + .varuint(1).varuint(1).varuint(0).unsigned(1, 1).varuint(0).name('counter').hex('d'.repeat(64)) + .unsigned('9007199254740995', 8).unsigned('9223372036854775808', 8).varuint(1) + .name('alice').unsigned('18446744073709551615', 8).varuint(2).varuint(3) + .name('counter').name('counter').name('setvalue').varuint(1).name('alice').name('active') + .vector(Buffer.from(actionHex, 'hex')).unsigned(0, 1).unsigned(0, 8).varuint(0).varuint(0) + .unsigned(0, 1).unsigned(0, 1).vector(Buffer.from('beef', 'hex')) + .unsigned(0, 1).unsigned(0, 1).unsigned(0, 1).unsigned(0, 1).unsigned(0, 1).done(); +function frame(traceBytes) { + const value = new Bytes().varuint(1).unsigned(blockNum, 4).hex(blockId) + .unsigned(11, 4).hex(previousBlockId).unsigned(1, 1).unsigned(blockNum, 4).hex(blockId) + .unsigned(1, 1).unsigned(11, 4).hex(previousBlockId).unsigned(1, 1).vector(blockBytes); + if (traceBytes === null) value.unsigned(0, 1); else value.unsigned(1, 1).vector(traceBytes); + return value.unsigned(1, 1).vector(Buffer.from([0])).done(); +} +const frames = Object.fromEntries([['handoff', traces], ['empty', Buffer.from([0])], ['missing', null]] + .map(([name, bytes]) => {const value = frame(bytes); return [name, {hex: value.toString('hex'), sha256: hash(value), bytes: value.length}];})); +const statusBytes = new Bytes().varuint(0).unsigned(blockNum, 4).hex(blockId).unsigned(11, 4).hex(previousBlockId) + .unsigned(1, 4).unsigned(13, 4).unsigned(1, 4).unsigned(13, 4).hex(chainId).done(); +const fixture = {schemaVersion: 1, constructed: true, consensusSignatureValid: false, + protocolId: 'antelope', profileId: 'antelope-default-v1', + upstream: {repository: 'AntelopeIO/leap', tag: 'v5.0.3', revision: 'd133c6413ce8ce2e96096a0513ec25b4a8dbe837', + path: 'libraries/state_history/abi.cpp', blob: 'd778d0c7fc40c96a070b860400f9d9244f1cba1b', + extractedAbiSha256: hash(abiBytes)}, + generator: {node: process.version, sourceSha256: hash(readFileSync(new URL(import.meta.url)))}, + source: {chainId, blockNum, blockId, previousBlockId, producer: 'producer', timestamp, traceOrdinal: 0, + transactionId, actionOrdinal: 1, receiptOrdinal: 0}, + contract: {account: 'counter', abiDeployment: {blockNum: 7, blockId: '00000007' + '7'.repeat(56), abiSequence: 3}, + abi: contractAbi, abiHex: contractAbiHex, abiSha256: hash(Buffer.from(contractAbiHex, 'hex')), actionHex}, + expected: {account: 'counter', name: 'setvalue', receiver: 'counter', authorization: [{actor: 'alice', permission: 'active'}], + data: {value: '9223372036854775809'}, globalSequence: '9007199254740995', receiverSequence: '9223372036854775808', + authSequence: '18446744073709551615', codeSequence: 2, abiSequence: 3, returnValue: 'beef'}, + status: {hex: statusBytes.toString('hex'), sha256: hash(statusBytes), bytes: statusBytes.length}, frames}; +writeFileSync(new URL('./source.json', import.meta.url), JSON.stringify(fixture, null, 2) + '\n'); +console.log(JSON.stringify({constructed: true, frames: Object.fromEntries(Object.entries(frames).map(([k,v]) => [k,{sha256:v.sha256,bytes:v.bytes}]))})); diff --git a/tests/fixtures/antelope-worker/ship.abi.json b/tests/fixtures/antelope-worker/ship.abi.json new file mode 100644 index 00000000..59563844 --- /dev/null +++ b/tests/fixtures/antelope-worker/ship.abi.json @@ -0,0 +1,613 @@ +{ + "version": "eosio::abi/1.1", + "structs": [ + { + "name": "get_status_request_v0", "fields": [] + }, + { + "name": "block_position", "fields": [ + { "name": "block_num", "type": "uint32" }, + { "name": "block_id", "type": "checksum256" } + ] + }, + { + "name": "get_status_result_v0", "fields": [ + { "name": "head", "type": "block_position" }, + { "name": "last_irreversible", "type": "block_position" }, + { "name": "trace_begin_block", "type": "uint32" }, + { "name": "trace_end_block", "type": "uint32" }, + { "name": "chain_state_begin_block", "type": "uint32" }, + { "name": "chain_state_end_block", "type": "uint32" }, + { "name": "chain_id", "type": "checksum256$" } + ] + }, + { + "name": "get_blocks_request_v0", "fields": [ + { "name": "start_block_num", "type": "uint32" }, + { "name": "end_block_num", "type": "uint32" }, + { "name": "max_messages_in_flight", "type": "uint32" }, + { "name": "have_positions", "type": "block_position[]" }, + { "name": "irreversible_only", "type": "bool" }, + { "name": "fetch_block", "type": "bool" }, + { "name": "fetch_traces", "type": "bool" }, + { "name": "fetch_deltas", "type": "bool" } + ] + }, + { + "name": "get_blocks_ack_request_v0", "fields": [ + { "name": "num_messages", "type": "uint32" } + ] + }, + { + "name": "get_blocks_result_v0", "fields": [ + { "name": "head", "type": "block_position" }, + { "name": "last_irreversible", "type": "block_position" }, + { "name": "this_block", "type": "block_position?" }, + { "name": "prev_block", "type": "block_position?" }, + { "name": "block", "type": "bytes?" }, + { "name": "traces", "type": "bytes?" }, + { "name": "deltas", "type": "bytes?" } + ] + }, + { + "name": "row", "fields": [ + { "name": "present", "type": "bool" }, + { "name": "data", "type": "bytes" } + ] + }, + { + "name": "table_delta_v0", "fields": [ + { "name": "name", "type": "string" }, + { "name": "rows", "type": "row[]" } + ] + }, + { + "name": "action", "fields": [ + { "name": "account", "type": "name" }, + { "name": "name", "type": "name" }, + { "name": "authorization", "type": "permission_level[]" }, + { "name": "data", "type": "bytes" } + ] + }, + { + "name": "account_auth_sequence", "fields": [ + { "name": "account", "type": "name" }, + { "name": "sequence", "type": "uint64" } + ] + }, + { + "name": "action_receipt_v0", "fields": [ + { "name": "receiver", "type": "name" }, + { "name": "act_digest", "type": "checksum256" }, + { "name": "global_sequence", "type": "uint64" }, + { "name": "recv_sequence", "type": "uint64" }, + { "name": "auth_sequence", "type": "account_auth_sequence[]" }, + { "name": "code_sequence", "type": "varuint32" }, + { "name": "abi_sequence", "type": "varuint32" } + ] + }, + { + "name": "account_delta", "fields": [ + { "name": "account", "type": "name" }, + { "name": "delta", "type": "int64" } + ] + }, + { + "name": "action_trace_v0", "fields": [ + { "name": "action_ordinal", "type": "varuint32" }, + { "name": "creator_action_ordinal", "type": "varuint32" }, + { "name": "receipt", "type": "action_receipt?" }, + { "name": "receiver", "type": "name" }, + { "name": "act", "type": "action" }, + { "name": "context_free", "type": "bool" }, + { "name": "elapsed", "type": "int64" }, + { "name": "console", "type": "string" }, + { "name": "account_ram_deltas", "type": "account_delta[]" }, + { "name": "except", "type": "string?" }, + { "name": "error_code", "type": "uint64?" } + ] + }, + { + "name": "action_trace_v1", "fields": [ + { "name": "action_ordinal", "type": "varuint32" }, + { "name": "creator_action_ordinal", "type": "varuint32" }, + { "name": "receipt", "type": "action_receipt?" }, + { "name": "receiver", "type": "name" }, + { "name": "act", "type": "action" }, + { "name": "context_free", "type": "bool" }, + { "name": "elapsed", "type": "int64" }, + { "name": "console", "type": "string" }, + { "name": "account_ram_deltas", "type": "account_delta[]" }, + { "name": "except", "type": "string?" }, + { "name": "error_code", "type": "uint64?" }, + { "name": "return_value", "type": "bytes"} + ] + }, + { + "name": "partial_transaction_v0", "fields": [ + { "name": "expiration", "type": "time_point_sec" }, + { "name": "ref_block_num", "type": "uint16" }, + { "name": "ref_block_prefix", "type": "uint32" }, + { "name": "max_net_usage_words", "type": "varuint32" }, + { "name": "max_cpu_usage_ms", "type": "uint8" }, + { "name": "delay_sec", "type": "varuint32" }, + { "name": "transaction_extensions", "type": "extension[]" }, + { "name": "signatures", "type": "signature[]" }, + { "name": "context_free_data", "type": "bytes[]" } + ] + }, + { + "name": "transaction_trace_v0", "fields": [ + { "name": "id", "type": "checksum256" }, + { "name": "status", "type": "uint8" }, + { "name": "cpu_usage_us", "type": "uint32" }, + { "name": "net_usage_words", "type": "varuint32" }, + { "name": "elapsed", "type": "int64" }, + { "name": "net_usage", "type": "uint64" }, + { "name": "scheduled", "type": "bool" }, + { "name": "action_traces", "type": "action_trace[]" }, + { "name": "account_ram_delta", "type": "account_delta?" }, + { "name": "except", "type": "string?" }, + { "name": "error_code", "type": "uint64?" }, + { "name": "failed_dtrx_trace", "type": "transaction_trace?" }, + { "name": "partial", "type": "partial_transaction?" } + ] + }, + { + "name": "packed_transaction", "fields": [ + { "name": "signatures", "type": "signature[]" }, + { "name": "compression", "type": "uint8" }, + { "name": "packed_context_free_data", "type": "bytes" }, + { "name": "packed_trx", "type": "bytes" } + ] + }, + { + "name": "transaction_receipt_header", "fields": [ + { "name": "status", "type": "uint8" }, + { "name": "cpu_usage_us", "type": "uint32" }, + { "name": "net_usage_words", "type": "varuint32" } + ] + }, + { + "name": "transaction_receipt", "base": "transaction_receipt_header", "fields": [ + { "name": "trx", "type": "transaction_variant" } + ] + }, + { + "name": "extension", "fields": [ + { "name": "type", "type": "uint16" }, + { "name": "data", "type": "bytes" } + ] + }, + { + "name": "block_header", "fields": [ + { "name": "timestamp", "type": "block_timestamp_type" }, + { "name": "producer", "type": "name" }, + { "name": "confirmed", "type": "uint16" }, + { "name": "previous", "type": "checksum256" }, + { "name": "transaction_mroot", "type": "checksum256" }, + { "name": "action_mroot", "type": "checksum256" }, + { "name": "schedule_version", "type": "uint32" }, + { "name": "new_producers", "type": "producer_schedule?" }, + { "name": "header_extensions", "type": "extension[]" } + ] + }, + { + "name": "signed_block_header", "base": "block_header", "fields": [ + { "name": "producer_signature", "type": "signature" } + ] + }, + { + "name": "signed_block", "base": "signed_block_header", "fields": [ + { "name": "transactions", "type": "transaction_receipt[]" }, + { "name": "block_extensions", "type": "extension[]" } + ] + }, + { "name": "transaction_header", "fields": [ + { "name": "expiration", "type": "time_point_sec" }, + { "name": "ref_block_num", "type": "uint16" }, + { "name": "ref_block_prefix", "type": "uint32" }, + { "name": "max_net_usage_words", "type": "varuint32" }, + { "name": "max_cpu_usage_ms", "type": "uint8" }, + { "name": "delay_sec", "type": "varuint32" } + ] + }, + { "name": "transaction", "base": "transaction_header", "fields": [ + { "name": "context_free_actions", "type": "action[]" }, + { "name": "actions", "type": "action[]" }, + { "name": "transaction_extensions", "type": "extension[]" } + ] + }, + { + "name": "code_id", "fields": [ + { "type": "uint8", "name": "vm_type" }, + { "type": "uint8", "name": "vm_version" }, + { "type": "checksum256", "name": "code_hash" } + ] + }, + { + "name": "account_v0", "fields": [ + { "type": "name", "name": "name" }, + { "type": "block_timestamp_type", "name": "creation_date" }, + { "type": "bytes", "name": "abi" } + ] + }, + { + "name": "account_metadata_v0", "fields": [ + { "type": "name", "name": "name" }, + { "type": "bool", "name": "privileged" }, + { "type": "time_point", "name": "last_code_update" }, + { "type": "code_id?", "name": "code" } + ] + }, + { + "name": "code_v0", "fields": [ + { "type": "uint8", "name": "vm_type" }, + { "type": "uint8", "name": "vm_version" }, + { "type": "checksum256", "name": "code_hash" }, + { "type": "bytes", "name": "code" } + ] + }, + { + "name": "contract_table_v0", "fields": [ + { "type": "name", "name": "code" }, + { "type": "name", "name": "scope" }, + { "type": "name", "name": "table" }, + { "type": "name", "name": "payer" } + ] + }, + { + "name": "contract_row_v0", "fields": [ + { "type": "name", "name": "code" }, + { "type": "name", "name": "scope" }, + { "type": "name", "name": "table" }, + { "type": "uint64", "name": "primary_key" }, + { "type": "name", "name": "payer" }, + { "type": "bytes", "name": "value" } + ] + }, + { + "name": "contract_index64_v0", "fields": [ + { "type": "name", "name": "code" }, + { "type": "name", "name": "scope" }, + { "type": "name", "name": "table" }, + { "type": "uint64", "name": "primary_key" }, + { "type": "name", "name": "payer" }, + { "type": "uint64", "name": "secondary_key" } + ] + }, + { + "name": "contract_index128_v0", "fields": [ + { "type": "name", "name": "code" }, + { "type": "name", "name": "scope" }, + { "type": "name", "name": "table" }, + { "type": "uint64", "name": "primary_key" }, + { "type": "name", "name": "payer" }, + { "type": "uint128", "name": "secondary_key" } + ] + }, + { + "name": "contract_index256_v0", "fields": [ + { "type": "name", "name": "code" }, + { "type": "name", "name": "scope" }, + { "type": "name", "name": "table" }, + { "type": "uint64", "name": "primary_key" }, + { "type": "name", "name": "payer" }, + { "type": "checksum256", "name": "secondary_key" } + ] + }, + { + "name": "contract_index_double_v0", "fields": [ + { "type": "name", "name": "code" }, + { "type": "name", "name": "scope" }, + { "type": "name", "name": "table" }, + { "type": "uint64", "name": "primary_key" }, + { "type": "name", "name": "payer" }, + { "type": "float64", "name": "secondary_key" } + ] + }, + { + "name": "contract_index_long_double_v0", "fields": [ + { "type": "name", "name": "code" }, + { "type": "name", "name": "scope" }, + { "type": "name", "name": "table" }, + { "type": "uint64", "name": "primary_key" }, + { "type": "name", "name": "payer" }, + { "type": "float128", "name": "secondary_key" } + ] + }, + { + "name": "producer_key", "fields": [ + { "type": "name", "name": "producer_name" }, + { "type": "public_key", "name": "block_signing_key" } + ] + }, + { + "name": "producer_schedule", "fields": [ + { "type": "uint32", "name": "version" }, + { "type": "producer_key[]", "name": "producers" } + ] + }, + { + "name": "block_signing_authority_v0", "fields": [ + { "type": "uint32", "name": "threshold" }, + { "type": "key_weight[]", "name": "keys" } + ] + }, + { + "name": "producer_authority", "fields": [ + { "type": "name", "name": "producer_name" }, + { "type": "block_signing_authority", "name": "authority" } + ] + }, + { + "name": "producer_authority_schedule", "fields": [ + { "type": "uint32", "name": "version" }, + { "type": "producer_authority[]", "name": "producers" } + ] + }, + { + "name": "chain_config_v0", "fields": [ + { "type": "uint64", "name": "max_block_net_usage" }, + { "type": "uint32", "name": "target_block_net_usage_pct" }, + { "type": "uint32", "name": "max_transaction_net_usage" }, + { "type": "uint32", "name": "base_per_transaction_net_usage" }, + { "type": "uint32", "name": "net_usage_leeway" }, + { "type": "uint32", "name": "context_free_discount_net_usage_num" }, + { "type": "uint32", "name": "context_free_discount_net_usage_den" }, + { "type": "uint32", "name": "max_block_cpu_usage" }, + { "type": "uint32", "name": "target_block_cpu_usage_pct" }, + { "type": "uint32", "name": "max_transaction_cpu_usage" }, + { "type": "uint32", "name": "min_transaction_cpu_usage" }, + { "type": "uint32", "name": "max_transaction_lifetime" }, + { "type": "uint32", "name": "deferred_trx_expiration_window" }, + { "type": "uint32", "name": "max_transaction_delay" }, + { "type": "uint32", "name": "max_inline_action_size" }, + { "type": "uint16", "name": "max_inline_action_depth" }, + { "type": "uint16", "name": "max_authority_depth" } + ] + }, + { + "name": "chain_config_v1", "fields": [ + { "type": "uint64", "name": "max_block_net_usage" }, + { "type": "uint32", "name": "target_block_net_usage_pct" }, + { "type": "uint32", "name": "max_transaction_net_usage" }, + { "type": "uint32", "name": "base_per_transaction_net_usage" }, + { "type": "uint32", "name": "net_usage_leeway" }, + { "type": "uint32", "name": "context_free_discount_net_usage_num" }, + { "type": "uint32", "name": "context_free_discount_net_usage_den" }, + { "type": "uint32", "name": "max_block_cpu_usage" }, + { "type": "uint32", "name": "target_block_cpu_usage_pct" }, + { "type": "uint32", "name": "max_transaction_cpu_usage" }, + { "type": "uint32", "name": "min_transaction_cpu_usage" }, + { "type": "uint32", "name": "max_transaction_lifetime" }, + { "type": "uint32", "name": "deferred_trx_expiration_window" }, + { "type": "uint32", "name": "max_transaction_delay" }, + { "type": "uint32", "name": "max_inline_action_size" }, + { "type": "uint16", "name": "max_inline_action_depth" }, + { "type": "uint16", "name": "max_authority_depth" }, + { "type": "uint32", "name": "max_action_return_value_size" } + ] + }, + { + "name": "wasm_config_v0", "fields": [ + { "type": "uint32", "name": "max_mutable_global_bytes" }, + { "type": "uint32", "name": "max_table_elements" }, + { "type": "uint32", "name": "max_section_elements" }, + { "type": "uint32", "name": "max_linear_memory_init" }, + { "type": "uint32", "name": "max_func_local_bytes" }, + { "type": "uint32", "name": "max_nested_structures" }, + { "type": "uint32", "name": "max_symbol_bytes" }, + { "type": "uint32", "name": "max_module_bytes" }, + { "type": "uint32", "name": "max_code_bytes" }, + { "type": "uint32", "name": "max_pages" }, + { "type": "uint32", "name": "max_call_depth" } + ] + }, + { + "name": "global_property_v0", "fields": [ + { "type": "uint32?", "name": "proposed_schedule_block_num" }, + { "type": "producer_schedule", "name": "proposed_schedule" }, + { "type": "chain_config", "name": "configuration" } + ] + }, + { + "name": "global_property_v1", "fields": [ + { "type": "uint32?", "name": "proposed_schedule_block_num" }, + { "type": "producer_authority_schedule", "name": "proposed_schedule" }, + { "type": "chain_config", "name": "configuration" }, + { "type": "checksum256", "name": "chain_id" }, + { "type": "wasm_config$", "name": "wasm_configuration" } + ] + }, + { + "name": "generated_transaction_v0", "fields": [ + { "type": "name", "name": "sender" }, + { "type": "uint128", "name": "sender_id" }, + { "type": "name", "name": "payer" }, + { "type": "checksum256", "name": "trx_id" }, + { "type": "bytes", "name": "packed_trx" } + ] + }, + { + "name": "activated_protocol_feature_v0", "fields": [ + { "type": "checksum256", "name": "feature_digest" }, + { "type": "uint32", "name": "activation_block_num" } + ] + }, + { + "name": "protocol_state_v0", "fields": [ + { "type": "activated_protocol_feature[]", "name": "activated_protocol_features" } + ] + }, + { + "name": "key_weight", "fields": [ + { "type": "public_key", "name": "key" }, + { "type": "uint16", "name": "weight" } + ] + }, + { + "name": "permission_level", "fields": [ + { "type": "name", "name": "actor" }, + { "type": "name", "name": "permission" } + ] + }, + { + "name": "permission_level_weight", "fields": [ + { "type": "permission_level", "name": "permission" }, + { "type": "uint16", "name": "weight" } + ] + }, + { + "name": "wait_weight", "fields": [ + { "type": "uint32", "name": "wait_sec" }, + { "type": "uint16", "name": "weight" } + ] + }, + { + "name": "authority", "fields": [ + { "type": "uint32", "name": "threshold" }, + { "type": "key_weight[]", "name": "keys" }, + { "type": "permission_level_weight[]", "name": "accounts" }, + { "type": "wait_weight[]", "name": "waits" } + ] + }, + { + "name": "permission_v0", "fields": [ + { "type": "name", "name": "owner" }, + { "type": "name", "name": "name" }, + { "type": "name", "name": "parent" }, + { "type": "time_point", "name": "last_updated" }, + { "type": "authority", "name": "auth" } + ] + }, + { + "name": "permission_link_v0", "fields": [ + { "type": "name", "name": "account" }, + { "type": "name", "name": "code" }, + { "type": "name", "name": "message_type" }, + { "type": "name", "name": "required_permission" } + ] + }, + { + "name": "resource_limits_v0", "fields": [ + { "type": "name", "name": "owner" }, + { "type": "int64", "name": "net_weight" }, + { "type": "int64", "name": "cpu_weight" }, + { "type": "int64", "name": "ram_bytes" } + ] + }, + { + "name": "usage_accumulator_v0", "fields": [ + { "type": "uint32", "name": "last_ordinal" }, + { "type": "uint64", "name": "value_ex" }, + { "type": "uint64", "name": "consumed" } + ] + }, + { + "name": "resource_usage_v0", "fields": [ + { "type": "name", "name": "owner" }, + { "type": "usage_accumulator", "name": "net_usage" }, + { "type": "usage_accumulator", "name": "cpu_usage" }, + { "type": "uint64", "name": "ram_usage" } + ] + }, + { + "name": "resource_limits_state_v0", "fields": [ + { "type": "usage_accumulator", "name": "average_block_net_usage" }, + { "type": "usage_accumulator", "name": "average_block_cpu_usage" }, + { "type": "uint64", "name": "total_net_weight" }, + { "type": "uint64", "name": "total_cpu_weight" }, + { "type": "uint64", "name": "total_ram_bytes" }, + { "type": "uint64", "name": "virtual_net_limit" }, + { "type": "uint64", "name": "virtual_cpu_limit" } + ] + }, + { + "name": "resource_limits_ratio_v0", "fields": [ + { "type": "uint64", "name": "numerator" }, + { "type": "uint64", "name": "denominator" } + ] + }, + { + "name": "elastic_limit_parameters_v0", "fields": [ + { "type": "uint64", "name": "target" }, + { "type": "uint64", "name": "max" }, + { "type": "uint32", "name": "periods" }, + { "type": "uint32", "name": "max_multiplier" }, + { "type": "resource_limits_ratio", "name": "contract_rate" }, + { "type": "resource_limits_ratio", "name": "expand_rate" } + ] + }, + { + "name": "resource_limits_config_v0", "fields": [ + { "type": "elastic_limit_parameters", "name": "cpu_limit_parameters" }, + { "type": "elastic_limit_parameters", "name": "net_limit_parameters" }, + { "type": "uint32", "name": "account_cpu_usage_average_window" }, + { "type": "uint32", "name": "account_net_usage_average_window" } + ] + } + ], + "types": [ + { "new_type_name": "transaction_id", "type": "checksum256" } + ], + "variants": [ + { "name": "request", "types": ["get_status_request_v0", "get_blocks_request_v0", "get_blocks_ack_request_v0"] }, + { "name": "result", "types": ["get_status_result_v0", "get_blocks_result_v0"] }, + + { "name": "action_receipt", "types": ["action_receipt_v0"] }, + { "name": "action_trace", "types": ["action_trace_v0", "action_trace_v1"] }, + { "name": "partial_transaction", "types": ["partial_transaction_v0"] }, + { "name": "transaction_trace", "types": ["transaction_trace_v0"] }, + { "name": "transaction_variant", "types": ["transaction_id", "packed_transaction"] }, + + { "name": "table_delta", "types": ["table_delta_v0"] }, + { "name": "account", "types": ["account_v0"] }, + { "name": "account_metadata", "types": ["account_metadata_v0"] }, + { "name": "code", "types": ["code_v0"] }, + { "name": "contract_table", "types": ["contract_table_v0"] }, + { "name": "contract_row", "types": ["contract_row_v0"] }, + { "name": "contract_index64", "types": ["contract_index64_v0"] }, + { "name": "contract_index128", "types": ["contract_index128_v0"] }, + { "name": "contract_index256", "types": ["contract_index256_v0"] }, + { "name": "contract_index_double", "types": ["contract_index_double_v0"] }, + { "name": "contract_index_long_double", "types": ["contract_index_long_double_v0"] }, + { "name": "chain_config", "types": ["chain_config_v0", "chain_config_v1"] }, + { "name": "wasm_config", "types": ["wasm_config_v0"] }, + { "name": "global_property", "types": ["global_property_v0", "global_property_v1"] }, + { "name": "generated_transaction", "types": ["generated_transaction_v0"] }, + { "name": "activated_protocol_feature", "types": ["activated_protocol_feature_v0"] }, + { "name": "protocol_state", "types": ["protocol_state_v0"] }, + { "name": "permission", "types": ["permission_v0"] }, + { "name": "permission_link", "types": ["permission_link_v0"] }, + { "name": "resource_limits", "types": ["resource_limits_v0"] }, + { "name": "usage_accumulator", "types": ["usage_accumulator_v0"] }, + { "name": "resource_usage", "types": ["resource_usage_v0"] }, + { "name": "resource_limits_state", "types": ["resource_limits_state_v0"] }, + { "name": "resource_limits_ratio", "types": ["resource_limits_ratio_v0"] }, + { "name": "elastic_limit_parameters", "types": ["elastic_limit_parameters_v0"] }, + { "name": "resource_limits_config", "types": ["resource_limits_config_v0"] }, + { "name": "block_signing_authority", "types": ["block_signing_authority_v0"] } + ], + "tables": [ + { "name": "account", "type": "account", "key_names": ["name"] }, + { "name": "account_metadata", "type": "account_metadata", "key_names": ["name"] }, + { "name": "code", "type": "code", "key_names": ["vm_type", "vm_version", "code_hash"] }, + { "name": "contract_table", "type": "contract_table", "key_names": ["code", "scope", "table"] }, + { "name": "contract_row", "type": "contract_row", "key_names": ["code", "scope", "table", "primary_key"] }, + { "name": "contract_index64", "type": "contract_index64", "key_names": ["code", "scope", "table", "primary_key"] }, + { "name": "contract_index128", "type": "contract_index128", "key_names": ["code", "scope", "table", "primary_key"] }, + { "name": "contract_index256", "type": "contract_index256", "key_names": ["code", "scope", "table", "primary_key"] }, + { "name": "contract_index_double", "type": "contract_index_double", "key_names": ["code", "scope", "table", "primary_key"] }, + { "name": "contract_index_long_double", "type": "contract_index_long_double", "key_names": ["code", "scope", "table", "primary_key"] }, + { "name": "global_property", "type": "global_property", "key_names": [] }, + { "name": "generated_transaction", "type": "generated_transaction", "key_names": ["sender", "sender_id"] }, + { "name": "protocol_state", "type": "protocol_state", "key_names": [] }, + { "name": "permission", "type": "permission", "key_names": ["owner", "name"] }, + { "name": "permission_link", "type": "permission_link", "key_names": ["account", "code", "message_type"] }, + { "name": "resource_limits", "type": "resource_limits", "key_names": ["owner"] }, + { "name": "resource_usage", "type": "resource_usage", "key_names": ["owner"] }, + { "name": "resource_limits_state", "type": "resource_limits_state", "key_names": [] }, + { "name": "resource_limits_config", "type": "resource_limits_config", "key_names": [] } + ] +} diff --git a/tests/fixtures/antelope-worker/source.json b/tests/fixtures/antelope-worker/source.json new file mode 100644 index 00000000..d689f751 --- /dev/null +++ b/tests/fixtures/antelope-worker/source.json @@ -0,0 +1,113 @@ +{ + "schemaVersion": 1, + "constructed": true, + "consensusSignatureValid": false, + "protocolId": "antelope", + "profileId": "antelope-default-v1", + "upstream": { + "repository": "AntelopeIO/leap", + "tag": "v5.0.3", + "revision": "d133c6413ce8ce2e96096a0513ec25b4a8dbe837", + "path": "libraries/state_history/abi.cpp", + "blob": "d778d0c7fc40c96a070b860400f9d9244f1cba1b", + "extractedAbiSha256": "02cf367cafebb669176e8843bc7684bff5780c5bfd2d6feff328ea9a4044e411" + }, + "generator": { + "node": "v26.4.0", + "sourceSha256": "c1489bad9a2f910aff0778590bdb4b61b5a00cd6250b769d7c3968cd3b50f940" + }, + "source": { + "chainId": "0265b2a2bc02d602de2eb41b8fa4c69ca3ea6824baed8fe00b34af19d036eefe", + "blockNum": 12, + "blockId": "0000000c1419577a44ca431cdc96d602ba9212767ac93e0d65b98796c4df2a86", + "previousBlockId": "0000000baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "producer": "producer", + "timestamp": "2026-10-25T00:00:00.000", + "traceOrdinal": 0, + "transactionId": "3333333333333333333333333333333333333333333333333333333333333333", + "actionOrdinal": 1, + "receiptOrdinal": 0 + }, + "contract": { + "account": "counter", + "abiDeployment": { + "blockNum": 7, + "blockId": "0000000777777777777777777777777777777777777777777777777777777777", + "abiSequence": 3 + }, + "abi": { + "version": "eosio::abi/1.2", + "types": [], + "structs": [ + { + "name": "setvalue", + "base": "", + "fields": [ + { + "name": "value", + "type": "uint64" + } + ] + } + ], + "actions": [ + { + "name": "setvalue", + "type": "setvalue", + "ricardian_contract": "" + } + ], + "tables": [], + "ricardian_clauses": [], + "error_messages": [], + "abi_extensions": [], + "variants": [], + "action_results": [] + }, + "abiHex": "0e656f73696f3a3a6162692f312e3200010873657476616c756500010576616c75650675696e743634010000004a47b3b3c20873657476616c756500000000000000", + "abiSha256": "07f56fcc3b5aabe97142aeeda89d97a3684de09a8213ab9773142f02115f816e", + "actionHex": "0100000000000080" + }, + "expected": { + "account": "counter", + "name": "setvalue", + "receiver": "counter", + "authorization": [ + { + "actor": "alice", + "permission": "active" + } + ], + "data": { + "value": "9223372036854775809" + }, + "globalSequence": "9007199254740995", + "receiverSequence": "9223372036854775808", + "authSequence": "18446744073709551615", + "codeSequence": 2, + "abiSequence": 3, + "returnValue": "beef" + }, + "status": { + "hex": "000c0000000000000c1419577a44ca431cdc96d602ba9212767ac93e0d65b98796c4df2a860b0000000000000baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa010000000d000000010000000d0000000265b2a2bc02d602de2eb41b8fa4c69ca3ea6824baed8fe00b34af19d036eefe", + "sha256": "0535f142b3cd50c6b693b7b2b5ef4b71505d60a339360f546e834f4837c208fd", + "bytes": 121 + }, + "frames": { + "handoff": { + "hex": "010c0000000000000c1419577a44ca431cdc96d602ba9212767ac93e0d65b98796c4df2a860b0000000000000baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa010c0000000000000c1419577a44ca431cdc96d602ba9212767ac93e0d65b98796c4df2a86010b0000000000000baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa01b8010006e06400000057219de8ad01000000000baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001d10101003333333333333333333333333333333333333333333333333333333333333333000f000000020700000000000000100000000000000000010101000100000000e0aa3c3545dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd03000000000020000000000000000080010000000000855c34ffffffffffffffff0203000000e0aa3c3545000000e0aa3c35450000004a47b3b3c2010000000000855c3400000000a8ed32320801000000000000800000000000000000000000000002beef0000000000010100", + "sha256": "33c1acd62700aa0c2d18c88aeca57aa1b026cd51ca8de7c72bc3c7db5a50dd07", + "bytes": 549 + }, + "empty": { + "hex": "010c0000000000000c1419577a44ca431cdc96d602ba9212767ac93e0d65b98796c4df2a860b0000000000000baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa010c0000000000000c1419577a44ca431cdc96d602ba9212767ac93e0d65b98796c4df2a86010b0000000000000baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa01b8010006e06400000057219de8ad01000000000baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010100010100", + "sha256": "c7d001a2785d19fece02e617b36e0424d7102387ed0a4f4634ee65565d1710e2", + "bytes": 340 + }, + "missing": { + "hex": "010c0000000000000c1419577a44ca431cdc96d602ba9212767ac93e0d65b98796c4df2a860b0000000000000baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa010c0000000000000c1419577a44ca431cdc96d602ba9212767ac93e0d65b98796c4df2a86010b0000000000000baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa01b8010006e06400000057219de8ad01000000000baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010100", + "sha256": "8d5329fe0a20df91cc0883d979c8cc51db2ed91575a47b59f7667399fd6251f1", + "bytes": 338 + } + } +} diff --git a/tests/unit/antelope-attempt-worker.mjs b/tests/unit/antelope-attempt-worker.mjs new file mode 100644 index 00000000..e8db640a --- /dev/null +++ b/tests/unit/antelope-attempt-worker.mjs @@ -0,0 +1,213 @@ +/** Native emitted Antelope callbacks. ES, RabbitMQ and ABI lookup transports are controlled. + * All retained-store, source, proof, fresh-write and attempt validators are production implementations. + */ +import assert from 'node:assert/strict'; +import {EventEmitter} from 'node:events'; +import {createHash, randomUUID} from 'node:crypto'; +import {mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync} from 'node:fs'; +import {join} from 'node:path'; +import {tmpdir} from 'node:os'; +import {registerHooks} from 'node:module'; +import {frontierMemoryClient} from './protocol-frontier-memory.mjs'; +import {controlledAttemptLifecycle} from './worker-attempt-fixture.mjs'; + +const mode = process.argv[2], managedMain = mode !== 'handoff'; +assert(['handoff', 'empty', 'missing'].includes(mode)); +assert.equal(process.env.HYPERION_QUALIFICATION_PLAIN_PROOF, 'false'); +assert.equal(process.env.HYPERION_QUALIFICATION_PLAIN_HANDOFF, 'false'); +assert(!process.versions.bun && [24, 26].includes(Number(process.versions.node.split('.')[0]))); +const fixtureBytes = readFileSync(new URL('../fixtures/antelope-worker/source.json', import.meta.url)); +const fixture = JSON.parse(fixtureBytes), hash = bytes => createHash('sha256').update(bytes).digest('hex'); +const schemaBytes = readFileSync(new URL('../fixtures/antelope-worker/ship.abi.json', import.meta.url)); +assert.equal(hash(schemaBytes), fixture.upstream.extractedAbiSha256); +assert.equal(hash(readFileSync(new URL('../fixtures/antelope-worker/generate.mjs', import.meta.url))), fixture.generator.sourceSha256); +assert.equal(hash(Buffer.from(fixture.contract.abiHex, 'hex')), fixture.contract.abiSha256); +const frame = Buffer.from(fixture.frames[mode].hex, 'hex'); +assert.equal(frame.length, fixture.frames[mode].bytes); assert.equal(hash(frame), fixture.frames[mode].sha256); +let addonResolutions = 0; +const hooks = registerHooks({resolve(specifier, context, next) { + assert(!specifier.includes('hyperion-wire-plugin'), 'Builtin Antelope must not load Wire code'); + if (specifier.includes('node-abieos')) addonResolutions++; + return next(specifier, context); +}}); +class Consumer extends EventEmitter { + acked = []; streamed = []; + ack(message) {this.onAck?.(message); this.acked.push(message);} + publish(exchange, queue, payload, options) {this.streamed.push({exchange, queue, payload: Buffer.from(payload), options}); return true;} +} +class Publisher extends EventEmitter { + sent = []; + sendToQueue(queue, payload, options, confirm) { + this.sent.push({queue, payload: Buffer.from(payload), options: structuredClone(options)}); + queueMicrotask(() => confirm()); return true; + } +} +const message = sent => ({content: Buffer.from(sent.payload), properties: {headers: structuredClone(sent.options.headers)}, + fields: {deliveryTag: 1, redelivered: false}}); +const chain = 'antelope-attempt-original', chainId = fixture.source.chainId; +const directory = mkdtempSync(join(tmpdir(), 'hyperion-antelope-attempt-config-')), previousDirectory = process.cwd(); +const previousConfig = process.env.CONFIG_JSON, previousRole = process.env.worker_role, previousLive = process.env.live_mode; +let managed, reader, main, snapshotPath, finish; +const finished = new Promise(resolve => {finish = resolve;}); +process.on('message', value => {if (value?.event === 'qualification_done') finish();}); +const config = {settings: {chain, eosio_alias: 'eosio', parser: '3.2', debug: false, index_version: 'v1', + index_partition_size: 1000, es_replicas: 1}, indexer: {fetch_block: true, fetch_traces: true, process_deltas: false, + disable_indexing: false, max_inline: 0, live_reader: false}, + features: {contract_console: false, failed_trx: false, streaming: {traces: true}}, + scaling: {routing_mode: 'round_robin', ds_pool_size: 1, ds_queues: 1, ad_idx_queues: 1, indexing_queues: 1}, + api: {tx_cache_mode: 'batch', disable_tx_cache: false}, whitelists: {actions: [], deltas: [], root_only: false}, + blacklists: {actions: [], deltas: []}, plugins: {}}; +try { + mkdirSync(join(directory, 'config')); + writeFileSync(join(directory, 'chain.json'), JSON.stringify(config)); + writeFileSync(join(directory, 'config/connections.json'), JSON.stringify({chains: {[chain]: {chain_id: chainId}}})); + process.chdir(directory); process.env.CONFIG_JSON = join(directory, 'chain.json'); + process.env.worker_role = managedMain ? 'deserializer' : 'ds_pool_worker'; + process.env.live_mode = 'false'; + const [{default: Reader}, {default: Main}, {default: Pool}, {default: Parser}, {HyperionModuleLoader}, + {loadChainProtocol}, {ActionSourceAdmissionStore, actionSourceAdmissionHeader}, + {WorkerAttemptClient}] = await Promise.all([ + import('../../build/indexer/workers/state-reader.js'), import('../../build/indexer/workers/deserializer.js'), + import('../../build/indexer/workers/ds-pool.js'), import('../../build/indexer/modules/parsers/3.2-parser.js'), + import('../../build/indexer/modules/loader.js'), import('../../build/indexer/protocol/selection.js'), + import('../../build/indexer/history/action-source-admission.js'), import('../../build/indexer/protocol/worker-attempt-client.js')]); + const selected = await loadChainProtocol(undefined); + assert.equal(selected.id, 'antelope'); assert.equal(selected.provider.profileId, fixture.profileId); + assert.equal(selected.provider.abiObservations, undefined); assert(addonResolutions > 0); + const nativeCalls = [], originalDecode = selected.decoder.binToJson.bind(selected.decoder); + selected.decoder.binToJson = (contract, type, bytes) => { + nativeCalls.push({contract, type, sha256: hash(bytes)}); return originalDecode(contract, type, bytes); + }; + const memory = frontierMemoryClient(1), store = new ActionSourceAdmissionStore(memory.client, chain, 1); + const targetName = chain + '-action-v1-000001', targetUuid = 'owned-antelope-worker-target'; + memory.settings.set(targetName, {'index.uuid': targetUuid}); + memory.client.indices.exists = async ({index}) => memory.settings.has(index); + const abiReads = []; + memory.client.search = async request => { + assert.equal(request.index, chain + '-abi-*', 'Only constructed historical ABI lookup is permitted'); + assert.deepEqual(request.query.bool.must, [{term: {account: fixture.contract.account}}, + {range: {block: {lte: fixture.source.blockNum}}}]); + abiReads.push(structuredClone(request)); + return {hits: {hits: [{_source: {account: fixture.contract.account, block: fixture.contract.abiDeployment.blockNum, + block_id: fixture.contract.abiDeployment.blockId, abi_hex: fixture.contract.abiHex, + actions: ['setvalue'], tables: []}}]}}; + }; + const filters = {action_blacklist: new Set(), action_whitelist: new Set(), delta_blacklist: new Set(), delta_whitelist: new Set()}; + const cm = {config, filters}, parser = new Parser(cm), loader = Object.create(HyperionModuleLoader.prototype); + Object.assign(loader, {handledActions: new Map(), handledDeltas: new Map(), parser, plugins: []}); + const receipts = [], recordIds = prefix => [...memory.documents.values()].map(x => x.document) + .filter(x => x.kind === 'manifest' && x.bundle_id.startsWith(prefix)).map(x => x.bundle_id); + memory.faults.receipt = (receipt, request) => { + if (request.document.kind === 'manifest' && /^(h45actionattempt1:|h45actionhandoff1:)/.test(request.id)) { + assert.equal(request.wait_for_active_shards, 'all'); + assert.deepEqual(receipt._shards, {total: 2, successful: 2, failed: 0}); + receipts.push({id: request.id, shards: receipt._shards}); + process.send({event: 'qualification_write_receipt', id: request.id, shards: receipt._shards}); + } + return receipt; + }; + function worker(prototype, role) { + const value = Object.create(prototype), consumer = new Consumer(), publisher = new Publisher(); + Object.assign(value, {chain, chainId, conf: config, configModule: cm, filters, mLoader: loader, + protocol: selected.provider, protocolId: selected.id, protocolComponents: selected.components, + protocolReady: Promise.resolve(), abieos: selected.decoder, client: memory.client, + ch: consumer, cch: publisher, ch_ready: true, cch_ready: true, profiler: {start: () => () => {}}, + actionAdmissionOwners: new WeakMap(), actionAdmissionStore: store}); + Object.defineProperty(value, 'actionSourceAdmissionEnabled', {value: true}); + if (role) { + if ((role === 'deserializer') === managedMain) { + managed = new WorkerAttemptClient({mode: 'managed', role, transport: process, timeoutMs: 1000, drainTimeoutMs: 5000, retries: 2}); + value.configureWorkerAttemptLifecycle(managed); + consumer.onAck = () => process.send({event: 'qualification_ack', role}); + } else controlledAttemptLifecycle(value, role); + } + return value; + } + reader = worker(Reader.prototype); + main = worker(Main.prototype, 'deserializer'); + const pool = worker(Pool.prototype, 'ds_pool_worker'), cache = [], blockWrites = []; + Object.assign(reader, {baseRequest: {}, shipRev: 0, socketGeneration: 0, activeIngressGeneration: 0, + isLiveReader: false, local_block_num: 11, local_distributed_count: 0, receivedFirstBlock: true, + future_block: 0, currentIdx: 1, range_size: 1, publications: new Map(), send() {}, handleStatusResult() {}}); + reader.stageOneDistQueue = {push: async payload => new Promise(resolve => reader.distribute([payload], resolve))}; + Object.assign(main, {local_block_count: 0, lastSelectedWorker: 0, dsPoolMap: {}, ds_pool_counters: {}, block_emit_idx: 1, + preIndexingQueue: {push: async payload => blockWrites.push(payload)}}); + Object.assign(pool, {poolAbiCache: new Map(), poolTypeCache: new Map(), loadedActionAbis: new Map(), failedAbiMap: new Map(), + protocolCacheViews: new Set(), contractUsage: {}, temp_ds_counter: 0, actionDsCounter: 0, act_emit_idx: 1, + txCacheExpiration: 3600, allowStreaming: true, + common: {deserializeActionAtBlockNative: Pool.prototype.deserializeActionAtBlockNative, attachActionExtras: Pool.prototype.attachActionExtras}, + pushToActionsQueue() {assert.fail('Original admitted Antelope action bypassed the confirmed publisher');}, + ioRedisClient: {pipeline() {const writes = []; return {hset(key, values) {writes.push({key, values: [...values]}); return this;}, + expire() {return this;}, async exec() {cache.push(...writes); return [];}};}}}); + const schemaJson = schemaBytes.toString(); + await reader.initializeProtocolShip(schemaJson); + await reader.processFirstABI(schemaBytes); + await main.initializeProtocolShip(schemaJson); + const status = Buffer.from(fixture.status.hex, 'hex'); + assert.equal(hash(status), fixture.status.sha256); assert.equal(status.length, fixture.status.bytes); + await reader.onMessage(status); + assert.equal(await reader.onMessage(frame), 'block'); + for (let i = 0; i < 5; i++) await new Promise(resolve => setImmediate(resolve)); + assert.equal(reader.cch.sent.length, 1); assert(reader.cch.sent[0].payload.equals(frame)); + const sourceMessage = message(reader.cch.sent[0]); + main.captureActionAdmissionDelivery(sourceMessage, main.ch); + await parser.parseMessage(main, [sourceMessage]); + assert.equal(main.ch.acked.length, 1); + if (mode === 'handoff') { + assert.equal(main.cch.sent.length, 1); const incoming = message(main.cch.sent[0]); + pool.captureActionAdmissionDelivery(incoming, pool.ch); + await pool.processMessages([incoming]); + assert.equal(pool.ch.acked.length, 1); assert.equal(pool.cch.sent.length, 1); + assert.equal(abiReads.length, 1); + const emitted = JSON.parse(pool.cch.sent[0].payload.toString()); + process.send({event: 'qualification_output', emitted}); + const output = Array.isArray(emitted) ? emitted[0] : emitted; + assert.equal(output.act.account, fixture.expected.account); + assert.equal(output.act.name, fixture.expected.name); + assert.deepEqual(output.act.data, fixture.expected.data); + assert.deepEqual(output.act.authorization, fixture.expected.authorization); + assert.equal(output.global_sequence, fixture.expected.globalSequence); + assert.equal(output.receipts[0].recv_sequence, fixture.expected.receiverSequence); + assert.equal(output.receipts[0].receiver, fixture.expected.receiver); + assert.deepEqual(output.receipts[0].auth_sequence, [{account: 'alice', sequence: fixture.expected.authSequence}]); + assert.equal(output.code_sequence, fixture.expected.codeSequence); + assert.equal(output.abi_sequence, fixture.expected.abiSequence); + assert.equal(output.return_value.toLowerCase(), fixture.expected.returnValue); + assert.equal(output.trx_id, fixture.source.transactionId); + assert.equal(output.block_num, fixture.source.blockNum); + assert.equal(output.block_id, fixture.source.blockId); + } else { + assert.equal(main.cch.sent.length, 0); assert.equal(abiReads.length, 0); + } + assert(nativeCalls.some(x => x.type === 'signed_block')); + if (mode !== 'missing') assert(nativeCalls.some(x => x.type === 'transaction_trace[]')); + if (mode === 'handoff') assert(nativeCalls.some(x => x.contract === fixture.contract.account && x.type === 'setvalue' + && x.sha256 === hash(Buffer.from(fixture.contract.actionHex, 'hex')))); + const prefix = mode === 'handoff' ? 'h45actionhandoff1:' : 'h45actionattempt1:'; + const ids = recordIds(prefix); assert.equal(ids.length, 1); assert.equal(receipts.length, 1); + const admissionId = mode === 'handoff' ? main.cch.sent[0].options.headers[actionSourceAdmissionHeader] + : recordIds('h45actionadmission1:')[0]; + const admission = await store.resolve(admissionId); + assert.equal(admission.source.frameHex, frame.toString('hex')); assert.equal(admission.source.inventoryInput.profileId, fixture.profileId); + assert.equal(admission.source.inventoryInput.source.blockId, fixture.source.blockId); + assert.equal(admission.source.producer.protocolId, 'antelope'); + const snapshot = {chain, mode, config, targetName, targetUuid, admissionId, recordId: ids[0], + fixtureSha256: hash(fixtureBytes), frameSha256: hash(frame), nativeCalls, abiReads, addonResolutions, + producer: admission.source.producer, executableSha256: admission.source.executableSha256, + runtime: {node: process.version, executableSha256: hash(readFileSync(process.execPath))}, + publishers: {reader: reader.cch.sent, main: main.cch.sent, pool: pool.cch.sent}, + documents: [...memory.documents], mappings: [...memory.mappings], settings: [...memory.settings], sequences: [...memory.sequences]}; + snapshotPath = join(tmpdir(), 'hyperion-antelope-attempt-' + randomUUID() + '.json'); + const bytes = Buffer.from(JSON.stringify(snapshot)); writeFileSync(snapshotPath, bytes, {flag: 'wx'}); + process.send({event: 'qualification_fixture', path: snapshotPath, bytes: bytes.length, sha256: hash(bytes)}); + await finished; +} finally { + managed?.dispose(); reader?.shipSession?.close?.(); main?.shipSession?.close?.(); hooks.deregister(); + process.chdir(previousDirectory); + if (previousConfig === undefined) delete process.env.CONFIG_JSON; else process.env.CONFIG_JSON = previousConfig; + if (previousRole === undefined) delete process.env.worker_role; else process.env.worker_role = previousRole; + if (previousLive === undefined) delete process.env.live_mode; else process.env.live_mode = previousLive; + rmSync(directory, {recursive: true, force: true}); + if (snapshotPath) rmSync(snapshotPath, {force: true}); + process.disconnect?.(); +} diff --git a/tests/unit/antelope-attempts.mjs b/tests/unit/antelope-attempts.mjs new file mode 100644 index 00000000..91e88a13 --- /dev/null +++ b/tests/unit/antelope-attempts.mjs @@ -0,0 +1,129 @@ +/** Native Antelope lifecycle plus actual master adjudication; external inputs are controlled. */ +import assert from 'node:assert/strict'; +import cluster from 'node:cluster'; +import {readFileSync, statSync} from 'node:fs'; +import {createHash} from 'node:crypto'; +import {tmpdir} from 'node:os'; +import {basename, dirname, resolve} from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {Checksum256, UInt32} from '@wharfkit/antelope'; +import {WorkerAttemptSupervisor, describeWorkerAttemptEnd} from '../../build/indexer/protocol/worker-attempt-supervisor.js'; +import {HyperionMaster} from '../../build/indexer/modules/master.js'; +import {HyperionModuleLoader} from '../../build/indexer/modules/loader.js'; +import {ActionSourceAdmissionStore} from '../../build/indexer/history/action-source-admission.js'; +import {frontierMemoryClient} from './protocol-frontier-memory.mjs'; + +const mode = process.argv[2] ?? 'handoff', role = mode === 'handoff' ? 'ds_pool_worker' : 'deserializer'; +const fixtureBytes = readFileSync(new URL('../fixtures/antelope-worker/source.json', import.meta.url)); +const input = JSON.parse(fixtureBytes), hash = value => createHash('sha256').update(value).digest('hex'); +const ends = [], supervisor = new WorkerAttemptSupervisor({onEnd: value => ends.push(value)}), events = []; +cluster.setupPrimary({exec: fileURLToPath(new URL('./antelope-attempt-worker.mjs', import.meta.url)), args: [mode], silent: true}); +const child = cluster.fork({HYPERION_QUALIFICATION_PLAIN_PROOF: 'false', HYPERION_QUALIFICATION_PLAIN_HANDOFF: 'false'}); +let exited = false, exitCode, stdout = '', stderr = ''; +child.on('exit', code => {exited = true; exitCode = code;}); +child.process.stdout.on('data', bytes => {stdout += bytes;}); +child.process.stderr.on('data', bytes => {stderr += bytes;}); +child.on('message', value => events.push(value)); supervisor.attach(child, role); +async function until(predicate, label) { + const deadline = Date.now() + 30_000; + while (!predicate()) { + assert(!exited && Date.now() < deadline, JSON.stringify({label, exitCode, stdout, stderr, events: events.map(x => x.event)})); + await new Promise(resolve => setTimeout(resolve, 5)); + } +} +try { + await until(() => events.some(x => x.event === 'qualification_fixture') && ends.length === 1, 'Original native callback and authentic end'); + const receipt = events.find(x => x.event === 'qualification_fixture'), path = resolve(receipt.path); + assert.equal(dirname(path), resolve(tmpdir())); assert.match(basename(path), /^hyperion-antelope-attempt-[0-9a-f-]+\.json$/); + assert(receipt.bytes > 0 && receipt.bytes <= 64 * 1024 * 1024); + assert.equal(statSync(path).size, receipt.bytes); const bytes = readFileSync(path); assert.equal(hash(bytes), receipt.sha256); + const retained = JSON.parse(bytes), memory = frontierMemoryClient(1); + for (const key of ['documents', 'mappings', 'settings', 'sequences']) for (const [id, value] of retained[key]) memory[key].set(id, value); + memory.client.indices.exists = async ({index}) => memory.settings.has(index); + assert.equal(retained.fixtureSha256, hash(fixtureBytes)); assert.equal(retained.frameSha256, input.frames[mode].sha256); + const end = describeWorkerAttemptEnd(supervisor, ends[0]); + assert.equal(end.role, role); assert.equal(end.status, 'quiescent'); assert.equal(end.rootResult, 'returned'); + assert.equal(end.acknowledgementAttempted, true); assert.equal(end.binding.blockNum, input.source.blockNum); + assert.equal(end.binding.blockId, input.source.blockId); assert.equal(end.freshWrites.length, 1); + assert.equal(end.freshWrites[0].id, retained.recordId); assert.equal(end.binding.admissionId, retained.admissionId); + assert.throws(() => describeWorkerAttemptEnd(supervisor, structuredClone(end)), /Unissued or foreign/); + const store = new ActionSourceAdmissionStore(memory.client, retained.chain, 1), admission = await store.resolve(retained.admissionId); + const originalPayload = role === 'deserializer' ? Buffer.from(admission.source.frameHex, 'hex') + : Buffer.from(admission.inventory.transactions[0].dispatch.canonicalJson); + assert.equal(hash(originalPayload), end.descriptor.payloadSha256); + const classification = mode === 'handoff' ? 'confirmed-handoff' : mode === 'empty' ? 'intentional-no-effect' : 'source-incomplete'; + if (mode !== 'handoff') { + const value = (await store.readAttemptDisposition(retained.recordId)).observation; + assert.equal(value.disposition, mode === 'empty' ? 'no-effects' : 'source-unprocessable'); + if (mode === 'missing') {assert.equal(value.reason, 'source-traces-missing'); assert.deepEqual(value.output, {kind: 'unresolved'});} + assert.equal(value.work.transaction, null); + } + const loader = Object.assign(Object.create(HyperionModuleLoader.prototype), {handledActions: new Map(), plugins: []}); + const filters = {action_blacklist: new Set(), action_whitelist: new Set(), delta_blacklist: new Set(), delta_whitelist: new Set()}; + let canonicalId = input.source.blockId, lib = input.source.blockNum; + const rpcCalls = [], master = Object.assign(Object.create(HyperionMaster.prototype), {chain: retained.chain, esClient: memory.client, + conf: retained.config, cm: {filters}, mLoader: loader, protocolSourceProbes: new Map(), + rpc: {v1: {chain: {get_info: async () => ({chain_id: Checksum256.from(input.source.chainId), + head_block_num: UInt32.from(input.source.blockNum), last_irreversible_block_num: UInt32.from(lib)})}}, + call: async request => {rpcCalls.push(request); assert.equal(request.path, '/v1/chain/get_block'); + assert.deepEqual(request.params, {block_num_or_id: input.source.blockNum}); + return {block_num: input.source.blockNum, id: canonicalId, previous: input.source.previousBlockId};}}}); + const adjudicator = master.attemptAdjudicatorFor(supervisor); + const associationIds = () => [...memory.documents.values()].map(x => x.document).filter(x => x.kind === 'manifest' + && x.bundle_id.startsWith('h45actionassociation1:')).map(x => x.bundle_id); + const guards = []; + if (mode === 'handoff') { + master.conf.indexer.disable_indexing = true; + assert.deepEqual(await adjudicator.adjudicate(ends[0]), {state: 'unresolved'}); + assert.equal(associationIds().length, 0); guards.push('current-configuration'); + master.conf.indexer.disable_indexing = false; + memory.settings.get(retained.targetName)['index.uuid'] = 'different-physical-target'; + assert.deepEqual(await adjudicator.adjudicate(ends[0]), {state: 'unresolved'}); + assert.equal(associationIds().length, 0); guards.push('physical-target-uuid'); + memory.settings.get(retained.targetName)['index.uuid'] = retained.targetUuid; + canonicalId = 'f'.repeat(64); + assert.deepEqual(await adjudicator.adjudicate(ends[0]), {state: 'unresolved'}); + assert.equal(associationIds().length, 0); guards.push('canonical-block-identity'); + canonicalId = input.source.blockId; lib--; + // Builtin Antelope currently has no selected reversible source. Do not grant one from an RPC boolean. + assert.deepEqual(await adjudicator.adjudicate(ends[0]), {state: 'unresolved'}); + assert.equal(associationIds().length, 0); guards.push('reversible-without-source-selection'); lib++; + const originalSettings = memory.client.indices.getSettings; + let entered, release; + const enteredRead = new Promise(resolve => {entered = resolve;}), releasedRead = new Promise(resolve => {release = resolve;}); + memory.client.indices.getSettings = async request => { + const result = await originalSettings(request); entered(); await releasedRead; return result; + }; + const pending = adjudicator.adjudicate(ends[0]); pending.catch(() => {}); + await enteredRead; master.conf.indexer.disable_indexing = true; release(); + assert.deepEqual(await pending, {state: 'unresolved'}); assert.equal(associationIds().length, 0); + memory.client.indices.getSettings = originalSettings; master.conf.indexer.disable_indexing = false; + guards.push('configuration-changed-during-actual-target-read'); + } + rpcCalls.length = 0; + assert.deepEqual(await adjudicator.adjudicate(ends[0]), {state: 'associated', id: expectAssociationId()}); + function expectAssociationId() { + const ids = associationIds(); + assert.equal(ids.length, 1); return ids[0]; + } + const association = (await store.readAttemptAssociation(expectAssociationId())).observation; + assert.equal(association.end.attemptId, end.attemptId); assert.equal(association.end.incarnation, end.incarnation); + assert.equal(association.sourceChecked, true); assert.equal(association.brokerSettlement, 'unknown'); + assert.deepEqual(association.results, [{id: retained.recordId, classification}]); assert.equal(rpcCalls.length, 1); + const receiptAt = events.findIndex(x => x.event === 'qualification_write_receipt'); + const observedAt = events.findIndex(x => x.event === 'worker_attempt_request' && x.payload?.operation === 'observe'); + const ackAt = events.findIndex(x => x.event === 'qualification_ack'); + const endAt = events.findIndex(x => x.event === 'worker_attempt_request' && x.payload?.operation === 'end'); + assert(receiptAt >= 0 && observedAt > receiptAt && ackAt > observedAt && endAt > ackAt); + child.send({event: 'qualification_done'}); await until(() => exited, 'Owned native child exits'); + assert.equal(exitCode, 0); assert.equal(stderr, ''); + console.log(JSON.stringify({passed: true, mode, runtime: retained.runtime, originalAntelopeCallbacks: true, + builtinNativeAddon: true, actualMasterAdjudicator: true, samePhysicalFreshReceiptAndEnd: true, + classification, guards, sourceSha256: retained.frameSha256, fixtureSha256: retained.fixtureSha256, + fixtureSourceSha256: hash(readFileSync(new URL('./antelope-attempt-worker.mjs', import.meta.url))), producer: retained.producer, + executableSha256: retained.executableSha256, sourceCheckedAssociation: association.results, + brokerSettlement: 'unknown', controlledSourceStorageRpcTransport: true, capacityAuthority: false, releaseQualified: false})); +} finally { + if (!exited) {child.send({event: 'qualification_done'}); child.process.kill(); await until(() => exited, 'Failed-case child cleanup').catch(() => {});} + supervisor.stop(); +} diff --git a/tests/unit/antelope-attempts.test.ts b/tests/unit/antelope-attempts.test.ts new file mode 100644 index 00000000..3616ab47 --- /dev/null +++ b/tests/unit/antelope-attempts.test.ts @@ -0,0 +1,19 @@ +import {expect, test} from 'bun:test'; +import {fileURLToPath} from 'node:url'; + +for (const mode of ['handoff', 'empty', 'missing']) test('original builtin Antelope attempt has source-bound ' + mode + ' evidence', async () => { + const child = Bun.spawn([process.env.HYPERION_NATIVE_NODE ?? 'node', + fileURLToPath(new URL('./antelope-attempts.mjs', import.meta.url)), mode], + {stdout: 'pipe', stderr: 'pipe', timeout: 30_000}); + const [code, stdout, stderr] = await Promise.all([child.exited, new Response(child.stdout).text(), new Response(child.stderr).text()]); + expect({code, stderr, stdout: stdout.slice(-12000)}).toMatchObject({code: 0, stderr: ''}); + const result = JSON.parse(stdout.trim().split('\n').at(-1)!); + expect(result).toMatchObject({passed: true, mode, originalAntelopeCallbacks: true, builtinNativeAddon: true, + actualMasterAdjudicator: true, samePhysicalFreshReceiptAndEnd: true, brokerSettlement: 'unknown', + controlledSourceStorageRpcTransport: true, capacityAuthority: false, releaseQualified: false}); + expect(result.producer).toMatchObject({protocolId: 'antelope', profileId: 'antelope-default-v1'}); + expect(result.runtime.node).toMatch(/^v(?:24|26)\./); + expect(result.runtime.executableSha256).toMatch(/^[0-9a-f]{64}$/); + if (mode === 'handoff') expect(result.guards).toEqual(['current-configuration', 'physical-target-uuid', + 'canonical-block-identity', 'reversible-without-source-selection', 'configuration-changed-during-actual-target-read']); +}, 35_000);