Skip to content

Commit

Permalink
docs: added some cookbook recipes
Browse files Browse the repository at this point in the history
  • Loading branch information
ricmoo committed Sep 12, 2023
1 parent 3291563 commit 77fcc7f
Show file tree
Hide file tree
Showing 6 changed files with 385 additions and 0 deletions.
66 changes: 66 additions & 0 deletions docs.wrm/cookbook/ens.wrm
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
_section: Cookbook: ENS Recipes @<cookbook-ens>

Here is a collection of short, but useful examples of working with
ENS entries.


_subsection: Get all Text rectods @<cookbook-ens-allText>

Here is a short recipe to get all the text records set for an ENS
name.

It first queries all ``TextChanged`` events on the resovler, and
uses a MulticallProvider to batch all the ``eth_call`` queries
for each key into a single ``eth_call``. As such, you will need
to install:

``/home/ricmoo> npm install @ethers-ext/provider-multicall``


_code: Fetching all ENS text records. @lang<script>

mport { ethers } from "ethers";
import { MulticallProvider } from "@ethers-ext/provider-multicall";

async function getTextRecords(_provider, name) {
// Prepare a multicall-based provider to batch all the call operations
const provider = new MulticallProvider(_provider);

// Get the resolver for the given name
const resolver = await provider.getResolver(name);

// A contract instance; used filter and parse logs
const contract = new ethers.Contract(resolver.address, [
"event TextChanged(bytes32 indexed node, string indexed _key, string key)"
], provider);

// A filter for the given name
const filter = contract.filters.TextChanged(ethers.namehash(name));

// Get the matching logs
const logs = await contract.queryFilter(filter);

// Filter the *unique* keys
const keys = [ ...(new Set(logs.map((log) => log.args.key))) ];

// Get the values for the keys; failures are discard
const values = await Promise.all(keys.map((key) => {
try {
return resolver.getText(key);
} catch (error) { }
return null;
}));

// Return a Map of the key/value pairs
return keys.reduce((accum, key, index) => {
const value = values[index];
if (value != null) { accum.set(key, value); }
return accum;
}, new Map());
}

// Example usage
(async function() {
const provider = new ethers.InfuraProvider();
console.log(await getTextRecords(provider, "ricmoo.eth"));
})();
7 changes: 7 additions & 0 deletions docs.wrm/cookbook/index.wrm
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
_section: Cookbook @<cookbook>

A growing collection of code snippets for common problems and use cases
when developing dapps and other blockchain tools.

- [Signing Messages and Data](cookbook-signing)
- [React Native Performance](cookbook-react-native)
36 changes: 36 additions & 0 deletions docs.wrm/cookbook/react-native.wrm
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
_section: React Native @<cookbook-react-native>

When using React Native, many of the built-in cryptographic primitives
can be replaced by native, substantially faster implementations.

This should be available in its own package in the future, but for now
this is highly recommended, and requires installing the
[[link-npm-react-native-quick-crypto]] package.


_code:

import { ethers } from "ethers";

import crypto from "react-native-quick-crypto";

ethers.randomBytes.register((length) => {
return new Uint8Array(crypto.randomBytes(length));
});

ethers.computeHmac.register((algo, key, data) => {
return crypto.createHmac(algo, key).update(data).digest();
});

ethers.pbkdf2.register((passwd, salt, iter, keylen, algo) => {
return crypto.pbkdf2Sync(passwd, salt, iter, keylen, algo);
});

ethers.sha256.register((data) => {
return crypto.createHash('sha256').update(data).digest();
});

ethers.sha512.register((data) => {
return crypto.createHash('sha512').update(data).digest();
});

273 changes: 273 additions & 0 deletions docs.wrm/cookbook/signing.wrm
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
_section: Signing @<cookbook-signing>

Signing content and providing the content and signature to a
Contract allows on-chain validation that a signer has access
to the private key of a specific address.

The ecrecover algorithm allows the public key to be determined
given some message digest and the signature generated by the
private key for that digest. From the public key, the address
can then be computed.

How a digest is derived depends on the type of data being
signed and a variety of encoding formats are employed. Each
format is designed to ensure that they do not collide, so for
example, a user **cannot** be tricked into signing a message
which is actually a valid transaction.

For this reason, most APIs in Ethereum do not permit signing a
raw digest, and instead require a separate API for each format
type and require the related data be specified, protecting the
user from accidentally authorizing an action they didn't intend.

_subsection: Messages @<cookbook-signing-messages>

A signed message can be any data, bu it is generally recommended
to use human-readable text, as this is easier for a user to
verify visually.

This technique could be used, for example, to sign into a service
by using the text ``"I am signing into ethers.org on 2023-06-04 12:57pm"``.
The user can then see the message in MetaMask or on a Ledger
Hardware Wallet and accept that they wish to sign the message which
the site can then autheticate them with. By providing a timestamp
the site can ensure that an older signed message cannot be used again
in the future.

