From 5e8a249c3886a09a3a40b5748484fbf5d57f2fa6 Mon Sep 17 00:00:00 2001 From: "moxey.eth" Date: Thu, 25 Apr 2024 09:33:52 +1000 Subject: [PATCH] feat: accept `Signature` as a valid type for `signature` parameters --- .changeset/rotten-hounds-press.md | 5 ++ .../docs/actions/public/verifyMessage.md | 2 +- .../docs/actions/public/verifyTypedData.md | 2 +- site/pages/docs/utilities/recoverAddress.md | 2 +- .../docs/utilities/recoverMessageAddress.md | 2 +- site/pages/docs/utilities/recoverPublicKey.md | 2 +- site/pages/docs/utilities/verifyMessage.md | 2 +- site/pages/docs/utilities/verifyTypedData.md | 2 +- .../eip3074/recoverAuthMessageAddress.mdx | 2 +- src/actions/public/verifyHash.test.ts | 19 +++++ src/actions/public/verifyHash.ts | 22 ++++-- src/actions/public/verifyMessage.ts | 9 ++- src/actions/public/verifyTypedData.ts | 4 +- .../utils/recoverAuthMessageAddress.ts | 4 +- src/utils/signature/hexToSignature.test.ts | 38 ++++++++++ src/utils/signature/hexToSignature.ts | 19 ++++- src/utils/signature/recoverAddress.test.ts | 37 +++++++++ src/utils/signature/recoverAddress.ts | 4 +- src/utils/signature/recoverMessageAddress.ts | 9 ++- src/utils/signature/recoverPublicKey.test.ts | 76 +++++++++++++++++++ src/utils/signature/recoverPublicKey.ts | 49 ++++++++---- .../recoverTransactionAddress.test.ts | 2 +- .../signature/recoverTransactionAddress.ts | 4 +- .../signature/recoverTypedDataAddress.ts | 4 +- src/utils/signature/verifyMessage.ts | 9 ++- src/utils/signature/verifyTypedData.ts | 4 +- 26 files changed, 284 insertions(+), 50 deletions(-) create mode 100644 .changeset/rotten-hounds-press.md diff --git a/.changeset/rotten-hounds-press.md b/.changeset/rotten-hounds-press.md new file mode 100644 index 0000000000..7ba28e3d5b --- /dev/null +++ b/.changeset/rotten-hounds-press.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +Added `Signature` as a valid input type to `signature` parameters. diff --git a/site/pages/docs/actions/public/verifyMessage.md b/site/pages/docs/actions/public/verifyMessage.md index f59b975ff8..e75b1316c8 100644 --- a/site/pages/docs/actions/public/verifyMessage.md +++ b/site/pages/docs/actions/public/verifyMessage.md @@ -115,7 +115,7 @@ const valid = await publicClient.verifyMessage({ ### signature -- **Type:** `Hex | ByteArray` +- **Type:** `Hex | ByteArray | Signature` The signature that was generated by signing the message with the address's signer. diff --git a/site/pages/docs/actions/public/verifyTypedData.md b/site/pages/docs/actions/public/verifyTypedData.md index 1ed376464f..2fbf6034e6 100644 --- a/site/pages/docs/actions/public/verifyTypedData.md +++ b/site/pages/docs/actions/public/verifyTypedData.md @@ -324,7 +324,7 @@ const valid = await publicClient.verifyTypedData({ ### signature -- **Type:** `Hex | ByteArray` +- **Type:** `Hex | ByteArray | Signature` The signature of the typed data. diff --git a/site/pages/docs/utilities/recoverAddress.md b/site/pages/docs/utilities/recoverAddress.md index 0f90c22e31..8ecfded9db 100644 --- a/site/pages/docs/utilities/recoverAddress.md +++ b/site/pages/docs/utilities/recoverAddress.md @@ -41,7 +41,7 @@ const address = await recoverAddress({ ### signature -- **Type:** `Hex | ByteArray` +- **Type:** `Hex | ByteArray | Signature` The signature of the hash. diff --git a/site/pages/docs/utilities/recoverMessageAddress.md b/site/pages/docs/utilities/recoverMessageAddress.md index ab56ec84a7..4728b2bca8 100644 --- a/site/pages/docs/utilities/recoverMessageAddress.md +++ b/site/pages/docs/utilities/recoverMessageAddress.md @@ -76,7 +76,7 @@ const address = await recoverMessageAddress({ ### signature -- **Type:** `Hex | ByteArray` +- **Type:** `Hex | ByteArray | Signature` The signature of the message. diff --git a/site/pages/docs/utilities/recoverPublicKey.md b/site/pages/docs/utilities/recoverPublicKey.md index 3613ba0dd8..a90096211d 100644 --- a/site/pages/docs/utilities/recoverPublicKey.md +++ b/site/pages/docs/utilities/recoverPublicKey.md @@ -41,7 +41,7 @@ const publicKey = await recoverPublicKey({ ### signature -- **Type:** `Hex | ByteArray` +- **Type:** `Hex | ByteArray | Signature` The signature of the hash. diff --git a/site/pages/docs/utilities/verifyMessage.md b/site/pages/docs/utilities/verifyMessage.md index dd97df393b..4803659bcc 100644 --- a/site/pages/docs/utilities/verifyMessage.md +++ b/site/pages/docs/utilities/verifyMessage.md @@ -101,7 +101,7 @@ const valid = await verifyMessage({ ### signature -- **Type:** `Hex | ByteArray` +- **Type:** `Hex | ByteArray | Signature` The signature that was generated by signing the message with the address's private key. diff --git a/site/pages/docs/utilities/verifyTypedData.md b/site/pages/docs/utilities/verifyTypedData.md index f4a411fa49..0a3cb89fcd 100644 --- a/site/pages/docs/utilities/verifyTypedData.md +++ b/site/pages/docs/utilities/verifyTypedData.md @@ -266,7 +266,7 @@ const valid = await verifyTypedData({ ### signature -- **Type:** `Hex | ByteArray` +- **Type:** `Hex | ByteArray | Signature` The signature of the typed data. diff --git a/site/pages/experimental/eip3074/recoverAuthMessageAddress.mdx b/site/pages/experimental/eip3074/recoverAuthMessageAddress.mdx index eee5a72e35..122a32980d 100644 --- a/site/pages/experimental/eip3074/recoverAuthMessageAddress.mdx +++ b/site/pages/experimental/eip3074/recoverAuthMessageAddress.mdx @@ -123,7 +123,7 @@ const address = await recoverAuthMessageAddress({ ### signature -- **Type:** `Hex | ByteArray` +- **Type:** `Hex | ByteArray | Signature` The signature that was generated by signing the message with the address's private key. diff --git a/src/actions/public/verifyHash.test.ts b/src/actions/public/verifyHash.test.ts index eb6d2015c5..7b21773227 100644 --- a/src/actions/public/verifyHash.test.ts +++ b/src/actions/public/verifyHash.test.ts @@ -6,6 +6,7 @@ import { anvilMainnet } from '../../../test/src/anvil.js' import type { Hex } from '../../types/misc.js' import { hashMessage, toBytes } from '../../utils/index.js' +import { hexToSignature } from '../../utils/signature/hexToSignature.js' import { verifyHash } from './verifyHash.js' const client = anvilMainnet.getClient() @@ -20,6 +21,15 @@ describe('verifyHash', async () => { '0xefd5fb29a274ea6682673d8b3caa9263e936d48d486e5df68893003e0a76496439594d12245008c6fba1c8e3ef28241cffe1bef27ff6bca487b167f261f329251c', expectedResult: true, }, + { + _name: 'deployed, supports ERC1271, valid signature, plaintext', + address: smartAccountConfig.address, + hash: hashMessage('This is a test message for viem!'), + signature: hexToSignature( + '0xefd5fb29a274ea6682673d8b3caa9263e936d48d486e5df68893003e0a76496439594d12245008c6fba1c8e3ef28241cffe1bef27ff6bca487b167f261f329251c', + ), + expectedResult: true, + }, { _name: 'deployed, supports ERC1271, invalid signature', address: smartAccountConfig.address, @@ -42,6 +52,15 @@ describe('verifyHash', async () => { '0xa461f509887bd19e312c0c58467ce8ff8e300d3c1a90b608a760c5b80318eaf15fe57c96f9175d6cd4daad4663763baa7e78836e067d0163e9a2ccf2ff753f5b1b', expectedResult: true, }, + { + _name: 'undeployed, with correct signature', + address: accounts[0].address, + hash: hashMessage('hello world'), + signature: hexToSignature( + '0xa461f509887bd19e312c0c58467ce8ff8e300d3c1a90b608a760c5b80318eaf15fe57c96f9175d6cd4daad4663763baa7e78836e067d0163e9a2ccf2ff753f5b1b', + ), + expectedResult: true, + }, { _name: 'undeployed, with wrong signature', address: address.notDeployed, diff --git a/src/actions/public/verifyHash.ts b/src/actions/public/verifyHash.ts index 93480bcd99..4826ade6d3 100644 --- a/src/actions/public/verifyHash.ts +++ b/src/actions/public/verifyHash.ts @@ -1,5 +1,6 @@ import type { Address } from 'abitype' +import { signatureToHex } from '../../accounts/index.js' import type { Client } from '../../clients/createClient.js' import type { Transport } from '../../clients/transports/createTransport.js' import { universalSignatureValidatorAbi } from '../../constants/abis.js' @@ -7,16 +8,18 @@ import { universalSignatureValidatorByteCode } from '../../constants/contracts.j import { CallExecutionError } from '../../errors/contract.js' import type { ErrorType } from '../../errors/utils.js' import type { Chain } from '../../types/chain.js' -import type { ByteArray, Hex } from '../../types/misc.js' -import type { EncodeDeployDataErrorType } from '../../utils/abi/encodeDeployData.js' +import type { ByteArray, Hex, Signature } from '../../types/misc.js' +import { + type EncodeDeployDataErrorType, + encodeDeployData, +} from '../../utils/abi/encodeDeployData.js' import { type IsBytesEqualErrorType, isBytesEqual, } from '../../utils/data/isBytesEqual.js' -import type { IsHexErrorType } from '../../utils/data/isHex.js' -import type { ToHexErrorType } from '../../utils/encoding/toHex.js' +import { type IsHexErrorType, isHex } from '../../utils/data/isHex.js' +import { type ToHexErrorType, bytesToHex } from '../../utils/encoding/toHex.js' import { getAction } from '../../utils/getAction.js' -import { encodeDeployData, isHex, toHex } from '../../utils/index.js' import { type CallErrorType, type CallParameters, call } from './call.js' export type VerifyHashParameters = Pick< @@ -28,7 +31,7 @@ export type VerifyHashParameters = Pick< /** The hash to be verified. */ hash: Hex /** The signature that was generated by signing the message with the address's private key. */ - signature: Hex | ByteArray + signature: Hex | ByteArray | Signature } export type VerifyHashReturnType = boolean @@ -52,7 +55,12 @@ export async function verifyHash( client: Client, { address, hash, signature, ...callRequest }: VerifyHashParameters, ): Promise { - const signatureHex = isHex(signature) ? signature : toHex(signature) + const signatureHex = (() => { + if (isHex(signature)) return signature + if (typeof signature === 'object' && 'r' in signature && 's' in signature) + return signatureToHex(signature) + return bytesToHex(signature) + })() try { const { data } = await getAction( diff --git a/src/actions/public/verifyMessage.ts b/src/actions/public/verifyMessage.ts index 696af3a338..038acbaec9 100644 --- a/src/actions/public/verifyMessage.ts +++ b/src/actions/public/verifyMessage.ts @@ -4,7 +4,12 @@ import type { Client } from '../../clients/createClient.js' import type { Transport } from '../../clients/transports/createTransport.js' import type { ErrorType } from '../../errors/utils.js' import type { Chain } from '../../types/chain.js' -import type { ByteArray, Hex, SignableMessage } from '../../types/misc.js' +import type { + ByteArray, + Hex, + SignableMessage, + Signature, +} from '../../types/misc.js' import { hashMessage } from '../../utils/index.js' import type { HashMessageErrorType } from '../../utils/signature/hashMessage.js' import { @@ -19,7 +24,7 @@ export type VerifyMessageParameters = Omit & { /** The message to be verified. */ message: SignableMessage /** The signature that was generated by signing the message with the address's private key. */ - signature: Hex | ByteArray + signature: Hex | ByteArray | Signature } export type VerifyMessageReturnType = boolean diff --git a/src/actions/public/verifyTypedData.ts b/src/actions/public/verifyTypedData.ts index 2b28deb15f..3eff94bcf0 100644 --- a/src/actions/public/verifyTypedData.ts +++ b/src/actions/public/verifyTypedData.ts @@ -4,7 +4,7 @@ import type { Client } from '../../clients/createClient.js' import type { Transport } from '../../clients/transports/createTransport.js' import type { ErrorType } from '../../errors/utils.js' import type { Chain } from '../../types/chain.js' -import type { ByteArray, Hex } from '../../types/misc.js' +import type { ByteArray, Hex, Signature } from '../../types/misc.js' import type { TypedDataDefinition } from '../../types/typedData.js' import { type HashTypedDataErrorType, @@ -24,7 +24,7 @@ export type VerifyTypedDataParameters< /** The address to verify the typed data for. */ address: Address /** The signature to verify */ - signature: Hex | ByteArray + signature: Hex | ByteArray | Signature } export type VerifyTypedDataReturnType = boolean diff --git a/src/experimental/eip3074/utils/recoverAuthMessageAddress.ts b/src/experimental/eip3074/utils/recoverAuthMessageAddress.ts index 88c9baf7a7..3a79ac4c12 100644 --- a/src/experimental/eip3074/utils/recoverAuthMessageAddress.ts +++ b/src/experimental/eip3074/utils/recoverAuthMessageAddress.ts @@ -1,6 +1,6 @@ import type { Address } from 'abitype' -import type { ByteArray, Hex } from '../../../types/misc.js' +import type { ByteArray, Hex, Signature } from '../../../types/misc.js' import type { ErrorType } from '../../../errors/utils.js' import { keccak256 } from '../../../utils/hash/keccak256.js' @@ -11,7 +11,7 @@ import { import { type ToAuthMessageParameters, toAuthMessage } from './toAuthMessage.js' export type RecoverAuthMessageAddressParameters = ToAuthMessageParameters & { - signature: Hex | ByteArray + signature: Hex | ByteArray | Signature } export type RecoverAuthMessageAddressReturnType = Address diff --git a/src/utils/signature/hexToSignature.test.ts b/src/utils/signature/hexToSignature.test.ts index 24f79601d8..6e8cdc85fa 100644 --- a/src/utils/signature/hexToSignature.test.ts +++ b/src/utils/signature/hexToSignature.test.ts @@ -45,4 +45,42 @@ test('default', () => { v: 27n, yParity: 0, }) + + expect( + hexToSignature( + '0xa461f509887bd19e312c0c58467ce8ff8e300d3c1a90b608a760c5b80318eaf15fe57c96f9175d6cd4daad4663763baa7e78836e067d0163e9a2ccf2ff753f5b00', + ), + ).toEqual({ + r: '0xa461f509887bd19e312c0c58467ce8ff8e300d3c1a90b608a760c5b80318eaf1', + s: '0x5fe57c96f9175d6cd4daad4663763baa7e78836e067d0163e9a2ccf2ff753f5b', + yParity: 0, + }) + + expect( + hexToSignature( + '0xc4d8bcda762d35ea79d9542b23200f46c2c1899db15bf929bbacaf609581db0831538374a01206517edd934e474212a0f1e2d62e9a01cd64f1cf94ea2e09884901', + ), + ).toEqual({ + r: '0xc4d8bcda762d35ea79d9542b23200f46c2c1899db15bf929bbacaf609581db08', + s: '0x31538374a01206517edd934e474212a0f1e2d62e9a01cd64f1cf94ea2e098849', + yParity: 1, + }) +}) + +test('invalid yParityOrV value', async () => { + expect(() => + hexToSignature( + '0x6e100a352ec6ad1b70802290e18aeed190704973570f3b8ed42cb9808e2ea6bf4a90a229a244495b41890987806fcbd2d5d23fc0dbe5f5256c2613c039d76db81d', + ), + ).toThrowErrorMatchingInlineSnapshot('[Error: Invalid yParityOrV value]') + expect(() => + hexToSignature( + '0x6e100a352ec6ad1b70802290e18aeed190704973570f3b8ed42cb9808e2ea6bf4a90a229a244495b41890987806fcbd2d5d23fc0dbe5f5256c2613c039d76db802', + ), + ).toThrowErrorMatchingInlineSnapshot('[Error: Invalid yParityOrV value]') + expect(() => + hexToSignature( + '0x6e100a352ec6ad1b70802290e18aeed190704973570f3b8ed42cb9808e2ea6bf4a90a229a244495b41890987806fcbd2d5d23fc0dbe5f5256c2613c039d76db81a', + ), + ).toThrowErrorMatchingInlineSnapshot('[Error: Invalid yParityOrV value]') }) diff --git a/src/utils/signature/hexToSignature.ts b/src/utils/signature/hexToSignature.ts index fb8b98bfa2..5252cf5ff8 100644 --- a/src/utils/signature/hexToSignature.ts +++ b/src/utils/signature/hexToSignature.ts @@ -21,11 +21,24 @@ export type HexToSignatureErrorType = NumberToHexErrorType | ErrorType */ export function hexToSignature(signatureHex: Hex) { const { r, s } = secp256k1.Signature.fromCompact(signatureHex.slice(2, 130)) - const v = BigInt(`0x${signatureHex.slice(130)}`) + const yParityOrV = Number(`0x${signatureHex.slice(130)}`) + const [v, yParity] = (() => { + if (yParityOrV === 0 || yParityOrV === 1) return [undefined, yParityOrV] + if (yParityOrV === 27) return [BigInt(yParityOrV), 0] + if (yParityOrV === 28) return [BigInt(yParityOrV), 1] + throw new Error('Invalid yParityOrV value') + })() + + if (typeof v !== 'undefined') + return { + r: numberToHex(r, { size: 32 }), + s: numberToHex(s, { size: 32 }), + v, + yParity, + } satisfies Signature return { r: numberToHex(r, { size: 32 }), s: numberToHex(s, { size: 32 }), - v, - yParity: v === 28n ? 1 : 0, + yParity, } satisfies Signature } diff --git a/src/utils/signature/recoverAddress.test.ts b/src/utils/signature/recoverAddress.test.ts index 2b6f227d0c..11176c550c 100644 --- a/src/utils/signature/recoverAddress.test.ts +++ b/src/utils/signature/recoverAddress.test.ts @@ -5,6 +5,7 @@ import { getAddress } from '../address/getAddress.js' import { toBytes } from '../encoding/toBytes.js' import { hashMessage } from './hashMessage.js' +import { hexToSignature } from './hexToSignature.js' import { recoverAddress } from './recoverAddress.js' test('default', async () => { @@ -16,6 +17,15 @@ test('default', async () => { }), ).toEqual(getAddress(accounts[0].address)) + expect( + await recoverAddress({ + hash: hashMessage('hello world'), + signature: hexToSignature( + '0xa461f509887bd19e312c0c58467ce8ff8e300d3c1a90b608a760c5b80318eaf15fe57c96f9175d6cd4daad4663763baa7e78836e067d0163e9a2ccf2ff753f5b1b', + ), + }), + ).toEqual(getAddress(accounts[0].address)) + expect( await recoverAddress({ hash: hashMessage('🥵'), @@ -24,6 +34,15 @@ test('default', async () => { }), ).toEqual(getAddress(accounts[0].address)) + expect( + await recoverAddress({ + hash: hashMessage('🥵'), + signature: hexToSignature( + '0x05c99bbbe9fac3ad61721a815d19d6771ad39f3e8dffa7ae7561358f20431d8e7f9e1d487c77355790c79c6eb0b0d63690f690615ef99ee3e4f25eef0317d0701b', + ), + }), + ).toEqual(getAddress(accounts[0].address)) + expect( await recoverAddress({ hash: hashMessage('hello world', 'bytes'), @@ -32,6 +51,15 @@ test('default', async () => { }), ).toEqual(getAddress(accounts[0].address)) + expect( + await recoverAddress({ + hash: hashMessage('hello world', 'bytes'), + signature: hexToSignature( + '0xa461f509887bd19e312c0c58467ce8ff8e300d3c1a90b608a760c5b80318eaf15fe57c96f9175d6cd4daad4663763baa7e78836e067d0163e9a2ccf2ff753f5b1b', + ), + }), + ).toEqual(getAddress(accounts[0].address)) + expect( await recoverAddress({ hash: hashMessage('🥵', 'bytes'), @@ -40,6 +68,15 @@ test('default', async () => { }), ).toEqual(getAddress(accounts[0].address)) + expect( + await recoverAddress({ + hash: hashMessage('🥵', 'bytes'), + signature: hexToSignature( + '0x05c99bbbe9fac3ad61721a815d19d6771ad39f3e8dffa7ae7561358f20431d8e7f9e1d487c77355790c79c6eb0b0d63690f690615ef99ee3e4f25eef0317d0701b', + ), + }), + ).toEqual(getAddress(accounts[0].address)) + expect( await recoverAddress({ hash: hashMessage('hello world', 'bytes'), diff --git a/src/utils/signature/recoverAddress.ts b/src/utils/signature/recoverAddress.ts index 304daa78b5..f9f2136380 100644 --- a/src/utils/signature/recoverAddress.ts +++ b/src/utils/signature/recoverAddress.ts @@ -1,14 +1,14 @@ import type { Address } from 'abitype' import { publicKeyToAddress } from '../../accounts/utils/publicKeyToAddress.js' -import type { ByteArray, Hex } from '../../types/misc.js' +import type { ByteArray, Hex, Signature } from '../../types/misc.js' import type { ErrorType } from '../../errors/utils.js' import { recoverPublicKey } from './recoverPublicKey.js' export type RecoverAddressParameters = { hash: Hex | ByteArray - signature: Hex | ByteArray + signature: Hex | ByteArray | Signature } export type RecoverAddressReturnType = Address diff --git a/src/utils/signature/recoverMessageAddress.ts b/src/utils/signature/recoverMessageAddress.ts index d9d9ba8397..c2f01ed963 100644 --- a/src/utils/signature/recoverMessageAddress.ts +++ b/src/utils/signature/recoverMessageAddress.ts @@ -1,6 +1,11 @@ import type { Address } from 'abitype' -import type { ByteArray, Hex, SignableMessage } from '../../types/misc.js' +import type { + ByteArray, + Hex, + SignableMessage, + Signature, +} from '../../types/misc.js' import type { ErrorType } from '../../errors/utils.js' import { type HashMessageErrorType, hashMessage } from './hashMessage.js' @@ -11,7 +16,7 @@ import { export type RecoverMessageAddressParameters = { message: SignableMessage - signature: Hex | ByteArray + signature: Hex | ByteArray | Signature } export type RecoverMessageAddressReturnType = Address diff --git a/src/utils/signature/recoverPublicKey.test.ts b/src/utils/signature/recoverPublicKey.test.ts index 8fa9a201dc..3462544467 100644 --- a/src/utils/signature/recoverPublicKey.test.ts +++ b/src/utils/signature/recoverPublicKey.test.ts @@ -5,6 +5,7 @@ import { privateKeyToAccount } from '../../accounts/privateKeyToAccount.js' import { toBytes } from '../encoding/toBytes.js' import { hashMessage } from './hashMessage.js' +import { hexToSignature } from './hexToSignature.js' import { recoverPublicKey } from './recoverPublicKey.js' test('default', async () => { @@ -16,6 +17,15 @@ test('default', async () => { }), ).toEqual(privateKeyToAccount(accounts[0].privateKey).publicKey) + expect( + await recoverPublicKey({ + hash: hashMessage('hello world'), + signature: hexToSignature( + '0xa461f509887bd19e312c0c58467ce8ff8e300d3c1a90b608a760c5b80318eaf15fe57c96f9175d6cd4daad4663763baa7e78836e067d0163e9a2ccf2ff753f5b1b', + ), + }), + ).toEqual(privateKeyToAccount(accounts[0].privateKey).publicKey) + expect( await recoverPublicKey({ hash: hashMessage('🥵'), @@ -24,6 +34,15 @@ test('default', async () => { }), ).toEqual(privateKeyToAccount(accounts[0].privateKey).publicKey) + expect( + await recoverPublicKey({ + hash: hashMessage('🥵'), + signature: hexToSignature( + '0x05c99bbbe9fac3ad61721a815d19d6771ad39f3e8dffa7ae7561358f20431d8e7f9e1d487c77355790c79c6eb0b0d63690f690615ef99ee3e4f25eef0317d0701b', + ), + }), + ).toEqual(privateKeyToAccount(accounts[0].privateKey).publicKey) + expect( await recoverPublicKey({ hash: hashMessage('hello world', 'bytes'), @@ -32,6 +51,15 @@ test('default', async () => { }), ).toEqual(privateKeyToAccount(accounts[0].privateKey).publicKey) + expect( + await recoverPublicKey({ + hash: hashMessage('hello world', 'bytes'), + signature: hexToSignature( + '0xa461f509887bd19e312c0c58467ce8ff8e300d3c1a90b608a760c5b80318eaf15fe57c96f9175d6cd4daad4663763baa7e78836e067d0163e9a2ccf2ff753f5b1b', + ), + }), + ).toEqual(privateKeyToAccount(accounts[0].privateKey).publicKey) + expect( await recoverPublicKey({ hash: hashMessage('🥵', 'bytes'), @@ -40,6 +68,15 @@ test('default', async () => { }), ).toEqual(privateKeyToAccount(accounts[0].privateKey).publicKey) + expect( + await recoverPublicKey({ + hash: hashMessage('🥵', 'bytes'), + signature: hexToSignature( + '0x05c99bbbe9fac3ad61721a815d19d6771ad39f3e8dffa7ae7561358f20431d8e7f9e1d487c77355790c79c6eb0b0d63690f690615ef99ee3e4f25eef0317d0701b', + ), + }), + ).toEqual(privateKeyToAccount(accounts[0].privateKey).publicKey) + expect( await recoverPublicKey({ hash: hashMessage('hello world', 'bytes'), @@ -57,6 +94,15 @@ test('default', async () => { }), ).toEqual(privateKeyToAccount(accounts[0].privateKey).publicKey) + expect( + await recoverPublicKey({ + hash: hashMessage('hello world'), + signature: hexToSignature( + '0xa461f509887bd19e312c0c58467ce8ff8e300d3c1a90b608a760c5b80318eaf15fe57c96f9175d6cd4daad4663763baa7e78836e067d0163e9a2ccf2ff753f5b00', + ), + }), + ).toEqual(privateKeyToAccount(accounts[0].privateKey).publicKey) + expect( await recoverPublicKey({ hash: '0x9a74cb859ad30835ffb2da406423233c212cf6dd78e6c2c98b0c9289568954ae', @@ -64,4 +110,34 @@ test('default', async () => { '0xc4d8bcda762d35ea79d9542b23200f46c2c1899db15bf929bbacaf609581db0831538374a01206517edd934e474212a0f1e2d62e9a01cd64f1cf94ea2e09884901', }), ).toEqual(privateKeyToAccount(accounts[0].privateKey).publicKey) + + expect( + await recoverPublicKey({ + hash: '0x9a74cb859ad30835ffb2da406423233c212cf6dd78e6c2c98b0c9289568954ae', + signature: hexToSignature( + '0xc4d8bcda762d35ea79d9542b23200f46c2c1899db15bf929bbacaf609581db0831538374a01206517edd934e474212a0f1e2d62e9a01cd64f1cf94ea2e09884901', + ), + }), + ).toEqual(privateKeyToAccount(accounts[0].privateKey).publicKey) +}) + +test('invalid yParityOrV value', async () => { + await expect(() => + recoverPublicKey({ + hash: hashMessage('hello world'), + signature: + '0xa461f509887bd19e312c0c58467ce8ff8e300d3c1a90b608a760c5b80318eaf15fe57c96f9175d6cd4daad4663763baa7e78836e067d0163e9a2ccf2ff753f5b1d', + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + '[Error: Invalid yParityOrV value]', + ) + await expect(() => + recoverPublicKey({ + hash: hashMessage('hello world'), + signature: + '0xa461f509887bd19e312c0c58467ce8ff8e300d3c1a90b608a760c5b80318eaf15fe57c96f9175d6cd4daad4663763baa7e78836e067d0163e9a2ccf2ff753f5b02', + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + '[Error: Invalid yParityOrV value]', + ) }) diff --git a/src/utils/signature/recoverPublicKey.ts b/src/utils/signature/recoverPublicKey.ts index bcce21524c..452383fa7e 100644 --- a/src/utils/signature/recoverPublicKey.ts +++ b/src/utils/signature/recoverPublicKey.ts @@ -1,12 +1,16 @@ import type { ErrorType } from '../../errors/utils.js' -import type { ByteArray, Hex } from '../../types/misc.js' +import type { ByteArray, Hex, Signature } from '../../types/misc.js' import { type IsHexErrorType, isHex } from '../data/isHex.js' -import { type HexToNumberErrorType, hexToNumber } from '../encoding/fromHex.js' +import { + type HexToNumberErrorType, + hexToBigInt, + hexToNumber, +} from '../encoding/fromHex.js' import { toHex } from '../encoding/toHex.js' export type RecoverPublicKeyParameters = { hash: Hex | ByteArray - signature: Hex | ByteArray + signature: Hex | ByteArray | Signature } export type RecoverPublicKeyReturnType = Hex @@ -20,20 +24,39 @@ export async function recoverPublicKey({ hash, signature, }: RecoverPublicKeyParameters): Promise { - const signatureHex = isHex(signature) ? signature : toHex(signature) const hashHex = isHex(hash) ? hash : toHex(hash) - // Derive v = recoveryId + 27 from end of the signature (27 is added when signing the message) - // The recoveryId represents the y-coordinate on the secp256k1 elliptic curve and can have a value [0, 1]. - let v = hexToNumber(`0x${signatureHex.slice(130)}`) - if (v === 0 || v === 1) v += 27 - const { secp256k1 } = await import('@noble/curves/secp256k1') - const publicKey = secp256k1.Signature.fromCompact( - signatureHex.substring(2, 130), - ) - .addRecoveryBit(v - 27) + const signature_ = (() => { + // typeof signature: `Signature` + if (typeof signature === 'object' && 'r' in signature && 's' in signature) { + const { r, s, v, yParity } = signature + const yParityOrV = Number(yParity ?? v)! + const recoveryBit = toRecoveryBit(yParityOrV) + return new secp256k1.Signature( + hexToBigInt(r), + hexToBigInt(s), + ).addRecoveryBit(recoveryBit) + } + + // typeof signature: `Hex | ByteArray` + const signatureHex = isHex(signature) ? signature : toHex(signature) + const yParityOrV = hexToNumber(`0x${signatureHex.slice(130)}`) + const recoveryBit = toRecoveryBit(yParityOrV) + return secp256k1.Signature.fromCompact( + signatureHex.substring(2, 130), + ).addRecoveryBit(recoveryBit) + })() + + const publicKey = signature_ .recoverPublicKey(hashHex.substring(2)) .toHex(false) return `0x${publicKey}` } + +function toRecoveryBit(yParityOrV: number) { + if (yParityOrV === 0 || yParityOrV === 1) return yParityOrV + if (yParityOrV === 27) return 0 + if (yParityOrV === 28) return 1 + throw new Error('Invalid yParityOrV value') +} diff --git a/src/utils/signature/recoverTransactionAddress.test.ts b/src/utils/signature/recoverTransactionAddress.test.ts index 11e0ebc053..09b02e1cda 100644 --- a/src/utils/signature/recoverTransactionAddress.test.ts +++ b/src/utils/signature/recoverTransactionAddress.test.ts @@ -92,7 +92,7 @@ test('4844 tx', async () => { const address = await recoverTransactionAddress({ serializedTransaction, - signature: signatureToHex(signature), + signature, }) expect(address.toLowerCase()).toBe(accounts[0].address) }) diff --git a/src/utils/signature/recoverTransactionAddress.ts b/src/utils/signature/recoverTransactionAddress.ts index 76716bd9ff..a622113dee 100644 --- a/src/utils/signature/recoverTransactionAddress.ts +++ b/src/utils/signature/recoverTransactionAddress.ts @@ -1,6 +1,6 @@ import type { Address } from 'abitype' import type { ErrorType } from '../../errors/utils.js' -import type { ByteArray, Hex } from '../../types/misc.js' +import type { ByteArray, Hex, Signature } from '../../types/misc.js' import type { TransactionSerialized } from '../../types/transaction.js' import { type Keccak256ErrorType, keccak256 } from '../hash/keccak256.js' import { parseTransaction } from '../transaction/parseTransaction.js' @@ -19,7 +19,7 @@ import { export type RecoverTransactionAddressParameters = { serializedTransaction: TransactionSerialized - signature?: Hex | ByteArray + signature?: Hex | ByteArray | Signature } export type RecoverTransactionAddressReturnType = Address diff --git a/src/utils/signature/recoverTypedDataAddress.ts b/src/utils/signature/recoverTypedDataAddress.ts index 6811adf7f5..1bc9b9deaa 100644 --- a/src/utils/signature/recoverTypedDataAddress.ts +++ b/src/utils/signature/recoverTypedDataAddress.ts @@ -1,6 +1,6 @@ import type { Address, TypedData } from 'abitype' -import type { ByteArray, Hex } from '../../types/misc.js' +import type { ByteArray, Hex, Signature } from '../../types/misc.js' import type { TypedDataDefinition } from '../../types/typedData.js' import type { ErrorType } from '../../errors/utils.js' @@ -14,7 +14,7 @@ export type RecoverTypedDataAddressParameters< typedData extends TypedData | Record = TypedData, primaryType extends keyof typedData | 'EIP712Domain' = keyof typedData, > = TypedDataDefinition & { - signature: Hex | ByteArray + signature: Hex | ByteArray | Signature } export type RecoverTypedDataAddressReturnType = Address diff --git a/src/utils/signature/verifyMessage.ts b/src/utils/signature/verifyMessage.ts index ac808705f0..c92f86e240 100644 --- a/src/utils/signature/verifyMessage.ts +++ b/src/utils/signature/verifyMessage.ts @@ -1,6 +1,11 @@ import type { Address } from 'abitype' -import type { ByteArray, Hex, SignableMessage } from '../../types/misc.js' +import type { + ByteArray, + Hex, + SignableMessage, + Signature, +} from '../../types/misc.js' import { type GetAddressErrorType, getAddress } from '../address/getAddress.js' import { type IsAddressEqualErrorType, @@ -19,7 +24,7 @@ export type VerifyMessageParameters = { /** The message to be verified. */ message: SignableMessage /** The signature that was generated by signing the message with the address's private key. */ - signature: Hex | ByteArray + signature: Hex | ByteArray | Signature } export type VerifyMessageReturnType = boolean diff --git a/src/utils/signature/verifyTypedData.ts b/src/utils/signature/verifyTypedData.ts index 51749aaeca..14914a95d1 100644 --- a/src/utils/signature/verifyTypedData.ts +++ b/src/utils/signature/verifyTypedData.ts @@ -1,6 +1,6 @@ import type { Address, TypedData } from 'abitype' -import type { ByteArray, Hex } from '../../types/misc.js' +import type { ByteArray, Hex, Signature } from '../../types/misc.js' import type { TypedDataDefinition } from '../../types/typedData.js' import { type GetAddressErrorType, getAddress } from '../address/getAddress.js' import { @@ -21,7 +21,7 @@ export type VerifyTypedDataParameters< /** The address to verify the typed data for. */ address: Address /** The signature to verify */ - signature: Hex | ByteArray + signature: Hex | ByteArray | Signature } export type VerifyTypedDataReturnType = boolean