import { toHex } from "viem" import BitcoinAddressHelper from "./address" /** * Normalizes a signature's `v` value (as an already-parsed number) to a * 4-number range starting at `rangeMin`, optionally also setting the high bit * of the value. * * `v` values are single bytes, so they have a max value of 254. The real value * produced for this byte by ECDSA signatures is typically 0, 1, 2, or 3. * Bitcoin encodes certain address information in signatures by offsetting the * byte. In particular, BIP137 specifies that `v` bytes represent: * - p2pkh with an uncompressed public key in the range [27,30] * - p2pkh with a compressed public key in the range [31,34] * - p2sh-p2wpkh (nested segwit) in the range [35,38] * - p2wpkh (native segwit) in the range [39,43] * * Additionally, OrangeKit sets the high bit of the `v` value to indicate that a * signature represents a signature of a string representation of a hex value, * rather than the bytes the hex value encodes. For example, if a signature is * requested over `0x1234`, the high `v` bit indicates whether the signature was * performed over the 2 bytes `0x12` and `0x34`, or the 6 bytes representing the * characters `0`, `x`, `1`, `2`, `3`, and `4`. Whether this is necessary will * depend on the signing wallet. * * This function takes any `v` value in any of those ranges and normalizes it to * the requested range (indicated by its minimum value), setting its high bit if * `setHighBit` is set to `true`. * * @return The updated `v` value as a `number`. */ function normalizeV( v: number, rangeMin: number, setHighBit: boolean = false, ): number { // Assumption: a `v` that is <27 must be [0,3] when evaluated mod 4. If it // isn't, someone has done something strange and our additional manipulations // will just make it stranger. // // We normalize here to that [0,3] range. const baseV = (v >= 27 ? v - 27 : v) % 4 // Normalize to the range [rangeMin, rangeMin + 3] const rangeNormalizedV = baseV + rangeMin // eslint-disable-next-line no-bitwise return setHighBit ? rangeNormalizedV | 0b1000_0000 : rangeNormalizedV } /** * Normalizes a signature by updating the `v` value for a given bitcoin address. * @see {normalizeV} for more details. * * @param signature Message signature. * @param bitcoinAddress The bitcoin address that signed the message. * @param publicKey The public key of the bitcoin address used to sign the * message. * * @throws An error when the address is not supported. * @returns The normalized signature as hex string with the updated `v` value. */ function normalizeSignature( signature: Buffer, bitcoinAddress: string, publicKey: string, ) { const normalizedSignature = signature const v = normalizedSignature[0] let normalizedV if (BitcoinAddressHelper.isP2WPKHAddress(bitcoinAddress)) { // For p2wpkh, normalize to the 39-42 range specified by BIP137. normalizedV = normalizeV(v, 39) } else if (BitcoinAddressHelper.isP2PKHAddress(bitcoinAddress)) { // For p2pkh, assume that an uncompressed p2pkh signature will already be // in the right range, and normalize any others to the 31-34 range // specified by BIP137. normalizedV = // BIP137 range for uncompressed p2pkh v >= 27 && v <= 30 ? v : // BIP137 range for compressed p2pkh normalizeV(v, 31) } else if ( BitcoinAddressHelper.isNestedSegwitAddress(bitcoinAddress, publicKey) ) { normalizedV = normalizeV(v, 35) } else { throw new Error("Unsupported Bitcoin address type") } normalizedSignature[0] = normalizedV return toHex(normalizedSignature) } export default { normalizeV, normalizeSignature, }