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;