diff --git a/Cargo.lock b/Cargo.lock index a2215336a..351323007 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -880,6 +880,7 @@ dependencies = [ "memory_pprof", "musig2", "operator-wallet", + "p2p-types", "rustls-pemfile", "secp256k1", "secret-service-client", @@ -896,7 +897,6 @@ dependencies = [ "strata-bridge-stake-chain", "strata-bridge-tx-graph", "strata-p2p", - "strata-p2p-types", "strata-primitives 0.2.0", "strata-tasks", "tikv-jemallocator", @@ -2924,6 +2924,15 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "cynosure" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e545892537ed04ec26b5fbbedcc05d95a0c5a7704fbb93a0b1a22ef344ff70" +dependencies = [ + "futures", +] + [[package]] name = "darling" version = "0.20.11" @@ -3353,9 +3362,13 @@ dependencies = [ "btc-tracker", "futures", "hex", + "libp2p-identity", "musig2", "operator-wallet", + "p2p-types", + "p2p-wire", "proptest", + "prost", "secp256k1", "secret-service-client", "secret-service-proto", @@ -3375,8 +3388,6 @@ dependencies = [ "strata-bridge-tx-graph", "strata-l1tx", "strata-p2p", - "strata-p2p-types", - "strata-p2p-wire", "strata-primitives 0.2.0", "strata-state", "strata-tasks", @@ -3766,6 +3777,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flexbuffers" +version = "25.9.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e6701cb4a12d88a63f83ac41a11aa61286b5a678445a1a5ab9bc8e38654910a" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "num_enum 0.5.11", + "serde", + "serde_derive", +] + [[package]] name = "flume" version = "0.11.1" @@ -5303,6 +5327,7 @@ dependencies = [ "libp2p-gossipsub", "libp2p-identify", "libp2p-identity", + "libp2p-kad", "libp2p-mdns", "libp2p-metrics", "libp2p-noise", @@ -5408,6 +5433,7 @@ dependencies = [ "quick-protobuf-codec", "rand 0.8.5", "regex", + "serde", "sha2", "tracing", "web-time", @@ -5448,12 +5474,41 @@ dependencies = [ "multihash", "quick-protobuf", "rand 0.8.5", + "serde", "sha2", "thiserror 2.0.12", "tracing", "zeroize", ] +[[package]] +name = "libp2p-kad" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bab0466a27ebe955bcbc27328fae5429c5b48c915fd6174931414149802ec23" +dependencies = [ + "asynchronous-codec", + "bytes", + "either", + "fnv", + "futures", + "futures-bounded", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "quick-protobuf", + "quick-protobuf-codec", + "rand 0.8.5", + "serde", + "sha2", + "smallvec", + "thiserror 2.0.12", + "tracing", + "uint 0.10.0", + "web-time", +] + [[package]] name = "libp2p-mdns" version = "0.47.0" @@ -5484,6 +5539,7 @@ dependencies = [ "libp2p-gossipsub", "libp2p-identify", "libp2p-identity", + "libp2p-kad", "libp2p-ping", "libp2p-swarm", "pin-project", @@ -6041,6 +6097,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" dependencies = [ "core2", + "serde", "unsigned-varint 0.8.0", ] @@ -6727,6 +6784,33 @@ dependencies = [ "sha2", ] +[[package]] +name = "p2p-types" +version = "0.1.0" +dependencies = [ + "bincode", + "bitcoin", + "hex", + "libp2p-identity", + "proptest", + "proptest-derive", + "secp256k1", + "serde", + "serde_json", +] + +[[package]] +name = "p2p-wire" +version = "0.1.0" +dependencies = [ + "bitcoin", + "libp2p", + "musig2", + "p2p-types", + "prost", + "prost-build", +] + [[package]] name = "p3-air" version = "0.2.3-succinct" @@ -7320,7 +7404,7 @@ checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" dependencies = [ "fixed-hash", "impl-codec", - "uint", + "uint 0.9.5", ] [[package]] @@ -9813,11 +9897,12 @@ dependencies = [ "futures", "libp2p", "musig2", + "p2p-types", + "p2p-wire", + "prost", "strata-bridge-common", "strata-bridge-test-utils", "strata-p2p", - "strata-p2p-types", - "strata-p2p-wire", "tokio", "tokio-util", "tracing", @@ -9836,6 +9921,7 @@ dependencies = [ "bitvm", "miniscript", "musig2", + "p2p-types", "proptest", "proptest-derive", "rkyv", @@ -9843,7 +9929,6 @@ dependencies = [ "serde", "serde_json", "sha2", - "strata-p2p-types", "strata-primitives 0.2.0", "thiserror 2.0.12", ] @@ -10127,49 +10212,20 @@ dependencies = [ [[package]] name = "strata-p2p" -version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata-p2p.git?tag=v0.1.0#d7f4d4acddd4d36a2962e948ffa03c1f0d01685a" +version = "0.3.0" +source = "git+https://github.com/alpenlabs/strata-p2p.git?tag=v0.3.0#666b61681681ba56a2bd85bf655d5d0e2c6caf2c" dependencies = [ - "async-trait", - "bitcoin", + "cynosure", + "flexbuffers", "futures", "libp2p", - "musig2", - "prost", - "strata-p2p-types", - "strata-p2p-wire", + "serde", "thiserror 2.0.12", "tokio", "tokio-util", "tracing", ] -[[package]] -name = "strata-p2p-types" -version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata-p2p.git?tag=v0.1.0#d7f4d4acddd4d36a2962e948ffa03c1f0d01685a" -dependencies = [ - "bitcoin", - "hex", - "libp2p-identity", - "proptest", - "proptest-derive", - "serde", -] - -[[package]] -name = "strata-p2p-wire" -version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata-p2p.git?tag=v0.1.0#d7f4d4acddd4d36a2962e948ffa03c1f0d01685a" -dependencies = [ - "bitcoin", - "libp2p", - "musig2", - "prost", - "prost-build", - "strata-p2p-types", -] - [[package]] name = "strata-primitives" version = "0.2.0" @@ -11186,6 +11242,18 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "uint" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + [[package]] name = "unarray" version = "0.1.4" diff --git a/Cargo.toml b/Cargo.toml index 9e73d98c8..8449e559e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,8 @@ members = [ "crates/operator-wallet", "crates/wots", "crates/memory_pprof", + "crates/p2p-wire", + "crates/p2p-types", # binaries listed separately "bin/secret-service", @@ -63,6 +65,8 @@ btc-tracker = { path = "crates/btc-tracker" } duty-tracker = { path = "crates/duty-tracker" } operator-wallet = { version = "0.1.0", path = "crates/operator-wallet" } prover-test-utils = { path = "crates/bridge-proof/test-utils" } +p2p-types = { path = "crates/p2p-types" } +p2p-wire = { path = "crates/p2p-wire" } secret-service-client = { path = "crates/secret-service-client" } secret-service-proto = { path = "crates/secret-service-proto" } secret-service-server = { path = "crates/secret-service-server" } @@ -94,9 +98,13 @@ strata-rpc-types = { git = "https://github.com/alpenlabs/alpen.git", tag = "v0.2 strata-state = { git = "https://github.com/alpenlabs/alpen.git", tag = "v0.2.0-rc4" } strata-tasks = { git = "https://github.com/alpenlabs/alpen.git", tag = "v0.2.0-rc4" } -strata-p2p = { git = "https://github.com/alpenlabs/strata-p2p.git", tag = "v0.1.0" } -strata-p2p-types = { git = "https://github.com/alpenlabs/strata-p2p.git", tag = "v0.1.0" } -strata-p2p-wire = { git = "https://github.com/alpenlabs/strata-p2p.git", tag = "v0.1.0" } +strata-p2p = { git = "https://github.com/alpenlabs/strata-p2p.git", tag = "v0.3.0", features = [ + # TODO(sistemd): BYOS (bring your own signer) is not enabled yet - open a ticket for this + "quic", + "request-response", + "gossipsub", + "kad", +], default-features = false } zkaleido = { git = "https://github.com/alpenlabs/zkaleido", tag = "v0.1.0-alpha-rc11" } zkaleido-native-adapter = { git = "https://github.com/alpenlabs/zkaleido", tag = "v0.1.0-alpha-rc11" } @@ -159,6 +167,7 @@ libp2p = { version = "0.55.0", features = [ "yamux", "identify", ] } +libp2p-identity = "0.2.10" miniscript = "12.3.0" musig2 = { version = "0.1.0", features = [ "serde", @@ -170,6 +179,8 @@ opentelemetry_sdk = { version = "0.29.0", features = ["rt-tokio"] } parking_lot = "0.12.3" proptest = "1.6.0" proptest-derive = "0.5.1" +prost = "0.13.4" +prost-build = "0.13.4" quinn = "0.11.6" rand = "0.8.5" reqwest = { version = "0.12.12", default-features = false, features = [ diff --git a/bin/alpen-bridge/Cargo.toml b/bin/alpen-bridge/Cargo.toml index de539b327..4d24d0e6b 100644 --- a/bin/alpen-bridge/Cargo.toml +++ b/bin/alpen-bridge/Cargo.toml @@ -24,7 +24,7 @@ strata-bridge-rpc.workspace = true strata-bridge-stake-chain.workspace = true strata-bridge-tx-graph.workspace = true strata-p2p.workspace = true -strata-p2p-types.workspace = true +p2p-types.workspace = true strata-primitives.workspace = true strata-bridge-common.workspace = true diff --git a/bin/alpen-bridge/src/mode/operator.rs b/bin/alpen-bridge/src/mode/operator.rs index 7eea57850..534f1d217 100644 --- a/bin/alpen-bridge/src/mode/operator.rs +++ b/bin/alpen-bridge/src/mode/operator.rs @@ -25,11 +25,15 @@ use duty_tracker::{ shutdown::ShutdownHandler, stake_chain_persister::StakeChainPersister, }; use libp2p::{ - identity::{secp256k1::PublicKey as LibP2pSecpPublicKey, PublicKey as LibP2pPublicKey}, + identity::{ + secp256k1::{Keypair, PublicKey as LibP2pSecpPublicKey}, + PublicKey as LibP2pPublicKey, + }, PeerId, }; use musig2::KeyAggContext; use operator_wallet::{sync::Backend, OperatorWallet, OperatorWalletConfig}; +use p2p_types::{P2POperatorPubKey, StakeChainId}; use secp256k1::{Parity, SECP256K1}; use secret_service_client::{ rustls::{ @@ -51,8 +55,7 @@ use strata_bridge_primitives::{ constants::SEGWIT_MIN_AMOUNT, operator_table::OperatorTable, types::OperatorIdx, }; use strata_bridge_stake_chain::prelude::OPERATOR_FUNDS; -use strata_p2p::swarm::handle::P2PHandle; -use strata_p2p_types::{P2POperatorPubKey, StakeChainId}; +use strata_p2p::swarm::handle::{CommandHandle, GossipHandle, ReqRespHandle}; use strata_tasks::TaskExecutor; use tokio::{net::lookup_host, select, sync::mpsc, task::JoinHandle}; use tracing::{debug, error, info}; @@ -152,9 +155,9 @@ pub(crate) async fn bootstrap( // Initialize the P2P handle. info!("initializing p2p handle"); - let (p2p_handle, p2p_task) = init_p2p_handle(&config, ¶ms, p2p_sk).await?; + let p2p_handles = init_p2p_handles(&config, ¶ms, p2p_sk).await?; debug!("p2p handle initialized"); - let p2p_handle_rpc = p2p_handle.clone(); + let command_handle_rpc = p2p_handles.command_handle.clone(); // Handle the stakechain genesis. handle_stakechain_genesis( @@ -176,12 +179,14 @@ pub(crate) async fn bootstrap( let (contract_manager, contract_persister, stake_chain_persister) = init_duty_tracker( ¶ms, &config, + p2p_handles.keypair, operator_table, pre_stake_pubkey.clone(), bitcoin_rpc_client.clone(), zmq_client, s2_client, - p2p_handle, + p2p_handles.gossip_handle, + p2p_handles.req_resp_handle, operator_wallet, db, ) @@ -196,12 +201,13 @@ pub(crate) async fn bootstrap( let rpc_params = params.clone(); let rpc_addr = rpc_config.rpc_addr.clone(); executor.spawn_critical_async_with_shutdown("rpc_server", |_| async move { - let rpc_client = BridgeRpc::new(db_rpc, p2p_handle_rpc, rpc_params, rpc_config); + let rpc_client = BridgeRpc::new(db_rpc, command_handle_rpc, rpc_params, rpc_config); start_rpc(&rpc_client, rpc_addr.as_str()).await }); debug!("rpc server started"); info!("starting p2p service"); + let p2p_task = p2p_handles.listen_task; executor.spawn_critical_async_with_shutdown("p2p_service", |_| async move { p2p_task.await.map_err(anyhow::Error::from) }); @@ -308,14 +314,23 @@ fn read_cert(path: &Path) -> io::Result>> { } } +#[derive(Debug)] +struct P2PHandles { + command_handle: CommandHandle, + gossip_handle: GossipHandle, + req_resp_handle: ReqRespHandle, + listen_task: JoinHandle<()>, + keypair: Keypair, +} + /// Initialize the P2P handle. /// /// Needs a secret key and configuration. -async fn init_p2p_handle( +async fn init_p2p_handles( config: &Config, params: &Params, sk: SecretKey, -) -> anyhow::Result<(P2PHandle, JoinHandle<()>)> { +) -> anyhow::Result { let my_key = LibP2pSecpPublicKey::try_from_bytes(&sk.public_key(SECP256K1).serialize()) .expect("infallible"); let other_operators: Vec = params @@ -358,8 +373,14 @@ async fn init_p2p_handle( general_timeout, connection_check_interval, ); - let (p2p_handle, _cancel, listen_task) = p2p_bootstrap(&config).await?; - Ok((p2p_handle, listen_task)) + let handles = p2p_bootstrap(&config).await?; + Ok(P2PHandles { + command_handle: handles.command_handle, + gossip_handle: handles.gossip_handle, + req_resp_handle: handles.req_resp_handle, + listen_task: handles.listen_task, + keypair: config.keypair, + }) } async fn init_database_handle(config: &Config) -> SqliteDb { @@ -438,12 +459,14 @@ fn create_db_file(datadir: impl AsRef, db_name: &str) -> PathBuf { async fn init_duty_tracker( params: &Params, config: &Config, + keypair: Keypair, operator_table: OperatorTable, pre_stake_pubkey: ScriptBuf, rpc_client: BitcoinClient, zmq_client: BtcNotifyClient, s2_client: SecretServiceClient, - p2p_handle: P2PHandle, + gossip_handle: GossipHandle, + req_resp_handle: ReqRespHandle, operator_wallet: OperatorWallet, db: SqliteDb, ) -> anyhow::Result<(ContractManager, ContractPersister, StakeChainPersister)> { @@ -471,6 +494,7 @@ async fn init_duty_tracker( let contract_manager = ContractManager::new( network, + keypair, nag_interval, connector_params, pegout_graph_params, @@ -484,7 +508,8 @@ async fn init_duty_tracker( pre_stake_pubkey, zmq_client, rpc_client, - p2p_handle, + gossip_handle, + req_resp_handle, contract_persister, stake_chain_persister, s2_client, diff --git a/bin/alpen-bridge/src/rpc_server.rs b/bin/alpen-bridge/src/rpc_server.rs index 13713866d..7da40345a 100644 --- a/bin/alpen-bridge/src/rpc_server.rs +++ b/bin/alpen-bridge/src/rpc_server.rs @@ -42,7 +42,7 @@ use strata_bridge_tx_graph::transactions::{ deposit::DepositTx, prelude::{ChallengeTx, ChallengeTxInput}, }; -use strata_p2p::swarm::handle::P2PHandle; +use strata_p2p::swarm::handle::CommandHandle; use strata_primitives::buf::Buf32; use tokio::{ sync::{oneshot, RwLock}, @@ -194,11 +194,11 @@ pub(crate) struct BridgeRpc { /// /// # Warning /// - /// The bridge RPC server should *NEVER* call [`P2PHandle::next_event`] as it will mess with - /// the duty tracker processing of messages in the P2P gossip network. + /// The bridge RPC server should *NEVER* call [`CommandHandle::next_event`] as it will mess + /// with the duty tracker processing of messages in the P2P gossip network. /// - /// The same applies for the `Stream` implementation of [`P2PHandle`]. - p2p_handle: P2PHandle, + /// The same applies for the `Stream` implementation of [`CommandHandle`]. + command_handle: CommandHandle, /// Consensus-critical parameters that dictate the behavior of the bridge node. params: Params, @@ -211,7 +211,7 @@ impl BridgeRpc { /// Create a new instance of [`BridgeRpc`]. pub(crate) fn new( db: SqliteDb, - p2p_handle: P2PHandle, + command_handle: CommandHandle, params: Params, config: RpcConfig, ) -> Self { @@ -223,7 +223,7 @@ impl BridgeRpc { start_time, db, cached_contracts, - p2p_handle, + command_handle, params, config, }; @@ -414,17 +414,15 @@ impl StrataBridgeMonitoringApiServer for BridgeRpc { } async fn get_operator_status(&self, operator_pk: PublicKey) -> RpcResult { - let conversion = convert_operator_pk_to_peer_id(&self.params, &operator_pk); - // Avoid DoS attacks by just returning an error if the public key is invalid - if conversion.is_err() { + let Ok(conversion) = convert_operator_pk_to_peer_id(&self.params, &operator_pk) else { + // Avoid DoS attacks by just returning an error if the public key is invalid return Err(rpc_error( ErrorCode::InvalidRequest, "Invalid operator public key", operator_pk, )); - } - // NOTE: safe to unwrap because we just checked if it's valid - if self.p2p_handle.is_connected(conversion.unwrap()).await { + }; + if self.command_handle.is_connected(&conversion, None).await { Ok(RpcOperatorStatus::Online) } else { Ok(RpcOperatorStatus::Offline) diff --git a/crates/duty-tracker/Cargo.toml b/crates/duty-tracker/Cargo.toml index ff6d45f91..0dc8497de 100644 --- a/crates/duty-tracker/Cargo.toml +++ b/crates/duty-tracker/Cargo.toml @@ -9,6 +9,9 @@ workspace = true [dependencies] algebra.workspace = true alpen-bridge-params.workspace = true +p2p-types = { workspace = true, features = ["proptest"] } +p2p-wire.workspace = true +prost.workspace = true strata-bridge-connectors.workspace = true strata-bridge-db.workspace = true strata-bridge-p2p-service.workspace = true @@ -21,8 +24,6 @@ strata-bridge-tx-graph.workspace = true strata-l1tx.workspace = true strata-p2p.workspace = true -strata-p2p-types = { workspace = true, features = ["proptest"] } -strata-p2p-wire.workspace = true strata-primitives.workspace = true strata-state.workspace = true strata-tasks.workspace = true @@ -39,6 +40,7 @@ bitvm.workspace = true borsh.workspace = true btc-tracker.workspace = true futures.workspace = true +libp2p-identity.workspace = true musig2.workspace = true operator-wallet.workspace = true secp256k1.workspace = true diff --git a/crates/duty-tracker/src/contract_manager.rs b/crates/duty-tracker/src/contract_manager.rs index d18c3528b..b3fe68ea2 100644 --- a/crates/duty-tracker/src/contract_manager.rs +++ b/crates/duty-tracker/src/contract_manager.rs @@ -20,8 +20,12 @@ use btc_tracker::{ event::BlockStatus, tx_driver::TxDriver, }; -use futures::{future::join_all, StreamExt}; +use futures::{future::join_all, SinkExt, StreamExt}; +use libp2p_identity::secp256k1::Keypair; use operator_wallet::OperatorWallet; +use p2p_types::{P2POperatorPubKey, Scope, SessionId, StakeChainId, WotsPublicKeys}; +use p2p_wire::p2p::v1::{GetMessageRequest, GossipsubMsg, UnsignedGossipsubMsg}; +use prost::Message; use secret_service_client::SecretServiceClient; use strata_bridge_db::persistent::sqlite::SqliteDb; use strata_bridge_p2p_service::MessageHandler; @@ -30,9 +34,12 @@ use strata_bridge_tx_graph::transactions::{ deposit::DepositTx, prelude::{AssertDataTxInput, CovenantTx}, }; -use strata_p2p::{self, commands::Command, events::Event, swarm::handle::P2PHandle}; -use strata_p2p_types::{P2POperatorPubKey, Scope, SessionId, StakeChainId, WotsPublicKeys}; -use strata_p2p_wire::p2p::v1::{GetMessageRequest, GossipsubMsg, UnsignedGossipsubMsg}; +use strata_p2p::{ + self, + commands::{GossipCommand, RequestResponseCommand}, + events::{GossipEvent, ReqRespEvent}, + swarm::handle::{GossipHandle, ReqRespHandle}, +}; use strata_primitives::params::RollupParams; use strata_state::{bridge_state::DepositState, chain_state::Chainstate}; use tokio::{ @@ -76,6 +83,7 @@ impl ContractManager { pub fn new( // Static Config Parameters network: Network, + keypair: Keypair, nag_interval: Duration, connector_params: ConnectorParams, pegout_graph_params: PegOutGraphParams, @@ -91,7 +99,8 @@ impl ContractManager { // Subsystem Handles zmq_client: BtcNotifyClient, rpc_client: BitcoinClient, - mut p2p_handle: P2PHandle, + mut gossip_handle: GossipHandle, + mut req_resp_handle: ReqRespHandle, contract_persister: ContractPersister, stake_chain_persister: StakeChainPersister, s2_client: SecretServiceClient, @@ -279,8 +288,11 @@ impl ContractManager { // If we successfully handle the processing of our message, we // can forward it to the rest of the p2p network. - let signed = p2p_handle.sign_message(msg); - p2p_handle.send_command(Command::PublishMessage(signed)).await; + let signed = msg.sign_secp256k1(&keypair); + let data = GossipsubMsg::from(signed).into_raw().encode_to_vec(); + if let Err(e) = gossip_handle.send(GossipCommand { data }).await { + error!(%e, "failed to forward ouroboros message to gossip handler"); + } } Err(e) => { error!(%e, "CRITICAL: failed to process ouroboros message"); @@ -335,7 +347,12 @@ impl ContractManager { } else { // If it wasn't meant for us we can forward the request to the p2p // network - p2p_handle.send_command(Command::RequestMessage(req)).await; + if let Err(e) = req_resp_handle.send(RequestResponseCommand { + target_transport_id: req.peer_id(), + data: req.into_msg().encode_to_vec(), + }).await { + error!(%e, "failed to forward ouroboros p2p request to req/resp handler"); + } } }, None => { @@ -404,35 +421,45 @@ impl ContractManager { // Finally, we process peer messages. We do this last so we have the best chance of // servicing peer requests and can sidestep the processing of unnecessary peer // messages. - Some(event) = p2p_handle.next() => match event { - Ok(Event::ReceivedMessage(msg)) => { - match ctx.process_p2p_message(msg.clone()).await { - Ok(msg_duties) if !msg_duties.is_empty() => { - duties.extend(msg_duties); - }, - Ok(_) => {}, - Err(e) => { - error!(?msg, %e, "failed to process p2p msg"); - // in case an error occurs, we will just nag again - // so no need to break out of the event loop + Ok(GossipEvent::ReceivedMessage(raw_msg)) = gossip_handle.next_event() => { + match GossipsubMsg::from_bytes(&raw_msg) { + Ok(msg) => { + match ctx.process_p2p_message(msg.clone()).await { + Ok(msg_duties) if !msg_duties.is_empty() => { + duties.extend(msg_duties); + }, + Ok(_) => {}, + Err(e) => { + error!(?msg, %e, "failed to process p2p msg"); + // in case an error occurs, we will just nag again + // so no need to break out of the event loop + } } } - }, - Ok(Event::ReceivedRequest(req)) => { - match ctx.process_p2p_request(req.clone()).await { - Ok(p2p_duties) => duties.extend(p2p_duties), - Err(e) => { - error!(?req, %e, "failed to process p2p request"); - // in case an error occurs, the requester will just nag again - // so no need to break out of the event loop - }, + Err(e) => { + error!(%e, "failed to decode p2p msg"); + } + } + }, + + Some(ReqRespEvent::ReceivedRequest(raw_req, sender)) = req_resp_handle.next_event() => { + match Message::decode(raw_req.as_slice()).and_then(GetMessageRequest::from_msg) { + Ok(req) => { + match ctx.process_p2p_request(req.clone()).await { + Ok(p2p_duties) => duties.extend(p2p_duties), + Err(e) => { + error!(?req, ?sender, %e, "failed to process p2p request"); + // in case an error occurs, the requester will just nag again + // so no need to break out of the event loop + }, + } + } + Err(e) => { + error!(%e, ?sender, "failed to decode p2p request"); } - }, - Err(e) => { - error!(%e, "error while polling for p2p messages"); - // this could be a transient issue, so no need to break immediately } }, + _instant = interval.tick() => { debug!("nagging peers for necessary p2p messages"); @@ -451,7 +478,6 @@ impl ContractManager { }); join_all(nags).await; - } } diff --git a/crates/duty-tracker/src/contract_state_machine.rs b/crates/duty-tracker/src/contract_state_machine.rs index 2809d959a..1027b45d1 100644 --- a/crates/duty-tracker/src/contract_state_machine.rs +++ b/crates/duty-tracker/src/contract_state_machine.rs @@ -21,6 +21,7 @@ use musig2::{ aggregate_partial_signatures, errors::VerifyError, secp256k1::Message, verify_partial, AggNonce, PartialSignature, PubNonce, }; +use p2p_types::{P2POperatorPubKey, WotsPublicKeys}; use secp256k1::schnorr; use strata_bridge_primitives::{ build_context::TxBuildContext, @@ -48,7 +49,6 @@ use strata_bridge_tx_graph::{ }, }, }; -use strata_p2p_types::{P2POperatorPubKey, WotsPublicKeys}; use strata_primitives::{buf::Buf32, params::RollupParams}; use strata_state::bridge_state::{DepositEntry, DepositState}; use thiserror::Error; @@ -3562,6 +3562,7 @@ mod prop_tests { hashes::{sha256, sha256d, Hash}, Network, Txid, }; + use p2p_types::P2POperatorPubKey; use proptest::{prelude::*, prop_compose}; use strata_bridge_common::logging::{self, LoggerConfig}; use strata_bridge_primitives::{ @@ -3572,7 +3573,6 @@ mod prop_tests { use strata_bridge_tx_graph::transactions::deposit::{ prop_tests::arb_deposit_request_data, DepositTx, }; - use strata_p2p_types::P2POperatorPubKey; use strata_primitives::{ block_credential::CredRule, buf::Buf32, diff --git a/crates/duty-tracker/src/errors.rs b/crates/duty-tracker/src/errors.rs index fe16412eb..2c7917f75 100644 --- a/crates/duty-tracker/src/errors.rs +++ b/crates/duty-tracker/src/errors.rs @@ -3,10 +3,10 @@ use bdk_wallet::error::CreateTxError; use bitcoind_async_client::error::ClientError; use btc_tracker::tx_driver::DriveErr; +use p2p_types::P2POperatorPubKey; +use p2p_wire::p2p::v1::{GetMessageRequest, UnsignedGossipsubMsg}; use strata_bridge_db::errors::DbError; use strata_bridge_tx_graph::errors::TxGraphError; -use strata_p2p_types::P2POperatorPubKey; -use strata_p2p_wire::p2p::v1::{GetMessageRequest, UnsignedGossipsubMsg}; use thiserror::Error; use crate::{contract_persister::ContractPersistErr, contract_state_machine::TransitionErr}; diff --git a/crates/duty-tracker/src/executors/contested_withdrawal.rs b/crates/duty-tracker/src/executors/contested_withdrawal.rs index 39cb416f4..e0abf1d6d 100644 --- a/crates/duty-tracker/src/executors/contested_withdrawal.rs +++ b/crates/duty-tracker/src/executors/contested_withdrawal.rs @@ -3,6 +3,7 @@ use bitcoin::{taproot, Network, OutPoint, Txid}; use bitvm::{chunk::api::generate_assertions, signatures::HASH_LEN}; use btc_tracker::event::TxStatus; use futures::future::join_all; +use p2p_types::WotsPublicKeys; use rand::thread_rng; use secp256k1::rand::{self, Rng}; use secret_service_proto::v2::traits::*; @@ -22,7 +23,6 @@ use strata_bridge_tx_graph::transactions::{ PreAssertData, PreAssertTx, }, }; -use strata_p2p_types::WotsPublicKeys; use tracing::{info, warn}; use crate::{ diff --git a/crates/duty-tracker/src/executors/deposit.rs b/crates/duty-tracker/src/executors/deposit.rs index 2051c656e..f221d1e05 100644 --- a/crates/duty-tracker/src/executors/deposit.rs +++ b/crates/duty-tracker/src/executors/deposit.rs @@ -13,6 +13,7 @@ use bitcoin::{ use btc_tracker::{event::TxStatus, tx_driver::TxDriver}; use futures::FutureExt; use musig2::{aggregate_partial_signatures, AggNonce, PartialSignature, PubNonce}; +use p2p_types::{Scope, SessionId, StakeChainId}; use secp256k1::{schnorr, Message, PublicKey}; use secret_service_client::SecretServiceClient; use secret_service_proto::v2::traits::*; @@ -24,7 +25,6 @@ use strata_bridge_tx_graph::{ pog_musig_functor::PogMusigF, transactions::{deposit::DepositTx, prelude::CovenantTx}, }; -use strata_p2p_types::{Scope, SessionId, StakeChainId}; use tracing::{debug, error, info, warn}; use crate::{ diff --git a/crates/duty-tracker/src/executors/wots_handler.rs b/crates/duty-tracker/src/executors/wots_handler.rs index ae2d069f2..0d853e896 100644 --- a/crates/duty-tracker/src/executors/wots_handler.rs +++ b/crates/duty-tracker/src/executors/wots_handler.rs @@ -3,10 +3,10 @@ use bitcoin::Txid; use bitvm::chunk::api::{NUM_HASH, NUM_PUBS, NUM_U256}; use futures::future::{join3, join_all}; +use p2p_types::{Wots128PublicKey, Wots256PublicKey, WotsPublicKeys}; use secret_service_client::{wots::WotsClient, SecretServiceClient}; use secret_service_proto::v2::traits::*; use strata_bridge_primitives::wots::{self, Assertions}; -use strata_p2p_types::{Wots128PublicKey, Wots256PublicKey, WotsPublicKeys}; use tracing::info; use crate::{ diff --git a/crates/duty-tracker/src/stake_chain_persister.rs b/crates/duty-tracker/src/stake_chain_persister.rs index 8ef85d418..97e43e616 100644 --- a/crates/duty-tracker/src/stake_chain_persister.rs +++ b/crates/duty-tracker/src/stake_chain_persister.rs @@ -3,10 +3,10 @@ use std::collections::BTreeMap; use bitcoin::{hex::DisplayHex, OutPoint}; +use p2p_types::P2POperatorPubKey; use strata_bridge_db::{errors::DbError, persistent::sqlite::SqliteDb, public::PublicDb}; use strata_bridge_primitives::operator_table::OperatorTable; use strata_bridge_stake_chain::stake_chain::StakeChainInputs; -use strata_p2p_types::P2POperatorPubKey; use tracing::{debug, info, trace, warn}; /// A database wrapper for dumping ad retrieving stake chain data. diff --git a/crates/duty-tracker/src/stake_chain_state_machine.rs b/crates/duty-tracker/src/stake_chain_state_machine.rs index 2e4a20a07..9ea68350d 100644 --- a/crates/duty-tracker/src/stake_chain_state_machine.rs +++ b/crates/duty-tracker/src/stake_chain_state_machine.rs @@ -3,6 +3,7 @@ use std::collections::BTreeMap; use alpen_bridge_params::prelude::StakeChainParams; use bitcoin::{Network, OutPoint, Txid}; +use p2p_types::P2POperatorPubKey; use strata_bridge_primitives::operator_table::OperatorTable; use strata_bridge_stake_chain::{ prelude::{StakeTx, STAKE_VOUT}, @@ -10,7 +11,6 @@ use strata_bridge_stake_chain::{ transactions::stake::{Head, StakeTxKind, Tail}, StakeChain, }; -use strata_p2p_types::P2POperatorPubKey; use tracing::{debug, info, span, warn, Level}; use crate::{contract_state_machine::DepositSetup, errors::StakeChainErr}; diff --git a/crates/p2p-service/Cargo.toml b/crates/p2p-service/Cargo.toml index dd58559cd..3eee4347a 100644 --- a/crates/p2p-service/Cargo.toml +++ b/crates/p2p-service/Cargo.toml @@ -7,9 +7,9 @@ version = "0.1.0" workspace = true [dependencies] +p2p-types.workspace = true +p2p-wire.workspace = true strata-p2p.workspace = true -strata-p2p-types.workspace = true -strata-p2p-wire.workspace = true anyhow.workspace = true bitcoin.workspace = true @@ -24,4 +24,5 @@ strata-bridge-test-utils.workspace = true strata-bridge-common.workspace = true futures.workspace = true +prost.workspace = true tokio.workspace = true diff --git a/crates/p2p-service/src/bootstrap.rs b/crates/p2p-service/src/bootstrap.rs index 25c6c937d..a47d7c6c2 100644 --- a/crates/p2p-service/src/bootstrap.rs +++ b/crates/p2p-service/src/bootstrap.rs @@ -3,8 +3,10 @@ use std::time::Duration; use strata_p2p::swarm::{ - self, handle::P2PHandle, P2PConfig, DEFAULT_CONNECTION_CHECK_INTERVAL, DEFAULT_DIAL_TIMEOUT, - DEFAULT_GENERAL_TIMEOUT, P2P, + self, + handle::{CommandHandle, GossipHandle, ReqRespHandle}, + P2PConfig, DEFAULT_CONNECTION_CHECK_INTERVAL, DEFAULT_DIAL_TIMEOUT, DEFAULT_GENERAL_TIMEOUT, + P2P, }; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; @@ -12,20 +14,37 @@ use tracing::{debug, info}; use crate::{config::Configuration, constants::DEFAULT_IDLE_CONNECTION_TIMEOUT}; +/// Handles returned after bootstrapping the p2p node. +#[derive(Debug)] +pub struct BootstrapHandles { + /// Handle to send commands to the p2p node. + pub command_handle: CommandHandle, + /// Handle to interact with the gossip protocol. + pub gossip_handle: GossipHandle, + /// Handle to interact with the request-response protocol. + pub req_resp_handle: ReqRespHandle, + /// Cancellation token to stop the p2p node. + pub cancel: CancellationToken, + /// Task handle for the p2p node listener. + pub listen_task: JoinHandle<()>, +} + /// Bootstrap the p2p node by hooking up all the required services. -pub async fn bootstrap( - config: &Configuration, -) -> anyhow::Result<(P2PHandle, CancellationToken, JoinHandle<()>)> { +pub async fn bootstrap(config: &Configuration) -> anyhow::Result { let p2p_config = P2PConfig { - keypair: config.keypair.clone(), + transport_keypair: config.keypair.clone().into(), idle_connection_timeout: config .idle_connection_timeout .unwrap_or(Duration::from_secs(DEFAULT_IDLE_CONNECTION_TIMEOUT)), max_retries: None, - listening_addr: config.listening_addr.clone(), - allowlist: config.allowlist.clone(), + listening_addrs: vec![config.listening_addr.clone()], + // TODO(sistemd): This will be passed into the P2PConfig struct once byos is enabled - + // reference the ticket + // allowlist: config.allowlist.clone(), connect_to: config.connect_to.clone(), - signers_allowlist: config.signers_allowlist.clone(), + // TODO(sistemd): This will be passed into the P2P object once byos is enabled - reference + // the ticket + // signers_allowlist: config.signers_allowlist.clone(), dial_timeout: Some(config.dial_timeout.unwrap_or(DEFAULT_DIAL_TIMEOUT)), general_timeout: Some(config.general_timeout.unwrap_or(DEFAULT_GENERAL_TIMEOUT)), connection_check_interval: Some( @@ -33,15 +52,39 @@ pub async fn bootstrap( .connection_check_interval .unwrap_or(DEFAULT_CONNECTION_CHECK_INTERVAL), ), + protocol_name: None, + channel_timeout: None, + gossipsub_topic: None, + gossipsub_max_transmit_size: None, + gossipsub_score_params: None, + gossipsub_score_thresholds: None, + gossip_event_buffer_size: None, + commands_event_buffer_size: None, + command_buffer_size: None, + handle_default_timeout: None, + req_resp_event_buffer_size: None, + req_resp_command_buffer_size: None, + request_max_bytes: None, + response_max_bytes: None, + gossip_command_buffer_size: None, + envelope_max_age: None, + max_clock_skew: None, + kad_protocol_name: None, + kad_record_ttl: None, + kad_timer_putrecorderror: None, + conn_limits: Default::default(), }; let cancel = CancellationToken::new(); info!("initializing swarm"); - let swarm = swarm::with_tcp_transport(&p2p_config)?; + let swarm = swarm::with_default_transport(&p2p_config)?; debug!("swarm initialized"); info!("initializing p2p node"); - let (mut p2p, handle) = P2P::from_config(p2p_config, cancel.clone(), swarm, None)?; + let (mut p2p, req_resp_handle) = + P2P::from_config(p2p_config, cancel.clone(), swarm, None, None)?; + let command_handle = p2p.new_command_handle(); + let gossip_handle = p2p.new_gossip_handle(); debug!("p2p node initialized"); info!("establishing connections"); @@ -51,5 +94,11 @@ pub async fn bootstrap( info!("listening for network events and commands"); let listen_task = tokio::spawn(p2p.listen()); - Ok((handle, cancel, listen_task)) + Ok(BootstrapHandles { + command_handle, + gossip_handle, + req_resp_handle, + cancel, + listen_task, + }) } diff --git a/crates/p2p-service/src/config.rs b/crates/p2p-service/src/config.rs index eed280ee2..d5ab44665 100644 --- a/crates/p2p-service/src/config.rs +++ b/crates/p2p-service/src/config.rs @@ -7,7 +7,7 @@ use libp2p::{ identity::secp256k1::{Keypair as Libp2pSecpKeypair, SecretKey as Libp2pSecpSecretKey}, Multiaddr, PeerId, }; -use strata_p2p_types::P2POperatorPubKey; +use p2p_types::P2POperatorPubKey; /// Configuration for the P2P. #[derive(Debug, Clone)] diff --git a/crates/p2p-service/src/message_handler.rs b/crates/p2p-service/src/message_handler.rs index 5c0570259..744f3c896 100644 --- a/crates/p2p-service/src/message_handler.rs +++ b/crates/p2p-service/src/message_handler.rs @@ -1,18 +1,18 @@ //! Message handler for the Strata Bridge P2P. use bitcoin::{hashes::sha256, OutPoint, Txid, XOnlyPublicKey}; +use libp2p::identity::secp256k1; use musig2::{PartialSignature, PubNonce}; -use strata_p2p::commands::UnsignedPublishMessage; -use strata_p2p_types::{P2POperatorPubKey, Scope, SessionId, StakeChainId, WotsPublicKeys}; -use strata_p2p_wire::p2p::v1::GetMessageRequest; +use p2p_types::{P2POperatorPubKey, Scope, SessionId, StakeChainId, WotsPublicKeys}; +use p2p_wire::p2p::v1::{GetMessageRequest, GossipsubMsg, UnsignedGossipsubMsg}; use tokio::sync::mpsc; use tracing::{debug, error, info, trace}; /// Message handler for the bridge node for relaying p2p messages. /// /// This exposes an interface that allows publishing messages to the node itself as [`libbp2p`](https://docs.rs/libp2p/latest/libp2p/) does not support self-publishing. -// TODO: (@Rajil1213) rename this to `Outbox` and create a newtype for `P2PHandle` that exposes the -// interface to read messages off of the p2p network (aka the `Inbox`). +// TODO: (@Rajil1213) rename this to `Outbox` and create a newtype that exposes the interface to +// read messages off of the p2p network (aka the `Inbox`). #[derive(Debug, Clone)] pub struct MessageHandler { /// The outbound channel used to self-publish gossipsub messages i.e., to send messages to @@ -136,7 +136,7 @@ impl MessageHandler { /// Requests a deposit setup message from an operator. /// /// The user needs to wait for the response by [`Poll`](std::task::Poll)ing the associated - /// [`P2PHandle`](strata_p2p::swarm::handle::P2PHandle). + /// [`ReqRespHandle`](strata_p2p::swarm::handle::ReqRespHandle). pub async fn request_deposit_setup(&self, scope: Scope, operator_pk: P2POperatorPubKey) { let req = GetMessageRequest::DepositSetup { scope, operator_pk }; self.request(req, "Deposit setup request").await; @@ -145,7 +145,7 @@ impl MessageHandler { /// Requests a Stake chain exchange message from an operator. /// /// The user needs to wait for the response by [`Poll`](std::task::Poll)ing the associated - /// [`P2PHandle`](strata_p2p::swarm::handle::P2PHandle). + /// [`ReqRespHandle`](strata_p2p::swarm::handle::ReqRespHandle). pub async fn request_stake_chain_exchange( &self, stake_chain_id: StakeChainId, @@ -161,7 +161,7 @@ impl MessageHandler { /// Requests a MuSig2 nonces exchange message from an operator. /// /// The user needs to wait for the response by [`Poll`](std::task::Poll)ing the associated - /// [`P2PHandle`](strata_p2p::swarm::handle::P2PHandle). + /// [`ReqRespHandle`](strata_p2p::swarm::handle::ReqRespHandle). pub async fn request_musig2_nonces( &self, session_id: SessionId, @@ -177,7 +177,7 @@ impl MessageHandler { /// Requests a MuSig2 signatures exchange message from an operator. /// /// The user needs to wait for the response by [`Poll`](std::task::Poll)ing the associated - /// [`P2PHandle`](strata_p2p::swarm::handle::P2PHandle). + /// [`ReqRespHandle`](strata_p2p::swarm::handle::ReqRespHandle). pub async fn request_musig2_signatures( &self, session_id: SessionId, @@ -191,3 +191,166 @@ impl MessageHandler { .await; } } + +/// Signed version of [`UnsignedPublishMessage`]. +#[derive(Debug, Clone)] +pub struct PublishMessage { + /// Operator's P2P public key. + pub key: P2POperatorPubKey, + + /// Operator's signature over the message. + pub signature: Vec, + + /// Unsigned message. + pub msg: UnsignedPublishMessage, +} + +/// Types of unsigned messages. +#[derive(Debug, Clone)] +#[expect(clippy::large_enum_variant)] +pub enum UnsignedPublishMessage { + /// Stake Chain information. + StakeChainExchange { + /// 32-byte hash of some unique to stake chain data. + stake_chain_id: StakeChainId, + + /// 32-byte x-only public key of the operator used to advance the stake chain. + operator_pk: XOnlyPublicKey, + + /// [`Txid`] of the pre-stake transaction. + pre_stake_txid: Txid, + + /// vout index of the pre-stake transaction. + pre_stake_vout: u32, + }, + + /// Deposit setup. + /// + /// Primarily used for the WOTS PKs. + DepositSetup { + /// The deposit [`Scope`]. + scope: Scope, + + /// Index of the deposit. + index: u32, + + /// [`sha256::Hash`] hash of the stake transaction that the preimage is revealed when + /// advancing the stake. + hash: sha256::Hash, + + /// Funding transaction ID. + /// + /// Used to cover the dust outputs in the transaction graph connectors. + funding_txid: Txid, + + /// Funding transaction output index. + /// + /// Used to cover the dust outputs in the transaction graph connectors. + funding_vout: u32, + + /// Operator's X-only public key to construct a P2TR address to reimburse the + /// operator for a valid withdraw fulfillment. + operator_pk: XOnlyPublicKey, + + /// Winternitz One-Time Signature (WOTS) public keys shared in a deposit. + wots_pks: WotsPublicKeys, + }, + + /// MuSig2 (public) nonces exchange. + Musig2NoncesExchange { + /// The [`SessionId`]. + session_id: SessionId, + + /// Payload, (public) nonces. + pub_nonces: Vec, + }, + + /// MuSig2 (partial) signatures exchange. + Musig2SignaturesExchange { + /// The [`SessionId`]. + session_id: SessionId, + + /// Payload, (partial) signatures. + partial_sigs: Vec, + }, +} + +impl From for GossipsubMsg { + /// Converts [`PublishMessage`] into [`GossipsubMsg`]. + fn from(value: PublishMessage) -> Self { + GossipsubMsg { + signature: value.signature, + key: value.key, + unsigned: value.msg.into(), + } + } +} + +impl UnsignedPublishMessage { + /// Signs `self` using supplied [`secp256k1::Keypair`]. Returns a `Command` + /// with resulting signature and public key from [`secp256k1::Keypair`]. + pub fn sign_secp256k1(&self, keypair: &secp256k1::Keypair) -> PublishMessage { + let kind: UnsignedGossipsubMsg = self.clone().into(); + let msg = kind.content(); + let signature = keypair.secret().sign(&msg); + + PublishMessage { + key: keypair.public().clone().into(), + signature, + msg: self.clone(), + } + } +} + +impl From for UnsignedGossipsubMsg { + /// Converts [`UnsignedPublishMessage`] into [`UnsignedGossipsubMsg`]. + fn from(value: UnsignedPublishMessage) -> Self { + match value { + UnsignedPublishMessage::StakeChainExchange { + stake_chain_id, + operator_pk, + pre_stake_txid, + pre_stake_vout, + } => UnsignedGossipsubMsg::StakeChainExchange { + stake_chain_id, + operator_pk, + pre_stake_txid, + pre_stake_vout, + }, + + UnsignedPublishMessage::DepositSetup { + scope, + index, + hash, + funding_txid, + funding_vout, + operator_pk, + wots_pks, + } => UnsignedGossipsubMsg::DepositSetup { + scope, + index, + hash, + funding_txid, + funding_vout, + operator_pk, + wots_pks, + }, + + UnsignedPublishMessage::Musig2NoncesExchange { + session_id, + pub_nonces, + } => UnsignedGossipsubMsg::Musig2NoncesExchange { + session_id, + nonces: pub_nonces, + }, + + UnsignedPublishMessage::Musig2SignaturesExchange { + session_id, + partial_sigs, + } => UnsignedGossipsubMsg::Musig2SignaturesExchange { + session_id, + signatures: partial_sigs, + }, + } + } +} diff --git a/crates/p2p-service/src/tests/common.rs b/crates/p2p-service/src/tests/common.rs index 46fdf54e3..026829183 100644 --- a/crates/p2p-service/src/tests/common.rs +++ b/crates/p2p-service/src/tests/common.rs @@ -7,26 +7,34 @@ use bitcoin::{ hashes::{sha256, Hash}, Txid, XOnlyPublicKey, }; -use futures::future::join_all; +use futures::{future::join_all, SinkExt}; use libp2p::{ build_multiaddr, identity::{secp256k1::Keypair as SecpKeypair, Keypair}, Multiaddr, PeerId, }; +use p2p_types::{P2POperatorPubKey, Scope, SessionId, StakeChainId, WotsPublicKeys}; +use p2p_wire::p2p::v1::{GossipsubMsg, UnsignedGossipsubMsg}; +use prost::Message; use strata_bridge_test_utils::musig2::{generate_partial_signature, generate_pubnonce}; use strata_p2p::{ - commands::{Command, UnsignedPublishMessage}, - events::Event, - swarm::{self, handle::P2PHandle, P2PConfig, P2P}, + commands::GossipCommand, + events::GossipEvent, + swarm::{ + self, + handle::{GossipHandle, ReqRespHandle}, + P2PConfig, P2P, + }, }; -use strata_p2p_types::{P2POperatorPubKey, Scope, SessionId, StakeChainId, WotsPublicKeys}; -use strata_p2p_wire::p2p::v1::{GossipsubMsg, UnsignedGossipsubMsg}; use tokio_util::{sync::CancellationToken, task::TaskTracker}; use tracing::{info, trace}; +use crate::message_handler::{PublishMessage, UnsignedPublishMessage}; + pub(crate) struct Operator { pub(crate) p2p: P2P, - pub(crate) handle: P2PHandle, + pub(crate) gossip_handle: GossipHandle, + pub(crate) req_resp_handle: ReqRespHandle, pub(crate) kp: SecpKeypair, } @@ -34,33 +42,56 @@ impl Operator { #[expect(clippy::too_many_arguments)] pub(crate) fn new( keypair: SecpKeypair, - allowlist: Vec, + #[allow(unused)] allowlist: Vec, connect_to: Vec, local_addr: Multiaddr, cancel: CancellationToken, - signers_allowlist: Vec, + #[allow(unused)] signers_allowlist: Vec, dial_timeout: Option, general_timeout: Option, connection_check_interval: Option, ) -> anyhow::Result { let config = P2PConfig { - keypair: keypair.clone(), + transport_keypair: keypair.clone().into(), idle_connection_timeout: Duration::from_secs(30), max_retries: Some(5), - listening_addr: local_addr, - allowlist, + listening_addrs: vec![local_addr], + //allowlist, connect_to, - signers_allowlist, + //signers_allowlist, dial_timeout, general_timeout, connection_check_interval, + protocol_name: None, + channel_timeout: None, + gossipsub_topic: None, + gossipsub_max_transmit_size: None, + gossipsub_score_params: None, + gossipsub_score_thresholds: None, + gossip_event_buffer_size: None, + commands_event_buffer_size: None, + command_buffer_size: None, + handle_default_timeout: None, + req_resp_event_buffer_size: None, + req_resp_command_buffer_size: None, + request_max_bytes: None, + response_max_bytes: None, + gossip_command_buffer_size: None, + envelope_max_age: None, + max_clock_skew: None, + kad_protocol_name: None, + kad_record_ttl: None, + kad_timer_putrecorderror: None, + conn_limits: Default::default(), }; let swarm = swarm::with_inmemory_transport(&config)?; - let (p2p, handle) = P2P::from_config(config, cancel, swarm, None)?; + let (p2p, req_resp_handle) = P2P::from_config(config, cancel, swarm, None, None)?; + let gossip_handle = p2p.new_gossip_handle(); Ok(Self { - handle, + gossip_handle, + req_resp_handle, p2p, kp: keypair, }) @@ -69,7 +100,8 @@ impl Operator { /// Auxiliary structure to control operators from outside. pub(crate) struct OperatorHandle { - pub(crate) handle: P2PHandle, + pub(crate) gossip_handle: GossipHandle, + pub(crate) req_resp_handle: ReqRespHandle, pub(crate) peer_id: PeerId, pub(crate) kp: SecpKeypair, } @@ -211,7 +243,8 @@ impl Setup { tasks.spawn(operator.p2p.listen()); levers.push(OperatorHandle { - handle: operator.handle, + gossip_handle: operator.gossip_handle, + req_resp_handle: operator.req_resp_handle, peer_id, kp: operator.kp, }); @@ -222,7 +255,10 @@ impl Setup { } } -pub(crate) fn mock_stake_chain_info(kp: &SecpKeypair, stake_chain_id: StakeChainId) -> Command { +pub(crate) fn mock_stake_chain_info( + kp: &SecpKeypair, + stake_chain_id: StakeChainId, +) -> PublishMessage { let kind = UnsignedPublishMessage::StakeChainExchange { stake_chain_id, // some random point @@ -230,10 +266,10 @@ pub(crate) fn mock_stake_chain_info(kp: &SecpKeypair, stake_chain_id: StakeChain pre_stake_txid: Txid::all_zeros(), pre_stake_vout: 0, }; - kind.sign_secp256k1(kp).into() + kind.sign_secp256k1(kp) } -pub(crate) fn mock_deposit_setup(kp: &SecpKeypair, scope: Scope) -> Command { +pub(crate) fn mock_deposit_setup(kp: &SecpKeypair, scope: Scope) -> PublishMessage { let mock_bytes = [0u8; 1_360 + 362_960]; let mock_index = 0; let unsigned = UnsignedPublishMessage::DepositSetup { @@ -245,23 +281,23 @@ pub(crate) fn mock_deposit_setup(kp: &SecpKeypair, scope: Scope) -> Command { operator_pk: XOnlyPublicKey::from_slice(&[2u8; 32]).unwrap(), wots_pks: WotsPublicKeys::from_flattened_bytes(&mock_bytes), }; - unsigned.sign_secp256k1(kp).into() + unsigned.sign_secp256k1(kp) } -pub(crate) fn mock_deposit_nonces(kp: &SecpKeypair, session_id: SessionId) -> Command { +pub(crate) fn mock_deposit_nonces(kp: &SecpKeypair, session_id: SessionId) -> PublishMessage { let unsigned = UnsignedPublishMessage::Musig2NoncesExchange { session_id, pub_nonces: (0..5).map(|_| generate_pubnonce()).collect(), }; - unsigned.sign_secp256k1(kp).into() + unsigned.sign_secp256k1(kp) } -pub(crate) fn mock_deposit_sigs(kp: &SecpKeypair, session_id: SessionId) -> Command { +pub(crate) fn mock_deposit_sigs(kp: &SecpKeypair, session_id: SessionId) -> PublishMessage { let unsigned = UnsignedPublishMessage::Musig2SignaturesExchange { session_id, partial_sigs: (0..5).map(|_| generate_partial_signature()).collect(), }; - unsigned.sign_secp256k1(kp).into() + unsigned.sign_secp256k1(kp) } pub(crate) async fn exchange_stake_chain_info( @@ -269,29 +305,29 @@ pub(crate) async fn exchange_stake_chain_info( operators_num: usize, stake_chain_id: StakeChainId, ) -> anyhow::Result<()> { - for operator in operators.iter() { - operator - .handle - .send_command(mock_stake_chain_info(&operator.kp, stake_chain_id)) - .await; + for operator in operators.iter_mut() { + let msg = mock_stake_chain_info(&operator.kp, stake_chain_id); + let data = GossipsubMsg::from(msg).into_raw().encode_to_vec(); + operator.gossip_handle.send(GossipCommand { data }).await?; } for operator in operators.iter_mut() { // received stake chain info from other n-1 operators for _ in 0..operators_num - 1 { - let event = operator.handle.next_event().await?; + let GossipEvent::ReceivedMessage(raw_msg) = operator.gossip_handle.next_event().await?; + let msg = GossipsubMsg::from_bytes(&raw_msg)?; if !matches!( - event, - Event::ReceivedMessage(GossipsubMsg { + msg, + GossipsubMsg { unsigned: UnsignedGossipsubMsg::StakeChainExchange { .. }, .. - }) + } ) { - bail!("Got event other than 'stake_chain_info' - {:?}", event); + bail!("Got event other than 'stake_chain_info' - {:?}", msg); } } - assert!(operator.handle.events_is_empty()); + assert!(operator.gossip_handle.events_is_empty()); } Ok(()) @@ -302,27 +338,28 @@ pub(crate) async fn exchange_deposit_setup( operators_num: usize, scope: Scope, ) -> anyhow::Result<()> { - for operator in operators.iter() { - operator - .handle - .send_command(mock_deposit_setup(&operator.kp, scope)) - .await; + for operator in operators.iter_mut() { + let msg = mock_deposit_setup(&operator.kp, scope); + let data = GossipsubMsg::from(msg).into_raw().encode_to_vec(); + operator.gossip_handle.send(GossipCommand { data }).await?; } for operator in operators.iter_mut() { for _ in 0..operators_num - 1 { - let event = operator.handle.next_event().await.unwrap(); + let GossipEvent::ReceivedMessage(raw_msg) = + operator.gossip_handle.next_event().await.unwrap(); + let msg = GossipsubMsg::from_bytes(&raw_msg).unwrap(); if !matches!( - event, - Event::ReceivedMessage(GossipsubMsg { + msg, + GossipsubMsg { unsigned: UnsignedGossipsubMsg::DepositSetup { .. }, .. - }) + } ) { - bail!("Got event other than 'deposit_setup' - {:?}", event); + bail!("Got event other than 'deposit_setup' - {:?}", msg); } info!(to=%operator.peer_id, "Got deposit setup"); } - assert!(operator.handle.events_is_empty()); + assert!(operator.gossip_handle.events_is_empty()); } Ok(()) } @@ -332,27 +369,28 @@ pub(crate) async fn exchange_deposit_nonces( operators_num: usize, session_id: SessionId, ) -> anyhow::Result<()> { - for operator in operators.iter() { - operator - .handle - .send_command(mock_deposit_nonces(&operator.kp, session_id)) - .await; + for operator in operators.iter_mut() { + let msg = mock_deposit_nonces(&operator.kp, session_id); + let data = GossipsubMsg::from(msg).into_raw().encode_to_vec(); + operator.gossip_handle.send(GossipCommand { data }).await?; } for operator in operators.iter_mut() { for _ in 0..operators_num - 1 { - let event = operator.handle.next_event().await.unwrap(); + let GossipEvent::ReceivedMessage(raw_msg) = + operator.gossip_handle.next_event().await.unwrap(); + let msg = GossipsubMsg::from_bytes(&raw_msg).unwrap(); if !matches!( - event, - Event::ReceivedMessage(GossipsubMsg { + msg, + GossipsubMsg { unsigned: UnsignedGossipsubMsg::Musig2NoncesExchange { .. }, .. - }) + } ) { - bail!("Got event other than 'deposit_nonces' - {:?}", event); + bail!("Got event other than 'deposit_nonces' - {:?}", msg); } info!(to=%operator.peer_id, "Got deposit setup"); } - assert!(operator.handle.events_is_empty()); + assert!(operator.gossip_handle.events_is_empty()); } Ok(()) } @@ -362,28 +400,29 @@ pub(crate) async fn exchange_deposit_sigs( operators_num: usize, session_id: SessionId, ) -> anyhow::Result<()> { - for operator in operators.iter() { - operator - .handle - .send_command(mock_deposit_sigs(&operator.kp, session_id)) - .await; + for operator in operators.iter_mut() { + let msg = mock_deposit_sigs(&operator.kp, session_id); + let data = GossipsubMsg::from(msg).into_raw().encode_to_vec(); + operator.gossip_handle.send(GossipCommand { data }).await?; } for operator in operators.iter_mut() { for _ in 0..operators_num - 1 { - let event = operator.handle.next_event().await.unwrap(); + let GossipEvent::ReceivedMessage(raw_msg) = + operator.gossip_handle.next_event().await.unwrap(); + let msg = GossipsubMsg::from_bytes(&raw_msg).unwrap(); if !matches!( - event, - Event::ReceivedMessage(GossipsubMsg { + msg, + GossipsubMsg { unsigned: UnsignedGossipsubMsg::Musig2SignaturesExchange { .. }, .. - }) + } ) { - bail!("Got event other than 'deposit_sigs' - {:?}", event); + bail!("Got event other than 'deposit_sigs' - {:?}", msg); } info!(to=%operator.peer_id, "Got deposit sigs"); } - assert!(operator.handle.events_is_empty()); + assert!(operator.gossip_handle.events_is_empty()); } Ok(()) diff --git a/crates/p2p-service/src/tests/gossipsub.rs b/crates/p2p-service/src/tests/gossipsub.rs index 3f542e25c..092ee33e9 100644 --- a/crates/p2p-service/src/tests/gossipsub.rs +++ b/crates/p2p-service/src/tests/gossipsub.rs @@ -1,5 +1,5 @@ +use p2p_types::{Scope, SessionId, StakeChainId}; use strata_bridge_common::logging::{self, LoggerConfig}; -use strata_p2p_types::{Scope, SessionId, StakeChainId}; use super::common::{ exchange_deposit_nonces, exchange_deposit_setup, exchange_deposit_sigs, diff --git a/crates/p2p-service/src/tests/request.rs b/crates/p2p-service/src/tests/request.rs index 7e3bb528c..5908038d2 100644 --- a/crates/p2p-service/src/tests/request.rs +++ b/crates/p2p-service/src/tests/request.rs @@ -1,10 +1,15 @@ //! GetMessage tests. use anyhow::bail; +use futures::SinkExt; +use p2p_types::{P2POperatorPubKey, Scope, SessionId, StakeChainId}; +use p2p_wire::p2p::v1::{GetMessageRequest, GossipsubMsg, UnsignedGossipsubMsg}; +use prost::Message; use strata_bridge_common::logging::{self, LoggerConfig}; -use strata_p2p::{commands::Command, events::Event}; -use strata_p2p_types::{P2POperatorPubKey, Scope, SessionId, StakeChainId}; -use strata_p2p_wire::p2p::v1::{GetMessageRequest, UnsignedGossipsubMsg}; +use strata_p2p::{ + commands::{GossipCommand, RequestResponseCommand}, + events::{GossipEvent, ReqRespEvent}, +}; use tracing::info; use super::common::{ @@ -60,193 +65,240 @@ async fn request_response() -> anyhow::Result<()> { // create command to request info from the first operator let operator_pk: P2POperatorPubKey = operators[0].kp.public().clone().into(); - let command_stake_chain = Command::RequestMessage(GetMessageRequest::StakeChainExchange { + let command_stake_chain = GetMessageRequest::StakeChainExchange { stake_chain_id, operator_pk: operator_pk.clone(), - }); - let command_deposit_setup = Command::RequestMessage(GetMessageRequest::DepositSetup { + }; + let command_deposit_setup = GetMessageRequest::DepositSetup { scope, operator_pk: operator_pk.clone(), - }); - let command_deposit_nonces = Command::RequestMessage(GetMessageRequest::Musig2NoncesExchange { + }; + let command_deposit_nonces = GetMessageRequest::Musig2NoncesExchange { + session_id, + operator_pk: operator_pk.clone(), + }; + let command_deposit_sigs = GetMessageRequest::Musig2SignaturesExchange { session_id, operator_pk: operator_pk.clone(), - }); - let command_deposit_sigs = - Command::RequestMessage(GetMessageRequest::Musig2SignaturesExchange { - session_id, - operator_pk: operator_pk.clone(), - }); + }; // Send stake chain request and handle response from the last operator operators[OPERATORS_NUM - 1] - .handle - .send_command(command_stake_chain) - .await; + .req_resp_handle + .send(RequestResponseCommand { + target_transport_id: command_stake_chain.peer_id(), + data: command_stake_chain.into_msg().encode_to_vec(), + }) + .await?; // Wait for request on the first operator - let event = operators[0].handle.next_event().await?; + let event = operators[0] + .req_resp_handle + .next_event() + .await + .ok_or_else(|| anyhow::anyhow!("no req_resp_handle event"))?; match event { - Event::ReceivedRequest(request) => match request { - GetMessageRequest::StakeChainExchange { - stake_chain_id: req_stake_chain_id, - operator_pk: req_operator_pk, - } if req_stake_chain_id == stake_chain_id && req_operator_pk == operator_pk => { - // Construct and send response - let mock_msg = mock_stake_chain_info(&operators[0].kp.clone(), stake_chain_id); - if let Command::PublishMessage(msg) = mock_msg { + ReqRespEvent::ReceivedRequest(raw_request, _) => { + let request = prost::Message::decode(raw_request.as_slice()) + .and_then(GetMessageRequest::from_msg)?; + match request { + GetMessageRequest::StakeChainExchange { + stake_chain_id: req_stake_chain_id, + operator_pk: req_operator_pk, + } if req_stake_chain_id == stake_chain_id && req_operator_pk == operator_pk => { + // Construct and send response + let mock_msg = mock_stake_chain_info(&operators[0].kp.clone(), stake_chain_id); + let data = GossipsubMsg::from(mock_msg).into_raw().encode_to_vec(); operators[0] - .handle - .send_command(Command::PublishMessage(msg)) - .await; + .gossip_handle + .send(GossipCommand { data }) + .await?; } + _ => bail!("Got unexpected request in the first operator"), } - _ => bail!("Got unexpected request in the first operator"), - }, + } _ => bail!("Got unexpected event in the first operator"), } // Wait for response on the last operator - let event = operators[OPERATORS_NUM - 1].handle.next_event().await?; + let event = operators[OPERATORS_NUM - 1] + .gossip_handle + .next_event() + .await?; match event { - Event::ReceivedMessage(msg) => match &msg.unsigned { - UnsignedGossipsubMsg::StakeChainExchange { - stake_chain_id: received_id, - .. - } if msg.key == operator_pk && *received_id == stake_chain_id => { - info!("Got stake chain info from the last operator") + GossipEvent::ReceivedMessage(raw_msg) => { + let msg = GossipsubMsg::from_bytes(&raw_msg)?; + match &msg.unsigned { + UnsignedGossipsubMsg::StakeChainExchange { + stake_chain_id: received_id, + .. + } if msg.key == operator_pk && *received_id == stake_chain_id => { + info!("Got stake chain info from the last operator") + } + _ => bail!("Got event other than expected 'stake_chain_info' in the last operator"), } - _ => bail!("Got event other than expected 'stake_chain_info' in the last operator"), - }, - _ => bail!("Got event other than expected 'stake_chain_info' in the last operator"), + } } // Send deposit setup request and handle response from the last operator operators[OPERATORS_NUM - 1] - .handle - .send_command(command_deposit_setup) - .await; + .req_resp_handle + .send(RequestResponseCommand { + target_transport_id: command_deposit_setup.peer_id(), + data: command_deposit_setup.into_msg().encode_to_vec(), + }) + .await?; // Wait for request on the first operator - let event = operators[0].handle.next_event().await?; + let event = operators[0] + .req_resp_handle + .next_event() + .await + .ok_or_else(|| anyhow::anyhow!("no req_resp_handle event"))?; match event { - Event::ReceivedRequest(request) => match request { - GetMessageRequest::DepositSetup { - scope: req_scope, - operator_pk: req_operator_pk, - } if req_scope == scope && req_operator_pk == operator_pk => { - // Construct and send response - let mock_msg = mock_deposit_setup(&operators[0].kp.clone(), scope); - if let Command::PublishMessage(msg) = mock_msg { + ReqRespEvent::ReceivedRequest(raw_request, _) => { + let request = prost::Message::decode(raw_request.as_slice()) + .and_then(GetMessageRequest::from_msg)?; + match request { + GetMessageRequest::DepositSetup { + scope: req_scope, + operator_pk: req_operator_pk, + } if req_scope == scope && req_operator_pk == operator_pk => { + // Construct and send response + let mock_msg = mock_deposit_setup(&operators[0].kp.clone(), scope); + let data = GossipsubMsg::from(mock_msg).into_raw().encode_to_vec(); operators[0] - .handle - .send_command(Command::PublishMessage(msg)) - .await; + .gossip_handle + .send(GossipCommand { data }) + .await?; } + _ => bail!("Got unexpected request in the first operator"), } - _ => bail!("Got unexpected request in the first operator"), - }, + } _ => bail!("Got unexpected event in the first operator"), } // Wait for response on the last operator - let event = operators[OPERATORS_NUM - 1].handle.next_event().await?; - match event { - Event::ReceivedMessage(msg) => match &msg.unsigned { - UnsignedGossipsubMsg::DepositSetup { - scope: received_scope, - .. - } if msg.key == operator_pk && *received_scope == scope => { - info!("Got deposit setup info from the last operator") - } - _ => bail!("Got event other than expected 'deposit_setup' in the last operator"), - }, + let GossipEvent::ReceivedMessage(raw_msg) = operators[OPERATORS_NUM - 1] + .gossip_handle + .next_event() + .await?; + let msg = GossipsubMsg::from_bytes(raw_msg.as_slice())?; + match &msg.unsigned { + UnsignedGossipsubMsg::DepositSetup { + scope: received_scope, + .. + } if msg.key == operator_pk && *received_scope == scope => { + info!("Got deposit setup info from the last operator") + } _ => bail!("Got event other than expected 'deposit_setup' in the last operator"), } // Send deposit nonces request and handle response from the last operator operators[OPERATORS_NUM - 1] - .handle - .send_command(command_deposit_nonces) - .await; + .req_resp_handle + .send(RequestResponseCommand { + target_transport_id: command_deposit_nonces.peer_id(), + data: command_deposit_nonces.into_msg().encode_to_vec(), + }) + .await?; // Wait for request on the first operator - let event = operators[0].handle.next_event().await?; + let event = operators[0] + .req_resp_handle + .next_event() + .await + .ok_or_else(|| anyhow::anyhow!("no req_resp_handle event"))?; match event { - Event::ReceivedRequest(request) => match request { - GetMessageRequest::Musig2NoncesExchange { - session_id: req_session_id, - operator_pk: req_operator_pk, - } if req_session_id == session_id && req_operator_pk == operator_pk => { - // Construct and send response - let mock_msg = mock_deposit_nonces(&operators[0].kp.clone(), session_id); - if let Command::PublishMessage(msg) = mock_msg { + ReqRespEvent::ReceivedRequest(raw_request, _) => { + let request = prost::Message::decode(raw_request.as_slice()) + .and_then(GetMessageRequest::from_msg)?; + match request { + GetMessageRequest::Musig2NoncesExchange { + session_id: req_session_id, + operator_pk: req_operator_pk, + } if req_session_id == session_id && req_operator_pk == operator_pk => { + // Construct and send response + let mock_msg = mock_deposit_nonces(&operators[0].kp.clone(), session_id); + let data = GossipsubMsg::from(mock_msg).into_raw().encode_to_vec(); operators[0] - .handle - .send_command(Command::PublishMessage(msg)) - .await; + .gossip_handle + .send(GossipCommand { data }) + .await?; } + _ => bail!("Got unexpected request in the first operator"), } - _ => bail!("Got unexpected request in the first operator"), - }, + } _ => bail!("Got unexpected event in the first operator"), } // Wait for response on the last operator - let event = operators[OPERATORS_NUM - 1].handle.next_event().await?; - match event { - Event::ReceivedMessage(msg) => match &msg.unsigned { - UnsignedGossipsubMsg::Musig2NoncesExchange { - session_id: received_session_id, - .. - } if msg.key == operator_pk && *received_session_id == session_id => { - info!("Got deposit pubnonces from the last operator") - } - _ => bail!("Got event other than expected 'deposit_pubnonces' in the last operator"), - }, + let GossipEvent::ReceivedMessage(raw_msg) = operators[OPERATORS_NUM - 1] + .gossip_handle + .next_event() + .await?; + let msg = GossipsubMsg::from_bytes(&raw_msg)?; + match &msg.unsigned { + UnsignedGossipsubMsg::Musig2NoncesExchange { + session_id: received_session_id, + .. + } if msg.key == operator_pk && *received_session_id == session_id => { + info!("Got deposit pubnonces from the last operator") + } _ => bail!("Got event other than expected 'deposit_pubnonces' in the last operator"), } // Send deposit signatures request and handle response from the last operator operators[OPERATORS_NUM - 1] - .handle - .send_command(command_deposit_sigs) - .await; + .req_resp_handle + .send(RequestResponseCommand { + target_transport_id: command_deposit_sigs.peer_id(), + data: command_deposit_sigs.into_msg().encode_to_vec(), + }) + .await?; // Wait for request on the first operator - let event = operators[0].handle.next_event().await?; + let event = operators[0] + .req_resp_handle + .next_event() + .await + .ok_or_else(|| anyhow::anyhow!("no req_resp_handle event"))?; match event { - Event::ReceivedRequest(request) => match request { - GetMessageRequest::Musig2SignaturesExchange { - session_id: req_session_id, - operator_pk: req_operator_pk, - } if req_session_id == session_id && req_operator_pk == operator_pk => { - // Construct and send response - let mock_msg = mock_deposit_sigs(&operators[0].kp.clone(), session_id); - if let Command::PublishMessage(msg) = mock_msg { + ReqRespEvent::ReceivedRequest(raw_request, _) => { + let request = prost::Message::decode(raw_request.as_slice()) + .and_then(GetMessageRequest::from_msg)?; + match request { + GetMessageRequest::Musig2SignaturesExchange { + session_id: req_session_id, + operator_pk: req_operator_pk, + } if req_session_id == session_id && req_operator_pk == operator_pk => { + // Construct and send response + let mock_msg = mock_deposit_sigs(&operators[0].kp.clone(), session_id); + let data = GossipsubMsg::from(mock_msg).into_raw().encode_to_vec(); operators[0] - .handle - .send_command(Command::PublishMessage(msg)) - .await; + .gossip_handle + .send(GossipCommand { data }) + .await?; } + _ => bail!("Got unexpected request in the first operator"), } - _ => bail!("Got unexpected request in the first operator"), - }, + } _ => bail!("Got unexpected event in the first operator"), } // Wait for response on the last operator - let event = operators[OPERATORS_NUM - 1].handle.next_event().await?; - match event { - Event::ReceivedMessage(msg) => match &msg.unsigned { - UnsignedGossipsubMsg::Musig2SignaturesExchange { - session_id: received_session_id, - .. - } if msg.key == operator_pk && *received_session_id == session_id => { - info!("Got deposit partial signatures from the last operator") - } - _ => bail!("Got event other than expected 'deposit_partial_sigs' in the last operator"), - }, + let GossipEvent::ReceivedMessage(raw_msg) = operators[OPERATORS_NUM - 1] + .gossip_handle + .next_event() + .await?; + let msg = GossipsubMsg::from_bytes(&raw_msg)?; + match &msg.unsigned { + UnsignedGossipsubMsg::Musig2SignaturesExchange { + session_id: received_session_id, + .. + } if msg.key == operator_pk && *received_session_id == session_id => { + info!("Got deposit partial signatures from the last operator") + } _ => bail!("Got event other than expected 'deposit_partial_sigs' in the last operator"), } diff --git a/crates/p2p-types/Cargo.toml b/crates/p2p-types/Cargo.toml new file mode 100644 index 000000000..6f3784df3 --- /dev/null +++ b/crates/p2p-types/Cargo.toml @@ -0,0 +1,28 @@ +[package] +edition = "2021" +name = "p2p-types" +version = "0.1.0" + +[features] +default = [] +proptest = ["dep:proptest", "dep:proptest-derive"] + +[lints] +rust.missing_debug_implementations = "warn" +rust.unreachable_pub = "warn" +rust.unused_crate_dependencies = "deny" +rust.unused_must_use = "deny" +rust.rust_2018_idioms = { level = "deny", priority = -1 } + +[dependencies] +bitcoin.workspace = true +hex = { workspace = true, features = ["serde"] } +libp2p-identity = { workspace = true, features = ["secp256k1"] } +proptest = { workspace = true, optional = true } +proptest-derive = { workspace = true, optional = true } +serde.workspace = true + +[dev-dependencies] +bincode.workspace = true +secp256k1 = { workspace = true, features = ["rand"] } +serde_json.workspace = true diff --git a/crates/p2p-types/src/deposit_data.rs b/crates/p2p-types/src/deposit_data.rs new file mode 100644 index 000000000..c04ae97b7 --- /dev/null +++ b/crates/p2p-types/src/deposit_data.rs @@ -0,0 +1,560 @@ +//! Data necessary to process a deposit. +//! +//! Primarily Winternitz One-Time Signature (WOTS). + +use std::fmt; + +#[cfg(feature = "proptest")] +use proptest_derive::Arbitrary; +use serde::{Deserialize, Serialize}; + +use crate::{wots::Wots128PublicKey, Wots256PublicKey, WOTS_SINGLE}; + +/// Winternitz One-Time Signature (WOTS) public keys shared in a deposit. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[cfg_attr(feature = "proptest", derive(Arbitrary))] +pub struct WotsPublicKeys { + /// WOTS public key used for the Withdrawal Fulfillment transaction. + pub withdrawal_fulfillment: Wots256PublicKey, + + /// WOTS public keys used for the Assert transaction in the Groth16 chunked proof. + pub groth16: Groth16PublicKeys, +} + +impl WotsPublicKeys { + /// Creates a new [`WotsPublicKeys`] instance. + /// + /// # Examples + /// + /// ```compile_fail + /// # this test fails to compile due to cyclic dependency issues in the latest nightly + /// # use strata_p2p_types::{WotsPublicKeys, Wots256PublicKey, Wots128PublicKey, Groth16PublicKeys}; + /// let withdrawal_key = Wots256PublicKey::new([[1u8; 20]; 68]); + /// + /// // Create a WotsPublicKeys with empty Groth16 parts + /// let empty_groth16_keys = WotsPublicKeys::new( + /// withdrawal_key.clone(), + /// vec![], + /// vec![], + /// vec![] + /// ); + /// + /// // Create a WotsPublicKeys with some Groth16 parts + /// let public_inputs = Wots256PublicKey::new([[2u8; 20]; 68]); + /// let field_elements = Wots256PublicKey::new([[3u8; 20]; 68]); + /// let hashes = Wots128PublicKey::new([[4u8; 20]; 36]); + /// + /// let wots_keys = WotsPublicKeys::new( + /// withdrawal_key, + /// vec![public_inputs], + /// vec![field_elements], + /// vec![hashes], + /// ); + /// ``` + pub fn new( + withdrawal_fulfillment: Wots256PublicKey, + public_inputs: Vec, + fqs: Vec, + hashes: Vec, + ) -> Self { + let groth16 = Groth16PublicKeys::new(public_inputs, fqs, hashes); + + // Verify the Groth16 public keys are not too large + assert!( + groth16.len() <= u16::MAX as usize, + "Groth16 public keys are too large, max is {}", + u16::MAX + ); + Self { + withdrawal_fulfillment, + groth16, + } + } + + /// Creates a [`WotsPublicKeys`] from a flattened byte array. + /// + /// # Format + /// + /// The flattened byte array is structured as follows: + /// + /// - The first 3 bytes of the Groth16PublicKeys (containing counts) + /// - The next `WOTS_SINGLE * Wots256PublicKey::SIZE` bytes represent the withdrawal_fulfillment + /// key + /// - The remaining bytes represent the flattened Groth16PublicKeys as described in + /// [`Groth16PublicKeys::to_flattened_bytes`] + pub fn from_flattened_bytes(bytes: &[u8]) -> Self { + // The withdrawal fulfillment key size in flattened form + let withdrawal_key_size = WOTS_SINGLE * Wots256PublicKey::SIZE; + + // Parse the withdrawal fulfillment key + let withdrawal_fulfillment = + Wots256PublicKey::from_flattened_bytes(&bytes[0..withdrawal_key_size]); + + // Parse the Groth16 public keys from the remaining bytes + let groth16 = Groth16PublicKeys::from_flattened_bytes(&bytes[withdrawal_key_size..]); + + // Verify the Groth16 public keys are not too large + assert!( + groth16.len() <= u16::MAX as usize, + "Groth16 public keys are too large, max is {}", + u16::MAX + ); + + Self { + withdrawal_fulfillment, + groth16, + } + } + + /// Converts [`WotsPublicKeys`] to a flattened byte array. + /// + /// # Format + /// + /// The flattened byte array is structured as follows: + /// + /// - The first `WOTS_SINGLE * Wots256PublicKey::SIZE` bytes represent the + /// withdrawal_fulfillment key + /// - The remaining bytes represent the flattened Groth16PublicKeys as described in + /// [`Groth16PublicKeys::to_flattened_bytes`] + pub fn to_flattened_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + + // Add withdrawal fulfillment key bytes + bytes.extend(self.withdrawal_fulfillment.to_flattened_bytes()); + + // Add Groth16 public keys bytes + bytes.extend(self.groth16.to_flattened_bytes()); + + bytes + } +} + +/// Winternitz One-Time Signature (WOTS) public keys used for the Assert transaction +/// in the Groth16 proof. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[cfg_attr(feature = "proptest", derive(Arbitrary))] +pub struct Groth16PublicKeys { + /// Number of public inputs. + pub n_public_inputs: u16, + + /// Public inputs used when passing state in chunked Groth16 proofs. + pub public_inputs: Vec, + + /// Number of field elements. + pub n_field_elements: u16, + + /// Field Elements used when passing state in chunked Groth16 proofs. + pub fqs: Vec, + + /// Number of hashes. + pub n_hashes: u16, + + /// Hashes used when passing state in chunked Groth16 proofs. + pub hashes: Vec, +} + +impl Groth16PublicKeys { + /// Creates a new [`Groth16PublicKeys`] instance. + /// + /// Note that you can create [`Groth16PublicKeys`] that contains no public inputs, field + /// elements, or hashes. For example: + /// + /// ```compile_fail + /// # this test fails to compile due to cyclic dependency issues in the latest nightly + /// # use strata_p2p_types::{Groth16PublicKeys, Wots256PublicKey, Wots128PublicKey}; + /// let empty_wots = Groth16PublicKeys::new(vec![], vec![], vec![]); + /// # assert!(empty_wots.is_empty()); + /// + /// let public_inputs = Wots256PublicKey::new([[1u8; 20]; 68]); + /// let just_public_inputs = Groth16PublicKeys::new(vec![public_inputs], vec![], vec![]); + /// + /// let field_elements = Wots256PublicKey::new([[2u8; 20]; 68]); + /// let just_field_elements = Groth16PublicKeys::new(vec![], vec![field_elements], vec![]); + /// + /// let hashes = Wots128PublicKey::new([[3u8; 20]; 36]); + /// let just_hashes = Groth16PublicKeys::new(vec![], vec![], vec![hashes]); + /// ``` + pub fn new( + public_inputs: Vec, + fqs: Vec, + hashes: Vec, + ) -> Self { + // Verify the Groth16 public keys are not too large + assert!( + public_inputs.len() <= u16::MAX as usize, + "Public inputs are too large, max is {}", + u16::MAX + ); + assert!( + fqs.len() <= u16::MAX as usize, + "Field elements are too large, max is {}", + u16::MAX + ); + assert!( + hashes.len() <= u16::MAX as usize, + "Hashes are too large, max is {}", + u16::MAX + ); + + Self { + n_public_inputs: public_inputs.len() as u16, + public_inputs, + n_field_elements: fqs.len() as u16, + fqs, + n_hashes: hashes.len() as u16, + hashes, + } + } + + /// Length of [`Groth16PublicKeys`]. + pub fn len(&self) -> usize { + (self.n_public_inputs + self.n_field_elements + self.n_hashes) as usize + } + + /// If the collection is empty. + pub fn is_empty(&self) -> bool { + self.n_public_inputs == 0 && self.n_field_elements == 0 && self.n_hashes == 0 + } + + /// Converts [`Groth16PublicKeys`] to a flattened byte array. + /// + /// # Format + /// + /// The flattened byte array is structured as follows: + /// + /// - The first byte represents the number of public inputs. + /// - The second byte represents the number of field elements. + /// - The third byte represents the number of hashes. + /// - The next `self.n_public_inputs * 256 * 20` bytes represent the flattened bytes of each + /// public input. + /// - The next `self.n_field_elements * 256 * 20` bytes represent the flattened bytes of each + /// field element. + /// - The next `self.n_hashes * 128 * 20` bytes represent the flattened bytes of each hash. + pub fn to_flattened_bytes(&self) -> Vec { + // space for number of public_inputs, field_elements, and hashes as well as the length of + // each + let mut bytes = vec![]; + + // Copy the number of public inputs, field elements, and hashes + bytes.extend_from_slice(&self.n_public_inputs.to_le_bytes()); + bytes.extend_from_slice(&self.n_field_elements.to_le_bytes()); + bytes.extend_from_slice(&self.n_hashes.to_le_bytes()); + + // Copy public_inputs bytes + for public_input in &self.public_inputs { + let flattened = public_input.to_flattened_bytes(); + bytes.extend(flattened); + } + + // Copy fqs bytes + for fq in &self.fqs { + let flattened = fq.to_flattened_bytes(); + bytes.extend(flattened); + } + + // Copy hashes bytes + for hash in &self.hashes { + let flattened = hash.to_flattened_bytes(); + bytes.extend(flattened); + } + + bytes + } + + /// Creates the [`Groth16PublicKeys`] from a flattened byte array. + /// + /// If you already have structured arrays then you should use [`WotsPublicKeys::new`]. + /// + /// # Format + /// + /// The flattened byte array is structured as follows: + /// + /// - The first byte represents the number of public inputs. + /// - The second byte represents the number of field elements. + /// - The third byte represents the number of hashes. + /// - The next `self.n_public_inputs * 256 * 20` bytes represent the flattened bytes of each + /// public input. + /// - The next `self.n_field_elements * 256 * 20` bytes represent the flattened bytes of each + /// field element. + /// - The next `self.n_hashes * 128 * 20` bytes represent the flattened bytes of each hash. + pub fn from_flattened_bytes(bytes: &[u8]) -> Self { + let mut offset = 0; + + // Read lengths + let n_public_inputs = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]); + offset += 2; + let n_field_elements = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]); + offset += 2; + let n_hashes = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]); + offset += 2; + + let mut public_inputs = Vec::with_capacity(n_public_inputs as usize); + let mut fqs = Vec::with_capacity(n_field_elements as usize); + let mut hashes = Vec::with_capacity(n_hashes as usize); + + // Read public_inputs + for _ in 0..n_public_inputs { + let slice = &bytes[offset..offset + WOTS_SINGLE * Wots256PublicKey::SIZE]; + public_inputs.push(Wots256PublicKey::from_flattened_bytes(slice)); + offset += WOTS_SINGLE * Wots256PublicKey::SIZE; + } + + // Read field elements + for _ in 0..n_field_elements { + let slice = &bytes[offset..offset + WOTS_SINGLE * Wots256PublicKey::SIZE]; + fqs.push(Wots256PublicKey::from_flattened_bytes(slice)); + offset += WOTS_SINGLE * Wots256PublicKey::SIZE; + } + + // Read hashes + for _ in 0..n_hashes { + let slice = &bytes[offset..offset + WOTS_SINGLE * Wots128PublicKey::SIZE]; + hashes.push(Wots128PublicKey::from_flattened_bytes(slice)); + offset += WOTS_SINGLE * Wots128PublicKey::SIZE; + } + + Self { + n_public_inputs, + public_inputs, + n_field_elements, + fqs, + n_hashes, + hashes, + } + } +} + +impl fmt::Debug for Groth16PublicKeys { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let public_inputs_len = self.public_inputs.len(); + let field_elements_len = self.fqs.len(); + let hashes_len = self.hashes.len(); + let first_public_input = &self.public_inputs[0]; + let first_field_element = &self.fqs[0]; + let first_hash = &self.hashes[0]; + + write!( + f, + "Groth16PublicKeys(#Public Inputs: {public_inputs_len}, #Field Elements: {field_elements_len}, #Hashes: {hashes_len}, First Public Input: {first_public_input:?}, First Field Element: {first_field_element:?}, First Hash: {first_hash:?})" + ) + } +} + +impl fmt::Display for Groth16PublicKeys { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let public_inputs_len = self.public_inputs.len(); + let field_elements_len = self.fqs.len(); + let hashes_len = self.hashes.len(); + + write!( + f, + "Groth16PublicKeys(#Public Inputs: {public_inputs_len}, #Field Elements: {field_elements_len}, #Hashes: {hashes_len})" + ) + } +} + +#[cfg(test)] +mod tests { + #[cfg(feature = "proptest")] + use proptest::prelude::*; + + use super::*; + use crate::wots::wots_total_digits; + + const N_PUBLIC_INPUTS: usize = 1; + const N_FIELD_ELEMENTS: usize = 14; + const N_HASHES: usize = 363; + + fn big_groth16_public_keys() -> Groth16PublicKeys { + Groth16PublicKeys::new( + vec![Wots256PublicKey::from_flattened_bytes(&[2u8; 68 * 20]); N_PUBLIC_INPUTS], + vec![Wots256PublicKey::from_flattened_bytes(&[3u8; 68 * 20]); N_FIELD_ELEMENTS], + vec![Wots128PublicKey::from_flattened_bytes(&[4u8; 36 * 20]); N_HASHES], + ) + } + + #[test] + fn big_groth16_public_keys_len() { + let keys = big_groth16_public_keys(); + assert_eq!(keys.len(), N_PUBLIC_INPUTS + N_FIELD_ELEMENTS + N_HASHES); + } + + #[test] + #[should_panic(expected = "Hashes are too large, max is 65535")] + fn big_groth16_public_keys_len_panic() { + let _ = Groth16PublicKeys::new( + vec![Wots256PublicKey::from_flattened_bytes(&[2u8; 68 * 20]); N_PUBLIC_INPUTS], + vec![Wots256PublicKey::from_flattened_bytes(&[3u8; 68 * 20]); N_FIELD_ELEMENTS], + vec![Wots128PublicKey::from_flattened_bytes(&[4u8; 36 * 20]); u16::MAX as usize + 1], + ); + } + + #[test] + fn groth16_wots_flattened_bytes_roundtrip() { + // Create test data with known values + let test_data = Groth16PublicKeys::new( + vec![Wots256PublicKey::new([[1u8; WOTS_SINGLE]; wots_total_digits(32)]); 2], /* 2 * 32 + 4 = 68 */ + vec![Wots256PublicKey::new([[2u8; WOTS_SINGLE]; wots_total_digits(32)]); 3], /* 2 * 32 + 4 = 68 */ + vec![Wots128PublicKey::new([[3u8; WOTS_SINGLE]; wots_total_digits(16)]); 4], /* 2 * 16 + 4 = 36 */ + ); + + // Convert to flattened bytes + let flattened = test_data.to_flattened_bytes(); + + // Verify the length matches what we expect + let expected_len = 6 + // 6 bytes for counts (2 bytes each for u16) + (2 * WOTS_SINGLE * Wots256PublicKey::SIZE) + // public inputs + (3 * WOTS_SINGLE * Wots256PublicKey::SIZE) + // field elements + (4 * WOTS_SINGLE * Wots128PublicKey::SIZE); // hashes + assert_eq!(flattened.len(), expected_len); + + // Verify the counts are correct + assert_eq!(flattened[0], 2); // n_public_inputs + assert_eq!(flattened[1], 0); // n_public_inputs (u16) + assert_eq!(flattened[2], 3); // n_field_elements + assert_eq!(flattened[3], 0); // n_field_elements (u16) + assert_eq!(flattened[4], 4); // n_hashes + assert_eq!(flattened[5], 0); // n_hashes (u16) + + // Convert back from flattened bytes + let reconstructed = Groth16PublicKeys::from_flattened_bytes(&flattened); + + // Verify all fields match + assert_eq!(test_data.n_public_inputs, reconstructed.n_public_inputs); + assert_eq!(test_data.n_field_elements, reconstructed.n_field_elements); + assert_eq!(test_data.n_hashes, reconstructed.n_hashes); + assert_eq!(test_data.public_inputs, reconstructed.public_inputs); + assert_eq!(test_data.fqs, reconstructed.fqs); + assert_eq!(test_data.hashes, reconstructed.hashes); + } + + #[test] + fn wots_public_keys_flattened_bytes_roundtrip() { + // Create withdrawal fulfillment key + let withdrawal_fulfillment = + Wots256PublicKey::new([[4u8; WOTS_SINGLE]; wots_total_digits(32)]); + + // Create Groth16 public keys + let groth16 = Groth16PublicKeys::new( + vec![Wots256PublicKey::new([[1u8; WOTS_SINGLE]; wots_total_digits(32)]); 2], + vec![Wots256PublicKey::new([[2u8; WOTS_SINGLE]; wots_total_digits(32)]); 3], + vec![Wots128PublicKey::new([[3u8; WOTS_SINGLE]; wots_total_digits(16)]); 4], + ); + + // Create the WotsPublicKeys + let wots_keys = WotsPublicKeys::new( + withdrawal_fulfillment, + groth16.public_inputs.clone(), + groth16.fqs.clone(), + groth16.hashes.clone(), + ); + + // Convert to flattened bytes + let flattened = wots_keys.to_flattened_bytes(); + + // Verify the length matches what we expect + let expected_len = (WOTS_SINGLE * Wots256PublicKey::SIZE) + // withdrawal_fulfillment + 6 + // 6 bytes for counts (2 bytes each for u16) + (2 * WOTS_SINGLE * Wots256PublicKey::SIZE) + // public inputs + (3 * WOTS_SINGLE * Wots256PublicKey::SIZE) + // field elements + (4 * WOTS_SINGLE * Wots128PublicKey::SIZE); // hashes + assert_eq!(flattened.len(), expected_len); + + // Check that the first part is the withdrawal fulfillment key + let withdrawal_bytes = withdrawal_fulfillment.to_flattened_bytes(); + assert_eq!(&flattened[0..withdrawal_bytes.len()], &withdrawal_bytes[..]); + + // Convert back from flattened bytes + let reconstructed = WotsPublicKeys::from_flattened_bytes(&flattened); + + // Verify all fields match + assert_eq!( + wots_keys.withdrawal_fulfillment, + reconstructed.withdrawal_fulfillment + ); + assert_eq!( + wots_keys.groth16.n_public_inputs, + reconstructed.groth16.n_public_inputs + ); + assert_eq!( + wots_keys.groth16.n_field_elements, + reconstructed.groth16.n_field_elements + ); + assert_eq!(wots_keys.groth16.n_hashes, reconstructed.groth16.n_hashes); + assert_eq!( + wots_keys.groth16.public_inputs, + reconstructed.groth16.public_inputs + ); + assert_eq!(wots_keys.groth16.fqs, reconstructed.groth16.fqs); + assert_eq!(wots_keys.groth16.hashes, reconstructed.groth16.hashes); + + // Full equality check + assert_eq!(wots_keys, reconstructed); + } + + #[cfg(feature = "proptest")] + proptest! { + #[test] + fn proptest_groth16_wots_flattened_bytes_roundtrip( + n_inputs in 0u8..5u8, + n_fqs in 0u8..5u8, + n_hashes in 0u8..5u8, + value in 0u8..255u8 + ) { + let test_data = Groth16PublicKeys::new( + vec![Wots256PublicKey::new([[value; WOTS_SINGLE]; Wots256PublicKey::SIZE]); n_inputs as usize], + vec![Wots256PublicKey::new([[value; WOTS_SINGLE]; Wots256PublicKey::SIZE]); n_fqs as usize], + vec![Wots128PublicKey::new([[value; WOTS_SINGLE]; Wots128PublicKey::SIZE]); n_hashes as usize], + ); + + let flattened = test_data.to_flattened_bytes(); + let reconstructed = Groth16PublicKeys::from_flattened_bytes(&flattened); + + prop_assert_eq!(test_data.n_public_inputs, reconstructed.n_public_inputs); + prop_assert_eq!(test_data.n_field_elements, reconstructed.n_field_elements); + prop_assert_eq!(test_data.n_hashes, reconstructed.n_hashes); + prop_assert_eq!(test_data.public_inputs, reconstructed.public_inputs); + prop_assert_eq!(test_data.fqs, reconstructed.fqs); + prop_assert_eq!(test_data.hashes, reconstructed.hashes); + } + + #[test] + fn proptest_wots_public_keys_flattened_bytes_roundtrip( + withdrawal_value in 0u8..255u8, + n_inputs in 0u8..3u8, + n_fqs in 0u8..3u8, + n_hashes in 0u8..3u8, + groth16_value in 0u8..255u8 + ) { + // Create test data with different values for withdrawal and groth16 + let withdrawal_fulfillment = Wots256PublicKey::new( + [[withdrawal_value; WOTS_SINGLE]; Wots256PublicKey::SIZE] + ); + + let groth16 = Groth16PublicKeys::new( + vec![Wots256PublicKey::new([[groth16_value; WOTS_SINGLE]; Wots256PublicKey::SIZE]); n_inputs as usize], + vec![Wots256PublicKey::new([[groth16_value; WOTS_SINGLE]; Wots256PublicKey::SIZE]); n_fqs as usize], + vec![Wots128PublicKey::new([[groth16_value; WOTS_SINGLE]; Wots128PublicKey::SIZE]); n_hashes as usize], + ); + + let wots_keys = WotsPublicKeys::new( + withdrawal_fulfillment, + groth16.public_inputs.clone(), + groth16.fqs.clone(), + groth16.hashes.clone(), + ); + + let flattened = wots_keys.to_flattened_bytes(); + let reconstructed = WotsPublicKeys::from_flattened_bytes(&flattened); + + prop_assert_eq!(wots_keys.withdrawal_fulfillment, reconstructed.withdrawal_fulfillment); + prop_assert_eq!(wots_keys.groth16.n_public_inputs, reconstructed.groth16.n_public_inputs); + prop_assert_eq!(wots_keys.groth16.n_field_elements, reconstructed.groth16.n_field_elements); + prop_assert_eq!(wots_keys.groth16.n_hashes, reconstructed.groth16.n_hashes); + prop_assert_eq!(wots_keys.groth16.public_inputs.clone(), reconstructed.groth16.public_inputs.clone()); + prop_assert_eq!(wots_keys.groth16.fqs.clone(), reconstructed.groth16.fqs.clone()); + prop_assert_eq!(wots_keys.groth16.hashes.clone(), reconstructed.groth16.hashes.clone()); + prop_assert_eq!(wots_keys, reconstructed); + } + } +} diff --git a/crates/p2p-types/src/lib.rs b/crates/p2p-types/src/lib.rs new file mode 100644 index 000000000..1b4d9a7d3 --- /dev/null +++ b/crates/p2p-types/src/lib.rs @@ -0,0 +1,17 @@ +//! Types for the Strata P2P messaging protocol. +#![expect(incomplete_features)] // the generic_const_exprs feature is incomplete +#![feature(generic_const_exprs)] // but necessary for using const generic bounds in + +mod deposit_data; +mod operator; +mod scope; +mod session_id; +mod stake_chain_id; +mod wots; + +pub use deposit_data::{Groth16PublicKeys, WotsPublicKeys}; +pub use operator::P2POperatorPubKey; +pub use scope::Scope; +pub use session_id::SessionId; +pub use stake_chain_id::StakeChainId; +pub use wots::{Wots128PublicKey, Wots160PublicKey, Wots256PublicKey, WOTS_SINGLE}; diff --git a/crates/p2p-types/src/operator.rs b/crates/p2p-types/src/operator.rs new file mode 100644 index 000000000..a89ee8fc9 --- /dev/null +++ b/crates/p2p-types/src/operator.rs @@ -0,0 +1,82 @@ +//! Operators need to exchange (authenticated) messages which are signed with P2P +//! [`P2POperatorPubKey`]. + +use std::fmt; + +use hex::ToHex; +use libp2p_identity::secp256k1::PublicKey; + +/// P2P [`P2POperatorPubKey`] serves as an identifier of protocol entity. +/// +/// De facto this is a wrapper over [`PublicKey`]. +#[derive( + serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, +)] +pub struct P2POperatorPubKey(#[serde(with = "hex::serde")] Vec); + +impl fmt::Display for P2POperatorPubKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0.encode_hex::()) + } +} + +impl AsRef<[u8]> for P2POperatorPubKey { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl From> for P2POperatorPubKey { + fn from(value: Vec) -> Self { + Self(value) + } +} + +impl From for Vec { + fn from(value: P2POperatorPubKey) -> Self { + value.0 + } +} + +impl From for P2POperatorPubKey { + fn from(value: PublicKey) -> Self { + Self(value.to_bytes().to_vec()) + } +} + +impl P2POperatorPubKey { + /// Verifies the `message` using the `signature` against this [`P2POperatorPubKey`]. + pub fn verify(&self, message: &[u8], signature: &[u8]) -> bool { + match PublicKey::try_from_bytes(&self.0) { + Ok(key) => key.verify(message, signature), + Err(_) => false, + } + } +} + +#[cfg(test)] +mod tests { + use bitcoin::hex::DisplayHex; + use secp256k1::rand::{rngs::OsRng, Rng}; + + use super::*; + + #[test] + fn test_p2p_operator_pub_key() { + let random_bytes: [u8; 32] = OsRng.gen(); + let hex_encoded_bytes = random_bytes.to_lower_hex_string(); + + let json_string = format!("\"{hex_encoded_bytes}\""); + + let deserialized = serde_json::from_str::(&json_string); + + assert!( + deserialized.is_ok(), + "must be able to deserialize hex-encoded string to P2POperatorPubKey" + ); + assert!( + deserialized.unwrap() == P2POperatorPubKey(random_bytes.to_vec()), + "deserialized value must be equal to original" + ); + } +} diff --git a/crates/p2p-types/src/scope.rs b/crates/p2p-types/src/scope.rs new file mode 100644 index 000000000..1109e17ba --- /dev/null +++ b/crates/p2p-types/src/scope.rs @@ -0,0 +1,52 @@ +//! Every deposit needs a unique identifier to deposit setup information, +//! which is primarily WOTS PKs. +//! This is the role of [`Scope`]. + +use core::fmt; + +use bitcoin::hashes::{sha256, Hash}; +use serde::{Deserialize, Serialize}; + +/// A unique identifier of deposit setup made by hashing unique data related to it. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] +pub struct Scope([u8; Scope::SIZE]); + +impl fmt::Display for Scope { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(self.0)) + } +} + +impl Scope { + /// Size in bytes. + const SIZE: usize = 32; + + /// Constructs a [`Scope`] from `data` by hashing it. + pub fn hash(data: &[u8]) -> Self { + Self(sha256::Hash::hash(data).to_byte_array()) + } + + /// Constructs a [`Scope`] from raw bytes. + /// + /// Note that the `bytes` should represent a hash. + pub fn from_bytes(bytes: [u8; Self::SIZE]) -> Self { + Self(bytes) + } + + /// Outputs the [`Scope`] as a vector of raw bytes. + pub fn to_vec(&self) -> Vec { + self.0.to_vec() + } +} + +impl AsRef<[u8; Scope::SIZE]> for Scope { + fn as_ref(&self) -> &[u8; Scope::SIZE] { + &self.0 + } +} + +impl From for Scope { + fn from(value: sha256::Hash) -> Self { + Self(value.to_byte_array()) + } +} diff --git a/crates/p2p-types/src/session_id.rs b/crates/p2p-types/src/session_id.rs new file mode 100644 index 000000000..b286c846a --- /dev/null +++ b/crates/p2p-types/src/session_id.rs @@ -0,0 +1,52 @@ +//! Every deposit needs a unique identifier to exchange (partial) signatures and (public) nonces. +//! This is the role of [`SessionId`]. + +use core::fmt; + +use bitcoin::hashes::{sha256, Hash}; +use serde::{Deserialize, Serialize}; + +/// A unique identifier of (partial) signatures and (public) nonces exchange session made by +/// hashing unique data related to it. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] +pub struct SessionId([u8; SessionId::SIZE]); + +impl SessionId { + /// Size in bytes. + const SIZE: usize = 32; + + /// Constructs a [`SessionId`] from `data` by hashing it. + pub fn hash(data: &[u8]) -> Self { + Self(sha256::Hash::const_hash(data).to_byte_array()) + } + + /// Constructs a [`SessionId`] from raw bytes. + /// + /// Note that the `bytes` should represent a hash. + pub fn from_bytes(bytes: [u8; Self::SIZE]) -> Self { + Self(bytes) + } + + /// Outputs the [`SessionId`] as a vector of raw bytes. + pub fn to_vec(&self) -> Vec { + self.0.to_vec() + } +} + +impl fmt::Display for SessionId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(self.0)) + } +} + +impl AsRef<[u8; SessionId::SIZE]> for SessionId { + fn as_ref(&self) -> &[u8; SessionId::SIZE] { + &self.0 + } +} + +impl From for SessionId { + fn from(value: sha256::Hash) -> Self { + Self(value.to_byte_array()) + } +} diff --git a/crates/p2p-types/src/stake_chain_id.rs b/crates/p2p-types/src/stake_chain_id.rs new file mode 100644 index 000000000..6c48ab815 --- /dev/null +++ b/crates/p2p-types/src/stake_chain_id.rs @@ -0,0 +1,52 @@ +//! Every deposit needs a unique identifier to exchange (partial) signatures and (public) nonces. +//! This is the role of [`StakeChainId`]. + +use core::fmt; + +use bitcoin::hashes::{sha256, Hash}; +use serde::{Deserialize, Serialize}; + +/// A unique identifier of (partial) signatures and (public) nonces exchange session made by +/// hashing unique data related to it. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] +pub struct StakeChainId([u8; StakeChainId::SIZE]); + +impl StakeChainId { + /// Size in bytes. + const SIZE: usize = 32; + + /// Constructs a [`StakeChainId`] from `data` by hashing it. + pub fn hash(data: &[u8]) -> Self { + Self(sha256::Hash::const_hash(data).to_byte_array()) + } + + /// Constructs a [`StakeChainId`] from raw bytes. + /// + /// Note that the `bytes` should represent a hash. + pub fn from_bytes(bytes: [u8; Self::SIZE]) -> Self { + Self(bytes) + } + + /// Outputs the [`StakeChainId`] as a vector of raw bytes. + pub fn to_vec(&self) -> Vec { + self.0.to_vec() + } +} + +impl fmt::Display for StakeChainId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(self.0)) + } +} + +impl AsRef<[u8]> for StakeChainId { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl From for StakeChainId { + fn from(value: sha256::Hash) -> Self { + Self(value.to_byte_array()) + } +} diff --git a/crates/p2p-types/src/wots.rs b/crates/p2p-types/src/wots.rs new file mode 100644 index 000000000..25e16d9eb --- /dev/null +++ b/crates/p2p-types/src/wots.rs @@ -0,0 +1,606 @@ +//! WOTS variable-length public keys. + +use std::{ + fmt, + ops::{Deref, DerefMut}, +}; + +use bitcoin::hex::DisplayHex; +#[cfg(feature = "proptest")] +use proptest_derive::Arbitrary; +use serde::{ + de::{self, SeqAccess, Visitor}, + ser::{Serialize, SerializeSeq, Serializer}, + Deserialize, Deserializer, +}; + +/// A single Winternitz One-Time Signature (WOTS) hash value. +pub const WOTS_SINGLE: usize = 20; + +/// The number of bits in an individual WOTS digit. +// WARNING(proofofkeags): MUST BE A FACTOR OF 8 WITH CURRENT IMPLEMENTATION (1,2,4,8) +pub(crate) const WOTS_DIGIT_WIDTH: usize = 4; + +/// The number of WOTS digits needed to commit a byte. +pub(crate) const WOTS_DIGITS_PER_BYTE: usize = 8 / WOTS_DIGIT_WIDTH; + +/// The maximum number a WOTS digit can hold. +#[allow(unfulfilled_lint_expectations)] +#[expect(dead_code)] +pub(crate) const WOTS_MAX_DIGIT: usize = (2 << WOTS_DIGIT_WIDTH) - 1; + +/// The number of WOTS digits required to represent a message for a given message length. +#[allow(unfulfilled_lint_expectations)] +#[expect(dead_code)] +pub(crate) const fn wots_msg_digits(msg_len_bytes: usize) -> usize { + WOTS_DIGITS_PER_BYTE * msg_len_bytes +} + +/// The number of WOTS digits required to sign the checksum for a given message length. +/// +/// The checksum of a WOTS commitment is the sum of the digit values themselves which is then +/// encoded as a base256 integer. That integer is then signed using the same WOTS scheme. +#[allow(unfulfilled_lint_expectations)] +#[expect(dead_code)] +pub(crate) const fn wots_checksum_digits(msg_len_bytes: usize) -> usize { + let max_checksum = wots_msg_digits(msg_len_bytes) * WOTS_MAX_DIGIT; + + // Compute how many bytes we need to represent the checksum itself + let mut exp = 1; + loop { + if 256u64.strict_pow(exp) > max_checksum as u64 { + break; + } else { + exp += 1; + } + } + let num_checksum_bytes = exp as usize; + + // Multiply the checksum bytes by the digits per byte value. + WOTS_DIGITS_PER_BYTE * num_checksum_bytes +} + +/// The total number of WOTS digit keys +#[allow(unfulfilled_lint_expectations)] +#[expect(dead_code)] +pub(crate) const fn wots_total_digits(msg_len_bytes: usize) -> usize { + wots_msg_digits(msg_len_bytes) + wots_checksum_digits(msg_len_bytes) +} + +/// A variable-length Winternitz One-Time Signature (WOTS) public key. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "proptest", derive(Arbitrary))] +pub struct WotsPublicKey(pub [[u8; WOTS_SINGLE]; MSG_LEN_BYTES]) +where + [(); MSG_LEN_BYTES]: Sized, + [(); WOTS_SINGLE * MSG_LEN_BYTES]: Sized; + +/// 128-bit Winternitz One-Time Signature (WOTS) public key. +pub type Wots128PublicKey = WotsPublicKey<36>; + +/// 160-bit Winternitz One-Time Signature (WOTS) public key. +pub type Wots160PublicKey = WotsPublicKey<44>; + +/// 256-bit Winternitz One-Time Signature (WOTS) public key. +pub type Wots256PublicKey = WotsPublicKey<68>; + +impl fmt::Debug for WotsPublicKey +where + [(); MSG_LEN_BYTES]: Sized, + [(); WOTS_SINGLE * MSG_LEN_BYTES]: Sized, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let first_bytes = &self.0[0]; + write!(f, "WotsPublicKey({first_bytes:?})") + } +} + +impl fmt::Display for WotsPublicKey +where + [(); MSG_LEN_BYTES]: Sized, + [(); WOTS_SINGLE * MSG_LEN_BYTES]: Sized, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let first_bytes = self.0[0].to_lower_hex_string(); + write!(f, "WotsPublicKey({first_bytes})") + } +} + +impl WotsPublicKey +where + [(); MSG_LEN_BYTES]: Sized, + [(); WOTS_SINGLE * MSG_LEN_BYTES]: Sized, +{ + /// The size of this WOTS public key in bytes. + pub const SIZE: usize = MSG_LEN_BYTES; + + /// Creates a new WOTS public key from a byte array. + pub fn new(bytes: [[u8; WOTS_SINGLE]; MSG_LEN_BYTES]) -> Self { + Self(bytes) + } + + /// Converts the public key to a byte array. + pub fn to_bytes(self) -> [[u8; WOTS_SINGLE]; MSG_LEN_BYTES] { + self.0 + } + + /// Converts the public key to a flattened byte array. + pub fn to_flattened_bytes(self) -> Vec { + // Changed return type to Vec + let mut bytes = Vec::with_capacity(WOTS_SINGLE * MSG_LEN_BYTES); + for byte_array in &self.0 { + bytes.extend_from_slice(byte_array); + } + bytes + } + + /// Creates the public key from a flattened byte array. + /// + /// If you already have a structured `[[u8; 20]; MSG_LEN]` then you should use + /// [`WotsPublicKey::new`]. + /// + /// # Panics + /// + /// Panics if the byte array is not of proper length. + pub fn from_flattened_bytes(bytes: &[u8]) -> Self { + assert_eq!( + bytes.len(), + WOTS_SINGLE * MSG_LEN_BYTES, + "Invalid byte array length" + ); + + let mut key = [[0u8; WOTS_SINGLE]; MSG_LEN_BYTES]; + for (i, byte_array) in key.iter_mut().enumerate() { + byte_array.copy_from_slice(&bytes[i * WOTS_SINGLE..(i + 1) * WOTS_SINGLE]); + } + Self(key) + } +} + +impl Deref for WotsPublicKey +where + [(); MSG_LEN_BYTES]: Sized, + [(); WOTS_SINGLE * MSG_LEN_BYTES]: Sized, +{ + type Target = [[u8; WOTS_SINGLE]; MSG_LEN_BYTES]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for WotsPublicKey +where + [(); MSG_LEN_BYTES]: Sized, + [(); WOTS_SINGLE * MSG_LEN_BYTES]: Sized, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +// Custom Serialization for WotsPublicKey +impl Serialize for WotsPublicKey +where + [(); MSG_LEN_BYTES]: Sized, + [(); WOTS_SINGLE * MSG_LEN_BYTES]: Sized, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if serializer.is_human_readable() { + let mut seq = serializer.serialize_seq(Some(2 * MSG_LEN_BYTES + 4))?; + for byte_array in &self.0 { + seq.serialize_element(byte_array)?; + } + seq.end() + } else { + serializer.serialize_bytes(&self.to_flattened_bytes()) + } + } +} + +// Custom Deserialization for WotsPublicKey +impl<'de, const MSG_LEN_BYTES: usize> Deserialize<'de> for WotsPublicKey +where + [(); MSG_LEN_BYTES]: Sized, + [(); WOTS_SINGLE * MSG_LEN_BYTES]: Sized, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct WotsPublicKeyVisitor; + + impl<'de, const M: usize> Visitor<'de> for WotsPublicKeyVisitor + where + [(); M]: Sized, + [(); WOTS_SINGLE * M]: Sized, + { + type Value = WotsPublicKey; + + #[expect(elided_lifetimes_in_paths)] + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str(&format!("a WotsPublicKey with {} bytes", WOTS_SINGLE * M)) + } + + fn visit_bytes(self, bytes: &[u8]) -> Result + where + E: de::Error, + { + if bytes.len() != WOTS_SINGLE * M { + return Err(E::invalid_length(bytes.len(), &self)); + } + + let mut array = [[0u8; WOTS_SINGLE]; M]; + for (i, chunk) in bytes.chunks(WOTS_SINGLE).enumerate() { + array[i].copy_from_slice(chunk); + } + Ok(WotsPublicKey(array)) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + [(); WOTS_SINGLE * M]: Sized, + { + let mut array = [[0u8; WOTS_SINGLE]; M]; + for (i, item) in array.iter_mut().enumerate() { + *item = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(i, &self))?; + } + if seq.next_element::<[u8; WOTS_SINGLE]>()?.is_some() { + return Err(de::Error::invalid_length(2 * M + 5, &self)); + } + Ok(WotsPublicKey(array)) + } + } + + if deserializer.is_human_readable() { + deserializer.deserialize_seq(WotsPublicKeyVisitor::) + } else { + deserializer.deserialize_bytes(WotsPublicKeyVisitor::) + } + } +} + +#[cfg(test)] +mod tests { + #[cfg(feature = "proptest")] + use proptest::prelude::*; + + use super::*; + + // Sanity checks for constants + #[test] + fn sanity_check_constants() { + assert_eq!(WOTS_SINGLE, 20); + assert_eq!(WOTS_DIGIT_WIDTH, 4); + assert_eq!(wots_msg_digits(32), 64); + assert_eq!(wots_msg_digits(20), 40); + assert_eq!(wots_checksum_digits(32), 4); + assert_eq!(wots_checksum_digits(20), 4); + } + + #[test] + fn flattened_bytes_roundtrip() { + let key128 = Wots128PublicKey::new([[1u8; WOTS_SINGLE]; 36]); // 2 * 16 + 4 + let key160 = Wots160PublicKey::new([[1u8; WOTS_SINGLE]; 44]); // 2 * 20 + 4 + let key256 = Wots256PublicKey::new([[1u8; WOTS_SINGLE]; 68]); // 2 * 32 + 4 + + // Test flattened bytes roundtrip + let flattened128 = key128.to_flattened_bytes(); + let flattened160 = key160.to_flattened_bytes(); + let flattened256 = key256.to_flattened_bytes(); + + let deserialized128 = Wots128PublicKey::from_flattened_bytes(&flattened128); + let deserialized160 = Wots160PublicKey::from_flattened_bytes(&flattened160); + let deserialized256 = Wots256PublicKey::from_flattened_bytes(&flattened256); + + assert_eq!(key128, deserialized128); + assert_eq!(key160, deserialized160); + assert_eq!(key256, deserialized256); + } + + #[test] + fn json_serialization() { + let key128 = Wots128PublicKey::new([[1u8; WOTS_SINGLE]; 36]); // 2 * 16 + 4 + let key160 = Wots160PublicKey::new([[1u8; WOTS_SINGLE]; 44]); // 2 * 20 + 4 + let key256 = Wots256PublicKey::new([[1u8; WOTS_SINGLE]; 68]); // 2 * 32 + 4 + + // Test JSON serialization + let serialized128 = serde_json::to_string(&key128).unwrap(); + let serialized160 = serde_json::to_string(&key160).unwrap(); + let serialized256 = serde_json::to_string(&key256).unwrap(); + let deserialized128: Wots128PublicKey = serde_json::from_str(&serialized128).unwrap(); + let deserialized160: Wots160PublicKey = serde_json::from_str(&serialized160).unwrap(); + let deserialized256: Wots256PublicKey = serde_json::from_str(&serialized256).unwrap(); + + assert_eq!(key128, deserialized128); + assert_eq!(key160, deserialized160); + assert_eq!(key256, deserialized256); + } + + #[test] + fn bincode_serialization() { + let key128 = Wots128PublicKey::new([[1u8; WOTS_SINGLE]; 36]); // 2 * 16 + 4 + let key160 = Wots160PublicKey::new([[1u8; WOTS_SINGLE]; 44]); // 2 * 20 + 4 + let key256 = Wots256PublicKey::new([[1u8; WOTS_SINGLE]; 68]); // 2 * 32 + 4 + + // Test bincode serialization + let serialized128 = bincode::serialize(&key128).unwrap(); + let deserialized128: Wots128PublicKey = bincode::deserialize(&serialized128).unwrap(); + + let serialized160 = bincode::serialize(&key160).unwrap(); + let deserialized160: Wots160PublicKey = bincode::deserialize(&serialized160).unwrap(); + + let serialized256 = bincode::serialize(&key256).unwrap(); + let deserialized256: Wots256PublicKey = bincode::deserialize(&serialized256).unwrap(); + + assert_eq!(key128, deserialized128); + assert_eq!(key160, deserialized160); + assert_eq!(key256, deserialized256); + } + + #[test] + #[should_panic] + fn deserialize_too_few_elements() { + let json = "[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]]"; // Only one array + let _: Wots128PublicKey = serde_json::from_str(json).unwrap(); + let _: Wots160PublicKey = serde_json::from_str(json).unwrap(); + let _: Wots256PublicKey = serde_json::from_str(json).unwrap(); + } + + #[test] + #[should_panic] + fn deserialize_too_many_elements128() { + // Create JSON string with 37 arrays + let mut json = String::from("["); + for i in 0..37 { + json.push_str("[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]"); + if i < 36 { + json.push(','); + } + } + json.push(']'); + + let _: Wots128PublicKey = serde_json::from_str(&json).unwrap(); + } + + #[test] + #[should_panic] + fn deserialize_too_many_elements160() { + // Create JSON string with 45 arrays + let mut json = String::from("["); + for i in 0..45 { + json.push_str("[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]"); + if i < 44 { + json.push(','); + } + } + json.push(']'); + + let _: Wots160PublicKey = serde_json::from_str(&json).unwrap(); + } + + #[test] + #[should_panic] + fn deserialize_too_many_elements256() { + // Create JSON string with 33 arrays + let mut json = String::from("["); + for i in 0..33 { + json.push_str("[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]"); + if i < 32 { + json.push(','); + } + } + json.push(']'); + + let _: Wots256PublicKey = serde_json::from_str(&json).unwrap(); + } + + #[test] + #[should_panic] + fn deserialize_invalid_array_length36() { + // Create JSON with one array having wrong length (19 instead of 20) + let mut json = String::from("["); + for i in 0..WotsPublicKey::<36>::SIZE { + if i == 8 { + json.push_str("[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]"); // 19 elements + } else { + json.push_str("[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]"); + } + if i < WotsPublicKey::<36>::SIZE - 1 { + json.push(','); + } + } + json.push(']'); + + let _: Wots160PublicKey = serde_json::from_str(&json).unwrap(); + } + + #[test] + #[should_panic] + fn deserialize_invalid_array_length44() { + // Create JSON with one array having wrong length (19 instead of 20) + let mut json = String::from("["); + for i in 0..WotsPublicKey::<44>::SIZE { + if i == 14 { + json.push_str("[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]"); // 19 elements + } else { + json.push_str("[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]"); + } + if i < WotsPublicKey::<44>::SIZE - 1 { + json.push(','); + } + } + json.push(']'); + + let _: Wots160PublicKey = serde_json::from_str(&json).unwrap(); + } + + #[test] + #[should_panic] + fn deserialize_invalid_array_length256() { + // Create JSON with one array having wrong length (19 instead of 20) + let mut json = String::from("["); + for i in 0..WotsPublicKey::<68>::SIZE { + if i == 20 { + json.push_str("[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]"); // 19 elements + } else { + json.push_str("[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]"); + } + if i < WotsPublicKey::<68>::SIZE - 1 { + json.push(','); + } + } + json.push(']'); + + let _: Wots256PublicKey = serde_json::from_str(&json).unwrap(); + } + + #[cfg(feature = "proptest")] + proptest! { + #![proptest_config(ProptestConfig::with_cases(1_000))] + + #[test] + fn proptest_serde_json_roundtrip128(key: Wots128PublicKey) { + let serialized = serde_json::to_string(&key).unwrap(); + let deserialized: Wots128PublicKey = serde_json::from_str(&serialized).unwrap(); + prop_assert_eq!(key, deserialized); + } + + #[test] + fn proptest_serde_json_roundtrip160(key: Wots160PublicKey) { + let serialized = serde_json::to_string(&key).unwrap(); + let deserialized: Wots160PublicKey = serde_json::from_str(&serialized).unwrap(); + prop_assert_eq!(key, deserialized); + } + + #[test] + fn proptest_serde_json_roundtrip256(key: Wots256PublicKey) { + let serialized = serde_json::to_string(&key).unwrap(); + let deserialized: Wots256PublicKey = serde_json::from_str(&serialized).unwrap(); + prop_assert_eq!(key, deserialized); + } + + #[test] + fn proptest_bincode_roundtrip128(key: Wots128PublicKey) { + let serialized = bincode::serialize(&key).unwrap(); + let deserialized: Wots128PublicKey = bincode::deserialize(&serialized).unwrap(); + prop_assert_eq!(key, deserialized); + } + + #[test] + fn proptest_bincode_roundtrip160(key: Wots160PublicKey) { + let serialized = bincode::serialize(&key).unwrap(); + let deserialized: Wots160PublicKey = bincode::deserialize(&serialized).unwrap(); + prop_assert_eq!(key, deserialized); + } + + #[test] + fn proptest_bincode_roundtrip256(key: Wots256PublicKey) { + let serialized = bincode::serialize(&key).unwrap(); + let deserialized: Wots256PublicKey = bincode::deserialize(&serialized).unwrap(); + prop_assert_eq!(key, deserialized); + } + + #[test] + fn proptest_deref_operations128(key: Wots128PublicKey) { + let mut key_copy = key; + + // Test Deref + prop_assert_eq!(&key.0, &*key); + + // Test DerefMut + (*key_copy)[0] = [34u8; WOTS_SINGLE]; + prop_assert_eq!(key_copy.0[0], [34u8; WOTS_SINGLE]); + } + + #[test] + fn proptest_deref_operations160(key: Wots160PublicKey) { + let mut key_copy = key; + + // Test Deref + prop_assert_eq!(&key.0, &*key); + + // Test DerefMut + (*key_copy)[0] = [42u8; WOTS_SINGLE]; + prop_assert_eq!(key_copy.0[0], [42u8; WOTS_SINGLE]); + } + + #[test] + fn proptest_deref_operations256(key: Wots256PublicKey) { + let mut key_copy = key; + + // Test Deref + prop_assert_eq!(&key.0, &*key); + + // Test DerefMut + (*key_copy)[0] = [42u8; WOTS_SINGLE]; + prop_assert_eq!(key_copy.0[0], [42u8; WOTS_SINGLE]); + } + + #[test] + fn proptest_new_constructor128(bytes: [[u8; WOTS_SINGLE]; 36]) { // 2 * 18 + 4 + let key = Wots128PublicKey::new(bytes); + prop_assert_eq!(key.0, bytes); + } + + #[test] + fn proptest_new_constructor160(bytes: [[u8; WOTS_SINGLE]; 44]) { // 2 * 20 + 4 + let key = Wots160PublicKey::new(bytes); + prop_assert_eq!(key.0, bytes); + } + + #[test] + fn proptest_new_constructor256(bytes: [[u8; WOTS_SINGLE]; 68]) { // 2 * 32 + 4 + let key = Wots256PublicKey::new(bytes); + prop_assert_eq!(key.0, bytes); + } + + #[test] + fn proptest_to_flattened_bytes128(key: Wots128PublicKey) { + let flattened = key.to_flattened_bytes(); + + // Verify the length is correct + prop_assert_eq!(flattened.len(), WOTS_SINGLE * 36); + + // Verify each segment matches the original arrays + for (i, original_array) in key.0.iter().enumerate() { + let segment = &flattened[i * WOTS_SINGLE..(i + 1) * WOTS_SINGLE]; + prop_assert_eq!(segment, original_array.as_slice()); + } + } + + #[test] + fn proptest_to_flattened_bytes160(key: Wots160PublicKey) { + let flattened = key.to_flattened_bytes(); + + // Verify the length is correct + prop_assert_eq!(flattened.len(), WOTS_SINGLE * 44); + + // Verify each segment matches the original arrays + for (i, original_array) in key.0.iter().enumerate() { + let segment = &flattened[i * WOTS_SINGLE..(i + 1) * WOTS_SINGLE]; + prop_assert_eq!(segment, original_array.as_slice()); + } + } + + #[test] + fn proptest_to_flattened_bytes256(key: Wots256PublicKey) { + let flattened = key.to_flattened_bytes(); + + // Verify the length is correct + prop_assert_eq!(flattened.len(), WOTS_SINGLE * 68); + + // Verify each segment matches the original arrays + for (i, original_array) in key.0.iter().enumerate() { + let segment = &flattened[i * WOTS_SINGLE..(i + 1) * WOTS_SINGLE]; + prop_assert_eq!(segment, original_array.as_slice()); + } + } + } +} diff --git a/crates/p2p-wire/Cargo.toml b/crates/p2p-wire/Cargo.toml new file mode 100644 index 000000000..c5da1b16f --- /dev/null +++ b/crates/p2p-wire/Cargo.toml @@ -0,0 +1,21 @@ +[package] +edition = "2021" +name = "p2p-wire" +version = "0.1.0" + +[lints] +rust.missing_debug_implementations = "warn" +rust.unreachable_pub = "warn" +rust.unused_crate_dependencies = "deny" +rust.unused_must_use = "deny" +rust.rust_2018_idioms = { level = "deny", priority = -1 } + +[dependencies] +bitcoin.workspace = true +libp2p.workspace = true +musig2.workspace = true +prost.workspace = true +p2p-types.workspace = true + +[build-dependencies] +prost-build.workspace = true diff --git a/crates/p2p-wire/build.rs b/crates/p2p-wire/build.rs new file mode 100644 index 000000000..20da9458d --- /dev/null +++ b/crates/p2p-wire/build.rs @@ -0,0 +1,14 @@ +//! Compile-time `.proto` files to Rust code and types. +use std::io::Result; + +/// Compiles the `.proto` files as Rust types. +fn main() -> Result<()> { + prost_build::compile_protos( + &[ + "proto/strata/bitvm2/p2p/v1/getmessage.proto", + "proto/strata/bitvm2/p2p/v1/gossipsub.proto", + ], + &["proto/"], + )?; + Ok(()) +} diff --git a/crates/p2p-wire/proto/strata/bitvm2/p2p/v1/getmessage.proto b/crates/p2p-wire/proto/strata/bitvm2/p2p/v1/getmessage.proto new file mode 100644 index 000000000..556515e4d --- /dev/null +++ b/crates/p2p-wire/proto/strata/bitvm2/p2p/v1/getmessage.proto @@ -0,0 +1,52 @@ +syntax = "proto3"; + +package strata.bitvm2.p2p.v1; + +import "strata/bitvm2/p2p/v1/gossipsub.proto"; + +// Request for WOTS PKs. +message DepositRequestKey { + // 32-byte hash of the deposit data. + bytes scope = 1; + + // Public key of target operator. + bytes operator = 2; +} + +// Request for Musig2 (public) nonces or (partial) signatures. +message Musig2RequestKey { + // 32-byte hash of the deposit data. + bytes session_id = 1; + + // Public key of target operator. + bytes operator = 2; +} + +// Request for Stake Chain information. +message StakeChainRequestKey { + // 32-byte hash of the stake chain data. + bytes stake_chain_id = 1; + + // Public key of target operator. + bytes operator = 2; +} + +// Catch-all message type for all messages. +message GetMessageRequest { + oneof body { + DepositRequestKey deposit_setup = 1; + Musig2RequestKey nonces = 2; + Musig2RequestKey sigs = 3; + StakeChainRequestKey stake_chain = 4; + } +} + +// `GetMessageRequest` batch. +message GetMessageRequestBatch { + repeated GetMessageRequest requests = 1; +} + +// The reply from an operator. +message GetMessageResponse { + repeated GossipsubMsg msg = 1; +} diff --git a/crates/p2p-wire/proto/strata/bitvm2/p2p/v1/gossipsub.proto b/crates/p2p-wire/proto/strata/bitvm2/p2p/v1/gossipsub.proto new file mode 100644 index 000000000..b1ae3d0e5 --- /dev/null +++ b/crates/p2p-wire/proto/strata/bitvm2/p2p/v1/gossipsub.proto @@ -0,0 +1,79 @@ +syntax = "proto3"; + +package strata.bitvm2.p2p.v1; + +// Used for the Stake Chain setup. +message StakeChainExchange { + /// 32-byte hash of some unique to stake chain data. + bytes stake_chain_id = 1; + + /// 32-byte operator x-only pk used to advance the stake chain. + bytes operator_pk = 2; + + // Transaction hash of pre stake tx. + bytes pre_stake_txid = 3; + + // vout of pre stake tx. + uint32 pre_stake_vout = 4; +} + +// Primarily used for the WOTS PKs for the Deposit. +message DepositSetupExchange { + // 32-byte hash of some unique to deposit data. + bytes scope = 1; + + // Index of the deposit. + uint32 index = 2; + + // Hash to use in the hashlock output from the Stake Chain + bytes hash = 3; + + // Funding Txid to cover for operator costs in dust connector outputs + bytes funding_txid = 4; + + // Funding vout to cover for operator costs in dust connector outputs + uint32 funding_vout = 5; + + // Operator's X-only public key to construct a P2TR address to reimburse the + // operator for a valid withdraw fulfillment. + bytes operator_pk = 6; + + // Deposit data and Withdraw fulfillment transaction data with + // all WOTS public keys. + bytes wots_pks = 7; +} + +// Musig2 first-round (public) nonces exchange. +message Musig2NoncesExchange { + // 32-byte hash of some unique to deposit data. + bytes session_id = 1; + + // (Public) Nonces for each transaction. + repeated bytes pub_nonces = 2; +} + +// Musig2 second-round (partial) signatures exchange. +message Musig2SignaturesExchange { + // 32-byte hash of some unique to deposit data. + bytes session_id = 1; + + // (Partial) Signatures for each transaction. + repeated bytes partial_sigs = 2; +} + +// Catch-all message type for all messages. +message GossipsubMsg { + oneof body { + StakeChainExchange stake_chain = 1; + DepositSetupExchange setup = 2; + Musig2NoncesExchange nonce = 3; + Musig2SignaturesExchange sigs = 4; + } + + // Public key of the operator used for P2P message signing only. + bytes key = 10; + + // Signature of concatenated content of body (without protobuf + // serialization). + bytes signature = 11; +} diff --git a/crates/p2p-wire/src/lib.rs b/crates/p2p-wire/src/lib.rs new file mode 100644 index 000000000..c1fea5572 --- /dev/null +++ b/crates/p2p-wire/src/lib.rs @@ -0,0 +1,5 @@ +//! Protobuf definitions for the Strata P2P protocol. +#![expect(incomplete_features)] // the generic_const_exprs feature is incomplete +#![feature(generic_const_exprs)] // but necessary for using const generic bounds + +pub mod p2p; diff --git a/crates/p2p-wire/src/p2p/mod.rs b/crates/p2p-wire/src/p2p/mod.rs new file mode 100644 index 000000000..83a1c327d --- /dev/null +++ b/crates/p2p-wire/src/p2p/mod.rs @@ -0,0 +1,3 @@ +//! Strata P2P protocol. + +pub mod v1; diff --git a/crates/p2p-wire/src/p2p/v1.rs b/crates/p2p-wire/src/p2p/v1.rs new file mode 100644 index 000000000..89ed83ded --- /dev/null +++ b/crates/p2p-wire/src/p2p/v1.rs @@ -0,0 +1,8 @@ +//! Strata P2P protocol v1 messages. + +pub mod proto { + include!(concat!(env!("OUT_DIR"), "/strata.bitvm2.p2p.v1.rs")); +} + +pub(crate) mod typed; +pub use typed::*; diff --git a/crates/p2p-wire/src/p2p/v1/typed.rs b/crates/p2p-wire/src/p2p/v1/typed.rs new file mode 100644 index 000000000..493d23c01 --- /dev/null +++ b/crates/p2p-wire/src/p2p/v1/typed.rs @@ -0,0 +1,831 @@ +//! Types derived from the `.proto` files. + +use std::fmt; + +use bitcoin::{ + consensus, + hashes::{sha256, Hash}, + hex::DisplayHex, + Txid, XOnlyPublicKey, +}; +use libp2p::{ + identity::{secp256k1::PublicKey as LibP2pSecp256k1PublicKey, PublicKey as LibP2pPublicKey}, + PeerId, +}; +use musig2::{PartialSignature, PubNonce}; +use p2p_types::{P2POperatorPubKey, Scope, SessionId, StakeChainId, WotsPublicKeys}; +use prost::{DecodeError, Message}; + +use super::proto::{ + get_message_request::Body as ProtoGetMessageRequestBody, + gossipsub_msg::Body as ProtoGossipsubMsgBody, DepositRequestKey, + DepositSetupExchange as ProtoDepositSetup, GetMessageRequest as ProtoGetMessageRequest, + GossipsubMsg as ProtoGossipMsg, Musig2NoncesExchange as ProtoMusig2NoncesExchange, + Musig2RequestKey, Musig2SignaturesExchange as ProtoMusig2SignaturesExchange, + StakeChainExchange as ProtoStakeChainExchange, StakeChainRequestKey, +}; + +/// Typed version of "get_message_request::GetMessageRequest". +#[derive(Clone)] +pub enum GetMessageRequest { + /// Request Stake Chain info for this operator. + StakeChainExchange { + /// 32-byte hash of some unique to stake chain data. + stake_chain_id: StakeChainId, + + /// The P2P Operator's public key that the request came from. + operator_pk: P2POperatorPubKey, + }, + + /// Request deposit setup info for [`Scope`] and operator. + DepositSetup { + /// [`Scope`] of the deposit data. + scope: Scope, + + /// The P2P Operator's public key that the request came from. + operator_pk: P2POperatorPubKey, + }, + + /// Request MuSig2 (partial) signatures from operator and for [`SessionId`]. + Musig2SignaturesExchange { + /// [`SessionId`] of either the deposit data or the root deposit data. + session_id: SessionId, + + /// The P2P Operator's public key that the request came from. + operator_pk: P2POperatorPubKey, + }, + + /// Request MuSig2 (public) nonces from operator and for [`SessionId`]. + Musig2NoncesExchange { + /// [`SessionId`] of either the deposit data or the root deposit data. + session_id: SessionId, + + /// The P2P Operator's public key that the request came from. + operator_pk: P2POperatorPubKey, + }, +} + +impl GetMessageRequest { + /// Converts [`ProtoGetMessageRequest`] into [`GetMessageRequest`] + /// by parsing raw vec values into specific types. + pub fn from_msg(msg: ProtoGetMessageRequest) -> Result { + let body = msg.body.ok_or(DecodeError::new("Message without body"))?; + + let request = match body { + ProtoGetMessageRequestBody::DepositSetup(DepositRequestKey { scope, operator }) => { + let bytes = scope + .try_into() + .map_err(|_| DecodeError::new("invalid length of bytes in scope"))?; + let scope = Scope::from_bytes(bytes); + + Self::DepositSetup { + scope, + operator_pk: operator.into(), + } + } + ProtoGetMessageRequestBody::Nonces(Musig2RequestKey { + session_id, + operator, + }) => { + let bytes = session_id + .try_into() + .map_err(|_| DecodeError::new("invalid length of bytes in session id"))?; + let session_id = SessionId::from_bytes(bytes); + + Self::Musig2NoncesExchange { + session_id, + operator_pk: operator.into(), + } + } + ProtoGetMessageRequestBody::Sigs(Musig2RequestKey { + session_id, + operator, + }) => { + let bytes = session_id + .try_into() + .map_err(|_| DecodeError::new("invalid length of bytes in session id"))?; + let session_id = SessionId::from_bytes(bytes); + + Self::Musig2SignaturesExchange { + session_id, + operator_pk: operator.into(), + } + } + ProtoGetMessageRequestBody::StakeChain(StakeChainRequestKey { + stake_chain_id, + operator, + }) => { + let bytes = stake_chain_id + .try_into() + .map_err(|_| DecodeError::new("invalid length of bytes in stake chain id"))?; + let stake_chain_id = StakeChainId::from_bytes(bytes); + Self::StakeChainExchange { + stake_chain_id, + operator_pk: operator.into(), + } + } + }; + + Ok(request) + } + + /// Converts [`GetMessageRequest`] into raw [`ProtoGetMessageRequest`]. + pub fn into_msg(self) -> ProtoGetMessageRequest { + let body = match self { + Self::StakeChainExchange { + stake_chain_id, + operator_pk, + } => ProtoGetMessageRequestBody::StakeChain(StakeChainRequestKey { + stake_chain_id: stake_chain_id.to_vec(), + operator: operator_pk.into(), + }), + Self::DepositSetup { scope, operator_pk } => { + ProtoGetMessageRequestBody::DepositSetup(DepositRequestKey { + scope: scope.to_vec(), + operator: operator_pk.into(), + }) + } + Self::Musig2SignaturesExchange { + session_id, + operator_pk, + } => ProtoGetMessageRequestBody::Sigs(Musig2RequestKey { + session_id: session_id.to_vec(), + operator: operator_pk.into(), + }), + Self::Musig2NoncesExchange { + session_id, + operator_pk, + } => ProtoGetMessageRequestBody::Nonces(Musig2RequestKey { + session_id: session_id.to_vec(), + operator: operator_pk.into(), + }), + }; + + ProtoGetMessageRequest { body: Some(body) } + } + + /// Returns the P2P [`P2POperatorPubKey`] with respect to this [`GetMessageRequest`]. + pub fn operator_pubkey(&self) -> &P2POperatorPubKey { + match self { + Self::StakeChainExchange { operator_pk, .. } + | Self::DepositSetup { operator_pk, .. } + | Self::Musig2NoncesExchange { operator_pk, .. } + | Self::Musig2SignaturesExchange { operator_pk, .. } => operator_pk, + } + } + + /// Returns the [`PeerId`] with respect to this [`GetMessageRequest`]. + pub fn peer_id(&self) -> PeerId { + match self { + Self::StakeChainExchange { operator_pk, .. } + | Self::DepositSetup { operator_pk, .. } + | Self::Musig2NoncesExchange { operator_pk, .. } + | Self::Musig2SignaturesExchange { operator_pk, .. } => { + // convert P2POperatorPubKey into LibP2P secp256k1 PK + let pk = LibP2pSecp256k1PublicKey::try_from_bytes(operator_pk.as_ref()) + .expect("infallible"); + let pk: LibP2pPublicKey = pk.into(); + pk.into() + } + } + } +} + +impl fmt::Debug for GetMessageRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + GetMessageRequest::StakeChainExchange { + operator_pk, + stake_chain_id, + } => write!( + f, + "StakeChainExchange(operator_pk: {operator_pk}, stake_chain_id: {stake_chain_id})" + ), + + GetMessageRequest::DepositSetup { operator_pk, scope } => write!( + f, + "DepositSetup(operator_pk: {operator_pk}, scope: {scope})" + ), + + GetMessageRequest::Musig2SignaturesExchange { + operator_pk, + session_id, + } => write!( + f, + "Musig2SignaturesExchange(operator_pk: {operator_pk}, session_id: {session_id})" + ), + + GetMessageRequest::Musig2NoncesExchange { + operator_pk, + session_id, + } => write!( + f, + "Musig2NoncesExchange(operator_pk: {operator_pk}, session_id: {session_id})" + ), + } + } +} + +impl fmt::Display for GetMessageRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::StakeChainExchange { + operator_pk, + stake_chain_id, + } => write!( + f, + "StakeChainExchange(operator_pk: {operator_pk}, stake_chain_id: {stake_chain_id})" + ), + + Self::DepositSetup { operator_pk, scope } => write!( + f, + "DepositSetup(operator_pk: {operator_pk}, scope: {scope})" + ), + + Self::Musig2NoncesExchange { + operator_pk, + session_id, + } => write!( + f, + "Musig2NoncesExchange(operator_pk: {operator_pk}, session_id: {session_id})" + ), + + Self::Musig2SignaturesExchange { + operator_pk, + session_id, + } => write!( + f, + "Musig2SignaturesExchange(operator_pk: {operator_pk}, session_id: {session_id})" + ), + } + } +} + +/// New deposit request appeared, and operators exchanging setup data. +#[derive(Clone)] +pub struct DepositSetup { + /// [`sha256::Hash`] hash of the stake transaction that the preimage is revealed when advancing + /// the stake. + pub hash: sha256::Hash, + + /// Funding transaction ID. + /// + /// Used to cover the dust outputs in the transaction graph connectors. + pub funding_txid: Txid, + + /// Funding transaction output index. + /// + /// Used to cover the dust outputs in the transaction graph connectors. + pub funding_vout: u32, + + /// Operator's X-only public key to construct a P2TR address to reimburse the + /// operator for a valid withdraw fulfillment. + // TODO: convert this a BOSD descriptor. + pub operator_pk: XOnlyPublicKey, + + /// Winternitz One-Time Signature (WOTS) public keys shared in a deposit. + pub wots_pks: WotsPublicKeys, +} + +impl DepositSetup { + /// Tries to convert a [`ProtoDepositSetup`] into [`DepositSetup`]. + pub fn from_proto_msg(proto: &ProtoDepositSetup) -> Result { + let hash = sha256::Hash::from_slice(&proto.hash) + .map_err(|_| DecodeError::new("invalid length of bytes for hash"))?; + let funding_txid = consensus::deserialize(&proto.funding_txid) + .map_err(|_| DecodeError::new("invalid length of bytes for funding txid"))?; + let funding_vout = proto.funding_vout; + let operator_pk = XOnlyPublicKey::from_slice(&proto.operator_pk) + .map_err(|_| DecodeError::new("invalid length of bytes for operator public key"))?; + let wots_pks = WotsPublicKeys::from_flattened_bytes(&proto.wots_pks); + + Ok(Self { + hash, + funding_txid, + funding_vout, + operator_pk, + wots_pks, + }) + } +} + +impl fmt::Debug for DepositSetup { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let hash = self.hash.as_byte_array().to_lower_hex_string(); + let funding_txid = self.funding_txid; + let funding_vout = self.funding_vout; + let operator_pk = self.operator_pk.serialize().to_lower_hex_string(); + let wots_pks = &self.wots_pks; // not so big because of the custom Debug implementation + + write!(f, "DepositSetup(hash: {hash}, funding_outpoint: {funding_txid}:{funding_vout}, operator_pk: {operator_pk}, wots_pks: {wots_pks:?})") + } +} + +impl fmt::Display for DepositSetup { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let hash = self.hash.as_byte_array().to_lower_hex_string(); + let funding_txid = self.funding_txid; + let funding_vout = self.funding_vout; + let operator_pk = self.operator_pk.serialize().to_lower_hex_string(); + + write!(f, "DepositSetup(hash: {hash}, funding_outpoint: {funding_txid}:{funding_vout}, operator_pk: {operator_pk})") + } +} + +/// Info provided during initial startup of nodes. +/// +/// This is primarily used for the Stake Chain setup. +#[derive(Clone)] +pub struct StakeChainExchange { + /// [`Txid`] of the pre-stake transaction. + pub pre_stake_txid: Txid, + + /// vout of the pre-stake transaction. + pub pre_stake_vout: u32, +} + +impl StakeChainExchange { + /// Tries to convert a [`ProtoStakeChainExchange`] into [`StakeChainExchange`]. + pub fn from_proto_msg(proto: &ProtoStakeChainExchange) -> Result { + let txid = consensus::deserialize(&proto.pre_stake_txid) + .map_err(|err| DecodeError::new(err.to_string()))?; + + Ok(Self { + pre_stake_txid: txid, + pre_stake_vout: proto.pre_stake_vout, + }) + } +} + +impl fmt::Debug for StakeChainExchange { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let pre_stake_txid = self.pre_stake_txid; + let pre_stake_vout = self.pre_stake_vout; + write!( + f, + "StakeChainExchange(pre_stake_outpoint: {pre_stake_txid}:{pre_stake_vout})" + ) + } +} + +impl fmt::Display for StakeChainExchange { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let pre_stake_txid = self.pre_stake_txid; + let pre_stake_vout = self.pre_stake_vout; + write!( + f, + "StakeChainExchange(pre_stake_outpoint: {pre_stake_txid}:{pre_stake_vout})" + ) + } +} + +/// Unsigned messages exchanged between operators. +#[derive(Clone)] +#[expect(clippy::large_enum_variant)] +pub enum UnsignedGossipsubMsg { + /// Operators exchange stake chain info. + StakeChainExchange { + /// 32-byte hash of some unique to stake chain data. + stake_chain_id: StakeChainId, + + /// 32-byte x-only public key of the operator used to advance the stake chain. + operator_pk: XOnlyPublicKey, + + /// [`Txid`] of the pre-stake transaction. + pre_stake_txid: Txid, + + /// vout of the pre-stake transaction. + pre_stake_vout: u32, + }, + + /// New deposit request appeared, and operators + /// exchanging setup data. + /// + /// This is primarily used for the WOTS PKs. + DepositSetup { + /// [`Scope`] of the deposit data. + scope: Scope, + + /// Index of the deposit. + index: u32, + + /// [`sha256::Hash`] hash of the stake transaction that the preimage is revealed when + /// advancing the stake. + hash: sha256::Hash, + + /// Funding transaction ID. + /// + /// Used to cover the dust outputs in the transaction graph connectors. + funding_txid: Txid, + + /// Funding transaction output index. + /// + /// Used to cover the dust outputs in the transaction graph connectors. + funding_vout: u32, + + /// Operator's X-only public key to construct a P2TR address to reimburse the + /// operator for a valid withdraw fulfillment. + // TODO: convert this a BOSD descriptor. + operator_pk: XOnlyPublicKey, + + /// Winternitz One-Time Signature (WOTS) public keys shared in a deposit. + wots_pks: WotsPublicKeys, + }, + + /// Operators exchange (public) nonces before signing. + Musig2NoncesExchange { + /// [`SessionId`] of either the deposit data or the root deposit data. + session_id: SessionId, + + /// (Public) Nonces for each transaction. + nonces: Vec, + }, + + /// Operators exchange (partial) signatures for the transaction graph. + Musig2SignaturesExchange { + /// [`SessionId`] of either the deposit data or the root deposit data. + session_id: SessionId, + + /// (Partial) Signatures for each transaction. + signatures: Vec, + }, +} + +impl UnsignedGossipsubMsg { + /// Tries to convert [`ProtoGossipsubMsgBody`] into typed [`UnsignedGossipsubMsg`] + /// with specific types instead of raw vectors. + pub fn from_msg_proto(proto: &ProtoGossipsubMsgBody) -> Result { + let unsigned = match proto { + ProtoGossipsubMsgBody::StakeChain(proto) => { + let bytes = + proto.stake_chain_id.as_slice().try_into().map_err(|_| { + DecodeError::new("invalid length of bytes for stake chain id") + })?; + let stake_chain_id = StakeChainId::from_bytes(bytes); + let operator_pk = XOnlyPublicKey::from_slice(&proto.operator_pk) + .map_err(|_| DecodeError::new("invalid length of bytes for operator pk"))?; + let pre_stake_txid = consensus::deserialize(&proto.pre_stake_txid) + .map_err(|_| DecodeError::new("invalid length of bytes for pre-stake txid"))?; + let pre_stake_vout = proto.pre_stake_vout; + Self::StakeChainExchange { + stake_chain_id, + operator_pk, + pre_stake_txid, + pre_stake_vout, + } + } + ProtoGossipsubMsgBody::Setup(proto) => { + let bytes = proto + .scope + .as_slice() + .try_into() + .map_err(|_| DecodeError::new("invalid length of bytes for scope"))?; + let scope = Scope::from_bytes(bytes); + let index = proto.index; + let hash = sha256::Hash::from_slice(&proto.hash) + .map_err(|_| DecodeError::new("invalid length of bytes for hash"))?; + let funding_txid = consensus::deserialize(&proto.funding_txid) + .map_err(|_| DecodeError::new("invalid funding txid"))?; + let funding_vout = proto.funding_vout; + let operator_pk = XOnlyPublicKey::from_slice(&proto.operator_pk) + .map_err(|_| DecodeError::new("invalid length of bytes for operator pk"))?; + let wots_pks = WotsPublicKeys::from_flattened_bytes(&proto.wots_pks); + + Self::DepositSetup { + scope, + index, + hash, + funding_txid, + funding_vout, + operator_pk, + wots_pks, + } + } + ProtoGossipsubMsgBody::Nonce(proto) => { + let bytes = proto + .session_id + .as_slice() + .try_into() + .map_err(|_| DecodeError::new("invalid length of bytes for session id"))?; + let session_id = SessionId::from_bytes(bytes); + + let nonces = proto + .pub_nonces + .iter() + .map(|bytes| PubNonce::from_bytes(bytes)) + .collect::, _>>() + .map_err(|err| DecodeError::new(err.to_string()))?; + + Self::Musig2NoncesExchange { session_id, nonces } + } + ProtoGossipsubMsgBody::Sigs(proto) => { + let bytes = proto + .session_id + .as_slice() + .try_into() + .map_err(|_| DecodeError::new("invalid length of bytes for session id"))?; + let session_id = SessionId::from_bytes(bytes); + + let partial_sigs = proto + .partial_sigs + .iter() + .map(|bytes| PartialSignature::from_slice(bytes)) + .collect::, _>>() + .map_err(|err| DecodeError::new(err.to_string()))?; + + Self::Musig2SignaturesExchange { + session_id, + signatures: partial_sigs, + } + } + }; + + Ok(unsigned) + } + + /// Returns content of the message for signing. + /// + /// Depending on the variant, concatenates serialized data of the variant and returns it as + /// a [`Vec`] of bytes. + pub fn content(&self) -> Vec { + let mut content = Vec::new(); + + match &self { + Self::StakeChainExchange { + stake_chain_id, + operator_pk, + pre_stake_txid, + pre_stake_vout, + } => { + content.extend(stake_chain_id.as_ref()); + content.extend(operator_pk.serialize()); + content.extend(pre_stake_txid.as_byte_array()); + content.extend(pre_stake_vout.to_le_bytes()); + } + Self::DepositSetup { + scope, + index, + hash, + funding_txid, + funding_vout, + operator_pk, + wots_pks, + } => { + content.extend(scope.as_ref()); + content.extend(index.to_le_bytes()); + content.extend(hash.as_byte_array()); + content.extend(funding_txid.as_byte_array()); + content.extend(funding_vout.to_le_bytes()); + content.extend(operator_pk.serialize()); + content.extend(wots_pks.to_flattened_bytes()); + } + Self::Musig2NoncesExchange { session_id, nonces } => { + content.extend(session_id.as_ref()); + for nonce in nonces { + content.extend(nonce.serialize()); + } + } + Self::Musig2SignaturesExchange { + session_id, + signatures, + } => { + content.extend(session_id.as_ref()); + for sig in signatures { + content.extend(sig.serialize()); + } + } + }; + + content + } + + /// Helper function to convert [`UnsignedGossipsubMsg`] into raw [`ProtoGossipsubMsgBody`]. + fn to_raw(&self) -> ProtoGossipsubMsgBody { + match self { + Self::StakeChainExchange { + stake_chain_id, + operator_pk, + pre_stake_txid, + pre_stake_vout, + } => ProtoGossipsubMsgBody::StakeChain(ProtoStakeChainExchange { + stake_chain_id: stake_chain_id.to_vec(), + operator_pk: operator_pk.serialize().to_vec(), + pre_stake_txid: pre_stake_txid.to_byte_array().to_vec(), + pre_stake_vout: *pre_stake_vout, + }), + Self::DepositSetup { + scope, + index, + hash, + funding_txid, + funding_vout, + operator_pk, + wots_pks, + } => ProtoGossipsubMsgBody::Setup(ProtoDepositSetup { + scope: scope.to_vec(), + index: *index, + hash: hash.as_byte_array().to_vec(), + funding_txid: funding_txid.to_byte_array().to_vec(), + funding_vout: *funding_vout, + operator_pk: operator_pk.serialize().to_vec(), + wots_pks: wots_pks.to_flattened_bytes().to_vec(), + }), + Self::Musig2NoncesExchange { session_id, nonces } => { + ProtoGossipsubMsgBody::Nonce(ProtoMusig2NoncesExchange { + session_id: session_id.to_vec(), + pub_nonces: nonces.iter().map(|n| n.serialize().to_vec()).collect(), + }) + } + Self::Musig2SignaturesExchange { + session_id, + signatures, + } => ProtoGossipsubMsgBody::Sigs(ProtoMusig2SignaturesExchange { + session_id: session_id.to_vec(), + partial_sigs: signatures.iter().map(|s| s.serialize().to_vec()).collect(), + }), + } + } +} + +impl fmt::Debug for UnsignedGossipsubMsg { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + UnsignedGossipsubMsg::StakeChainExchange { + stake_chain_id, + operator_pk, + pre_stake_txid, + pre_stake_vout, + } => { + let operator_pk = operator_pk.serialize().to_lower_hex_string(); + write!( + f, + "StakeChainExchange(stake_chain_id: {stake_chain_id}, operator_pk: {operator_pk}, pre_stake_outpoint: {pre_stake_txid}:{pre_stake_vout})" + ) + } + UnsignedGossipsubMsg::DepositSetup { + scope, + index, + hash, + funding_txid, + funding_vout, + operator_pk, + wots_pks, + } => { + let hash = hash.as_byte_array().to_lower_hex_string(); + let operator_pk = operator_pk.serialize().to_lower_hex_string(); + write!( + f, + "DepositSetup(scope: {scope}, index: {index}, hash: {hash}, funding_outpoint: {funding_txid}:{funding_vout}, operator_pk: {operator_pk}, wots_pks: {wots_pks:?})" + ) + } + UnsignedGossipsubMsg::Musig2NoncesExchange { session_id, nonces } => { + let nonces_count = nonces.len(); + write!( + f, + "Musig2NoncesExchange(session_id: {session_id}, nonces_count: {nonces_count})" + ) + } + UnsignedGossipsubMsg::Musig2SignaturesExchange { + session_id, + signatures, + } => { + let signatures_count = signatures.len(); + write!( + f, + "Musig2SignaturesExchange(session_id: {session_id}, signatures_count: {signatures_count})" + ) + } + } + } +} + +impl fmt::Display for UnsignedGossipsubMsg { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + UnsignedGossipsubMsg::StakeChainExchange { + stake_chain_id, + operator_pk, + pre_stake_txid, + pre_stake_vout, + } => { + let operator_pk = operator_pk.serialize().to_lower_hex_string(); + write!( + f, + "StakeChainExchange(stake_chain_id: {stake_chain_id}, operator_pk: {operator_pk}, pre_stake_outpoint: {pre_stake_txid}:{pre_stake_vout})" + ) + } + UnsignedGossipsubMsg::DepositSetup { + scope, + index, + hash, + funding_txid, + funding_vout, + operator_pk, + .. + } => { + let hash = hash.as_byte_array().to_lower_hex_string(); + let operator_pk = operator_pk.serialize().to_lower_hex_string(); + write!( + f, + "DepositSetup(scope: {scope}, index: {index}, hash: {hash}, funding_outpoint: {funding_txid}:{funding_vout}, operator_pk: {operator_pk})" + ) + } + UnsignedGossipsubMsg::Musig2NoncesExchange { session_id, nonces } => { + let nonces_count = nonces.len(); + write!( + f, + "Musig2NoncesExchange(session_id: {session_id}, nonces_count: {nonces_count})" + ) + } + UnsignedGossipsubMsg::Musig2SignaturesExchange { + session_id, + signatures, + } => { + let signatures_count = signatures.len(); + write!( + f, + "Musig2SignaturesExchange(session_id: {session_id}, signatures_count: {signatures_count})" + ) + } + } + } +} + +/// Gossipsub message. +#[derive(Clone)] +pub struct GossipsubMsg { + /// Operator's signature of the message. + pub signature: Vec, + + /// Operator's P2P public key. + pub key: P2POperatorPubKey, + + /// Unsigned payload. + pub unsigned: UnsignedGossipsubMsg, +} + +impl GossipsubMsg { + /// Tries to decode a Gossipsub message from bytes. + pub fn from_bytes(bytes: &[u8]) -> Result { + let msg = ProtoGossipMsg::decode(bytes)?; + let Some(body) = msg.body else { + return Err(DecodeError::new("Message with empty body")); + }; + + let kind = UnsignedGossipsubMsg::from_msg_proto(&body)?; + let key = msg.key.into(); + + Ok(Self { + signature: msg.signature, + key, + unsigned: kind, + }) + } + + /// Tries to decode a Gossipsub message from a protobuf message. + pub fn from_proto(msg: ProtoGossipMsg) -> Result { + Ok(Self { + signature: msg.signature, + key: msg.key.into(), + unsigned: UnsignedGossipsubMsg::from_msg_proto(&msg.body.unwrap())?, + }) + } + + /// Converts a Gossipsub message into a protobuf message. + pub fn into_raw(self) -> ProtoGossipMsg { + ProtoGossipMsg { + key: self.key.into(), + signature: self.signature, + body: Some(self.unsigned.to_raw()), + } + } + + /// Returns the content of the message as raw bytes. + pub fn content(&self) -> Vec { + self.unsigned.content() + } +} + +impl fmt::Debug for GossipsubMsg { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let key = self.key.to_string(); + let signature = self.signature.to_lower_hex_string(); + let unsigned = &self.unsigned; + write!( + f, + "GossipsubMsg(key: {key}, signature: {signature}, unsigned: {unsigned:?})" + ) + } +} + +impl fmt::Display for GossipsubMsg { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let key = self.key.to_string(); + let signature = self.signature.to_lower_hex_string(); + let unsigned = &self.unsigned; + write!( + f, + "GossipsubMsg(key: {key}, signature: {signature}, unsigned: {unsigned})", + ) + } +} diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index f5d067725..b85f6104c 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -24,7 +24,7 @@ rkyv.workspace = true secp256k1 = { workspace = true, features = ["global-context", "rand-std"] } serde.workspace = true sha2.workspace = true -strata-p2p-types.workspace = true +p2p-types.workspace = true thiserror.workspace = true [dev-dependencies] diff --git a/crates/primitives/src/operator_table.rs b/crates/primitives/src/operator_table.rs index cfef850c1..efc94c5c8 100644 --- a/crates/primitives/src/operator_table.rs +++ b/crates/primitives/src/operator_table.rs @@ -8,8 +8,8 @@ use std::{ use algebra::category; use bitcoin::{Network, XOnlyPublicKey}; use musig2::KeyAggContext; +use p2p_types::P2POperatorPubKey; use serde::{Deserialize, Serialize}; -use strata_p2p_types::P2POperatorPubKey; use strata_primitives::bridge::PublickeyTable; use crate::{build_context::TxBuildContext, types::OperatorIdx}; @@ -281,8 +281,8 @@ impl OperatorTable { /// Proptest generators for the operator table. pub mod prop_test_generators { + use p2p_types::P2POperatorPubKey; use proptest::{prelude::*, prop_compose}; - use strata_p2p_types::P2POperatorPubKey; use super::OperatorTable; use crate::secp::EvenSecretKey; diff --git a/crates/primitives/src/wots.rs b/crates/primitives/src/wots.rs index faf00a1f6..69f46108b 100644 --- a/crates/primitives/src/wots.rs +++ b/crates/primitives/src/wots.rs @@ -9,6 +9,7 @@ use bitvm::{ chunk::api::{NUM_HASH, NUM_PUBS, NUM_U256}, signatures::{Wots, Wots16 as wots_hash, Wots32 as wots256}, }; +use p2p_types::WotsPublicKeys; use proptest::prelude::{any, Arbitrary, BoxedStrategy, Strategy}; use proptest_derive::Arbitrary; use serde::{ @@ -16,7 +17,6 @@ use serde::{ ser::SerializeSeq, Deserialize, Deserializer, Serialize, Serializer, }; -use strata_p2p_types::WotsPublicKeys; use crate::scripts::{ commitments::{ @@ -75,15 +75,15 @@ impl Wots256PublicKey { } } -impl From for Wots256PublicKey { - fn from(value: strata_p2p_types::Wots256PublicKey) -> Self { +impl From for Wots256PublicKey { + fn from(value: p2p_types::Wots256PublicKey) -> Self { Self(Arc::new(value.0)) } } -impl From for strata_p2p_types::Wots256PublicKey { +impl From for p2p_types::Wots256PublicKey { fn from(value: Wots256PublicKey) -> Self { - strata_p2p_types::Wots256PublicKey::new(*value.0) + p2p_types::Wots256PublicKey::new(*value.0) } } @@ -169,15 +169,15 @@ impl Arbitrary for Wots256PublicKey { #[derive(Debug, Clone, Copy, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] pub struct WotsHashPublicKey(pub ::PublicKey); -impl From for WotsHashPublicKey { - fn from(value: strata_p2p_types::Wots128PublicKey) -> Self { +impl From for WotsHashPublicKey { + fn from(value: p2p_types::Wots128PublicKey) -> Self { Self(value.0) } } -impl From for strata_p2p_types::Wots128PublicKey { +impl From for p2p_types::Wots128PublicKey { fn from(value: WotsHashPublicKey) -> Self { - strata_p2p_types::Wots128PublicKey::new(value.0) + p2p_types::Wots128PublicKey::new(value.0) } } @@ -194,10 +194,10 @@ impl Deref for Groth16PublicKeys { } } -impl TryFrom for Groth16PublicKeys { - type Error = (String, strata_p2p_types::Groth16PublicKeys); +impl TryFrom for Groth16PublicKeys { + type Error = (String, p2p_types::Groth16PublicKeys); - fn try_from(g16_keys: strata_p2p_types::Groth16PublicKeys) -> Result { + fn try_from(g16_keys: p2p_types::Groth16PublicKeys) -> Result { if g16_keys.public_inputs.len() != NUM_PUBS { return Err(( format!( @@ -235,20 +235,20 @@ impl TryFrom for Groth16PublicKeys { } } -impl From for strata_p2p_types::Groth16PublicKeys { +impl From for p2p_types::Groth16PublicKeys { fn from(value: Groth16PublicKeys) -> Self { let (public_inputs, fqs, hashes) = *value.0; Self::new( public_inputs - .map(strata_p2p_types::Wots256PublicKey::new) + .map(p2p_types::Wots256PublicKey::new) .into_iter() .collect(), - fqs.map(strata_p2p_types::Wots256PublicKey::new) + fqs.map(p2p_types::Wots256PublicKey::new) .into_iter() .collect(), hashes - .map(strata_p2p_types::Wots128PublicKey::new) + .map(p2p_types::Wots128PublicKey::new) .into_iter() .collect(), ) @@ -641,21 +641,23 @@ impl PublicKeys { } } -impl TryFrom for PublicKeys { - type Error = (String, strata_p2p_types::WotsPublicKeys); +impl TryFrom for PublicKeys { + type Error = (String, p2p_types::WotsPublicKeys); - fn try_from(value: strata_p2p_types::WotsPublicKeys) -> Result { - let groth16 = value.groth16.try_into().map_err( - |e: (String, strata_p2p_types::Groth16PublicKeys)| { - ( - e.0, - WotsPublicKeys { - withdrawal_fulfillment: value.withdrawal_fulfillment, - groth16: e.1, - }, - ) - }, - )?; + fn try_from(value: p2p_types::WotsPublicKeys) -> Result { + let groth16 = + value + .groth16 + .try_into() + .map_err(|e: (String, p2p_types::Groth16PublicKeys)| { + ( + e.0, + WotsPublicKeys { + withdrawal_fulfillment: value.withdrawal_fulfillment, + groth16: e.1, + }, + ) + })?; let withdrawal_fulfillment = value.withdrawal_fulfillment.into(); @@ -666,7 +668,7 @@ impl TryFrom for PublicKeys { } } -impl From for strata_p2p_types::WotsPublicKeys { +impl From for p2p_types::WotsPublicKeys { fn from(value: PublicKeys) -> Self { Self { withdrawal_fulfillment: value.withdrawal_fulfillment.into(),