UNPKG

check-ethereum-scanner

Version:

Safety checks for new Ethereum tokens

195 lines (168 loc) 5.94 kB
import { keccak256 } from 'ethereum-cryptography/keccak' import { secp256k1 } from 'ethereum-cryptography/secp256k1' import { bufferToBigInt, bufferToHex, bufferToInt, setLengthLeft, toBuffer } from './bytes' import { SECP256K1_ORDER, SECP256K1_ORDER_DIV_2 } from './constants' import { assertIsBuffer } from './helpers' export interface ECDSASignature { v: bigint r: Buffer s: Buffer } /** * Returns the ECDSA signature of a message hash. * * If `chainId` is provided assume an EIP-155-style signature and calculate the `v` value * accordingly, otherwise return a "static" `v` just derived from the `recovery` bit */ export function ecsign(msgHash: Buffer, privateKey: Buffer, chainId?: bigint): ECDSASignature { const sig = secp256k1.sign(msgHash, privateKey) const buf = sig.toCompactRawBytes() const r = Buffer.from(buf.slice(0, 32)) const s = Buffer.from(buf.slice(32, 64)) const v = chainId === undefined ? BigInt(sig.recovery! + 27) : BigInt(sig.recovery! + 35) + BigInt(chainId) * BigInt(2) return { r, s, v } } function calculateSigRecovery(v: bigint, chainId?: bigint): bigint { if (v === BigInt(0) || v === BigInt(1)) return v if (chainId === undefined) { return v - BigInt(27) } return v - (chainId * BigInt(2) + BigInt(35)) } function isValidSigRecovery(recovery: bigint): boolean { return recovery === BigInt(0) || recovery === BigInt(1) } /** * ECDSA public key recovery from signature. * NOTE: Accepts `v === 0 | v === 1` for EIP1559 transactions * @returns Recovered public key */ export const ecrecover = function ( msgHash: Buffer, v: bigint, r: Buffer, s: Buffer, chainId?: bigint ): Buffer { const signature = Buffer.concat([setLengthLeft(r, 32), setLengthLeft(s, 32)], 64) const recovery = calculateSigRecovery(v, chainId) if (!isValidSigRecovery(recovery)) { throw new Error('Invalid signature v value') } const sig = secp256k1.Signature.fromCompact(signature).addRecoveryBit(Number(recovery)) const senderPubKey = sig.recoverPublicKey(msgHash) return Buffer.from(senderPubKey.toRawBytes(false).slice(1)) } /** * Convert signature parameters into the format of `eth_sign` RPC method. * NOTE: Accepts `v === 0 | v === 1` for EIP1559 transactions * @returns Signature */ export const toRpcSig = function (v: bigint, r: Buffer, s: Buffer, chainId?: bigint): string { const recovery = calculateSigRecovery(v, chainId) if (!isValidSigRecovery(recovery)) { throw new Error('Invalid signature v value') } // geth (and the RPC eth_sign method) uses the 65 byte format used by Bitcoin return bufferToHex(Buffer.concat([setLengthLeft(r, 32), setLengthLeft(s, 32), toBuffer(v)])) } /** * Convert signature parameters into the format of Compact Signature Representation (EIP-2098). * NOTE: Accepts `v === 0 | v === 1` for EIP1559 transactions * @returns Signature */ export const toCompactSig = function (v: bigint, r: Buffer, s: Buffer, chainId?: bigint): string { const recovery = calculateSigRecovery(v, chainId) if (!isValidSigRecovery(recovery)) { throw new Error('Invalid signature v value') } let ss = s if ((v > BigInt(28) && v % BigInt(2) === BigInt(1)) || v === BigInt(1) || v === BigInt(28)) { ss = Buffer.from(s) ss[0] |= 0x80 } return bufferToHex(Buffer.concat([setLengthLeft(r, 32), setLengthLeft(ss, 32)])) } /** * Convert signature format of the `eth_sign` RPC method to signature parameters * * NOTE: For an extracted `v` value < 27 (see Geth bug https://github.com/ethereum/go-ethereum/issues/2053) * `v + 27` is returned for the `v` value * NOTE: After EIP1559, `v` could be `0` or `1` but this function assumes * it's a signed message (EIP-191 or EIP-712) adding `27` at the end. Remove if needed. */ export const fromRpcSig = function (sig: string): ECDSASignature { const buf: Buffer = toBuffer(sig) let r: Buffer let s: Buffer let v: bigint if (buf.length >= 65) { r = buf.slice(0, 32) s = buf.slice(32, 64) v = bufferToBigInt(buf.slice(64)) } else if (buf.length === 64) { // Compact Signature Representation (https://eips.ethereum.org/EIPS/eip-2098) r = buf.slice(0, 32) s = buf.slice(32, 64) v = BigInt(bufferToInt(buf.slice(32, 33)) >> 7) s[0] &= 0x7f } else { throw new Error('Invalid signature length') } // support both versions of `eth_sign` responses if (v < 27) { v = v + BigInt(27) } return { v, r, s, } } /** * Validate a ECDSA signature. * NOTE: Accepts `v === 0 | v === 1` for EIP1559 transactions * @param homesteadOrLater Indicates whether this is being used on either the homestead hardfork or a later one */ export const isValidSignature = function ( v: bigint, r: Buffer, s: Buffer, homesteadOrLater: boolean = true, chainId?: bigint ): boolean { if (r.length !== 32 || s.length !== 32) { return false } if (!isValidSigRecovery(calculateSigRecovery(v, chainId))) { return false } const rBigInt = bufferToBigInt(r) const sBigInt = bufferToBigInt(s) if ( rBigInt === BigInt(0) || rBigInt >= SECP256K1_ORDER || sBigInt === BigInt(0) || sBigInt >= SECP256K1_ORDER ) { return false } if (homesteadOrLater && sBigInt >= SECP256K1_ORDER_DIV_2) { return false } return true } /** * Returns the keccak-256 hash of `message`, prefixed with the header used by the `eth_sign` RPC call. * The output of this function can be fed into `ecsign` to produce the same signature as the `eth_sign` * call for a given `message`, or fed to `ecrecover` along with a signature to recover the public key * used to produce the signature. */ export const hashPersonalMessage = function (message: Buffer): Buffer { assertIsBuffer(message) const prefix = Buffer.from(`\u0019Ethereum Signed Message:\n${message.length}`, 'utf-8') return Buffer.from(keccak256(Buffer.concat([prefix, message]))) }