@btc-vision/btc-runtime
Version:
Bitcoin Smart Contract Runtime
134 lines (107 loc) • 4.48 kB
text/typescript
import { ICodec } from '../interfaces/ICodec';
import { PointerManager } from '../PointerManager';
import { Blockchain } from '../../env';
import { bigEndianAdd } from '../../math/bytes';
/**
* A generic codec to store an arbitrary-length byte array ("payload") in chunked 32-byte slots.
* Layout:
* - chunk 0 (pointer+0):
* first 4 bytes = length (u32 big-endian),
* next 28 bytes = partial data
* - chunk i>0 (pointer + i): 32 bytes of subsequent data
*/
export class IVariableBytesCodec implements ICodec<Uint8Array> {
public encode(value: Uint8Array): Uint8Array {
const length = value.length;
// Number of bytes that fit in "the first chunk" = 28 (since 4 bytes used by length)
const firstChunkDataLen = length < 28 ? length : 28;
let remaining = length - firstChunkDataLen;
// If remaining > 0, each chunk is 32 bytes.
// So total chunks needed = 1 (for first chunk) + ceil(remaining / 32).
const additionalChunks: u32 = remaining == 0 ? 0 : ((remaining + 32 - 1) / 32);
const totalChunks: u32 = 1 + additionalChunks;
// 1) Allocate `totalChunks` from PointerManager
const pointerBytes = PointerManager.allocateSlots(totalChunks);
// 2) Write chunk 0: length + up to 28 bytes
const chunk0 = new Uint8Array(32);
// store length in big-endian (4 bytes)
chunk0[0] = <u8>((length >> 24) & 0xff);
chunk0[1] = <u8>((length >> 16) & 0xff);
chunk0[2] = <u8>((length >> 8) & 0xff);
chunk0[3] = <u8>(length & 0xff);
for (let i = 0; i < firstChunkDataLen; i++) {
chunk0[4 + i] = value[i];
}
// store chunk0
Blockchain.setStorageAt(pointerBytes, chunk0);
// 3) Write subsequent chunks
let offset: u32 = firstChunkDataLen;
for (let i: u64 = 1; i < u64(totalChunks); i++) {
const chunk = new Uint8Array(32);
const chunkSize: u32 = remaining < 32 ? remaining : 32;
for (let j: u32 = 0; j < chunkSize; j++) {
chunk[j] = value[offset + j];
}
offset += chunkSize;
remaining -= chunkSize;
// compute pointer + i in big-endian
const chunkPointer = bigEndianAdd(pointerBytes, i);
Blockchain.setStorageAt(chunkPointer, chunk);
}
// 4) Return the pointer as the "encoded" data (32 bytes).
return pointerBytes;
}
public decode(buffer: Uint8Array): Uint8Array {
// If buffer is 0 or all zero => means no data
if (buffer.length == 0 || isAllZero(buffer)) {
return new Uint8Array(0);
}
const pointer = buffer; // the pointer (32 bytes)
// chunk0
const chunk0 = Blockchain.getStorageAt(pointer);
if (chunk0.length == 0) {
// No data stored => empty
return new Uint8Array(32);
}
// read length from first 4 bytes
const b0 = <u32>chunk0[0];
const b1 = <u32>chunk0[1];
const b2 = <u32>chunk0[2];
const b3 = <u32>chunk0[3];
const length = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
if (length == 0) {
return new Uint8Array(0);
}
// read the data
const out = new Uint8Array(length);
const firstChunkLen = length < 28 ? length : 28;
// copy from chunk0
for (let i: u32 = 0; i < firstChunkLen; i++) {
out[i] = chunk0[4 + i];
}
let offset = firstChunkLen;
let remaining = length - firstChunkLen;
// read subsequent chunks
let chunkIndex: u64 = 1;
while (remaining > 0) {
const chunkPointer = bigEndianAdd(pointer, chunkIndex);
const chunkData = Blockchain.getStorageAt(chunkPointer);
const chunkSize: u32 = remaining < 32 ? remaining : 32;
for (let j: u32 = 0; j < chunkSize; j++) {
out[offset + j] = chunkData[j];
}
offset += chunkSize;
remaining -= chunkSize;
chunkIndex++;
}
return out;
}
}
function isAllZero(arr: Uint8Array): bool {
for (let i = 0; i < arr.length; i++) {
if (arr[i] != 0) return false;
}
return true;
}
export const idOfVariableBytes = idof<IVariableBytesCodec>();
export const VariableBytesCodec = new IVariableBytesCodec();