UNPKG

@btc-vision/btc-runtime

Version:

Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.

440 lines (407 loc) 15.7 kB
import { Potential } from '../lang/Definitions'; import { decodeHexArray } from '../utils'; import { Blockchain } from '../env'; import { Network } from '../script/Networks'; import { Address } from './Address'; import { BitcoinAddresses } from '../script/BitcoinAddresses'; import { DEAD_ARRAY, getCachedDeadAddress, getCachedZeroAddress, setCachedDeadAddress, setCachedZeroAddress, ZERO_ARRAY, } from './ExtendedAddressCache'; import { Revert } from './Revert'; /** * Extended address implementation for Bitcoin with dual-key support. * * ExtendedAddress combines both Schnorr (taproot) and ML-DSA public keys to provide * a migration path from classical to quantum-resistant cryptography. The address * stores the ML-DSA public key hash (inherited from Address) and additionally * maintains the tweaked Schnorr public key for Bitcoin taproot compatibility. * * @remarks * This class is marked as @final and cannot be extended. It represents the complete * address format for OP_NET's quantum-resistant transition, supporting both legacy * Schnorr signatures and future ML-DSA signatures within the same address structure. * * The tweaked public key enables P2TR (pay-to-taproot) address generation while * the ML-DSA key hash provides quantum resistance when consensus transitions. * * @example * ```typescript * // Create from hex strings * const addr = ExtendedAddress.fromStringPair( * '0x' + '11'.repeat(32), // tweaked Schnorr key * '0x' + '22'.repeat(32) // ML-DSA key hash * ); * * // Generate P2TR address * const bitcoinAddr = addr.p2tr(); * * // Check if address is dead/zero * if (addr.isDead()) { * // Handle dead address * } * ``` */ @final export class ExtendedAddress extends Address { /** * The 32-byte tweaked Schnorr public key for taproot compatibility. * This key is used for P2TR address generation and Schnorr signature verification. */ public readonly tweakedPublicKey: Uint8Array; /** * Creates a new ExtendedAddress instance. * * @param tweakedPublicKey - 32-byte tweaked Schnorr public key for taproot * @param publicKey - 32-byte ML-DSA public key hash (SHA256 of full ML-DSA key) * * @throws {Revert} If tweakedPublicKey is not exactly 32 bytes * @throws {Revert} If publicKey is not exactly 32 bytes (thrown by parent class) */ public constructor(tweakedPublicKey: u8[], publicKey: u8[]) { super(publicKey); if (tweakedPublicKey.length !== 32) { throw new Revert('Tweaked public key must be 32 bytes long'); } this.tweakedPublicKey = new Uint8Array(32); this.tweakedPublicKey.set(tweakedPublicKey); } /** * Returns the canonical dead address. * * The dead address (284ae4acdb32a99ba3ebfa66a91ddb41a7b7a1d2fef415399922cd8a04485c02) * is generated from the uncompressed public key: * 04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f * * This address is commonly used as a burn address or null recipient in contracts. * * @returns A clone of the predefined DEAD_ADDRESS constant * * @example * ```typescript * const burnAddr = ExtendedAddress.dead(); * if (recipient.isDead()) { * // Tokens are being burned * } * ``` */ public static dead(): ExtendedAddress { let cached = getCachedDeadAddress(); if (cached === 0) { const addr = new ExtendedAddress(DEAD_ARRAY, ZERO_ARRAY); cached = changetype<usize>(addr); setCachedDeadAddress(cached); } return changetype<ExtendedAddress>(cached).clone(); } /** * Returns the zero address with all bytes set to 0x00. * * @returns A clone of the predefined ZERO_BITCOIN_ADDRESS constant * * @example * ```typescript * const zero = ExtendedAddress.zero(); * if (addr.isZero()) { * // Handle uninitialized address * } * ``` */ public static override zero(): ExtendedAddress { let cached = getCachedZeroAddress(); if (cached === 0) { const addr = new ExtendedAddress(ZERO_ARRAY, ZERO_ARRAY); cached = changetype<usize>(addr); setCachedZeroAddress(cached); } return changetype<ExtendedAddress>(cached).clone(); } /** * Disabled method - use fromStringPair instead. * * This method is intentionally disabled for ExtendedAddress to prevent * accidental creation with only one key. ExtendedAddress requires both * the tweaked Schnorr key and ML-DSA key hash for proper functionality. * * @param _ - Unused parameter * * @throws {Error} Always throws with instruction to use fromStringPair * * @deprecated Use {@link fromStringPair} instead */ public static override fromString(_: string): ExtendedAddress { ERROR( `Use ExtendedAddress.fromStringPair instead. This method is disabled. You must provide both the tweaked public key and the ML-DSA public key.`, ); } /** * Creates an ExtendedAddress from hexadecimal string representations of both keys. * * This is the preferred factory method for creating ExtendedAddress instances when * both the Schnorr tweaked public key and ML-DSA public key are available as strings. * Unlike the single-parameter fromString method (which maintains backward compatibility * with the base Address class), this method properly initializes both key components * required for full ExtendedAddress functionality. * * @param tweakedPubKey - The 32-byte Schnorr tweaked public key as a hexadecimal string. * Can be prefixed with '0x' or unprefixed. Must decode to exactly * 32 bytes for taproot compatibility. * @param mldsaPubKey - The 32-byte ML-DSA public key hash (SHA256 of the full ML-DSA public key) * as a hexadecimal string. Can be prefixed with '0x' or unprefixed. * Must decode to exactly 32 bytes for address derivation. * * @returns A new ExtendedAddress instance with both keys properly initialized * * @throws {Error} If tweakedPubKey doesn't decode to exactly 32 bytes * @throws {Error} If mldsaPubKey doesn't decode to exactly 32 bytes * @throws {Error} If either string contains invalid hexadecimal characters * * @example * ```typescript * // With 0x prefix * const addr1 = ExtendedAddress.fromStringPair( * '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', * '0xfedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210' * ); * * // Without 0x prefix * const addr2 = ExtendedAddress.fromStringPair( * '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', * 'fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210' * ); * ``` * * @remarks * For quantum-resistant applications, ensure the mldsaPubKey parameter is the SHA256 hash * of a valid ML-DSA-44 (Level 2) public key. The full ML-DSA public key will be loaded * from storage when needed for signature verification. * * @see {@link fromUint8Array} for binary initialization with concatenated keys */ public static fromStringPair(tweakedPubKey: string, mldsaPubKey: string): ExtendedAddress { if (tweakedPubKey.startsWith('0x')) { tweakedPubKey = tweakedPubKey.slice(2); } if (mldsaPubKey.startsWith('0x')) { mldsaPubKey = mldsaPubKey.slice(2); } return new ExtendedAddress(decodeHexArray(tweakedPubKey), decodeHexArray(mldsaPubKey)); } /** * Creates an ExtendedAddress from a concatenated 64-byte array. * * The input must contain exactly 64 bytes: the first 32 bytes are the tweaked * Schnorr public key, followed by 32 bytes of the ML-DSA public key hash. * * @param bytes - A 64-byte Uint8Array containing both keys concatenated * * @returns A new ExtendedAddress instance * * @throws {Error} If the input is not exactly 64 bytes * * @example * ```typescript * const combined = new Uint8Array(64); * combined.set(tweakedKey, 0); * combined.set(mldsaKeyHash, 32); * const addr = ExtendedAddress.fromUint8Array(combined); * ``` */ public static override fromUint8Array(bytes: Uint8Array): ExtendedAddress { if (bytes.length !== 64) { throw new Error('Expected 64 bytes: 32 for tweakedPublicKey, 32 for publicKey'); } const tweakedPublicKey: u8[] = new Array<u8>(32); const publicKey: u8[] = new Array<u8>(32); for (let i = 0; i < 32; i++) { tweakedPublicKey[i] = bytes[i]; publicKey[i] = bytes[32 + i]; } return new ExtendedAddress(tweakedPublicKey, publicKey); } /** * Generates a CSV (CheckSequenceVerify) timelocked P2WSH address. * * Creates a Bitcoin address that requires both the specified number of blocks * to pass and the correct signature to spend funds. Used for timelocked * liquidity provisions in NativeSwap and similar protocols. * * @param address - The address bytes to create CSV lock for * @param blocks - Number of blocks for the CSV timelock * * @returns The generated P2WSH address string with CSV timelock * * @example * ```typescript * // Create 144-block (approximately 1 day) timelock * const timelocked = ExtendedAddress.toCSV(addr.toBytes(), 144); * ``` */ public static toCSV(address: Uint8Array, blocks: u32): string { return BitcoinAddresses.csvP2wshAddress(address, blocks, Network.hrp(Blockchain.network)) .address; } /** * Generates a P2WPKH (pay-to-witness-public-key-hash) address. * * Creates a native SegWit address (bc1...) from the provided address bytes. * This is the standard Bitcoin address format for SegWit transactions. * * @param address - The address bytes to encode * * @returns The bech32-encoded P2WPKH address string * * @example * ```typescript * const segwitAddr = ExtendedAddress.p2wpkh(addr.toBytes()); * // Returns: "bc1q..." on mainnet or "tb1q..." on testnet * ``` */ public static p2wpkh(address: Uint8Array): string { return BitcoinAddresses.p2wpkh(address, Network.hrp(Blockchain.network)); } /** * Downcasts this ExtendedAddress to a base Address. * * Returns this instance as the base Address type, maintaining only * the ML-DSA public key hash and losing access to the tweaked key. * * @returns This instance cast as Address * * @remarks * The returned Address still contains the same data but without * access to ExtendedAddress-specific methods and the tweaked key. */ public downCast(): Address { return this; } /** * Checks if all bytes of the ML-DSA key hash are zero. * * @returns `true` if the ML-DSA key hash is all zeros, `false` otherwise * * @remarks * This only checks the ML-DSA key hash portion (inherited from Address), * not the tweaked Schnorr key. Use ZERO_BITCOIN_ADDRESS for a fully * zero ExtendedAddress. */ public override isZero(): bool { for (let i = 0; i < this.length; i++) { if (this[i] != 0) { return false; } } return true; } /** * Checks if this address equals the canonical dead address. * * @returns `true` if this address matches the dead address, `false` otherwise * * @example * ```typescript * if (recipient.isDead()) { * // Funds are being burned * return; * } * ``` */ public isDead(): bool { // Use cached dead address for comparison const deadAddr = ExtendedAddress.dead(); // Compare both ML-DSA key hash (this) and tweaked key for (let i = 0; i < this.length; i++) { if (this[i] != deadAddr[i]) { return false; } } for (let i = 0; i < this.tweakedPublicKey.length; i++) { if (this.tweakedPublicKey[i] != deadAddr.tweakedPublicKey[i]) { return false; } } return true; } /** * Generates the P2TR (pay-to-taproot) address for this ExtendedAddress. * * Uses the tweaked Schnorr public key to create a taproot address * following BIP341 specifications. This is the primary Bitcoin address * format for this ExtendedAddress. * * @returns The bech32m-encoded P2TR address string * * @example * ```typescript * const taprootAddr = addr.p2tr(); * // Returns: "bc1p..." on mainnet or "tb1p..." on testnet * ``` * * @remarks * The address is generated from the tweaked public key only. * The ML-DSA key hash is not used in P2TR address generation. */ public p2tr(): string { return BitcoinAddresses.p2trKeyPathAddress( this.tweakedPublicKey, Network.hrp(Blockchain.network), ); } /** * Returns the P2TR address string representation. * * @returns The P2TR address (delegates to p2tr()) * * @override Overrides the base Address.toString() which returns hex */ public override toString(): string { return this.p2tr(); } /** * Creates a deep copy of this ExtendedAddress. * * @returns A new ExtendedAddress instance with identical data * * @override Overrides Address.clone() to return ExtendedAddress type * * @remarks * Both the tweaked Schnorr key and ML-DSA key hash are cloned. * The isDefined flag state is also preserved in the clone. */ public override clone(): ExtendedAddress { // Convert Uint8Array to u8[] for the tweakedPublicKey const tweakedKeyArray: u8[] = new Array<u8>(this.tweakedPublicKey.length); for (let i = 0; i < this.tweakedPublicKey.length; i++) { tweakedKeyArray[i] = this.tweakedPublicKey[i]; } // Convert the address bytes (this.slice(0)) to u8[] const addressSlice = this.slice(0); const addressArray: u8[] = new Array<u8>(addressSlice.length); for (let i = 0; i < addressSlice.length; i++) { addressArray[i] = addressSlice[i]; } const cloned = new ExtendedAddress(tweakedKeyArray, addressArray); // Duplicate the isDefined flag as well: cloned.isDefined = this.isDefined; return cloned; } } /** * Pre-initialized zero ExtendedAddress constant. * Both the tweaked key and ML-DSA key hash are all zeros. */ export const ZERO_BITCOIN_ADDRESS: ExtendedAddress = new ExtendedAddress(ZERO_ARRAY, ZERO_ARRAY); /** * Pre-initialized dead ExtendedAddress constant. * The tweaked key is zero while the ML-DSA key hash represents the canonical dead address. * Hash: 284ae4acdb32a99ba3ebfa66a91ddb41a7b7a1d2fef415399922cd8a04485c02 */ export const DEAD_ADDRESS: ExtendedAddress = new ExtendedAddress(DEAD_ARRAY, ZERO_ARRAY); /** * Type alias for nullable ExtendedAddress references. */ export declare type PotentialBitcoinAddress = Potential<ExtendedAddress>;