@exodus/bip322-js
Version:
A Javascript library that provides utility functions related to the BIP-322 signature scheme
88 lines (87 loc) • 3.59 kB
JavaScript
import { fromBase58checkSync } from '@exodus/bytes/base58check.js';
import { hashSync } from '@exodus/crypto/hash';
import { recoverPublicKey } from '@noble/secp256k1';
import bech32 from 'bech32';
import varuint from 'varuint-bitcoin';
const SEGWIT_TYPES = {
P2WPKH: 'p2wpkh',
P2SH_P2WPKH: 'p2sh(p2wpkh)',
};
const hash256 = (buffer) => hashSync('sha256', hashSync('sha256', buffer), 'buffer');
const hash160 = (buffer) => hashSync('hash160', buffer, 'buffer');
const bufferEquals = (buffer1, buffer2) => {
return Buffer.compare(buffer1, buffer2) === 0;
};
function decodeSignature(buffer) {
if (buffer.length !== 65)
throw new Error('Invalid signature length');
const flagByte = buffer.readUInt8(0) - 27;
if (flagByte > 15 || flagByte < 0)
throw new Error('Invalid signature parameter');
return {
compressed: !!(flagByte & 12),
segwitType: flagByte & 8 ? (flagByte & 4 ? SEGWIT_TYPES.P2WPKH : SEGWIT_TYPES.P2SH_P2WPKH) : null,
recovery: flagByte & 3,
signature: buffer.slice(1),
};
}
function magicHash(message, messagePrefix = '\u0018Bitcoin Signed Message:\n') {
if (!Buffer.isBuffer(messagePrefix))
messagePrefix = Buffer.from(messagePrefix, 'utf8');
if (!Buffer.isBuffer(message))
message = Buffer.from(message, 'utf8');
const messageVISize = varuint.encodingLength(message.length);
const buffer = Buffer.alloc(messagePrefix.length + messageVISize + message.length);
messagePrefix.copy(buffer, 0);
varuint.encode(message.length, buffer, messagePrefix.length);
message.copy(buffer, messagePrefix.length + messageVISize);
return hash256(buffer);
}
function segwitRedeemHash(publicKeyHash) {
const redeemScript = Buffer.concat([Buffer.from('0014', 'hex'), publicKeyHash]);
return hash160(redeemScript);
}
function decodeBech32(address) {
const result = bech32.decode(address);
const data = bech32.fromWords(result.words.slice(1));
return Buffer.from(data);
}
export function verify(message, address, signature, messagePrefix, checkSegwitAlways) {
if (!Buffer.isBuffer(signature)) {
throw new TypeError('signature must be a Buffer');
}
const parsed = decodeSignature(signature);
if (checkSegwitAlways && !parsed.compressed) {
throw new Error('checkSegwitAlways can only be used with a compressed pubkey signature flagbyte');
}
const hash = magicHash(message, messagePrefix);
const publicKey = Buffer.from(recoverPublicKey(hash, parsed.signature, parsed.recovery, parsed.compressed));
const publicKeyHash = hash160(publicKey);
let actual, expected;
if (parsed.segwitType) {
if (parsed.segwitType === SEGWIT_TYPES.P2SH_P2WPKH) {
actual = segwitRedeemHash(publicKeyHash);
expected = Buffer.from(fromBase58checkSync(address).slice(1));
}
else {
actual = publicKeyHash;
expected = decodeBech32(address);
}
}
else if (checkSegwitAlways) {
try {
expected = decodeBech32(address);
return bufferEquals(publicKeyHash, expected);
}
catch {
const redeemHash = segwitRedeemHash(publicKeyHash);
expected = Buffer.from(fromBase58checkSync(address).slice(1));
return bufferEquals(publicKeyHash, expected) || bufferEquals(redeemHash, expected);
}
}
else {
actual = publicKeyHash;
expected = Buffer.from(fromBase58checkSync(address).slice(1));
}
return bufferEquals(actual, expected);
}