From 8d3c3ffb8321348c35917a1d926f936b14421100 Mon Sep 17 00:00:00 2001 From: CedarMist <134699267+CedarMist@users.noreply.github.com> Date: Wed, 16 Oct 2024 13:09:18 +0300 Subject: [PATCH 1/5] contracts: move CBOR related encoding & decoding into separate module --- contracts/contracts/CBOR.sol | 172 +++++++++++++++++ contracts/contracts/Subcall.sol | 211 ++++++--------------- contracts/contracts/tests/SubcallTests.sol | 17 +- contracts/contracts/tests/TestCBOR.sol | 40 ++++ contracts/test/cbor.spec.ts | 63 ++++++ contracts/test/subcall.ts | 115 +---------- contracts/tsconfig.json | 2 +- 7 files changed, 340 insertions(+), 280 deletions(-) create mode 100644 contracts/contracts/CBOR.sol create mode 100644 contracts/contracts/tests/TestCBOR.sol create mode 100644 contracts/test/cbor.spec.ts diff --git a/contracts/contracts/CBOR.sol b/contracts/contracts/CBOR.sol new file mode 100644 index 000000000..51bab5641 --- /dev/null +++ b/contracts/contracts/CBOR.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +/// While parsing CBOR map, unexpected key +error CBOR_Error_InvalidKey(); + +/// While parsing CBOR map, length is invalid, or other parse error +error CBOR_Error_InvalidMap(); + +/// While parsing CBOR structure, data length was unexpected +error CBOR_Error_InvalidLength(uint256); + +/// Value cannot be parsed as a uint +error CBOR_Error_InvalidUintPrefix(uint8); + +/// Unsigned integer of unknown size +error CBOR_Error_InvalidUintSize(uint8); + +/// CBOR parsed value is out of expected range +error CBOR_Error_ValueOutOfRange(); + +error CBOR_Error_BytesTooLong(uint256 byteLength); + +function encodeUint(uint256 value) pure returns (bytes memory) { + // NOTE: we don't follow bignum tagged encoding + // See: https://www.rfc-editor.org/rfc/rfc8949.html#section-3.4.3 + if (value < 24) { + return abi.encodePacked(uint8(value)); + } else if (value <= type(uint8).max) { + return abi.encodePacked(uint8(24), uint8(value)); + } else if (value <= type(uint16).max) { + return abi.encodePacked(uint8(25), uint16(value)); + } else if (value <= type(uint32).max) { + return abi.encodePacked(uint8(26), uint32(value)); + } else if (value <= type(uint64).max) { + return abi.encodePacked(uint8(27), uint64(value)); + } else if (value <= type(uint128).max) { + return abi.encodePacked(uint8(0x50), uint128(value)); + } + return abi.encodePacked(uint8(0x58), uint256(32), value); +} + +function encodeBytes(bytes memory in_bytes) + pure + returns (bytes memory out_cbor) +{ + /* + 0x40..0x57 byte string (0x00..0x17 bytes follow) + 0x58 byte string (one-byte uint8_t for n, and then n bytes follow) + 0x59 byte string (two-byte uint16_t for n, and then n bytes follow) + 0x5a byte string (four-byte uint32_t for n, and then n bytes follow) + 0x5b byte string (eight-byte uint64_t for n, and then n bytes follow) + */ + if (in_bytes.length <= 0x17) { + return abi.encodePacked(uint8(0x40 + in_bytes.length), in_bytes); + } + if (in_bytes.length <= 0xFF) { + return abi.encodePacked(uint8(0x58), uint8(in_bytes.length), in_bytes); + } + if (in_bytes.length <= 0xFFFF) { + return abi.encodePacked(uint8(0x59), uint16(in_bytes.length), in_bytes); + } + // We assume Solidity won't be encoding anything larger than 64kb + revert CBOR_Error_BytesTooLong(in_bytes.length); +} + +function parseMapStart(bytes memory in_data, uint256 in_offset) + pure + returns (uint256 n_entries, uint256 out_offset) +{ + uint256 b = uint256(uint8(in_data[in_offset])); + if (b < 0xa0 || b > 0xb7) { + revert CBOR_Error_InvalidMap(); + } + + n_entries = b - 0xa0; + out_offset = in_offset + 1; +} + +function parseUint(bytes memory result, uint256 offset) + pure + returns (uint256 newOffset, uint256 value) +{ + uint8 prefix = uint8(result[offset]); + uint256 len; + + if (prefix <= 0x17) { + return (offset + 1, prefix); + } + // Byte array(uint256), parsed as a big-endian integer. + else if (prefix == 0x58) { + len = uint8(result[++offset]); + offset++; + } + // Byte array, parsed as a big-endian integer. + else if (prefix & 0x40 == 0x40) { + len = uint8(result[offset++]) ^ 0x40; + } + // Unsigned integer, CBOR encoded. + else if (prefix & 0x10 == 0x10) { + if (prefix == 0x18) { + len = 1; + } else if (prefix == 0x19) { + len = 2; + } else if (prefix == 0x1a) { + len = 4; + } else if (prefix == 0x1b) { + len = 8; + } else { + revert CBOR_Error_InvalidUintSize(prefix); + } + offset += 1; + } + // Unknown... + else { + revert CBOR_Error_InvalidUintPrefix(prefix); + } + + if (len > 0x20) revert CBOR_Error_InvalidLength(len); + + // Load 32 bytes from the buffer at the given offset + assembly { + value := mload(add(add(0x20, result), offset)) + } + + // Then shift the value right, until it matches the required bit-width + value = value >> (256 - (len * 8)); + + newOffset = offset + len; +} + +function parseUint64(bytes memory result, uint256 offset) + pure + returns (uint256 newOffset, uint64 value) +{ + uint256 tmp; + + (newOffset, tmp) = parseUint(result, offset); + + if (tmp > type(uint64).max) revert CBOR_Error_ValueOutOfRange(); + + value = uint64(tmp); +} + +function parseUint128(bytes memory result, uint256 offset) + pure + returns (uint256 newOffset, uint128 value) +{ + uint256 tmp; + + (newOffset, tmp) = parseUint(result, offset); + + if (tmp > type(uint128).max) revert CBOR_Error_ValueOutOfRange(); + + value = uint128(tmp); +} + +function parseKey(bytes memory result, uint256 offset) + pure + returns (uint256 newOffset, bytes32 keyDigest) +{ + if (result[offset] & 0x60 != 0x60) revert CBOR_Error_InvalidKey(); + + uint8 len = uint8(result[offset++]) ^ 0x60; + + assembly { + keyDigest := keccak256(add(add(0x20, result), offset), len) + } + + newOffset = offset + len; +} diff --git a/contracts/contracts/Subcall.sol b/contracts/contracts/Subcall.sol index 03232d881..d330776a7 100644 --- a/contracts/contracts/Subcall.sol +++ b/contracts/contracts/Subcall.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.0; import {StakingAddress, StakingSecretKey} from "./ConsensusUtils.sol"; +import "./CBOR.sol" as CBOR; enum SubcallReceiptKind { Invalid, @@ -56,30 +57,12 @@ library Subcall { /// Name of token cannot be CBOR encoded with current functions error TokenNameTooLong(); - /// While parsing CBOR map, unexpected key - error InvalidKey(); - - /// While parsing CBOR map, length is invalid, or other parse error - error InvalidMap(); - - /// While parsing CBOR structure, data length was unexpected - error InvalidLength(uint256); - /// Invalid receipt ID error InvalidReceiptId(); - /// CBOR parsed valid is out of expected range - error ValueOutOfRange(); - /// CBOR parser expected a key, but it was not found in the map! error MissingKey(); - /// Value cannot be parsed as a uint - error InvalidUintPrefix(uint8); - - /// Unsigned integer of unknown size - error InvalidUintSize(uint8); - /// Error while trying to retrieve current epoch error CoreCurrentEpochError(uint64); @@ -98,7 +81,23 @@ library Subcall { internal returns (uint64 status, bytes memory data) { - (bool success, bytes memory tmp) = SUBCALL.call( + (bool success, bytes memory tmp) = SUBCALL.call( // solhint-disable-line + abi.encode(method, body) + ); + + if (!success) { + revert SubcallError(); + } + + (status, data) = abi.decode(tmp, (uint64, bytes)); + } + + function subcall_static(string memory method, bytes memory body) + internal + view + returns (uint64 status, bytes memory data) + { + (bool success, bytes memory tmp) = SUBCALL.staticcall( abi.encode(method, body) ); @@ -168,9 +167,10 @@ library Subcall { { if (receiptId == 0) revert InvalidReceiptId(); - if (uint256(kind) == 0 || uint256(kind) > 23) revert ValueOutOfRange(); + if (uint256(kind) == 0 || uint256(kind) > 23) + revert CBOR.CBOR_Error_ValueOutOfRange(); - (bool success, bytes memory data) = SUBCALL.call( + (bool success, bytes memory data) = SUBCALL.call( // solhint-disable-line abi.encode( CONSENSUS_TAKE_RECEIPT, abi.encodePacked( // CBOR encoded, {'id': x, 'kind': y} @@ -205,101 +205,6 @@ library Subcall { return result; } - function _parseCBORUint(bytes memory result, uint256 offset) - public - pure - returns (uint256 newOffset, uint256 value) - { - uint8 prefix = uint8(result[offset]); - uint256 len; - - if (prefix <= 0x17) { - return (offset + 1, prefix); - } - // Byte array(uint256), parsed as a big-endian integer. - else if (prefix == 0x58) { - len = uint8(result[++offset]); - offset++; - } - // Byte array, parsed as a big-endian integer. - else if (prefix & 0x40 == 0x40) { - len = uint8(result[offset++]) ^ 0x40; - } - // Unsigned integer, CBOR encoded. - else if (prefix & 0x10 == 0x10) { - if (prefix == 0x18) { - len = 1; - } else if (prefix == 0x19) { - len = 2; - } else if (prefix == 0x1a) { - len = 4; - } else if (prefix == 0x1b) { - len = 8; - } else { - revert InvalidUintSize(prefix); - } - offset += 1; - } - // Unknown... - else { - revert InvalidUintPrefix(prefix); - } - - if (len > 0x20) revert InvalidLength(len); - - assembly { - value := mload(add(add(0x20, result), offset)) - } - - value = value >> (256 - (len * 8)); - - newOffset = offset + len; - } - - function _parseCBORUint64(bytes memory result, uint256 offset) - public - pure - returns (uint256 newOffset, uint64 value) - { - uint256 tmp; - - (newOffset, tmp) = _parseCBORUint(result, offset); - - if (tmp > type(uint64).max) revert ValueOutOfRange(); - - value = uint64(tmp); - } - - function _parseCBORUint128(bytes memory result, uint256 offset) - public - pure - returns (uint256 newOffset, uint128 value) - { - uint256 tmp; - - (newOffset, tmp) = _parseCBORUint(result, offset); - - if (tmp > type(uint128).max) revert ValueOutOfRange(); - - value = uint128(tmp); - } - - function _parseCBORKey(bytes memory result, uint256 offset) - internal - pure - returns (uint256 newOffset, bytes32 keyDigest) - { - if (result[offset] & 0x60 != 0x60) revert InvalidKey(); - - uint8 len = uint8(result[offset++]) ^ 0x60; - - assembly { - keyDigest := keccak256(add(add(0x20, result), offset), len) - } - - newOffset = offset + len; - } - function _decodeReceiptUndelegateStart(bytes memory result) internal pure @@ -311,19 +216,19 @@ library Subcall { bool hasReceipt = false; - if (result[0] != 0xA2) revert InvalidMap(); + if (result[0] != 0xA2) revert CBOR.CBOR_Error_InvalidMap(); while (offset < result.length) { bytes32 keyDigest; - (offset, keyDigest) = _parseCBORKey(result, offset); + (offset, keyDigest) = CBOR.parseKey(result, offset); if (keyDigest == keccak256("epoch")) { - (offset, epoch) = _parseCBORUint64(result, offset); + (offset, epoch) = CBOR.parseUint64(result, offset); hasEpoch = true; } else if (keyDigest == keccak256("receipt")) { - (offset, endReceipt) = _parseCBORUint64(result, offset); + (offset, endReceipt) = CBOR.parseUint64(result, offset); hasReceipt = true; } @@ -341,15 +246,15 @@ library Subcall { bool hasAmount = false; - if (result[0] != 0xA1) revert InvalidMap(); + if (result[0] != 0xA1) revert CBOR.CBOR_Error_InvalidMap(); while (offset < result.length) { bytes32 keyDigest; - (offset, keyDigest) = _parseCBORKey(result, offset); + (offset, keyDigest) = CBOR.parseKey(result, offset); if (keyDigest == keccak256("amount")) { - (offset, amount) = _parseCBORUint128(result, offset); + (offset, amount) = CBOR.parseUint128(result, offset); hasAmount = true; } @@ -368,13 +273,14 @@ library Subcall { pure returns (uint128 shares) { - if (result[0] != 0xA1) revert InvalidMap(); + if (result[0] != 0xA1) revert CBOR.CBOR_Error_InvalidMap(); if (result[0] == 0xA1 && result[1] == 0x66 && result[2] == "s") { // Delegation succeeded, decode number of shares. uint8 sharesLen = uint8(result[8]) & 0x1f; // Assume shares field is never greater than 16 bytes. - if (9 + sharesLen != result.length) revert InvalidLength(sharesLen); + if (9 + sharesLen != result.length) + revert CBOR.CBOR_Error_InvalidLength(sharesLen); for (uint256 offset = 0; offset < sharesLen; offset++) { uint8 v = uint8(result[9 + offset]); @@ -601,8 +507,8 @@ library Subcall { * ROFL instance for the given application. * @param appId ROFL app identifier */ - function roflEnsureAuthorizedOrigin(bytes21 appId) internal { - (uint64 status, bytes memory data) = subcall( + function roflEnsureAuthorizedOrigin(bytes21 appId) internal view { + (uint64 status, bytes memory data) = subcall_static( ROFL_IS_AUTHORIZED_ORIGIN, abi.encodePacked(hex"55", appId) // CBOR byte string, 21 bytes. ); @@ -614,20 +520,6 @@ library Subcall { } } - function _parseCBORMapStart(bytes memory in_data, uint256 in_offset) - internal - pure - returns (uint256 n_entries, uint256 out_offset) - { - uint256 b = uint256(uint8(in_data[in_offset])); - if (b < 0xa0 || b > 0xb7) { - revert InvalidMap(); - } - - n_entries = b - 0xa0; - out_offset = in_offset + 1; - } - struct CallDataPublicKey { bytes32 key; bytes32 checksum; @@ -642,34 +534,38 @@ library Subcall { { uint256 mapLen; - (mapLen, offset) = _parseCBORMapStart(in_data, in_offset); + (mapLen, offset) = CBOR.parseMapStart(in_data, in_offset); while (mapLen > 0) { mapLen -= 1; bytes32 keyDigest; - (offset, keyDigest) = _parseCBORKey(in_data, offset); + (offset, keyDigest) = CBOR.parseKey(in_data, offset); if (keyDigest == keccak256("key")) { uint256 tmp; - (offset, tmp) = _parseCBORUint(in_data, offset); + (offset, tmp) = CBOR.parseUint(in_data, offset); public_key.key = bytes32(tmp); } else if (keyDigest == keccak256("checksum")) { uint256 tmp; - (offset, tmp) = _parseCBORUint(in_data, offset); + (offset, tmp) = CBOR.parseUint(in_data, offset); public_key.checksum = bytes32(tmp); } else if (keyDigest == keccak256("expiration")) { - (offset, public_key.expiration) = _parseCBORUint( + (offset, public_key.expiration) = CBOR.parseUint( in_data, offset ); } else if (keyDigest == keccak256("signature")) { if (in_data[offset++] != 0x58) { - revert InvalidUintPrefix(uint8(in_data[offset - 1])); + revert CBOR.CBOR_Error_InvalidUintPrefix( + uint8(in_data[offset - 1]) + ); } if (in_data[offset++] != 0x40) { - revert InvalidUintSize(uint8(in_data[offset - 1])); + revert CBOR.CBOR_Error_InvalidUintSize( + uint8(in_data[offset - 1]) + ); } uint256 tmp; assembly { @@ -683,7 +579,7 @@ library Subcall { offset += 0x40; } else { - revert InvalidKey(); + revert CBOR.CBOR_Error_InvalidKey(); } } } @@ -693,24 +589,24 @@ library Subcall { pure returns (uint256 epoch, CallDataPublicKey memory public_key) { - (uint256 outerMapLen, uint256 offset) = _parseCBORMapStart(in_data, 0); + (uint256 outerMapLen, uint256 offset) = CBOR.parseMapStart(in_data, 0); while (outerMapLen > 0) { bytes32 keyDigest; outerMapLen -= 1; - (offset, keyDigest) = _parseCBORKey(in_data, offset); + (offset, keyDigest) = CBOR.parseKey(in_data, offset); if (keyDigest == keccak256("epoch")) { - (offset, epoch) = _parseCBORUint(in_data, offset); + (offset, epoch) = CBOR.parseUint(in_data, offset); } else if (keyDigest == keccak256("public_key")) { (offset, public_key) = _parseCBORPublicKeyInner( in_data, offset ); } else { - revert InvalidKey(); + revert CBOR.CBOR_Error_InvalidKey(); } } } @@ -718,9 +614,10 @@ library Subcall { // core.CallDataPublicKey function coreCallDataPublicKey() internal + view returns (uint256 epoch, CallDataPublicKey memory public_key) { - (uint64 status, bytes memory data) = subcall( + (uint64 status, bytes memory data) = subcall_static( CORE_CALLDATAPUBLICKEY, hex"f6" // null ); @@ -733,8 +630,8 @@ library Subcall { } // core.CurrentEpoch - function coreCurrentEpoch() internal returns (uint256) { - (uint64 status, bytes memory data) = subcall( + function coreCurrentEpoch() internal view returns (uint256) { + (uint64 status, bytes memory data) = subcall_static( CORE_CURRENT_EPOCH, hex"f6" // null ); @@ -743,7 +640,7 @@ library Subcall { revert CoreCurrentEpochError(status); } - (, uint256 result) = _parseCBORUint(data, 0); + (, uint256 result) = CBOR.parseUint(data, 0); return result; } diff --git a/contracts/contracts/tests/SubcallTests.sol b/contracts/contracts/tests/SubcallTests.sol index 474fc902d..09c2a13d8 100644 --- a/contracts/contracts/tests/SubcallTests.sol +++ b/contracts/contracts/tests/SubcallTests.sol @@ -8,11 +8,11 @@ import {Subcall, SubcallReceiptKind} from "../Subcall.sol"; contract SubcallTests { event SubcallResult(uint64 status, bytes data); - constructor() payable { + constructor() payable { // solhint-disable-line // Do nothing, but allow balances to be sent on construction. } - receive() external payable { + receive() external payable { // solhint-disable-line // Do nothing, but allow contract to receive native ROSE. } @@ -109,18 +109,10 @@ contract SubcallTests { Subcall.consensusWithdraw(to, value); } - function testRoflEnsureAuthorizedOrigin(bytes21 appId) external { + function testRoflEnsureAuthorizedOrigin(bytes21 appId) external view { Subcall.roflEnsureAuthorizedOrigin(appId); } - function testParseCBORUint(bytes memory result, uint256 offset) - external - pure - returns (uint256, uint256) - { - return Subcall._parseCBORUint(result, offset); - } - event RawResult(uint64, bytes); function testParseCallDataPublicKey(bytes memory data) @@ -133,12 +125,13 @@ contract SubcallTests { function testCoreCallDataPublicKey() external + view returns (uint256 epoch, Subcall.CallDataPublicKey memory public_key) { (epoch, public_key) = Subcall.coreCallDataPublicKey(); } - function testCoreCurrentEpoch() external returns (uint256 epoch) { + function testCoreCurrentEpoch() external view returns (uint256 epoch) { return Subcall.coreCurrentEpoch(); } } diff --git a/contracts/contracts/tests/TestCBOR.sol b/contracts/contracts/tests/TestCBOR.sol new file mode 100644 index 000000000..6af96c208 --- /dev/null +++ b/contracts/contracts/tests/TestCBOR.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import "../CBOR.sol" as CBOR; + +contract TestCBOR { + function testBytesEncoding(bytes memory in_data) + external pure + returns (bytes memory) + { + return CBOR.encodeBytes(in_data); + } + + function testUintEncoding(uint value) + external pure + returns (bytes memory) + { + return CBOR.encodeUint(value); + } + + function testParseUint(bytes memory result, uint256 offset) + external + pure + returns (uint256, uint256) + { + return CBOR.parseUint(result, offset); + } + + function testUintRoundtrip(uint value) + external pure + returns (bool) + { + bytes memory encoded = CBOR.encodeUint(value); + (uint newOffset, uint result) = CBOR.parseUint(encoded, 0); + require( result == value, "value wrong!" ); + require( newOffset == encoded.length, "didn't parse everything!" ); + return true; + } +} diff --git a/contracts/test/cbor.spec.ts b/contracts/test/cbor.spec.ts new file mode 100644 index 000000000..c2b74741b --- /dev/null +++ b/contracts/test/cbor.spec.ts @@ -0,0 +1,63 @@ +import * as cborg from 'cborg'; +import { ethers } from 'hardhat'; +import { expect } from 'chai'; +import { TestCBOR } from '../typechain-types'; +import { getBytes, hexlify, randomBytes, toQuantity } from 'ethers'; +import { encode as cborEncode, decode as cborDecode } from 'cborg'; + +describe('CBOR', () => { + let contract: TestCBOR; + + before(async () => { + const factory = await ethers.getContractFactory('TestCBOR'); + + contract = await factory.deploy(); + }); + + it('Uint encoding', async () => { + // NOTE: native bigint encoding beyond 64bit isn't supported by cborg lib + for (let i = 0n; i < 1n << 64n; i = i + 1n + i / 2n) { + const result = getBytes(await contract.testUintEncoding(i)); + expect(BigInt(cborDecode(getBytes(result)))).equal(i); + expect(await contract.testUintRoundtrip(i)).eq(true); + expect(hexlify(cborEncode(i))).eq(hexlify(result)); + } + }); + + it('Bytes encoding', async () => { + for (let i = 0; i <= 16; i += 1) { + const nBytes = (1 << i) - 1; + const input = randomBytes(nBytes); + const result = await contract.testBytesEncoding(input); + const output = cborDecode(getBytes(result)); + expect(hexlify(output)).eq(hexlify(input)); + expect(hexlify(cborEncode(input))).eq(hexlify(result)); + } + }); + + it('Bytes encoding (errors)', async () => { + const length = 0xffff + 1; + const bytes = randomBytes(length); + await expect(contract.testBytesEncoding(bytes)).revertedWithCustomError( + contract, + 'CBOR_Error_BytesTooLong', + ); + }); + + /// Verifies that bigints (byte encoded) can be parsed from 1 to 255 bits + it('Parse uint (bytes)', async () => { + for (let i = 0n; i < 256n; i += 1n) { + const value = 1n << i; + const hex = value.toString(16); + const hexPadded = hex.padStart(hex.length + (hex.length % 2), '0'); + const encoded = cborEncode(getBytes(`0x${hexPadded}`)); + const [newOffset, parsedCborUint] = await contract.testParseUint( + encoded, + 0, + ); + expect(await contract.testUintRoundtrip(i)).eq(true); + expect(newOffset).eq(encoded.length); + expect(toQuantity(parsedCborUint)).eq(`0x${hex}`); + } + }); +}); diff --git a/contracts/test/subcall.ts b/contracts/test/subcall.ts index 253cf1537..db4a65605 100644 --- a/contracts/test/subcall.ts +++ b/contracts/test/subcall.ts @@ -111,16 +111,14 @@ describe('Subcall', () => { const subcallLib = await subcallFactory.deploy(); await subcallLib.waitForDeployment(); - const factory = await ethers.getContractFactory('SubcallTests', { - libraries: { Subcall: await subcallLib.getAddress() }, - }); - contract = (await factory.deploy({ + const factory = await ethers.getContractFactory('SubcallTests'); + contract = await factory.deploy({ value: parseEther('1.0'), - })) as unknown as SubcallTests; + }); provider = contract.runner!.provider!; const signers = await ethers.getSigners(); - owner = signers[0] as unknown as SignerWithAddress; + owner = signers[0]; ownerAddr = await owner.getAddress(); // Convert Ethereum address to native bytes with version prefix (V1=0x00) @@ -390,110 +388,7 @@ describe('Subcall', () => { expect(event.data).eq(false); // Boolean false to indicate failure. // Also test the Subcall.roflEnsureAuthorizedOrigin wrapper. - tx = await contract.testRoflEnsureAuthorizedOrigin(appId); - await expect(tx).to.be.reverted; - }); - - describe('Should successfully parse CBOR uint/s', () => { - it('Should successfully parse CBOR uint8', async () => { - const MAX_SAFE_UINT8 = 255n; - - // bytes = 0x18FF - const bytes = cborg.encode(MAX_SAFE_UINT8); - - const [newOffset, parsedCborUint] = await contract.testParseCBORUint( - bytes, - 0, - ); - - expect(parsedCborUint).eq(MAX_SAFE_UINT8); - expect(newOffset).eq(1 + 1); - }); - - it('Should successfully parse CBOR uint16', async () => { - const MAX_SAFE_UINT16 = 65535n; - - // bytes = 0x19FFFF - const bytes = cborg.encode(MAX_SAFE_UINT16); - - const [newOffset, parsedCborUint] = await contract.testParseCBORUint( - bytes, - 0, - ); - - expect(parsedCborUint).eq(MAX_SAFE_UINT16); - expect(newOffset).eq(2 + 1); - }); - - it('Should successfully parse CBOR uint32', async () => { - const MAX_SAFE_UINT32 = 4294967295n; - - // bytes = 0x1AFFFFFFFF - const bytes = cborg.encode(MAX_SAFE_UINT32); - - const [newOffset, parsedCborUint] = await contract.testParseCBORUint( - bytes, - 0, - ); - - expect(parsedCborUint).eq(MAX_SAFE_UINT32); - expect(newOffset).eq(4 + 1); - }); - - it('Should successfully parse CBOR uint64', async () => { - const MAX_SAFE_UINT64 = 18446744073709551615n; - - // bytes = 0x1BFFFFFFFFFFFFFFFF - const bytes = cborg.encode(MAX_SAFE_UINT64); - - const [newOffset, parsedCborUint] = await contract.testParseCBORUint( - bytes, - 0, - ); - - expect(parsedCborUint).eq(MAX_SAFE_UINT64); - expect(newOffset).eq(8 + 1); - }); - - it('Should successfully parse CBOR uint128', async () => { - const MAX_SAFE_UINT128 = 340282366920938463463374607431768211455n; - - const hex = '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'; - const uint128bytes = Uint8Array.from( - Buffer.from(hex.replace('0x', ''), 'hex'), - ); - - const bytes = cborg.encode(uint128bytes); - - const [newOffset, parsedCborUint] = await contract.testParseCBORUint( - bytes, - 0, - ); - - expect(parsedCborUint).eq(MAX_SAFE_UINT128); - expect(newOffset).eq(16 + 1); - }); - - it('Should successfully parse CBOR uint256', async () => { - const MAX_SAFE_UINT256 = - 115792089237316195423570985008687907853269984665640564039457584007913129639935n; - - const hex = - '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'; - const uint256bytes = Uint8Array.from( - Buffer.from(hex.replace('0x', ''), 'hex'), - ); - - const bytes = cborg.encode(uint256bytes); - - const [newOffset, parsedCborUint] = await contract.testParseCBORUint( - bytes, - 0, - ); - - expect(parsedCborUint).eq(MAX_SAFE_UINT256); - expect(newOffset).eq(33 + 1); - }); + await expect(contract.testRoflEnsureAuthorizedOrigin(appId)).to.be.reverted; }); it('CallDataPublicKey CBOR parsing works', async () => { diff --git a/contracts/tsconfig.json b/contracts/tsconfig.json index 2d43ccd08..642533d13 100644 --- a/contracts/tsconfig.json +++ b/contracts/tsconfig.json @@ -9,6 +9,6 @@ "strict": true, "target": "es2020" }, - "include": ["./src", "./scripts", "./test"], + "include": ["./src", "./scripts", "./test", "./typechain-types"], "files": ["./hardhat.config.ts"] } From 02c37975dce57ceea9056d40b506dd39732e641f Mon Sep 17 00:00:00 2001 From: CedarMist <134699267+CedarMist@users.noreply.github.com> Date: Wed, 16 Oct 2024 17:00:04 +0300 Subject: [PATCH 2/5] contracts: minor documentation fixes --- contracts/contracts/Subcall.sol | 8 ++++++++ contracts/test/cbor.spec.ts | 1 - 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/Subcall.sol b/contracts/contracts/Subcall.sol index d330776a7..924d62f89 100644 --- a/contracts/contracts/Subcall.sol +++ b/contracts/contracts/Subcall.sol @@ -92,6 +92,14 @@ library Subcall { (status, data) = abi.decode(tmp, (uint64, bytes)); } + /** + * @notice Submit a read-only native message to the Oasis runtime layer using STATICCALL. + * Messages which re-enter the EVM module are forbidden: `evm.*`. + * @param method Native message type. + * @param body CBOR encoded body. + * @return status Result of call. + * @return data CBOR encoded result. + */ function subcall_static(string memory method, bytes memory body) internal view diff --git a/contracts/test/cbor.spec.ts b/contracts/test/cbor.spec.ts index c2b74741b..dcfe812d1 100644 --- a/contracts/test/cbor.spec.ts +++ b/contracts/test/cbor.spec.ts @@ -1,4 +1,3 @@ -import * as cborg from 'cborg'; import { ethers } from 'hardhat'; import { expect } from 'chai'; import { TestCBOR } from '../typechain-types'; From d09a36d183ce3d9d23456fd94efe332ac7c989cf Mon Sep 17 00:00:00 2001 From: CedarMist <134699267+CedarMist@users.noreply.github.com> Date: Wed, 16 Oct 2024 17:00:19 +0300 Subject: [PATCH 3/5] contracts: don't run hardhat tests with --verbose This avoids much noise --- contracts/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/package.json b/contracts/package.json index 15239410a..abe630b39 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -19,7 +19,7 @@ "format::prettier": "prettier --write --plugin-search-dir=. '*.json' '**/*.ts' '**/*.sol'", "format": "npm-run-all format:**", "build": "hardhat compile", - "test": "hardhat test --network sapphire-localnet --verbose" + "test": "hardhat test --network sapphire-localnet" }, "files": [ "contracts" From 7498c4e29b8af527a1f42afd48125a7b6d97f182 Mon Sep 17 00:00:00 2001 From: CedarMist <134699267+CedarMist@users.noreply.github.com> Date: Mon, 28 Oct 2024 10:59:12 +0100 Subject: [PATCH 4/5] contracts: minor feedback on CBOR library docs --- contracts/contracts/CBOR.sol | 46 +++++++++++++++++++++------------ contracts/contracts/Subcall.sol | 41 +++++++++++++++++------------ 2 files changed, 54 insertions(+), 33 deletions(-) diff --git a/contracts/contracts/CBOR.sol b/contracts/contracts/CBOR.sol index 51bab5641..24b9812b6 100644 --- a/contracts/contracts/CBOR.sol +++ b/contracts/contracts/CBOR.sol @@ -3,28 +3,35 @@ pragma solidity ^0.8.0; /// While parsing CBOR map, unexpected key -error CBOR_Error_InvalidKey(); +error CBOR_InvalidKey(); /// While parsing CBOR map, length is invalid, or other parse error -error CBOR_Error_InvalidMap(); +error CBOR_InvalidMap(); /// While parsing CBOR structure, data length was unexpected -error CBOR_Error_InvalidLength(uint256); +error CBOR_InvalidLength(uint256); /// Value cannot be parsed as a uint -error CBOR_Error_InvalidUintPrefix(uint8); +error CBOR_InvalidUintPrefix(uint8); -/// Unsigned integer of unknown size -error CBOR_Error_InvalidUintSize(uint8); +/// CBOR spec supports, 1, 2, 4 & 8 byte uints. Caused by parse error. +error CBOR_InvalidUintSize(uint8); /// CBOR parsed value is out of expected range -error CBOR_Error_ValueOutOfRange(); +error CBOR_Error_ValueOutOfRange(uint256 value, uint256 maxValue); +/// Buffer too short to parse expected value +error CBOR_Error_BufferOverrun(uint256 len, uint256 offset, uint256 need); + +/// @notice Byte array is too long +/// @dev Solidity has 256bit length prefixes for byte arrays. We decided for +/// a reasonable cutoff point (64kb), so at most a 2-byte uint describing +/// the array length. error CBOR_Error_BytesTooLong(uint256 byteLength); +/// @dev we don't follow bignum tagged encoding +/// See: https://www.rfc-editor.org/rfc/rfc8949.html#section-3.4.3 function encodeUint(uint256 value) pure returns (bytes memory) { - // NOTE: we don't follow bignum tagged encoding - // See: https://www.rfc-editor.org/rfc/rfc8949.html#section-3.4.3 if (value < 24) { return abi.encodePacked(uint8(value)); } else if (value <= type(uint8).max) { @@ -71,7 +78,7 @@ function parseMapStart(bytes memory in_data, uint256 in_offset) { uint256 b = uint256(uint8(in_data[in_offset])); if (b < 0xa0 || b > 0xb7) { - revert CBOR_Error_InvalidMap(); + revert CBOR_InvalidMap(); } n_entries = b - 0xa0; @@ -108,16 +115,21 @@ function parseUint(bytes memory result, uint256 offset) } else if (prefix == 0x1b) { len = 8; } else { - revert CBOR_Error_InvalidUintSize(prefix); + // Falls outside of the CBOR spec, tagged as uint, but unsupported + revert CBOR_InvalidUintSize(prefix); } offset += 1; } // Unknown... else { - revert CBOR_Error_InvalidUintPrefix(prefix); + revert CBOR_InvalidUintPrefix(prefix); } - if (len > 0x20) revert CBOR_Error_InvalidLength(len); + // Exceeds what can be represented by a 256bit word + if (len > 0x20) revert CBOR_InvalidLength(len); + + if (offset + len > result.length) + revert CBOR_Error_BufferOverrun(result.length, offset, len); // Load 32 bytes from the buffer at the given offset assembly { @@ -138,7 +150,8 @@ function parseUint64(bytes memory result, uint256 offset) (newOffset, tmp) = parseUint(result, offset); - if (tmp > type(uint64).max) revert CBOR_Error_ValueOutOfRange(); + if (tmp > type(uint64).max) + revert CBOR_Error_ValueOutOfRange(tmp, type(uint64).max); value = uint64(tmp); } @@ -151,7 +164,8 @@ function parseUint128(bytes memory result, uint256 offset) (newOffset, tmp) = parseUint(result, offset); - if (tmp > type(uint128).max) revert CBOR_Error_ValueOutOfRange(); + if (tmp > type(uint128).max) + revert CBOR_Error_ValueOutOfRange(tmp, type(uint128).max); value = uint128(tmp); } @@ -160,7 +174,7 @@ function parseKey(bytes memory result, uint256 offset) pure returns (uint256 newOffset, bytes32 keyDigest) { - if (result[offset] & 0x60 != 0x60) revert CBOR_Error_InvalidKey(); + if (result[offset] & 0x60 != 0x60) revert CBOR_InvalidKey(); uint8 len = uint8(result[offset++]) ^ 0x60; diff --git a/contracts/contracts/Subcall.sol b/contracts/contracts/Subcall.sol index 924d62f89..d0917aa5d 100644 --- a/contracts/contracts/Subcall.sol +++ b/contracts/contracts/Subcall.sol @@ -51,6 +51,12 @@ library Subcall { error AccountsTransferError(uint64 status, string data); + /// Expected map of different size! + error WrongMapSizeError(); + + /// Unknown type of receipt! + error TakeReceiptKindOutOfRange(uint receiptKind); + /// The origin is not authorized for the given ROFL app error RoflOriginNotAuthorizedForApp(); @@ -63,6 +69,9 @@ library Subcall { /// CBOR parser expected a key, but it was not found in the map! error MissingKey(); + /// We expected to have parsed everything, but there are excess bytes! + error IncompleteParse(); + /// Error while trying to retrieve current epoch error CoreCurrentEpochError(uint64); @@ -176,7 +185,7 @@ library Subcall { if (receiptId == 0) revert InvalidReceiptId(); if (uint256(kind) == 0 || uint256(kind) > 23) - revert CBOR.CBOR_Error_ValueOutOfRange(); + revert TakeReceiptKindOutOfRange(uint256(kind)); (bool success, bytes memory data) = SUBCALL.call( // solhint-disable-line abi.encode( @@ -224,7 +233,8 @@ library Subcall { bool hasReceipt = false; - if (result[0] != 0xA2) revert CBOR.CBOR_Error_InvalidMap(); + // Expects map with 2 pairs + if (result[0] != 0xA2) revert WrongMapSizeError(); while (offset < result.length) { bytes32 keyDigest; @@ -254,7 +264,8 @@ library Subcall { bool hasAmount = false; - if (result[0] != 0xA1) revert CBOR.CBOR_Error_InvalidMap(); + // Expects map with 1 pair + if (result[0] != 0xA1) revert WrongMapSizeError(); while (offset < result.length) { bytes32 keyDigest; @@ -281,19 +292,15 @@ library Subcall { pure returns (uint128 shares) { - if (result[0] != 0xA1) revert CBOR.CBOR_Error_InvalidMap(); + // Expects map with 1 pair + if (result[0] != 0xA1) revert WrongMapSizeError(); if (result[0] == 0xA1 && result[1] == 0x66 && result[2] == "s") { // Delegation succeeded, decode number of shares. - uint8 sharesLen = uint8(result[8]) & 0x1f; // Assume shares field is never greater than 16 bytes. - - if (9 + sharesLen != result.length) - revert CBOR.CBOR_Error_InvalidLength(sharesLen); - - for (uint256 offset = 0; offset < sharesLen; offset++) { - uint8 v = uint8(result[9 + offset]); - - shares += uint128(v) << (8 * uint128(sharesLen - offset - 1)); + uint newOffset; + (newOffset,shares) = CBOR.parseUint128(result, 8); + if( newOffset != result.length ) { + revert IncompleteParse(); } } else { revert ParseReceiptError(receiptId); @@ -566,12 +573,12 @@ library Subcall { ); } else if (keyDigest == keccak256("signature")) { if (in_data[offset++] != 0x58) { - revert CBOR.CBOR_Error_InvalidUintPrefix( + revert CBOR.CBOR_InvalidUintPrefix( uint8(in_data[offset - 1]) ); } if (in_data[offset++] != 0x40) { - revert CBOR.CBOR_Error_InvalidUintSize( + revert CBOR.CBOR_InvalidUintSize( uint8(in_data[offset - 1]) ); } @@ -587,7 +594,7 @@ library Subcall { offset += 0x40; } else { - revert CBOR.CBOR_Error_InvalidKey(); + revert CBOR.CBOR_InvalidKey(); } } } @@ -614,7 +621,7 @@ library Subcall { offset ); } else { - revert CBOR.CBOR_Error_InvalidKey(); + revert CBOR.CBOR_InvalidKey(); } } } From 7914b1f5e5b5f8dc2f7ff8182be9d6ce5da77102 Mon Sep 17 00:00:00 2001 From: CedarMist <134699267+CedarMist@users.noreply.github.com> Date: Mon, 28 Oct 2024 11:05:40 +0100 Subject: [PATCH 5/5] contracts: minor feedback on CBOR library docs --- contracts/contracts/Subcall.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/contracts/Subcall.sol b/contracts/contracts/Subcall.sol index d0917aa5d..bb4208c42 100644 --- a/contracts/contracts/Subcall.sol +++ b/contracts/contracts/Subcall.sol @@ -55,7 +55,7 @@ library Subcall { error WrongMapSizeError(); /// Unknown type of receipt! - error TakeReceiptKindOutOfRange(uint receiptKind); + error TakeReceiptKindOutOfRange(uint256 receiptKind); /// The origin is not authorized for the given ROFL app error RoflOriginNotAuthorizedForApp(); @@ -297,9 +297,9 @@ library Subcall { if (result[0] == 0xA1 && result[1] == 0x66 && result[2] == "s") { // Delegation succeeded, decode number of shares. - uint newOffset; - (newOffset,shares) = CBOR.parseUint128(result, 8); - if( newOffset != result.length ) { + uint256 newOffset; + (newOffset, shares) = CBOR.parseUint128(result, 8); + if (newOffset != result.length) { revert IncompleteParse(); } } else {