@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
text/typescript
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>;