@btc-vision/btc-runtime
Version:
Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.
423 lines (356 loc) • 11.9 kB
text/typescript
import { BytesWriter } from '../../buffer/BytesWriter';
import { Blockchain } from '../../env';
import { Revert } from '../../types/Revert';
import {
addUint8ArraysBE,
bigEndianAdd,
GET_EMPTY_BUFFER,
getBit,
readLengthAndStartIndex,
setBit,
u64ToBE32Bytes,
} from '../../math/bytes';
import { DEFAULT_MAX_LENGTH } from './StoredPackedArray';
import { encodePointer } from '../../math/abi';
/**
* @class StoredBooleanArray
* Manages an array of boolean values across multiple storage slots.
* Each slot is a 32-byte Uint8Array, each containing 256 bits (1 bit per boolean).
*
* For example:
* - slot 0 stores indexes [0..255]
* - slot 1 stores indexes [256..511]
* - ...
* Note: This is designed to wrap around.
*/
export class StoredBooleanArray {
private readonly basePointer: Uint8Array;
private readonly lengthPointer: Uint8Array;
private _values: Map<u32, Uint8Array> = new Map();
private _isChanged: Set<u32> = new Set();
private _length: u32 = 0;
private _startIndex: u32 = 0;
private _isChangedLength: bool = false;
private _isChangedStartIndex: bool = false;
private nextItemOffset: u32 = 0;
/**
* @constructor
* @param {u16} pointer - The primary pointer identifier.
* @param {Uint8Array} subPtr - The sub-pointer for memory slot addressing.
* @param {u32} [MAX_LENGTH=DEFAULT_MAX_LENGTH] - The maximum length of the array.
*
* The code below treats the first 16 bytes of `lengthPointer` as storing [length, startIndex].
*/
constructor(
public pointer: u16,
public subPtr: Uint8Array,
protected MAX_LENGTH: u32 = DEFAULT_MAX_LENGTH,
) {
const basePointer = encodePointer(pointer, subPtr, true, 'StoredBooleanArray');
this.lengthPointer = Uint8Array.wrap(basePointer.buffer);
this.basePointer = bigEndianAdd(basePointer, 1);
const storedLenStart = Blockchain.getStorageAt(basePointer);
const data = readLengthAndStartIndex(storedLenStart);
this._length = data[0];
this._startIndex = data[1];
}
public get previousOffset(): u32 {
return <u32>(
((this._startIndex +
<u64>(this.nextItemOffset === 0 ? this.nextItemOffset : this.nextItemOffset - 1)) %
this.MAX_LENGTH)
);
}
/**
* Set the maximum length of the array.
* This is a safety check to prevent unbounded usage.
*/
public setMaxLength(maxLength: u32): void {
if (maxLength > this.MAX_LENGTH) {
throw new Revert('setMaxLength: maxLength exceeds MAX_LENGTH');
}
this.MAX_LENGTH = maxLength;
}
public has(index: u32): bool {
return index < this._length;
}
/**
* Get the next item in the array, starting from the current offset.
* This is useful for iterating through the array.
*/
public next(): bool {
const value = this.get(this.nextItemOffset);
this.nextItemOffset += 1;
return value;
}
/**
* Apply the starting index with n offset.
*/
public applyNextOffsetToStartingIndex(): void {
if (!this.nextItemOffset) return;
this._startIndex += this.nextItemOffset - 1;
this._isChangedStartIndex = true;
this.nextItemOffset = 0;
}
public incrementStartingIndex(): void {
if (this._startIndex >= this.MAX_LENGTH) {
this._startIndex = 0;
} else {
this._startIndex += 1;
}
this._isChangedStartIndex = true;
}
/**
* Retrieve boolean at `index`.
*/
public get(index: u32): bool {
if (index >= this._length) {
throw new Revert(
`get: index out of range (${index} >= ${this._length}, boolean array)`,
);
}
const wrappedIndex = this.getRealIndex(index);
const slotIndex = wrappedIndex / 256;
const bitIndex = <u16>(wrappedIndex % 256);
this.ensureSlotLoaded(slotIndex);
const slotValue = this._values.get(slotIndex);
return slotValue ? getBit(slotValue, bitIndex) : false;
}
/**
* Set boolean at `index`.
*/
public set(index: u32, value: bool): void {
if (index >= this._length) {
throw new Revert(
`set: index out of range (${index} >= ${this._length}, boolean array)`,
);
}
const wrappedIndex = this.getRealIndex(index);
const slotIndex = wrappedIndex / 256;
const bitIndex = <u16>(wrappedIndex % 256);
this.ensureSlotLoaded(slotIndex);
const slotValue = this._values.get(slotIndex);
if (slotValue) {
const oldVal = getBit(slotValue, bitIndex);
if (oldVal != value) {
setBit(slotValue, bitIndex, value);
this._isChanged.add(slotIndex);
}
}
}
/**
* Push a new boolean at the "end" of the array.
*/
public push(value: bool): u32 {
if (this._length >= this.MAX_LENGTH) {
throw new Revert('push: reached max allowed length (boolean array)');
}
const wrappedIndex = this.getRealIndex(this._length);
const slotIndex = wrappedIndex / 256;
const bitIndex = <u16>(wrappedIndex % 256);
this.ensureSlotLoaded(slotIndex);
const slotValue = this._values.get(slotIndex);
if (slotValue) {
setBit(slotValue, bitIndex, value);
this._isChanged.add(slotIndex);
}
this._length += 1;
this._isChangedLength = true;
return wrappedIndex;
}
/**
* Delete the boolean at `index` by setting it to false.
*/
public delete(index: u32): void {
if (index >= this._length) {
throw new Revert('delete: index out of range (boolean array)');
}
const wrappedIndex = this.getRealIndex(index);
const slotIndex = wrappedIndex / 256;
const bitIndex = <u16>(wrappedIndex % 256);
this.ensureSlotLoaded(slotIndex);
const slotValue = this._values.get(slotIndex);
if (slotValue) {
const oldVal = getBit(slotValue, bitIndex);
if (oldVal) {
setBit(slotValue, bitIndex, false);
this._isChanged.add(slotIndex);
}
}
}
public removeItemFromLength(): void {
if (this._length == 0) {
throw new Revert('delete: array is empty');
}
this._length -= 1;
this._isChangedLength = true;
}
/**
* Remove the last element by setting it false and decrementing length.
*/
public deleteLast(): void {
if (this._length === 0) {
throw new Revert('deleteLast: array is empty');
}
const lastIndex = this._length - 1;
this.delete(lastIndex);
this._length -= 1;
this._isChangedLength = true;
}
/**
* Commit any changed slots to storage, as well as length / startIndex if changed.
*/
public save(): void {
const changed = this._isChanged.values();
for (let i = 0; i < changed.length; i++) {
const slotIndex = changed[i];
const slotValue = this._values.get(slotIndex);
const storagePointer = this.calculateStoragePointer(slotIndex);
Blockchain.setStorageAt(storagePointer, slotValue);
}
this._isChanged.clear();
if (this._isChangedLength || this._isChangedStartIndex) {
const w = new BytesWriter(32);
w.writeU32(this._length);
w.writeU32(this._startIndex);
const data = w.getBuffer();
Blockchain.setStorageAt(this.lengthPointer, data);
this._isChangedLength = false;
this._isChangedStartIndex = false;
}
}
/**
* Delete all slots in storage (that are loaded) + reset length + _startIndex.
*/
public deleteAll(): void {
const keys = this._values.keys();
const zeroArr = GET_EMPTY_BUFFER();
for (let i = 0; i < keys.length; i++) {
const slotIndex = keys[i];
const storagePointer = this.calculateStoragePointer(slotIndex);
Blockchain.setStorageAt(storagePointer, zeroArr);
}
const writer = new BytesWriter(32);
Blockchain.setStorageAt(this.lengthPointer, writer.getBuffer());
this._length = 0;
this._startIndex = 0;
this._isChangedLength = false;
this._isChangedStartIndex = false;
this._values.clear();
this._isChanged.clear();
}
/**
* Set multiple bools starting at `startIndex`.
*/
public setMultiple(startIndex: u32, values: bool[]): void {
for (let i: u32 = 0; i < values.length; i++) {
this.set(startIndex + i, values[i]);
}
}
/**
* Retrieve a batch of bools.
*/
public getAll(start: u32, count: u32): bool[] {
if (start + count > this._length) {
throw new Revert('getAll: range exceeds array length (boolean array)');
}
if (count > u32(u32.MAX_VALUE)) {
throw new Revert('getAll: range exceeds max allowed (boolean array)');
}
const result = new Array<bool>(<i32>count);
for (let i: u32 = 0; i < count; i++) {
result[<i32>i] = this.get(start + i);
}
return result;
}
/**
* Print out the array as "[true, false, ...]".
*/
public toString(): string {
let s = '[';
for (let i: u32 = 0; i < this._length; i++) {
s += this.get(i).toString();
if (i < this._length - 1) {
s += ', ';
}
}
s += ']';
return s;
}
/**
* Reset the array in memory (clear length, startIndex, caches), then save to storage.
*/
public reset(): void {
this._length = 0;
this._startIndex = 0;
this._isChangedLength = true;
this._isChangedStartIndex = true;
this._values.clear();
this._isChanged.clear();
this.save();
}
/**
* Current array length (number of booleans stored).
*/
public getLength(): u32 {
return this._length;
}
public setStartingIndex(index: u32): void {
this._startIndex = index;
this._isChangedStartIndex = true;
}
/**
* Current starting index for the array.
*/
public startingIndex(): u32 {
return this._startIndex;
}
private getRealIndex(index: u32): u32 {
const maxLength: u64 = <u64>this.MAX_LENGTH;
let realIndex: u64 = <u64>this._startIndex + <u64>index;
if (!(realIndex < maxLength)) {
realIndex %= maxLength;
}
return <u32>realIndex;
}
/**
* Ensure the 32-byte slot for `slotIndex` is loaded into _values.
*/
private ensureSlotLoaded(slotIndex: u32): void {
if (!this._values.has(slotIndex)) {
const pointer = this.calculateStoragePointer(slotIndex);
const stored = Blockchain.getStorageAt(pointer);
this._values.set(slotIndex, stored);
}
}
/**
* Convert `slotIndex` -> pointer = basePointer + (slotIndex + 1), as big-endian addition.
*/
private calculateStoragePointer(slotIndex: u32): Uint8Array {
const offset = u64ToBE32Bytes(slotIndex);
return addUint8ArraysBE(this.basePointer, offset);
}
}