diff --git a/README.md b/README.md index 4ff7453..be09fc6 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ > Order on Starknet, write on Bitcoin, get money trustlessly, repeat -Broly is a decentralized Bitcoin inscription service that uses Starknet for orderbook management and escrow. It enables trustless Bitcoin inscriptions with guaranteed payments through smart contracts. +Broly is a decentralized Bitcoin inscription service that uses Starknet for orderbook management. It enables trustless Bitcoin inscriptions with guaranteed payments through smart contracts.
GitHub Workflow Status @@ -34,7 +34,6 @@ flowchart TB subgraph Starknet OB[Orderbook Contract] - ES[Escrow Contract] end subgraph Bitcoin @@ -51,10 +50,8 @@ flowchart TB UI <--> SW API --> DB SW <--> OB - SW <--> ES IS --> BTC OM --> OB - OM --> ES API --> IS IS --> API ``` @@ -65,11 +62,11 @@ flowchart TB 2. User creates an inscription order: - Specifies inscription content and reward amount - Order is created on Starknet orderbook - - Funds are locked in escrow contract + - Funds are locked in the contract 3. Inscribor service: - Monitors pending orders - Creates Bitcoin inscriptions - - Triggers escrow release on successful inscription + - Triggers reward release on successful inscription 4. User receives inscription, inscribor receives reward ## Getting Started @@ -145,7 +142,6 @@ broly/ ### Smart Contracts (onchain) - Orderbook contract -- Escrow contract - Payment handling ### Inscribor Service @@ -153,7 +149,7 @@ broly/ - Order monitoring - Bitcoin inscription creation - Transaction verification -- Starknet interaction for escrow release +- Starknet interaction for reward release ## License diff --git a/apps/web/src/components/Header.tsx b/apps/web/src/components/Header.tsx index 50a91e0..f65273c 100644 --- a/apps/web/src/components/Header.tsx +++ b/apps/web/src/components/Header.tsx @@ -1,4 +1,5 @@ import { NavLink } from "react-router"; +import { useAccount } from "@starknet-react/core"; import "./Header.css"; function Header(props: any) { diff --git a/packages/onchain/Scarb.lock b/packages/onchain/Scarb.lock index ac734f9..aee286b 100644 --- a/packages/onchain/Scarb.lock +++ b/packages/onchain/Scarb.lock @@ -6,15 +6,27 @@ name = "alexandria_math" version = "0.2.1" source = "git+https://github.com/keep-starknet-strange/alexandria#95d98a5182001d07673b856a356eff0e6bd05354" +[[package]] +name = "consensus" +version = "0.1.0" +source = "git+https://github.com/keep-starknet-strange/raito.git?rev=02a13045b7074ae2b3247431cd91f1ad76263fb2#02a13045b7074ae2b3247431cd91f1ad76263fb2" +dependencies = [ + "shinigami_engine", + "utils", +] + [[package]] name = "onchain" version = "0.1.0" dependencies = [ "alexandria_math", + "consensus", "openzeppelin", "openzeppelin_token", "openzeppelin_utils", "snforge_std", + "utils", + "utu_relay", ] [[package]] @@ -123,6 +135,31 @@ name = "openzeppelin_utils" version = "0.19.0" source = "git+https://github.com/openzeppelin/cairo-contracts?tag=v0.19.0#8d49e8c445efd9bdc99b050c8b7d11ae5ad19628" +[[package]] +name = "ripemd160" +version = "0.1.0" +source = "git+https://github.com/j1mbo64/ripemd160_cairo.git#833e07d7d074d4ee51ceb40a5bcb4af2fe6898f3" + +[[package]] +name = "sha1" +version = "0.1.0" +source = "git+https://github.com/j1mbo64/sha1_cairo.git#2b65bc00a829bdcc244c140d0f31feda32f8d2c4" + +[[package]] +name = "shinigami_engine" +version = "0.1.0" +source = "git+https://github.com/keep-starknet-strange/shinigami.git?rev=3415ed6#3415ed6331d3ea2dc2de6f9ab8e0be6562585f2d" +dependencies = [ + "ripemd160", + "sha1", + "shinigami_utils", +] + +[[package]] +name = "shinigami_utils" +version = "0.1.0" +source = "git+https://github.com/keep-starknet-strange/shinigami.git?rev=3415ed6#3415ed6331d3ea2dc2de6f9ab8e0be6562585f2d" + [[package]] name = "snforge_scarb_plugin" version = "0.33.0" @@ -135,3 +172,18 @@ source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.33.0#221b1db dependencies = [ "snforge_scarb_plugin", ] + +[[package]] +name = "utils" +version = "0.1.0" +source = "git+https://github.com/keep-starknet-strange/raito.git?rev=02a13045b7074ae2b3247431cd91f1ad76263fb2#02a13045b7074ae2b3247431cd91f1ad76263fb2" + +[[package]] +name = "utu_relay" +version = "0.1.0" +source = "git+https://github.com/lana-shanghai/utu_relay.git#25f8d9799df04465c716155e9ece61d9145b0f8c" +dependencies = [ + "openzeppelin", + "openzeppelin_upgrades", + "utils", +] diff --git a/packages/onchain/Scarb.toml b/packages/onchain/Scarb.toml index 4a81186..dc3e2ff 100644 --- a/packages/onchain/Scarb.toml +++ b/packages/onchain/Scarb.toml @@ -5,6 +5,9 @@ edition = "2024_07" # See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html +[patch.crates-io] +openzeppelin = "0.19.0" + [dependencies] snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.33.0" } openzeppelin = { git = "https://github.com/openzeppelin/cairo-contracts", tag = "v0.19.0" } @@ -12,11 +15,19 @@ starknet = "2.9.1" alexandria_math = { git = "https://github.com/keep-starknet-strange/alexandria" } openzeppelin_token = { git = "https://github.com/openzeppelin/cairo-contracts", tag = "v0.19.0" } openzeppelin_utils = { git = "https://github.com/openzeppelin/cairo-contracts", tag = "v0.19.0" } +utils = { git = "https://github.com/keep-starknet-strange/raito.git", rev = "02a13045b7074ae2b3247431cd91f1ad76263fb2" } +consensus = { git = "https://github.com/keep-starknet-strange/raito.git", rev = "02a13045b7074ae2b3247431cd91f1ad76263fb2" } +utu_relay = { git = "https://github.com/lana-shanghai/utu_relay.git" } [[target.starknet-contract]] casm = true sierra = true -build-external-contracts = ["openzeppelin_presets::erc20::ERC20Upgradeable"] +build-external-contracts = [ + "openzeppelin_presets::erc20::ERC20Upgradeable", + "utu_relay::utu_relay::UtuRelay" +] +allowed-libfuncs-list.name = "experimental" +casm-add-pythonic-hints = true [dev-dependencies] assert_macros = "2.9.1" diff --git a/packages/onchain/src/escrow.cairo b/packages/onchain/src/escrow.cairo deleted file mode 100644 index 2af7051..0000000 --- a/packages/onchain/src/escrow.cairo +++ /dev/null @@ -1 +0,0 @@ -mod escrow; diff --git a/packages/onchain/src/escrow/escrow.cairo b/packages/onchain/src/escrow/escrow.cairo deleted file mode 100644 index b69d849..0000000 --- a/packages/onchain/src/escrow/escrow.cairo +++ /dev/null @@ -1,17 +0,0 @@ -#[starknet::interface] -trait IEscrow { - fn greet(ref self: TContractState) -> felt252; -} - -#[starknet::contract] -mod Escrow { - #[storage] - struct Storage {} - - #[abi(embed_v0)] - impl EscrowImpl of super::IEscrow { - fn greet(ref self: ContractState) -> felt252 { - 'Kakarotto' - } - } -} diff --git a/packages/onchain/src/lib.cairo b/packages/onchain/src/lib.cairo index 9bfc256..51b511c 100644 --- a/packages/onchain/src/lib.cairo +++ b/packages/onchain/src/lib.cairo @@ -1,3 +1,3 @@ -mod escrow; mod orderbook; mod utils; +mod relay; diff --git a/packages/onchain/src/orderbook/interface.cairo b/packages/onchain/src/orderbook/interface.cairo index 43f720d..aa99638 100644 --- a/packages/onchain/src/orderbook/interface.cairo +++ b/packages/onchain/src/orderbook/interface.cairo @@ -16,16 +16,15 @@ pub trait IOrderbook { ref self: TContractState, inscription_data: ByteArray, receiving_address: ByteArray, - satoshi: felt252, currency_fee: felt252, submitter_fee: u256, ) -> u32; fn cancel_inscription(ref self: TContractState, inscription_id: u32, currency_fee: felt252); fn lock_inscription(ref self: TContractState, inscription_id: u32, tx_hash: ByteArray); fn submit_inscription(ref self: TContractState, inscription_id: u32, tx_hash: ByteArray); - fn query_inscription(self: @TContractState, inscription_id: u32) -> (ByteArray, u256); - fn is_valid_bitcoin_address(self: @TContractState, receiving_address: ByteArray) -> bool; - fn is_locked(self: @TContractState, tx_hash: ByteArray) -> (bool, ContractAddress); + fn query_inscription( + self: @TContractState, inscription_id: u32, + ) -> (ContractAddress, ByteArray, u256); } #[starknet::interface] @@ -34,16 +33,15 @@ pub trait OrderbookABI { ref self: TContractState, inscription_data: ByteArray, receiving_address: ByteArray, - satoshi: felt252, currency_fee: felt252, submitter_fee: u256, ) -> u32; fn cancel_inscription(ref self: TContractState, inscription_id: u32, currency_fee: felt252); fn lock_inscription(ref self: TContractState, inscription_id: u32, tx_hash: ByteArray); fn submit_inscription(ref self: TContractState, inscription_id: u32, tx_hash: ByteArray); - fn query_inscription(self: @TContractState, inscription_id: u32) -> (ByteArray, u256); - fn is_valid_bitcoin_address(self: @TContractState, receiving_address: ByteArray) -> bool; - fn is_locked(self: @TContractState, tx_hash: ByteArray) -> (bool, ContractAddress); + fn query_inscription( + self: @TContractState, inscription_id: u32, + ) -> (ContractAddress, ByteArray, u256); // ERC20 functions fn balance_of(self: @TContractState, account: ContractAddress) -> felt252; diff --git a/packages/onchain/src/orderbook/mock.cairo b/packages/onchain/src/orderbook/mock.cairo index 96cbc75..6f58bf8 100644 --- a/packages/onchain/src/orderbook/mock.cairo +++ b/packages/onchain/src/orderbook/mock.cairo @@ -17,7 +17,7 @@ mod OrderbookMock { new_inscription_id: u32, // A map from the inscription ID to a tuple with the inscribed // data and submitter fee. - inscriptions: Map, + inscriptions: Map, // A map from the inscription ID to status. Possible values: // 'Open', 'Locked', 'Canceled', 'Closed'. inscription_statuses: Map, @@ -47,7 +47,6 @@ mod OrderbookMock { caller: ContractAddress, inscription_data: ByteArray, receiving_address: ByteArray, - satoshi: felt252, currency_fee: felt252, submitter_fee: u256, } @@ -85,7 +84,6 @@ mod OrderbookMock { /// Inputs: /// - `inscription_data: ByteArray`, the data to be inscribed on Bitcoin. /// - `receiving_address: ByteArray`, the taproot address that will own the inscription. - /// - `satoshi: felt252`, the Sat where the user wants to inscribe data. /// - `currency_fee: felt252`, 'STRK' tokens. /// - `submitter_fee: u256`, fee to be paid to the submitter for the inscription. /// Returns: @@ -94,12 +92,13 @@ mod OrderbookMock { ref self: ContractState, inscription_data: ByteArray, receiving_address: ByteArray, - satoshi: felt252, currency_fee: felt252, submitter_fee: u256, ) -> u32 { + assert(currency_fee == 'STRK'.into(), 'The currency is not supported'); + let caller = get_caller_address(); let id = self.new_inscription_id.read(); - self.inscriptions.write(id, (inscription_data.clone(), submitter_fee)); + self.inscriptions.write(id, (caller, inscription_data.clone(), submitter_fee)); self.inscription_statuses.write(id, Status::Open); self.new_inscription_id.write(id + 1); self @@ -109,7 +108,6 @@ mod OrderbookMock { caller: get_caller_address(), inscription_data: inscription_data, receiving_address: receiving_address, - satoshi: satoshi, currency_fee: currency_fee, submitter_fee: submitter_fee, }, @@ -123,8 +121,24 @@ mod OrderbookMock { /// cancel. /// - `currency_fee: felt252`, the token that the user paid the submitter fee in. fn cancel_inscription(ref self: ContractState, inscription_id: u32, currency_fee: felt252) { - let (inscription_data, _) = self.inscriptions.read(inscription_id); - self.inscriptions.write(inscription_id, (inscription_data, 0)); + let caller = get_caller_address(); + let (request_creator, inscription_data, amount) = self + .inscriptions + .read(inscription_id); + assert(caller == request_creator, 'Caller cannot cancel this id'); + + let status = self.inscription_statuses.read(inscription_id); + assert(status != Status::Undefined, 'Inscription does not exist'); + assert(status != Status::Locked, 'The inscription is locked'); + assert(status != Status::Canceled, 'The inscription is canceled'); + assert(status != Status::Closed, 'The inscription has been closed'); + + let escrow_address = get_contract_address(); + if (currency_fee == 'STRK'.into()) { + let strk_token = self.strk_token.read(); + strk_token.transfer_from(sender: escrow_address, recipient: caller, amount: amount); + } + self.inscriptions.write(inscription_id, (caller, inscription_data, 0)); self.inscription_statuses.write(inscription_id, Status::Canceled); self .emit( @@ -165,18 +179,11 @@ mod OrderbookMock { self.emit(RequestCompleted { inscription_id: inscription_id, tx_hash: tx_hash }); } - fn query_inscription(self: @ContractState, inscription_id: u32) -> (ByteArray, u256) { + fn query_inscription( + self: @ContractState, inscription_id: u32, + ) -> (ContractAddress, ByteArray, u256) { return self.inscriptions.read(inscription_id); } - - - fn is_valid_bitcoin_address(self: @ContractState, receiving_address: ByteArray) -> bool { - return true; - } - - fn is_locked(self: @ContractState, tx_hash: ByteArray) -> (bool, ContractAddress) { - return (true, get_contract_address()); - } } #[generate_trait] diff --git a/packages/onchain/src/orderbook/orderbook.cairo b/packages/onchain/src/orderbook/orderbook.cairo index 4bcedaf..7cfb441 100644 --- a/packages/onchain/src/orderbook/orderbook.cairo +++ b/packages/onchain/src/orderbook/orderbook.cairo @@ -10,14 +10,15 @@ mod Orderbook { StoragePointerReadAccess, StoragePointerWriteAccess, }; use starknet::{ContractAddress, get_caller_address, get_contract_address, get_block_number}; + use starknet::{SyscallResultTrait, syscalls::call_contract_syscall}; #[storage] struct Storage { // ID of the next inscription. new_inscription_id: u32, - // A map from the inscription ID to a tuple with the inscribed - // data and submitter fee. - inscriptions: Map, + // A map from the inscription ID to a tuple with the caller, the + // inscribed data, and submitter fee. + inscriptions: Map, // A map from the inscription ID to status. Possible values: // 'Open', 'Locked', 'Canceled', 'Closed'. inscription_statuses: Map, @@ -28,12 +29,60 @@ mod Orderbook { inscription_locks: Map, // STRK fee token. strk_token: ERC20ABIDispatcher, + // Address of the contract checking transaction inclusion. + relay_address: ContractAddress, } #[constructor] - fn constructor(ref self: ContractState, strk_token: ContractAddress) { + fn constructor( + ref self: ContractState, strk_token: ContractAddress, relay_address: ContractAddress, + ) { // initialize contract - self.initializer(:strk_token); + self.initializer(:strk_token, :relay_address); + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + RequestCreated: RequestCreated, + RequestCanceled: RequestCanceled, + RequestLocked: RequestLocked, + RequestCompleted: RequestCompleted, + } + + #[derive(Drop, starknet::Event)] + pub struct RequestCreated { + #[key] + pub id: u32, + #[key] + pub caller: ContractAddress, + pub inscription_data: ByteArray, + pub receiving_address: ByteArray, + pub currency_fee: felt252, + pub submitter_fee: u256, + } + + #[derive(Drop, starknet::Event)] + pub struct RequestCanceled { + #[key] + pub id: u32, + pub currency_fee: felt252, + } + + #[derive(Drop, starknet::Event)] + pub struct RequestLocked { + #[key] + pub id: u32, + pub submitter: ContractAddress, + pub tx_hash: ByteArray, + } + + #[derive(Drop, starknet::Event)] + pub struct RequestCompleted { + #[key] + pub id: u32, + pub submitter: ContractAddress, + pub tx_hash: ByteArray, } #[abi(embed_v0)] @@ -42,7 +91,6 @@ mod Orderbook { /// Inputs: /// - `inscription_data: ByteArray`, the data to be inscribed on Bitcoin. /// - `receiving_address: ByteArray`, the taproot address that will own the inscription. - /// - `satoshi: felt252`, the Sat where the user wants to inscribe data. /// - `currency_fee: felt252`, 'STRK' tokens. /// - `submitter_fee: u256`, fee to be paid to the submitter for the inscription. /// Returns: @@ -51,46 +99,43 @@ mod Orderbook { ref self: ContractState, inscription_data: ByteArray, receiving_address: ByteArray, - satoshi: felt252, currency_fee: felt252, submitter_fee: u256, ) -> u32 { - assert( - self.is_valid_bitcoin_address(receiving_address) == true, - 'Not a valid bitcoin address', - ); assert(currency_fee == 'STRK'.into(), 'The currency is not supported'); let caller = get_caller_address(); let escrow_address = get_contract_address(); if (currency_fee == 'STRK'.into()) { let strk_token = self.strk_token.read(); - // TODO: change the transfer to the escrow contract once it's implemented. strk_token .transfer_from( sender: caller, recipient: escrow_address, amount: submitter_fee, ); } let id = self.new_inscription_id.read(); - self.inscriptions.write(id, (inscription_data, submitter_fee)); + self.inscriptions.write(id, (caller, inscription_data.clone(), submitter_fee)); self.inscription_statuses.write(id, Status::Open); + self + .emit( + RequestCreated { + id: id, + caller: caller, + inscription_data: inscription_data, + receiving_address: receiving_address, + currency_fee: currency_fee, + submitter_fee: submitter_fee, + }, + ); id } - /// Helper function that checks the format of the taproot address. - /// Inputs: - /// - `receiving_address: ByteArray`, the ID of the inscription. - /// Returns: - /// - `bool` - fn is_valid_bitcoin_address(self: @ContractState, receiving_address: ByteArray) -> bool { - // TODO: implement the check that the receiving address is in valid format. - true - } - /// Inputs: /// - `inscription_id: felt252`, the ID of the inscription. /// Returns: /// - `(ByteArray, felt252)`, the tuple with the inscribed data and the fee. - fn query_inscription(self: @ContractState, inscription_id: u32) -> (ByteArray, u256) { + fn query_inscription( + self: @ContractState, inscription_id: u32, + ) -> (ContractAddress, ByteArray, u256) { self.inscriptions.read(inscription_id) } @@ -100,23 +145,26 @@ mod Orderbook { /// cancel. /// - `currency_fee: felt252`, the token that the user paid the submitter fee in. fn cancel_inscription(ref self: ContractState, inscription_id: u32, currency_fee: felt252) { + let caller = get_caller_address(); + let (request_creator, inscription_data, amount) = self + .inscriptions + .read(inscription_id); + assert(caller == request_creator, 'Caller cannot cancel this id'); + let status = self.inscription_statuses.read(inscription_id); assert(status != Status::Undefined, 'Inscription does not exist'); assert(status != Status::Locked, 'The inscription is locked'); assert(status != Status::Canceled, 'The inscription is canceled'); assert(status != Status::Closed, 'The inscription has been closed'); - let caller = get_caller_address(); - // TODO: change the address to the actual escrow contract once it's implemented. let escrow_address = get_contract_address(); if (currency_fee == 'STRK'.into()) { let strk_token = self.strk_token.read(); - let (_, amount) = self.inscriptions.read(inscription_id); strk_token.transfer_from(sender: escrow_address, recipient: caller, amount: amount); } - let (inscription_data, _) = self.inscriptions.read(inscription_id); - self.inscriptions.write(inscription_id, (inscription_data, 0)); + self.inscriptions.write(inscription_id, (caller, inscription_data, 0)); self.inscription_statuses.write(inscription_id, Status::Canceled); + self.emit(RequestCanceled { id: inscription_id, currency_fee: currency_fee }); } /// Called by a submitter. Multiple submitters are allowed to lock the @@ -146,6 +194,7 @@ mod Orderbook { submitters.write(submitter, submitter); self.inscription_statuses.write(inscription_id, Status::Locked); + self.emit(RequestLocked { id: inscription_id, submitter: submitter, tx_hash: tx_hash }); } /// Called by a submitter. The fee is transferred to the submitter if @@ -156,25 +205,28 @@ mod Orderbook { /// - `inscription_id: felt252`, the ID of the inscription being locked. /// - `tx_hash: ByteArray`, the hash of the transaction submitted to Bitcoin. fn submit_inscription(ref self: ContractState, inscription_id: u32, tx_hash: ByteArray) { + let caller = get_caller_address(); + let submitters = self.submitters.entry(inscription_id); + let submitter = submitters.read(caller); + assert(caller == submitter, 'Caller does not match submitter'); + let (_, precomputed_tx_hash, _) = self.inscription_locks.read(inscription_id); assert(precomputed_tx_hash == tx_hash, 'Precomputed hash != submitted'); - // TODO: process the submitted transaction hash, verify that it is on Bitcoin + const selector: felt252 = selector!("prove_inclusion"); + let to = self.relay_address.read(); + let calldata: Array = array![]; - self.inscription_statuses.write(inscription_id, Status::Closed); - } + // TODO: assert successful inclusion call + call_contract_syscall(to, selector, calldata.span()).unwrap_syscall(); - /// Helper function that checks if the inscription has already been locked. - /// Inputs: - /// - `tx_hash: ByteArray`, the precomputed transaction hash for the inscription - /// being locked. - /// Returns: - /// - `(bool, ContractAddress)` - fn is_locked(self: @ContractState, tx_hash: ByteArray) -> (bool, ContractAddress) { - // TODO: fetch the relevant lock made with the precomputed tx hash + // TODO: assert that the witness data contains the requested inscription - let caller = get_caller_address(); - (true, caller) + self.inscription_statuses.write(inscription_id, Status::Closed); + self + .emit( + RequestCompleted { id: inscription_id, submitter: submitter, tx_hash: tx_hash }, + ); } } @@ -182,8 +234,11 @@ mod Orderbook { pub impl InternalImpl of InternalTrait { /// Executed once when the Orderbook contract is deployed. Used to set /// initial values for contract storage variables for the fee tokens. - fn initializer(ref self: ContractState, strk_token: ContractAddress) { + fn initializer( + ref self: ContractState, strk_token: ContractAddress, relay_address: ContractAddress, + ) { self.strk_token.write(ERC20ABIDispatcher { contract_address: strk_token }); + self.relay_address.write(relay_address); } } } diff --git a/packages/onchain/src/orderbook/test_orderbook.cairo b/packages/onchain/src/orderbook/test_orderbook.cairo index 050bd78..5bb5bdb 100644 --- a/packages/onchain/src/orderbook/test_orderbook.cairo +++ b/packages/onchain/src/orderbook/test_orderbook.cairo @@ -13,50 +13,66 @@ use onchain::utils::{constants, erc20_utils}; fn setup_orderbook( - erc20_contract_address: ContractAddress, -) -> (OrderbookABIDispatcher, ContractAddress) { + erc20_contract_address: ContractAddress, relay_address: ContractAddress, +) -> OrderbookABIDispatcher { // declare Orderbook contract let contract_class = declare("Orderbook").unwrap().contract_class(); // deploy Orderbook contract let mut calldata = array![]; calldata.append_serde(erc20_contract_address); + calldata.append_serde(relay_address); let (contract_address, _) = contract_class.deploy(@calldata).unwrap(); - (OrderbookABIDispatcher { contract_address }, contract_address) + OrderbookABIDispatcher { contract_address } } -fn setup() -> ( - OrderbookABIDispatcher, ContractAddress, ERC20UpgradeableABIDispatcher, ContractAddress, -) { +fn setup_relay() -> ContractAddress { + // declare TransactionInclusion contract + let contract_class = declare("TransactionInclusion").unwrap().contract_class(); + + // deploy TransactionInclusion contract + let mut calldata = array![]; + calldata.append_serde(constants::UTU()); // TODO replace with deployed Utu contract + + let (relay_address, _) = contract_class.deploy(@calldata).unwrap(); + + relay_address +} + +fn setup() -> (OrderbookABIDispatcher, ERC20UpgradeableABIDispatcher) { // deploy an ERC20 - let (erc20_strk, erc20_address) = erc20_utils::setup_erc20(test_address()); + let (erc20_strk, _) = erc20_utils::setup_erc20(test_address()); + + // deploy relay contract + let relay_address = setup_relay(); // deploy Orderbook contract - let (orderbook, contract_address) = setup_orderbook(erc20_strk.contract_address); + let orderbook = setup_orderbook(erc20_strk.contract_address, relay_address); - (orderbook, contract_address, erc20_strk, erc20_address) + (orderbook, erc20_strk) } #[test] fn test_request_inscription_stored_and_retrieved() { - let (orderbook_dispatcher, contract_address, token_dispatcher, _) = setup(); + let (orderbook_dispatcher, token_dispatcher) = setup(); let test_taproot_address: ByteArray = "bc1p5d7rjq7g6r4jdyhzks9smlaqtedr4dekq08ge8ztwac72sfr9rusxg3297"; let test_data: ByteArray = "data"; - token_dispatcher.approve(contract_address, 100); + token_dispatcher.approve(orderbook_dispatcher.contract_address, 100); - orderbook_dispatcher.request_inscription(test_data, test_taproot_address, 1, 'STRK'.into(), 10); + orderbook_dispatcher.request_inscription(test_data, test_taproot_address, 'STRK'.into(), 10); - let expected = ("data", 10); // the inscription data and the submitter fee + let expected = (test_address(), "data", 10); // the inscription data and the submitter fee let actual = orderbook_dispatcher.query_inscription(0); assert_eq!(expected, actual); let expected_contract_balance = 10; // the submitter fee transferred to the contract - let actual_contract_balance = token_dispatcher.balance_of(contract_address); + let actual_contract_balance = token_dispatcher + .balance_of(orderbook_dispatcher.contract_address); assert_eq!(expected_contract_balance, actual_contract_balance); let expected_user_balance = constants::SUPPLY - 10; // the user balance after the request call @@ -67,41 +83,40 @@ fn test_request_inscription_stored_and_retrieved() { #[test] #[should_panic] fn test_request_inscription_fails_wrong_currency() { - let (orderbook_dispatcher, contract_address, token_dispatcher, _) = setup(); + let (orderbook_dispatcher, token_dispatcher) = setup(); let test_taproot_address: ByteArray = "test"; let test_data: ByteArray = "data"; - token_dispatcher.approve(contract_address, 100); + token_dispatcher.approve(orderbook_dispatcher.contract_address, 100); - orderbook_dispatcher.request_inscription(test_data, test_taproot_address, 1, 'BTC'.into(), 10); + orderbook_dispatcher.request_inscription(test_data, test_taproot_address, 'BTC'.into(), 10); } #[test] #[should_panic] fn test_request_inscription_fails_insufficient_balance() { - let (orderbook_dispatcher, contract_address, token_dispatcher, _) = setup(); + let (orderbook_dispatcher, token_dispatcher) = setup(); let test_taproot_address: ByteArray = "test"; let test_data: ByteArray = "data"; - token_dispatcher.approve(contract_address, 2000); + token_dispatcher.approve(orderbook_dispatcher.contract_address, 2000); - orderbook_dispatcher - .request_inscription(test_data, test_taproot_address, 1, 'STRK'.into(), 2000); + orderbook_dispatcher.request_inscription(test_data, test_taproot_address, 'STRK'.into(), 2000); } #[test] fn test_lock_inscription_works() { - let (orderbook_dispatcher, contract_address, token_dispatcher, _) = setup(); + let (orderbook_dispatcher, token_dispatcher) = setup(); let test_taproot_address: ByteArray = "test"; let test_data: ByteArray = "data"; - token_dispatcher.approve(contract_address, 100); + token_dispatcher.approve(orderbook_dispatcher.contract_address, 100); let id = orderbook_dispatcher - .request_inscription(test_data, test_taproot_address, 1, 'STRK'.into(), 10); + .request_inscription(test_data, test_taproot_address, 'STRK'.into(), 10); start_cheat_block_number_global(1000); orderbook_dispatcher.lock_inscription(id, "hash"); @@ -111,15 +126,15 @@ fn test_lock_inscription_works() { #[test] #[should_panic] fn test_lock_inscription_fails_prior_lock_not_expired() { - let (orderbook_dispatcher, contract_address, token_dispatcher, _) = setup(); + let (orderbook_dispatcher, token_dispatcher) = setup(); let test_taproot_address: ByteArray = "test"; let test_data: ByteArray = "data"; - token_dispatcher.approve(contract_address, 100); + token_dispatcher.approve(orderbook_dispatcher.contract_address, 100); let id = orderbook_dispatcher - .request_inscription(test_data, test_taproot_address, 1, 'STRK'.into(), 10); + .request_inscription(test_data, test_taproot_address, 'STRK'.into(), 10); orderbook_dispatcher.lock_inscription(id, "hash"); orderbook_dispatcher.lock_inscription(id, "other_hash"); @@ -128,15 +143,15 @@ fn test_lock_inscription_fails_prior_lock_not_expired() { #[test] #[should_panic] fn test_lock_inscription_fails_inscription_not_found() { - let (orderbook_dispatcher, contract_address, token_dispatcher, _) = setup(); + let (orderbook_dispatcher, token_dispatcher) = setup(); let test_taproot_address: ByteArray = "test"; let test_data: ByteArray = "data"; - token_dispatcher.approve(contract_address, 100); + token_dispatcher.approve(orderbook_dispatcher.contract_address, 100); let _ = orderbook_dispatcher - .request_inscription(test_data, test_taproot_address, 1, 'STRK'.into(), 10); + .request_inscription(test_data, test_taproot_address, 'STRK'.into(), 10); orderbook_dispatcher.lock_inscription(42, "hash"); } @@ -147,19 +162,19 @@ fn test_lock_inscription_fails_status_closed() { // TODO: when `submit_inscripti #[test] fn test_cancel_inscription_works() { - let (orderbook_dispatcher, contract_address, token_dispatcher, _) = setup(); + let (orderbook_dispatcher, token_dispatcher) = setup(); let test_taproot_address: ByteArray = "test"; let test_data: ByteArray = "data"; - token_dispatcher.approve(contract_address, 100); + token_dispatcher.approve(orderbook_dispatcher.contract_address, 100); let id = orderbook_dispatcher - .request_inscription(test_data, test_taproot_address, 1, 'STRK'.into(), 10); + .request_inscription(test_data, test_taproot_address, 'STRK'.into(), 10); - start_cheat_caller_address_global(contract_address); + start_cheat_caller_address_global(orderbook_dispatcher.contract_address); // TODO: is this the correct way to set permissions? - token_dispatcher.approve(contract_address, 100); + token_dispatcher.approve(orderbook_dispatcher.contract_address, 100); stop_cheat_caller_address_global(); orderbook_dispatcher.cancel_inscription(id, 'STRK'.into()); @@ -168,15 +183,15 @@ fn test_cancel_inscription_works() { #[test] #[should_panic] fn test_cancel_inscription_fails_locked() { - let (orderbook_dispatcher, contract_address, token_dispatcher, _) = setup(); + let (orderbook_dispatcher, token_dispatcher) = setup(); let test_taproot_address: ByteArray = "test"; let test_data: ByteArray = "data"; - token_dispatcher.approve(contract_address, 100); + token_dispatcher.approve(orderbook_dispatcher.contract_address, 100); let id = orderbook_dispatcher - .request_inscription(test_data, test_taproot_address, 1, 'STRK'.into(), 10); + .request_inscription(test_data, test_taproot_address, 'STRK'.into(), 10); orderbook_dispatcher.lock_inscription(id, "hash"); orderbook_dispatcher.cancel_inscription(id, 'STRK'.into()) @@ -189,18 +204,18 @@ fn test_cancel_inscription_fails_closed() { // TODO: when `submit_inscription` i #[test] #[should_panic] fn test_cancel_inscription_fails_canceled() { - let (orderbook_dispatcher, contract_address, token_dispatcher, _) = setup(); + let (orderbook_dispatcher, token_dispatcher) = setup(); let test_taproot_address: ByteArray = "test"; let test_data: ByteArray = "data"; - token_dispatcher.approve(contract_address, 100); + token_dispatcher.approve(orderbook_dispatcher.contract_address, 100); let id = orderbook_dispatcher - .request_inscription(test_data, test_taproot_address, 1, 'STRK'.into(), 10); + .request_inscription(test_data, test_taproot_address, 'STRK'.into(), 10); - start_cheat_caller_address_global(contract_address); - token_dispatcher.approve(contract_address, 100); + start_cheat_caller_address_global(orderbook_dispatcher.contract_address); + token_dispatcher.approve(orderbook_dispatcher.contract_address, 100); stop_cheat_caller_address_global(); orderbook_dispatcher.cancel_inscription(id, 'STRK'.into()); diff --git a/packages/onchain/src/relay.cairo b/packages/onchain/src/relay.cairo new file mode 100644 index 0000000..a723721 --- /dev/null +++ b/packages/onchain/src/relay.cairo @@ -0,0 +1 @@ +mod relay; diff --git a/packages/onchain/src/relay/relay.cairo b/packages/onchain/src/relay/relay.cairo new file mode 100644 index 0000000..0f10308 --- /dev/null +++ b/packages/onchain/src/relay/relay.cairo @@ -0,0 +1,61 @@ +use utils::hash::Digest; +use utu_relay::bitcoin::block::BlockHeader; + +#[starknet::interface] +pub trait ITransactionInclusion { + fn prove_inclusion( + ref self: TContractState, + tx_id: Digest, + block_height: u64, + block_header: BlockHeader, + tx_inclusion: Array<(Digest, bool)>, + ); +} + +#[starknet::contract] +mod TransactionInclusion { + use onchain::utils::utils::compute_merkle_root; + use utu_relay::bitcoin::block::BlockHashTrait; + use starknet::{ContractAddress, get_block_timestamp}; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + use utils::{hash::Digest, numeric::u32_byte_reverse}; + use utu_relay::{ + interfaces::{IUtuRelayDispatcher, IUtuRelayDispatcherTrait}, bitcoin::block::BlockHeader, + }; + + #[storage] + struct Storage { + utu_address: ContractAddress, + } + + #[constructor] + fn constructor(ref self: ContractState, utu_address: ContractAddress) { + self.utu_address.write(utu_address); + } + + #[abi(embed_v0)] + impl TransactionInclusionImpl of super::ITransactionInclusion { + fn prove_inclusion( + ref self: ContractState, + tx_id: Digest, + block_height: u64, + block_header: BlockHeader, + tx_inclusion: Array<(Digest, bool)>, + ) { + // we verify this tx is included in the provided block + let merkle_root = compute_merkle_root(tx_id, tx_inclusion); + assert( + block_header.merkle_root_hash.value == merkle_root.value, + 'Invalid inclusion proof.', + ); + + // we verify this block is safe to use (part of the canonical chain & sufficient pow) + // sufficient pow for our usecase: 100 sextillion expected hashes + let utu = IUtuRelayDispatcher { contract_address: self.utu_address.read() }; + utu.assert_safe(block_height, block_header.hash(), 100_000_000_000_000_000_000_000, 0); + // we ensure this block was not premined + let block_time = u32_byte_reverse(block_header.time).into(); + assert(block_time <= get_block_timestamp(), 'Block comes from the future.'); + } + } +} diff --git a/packages/onchain/src/utils/constants.cairo b/packages/onchain/src/utils/constants.cairo index b586186..392a227 100644 --- a/packages/onchain/src/utils/constants.cairo +++ b/packages/onchain/src/utils/constants.cairo @@ -13,3 +13,7 @@ pub fn SYMBOL() -> ByteArray { pub fn OWNER() -> ContractAddress { contract_address_const::<'owner'>() } + +pub fn UTU() -> ContractAddress { + contract_address_const::<'utu'>() +} diff --git a/packages/onchain/src/utils/utils.cairo b/packages/onchain/src/utils/utils.cairo index 8b13789..b5de110 100644 --- a/packages/onchain/src/utils/utils.cairo +++ b/packages/onchain/src/utils/utils.cairo @@ -1 +1,37 @@ +use utils::hash::Digest; +use utils::double_sha256::double_sha256_parent; +/// Computes the Merkle root from a transaction hash and its siblings. +/// +/// Arguments: +/// - `tx_hash: Digest`: The transaction hash as a Digest +/// - `siblings: Array<(Digest, bool)>`: An array of tuples (Digest, bool), where the bool indicates +/// if the sibling is on the right +/// +/// Returns: +/// - `Digest`: The computed Merkle root as a Digest +pub fn compute_merkle_root(tx_hash: Digest, siblings: Array<(Digest, bool)>) -> Digest { + let mut current_hash = tx_hash; + + // Iterate through all siblings + let mut i = 0; + loop { + if i == siblings.len() { + break; + } + + let (sibling, is_left) = *siblings.at(i); + + // Concatenate current_hash and sibling based on the order + current_hash = + if is_left { + double_sha256_parent(@sibling, @current_hash) + } else { + double_sha256_parent(@current_hash, @sibling) + }; + + i += 1; + }; + + current_hash +}