diff --git a/Cargo.lock b/Cargo.lock index b8c7da4..f3dd3f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -725,7 +725,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", - "regex-automata", + "regex-automata 0.4.9", "serde", ] @@ -2400,6 +2400,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "memchr" version = "2.7.4" @@ -2530,6 +2539,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num" version = "0.2.1" @@ -2760,6 +2779,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking" version = "2.2.1" @@ -2936,6 +2961,7 @@ dependencies = [ "tokio", "tokio-stream", "tracing", + "tracing-subscriber", "wormhole-vaas-serde", ] @@ -3181,8 +3207,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -3193,9 +3228,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.5" @@ -3546,20 +3587,20 @@ dependencies = [ [[package]] name = "secp256k1" -version = "0.31.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3dff2d01c9aa65c3186a45ff846bfea52cbe6de3b6320ed2a358d90dad0d76" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" dependencies = [ "bitcoin_hashes", - "rand 0.9.1", + "rand 0.8.5", "secp256k1-sys", ] [[package]] name = "secp256k1-sys" -version = "0.11.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" dependencies = [ "cc", ] @@ -3760,6 +3801,15 @@ dependencies = [ "keccak", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -6485,6 +6535,15 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.41" @@ -6740,6 +6799,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", ] [[package]] @@ -6863,6 +6965,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 284e728..179b748 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ borsh = "0.9.3" clap = { version = "4.5.39", features = ["derive", "env"] } hex = { version = "0.4.3", features = ["serde"] } reqwest = { version = "0.12.19", features = ["json"] } -secp256k1 = { version = "0.31.0", features = ["recovery"] } +secp256k1 = { version = "0.30.0", features = ["recovery", "rand"] } serde = "1.0.219" serde_wormhole = "0.1.0" sha3 = "0.10.8" @@ -19,6 +19,7 @@ solana-sdk = "2.2.2" tokio = "1.45.1" tokio-stream = "0.1.17" tracing = "0.1.41" +tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] } wormhole-vaas-serde = "0.1.0" [dev-dependencies] diff --git a/README.md b/README.md index 915500a..64e8775 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,10 @@ cargo build ### ▶️ Run the Project You can run the project using `cargo run` by passing the required flags: +Make sure to set `RUST_LOG=INFO` to enable logs from tracing: ```bash -cargo run -- \ +RUST_LOG=INFO cargo run -- run \ --pythnet-url wss://api2.pythnet.pyth.network \ --server-url https://watcher.pyth.network \ --secret-key /path/to/secret.key \ @@ -43,12 +44,25 @@ export PYTHNET_URL=wss://api2.pythnet.pyth.network export SERVER_URL=https://watcher.pyth.network export SECRET_KEY=/path/to/secret.key export WORMHOLE_PID=H3fxXJ86ADW2PNuDDmZJg6mzTtPxkYCpNuQUTgmJ7AjU +export RUST_LOG=INFO cargo run ``` --- +### 🔑 Generate a Secret Key + +To generate a new secp256k1 secret key and write it to a file: + +```bash +RUST_LOG=INFO cargo run -- generate-key --output-file .secret +``` + +This will save the key in raw byte format to the file named `.secret`. + +--- + ### 🧪 Testing Locally To test in a non-production environment (e.g. with devnet or a local Pythnet fork), just provide a different `--pythnet-url`, and `--server-url`, and optionally use custom `--wormhole-pid`. diff --git a/src/api_client.rs b/src/api_client.rs index 4e05ca7..75a4a10 100644 --- a/src/api_client.rs +++ b/src/api_client.rs @@ -44,7 +44,7 @@ impl Observation

{ pub fn try_new(body: Body

, secret_key: SecretKey) -> Result { let digest = body.digest()?; let signature = Secp256k1::new() - .sign_ecdsa_recoverable(Message::from_digest(digest.secp256k_hash), &secret_key); + .sign_ecdsa_recoverable(&Message::from_digest(digest.secp256k_hash), &secret_key); let (recovery_id, signature_bytes) = signature.serialize_compact(); let recovery_id: i32 = recovery_id.into(); let mut signature = [0u8; 65]; @@ -126,7 +126,7 @@ mod tests { #[test] fn test_new_signed_observation() { - let secret_key = SecretKey::from_byte_array([1u8; 32]).expect("Invalid secret key length"); + let secret_key = SecretKey::from_byte_array(&[1u8; 32]).expect("Invalid secret key length"); let body = Body { timestamp: 1234567890, nonce: 42, @@ -154,7 +154,7 @@ mod tests { .expect("Invalid recoverable signature"); let pubkey = secp - .recover_ecdsa(message, &recoverable_sig) + .recover_ecdsa(&message, &recoverable_sig) .expect("Failed to recover pubkey"); let expected_pubkey = PublicKey::from_secret_key(&secp, &secret_key); diff --git a/src/config.rs b/src/config.rs index beacc98..3c6fcf5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -18,3 +18,18 @@ pub struct RunOptions { #[arg(long = "server-url", env = "SERVER_URL")] pub server_url: String, } + +#[derive(Parser, Clone, Debug)] +pub struct GenerateKeyOptions { + /// Output path for the generated secret key. + #[arg(long = "output-file", env = "OUTPUT_FILE")] + pub output_path: String, +} + +#[derive(Parser, Debug)] +pub enum Command { + /// Run the auction server service. + Run(RunOptions), + /// Run db migrations and exit. + GenerateKey(GenerateKeyOptions), +} diff --git a/src/main.rs b/src/main.rs index 95e9793..4892b82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,12 @@ use { + crate::config::Command, api_client::{ApiClient, Observation}, borsh::BorshDeserialize, clap::Parser, posted_message::PostedMessageUnreliableData, - secp256k1::SecretKey, + secp256k1::{rand::rngs::OsRng, PublicKey, Secp256k1, SecretKey}, serde_wormhole::RawMessage, + sha3::{Digest, Keccak256}, solana_account_decoder::UiAccountEncoding, solana_client::{ nonblocking::pubsub_client::PubsubClient, @@ -14,7 +16,7 @@ use { rpc_response::{Response, RpcKeyedAccount}, }, solana_sdk::pubkey::Pubkey, - std::{fs, str::FromStr, time::Duration}, + std::{fs, io::IsTerminal, str::FromStr, time::Duration}, tokio::time::sleep, tokio_stream::StreamExt, wormhole_sdk::{vaa::Body, Address, Chain}, @@ -162,7 +164,7 @@ fn load_secret_key(path: String) -> SecretKey { let bytes = fs::read(path.clone()).expect("Invalid secret key file"); if bytes.len() == 32 { let byte_array: [u8; 32] = bytes.try_into().expect("Invalid secret key length"); - return SecretKey::from_byte_array(byte_array).expect("Invalid secret key length"); + return SecretKey::from_byte_array(&byte_array).expect("Invalid secret key length"); } let content = fs::read_to_string(path) @@ -172,9 +174,20 @@ fn load_secret_key(path: String) -> SecretKey { SecretKey::from_str(&content).expect("Invalid secret key") } -#[tokio::main] -async fn main() { - let run_options = config::RunOptions::parse(); +fn get_public_key(secret_key: &SecretKey) -> (PublicKey, [u8; 20]) { + let secp = Secp256k1::new(); + let public_key = secret_key.public_key(&secp); + let pubkey_uncompressed = public_key.serialize_uncompressed(); + let pubkey_hash: [u8; 32] = Keccak256::new_with_prefix(&pubkey_uncompressed[1..]) + .finalize() + .into(); + let pubkey_evm: [u8; 20] = pubkey_hash[pubkey_hash.len() - 20..] + .try_into() + .expect("Invalid address length"); + (public_key, pubkey_evm) +} + +async fn run(run_options: config::RunOptions) { let secret_key = load_secret_key(run_options.secret_key_path); let client = PubsubClient::new(&run_options.pythnet_url) .await @@ -187,6 +200,14 @@ async fn main() { let api_client = ApiClient::try_new(run_options.server_url, None).expect("Failed to create API client"); + let (pubkey, pubkey_evm) = get_public_key(&secret_key); + let evm_encded_public_key = format!("0x{}", hex::encode(pubkey_evm)); + tracing::info!( + public_key = ?pubkey, + evm_encoded_public_key = ?evm_encded_public_key, + "Running listener...", + ); + loop { if let Err(e) = run_listener(RunListenerInput { ws_url: run_options.pythnet_url.clone(), @@ -203,6 +224,45 @@ async fn main() { } } +#[tokio::main] +async fn main() { + // Initialize a Tracing Subscriber + let fmt_builder = tracing_subscriber::fmt() + .with_file(false) + .with_line_number(true) + .with_thread_ids(true) + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .with_ansi(std::io::stderr().is_terminal()); + + // Use the compact formatter if we're in a terminal, otherwise use the JSON formatter. + if std::io::stderr().is_terminal() { + tracing::subscriber::set_global_default(fmt_builder.compact().finish()) + .expect("Failed to set global default subscriber"); + } else { + tracing::subscriber::set_global_default(fmt_builder.json().finish()) + .expect("Failed to set global default subscriber"); + } + + // Parse the command line arguments with StructOpt, will exit automatically on `--help` or + // with invalid arguments. + match Command::parse() { + Command::Run(run_options) => run(run_options).await, + Command::GenerateKey(opts) => { + let secp = Secp256k1::new(); + let mut rng = OsRng; + + // Generate keypair (secret + public key) + let (secret_key, _) = secp.generate_keypair(&mut rng); + fs::write(opts.output_path.clone(), secret_key.secret_bytes()) + .expect("Failed to write secret key to file"); + let (pubkey, pubkey_evm) = get_public_key(&secret_key); + tracing::info!("Generated secret key at: {}", opts.output_path); + tracing::info!("Public key: {}", pubkey); + tracing::info!("EVM encoded public key: 0x{}", hex::encode(pubkey_evm)); + } + } +} + #[cfg(test)] mod tests { use super::*;