The format that is signed uses [[link-eip-191]] with the
**personal sign** version code (``0x45``, or ``"E"``).

For those interested in the choice of this prefix, signed messages
began as a Bitcoin feature, which used ``"\\x18Bitcoin Signed Message:\\n"``,
which was a Bitcoin var-int length-prefixed string (as ``0x18`` is 24,
the length of ``"Bitcoin Signed Message:\\n"``.). When Ethereum adopted
the similar feature, the relevant string was ``"\\x19Ethereum Signed Message:\\n"``.

In one of the most brilliant instances of technical retcon-ing,
since 0x19 is invalid as the first byte of a transaction (in [[link-rlp]] it
indicates a single byte of value 25), the initial byte ``\\x19`` has
now been adopted as a prefix for //some sort of signed data//,
where the second byte determines how to interpret that data. If the
second bytes is 69 (the letter ``"E"``, as in
``"Ethereum Signed Message:\\n"``), then the format is a
the above prefixed message format.

So, all existing messages, tools and instances using the signed
message format were already EIP-191 compliant, long before the
standard existed or was even conceived and allowed for an extensible
format for future formats (of which there now a few).

Anyways, the necessary JavaScript and Solidity are provided below.

_code: JavaScript @lang<javascript>

// The contract below is deployed to Sepolia at this address
contractAddress = "0xf554DA5e35b2e40C09DDB481545A395da1736513";
contract = new Contract(contractAddress, [
"function recoverStringFromCompact(string message, (bytes32 r, bytes32 yParityAndS) sig) pure returns (address)",
"function recoverStringFromExpanded(string message, (uint8 v, bytes32 r, bytes32 s) sig) pure returns (address)",
"function recoverStringFromVRS(string message, uint8 v, bytes32 r, bytes32 s) pure returns (address)",
"function recoverStringFromRaw(string message, bytes sig) pure returns (address)",
"function recoverHashFromCompact(bytes32 hash, (bytes32 r, bytes32 yParityAndS) sig) pure returns (address)"
], new ethers.InfuraProvider("sepolia"));

// The Signer; it does not need to be connected to a Provider to sign
signer = new Wallet(id("foobar"));
signer.address
//_result:

// Our message
message = "Hello World";

// The raw signature; 65 bytes
rawSig = await signer.signMessage(message);
//_result:

// Converting it to a Signature object provides more
// flexibility, such as using it as a struct
sig = Signature.from(rawSig);
//_result:


// If the signature matches the EIP-2098 format, a Signature
// can be passed as the struct value directly, since the
// parser will pull out the matching struct keys from sig.
await contract.recoverStringFromCompact(message, sig);
//_result:

// Likewise, if the struct keys match an expanded signature
// struct, it can also be passed as the struct value directly.
await contract.recoverStringFromExpanded(message, sig);
//_result:

// If using an older API which requires the v, r and s be passed
// separately, those members are present on the Signature.
await contract.recoverStringFromVRS(message, sig.v, sig.r, sig.s);
//_result:

// Or if using a an API that expects a raw signature.
await contract.recoverStringFromRaw(message, rawSig);
//_result:

// Note: The above recovered addresses matches the signer address

_null:

The Solidity Contract has been deployed and verified on
the Sepolia testnet at the address
[0xf554DA5e35b2e40C09DDB481545A395da1736513](link-sol-recovermessage).

It provides a variety of examples using various Signature
encodings and formats, to recover the address for an [[link-eip-191]]
signed message.

_code: Solidity @lang<solidity>

// SPDX-License-Identifier: MIT

// For more info, see: https://docs.ethers.org


pragma solidity ^0.8.21;

// Returns the decimal string representation of value
function itoa(uint value) pure returns (string memory) {

// Count the length of the decimal string representation
uint length = 1;
uint v = value;
while ((v /= 10) != 0) { length++; }

// Allocated enough bytes
bytes memory result = new bytes(length);

// Place each ASCII string character in the string,
// right to left
while (true) {
length--;

// The ASCII value of the modulo 10 value
result[length] = bytes1(uint8(0x30 + (value % 10)));

value /= 10;

if (length == 0) { break; }
}

return string(result);
}

