diff --git a/.gitmodules b/.gitmodules index b1c953dd35..510d75b006 100644 --- a/.gitmodules +++ b/.gitmodules @@ -11,3 +11,6 @@ [submodule "external/cli"] path = external/cli url = https://github.com/oasisprotocol/cli +[submodule "external/sapphire-paratime"] + path = external/sapphire-paratime + url = https://github.com/oasisprotocol/sapphire-paratime diff --git a/docs/dapp/images/sapphire/gasless-gsn-flow.jpg b/docs/dapp/images/sapphire/gasless-gsn-flow.jpg deleted file mode 100644 index 4a3683560e..0000000000 Binary files a/docs/dapp/images/sapphire/gasless-gsn-flow.jpg and /dev/null differ diff --git a/docs/dapp/images/sapphire/gasless-on-chain-signer.svg b/docs/dapp/images/sapphire/gasless-on-chain-signer.svg deleted file mode 100644 index 6ca4a197c6..0000000000 --- a/docs/dapp/images/sapphire/gasless-on-chain-signer.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - -
1.
πŸ”’ call
makeProxyTransaction
(nonce, gasPrice, request, rsv)
1....
3.
Β πŸ”’eth.sendTransaction(tx)
3....
Gasless Client
(Node.js, Metamask, Oasis CLI)
Gasless Client...
5.
addr.call(tx)
5....
On-Chain Proxy
contract
On-Chain Proxy...
DAOv1
DAOv1
4.
decrypt tx
4....
Trust
Trust
2.
verify tx
2....
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/dapp/images/sapphire/hardhat-boilerplate-frontend1.png b/docs/dapp/images/sapphire/hardhat-boilerplate-frontend1.png deleted file mode 100644 index 8d08559908..0000000000 Binary files a/docs/dapp/images/sapphire/hardhat-boilerplate-frontend1.png and /dev/null differ diff --git a/docs/dapp/images/sapphire/hardhat-boilerplate-frontend2.png b/docs/dapp/images/sapphire/hardhat-boilerplate-frontend2.png deleted file mode 100644 index 49a293bba2..0000000000 Binary files a/docs/dapp/images/sapphire/hardhat-boilerplate-frontend2.png and /dev/null differ diff --git a/docs/dapp/images/sapphire/hardhat-boilerplate-frontend3.png b/docs/dapp/images/sapphire/hardhat-boilerplate-frontend3.png deleted file mode 100644 index 16c4f944fb..0000000000 Binary files a/docs/dapp/images/sapphire/hardhat-boilerplate-frontend3.png and /dev/null differ diff --git a/docs/dapp/images/sapphire/sourcify1.png b/docs/dapp/images/sapphire/sourcify1.png deleted file mode 100644 index b6dba793af..0000000000 Binary files a/docs/dapp/images/sapphire/sourcify1.png and /dev/null differ diff --git a/docs/dapp/images/sapphire/sourcify2.png b/docs/dapp/images/sapphire/sourcify2.png deleted file mode 100644 index 42b201dfe2..0000000000 Binary files a/docs/dapp/images/sapphire/sourcify2.png and /dev/null differ diff --git a/docs/dapp/images/sapphire/sourcify3.png b/docs/dapp/images/sapphire/sourcify3.png deleted file mode 100644 index ac32713f68..0000000000 Binary files a/docs/dapp/images/sapphire/sourcify3.png and /dev/null differ diff --git a/docs/dapp/sapphire/addresses.md b/docs/dapp/sapphire/addresses.md deleted file mode 100644 index 6fd24eb2bf..0000000000 --- a/docs/dapp/sapphire/addresses.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -description: List of Standard Contract Addresses ---- - -# Standard Contract Addresses - -| Name | Mainnet Address | Testnet Address | Verify | Source | -|--------------|--------------------------------------------|--------------------------------------------|------------------------------------------------------------------|---------------------------------| -| [Multicall V3][multicall] | `0xcA11bde05977b3631167028862bE2a173976CA11` | - | [Mainnet][multicall-verify-mainnet] | [Multicall3.sol][multicall-source] | -| Wrapped ROSE | `0x8Bc2B030b299964eEfb5e1e0b36991352E56D2D3` | `0xB759a0fbc1dA517aF257D5Cf039aB4D86dFB3b94` | [Mainnet][wrose-verify-mainnet], [Testnet][wrose-verify-testnet] | [WrappedROSE.sol][wrose-source] | - -[multicall-source]: https://github.com/mds1/multicall/blob/main/src/Multicall3.sol -[multicall-verify-mainnet]: https://sourcify.dev/#/lookup/0xcA11bde05977b3631167028862bE2a173976CA11 -[multicall]: https://multicall3.com/ - -[wrose-source]: https://github.com/oasisprotocol/sapphire-paratime/blob/main/contracts/contracts/WrappedROSE.sol -[wrose-verify-mainnet]: https://sourcify.dev/#/lookup/0x8Bc2B030b299964eEfb5e1e0b36991352E56D2D3 -[wrose-verify-testnet]: https://sourcify.dev/#/lookup/0xB759a0fbc1dA517aF257D5Cf039aB4D86dFB3b94 diff --git a/docs/dapp/sapphire/addresses.md b/docs/dapp/sapphire/addresses.md new file mode 120000 index 0000000000..8ef4fd10cf --- /dev/null +++ b/docs/dapp/sapphire/addresses.md @@ -0,0 +1 @@ +../../../external/sapphire-paratime/docs/addresses.md \ No newline at end of file diff --git a/docs/dapp/sapphire/authentication.md b/docs/dapp/sapphire/authentication.md deleted file mode 100644 index 341f1b2b50..0000000000 --- a/docs/dapp/sapphire/authentication.md +++ /dev/null @@ -1,209 +0,0 @@ ---- -description: Authenticate users with your confidential contracts ---- - -# View-Call Authentication - -User impersonation on Ethereum and other "Transparent EVMs" isn't a problem -because **everybody** can see **all** data however the Sapphire confidential -EVM prevents contracts from revealing confidential information to the wrong -party (account or contract) - for this reason we cannot allow arbitrary -impersonation of any `msg.sender`. - -In Sapphire, there are four types of contract calls: - - 1. Contract to contract calls (also known as *internal calls*) - 2. Unauthenticted view calls (queries using `eth_call`) - 3. Authenticated view calls (signed queries) - 4. Transactions (authenticated by signature) - -Intra-contract calls always set `msg.sender` appropriately, if a contract calls -another contract in a way which could reveal sensitive information, the calling -contract must implement access control or authentication. - -By default all `eth_call` queries used to invoke contract functions have the -`msg.sender` parameter set to `address(0x0)`. In contrast, authenticated calls are -signed by a keypair and will have the `msg.sender` parameter correctly initialized -(more on that later). Also, when a transaction is -submitted it is signed by a keypair (thus costs gas and can make state updates) -and the `msg.sender` will be set to the signing account. - -## Sapphire Wrapper - -The [@oasisprotocol/sapphire-paratime][sp-npm] Ethereum provider wrapper -`sapphire.wrap` function will **automatically end-to-end encrypt calldata** when -interacting with contracts on Sapphire, this is an easy way to ensure the -calldata of your dApp transactions remain confidential - although the `from`, -`to`, and `gasprice` parameters are not encrypted. - -[sp-npm]: https://www.npmjs.com/package/@oasisprotocol/sapphire-paratime - -:::tip Unauthenticated calls and Encryption - -Although the calls may be unauthenticated, they can still be encrypted! - -::: - -However, if the Sapphire wrapper has been attached to a signer then subsequent -view calls via `eth_call` will request that the user sign them (e.g. a -MetaMask popup), these are called **signed queries** meaning `msg.sender` will be -set to the signing account and can be used for authentication or to implement -access control. This may add friction to the end-user experience and can result -in frequent pop-ups requesting they sign queries which wouldn't normally require -any interaction on Transparent EVMs. - -Let's see how Sapphire interprets different contract calls. Suppose the -following solidity code: - -```solidity -contract Example { - address owner; - constructor () { - owner = msg.sender; - } - function isOwner () public view returns (bool) { - return msg.sender == owner; - } -} -``` - -In the sample above, assuming we're calling from the same contract or account -which created the contract, calling `isOwner` will return: - - * `false`, for `eth_call` - * `false`, with `sapphire.wrap` but without an attached signer - * `true`, with `sapphire.wrap` and an attached signer - * `true`, if called via the contract which created it -* `true`, if called via transaction - -## Caching Signed Queries - -When using signed queries the blockchain will be queried each time, however -the Sapphire wrapper will cache signatures for signed queries with the same -parameters to avoid asking the user to sign the same thing multiple times. - -Behind the scenes the signed queries use a "leash" to specify validity conditions -so the query can only be performed within a block and account `nonce` range. -These parameters are visible in the EIP-712 popup signed by the user. Queries -with the same parameters will use the same leash. - -## Daily Sign-In with EIP-712 - -One strategy which can be used to reduce the number of transaction signing -prompts when a user interacts with contracts via a dApp is to use -[EIP-712][eip-712] to "sign-in" once per day (or per-session), in combination -with using two wrapped providers: - -[eip-712]: https://eips.ethereum.org/EIPS/eip-712 - - 1. Provider to perform encrypted but unauthenticated view calls - 2. Another provider to perform encrypted and authenticated transactions (or view calls) - - The user will be prompted to sign each action. - -The two-provider pattern, in conjunction with a daily EIP-712 sign-in prompt -ensures all transactions are end-to-end encrypted and the contract can -authenticate users in view calls without frequent annoying popups. - -The code sample below uses an `authenticated` modifier to verify the sign-in: - -```solidity -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; - -struct SignatureRSV { - bytes32 r; - bytes32 s; - uint256 v; -} - -contract SignInExample { - bytes32 public constant EIP712_DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); - string public constant SIGNIN_TYPE = "SignIn(address user,uint32 time)"; - bytes32 public constant SIGNIN_TYPEHASH = keccak256(bytes(SIGNIN_TYPE)); - bytes32 public immutable DOMAIN_SEPARATOR; - - constructor () { - DOMAIN_SEPARATOR = keccak256(abi.encode( - EIP712_DOMAIN_TYPEHASH, - keccak256("SignInExample.SignIn"), - keccak256("1"), - block.chainid, - address(this) - )); - } - - struct SignIn { - address user; - uint32 time; - SignatureRSV rsv; - } - - modifier authenticated(SignIn calldata auth) - { - // Must be signed within 24 hours ago. - require( auth.time > (block.timestamp - (60*60*24)) ); - - // Validate EIP-712 sign-in authentication. - bytes32 authdataDigest = keccak256(abi.encodePacked( - "\x19\x01", - DOMAIN_SEPARATOR, - keccak256(abi.encode( - SIGNIN_TYPEHASH, - auth.user, - auth.time - )) - )); - - address recovered_address = ecrecover( - authdataDigest, uint8(auth.rsv.v), auth.rsv.r, auth.rsv.s); - - require( auth.user == recovered_address, "Invalid Sign-In" ); - - _; - } - - function authenticatedViewCall( - SignIn calldata auth, - ... args - ) - external view - authenticated(auth) - returns (bytes memory output) - { - // Use `auth.user` instead of `msg.sender`! - } -} -``` - -With the above contract code deployed, let's look at the frontend dApp and how -it can request the user to sign-in using EIP-712. You may wish to add additional -parameters which are authenticated such as the domain name. The following code -example uses Ethers: - -```typescript -const time = new Date().getTime(); -const user = await eth.signer.getAddress(); - -// Ask user to "Sign-In" every 24 hours. -const signature = await eth.signer._signTypedData({ - name: "SignInExample.SignIn", - version: "1", - chainId: import.meta.env.CHAINID, - verifyingContract: contract.address -}, { - SignIn: [ - { name: 'user', type: "address" }, - { name: 'time', type: 'uint32' }, - ] -}, { - user, - time: time -}); -const rsv = ethers.utils.splitSignature(signature); -const auth = {user, time, rsv}; -// The `auth` variable can then be cached. - -// Then in the future, authenticated view calls can be performed by -// passing auth without further user interaction authenticated data. -await contract.authenticatedViewCall(auth, ...args); -``` diff --git a/docs/dapp/sapphire/authentication.md b/docs/dapp/sapphire/authentication.md new file mode 120000 index 0000000000..912a6e5c27 --- /dev/null +++ b/docs/dapp/sapphire/authentication.md @@ -0,0 +1 @@ +../../../external/sapphire-paratime/docs/authentication.md \ No newline at end of file diff --git a/docs/dapp/sapphire/browser.md b/docs/dapp/sapphire/browser.md deleted file mode 100644 index 20ab2f9a1a..0000000000 --- a/docs/dapp/sapphire/browser.md +++ /dev/null @@ -1,208 +0,0 @@ ---- -description: Writing Sapphire dApp for browser and Metamask ---- - -# Browser Support - -Confidential Sapphire dApps work in web browsers by wrapping the -Ethereum provider such as Metamask to enable signing and encrypting -calls and transactions. - -Let's begin with the [Hardhat boilerplate]. As mentioned on their website -the boilerplate provides the following: - -- The Solidity contract implementing an ERC-20 token -- Tests for the entire functionality of the contract -- A minimal React front-end to interact with the contract using ethers.js - -Go ahead and clone the original [Hardhat boilerplate repo]. Move to the checked -out folder and **apply the Sapphire-specific changes** to `hardhat.config.js` -as [described in the quickstart][quickstart]. - -Next, install dependencies. The boilerplate project uses -[pnpm], but `yarn` and `npm` will also work with some modifications -around workspaces: - -```shell npm2yarn -npm install -npm install -D @oasisprotocol/sapphire-paratime -``` - -Now, you can deploy the contract on the Testnet with the private key of the -account holding some [TEST tokens]: - -```shell -PRIVATE_KEY="0x..." npx hardhat run scripts/deploy.js --network sapphire-testnet -``` - -This will compile the contract and deploy it on the Testnet. In addition -to the quickstart steps, **the contract address and ABI will also automatically -be copied over to the `frontend/src/contracts` folder so that the frontend can -access them!** - -:::warning - -The contract in the Hardhat boilerplate is ERC-20-compatible and emits the -`transfer` event. If your wish to preserve confidentiality, you can comment -out [line 66]. Read [the guide](guide.mdx#contract-logs) to learn more. - -::: - -## Signing Sapphire Calls and Transactions in Browser - -Now, let's explore the frontend of our dApp. Begin by moving into the -`frontend` folder and install dependencies: - -```shell npm2yarn -npm install -npm install -D @oasisprotocol/sapphire-paratime -``` - -The main frontend logic is stored in `frontend/src/components/Dapp.js`. Apply -the following changes: - -```diff title="frontend/src/components/Dapp.js" ---- a/hardhat-boilerplate/frontend/src/components/Dapp.js -+++ b/hardhat-boilerplate/frontend/src/components/Dapp.js -@@ -2,6 +2,7 @@ - - // We'll use ethers to interact with the Ethereum network and our contract - import { ethers } from "ethers"; -+import * as sapphire from '@oasisprotocol/sapphire-paratime'; - - // We import the contract's artifacts and address here, as we are going to be - // using them with ethers -@@ -22,7 +23,7 @@ - // This is the Hardhat Network id that we set in our hardhat.config.js. - // Here's a list of network ids https://docs.metamask.io/guide/ethereum-provider.html#properties - // to use when deploying to other networks. --const HARDHAT_NETWORK_ID = '1337'; -+const HARDHAT_NETWORK_ID = '23295'; // Sapphire Testnet - - // This is an error code that indicates that the user canceled a transaction - const ERROR_CODE_TX_REJECTED_BY_USER = 4001; -@@ -225,14 +226,20 @@ - - async _initializeEthers() { - // We first initialize ethers by creating a provider using window.ethereum -- this._provider = new ethers.providers.Web3Provider(window.ethereum); -+ this._provider = sapphire.wrap(new ethers.providers.Web3Provider(window.ethereum)); - -- // Then, we initialize the contract using that provider and the token's -- // artifact. You can do this same thing with your contracts. -+ // Then, we initialize two contract instances: -+ // - _token: Used for eth_calls (e.g. balanceOf, name, symbol) -+ // - _tokenWrite: Used for on-chain transactions (e.g. transfer) - this._token = new ethers.Contract( - contractAddress.Token, - TokenArtifact.abi, -- this._provider.getSigner(0) -+ this._provider, -+ ); -+ this._tokenWrite = new ethers.Contract( -+ contractAddress.Token, -+ TokenArtifact.abi, -+ sapphire.wrap(new ethers.providers.Web3Provider(window.ethereum).getSigner()) - ); - } - -@@ -294,7 +301,7 @@ - - // We send the transaction, and save its hash in the Dapp's state. This - // way we can indicate that we are waiting for it to be mined. -- const tx = await this._token.transfer(to, amount); -+ const tx = await this._tokenWrite.transfer(to, amount); - this.setState({ txBeingSent: tx.hash }); - - // We use .wait() to wait for the transaction to be mined. This method -@@ -360,8 +367,8 @@ - return true; - } - -- this.setState({ -- networkError: 'Please connect Metamask to Localhost:8545' -+ this.setState({ -+ networkError: 'Please connect to Sapphire ParaTime Testnet' - }); - - return false; -``` - -Beside the obvious change to the chain ID and wrapping ethers.js objects with the -Sapphire wrapper you can notice that we initialized **two** contract -instances: - -- `this._token` object will be used for unsigned eth calls. This is the - Hardhat method of [setting `from` transaction field to all zeros] - [guide-transaction-calls] in order to avoid Metamask signature popups. - Although the call is unsigned, **it is still encrypted** with the - corresponding runtime key to preserve confidentiality. -- `this._tokenWrite` object will be used for signed calls and transactions. The - user will be prompted by Metamask for the signature. Both β€” calls and - transactions β€” will be encrypted. - -## Trying it - -Start the frontend by typing: - -```shell npm2yarn -npm run start -``` - -If all goes well the web server will spin up and your browser should -automatically open `http://localhost:3000`. - -![Hardhat boilerplate frontend](../images/sapphire/hardhat-boilerplate-frontend1.png) - -Go ahead and connect the wallet. If you haven't done it yet, you will have -to add the [Sapphire ParaTime Testnet network to your Metamask] -[sapphire-testnet]. Once connected, the frontend will make an unsigned call to -the `balanceOf` view and show you the amount of `MHT`s in your selected -Metamask account. - -![MHT balance of your account](../images/sapphire/hardhat-boilerplate-frontend2.png) - -Next, let's transfer some `MHT`s. Fill in the amount, the address and hit the -*Transfer* button. Metamask will show you the popup to sign and submit the -transfer transaction. Once confirmed, Metamask will both **sign and encrypt** the transaction. - -![Sign and encrypt the transfer transaction](../images/sapphire/hardhat-boilerplate-frontend3.png) - -Once the transaction is processed, you will get a notification from Metamask -and the balance in the dApp will be updated. - -:::tip - -If you commented out `emit Transfer(...)`, the transfer of `MHT`s would have -been completely confidential. In the example above, the [following transaction] -[block explorer] was generated. Go ahead and check your transaction on the -block explorer too, to make sure no sensitive data was leaked! - -::: - -Congratulations, you successfully implemented your first truly confidential -dApp which runs in the browser and wraps Metamask to both sign and encrypt the -transactions! - -Should you have any questions or ideas to share, feel free to reach out to us -on [discord and other social media channels][social-media]. - -:::info Example - -You can download a full working example from the [Sapphire ParaTime examples] -repository. - -::: - -[block explorer]: https://testnet.explorer.sapphire.oasis.dev/tx/0x3303dea5d48291d1564cad573f21fc71fcbdc2b862e17e056287fd9207e3bc53 -[guide-transaction-calls]: guide.mdx#transactions--calls -[Hardhat boilerplate repo]: https://github.com/NomicFoundation/hardhat-boilerplate -[Hardhat boilerplate]: https://hardhat.org/tutorial/boilerplate-project -[Hardhat tutorial]: https://hardhat.org/tutorial -[line 66]: https://github.com/NomicFoundation/hardhat-boilerplate/blob/13bd712c1285b2de572f14d20e6a750ae08565c0/contracts/Token.sol#L66 -[quickstart]: quickstart.mdx#add-the-sapphire-testnet-to-hardhat -[sapphire-testnet]: ./README.mdx#testnet -[Sapphire ParaTime examples]: https://github.com/oasisprotocol/sapphire-paratime/tree/main/examples/hardhat-boilerplate -[social-media]: ../../get-involved/README.md#social-media-channels -[pnpm]: https://pnpm.io -[TEST tokens]: quickstart.mdx#get-some-sapphire-testnet-tokens diff --git a/docs/dapp/sapphire/browser.md b/docs/dapp/sapphire/browser.md new file mode 120000 index 0000000000..1b1dfca35c --- /dev/null +++ b/docs/dapp/sapphire/browser.md @@ -0,0 +1 @@ +../../../external/sapphire-paratime/docs/browser.md \ No newline at end of file diff --git a/docs/dapp/sapphire/gasless.md b/docs/dapp/sapphire/gasless.md deleted file mode 100644 index 775690d504..0000000000 --- a/docs/dapp/sapphire/gasless.md +++ /dev/null @@ -1,388 +0,0 @@ ---- -description: Submitting transactions without paying for fees ---- - -# Gasless Transactions - -When you submit a transaction to a blockchain, you need to pay certain fee -(called *gas* in Ethereum jargon). Since only the transactions with the highest -fee will be included in the block, this mechanism effectively prevents denial -of service attacks on the network. On the other hand, paying for gas requires -from the user that they have certain amount of blockchain-native tokens -available in their wallet which may not be feasible. - -In this chapter we will learn how the user signs and sends their transaction to -a *relayer*. The relayer then wraps the original signed transaction into a new -*meta-transaction* (see [ERC-2771] for details), signs it and pays for the -necessary transaction fees. When the transaction is submitted the on-chain -recipient contract decodes the meta-transaction, verifies both signatures and -executes the original transaction. - -Oasis Sapphire supports two transaction relaying methods: The **on-chain -signer** exposes the Oasis-specific contract state encryption functionality -while the **gas station network** method is a standardized approach known in -other blockchains as well. - -:::caution - -The gas station network implementation on Sapphire is still in early beta. Some -features such as the browser support are not fully implemented yet. - -::: - -[ERC-2771]: https://eips.ethereum.org/EIPS/eip-2771 - -## On-Chain Signer - -The on-chain signer is a smart contract which receives the user's transaction, -checks whether the transaction is valid, wraps it into a meta-transaction -(which includes paying for the transaction fee) and returns it back to the user -in [EIP-155] format. These steps are executed as a confidential call. Finally, -the user submits the generated transaction to the network. - -![Diagram of the On-Chain Signing](../images/sapphire/gasless-on-chain-signer.svg) - -### EIP155Signer - -To sign a transaction, the Sapphire's `EIP155Signer` library bundled along the -`@oasisprotocol/sapphire-contract` package comes with the following helper which -returns a raw, RLP-encoded, signed transaction ready to be broadcast: - -```solidity -function sign(address publicAddress, bytes32 secretKey, EthTx memory transaction) internal view returns (bytes memory); -``` - -`publicAddress` and `secretKey` are the signer's address and their private key -used to sign a meta-transaction (and pay for the fees). We will store these -sensitive data inside the encrypted smart contract state together with the -signer's `nonce` field in the following struct: - -```solidity -struct EthereumKeypair { - address addr; - bytes32 secret; - uint64 nonce; -} -``` - -The last `transaction` parameter in the `sign()` function is the transaction -encoded in a format based on [EIP-155]. This can either be the original user's -transaction or a meta-transaction. - -### Gasless Proxy Contract - -The following snippet is a complete *Gasless* contract for wrapping the user's -transactions (`makeProxyTx()`) and executing them (`proxy()`). The signer's -private key containing enough balance to cover transaction fees should be -provided in the constructor. - -```solidity -import {EIP155Signer} from "@oasisprotocol/sapphire-contracts/contracts/EIP155Signer.sol"; - -struct EthereumKeypair { - address addr; - bytes32 secret; - uint64 nonce; -} - -struct EthTx { - uint64 nonce; - uint256 gasPrice; - uint64 gasLimit; - address to; - uint256 value; - bytes data; - uint256 chainId; -} - -// Proxy for gasless transaction. -contract Gasless { - EthereumKeypair private kp; - - function setKeypair(EthereumKeypair memory keypair) external payable { - kp = keypair; - } - - function makeProxyTx(address innercallAddr, bytes memory innercall) - external - view - returns (bytes memory output) - { - bytes memory data = abi.encode(innercallAddr, innercall); - - // Call will invoke proxy(). - return - EIP155Signer.sign( - kp.addr, - kp.secret, - EIP155Signer.EthTx({ - nonce: kp.nonce, - gasPrice: 100_000_000_000, - gasLimit: 250000, - to: address(this), - value: 0, - data: abi.encodeCall(this.proxy, data), - chainId: block.chainid - }) - ); - } - - function proxy(bytes memory data) external payable { - (address addr, bytes memory subcallData) = abi.decode( - data, - (address, bytes) - ); - (bool success, bytes memory outData) = addr.call{value: msg.value}( - subcallData - ); - if (!success) { - // Add inner-transaction meaningful data in case of error. - assembly { - revert(add(outData, 32), mload(outData)) - } - } - kp.nonce += 1; - } -} -``` - -:::tip - -The snippet above only runs on Sapphire Mainnet, Testnet or Localnet. -`EIP155Signer.sign()` is not supported on other EVM chains. - -::: - -### Simple Gasless Commenting dApp - -Let's see how we can implement on-chain signer for a gasless commenting dApp -like this: - -```solidity -contract CommentBox { - string[] public comments; - - function comment(string memory commentText) external { - comments.push(commentText); - } -} -``` - -Then, the TypeScript code on a client side for submitting a comment in a gasless -fashion would look like this: - -```typescript -const CommentBox = await ethers.getContractFactory("CommentBox"); -const commentBox = await CommentBox.deploy(); -const Gasless = await ethers.getContractFactory("Gasless"); -const gasless = await Gasless.deploy(); - -// Set the keypair used to sign the meta-transaction. -await gasless.setKeypair({ - addr: "70997970C51812dc3A010C7d01b50e0d17dc79C8", - secret: Uint8Array.from(Buffer.from("59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", 'hex')), - nonce: 0, -}); - -const innercall = commentBox.interface.encodeFunctionData('comment', ['Hello, free world!']); -const tx = await gasless.makeProxyTx(commentBox.address, innercall); - -const plainProvider = new ethers.providers.JsonRpcProvider(ethers.provider.connection); -const plainResp = await plainProvider.sendTransaction(tx); - -const receipt = await ethers.provider.waitForTransaction(plainResp.hash); -if (!receipt || receipt.status != 1) throw new Error('tx failed'); -``` - -:::info Example - -You can download a complete on-chain signer example based on the above snippets -from the [Sapphire ParaTime examples] repository. - -::: - -[Sapphire ParaTime examples]: - https://github.com/oasisprotocol/sapphire-paratime/tree/main/examples/onchain-signer - -### Gasless Proxy in Production - -The snippets above have shown how the on-chain signer can generate and sign a -meta-transaction for arbitrary transaction. In production environment however, -you must consider the following: - -#### Confidentiality - -Both the inner- and the meta-transaction are stored on-chain unencrypted. Use -`Sapphire.encrypt()` and `Sapphire.decrypt()` call on the inner-transaction with -an encryption key generated and stored inside a confidential contract state. - -#### Gas Cost and Gas Limit - -The gas cost and the gas limit in our snippet were hardcoded inside the -contract. Ideally the gas cost should be dynamically adjusted by an oracle and -the gas limit determined based on the type of transactions. **Never let gas cost -and limit to be freely defined by the user, since they can drain your relayer's -account.** - -#### Allowed Transactions - -Your relayer will probably be used for transactions of a specific contract only. -One approach is to store the allowed address of the target contract and **only -allow calls to this contract address**. - -#### Access Control - -You can either whitelist specific addresses of the users in the relayer contract -or implement the access control in the target contract. In the latter case, the -relayer's `makeProxyTx()` should simulate the execution of the inner-transaction -and generate the meta-transaction only if it inner-transaction succeeded. - -#### Multiple Signers - -Only one transaction per block can be relayed by the same signer since the order -of the transactions is not deterministic and nonces could mismatch. To overcome -this, relayer can randomly pick a signer from the **pool of signers**. When the -transaction is relayed, don't forget to reimburse the signer of the transaction! - -:::info Example - -All the above points are considered in the [Demo Voting dApp][demo-voting]. -You can explore the code and also try out a deployed gasless version of the -voting dApp on the [Oasis Playground site][demo-voting-playground]. The access -control list is configured so that anyone can vote on any poll and only poll -creators can close the poll. - -::: - -[demo-voting]: https://github.com/oasisprotocol/demo-voting -[demo-voting-playground]: https://playground.oasis.io/demo-voting -[dao-opl]: ../opl/host.md -[EIP-155]: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md - -## Gas Station Network - -[Gas Station Network](https://docs.opengsn.org) (GSN) was adapted to work with -Sapphire in a forked `@oasislabs/opengsn-cli` package. The diagram below -illustrates a flow for signing a transaction by using a GSN[^1]. - -![Diagram of the Gas Station Network Flow](../images/sapphire/gasless-gsn-flow.jpg) - -[^1]: The GSN flow diagram is courtesy of [OpenGSN documentation][opengsn-docs]. - -[opengsn-docs]: https://github.com/opengsn/docs - -### Package Install - -Starting with an empty folder, let us install the -[Oasis fork of the GSN command line tool](https://github.com/oasislabs/gsn) by -using the following commands: - -```shell npm2yarn -npm init -npm install -D @oasislabs/opengsn-cli -``` - -Next, we will export our hex-encoded private key (**without** the leading `0x`) -for deploying the gas station network as an environment variable: - -```shell -export PRIVATE_KEY=... -``` - -### Deploy GSN - -Deploy GSN relaying contracts along with the test paymaster using a -test token. Use the address of your account as `--burnAddress` and -`--devAddress` parameters: - -```shell -npx gsn deploy --network sapphire-testnet --burnAddress 0xfA3AC9f65C9D75EE3978ab76c6a1105f03156204 --devAddress 0xfA3AC9f65C9D75EE3978ab76c6a1105f03156204 --testToken true --testPaymaster true --yes --privateKeyHex $PRIVATE_KEY -``` - -After the command finishes successfully, you should find the addreses of -deployed contracts at the end: - -``` - Deployed TestRecipient at address 0x594cd6354b23A5200a57355072E2A5B15354ee21 - - RelayHub: 0xc4423AB6133B06e4e60D594Ac49abE53374124b3 - RelayRegistrar: 0x196036FBeC1dA841C60145Ce12b0c66078e141E6 - StakeManager: 0x6763c3fede9EBBCFbE4FEe6a4DE6C326ECCdacFc - Penalizer: 0xA58A0D302e470490c064EEd5f752Df4095d3A002 - Forwarder: 0x59001d07a1Cd4836D22868fcc0dAf3732E93be81 - TestToken (test only): 0x6Ed21672c0c26Daa32943F7b1cA1f1d0ABdbac66 - Paymaster (Default): 0x8C06261f58a024C958d42df89be7195c8690008d -``` - - -### Start GSN Relay Server - -Now we are ready to start our own relay server by using the following command. -Use the newly deployed: - -- `RelayHub` address for `--relayHubAddress`, -- `TestToken` address for `--managerStakeTokenAddress`, -- address of your account for `--owner-address` - -```shell -npx gsn relayer-run --relayHubAddress 0xc4423AB6133B06e4e60D594Ac49abE53374124b3 --managerStakeTokenAddress 0x6Ed21672c0c26Daa32943F7b1cA1f1d0ABdbac66 --ownerAddress '0xfA3AC9f65C9D75EE3978ab76c6a1105f03156204' --ethereumNodeUrl 'https://testnet.sapphire.oasis.dev' --workdir . -``` - -### Fund and Register GSN Relay Server - -The first thing is to fund your relay server so that it has enough native -tokens to pay for others' transactions. Let's fund the paymaster with -**5 tokens**. Use the `RelayHub` and `Paymaster` addresses for `--hub` -and `--paymaster` values: - -```shell -npx gsn paymaster-fund --network sapphire-testnet --hub 0xc4423AB6133B06e4e60D594Ac49abE53374124b3 --paymaster 0x8C06261f58a024C958d42df89be7195c8690008d --privateKeyHex $PRIVATE_KEY --amount 5000000000000000000 -``` - -You can check the balance of the paymaster by running: - -```shell -npx gsn paymaster-balance --network sapphire-testnet --hub 0xc4423AB6133B06e4e60D594Ac49abE53374124b3 --paymaster 0x8C06261f58a024C958d42df89be7195c8690008d -``` - -Next, we need to register the relay server with the your desired `relayUrl` by -staking the `token` the relayHub requires. - -```shell -npx gsn relayer-register --network sapphire-testnet --relayUrl 'http://localhost:8090' --token 0x6Ed21672c0c26Daa32943F7b1cA1f1d0ABdbac66 --wrap true --privateKeyHex $PRIVATE_KEY -``` - -After this step, your relay server should be ready to take incoming relay -requests and forward them to the relay hub on Sapphire Testnet. - -### Send Testing Relayed Requests: - -We can test whether a relayed request can be forwarded and processed correctly. -Scroll up to find the GSN deployment response and use the following parameters: -- `Forwarder` as `--to`, -- `Paymaster` as `--paymaster`, -- your account address as `--from` - -Parameters matching our deployment would be: - -```shell -npx gsn send-request --network sapphire-testnet --abiFile 'node_modules/@oasislabs/opengsn-cli/dist/compiled/TestRecipient.json' --method emitMessage --methodParams 'hello world!' --to 0x594cd6354b23A5200a57355072E2A5B15354ee21 --paymaster 0x8C06261f58a024C958d42df89be7195c8690008d --privateKeyHex $PRIVATE_KEY --from 0xfA3AC9f65C9D75EE3978ab76c6a1105f03156204 --gasLimit 150000 --gasPrice 100 -``` - -:::info - -More detailed explanations of these GSN commands and parameters can be found on -the [upstream OpenGSN website](https://docs.opengsn.org/javascript-client/gsn-helpers.html). - -::: - -### Writing a GSN-enabled Smart Contract - -First, install the OpenGSN contracts package: - -```shell npm2yarn -npm install -D @opengsn/contracts@3.0.0-beta.2 -``` - -Then follow the remainder of the steps from the -[upstream OpenGSN docs](https://docs.opengsn.org/contracts/#receiving-a-relayed-call). diff --git a/docs/dapp/sapphire/gasless.md b/docs/dapp/sapphire/gasless.md new file mode 120000 index 0000000000..1ae10d0a76 --- /dev/null +++ b/docs/dapp/sapphire/gasless.md @@ -0,0 +1 @@ +../../../external/sapphire-paratime/docs/gasless.md \ No newline at end of file diff --git a/docs/dapp/sapphire/guide.mdx b/docs/dapp/sapphire/guide.mdx deleted file mode 100644 index b7689b5aae..0000000000 --- a/docs/dapp/sapphire/guide.mdx +++ /dev/null @@ -1,390 +0,0 @@ ---- -description: Guide to creating secure dApps on Sapphire ---- - -import DocCard from '@theme/DocCard'; -import {findSidebarItem} from '@site/src/sidebarUtils'; - -# Guide - -This page mainly describes the differences between Sapphire and Ethereum -since there are a number of excellent tutorials on developing for Ethereum. -If you don't know where to begin, the [Hardhat tutorial], [Solidity docs], and -[Emerald dApp tutorial] are great places to start. You can continue following -this guide once you've set up your development environment and have deployed -your contract to a non-confidential EVM network (e.g., Ropsten, Emerald). - - -[Hardhat tutorial]: https://hardhat.org/tutorial -[Solidity docs]: https://docs.soliditylang.org/en/v0.8.15/solidity-by-example.html -[Emerald dApp tutorial]: ../emerald/writing-dapps-on-emerald.mdx - -## Oasis Consensus Layer and Sapphire ParaTime - -The Oasis Network consists of the consensus layer and a number of ParaTimes. -ParaTimes are independent replicated state machines that settle transactions -using the consensus layer (to learn more, check the [Oasis Network Overview] -[overview chapter]). Sapphire is a ParaTime which implements the Ethereum -Virtual Machine (EVM). - -The minimum and also expected block time in Sapphire is **6 seconds**. Any -Sapphire transaction will require at least this amount of time to be executed, -and probably no more. - -ParaTimes, Sapphire included, are not allowed to directly access your tokens stored -in consensus layer accounts. You will need to _deposit_ tokens from your consensus -account to Sapphire. Consult the [How to transfer ROSE into an EVM ParaTime] -[how-to-deposit-rose] chapter to learn more. - - -[overview chapter]: ../../general/oasis-network/README.mdx -[how-to-deposit-rose]: ../../general/manage-tokens/how-to-transfer-rose-into-paratime.mdx -[Testnet faucet]: https://faucet.testnet.oasis.dev/ - -## Testnet and Mainnet - -Sapphire is deployed on both Testnet and Mainnet. The Testnet should be -considered unstable software and may have its state wiped at any time. As -the name implies, only use the Testnet for testing unless you're testing how -angry your users get when state is wiped. - -:::danger Never deploy production services on Testnet - -Because Testnet state can be wiped in the future, you should **never** deploy a -production service on the Testnet! Just don't do it! - -Also note that while the Testnet does use actual TEEs, due to experimental -software and different security parameters, **confidentiality of Sapphire on the -Testnet is not guaranteed** -- all transactions and state published on the -Sapphire Testnet should be considered public. - -::: - -:::tip - -For testing purposes, visit our [Testnet faucet] to obtain some TEST which you -can then use on the Sapphire Testnet to pay for gas fees. The faucet supports -sending TEST both to your consensus layer address or to your address inside the -ParaTime. - -::: - -[network-parameters]: ../../node/mainnet/README.md -[Testnet]: ../../node/testnet/README.md - -## Sapphire vs Ethereum - -Sapphire is generally compatible with Ethereum, the EVM, and all of the -user and developer tooling that you already use. There are a few breaking changes, -but we think that you'll like them: - -* Contract state is only visible to the contract that wrote it. With respect - to the contract API, it's as if all state variables are declared as `private`, but - with the further restriction that not even full nodes can read the values. Public or - access-controlled values are provided instead through explicit getters. -* Transactions and calls are end-to-end encrypted into the contract. Only the caller - and the contract can see the data sent to/received from the ParaTime. This ends up - defeating most of the utility of block explorers, however. -* The `from` address using of calls is derived from a signature attached to the call. - Unsigned calls have their sender set to the zero address. This allows contract authors - to write getters that release secrets to authenticated callers, but without - requiring a transaction to be posted on-chain. - -In addition to confidentiality, you get a few extra benefits including the ability to generate private -entropy, and make signatures on-chain. An example of a dApp that uses both is a HSM contract -that generates an Ethereum wallet and signs transactions sent to it via transactions. - -Otherwise Sapphire is like Emerald, which is like a fast, cheap Ethereum. - -## Integrating Sapphire - -Once ROSE tokens are [deposited into Sapphire], it should be painless for users to begin -using dApps. To achieve this ideal user experience, we have to modify the dApp a little, -but it's made simple by our compatibility library, [@oasisprotocol/sapphire-paratime]. - -There are compatibility layers in other languages, which may be found in [the repo]. - - -[deposited into Sapphire]: ../../general/manage-tokens/how-to-transfer-rose-into-paratime.mdx -[@oasisprotocol/sapphire-paratime]: https://www.npmjs.com/package/@oasisprotocol/sapphire-paratime -[the repo]: https://github.com/oasisprotocol/sapphire-paratime/tree/main/clients - -## Writing Secure dApps - -### Wallets - -Sapphire is compatible with popular self-custodial wallets including MetaMask, -Ledger, Brave, and so forth. You can also use libraries like Web3.js and Ethers -to create programmatic wallets. In general, if it generates secp256k1 signatures, -it'll work just fine. - -### Languages & Frameworks - -Sapphire is programmable using any language that targets the EVM, such as Solidity -or Vyper. If you prefer to use an Ethereum framework like Hardhat or Foundry, you -can also use those with Sapphire; all you need to do is set your Web3 gateway URL. -You can find the details of the Oasis Sapphire Web3 gateway -[here](/dapp/sapphire#web3-gateway). - -### Transactions & Calls - -![Client, Key Manager, Compute Node diagram](../../general/images/architecture/client-km-compute.svg) - -The figure above illustrates the flow of a confidential smart contract -*transaction* executed on the Sapphire ParaTime. - -Transactions and calls must be encrypted and signed for maximum security. -You can use the [@oasisprotocol/sapphire-paratime] JS package to make your life -easy. It'll handle cryptography and signing for you. - -You should be aware that taking actions based on the value of private data may -leak the private data through side channels like time spent and gas use. If you -need to branch on private data, you should in most cases ensure that both -branches exhibit similar time/gas and storage patterns. - -You can also make confidential smart contract *calls* on Sapphire. If you -use `msg.sender` for access control in your contract, the call **must be -signed**, otherwise `msg.sender` will be zeroed. On the other hand, set the -`from` address to all zeros, if you want to avoid annoying signature popups in -the user's wallet for calls that do not need to be signed. The JS library will -do this for you. - -:::note - -Inside the smart contract code, there is no way of knowing whether the -client's call data were originally encrypted or not. - -::: - -
- Detailed confidential smart contract transaction flow on Sapphire -
- -
-
- -
- Detailed confidential smart contract call flow on Sapphire -
- -
-
- -### Contract State - -The Sapphire state model is like Ethereum's except for all state being encrypted -and not accessible to anyone except the contract. The contract, executing in an -active (attested) Oasis compute node is the only entity that can request its -state encryption key from the Oasis key manager. Both the keys and values of the -items stored in state are encrypted, but the size of either is *not* hidden. You -app may need to pad state items to a constant length, or use other obfuscation. -Observers may also be able to infer computation based on storage access patterns, -so you may need to obfuscate that, too. See [Security chapter] for more -recommendations. - -[Security chapter]: ./security.md#storage-access-patterns - -:::danger Contract state leaks a fine-grained access pattern - -Contract state is backed by an encrypted key-value store. However, the trace of -encrypted records is leaked to the compute node. As a concrete example, an ERC-20 -token transfer would leak which encrypted record is for the sender's account -balance and which is for the receiver's account balance. Such a token would be -traceable from sender address to receiver address. Obfuscating the storage access -patterns may be done by using an ORAM implementation. - -::: - -Contract state may be made available to third parties through logs/events, or -explicit getters. - -### Contract Logs - -Contract logs/events (e.g., those emitted by the Solidity `emit` keyword) -are exactly like Ethereum. Data contained in events is *not* encrypted. -Precompiled contracts are available to help you encrypt data that you can -then pack into an event, however. - -:::danger Unmodified contracts may leak state through logs - -Base contracts like those provided by OpenZeppelin often emit logs containing -private information. If you don't know they're doing that, you might undermine -the confidentiality of your state. As a concrete example, the ERC-20 spec -requires implementers to emit an `event Transfer(from, to, amount)`, which is -obviously problematic if you're writing a confidential token. What you can -do instead is fork that contract and remove the offending emissions. - -::: - -## Contract Verification - -[Sourcify] is the preferred service for the [verification of smart -contracts][ethereum-contract-verify] deployed on Sapphire. Make sure you have -the **address of each deployed contract** available (your deployment scripts -should report those) and the **contracts JSON metadata file** generated when -compiling contracts (Hardhat stores it inside the `artifacts/build-info` folder -and names it as a 32-digit hex number). If your project contains multiple -contracts, you will need to verify each contract separately. - -:::danger Contract deployment encryption - -**Do not deploy your contract with an encrypted contract deployment transaction, -if you want to verify it.** For example, if your `hardhat.config.ts` -or deployment script contains `import '@oasisprotocol/sapphire-hardhat'` or -`import '@oasisprotocol/sapphire-paratime'` lines at the beginning, you should -comment those out for the deployment. - -Verification services will try to match the contract deployment transaction code -with the one in the provided contract's metadata and since the transaction was -encrypted with an ephemeral ParaTime key, the verification service will not be -able to decrypt it. Some services may extract the contract's bytecode from the -chain directly by calling `eth_getCode` RPC, but this will not work correctly -for contracts with immutable variables. - -::: - -To verify a contract deployed on Sapphire Mainnet or Testnet: - -1. Visit the [Sourcify] website and hit the "VERIFY CONTRACT" button. - - ![Sourcify website](../images/sapphire/sourcify1.png) - -2. Upload the contracts JSON metadata file. - - ![Sourcify: Upload metadata JSON file](../images/sapphire/sourcify2.png) - - :::tip Store your metadata files - - For production deployments, it is generally a good idea to **archive your - contract metadata JSON file** since it is not only useful for the - verification, but contains a copy of all the source files, produced bytecode, - an ABI, compiler and other relevant contract-related settings that may be - useful in the future. Sourcify will store the metadata file for you and will - even make it available via IPFS, but it is still a good idea to store it - yourself. - - ::: - -3. Sourcify will decode the metadata and prepare a list of included contracts on - the right. Enter the address of the specific contract and select the "Oasis - Sapphire" or "Oasis Sapphire Testnet" chain for the Mainnet or Testnet - accordingly. If your contract assigns any immutable variables in the - constructor, you will also need to correctly fill those out under the "More - Inputs (optional)" panel. Finally, click on the "Verify" button. - - ![Sourcify: Verify contract](../images/sapphire/sourcify3.png) - -4. If everything goes well, you will get a ** *Perfect match* notice and your - contract is now verified**. Congratulations! - -In case of a *Partial match*, the contracts metadata JSON differs from the one -used for deployment although the compiled contract bytecode matched. Make sure -the source code `.sol` file of the contract is the same as the one used during the -deployment (including the comments, variable names and source code file -names) and use the same version of Hardhat and solc compiler. - -You can also explore other verification methods on Sourcify by reading the -[official Sourcify contract verification instructions][sourcify-contract-verify]. - -[Sourcify]: https://sourcify.dev/ -[hardhat-example]: https://github.com/oasisprotocol/sapphire-paratime/tree/main/examples/hardhat -[sourcify-contract-verify]: https://docs.sourcify.dev/docs/how-to-verify/ -[ethereum-contract-verify]: https://ethereum.org/en/developers/docs/smart-contracts/verifying/ - -## Running a Private Oasis Network Locally - -For convenient development and testing of your dApps the Oasis team prepared -the [ghcr.io/oasisprotocol/sapphire-dev][sapphire-dev] Docker image which brings you a -complete Oasis stack to your desktop. The Localnet Sapphire instance **mimics -confidential transactions**, but it does not run in a trusted execution -environment nor does it require Intel's SGX on your computer. The network is -isolated from the Mainnet or Testnet and consists of: - -- single Oasis validator node with 1-second block time and 30-second epoch, -- single Oasis client node, -- three compute nodes running Oasis Sapphire, -- single key manager node, -- PostgreSQL instance, -- Oasis Web3 gateway with transaction indexer and enabled Oasis RPCs, -- helper script which populates initial test accounts for you. - -To run the image, execute: - -```sh -docker run -it -p8545:8545 -p8546:8546 ghcr.io/oasisprotocol/sapphire-dev -``` - -After a while, the tool will show you something like this: - -``` -sapphire-dev 2023-02-28-git84730b2 (oasis-core: 22.2.6, sapphire-paratime: 0.4.0, oasis-web3-gateway: 3.2.0-git84730b2) - -Starting oasis-net-runner with sapphire... -Starting postgresql... -Starting oasis-web3-gateway... -Bootstrapping network and populating account(s) (this might take a minute)... - -Available Accounts -================== -(0) 0x75eCF0d4496C2f10e4e9aF3D4d174576Ee9010E2 (100 ROSE) -(1) 0x903a7dce5a26a3f4DE2d157606c2191740Bc4BC9 (100 ROSE) -(2) 0xF149ad5CBFfD92ba84F5784106f6Cb071A32a1b8 (100 ROSE) -(3) 0x2315F40C1122400Df55483743B051D2997ef0a62 (100 ROSE) -(4) 0xf6FdcacbA93A428A07d27dacEf1fBF25E2C65B0F (100 ROSE) - -Private Keys -================== -(0) 0x160f52faa5c0aecfa26c793424a04d53cbf23dcad5901ce15b50c2e85b9d6ca7 -(1) 0x0ba685723b47d8e744b1b70a9bea9d4d968f60205385ae9de99865174c1af110 -(2) 0xfa990cf0c22af455d2734c879a2a844ff99bd779b400bb0e2919758d1be284b5 -(3) 0x3bf225ef73b1b56b03ceec8bb4dfb4830b662b073b312beb7e7fec3159b1bb4f -(4) 0xad0dd7ceb896fd5f5ddc76d56e54ee6d5c2a3ffeac7714d3ef544d3d6262512c - -HD Wallet -================== -Mnemonic: bench remain brave curve frozen verify dream margin alarm world repair innocent -Base HD Path: m/44'/60'/0'/0/%d - -WARNING: The chain is running in ephemeral mode. State will be lost after restart! - -Listening on http://localhost:8545 and ws://localhost:8546 -``` - -Those familiar with local dApp environments will find the output above similar -to `geth --dev` or `ganache-cli` commands or the `geth-dev-assistant` npm -package. [sapphire-dev] will spin up a private Oasis Network locally, generate -and populate test accounts and make the following Web3 endpoints available for -you to use: -- `http://localhost:8545` -- `ws://localhost:8546` - -:::tip - -If you prefer using the same mnemonics each time (e.g. for testing purposes) -or to populate just a single wallet, use `-to` flag and pass the mnemonics or -the wallet addresses. For example - -```sh -docker run -it -p8545:8545 -p8546:8546 ghcr.io/oasisprotocol/sapphire-dev -to "bench remain brave curve frozen verify dream margin alarm world repair innocent" -docker run -it -p8545:8545 -p8546:8546 ghcr.io/oasisprotocol/sapphire-dev -to "0x75eCF0d4496C2f10e4e9aF3D4d174576Ee9010E2,0xbDA5747bFD65F08deb54cb465eB87D40e51B197E" -``` - -::: - -:::danger - -[sapphire-dev] runs in ephemeral mode. Any smart contract and wallet balance -will be lost after you quit the Docker container! - -::: - -[sapphire-dev]: https://github.com/oasisprotocol/oasis-web3-gateway/pkgs/container/sapphire-dev - -## See also - - - diff --git a/docs/dapp/sapphire/guide.mdx b/docs/dapp/sapphire/guide.mdx new file mode 120000 index 0000000000..872eeaa164 --- /dev/null +++ b/docs/dapp/sapphire/guide.mdx @@ -0,0 +1 @@ +../../../external/sapphire-paratime/docs/guide.mdx \ No newline at end of file diff --git a/docs/dapp/sapphire/images b/docs/dapp/sapphire/images new file mode 120000 index 0000000000..dbac58ba8e --- /dev/null +++ b/docs/dapp/sapphire/images @@ -0,0 +1 @@ +../../../external/sapphire-paratime/docs/images \ No newline at end of file diff --git a/docs/dapp/sapphire/precompiles.md b/docs/dapp/sapphire/precompiles.md deleted file mode 100644 index 1c09d2c27a..0000000000 --- a/docs/dapp/sapphire/precompiles.md +++ /dev/null @@ -1,317 +0,0 @@ ---- -description: Additional Sapphire precompiles for encryption and confidentiality ---- - -# Precompiles - -In addition to the standard EVM precompiles, Sapphire provides a number -of further cryptography-related ones to make some operations easier and -cheaper to perform: x25519 key derivation, Deoxys-II-based encryption -and decryption, signing key generation, message digest signing and -verification. - -These can be called in the same way as other precompiles by dispatching -calls to specific well-known contract addresses, as described below. - -Input parameters should be packed into a contiguous memory region with -each chunk of data padded to 32 bytes as usual. The recommended way to -construct parameter byte sequences in Solidity is with `abi.encode` and -`abi.decode`, which will transparently handle things like putting -`bytes` lengths in the correct position. - -## Library - -While it is possible to call the precompiles directly using Yul or, for -example, `abi.encode` and `abi.decode` in Solidity, we recommend always -using the `contracts/Sapphire.sol` wrapper library for a more comfortable -experience. The examples below are written against it. The library is provided -by the `@oasisprotocol/sapphire-contracts` npm package. - -```shell npm2yarn -npm install -D @oasisprotocol/sapphire-contracts -``` - -Then, you can use the wrapper library inside your `.sol` contract file as -follows: - -```solidity -pragma solidity ^0.8.13; - -import "@oasisprotocol/sapphire-contracts/contracts/Sapphire.sol"; - -contract Test { - constructor() {} - function test() public view returns (bytes32) { - return Sapphire.deriveSymmetricKey("public key as bytes32", "private key as bytes32"); - } -} -``` - -Feel free to discover other convenient libraries for Solidity inside the -`contracts/` folder of the -[Oasis Sapphire repository](https://github.com/oasisprotocol/sapphire-paratime)! - -## Generating Pseudo-Random Bytes - -* Precompile address: `0x0100000000000000000000000000000000000001` -* Parameters: `uint num_bytes, bytes pers` -* Gas cost: 10,000 minimum plus 240 per output word plus 60 per word of - the personalization string. - -Generate `num_bytes` pseudo-random bytes, with an optional personalization -string (`pers`) added into the hashing algorithm to increase domain separation -when needed. - -```solidity -bytes memory randomPad = Sapphire.randomBytes(64, ""); -``` - -### Implementation Details - -:::danger Prior to 0.6.0 -All view queries and simulated transactions (via `eth_call`) would receive the -same entropy in-between blocks if they use the same `num_bytes` and `pers` parameters. -If your contract requires confidentiality you should generate a secret in the constructor -to be used with view calls: - -```solidity -Sapphire.randomBytes(64, abi.encodePacked(msg.sender, this.perContactSecret)); -``` -::: - -The mode (e.g. simulation or 'view call' vs transaction execution) is fed to TupleHash (among other -block-dependent components) to derive the "key id", which is then used to derive a per-block VRF key -from epoch-ephemeral entropy (using KMAC256 and cSHAKE) so a different "key id" will result in a -unique per-block VRF key. This per-block VRF key is then used to create the per-block root RNG which -is then used to derive domain-separated (using Merlin transcripts) per-transaction random RNGs which -are then exposed via this precompile. The KMAC, cSHAKE and TupleHash algorithms are SHA-3 derived functions -defined in [NIST Special Publication 800-185](https://nvlpubs.nist.gov/nistpubs/specialpublications/nist.sp.800-185.pdf). - -## X25519 Key Derivation - -* Precompile address: `0x0100000000000000000000000000000000000002` -* Parameters: `bytes32 public_key, bytes32 private_key` -* Gas cost: 100,000 - -### Example - -```solidity -bytes32 publicKey = ... ; -bytes32 privateKey = ... ; -bytes32 symmetric = Sapphire.deriveSymmetricKey(publicKey, privateKey); -``` - -## Deoxys-II Encryption - -* Encryption precompile address: `0x0100000000000000000000000000000000000003` -* Decryption precompile address: `0x0100000000000000000000000000000000000004` -* Parameters: `bytes32 key, bytes32 nonce, bytes text_or_ciphertext, bytes additional_data` -* Gas cost: 50,000 minimum plus 100 per word of input - -### Example - -```solidity -bytes32 key = ... ; -bytes32 nonce = ... ; -bytes memory text = "plain text"; -bytes memory ad = "additional data"; -bytes memory encrypted = Sapphire.encrypt(key, nonce, text, ad); -bytes memory decrypted = Sapphire.decrypt(key, nonce, encrypted, ad); -``` - -## Signing Keypairs Generation - -* Precompile address: `0x0100000000000000000000000000000000000005` -* Parameters: `uint method, bytes seed` -* Return value: `bytes public_key, bytes private_key` -* Gas cost: method-dependent base cost, see below - -The available methods are items in the `Sapphire.SigningAlg` enum. Note, -however, that the generation method ignores subvariants, so all three -ed25519-based are equivalent, and all secp256k1 & secp256r1 based methods are -equivalent. `Sr25519` is not available and will return an error. - -### Gas Cost -* Ed25519: 1,000 gas - * `0` (`Ed25519Oasis`) - * `1` (`Ed25519Pure`) - * `2` (`Ed25519PrehashedSha512`) -* Secp256k1: 1,500 gas. - * `3` (`Secp256k1Oasis`) - * `4` (`Secp256k1PrehashedKeccak256`) - * `5` (`Secp256k1PrehashedSha256`) -* Secp256r1: 4,000 gas - * `7` (`Secp256r1PrehashedSha256`) - -### Public Key Format - - * Ed25519: 32 bytes - * Secp256k1 & Secp256r1: 33 bytes, compressed format (0x02 or 0x03 prefix, then 32 byte X coordinate) - -### Example - -Using the Sapphire library: - -```solidity -bytes memory seed = hex"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; -bytes memory publicKey; -bytes memory privateKey; -(publicKey, privateKey) = Sapphire.generateSigningKeyPair(Sapphire.SigningAlg.Ed25519Pure, seed); -``` - -## Message Signing - -* Precompile address: `0x0100000000000000000000000000000000000006` -* Parameters: `uint method, bytes private_key, bytes context_or_digest, bytes message` -* Gas cost: see below for the method-dependent base cost, plus 8 gas per 32 bytes of context and message except digest. - -The `context_or_digest` and `messages` parameters change in meaning slightly -depending on the method requested. For methods that take a context in addition -to the message you must pass the context in the `context_or_digest` parameter -and use `message` as expected. For methods that take a pre-existing hash of the -message, pass that in `context_or_digest` and leave `message` empty. -Specifically the `Ed25519Oasis` and `Secp256k1Oasis` variants take both a -context and a message (each are variable length `bytes`), the context serves as -a domain separator. - -### Signing Algorithms - -* `0` (`Ed25519Oasis`) - * 1,500 gas - * variable length context and message -* `1` (`Ed25519Pure`) - * 1,500 gas - * empty context, variable length message -* `2` (`Ed25519PrehashedSha512`) - * 1,500 gas - * pre-existing SHA-512 hash (64 bytes) as context, empty message -* `3` (`Secp256k1Oasis`) - * 3,000 gas - * variable length context and message -* `4` (`Secp256k1PrehashedKeccak256`) - * 3,000 gas - * pre-existing hash (32 bytes) as context, empty message -* `5` (`Secp256k1PrehashedSha256`) - * 3,000 gas - * pre-existing hash (32 bytes) as context, empty message -* `7` (`Secp256r1PrehashedSha256`) - * 9,000 gas - * pre-existing hash (32 bytes) as context, empty message - -### Example - -Using the Sapphire library: - -```solidity -Sapphire.SigningAlg alg = Sapphire.SigningAlg.Ed25519Pure; -bytes memory pk; -bytes memory sk; -(pk, sk) = Sapphire.generateSigningKeyPair(alg, Sapphire.randomBytes(32, "")); -bytes memory signature = Sapphire.sign(alg, sk, "", "signed message"); -``` - -## Signature Verification - -* Precompile address: `0x0100000000000000000000000000000000000007` -* Parameters: `uint method, bytes public_key, bytes context_or_digest, bytes message, bytes signature` - -The `method`, `context_or_digest` and `message` parameters have the same meaning -as described above in the Message Signing section. - -### Gas Cost - -The algorithm-specific base cost below, with an additional 8 gas per 32 bytes of -`context` and `message` for the `Ed25519Oasis`, `Ed25519Pure` and `Secp256k1Oasis` algorithms. - -* Ed25519: 2,000 gas - * `0` (`Ed25519Oasis`) - * `1` (`Ed25519Pure`) - * `2` (`Ed25519PrehashedSha512`) -* Secp256k1: 3,000 gas - * `3` (`Secp256k1Oasis`) - * `4` (`Secp256k1PrehashedKeccak256`) - * `5` (`Secp256k1PrehashedSha256`) -* Secp256r1: 7,900 gas - * `7` (`Secp256r1PrehashedSha256`) - -### Example - -Using the Sapphire library: - -```solidity -Sapphire.SigningAlg alg = Sapphire.SigningAlg.Secp256k1PrehashedKeccak256; -bytes memory pk; -bytes memory sk; -bytes memory digest = abi.encodePacked(keccak256("signed message")); -(pk, sk) = Sapphire.generateSigningKeyPair(alg, Sapphire.randomBytes(32, "")); -bytes memory signature = Sapphire.sign(alg, sk, digest, ""); -require( Sapphire.verify(alg, pk, digest, "", signature) ); -``` - -## SHA-512 - - * Precompile address: `0x0100000000000000000000000000000000000101` - * Parameters: `bytes input_data` - -Hash the input data with SHA-512, according to [NIST.FIPS.180-4] - -:::warning SHA-512 is vulnerable to length-extension attacks - -The SHA-512/256 variant (below) is not vulnerable to [length-extension attacks]. -Length extension attacks are relevant if, among other things, you are computing -the hash of a secret message or computing merkle trees. - -::: - -[length-extension attacks]: https://en.wikipedia.org/wiki/Length_extension_attack - -### Gas Cost - -* 115 gas, then 13 gas per word - -### Example - -```solidity -bytes memory result = sha512(abi.encodePacked("input data")); -``` - - -## SHA-512/256 - - * Precompile address: `0x0100000000000000000000000000000000000102` - * Parameters: `bytes input_data` - -Hash the input data with SHA-512/256, according to [NIST.FIPS.180-4] - -[NIST.FIPS.180-4]: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf - -### Gas Cost - - * 115 gas, then 13 gas per word - -### Example - -```solidity -bytes32 result = sha512_256(abi.encodePacked("input data")); -``` - - -## Subcall - - * Precompile address: `0x0100000000000000000000000000000000000102` - * Parameters: `string method, bytes cborEncodedParams` - -Subcall performs an Oasis SDK call. This allows Sapphire contracts to interact -with the Consensus layer and other modules supported by the SDK. For more -information about the specific modules and their available calls see the Oasis -SDK [source code]. - -### Gas Cost - -Varies per operation, refer to the oasis-sdk [source code]. - -### Example - -TODO: an example - -[source code]: https://github.com/oasisprotocol/oasis-sdk/tree/main/runtime-sdk/src/modules diff --git a/docs/dapp/sapphire/precompiles.md b/docs/dapp/sapphire/precompiles.md new file mode 120000 index 0000000000..9d4de035b3 --- /dev/null +++ b/docs/dapp/sapphire/precompiles.md @@ -0,0 +1 @@ +../../../external/sapphire-paratime/docs/precompiles.md \ No newline at end of file diff --git a/docs/dapp/sapphire/quickstart.mdx b/docs/dapp/sapphire/quickstart.mdx deleted file mode 100644 index 76dd35f4a2..0000000000 --- a/docs/dapp/sapphire/quickstart.mdx +++ /dev/null @@ -1,207 +0,0 @@ -import DocCard from '@theme/DocCard'; -import {findSidebarItem} from '@site/src/sidebarUtils'; - -# Quickstart - -

