diff --git a/contracts/README.md b/contracts/README.md index 0f53f35..cefe924 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -1,24 +1,39 @@ +# VRF for Cartridge Controller with paymaster +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 -# VRF for Cartridge Controller with paymaster +## How it works caller send multicall : + ``` [ - vrf_provider.request_random(), + 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(), + vrf_provider.request_random(game_contract.address, Source::Nonce(wallet_address)), game_contract.you_function_consuming_randomness(...params) ]) vrf_provider.assert_consumed( seed ), @@ -31,6 +46,6 @@ and inject calls to sandwitch caller in a multicall : - 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 +- 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 \ No newline at end of file +- User cannot probe randomness diff --git a/contracts/dojo/vrf_consumer_template.cairo b/contracts/dojo/vrf_consumer_template.cairo index 82b72ab..e55b533 100644 --- a/contracts/dojo/vrf_consumer_template.cairo +++ b/contracts/dojo/vrf_consumer_template.cairo @@ -9,6 +9,7 @@ mod consumer_template { use starknet::get_caller_address; use vrf_contracts::vrf_consumer::vrf_consumer_component::VrfConsumerComponent; + use vrf_contracts::vrf_provider::vrf_provider_component::Source; component!(path: VrfConsumerComponent, storage: vrf_consumer, event: VrfConsumerEvent); @@ -39,7 +40,7 @@ mod consumer_template { impl VrfConsumerTemplateImpl of super::IVrfConsumerTemplate { fn dice(ref self: ContractState) { let player_id = get_caller_address(); - let random: u256 = self.vrf_consumer.consume_random(player_id).into(); + 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 } diff --git a/contracts/src/tests/common.cairo b/contracts/src/tests/common.cairo index 8b142e3..38a19bc 100644 --- a/contracts/src/tests/common.cairo +++ b/contracts/src/tests/common.cairo @@ -27,6 +27,10 @@ pub fn CONSUMER2() -> ContractAddress { contract_address_const::<'CONSUMER2'>() } +pub fn PLAYER1() -> ContractAddress { + contract_address_const::<'PLAYER1'>() +} + #[derive(Drop, Copy, Clone)] pub struct SetupResult { provider: IVrfProviderDispatcher, diff --git a/contracts/src/tests/test_dice.cairo b/contracts/src/tests/test_dice.cairo index 7488888..f452cf4 100644 --- a/contracts/src/tests/test_dice.cairo +++ b/contracts/src/tests/test_dice.cairo @@ -10,7 +10,7 @@ use openzeppelin_utils::serde::SerializedAppend; use vrf_contracts::vrf_provider::vrf_provider::VrfProvider; use vrf_contracts::vrf_provider::vrf_provider_component::{ - IVrfProvider, IVrfProviderDispatcher, IVrfProviderDispatcherTrait, PublicKey, + IVrfProvider, IVrfProviderDispatcher, IVrfProviderDispatcherTrait, PublicKey, Source }; use vrf_contracts::vrf_consumer::vrf_consumer_example::{ @@ -18,47 +18,56 @@ use vrf_contracts::vrf_consumer::vrf_consumer_example::{ IVrfConsumerExampleDispatcherTrait }; -use super::common::{setup, submit_random, SetupResult, CONSUMER1, CONSUMER2}; +use super::common::{setup, submit_random, SetupResult, CONSUMER1, CONSUMER2, PLAYER1}; // private key: 420 // {"public_key_x":"0x66da5d53168d591c55d4c05f3681663ac51bcdccd5ca09e366b71b0c40ccff4","public_key_y":"0x6d3eb29920bf55195e5ec76f69e247c0942c7ef85f6640896c058ec75ca2232"} -// seed: 0x334b8c0ea68406b183b5affd81ce11bec1a0807d3fd68a54ee75ec148053b09 -// curl -X POST -H "Content-Type: application/json" -d '{"seed": ["0x334b8c0ea68406b183b5affd81ce11bec1a0807d3fd68a54ee75ec148053b09"]}' http://0.0.0.0:3000/stark_vrf -// { -// "result": { -// "gamma_x": "0xf010d3727eb8aee76c7bc81f399805f4c2c39708451d933ef4d7f909248a6d", -// "gamma_y": "0x18a8fab3c58608505953d0fa0376ab454907d6e88db83702a36294faa937ac8", -// "c": "0x10e06538fdb8d943ecbf03e519500e258a83248d5a457ff2803c54c583f6302", -// "s": "0x150f672c657e116cd3966b74a2320e600c853801612b56f3a9cb31063f763c6", -// "sqrt_ratio": "0x8b09cf018201f7702d638b23d3cd10f577f7973369e79e5974ab33c1d64e01", -// "rnd": "0x735e9c275caf267880ec4e5967fde13ca084244384c03c739fcc54ac23789a4" -// } -// } + const SEED: felt252 = 0x334b8c0ea68406b183b5affd81ce11bec1a0807d3fd68a54ee75ec148053b09; +// 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 { + x: 0xf010d3727eb8aee76c7bc81f399805f4c2c39708451d933ef4d7f909248a6d, + y: 0x18a8fab3c58608505953d0fa0376ab454907d6e88db83702a36294faa937ac8 + }, + c: 0x10e06538fdb8d943ecbf03e519500e258a83248d5a457ff2803c54c583f6302, + s: 0x150f672c657e116cd3966b74a2320e600c853801612b56f3a9cb31063f763c6, + sqrt_ratio_hint: 0x8b09cf018201f7702d638b23d3cd10f577f7973369e79e5974ab33c1d64e01, + } +} + +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 +pub fn proof_from_salt() -> Proof { + Proof { + gamma: Point { + x: 0x28473f4cad1406e83a766a1137281340b93600661af4eda228d5f73ae4e0fe9, + y: 0x36bf0d2884aa739d5e419f1eb9fdf4af61c489169830d748ae0bbc7707b95ae + }, + c: 0x137ed85cd1ae3b25d6f9c2e3e23912cde001696fb4c0caba403967ecdab4bb4, + s: 0x74b0e76d8cfffc8a4755f4bcdefd3b1339d68e2c06b1072b36cfb2661fd4a27, + sqrt_ratio_hint: 0x29523a3636251c7085188108d8912577076ab6337892a3aa492dea4015d3a0c, + } +} + #[test] fn test_dice() { let setup = setup(); - setup.provider.request_random(CALLER(), Option::None); + setup.provider.request_random(CONSUMER1(), Source::Nonce(PLAYER1())); submit_random( setup.provider, SEED, - Proof { - gamma: Point { - x: 0xf010d3727eb8aee76c7bc81f399805f4c2c39708451d933ef4d7f909248a6d, - y: 0x18a8fab3c58608505953d0fa0376ab454907d6e88db83702a36294faa937ac8 - }, - c: 0x10e06538fdb8d943ecbf03e519500e258a83248d5a457ff2803c54c583f6302, - s: 0x150f672c657e116cd3966b74a2320e600c853801612b56f3a9cb31063f763c6, - sqrt_ratio_hint: 0x8b09cf018201f7702d638b23d3cd10f577f7973369e79e5974ab33c1d64e01, - }, + proof(), ); - // CALLER consume - start_cheat_caller_address(setup.consumer1.contract_address, CALLER()); + // PLAYER1 call dice, CONSUMER1 is caller of consume_random + start_cheat_caller_address(setup.consumer1.contract_address, PLAYER1()); let dice1 = setup.consumer1.dice(); assert(dice1 == 3, 'dice1 should be 3'); stop_cheat_caller_address(setup.consumer1.contract_address); @@ -71,24 +80,17 @@ fn test_dice() { fn test_not_consuming__must_consume() { let setup = setup(); - setup.provider.request_random(CALLER(), Option::None); + // noop just here for example + setup.provider.request_random(CONSUMER1(), Source::Nonce(PLAYER1())); submit_random( setup.provider, SEED, - Proof { - gamma: Point { - x: 0xf010d3727eb8aee76c7bc81f399805f4c2c39708451d933ef4d7f909248a6d, - y: 0x18a8fab3c58608505953d0fa0376ab454907d6e88db83702a36294faa937ac8 - }, - c: 0x10e06538fdb8d943ecbf03e519500e258a83248d5a457ff2803c54c583f6302, - s: 0x150f672c657e116cd3966b74a2320e600c853801612b56f3a9cb31063f763c6, - sqrt_ratio_hint: 0x8b09cf018201f7702d638b23d3cd10f577f7973369e79e5974ab33c1d64e01, - }, + proof(), ); - // CALLER dont consume - start_cheat_caller_address(setup.consumer1.contract_address, CALLER()); + // PLAYER1 dont consume + start_cheat_caller_address(setup.consumer1.contract_address, PLAYER1()); setup.consumer1.not_consuming(); stop_cheat_caller_address(setup.consumer1.contract_address); @@ -100,27 +102,61 @@ fn test_not_consuming__must_consume() { fn test_dice__cannot_consume_twice() { let setup = setup(); - setup.provider.request_random(CALLER(), Option::None); + // noop just here for example + setup.provider.request_random(CONSUMER1(), Source::Nonce(PLAYER1())); // provider submit_random submit_random( setup.provider, SEED, - Proof { - gamma: Point { - x: 0xf010d3727eb8aee76c7bc81f399805f4c2c39708451d933ef4d7f909248a6d, - y: 0x18a8fab3c58608505953d0fa0376ab454907d6e88db83702a36294faa937ac8 - }, - c: 0x10e06538fdb8d943ecbf03e519500e258a83248d5a457ff2803c54c583f6302, - s: 0x150f672c657e116cd3966b74a2320e600c853801612b56f3a9cb31063f763c6, - sqrt_ratio_hint: 0x8b09cf018201f7702d638b23d3cd10f577f7973369e79e5974ab33c1d64e01, - }, + proof(), ); - // CALLER consume - start_cheat_caller_address(setup.consumer1.contract_address, CALLER()); - start_cheat_caller_address(setup.consumer2.contract_address, CALLER()); + // PLAYER1 consume twice + start_cheat_caller_address(setup.consumer1.contract_address, PLAYER1()); + start_cheat_caller_address(setup.consumer2.contract_address, PLAYER1()); let _dice1 = setup.consumer1.dice(); let _dice2 = setup.consumer1.dice(); } + +#[test] +fn test_dice_with_salt() { + let setup = setup(); + + // noop just here for example + setup.provider.request_random(CONSUMER1(),Source::Salt('salt')); + + submit_random( + setup.provider, + SEED_FROM_SALT, + proof_from_salt(), + ); + + // PLAYER1 call dice_with_salt, CONSUMER1 is caller of consume_random + start_cheat_caller_address(setup.consumer1.contract_address, PLAYER1()); + let dice1 = setup.consumer1.dice_with_salt(); + assert(dice1 == 2, 'dice1 should be 2'); + stop_cheat_caller_address(setup.consumer1.contract_address); + + setup.provider.assert_consumed(SEED); +} + +#[test] +#[should_panic(expected: 'VrfProvider: not fulfilled')] +fn test_dice_with_salt__wrong_proof() { + let setup = setup(); + + // noop just here for example + setup.provider.request_random(CONSUMER1(),Source::Salt('salt')); + + submit_random( + setup.provider, + SEED, + proof(), + ); + + // PLAYER1 consume + start_cheat_caller_address(setup.consumer1.contract_address, PLAYER1()); + let _dice1 = setup.consumer1.dice_with_salt(); +} \ No newline at end of file diff --git a/contracts/src/vrf_consumer/vrf_consumer_component.cairo b/contracts/src/vrf_consumer/vrf_consumer_component.cairo index 0b61311..1ff3567 100644 --- a/contracts/src/vrf_consumer/vrf_consumer_component.cairo +++ b/contracts/src/vrf_consumer/vrf_consumer_component.cairo @@ -20,7 +20,7 @@ pub mod VrfConsumerComponent { use vrf_contracts::vrf_provider::vrf_provider_component::{ IVrfProvider, IVrfProviderDispatcher, IVrfProviderDispatcherTrait, PublicKey, - PublicKeyIntoPoint + PublicKeyIntoPoint, Source }; #[storage] @@ -67,10 +67,8 @@ pub mod VrfConsumerComponent { self.set_vrf_provider(vrf_provider); } - fn consume_random( - self: @ComponentState, caller: ContractAddress - ) -> felt252 { - self.vrf_provider_disp().consume_random(Option::None) + fn consume_random(self: @ComponentState, source: Source) -> felt252 { + self.vrf_provider_disp().consume_random(source) } fn vrf_provider_disp(self: @ComponentState,) -> IVrfProviderDispatcher { diff --git a/contracts/src/vrf_consumer/vrf_consumer_example.cairo b/contracts/src/vrf_consumer/vrf_consumer_example.cairo index 609dedb..007bbc0 100644 --- a/contracts/src/vrf_consumer/vrf_consumer_example.cairo +++ b/contracts/src/vrf_consumer/vrf_consumer_example.cairo @@ -4,6 +4,8 @@ #[starknet::interface] trait IVrfConsumerExample { fn dice(ref self: TContractState) -> u8; + fn dice_with_salt(ref self: TContractState) -> u8; + fn not_consuming(ref self: TContractState); // admin @@ -22,6 +24,7 @@ mod VrfConsumer { use stark_vrf::ecvrf::{Point, Proof, ECVRF, ECVRFImpl}; use vrf_contracts::vrf_consumer::vrf_consumer_component::{VrfConsumerComponent}; + use vrf_contracts::vrf_provider::vrf_provider_component::Source; component!(path: VrfConsumerComponent, storage: vrf_consumer, event: VrfConsumerEvent); @@ -52,15 +55,21 @@ mod VrfConsumer { impl ConsumerImpl of super::IVrfConsumerExample { // throw dice fn dice(ref self: ContractState) -> u8 { - let caller = get_caller_address(); - let random: u256 = self.vrf_consumer.consume_random(caller).into(); + let player_id = get_caller_address(); + let random: u256 = self.vrf_consumer.consume_random(Source::Nonce(player_id)).into(); + + ((random % 6) + 1).try_into().unwrap() + } + + fn dice_with_salt(ref self: ContractState) -> u8 { + let random: u256 = self.vrf_consumer.consume_random(Source::Salt('salt')).into(); ((random % 6) + 1).try_into().unwrap() } 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_provider/vrf_provider_component.cairo b/contracts/src/vrf_provider/vrf_provider_component.cairo index 27c2594..680438e 100644 --- a/contracts/src/vrf_provider/vrf_provider_component.cairo +++ b/contracts/src/vrf_provider/vrf_provider_component.cairo @@ -3,9 +3,9 @@ use stark_vrf::ecvrf::{Point, Proof, ECVRF, ECVRFImpl}; #[starknet::interface] trait IVrfProvider { - fn request_random(self: @TContractState, caller: ContractAddress, salt: Option); + fn request_random(self: @TContractState, caller: ContractAddress, source: Source); fn submit_random(ref self: TContractState, seed: felt252, proof: Proof); - fn consume_random(ref self: TContractState, salt: Option) -> felt252; + fn consume_random(ref self: TContractState, source: Source) -> felt252; fn assert_consumed(ref self: TContractState, seed: felt252); fn get_public_key(self: @TContractState) -> PublicKey; @@ -24,6 +24,13 @@ impl PublicKeyIntoPoint of Into { } } +#[derive(Drop, Copy, Clone, Serde)] +pub enum Source { + Nonce: ContractAddress, + Salt: felt252, +} + + #[starknet::component] pub mod VrfProviderComponent { use starknet::ContractAddress; @@ -35,14 +42,14 @@ pub mod VrfProviderComponent { OwnableComponent, OwnableComponent::InternalImpl as OwnableInternalImpl }; - use super::PublicKey; + use super::{PublicKey, Source}; use stark_vrf::ecvrf::{Point, Proof, ECVRF, ECVRFImpl}; #[storage] struct Storage { VrfProvider_pubkey: PublicKey, - // caller -> nonce + // wallet -> nonce VrfProvider_nonces: Map, // seed -> random VrfProvider_random: Map, @@ -83,7 +90,7 @@ pub mod VrfProviderComponent { impl Owner: OwnableComponent::HasComponent, > of super::IVrfProvider> { fn request_random( - self: @ComponentState, caller: ContractAddress, salt: Option + self: @ComponentState, caller: ContractAddress, source: Source ) {} fn submit_random(ref self: ComponentState, seed: felt252, proof: Proof) { @@ -99,9 +106,7 @@ pub mod VrfProviderComponent { self.emit(SubmitRandom { seed, proof }); } - fn consume_random( - ref self: ComponentState, salt: Option - ) -> felt252 { + fn consume_random(ref self: ComponentState, source: Source,) -> felt252 { let caller = get_caller_address(); let tx_info = starknet::get_execution_info().tx_info.unbox(); @@ -110,15 +115,15 @@ pub mod VrfProviderComponent { return 0; } - let seed = match salt { - Option::Some(s) => { - poseidon_hash_span(array![s, caller.into(), tx_info.chain_id].span()) - }, - Option::None => { - let nonce = self.VrfProvider_nonces.read(caller); - self.VrfProvider_nonces.write(caller, nonce + 1); + let seed = match source { + Source::Nonce(addr) => { + let nonce = self.VrfProvider_nonces.read(addr); + self.VrfProvider_nonces.write(addr, nonce + 1); poseidon_hash_span(array![nonce, caller.into(), tx_info.chain_id].span()) - } + }, + Source::Salt(salt) => { + poseidon_hash_span(array![salt, caller.into(), tx_info.chain_id].span()) + }, }; let random = self.VrfProvider_random.read(seed);