contract RecoverMessage {

// This is the EIP-2098 compact representation, which reduces gas costs
struct SignatureCompact {
bytes32 r;
bytes32 yParityAndS;
}

// This is an expaned Signature representation
struct SignatureExpanded {
uint8 v;
bytes32 r;
bytes32 s;
}

// Helper function
function _ecrecover(string memory message, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {
// Compute the EIP-191 prefixed message
bytes memory prefixedMessage = abi.encodePacked(
"\x19Ethereum Signed Message:\n",
itoa(bytes(message).length),
message
);

// Compute the message digest
bytes32 digest = keccak256(prefixedMessage);

// Use the native ecrecover provided by the EVM
return ecrecover(digest, v, r, s);
}

// Recover the address from an EIP-2098 compact Signature, which packs the bit for
// v into an unused bit within s, which saves gas overall, costing a little extra
// in computation, but saves far more in calldata length.
//
// This Signature format is 64 bytes in length.
function recoverStringFromCompact(string calldata message, SignatureCompact calldata sig) public pure returns (address) {

// Decompose the EIP-2098 signature (the struct is 64 bytes in length)
uint8 v = 27 + uint8(uint256(sig.yParityAndS) >> 255);
bytes32 s = bytes32((uint256(sig.yParityAndS) << 1) >> 1);

return _ecrecover(message, v, sig.r, s);
}

// Recover the address from the an expanded Signature struct.
//
// This Signature format is 96 bytes in length.
function recoverStringFromExpanded(string calldata message, SignatureExpanded calldata sig) public pure returns (address) {

// The v, r and s are included directly within the struct, which is 96 bytes in length
return _ecrecover(message, sig.v, sig.r, sig.s);
}

// Recover the address from a v, r and s passed directly into the method.
//
// This Signature format is 96 bytes in length.
function recoverStringFromVRS(string calldata message, uint8 v, bytes32 r, bytes32 s) public pure returns (address) {

// The v, r and s are included directly within the struct, which is 96 bytes in length
return _ecrecover(message, v, r, s);
}

// Recover the address from a raw signature. The signature is 65 bytes, which when
// ABI encoded is 160 bytes long (a pointer, a length and the padded 3 words of data).
//
// When using raw signatures, some tools return the v as 0 or 1. In this case you must
// add 27 to that value as v must be either 27 or 28.
//
// This Signature format is 65 bytes of data, but when ABI encoded is 160 bytes in length;
// a pointer (32 bytes), a length (32 bytes) and the padded 3 words of data (96 bytes).
function recoverStringFromRaw(string calldata message, bytes calldata sig) public pure returns (address) {

// Sanity check before using assembly
require(sig.length == 65, "invalid signature");

// Decompose the raw signature into r, s and v (note the order)
uint8 v;
bytes32 r;
bytes32 s;
assembly {
r := calldataload(sig.offset)
s := calldataload(add(sig.offset, 0x20))
v := calldataload(add(sig.offset, 0x21))
}

return _ecrecover(message, v, r, s);
}

// This is provided as a quick example for those that only need to recover a signature
// for a signed hash (highly discouraged; but common), which means we can hardcode the
// length in the prefix. This means we can drop the itoa and _ecrecover functions above.
function recoverHashFromCompact(bytes32 hash, SignatureCompact calldata sig) public pure returns (address) {
bytes memory prefixedMessage = abi.encodePacked(
// Notice the length of the message is hard-coded to 32
// here -----------------------v
"\x19Ethereum Signed Message:\n32",
hash
);

bytes32 digest = keccak256(prefixedMessage);

// Decompose the EIP-2098 signature
uint8 v = 27 + uint8(uint256(sig.yParityAndS) >> 255);
bytes32 s = bytes32((uint256(sig.yParityAndS) << 1) >> 1);

return ecrecover(digest, v, sig.r, s);
}
}


_subsection: EIP-712 Typed Data @<cookbook-signing-eip712>

//Coming soon...//
1 change: 1 addition & 0 deletions docs.wrm/links/npm.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ link-npm-ethers [link-ethers-npm](https://www.npmjs.com/search?q=%40ethersprojec
link-npm-events [EventEmitter](https://nodejs.org/dist/latest-v13.x/docs/api/events.html#events_class_eventemitter)
link-npm-query-bignumber [link-npm-query-bignumber](https://www.npmjs.com/search?q=bignumber)
link-npm-react-native-get-random-values [React Native get-random-values](https://www.npmjs.com/package/react-native-get-random-values)
link-npm-react-native-quick-crypto [Quick Crypto](https://www.npmjs.com/package/react-native-quick-crypto)
2 changes: 2 additions & 0 deletions docs.wrm/links/projects.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,5 @@ link-solidity-errors [link-solidity-errors](https://docs.soliditylang.org/en/v0.
link-solidity-events [link-solidity-events](https://docs.soliditylang.org/en/v0.8.4/abi-spec.html#events)

link-other-ethereum-dev-docs [link-other-ethereum-dev-docs](https://ethereum.org/en/developers/docs/)

link-sol-recovermessage [RecoverMessage.sol](https://sepolia.etherscan.io/address/0xf554da5e35b2e40c09ddb481545a395da1736513#code)

0 comments on commit 77fcc7f

Please sign in to comment.