From 8828e2c057847d4be583f1fcda71476d85a01887 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Matev=C5=BE=20Jekovec?= <matevz@oasisprotocol.org>
Date: Wed, 13 Dec 2023 17:35:36 +0100
Subject: [PATCH 1/2] docs: Migrate Precompiles chapter to inline .sol API docs

---
 contracts/.gitignore                   |   1 +
 contracts/README.md                    |  14 +-
 contracts/contracts/ConsensusUtils.sol |  29 +-
 contracts/contracts/EIP155Signer.sol   |  23 +-
 contracts/contracts/EthereumUtils.sol  |  62 ++--
 contracts/contracts/RLPWriter.sol      |  36 +--
 contracts/contracts/Sapphire.sol       | 395 ++++++++++++++++++++++---
 contracts/contracts/Subcall.sol        | 106 ++++---
 contracts/contracts/opl/Enclave.sol    |   2 +-
 contracts/contracts/opl/Endpoint.sol   |  11 +-
 contracts/contracts/opl/Host.sol       |   2 +-
 docs/precompiles.md                    | 317 --------------------
 12 files changed, 488 insertions(+), 510 deletions(-)
 delete mode 100644 docs/precompiles.md

diff --git a/contracts/.gitignore b/contracts/.gitignore
index 6810a3fe..5db51688 100644
--- a/contracts/.gitignore
+++ b/contracts/.gitignore
@@ -5,3 +5,4 @@ coverage.json
 coverage/
 gasReporterOutput.json
 typechain-types/
+sol/
diff --git a/contracts/README.md b/contracts/README.md
index 58b71064..dbe35223 100644
--- a/contracts/README.md
+++ b/contracts/README.md
@@ -18,10 +18,18 @@ $ pnpm install @oasisprotocol/sapphire-contracts
 
 #### Usage
 
-Once installed, you can import Sapphire contracts.
+Once installed, you can import and use the Sapphire contracts as follows.
 
 ```solidity
-import {Enclave} from "@oasisprotocol/sapphire-contracts/contracts/OPL.sol";
+pragma solidity ^0.8.13;
+
+import "@oasisprotocol/sapphire-contracts/contracts/Sapphire.sol";
+
+contract RandomNumber {
+  function generateNumber() public view returns (uint) {
+    return uint(bytes32(Sapphire.randomBytes(32, "")));
+  }
+}
 ```
 
 ## Documentation
