Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

contracts: add support for core.CallDataPublicKey and core.CurrentEpoch #370

Merged
merged 1 commit into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions contracts/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ The format is inspired by [Keep a Changelog].

[Keep a Changelog]: https://keepachangelog.com/en/1.0.0/

## 0.2.11 (2024-09)

### Added

* `Subcall.sol` support for `core.CallDataPublicKey` and `core.CurrentEpoch`

## 0.2.10 (2024-08-20)

### Added
Expand Down
143 changes: 143 additions & 0 deletions contracts/contracts/Subcall.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ library Subcall {
string private constant CONSENSUS_TAKE_RECEIPT = "consensus.TakeReceipt";
// Accounts
string private constant ACCOUNTS_TRANSFER = "accounts.Transfer";
// Core
string private constant CORE_CALLDATAPUBLICKEY = "core.CallDataPublicKey";
string private constant CORE_CURRENT_EPOCH = "core.CurrentEpoch";
// ROFL
string private constant ROFL_IS_AUTHORIZED_ORIGIN =
"rofl.IsAuthorizedOrigin";
Expand Down Expand Up @@ -77,6 +80,12 @@ library Subcall {
/// Unsigned integer of unknown size
error InvalidUintSize(uint8);

/// Error while trying to retrieve current epoch
error CoreCurrentEpochError(uint64);

/// Error while trying to retrieve the calldata public key
error CoreCallDataPublicKeyError(uint64);

/**
* @notice Submit a native message to the Oasis runtime layer. Messages
* which re-enter the EVM module are forbidden: `evm.*`.
Expand Down Expand Up @@ -604,4 +613,138 @@ library Subcall {
revert RoflOriginNotAuthorizedForApp();
}
}

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;
bytes32[2] signature;
uint256 expiration;
}

function _parseCBORPublicKeyInner(bytes memory in_data, uint256 in_offset)
internal
pure
returns (uint256 offset, CallDataPublicKey memory public_key)
{
uint256 mapLen;

(mapLen, offset) = _parseCBORMapStart(in_data, in_offset);

while (mapLen > 0) {
mapLen -= 1;

bytes32 keyDigest;

(offset, keyDigest) = _parseCBORKey(in_data, offset);

if (keyDigest == keccak256("key")) {
uint256 tmp;
(offset, tmp) = _parseCBORUint(in_data, offset);
public_key.key = bytes32(tmp);
} else if (keyDigest == keccak256("checksum")) {
uint256 tmp;
(offset, tmp) = _parseCBORUint(in_data, offset);
public_key.checksum = bytes32(tmp);
} else if (keyDigest == keccak256("expiration")) {
(offset, public_key.expiration) = _parseCBORUint(
in_data,
offset
);
} else if (keyDigest == keccak256("signature")) {
if (in_data[offset++] != 0x58) {
revert InvalidUintPrefix(uint8(in_data[offset - 1]));
}
if (in_data[offset++] != 0x40) {
revert InvalidUintSize(uint8(in_data[offset - 1]));
}
uint256 tmp;
assembly {
tmp := mload(add(in_data, add(offset, 0x20)))
}
public_key.signature[0] = bytes32(tmp);
assembly {
tmp := mload(add(in_data, add(offset, 0x40)))
}
public_key.signature[1] = bytes32(tmp);

offset += 0x40;
} else {
revert InvalidKey();
}
}
}

function _parseCBORCallDataPublicKey(bytes memory in_data)
internal
pure
returns (uint256 epoch, CallDataPublicKey memory public_key)
{
(uint256 outerMapLen, uint256 offset) = _parseCBORMapStart(in_data, 0);

while (outerMapLen > 0) {
bytes32 keyDigest;

outerMapLen -= 1;

(offset, keyDigest) = _parseCBORKey(in_data, offset);

if (keyDigest == keccak256("epoch")) {
(offset, epoch) = _parseCBORUint(in_data, offset);
} else if (keyDigest == keccak256("public_key")) {
(offset, public_key) = _parseCBORPublicKeyInner(
in_data,
offset
);
} else {
revert InvalidKey();
}
}
}

// core.CallDataPublicKey
function coreCallDataPublicKey()
internal
returns (uint256 epoch, CallDataPublicKey memory public_key)
{
(uint64 status, bytes memory data) = subcall(
CORE_CALLDATAPUBLICKEY,
hex"f6" // null
);

if (status != 0) {
revert CoreCallDataPublicKeyError(status);
}

return _parseCBORCallDataPublicKey(data);
}

// core.CurrentEpoch
function coreCurrentEpoch() internal returns (uint256) {
(uint64 status, bytes memory data) = subcall(
CORE_CURRENT_EPOCH,
hex"f6" // null
);

if (status != 0) {
revert CoreCurrentEpochError(status);
}

(, uint256 result) = _parseCBORUint(data, 0);

return result;
}
}
21 changes: 21 additions & 0 deletions contracts/contracts/tests/SubcallTests.sol
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,25 @@ contract SubcallTests {
{
return Subcall._parseCBORUint(result, offset);
}

event RawResult(uint64, bytes);

function testParseCallDataPublicKey(bytes memory data)
external
pure
returns (uint256 epoch, Subcall.CallDataPublicKey memory public_key)
{
(epoch, public_key) = Subcall._parseCBORCallDataPublicKey(data);
}

function testCoreCallDataPublicKey()
external
returns (uint256 epoch, Subcall.CallDataPublicKey memory public_key)
{
(epoch, public_key) = Subcall.coreCallDataPublicKey();
}

function testCoreCurrentEpoch() external returns (uint256 epoch) {
return Subcall.coreCurrentEpoch();
}
}
2 changes: 1 addition & 1 deletion contracts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@oasisprotocol/sapphire-contracts",
"version": "0.2.10",
"version": "0.2.11",
"license": "Apache-2.0",
"description": "Solidity smart contract library for confidential contract development",
"homepage": "https://github.com/oasisprotocol/sapphire-paratime/tree/main/contracts",
Expand Down
83 changes: 78 additions & 5 deletions contracts/test/subcall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as cborg from 'cborg';
import { SubcallTests } from '../typechain-types/contracts/tests/SubcallTests';
import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers';
import {
AbiCoder,
ContractTransactionReceipt,
EventLog,
Provider,
Expand Down Expand Up @@ -126,11 +127,14 @@ describe('Subcall', () => {
ownerNativeAddr = getBytes(zeroPadValue(ownerAddr, 21));
expect(ownerNativeAddr.length).eq(21);

const rawKp = await contract.generateRandomAddress();
kp = {
publicKey: getBytes(rawKp.publicKey),
secretKey: getBytes(rawKp.secretKey),
};
// Skip random address generation when running in hardhat
if ((await provider.getNetwork()).chainId != 31337n) {
const rawKp = await contract.generateRandomAddress();
kp = {
publicKey: getBytes(rawKp.publicKey),
secretKey: getBytes(rawKp.secretKey),
};
}
});

it('Derive Staking Addresses', async () => {
Expand Down Expand Up @@ -491,4 +495,73 @@ describe('Subcall', () => {
expect(newOffset).eq(33 + 1);
});
});

it('CallDataPublicKey CBOR parsing works', async () => {
const example =
'0xa26565706f636818336a7075626c69635f6b6579a4636b65795820969010b54ebcda50415eedf2554109edac4735a58ddf1e4b43b9a765fa734f0a68636865636b73756d5820dfe9285ada1376ac95a411ee68d3991a8c72b68cea1fcc79d084f8df2d93f646697369676e617475726558405ed83560ea48a003993cb0b1c5610272a4077bc02242215996029c14476fd33e1c04a84dc99a8c76f4111a758dd185cd0b588469cfde1214898c8571ac170e066a65787069726174696f6e183d';
const data = cborg.decode(getBytes(example));
const result = await contract.testParseCallDataPublicKey(example);
expect(result.epoch).eq(data.epoch);
expect(result.public_key.key).eq(hexlify(data.public_key.key));
expect(result.public_key.checksum).eq(hexlify(data.public_key.checksum));
expect(result.public_key.expiration).eq(data.public_key.expiration);
expect(result.public_key.signature[0]).eq(
hexlify(data.public_key.signature.slice(0, 32)),
);
expect(result.public_key.signature[1]).eq(
hexlify(data.public_key.signature.slice(32)),
);
});

it('core.CallDataPublicKey works', async () => {
// Perform call directly using eth_call
const coder = AbiCoder.defaultAbiCoder();
const doop = await provider.call({
to: '0x0100000000000000000000000000000000000103',
data: coder.encode(
['string', 'bytes'],
['core.CallDataPublicKey', cborg.encode(null)],
),
});
const [subcall_status, subcall_raw_response] = coder.decode(
['uint', 'bytes'],
doop,
);
expect(subcall_status).eq(0n);

// Verify the form of the raw response
const subcall_data = cborg.decode(getBytes(subcall_raw_response));
expect(subcall_data).haveOwnProperty('epoch');
expect(subcall_data).haveOwnProperty('public_key');

const subcall_publickey = subcall_data.public_key;
expect(subcall_publickey).has.haveOwnProperty('key');
expect(subcall_publickey.key).lengthOf(32);
expect(subcall_publickey).has.haveOwnProperty('checksum');
expect(subcall_publickey.checksum).lengthOf(32);
expect(subcall_publickey).has.haveOwnProperty('signature');
expect(subcall_publickey.signature).lengthOf(64);
expect(subcall_publickey).has.haveOwnProperty('expiration');

// Verify CBOR parsing via contract returns the same result
const result = await contract.testCoreCallDataPublicKey.staticCall();
expect(result.epoch).eq(subcall_data.epoch);
CedarMist marked this conversation as resolved.
Show resolved Hide resolved
expect(result.public_key.key).eq(hexlify(subcall_data.public_key.key));
expect(result.public_key.checksum).eq(
hexlify(subcall_data.public_key.checksum),
);
expect(result.public_key.expiration).eq(subcall_data.public_key.expiration);

// Signature is sliced in half, to fit into two bytes32 elements
const sig0 = subcall_publickey.signature.slice(0, 32);
const sig1 = subcall_publickey.signature.slice(32);
expect(result.public_key.signature[0]).eq(hexlify(sig0), 'Sig0 mismatch');
expect(result.public_key.signature[1]).eq(hexlify(sig1), 'Sig1 mismatch');
});

it('core.CurrentEpoch works', async () => {
const actualEpoch = await contract.testCoreCurrentEpoch.staticCall();
const expectedEpoch = await getDockerEpoch(await getDockerName());
expect(Number(actualEpoch)).eq(expectedEpoch);
});
});
Loading