diff --git a/pyth-rng/Cargo.lock b/pyth-rng/Cargo.lock index 541ca74cb..277d456b2 100644 --- a/pyth-rng/Cargo.lock +++ b/pyth-rng/Cargo.lock @@ -2625,6 +2625,7 @@ dependencies = [ "serde", "serde_json", "serde_qs", + "serde_yaml", "sha3", "tokio", "tower-http", @@ -3197,6 +3198,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +dependencies = [ + "indexmap 2.0.2", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3886,6 +3900,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "unsafe-libyaml" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" + [[package]] name = "untrusted" version = "0.7.1" diff --git a/pyth-rng/Cargo.toml b/pyth-rng/Cargo.toml index 7534781f2..be3d16e2f 100644 --- a/pyth-rng/Cargo.toml +++ b/pyth-rng/Cargo.toml @@ -19,6 +19,7 @@ reqwest = { version = "0.11.22", features = ["json", "blocking"] } serde = { version = "1.0.188", features = ["derive"] } serde_qs = { version = "0.12.0", features = ["axum"] } serde_json = "1.0.107" +serde_yaml = "0.9.25" sha3 = "0.10.8" tokio = { version = "1.33.0", features = ["full"] } tower-http = { version = "0.4.0", features = ["cors"] } diff --git a/pyth-rng/src/api.rs b/pyth-rng/src/api.rs index 882fbb164..548200c44 100644 --- a/pyth-rng/src/api.rs +++ b/pyth-rng/src/api.rs @@ -11,19 +11,30 @@ use { }, }, ethers::core::types::Address, - std::sync::Arc, + std::{ + collections::HashMap, + sync::Arc, + }, }; pub use { + chain_ids::*, index::*, revelation::*, }; +mod chain_ids; mod index; mod revelation; -/// The state of the randomness service for a single blockchain. +pub type ChainId = String; + #[derive(Clone)] pub struct ApiState { + pub chains: Arc>, +} + +/// The state of the randomness service for a single blockchain. +pub struct BlockchainState { /// The hash chain(s) required to serve random numbers for this blockchain pub state: Arc, /// The EVM contract where the protocol is running. @@ -32,10 +43,11 @@ pub struct ApiState { pub provider_address: Address, } - pub enum RestError { /// The caller passed a sequence number that isn't within the supported range InvalidSequenceNumber, + /// The caller passed an unsupported chain id + InvalidChainId, /// The caller requested a random value that can't currently be revealed (because it /// hasn't been committed to on-chain) NoPendingRequest, @@ -54,6 +66,9 @@ impl IntoResponse for RestError { "The sequence number is out of the permitted range", ) .into_response(), + RestError::InvalidChainId => { + (StatusCode::BAD_REQUEST, "The chain id is not supported").into_response() + } RestError::NoPendingRequest => ( StatusCode::FORBIDDEN, "The random value cannot currently be retrieved", diff --git a/pyth-rng/src/api/index.rs b/pyth-rng/src/api/index.rs index 06137556d..fe83daabe 100644 --- a/pyth-rng/src/api/index.rs +++ b/pyth-rng/src/api/index.rs @@ -7,5 +7,5 @@ use axum::{ /// /// TODO: Dynamically generate this list if possible. pub async fn index() -> impl IntoResponse { - Json(["/v1/revelation"]) + Json(["/v1/chains", "/v1/chains/:chain_id/revelations/:sequence"]) } diff --git a/pyth-rng/src/api/revelation.rs b/pyth-rng/src/api/revelation.rs index 60bfc6adc..f7740eae6 100644 --- a/pyth-rng/src/api/revelation.rs +++ b/pyth-rng/src/api/revelation.rs @@ -1,12 +1,17 @@ use { - crate::api::RestError, + crate::api::{ + ChainId, + RestError, + }, anyhow::Result, axum::{ - extract::State, + extract::{ + Path, + State, + }, Json, }, pythnet_sdk::wire::array, - serde_qs::axum::QsQuery, utoipa::{ IntoParams, ToSchema, @@ -14,30 +19,35 @@ use { }; // TODO: this should probably take path parameters /v1/revelation// -/// Reveal the random value for a given sequence number. +/// Reveal the random value for a given sequence number and blockchain. /// /// Given a sequence number, retrieve the corresponding random value that this provider has committed to. /// This endpoint will not return the random value unless someone has requested the sequence number on-chain. +/// +/// Every blockchain supported by this service has a distinct sequence of random numbers and chain_id. +/// Callers must pass the appropriate chain_id to ensure they fetch the correct random number. #[utoipa::path( get, -path = "/v1/revelation", +path = "/v1/chains/{chain_id}/revelations/{sequence}", responses( (status = 200, description = "Random value successfully retrieved", body = GetRandomValueResponse), (status = 403, description = "Random value cannot currently be retrieved", body = String) ), -params( -GetRandomValueQueryParams -) +params(GetRandomValueQueryParams) )] pub async fn revelation( State(state): State, - QsQuery(params): QsQuery, + Path(GetRandomValueQueryParams { chain_id, sequence }): Path, ) -> Result, RestError> { - let sequence: u64 = params - .sequence + let sequence: u64 = sequence .try_into() .map_err(|_| RestError::InvalidSequenceNumber)?; + let state = state + .chains + .get(&chain_id) + .ok_or_else(|| RestError::InvalidChainId)?; + let r = state .contract .get_request(state.provider_address, sequence) @@ -60,8 +70,10 @@ pub async fn revelation( } #[derive(Debug, serde::Serialize, serde::Deserialize, IntoParams)] -#[into_params(parameter_in=Query)] +#[into_params(parameter_in=Path)] pub struct GetRandomValueQueryParams { + #[param(value_type = String)] + pub chain_id: ChainId, pub sequence: u64, } diff --git a/pyth-rng/src/command/generate.rs b/pyth-rng/src/command/generate.rs index 3fc139ddd..4968512ff 100644 --- a/pyth-rng/src/command/generate.rs +++ b/pyth-rng/src/command/generate.rs @@ -2,7 +2,7 @@ use { crate::{ api::GetRandomValueResponse, config::GenerateOptions, - ethereum::PythContract, + ethereum::SignablePythContract, }, std::{ error::Error, @@ -12,7 +12,13 @@ use { /// Run the entire random number generation protocol to produce a random number. pub async fn generate(opts: &GenerateOptions) -> Result<(), Box> { - let contract = Arc::new(PythContract::from_opts(&opts.ethereum).await?); + let contract = Arc::new( + SignablePythContract::from_config( + &opts.config.load()?.get_chain_config(&opts.chain_id)?, + &opts.private_key, + ) + .await?, + ); let user_randomness = rand::random::<[u8; 32]>(); let provider = opts.provider; @@ -27,16 +33,13 @@ pub async fn generate(opts: &GenerateOptions) -> Result<(), Box> { ); // Get the committed value from the provider - let client = reqwest::Client::new(); - let request_url = client - .get(opts.url.join("/v1/revelation")?) - .query(&[("sequence", sequence_number)]) - .build()?; - let resp = client - .execute(request_url) - .await? - .json::() - .await?; + let resp = reqwest::get(opts.url.join(&format!( + "/v1/chains/{}/revelations/{}", + opts.chain_id, sequence_number + ))?) + .await? + .json::() + .await?; println!( "Retrieved the provider's random value. Server response: {:#?}", diff --git a/pyth-rng/src/command/get_request.rs b/pyth-rng/src/command/get_request.rs index 399428918..bf01fcc11 100644 --- a/pyth-rng/src/command/get_request.rs +++ b/pyth-rng/src/command/get_request.rs @@ -12,7 +12,9 @@ use { /// Get the on-chain request metadata for a provider and sequence number. pub async fn get_request(opts: &GetRequestOptions) -> Result<(), Box> { // Initialize a Provider to interface with the EVM contract. - let contract = Arc::new(PythContract::from_opts(&opts.ethereum).await?); + let contract = Arc::new(PythContract::from_config( + &opts.config.load()?.get_chain_config(&opts.chain_id)?, + )?); let r = contract .get_request(opts.provider, opts.sequence) diff --git a/pyth-rng/src/command/register_provider.rs b/pyth-rng/src/command/register_provider.rs index 9675dddaf..fa2d46ca8 100644 --- a/pyth-rng/src/command/register_provider.rs +++ b/pyth-rng/src/command/register_provider.rs @@ -1,7 +1,7 @@ use { crate::{ config::RegisterProviderOptions, - ethereum::PythContract, + ethereum::SignablePythContract, state::PebbleHashChain, }, std::{ @@ -14,11 +14,17 @@ use { /// hash chain from the configured secret & a newly generated random value. pub async fn register_provider(opts: &RegisterProviderOptions) -> Result<(), Box> { // Initialize a Provider to interface with the EVM contract. - let contract = Arc::new(PythContract::from_opts(&opts.ethereum).await?); + let contract = Arc::new( + SignablePythContract::from_config( + &opts.config.load()?.get_chain_config(&opts.chain_id)?, + &opts.private_key, + ) + .await?, + ); // Create a new random hash chain. let random = rand::random::<[u8; 32]>(); - let mut chain = PebbleHashChain::from_config(&opts.randomness, random)?; + let mut chain = PebbleHashChain::from_config(&opts.randomness, &opts.chain_id, random)?; // Arguments to the contract to register our new provider. let fee_in_wei = opts.fee; diff --git a/pyth-rng/src/command/request_randomness.rs b/pyth-rng/src/command/request_randomness.rs index 03e983848..6d3096210 100644 --- a/pyth-rng/src/command/request_randomness.rs +++ b/pyth-rng/src/command/request_randomness.rs @@ -1,7 +1,7 @@ use { crate::{ config::RequestRandomnessOptions, - ethereum::PythContract, + ethereum::SignablePythContract, }, std::{ error::Error, @@ -10,7 +10,13 @@ use { }; pub async fn request_randomness(opts: &RequestRandomnessOptions) -> Result<(), Box> { - let contract = Arc::new(PythContract::from_opts(&opts.ethereum).await?); + let contract = Arc::new( + SignablePythContract::from_config( + &opts.config.load()?.get_chain_config(&opts.chain_id)?, + &opts.private_key, + ) + .await?, + ); let user_randomness = rand::random::<[u8; 32]>(); let sequence_number = contract diff --git a/pyth-rng/src/command/run.rs b/pyth-rng/src/command/run.rs index 15407bd72..3ab003db0 100644 --- a/pyth-rng/src/command/run.rs +++ b/pyth-rng/src/command/run.rs @@ -17,6 +17,7 @@ use { Router, }, std::{ + collections::HashMap, error::Error, sync::Arc, }, @@ -30,10 +31,11 @@ pub async fn run(opts: &RunOptions) -> Result<(), Box> { #[openapi( paths( crate::api::revelation, + crate::api::chain_ids, ), components( schemas( - crate::api::GetRandomValueResponse + crate::api::GetRandomValueResponse, ) ), tags( @@ -42,34 +44,52 @@ pub async fn run(opts: &RunOptions) -> Result<(), Box> { )] struct ApiDoc; - let contract = Arc::new(PythContract::from_opts(&opts.ethereum).await?); - let provider_info = contract.get_provider_info(opts.provider).call().await?; + let config = opts.config.load()?; - // Reconstruct the hash chain based on the metadata and check that it matches the on-chain commitment. - // TODO: we should instantiate the state here with multiple hash chains. - // This approach works fine as long as we haven't rotated the commitment (i.e., all user requests - // are for the most recent chain). - let random: [u8; 32] = provider_info.commitment_metadata; - let chain = PebbleHashChain::from_config(&opts.randomness, random)?; - let chain_state = HashChainState { - offsets: vec![provider_info - .original_commitment_sequence_number - .try_into()?], - hash_chains: vec![chain], - }; + let mut chains = HashMap::new(); + for chain_config in &config.chains { + let contract = Arc::new(PythContract::from_config(&chain_config)?); + let provider_info = contract.get_provider_info(opts.provider).call().await?; + + // Reconstruct the hash chain based on the metadata and check that it matches the on-chain commitment. + // TODO: we should instantiate the state here with multiple hash chains. + // This approach works fine as long as we haven't rotated the commitment (i.e., all user requests + // are for the most recent chain). + // TODO: we may want to load the hash chain in a lazy/fault-tolerant way. If there are many blockchains, + // then it's more likely that some RPC fails. We should tolerate these faults and generate the hash chain + // later when a user request comes in for that chain. + let random: [u8; 32] = provider_info.commitment_metadata; + let hash_chain = + PebbleHashChain::from_config(&opts.randomness, &chain_config.chain_id, random)?; + let chain_state = HashChainState { + offsets: vec![provider_info + .original_commitment_sequence_number + .try_into()?], + hash_chains: vec![hash_chain], + }; + + if chain_state.reveal(provider_info.original_commitment_sequence_number)? + != provider_info.original_commitment + { + return Err(anyhow!(format!("The root of the generated hash chain for chain id {} does not match the commitment. Are the secret and chain length configured correctly?", &chain_config.chain_id)).into()); + } else { + println!( + "Root of chain id {} matches commitment", + &chain_config.chain_id + ); + } + + let state = api::BlockchainState { + state: Arc::new(chain_state), + contract, + provider_address: opts.provider, + }; - if chain_state.reveal(provider_info.original_commitment_sequence_number)? - != provider_info.original_commitment - { - return Err(anyhow!("The root of the generated hash chain does not match the commitment. Is the secret configured correctly?").into()); - } else { - println!("Root of chain matches commitment"); + chains.insert(chain_config.chain_id.clone(), state); } - let state = api::ApiState { - state: Arc::new(chain_state), - contract, - provider_address: opts.provider, + let api_state = api::ApiState { + chains: Arc::new(chains), }; // Initialize Axum Router. Note the type here is a `Router` due to the use of the @@ -78,8 +98,12 @@ pub async fn run(opts: &RunOptions) -> Result<(), Box> { let app = app .merge(SwaggerUi::new("/docs").url("/docs/openapi.json", ApiDoc::openapi())) .route("/", get(api::index)) - .route("/v1/revelation", get(api::revelation)) - .with_state(state.clone()) + .route("/v1/chains", get(api::chain_ids)) + .route( + "/v1/chains/:chain_id/revelations/:sequence", + get(api::revelation), + ) + .with_state(api_state) // Permissive CORS layer to allow all origins .layer(CorsLayer::permissive()); diff --git a/pyth-rng/src/config.rs b/pyth-rng/src/config.rs index 6bfa9ef70..9a1a644fd 100644 --- a/pyth-rng/src/config.rs +++ b/pyth-rng/src/config.rs @@ -1,4 +1,6 @@ use { + crate::api::ChainId, + anyhow::anyhow, clap::{ crate_authors, crate_description, @@ -8,15 +10,12 @@ use { Parser, }, ethers::types::Address, + std::{ + collections::HashMap, + error::Error, + fs, + }, }; - - -mod generate; -mod get_request; -mod register_provider; -mod request_randomness; -mod run; - pub use { generate::GenerateOptions, get_request::GetRequestOptions, @@ -25,6 +24,12 @@ pub use { run::RunOptions, }; +mod generate; +mod get_request; +mod register_provider; +mod request_randomness; +mod run; + const DEFAULT_RPC_ADDR: &str = "127.0.0.1:34000"; const DEFAULT_HTTP_ADDR: &str = "http://127.0.0.1:34000"; @@ -36,7 +41,7 @@ const DEFAULT_HTTP_ADDR: &str = "http://127.0.0.1:34000"; #[allow(clippy::large_enum_variant)] pub enum Options { /// Run the Randomness Service. - Run(run::RunOptions), + Run(RunOptions), /// Register a new provider with the Pyth Random oracle. RegisterProvider(RegisterProviderOptions), @@ -47,31 +52,31 @@ pub enum Options { /// Generate a random number by running the entire protocol end-to-end Generate(GenerateOptions), + /// Get the status of a pending request for a random number. GetRequest(GetRequestOptions), } #[derive(Args, Clone, Debug)] -#[command(next_help_heading = "Ethereum Options")] -#[group(id = "Ethereum")] -pub struct EthereumOptions { - /// A 20-byte (40 char) hex encoded Ethereum private key. - /// This key is required to submit transactions (such as registering with the contract). - #[arg(long = "private-key")] - #[arg(env = "PRIVATE_KEY")] - #[arg(default_value = None)] - pub private_key: Option, +#[command(next_help_heading = "Config Options")] +#[group(id = "Config")] +pub struct ConfigOptions { + /// A secret used for generating new hash chains. A 64-char hex string. + #[arg(long = "config")] + #[arg(env = "PYTH_CONFIG")] + #[arg(default_value = "config.yaml")] + pub config: String, +} - /// URL of a Geth RPC endpoint to use for interacting with the blockchain. - #[arg(long = "geth-rpc-addr")] - #[arg(env = "GETH_RPC_ADDR")] - #[arg(default_value = "https://goerli.optimism.io")] - pub geth_rpc_addr: String, +impl ConfigOptions { + pub fn load(&self) -> Result> { + // Open and read the YAML file + let yaml_content = fs::read_to_string(&self.config)?; + let config: Config = serde_yaml::from_str(&yaml_content)?; - /// Address of a Pyth Randomness contract to interact with. - #[arg(long = "pyth-contract-addr")] - #[arg(env = "PYTH_CONTRACT_ADDR")] - #[arg(default_value = "0x28F16Af4D87523910b843a801454AEde5F9B0459")] - pub contract_addr: Address, + config.check_is_valid()?; + + Ok(config) + } } #[derive(Args, Clone, Debug)] @@ -90,3 +95,60 @@ pub struct RandomnessOptions { #[arg(default_value = "32")] pub chain_length: u64, } + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct Config { + pub chains: Vec, +} + +impl Config { + pub fn check_is_valid(&self) -> Result<(), Box> { + let counts: HashMap<_, _> = self.chains.iter().fold(HashMap::new(), |mut map, s| { + *map.entry(s.chain_id.clone()).or_insert(0) += 1; + map + }); + + let duplicates: Vec<_> = counts + .iter() + .filter(|&(_, &c)| c > 1) + .map(|(id, _)| id.clone()) + .collect(); + + if duplicates.len() > 0 { + Err(anyhow!(format!( + "Config has duplicated chain ids: {}", + duplicates.join(",") + )) + .into()) + } else { + Ok(()) + } + } + + pub fn get_chain_config(&self, chain_id: &ChainId) -> Result> { + self.chains + .iter() + .find(|x| x.chain_id == *chain_id) + .map(|c| c.clone()) + .ok_or( + anyhow!(format!( + "Could not find chain id {} in the configuration file", + &chain_id + )) + .into(), + ) + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct EthereumConfig { + /// A unique identifier for the chain. Endpoints of this server require users to pass + /// this value to identify which blockchain they are operating on. + pub chain_id: ChainId, + + /// URL of a Geth RPC endpoint to use for interacting with the blockchain. + pub geth_rpc_addr: String, + + /// Address of a Pyth Randomness contract to interact with. + pub contract_addr: Address, +} diff --git a/pyth-rng/src/config/generate.rs b/pyth-rng/src/config/generate.rs index 5905d525e..0e5edcd7f 100644 --- a/pyth-rng/src/config/generate.rs +++ b/pyth-rng/src/config/generate.rs @@ -1,5 +1,8 @@ use { - crate::config::EthereumOptions, + crate::{ + api::ChainId, + config::ConfigOptions, + }, clap::Args, ethers::types::Address, reqwest::Url, @@ -10,7 +13,19 @@ use { #[group(id = "Generate")] pub struct GenerateOptions { #[command(flatten)] - pub ethereum: EthereumOptions, + pub config: ConfigOptions, + + /// Retrieve a randomness request to this provider + #[arg(long = "chain-id")] + #[arg(env = "PYTH_CHAIN_ID")] + pub chain_id: ChainId, + + /// A 20-byte (40 char) hex encoded Ethereum private key. + /// This key is required to submit transactions (such as registering with the contract). + #[arg(long = "private-key")] + #[arg(env = "PRIVATE_KEY")] + #[arg(default_value = None)] + pub private_key: String, /// Submit a randomness request to this provider #[arg(long = "provider")] diff --git a/pyth-rng/src/config/get_request.rs b/pyth-rng/src/config/get_request.rs index 81526d408..11e80aedf 100644 --- a/pyth-rng/src/config/get_request.rs +++ b/pyth-rng/src/config/get_request.rs @@ -1,5 +1,8 @@ use { - crate::config::EthereumOptions, + crate::{ + api::ChainId, + config::ConfigOptions, + }, clap::Args, ethers::types::Address, }; @@ -9,7 +12,12 @@ use { #[group(id = "GetRequest")] pub struct GetRequestOptions { #[command(flatten)] - pub ethereum: EthereumOptions, + pub config: ConfigOptions, + + /// Retrieve a randomness request to this provider + #[arg(long = "chain-id")] + #[arg(env = "PYTH_CHAIN_ID")] + pub chain_id: ChainId, /// Retrieve a randomness request to this provider #[arg(long = "provider")] diff --git a/pyth-rng/src/config/register_provider.rs b/pyth-rng/src/config/register_provider.rs index cac7a5d9e..4209247e3 100644 --- a/pyth-rng/src/config/register_provider.rs +++ b/pyth-rng/src/config/register_provider.rs @@ -1,7 +1,10 @@ use { - crate::config::{ - EthereumOptions, - RandomnessOptions, + crate::{ + api::ChainId, + config::{ + ConfigOptions, + RandomnessOptions, + }, }, clap::Args, ethers::types::U256, @@ -12,7 +15,19 @@ use { #[group(id = "RegisterProvider")] pub struct RegisterProviderOptions { #[command(flatten)] - pub ethereum: EthereumOptions, + pub config: ConfigOptions, + + /// Retrieve a randomness request to this provider + #[arg(long = "chain-id")] + #[arg(env = "PYTH_CHAIN_ID")] + pub chain_id: ChainId, + + /// A 20-byte (40 char) hex encoded Ethereum private key. + /// This key is required to submit transactions (such as registering with the contract). + #[arg(long = "private-key")] + #[arg(env = "PRIVATE_KEY")] + #[arg(default_value = None)] + pub private_key: String, #[command(flatten)] pub randomness: RandomnessOptions, diff --git a/pyth-rng/src/config/request_randomness.rs b/pyth-rng/src/config/request_randomness.rs index 446a2d231..38b88b88a 100644 --- a/pyth-rng/src/config/request_randomness.rs +++ b/pyth-rng/src/config/request_randomness.rs @@ -1,7 +1,7 @@ use { - crate::config::{ - EthereumOptions, - RandomnessOptions, + crate::{ + api::ChainId, + config::ConfigOptions, }, clap::Args, ethers::types::Address, @@ -12,10 +12,18 @@ use { #[group(id = "RequestRandomness")] pub struct RequestRandomnessOptions { #[command(flatten)] - pub ethereum: EthereumOptions, + pub config: ConfigOptions, - #[command(flatten)] - pub randomness: RandomnessOptions, + /// Request randomness on this blockchain. + #[arg(long = "chain-id")] + #[arg(env = "PYTH_CHAIN_ID")] + pub chain_id: ChainId, + + /// A 20-byte (40 char) hex encoded Ethereum private key. + /// This key is required to submit transactions (such as registering with the contract). + #[arg(long = "private-key")] + #[arg(env = "PRIVATE_KEY")] + pub private_key: String, /// Submit a randomness request to this provider #[arg(long = "provider")] diff --git a/pyth-rng/src/config/run.rs b/pyth-rng/src/config/run.rs index c765d598e..0a4ff0f7e 100644 --- a/pyth-rng/src/config/run.rs +++ b/pyth-rng/src/config/run.rs @@ -1,6 +1,6 @@ use { crate::config::{ - EthereumOptions, + ConfigOptions, RandomnessOptions, }, clap::Args, @@ -12,7 +12,7 @@ use { #[derive(Args, Clone, Debug)] pub struct RunOptions { #[command(flatten)] - pub ethereum: EthereumOptions, + pub config: ConfigOptions, #[command(flatten)] pub randomness: RandomnessOptions, diff --git a/pyth-rng/src/ethereum.rs b/pyth-rng/src/ethereum.rs index a64dc55c4..aecba6aa0 100644 --- a/pyth-rng/src/ethereum.rs +++ b/pyth-rng/src/ethereum.rs @@ -1,5 +1,5 @@ use { - crate::config::EthereumOptions, + crate::config::EthereumConfig, anyhow::anyhow, ethers::{ abi::RawLog, @@ -33,23 +33,24 @@ use { // contract in the same repo. abigen!(PythRandom, "src/abi.json"); -pub type PythContract = PythRandom, LocalWallet>>; +pub type SignablePythContract = PythRandom, LocalWallet>>; +pub type PythContract = PythRandom>; -impl PythContract { - // TODO: this method requires a private key to instantiate the contract. This key - // shouldn't be required for read-only uses (e.g., when the server is running). - pub async fn from_opts(opts: &EthereumOptions) -> Result> { - let provider = Provider::::try_from(&opts.geth_rpc_addr)?; +impl SignablePythContract { + pub async fn from_config( + chain_config: &EthereumConfig, + private_key: &str, + ) -> Result> { + let provider = Provider::::try_from(&chain_config.geth_rpc_addr)?; let chain_id = provider.get_chainid().await?; - let wallet__ = opts - .private_key + + let wallet__ = private_key .clone() - .ok_or(anyhow!("No private key specified"))? .parse::()? .with_chain_id(chain_id.as_u64()); Ok(PythRandom::new( - opts.contract_addr, + chain_config.contract_addr, Arc::new(SignerMiddleware::new(provider, wallet__)), )) } @@ -121,3 +122,14 @@ impl PythContract { } } } + +impl PythContract { + pub fn from_config(chain_config: &EthereumConfig) -> Result> { + let provider = Provider::::try_from(&chain_config.geth_rpc_addr)?; + + Ok(PythRandom::new( + chain_config.contract_addr, + Arc::new(provider), + )) + } +} diff --git a/pyth-rng/src/main.rs b/pyth-rng/src/main.rs index dba286228..d48f25ac5 100644 --- a/pyth-rng/src/main.rs +++ b/pyth-rng/src/main.rs @@ -13,6 +13,14 @@ pub mod config; pub mod ethereum; pub mod state; +// Server TODO list: +// - Tests +// - Metrics / liveness / readiness endpoints +// - replace println! with proper logging +// - Reduce memory requirements for storing hash chains to increase scalability +// - Name things nicely (service name, API resource names) +// - README +// - use anyhow::Result #[tokio::main] async fn main() -> Result<(), Box> { match config::Options::parse() { diff --git a/pyth-rng/src/state.rs b/pyth-rng/src/state.rs index 65b06c56f..9895c3b0e 100644 --- a/pyth-rng/src/state.rs +++ b/pyth-rng/src/state.rs @@ -1,5 +1,8 @@ use { - crate::config::RandomnessOptions, + crate::{ + api::ChainId, + config::RandomnessOptions, + }, anyhow::{ ensure, Result, @@ -32,12 +35,17 @@ impl PebbleHashChain { Self { hash, next: 0 } } - // TODO: possibly take the chain id here to ensure different hash chains on every blockchain - pub fn from_config(opts: &RandomnessOptions, random: [u8; 32]) -> Result> { - let mut secret: [u8; 32] = [0u8; 32]; - secret.copy_from_slice(&hex::decode(opts.secret.clone())?[0..32]); - let secret: [u8; 32] = Keccak256::digest([random, secret].flatten()).into(); + pub fn from_config( + opts: &RandomnessOptions, + chain_id: &ChainId, + random: [u8; 32], + ) -> Result> { + let mut input: Vec = vec![]; + input.extend_from_slice(&hex::decode(opts.secret.clone())?); + input.extend_from_slice(&chain_id.as_bytes()); + input.extend_from_slice(&random); + let secret: [u8; 32] = Keccak256::digest(input).into(); Ok(Self::new(secret, opts.chain_length.try_into()?)) }