@@ -29,7 +37,7 @@ import {Enclave} from "@oasisprotocol/sapphire-contracts/contracts/OPL.sol";
 See the user's guide for [Sapphire](https://docs.oasis.io/dapp/sapphire/) and
 [OPL](https://docs.oasis.io/dapp/opl/).
 
-API reference is available at [api.docs.oasis.io](https://docs.oasis.io/sol/sapphire-contracts).
+API reference is available at [api.docs.oasis.io](https://api.docs.oasis.io/sol/sapphire-contracts).
 
 ## Contribute
 
diff --git a/contracts/contracts/ConsensusUtils.sol b/contracts/contracts/ConsensusUtils.sol
index c84d7e46..272e1785 100644
--- a/contracts/contracts/ConsensusUtils.sol
+++ b/contracts/contracts/ConsensusUtils.sol
@@ -12,20 +12,23 @@ type StakingSecretKey is bytes32;
 
 /**
  * @title Consensus-level utilities
- * @dev Generate Oasis wallets for use with staking at the consensus level
+ * @notice Generate Oasis wallets for use with staking at the consensus level.
  */
 library ConsensusUtils {
-    /// The unique context for v0 staking account addresses.
-    /// https://github.com/oasisprotocol/oasis-core/blob/master/go/staking/api/address.go#L16
+    /**
+     * @notice The unique context for v0 staking account addresses.
+     * @custom:see @oasisprotocol/oasis-core :: go/staking/api/address.go
+     */
     string private constant ADDRESS_V0_CONTEXT_IDENTIFIER =
         "oasis-core/address: staking";
     uint8 private constant ADDRESS_V0_CONTEXT_VERSION = 0;
 
     /**
-     * @dev Generate a random Ed25519 wallet for Oasis consensus-layer staking
-     * @param personalization Optional user-specified entropy
-     * @return publicAddress Public address of the keypair
-     * @return secretKey Secret key for the keypair
+     * @notice Generate a random Ed25519 wallet for Oasis consensus-layer
+     * staking.
+     * @param personalization Optional user-specified entropy.
+     * @return publicAddress Public address of the keypair.
+     * @return secretKey Secret key for the keypair.
      */
     function generateStakingAddress(bytes memory personalization)
         internal
@@ -47,8 +50,8 @@ library ConsensusUtils {
     }
 
     /**
-     * @dev Derive the staking address from the public key
-     * @param ed25519publicKey Ed25519 public key
+     * @notice Derive the staking address from the public key.
+     * @param ed25519publicKey Ed25519 public key.
      */
     function _stakingAddressFromPublicKey(bytes32 ed25519publicKey)
         internal
@@ -64,10 +67,10 @@ library ConsensusUtils {
     }
 
     /**
-     * @dev Derive an Oasis-style address
-     * @param contextIdentifier Domain separator
-     * @param contextVersion Domain version
-     * @param data Public point of the keypair
+     * @notice Derive an Oasis-style address.
+     * @param contextIdentifier Domain separator.
+     * @param contextVersion Domain version.
+     * @param data Public point of the keypair.
      */
     function _addressFromData(
         string memory contextIdentifier,
diff --git a/contracts/contracts/EIP155Signer.sol b/contracts/contracts/EIP155Signer.sol
index c7be1f0c..33de1cb7 100644
--- a/contracts/contracts/EIP155Signer.sol
+++ b/contracts/contracts/EIP155Signer.sol
@@ -20,9 +20,9 @@ library EIP155Signer {
     }
 
     /**
-     * Encode a signed EIP-155 transaction
-     * @param rawTx Transaction which was signed
-     * @param rsv R, S & V parameters of signature
+     * @notice Encode a signed EIP-155 transaction.
+     * @param rawTx Transaction which was signed.
+     * @param rsv R, S & V parameters of signature.
      */
     function encodeSignedTx(EthTx memory rawTx, SignatureRSV memory rsv)
         internal
@@ -43,10 +43,11 @@ library EIP155Signer {
     }
 
     /**
-     * Sign a raw transaction, which will then need to be encoded to include the signature
-     * @param rawTx Transaction to sign
-     * @param pubkeyAddr Ethereum address of secret key
-     * @param secretKey Secret key used to sign
+     * @notice Sign a raw transaction, which will then need to be encoded to
+     * include the signature.
+     * @param rawTx Transaction to sign.
+     * @param pubkeyAddr Ethereum address of secret key.
+     * @param secretKey Secret key used to sign.
      */
     function signRawTx(
         EthTx memory rawTx,
@@ -64,10 +65,10 @@ library EIP155Signer {
     }
 
     /**
-     * Sign a transaction, returning it in EIP-155 encoded form
-     * @param publicAddress Ethereum address of secret key
-     * @param secretKey Secret key used to sign
-     * @param transaction Transaction to sign
+     * @notice Sign a transaction, returning it in EIP-155 encoded form.
+     * @param publicAddress Ethereum address of secret key.
+     * @param secretKey Secret key used to sign.
+     * @param transaction Transaction to sign.
      */
     function sign(
         address publicAddress,
diff --git a/contracts/contracts/EthereumUtils.sol b/contracts/contracts/EthereumUtils.sol
index 62cdba1d..9c2987fd 100644
--- a/contracts/contracts/EthereumUtils.sol
+++ b/contracts/contracts/EthereumUtils.sol
@@ -46,9 +46,9 @@ library EthereumUtils {
     error k256DeriveY_Invalid_Prefix_Error();
 
     /**
-     * Recover Y coordinate from X coordinate and sign bit
-     * @param prefix 0x02 or 0x03 indicates sign bit of compressed point
-     * @param x X coordinate
+     * @notice Recover Y coordinate from X coordinate and sign bit.
+     * @param prefix 0x02 or 0x03 indicates sign bit of compressed point.
+     * @param x X coordinate.
      */
     function k256DeriveY(uint8 prefix, uint256 x)
         internal
@@ -73,10 +73,10 @@ library EthereumUtils {
     error k256Decompress_Invalid_Length_Error();
 
     /**
-     * Decompress SEC P256 k1 point
-     * @param pk 33 byte compressed public key
-     * @return x coordinate
-     * @return y coordinate
+     * @notice Decompress SEC P256 k1 point.
+     * @param pk 33 byte compressed public key.
+     * @return x X coordinate.
+     * @return y Y coordinate.
      */
     function k256Decompress(bytes memory pk)
         internal
@@ -101,10 +101,10 @@ library EthereumUtils {
     }
 
     /**
-     * Convert SEC P256 k1 curve point to Ethereum address
-     * @param x coordinate
-     * @param y coordinate
-     * @custom:see https://gavwood.com/paper.pdf (212)
+     * @notice Convert SEC P256 k1 curve point to Ethereum address.
+     * @param x X coordinate.
+     * @param y Y coordinate.
+     * @custom:see https://gavwood.com/paper.pdf (pp. 212)
      */
     function toEthereumAddress(uint256 x, uint256 y)
         internal
@@ -119,17 +119,22 @@ library EthereumUtils {
     error DER_Split_Error();
 
     /**
-     * Extracts the `r` and `s` parameters from a DER encoded ECDSA signature.
+     * @notice Extracts the `r` and `s` parameters from a DER encoded ECDSA
+     * signature.
      *
-     * The signature is an ASN1 encoded SEQUENCE of the variable length `r` and `s` INTEGERs.
+     * The signature is an ASN1 encoded SEQUENCE of the variable length `r` and
+     * `s` INTEGERs.
      *
+     * ```
      * | 0x30 | len(z) | 0x02 | len(r) |  r   | 0x02 | len(s) |  s   | = hex value
      * |  1   |   1    |   1  |   1    | 1-33 |  1   |   1    | 1-33 | = byte length
+     * ```
      *
-     * If the highest bit of either `r` or `s` is set, it will be prefix padded with a zero byte
-     * There is exponentially decreasing probability that either `r` or `s` will be below 32 bytes.
-     * There is a very high probability that either `r` or `s` will be 33 bytes.
-     * This function only works if either `r` or `s` are 256bits or lower.
+     * If the highest bit of either `r` or `s` is set, it will be prefix padded
+     * with a zero byte. There is exponentially decreasing probability that
+     * either `r` or `s` will be below 32 bytes. There is a very high
+     * probability that either `r` or `s` will be 33 bytes. This function only
+     * works if either `r` or `s` are 256bits or lower.
      *
      * @param der DER encoded ECDSA signature
      * @return rsv ECDSA R point X coordinate, and S scalar
@@ -214,13 +219,15 @@ library EthereumUtils {
     }
 
     /**
-     * Convert a Secp256k1PrehashedKeccak256 signature to one accepted by ecrecover
-     * @param pubkey 33 byte compressed public key
-     * @param digest 32 byte pre-hashed message digest
-     * @param signature ASN.1 DER encoded signature, as returned from `Sapphire.sign`
-     * @return pubkeyAddr 20 byte Ethereum address
-     * @return rsv Ethereum EcDSA RSV signature values
-     * @custom:see https://gavwood.com/paper.pdf (206)
+     * @notice Convert a Secp256k1PrehashedKeccak256 signature to one accepted
+     * by ecrecover.
+     * @param pubkey 33 byte compressed public key.
+     * @param digest 32 byte pre-hashed message digest.
+     * @param signature ASN.1 DER encoded signature, as returned from
+     * [`Sapphire.sign`](../Sapphire.sol/library.Sapphire.md#sign).
+     * @return pubkeyAddr 20 byte Ethereum address.
+     * @return rsv Ethereum EcDSA RSV signature values.
+     * @custom:see https://gavwood.com/paper.pdf (pp. 206)
      */
     function toEthereumSignature(
         bytes memory pubkey,
@@ -252,9 +259,10 @@ library EthereumUtils {
     }
 
     /**
-     * Generates an Ethereum compatible SEC P256 k1 keypair and corresponding public address
-     * @return pubkeyAddr Ethereum address
-     * @return secretKey Secret key used for signing
+     * @notice Generate an Ethereum compatible SEC P256 k1 keypair and
+     * corresponding public address.
+     * @return pubkeyAddr Ethereum address.
+     * @return secretKey Secret key used for signing.
      */
     function generateKeypair()
         internal
diff --git a/contracts/contracts/RLPWriter.sol b/contracts/contracts/RLPWriter.sol
index 733b09bd..06b67cd7 100644
--- a/contracts/contracts/RLPWriter.sol
+++ b/contracts/contracts/RLPWriter.sol
@@ -2,19 +2,17 @@
 pragma solidity ^0.8.0;
 
 /**
- * @custom:attribution https://github.com/bakaoh/solidity-rlp-encode
- * @custom:attribution https://raw.githubusercontent.com/Maia-DAO/solidity-rlp-encoder/main/src/rlp/RLPWriter.sol
  * @title RLPWriter
  * @author RLPWriter is a library for encoding Solidity types to RLP bytes. Adapted from Bakaoh's
- *         RLPEncode library (https://github.com/bakaoh/solidity-rlp-encode) with minor
- *         modifications to improve legibility.
+ * RLPEncode library (https://github.com/bakaoh/solidity-rlp-encode) with minor
+ * modifications to improve legibility.
+ * @custom:attribution https://github.com/bakaoh/solidity-rlp-encode
+ * @custom:attribution https://raw.githubusercontent.com/Maia-DAO/solidity-rlp-encoder/main/src/rlp/RLPWriter.sol
  */
 library RLPWriter {
     /**
      * @notice RLP encodes a byte string.
-     *
      * @param _in The byte string to encode.
-     *
      * @return The RLP encoded string in bytes.
      */
     function writeBytes(bytes memory _in) internal pure returns (bytes memory) {
@@ -31,9 +29,7 @@ library RLPWriter {
 
     /**
      * @notice RLP encodes a list of RLP encoded byte byte strings.
-     *
      * @param _in The RLP encoded byte strings.
-     *
      * @return The RLP encoded list of items in bytes.
      */
     function writeList(bytes memory _in) internal pure returns (bytes memory) {
@@ -42,9 +38,7 @@ library RLPWriter {
 
     /**
      * @notice RLP encodes a list of RLP encoded byte byte strings.
-     *
      * @param _in The list of RLP encoded byte strings.
-     *
      * @return The RLP encoded list of items in bytes.
      */
     function writeList(bytes[] memory _in)
@@ -58,9 +52,7 @@ library RLPWriter {
 
     /**
      * @notice RLP encodes a string.
-     *
      * @param _in The string to encode.
-     *
      * @return The RLP encoded string in bytes.
      */
     function writeString(string memory _in)
@@ -73,9 +65,7 @@ library RLPWriter {
 
     /**
      * @notice RLP encodes an address.
-     *
      * @param _in The address to encode.
-     *
      * @return The RLP encoded address in bytes.
      */
     function writeAddress(address _in) internal pure returns (bytes memory) {
@@ -84,9 +74,7 @@ library RLPWriter {
 
     /**
      * @notice RLP encodes a uint.
-     *
      * @param _in The uint256 to encode.
-     *
      * @return The RLP encoded uint256 in bytes.
      */
     function writeUint(uint256 _in) internal pure returns (bytes memory) {
@@ -95,9 +83,7 @@ library RLPWriter {
 
     /**
      * @notice RLP encodes a bool.
-     *
      * @param _in The bool to encode.
-     *
      * @return The RLP encoded bool in bytes.
      */
     function writeBool(bool _in) internal pure returns (bytes memory) {
@@ -107,11 +93,10 @@ library RLPWriter {
     }
 
     /**
-     * @notice Encode the first byte and then the `len` in binary form if `length` is more than 55.
-     *
+     * @notice Encode the first byte and then the `len` in binary form if
+     * `length` is more than 55.
      * @param _len    The length of the string or the payload.
      * @param _offset 128 if item is string, 192 if item is list.
-     *
      * @return RLP encoded bytes.
      */
     function _writeLength(uint256 _len, uint256 _offset)
@@ -144,9 +129,7 @@ library RLPWriter {
 
     /**
      * @notice Encode integer in big endian binary form with no leading zeroes.
-     *
      * @param _x The integer to encode.
-     *
      * @return RLP encoded bytes.
      */
     function _toBinary(uint256 _x) private pure returns (bytes memory) {
@@ -168,12 +151,11 @@ library RLPWriter {
     }
 
     /**
-     * @custom:attribution https://github.com/Arachnid/solidity-stringutils
      * @notice Copies a piece of memory to another location.
-     *
      * @param _dest Destination location.
      * @param _src  Source location.
      * @param _len  Length of memory to copy.
+     * @custom:attribution https://github.com/Arachnid/solidity-stringutils
      */
     function _memcpy(
         uint256 _dest,
@@ -204,12 +186,10 @@ library RLPWriter {
     }
 
     /**
-     * @custom:attribution https://github.com/sammayo/solidity-rlp-encoder
      * @notice Flattens a list of byte strings into one byte string.
-     *
      * @param _list List of byte strings to flatten.
-     *
      * @return The flattened byte string.
+     * @custom:attribution https://github.com/sammayo/solidity-rlp-encoder
      */
     function _flatten(bytes[] memory _list)
         private
diff --git a/contracts/contracts/Sapphire.sol b/contracts/contracts/Sapphire.sol
index 785c501a..7cd740f7 100644
--- a/contracts/contracts/Sapphire.sol
+++ b/contracts/contracts/Sapphire.sol
@@ -3,7 +3,26 @@ pragma solidity ^0.8.0;
 
 /**
  * @title Sapphire
- * @dev Convenient wrapper methods for Sapphire's cryptographic primitives.
+ * @notice This library provides a number of convenient wrappers for
+ * cryptographic operations such as the x25519 key derivation, Deoxys-II-based
+ * encryption and decryption, signing key generation, message digest signing and
+ * verification, gas padding and hashing.
+ *
+ * Most of the mentioned functions are implemented as Sapphire's precompiles and
+ * are cheap to call.
+ *
+ * #### Calling Precompiles Manually
+ *
+ * You can override the wrappers and call Sapphire precompiles by dispatching
+ * calls to specific well-known contract addresses, as described below. The
+ * __Precompile address__ section of each function will show you the address
+ * of the corresponding precompile.
+ *
+ * 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 Sapphire {
     // Oasis-specific, confidential precompiles
@@ -40,34 +59,79 @@ library Sapphire {
     type Curve25519SecretKey is bytes32;
 
     enum SigningAlg {
-        // Ed25519 signature over the provided message using SHA-512/265 with a domain separator.
-        // Can be used to sign transactions for the Oasis consensus layer and SDK paratimes.
+        /// Ed25519 signature over the provided message using SHA-512/265 with a domain separator.
+        /// Can be used to sign transactions for the Oasis consensus layer and SDK paratimes.
         Ed25519Oasis,
-        // Ed25519 signature over the provided message.
+        /// Ed25519 signature over the provided message.
         Ed25519Pure,
-        // Ed25519 signature over the provided prehashed SHA-512 digest.
+        /// Ed25519 signature over the provided prehashed SHA-512 digest.
         Ed25519PrehashedSha512,
-        // Secp256k1 signature over the provided message using SHA-512/256 with a domain separator.
-        // Can be used to sign transactions for the Oasis consensus layer and SDK paratimes.
+        /// Secp256k1 signature over the provided message using SHA-512/256 with a domain separator.
+        /// Can be used to sign transactions for the Oasis consensus layer and SDK paratimes.
         Secp256k1Oasis,
-        // Secp256k1 over the provided Keccak256 digest.
-        // Can be used to sign transactions for Ethereum-compatible networks.
+        /// Secp256k1 over the provided Keccak256 digest.
+        /// Can be used to sign transactions for Ethereum-compatible networks.
         Secp256k1PrehashedKeccak256,
-        // Secp256k1 signature over the provided SHA-256 digest.
+        /// Secp256k1 signature over the provided SHA-256 digest.
         Secp256k1PrehashedSha256,
-        // Sr25519 signature over the provided message.
+        /// Sr25519 signature over the provided message.
         Sr25519,
-        // Secp256r1 signature over the provided SHA-256 digest.
+        /// Secp256r1 signature over the provided SHA-256 digest.
         Secp256r1PrehashedSha256,
-        // Secp384r1 signature over the provided SHA-384 digest.
+        /// Secp384r1 signature over the provided SHA-384 digest.
         Secp384r1PrehashedSha384
     }
 
     /**
-     * @dev Returns cryptographically secure random bytes.
+     * @notice Generate `num_bytes` pseudo-random bytes, with an optional
+     * personalization string (`pers`) added into the hashing algorithm to
+     * increase domain separation when needed.
+     *
+     * #### Precompile address
+     *
+     * `0x0100000000000000000000000000000000000001`
+     *
+     * #### Gas cost
+     *
+     * 10,000 minimum plus 240 per output word plus 60 per word of the
+     * personalization string.
+     *
+     * #### Implementation details
+     *
+     * 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).
+     *
+     * #### DANGER: Prior to Sapphire ParaTime 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));
+     * ```
+     *
+     * #### Example
+     *
+     * ```solidity
+     * bytes memory randomPad = Sapphire.randomBytes(64, "");
+     * ```
+     *
      * @param numBytes The number of bytes to return.
-     * @param pers An optional personalization string to increase domain separation.
-     * @return The random bytes. If the number of bytes requested is too large (over 1024), a smaller amount (1024) will be returned.
+     * @param pers An optional personalization string to increase domain
+     *        separation.
+     * @return The random bytes. If the number of bytes requested is too large
+     *         (over 1024), a smaller amount (1024) will be returned.
      */
     function randomBytes(uint256 numBytes, bytes memory pers)
         internal
@@ -82,10 +146,12 @@ library Sapphire {
     }
 
     /**
-     * @dev Generates a Curve25519 keypair.
-     * @param pers An optional personalization string used to add domain separation.
+     * @notice Generates a Curve25519 keypair.
+     * @param pers An optional personalization string used to add domain
+     * separation.
      * @return pk The Curve25519 public key. Useful for key exchange.
-     * @return sk The Curve25519 secret key. Pairs well with {`deriveSymmetricKey`}.
+     * @return sk The Curve25519 secret key. Pairs well with
+     * [deriveSymmetricKey](#derivesymmetrickey).
      */
     function generateCurve25519KeyPair(bytes memory pers)
         internal
@@ -108,7 +174,24 @@ library Sapphire {
     }
 
     /**
-     * @dev Derive a symmetric key from a pair of keys using x25519.
+     * @notice Derive a symmetric key from a pair of keys using x25519.
+     *
+     * #### Precompile address
+     *
+     * `0x0100000000000000000000000000000000000002`
+     *
+     * #### Gas cost
+     *
+     * 100,000
+     *
+     * #### Example
+     *
+     * ```solidity
+     * bytes32 publicKey = ... ;
+     * bytes32 privateKey = ... ;
+     * bytes32 symmetric = Sapphire.deriveSymmetricKey(publicKey, privateKey);
+     * ```
+     *
      * @param peerPublicKey The peer's public key.
      * @param secretKey Your secret key.
      * @return A derived symmetric key.
@@ -125,9 +208,31 @@ library Sapphire {
     }
 
     /**
-     * @dev Encrypt and authenticate the plaintext and additional data using DeoxysII.
+     * @notice Encrypt and authenticate the plaintext and additional data using
+     * DeoxysII.
+     *
+     * #### Precompile address
+     *
+     * `0x0100000000000000000000000000000000000003`
+     *
+     * #### 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);
+     * ```
+     *
      * @param key The key to use for encryption.
-     * @param nonce The nonce. Note that only the first 15 bytes of this parameter are used.
+     * @param nonce The nonce. Note that only the first 15 bytes of this
+     * parameter are used.
      * @param plaintext The plaintext to encrypt and authenticate.
      * @param additionalData The additional data to authenticate.
      * @return The ciphertext with appended auth tag.
@@ -146,11 +251,34 @@ library Sapphire {
     }
 
     /**
-     * @dev Decrypt and authenticate the ciphertext and additional data using DeoxysII. Reverts if the auth tag is incorrect.
+     * @notice Decrypt and authenticate the ciphertext and additional data using
+     * DeoxysII. Reverts if the auth tag is incorrect.
+     *
+     * #### Precompile address
+     *
+     * `0x0100000000000000000000000000000000000004`
+     *
+     * #### 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);
+     * ```
+     *
      * @param key The key to use for decryption.
-     * @param nonce The nonce. Note that only the first 15 bytes of this parameter are used.
+     * @param nonce The nonce. Note that only the first 15 bytes of this
+     * parameter are used.
      * @param ciphertext The ciphertext with tag to decrypt and authenticate.
-     * @param additionalData The additional data to authenticate against the ciphertext.
+     * @param additionalData The additional data to authenticate against the
+     * ciphertext.
      * @return The original plaintext.
      */
     function decrypt(
@@ -167,9 +295,55 @@ library Sapphire {
     }
 
     /**
-     * @dev Generate a public/private key pair using the specified method and seed.
+     * @notice Generate a public/private key pair using the specified method and
+     * seed. The available methods are items in the
+     * [`Sapphire.SigningAlg`](#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.
+     *
+     * #### Precompile address
+     * `0x0100000000000000000000000000000000000005`
+     *
+     * #### 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
+     *
+     * ```solidity
+     * bytes memory seed = hex"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
+     * bytes memory publicKey;
+     * bytes memory privateKey;
+     * (publicKey, privateKey) = Sapphire.generateSigningKeyPair(Sapphire.SigningAlg.Ed25519Pure, seed);
+     * ```
+     *
      * @param alg The signing alg for which to generate a keypair.
-     * @param seed The seed to use for generating the key pair. You can use the `randomBytes` method if you don't already have a seed.
+     * @param seed The seed to use for generating the key pair. You can use the
+     * `randomBytes` method if you don't already have a seed.
      * @return publicKey The public half of the keypair.
      * @return secretKey The secret half of the keypair.
      */
@@ -185,11 +359,56 @@ library Sapphire {
     }
 
     /**
-     * @dev Sign a message within the provided context using the specified algorithm, and return the signature.
+     * @notice Sign a message within the provided context using the specified
+     * algorithm, and return the signature. 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.
+     *
+     * #### Precompile address
+     *
+     * `0x0100000000000000000000000000000000000006`
+     *
+     * #### Gas cost
+     *
+     * See below for the method-dependent base cost, plus 8 gas per 32 bytes of
+     * context and message except digest.
+     *
+     * #### 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
+     *
+     * ```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");
+     * ```
+     *
      * @param alg The signing algorithm to use.
-     * @param secretKey The secret key to use for signing. The key must be valid for use with the requested algorithm.
-     * @param contextOrHash Domain-Separator Context, or precomputed hash bytes
-     * @param message Message to sign, should be zero-length if precomputed hash given
+     * @param secretKey The secret key to use for signing. The key must be valid
+     * for use with the requested algorithm.
+     * @param contextOrHash Domain-Separator Context, or precomputed hash bytes.
+     * @param message Message to sign, should be zero-length if precomputed hash
+     * given.
      * @return signature The resulting signature.
      * @custom:see @oasisprotocol/oasis-sdk :: precompile/confidential.rs :: call_sign
      */
@@ -207,11 +426,48 @@ library Sapphire {
     }
 
     /**
-     * @dev Verifies that the provided digest was signed with using the secret key corresponding to the provided private key and the specified signing algorithm.
+     * @notice Verifies that the provided digest was signed with using the
+     * secret key corresponding to the provided private key and the specified
+     * signing algorithm.
+     *
+     * The `method`, `context_or_digest` and `message` parameters have the same
+     * meaning as described above in the [sign()](#sign) function.
+     *
+     * #### Precompile address
+     *
+     * `0x0100000000000000000000000000000000000007`
+     *
+     * #### 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.
+     *
+     * - `0` (`Ed25519Oasis`): 2,000 gas
+     * - `1` (`Ed25519Pure`): 2,000 gas
+     * - `2` (`Ed25519PrehashedSha512`): 2,000 gas
+     * - `3` (`Secp256k1Oasis`): 3,000 gas
+     * - `4` (`Secp256k1PrehashedKeccak256`): 3,000 gas
+     * - `5` (`Secp256k1PrehashedSha256`): 3,000 gas
+     * - `7` (`Secp256r1PrehashedSha256`): 7,900 gas
+     *
+     * #### Example
+     *
+     * ```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) );
+     * ```
+     *
      * @param alg The signing algorithm by which the signature was generated.
      * @param publicKey The public key against which to check the signature.
      * @param contextOrHash Domain-Separator Context, or precomputed hash bytes
-     * @param message The hash of the message that was signed, should be zero-length if precomputed hash was given
+     * @param message The hash of the message that was signed, should be
+     * zero-length if precomputed hash was given.
      * @param signature The signature to check.
      * @return verified Whether the signature is valid for the given parameters.
      * @custom:see @oasisprotocol/oasis-sdk :: precompile/confidential.rs :: call_verify
@@ -231,11 +487,11 @@ library Sapphire {
     }
 
     /**
-     * @dev Set the current transactions gas usage to a specific amount
+     * @notice Set the current transactions gas usage to a specific amount
+     * @dev Will cause a reversion if the current usage is more than the amount.
      * @param toAmount Gas usage will be set to this amount
      * @custom:see @oasisprotocol/oasis-sdk :: precompile/gas.rs :: call_pad_gas
      *
-     * Will cause a reversion if the current usage is more than the amount
      */
     function padGas(uint128 toAmount) internal view {
         (bool success, ) = PAD_GAS.staticcall(abi.encode(toAmount));
@@ -243,7 +499,7 @@ library Sapphire {
     }
 
     /**
-     * @dev Returns the amount of gas currently used by the transaction
+     * @notice Returns the amount of gas currently used by the transaction
      * @custom:see @oasisprotocol/oasis-sdk :: precompile/gas.rs :: call_gas_used
      */
     function gasUsed() internal view returns (uint64) {
@@ -254,16 +510,35 @@ library Sapphire {
 }
 
 /**
- * Hash the input data with SHA-512/256
+ * @notice Hash the input data with SHA-512/256, according to
+ * [NIST.FIPS.180-4](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf).
+ *
+ * #### Precompile address
+ *
+ * `0x0100000000000000000000000000000000000102`
+ *
+ * #### Gas cost
+ *
+ * 115 gas, then 13 gas per word
+ *
+ * #### Example
+ *
+ * ```solidity
+ * bytes32 result = sha512_256(abi.encodePacked("input data"));
+ * ```
  *
- * SHA-512 is vulnerable to length-extension attacks, which are relevant if you
- * are computing the hash of a secret message. The SHA-512/256 variant is
- * **not** vulnerable to length-extension attacks.
+ * #### Warning: SHA-512 vs SHA-512/256 Length-Extension Attacks
  *
+ * [SHA-512](function.sha512.md#sha512) is vulnerable to [length-extension
+ * attacks](https://en.wikipedia.org/wiki/Length_extension_attack), which are
+ * relevant if you are computing the hash of a secret message. The
+ * [SHA-512/256](function.sha512_256.md#sha512_256) variant is **not**
+ * vulnerable to length-extension attacks.
+ *
+ * @param input Bytes to hash.
+ * @return result 32 byte digest.
  * @custom:standard https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf
  * @custom:see @oasisprotocol/oasis-sdk :: precompile/sha2.rs :: call_sha512_256
- * @param input Bytes to hash
- * @return result 32 byte digest
  */
 function sha512_256(bytes memory input) view returns (bytes32 result) {
     (bool success, bytes memory output) = Sapphire.SHA512_256.staticcall(input);
@@ -274,12 +549,35 @@ function sha512_256(bytes memory input) view returns (bytes32 result) {
 }
 
 /**
- * Hash the input data with SHA-512
+ * @notice Hash the input data with SHA-512, according to
+ * [NIST.FIPS.180-4](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf)
+ *
+ * #### Precompile address
+ *
+ * `0x0100000000000000000000000000000000000101`
+ *
+ * #### Warning: SHA-512 vs SHA-512/256 Length-Extension Attacks
+ *
+ * [SHA-512](function.sha512.md#sha512) is vulnerable to [length-extension
+ * attacks](https://en.wikipedia.org/wiki/Length_extension_attack), which are
+ * relevant if you are computing the hash of a secret message. The
+ * [SHA-512/256](function.sha512_256.md#sha512_256) variant is **not**
+ * vulnerable to length-extension attacks.
  *
+ * #### Gas Cost
+ *
+ * 115 gas, then 13 gas per word
+ *
+ * #### Example
+ *
+ * ```solidity
+ * bytes memory result = sha512(abi.encodePacked("input data"));
+ * ```
+ *
+ * @param input Bytes to hash.
+ * @return output 64 byte digest.
  * @custom:standard https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf
  * @custom:see @oasisprotocol/oasis-sdk :: precompile/sha2.rs :: call_sha512
- * @param input Bytes to hash
- * @return output 64 byte digest
  */
 function sha512(bytes memory input) view returns (bytes memory output) {
     bool success;
@@ -290,12 +588,11 @@ function sha512(bytes memory input) view returns (bytes memory output) {
 }
 
 /**
- * Hash the input data with SHA-384
- *
+ * @notice Hash the input data with SHA-384.
+ * @param input Bytes to hash.
+ * @return output 48 byte digest.
  * @custom:standard https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf
  * @custom:see @oasisprotocol/oasis-sdk :: precompile/sha2.rs :: call_sha384
- * @param input Bytes to hash
- * @return output 48 byte digest
  */
 function sha384(bytes memory input) view returns (bytes memory output) {
     bool success;
diff --git a/contracts/contracts/Subcall.sol b/contracts/contracts/Subcall.sol
index 45d3ac13..a87f2a77 100644
--- a/contracts/contracts/Subcall.sol
+++ b/contracts/contracts/Subcall.sol
@@ -13,7 +13,7 @@ enum SubcallReceiptKind {
 
 /**
  * @title SDK Subcall wrappers
- * @dev Interact with Oasis Runtime SDK modules from Sapphire.
+ * @notice Interact with Oasis Runtime SDK modules from Sapphire.
  */
 library Subcall {
     string private constant CONSENSUS_DELEGATE = "consensus.Delegate";
@@ -64,14 +64,12 @@ library Subcall {
     error MissingKey();
 
     /**
-     * Submit a native message to the Oasis runtime layer.
-     *
-     * Messages which re-enter the EVM module are forbidden: evm.*
-     *
-     * @param method Native message type
-     * @param body CBOR encoded body
-     * @return status Result of call
-     * @return data CBOR encoded result
+     * @notice Submit a native message to the Oasis runtime layer. Messages
+     * which re-enter the EVM module are forbidden: `evm.*`.
+     * @param method Native message type.
+     * @param body CBOR encoded body.
+     * @return status Result of call.
+     * @return data CBOR encoded result.
      */
     function subcall(string memory method, bytes memory body)
         internal
@@ -89,12 +87,12 @@ library Subcall {
     }
 
     /**
-     * @dev Generic method to call `{to:address, amount:uint128}`
-     * @param method Runtime SDK method name ('module.Action')
-     * @param to Destination address
-     * @param value Amount specified
-     * @return status Non-zero on error
-     * @return data Module name on error
+     * @notice Generic method to call `{to:address, amount:uint128}`.
+     * @param method Runtime SDK method name ('module.Action').
+     * @param to Destination address.
+     * @param value Amount specified.
+     * @return status Non-zero on error.
+     * @return data Module name on error.
      */
     function _subcallWithToAndAmount(
         string memory method,
@@ -123,20 +121,22 @@ library Subcall {
     }
 
     /**
-     * Returns a CBOR encoded structure, containing the following possible keys.
-     * All keys are optional:
+     * @notice Returns a CBOR encoded structure, containing the following
+     * possible keys. All keys are optional:
+     *
+     *  - shares: `u128`
+     *  - epoch: `EpochTime`
+     *  - receipt: `u64`
+     *  - amount: `u128`
+     *  - error: `{module: string, code: u32}`
      *
-     *  shares: u128
-     *  epoch: EpochTime
-     *  receipt: u64
-     *  amount: u128
-     *  error: {module: string, code: u32}
+     * #### Keys returned by specific subcalls
      *
-     * Delegate will have the `error` or `shares` keys.
-     * UndelegateStart will have the `epoch` and `receipt` keys.
-     * UndelegateDone will have the `amount` key
+     * - `Delegate` will have the `error` or `shares` keys.
+     * - `UndelegateStart` will have the `epoch` and `receipt` keys.
+     * - `UndelegateDone` will have the `amount` key.
      *
-     * @param kind 1=Delegate, 2=UndelegateStart, 3=UndelegateDone
+     * @param kind `1` (`Delegate`), `2` (`UndelegateStart`) or `3` (`UndelegateDone`)
      * @param receiptId ID of receipt
      */
     function consensusTakeReceipt(SubcallReceiptKind kind, uint64 receiptId)
@@ -306,9 +306,9 @@ library Subcall {
     }
 
     /**
-     * Decodes a 'Delegate' receipt
-     * @param receiptId Previously unretrieved receipt
-     * @param result CBOR encoded {shares: u128}
+     * @notice Decodes a 'Delegate' receipt.
+     * @param receiptId Previously unretrieved receipt.
+     * @param result CBOR encoded {shares: u128}.
      */
     function _decodeReceiptDelegate(uint64 receiptId, bytes memory result)
         internal
@@ -370,11 +370,10 @@ library Subcall {
     }
 
     /**
-     * Start the undelegation process of the given number of shares from
+     * @notice Start the undelegation process of the given number of shares from
      * consensus staking account to runtime account.
-     *
-     * @param from Consensus address which shares were delegated to
-     * @param shares Number of shares to withdraw back to us
+     * @param from Consensus address which shares were delegated to.
+     * @param shares Number of shares to withdraw back to us.
      */
     function consensusUndelegate(StakingAddress from, uint128 shares) internal {
         (uint64 status, bytes memory data) = subcall(
@@ -435,10 +434,9 @@ library Subcall {
     }
 
     /**
-     * Delegate native token to consensus level.
-     *
-     * @param to Consensus address shares are delegated to
-     * @param amount Native token amount (in wei)
+     * @notice Delegate native token to consensus level.
+     * @param to Consensus address shares are delegated to.
+     * @param amount Native token amount (in wei).
      */
     function consensusDelegate(StakingAddress to, uint128 amount)
         internal
@@ -459,15 +457,13 @@ library Subcall {
     }
 
     /**
-     * Delegate native token to consensus level.
-     *
-     * Requests that the number of shares allocated can be retrieved with a
-     * receipt. The receipt will be of `ReceiptKind.DelegateDone` and can be
-     * decoded using `decodeReceiptDelegateDone`
-     *
-     * @param to Consensus address shares are delegated to
-     * @param amount Native token amount (in wei)
-     * @param receiptId contract-specific receipt to retrieve result
+     * @notice Delegate native token to consensus level. Requests that the
+     * number of shares allocated can be retrieved with a receipt. The receipt
+     * will be of `ReceiptKind.DelegateDone` and can be decoded using
+     * `decodeReceiptDelegateDone`.
+     * @param to Consensus address shares are delegated to.
+     * @param amount Native token amount (in wei).
+     * @param receiptId contract-specific receipt to retrieve result.
      */
     function consensusDelegate(
         StakingAddress to,
@@ -510,10 +506,10 @@ library Subcall {
     }
 
     /**
-     * Transfer from an account in this runtime to a consensus staking account.
-     *
-     * @param to Consensus address which gets the tokens
-     * @param value Token amount (in wei)
+     * @notice Transfer from an account in this runtime to a consensus staking
+     * account.
+     * @param to Consensus address which gets the tokens.
+     * @param value Token amount (in wei).
      */
     function consensusWithdraw(StakingAddress to, uint128 value) internal {
         (uint64 status, bytes memory data) = _subcallWithToAndAmount(
@@ -529,12 +525,10 @@ library Subcall {
     }
 
     /**
-     * Perform a transfer to another account.
-     *
-     * This is equivalent of `payable(to).transfer(value);`
-     *
-     * @param to Destination account
-     * @param value native token amount (in wei)
+     * @notice Perform a transfer to another account. This is equivalent of
+     * `payable(to).transfer(value);`.
+     * @param to Destination account.
+     * @param value native token amount (in wei).
      */
     function accountsTransfer(address to, uint128 value) internal {
         (uint64 status, bytes memory data) = _subcallWithToAndAmount(
diff --git a/contracts/contracts/opl/Enclave.sol b/contracts/contracts/opl/Enclave.sol
index c6913ff7..106f03e4 100644
--- a/contracts/contracts/opl/Enclave.sol
+++ b/contracts/contracts/opl/Enclave.sol
@@ -5,7 +5,7 @@ import {Endpoint} from "./Endpoint.sol";
 
 /**
  * @title OPL Enclave
- * @dev The Sapphire-side of an OPL dapp.
+ * @notice The Sapphire-side of an OPL dApp.
  */
 contract Enclave is Endpoint {
     constructor(address _host, bytes32 _hostChain)
diff --git a/contracts/contracts/opl/Endpoint.sol b/contracts/contracts/opl/Endpoint.sol
index 9be0f744..e5ae6a91 100644
--- a/contracts/contracts/opl/Endpoint.sol
+++ b/contracts/contracts/opl/Endpoint.sol
@@ -160,7 +160,7 @@ contract BaseEndpoint is Context {
 
 /**
  * @title OPL Endpoint
- * @dev An app that sends or receives using OPL.
+ * @notice An app that sends or receives using OPL.
  */
 contract Endpoint is BaseEndpoint {
     constructor(address _remote, bytes32 _remoteChainName)
@@ -176,9 +176,12 @@ contract Endpoint is BaseEndpoint {
 /* solhint-disable func-visibility */
 
 /**
- * @dev Autoswitch automatically picks the remote network based on the network the contract on which the contract has already been deployed.
- * @dev When on testnet, the remote chain will be the testnet version of the provided chain.
- * @dev When running locally, the remote chain will be this one and the contracts will call each other without going through a message bus. This is helpful for debugging logic but does not test gas fee payment and other moving parts.
+ * @notice Autoswitch automatically picks the remote network based on the
+ * network the contract on which the contract has already been deployed. When on
+ * testnet, the remote chain will be the testnet version of the provided chain.
+ * When running locally, the remote chain will be this one and the contracts
+ * will call each other without going through a message bus. This is helpful for
+ * debugging logic but does not test gas fee payment and other moving parts.
  */
 function autoswitch(bytes32 protocol) view returns (bytes32 networkName) {
     if (block.chainid == 1337 || block.chainid == 31337) return "local";
diff --git a/contracts/contracts/opl/Host.sol b/contracts/contracts/opl/Host.sol
index b333c2e4..0740a948 100644
--- a/contracts/contracts/opl/Host.sol
+++ b/contracts/contracts/opl/Host.sol
@@ -5,7 +5,7 @@ import {Endpoint, autoswitch} from "./Endpoint.sol";
 
 /**
  * @title OPL Host
- * @dev The L1-side of an OPL dapp.
+ * @notice The L1-side of an OPL dApp.
  */
 contract Host is Endpoint {
     // solhint-disable-next-line no-empty-blocks
diff --git a/docs/precompiles.md b/docs/precompiles.md
deleted file mode 100644
index 1c09d2c2..00000000
--- a/docs/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

From a679e050c588f6e29073c400b4ead279f2d7141f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Matev=C5=BE=20Jekovec?= <matevz@oasisprotocol.org>
Date: Mon, 22 Jan 2024 17:04:50 +0100
Subject: [PATCH 2/2] docs: Add pagetoc to API docs

---
 .github/workflows/ci-docs.yaml        | 10 ++---
 .github/workflows/contracts-test.yaml |  5 ++-
 contracts/README.md                   | 16 +++++++-
 contracts/book.toml                   |  4 ++
 contracts/package.json                |  1 +
 contracts/post-build-doc.sh           | 12 ++++++
 contracts/theme/pagetoc.css           | 58 +++++++++++++++++++++++++++
 7 files changed, 96 insertions(+), 10 deletions(-)
 create mode 100644 contracts/book.toml
 create mode 100755 contracts/post-build-doc.sh
 create mode 100644 contracts/theme/pagetoc.css

diff --git a/.github/workflows/ci-docs.yaml b/.github/workflows/ci-docs.yaml
index b2703ccf..c868748a 100644
--- a/.github/workflows/ci-docs.yaml
+++ b/.github/workflows/ci-docs.yaml
@@ -81,13 +81,11 @@ jobs:
       - name: Install Foundry
         uses: foundry-rs/foundry-toolchain@v1
 
+      - name: Install forge doc deps
+        run: cargo install mdbook-pagetoc
+
       - name: Build docs
-        run: |
-          forge doc --build
-          # Inject another /contracts/ for github.com URLs.
-          find sol/sapphire-contracts/book -name *.html | xargs sed -i -E "s+(blob/.*/contracts)+\1/contracts+"
-          # Remove /src/ from "Inherits" links.
-          find sol/sapphire-contracts/book -name *.html | xargs sed -i "s+/src/+/+"
+        run: pnpm doc
 
       - name: Deploy to api-reference branch
         uses: peaceiris/actions-gh-pages@v3
diff --git a/.github/workflows/contracts-test.yaml b/.github/workflows/contracts-test.yaml
index 3ed3b0a9..913ad08b 100644
--- a/.github/workflows/contracts-test.yaml
+++ b/.github/workflows/contracts-test.yaml
@@ -56,6 +56,8 @@ jobs:
             ${{ runner.os }}-pnpm-store-
       - name: Install Foundry
         uses: foundry-rs/foundry-toolchain@v1
+      - name: Install forge doc deps
+        run: cargo install mdbook-pagetoc
       - name: Install dependencies
         run: pnpm install
       - name: Build JS client
@@ -69,8 +71,7 @@ jobs:
         run: pnpm hardhat test --network sapphire-localnet-ci
       - name: Build docs
         working-directory: contracts
-        run: |
-          forge doc --build
+        run: pnpm doc
       - name: hardhat test examples/hardhat
         working-directory: examples/hardhat
         run: pnpm hardhat run --network sapphire-localnet scripts/run-vigil.ts
diff --git a/contracts/README.md b/contracts/README.md
index dbe35223..1a55d8c8 100644
--- a/contracts/README.md
+++ b/contracts/README.md
@@ -18,7 +18,7 @@ $ pnpm install @oasisprotocol/sapphire-contracts
 
 #### Usage
 
-Once installed, you can import and use the Sapphire contracts as follows.
+Once installed, you can import and use the Sapphire contracts as follows:
 
 ```solidity
 pragma solidity ^0.8.13;
@@ -37,7 +37,19 @@ contract RandomNumber {
 See the user's guide for [Sapphire](https://docs.oasis.io/dapp/sapphire/) and
 [OPL](https://docs.oasis.io/dapp/opl/).
 
-API reference is available at [api.docs.oasis.io](https://api.docs.oasis.io/sol/sapphire-contracts).
+The generated API reference is hosted at
+[api.docs.oasis.io](https://api.docs.oasis.io/sol/sapphire-contracts).
+
+Generating API docs locally requires Foundry and mdbook-pagetoc. To install
+them and generate the docs execute:
+
+```shell
+curl -L https://foundry.paradigm.xyz | bash
+cargo install mdbook-pagetoc
+pnpm doc
+```
+
+The API docs index will be located in `sol/sapphire-contracts/book/index.html`.
 
 ## Contribute
 
diff --git a/contracts/book.toml b/contracts/book.toml
new file mode 100644
index 00000000..5bd5fd28
--- /dev/null
+++ b/contracts/book.toml
@@ -0,0 +1,4 @@
+[preprocessor.pagetoc]
+[output.html]
+additional-css = ["theme/pagetoc.css"]
+additional-js  = ["theme/pagetoc.js"]
diff --git a/contracts/package.json b/contracts/package.json
index 1fa432a0..b895bb21 100644
--- a/contracts/package.json
+++ b/contracts/package.json
@@ -9,6 +9,7 @@
     "url": "https://github.com/oasisprotocol/sapphire-paratime.git"
   },
   "scripts": {
+    "doc": "forge doc --build && ./post-build-doc.sh",
     "lint:eslint": "eslint --ignore-path .gitignore --ext .ts",
     "lint:solhint": "solhint 'contracts/**/*.sol'",
     "lint::prettier": "prettier --cache --check --plugin-search-dir=. --cache '*.json' '**/*.ts' '**/*.sol'",
diff --git a/contracts/post-build-doc.sh b/contracts/post-build-doc.sh
new file mode 100755
index 00000000..944f9beb
--- /dev/null
+++ b/contracts/post-build-doc.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+# This script is to be executed by `pnpm doc` after building the docs.
+
+# Inject another /contracts/ for github.com URLs.
+find sol/sapphire-contracts/book -name *.html | xargs sed -i -E "s+(blob/.*/contracts)+\1/contracts+"
+
+# Remove /src/ from "Inherits" links.
+find sol/sapphire-contracts/book -name *.html | xargs sed -i "s+/src/+/+"
+
+# Inject nicer Pagetoc theme (hides level-4 headings, smaller font, wider toc).
+cp theme/* sol/sapphire-contracts/book/theme
diff --git a/contracts/theme/pagetoc.css b/contracts/theme/pagetoc.css
new file mode 100644
index 00000000..fc3d8d31
--- /dev/null
+++ b/contracts/theme/pagetoc.css
@@ -0,0 +1,58 @@
+@media only screen and (max-width:1439px) {
+    .sidetoc {
+        display: none;
+    }
+}
+
+@media only screen and (min-width:1440px) {
+    main {
+        position: relative;
+    }
+    .sidetoc {
+        margin-left: auto;
+        margin-right: auto;
+        left: calc(100% + (var(--content-max-width))/4 - 140px);
+        position: absolute;
+    }
+    .pagetoc {
+	font-size: 12px;
+        position: fixed;
+        width: 220px;
+        height: calc(100vh - var(--menu-bar-height) - 0.67em * 4);
+        overflow: auto;
+    }
+    .pagetoc a {
+        border-left: 1px solid var(--sidebar-bg);
+        color: var(--fg) !important;
+        display: block;
+        padding-bottom: 5px;
+        padding-top: 5px;
+        padding-left: 10px;
+        text-align: left;
+        text-decoration: none;
+    }
+    .pagetoc a:hover,
+    .pagetoc a.active {
+        background: var(--sidebar-bg);
+        color: var(--sidebar-fg) !important;
+    }
+    .pagetoc .active {
+        background: var(--sidebar-bg);
+        color: var(--sidebar-fg);
+    }
+    .pagetoc .pagetoc-H2 {
+        padding-left: 20px;
+    }
+    .pagetoc .pagetoc-H3 {
+        padding-left: 40px;
+    }
+    .pagetoc .pagetoc-H4 {
+	display: none;
+    }
+    .pagetoc .pagetoc-H5 {
+        display: none;
+    }
+    .pagetoc .pagetoc-H6 {
+        display: none;
+    }
+}