@btc-vision/btc-runtime
Version:
Bitcoin Smart Contract Runtime
388 lines (295 loc) • 11.7 kB
text/typescript
import { u256 } from '@btc-vision/as-bignum/assembly';
import { BytesReader } from '../buffer/BytesReader';
import { BytesWriter } from '../buffer/BytesWriter';
import { OP_NET } from '../contracts/OP_NET';
import { NetEvent } from '../events/NetEvent';
import { Potential } from '../lang/Definitions';
import { Address } from '../types/Address';
import { ADDRESS_BYTE_LENGTH } from '../utils';
import { Block } from './classes/Block';
import { Transaction } from './classes/Transaction';
import {
callContract,
deployFromAddress,
emit,
env_exit,
getAccountType,
getBlockHash,
getCallResult,
loadPointer,
log,
sha256,
storePointer,
tLoadPointer,
tStorePointer,
validateBitcoinAddress,
verifySchnorrSignature,
} from './global';
import { eqUint, MapUint8Array } from '../generic/MapUint8Array';
import { EMPTY_BUFFER } from '../math/bytes';
import { Plugin } from '../plugins/Plugin';
import { Calldata } from '../types';
import { Revert } from '../types/Revert';
export * from '../env/global';
export class BlockchainEnvironment {
public readonly DEAD_ADDRESS: Address = Address.dead();
private storage: MapUint8Array = new MapUint8Array();
private transientStorage: MapUint8Array = new MapUint8Array();
private _selfContract: Potential<OP_NET> = null;
private _plugins: Plugin[] = [];
private _block: Potential<Block> = null;
public get block(): Block {
if (!this._block) {
throw new Revert('Block is required');
}
return this._block as Block;
}
private _tx: Potential<Transaction> = null;
public get tx(): Transaction {
if (!this._tx) {
throw new Revert('Transaction is required');
}
return this._tx as Transaction;
}
private _contract: Potential<() => OP_NET> = null;
public get contract(): OP_NET {
return this._selfContract as OP_NET;
}
public set contract(contract: () => OP_NET) {
this._contract = contract;
this.createContractIfNotExists();
}
private _nextPointer: u16 = 0;
public get nextPointer(): u16 {
if (this._nextPointer === u16.MAX_VALUE) {
throw new Revert(`Out of storage pointer.`);
}
this._nextPointer += 1;
return this._nextPointer;
}
public _contractDeployer: Potential<Address> = null;
public get contractDeployer(): Address {
if (!this._contractDeployer) {
throw new Revert('Deployer is required');
}
return this._contractDeployer as Address;
}
public _contractAddress: Potential<Address> = null;
public get contractAddress(): Address {
if (!this._contractAddress) {
throw new Revert('Contract address is required');
}
return this._contractAddress as Address;
}
public _chainId: Potential<Uint8Array> = null;
public get chainId(): Uint8Array {
if (!this._chainId) {
throw new Revert('Chain id is required');
}
return this._chainId as Uint8Array;
}
public _protocolId: Potential<Uint8Array> = null;
public get protocolId(): Uint8Array {
if (!this._protocolId) {
throw new Revert('Protocol id is required');
}
return this._protocolId as Uint8Array;
}
public registerPlugin(plugin: Plugin): void {
this._plugins.push(plugin);
}
public onDeployment(calldata: Calldata): void {
for (let i: i32 = 0; i < this._plugins.length; i++) {
const plugin = this._plugins[i];
plugin.onDeployment(calldata);
}
this.contract.onDeployment(calldata);
}
public onExecutionStarted(): void {
for (let i: i32 = 0; i < this._plugins.length; i++) {
const plugin = this._plugins[i];
plugin.onExecutionStarted();
}
this.contract.onExecutionStarted();
}
public onExecutionCompleted(): void {
for (let i: i32 = 0; i < this._plugins.length; i++) {
const plugin = this._plugins[i];
plugin.onExecutionCompleted();
}
this.contract.onExecutionCompleted();
}
public setEnvironmentVariables(data: Uint8Array): void {
const reader: BytesReader = new BytesReader(data);
const blockHash = reader.readBytes(32);
const blockNumber = reader.readU64();
const blockMedianTime = reader.readU64();
const txId = reader.readBytes(32);
const txHash = reader.readBytes(32);
const contractAddress = reader.readAddress();
const contractDeployer = reader.readAddress();
const caller = reader.readAddress();
const origin = reader.readAddress();
const chainId = reader.readBytes(32);
const protocolId = reader.readBytes(32);
this._tx = new Transaction(caller, origin, txId, txHash);
this._contractDeployer = contractDeployer;
this._contractAddress = contractAddress;
this._chainId = chainId;
this._protocolId = protocolId;
this._block = new Block(blockHash, blockNumber, blockMedianTime);
this.createContractIfNotExists();
}
public call(destinationContract: Address, calldata: BytesWriter): BytesReader {
if (!destinationContract) {
throw new Revert('Destination contract is required');
}
const resultLengthBuffer = new ArrayBuffer(32);
const status = callContract(
destinationContract.buffer,
calldata.getBuffer().buffer,
calldata.bufferLength(),
resultLengthBuffer,
);
const reader = new BytesReader(Uint8Array.wrap(resultLengthBuffer));
const resultLength = reader.readU32(true);
const resultBuffer = new ArrayBuffer(resultLength);
getCallResult(0, resultLength, resultBuffer);
if (status !== 0) {
env_exit(status, resultBuffer, resultLength);
}
return new BytesReader(Uint8Array.wrap(resultBuffer));
}
public log(data: string): void {
const writer = new BytesWriter(String.UTF8.byteLength(data));
writer.writeString(data);
const buffer = writer.getBuffer();
log(buffer.buffer, buffer.length);
}
public emit(event: NetEvent): void {
const data = event.getEventData();
const writer = new BytesWriter(String.UTF8.byteLength(event.eventType) + 8 + data.byteLength);
writer.writeStringWithLength(event.eventType);
writer.writeBytesWithLength(data);
emit(writer.getBuffer().buffer, writer.bufferLength());
}
public validateBitcoinAddress(address: string): bool {
const writer = new BytesWriter(String.UTF8.byteLength(address));
writer.writeString(address);
const result = validateBitcoinAddress(writer.getBuffer().buffer, address.length);
return result === 1;
}
public deployContractFromExisting(
existingAddress: Address,
salt: u256,
calldata: BytesWriter,
): Address {
const resultAddressBuffer = new ArrayBuffer(ADDRESS_BYTE_LENGTH);
const callDataBuffer = calldata.getBuffer().buffer;
const status = deployFromAddress(
existingAddress.buffer,
salt.toUint8Array(true).buffer,
callDataBuffer,
callDataBuffer.byteLength,
resultAddressBuffer,
);
// TODO: Decode revert data if status is not 0
if (status !== 0) {
throw new Revert('Failed to deploy contract');
}
const contractAddressReader = new BytesReader(Uint8Array.wrap(resultAddressBuffer));
return contractAddressReader.readAddress();
}
public getStorageAt(pointerHash: Uint8Array): Uint8Array {
this.hasPointerStorageHash(pointerHash);
if (this.storage.has(pointerHash)) {
return this.storage.get(pointerHash);
}
return new Uint8Array(32);
}
public getTransientStorageAt(pointerHash: Uint8Array): Uint8Array {
if (this.hasPointerTransientStorageHash(pointerHash)) {
return this.transientStorage.get(pointerHash);
}
return new Uint8Array(32);
}
public sha256(buffer: Uint8Array): Uint8Array {
return sha256(buffer);
}
public hash256(buffer: Uint8Array): Uint8Array {
return sha256(sha256(buffer));
}
public verifySchnorrSignature(
publicKey: Address,
signature: Uint8Array,
hash: Uint8Array,
): boolean {
const result: u32 = verifySchnorrSignature(publicKey.buffer, signature.buffer, hash.buffer);
return result === 1;
}
public hasStorageAt(pointerHash: Uint8Array): bool {
// We mark zero as the default value for the storage, if something is 0, the storage slot get deleted or is non-existent
const val: Uint8Array = this.getStorageAt(pointerHash);
return !eqUint(val, EMPTY_BUFFER);
}
public hasTransientStorageAt(pointerHash: Uint8Array): bool {
// We mark zero as the default value for the storage, if something is 0, the storage slot get deleted or is non-existent
const val: Uint8Array = this.getTransientStorageAt(pointerHash);
return !eqUint(val, EMPTY_BUFFER);
}
public setStorageAt(pointerHash: Uint8Array, value: Uint8Array): void {
this._internalSetStorageAt(pointerHash, value);
}
public setTransientStorageAt(pointerHash: Uint8Array, value: Uint8Array): void {
this._internalSetTransientStorageAt(pointerHash, value);
}
public getAccountType(address: Address): u32 {
return getAccountType(address.buffer);
}
public getBlockHash(blockNumber: u64): Uint8Array {
const hash = new ArrayBuffer(32);
getBlockHash(blockNumber, hash);
return Uint8Array.wrap(hash);
}
private createContractIfNotExists(): void {
if (!this._contract) {
throw new Revert('Contract is required');
}
if (!this._selfContract) {
this._selfContract = this._contract();
}
}
private _internalSetStorageAt(pointerHash: Uint8Array, value: Uint8Array): void {
this.storage.set(pointerHash, value);
storePointer(pointerHash.buffer, value.buffer);
}
private _internalSetTransientStorageAt(pointerHash: Uint8Array, value: Uint8Array): void {
this.transientStorage.set(pointerHash, value);
tStorePointer(pointerHash.buffer, value.buffer);
}
private hasPointerStorageHash(pointer: Uint8Array): bool {
if (this.storage.has(pointer)) {
return true;
}
// we attempt to load the requested pointer.
const resultBuffer = new ArrayBuffer(32);
loadPointer(pointer.buffer, resultBuffer);
const value: Uint8Array = Uint8Array.wrap(resultBuffer);
this.storage.set(pointer, value); // cache the value
return !eqUint(value, EMPTY_BUFFER);
}
private hasPointerTransientStorageHash(pointer: Uint8Array): bool {
if (this.transientStorage.has(pointer)) {
return true;
}
// we attempt to load the requested pointer.
const resultBuffer = new ArrayBuffer(32);
tLoadPointer(pointer.buffer, resultBuffer);
const value: Uint8Array = Uint8Array.wrap(resultBuffer);
this.transientStorage.set(pointer, value); // cache the value
return !eqUint(value, EMPTY_BUFFER);
}
}