diff --git a/Scarb.lock b/Scarb.lock new file mode 100644 index 0000000..386338f --- /dev/null +++ b/Scarb.lock @@ -0,0 +1,117 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "cartridge_vrf" +version = "0.1.0" +dependencies = [ + "openzeppelin", + "openzeppelin_testing", + "openzeppelin_utils", + "snforge_std", + "stark_vrf", +] + +[[package]] +name = "openzeppelin" +version = "0.15.0-rc.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?rev=a13bae3#a13bae3390a7114a8c14c0352c6898eff5f4f5d7" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_governance", + "openzeppelin_introspection", + "openzeppelin_presets", + "openzeppelin_security", + "openzeppelin_token", + "openzeppelin_upgrades", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_access" +version = "0.15.0-rc.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?rev=a13bae3#a13bae3390a7114a8c14c0352c6898eff5f4f5d7" +dependencies = [ + "openzeppelin_introspection", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_account" +version = "0.15.0-rc.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?rev=a13bae3#a13bae3390a7114a8c14c0352c6898eff5f4f5d7" +dependencies = [ + "openzeppelin_introspection", + "openzeppelin_token", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_governance" +version = "0.15.0-rc.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?rev=a13bae3#a13bae3390a7114a8c14c0352c6898eff5f4f5d7" +dependencies = [ + "openzeppelin_access", + "openzeppelin_introspection", +] + +[[package]] +name = "openzeppelin_introspection" +version = "0.15.0-rc.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?rev=a13bae3#a13bae3390a7114a8c14c0352c6898eff5f4f5d7" + +[[package]] +name = "openzeppelin_presets" +version = "0.15.0-rc.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?rev=a13bae3#a13bae3390a7114a8c14c0352c6898eff5f4f5d7" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_introspection", + "openzeppelin_token", + "openzeppelin_upgrades", +] + +[[package]] +name = "openzeppelin_security" +version = "0.15.0-rc.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?rev=a13bae3#a13bae3390a7114a8c14c0352c6898eff5f4f5d7" + +[[package]] +name = "openzeppelin_testing" +version = "0.15.0-rc.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?rev=a13bae3#a13bae3390a7114a8c14c0352c6898eff5f4f5d7" +dependencies = [ + "snforge_std", +] + +[[package]] +name = "openzeppelin_token" +version = "0.15.0-rc.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?rev=a13bae3#a13bae3390a7114a8c14c0352c6898eff5f4f5d7" +dependencies = [ + "openzeppelin_account", + "openzeppelin_governance", + "openzeppelin_introspection", +] + +[[package]] +name = "openzeppelin_upgrades" +version = "0.15.0-rc.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?rev=a13bae3#a13bae3390a7114a8c14c0352c6898eff5f4f5d7" + +[[package]] +name = "openzeppelin_utils" +version = "0.15.0-rc.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?rev=a13bae3#a13bae3390a7114a8c14c0352c6898eff5f4f5d7" + +[[package]] +name = "snforge_std" +version = "0.26.0" +source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.26.0#50eb589db65e113efe4f09241feb59b574228c7e" + +[[package]] +name = "stark_vrf" +version = "0.1.0" +source = "git+https://github.com/dojoengine/stark-vrf.git#96d6d2a88b1ef46c4a285d0ccc334237205edae3" diff --git a/contracts/README.md b/contracts/README.md index cefe924..7323481 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -1,51 +1,3 @@ -# VRF for Cartridge Controller with paymaster +## Documentation -Randomness is requested by calling `vrf_provider.request_random` with: -- caller : the contract that will call consume_random -- source : one of this 2 options : - -**Source::Nonce(address)** : -- each request_random will generate a unique seed using a nonce by address (nonce is increased after each call) -- (its recommanded to use wallet address to avoid nonce collisions) - -**Source::Salt(salt)** : -- you have to provider you own salt -- using same salt will generate same seed = same randomness - - -## How it works - -caller send multicall : - -``` -[ - vrf_provider.request_random(game_contract.address, Source::Nonce(wallet_address)), - game_contract.you_function_consuming_randomness(...params) -] -``` - -Cartridge backend receive the tx, -retrieve seed using vrf_provider.get_next_seed( caller ), -compute proof for seed -and inject calls to sandwitch caller in a multicall : - -``` -[ - vrf_provider.submit_random( seed, proof), - controller.outside_execution([ - vrf_provider.request_random(game_contract.address, Source::Nonce(wallet_address)), - game_contract.you_function_consuming_randomness(...params) - ]) - vrf_provider.assert_consumed( seed ), -] -``` - -# Notes - -- caller must be a Cartridge Controller -- Randomness must be consume -- Randomness can only be consumed once -- Tx (submit_random / user calls / assert_consumed) is executed atomically by Cartridge backend -- Sumbitted randomness only last for the tx duration -- It's not possible to request_random in a tx and consume_random in another tx -- User cannot probe randomness +[Cartridge Controller Documentation](https://docs.cartridge.gg/controller/overview) \ No newline at end of file diff --git a/contracts/dojo/buildVrfCalls.ts b/contracts/dojo/buildVrfCalls.ts index b42168b..d0ad756 100644 --- a/contracts/dojo/buildVrfCalls.ts +++ b/contracts/dojo/buildVrfCalls.ts @@ -1,67 +1,80 @@ -import { StarkVRF } from "stark-vrf-wasm"; -import { AccountInterface, Call, CallData} from "starknet"; +import { AccountInterface, BlockTag, Call, CallData, hash, selector } from "starknet"; +enum Source { + Nonce = 0x0, + Salt = 0x1, +} export const buildVrfCalls = async ({ - account, - call, - vrfProviderAddress, - vrfProviderSecret, - }: { - account: AccountInterface; - call: Call; - vrfProviderAddress: string; - vrfProviderSecret?: string; - }): Promise => { - - const [seed] = await account.callContract({ + account, + call, + vrfProviderAddress, + vrfProviderSecret, +}: { + account: AccountInterface; + call: Call; + vrfProviderAddress: string; + vrfProviderSecret?: string; +}): Promise => { + // fn request_random(caller: ContractAddress, source: Source) -> felt252; + const requestRandomCall: Call = { + contractAddress: vrfProviderAddress, + entrypoint: "request_random", + calldata: [call.contractAddress, Source.Nonce, account.address], + }; + + let submitRandomCall = undefined; + let assertConsumedCall = undefined; + + if (vrfProviderSecret) { + const chainId = await account.getChainId(); + + const nonceStorageSlot = hash.computePedersenHash( + selector.getSelectorFromName("VrfProvider_nonces"), + account.address, + ); + + const nonce = await account.getStorageAt(vrfProviderAddress, nonceStorageSlot, BlockTag.pending); + const seed = hash.computePoseidonHashOnElements([nonce, call.contractAddress, chainId]); + console.log(chainId); + console.log(nonceStorageSlot); + console.log(nonce); + console.log(seed); + + // const vrf = StarkVRF.new(vrfProviderSecret); + + const vrf = (await import("stark-vrf-wasm")).StarkVRF.new(vrfProviderSecret); + const proof = vrf.prove(vrfProviderSecret, seed); + const sqrt_ratio_hint = vrf.hashToSqrtRatioHint(seed); + + // fn submit_random( seed: felt252, proof: Proof); + submitRandomCall = { contractAddress: vrfProviderAddress, - entrypoint: "get_next_seed", - calldata: [account.address], - }); - - const requestRandomCall: Call = { + entrypoint: "submit_random", + calldata: CallData.compile([seed, proof, sqrt_ratio_hint]), + }; + + // fn assert_consumed( seed: felt252,); + assertConsumedCall = { contractAddress: vrfProviderAddress, - entrypoint: "request_random", - calldata: [], + entrypoint: "assert_consumed", + calldata: [seed], }; - - let submitRandomCall: Call|undefined = undefined; - let assertConsumedCall: Call|undefined = undefined; - - if (vrfProviderSecret) { - const vrf = StarkVRF.new(vrfProviderSecret); - const proof = vrf.prove(vrfProviderSecret, seed); - const sqrt_ratio_hint = vrf.hashToSqrtRatioHint(seed); - - // fn submit_random( seed: felt252, proof: Proof); - submitRandomCall = { - contractAddress: vrfProviderAddress, - entrypoint: "submit_random", - calldata: CallData.compile([seed, proof, sqrt_ratio_hint]), - }; - - // fn assert_consumed( seed: felt252,); - assertConsumedCall = { - contractAddress: vrfProviderAddress, - entrypoint: "assert_consumed", - calldata: [seed], - }; - } - - let calls = []; - if (vrfProviderSecret) { - calls.push(submitRandomCall as Call); - } - - calls.push(requestRandomCall); - calls.push(call); - - if (vrfProviderSecret) { - calls.push(assertConsumedCall as Call); - } - - console.log("calls", calls); - return calls; - }; \ No newline at end of file + } + + let calls = []; + if (vrfProviderSecret) { + calls.push(submitRandomCall as Call); + } + + calls.push(requestRandomCall); + calls.push(call); + + if (vrfProviderSecret) { + calls.push(assertConsumedCall as Call); + } + + console.log("calls", calls); + return calls; +}; diff --git a/contracts/dojo/vrf_consumer_template.cairo b/contracts/dojo/vrf_consumer_template.cairo deleted file mode 100644 index 1f97b48..0000000 --- a/contracts/dojo/vrf_consumer_template.cairo +++ /dev/null @@ -1,52 +0,0 @@ -#[starknet::interface] -trait IVrfConsumerTemplate { - fn dice(ref self: T); -} - -#[dojo::contract] -mod consumer_template { - use starknet::ContractAddress; - use starknet::get_caller_address; - - use cartridge_vrf::vrf_consumer::vrf_consumer_component::VrfConsumerComponent; - use cartridge_vrf::vrf_provider::vrf_provider_component::Source; - - component!(path: VrfConsumerComponent, storage: vrf_consumer, event: VrfConsumerEvent); - - #[abi(embed_v0)] - impl VrfConsumerImpl = VrfConsumerComponent::VrfConsumerImpl; - - impl VrfConsumerInternalImpl = VrfConsumerComponent::InternalImpl; - - #[storage] - struct Storage { - #[substorage(v0)] - vrf_consumer: VrfConsumerComponent::Storage, - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - VrfConsumerEvent: VrfConsumerComponent::Event, - } - - #[abi(embed_v0)] - fn dojo_init(ref self: ContractState, vrf_provider: ContractAddress) { - self.vrf_consumer.initializer(vrf_provider); - } - - #[abi(embed_v0)] - impl VrfConsumerTemplateImpl of super::IVrfConsumerTemplate { - fn dice(ref self: ContractState) { - let player_id = get_caller_address(); - let random: u256 = self.vrf_consumer.consume_random(Source::Nonce(player_id)).into(); - let value: u8 = (random % 6).try_into().unwrap() + 1; - // do the right things - } - } - - #[generate_trait] - impl InternalImpl of InternalTrait {} -} - diff --git a/contracts/dojo/vrf_provider_mock.cairo b/contracts/dojo/vrf_provider_mock.cairo index aad5086..fde3850 100644 --- a/contracts/dojo/vrf_provider_mock.cairo +++ b/contracts/dojo/vrf_provider_mock.cairo @@ -5,7 +5,7 @@ mod vrf_provider_mock { use openzeppelin::access::ownable::OwnableComponent; use cartridge_vrf::vrf_provider::vrf_provider_component::VrfProviderComponent; - use cartridge_vrf::vrf_provider::vrf_provider_component::PublicKey; + use cartridge_vrf::PublicKey; component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); component!(path: VrfProviderComponent, storage: vrf_provider, event: VrfProviderEvent); diff --git a/contracts/src/lib.cairo b/contracts/src/lib.cairo index 4a6ba13..d4f49d6 100644 --- a/contracts/src/lib.cairo +++ b/contracts/src/lib.cairo @@ -5,7 +5,6 @@ pub mod vrf_provider { pub mod vrf_consumer { pub mod vrf_consumer_component; - pub mod vrf_consumer_example; } pub use vrf_provider::vrf_provider_component::{ @@ -17,4 +16,5 @@ pub use vrf_consumer::vrf_consumer_component::VrfConsumerComponent; pub mod tests { pub mod common; pub mod test_dice; + pub mod vrf_consumer_mock; } diff --git a/contracts/src/tests/common.cairo b/contracts/src/tests/common.cairo index 4346445..afcbf96 100644 --- a/contracts/src/tests/common.cairo +++ b/contracts/src/tests/common.cairo @@ -10,9 +10,9 @@ use cartridge_vrf::vrf_provider::vrf_provider_component::{ IVrfProvider, IVrfProviderDispatcher, IVrfProviderDispatcherTrait, PublicKey, }; -use cartridge_vrf::vrf_consumer::vrf_consumer_example::{ - VrfConsumer, IVrfConsumerExample, IVrfConsumerExampleDispatcher, - IVrfConsumerExampleDispatcherTrait +use super::vrf_consumer_mock::{ + VrfConsumer, IVrfConsumerMock, IVrfConsumerMockDispatcher, + IVrfConsumerMockDispatcherTrait }; pub fn PROVIDER() -> ContractAddress { @@ -34,8 +34,8 @@ pub fn PLAYER1() -> ContractAddress { #[derive(Drop, Copy, Clone)] pub struct SetupResult { provider: IVrfProviderDispatcher, - consumer1: IVrfConsumerExampleDispatcher, - consumer2: IVrfConsumerExampleDispatcher, + consumer1: IVrfConsumerMockDispatcher, + consumer2: IVrfConsumerMockDispatcher, } // lauch vrf-server : cargo run -r -- -s 420 @@ -65,8 +65,8 @@ pub fn setup() -> SetupResult { SetupResult { provider: IVrfProviderDispatcher { contract_address: PROVIDER() }, - consumer1: IVrfConsumerExampleDispatcher { contract_address: CONSUMER1() }, - consumer2: IVrfConsumerExampleDispatcher { contract_address: CONSUMER2() }, + consumer1: IVrfConsumerMockDispatcher { contract_address: CONSUMER1() }, + consumer2: IVrfConsumerMockDispatcher { contract_address: CONSUMER2() }, } } diff --git a/contracts/src/tests/test_dice.cairo b/contracts/src/tests/test_dice.cairo index 233e3ff..45fd48e 100644 --- a/contracts/src/tests/test_dice.cairo +++ b/contracts/src/tests/test_dice.cairo @@ -9,13 +9,13 @@ use stark_vrf::ecvrf::{Point, Proof, ECVRF, ECVRFImpl}; use openzeppelin_utils::serde::SerializedAppend; use cartridge_vrf::vrf_provider::vrf_provider::VrfProvider; -use cartridge_vrf::vrf_provider::vrf_provider_component::{ +use cartridge_vrf::{ IVrfProvider, IVrfProviderDispatcher, IVrfProviderDispatcherTrait, PublicKey, Source }; -use cartridge_vrf::vrf_consumer::vrf_consumer_example::{ - VrfConsumer, IVrfConsumerExample, IVrfConsumerExampleDispatcher, - IVrfConsumerExampleDispatcherTrait +use super::vrf_consumer_mock::{ + VrfConsumer, IVrfConsumerMock, IVrfConsumerMockDispatcher, + IVrfConsumerMockDispatcherTrait }; use super::common::{setup, submit_random, SetupResult, CONSUMER1, CONSUMER2, PLAYER1}; @@ -25,7 +25,9 @@ use super::common::{setup, submit_random, SetupResult, CONSUMER1, CONSUMER2, PLA const SEED: felt252 = 0x334b8c0ea68406b183b5affd81ce11bec1a0807d3fd68a54ee75ec148053b09; -// curl -X POST -H "Content-Type: application/json" -d '{"seed": ["0x334b8c0ea68406b183b5affd81ce11bec1a0807d3fd68a54ee75ec148053b09"]}' http://0.0.0.0:3000/stark_vrf +// curl -X POST -H "Content-Type: application/json" -d '{"seed": +// ["0x334b8c0ea68406b183b5affd81ce11bec1a0807d3fd68a54ee75ec148053b09"]}' +// http://0.0.0.0:3000/stark_vrf pub fn proof() -> Proof { Proof { gamma: Point { @@ -40,7 +42,9 @@ pub fn proof() -> Proof { const SEED_FROM_SALT: felt252 = 0x767EBFD1241683397A6CB06FDE012811BB27FD6E768D7A4BB8670ED10DF95C0; -// curl -X POST -H "Content-Type: application/json" -d '{"seed": ["0x767EBFD1241683397A6CB06FDE012811BB27FD6E768D7A4BB8670ED10DF95C0"]}' http://0.0.0.0:3000/stark_vrf +// curl -X POST -H "Content-Type: application/json" -d '{"seed": +// ["0x767EBFD1241683397A6CB06FDE012811BB27FD6E768D7A4BB8670ED10DF95C0"]}' +// http://0.0.0.0:3000/stark_vrf pub fn proof_from_salt() -> Proof { Proof { gamma: Point { diff --git a/contracts/src/vrf_consumer/vrf_consumer_example.cairo b/contracts/src/tests/vrf_consumer_mock.cairo similarity index 88% rename from contracts/src/vrf_consumer/vrf_consumer_example.cairo rename to contracts/src/tests/vrf_consumer_mock.cairo index a0ce4b6..a94d9cd 100644 --- a/contracts/src/vrf_consumer/vrf_consumer_example.cairo +++ b/contracts/src/tests/vrf_consumer_mock.cairo @@ -1,8 +1,6 @@ -// SPDX-License-Identifier: MIT -// Compatible with OpenZeppelin Contracts for Cairo ^0.16.0 #[starknet::interface] -trait IVrfConsumerExample { +trait IVrfConsumerMock { fn dice(ref self: TContractState) -> u8; fn dice_with_salt(ref self: TContractState) -> u8; @@ -24,7 +22,7 @@ mod VrfConsumer { use stark_vrf::ecvrf::{Point, Proof, ECVRF, ECVRFImpl}; use cartridge_vrf::vrf_consumer::vrf_consumer_component::{VrfConsumerComponent}; - use cartridge_vrf::vrf_provider::vrf_provider_component::Source; + use cartridge_vrf::Source; component!(path: VrfConsumerComponent, storage: vrf_consumer, event: VrfConsumerEvent); @@ -52,7 +50,7 @@ mod VrfConsumer { } #[abi(embed_v0)] - impl ConsumerImpl of super::IVrfConsumerExample { + impl ConsumerImpl of super::IVrfConsumerMock { // throw dice fn dice(ref self: ContractState) -> u8 { let player_id = get_caller_address(); @@ -69,7 +67,7 @@ mod VrfConsumer { fn not_consuming(ref self: ContractState) { let _player_id = get_caller_address(); - // do the nothing + // do the nothing } fn set_vrf_provider(ref self: ContractState, new_vrf_provider: ContractAddress) { diff --git a/contracts/src/vrf_consumer/vrf_consumer_component.cairo b/contracts/src/vrf_consumer/vrf_consumer_component.cairo index 578c652..ddabab3 100644 --- a/contracts/src/vrf_consumer/vrf_consumer_component.cairo +++ b/contracts/src/vrf_consumer/vrf_consumer_component.cairo @@ -1,6 +1,6 @@ use starknet::ContractAddress; use stark_vrf::ecvrf::{Point, Proof, ECVRF, ECVRFImpl}; -use cartridge_vrf::vrf_provider::vrf_provider_component::PublicKey; +use cartridge_vrf::PublicKey; #[starknet::interface] trait IVrfConsumer { @@ -18,9 +18,8 @@ pub mod VrfConsumerComponent { use stark_vrf::ecvrf::{Point, Proof, ECVRF, ECVRFImpl}; - use cartridge_vrf::vrf_provider::vrf_provider_component::{ - IVrfProvider, IVrfProviderDispatcher, IVrfProviderDispatcherTrait, PublicKey, - PublicKeyIntoPoint, Source + use cartridge_vrf::{ + IVrfProvider, IVrfProviderDispatcher, IVrfProviderDispatcherTrait, PublicKey, Source }; #[storage] diff --git a/contracts/src/vrf_provider/vrf_provider.cairo b/contracts/src/vrf_provider/vrf_provider.cairo index be932ab..8c5bd37 100644 --- a/contracts/src/vrf_provider/vrf_provider.cairo +++ b/contracts/src/vrf_provider/vrf_provider.cairo @@ -9,7 +9,7 @@ mod VrfProvider { use openzeppelin::upgrades::interface::IUpgradeable; use cartridge_vrf::vrf_provider::vrf_provider_component::VrfProviderComponent; - use cartridge_vrf::vrf_provider::vrf_provider_component::PublicKey; + use cartridge_vrf::PublicKey; component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); diff --git a/contracts/src/vrf_provider/vrf_provider_component.cairo b/contracts/src/vrf_provider/vrf_provider_component.cairo index a770dd0..e5f15f4 100644 --- a/contracts/src/vrf_provider/vrf_provider_component.cairo +++ b/contracts/src/vrf_provider/vrf_provider_component.cairo @@ -78,7 +78,6 @@ pub mod VrfProviderComponent { pub const PUBKEY_ZERO: felt252 = 'VrfProvider: pubkey is zero'; pub const INVALID_PROOF: felt252 = 'VrfProvider: invalid proof'; pub const NOT_FULFILLED: felt252 = 'VrfProvider: not fulfilled'; - pub const SEED_MISMATCH: felt252 = 'VrfProvider: seed mismatch'; pub const NOT_CONSUMED: felt252 = 'VrfProvider: not consumed'; } @@ -120,7 +119,7 @@ pub mod VrfProviderComponent { poseidon_hash_span(array![salt, caller.into(), tx_info.chain_id].span()) }, }; - + // Always return 0 during fee estimation to avoid leaking vrf info. if tx_info.max_fee == 0 { // simulate consumed