From f743234e8f3be2f923374ad84e00d73d9013a572 Mon Sep 17 00:00:00 2001 From: Robrigo <590263+robrigo@users.noreply.github.com> Date: Thu, 16 Jul 2026 12:31:07 +0000 Subject: [PATCH] feat: gate chain-specific host functions to the emulated chain wax-vert registers verify_rsa_sha256_sig for every Blockchain. It exists on WAX and not on EOS, Jungle4 or Vaulta, so a contract importing it passed the harness and would fail to load on those chains: the harness was more permissive than three of the four chains these contracts deploy to. Host functions unique to a chain are now withheld unless the emulated chain provides them. A Blockchain is generic Antelope by default and exposes none; new Blockchain({ chain: 'wax' }) adds the WAX set. The VM deletes any withheld function from its env imports before instantiating, so a contract importing one fails to instantiate under the wrong chain, mirroring how setcode rejects it. The two existing RSA tests now run against a wax blockchain, and new tests assert the function is absent by default, present under wax, absent under an unknown chain name, and that withholding it leaves the shared host functions intact. The set is declared in CHAIN_SPECIFIC_HOST_FUNCTIONS, so a new chain-specific function is one map entry. Version 2.2.0: this adds behavior over the 2.1.1 base and gives the fork its own version line. --- README.md | 22 ++++++++++++++++----- examples/rsa/rsa.spec.ts | 2 +- package.json | 2 +- src/antelope/blockchain.ts | 32 +++++++++++++++++++++++++++++-- src/antelope/tests/vm.spec.ts | 36 ++++++++++++++++++++++++++++++++--- src/antelope/vm.ts | 12 +++++++++++- 6 files changed, 93 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index ab3470f..9652e0a 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,24 @@ The focus of VeRT is on the better compatibility than the performance, so it can ## Chain compatibility Antelope chains do not all expose the same host functions, and a harness that offers more than the -target chain will link a contract that the chain rejects at `setcode`. +target chain will link a contract that the chain rejects at `setcode`. To keep a passing suite +meaningful, chain-specific host functions are withheld unless the emulated chain provides them. -`verify_rsa_sha256_sig` is currently registered for every `Blockchain`. It exists on WAX; it does not -exist on EOS, Jungle4, or Vaulta. A contract that calls it therefore passes here and fails to load on -those chains. Until the host function set is selectable per chain, treat a passing suite as evidence -for WAX only when RSA is involved. +A `Blockchain` emulates generic Antelope by default, which exposes no chain-specific host functions. +Name a chain to add the ones unique to it: + +```typescript +const bc = new Blockchain(); // generic Antelope +const wax = new Blockchain({ chain: 'wax' }); // adds verify_rsa_sha256_sig +``` + +`verify_rsa_sha256_sig` exists on WAX and not on EOS, Jungle4, or Vaulta. A contract that imports it +instantiates under a `wax` blockchain and fails to instantiate under any other, which mirrors how +`setcode` accepts it on WAX and rejects it elsewhere. Test WAX contracts that use RSA against a +`wax` blockchain, and test everything else against the default. + +The chain-specific host functions are declared in `CHAIN_SPECIFIC_HOST_FUNCTIONS`; add an entry +there to model a new one. ## Installation diff --git a/examples/rsa/rsa.spec.ts b/examples/rsa/rsa.spec.ts index f3d89da..f90d888 100644 --- a/examples/rsa/rsa.spec.ts +++ b/examples/rsa/rsa.spec.ts @@ -30,7 +30,7 @@ const modulus1 = 'b67b5732be0888309dfba35e310eee09641a3f609ec94fdfb45aeaec1231e08268f2a065fffb00aa41eaec560af2bedc0d48cd647b89a8a44b4e0a5fef365640ad379d05112e063467f973c0053657534b1c76cbed8aae705d3453b1581b6badbff41ea2ff5a84e84b06e4293978f7d5389180803f5b27c13290f209c647ee0a8de4184d39f6d4e66a01ffd13ac0740a997b9e05023a51b9c281485685c0cfe3743dbc788cc3aac31c2f35a53414ff236ed2a998aa3617f3bda2f6163aa5254cf60f7d73b4d553b1d2fbd057299a297832cd9e8d2a1786b4260188889e9f7dd713dc1c22c6dda8e001ed76114e41529caa575ff6bc54a79d7ed6f6442b9fe84712ec2bae06560eb3fe40292143f69ae67e72ef7a010d95879df4edfb0ed74a2a7b9aeaade0c02a73a9a27c710dba0020891a9585cae9b6937f82c56c20017107990101a86c71b6c759abc5be23eb790c795e138363c40c29c8ec0fae65ad1de30bd2a5b0bbedc633caf21a8eae0d5afced68fb1a2a1cf5a175d5207ffcfad69de17cb839ab82f6ac1833fbe641eb869be9d9cd5e742bd79b7472eed3d39956c4b5eb9578cf92ba9202ddab1b0f81dc05c85380fb85a67adc88ae295de66cdc2977c2f6273acd65f234684cb9b5e60ab75cb6f433eb12961afe295247d7819d5ba4213d4902039234506f5109534734e65c28e2a8078afc3b59b92e7f329f791b'; // Initialize blockchain -const blockchain = new Blockchain(); +const blockchain = new Blockchain({ chain: 'wax' }); // verify_rsa_sha256_sig is a WAX host function // Create RSA contract const contractName = Name.from('rsa'); diff --git a/package.json b/package.json index 65e805a..3f6713c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@atomichub/vert", - "version": "2.1.1", + "version": "2.2.0", "homepage": "https://github.com/atomicassets/vert", "description": "Testing library for Antelope smart contracts, with per-chain host function parity", "main": "dist/index.js", diff --git a/src/antelope/blockchain.ts b/src/antelope/blockchain.ts index 1b5094f..ebd02d0 100644 --- a/src/antelope/blockchain.ts +++ b/src/antelope/blockchain.ts @@ -14,6 +14,20 @@ import colors from 'colors' import { ACTIVATED_PROTOCOL_FEATURES } from "../utils/activatedFeatures"; import Buffer from '../buffer' +// Host functions that only some Antelope chains expose. A contract importing one +// fails to load (setcode) on a chain that lacks it, so the VM must withhold it +// on those chains too, or a passing test would not reflect the target chain. +// Keyed by chain, valued by the host functions unique to it. +export const CHAIN_SPECIFIC_HOST_FUNCTIONS: Record = { + wax: ['verify_rsa_sha256_sig'], +} + +// The union of every chain-specific host function, used by the VM to withhold +// any that the target chain does not provide. +export const ALL_CHAIN_SPECIFIC_HOST_FUNCTIONS: ReadonlySet = new Set( + Object.values(CHAIN_SPECIFIC_HOST_FUNCTIONS).flat() +) + export class Blockchain { accounts: { [key: string]: Account } timestamp: TimePoint @@ -30,22 +44,36 @@ export class Blockchain { postStorage: any storageDeltaChangesets: any _storageDeltas: any - + + // The chain being emulated, if any. Undefined means generic Antelope, which + // exposes no chain-specific host functions. Naming a chain (e.g. 'wax') adds + // the host functions unique to it. + chain?: string + constructor ({ accounts, timestamp, blockNum, store, + chain, }: { accounts?: { [key: string]: Account }, timestamp?: TimePoint, blockNum?: number, - store?: TableStore + store?: TableStore, + chain?: string } = {}) { this.accounts = accounts || {} this.timestamp = timestamp || TimePoint.fromMilliseconds(0) this.blockNum = blockNum || 0 this.store = store || new TableStore() + this.chain = chain + } + + // The chain-specific host functions this chain provides. Generic Antelope + // provides none. + enabledChainHostFunctions (): ReadonlySet { + return new Set(this.chain ? (CHAIN_SPECIFIC_HOST_FUNCTIONS[this.chain] ?? []) : []) } private applyTransactionActions(transaction: Transaction, decodedData?: any) { diff --git a/src/antelope/tests/vm.spec.ts b/src/antelope/tests/vm.spec.ts index 979fea8..ae55027 100644 --- a/src/antelope/tests/vm.spec.ts +++ b/src/antelope/tests/vm.spec.ts @@ -10,16 +10,23 @@ const NodeRSA = require('node-rsa'); const crypto = require('crypto'); const bc = new Blockchain() +// A WAX chain exposes verify_rsa_sha256_sig, which generic Antelope withholds. +const waxBc = new Blockchain({ chain: 'wax' }) let vm; +let waxVm; let memory; beforeEach(() => { bc.clearConsole() - vm = VM.from(new Uint8Array(), bc); + waxBc.clearConsole() memory = Memory.create(256); + vm = VM.from(new Uint8Array(), bc); + waxVm = VM.from(new Uint8Array(), waxBc); // @ts-ignore vm._memory = memory; + // @ts-ignore + waxVm._memory = memory; }); describe('eos-vm imports', () => { @@ -108,7 +115,7 @@ describe('eos-vm imports', () => { const modulusLen = modulus.length; // Call assert_valid_rsa_sig - should not throw - let valid = vm.imports.env.verify_rsa_sha256_sig( + let valid = waxVm.imports.env.verify_rsa_sha256_sig( messageHashOffset, messageHashLen, signatureOffset, signatureLen, exponentOffset, exponentLen, @@ -169,7 +176,7 @@ describe('eos-vm imports', () => { const modulusLen = modulus.length; // Call assert_invalid_rsa_sig - should not throw because signature is indeed invalid - let result = vm.imports.env.verify_rsa_sha256_sig( + let result = waxVm.imports.env.verify_rsa_sha256_sig( messageHashOffset, messageHashLen, signatureOffset, signatureLen, exponentOffset, exponentLen, @@ -201,6 +208,29 @@ describe('eos-vm imports', () => { }); }); + describe('chain-specific host functions', () => { + it('generic Antelope withholds verify_rsa_sha256_sig', () => { + // A contract importing it would fail to instantiate here, exactly as + // setcode rejects it on EOS, Jungle4 and Vaulta. + expect(vm.imports.env.verify_rsa_sha256_sig).to.equal(undefined); + }); + + it('a WAX chain provides verify_rsa_sha256_sig', () => { + expect(waxVm.imports.env.verify_rsa_sha256_sig).to.be.a('function'); + }); + + it('an unknown chain name withholds it', () => { + const other = VM.from(new Uint8Array(), new Blockchain({ chain: 'not-a-chain' })); + expect(other.imports.env.verify_rsa_sha256_sig).to.equal(undefined); + }); + + it('withholding a chain-specific function does not disturb the shared ones', () => { + // recover_key exists on every chain, so it must survive the gate on both. + expect(vm.imports.env.recover_key).to.be.a('function'); + expect(waxVm.imports.env.recover_key).to.be.a('function'); + }); + }); + describe('print', () => { it('prints', () => { const buffer = Buffer.from_(memory.buffer); diff --git a/src/antelope/vm.ts b/src/antelope/vm.ts index 3b94217..01f53dc 100644 --- a/src/antelope/vm.ts +++ b/src/antelope/vm.ts @@ -7,7 +7,7 @@ import { Action, Name, NameType, PermissionLevel, PublicKey, Serializer, Signatu import { sha256, sha512, sha1, ripemd160 } from "hash.js"; import { sha3_256, keccak256 } from "js-sha3" import { bigIntToName, nameToBigInt, nameTypeToBigInt } from "./bn"; -import { Blockchain } from "./blockchain"; +import { Blockchain, ALL_CHAIN_SPECIFIC_HOST_FUNCTIONS } from "./blockchain"; import { Account } from "./account"; import { antelopeAssert, antelopeAssertMessage, antelopeAssertCode } from "./errors"; import { isAuthoritySatisfied } from "./utils"; @@ -1474,6 +1474,16 @@ class VM extends Vert { }, }; + // Withhold chain-specific host functions the target chain does not provide, + // so a contract importing one fails to instantiate here exactly as setcode + // would reject it on that chain. Generic Antelope withholds all of them. + const enabledChainHostFunctions = bc.enabledChainHostFunctions(); + for (const fn of ALL_CHAIN_SPECIFIC_HOST_FUNCTIONS) { + if (!enabledChainHostFunctions.has(fn)) { + delete (imports.env as any)[fn]; + } + } + super(imports, wasm); this.imports = imports; this.bc = bc;