Skip to content

Commit

Permalink
Merge branch 'main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
rube-de committed Sep 17, 2024
2 parents 5bb1774 + d9041c1 commit 5ba383a
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 111 deletions.
2 changes: 1 addition & 1 deletion clients/js/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ NPM ?= pnpm

all: build test lint

build test lint::
build test lint format::
$(NPM) $@

clean:
Expand Down
150 changes: 60 additions & 90 deletions clients/js/src/calldatapublickey.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: Apache-2.0

import { decode as cborDecode } from 'cborg';
import { fromQuantity, getBytes } from './ethersutils.js';
import type { EIP2696_EthereumProvider } from './provider.js';
import { NETWORKS } from './networks.js';
import { SUBCALL_ADDR, CALLDATAPUBLICKEY_CALLDATA } from './constants.js';
import { Cipher, X25519DeoxysII } from './cipher.js';

/**
Expand All @@ -12,8 +13,6 @@ import { Cipher, X25519DeoxysII } from './cipher.js';
*/
const DEFAULT_PUBKEY_CACHE_EXPIRATION_MS = 60 * 5 * 1000;

export const OASIS_CALL_DATA_PUBLIC_KEY = 'oasis_callDataPublicKey';

// -----------------------------------------------------------------------------
// Fetch calldata public key
// Well use provider when possible, and fallback to HTTP(S)? requests
Expand Down Expand Up @@ -56,69 +55,31 @@ export interface CallDataPublicKey {
fetched: Date;
}

function toCallDataPublicKey(
result: RawCallDataPublicKeyResponseResult,
chainId: number,
) {
const key = getBytes(result.key);
return {
key,
checksum: getBytes(result.checksum),
signature: getBytes(result.signature),
epoch: result.epoch,
chainId,
fetched: new Date(),
} as CallDataPublicKey;
function parseBigIntFromByteArray(bytes: Uint8Array): bigint {
const eight = BigInt(8);
return bytes.reduce((acc, byte) => (acc << eight) | BigInt(byte), BigInt(0));
}

export async function fetchRuntimePublicKeyFromURL(
gwUrl: string,
fetchImpl: typeof fetch,
): Promise<RawCallDataPublicKeyResponse> {
const res = await fetchImpl(gwUrl, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: makeCallDataPublicKeyBody(),
});
if (!res.ok) {
throw new FetchError('Failed to fetch runtime public key.', res);
}
return await res.json();
}

function makeCallDataPublicKeyBody(): string {
return JSON.stringify({
jsonrpc: '2.0',
id: Math.floor(Math.random() * 1e9),
method: OASIS_CALL_DATA_PUBLIC_KEY,
params: [],
});
}
class AbiDecodeError extends Error {}

