UNPKG

@btc-vision/btc-runtime

Version:

Bitcoin Smart Contract Runtime

204 lines (175 loc) 6.88 kB
import { Blockchain } from '../env'; import { encodePointer } from '../math/abi'; import { bigEndianAdd } from '../math/bytes'; import { Revert } from '../types/Revert'; import { u256 } from '@btc-vision/as-bignum/assembly'; import { SafeMath } from '../types/SafeMath'; const MAX_LENGTH = <u32>u16.MAX_VALUE; const MAX_LENGTH_U256 = u256.fromU32(<u32>MAX_LENGTH); /** * @class StoredString * @description * Stores a string in a sequence of 32-byte storage slots, in UTF-8 format: * - Slot 0: first 4 bytes = length (big-endian), next 28 bytes = partial data * - Slot N>0: 32 bytes of data each * * The maximum is 65,535 bytes in UTF-8 form (not necessarily the same as code points). */ @final export class StoredString { private readonly subPointer: Uint8Array; constructor(public pointer: u16, index: u64 = 0) { const indexed = SafeMath.mul(u256.fromU64(index), MAX_LENGTH_U256); this.subPointer = indexed.toUint8Array(true).slice(0, 30); } private _value: string = ''; /** * Cached string value. If `_value` is empty, we call `load()` on first access. */ public get value(): string { if (!this._value) { this.load(); } return this._value; } public set value(v: string) { this._value = v; this.save(); } /** * Derives a 32-byte pointer for the given chunkIndex and performs big-endian addition. * chunkIndex=0 => header slot, 1 => second slot, etc. */ private getPointer(chunkIndex: u64): Uint8Array { const base = encodePointer(this.pointer, this.subPointer); return bigEndianAdd(base, chunkIndex); } /** * Reads the first slot and returns the stored byte length (big-endian). * Returns 0 if the slot is all zero. */ private getStoredLength(): u32 { const headerSlot = Blockchain.getStorageAt(this.getPointer(0)); const b0 = <u32>headerSlot[0]; const b1 = <u32>headerSlot[1]; const b2 = <u32>headerSlot[2]; const b3 = <u32>headerSlot[3]; return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3; } /** * Clears old data from storage. Based on `oldLength`, determines how many slots * were used, and writes zeroed 32-byte arrays to each. */ private clearOldStorage(oldLength: u32): void { if (oldLength == 0) { return; } // We always use at least 1 slot (the header slot). let chunkCount: u64 = 1; // In the header slot, we can store up to 28 bytes of data. const remaining = oldLength > 28 ? oldLength - 28 : 0; if (remaining > 0) { // Each additional chunk is 32 bytes. // Use integer math ceiling: (remaining + 32 - 1) / 32 chunkCount += (remaining + 32 - 1) / 32; } // Zero out each previously used slot for (let i: u64 = 0; i < chunkCount; i++) { Blockchain.setStorageAt(this.getPointer(i), new Uint8Array(32)); } } /** * Saves the current string to storage in UTF-8 form. */ private save(): void { // 1) Clear old data const oldLen = this.getStoredLength(); this.clearOldStorage(oldLen); // 2) Encode new string as UTF-8 const utf8Data = String.UTF8.encode(this._value, false); const length = <u32>utf8Data.byteLength; // Enforce max length if (length > MAX_LENGTH) { throw new Revert(`StoredString: value is too long (max=${MAX_LENGTH})`); } // 3) If new string is empty, just store a zeroed header and return if (length == 0) { // A zeroed 32-byte array => indicates length=0 Blockchain.setStorageAt(this.getPointer(0), new Uint8Array(32)); return; } // 4) Write the first slot: length + up to 28 bytes let remaining: u32 = length; let offset: u32 = 0; const firstSlot = new Uint8Array(32); firstSlot[0] = <u8>((length >> 24) & 0xff); firstSlot[1] = <u8>((length >> 16) & 0xff); firstSlot[2] = <u8>((length >> 8) & 0xff); firstSlot[3] = <u8>(length & 0xff); const bytes = Uint8Array.wrap(utf8Data); const firstChunkSize = remaining < 28 ? remaining : 28; for (let i: u32 = 0; i < firstChunkSize; i++) { firstSlot[4 + i] = bytes[i]; } Blockchain.setStorageAt(this.getPointer(0), firstSlot); remaining -= firstChunkSize; offset += firstChunkSize; // 5) Write subsequent slots (32 bytes each) let chunkIndex: u64 = 1; while (remaining > 0) { const slotData = new Uint8Array(32); const chunkSize = remaining < u32(32) ? remaining : u32(32); for (let i: u32 = 0; i < chunkSize; i++) { slotData[i] = bytes[offset + i]; } Blockchain.setStorageAt(this.getPointer(chunkIndex), slotData); remaining -= chunkSize; offset += chunkSize; chunkIndex++; } } /** * Loads the string from storage by reading the stored byte length, then decoding * the corresponding UTF-8 data from the slots. */ private load(): void { // Read the header slot first const headerSlot = Blockchain.getStorageAt(this.getPointer(0)); // Parse the big-endian length const b0 = <u32>headerSlot[0]; const b1 = <u32>headerSlot[1]; const b2 = <u32>headerSlot[2]; const b3 = <u32>headerSlot[3]; const length: u32 = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3; // If length=0, then the string is empty if (length == 0) { this._value = ''; return; } // Read the UTF-8 bytes from storage let remaining: u32 = length; let offset: u32 = 0; const out = new Uint8Array(length); // First slot can hold up to 28 bytes after the length const firstChunkSize = remaining < 28 ? remaining : 28; for (let i: u32 = 0; i < firstChunkSize; i++) { out[i] = headerSlot[4 + i]; } remaining -= firstChunkSize; offset += firstChunkSize; // Read the subsequent slots of 32 bytes each let chunkIndex: u64 = 1; while (remaining > 0) { const slotData = Blockchain.getStorageAt(this.getPointer(chunkIndex)); const chunkSize = remaining < 32 ? remaining : 32; for (let i: u32 = 0; i < chunkSize; i++) { out[offset + i] = slotData[i]; } remaining -= chunkSize; offset += chunkSize; chunkIndex++; } // Decode UTF-8 into a normal string this._value = String.UTF8.decode(out.buffer, false); } }