Skip to content
Draft
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
3 changes: 3 additions & 0 deletions e2eproxy/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
.local
.cache
8 changes: 8 additions & 0 deletions e2eproxy/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules
.cache
.local
pnpm-lock.yaml
typechain-types
cache
artifacts
.bash_history
3 changes: 3 additions & 0 deletions e2eproxy/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
FROM node:lts
RUN npm install -g pnpm
WORKDIR /src
42 changes: 42 additions & 0 deletions e2eproxy/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
REPO=e2eproxy-sapphire
LABEL=cedarmist
DOCKER_RUN=docker run -v `pwd`:/src:rw --rm -ti -u `id -u`:`id -g`
DOCKER_RUN_DEV=$(DOCKER_RUN) --network host -w /src -h $(REPO)-dev -e HOME=/src -e HISTFILESIZE=0 -e HISTCONTROL=ignoreboth:erasedups $(REPO)/dev

all:
@echo ...

tsc:
$(DOCKER_RUN_DEV) pnpm tsc

hardhat-compile:
$(DOCKER_RUN_DEV) pnpm hardhat compile

hardhat-test:
$(DOCKER_RUN_DEV) pnpm hardhat test --network sapphire_local

pnpm-install:
$(DOCKER_RUN_DEV) pnpm install

#SAPPHIRE_DEV_DOCKER=ghcr.io/oasisprotocol/sapphire-dev:local
SAPPHIRE_DEV_DOCKER=ghcr.io/oasisprotocol/sapphire-dev:latest

sapphire-dev:
docker run --rm -it -p8545:8545 -p8546:8546 $(SAPPHIRE_DEV_DOCKER) -to 'test test test test test test test test test test test junk' -n 20

cache:
mkdir cache

cache/%.docker: Dockerfile.% cache
if [ ! -f "$@" ]; then \
docker build -f $< -t "${REPO}/$*" . ; \
docker image inspect "${REPO}/$*" > $@ ; \
fi

clean:
rm -rf artifacts cache coverage lib node_modules typechain-types
rm -rf .cache .config .local .npm
rm -rf .bash_history .node_repl_history .ts_node_repl_history coverage.json pnpm-lock.yaml

shell: cache/dev.docker
$(DOCKER_RUN_DEV) /bin/bash
16 changes: 16 additions & 0 deletions e2eproxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# End-to-End encrypted proxy contract example

This repo provides an example of how a contract can relay encrypted transactions on Oasis Sapphire, so the relayer cannot see which contract is being invoked or what the parameters are.