- -

- -In this tutorial, you will build and deploy a unique dApp that requires -confidentiality to work. By the end of the tutorial, you should feel -comfortable setting up your Eth development environment to target Sapphire, -and know how and when to use confidentiality. - -The expected completion time of this tutorial is 15 minutes. - -:::info Sunsetting Truffle - -Per Consensys [announcement], Oasis will no longer support Truffle as of -2023-10-05 and encourage immediate [migration] to Hardhat. Please see our -repository for the archived Truffle [tutorial] and the deprecated [example]. - -::: - -[announcement]: https://consensys.io/blog/consensys-announces-the-sunset-of-truffle-and-ganache-and-new-hardhat -[migration]: https://trufflesuite.com/docs/truffle/how-to/migrate-to-hardhat/ -[tutorial]: https://github.com/oasisprotocol/docs/blob/2f4a1a3c217b82687ab9440bf051762ae369ed45/docs/dapp/sapphire/quickstart.mdx -[example]: https://github.com/oasisprotocol/sapphire-paratime/tree/3a85e42e6c1cc090c28a521cf7df6353aa8a30c8/examples/truffle - - -## Create a Sapphire-Native dApp - -Porting an existing Eth app is cool, and will provide benefits such as -protection against MEV. -However, starting from scratch with confidentiality in mind can unlock some -really novel dApps and provide a [higher level of security]. - -One simple-but-useful dApp that takes advantage of confidentiality is a -[dead person's switch] that reveals a secret (let's say the encryption key to a -data trove) if the operator fails to re-up before too long. -Let's make it happen! - -[higher level of security]: guide.mdx#writing-secure-dapps -[dead person's switch]: https://en.wikipedia.org/wiki/Dead_man%27s_switch - -### Init a new Hardhat project - -We're going to use Hardhat, but Sapphire should be compatible with your dev -environment of choice. Let us know if things are not as expected! - -1. Make & enter a new directory -2. `npx hardhat@~2.16.0 init` then create a TypeScript project. -3. Add [`@oasisprotocol/sapphire-hardhat`] as dependency: - - ```shell npm2yarn - npm install -D @oasisprotocol/sapphire-hardhat - ``` - -4. Install `@nomicfoundation/hardhat-toolbox`, TypeScript and other peer - dependencies required by HardHat. - -### Add the Sapphire Testnet to Hardhat - -Open up your `hardhat.config.ts` and drop in these lines. - -```diff -diff --git a/hardhat.config.ts b/hardhat.config.ts -index 414e974..49c95f9 100644 ---- a/hardhat.config.ts -+++ b/hardhat.config.ts -@@ -1,8 +1,19 @@ - import { HardhatUserConfig } from "hardhat/config"; -+import '@oasisprotocol/sapphire-hardhat'; - import "@nomicfoundation/hardhat-toolbox"; - - const config: HardhatUserConfig = { - solidity: "0.8.17", -+ networks: { -+ 'sapphire-testnet': { -+ // This is Testnet! If you want Mainnet, add a new network config item. -+ url: "https://testnet.sapphire.oasis.dev", -+ accounts: process.env.PRIVATE_KEY -+ ? [process.env.PRIVATE_KEY] -+ : [], -+ chainId: 0x5aff, -+ }, -+ }, - }; - - export default config; -``` - -By importing `@oasisprotocol/sapphire-hardhat` at the top of the config file, -**any network config entry corresponding to the Sapphire's chain ID will -automatically be wrapped with Sapphire specifics for encrypting and signing the -transactions**. - -### Get some Sapphire Testnet tokens - -Now for the fun part. We need to configure the Sapphire network and get some tokens. -Hit up the one and only [Oasis Testnet faucet] and select "Sapphire". -Submit the form and be on your way. - -[Oasis Testnet faucet]: https://faucet.testnet.oasis.dev - -### Get the Contract - -This is a Sapphire tutorial and you're already a Solidity expert, so let's not -bore you with explaining the gritty details of the contract. -Start by pasting [Vigil.sol] into `contracts/Vigil.sol`. - -While you're there, also place [run-vigil.ts] into `scripts/run-vigil.ts`. -We'll need that later. - -[Vigil.sol]: https://github.com/oasisprotocol/sapphire-paratime/blob/main/examples/hardhat/contracts/Vigil.sol -[run-vigil.ts]: https://github.com/oasisprotocol/sapphire-paratime/blob/main/examples/hardhat/scripts/run-vigil.ts - -#### Vigil.sol, the interesting parts - -The key state variables are: - -```solidity - SecretMetadata[] public _metas; - bytes[] private _secrets; -``` - -* `_metas` is marked with `public` visibility, so despite the state itself being - encrypted and not readable directly, Solidity will generate a getter that will - do the decryption for you. -* `_secrets` is `private` and therefore truly secret; only the contract can - access the data contained in this mapping. - -And the methods we'll care most about are - -* `createSecret`, which adds an entry to both `_metas` and `_secrets`. -* `revealSecret`, which acts as an access-controlled getter for the data - contained with `_secrets`. Due to trusted execution and confidentiality, the - only way that the secret will get revealed is if execution proceeds all the - way to the end of the function and does not revert. - -The rest of the methods are useful if you actually intended to use the contract, -but they demonstrate that developing for Sapphire is essentially the same as for -Ethereum. -You can even write tests against the Hardhat network and use Hardhat plugins. - -### Run the Contract - -And to wrap things up, we'll put `Vigil` through its paces. -First, let's see what's actually going on. - -After deploying the contract, we can create a secret, check that it's not -readable, wait a bit, and then check that it has become readable. -Pretty cool if you ask me! - -Anyway, make it happen by running - -```shell -PRIVATE_KEY="0x..." npx hardhat run scripts/run-vigil.ts --network sapphire-testnet -``` - -And if you see something like the following, you'll know you're well on the road -to deploying confidential dApps on Sapphire. - -``` -Vigil deployed to: 0x74dC4879B152FDD1DDe834E9ba187b3e14f462f1 -Storing a secret in 0x13125d868f5fb3cbc501466df26055ea063a90014b5ccc8dfd5164dc1dd67543 -Checking the secret -failed to fetch secret: reverted: not expired -Waiting... -Checking the secret again -The secret ingredient is brussels sprouts -``` - -## All done! - -Congratulations, you made it through the Sapphire tutorial! If you have any -questions, please check out the [guide] and join the discussion on the -[#sapphire-paratime Discord channel][social-media]. - -Best of luck on your future forays into confidentiality! - -:::info Example - -Visit the Sapphire ParaTime repository to download the [Hardhat][hardhat-example] -example of this quickstart. - -::: - -:::info Example - -If your project involves building a web frontend, we recommend that you check -out the official [Oasis starter] files. - -[Oasis starter]: https://github.com/oasisprotocol/demo-starter - -::: - -## See also - - - - - -[social-media]: ../../get-involved/README.md#social-media-channels -[guide]: guide.mdx -[hardhat-example]: https://github.com/oasisprotocol/sapphire-paratime/tree/main/examples/hardhat -[`@oasisprotocol/sapphire-hardhat`]: https://www.npmjs.com/package/@oasisprotocol/sapphire-hardhat diff --git a/docs/dapp/sapphire/quickstart.mdx b/docs/dapp/sapphire/quickstart.mdx new file mode 120000 index 0000000000..b57943dc32 --- /dev/null +++ b/docs/dapp/sapphire/quickstart.mdx @@ -0,0 +1 @@ +../../../external/sapphire-paratime/docs/quickstart.mdx \ No newline at end of file diff --git a/docs/dapp/sapphire/security.md b/docs/dapp/sapphire/security.md deleted file mode 100644 index e9f869e050..0000000000 --- a/docs/dapp/sapphire/security.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -description: "Secure dApps: Recipes for Confidentiality" ---- - -# Security - -This page is an ongoing work in progress to support confidential smart contract -development. At the moment we address safeguarding storage variable access -patterns and provide best practices for more secure orderings of error checking -to prevent leaking contract state. - -## Storage Access Patterns - -You can use a tool such as [hardhat-tracer] to examine the base EVM state -transitions under the hood. - -```shell npm2yarn -npm install -D hardhat-tracer -``` - -and add `hardhat-tracer` to your `config.ts` file, - -```typescript -import "hardhat-tracer" -``` - -in order to test and show call traces. - -```shell -npx hardhat test --vvv --opcodes SSTORE,SLOAD -``` - -You can also trace a particular transaction, once you know its hash. - -```shell -npx hardhat trace --hash 0xTransactionHash -``` - -For both [gas] usage and confidentiality purposes, we **recommend using -non-unique data size**. E.g. 64-byte value will still be distinct from a -128-byte value. - -:::caution Inference based on access patterns - -`SSTORE` keys from one transaction may be linked to `SLOAD` keys of another -transaction. - -::: - -## Order of Operations - -When handling errors, gas usage patterns not only can reveal the code path -taken, **but sometimes the balance of a user as well** (in the case of a diligent -attacker using binary search). - -```solidity -function transferFrom(address who, address to, uint amount) - external -{ - require( balances[who] >= amount ); - require( allowances[who][msg.sender] >= amount ); - // ... -} -``` - -Modifying the order of error checking can prevent the accidental disclosure of -balance information in the example above. - -```solidity -function transferFrom(address who, address to, uint amount) - external -{ - require( allowances[who][msg.sender] >= amount ); - require( balances[who] >= amount ); - // ... -} -``` - -## Gas Padding - -To prevent leaking information about a particular transaction, Sapphire -provides a [precompile] for dApp developers to **pad the amount of gas used -in a transaction**. - -```solidity -contract GasExample { - bytes32 tmp; - - function constantMath(bool doMath, uint128 padGasAmount) external { - if (doMath) { - bytes32 x; - - for (uint256 i = 0; i < 100; i++) { - x = keccak256(abi.encodePacked(x, tmp)); - } - - tmp = x; - } - - Sapphire.padGas(padGasAmount); - } -} -``` - -Both contract calls below should use the same amount of gas. Sapphire also -provides the precompile to return the gas [used] by the current transaction. - -```typescript -await contract.constantMath(true, 100000); -await contract.constantMath(false, 100000); -``` - -[gas]: https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html -[hardhat-tracer]: https://www.npmjs.com/package/hardhat-tracer -[precompile]: https://api.docs.oasis.io/sol/sapphire-contracts/contracts/Sapphire.sol/library.Sapphire.html#padgas -[used]: https://api.docs.oasis.io/sol/sapphire-contracts/contracts/Sapphire.sol/library.Sapphire.html#gasused diff --git a/docs/dapp/sapphire/security.md b/docs/dapp/sapphire/security.md new file mode 120000 index 0000000000..701ce461be --- /dev/null +++ b/docs/dapp/sapphire/security.md @@ -0,0 +1 @@ +../../../external/sapphire-paratime/docs/security.md \ No newline at end of file diff --git a/external/sapphire-paratime b/external/sapphire-paratime new file mode 160000 index 0000000000..4dced37d7b --- /dev/null +++ b/external/sapphire-paratime @@ -0,0 +1 @@ +Subproject commit 4dced37d7b7db53be1902fddb3fd022cae617593 diff --git a/src/editUrl.js b/src/editUrl.js index 1b15c6c689..2aff35b4c0 100644 --- a/src/editUrl.js +++ b/src/editUrl.js @@ -7,6 +7,7 @@ const gitModules = { 'external/cli/': 'https://github.com/oasisprotocol/cli/{mode}/master/', 'external/oasis-core/': 'https://github.com/oasisprotocol/oasis-core/{mode}/stable/22.2.x/', 'external/oasis-sdk/': 'https://github.com/oasisprotocol/oasis-sdk/{mode}/main/', + 'external/sapphire-paratime/': 'https://github.com/oasisprotocol/sapphire-paratime/{mode}/main/', }; /** diff --git a/src/remark/cross-repo-links.js b/src/remark/cross-repo-links.js index 83a0bd3501..5df0f4520c 100644 --- a/src/remark/cross-repo-links.js +++ b/src/remark/cross-repo-links.js @@ -5,6 +5,7 @@ const oasisSdkContractRegex = /https:\/\/github\.com\/oasisprotocol\/oasis-sdk\/ const oasisSdkRuntimeRegex = /https:\/\/github\.com\/oasisprotocol\/oasis-sdk\/blob\/main\/docs\/runtime\/(.*)\.mdx?(#.*)?/; const oasisCoreRegex = /https:\/\/github\.com\/oasisprotocol\/oasis-core\/blob\/master\/docs\/(.*)\.mdx?(#.*)?/; const adrsRegex = /https:\/\/github\.com\/oasisprotocol\/adrs\/blob\/main\/(.*)\.mdx?(#.*)?/; +const sapphireParatimeRegex = /https:\/\/github\.com\/oasisprotocol\/sapphire-paratime\/blob\/main\/(.*)\.mdx?(#.*)?/; const docsRegex = /https:\/\/github\.com\/oasisprotocol\/docs\/blob\/main\/docs\/(.*)\.mdx?(#.*)?/; const indexReadmeRegex = /(index|README)($|#)/; @@ -33,6 +34,8 @@ module.exports = function (options) { node.url = node.url.replace(oasisCoreRegex, '/core/$1$2'); } else if (adrsRegex.test(node.url)) { node.url = node.url.replace(adrsRegex, '/adrs/$1$2'); + } else if (sapphireParatimeRegex.test(node.url)) { + node.url = node.url.replace(sapphireParatimeRegex, '/sapphire-paratime/$1$2'); } else if (docsRegex.test(node.url)) { node.url = node.url.replace(docsRegex, '/$1$2'); } else {