Skip to content

Instantly share code, notes, and snippets.

@Vectorized
Last active July 12, 2022 10:04
Show Gist options
  • Save Vectorized/860a81835bb47f7884770d440c6b3574 to your computer and use it in GitHub Desktop.
Save Vectorized/860a81835bb47f7884770d440c6b3574 to your computer and use it in GitHub Desktop.
Vyper vs Solidity Gas Benchmarks

Vyper vs Solidity Gas Benchmarks

Summary

EDCSA.recover(bytes32 hash, bytes calldata signature)

Solidity: 26869
Vyper: 27729

Note that the Solidity version has the added functionality:

  • ensures validity and non-malleability
  • supports short EIP2098 format

EDCSA.toEthSignedMessageHash(bytes32 hash)

Solidity: 21970
Vyper: 22094

toString(uint256 value)

Solidity: 22122
Vyper: 35995
Vyper (inlined): 25344

Note that the Vyper version is not able to return a string without left-padded zeros, and is restricted to 4 digits.

MerkleProof.verify(bytes32[] calldata proof, bytes32 root, bytes32 leaf)

Solidity: 29481
Vyper: 30475

Tested with proof.length of 10. Note that the Vyper version can only accept proofs of length 10 (hardcoded due to language restrictions). This puts the Solidity version at a slight handicap.

Notes

All Solidity files are compiled with solc v0.8.7 with 200 optimizer runs.

// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
/// @notice Gas optimized ECDSA wrapper.
/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/ECDSA.sol)
/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol)
library ECDSA {
function recover(bytes32 hash, bytes calldata signature) internal view returns (address result) {
assembly {
// Copy the free memory pointer so that we can restore it later.
let m := mload(0x40)
// Directly load the fields from the calldata.
let s := calldataload(add(signature.offset, 0x20))
// If `signature.length == 65`, but just do it anyway as it costs less gas than a switch.
let v := byte(0, calldataload(add(signature.offset, 0x40)))
// If `signature.length == 64`.
if iszero(sub(signature.length, 64)) {
// Here, `s` is actually `vs` that needs to be recovered into `v` and `s`.
v := add(shr(255, s), 27)
// prettier-ignore
s := and(s, 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff)
}
// If signature is valid and not malleable.
if and(
// `s` in lower half order.
// prettier-ignore
lt(s, 0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a1),
// `v` is 27 or 28
byte(v, 0x0101000000)
) {
mstore(0x00, hash)
mstore(0x20, v)
calldatacopy(0x40, signature.offset, 0x20) // Directly copy `r` over.
mstore(0x60, s)
let success := staticcall(
gas(), // Amount of gas left for the transaction.
0x01, // Address of `ecrecover`.
0x00, // Start of input.
0x80, // Size of input.
0x40, // Start of output.
0x20 // Size of output.
)
// Restore the zero slot.
mstore(0x60, 0)
// `returndatasize()` will be `0x20` upon success, and `0x00` otherwise.
result := mload(sub(0x60, mul(returndatasize(), success)))
}
// Restore the free memory pointer.
mstore(0x40, m)
}
}
function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32 result) {
assembly {
// Store into scratch space for keccak256.
mstore(0x20, hash)
mstore(0x00, "\x00\x00\x00\x00\x19Ethereum Signed Message:\n32")
// 0x40 - 0x04 = 0x3c
result := keccak256(0x04, 0x3c)
}
}
function toEthSignedMessageHash(bytes memory s) internal pure returns (bytes32 result) {
assembly {
// We need at most 128 bytes for Ethereum signed message header.
// The max length of the ASCII reprenstation of a uint256 is 78 bytes.
// The length of "\x19Ethereum Signed Message:\n" is 26 bytes.
// The next multiple of 32 above 78 + 26 is 128.
// Instead of allocating, we temporarily copy the 128 bytes before the
// start of `s` data to some variables.
let m3 := mload(sub(s, 0x60))
let m2 := mload(sub(s, 0x40))
let m1 := mload(sub(s, 0x20))
// The length of `s` is in bytes.
let sLength := mload(s)
let ptr := add(s, 0x20)
// `end` marks the end of the memory which we will compute the keccak256 of.
let end := add(ptr, sLength)
// Convert the length of the bytes to ASCII decimal representation
// and store it into the memory.
for {
let temp := sLength
ptr := sub(ptr, 1)
mstore8(ptr, add(48, mod(temp, 10)))
temp := div(temp, 10)
} temp {
temp := div(temp, 10)
} {
ptr := sub(ptr, 1)
mstore8(ptr, add(48, mod(temp, 10)))
}
// Move the pointer 32 bytes lower to make room for the string.
// `start` marks the start of the memory which we will compute the keccak256 of.
let start := sub(ptr, 32)
// Copy the header over to the memory.
mstore(start, "\x00\x00\x00\x00\x00\x00\x19Ethereum Signed Message:\n")
start := add(start, 6)
// Compute the keccak256 of the memory.
result := keccak256(start, sub(end, start))
// Restore the previous memory.
mstore(s, sLength)
mstore(sub(s, 0x20), m1)
mstore(sub(s, 0x40), m2)
mstore(sub(s, 0x60), m3)
}
}
}
/// @notice Gas optimized verification of proof of inclusion for a leaf in a Merkle tree.
/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/MerkleProof.sol)
/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/MerkleProof.sol)
library MerkleProof {
function verify(
bytes32[] calldata proof,
bytes32 root,
bytes32 leaf
) internal pure returns (bool isValid) {
assembly {
// Left shift by 5 is equivalent to multiplying by 0x20.
let end := add(proof.offset, shl(5, proof.length))
// Iterate over proof elements to compute root hash.
for {
// Initialize `data` to the offset of `proof` in the calldata.
let data := proof.offset
} iszero(eq(data, end)) {
data := add(data, 0x20)
} {
// Slot of `leaf` in scratch space.
// If the condition is true: 0x20, otherwise: 0x00.
let scratch := shl(5, gt(leaf, calldataload(data)))
// Store elements to hash contiguously in scratch space.
// Scratch space is 64 bytes (0x00 - 0x3f) and both elements are 32 bytes.
mstore(scratch, leaf)
mstore(xor(scratch, 0x20), calldataload(data))
// Reuse `leaf` to store the hash to reduce stack operations.
leaf := keccak256(0x00, 0x40)
}
isValid := eq(leaf, root)
}
}
}
contract TestSolidity {
// Input: 0x2d0828dd7c97cff316356da3c16c68ba2316886a0e05ebafb8291939310d51a3,
// 0x331fe75a821c982f9127538858900d87d3ec1f9f737338ad67cad133fa48feff48e6fa0c18abc62e42820f05943e47af3e9fbe306ce74d64094bdf1691ee53e01c
// Gas used: 26869
function recover(bytes32 hash, bytes calldata signature) public view returns (address result) {
result = ECDSA.recover(hash, signature);
}
// Input: 0x2d0828dd7c97cff316356da3c16c68ba2316886a0e05ebafb8291939310d51a3
// Gas used: 21970
function toEthSignedMessageHash(bytes32 hash) public view returns (bytes32 result) {
result = ECDSA.toEthSignedMessageHash(hash);
}
// Input: ["0xbc954aa7056a720314aebed21b6994f389984cd457eeb75b766bbe2454fe5074", "0x6b134bd38f9b7cca9c5619690efeed06cc215e0e99ec813d4cdc787d3059c134", "0x0550e4726615ca382b3453a849fab0d68175c7d51d1b4fc0098bc6728272f1a8", "0x88e86d4b2c853a78cad494383812e063579c739ae56172b819f36ac881b21406", "0x0e5981d26305d5fae2eb1c45c8db50d44417870d4e1b66b2b296f2ed3b6cc9d6", "0xbcd65a69edd6ca87dfc5db88ab259577604fa7ad5ec6b5bd7841b1a0fae4a18f", "0xdcbeba93024e673b78504e60eea6a848b1d04970b0e1192f02ea91de1e12e31f", "0xf7fd534621bec145aa8b37c12e4fc58413adaf76d3c38c42ad3d464dfd239cd6", "0xcf20917b390bf32bf4ead5776b5254325bd8b13d9e1308d6b3ad52ab37d9a040", "0xafee348eb2224de248b5d43949d38d5be97fe1b72637f7d9c776561a3f75e17e"],
// 0xef35dac8c7728a6c30dc702829819d9d3349f1435480726d0a865665ef8ace69,
// 0x54a6ae86104cedff370ad1da9bb9f2fc64a6e5fb3f2a8ee17a1d1c0d8ecd2267
// Gas used: 29481
function merkleVerify(bytes32[] calldata proof, bytes32 root, bytes32 leaf) external pure returns (bool isValid) {
isValid = MerkleProof.verify(proof, root, leaf);
}
// Input: 1234
// Gas used: 22122
function toString(uint256 value) external pure returns (string memory ptr) {
assembly {
// The maximum value of a uint256 contains 78 digits (1 byte per digit),
// but we allocate 128 bytes to keep the free memory pointer 32-byte word aliged.
// We will need 1 32-byte word to store the length,
// and 3 32-byte words to store a maximum of 78 digits. Total: 32 + 3 * 32 = 128.
ptr := add(mload(0x40), 128)
// Update the free memory pointer to allocate.
mstore(0x40, ptr)
// Cache the end of the memory to calculate the length later.
let end := ptr
// We write the string from the rightmost digit to the leftmost digit.
// The following is essentially a do-while loop that also handles the zero case.
// Costs a bit more than early returning for the zero case,
// but cheaper in terms of deployment and overall runtime costs.
for {
// Initialize and perform the first pass without check.
let temp := value
// Move the pointer 1 byte leftwards to point to an empty character slot.
ptr := sub(ptr, 1)
// Write the character to the pointer. 48 is the ASCII index of '0'.
mstore8(ptr, add(48, mod(temp, 10)))
temp := div(temp, 10)
} temp {
// Keep dividing `temp` until zero.
temp := div(temp, 10)
} { // Body of the for loop.
ptr := sub(ptr, 1)
mstore8(ptr, add(48, mod(temp, 10)))
}
let length := sub(end, ptr)
// Move the pointer 32 bytes leftwards to make room for the length.
ptr := sub(ptr, 32)
// Store the length.
mstore(ptr, length)
}
}
}
# @version ^0.2.0
# Input: 0x2d0828dd7c97cff316356da3c16c68ba2316886a0e05ebafb8291939310d51a3
# Gas used: 22094
@external
@pure
def getEthSignedHash(_hash: bytes32) -> bytes32:
return keccak256(
concat(
b'\x19Ethereum Signed Message:\n32',
_hash
)
)
# Input: 0x7dbaf558b0a1a5dc7a67202117ab143c1d8605a983e4a743bc06fcc03162dc0d,
# 0x331fe75a821c982f9127538858900d87d3ec1f9f737338ad67cad133fa48feff48e6fa0c18abc62e42820f05943e47af3e9fbe306ce74d64094bdf1691ee53e01c
# Gas used: 27729
#
# Note that it does not check non-malleability, and does not support short EIP2098 format.
@external
@pure
def verifyECDSA(ethSignedHash: bytes32, sig: Bytes[65]) -> address:
r: uint256 = convert(slice(sig, 0, 32), uint256)
s: uint256 = convert(slice(sig, 32, 32), uint256)
v: uint256 = convert(slice(sig, 64, 1), uint256)
return ecrecover(ethSignedHash, v, r, s)
# Input: ["0xbc954aa7056a720314aebed21b6994f389984cd457eeb75b766bbe2454fe5074", "0x6b134bd38f9b7cca9c5619690efeed06cc215e0e99ec813d4cdc787d3059c134", "0x0550e4726615ca382b3453a849fab0d68175c7d51d1b4fc0098bc6728272f1a8", "0x88e86d4b2c853a78cad494383812e063579c739ae56172b819f36ac881b21406", "0x0e5981d26305d5fae2eb1c45c8db50d44417870d4e1b66b2b296f2ed3b6cc9d6", "0xbcd65a69edd6ca87dfc5db88ab259577604fa7ad5ec6b5bd7841b1a0fae4a18f", "0xdcbeba93024e673b78504e60eea6a848b1d04970b0e1192f02ea91de1e12e31f", "0xf7fd534621bec145aa8b37c12e4fc58413adaf76d3c38c42ad3d464dfd239cd6", "0xcf20917b390bf32bf4ead5776b5254325bd8b13d9e1308d6b3ad52ab37d9a040", "0xafee348eb2224de248b5d43949d38d5be97fe1b72637f7d9c776561a3f75e17e"],
# 0xef35dac8c7728a6c30dc702829819d9d3349f1435480726d0a865665ef8ace69,
# 0x54a6ae86104cedff370ad1da9bb9f2fc64a6e5fb3f2a8ee17a1d1c0d8ecd2267
# Gas used: 30475
@external
@pure
def verifyMerkle(proof: bytes32[10], root: bytes32, leaf: bytes32) -> bool:
computedHash: bytes32 = leaf
for i in range(10):
sister: bytes32 = proof[i]
if convert(computedHash, uint256) < convert(sister, uint256):
computedHash = keccak256(concat(computedHash, sister))
else:
computedHash = keccak256(concat(sister, computedHash))
return computedHash == root
@pure
@internal
def _digitToString(digit: uint256) -> String[1]:
assert digit < 10 # only works with digits 0-9
digit_bytes32: bytes32 = convert(digit + 48, bytes32) # ASCII `0` is 0x30 (48 in decimal)
digit_bytes1: Bytes[1] = slice(digit_bytes32, 31, 1) # Remove padding bytes
return convert(digit_bytes1, String[1])
@view
@internal
def _tokenIdToString(tokenId: uint256) -> String[4]:
# NOTE: Only handles up to 4 digits, e.g. tokenId in [0, 9999]
digit1: uint256 = tokenId % 10
digit2: uint256 = (tokenId % 100) / 10
digit3: uint256 = (tokenId % 1000) / 100
digit4: uint256 = tokenId / 1000
return concat(
self._digitToString(digit4),
self._digitToString(digit3),
self._digitToString(digit2),
self._digitToString(digit1),
)
# Input: 1234
# Gas used: 35995
@external
@view
def uint256ToString(x: uint256) -> String[4]:
return self._tokenIdToString(x)
# Input: 1234
# Gas used: 25344
@external
@view
def uint256ToStringInlined(x: uint256) -> String[4]:
d1: uint256 = x % 10
d2: uint256 = (x % 100) / 10
d3: uint256 = (x % 1000) / 100
d4: uint256 = x / 1000
d4_32: bytes32 = convert(d4 + 48, bytes32)
d4_1: Bytes[1] = slice(d4_32, 31, 1)
d4s_1: String[1] = convert(d4_1, String[1])
d3_32: bytes32 = convert(d3 + 48, bytes32)
d3_1: Bytes[1] = slice(d3_32, 31, 1)
d3s_1: String[1] = convert(d3_1, String[1])
d2_32: bytes32 = convert(d2 + 48, bytes32)
d2_1: Bytes[1] = slice(d2_32, 31, 1)
d2s_1: String[1] = convert(d2_1, String[1])
d1_32: bytes32 = convert(d1 + 48, bytes32)
d1_1: Bytes[1] = slice(d1_32, 31, 1)
d1s_1: String[1] = convert(d1_1, String[1])
return concat(
d4s_1,
d3s_1,
d2s_1,
d1s_1,
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment