@btc-vision/btc-runtime
Version:
Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.
398 lines (355 loc) • 12 kB
text/typescript
import { Potential } from '../lang/Definitions';
import { ADDRESS_BYTE_LENGTH, decodeHexArray, encodeHexFromBuffer } from '../utils';
import { Revert } from './Revert';
import { loadMLDSAPublicKey } from '../env/global';
import { MLDSASecurityLevel } from '../env/consensus/MLDSAMetadata';
import { ArrayLike } from '../interfaces/as';
/**
* Represents a 32-byte address in the OP_NET system.
*
* This class extends Uint8Array to provide a fixed-size 32-byte address with additional
* functionality for quantum-resistant cryptography. The address stores the SHA256 hash
* of an ML-DSA public key and provides lazy loading of the full ML-DSA public key when
* needed for signature verification.
*
* @remarks
* Addresses are immutable once set - attempting to modify an address after construction
* will throw an error. This ensures address integrity throughout the contract lifecycle.
*
* The class provides operator overloading for comparison operations, treating addresses
* as big-endian 256-bit integers for ordering purposes.
*
* @example
* ```typescript
* // Create from hex string
* const addr1 = Address.fromString('0x1234...abcd');
*
* // Create zero address
* const zero = Address.zero();
*
* // Compare addresses
* if (addr1 > zero) {
* // addr1 is greater than zero
* }
*
* // Get ML-DSA public key for verification
* const mldsaKey = addr1.mldsaPublicKey;
* ```
*/
export class Address extends Uint8Array {
/**
* Indicates whether the address has been initialized with data.
* Once true, the address becomes immutable.
*/
protected isDefined: boolean = false;
/**
* Creates a new Address instance.
*
* @param bytes - Optional 32-byte array representing the address.
* If empty or not provided, creates a zero address.
*
* @throws {Revert} If bytes length is not exactly 32
*/
public constructor(bytes: u8[] = []) {
super(ADDRESS_BYTE_LENGTH);
if (!(!bytes || bytes.length === 0)) {
this.newSet(bytes);
}
}
/**
* Cached ML-DSA public key, loaded lazily when first accessed.
*/
protected _mldsaPublicKey: Potential<Uint8Array> = null;
/**
* Gets the ML-DSA public key associated with this address.
*
* The full ML-DSA public key is loaded from storage on first access and cached
* for subsequent uses. This key is used for quantum-resistant signature verification
* when the consensus transitions away from Schnorr signatures.
*
* @returns The ML-DSA Level 2 (ML-DSA-44) public key for this address
*
* @remarks
* The address itself stores only the SHA256 hash of the ML-DSA public key.
* The full key (which is much larger, typically ~1312 bytes for Level 2)
* is stored separately and loaded on demand for efficiency.
*/
public get mldsaPublicKey(): Uint8Array {
if (!this._mldsaPublicKey) {
this._mldsaPublicKey = loadMLDSAPublicKey(this, MLDSASecurityLevel.Level2);
}
return this._mldsaPublicKey as Uint8Array;
}
/**
* Creates a zero address (all 32 bytes are 0x00).
*
* @returns A new Address instance with all bytes set to zero
*
* @example
* ```typescript
* const zero = Address.zero();
* console.log(zero.isZero()); // true
* ```
*/
public static zero(): Address {
return ZERO_ADDRESS.clone();
}
/**
* Creates an Address from a hexadecimal string representation.
*
* @param pubKey - The 32-byte address as a hexadecimal string.
* Can be prefixed with '0x' or unprefixed.
*
* @returns A new Address instance
*
* @throws {Error} If the decoded hex string is not exactly 32 bytes
* @throws {Error} If the string contains invalid hexadecimal characters
*
* @example
* ```typescript
* const addr1 = Address.fromString('0x' + '00'.repeat(32));
* const addr2 = Address.fromString('deadbeef'.repeat(8));
* ```
*/
public static fromString(pubKey: string): Address {
if (pubKey.startsWith('0x')) {
pubKey = pubKey.slice(2);
}
return new Address(decodeHexArray(pubKey));
}
/**
* Creates an Address from a Uint8Array using direct memory copy.
*
* This method is more efficient than the constructor for creating
* addresses from existing Uint8Array data as it uses direct memory
* copying instead of element-by-element copying.
*
* @param bytes - The source Uint8Array containing exactly 32 bytes
*
* @returns A new Address instance with data copied from the input
*
* @remarks
* The input array must be exactly ADDRESS_BYTE_LENGTH (32) bytes.
* This method performs a raw memory copy, so ensure the input is valid.
*/
public static fromUint8Array(bytes: Uint8Array): Address {
const cloned = new Address([]);
// Copy the raw memory directly:
memory.copy(cloned.dataStart, bytes.dataStart, ADDRESS_BYTE_LENGTH);
return cloned;
}
/**
* Checks if this address is the zero address.
*
* @returns `true` if all 32 bytes are zero, `false` otherwise
*
* @example
* ```typescript
* const addr = Address.zero();
* console.log(addr.isZero()); // true
* ```
*/
public isZero(): bool {
for (let i = 0; i < this.length; i++) {
if (this[i] != 0) {
return false;
}
}
return true;
}
/**
* Creates a deep copy of this Address.
*
* @returns A new Address instance with the same data and isDefined state
*
* @remarks
* Uses direct memory copy for efficiency. The cloned address maintains
* the same mutability state as the original.
*/
public clone(): Address {
const cloned = new Address([]);
// Copy the raw memory directly:
memory.copy(cloned.dataStart, this.dataStart, ADDRESS_BYTE_LENGTH);
// Duplicate the isDefined flag as well:
cloned.isDefined = this.isDefined;
return cloned;
}
/**
* Converts the address to a hexadecimal string representation.
*
* @returns The address as a lowercase hex string without '0x' prefix
*
* @example
* ```typescript
* const addr = Address.fromString('0xdead' + '00'.repeat(30));
* console.log(addr.toHex()); // "dead" + "00".repeat(30)
* ```
*/
public toHex(): string {
return encodeHexFromBuffer(this.buffer);
}
/**
* Checks equality with another address (operator ==).
*
* @param a - The address to compare with
* @returns `true` if both addresses have identical bytes, `false` otherwise
*/
public equals(a: Address): bool {
if (a.length != this.length) {
return false;
}
for (let i = 0; i < this.length; i++) {
if (this[i] != a[i]) {
return false;
}
}
return true;
}
/**
* Checks if this address is less than another (operator <).
*
* Comparison is done byte-by-byte treating addresses as big-endian 256-bit integers.
*
* @param a - The address to compare with
* @returns `true` if this address is numerically less than `a`
*/
public lessThan(a: Address): bool {
// Compare the two addresses byte-by-byte, treating them as big-endian uint256
for (let i = 0; i < 32; i++) {
const thisByte = this[i];
const aByte = a[i];
if (thisByte < aByte) {
return true; // this is less than a
} else if (thisByte > aByte) {
return false; // this is greater than or equal to a
}
}
return false;
}
/**
* Checks if this address is greater than another (operator >).
*
* Comparison is done byte-by-byte treating addresses as big-endian 256-bit integers.
*
* @param a - The address to compare with
* @returns `true` if this address is numerically greater than `a`
*/
public greaterThan(a: Address): bool {
// Compare the two addresses byte-by-byte, treating them as big-endian uint256
for (let i = 0; i < 32; i++) {
const thisByte = this[i];
const aByte = a[i];
if (thisByte > aByte) {
return true; // this is greater than a
} else if (thisByte < aByte) {
return false; // this is less than or equal to a
}
}
return false;
}
/**
* Checks if this address is less than or equal to another (operator <=).
*
* @param a - The address to compare with
* @returns `true` if this address is numerically less than or equal to `a`
*/
public lessThanOrEqual(a: Address): bool {
return this.lessThan(a) || this.equals(a);
}
/**
* Checks if this address is greater than or equal to another (operator >=).
*
* @param a - The address to compare with
* @returns `true` if this address is numerically greater than or equal to `a`
*/
public greaterThanOrEqual(a: Address): bool {
return this.greaterThan(a) || this.equals(a);
}
/**
* Checks inequality with another address (operator !=).
*
* @param a - The address to compare with
* @returns `true` if addresses have different bytes, `false` if identical
*/
public notEquals(a: Address): bool {
return !this.equals(a);
}
/**
* Returns the hexadecimal string representation of the address.
*
* @returns The address as a hex string (delegates to toHex())
*/
public override toString(): string {
return this.toHex();
}
/**
* Sets the address data and marks it as immutable.
*
* @param publicKey - The 32-byte public key data
*
* @throws {Revert} If publicKey length is not exactly 32 bytes
*
* @private
*/
private newSet<U extends ArrayLike<number>>(publicKey: U): void {
if (publicKey.length !== 32) {
throw new Revert(`Invalid public key length (${publicKey.length})`);
}
super.set(publicKey);
this.isDefined = true;
}
/**
* Array index getter (operator []).
*
* @param index - The byte index to access (0-31)
* @returns The byte value at the specified index
*
* @throws {RangeError} If index is out of bounds
*
* @private
*/
// @ts-ignore
private ___get(index: i32): u8 {
if (u32(index) >= u32(this.length)) {
throw new RangeError('Index out of range');
}
return load<u8>(this.dataStart + <usize>index);
}
/**
* Array index setter (operator []=).
*
* @param index - The byte index to set (0-31)
* @param value - The byte value to set
*
* @throws {Revert} If the address is already defined (immutable)
* @throws {RangeError} If index is out of bounds
*
* @private
*/
// @ts-ignore
private ___set(index: i32, value: u8): void {
if (this.isDefined) {
throw new Revert(`Cannot modify address data.`);
}
if (u32(index) >= u32(this.length)) {
throw new RangeError('Index out of range');
}
store<u8>(this.dataStart + <usize>index, value);
}
}
/**
* Pre-initialized zero address constant for efficiency.
*/
export const ZERO_ADDRESS: Address = new Address([]);
/**
* Type alias for nullable Address references.
*/
export declare type PotentialAddress = Potential<Address>;