export async function fetchRuntimePublicKeyByChainId(
chainId: number,
opts?: { fetch?: typeof fetch },
): Promise<CallDataPublicKey> {
const { defaultGateway } = NETWORKS[chainId];
if (!defaultGateway)
throw new Error(
`Unable to fetch runtime public key for network with unknown ID: ${chainId}.`,
);
const fetchImpl = opts?.fetch ?? globalThis?.fetch;
if (!fetchImpl) {
throw new Error('No fetch implementation found!');
/// Manual ABI-parsing of ['uint', 'bytes']
function parseAbiEncodedUintBytes(bytes: Uint8Array): [bigint, Uint8Array] {
if (bytes.length < 32 * 3) {
throw new AbiDecodeError('too short');
}
const res = await fetchRuntimePublicKeyFromURL(defaultGateway, fetchImpl);
if (!res.result) {
throw new Error(
`fetchRuntimePublicKeyByChainId failed, empty result in: ${JSON.stringify(
res,
)}`,
);
const status = parseBigIntFromByteArray(bytes.slice(0, 32));
const offset = Number(parseBigIntFromByteArray(bytes.slice(32, 64)));
if (bytes.length < offset + 32) {
throw new AbiDecodeError('too short, offset');
}
return toCallDataPublicKey(res.result, chainId);
const data_length = Number(
parseBigIntFromByteArray(bytes.slice(offset, offset + 32)),
);
if (bytes.length < offset + 32 + data_length) {
throw new AbiDecodeError('too short, data');
}
const data = bytes.slice(offset + 32, offset + 32 + data_length);
return [status, data];
}

/**
Expand All @@ -128,39 +89,48 @@ export async function fetchRuntimePublicKeyByChainId(
* NOTE: MetaMask does not support Web3 methods it doesn't know about, so we
* have to fall-back to fetch()ing directly via the default chain gateway.
*/
export async function fetchRuntimePublicKey(
args: { upstream: EIP2696_EthereumProvider } | { chainId: number },
) {
export async function fetchRuntimePublicKey(args: {
upstream: EIP2696_EthereumProvider;
}) {
let chainId: number | undefined = undefined;
if ('upstream' in args) {
let resp: any | undefined = undefined;
const { upstream } = args;
chainId = fromQuantity(
(await upstream.request({
method: 'eth_chainId',
})) as string | number,
);

try {
resp = await upstream.request({
method: OASIS_CALL_DATA_PUBLIC_KEY,
params: [],
});
} catch (ex) {
// ignore RPC errors / failures
}

if (resp && 'key' in resp) {
return toCallDataPublicKey(resp, chainId);
}
const { upstream } = args;
chainId = fromQuantity(
(await upstream.request({
method: 'eth_chainId',
})) as string | number,
);

// NOTE: we hard-code the eth_call data, as it never changes
// It's equivalent to: // abi_encode(['string', 'bytes'], ['core.CallDataPublicKey', cborEncode(null)])
const call_resp = (await upstream.request({
method: 'eth_call',
params: [
{
to: SUBCALL_ADDR,
data: CALLDATAPUBLICKEY_CALLDATA,
},
'latest',
],
})) as string;
const resp_bytes = getBytes(call_resp);

// NOTE: to avoid pulling-in a full ABI decoder dependency, slice it manually
const [resp_status, resp_cbor] = parseAbiEncodedUintBytes(resp_bytes);
if (resp_status !== BigInt(0)) {
throw new Error(`fetchRuntimePublicKey - invalid status: ${resp_status}`);
}

if (!chainId) {
throw new Error(
'fetchRuntimePublicKey failed to retrieve chainId from provider',
);
}
return fetchRuntimePublicKeyByChainId(chainId);
const response = cborDecode(resp_cbor);

return {
key: response.public_key.key,
checksum: response.public_key.checksum,
signature: response.public_key.signature,
epoch: response.epoch,
chainId,
fetched: new Date(),
} as CallDataPublicKey;
}

/**
Expand Down
8 changes: 8 additions & 0 deletions clients/js/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: Apache-2.0

export const SUBCALL_ADDR = '0x0100000000000000000000000000000000000103';

// NOTE: we hard-code the eth_call data, as it never changes
// It's equivalent to: abi_encode(['string', 'bytes'], ['core.CallDataPublicKey', cborEncode(null)])
export const CALLDATAPUBLICKEY_CALLDATA =
'0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000016636f72652e43616c6c446174615075626c69634b6579000000000000000000000000000000000000000000000000000000000000000000000000000000000001f600000000000000000000000000000000000000000000000000000000000000';
16 changes: 15 additions & 1 deletion clients/js/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { BytesLike } from './ethersutils.js';
import { KeyFetcher } from './calldatapublickey.js';
import { SUBCALL_ADDR, CALLDATAPUBLICKEY_CALLDATA } from './constants.js';

// -----------------------------------------------------------------------------
// https://eips.ethereum.org/EIPS/eip-2696#interface
Expand Down Expand Up @@ -131,6 +132,16 @@ export function isWrappedRequestFn<
return p && SAPPHIRE_EIP1193_REQUESTFN in p;
}

export function isCallDataPublicKeyQuery(params?: object | readonly unknown[]) {
return (
params &&
Array.isArray(params) &&
params.length > 0 &&
params[0].to === SUBCALL_ADDR &&
params[0].data === CALLDATAPUBLICKEY_CALLDATA
);
}

/**
* Creates an EIP-1193 compatible request() function
* @param provider Upstream EIP-1193 provider to forward requests to
Expand Down Expand Up @@ -168,7 +179,10 @@ export function makeSapphireRequestFn(

// Decrypt responses which return encrypted data
if (method === 'eth_call') {
return cipher.decryptResult(res as BytesLike);
// If it's an unencrypted core.CallDataPublicKey query, don't attempt to decrypt the response
if (!isCallDataPublicKeyQuery(params)) {
return cipher.decryptResult(res as BytesLike);
}
}

return res;
Expand Down
2 changes: 0 additions & 2 deletions clients/js/test/calldatapublickey.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import {
hexlify,
fetchRuntimePublicKey,
OASIS_CALL_DATA_PUBLIC_KEY,
NETWORKS,
KeyFetcher,
} from '@oasisprotocol/sapphire-paratime';
Expand All @@ -18,7 +17,6 @@ describe('fetchRuntimePublicKey', () => {
/// Verifies call data public key fetching works
it('mock provider', async () => {
const upstream = new MockEIP1193Provider(NETWORKS.localnet.chainId);
await upstream.request({ method: OASIS_CALL_DATA_PUBLIC_KEY });

const pk = await fetchRuntimePublicKey({ upstream });
expect(hexlify(pk.key)).toEqual(hexlify(upstream.calldatapublickey));
Expand Down
34 changes: 17 additions & 17 deletions clients/js/test/utils.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
// SPDX-License-Identifier: Apache-2.0

export const CHAIN_ID = 0x5afe;

import {
EIP2696_EthereumProvider,
OASIS_CALL_DATA_PUBLIC_KEY,
hexlify,
isCallDataPublicKeyQuery,
} from '@oasisprotocol/sapphire-paratime';
import { SUBCALL_ADDR, CALLDATAPUBLICKEY_CALLDATA } from '../src/constants';

Check warning on line 8 in clients/js/test/utils.ts

View workflow job for this annotation

GitHub Actions / contracts-test

'SUBCALL_ADDR' is defined but never used

Check warning on line 8 in clients/js/test/utils.ts

View workflow job for this annotation

GitHub Actions / contracts-test

'CALLDATAPUBLICKEY_CALLDATA' is defined but never used
import { encode as cborEncode } from 'cborg';
import nacl from 'tweetnacl';
import { AbiCoder } from 'ethers';

export class MockEIP1193Provider {
public readonly request: jest.Mock<
Expand All @@ -28,20 +29,24 @@ export class MockEIP1193Provider {
public constructor(public chainId: number) {
this.calldatakeypair = nacl.sign.keyPair();
this.request = jest.fn(async ({ method, params }) => {
if (method === OASIS_CALL_DATA_PUBLIC_KEY) {
// Intercept calls to the `core.CallDataPublicKey` subcall
if (method === 'eth_call' && isCallDataPublicKeyQuery(params)) {
const signature = nacl.sign(
this.calldatapublickey,
this.calldatakeypair.secretKey,
);
return {
key: hexlify(this.calldatapublickey),
checksum: '0x',
const coder = AbiCoder.defaultAbiCoder();
const response = cborEncode({
epoch: 1,
signature: hexlify(signature),
chainId: chainId,
};
}
if (method === 'eth_chainId') {
public_key: {
key: hexlify(this.calldatapublickey),
checksum: '0x',
epoch: 1,
signature: hexlify(signature),
},
});
return hexlify(coder.encode(['uint', 'bytes'], [0n, response]));
} else if (method === 'eth_chainId') {
return chainId;
}
throw new Error(
Expand Down Expand Up @@ -70,11 +75,6 @@ export class MockNonRuntimePublicKeyProvider {
public constructor(public chainId: number) {
this.upstream = new MockEIP1193Provider(chainId);
this.request = jest.fn((args) => {
// Always errors while requesting the calldata public key
// This simulates, e.g. MetaMask, which doesn't allow arbitrary requests
if (args.method === OASIS_CALL_DATA_PUBLIC_KEY) {
throw new Error(`unhandled web3 call`);
}
return this.upstream.request(args);
});
}
Expand Down

0 comments on commit 5ba383a

Please sign in to comment.