diff --git a/Cargo.lock b/Cargo.lock index c8205a658..4c0fbf424 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,6 +90,17 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "alloy-consensus" +version = "0.1.0" +source = "git+https://github.com/alloy-rs/alloy?rev=6f8ebb4#6f8ebb45afca1a201a11d421ec46db0f7a1d8d08" +dependencies = [ + "alloy-eips 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=6f8ebb4)", + "alloy-network", + "alloy-primitives 0.6.4", + "alloy-rlp", +] + [[package]] name = "alloy-consensus" version = "0.1.0" @@ -104,6 +115,38 @@ dependencies = [ "sha2", ] +[[package]] +name = "alloy-contract" +version = "0.1.0" +source = "git+https://github.com/alloy-rs/alloy?rev=6f8ebb4#6f8ebb45afca1a201a11d421ec46db0f7a1d8d08" +dependencies = [ + "alloy-dyn-abi", + "alloy-json-abi", + "alloy-primitives 0.6.4", + "alloy-providers", + "alloy-rpc-types 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=6f8ebb4)", + "alloy-sol-types 0.6.4", + "alloy-transport 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=6f8ebb4)", + "thiserror", +] + +[[package]] +name = "alloy-dyn-abi" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2919acdad13336bc5dc26b636cdd6892c2f27fb0d4a58320a00c2713cf6a4e9a" +dependencies = [ + "alloy-json-abi", + "alloy-primitives 0.6.4", + "alloy-sol-type-parser", + "alloy-sol-types 0.6.4", + "const-hex", + "itoa", + "serde", + "serde_json", + "winnow 0.6.6", +] + [[package]] name = "alloy-eips" version = "0.1.0" @@ -138,6 +181,18 @@ dependencies = [ "serde", ] +[[package]] +name = "alloy-json-abi" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ed0f2a6c3a1c947b4508522a53a190dba8f94dcd4e3e1a5af945a498e78f2f" +dependencies = [ + "alloy-primitives 0.6.4", + "alloy-sol-type-parser", + "serde", + "serde_json", +] + [[package]] name = "alloy-json-rpc" version = "0.1.0" @@ -325,7 +380,7 @@ name = "alloy-rpc-types" version = "0.1.0" source = "git+https://github.com/alloy-rs/alloy?rev=cad7935#cad7935d69f433e45d190902e58b1c996b35adfa" dependencies = [ - "alloy-consensus", + "alloy-consensus 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=cad7935)", "alloy-eips 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=cad7935)", "alloy-genesis", "alloy-primitives 0.7.0", @@ -414,6 +469,15 @@ dependencies = [ "syn-solidity 0.7.0", ] +[[package]] +name = "alloy-sol-type-parser" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0045cc89524e1451ccf33e8581355b6027ac7c6e494bb02959d4213ad0d8e91d" +dependencies = [ + "winnow 0.6.6", +] + [[package]] name = "alloy-sol-types" version = "0.6.4" @@ -2603,7 +2667,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.6", + "socket2 0.4.10", "tokio", "tower-service", "tracing", @@ -2985,6 +3049,9 @@ name = "kinode" version = "0.7.1" dependencies = [ "aes-gcm", + "alloy-consensus 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=6f8ebb4)", + "alloy-contract", + "alloy-network", "alloy-primitives 0.6.4", "alloy-providers", "alloy-pubsub", @@ -3037,6 +3104,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sha2", + "sha3", "snow", "static_dir", "thiserror", diff --git a/kinode/Cargo.toml b/kinode/Cargo.toml index 1bb145a00..f71c34122 100644 --- a/kinode/Cargo.toml +++ b/kinode/Cargo.toml @@ -26,11 +26,14 @@ simulation-mode = [] [dependencies] aes-gcm = "0.10.3" +alloy-contract = { git = "https://github.com/alloy-rs/alloy", rev = "6f8ebb4" } +alloy-consensus = { git = "https://github.com/alloy-rs/alloy", rev = "6f8ebb4" } alloy-pubsub = { git = "https://github.com/alloy-rs/alloy", rev = "6f8ebb4" } alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy", rev = "6f8ebb4" } alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", rev = "6f8ebb4", features = ["ws"]} alloy-transport-ws = { git = "https://github.com/alloy-rs/alloy", rev = "6f8ebb4" } alloy-providers = { git = "https://github.com/alloy-rs/alloy", rev = "6f8ebb4" } +alloy-network = { git = "https://github.com/alloy-rs/alloy", rev = "6f8ebb4" } alloy-primitives = "0.6.2" alloy-sol-macro = "0.6.2" alloy-sol-types = "0.6.2" @@ -76,6 +79,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_urlencoded = "0.7" sha2 = "0.10" +sha3 = "0.10.8" snow = { version = "0.9.5", features = ["ring-resolver"] } static_dir = "0.2.0" thiserror = "1.0" diff --git a/kinode/packages/app_store/app_store/src/lib.rs b/kinode/packages/app_store/app_store/src/lib.rs index c85e84991..a8b719084 100644 --- a/kinode/packages/app_store/app_store/src/lib.rs +++ b/kinode/packages/app_store/app_store/src/lib.rs @@ -39,9 +39,20 @@ use ft_worker_lib::{ const ICON: &str = include_str!("icon"); +#[cfg(not(feature = "simulation-mode"))] const CHAIN_ID: u64 = 10; // optimism +#[cfg(feature = "simulation-mode")] +const CHAIN_ID: u64 = 31337; // local + +#[cfg(not(feature = "simulation-mode"))] const CONTRACT_ADDRESS: &str = "0x52185B6a6017E6f079B994452F234f7C2533787B"; // optimism +#[cfg(feature = "simulation-mode")] +const CONTRACT_ADDRESS: &str = "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318"; // local + +#[cfg(not(feature = "simulation-mode"))] const CONTRACT_FIRST_BLOCK: u64 = 118_590_088; +#[cfg(feature = "simulation-mode")] +const CONTRACT_FIRST_BLOCK: u64 = 1; const EVENTS: [&str; 3] = [ "AppRegistered(uint256,string,bytes,string,bytes32)", @@ -71,7 +82,6 @@ pub enum Resp { } fn fetch_logs(eth_provider: ð::Provider, filter: ð::Filter) -> Vec { - #[cfg(not(feature = "simulation-mode"))] loop { match eth_provider.get_logs(filter) { Ok(res) => return res, @@ -82,13 +92,10 @@ fn fetch_logs(eth_provider: ð::Provider, filter: ð::Filter) -> Vec break, @@ -99,7 +106,6 @@ fn subscribe_to_logs(eth_provider: ð::Provider, filter: eth::Filter) { } } } - #[cfg(not(feature = "simulation-mode"))] println!("subscribed to logs successfully"); } @@ -221,10 +227,7 @@ fn init(our: Address) { state = State::new(CONTRACT_ADDRESS.to_string()).unwrap(); } - #[cfg(not(feature = "simulation-mode"))] println!("indexing on contract address {}", state.contract_address); - #[cfg(feature = "simulation-mode")] - println!("simulation mode: not indexing packages"); // create new provider for sepolia with request-timeout of 60s // can change, log requests can take quite a long time. diff --git a/kinode/packages/kino_updates/widget/Cargo.toml b/kinode/packages/kino_updates/widget/Cargo.toml index 33cdec77d..617d188e9 100644 --- a/kinode/packages/kino_updates/widget/Cargo.toml +++ b/kinode/packages/kino_updates/widget/Cargo.toml @@ -3,6 +3,9 @@ name = "widget" version = "0.1.0" edition = "2021" +[features] +simulation-mode = [] + [dependencies] anyhow = "1.0" bincode = "1.3.3" diff --git a/kinode/packages/kns_indexer/kns_indexer/src/lib.rs b/kinode/packages/kns_indexer/kns_indexer/src/lib.rs index 6fece0bcc..588766fb6 100644 --- a/kinode/packages/kns_indexer/kns_indexer/src/lib.rs +++ b/kinode/packages/kns_indexer/kns_indexer/src/lib.rs @@ -14,9 +14,20 @@ wit_bindgen::generate!({ world: "process", }); -// perhaps a constant in process_lib? -const KNS_OPTIMISM_ADDRESS: &'static str = "0xca5b5811c0c40aab3295f932b1b5112eb7bb4bd6"; -const KNS_FIRST_BLOCK: u64 = 114_923_786; +#[cfg(not(feature = "simulation-mode"))] +const KNS_ADDRESS: &'static str = "0xca5b5811c0c40aab3295f932b1b5112eb7bb4bd6"; // optimism +#[cfg(feature = "simulation-mode")] +const KNS_ADDRESS: &'static str = "0x5FbDB2315678afecb367f032d93F642f64180aa3"; // local + +#[cfg(not(feature = "simulation-mode"))] +const CHAIN_ID: u64 = 10; // optimism +#[cfg(feature = "simulation-mode")] +const CHAIN_ID: u64 = 31337; // local + +#[cfg(not(feature = "simulation-mode"))] +const KNS_FIRST_BLOCK: u64 = 114_923_786; // optimism +#[cfg(feature = "simulation-mode")] +const KNS_FIRST_BLOCK: u64 = 1; // local #[derive(Clone, Debug, Serialize, Deserialize)] struct State { @@ -100,7 +111,6 @@ sol! { } fn subscribe_to_logs(eth_provider: ð::Provider, from_block: u64, filter: eth::Filter) { - #[cfg(not(feature = "simulation-mode"))] loop { match eth_provider.subscribe(1, filter.clone().from_block(from_block)) { Ok(()) => break, @@ -111,28 +121,22 @@ fn subscribe_to_logs(eth_provider: ð::Provider, from_block: u64, filter: eth: } } } - #[cfg(not(feature = "simulation-mode"))] println!("subscribed to logs successfully"); } call_init!(init); fn init(our: Address) { - let (chain_id, contract_address) = (10, KNS_OPTIMISM_ADDRESS.to_string()); - - #[cfg(not(feature = "simulation-mode"))] - println!("indexing on contract address {}", contract_address); - #[cfg(feature = "simulation-mode")] - println!("simulation mode: not indexing KNS"); + println!("indexing on contract address {}", KNS_ADDRESS); // if we have state, load it in let state: State = match get_typed_state(|bytes| Ok(bincode::deserialize::(bytes)?)) { Some(s) => { // if chain id or contract address changed from a previous run, reset state - if s.chain_id != chain_id || s.contract_address != contract_address { + if s.chain_id != CHAIN_ID || s.contract_address != KNS_ADDRESS { println!("resetting state because runtime contract address or chain ID changed"); State { - chain_id, - contract_address, + chain_id: CHAIN_ID, + contract_address: KNS_ADDRESS.to_string(), names: HashMap::new(), nodes: HashMap::new(), block: KNS_FIRST_BLOCK, @@ -143,8 +147,8 @@ fn init(our: Address) { } } None => State { - chain_id, - contract_address: contract_address.clone(), + chain_id: CHAIN_ID, + contract_address: KNS_ADDRESS.to_string(), names: HashMap::new(), nodes: HashMap::new(), block: KNS_FIRST_BLOCK, @@ -184,8 +188,12 @@ fn main(our: Address, mut state: State) -> anyhow::Result<()> { // if they do time out, we try them again let eth_provider = eth::Provider::new(state.chain_id, 60); + println!( + "subscribing, state.block: {}, chain_id: {}", + state.block, state.chain_id + ); + // if block in state is < current_block, get logs from that part. - #[cfg(not(feature = "simulation-mode"))] if state.block < eth_provider.get_block_number().unwrap_or(u64::MAX) { loop { match eth_provider.get_logs(&filter) { @@ -200,8 +208,11 @@ fn main(our: Address, mut state: State) -> anyhow::Result<()> { } break; } - Err(_) => { - println!("failed to fetch logs! trying again in 5s..."); + Err(e) => { + println!( + "got eth error while fetching logs: {:?}, trying again in 5s...", + e + ); std::thread::sleep(std::time::Duration::from_secs(5)); continue; } diff --git a/kinode/src/eth/default_providers_mainnet.json b/kinode/src/eth/default_providers_mainnet.json index 9f8c00bf5..9eeae49e9 100644 --- a/kinode/src/eth/default_providers_mainnet.json +++ b/kinode/src/eth/default_providers_mainnet.json @@ -1,11 +1,4 @@ [ - { - "chain_id": 31337, - "trusted": true, - "provider": { - "RpcUrl": "wss://localhost:8545" - } - }, { "chain_id": 1, "trusted": false, diff --git a/kinode/src/fakenet/helpers.rs b/kinode/src/fakenet/helpers.rs new file mode 100644 index 000000000..cf543b85e --- /dev/null +++ b/kinode/src/fakenet/helpers.rs @@ -0,0 +1,116 @@ +use alloy_sol_macro::sol; +use sha3::{Digest, Keccak256}; + +sol! { + #[sol(rpc)] + contract RegisterHelpers { + function register( + bytes calldata _name, + address _to, + bytes[] calldata _data + ) external payable returns (uint256 nodeId_); + + function setKey(bytes32 _node, bytes32 _key); + + function setAllIp( + bytes32 _node, + uint128 _ip, + uint16 _ws, + uint16 _wt, + uint16 _tcp, + uint16 _udp + ); + + function ip(bytes32) external view returns (uint128, uint16, uint16, uint16, uint16); + + function ownerOf(uint256 node) returns (address); + + function multicall(bytes[] calldata data); + } +} + +pub fn dns_encode_fqdn(name: &str) -> Vec { + let bytes_name = name.as_bytes(); + let mut dns_name = Vec::new(); + + let mut last_pos = 0; + for (i, &b) in bytes_name.iter().enumerate() { + if b == b'.' { + if i != last_pos { + dns_name.push((i - last_pos) as u8); // length of the label + dns_name.extend_from_slice(&bytes_name[last_pos..i]); + } + last_pos = i + 1; + } + } + + if last_pos < bytes_name.len() { + dns_name.push((bytes_name.len() - last_pos) as u8); + dns_name.extend_from_slice(&bytes_name[last_pos..]); + } + + dns_name.push(0); + + dns_name +} + +pub fn encode_namehash(name: &str) -> [u8; 32] { + let mut node = [0u8; 32]; + if name.is_empty() { + return node; + } + let mut labels: Vec<&str> = name.split('.').collect(); + labels.reverse(); + + for label in labels.iter() { + let mut hasher = Keccak256::new(); + hasher.update(label.as_bytes()); + let labelhash = hasher.finalize(); + hasher = Keccak256::new(); + hasher.update(&node); + hasher.update(labelhash); + node = hasher.finalize().into(); + } + node +} + +#[cfg(test)] +mod test { + use super::encode_namehash; + + #[test] + fn test_namehash() { + // Test cases, same than used @ EIP137 `https://github.com/ethereum/EIPs/blob/master/EIPS/eip-137.md` + let cases = vec![ + ( + "", + &[ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ], + ), + ( + "eth", + &[ + 0x93, 0xcd, 0xeb, 0x70, 0x8b, 0x75, 0x45, 0xdc, 0x66, 0x8e, 0xb9, 0x28, 0x1, + 0x76, 0x16, 0x9d, 0x1c, 0x33, 0xcf, 0xd8, 0xed, 0x6f, 0x4, 0x69, 0xa, 0xb, + 0xcc, 0x88, 0xa9, 0x3f, 0xc4, 0xae, + ], + ), + ( + "foo.eth", + &[ + 0xde, 0x9b, 0x9, 0xfd, 0x7c, 0x5f, 0x90, 0x1e, 0x23, 0xa3, 0xf1, 0x9f, 0xec, + 0xc5, 0x48, 0x28, 0xe9, 0xc8, 0x48, 0x53, 0x98, 0x1, 0xe8, 0x65, 0x91, 0xbd, + 0x98, 0x1, 0xb0, 0x19, 0xf8, 0x4f, + ], + ), + ]; + + for (name, expected_namehash) in cases { + let namehash: &[u8] = &encode_namehash(name); + assert_eq!(namehash, expected_namehash); + } + } +} diff --git a/kinode/src/fakenet/mod.rs b/kinode/src/fakenet/mod.rs new file mode 100644 index 000000000..4ee5ef60a --- /dev/null +++ b/kinode/src/fakenet/mod.rs @@ -0,0 +1,197 @@ +use alloy_consensus::TxLegacy; +use alloy_network::{Transaction, TxKind}; +use alloy_primitives::{Address, Bytes, FixedBytes, B256, U256}; +use alloy_providers::provider::{Provider, TempProvider}; +use alloy_rpc_client::ClientBuilder; +use alloy_rpc_types::request::{TransactionInput, TransactionRequest}; +use alloy_signer::{LocalWallet, Signer, SignerSync}; +use alloy_sol_types::{SolCall, SolValue}; +use alloy_transport_ws::WsConnect; +use lib::core::Identity; +use std::str::FromStr; + +pub mod helpers; + +use crate::{keygen, KNS_ADDRESS}; +pub use helpers::RegisterHelpers::*; +pub use helpers::*; + +const FAKE_DOTDEV: &str = "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9"; + +/// Attempts to connect to a local anvil fakechain, +/// registering a name with its KNS contract. +/// If name is already registered, resets it. +pub async fn register_local( + name: &str, + ws_port: u16, + pubkey: &str, + fakechain_port: u16, +) -> Result<(), anyhow::Error> { + let wallet = LocalWallet::from_str( + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + )?; + + let dotdev = Address::from_str(FAKE_DOTDEV)?; + let kns = Address::from_str(KNS_ADDRESS)?; + + let endpoint = format!("ws://localhost:{}", fakechain_port); + let ws = WsConnect { + url: endpoint, + auth: None, + }; + + let client = ClientBuilder::default().ws(ws).await?; + let provider = Provider::new_with_client(client); + + let fqdn = dns_encode_fqdn(name); + let namehash = encode_namehash(name); + // todo: find a better way? + let namehash_bint: B256 = namehash.into(); + let namehash_uint: U256 = namehash_bint.into(); + + let ip: u128 = 0x7F000001; // localhost IP (127.0.0.1) + + let set_ip = setAllIpCall { + _node: namehash.into(), + _ip: ip, + _ws: ws_port, + _wt: 0, + _tcp: 0, + _udp: 0, + } + .abi_encode(); + + let set_key = setKeyCall { + _node: namehash.into(), + _key: pubkey.parse()?, + } + .abi_encode(); + + let exists_call = ownerOfCall { + node: namehash_uint, + } + .abi_encode(); + + let exists_tx = TransactionRequest::default() + .to(Some(dotdev)) + .input(TransactionInput::new(exists_call.into())); + + let exists = provider.call(exists_tx, None).await; + + let (call_input, to) = match exists { + Err(_e) => { + // name is not taken, register normally + let register = registerCall { + _name: fqdn.into(), + _to: Address::from_str("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")?, + _data: vec![set_ip.into(), set_key.into()], + } + .abi_encode(); + + (register, dotdev) + } + Ok(_owner) => { + // name is taken, call setAllIp an setKey directly with multicall + let set_ip = setAllIpCall { + _node: namehash.into(), + _ip: ip, + _ws: ws_port, + _wt: 0, + _tcp: 0, + _udp: 0, + }; + let set_key = setKeyCall { + _node: namehash.into(), + _key: pubkey.parse()?, + }; + + let multicall = multicallCall { + data: vec![set_ip.abi_encode(), set_key.abi_encode()], + } + .abi_encode(); + + (multicall, kns) + } + }; + let nonce = provider + .get_transaction_count(wallet.address(), None) + .await?; + + let mut tx = TxLegacy { + to: TxKind::Call(to), + nonce: nonce.to::(), + input: call_input.into(), + chain_id: Some(31337), + gas_limit: 3000000, + gas_price: 100000000000, + ..Default::default() + }; + + let sig = wallet.sign_transaction_sync(&mut tx)?; + let signed_tx = tx.into_signed(sig); + let mut buf = vec![]; + signed_tx.encode_signed(&mut buf); + + let _tx_hash = provider.send_raw_transaction(buf.into()).await?; + + Ok(()) +} + +/// Booting from a keyfile, fetches the node's IP data from the KNS contract +/// and assigns it to the Identity struct. +pub async fn assign_ws_local_helper( + our: &mut Identity, + ws_port: u16, + fakechain_port: u16, +) -> Result<(), anyhow::Error> { + let kns = Address::from_str(KNS_ADDRESS)?; + let endpoint = format!("ws://localhost:{}", fakechain_port); + + let ws = WsConnect { + url: endpoint, + auth: None, + }; + + let client = ClientBuilder::default().ws(ws).await?; + let provider = Provider::new_with_client(client); + + let namehash = FixedBytes::<32>::from_slice(&keygen::namehash(&our.name)); + let ip_call = ipCall { _0: namehash }.abi_encode(); + let tx_input = TransactionInput::new(Bytes::from(ip_call)); + let tx = TransactionRequest { + to: Some(kns), + input: tx_input, + ..Default::default() + }; + + let Ok(ip_data) = provider.call(tx, None).await else { + return Err(anyhow::anyhow!("Failed to fetch node IP data from PKI")); + }; + + let Ok((ip, ws, _wt, _tcp, _udp)) = <(u128, u16, u16, u16, u16)>::abi_decode(&ip_data, false) + else { + return Err(anyhow::anyhow!("Failed to decode node IP data from PKI")); + }; + + let node_ip = format!( + "{}.{}.{}.{}", + (ip >> 24) & 0xFF, + (ip >> 16) & 0xFF, + (ip >> 8) & 0xFF, + ip & 0xFF + ); + + if node_ip != *"0.0.0.0" || ws != 0 { + // direct node + if ws_port != ws { + return Err(anyhow::anyhow!( + "Binary used --ws-port flag to set port to {}, but node is using port {} onchain.", + ws_port, + ws + )); + } + + our.ws_routing = Some((node_ip, ws)); + } + Ok(()) +} diff --git a/kinode/src/keygen.rs b/kinode/src/keygen.rs index 0d293f1d9..3e7d40e0d 100644 --- a/kinode/src/keygen.rs +++ b/kinode/src/keygen.rs @@ -2,7 +2,6 @@ use aes_gcm::{ aead::{Aead, AeadCore, KeyInit, OsRng}, Aes256Gcm, Key, }; -#[cfg(not(feature = "simulation-mode"))] use alloy_primitives::keccak256; use anyhow::Result; use digest::generic_array::GenericArray; @@ -139,7 +138,6 @@ pub fn get_username_and_routers(keyfile: &[u8]) -> Result<(String, Vec), Ok((username, routers)) } -#[cfg(not(feature = "simulation-mode"))] pub fn namehash(name: &str) -> Vec { let mut node = vec![0u8; 32]; if name.is_empty() { diff --git a/kinode/src/main.rs b/kinode/src/main.rs index 65f9ef7ae..68292d3af 100644 --- a/kinode/src/main.rs +++ b/kinode/src/main.rs @@ -10,6 +10,8 @@ use std::sync::Arc; use tokio::sync::mpsc; mod eth; +#[cfg(feature = "simulation-mode")] +mod fakenet; mod http; mod kernel; mod keygen; @@ -38,9 +40,13 @@ const VERSION: &str = env!("CARGO_PKG_VERSION"); /// default routers as a eth-provider fallback const DEFAULT_ETH_PROVIDERS: &str = include_str!("eth/default_providers_mainnet.json"); #[cfg(not(feature = "simulation-mode"))] -const CHAIN_ID: u64 = 10; +pub const CHAIN_ID: u64 = 10; +#[cfg(feature = "simulation-mode")] +pub const CHAIN_ID: u64 = 31337; +#[cfg(not(feature = "simulation-mode"))] +pub const KNS_ADDRESS: &str = "0xca5b5811c0c40aab3295f932b1b5112eb7bb4bd6"; #[cfg(feature = "simulation-mode")] -const CHAIN_ID: u64 = 31337; +pub const KNS_ADDRESS: &str = "0x5FbDB2315678afecb367f032d93F642f64180aa3"; #[tokio::main] async fn main() { @@ -56,16 +62,18 @@ async fn main() { let verbose_mode = *matches .get_one::("verbosity") .expect("verbosity required"); + let rpc = matches.get_one::("rpc"); // if we are in sim-mode, detached determines whether terminal is interactive #[cfg(not(feature = "simulation-mode"))] let is_detached = false; #[cfg(feature = "simulation-mode")] - let (password, fake_node_name, is_detached) = ( + let (password, fake_node_name, is_detached, fakechain_port) = ( matches.get_one::("password"), matches.get_one::("fake-node-name"), *matches.get_one::("detached").unwrap(), + matches.get_one::("fakechain-port").cloned(), ); // default eth providers/routers @@ -77,7 +85,7 @@ async fn main() { } Err(_) => serde_json::from_str(DEFAULT_ETH_PROVIDERS).unwrap(), }; - if let Some(rpc) = matches.get_one::("rpc") { + if let Some(rpc) = rpc { eth_provider_config.push(lib::eth::ProviderConfig { chain_id: CHAIN_ID, trusted: true, @@ -92,6 +100,22 @@ async fn main() { .expect("failed to save new eth provider config!"); } + #[cfg(feature = "simulation-mode")] + { + let local_chain_port = matches + .get_one::("fakechain-port") + .cloned() + .unwrap_or(8545); + eth_provider_config.push(lib::eth::ProviderConfig { + chain_id: 31337, + trusted: true, + provider: lib::eth::NodeOrRpcUrl::RpcUrl(format!( + "ws://localhost:{}", + local_chain_port + )), + }); + } + // kernel receives system messages via this channel, all other modules send messages let (kernel_message_sender, kernel_message_receiver): (MessageSender, MessageReceiver) = mpsc::channel(EVENT_LOOP_CHANNEL_CAPACITY); @@ -135,146 +159,28 @@ async fn main() { let (print_sender, print_receiver): (PrintSender, PrintReceiver) = mpsc::channel(TERMINAL_CHANNEL_CAPACITY); - let our; - #[cfg(not(feature = "simulation-mode"))] - let our_ip: std::net::Ipv4Addr; - let encoded_keyfile; - let decoded_keyfile; - - #[cfg(not(feature = "simulation-mode"))] - { - println!("finding public IP address..."); - our_ip = { - if let Ok(Some(ip)) = - tokio::time::timeout(std::time::Duration::from_secs(5), public_ip::addr_v4()).await - { - ip - } else { - println!("failed to find public IPv4 address: booting as a routed node"); - std::net::Ipv4Addr::LOCALHOST - } - }; - - // if the --ws-port flag is used, bind to that port right away. - // if the flag is not used, find the first available port between 9000 and 65535. - // NOTE: if the node has a different port specified in its onchain (direct) id, - // booting will fail if the flag was used to select a different port. - // if the flag was not used, the bound port will be dropped in favor of the onchain port. - - #[cfg(not(feature = "simulation-mode"))] - let (ws_tcp_handle, flag_used) = if let Some(port) = ws_networking_port { - ( - http::utils::find_open_port(*port, port + 1) - .await - .expect("ws-port selected with flag could not be bound!"), - true, - ) - } else { - ( - http::utils::find_open_port(9000, 65535) - .await - .expect("no ports found in range 9000-65535 for websocket server!"), - false, - ) - }; - - println!( - "WS networking at port {}: if a different port should be used, boot with --ws-port\r", - ws_tcp_handle.local_addr().unwrap().port() - ); - - println!("login or register at http://localhost:{http_server_port}\r"); - - (our, encoded_keyfile, decoded_keyfile) = serve_register_fe( - home_directory_path, - our_ip.to_string(), - (ws_tcp_handle, flag_used), - http_server_port, - matches.get_one::("rpc").cloned(), - ) - .await; - } + let our_ip = find_public_ip().await; + let (wc_tcp_handle, flag_used) = setup_ws_networking(ws_networking_port.cloned()).await; #[cfg(feature = "simulation-mode")] - match fake_node_name { - None => { - match password { - None => { - panic!("Fake node must be booted with either a --fake-node-name, --password, or both."); - } - Some(password) => { - match tokio::fs::read(format!("{}/.keys", home_directory_path)).await { - Err(e) => panic!("could not read keyfile: {}", e), - Ok(keyfile) => { - match keygen::decode_keyfile(&keyfile, &password) { - Err(e) => panic!("could not decode keyfile: {}", e), - Ok(decoded) => { - our = Identity { - name: decoded.username.clone(), - networking_key: format!( - "0x{}", - hex::encode( - decoded.networking_keypair.public_key().as_ref() - ) - ), - ws_routing: None, // TODO - allowed_routers: decoded.routers.clone(), - }; - decoded_keyfile = decoded; - encoded_keyfile = keyfile; - } - } - } - } - } - } - } - Some(name) => { - let password_hash = match password { - None => "secret".to_string(), - Some(password) => password.to_string(), - }; - let (pubkey, networking_keypair) = keygen::generate_networking_key(); - - let seed = SystemRandom::new(); - let mut jwt_secret = [0u8, 32]; - ring::rand::SecureRandom::fill(&seed, &mut jwt_secret).unwrap(); - - our = Identity { - name: name.clone(), - networking_key: pubkey, - ws_routing: None, - allowed_routers: vec![], - }; - - decoded_keyfile = Keyfile { - username: name.clone(), - routers: vec![], - networking_keypair: signature::Ed25519KeyPair::from_pkcs8( - networking_keypair.as_ref(), - ) - .unwrap(), - jwt_secret_bytes: jwt_secret.to_vec(), - file_key: keygen::generate_file_key(), - }; - - encoded_keyfile = keygen::encode_keyfile( - password_hash, - name.clone(), - decoded_keyfile.routers.clone(), - networking_keypair.as_ref(), - &decoded_keyfile.jwt_secret_bytes, - &decoded_keyfile.file_key, - ); + let (our, encoded_keyfile, decoded_keyfile) = simulate_node( + fake_node_name.cloned(), + password.cloned(), + home_directory_path, + (wc_tcp_handle, flag_used), + fakechain_port, + ) + .await; - tokio::fs::write( - format!("{}/.keys", home_directory_path), - encoded_keyfile.clone(), - ) - .await - .unwrap(); - } - }; + #[cfg(not(feature = "simulation-mode"))] + let (our, encoded_keyfile, decoded_keyfile) = serve_register_fe( + &home_directory_path, + our_ip.to_string(), + (wc_tcp_handle, flag_used), + http_server_port, + rpc.cloned(), + ) + .await; // the boolean flag determines whether the runtime module is *public* or not, // where public means that any process can always message it. @@ -378,7 +284,6 @@ async fn main() { }) .collect(), )); - #[cfg(not(feature = "simulation-mode"))] tasks.spawn(net::ws::networking( our.clone(), our_ip.to_string(), @@ -388,18 +293,8 @@ async fn main() { print_sender.clone(), net_message_sender, net_message_receiver, - register::KNS_OPTIMISM_ADDRESS.to_string(), *matches.get_one::("reveal-ip").unwrap_or(&true), )); - #[cfg(feature = "simulation-mode")] - tasks.spawn(net::mock::mock_client( - *ws_networking_port.unwrap_or(&9000), - our.name.clone(), - kernel_message_sender.clone(), - net_message_receiver, - print_sender.clone(), - network_error_sender, - )); tasks.spawn(state::state_sender( our.name.clone(), kernel_message_sender.clone(), @@ -570,6 +465,123 @@ async fn set_http_server_port(set_port: Option<&u16>) -> u16 { } } +/// Sets up WebSocket networking by finding an open port and creating a TCP listener. +/// If a specific port is provided, it attempts to bind to it directly. +/// If no port is provided, it searches for the first available port between 9000 and 65535. +/// Returns a tuple containing the TcpListener and a boolean indicating if a specific port was used. +async fn setup_ws_networking(ws_networking_port: Option) -> (tokio::net::TcpListener, bool) { + match ws_networking_port { + Some(port) => { + let listener = http::utils::find_open_port(port, port + 1) + .await + .expect("ws-port selected with flag could not be bound"); + (listener, true) + } + None => { + let listener = http::utils::find_open_port(9000, 65535) + .await + .expect("no ports found in range 9000-65535 for websocket server"); + (listener, false) + } + } +} + +/// On simulation mode, we either boot from existing keys, or generate and post keys to chain. +#[cfg(feature = "simulation-mode")] +pub async fn simulate_node( + fake_node_name: Option, + password: Option, + home_directory_path: &str, + (ws_networking, _ws_used): (tokio::net::TcpListener, bool), + fakechain_port: Option, +) -> (Identity, Vec, Keyfile) { + match fake_node_name { + None => { + match password { + None => { + panic!("Fake node must be booted with either a --fake-node-name, --password, or both."); + } + Some(password) => { + let keyfile = tokio::fs::read(format!("{}/.keys", home_directory_path)) + .await + .expect("could not read keyfile"); + let decoded = keygen::decode_keyfile(&keyfile, &password) + .expect("could not decode keyfile"); + let mut identity = Identity { + name: decoded.username.clone(), + networking_key: format!( + "0x{}", + hex::encode(decoded.networking_keypair.public_key().as_ref()) + ), + ws_routing: None, + allowed_routers: decoded.routers.clone(), + }; + + fakenet::assign_ws_local_helper( + &mut identity, + ws_networking.local_addr().unwrap().port(), + fakechain_port.unwrap_or(8545), + ) + .await + .unwrap(); + + (identity, keyfile, decoded) + } + } + } + Some(name) => { + let password_hash = password.unwrap_or_else(|| "secret".to_string()); + let (pubkey, networking_keypair) = keygen::generate_networking_key(); + let seed = SystemRandom::new(); + let mut jwt_secret = [0u8; 32]; + ring::rand::SecureRandom::fill(&seed, &mut jwt_secret).unwrap(); + + let fakechain_port: u16 = fakechain_port.unwrap_or(8545); + let ws_port = ws_networking.local_addr().unwrap().port(); + + fakenet::register_local(&name, ws_port, &pubkey, fakechain_port) + .await + .unwrap(); + + let identity = Identity { + name: name.clone(), + networking_key: pubkey, + ws_routing: Some(("127.0.0.1".into(), ws_port)), + allowed_routers: vec![], + }; + + let decoded_keyfile = Keyfile { + username: name.clone(), + routers: vec![], + networking_keypair: signature::Ed25519KeyPair::from_pkcs8( + networking_keypair.as_ref(), + ) + .unwrap(), + jwt_secret_bytes: jwt_secret.to_vec(), + file_key: keygen::generate_file_key(), + }; + + let encoded_keyfile = keygen::encode_keyfile( + password_hash, + name.clone(), + decoded_keyfile.routers.clone(), + networking_keypair.as_ref(), + &decoded_keyfile.jwt_secret_bytes, + &decoded_keyfile.file_key, + ); + + tokio::fs::write( + format!("{}/.keys", home_directory_path), + encoded_keyfile.clone(), + ) + .await + .expect("Failed to write keyfile"); + + (identity, encoded_keyfile, decoded_keyfile) + } + } +} + async fn create_home_directory(home_directory_path: &str) { if let Err(e) = tokio::fs::create_dir_all(home_directory_path).await { panic!("failed to create home directory: {:?}", e); @@ -579,7 +591,6 @@ async fn create_home_directory(home_directory_path: &str) { /// build the command line interface for kinode /// -/// TODO: add arg for fakechain bootup w/ kit? fn build_command() -> Command { let app = Command::new("kinode") .version(VERSION) @@ -592,7 +603,7 @@ fn build_command() -> Command { ) .arg( arg!(--"ws-port" "Kinode internal WebSockets protocol port [default: first unbound at or above 9000]") - .alias("network-router-port") + .alias("--ws-port") .value_parser(value_parser!(u16)), ) .arg( @@ -611,6 +622,10 @@ fn build_command() -> Command { let app = app .arg(arg!(--password "Networking password")) .arg(arg!(--"fake-node-name" "Name of fake node to boot")) + .arg( + arg!(--"fakechain-port" "Port to bind to for fakechain") + .value_parser(value_parser!(u16)), + ) .arg( arg!(--detached "Run in detached mode (don't accept input)") .action(clap::ArgAction::SetTrue), @@ -618,6 +633,31 @@ fn build_command() -> Command { app } +/// Attempts to find the public IPv4 address of the node. +/// If in simulation mode, it immediately returns localhost. +/// Otherwise, it tries to find the public IP and defaults to localhost on failure. +async fn find_public_ip() -> std::net::Ipv4Addr { + #[cfg(feature = "simulation-mode")] + { + std::net::Ipv4Addr::LOCALHOST + } + + #[cfg(not(feature = "simulation-mode"))] + { + println!("Finding public IP address..."); + match tokio::time::timeout(std::time::Duration::from_secs(5), public_ip::addr_v4()).await { + Ok(Some(ip)) => { + println!("Public IP found: {}", ip); + ip + } + _ => { + println!("Failed to find public IPv4 address: booting as a routed node."); + std::net::Ipv4Addr::LOCALHOST + } + } + } +} + /// check if we have keys saved on disk, encrypted /// if so, prompt user for "password" to decrypt with /// diff --git a/kinode/src/net/mock.rs b/kinode/src/net/mock.rs deleted file mode 100644 index d0dfa62e5..000000000 --- a/kinode/src/net/mock.rs +++ /dev/null @@ -1,85 +0,0 @@ -use futures::{SinkExt, StreamExt}; -use tokio::sync::mpsc; -use tokio_tungstenite::{ - connect_async, - tungstenite::protocol::Message::{Binary, Text}, -}; - -use lib::types::core as types; - -type Sender = mpsc::Sender; -type Receiver = mpsc::Receiver; - -pub async fn mock_client( - port: u16, - node_identity: types::NodeId, - send_to_loop: Sender, - mut recv_from_loop: Receiver, - print_tx: types::PrintSender, - _network_error_sender: types::NetworkErrorSender, -) -> anyhow::Result<()> { - let url = format!("ws://127.0.0.1:{}", port); - - let (ws_stream, _) = connect_async(url).await?; - let (mut send_to_ws, mut recv_from_ws) = ws_stream.split(); - - // Send node identity - send_to_ws.send(Text(node_identity.clone())).await?; - - loop { - tokio::select! { - Some(kernel_message) = recv_from_loop.recv() => { - if kernel_message.target.node != node_identity { - // Serialize and send the message through WebSocket - // println!("{}:mock: outgoing {}\r", node_identity ,kernel_message); - let message = Binary(rmp_serde::to_vec(&kernel_message)?); - send_to_ws.send(message).await?; - } - }, - Some(Ok(message)) = recv_from_ws.next() => { - // Deserialize and forward the message to the loop - // println!("{}:mock: incoming {}\r", node_identity, message); - if let Binary(ref bin) = message { - let km: types::KernelMessage = rmp_serde::from_slice(bin)?; - if km.target.process == "net:distro:sys" { - if let types::Message::Request(types::Request { ref body, .. }) = km.message { - print_tx - .send(types::Printout { - verbosity: 0, - content: format!( - "\x1b[3;32m{}: {}\x1b[0m", - km.source.node, - std::str::from_utf8(body).unwrap_or("!!message parse error!!") - ), - }) - .await?; - send_to_loop - .send(types::KernelMessage { - id: km.id, - source: types::Address { - node: node_identity.clone(), - process: types::ProcessId::new(Some("net"), "distro", "sys"), - }, - target: km.rsvp.as_ref().unwrap_or(&km.source).clone(), - rsvp: None, - message: types::Message::Response(( - types::Response { - inherit: false, - body: "delivered".as_bytes().to_vec(), - metadata: None, - capabilities: vec![], - }, - None, - )), - lazy_load_blob: None, - }) - .await?; - } - } else { - send_to_loop.send(km).await?; - } - } - }, - } - } -} diff --git a/kinode/src/net/mod.rs b/kinode/src/net/mod.rs index 1baf14d6a..8c137f44c 100644 --- a/kinode/src/net/mod.rs +++ b/kinode/src/net/mod.rs @@ -1,9 +1,3 @@ -#[cfg(feature = "simulation-mode")] -pub mod mock; - -#[cfg(not(feature = "simulation-mode"))] pub mod types; -#[cfg(not(feature = "simulation-mode"))] pub mod utils; -#[cfg(not(feature = "simulation-mode"))] pub mod ws; diff --git a/kinode/src/net/ws.rs b/kinode/src/net/ws.rs index 1be934dd5..9b8ab9b46 100644 --- a/kinode/src/net/ws.rs +++ b/kinode/src/net/ws.rs @@ -15,6 +15,7 @@ use { use crate::net::types::*; use crate::net::utils::*; +use crate::KNS_ADDRESS; use lib::types::core::*; /// only used in connection initialization, otherwise, nacks and Responses are only used for "timeouts" @@ -34,7 +35,6 @@ pub async fn networking( print_tx: PrintSender, self_message_tx: MessageSender, message_rx: MessageReceiver, - contract_address: String, reveal_ip: bool, ) -> Result<()> { // branch on whether we are a direct or indirect node @@ -57,7 +57,6 @@ pub async fn networking( self_message_tx, message_rx, reveal_ip, - contract_address, ) .await } @@ -95,7 +94,6 @@ pub async fn networking( print_tx, self_message_tx, message_rx, - contract_address, ) .await } @@ -112,7 +110,6 @@ async fn indirect_networking( _self_message_tx: MessageSender, mut message_rx: MessageReceiver, reveal_ip: bool, - contract_address: String, ) -> Result<()> { print_debug(&print_tx, "net: starting as indirect").await; let pki: OnchainPKI = Arc::new(DashMap::new()); @@ -148,7 +145,6 @@ async fn indirect_networking( names.clone(), &kernel_message_tx, &print_tx, - &contract_address, ) .await { Ok(()) => continue, @@ -283,7 +279,6 @@ async fn direct_networking( print_tx: PrintSender, _self_message_tx: MessageSender, mut message_rx: MessageReceiver, - contract_address: String, ) -> Result<()> { print_debug(&print_tx, "net: starting as direct").await; let pki: OnchainPKI = Arc::new(DashMap::new()); @@ -318,7 +313,6 @@ async fn direct_networking( names.clone(), &kernel_message_tx, &print_tx, - &contract_address, ) .await { Ok(()) => continue, @@ -855,7 +849,6 @@ async fn handle_local_message( names: PKINames, kernel_message_tx: &MessageSender, print_tx: &PrintSender, - contract_address: &str, ) -> Result<()> { print_debug(print_tx, "net: handling local message").await; let body = match km.message { @@ -1019,7 +1012,7 @@ async fn handle_local_message( let mut printout = String::new(); printout.push_str(&format!( "indexing from contract address {}\r\n", - contract_address + KNS_ADDRESS )); printout.push_str(&format!("our Identity: {:#?}\r\n", our)); printout.push_str("we have connections with peers:\r\n"); diff --git a/kinode/src/register.rs b/kinode/src/register.rs index f1b12e825..e0668c137 100644 --- a/kinode/src/register.rs +++ b/kinode/src/register.rs @@ -1,4 +1,5 @@ use crate::keygen; +use crate::KNS_ADDRESS; use alloy_primitives::{Address as EthAddress, Bytes, FixedBytes, U256}; use alloy_providers::provider::{Provider, TempProvider}; use alloy_pubsub::PubSubFrontend; @@ -28,16 +29,6 @@ use warp::{ type RegistrationSender = mpsc::Sender<(Identity, Keyfile, Vec)>; -// pub const KNS_SEPOLIA_ADDRESS: EthAddress = EthAddress::new([ -// 0x38, 0x07, 0xFB, 0xD6, 0x92, 0xAa, 0x5c, 0x96, 0xF1, 0xD8, 0xD7, 0xc5, 0x9a, 0x13, 0x46, 0xa8, -// 0x85, 0xF4, 0x0B, 0x1C, -// ]); - -pub const KNS_OPTIMISM_ADDRESS: EthAddress = EthAddress::new([ - 0xca, 0x5b, 0x58, 0x11, 0xc0, 0xC4, 0x0a, 0xAB, 0x32, 0x95, 0xf9, 0x32, 0xb1, 0xB5, 0x11, 0x2E, - 0xb7, 0xbb, 0x4b, 0xD6, -]); - sol! { function auth( bytes32 _node, @@ -126,7 +117,7 @@ pub async fn register( }); // KnsRegistrar contract address - let kns_address = KNS_OPTIMISM_ADDRESS; + let kns_address = EthAddress::from_str(KNS_ADDRESS).unwrap(); // This ETH provider uses public rpc endpoints to verify registration signatures. let url = if let Some(rpc_url) = maybe_rpc { @@ -690,7 +681,7 @@ async fn confirm_change_network_keys( success_response(sender, our.clone(), decoded_keyfile, encoded_keyfile).await } -async fn assign_ws_routing( +pub async fn assign_ws_routing( our: &mut Identity, kns_address: EthAddress, provider: Arc>, @@ -737,7 +728,6 @@ async fn assign_ws_routing( } Ok(()) } - async fn success_response( sender: Arc, our: Identity,