Skip to content

Commit

Permalink
Merge pull request #370 from oasisprotocol/CedarMist/cdpk_subcall
Browse files Browse the repository at this point in the history
contracts: add support for core.CallDataPublicKey and core.CurrentEpoch
  • Loading branch information
CedarMist authored Aug 30, 2024
2 parents d1374e2 + cc01fde commit 1bb637b
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 6 deletions.
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);
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);
});
});

0 comments on commit 1bb637b

Please sign in to comment.