diff --git a/src/blobstreamx.cairo b/src/blobstreamx.cairo new file mode 100644 index 0000000..a5b6162 --- /dev/null +++ b/src/blobstreamx.cairo @@ -0,0 +1,361 @@ +#[starknet::contract] +mod blobstreamx { + use alexandria_bytes::{Bytes, BytesTrait}; + use blobstream_sn::interfaces::{ + DataRoot, TendermintXErrors, IBlobstreamX, IDAOracle, ITendermintX + }; + use blobstream_sn::succinctx::interfaces::{ + ISuccinctGatewayDispatcher, ISuccinctGatewayDispatcherTrait + }; + use blobstream_sn::tree::binary::merkle_proof::BinaryMerkleProof; + use core::starknet::event::EventEmitter; + use core::traits::Into; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + use starknet::info::{get_block_number, get_contract_address}; + use starknet::{ClassHash, ContractAddress}; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[storage] + struct Storage { + data_commitment_max: u64, + gateway: ContractAddress, + latest_block: u64, + state_proof_nonce: u64, + state_data_commitments: LegacyMap::, + block_height_to_header_hash: LegacyMap::, + header_range_function_id: u256, + next_header_function_id: u256, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + DataCommitmentStored: DataCommitmentStored, + HeaderRangeRequested: HeaderRangeRequested, + HeadUpdate: HeadUpdate, + NextHeaderRequested: NextHeaderRequested, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + } + + /// Data commitment stored for the block range [startBlock, endBlock] with proof nonce + #[derive(Drop, starknet::Event)] + struct DataCommitmentStored { + // nonce of the proof + proof_nonce: u64, + // start block of the block range + #[key] + start_block: u64, + // end block of the block range + #[key] + end_block: u64, + // data commitment for the block range + #[key] + data_commitment: u256, + } + + /// Inputs of a header range request + #[derive(Drop, starknet::Event)] + struct HeaderRangeRequested { + // trusted block for the header range request + #[key] + trusted_block: u64, + // header hash of the trusted block + #[key] + trusted_header: u256, + // target block of the header range request + target_block: u64 + } + + /// Inputs of a next header request + #[derive(Drop, starknet::Event)] + struct NextHeaderRequested { + // trusted block for the next header request + #[key] + trusted_block: u64, + // header hash of the trusted block + #[key] + trusted_header: u256, + } + + /// Head Update + #[derive(Drop, starknet::Event)] + struct HeadUpdate { + target_block: u64, + target_header: u256 + } + + mod Errors { + const DataCommitmentNotFound: felt252 = 'bad data commitment for range'; + } + + #[constructor] + fn constructor( + ref self: ContractState, + gateway: ContractAddress, + owner: ContractAddress, + height: u64, + header: u256, + header_range_function_id: u256, + next_header_function_id: u256, + ) { + self.data_commitment_max.write(1000); + self.gateway.write(gateway); + self.latest_block.write(height); + self.state_proof_nonce.write(1); + self.ownable.initializer(owner); + self.block_height_to_header_hash.write(height, header); + self.header_range_function_id.write(header_range_function_id); + self.next_header_function_id.write(next_header_function_id); + } + + #[abi(embed_v0)] + impl Upgradeable of IUpgradeable { + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable._upgrade(new_class_hash); + } + } + + #[abi(embed_v0)] + impl IDAOracleImpl of IDAOracle { + fn verify_attestation( + self: @ContractState, proof_nonce: u64, root: DataRoot, proof: BinaryMerkleProof + ) -> bool { + if (proof_nonce >= self.state_proof_nonce.read()) { + return false; + } + + // load the tuple root at the given index from storage. + let _data_root = self.state_data_commitments.read(proof_nonce); + + // return isProofValid; + // TODO(#69 + #24): BinaryMerkleTree.verify(root, _proof, abi.encode(_tuple)); + false + } + } + + #[abi(embed_v0)] + impl ITendermintXImpl of ITendermintX { + /// Get the header hash for a block height. + /// # Arguments + /// * `_height` - the height to consider + /// # Returns + /// The associated hash + fn get_header_hash(self: @ContractState, _height: u64) -> u256 { + self.block_height_to_header_hash.read(_height) + } + fn get_latest_block(self: @ContractState) -> u64 { + self.latest_block.read() + } + } + + #[abi(embed_v0)] + impl IBlobstreamXImpl of IBlobstreamX { + fn data_commitment_max(self: @ContractState) -> u64 { + self.data_commitment_max.read() + } + fn set_gateway(ref self: ContractState, new_gateway: ContractAddress) { + self.ownable.assert_only_owner(); + self.gateway.write(new_gateway); + } + fn get_gateway(self: @ContractState) -> ContractAddress { + self.gateway.read() + } + + fn get_state_proof_nonce(self: @ContractState) -> u64 { + self.state_proof_nonce.read() + } + + fn get_header_range_id(self: @ContractState) -> u256 { + self.header_range_function_id.read() + } + + fn set_header_range_id(ref self: ContractState, _function_id: u256) { + self.ownable.assert_only_owner(); + self.header_range_function_id.write(_function_id); + } + + fn get_next_header_id(self: @ContractState) -> u256 { + self.next_header_function_id.read() + } + + fn set_next_header_id(ref self: ContractState, _function_id: u256) { + self.ownable.assert_only_owner(); + self.next_header_function_id.write(_function_id); + } + + /// Prove the validity of the header at the target block and a data commitment for the block range [latestBlock, _targetBlock). + /// Used to skip from the latest block to the target block. + /// + /// # Arguments + /// + /// * `_target_block` - The end block of the header range proof. + fn request_header_range(ref self: ContractState, _target_block: u64) { + let latest_block = self.get_latest_block(); + let latest_header = self.block_height_to_header_hash.read(latest_block); + assert(latest_header != 0, TendermintXErrors::LatestHeaderNotFound); + // A request can be at most data_commitment_max blocks ahead of the latest block. + assert(_target_block > latest_block, TendermintXErrors::TargetBlockNotInRange); + assert( + _target_block - latest_block <= self.data_commitment_max(), + TendermintXErrors::TargetBlockNotInRange + ); + + let mut input = BytesTrait::new_empty(); + input.append_u64(latest_block); + input.append_u256(latest_header); + input.append_u64(_target_block); + + let mut entry_calldata = BytesTrait::new_empty(); + entry_calldata.append_felt252(selector!("commit_header_range")); + entry_calldata.append_u64(_target_block); + + ISuccinctGatewayDispatcher { contract_address: self.gateway.read() } + .request_call( + self.header_range_function_id.read(), + input, + get_contract_address(), + entry_calldata, + 500000 + ); + + self + .emit( + HeaderRangeRequested { + trusted_block: latest_block, + trusted_header: latest_header, + target_block: _target_block + } + ); + } + + /// Commits the new header at targetBlock and the data commitment for the block range [trustedBlock, targetBlock). + /// + /// # Arguments + /// + /// * `_target_block` - The end block of the header range request + fn commit_header_range(ref self: ContractState, _target_block: u64) { + let latest_block = self.get_latest_block(); + let trusted_header = self.block_height_to_header_hash.read(latest_block); + assert(trusted_header != 0, TendermintXErrors::TrustedHeaderNotFound); + + assert(_target_block > latest_block, TendermintXErrors::TargetBlockNotInRange); + assert( + _target_block - latest_block <= self.data_commitment_max(), + TendermintXErrors::TargetBlockNotInRange + ); + + let mut input = BytesTrait::new_empty(); + input.append_u64(latest_block); + input.append_u256(trusted_header); + input.append_u64(_target_block); + + let (target_header, data_commitment) = ISuccinctGatewayDispatcher { + contract_address: self.get_gateway() + } + .verified_call(self.get_header_range_id(), input); + + let proof_nonce = self.get_state_proof_nonce(); + self.block_height_to_header_hash.write(_target_block, target_header); + self.state_data_commitments.write(proof_nonce, data_commitment); + self + .emit( + DataCommitmentStored { + proof_nonce, + data_commitment, + start_block: latest_block, + end_block: _target_block, + } + ); + self.emit(HeadUpdate { target_block: latest_block, target_header: target_header }); + self.state_proof_nonce.write(proof_nonce + 1); + self.latest_block.write(_target_block); + } + + + /// Prove the validity of the next header and a data commitment for the block range [latestBlock, latestBlock + 1). + fn request_next_header(ref self: ContractState) { + let latest_block = self.get_latest_block(); + let latest_header = self.block_height_to_header_hash.read(latest_block); + assert(latest_header != 0, TendermintXErrors::LatestHeaderNotFound); + + let mut input = BytesTrait::new_empty(); + input.append_u64(latest_block); + input.append_u256(latest_header); + + let mut entry_calldata = BytesTrait::new_empty(); + entry_calldata.append_felt252(selector!("commit_next_header")); + entry_calldata.append_u64(latest_block); + + ISuccinctGatewayDispatcher { contract_address: self.gateway.read() } + .request_call( + self.next_header_function_id.read(), + input, + get_contract_address(), + entry_calldata, + 500000 + ); + + self + .emit( + NextHeaderRequested { + trusted_block: latest_block, trusted_header: latest_header + } + ); + } + + + /// Stores the new header for _trustedBlock + 1 and the data commitment for the block range [_trustedBlock, _trustedBlock + 1). + /// + /// # Arguments + /// + /// * `_trusted_block` - The latest block when the request was made. + fn commit_next_header(ref self: ContractState, _trusted_block: u64) { + let trusted_header = self.block_height_to_header_hash.read(_trusted_block); + assert(trusted_header != 0, TendermintXErrors::TrustedHeaderNotFound); + + let next_block = _trusted_block + 1; + assert(next_block > self.get_latest_block(), TendermintXErrors::TargetBlockNotInRange); + + let mut input = BytesTrait::new_empty(); + input.append_u64(_trusted_block); + input.append_u256(trusted_header); + + let (next_header, data_commitment) = ISuccinctGatewayDispatcher { + contract_address: self.gateway.read() + } + .verified_call(self.next_header_function_id.read(), input); + + let proof_nonce = self.get_state_proof_nonce(); + self.block_height_to_header_hash.write(next_block, next_header); + self.state_data_commitments.write(proof_nonce, data_commitment); + self + .emit( + DataCommitmentStored { + proof_nonce, + data_commitment, + start_block: _trusted_block, + end_block: next_block + } + ); + self.emit(HeadUpdate { target_block: next_block, target_header: next_header }); + self.state_proof_nonce.write(proof_nonce + 1); + self.latest_block.write(next_block); + } + } +} diff --git a/src/interfaces.cairo b/src/interfaces.cairo index 0416d2d..d531a05 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -16,6 +16,19 @@ struct DataRoot { data_root: u256, } +mod TendermintXErrors { + const TrustedHeaderNotFound: felt252 = 'Trusted header not found'; + const TargetBlockNotInRange: felt252 = 'Target block not in range'; + const LatestHeaderNotFound: felt252 = 'Latest header not found'; +} + +#[starknet::interface] +trait ITendermintX { + // Get the header hash for a block height. + fn get_header_hash(self: @TContractState, _height: u64) -> u256; + // Get latest block number updated by the light client. + fn get_latest_block(self: @TContractState) -> u64; +} /// Data Availability Oracle interface. #[starknet::interface] @@ -33,12 +46,10 @@ trait IDAOracle { trait IBlobstreamX { /// Max num of blocks that can be skipped in a single request /// ref: https://github.com/celestiaorg/celestia-core/blob/main/pkg/consts/consts.go#L43-L44 - fn DATA_COMMITMENT_MAX(self: @TContractState) -> u64; + fn data_commitment_max(self: @TContractState) -> u64; // Address of the gateway contract fn set_gateway(ref self: TContractState, new_gateway: ContractAddress); fn get_gateway(self: @TContractState) -> ContractAddress; - // Block is the first one in the next data commitment - fn get_latest_block(self: @TContractState) -> u64; // Nonce for proof events. Must be incremented sequentially fn get_state_proof_nonce(self: @TContractState) -> u64; // Header range function id @@ -50,11 +61,9 @@ trait IBlobstreamX { // Prove the validity of the header at the target block and a data commitment for the block range [latestBlock, _targetBlock) fn request_header_range(ref self: TContractState, _target_block: u64); // Commits the new header at targetBlock and the data commitment for the block range [trustedBlock, targetBlock). - fn commit_header_range(ref self: TContractState, _trusted_block: u64, _target_block: u64); + fn commit_header_range(ref self: TContractState, _target_block: u64); // Prove the validity of the next header and a data commitment for the block range [latestBlock, latestBlock + 1). fn request_next_header(ref self: TContractState); // Stores the new header for _trustedBlock + 1 and the data commitment for the block range [_trustedBlock, _trustedBlock + 1). fn commit_next_header(ref self: TContractState, _trusted_block: u64); - // Get the header hash for a block height. - fn get_header_hash(self: @TContractState, _height: u64) -> u256; } diff --git a/src/lib.cairo b/src/lib.cairo index 1520fa3..9d271fc 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -1,343 +1,61 @@ +pub mod blobstreamx; mod interfaces; -mod tree; -mod verifier; - -#[starknet::contract] -mod BlobstreamX { - use alexandria_bytes::Bytes; - use alexandria_bytes::BytesTrait; - use blobstream_sn::interfaces::{IBlobstreamX, IDAOracle, DataRoot}; - use blobstream_sn::tree::binary::merkle_proof::BinaryMerkleProof; - use core::starknet::event::EventEmitter; - use core::traits::Into; - use openzeppelin::access::ownable::OwnableComponent; - use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; - use starknet::info::get_block_number; - use starknet::{ClassHash, ContractAddress}; - - component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); - #[abi(embed_v0)] - impl OwnableImpl = OwnableComponent::OwnableImpl; - impl OwnableInternalImpl = OwnableComponent::InternalImpl; - - component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); - impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; - - #[storage] - struct Storage { - // CONTRACT STORAGE - DATA_COMMITMENT_MAX: u64, - gateway: ContractAddress, - latest_block: u64, - state_proof_nonce: u64, - state_data_commitments: LegacyMap::, - block_height_to_header_hash: LegacyMap::, - header_range_function_id: u256, - next_header_function_id: u256, - // COMPONENT STORAGE - #[substorage(v0)] - ownable: OwnableComponent::Storage, - #[substorage(v0)] - upgradeable: UpgradeableComponent::Storage, - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - // CONTRACT EVENTS - // TODO(#68): impl header range - DataCommitmentStored: DataCommitmentStored, - NextHeaderRequested: NextHeaderRequested, - HeaderRangeRequested: HeaderRangeRequested, - HeadUpdate: HeadUpdate, - // COMPONENT EVENTS - #[flat] - OwnableEvent: OwnableComponent::Event, - #[flat] - UpgradeableEvent: UpgradeableComponent::Event, - } - - /// Data commitment stored for the block range [startBlock, endBlock] with proof nonce - #[derive(Drop, starknet::Event)] - struct DataCommitmentStored { - // nonce of the proof - proof_nonce: u64, - // start block of the block range - #[key] - start_block: u64, - // end block of the block range - #[key] - end_block: u64, - // data commitment for the block range - #[key] - data_commitment: u256, - } - - /// Inputs of a next header request - #[derive(Drop, starknet::Event)] - struct NextHeaderRequested { - // trusted block for the next header request - #[key] - trusted_block: u64, - // header hash of the trusted block - #[key] - trusted_header: u256, - } - - /// Inputs of a header range request - #[derive(Drop, starknet::Event)] - struct HeaderRangeRequested { - // trusted block for the header range request - #[key] - trusted_block: u64, - // header hash of the trusted block - #[key] - trusted_header: u256, - // target block of the header range request - target_block: u64 - } - - /// Head Update - #[derive(Drop, starknet::Event)] - struct HeadUpdate { - target_block: u64, - target_header: u256 - } +mod mocks { + mod function_verifier; + mod upgradeable; +} - mod Errors { - /// data commitment for specified block range does not exist - const DataCommitmentNotFound: felt252 = 'Data commitment not found'; - const TrustedHeaderNotFound: felt252 = 'Trusted header not found'; - const TargetBlockNotInRange: felt252 = 'Target block not in range'; - const LatestHeaderNotFound: felt252 = 'Latest header not found'; +mod succinctx { + mod gateway; + mod interfaces; + mod function_registry { + mod component; + mod interfaces; + mod mock; } +} - #[constructor] - fn constructor( - ref self: ContractState, gateway: ContractAddress, owner: ContractAddress, header: u256 - ) { - self.DATA_COMMITMENT_MAX.write(1000); - self.gateway.write(gateway); - self.latest_block.write(get_block_number()); - self.state_proof_nonce.write(1); - self.ownable.initializer(owner); - self.block_height_to_header_hash.write(get_block_number(), header); - // self.header_range_function_id.write(header_range_function_id); - // self.next_header_function_id.write(next_header_function_id); - } +#[cfg(test)] +mod tests { + mod common; + mod test_blobstreamx; + mod test_ownable; + mod test_upgradeable; +} - #[abi(embed_v0)] - impl Upgradeable of IUpgradeable { - fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { - self.ownable.assert_only_owner(); - self.upgradeable._upgrade(new_class_hash); +mod tree { + mod consts; + mod utils; + mod binary { + mod hasher; + mod merkle_proof; + mod merkle_tree; + #[cfg(test)] + mod tests { + mod test_hasher; + mod test_merkle_proof; } } - - #[abi(embed_v0)] - impl IDAOracleImpl of IDAOracle { - fn verify_attestation( - self: @ContractState, proof_nonce: u64, root: DataRoot, proof: BinaryMerkleProof - ) -> bool { - if (proof_nonce >= self.state_proof_nonce.read()) { - return false; - } - - // load the tuple root at the given index from storage. - let _ = self.state_data_commitments.read(proof_nonce); - - // return isProofValid; - // TODO(#69 + #24): BinaryMerkleTree.verify(root, _proof, abi.encode(_tuple)); - false + mod namespace { + mod merkle_tree; + #[cfg(test)] + mod tests { + mod test_merkle_tree; } } - - #[abi(embed_v0)] - impl IBlobstreamXImpl of IBlobstreamX { - fn DATA_COMMITMENT_MAX(self: @ContractState) -> u64 { - self.DATA_COMMITMENT_MAX.read() - } - fn set_gateway(ref self: ContractState, new_gateway: ContractAddress) { - self.ownable.assert_only_owner(); - self.gateway.write(new_gateway); - } - fn get_gateway(self: @ContractState) -> ContractAddress { - self.gateway.read() - } - fn get_latest_block(self: @ContractState) -> u64 { - self.latest_block.read() - } - fn get_state_proof_nonce(self: @ContractState) -> u64 { - self.state_proof_nonce.read() - } - - fn get_header_range_id(self: @ContractState) -> u256 { - self.header_range_function_id.read() - } - - fn set_header_range_id(ref self: ContractState, _function_id: u256) { - self.ownable.assert_only_owner(); - self.header_range_function_id.write(_function_id); - } - - fn get_next_header_id(self: @ContractState) -> u256 { - self.next_header_function_id.read() - } - - fn set_next_header_id(ref self: ContractState, _function_id: u256) { - self.ownable.assert_only_owner(); - self.next_header_function_id.write(_function_id); - } - - /// @notice Prove the validity of the header at the target block and a data commitment for the block range [latestBlock, _targetBlock). - /// # Arguments - /// _target_block The end block of the header range proof. - /// @dev requestHeaderRange is used to skip from the latest block to the target block. - fn request_header_range(ref self: ContractState, _target_block: u64) { - let latest_block = self.get_latest_block(); - let latest_header = self.block_height_to_header_hash.read(latest_block); - assert(latest_header != 0, Errors::LatestHeaderNotFound); - // A request can be at most DATA_COMMITMENT_MAX blocks ahead of the latest block. - assert(_target_block > latest_block, Errors::TargetBlockNotInRange); - assert( - _target_block - latest_block <= self.DATA_COMMITMENT_MAX(), - Errors::TargetBlockNotInRange - ); - //TODO: SunccinctGateway call - self - .emit( - HeaderRangeRequested { - trusted_block: latest_block, - trusted_header: latest_header, - target_block: _target_block - } - ); - } - - /// @notice Commits the new header at targetBlock and the data commitment for the block range [trustedBlock, targetBlock). - /// # Arguments - /// * `_trustedBlock` - The latest block when the request was made. - /// * `_target_block` - The end block of the header range request - fn commit_header_range(ref self: ContractState, _trusted_block: u64, _target_block: u64) { - let trusted_header = self.block_height_to_header_hash.read(_trusted_block); - let latest_block = self.get_latest_block(); - let state_proof_nonce = self.get_state_proof_nonce(); - assert(trusted_header != 0, Errors::TrustedHeaderNotFound); - let mut bytes: Bytes = BytesTrait::new(0, array![0]); - bytes.append_u64(_trusted_block); - bytes.append_u256(trusted_header); - bytes.append_u64(_target_block); - - // MOCK INFORMATION FOR NOW - //TODO(#73): SunccinctGateway - // let request_result = ISuccinctGateway... - let mut request_result: Bytes = BytesTrait::new(32, array![0]); - request_result.append_u256(12314123123); - request_result.append_u256(32131232); - let (_, data_commitment) = request_result.read_u256(0); - let (_, target_header) = request_result.read_u256(0); - //// END - - assert(_target_block > latest_block, Errors::TargetBlockNotInRange); - assert( - _target_block - latest_block <= self.DATA_COMMITMENT_MAX(), - Errors::TargetBlockNotInRange - ); - self.block_height_to_header_hash.write(_target_block, target_header); - self.state_data_commitments.write(state_proof_nonce, data_commitment); - self - .emit( - DataCommitmentStored { - proof_nonce: state_proof_nonce, - start_block: _trusted_block, - end_block: _target_block, - data_commitment: data_commitment - } - ); - self.emit(HeadUpdate { target_block: _trusted_block, target_header: target_header }); - self.state_proof_nonce.write(state_proof_nonce + 1); - self.latest_block.write(_target_block); - } - - - /// Prove the validity of the next header and a data commitment for the block range [latestBlock, latestBlock + 1). - fn request_next_header(ref self: ContractState) { - let latest_block = self.get_latest_block(); - let latest_header = self.block_height_to_header_hash.read(latest_block); - assert(latest_header != 0, Errors::LatestHeaderNotFound); - - //TODO(#73): SunccinctGateway - // ISuccintGateway... - - self - .emit( - NextHeaderRequested { - trusted_block: latest_block, trusted_header: latest_header - } - ); - } - - - /// Stores the new header for _trustedBlock + 1 and the data commitment for the block range [_trustedBlock, _trustedBlock + 1). - /// # Arguments - /// * `_trusted_block` - The latest block when the request was made. - fn commit_next_header(ref self: ContractState, _trusted_block: u64) { - let trusted_header = self.block_height_to_header_hash.read(_trusted_block); - let state_proof_nonce = self.get_state_proof_nonce(); - let latest_block = self.latest_block.read(); - assert(trusted_header != 0, Errors::TrustedHeaderNotFound); - let mut bytes: Bytes = BytesTrait::new(0, array![0]); - bytes.append_u64(_trusted_block); - bytes.append_u256(trusted_header); - - // MOCK INFORMATION FOR NOW - //TODO(#73): SunccinctGateway - // let request_result = ISuccinctGateway... - let mut request_result: Bytes = BytesTrait::new(32, array![0]); - request_result.append_u256(12314123123); - request_result.append_u256(32131232); - let (_, data_commitment) = request_result.read_u256(0); - let (_, next_header) = request_result.read_u256(0); - //END - - let next_block = _trusted_block + 1; - assert(next_block > latest_block, Errors::TargetBlockNotInRange); - self.block_height_to_header_hash.write(next_block, next_header); - self.state_data_commitments.write(state_proof_nonce, data_commitment); - self - .emit( - DataCommitmentStored { - proof_nonce: state_proof_nonce, - start_block: _trusted_block, - end_block: next_block, - data_commitment: data_commitment - } - ); - self.emit(HeadUpdate { target_block: next_block, target_header: next_header }); - self.state_proof_nonce.write(state_proof_nonce + 1); - self.latest_block.write(next_block); - } - - /// Get the header hash for a block height. - /// # Arguments - /// * `_height` - the height to consider - /// # Returns - /// The associated hash - fn get_header_hash(self: @ContractState, _height: u64) -> u256 { - self.block_height_to_header_hash.read(_height) - } + #[cfg(test)] + mod tests { + mod test_consts; + mod test_utils; } } -mod mocks { - mod upgradeable; +mod verifier { + mod types; + #[cfg(test)] + mod tests { + mod test_verifier; + } } -#[cfg(test)] -mod tests { - mod common; - mod test_blobstreamx; - mod test_ownable; - mod test_upgradeable; -} diff --git a/src/mocks/function_verifier.cairo b/src/mocks/function_verifier.cairo new file mode 100644 index 0000000..f8f51f7 --- /dev/null +++ b/src/mocks/function_verifier.cairo @@ -0,0 +1,25 @@ +#[starknet::contract] +mod function_verifier_mock { + use alexandria_bytes::Bytes; + use blobstream_sn::succinctx::interfaces::IFunctionVerifier; + + #[storage] + struct Storage { + circuit_digest: u256, + } + + #[constructor] + fn constructor(ref self: ContractState, circuit_digest: u256) { + self.circuit_digest.write(circuit_digest); + } + + #[abi(embed_v0)] + impl FunctionVerifier of IFunctionVerifier { + fn verify(self: @ContractState, input_hash: u256, output_hash: u256, proof: Bytes) -> bool { + true + } + fn verification_key_hash(self: @ContractState) -> u256 { + self.circuit_digest.read() + } + } +} diff --git a/src/mocks/upgradeable.cairo b/src/mocks/upgradeable.cairo index 3092a41..56b581d 100644 --- a/src/mocks/upgradeable.cairo +++ b/src/mocks/upgradeable.cairo @@ -9,7 +9,7 @@ trait IMockUpgradeable { #[starknet::contract] -mod MockUpgradeable { +mod mock_upgradeable { use openzeppelin::upgrades::UpgradeableComponent; use starknet::{ClassHash, ContractAddress}; diff --git a/src/succinctx/function_registry/component.cairo b/src/succinctx/function_registry/component.cairo new file mode 100644 index 0000000..47ef9df --- /dev/null +++ b/src/succinctx/function_registry/component.cairo @@ -0,0 +1,102 @@ +mod errors { + const EMPTY_BYTECODE: felt252 = 'EMPTY_BYTECODE'; + const FAILED_DEPLOY: felt252 = 'FAILED_DEPLOY'; + const VERIFIER_CANNOT_BE_ZERO: felt252 = 'VERIFIER_CANNOT_BE_ZERO'; + const VERIFIER_ALREADY_UPDATED: felt252 = 'VERIFIER_ALREADY_UPDATED'; + const FUNCTION_ALREADY_REGISTERED: felt252 = 'FUNCTION_ALREADY_REGISTERED'; + const NOT_FUNCTION_OWNER: felt252 = 'NOT_FUNCTION_OWNER'; +} + +#[starknet::component] +mod function_registry_cpt { + use alexandria_bytes::{Bytes, BytesTrait}; + use blobstream_sn::succinctx::function_registry::interfaces::IFunctionRegistry; + use core::traits::Into; + use starknet::info::get_caller_address; + use starknet::{ContractAddress, contract_address_const}; + use super::errors; + + #[storage] + struct Storage { + verifiers: LegacyMap, + verifier_owners: LegacyMap, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + FunctionRegistered: FunctionRegistered, + FunctionVerifierUpdated: FunctionVerifierUpdated, + } + + #[derive(Drop, starknet::Event)] + struct FunctionRegistered { + #[key] + function_id: u256, + verifier: ContractAddress, + name: felt252, + owner: ContractAddress, + } + + #[derive(Drop, starknet::Event)] + struct FunctionVerifierUpdated { + #[key] + function_id: u256, + verifier: ContractAddress, + } + + #[embeddable_as(FunctionRegistryImpl)] + impl FunctionRegistry< + TContractState, +HasComponent + > of IFunctionRegistry> { + fn verifiers(self: @ComponentState, function_id: u256) -> ContractAddress { + self.verifiers.read(function_id) + } + fn verifier_owners( + self: @ComponentState, function_id: u256 + ) -> ContractAddress { + self.verifier_owners.read(function_id) + } + fn get_function_id( + self: @ComponentState, owner: ContractAddress, name: felt252 + ) -> u256 { + let mut function_id_digest = BytesTrait::new_empty(); + function_id_digest.append_felt252(owner.into()); + function_id_digest.append_felt252(name); + function_id_digest.keccak() + } + fn register_function( + ref self: ComponentState, + owner: ContractAddress, + verifier: ContractAddress, + name: felt252 + ) -> u256 { + assert(verifier.is_non_zero(), errors::VERIFIER_CANNOT_BE_ZERO); + + let function_id = self.get_function_id(owner, name); + assert(self.verifiers.read(function_id).is_zero(), errors::FUNCTION_ALREADY_REGISTERED); + + self.verifier_owners.write(function_id, owner); + self.verifiers.write(function_id, verifier); + + self.emit(FunctionRegistered { function_id, verifier, name, owner, }); + + function_id + } + fn update_function( + ref self: ComponentState, verifier: ContractAddress, name: felt252 + ) -> u256 { + assert(verifier.is_non_zero(), errors::VERIFIER_CANNOT_BE_ZERO); + + let caller = get_caller_address(); + let function_id = self.get_function_id(caller, name); + assert(self.verifier_owners.read(function_id) != caller, errors::NOT_FUNCTION_OWNER); + assert(self.verifiers.read(function_id) == verifier, errors::VERIFIER_ALREADY_UPDATED); + + self.verifiers.write(function_id, verifier); + self.emit(FunctionVerifierUpdated { function_id, verifier }); + + function_id + } + } +} diff --git a/src/succinctx/function_registry/interfaces.cairo b/src/succinctx/function_registry/interfaces.cairo new file mode 100644 index 0000000..c64fdba --- /dev/null +++ b/src/succinctx/function_registry/interfaces.cairo @@ -0,0 +1,12 @@ +use starknet::ContractAddress; + +#[starknet::interface] +trait IFunctionRegistry { + fn verifiers(self: @TContractState, function_id: u256) -> ContractAddress; + fn verifier_owners(self: @TContractState, function_id: u256) -> ContractAddress; + fn get_function_id(self: @TContractState, owner: ContractAddress, name: felt252) -> u256; + fn register_function( + ref self: TContractState, owner: ContractAddress, verifier: ContractAddress, name: felt252 + ) -> u256; + fn update_function(ref self: TContractState, verifier: ContractAddress, name: felt252) -> u256; +} diff --git a/src/succinctx/function_registry/mock.cairo b/src/succinctx/function_registry/mock.cairo new file mode 100644 index 0000000..7d7eb4d --- /dev/null +++ b/src/succinctx/function_registry/mock.cairo @@ -0,0 +1,27 @@ +#[starknet::contract] +mod function_registry_mock { + use blobstream_sn::succinctx::function_registry::component::function_registry_cpt; + use blobstream_sn::succinctx::function_registry::interfaces::IFunctionRegistry; + use starknet::ContractAddress; + + component!( + path: function_registry_cpt, storage: function_registry, event: FunctionRegistryEvent + ); + + #[abi(embed_v0)] + impl FunctionRegistryImpl = + function_registry_cpt::FunctionRegistryImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + function_registry: function_registry_cpt::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + FunctionRegistryEvent: function_registry_cpt::Event + } +} diff --git a/src/succinctx/gateway.cairo b/src/succinctx/gateway.cairo new file mode 100644 index 0000000..47d3492 --- /dev/null +++ b/src/succinctx/gateway.cairo @@ -0,0 +1,341 @@ +#[starknet::contract] +mod succinct_gateway { + use alexandria_bytes::{Bytes, BytesTrait}; + use blobstream_sn::succinctx::function_registry::component::function_registry_cpt; + use blobstream_sn::succinctx::function_registry::interfaces::IFunctionRegistry; + use blobstream_sn::succinctx::interfaces::{ + ISuccinctGateway, IFunctionVerifierDispatcher, IFunctionVerifierDispatcherTrait + }; + use core::array::SpanTrait; + use openzeppelin::access::ownable::{OwnableComponent as ownable_cpt, interface::IOwnable}; + use openzeppelin::security::reentrancyguard::ReentrancyGuardComponent; + use starknet::{ContractAddress, SyscallResultTrait, syscalls::call_contract_syscall}; + + component!( + path: function_registry_cpt, storage: function_registry, event: FunctionRegistryEvent + ); + component!(path: ownable_cpt, storage: ownable, event: OwnableEvent); + component!( + path: ReentrancyGuardComponent, storage: reentrancy_guard, event: ReentrancyGuardEvent + ); + + impl ReentrancyGuardInternalImpl = ReentrancyGuardComponent::InternalImpl; + #[abi(embed_v0)] + impl FunctionRegistryImpl = + function_registry_cpt::FunctionRegistryImpl; + #[abi(embed_v0)] + impl OwnableImpl = ownable_cpt::OwnableImpl; + impl OwnableInternalImpl = ownable_cpt::InternalImpl; + + #[storage] + struct Storage { + allowed_provers: LegacyMap, + is_callback: bool, + nonce: u32, + requests: LegacyMap, + verified_function_id: u256, + verified_input_hash: u256, + verified_output: (u256, u256), + #[substorage(v0)] + function_registry: function_registry_cpt::Storage, + #[substorage(v0)] + ownable: ownable_cpt::Storage, + #[substorage(v0)] + reentrancy_guard: ReentrancyGuardComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + RequestCall: RequestCall, + RequestCallback: RequestCallback, + RequestFulfilled: RequestFulfilled, + Call: Call, + #[flat] + FunctionRegistryEvent: function_registry_cpt::Event, + #[flat] + OwnableEvent: ownable_cpt::Event, + #[flat] + ReentrancyGuardEvent: ReentrancyGuardComponent::Event + } + + #[derive(Drop, starknet::Event)] + struct RequestCall { + #[key] + function_id: u256, + input: Bytes, + entry_address: ContractAddress, + entry_calldata: Bytes, + entry_gas_limit: u32, + sender: ContractAddress, + fee_amount: u128, + } + + #[derive(Drop, starknet::Event)] + struct RequestCallback { + #[key] + nonce: u32, + #[key] + function_id: u256, + input: Bytes, + context: Bytes, + callback_addr: ContractAddress, + callback_selector: felt252, + callback_gas_limit: u32, + fee_amount: u128, + } + + #[derive(Drop, starknet::Event)] + struct RequestFulfilled { + #[key] + nonce: u32, + #[key] + function_id: u256, + input_hash: u256, + output_hash: u256, + } + + #[derive(Drop, starknet::Event)] + struct Call { + #[key] + function_id: u256, + input_hash: u256, + output_hash: u256, + } + + mod Errors { + const INVALID_CALL: felt252 = 'Invalid call to verify'; + const INVALID_REQUEST: felt252 = 'Invalid request for fullfilment'; + const INVALID_PROOF: felt252 = 'Invalid proof provided'; + } + + #[constructor] + fn constructor(ref self: ContractState, owner: ContractAddress) { + self.ownable.initializer(owner); + } + + #[abi(embed_v0)] + impl ISuccinctGatewayImpl of ISuccinctGateway { + /// Creates a onchain request for a proof. The output and proof is fulfilled asynchronously + /// by the provided callback. + /// + /// # Arguments + /// + /// * `function_id` - The function identifier. + /// * `input` - The function input. + /// * `context` - The function context. + /// * `callback_selector` - The selector of the callback function. + /// * `callback_gas_limit` - The gas limit for the callback function. + fn request_callback( + ref self: ContractState, + function_id: u256, + input: Bytes, + context: Bytes, + callback_selector: felt252, + callback_gas_limit: u32, + ) -> u256 { + let nonce = self.nonce.read(); + let callback_addr = starknet::info::get_caller_address(); + let request_hash = InternalImpl::_request_hash( + nonce, + function_id, + input.sha256(), + context.keccak(), + callback_addr, + callback_selector, + callback_gas_limit + ); + self.requests.write(nonce, request_hash); + self + .emit( + RequestCallback { + nonce, + function_id, + input, + context, + callback_addr, + callback_selector, + callback_gas_limit, + fee_amount: starknet::info::get_tx_info().unbox().max_fee, + } + ); + + self.nonce.write(nonce + 1); + + // TODO(#80): Implement Fee Vault and send fee + + request_hash + } + /// Creates a proof request for a call. Equivalent to an off-chain request through an API. + /// + /// # Arguments + /// + /// * `function_id` - The function identifier. + /// * `input` - The function input. + /// * `entry_address` - The address of the callback contract. + /// * `entry_calldata` - The entry calldata for the call. + /// * `entry_gas_limit` - The gas limit for the call. + fn request_call( + ref self: ContractState, + function_id: u256, + input: Bytes, + entry_address: ContractAddress, + entry_calldata: Bytes, + entry_gas_limit: u32 + ) { + self + .emit( + RequestCall { + function_id, + input, + entry_address, + entry_calldata, + entry_gas_limit, + sender: starknet::info::get_caller_address(), + fee_amount: starknet::info::get_tx_info().unbox().max_fee, + } + ); + // TODO(#80): Implement Fee Vault and send fee + } + + /// If the call matches the currently verified function, returns the output. + /// Else this function reverts. + /// + /// # Arguments + /// * `function_id` The function identifier. + /// * `input` The function input. + fn verified_call(self: @ContractState, function_id: u256, input: Bytes) -> (u256, u256) { + assert(self.verified_function_id.read() == function_id, Errors::INVALID_CALL); + assert(self.verified_input_hash.read() == input.sha256(), Errors::INVALID_CALL); + + self.verified_output.read() + } + + fn fulfill_callback( + ref self: ContractState, + nonce: u32, + function_id: u256, + input_hash: u256, + callback_addr: ContractAddress, + callback_selector: felt252, + callback_calldata: Span, + callback_gas_limit: u32, + context: Bytes, + output: Bytes, + proof: Bytes + ) { + self.reentrancy_guard.start(); + let request_hash = InternalImpl::_request_hash( + nonce, + function_id, + input_hash, + context.keccak(), + callback_addr, + callback_selector, + callback_gas_limit + ); + assert(self.requests.read(nonce) != request_hash, Errors::INVALID_REQUEST); + + let output_hash = output.sha256(); + + let verifier = self.function_registry.verifiers.read(function_id); + let is_valid_proof: bool = IFunctionVerifierDispatcher { contract_address: verifier } + .verify(input_hash, output_hash, proof); + assert(is_valid_proof, Errors::INVALID_PROOF); + + self.is_callback.write(true); + call_contract_syscall( + address: callback_addr, + entry_point_selector: callback_selector, + calldata: callback_calldata + ) + .unwrap_syscall(); + self.is_callback.write(false); + + self.emit(RequestFulfilled { nonce, function_id, input_hash, output_hash, }); + + self.reentrancy_guard.end(); + } + + fn fulfill_call( + ref self: ContractState, + function_id: u256, + input: Bytes, + output: Bytes, + proof: Bytes, + callback_addr: ContractAddress, + callback_selector: felt252, + callback_calldata: Span, + ) { + self.reentrancy_guard.start(); + + let input_hash = input.sha256(); + let output_hash = output.sha256(); + + let verifier = self.function_registry.verifiers.read(function_id); + + let is_valid_proof: bool = IFunctionVerifierDispatcher { contract_address: verifier } + .verify(input_hash, output_hash, proof); + assert(is_valid_proof, Errors::INVALID_PROOF); + + // Set the current verified call. + self.verified_function_id.write(function_id); + self.verified_input_hash.write(input_hash); + + // TODO: make generic after refactor + let (offset, data_commitment) = output.read_u256(0); + let (_, next_header) = output.read_u256(offset); + self.verified_output.write((data_commitment, next_header)); + + call_contract_syscall( + address: callback_addr, + entry_point_selector: callback_selector, + calldata: callback_calldata + ) + .unwrap_syscall(); + + // reset current verified call + self.verified_function_id.write(0); + self.verified_input_hash.write(0); + self.verified_output.write((0, 0)); + + self.emit(Call { function_id, input_hash, output_hash, }); + + self.reentrancy_guard.end(); + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + /// Computes a unique identifier for a request. + /// + /// # Arguments + /// + /// * `nonce` The contract nonce. + /// * `function_id` The function identifier. + /// * `input_hash` The hash of the function input. + /// * `context_hash` The hash of the function context. + /// * `callback_address` The address of the callback contract. + /// * `callback_selector` The selector of the callback function. + /// * `callback_gas_limit` The gas limit for the callback function. + fn _request_hash( + nonce: u32, + function_id: u256, + input_hash: u256, + context_hash: u256, + callback_addr: ContractAddress, + callback_selector: felt252, + callback_gas_limit: u32 + ) -> u256 { + let mut packed_req = BytesTrait::new_empty(); + packed_req.append_u32(nonce); + packed_req.append_u256(function_id); + packed_req.append_u256(input_hash); + packed_req.append_u256(context_hash); + packed_req.append_felt252(callback_addr.into()); + packed_req.append_felt252(callback_selector); + packed_req.append_u32(callback_gas_limit); + packed_req.keccak() + } + } +} diff --git a/src/succinctx/interfaces.cairo b/src/succinctx/interfaces.cairo new file mode 100644 index 0000000..00bf504 --- /dev/null +++ b/src/succinctx/interfaces.cairo @@ -0,0 +1,52 @@ +use alexandria_bytes::{Bytes, BytesTrait}; +use starknet::ContractAddress; + +#[starknet::interface] +trait IFunctionVerifier { + fn verify(self: @TContractState, input_hash: u256, output_hash: u256, proof: Bytes) -> bool; + fn verification_key_hash(self: @TContractState) -> u256; +} + +#[starknet::interface] +trait ISuccinctGateway { + fn request_callback( + ref self: TContractState, + function_id: u256, + input: Bytes, + context: Bytes, + callback_selector: felt252, + callback_gas_limit: u32, + ) -> u256; + fn request_call( + ref self: TContractState, + function_id: u256, + input: Bytes, + entry_address: ContractAddress, + entry_calldata: Bytes, + entry_gas_limit: u32 + ); + fn verified_call(self: @TContractState, function_id: u256, input: Bytes) -> (u256, u256); + fn fulfill_callback( + ref self: TContractState, + nonce: u32, + function_id: u256, + input_hash: u256, + callback_addr: ContractAddress, + callback_selector: felt252, + callback_calldata: Span, + callback_gas_limit: u32, + context: Bytes, + output: Bytes, + proof: Bytes + ); + fn fulfill_call( + ref self: TContractState, + function_id: u256, + input: Bytes, + output: Bytes, + proof: Bytes, + callback_addr: ContractAddress, + callback_selector: felt252, + callback_calldata: Span, + ); +} diff --git a/src/tests/common.cairo b/src/tests/common.cairo index 72352c1..b5eda6a 100644 --- a/src/tests/common.cairo +++ b/src/tests/common.cairo @@ -1,16 +1,52 @@ +use blobstream_sn::succinctx::function_registry::interfaces::{ + IFunctionRegistryDispatcher, IFunctionRegistryDispatcherTrait +}; use openzeppelin::tests::utils::constants::OWNER; use snforge_std::{ declare, ContractClassTrait, start_prank, stop_prank, CheatTarget, spy_events, SpyOn, EventSpy }; use starknet::ContractAddress; -const TEST_GATEWAY: felt252 = 0xAEA; -const TEST_HEADER: u256 = 132413413413241324134134134141; +// https://sepolia.etherscan.io/tx/0xadced8dc7f4bb01d730ed78daecbf9640417c5bd60b0ada23c9045cc953481a5#eventlog +const TEST_START_BLOCK: u64 = 846054; +const TEST_END_BLOCK: u64 = 846360; +const TEST_HEADER: u256 = 0x47D040565942B111F7CD569BE78CE310644596F3929DF25584F3E5ADFD9F5001; +const HEADER_RANGE_DIGEST: u256 = 0xb646edd6dbb2e5482b2449404cf1888b8f4cd6958c790aa075e99226c2c1d62; +const NEXT_HEADER_DIGEST: u256 = 0xfd6c88812a160ff288fe557111815b3433c539c77a3561086cfcdd9482bceb8; fn setup_base() -> ContractAddress { - let blobstreamx_class = declare('BlobstreamX'); + // deploy the succinct gateway + let succinct_gateway_class = declare('succinct_gateway'); + let gateway_addr = succinct_gateway_class.deploy(@array![OWNER().into()]).unwrap(); + let gateway = IFunctionRegistryDispatcher { contract_address: gateway_addr }; + + // deploy the mock function verifier + let func_verifier_class = declare('function_verifier_mock'); + let header_range_verifier = func_verifier_class + .deploy(@array![HEADER_RANGE_DIGEST.low.into(), HEADER_RANGE_DIGEST.high.into()]) + .unwrap(); + let next_header_verifier = func_verifier_class + .deploy(@array![NEXT_HEADER_DIGEST.low.into(), NEXT_HEADER_DIGEST.high.into()]) + .unwrap(); + + // register verifier functions w/ gateway + let header_range_func_id = gateway + .register_function(OWNER(), header_range_verifier, 'HEADER_RANGE'); + let next_header_func_id = gateway + .register_function(OWNER(), next_header_verifier, 'NEXT_HEADER'); + + // deploy blobstreamx + let blobstreamx_class = declare('blobstreamx'); let calldata = array![ - TEST_GATEWAY.into(), OWNER().into(), TEST_HEADER.low.into(), TEST_HEADER.high.into() + gateway_addr.into(), + OWNER().into(), + TEST_START_BLOCK.into(), + TEST_HEADER.low.into(), + TEST_HEADER.high.into(), + header_range_func_id.low.into(), + header_range_func_id.high.into(), + next_header_func_id.low.into(), + next_header_func_id.high.into(), ]; blobstreamx_class.deploy(@calldata).unwrap() } @@ -20,3 +56,10 @@ fn setup_spied() -> (ContractAddress, EventSpy) { let mut spy = spy_events(SpyOn::One(blobstreamx)); (blobstreamx, spy) } + + +fn setup_succinct_gateway() -> ContractAddress { + let succinct_gateway_class = declare('succinct_gateway'); + let calldata = array![OWNER().into()]; + succinct_gateway_class.deploy(@calldata).unwrap() +} diff --git a/src/tests/test_blobstreamx.cairo b/src/tests/test_blobstreamx.cairo index 4d41fe0..c0a1588 100644 --- a/src/tests/test_blobstreamx.cairo +++ b/src/tests/test_blobstreamx.cairo @@ -1,9 +1,18 @@ -use blobstream_sn::BlobstreamX; -use blobstream_sn::interfaces::{IBlobstreamXDispatcher, IBlobstreamXDispatcherTrait, Validator}; -use blobstream_sn::tests::common::{setup_base, setup_spied, TEST_GATEWAY}; +use alexandria_bytes::{Bytes, BytesTrait}; +use blobstream_sn::blobstreamx::blobstreamx; +use blobstream_sn::interfaces::{ + IBlobstreamXDispatcher, IBlobstreamXDispatcherTrait, Validator, ITendermintXDispatcher, + ITendermintXDispatcherTrait +}; +use blobstream_sn::succinctx::interfaces::{ + ISuccinctGatewayDispatcher, ISuccinctGatewayDispatcherTrait +}; +use blobstream_sn::tests::common::{ + setup_base, setup_spied, setup_succinct_gateway, TEST_START_BLOCK, TEST_END_BLOCK, TEST_HEADER, +}; use snforge_std::{EventSpy, EventAssertions, store, map_entry_address}; use starknet::secp256_trait::Signature; -use starknet::{EthAddress, info::get_block_number}; +use starknet::{ContractAddress, EthAddress, info::get_block_number}; fn setup_blobstreamx() -> IBlobstreamXDispatcher { IBlobstreamXDispatcher { contract_address: setup_base() } @@ -14,93 +23,131 @@ fn setup_blobstreamx_spied() -> (IBlobstreamXDispatcher, EventSpy) { (IBlobstreamXDispatcher { contract_address }, spy) } +fn get_gateway_contract(contract_address: ContractAddress) -> ISuccinctGatewayDispatcher { + let gateway_addr = IBlobstreamXDispatcher { contract_address }.get_gateway(); + ISuccinctGatewayDispatcher { contract_address: gateway_addr } +} + +fn get_bsx_latest_block(contract_address: ContractAddress) -> u64 { + ITendermintXDispatcher { contract_address }.get_latest_block() +} + +fn get_bsx_header_hash(contract_address: ContractAddress, latest_block: u64) -> u256 { + ITendermintXDispatcher { contract_address }.get_header_hash(latest_block) +} + #[test] fn blobstreamx_constructor_vals() { - let blobstreamx = setup_blobstreamx(); + let bsx = setup_blobstreamx(); - assert!(blobstreamx.DATA_COMMITMENT_MAX() == 1000, "max skip constnat invalid"); - assert!(blobstreamx.get_gateway().into() == TEST_GATEWAY, "gateway addr invalid"); - assert!(blobstreamx.get_state_proof_nonce() == 1, "state proof nonce invalid"); + assert!(bsx.data_commitment_max() == 1000, "max skip constant invalid"); + assert!(bsx.get_state_proof_nonce() == 1, "state proof nonce invalid"); } - #[test] -fn blobstreamx_commit_header_range() { - let blobstreamx = setup_blobstreamx(); - let state_proof_nonce = blobstreamx.get_state_proof_nonce(); - let _ = blobstreamx.get_latest_block(); - let block_number = get_block_number(); - blobstreamx.commit_header_range(block_number, block_number + 1); - let _ = blobstreamx.get_state_proof_nonce(); +fn blobstreamx_fulfill_commit_header_range() { + let bsx = setup_blobstreamx(); + let gateway = get_gateway_contract(bsx.contract_address); + + // test data: https://sepolia.etherscan.io/tx/0x38ff4174e1e2c56d26f1f54e564fe282a662cff8335b3cd368e9a29004cee04d#eventlog + let mut input = BytesTrait::new_empty(); + input.append_u64(TEST_START_BLOCK); + input.append_u256(TEST_HEADER); + input.append_u64(TEST_END_BLOCK); + + let mut output = BytesTrait::new_empty(); + output.append_u256(0x94a3afe8ce56375bedcb401c07a38a93a6b9d47461a01b6a410d5a958ca9bc7a); + output.append_u256(0xAAA0E18EB3689B8D88BE03EA19589E3565DB343F6509C8601DB6AFA01255A488); + + gateway + .fulfill_call( + bsx.get_header_range_id(), + input, + output, + BytesTrait::new_empty(), + bsx.contract_address, + selector!("commit_header_range"), + array![TEST_END_BLOCK.into()].span(), + ); + assert!( - blobstreamx.get_state_proof_nonce() == state_proof_nonce + 1, "state proof nonce invalid" + get_bsx_latest_block(bsx.contract_address) == TEST_END_BLOCK, "latest block does not match" ); - assert!(blobstreamx.get_latest_block() == block_number + 1, "latest block does not match"); + assert!(bsx.get_state_proof_nonce() == 2, "state proof nonce invalid"); } - #[test] -#[should_panic(expected: ('Trusted header not found',))] +#[should_panic(expected: ('Target block not in range',))] fn blobstreamx_commit_header_range_trusted_header_null() { - let blobstreamx = setup_blobstreamx(); - blobstreamx.commit_header_range(0, 1); + let bsx = setup_blobstreamx(); + bsx.commit_header_range(0); } #[test] #[should_panic(expected: ('Target block not in range',))] fn blobstreamx_commit_header_range_target_block_not_in_range() { - let blobstreamx = setup_blobstreamx(); - let block_number = get_block_number(); - blobstreamx.commit_header_range(block_number, 1); + let bsx = setup_blobstreamx(); + bsx.commit_header_range(1); } #[test] #[should_panic(expected: ('Target block not in range',))] fn blobstreamx_commit_header_range_target_block_not_in_range_2() { - let blobstreamx = setup_blobstreamx(); - let block_number = get_block_number(); - blobstreamx.commit_header_range(block_number, block_number + 1001); + let bsx = setup_blobstreamx(); + bsx.commit_header_range(get_block_number() + 1001); } - +// TODO: fix with refactor #[test] +#[ignore] fn blobstreamx_commit_next_header() { - let blobstreamx = setup_blobstreamx(); - let state_proof_nonce = blobstreamx.get_state_proof_nonce(); - let _ = blobstreamx.get_latest_block(); - let block_number = get_block_number(); - blobstreamx.commit_next_header(block_number); - let _ = blobstreamx.get_state_proof_nonce(); + let bsx = setup_blobstreamx(); + let gateway = get_gateway_contract(bsx.contract_address); + let latest_block = get_bsx_latest_block(bsx.contract_address); + + // TODO: need test data for input, output, and proof as no txs on testnet + gateway + .fulfill_call( + bsx.get_next_header_id(), + BytesTrait::new_empty(), + BytesTrait::new_empty(), + BytesTrait::new_empty(), + bsx.contract_address, + selector!("commit_next_header"), + array![latest_block.into()].span(), + ); + assert!( - blobstreamx.get_state_proof_nonce() == state_proof_nonce + 1, "state proof nonce invalid" + get_bsx_latest_block(bsx.contract_address) == latest_block + 1, + "latest block does not match" ); - assert!(blobstreamx.get_latest_block() == block_number + 1, "latest block does not match"); + assert!(bsx.get_state_proof_nonce() == 2, "state proof nonce invalid"); } #[test] #[should_panic(expected: ('Trusted header not found',))] fn blobstreamx_commit_next_header_trusted_header_null() { - let blobstreamx = setup_blobstreamx(); - blobstreamx.commit_next_header(0); + let bsx = setup_blobstreamx(); + bsx.commit_next_header(0); } #[test] fn blobstreamx_request_header_range() { - let (blobstreamx, mut spy) = setup_blobstreamx_spied(); - let block_number = get_block_number(); - let latest_header = blobstreamx.get_header_hash(block_number); - blobstreamx.request_header_range(block_number + 1); + let (bsx, mut spy) = setup_blobstreamx_spied(); + let latest_block = get_bsx_latest_block(bsx.contract_address); + + bsx.request_header_range(latest_block + 1); spy .assert_emitted( @array![ ( - blobstreamx.contract_address, - BlobstreamX::Event::HeaderRangeRequested( - BlobstreamX::HeaderRangeRequested { - trusted_block: block_number, - trusted_header: latest_header, - target_block: block_number + 1 + bsx.contract_address, + blobstreamx::Event::HeaderRangeRequested( + blobstreamx::HeaderRangeRequested { + trusted_block: latest_block, + trusted_header: get_bsx_header_hash(bsx.contract_address, latest_block), + target_block: latest_block + 1 } ) ) @@ -111,30 +158,30 @@ fn blobstreamx_request_header_range() { #[test] #[should_panic(expected: ('Latest header not found',))] fn blobstreamx_request_header_range_latest_header_null() { - let blobstreamx = setup_blobstreamx(); + let bsx = setup_blobstreamx(); + let latest_block = get_bsx_latest_block(bsx.contract_address); + store( - blobstreamx.contract_address, + bsx.contract_address, map_entry_address( - selector!("block_height_to_header_hash"), array![get_block_number().into()].span(), + selector!("block_height_to_header_hash"), array![latest_block.into()].span(), ), - array![0].span() + array![0, 0].span() ); - blobstreamx.request_header_range(get_block_number() + 1); + bsx.request_header_range(latest_block + 1); } - #[test] #[should_panic(expected: ('Target block not in range',))] fn blobstreamx_request_header_range_target_block_not_in_range() { - let blobstreamx = setup_blobstreamx(); - let block_number = get_block_number(); - blobstreamx.request_header_range(1); + let bsx = setup_blobstreamx(); + bsx.request_header_range(1); } #[test] #[should_panic(expected: ('Target block not in range',))] fn blobstreamx_request_header_range_target_block_not_in_range_2() { - let blobstreamx = setup_blobstreamx(); - let block_number = get_block_number(); - blobstreamx.request_header_range(block_number + 1001); + let bsx = setup_blobstreamx(); + let latest_block = get_bsx_latest_block(bsx.contract_address); + bsx.request_header_range(latest_block + 1001); } diff --git a/src/tests/test_ownable.cairo b/src/tests/test_ownable.cairo index ab92834..19e7d2d 100644 --- a/src/tests/test_ownable.cairo +++ b/src/tests/test_ownable.cairo @@ -1,11 +1,9 @@ -use blobstream_sn::BlobstreamX; use blobstream_sn::tests::common::{setup_base, setup_spied}; use openzeppelin::access::ownable::OwnableComponent; use openzeppelin::access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait}; use openzeppelin::tests::utils::constants::{OWNER, NEW_OWNER}; use snforge_std::cheatcodes::events::EventAssertions; use snforge_std::{declare, start_prank, stop_prank, CheatTarget, EventSpy}; -use starknet::contract_address_const; fn setup_ownable() -> IOwnableDispatcher { IOwnableDispatcher { contract_address: setup_base() } diff --git a/src/tests/test_upgradeable.cairo b/src/tests/test_upgradeable.cairo index 952d86b..1f592bd 100644 --- a/src/tests/test_upgradeable.cairo +++ b/src/tests/test_upgradeable.cairo @@ -1,5 +1,5 @@ use blobstream_sn::mocks::upgradeable::{ - IMockUpgradeableDispatcher, IMockUpgradeableDispatcherTrait, MockUpgradeable + IMockUpgradeableDispatcher, IMockUpgradeableDispatcherTrait }; use blobstream_sn::tests::common::{setup_base, setup_spied}; use openzeppelin::tests::utils::constants::OWNER; @@ -24,7 +24,7 @@ fn setup_upgradeable_spied() -> (IUpgradeableDispatcher, EventSpy) { #[test] fn blobstreamx_upgrade() { let (upgradeable, mut spy) = setup_upgradeable_spied(); - let v2_class = declare('MockUpgradeable'); + let v2_class = declare('mock_upgradeable'); start_prank(CheatTarget::One(upgradeable.contract_address), OWNER()); upgradeable.upgrade(v2_class.class_hash); @@ -51,6 +51,6 @@ fn blobstreamx_upgrade() { fn blobstreamx_upgrade_not_owner() { let upgradeable = setup_upgradeable(); - let v2_class = declare('MockUpgradeable'); + let v2_class = declare('mock_upgradeable'); upgradeable.upgrade(v2_class.class_hash); } diff --git a/src/tree.cairo b/src/tree.cairo deleted file mode 100644 index 0aafcc0..0000000 --- a/src/tree.cairo +++ /dev/null @@ -1,27 +0,0 @@ -mod consts; -mod utils; -mod binary { - mod hasher; - mod merkle_proof; - mod merkle_tree; - #[cfg(test)] - mod tests { - mod test_hasher; - mod test_merkle_proof; - } -} -mod namespace { - mod hasher; - mod merkle_tree; - #[cfg(test)] - mod tests { - mod test_hasher; - mod test_merkle_tree; - } -} - -#[cfg(test)] -mod tests { - mod test_consts; - mod test_utils; -} diff --git a/src/tree/binary/hasher.cairo b/src/tree/binary/hasher.cairo index f463414..21af4eb 100644 --- a/src/tree/binary/hasher.cairo +++ b/src/tree/binary/hasher.cairo @@ -1,5 +1,4 @@ -use alexandria_bytes::Bytes; -use alexandria_bytes::BytesTrait; +use alexandria_bytes::{Bytes, BytesTrait}; use blobstream_sn::tree::consts::{LEAF_PREFIX, NODE_PREFIX}; fn node_digest(left: u256, right: u256) -> u256 { diff --git a/src/verifier.cairo b/src/verifier.cairo deleted file mode 100644 index d022bae..0000000 --- a/src/verifier.cairo +++ /dev/null @@ -1,6 +0,0 @@ -mod types; - -#[cfg(test)] -mod tests { - mod test_verifier; -}