Sapphire

Git Source

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.

State Variables

RANDOM_BYTES

address internal constant RANDOM_BYTES = 0x0100000000000000000000000000000000000001;

DERIVE_KEY

address internal constant DERIVE_KEY = 0x0100000000000000000000000000000000000002;

ENCRYPT

address internal constant ENCRYPT = 0x0100000000000000000000000000000000000003;

DECRYPT

address internal constant DECRYPT = 0x0100000000000000000000000000000000000004;

GENERATE_SIGNING_KEYPAIR

address internal constant GENERATE_SIGNING_KEYPAIR = 0x0100000000000000000000000000000000000005;

SIGN_DIGEST

address internal constant SIGN_DIGEST = 0x0100000000000000000000000000000000000006;

VERIFY_DIGEST

address internal constant VERIFY_DIGEST = 0x0100000000000000000000000000000000000007;

CURVE25519_PUBLIC_KEY

address internal constant CURVE25519_PUBLIC_KEY = 0x0100000000000000000000000000000000000008;

GAS_USED

address internal constant GAS_USED = 0x0100000000000000000000000000000000000009;

PAD_GAS

address internal constant PAD_GAS = 0x010000000000000000000000000000000000000a;

SHA512_256

address internal constant SHA512_256 = 0x0100000000000000000000000000000000000101;

SHA512

address internal constant SHA512 = 0x0100000000000000000000000000000000000102;

SHA384

address internal constant SHA384 = 0x0100000000000000000000000000000000000104;

Functions

randomBytes

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.

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:

Sapphire.randomBytes(64, abi.encodePacked(msg.sender, this.perContactSecret));

Example

bytes memory randomPad = Sapphire.randomBytes(64, "");
function randomBytes(uint256 numBytes, bytes memory pers) internal view returns (bytes memory);

Parameters

NameTypeDescription
numBytesuint256The number of bytes to return.
persbytesAn optional personalization string to increase domain separation.

Returns

NameTypeDescription
<none>bytesThe random bytes. If the number of bytes requested is too large (over 1024), a smaller amount (1024) will be returned.

generateCurve25519KeyPair

Generates a Curve25519 keypair.

function generateCurve25519KeyPair(bytes memory pers)
    internal
    view
    returns (Curve25519PublicKey pk, Curve25519SecretKey sk);

Parameters

NameTypeDescription
persbytesAn optional personalization string used to add domain separation.

Returns

NameTypeDescription
pkCurve25519PublicKeyThe Curve25519 public key. Useful for key exchange.
skCurve25519SecretKeyThe Curve25519 secret key. Pairs well with deriveSymmetricKey.

deriveSymmetricKey

Derive a symmetric key from a pair of keys using x25519.

Precompile address

0x0100000000000000000000000000000000000002

Gas cost

100,000

Example

bytes32 publicKey = ... ;
bytes32 privateKey = ... ;
bytes32 symmetric = Sapphire.deriveSymmetricKey(publicKey, privateKey);
function deriveSymmetricKey(Curve25519PublicKey peerPublicKey, Curve25519SecretKey secretKey)
    internal
    view
    returns (bytes32);

Parameters

NameTypeDescription
peerPublicKeyCurve25519PublicKeyThe peer's public key.
secretKeyCurve25519SecretKeyYour secret key.

Returns

NameTypeDescription
<none>bytes32A derived symmetric key.

encrypt

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

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);
function encrypt(bytes32 key, bytes32 nonce, bytes memory plaintext, bytes memory additionalData)
    internal
    view
    returns (bytes memory);

Parameters

NameTypeDescription
keybytes32The key to use for encryption.
noncebytes32The nonce. Note that only the first 15 bytes of this parameter are used.
plaintextbytesThe plaintext to encrypt and authenticate.
additionalDatabytesThe additional data to authenticate.

Returns

NameTypeDescription
<none>bytesThe ciphertext with appended auth tag.

decrypt

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

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);
function decrypt(bytes32 key, bytes32 nonce, bytes memory ciphertext, bytes memory additionalData)
    internal
    view
    returns (bytes memory);

Parameters

NameTypeDescription
keybytes32The key to use for decryption.
noncebytes32The nonce. Note that only the first 15 bytes of this parameter are used.
ciphertextbytesThe ciphertext with tag to decrypt and authenticate.
additionalDatabytesThe additional data to authenticate against the ciphertext.

Returns

NameTypeDescription
<none>bytesThe original plaintext.

generateSigningKeyPair

Generate a public/private key pair using the specified method and seed. 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.

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

bytes memory seed = hex"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
bytes memory publicKey;
bytes memory privateKey;
(publicKey, privateKey) = Sapphire.generateSigningKeyPair(Sapphire.SigningAlg.Ed25519Pure, seed);
function generateSigningKeyPair(SigningAlg alg, bytes memory seed)
    internal
    view
    returns (bytes memory publicKey, bytes memory secretKey);

Parameters

NameTypeDescription
algSigningAlgThe signing alg for which to generate a keypair.
seedbytesThe seed to use for generating the key pair. You can use the randomBytes method if you don't already have a seed.

Returns

NameTypeDescription
publicKeybytesThe public half of the keypair.
secretKeybytesThe secret half of the keypair.

sign

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

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");
function sign(SigningAlg alg, bytes memory secretKey, bytes memory contextOrHash, bytes memory message)
    internal
    view
    returns (bytes memory signature);

Parameters

NameTypeDescription
algSigningAlgThe signing algorithm to use.
secretKeybytesThe secret key to use for signing. The key must be valid for use with the requested algorithm.
contextOrHashbytesDomain-Separator Context, or precomputed hash bytes.
messagebytesMessage to sign, should be zero-length if precomputed hash given.

Returns

NameTypeDescription
signaturebytesThe resulting signature.

verify

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() 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

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) );
function verify(
    SigningAlg alg,
    bytes memory publicKey,
    bytes memory contextOrHash,
    bytes memory message,
    bytes memory signature
) internal view returns (bool verified);

Parameters

NameTypeDescription
algSigningAlgThe signing algorithm by which the signature was generated.
publicKeybytesThe public key against which to check the signature.
contextOrHashbytesDomain-Separator Context, or precomputed hash bytes
messagebytesThe hash of the message that was signed, should be zero-length if precomputed hash was given.
signaturebytesThe signature to check.

Returns

NameTypeDescription
verifiedboolWhether the signature is valid for the given parameters.

padGas

Set the current transactions gas usage to a specific amount

Will cause a reversion if the current usage is more than the amount.

function padGas(uint128 toAmount) internal view;

Parameters

NameTypeDescription
toAmountuint128Gas usage will be set to this amount

gasUsed

Returns the amount of gas currently used by the transaction

function gasUsed() internal view returns (uint64);

Enums

SigningAlg

enum SigningAlg {
    Ed25519Oasis,
    Ed25519Pure,
    Ed25519PrehashedSha512,
    Secp256k1Oasis,
    Secp256k1PrehashedKeccak256,
    Secp256k1PrehashedSha256,
    Sr25519,
    Secp256r1PrehashedSha256,
    Secp384r1PrehashedSha384
}