Using the [@oasis-protocol/sapphire-contracts](https://www.npmjs.com/package/@oasisprotocol/sapphire-contracts) library The E2EProxy contract generates a long-term X25519 keypair which allows users to submit a Deoxys-II encrypted payload (with forward secrecy) containing the contract address to invoke and the calldata to pass.

While [@oasis-protocol/sapphire-hardhat](https://www.npmjs.com/package/@oasisprotocol/sapphire-hardhat) package makes testing easy with Hardhat, you also need to run a local [sapphire-dev](https://github.com/oasisprotocol/oasis-web3-gateway/pkgs/container/sapphire-dev) instance which supports the necessary EVM precompiles.

For your convenience there is a `Makefile` which uses Docker to keep everything neatly contained:

```
make sapphire-dev & # This will take a few minutes
make pnpm-install
make hardhat-compile
make hardhat-test
```
32 changes: 32 additions & 0 deletions e2eproxy/contracts/E2EProxy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: CC-PDDC

pragma solidity ^0.8.0;

import "@oasisprotocol/sapphire-contracts/contracts/Sapphire.sol";

contract E2EProxy {
Sapphire.Curve25519PublicKey internal immutable publicKey;

Sapphire.Curve25519SecretKey internal immutable privateKey;

constructor (bytes memory extra_entropy) {
(publicKey, privateKey) = Sapphire.generateCurve25519KeyPair(extra_entropy);
}

function getPublicKey()
external view
returns (bytes32)
{
return Sapphire.Curve25519PublicKey.unwrap(publicKey);
}

function proxy(bytes32 peerPublicKey, bytes32 nonce, bytes memory data)
external payable
{
bytes32 symmetricKey = Sapphire.deriveSymmetricKey(Sapphire.Curve25519PublicKey.wrap(peerPublicKey), privateKey);

(address addr, bytes memory subcall_data) = abi.decode(Sapphire.decrypt(symmetricKey, nonce, data, ""), (address, bytes));

(bool success, bytes memory subcall_ret) = addr.call{value: msg.value}(subcall_data);
}
}
14 changes: 14 additions & 0 deletions e2eproxy/contracts/Example.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: CC-PDDC

pragma solidity ^0.8.0;

contract Example {
event Called(address from, uint value, bytes data);

function transferFrom(address from, address to, uint amount)
external
payable
{
emit Called(msg.sender, msg.value, abi.encodeWithSelector(msg.sig, from, to, amount));
}
}
63 changes: 63 additions & 0 deletions e2eproxy/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { HardhatUserConfig } from "hardhat/config";
import '@oasisprotocol/sapphire-hardhat';
import "@nomicfoundation/hardhat-toolbox";

const env_private_key = process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [];

const TEST_HDWALLET = {
mnemonic: "test test test test test test test test test test test junk",
path: "m/44'/60'/0'/0",
initialIndex: 0,
count: 20,
passphrase: "",
};

const config: HardhatUserConfig = {
mocha: {
timeout: 400000
},
paths: {
tests: "./tests"
},
solidity: {
version: "0.8.18",
settings: {
viaIR: false,
/*
debug: {
revertStrings: "debug"
},
*/
optimizer: {
enabled: true,
runs: 200,
}
},
},
typechain: {
target: "ethers-v5" // TODO: upgrade to ethers-v6 when hardhat-toolbox supports it
},
networks: {
hardhat: {
chainId: 1337 // We set 1337 to make interacting with MetaMask simpler
},
sapphire_local: {
url: "http://localhost:8545",
accounts: TEST_HDWALLET,
chainId: 0x5afd,
},
// https://docs.oasis.io/dapp/sapphire/
sapphire_mainnet: {
url: "https://sapphire.oasis.io/",
accounts: env_private_key,
chainId: 0x5afe,
},
sapphire_testnet: {
url: "https://testnet.sapphire.oasis.dev",
accounts: TEST_HDWALLET,
chainId: 0x5aff,
}
}
};

export default config;
37 changes: 37 additions & 0 deletions e2eproxy/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "e2eproxy-sapphire",
"version": "0.1.0",
"description": "End-to-End encryption between Client and Sapphire EVM contract using X25519 and Deoxys-II-256-128",
"main": "lib/index.js",
"keywords": [
"oasis",
"sapphire",
"e2e",
"encryption",
"evm",
"x25519",
"deoxys-ii"
],
"directories": {
"test": "tests"
},
"author": "CedarMist",
"license": "CC-PDDC",
"devDependencies": {
"@ethersproject/abi": "^5.7.0",
"@nomicfoundation/hardhat-network-helpers": "^1.0.8",
"@nomicfoundation/hardhat-toolbox": "^2.0.2",
"@nomiclabs/hardhat-ethers": "^2.2.3",
"@oasisprotocol/sapphire-hardhat": "^1.0.3",
"@types/node": "^18.16.0",
"hardhat": "^2.14.0",
"ts-node": "^10.9.1",
"typescript": "^5.0.4"
},
"dependencies": {
"@ethersproject/providers": "^5.7.2",
"@oasisprotocol/sapphire-contracts": "^0.2.1",
"@oasisprotocol/sapphire-paratime": "^1.0.15",
"ethers": "^5.7.2"
}
}
66 changes: 66 additions & 0 deletions e2eproxy/tests/e2eproxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: CC-PDDC

import { randomBytes } from "crypto";
import { expect } from 'chai';
import { ethers } from "hardhat";
import { Interface } from '@ethersproject/abi';

import * as sapphire from '@oasisprotocol/sapphire-paratime'

describe('E2EProxy', function () {
async function deploy() {
const Example_Contract = await ethers.getContractFactory("Example");
const example = await Example_Contract.deploy();

const extra_entropy = randomBytes(128);

const E2EProxy_Contract = await ethers.getContractFactory("E2EProxy");
const e2e = await E2EProxy_Contract.deploy(ethers.utils.arrayify(extra_entropy));

return { example, e2e };
}

it("End-to-end encrypted proxied call", async function ()
{
const {example, e2e} = await deploy();

// Create the calldata for an example function call
const iface = new Interface([
"function transferFrom(address from, address to, uint amount)"
]);
const example_calldata = iface.encodeFunctionData("transferFrom", [
"0x8ba1f109551bD432803012645Ac136ddd64DBA72",
"0xaB7C8803962c0f2F5BBBe3FA8bf41cd82AA1923C",
ethers.utils.parseEther("789.10111213")
])

// Encode the proxied call, specifying the address of the contract to invoke and its calldata
const plaintext = ethers.utils.defaultAbiCoder.encode([ "address", "bytes" ], [ example.address, example_calldata ]);

// Retrieve E2EProxy long-term public key & encrypt the proxied contract call with an ephemeral keypair
const e2e_pubkey = await e2e.getPublicKey();
const box = sapphire.cipher.X25519DeoxysII.ephemeral(e2e_pubkey);
let {nonce, ciphertext} = await box.encrypt(ethers.utils.arrayify(plaintext));
const nonce_bytes32_hex = ethers.utils.hexlify(nonce) + "0000000000000000000000000000000000";

// Invoke the proxy contract
const result = await e2e.proxy(box.publicKey, nonce_bytes32_hex, ciphertext, {value: ethers.utils.parseUnits("123456", "wei")});
const receipt = await result.wait();

// Verify the parameters received from the Example contract via proxy
let found = false;
if( receipt.events ) {
for( const r of receipt.events?.values() ) {
if( r.address == example.address ) {
const ev = example.interface.events['Called(address,uint256,bytes)'];
const decoded = example.interface.decodeEventLog(ev, r.data, r.topics);
expect(decoded.from).equals(e2e.address);
expect(decoded.value).equals(123456);
expect(decoded.data).equals(example_calldata);
found = true;
}
}
}
expect(found).equals(true);
});
});
12 changes: 12 additions & 0 deletions e2eproxy/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true
}
}