diff --git a/README.md b/README.md index 4abc7434..f03bc941 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Massa-as-sdk -![check-code-coverage](https://img.shields.io/badge/coverage-82%25-green) +![check-code-coverage](https://img.shields.io/badge/coverage-83%%25-red) Massa-as-sdk is a collection of tools, objects, and functions specifically designed for Massa smart contracts in AssemblyScript. This SDK enables you to import object classes, such as address and storage objects, and use them without having to write them from scratch every time. Additionally, it allows you to use Massa's ABI functions. diff --git a/assembly/__tests__/utils.ts b/assembly/__tests__/utils.ts index 93b01f4c..dcdba7d7 100644 --- a/assembly/__tests__/utils.ts +++ b/assembly/__tests__/utils.ts @@ -6,3 +6,14 @@ export function staticArrayToHexString(arr: StaticArray): string { } return result; } + +export function hexStringToStaticArray(hexString: string): StaticArray { + let result = new StaticArray(hexString.length / 2); + + for (let i = 0; i < hexString.length; i += 2) { + let byte = parseInt(hexString.substr(i, 2), 16); + result[i / 2] = byte; + } + + return result; +} diff --git a/assembly/__tests__/vm-mock.spec.ts b/assembly/__tests__/vm-mock.spec.ts index 8d083e7a..e9e4cd18 100644 --- a/assembly/__tests__/vm-mock.spec.ts +++ b/assembly/__tests__/vm-mock.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-len */ import { Address, Storage, @@ -7,6 +8,8 @@ import { getKeysOf, validateAddress, keccak256, + isEvmSignatureValid, + evmGetPubkeyFromSignature, } from '../std'; import { changeCallStack, resetStorage } from '../vm-mock/storage'; import { @@ -17,7 +20,7 @@ import { import { Args, bytesToString, stringToBytes } from '@massalabs/as-types'; import { env } from '../env/index'; import { callee, caller, isDeployingContract } from '../std/context'; -import { staticArrayToHexString } from './utils'; +import { hexStringToStaticArray, staticArrayToHexString } from './utils'; const testAddress = new Address( 'AU12E6N5BFAdC2wyiBV6VJjqkWhpz1kLVp2XpbRdSnL1mKjCWT6oR', @@ -256,4 +259,34 @@ describe('Testing mocked Context', () => { expect(caller().toString()).toStrictEqual(callee().toString()); }); + + it('should verify evm signature', () => { + const data = stringToBytes('Hello World'); + const signature = hexStringToStaticArray( + '1617966ef37eaff4312132243df65cfb66ccda057e996b586f910d6eb422787453b0a8465be9716493650d6706bc84efe85a5d76a0111c89d0cdf8ba57e510571c', + ); + // secp256k1 raw public key (compression header byte has been removed) + const publicKey = hexStringToStaticArray( + 'ae04a0fb4545138d22ed46eee76e683c50412ffb8cb02ee8fa5a5c8eec35f72dc076cb1b7468f8cacf136a7e0609b31ed580746d8efc659a993ccd695d6387ff', + ); + + expect(isEvmSignatureValid(data, signature, publicKey)).toStrictEqual(true); + }); + + it('should get evm public key from signature', () => { + const digest = hexStringToStaticArray( + 'a1de988600a42c4b4ab089b619297c17d53cffae5d5120d82d8a92d0bb3b78f2', + ); + const signature = hexStringToStaticArray( + '1617966ef37eaff4312132243df65cfb66ccda057e996b586f910d6eb422787453b0a8465be9716493650d6706bc84efe85a5d76a0111c89d0cdf8ba57e510571c', + ); + // secp256k1 public key (with compression header byte) + const publicKey = hexStringToStaticArray( + '04ae04a0fb4545138d22ed46eee76e683c50412ffb8cb02ee8fa5a5c8eec35f72dc076cb1b7468f8cacf136a7e0609b31ed580746d8efc659a993ccd695d6387ff', + ); + + expect(evmGetPubkeyFromSignature(digest, signature)).toStrictEqual( + publicKey, + ); + }); }); diff --git a/package-lock.json b/package-lock.json index bd573430..a1ac1291 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@massalabs/eslint-config": "^0.0.8", "@massalabs/prettier-config-as": "^0.0.2", "as-bignum": "^0.2.40", + "ethers": "^6.8.1", "husky": "^8.0.2", "js-sha3": "^0.9.2", "lint-staged": "^13.0.3", @@ -23,6 +24,12 @@ "typescript": "^4.8.3" } }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz", + "integrity": "sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==", + "dev": true + }, "node_modules/@as-covers/assembly": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@as-covers/assembly/-/assembly-0.4.1.tgz", @@ -350,6 +357,30 @@ "resolve": "~1.19.0" } }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dev": true, + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "dev": true, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -450,9 +481,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.14.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.0.tgz", - "integrity": "sha512-5EWrvLmglK+imbCJY0+INViFWUHg1AHel1sq4ZVSfdcNqGy9Edv3UB9IIzzg+xPaUcAgZYcfVs2fBcwDeZzU0A==", + "version": "18.15.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", + "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==", "dev": true }, "node_modules/@types/semver": { @@ -670,6 +701,12 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "dev": true + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -1528,6 +1565,40 @@ "node": ">=0.10.0" } }, + "node_modules/ethers": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.8.1.tgz", + "integrity": "sha512-iEKm6zox5h1lDn6scuRWdIdFJUCGg3+/aQWu0F4K0GVyEZiktFkqrJbRjTn1FlYEPz7RKA707D6g5Kdk6j7Ljg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@adraffy/ens-normalize": "1.10.0", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "18.15.13", + "aes-js": "4.0.0-beta.5", + "tslib": "2.4.0", + "ws": "8.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "dev": true + }, "node_modules/execa": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", @@ -8431,6 +8502,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -8460,6 +8552,12 @@ } }, "dependencies": { + "@adraffy/ens-normalize": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz", + "integrity": "sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==", + "dev": true + }, "@as-covers/assembly": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@as-covers/assembly/-/assembly-0.4.1.tgz", @@ -8749,6 +8847,21 @@ "resolve": "~1.19.0" } }, + "@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dev": true, + "requires": { + "@noble/hashes": "1.3.2" + } + }, + "@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "dev": true + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -8836,9 +8949,9 @@ "dev": true }, "@types/node": { - "version": "18.14.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.0.tgz", - "integrity": "sha512-5EWrvLmglK+imbCJY0+INViFWUHg1AHel1sq4ZVSfdcNqGy9Edv3UB9IIzzg+xPaUcAgZYcfVs2fBcwDeZzU0A==", + "version": "18.15.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", + "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==", "dev": true }, "@types/semver": { @@ -8959,6 +9072,12 @@ "dev": true, "requires": {} }, + "aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "dev": true + }, "aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -9585,6 +9704,29 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "ethers": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.8.1.tgz", + "integrity": "sha512-iEKm6zox5h1lDn6scuRWdIdFJUCGg3+/aQWu0F4K0GVyEZiktFkqrJbRjTn1FlYEPz7RKA707D6g5Kdk6j7Ljg==", + "dev": true, + "requires": { + "@adraffy/ens-normalize": "1.10.0", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "18.15.13", + "aes-js": "4.0.0-beta.5", + "tslib": "2.4.0", + "ws": "8.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "dev": true + } + } + }, "execa": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", @@ -14811,6 +14953,13 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "dev": true, + "requires": {} + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index 1134a524..aa9fb684 100644 --- a/package.json +++ b/package.json @@ -37,9 +37,10 @@ "@massalabs/as-types": "^1.0.0", "@massalabs/eslint-config": "^0.0.8", "@massalabs/prettier-config-as": "^0.0.2", - "js-sha3": "^0.9.2", "as-bignum": "^0.2.40", + "ethers": "^6.8.1", "husky": "^8.0.2", + "js-sha3": "^0.9.2", "lint-staged": "^13.0.3", "prettier": "^2.7.1", "typedoc": "^0.23.23", diff --git a/vm-mock/vm.js b/vm-mock/vm.js index f7755298..97b6aef5 100644 --- a/vm-mock/vm.js +++ b/vm-mock/vm.js @@ -1,5 +1,6 @@ const { createHash } = await import('node:crypto'); -import sha3 from 'js-sha3'; +import { SigningKey, hashMessage } from 'ethers'; +import sha3 from 'js-sha3'; /** * Addresses and callstack @@ -453,11 +454,11 @@ export default function createMockedABI( // Ensure the caller address is different from the contract address if (!addrPtr || ptrToString(addrPtr) === contractAddress) { // generate a new address if it is the same as the contract address - callerAddress = generateDumbAddress(); + callerAddress = generateDumbAddress(); } else { callerAddress = ptrToString(addrPtr); } - + if (!ledger.has(callerAddress)) { // add the new address to the ledger ledger.set(callerAddress, { @@ -515,7 +516,7 @@ export default function createMockedABI( const addressStorage = ledger.get(a).storage; let keysArr = Array.from(addressStorage.keys()); - if(prefix) { + if (prefix) { keysArr = keysArr.filter((key) => { const prefixStr = ptrToUint8ArrayString(prefix); return key.startsWith(prefixStr); @@ -530,7 +531,7 @@ export default function createMockedABI( const addressStorage = ledger.get(contractAddress).storage; let keysArr = Array.from(addressStorage.keys()); - if(prefix) { + if (prefix) { keysArr = keysArr.filter((key) => { const prefixStr = ptrToUint8ArrayString(prefix); return key.startsWith(prefixStr); @@ -719,6 +720,44 @@ export default function createMockedABI( return true; }, + assembly_script_evm_signature_verify(dataPtr, signaturePtr, publicKeyPtr) { + + const signatureBuf = getArrayBuffer(signaturePtr); + if (signatureBuf.byteLength !== 65) { + console.log('Invalid signature length. Expected 65 bytes'); + throw new Error(); + } + + const pubKeyBuf = getArrayBuffer(publicKeyPtr); + if (pubKeyBuf.byteLength !== 64) { + console.log('Invalid publickey length. Expected 64 bytes uncompressed secp256k1 public key'); + throw new Error(); + } + + const digest = hashMessage(new Uint8Array(getArrayBuffer(dataPtr))); + const signature = "0x" + Buffer.from(signatureBuf).toString('hex'); + const recovered = SigningKey.recoverPublicKey(digest, signature); + + const publicKey = "0x" + "04"/* compression header*/ + Buffer.from(pubKeyBuf).toString('hex'); + return recovered === publicKey; + }, + + assembly_script_evm_get_pubkey_from_signature(dataPtr, signaturePtr) { + + const signatureBuf = getArrayBuffer(signaturePtr); + if (signatureBuf.byteLength !== 65) { + console.log('Invalid signature length. Expected 65 bytes'); + throw new Error(); + } + const digest = "0x" + Buffer.from(getArrayBuffer(dataPtr)).toString('hex'); + + const signature = "0x" + Buffer.from(signatureBuf).toString('hex'); + + const recovered = SigningKey.recoverPublicKey(digest, signature); + + return newArrayBuffer(Buffer.from(recovered.substring(2), "hex")); + }, + assembly_script_address_from_public_key(publicKeyPtr) { return newString( 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq', @@ -732,7 +771,7 @@ export default function createMockedABI( assembly_script_keccak256_hash(dataPtr) { const data = getArrayBuffer(dataPtr); - const hash = sha3.keccak256.arrayBuffer(data); + const hash = sha3.keccak256.arrayBuffer(data); return newArrayBuffer(hash); }, },