diff --git a/.gitignore b/.gitignore index c9665775..e2e72cfe 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ crates/sp1-interactive-fraud-proof/*.bin # e2e 1-lighthouse-geth-0-63 el_cl_genesis_data +*.db \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 8e8722e7..5bbe85fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2056,6 +2056,26 @@ dependencies = [ "serde", ] +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + [[package]] name = "bindgen" version = "0.70.1" @@ -7169,6 +7189,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" +[[package]] +name = "redb" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34bc6763177194266fc3773e2b2bb3693f7b02fdf461e285aa33202e3164b74e" +dependencies = [ + "libc", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -8269,7 +8298,7 @@ version = "4.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed3585aebe731e76b8f3549cccbaff34de3a58cbf22af074445a168941ef5d18" dependencies = [ - "bincode", + "bincode 1.3.3", "bytemuck", "clap", "elf", @@ -8308,7 +8337,7 @@ version = "4.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "430d6534e76cbd20e01b066c290bfce7663be5230a9c83cef49a5390c2af89e6" dependencies = [ - "bincode", + "bincode 1.3.3", "cbindgen", "cc", "cfg-if", @@ -8364,7 +8393,7 @@ version = "4.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a87155cf3b2ce1021150b02114b02f0f9c5f2e9304061b667e7de2beb22ab016" dependencies = [ - "bincode", + "bincode 1.3.3", "ctrlc", "prost", "serde", @@ -8413,7 +8442,7 @@ version = "4.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4b2747ae411bca4ba0dd4112779246b101ac7e8f70afc1a33cf8ee003980d7" dependencies = [ - "bincode", + "bincode 1.3.3", "elliptic-curve 0.13.8", "serde", "sp1-primitives", @@ -8425,7 +8454,7 @@ version = "4.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7939f86891aa5995fa863abf296645a5ad4611d9598a8101389f85c624d41043" dependencies = [ - "bincode", + "bincode 1.3.3", "hex", "lazy_static", "num-bigint 0.4.6", @@ -8444,7 +8473,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3414cf7ef669ae417adfb0e78f871f9bb9c04bc208243645bcc8ca4437cd7c63" dependencies = [ "anyhow", - "bincode", + "bincode 1.3.3", "clap", "dirs 5.0.1", "downloader", @@ -8599,7 +8628,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc32b4b8833f849a7083f204261403711e27c9cfb958c95ad06af6666361bdd3" dependencies = [ "anyhow", - "bincode", + "bincode 1.3.3", "bindgen", "cc", "cfg-if", @@ -8631,7 +8660,7 @@ dependencies = [ "anyhow", "async-trait", "backoff", - "bincode", + "bincode 1.3.3", "cfg-if", "dirs 5.0.1", "futures", @@ -9346,6 +9375,32 @@ dependencies = [ "types", ] +[[package]] +name = "taiyi-challenger" +version = "0.1.5" +dependencies = [ + "alloy-contract", + "alloy-eips 0.12.6", + "alloy-primitives 0.8.25", + "alloy-provider 0.12.6", + "alloy-signer 0.12.6", + "alloy-signer-local 0.12.6", + "alloy-sol-types", + "bincode 2.0.1", + "clap", + "ethereum-consensus", + "eyre", + "futures-util", + "redb", + "reqwest 0.12.9", + "reqwest-eventsource", + "serde_json", + "taiyi-primitives", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "taiyi-cli" version = "0.1.0" @@ -10541,6 +10596,12 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "url" version = "2.5.4" @@ -10625,6 +10686,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + [[package]] name = "vsss-rs" version = "4.3.8" diff --git a/Cargo.toml b/Cargo.toml index 3c9e6a1b..a2981eed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" members = [ "bin/taiyi", "bin/taiyi-boost", + "bin/taiyi-challenger", "bin/taiyi-cli", "bin/taiyi-fraud-proof-cli", "bin/taiyi-underwriter-monitor", @@ -27,6 +28,7 @@ default-members = [ "bin/taiyi", "bin/taiyi-boost", "bin/taiyi-cli", + "bin/taiyi-challenger", "bin/taiyi-fraud-proof-cli", "bin/taiyi-underwriter-monitor", "crates/cli", @@ -153,6 +155,8 @@ ssz_types = "0.10" http-body-util = "0.1.2" tower = "0.5.2" sha2 = "0.10.8" +redb = "2.4.0" +bincode = "2.0.1" sqlx = { default-features = true, version = "*", features = [ "runtime-tokio-rustls", "postgres", diff --git a/bin/taiyi-challenger/Cargo.toml b/bin/taiyi-challenger/Cargo.toml new file mode 100644 index 00000000..4e2c3fb5 --- /dev/null +++ b/bin/taiyi-challenger/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "taiyi-challenger" +version = { workspace = true } +edition = { workspace = true } + +[dependencies] +alloy-contract = { workspace = true } +alloy-eips = { workspace = true } +alloy-primitives = { workspace = true } +alloy-provider = { workspace = true } +alloy-signer = { workspace = true } +alloy-signer-local = { workspace = true } +alloy-sol-types = { workspace = true } +bincode = { workspace = true } + +tracing = { workspace = true } +tracing-subscriber = { workspace = true } + +clap = { workspace = true } +ethereum-consensus = { workspace = true } +eyre = { workspace = true } +futures-util = { workspace = true } +redb = { workspace = true } +reqwest = { workspace = true } +reqwest-eventsource = "0.6" +serde_json = { workspace = true } +taiyi-primitives = { workspace = true } +tokio = { workspace = true } + +[[bin]] +name = "taiyi-challenger" +path = "src/main.rs" diff --git a/bin/taiyi-challenger/src/handle_challenge_creation.rs b/bin/taiyi-challenger/src/handle_challenge_creation.rs new file mode 100644 index 00000000..923c4225 --- /dev/null +++ b/bin/taiyi-challenger/src/handle_challenge_creation.rs @@ -0,0 +1,200 @@ +use std::{collections::HashSet, sync::Arc}; + +use alloy_eips::BlockNumberOrTag; +use alloy_provider::{Provider, ProviderBuilder, WsConnect}; +use futures_util::StreamExt; +use redb::Database; +use taiyi_primitives::{PreconfRequestTypeA, PreconfRequestTypeB}; +use tracing::{debug, error}; + +use crate::{ + get_slot_from_timestamp, + preconf_request_data::PreconfRequestData, + table_definitions::{CHALLENGE_TABLE, PRECONF_TABLE}, + Opts, +}; + +/// Store a challenge in the database for a given slot +fn store_challenge( + challenge_db: &Database, + submission_slot: u64, + preconf: PreconfRequestData, +) -> Result<(), redb::Error> { + let read_tx = challenge_db.begin_read()?; + + let table = read_tx.open_table(CHALLENGE_TABLE)?; + + // Get existing challenges or create a new vec + let mut challenges_data = match table.get(&submission_slot)? { + Some(val) => val.value(), + None => Vec::new(), + }; + + // Add the new preconf + challenges_data.push(preconf); + + // Write the updated challenges + let write_tx = challenge_db.begin_write()?; + { + let mut table = write_tx.open_table(CHALLENGE_TABLE)?; + table.insert(&submission_slot, challenges_data)?; + } + write_tx.commit()?; + + Ok(()) +} + +pub async fn handle_challenge_creation( + preconf_db: Arc, + challenge_db: Arc, + opts: Arc, + genesis_timestamp: u64, +) -> eyre::Result<()> { + // Create a ws provider + let ws = WsConnect::new(&opts.execution_client_ws_url); + let provider = match ProviderBuilder::new().on_ws(ws).await { + Ok(provider) => provider, + Err(e) => { + error!("[Challenger Creator]: Failed to create provider: {}", e); + return Err(eyre::eyre!("Failed to create provider: {}", e)); + } + }; + + // Subscribe to block headers. + let subscription = provider.subscribe_blocks().await?; + let mut stream = subscription.into_stream(); + + while let Some(header) = stream.next().await { + debug!("[Challenger Creator]: Processing block {:?}", header.number); + let slot = get_slot_from_timestamp(header.timestamp, genesis_timestamp); + debug!("[Challenger Creator]: Slot: {:?}", slot); + + // Check if preconfirmations exists for the slot + let read_tx = match preconf_db.begin_read() { + Ok(tx) => tx, + Err(e) => { + error!("[Challenger Creator]: Failed to begin read transaction: {}", e); + continue; + } + }; + + let table = match read_tx.open_table(PRECONF_TABLE) { + Ok(table) => table, + Err(e) => { + error!("[Challenger Creator]: Failed to open preconf table: {}", e); + continue; + } + }; + + let preconfs = match table.get(&slot) { + Ok(Some(val)) => val.value(), + Ok(None) => { + debug!("[Challenger Creator]: No preconfirmations found for slot {}", slot); + continue; + } + Err(e) => { + error!("[Challenger Creator]: Storage error for slot {}. Error: {:?}", slot, e); + continue; + } + }; + + debug!("[Challenger Creator]: Found {} preconfirmations for slot {}", preconfs.len(), slot); + + let block = + match provider.get_block_by_number(BlockNumberOrTag::Number(header.number)).await { + Ok(Some(b)) => b, + Ok(None) => { + error!("[Challenger Creator]: Block {} not found", header.number); + continue; + } + Err(e) => { + error!("[Challenger Creator]: Failed to get block {}: {}", header.number, e); + continue; + } + }; + + let tx_hashes = block.transactions.hashes().collect::>(); + + // Calculate the challenge submission slot. We need to wait for the block to be finalized + // before we can open a challenge. + let challenge_submission_slot = slot + opts.finalization_window; + + // For each preconfirmation, check if the required txs are included in the block + for preconf in preconfs { + let preconf_type = preconf.preconf_type; + + if preconf_type == 0 { + // Type A + let preconf_request = + match serde_json::from_str::(&preconf.preconf_request) { + Ok(req) => req, + Err(e) => { + error!( + "[Challenger Creator]: Failed to parse PreconfRequestTypeA: {}", + e + ); + continue; + } + }; + + let mut open_challenge = false; + + // Check if all user txs are included in the block and if the tip transaction is included in the block + if !preconf_request.preconf_tx.iter().all(|tx| tx_hashes.contains(tx.tx_hash())) + || !tx_hashes.contains(preconf_request.tip_transaction.tx_hash()) + { + open_challenge = true; + } + + if open_challenge || opts.always_open_challenges { + if let Err(e) = + store_challenge(&challenge_db, challenge_submission_slot, preconf) + { + error!("[Challenger Creator]: Failed to write challenge data: {}", e); + continue; + } + + debug!( + "[Challenger Creator]: Stored challenge for slot {}", + challenge_submission_slot + ); + } + } else { + // Type B + let preconf_request = + match serde_json::from_str::(&preconf.preconf_request) { + Ok(req) => req, + Err(e) => { + error!( + "[Challenger Creator]: Failed to parse PreconfRequestTypeB: {}", + e + ); + continue; + } + }; + + let transaction = match &preconf_request.transaction { + Some(tx) => tx, + None => { + error!("[Challenger Creator]: Missing transaction in PreconfRequestTypeB"); + continue; + } + }; + + // Check if all user txs are included in the block + if !tx_hashes.contains(transaction.tx_hash()) || opts.always_open_challenges { + if let Err(e) = store_challenge(&challenge_db, slot, preconf) { + error!("[Challenger Creator]: Failed to write challenge data: {}", e); + continue; + } + + debug!("[Challenger Creator]: Stored challenge for slot {}", slot); + } + } + } + + debug!("[Challenger Creator]: Processed block {:?}", header.number); + } + + Ok(()) +} diff --git a/bin/taiyi-challenger/src/handle_challenge_submission.rs b/bin/taiyi-challenger/src/handle_challenge_submission.rs new file mode 100644 index 00000000..54c7acd7 --- /dev/null +++ b/bin/taiyi-challenger/src/handle_challenge_submission.rs @@ -0,0 +1,277 @@ +use std::sync::Arc; + +use alloy_eips::eip2718::Encodable2718; +use alloy_primitives::{hex, Bytes, U256}; +use alloy_provider::{Provider, ProviderBuilder, WsConnect}; +use alloy_signer::k256::{self}; +use alloy_sol_types::sol; +use futures_util::StreamExt; +use redb::Database; +use taiyi_primitives::{PreconfRequestTypeA, PreconfRequestTypeB}; +use tracing::{debug, error}; + +use crate::{get_slot_from_timestamp, table_definitions::CHALLENGE_TABLE, Opts}; + +sol! { + #[sol(rpc)] + contract TaiyiInteractiveChallenger { + #[derive(Debug)] + struct PreconfRequestAType { + string[] txs; + string tipTx; + uint256 slot; + uint256 sequenceNum; + address signer; + } + + #[derive(Debug)] + struct BlockspaceAllocation { + uint256 gasLimit; + address sender; + address recipient; + uint256 deposit; + uint256 tip; + uint256 targetSlot; + uint256 blobCount; + } + + #[derive(Debug)] + struct PreconfRequestBType { + BlockspaceAllocation blockspaceAllocation; + bytes blockspaceAllocationSignature; + bytes underwriterSignedBlockspaceAllocation; + bytes rawTx; + bytes underwriterSignedRawTx; + } + + + #[derive(Debug)] + function createChallengeAType( + PreconfRequestAType calldata preconfRequestAType, + bytes calldata signature + ) + external + payable; + + #[derive(Debug)] + function createChallengeBType( + PreconfRequestBType calldata preconfRequestBType, + bytes calldata signature + ) + external + payable; + + #[derive(Debug)] + function resolveExpiredChallenge(bytes32 id) external; + } +} + +pub async fn handle_challenge_submission( + challenge_db: Arc, + opts: Arc, + genesis_timestamp: u64, +) -> eyre::Result<()> { + // Initialize signer + let private_key_bytes = + hex::decode(opts.private_key.strip_prefix("0x").unwrap_or(&opts.private_key))?; + + let signing_key = k256::ecdsa::SigningKey::from_slice(&private_key_bytes)?; + + let private_key_signer = alloy_signer_local::PrivateKeySigner::from_signing_key(signing_key); + let signer_address = private_key_signer.address(); + + debug!("[Challenger Submitter]: Signer address: {}", signer_address); + + let ws = WsConnect::new(&opts.execution_client_ws_url); + let provider = ProviderBuilder::new().wallet(private_key_signer).on_ws(ws).await?; + + let taiyi_challenger = + TaiyiInteractiveChallenger::new(opts.taiyi_challenger_address, provider.clone()); + + let subscription = provider.subscribe_blocks().await?; + + let mut stream = subscription.into_stream(); + + while let Some(header) = stream.next().await { + debug!("[Challenger Submitter]: Processing block {:?}", header.number); + let slot = get_slot_from_timestamp(header.timestamp, genesis_timestamp); + debug!("[Challenger Submitter]: Slot: {:?}", slot); + + // Check if challenges exists for the slot + let read_tx = match challenge_db.begin_read() { + Ok(tx) => tx, + Err(e) => { + error!("[Challenger Submitter]: Failed to begin read transaction: {}", e); + continue; + } + }; + + let table = match read_tx.open_table(CHALLENGE_TABLE) { + Ok(table) => table, + Err(e) => { + error!("[Challenger Submitter]: Failed to open challenge table: {}", e); + continue; + } + }; + + let challenges_data = match table.get(&slot) { + Ok(Some(val)) => val.value(), + Ok(None) => Vec::new(), + Err(e) => { + error!("[Challenger Submitter]: Storage error for slot {}: {}", slot, e); + continue; + } + }; + + debug!( + "[Challenger Submitter]: Found {} challenges for slot {}", + challenges_data.len(), + slot + ); + + // For each challenge, check if the challenge is expired + for challenge in challenges_data { + if challenge.preconf_type == 0 { + // Type A + let preconf_request = + match serde_json::from_str::(&challenge.preconf_request) { + Ok(req) => req, + Err(e) => { + error!( + "[Challenger Submitter]: Failed to parse PreconfRequestTypeA: {}", + e + ); + continue; + } + }; + + let mut txs: Vec = Vec::new(); + + for tx in preconf_request.preconf_tx { + let mut tx_bytes = Vec::new(); + tx.encode_2718(&mut tx_bytes); + let hex_encoded_tx = format!("0x{}", hex::encode(&tx_bytes)); + txs.push(hex_encoded_tx); + } + + let mut tip_tx = Vec::new(); + preconf_request.tip_transaction.encode_2718(&mut tip_tx); + let tip_tx_raw = format!("0x{}", hex::encode(&tip_tx)); + + let sequence_number = match preconf_request.sequence_number { + Some(num) => num, + None => { + error!("[Challenger Submitter]: Missing sequence number in PreconfRequestTypeA"); + continue; + } + }; + + let preconf_request_a_type = TaiyiInteractiveChallenger::PreconfRequestAType { + txs, + tipTx: tip_tx_raw, + slot: U256::from(slot), + sequenceNum: U256::from(sequence_number), + signer: preconf_request.signer, + }; + + // Decode signature + let signature_bytes = match hex::decode(&challenge.preconf_request_signature) { + Ok(bytes) => Bytes::from(bytes), + Err(e) => { + error!("[Challenger Submitter]: Failed to decode signature: {}", e); + continue; + } + }; + + // Submit challenge to the contract + debug!("[Challenger Submitter]: Submitting challenge type A for slot {}", slot); + match taiyi_challenger + .createChallengeAType(preconf_request_a_type, signature_bytes) + .send() + .await + { + Ok(tx) => { + debug!("[Challenger Submitter]: Challenge type A submitted. TX: {:?}", tx); + } + Err(e) => { + error!("[Challenger Submitter]: Failed to create challenge type A: {}", e); + // Continue to next challenge, we may be able to submit others + } + } + } else { + // Type B + let preconf_request = + match serde_json::from_str::(&challenge.preconf_request) { + Ok(req) => req, + Err(e) => { + error!( + "[Challenger Submitter]: Failed to parse PreconfRequestTypeB: {}", + e + ); + continue; + } + }; + + let transaction = match &preconf_request.transaction { + Some(tx) => tx, + None => { + error!( + "[Challenger Submitter]: Missing transaction in PreconfRequestTypeB" + ); + continue; + } + }; + + let mut tx_bytes = Vec::new(); + transaction.encode_2718(&mut tx_bytes); + let tx_raw = format!("0x{}", hex::encode(&tx_bytes)); + + let preconf_request_b_type = TaiyiInteractiveChallenger::PreconfRequestBType { + blockspaceAllocation: TaiyiInteractiveChallenger::BlockspaceAllocation { + gasLimit: U256::from(preconf_request.allocation.gas_limit), + sender: preconf_request.allocation.sender, + recipient: preconf_request.allocation.recipient, + deposit: U256::from(preconf_request.allocation.deposit), + tip: U256::from(preconf_request.allocation.tip), + targetSlot: U256::from(preconf_request.allocation.target_slot), + blobCount: U256::from(preconf_request.allocation.blob_count), + }, + blockspaceAllocationSignature: preconf_request.alloc_sig.as_bytes().into(), + rawTx: Bytes::from(tx_raw), + // TODO: Can we remove this two fields ? + underwriterSignedBlockspaceAllocation: Bytes::from([]), + underwriterSignedRawTx: Bytes::from([]), + }; + + // Decode signature + let signature_bytes = match hex::decode(&challenge.preconf_request_signature) { + Ok(bytes) => Bytes::from(bytes), + Err(e) => { + error!("[Challenger Submitter]: Failed to decode signature: {}", e); + continue; + } + }; + + // Submit challenge to the contract + debug!("[Challenger Submitter]: Submitting challenge type B for slot {}", slot); + match taiyi_challenger + .createChallengeBType(preconf_request_b_type, signature_bytes) + .send() + .await + { + Ok(tx) => { + debug!("[Challenger Submitter]: Challenge type B submitted. TX: {:?}", tx); + } + Err(e) => { + error!("[Challenger Submitter]: Failed to create challenge type B: {}", e); + // Continue to next challenge, we may be able to submit others + } + } + } + } + + debug!("[Challenger Submitter]: Processed block {:?}", header.number); + } + + Ok(()) +} diff --git a/bin/taiyi-challenger/src/handle_underwriter_stream.rs b/bin/taiyi-challenger/src/handle_underwriter_stream.rs new file mode 100644 index 00000000..6236ebb6 --- /dev/null +++ b/bin/taiyi-challenger/src/handle_underwriter_stream.rs @@ -0,0 +1,134 @@ +use std::sync::Arc; + +use alloy_primitives::hex; +use futures_util::StreamExt; +use redb::Database; +use reqwest::Url; +use reqwest_eventsource::{Event, EventSource}; +use taiyi_primitives::{PreconfRequest, PreconfResponseData}; +use tracing::{debug, error}; + +use crate::{preconf_request_data::PreconfRequestData, table_definitions::PRECONF_TABLE}; + +pub async fn handle_underwriter_stream(preconf_db: Arc, url: Url) -> eyre::Result<()> { + let req = reqwest::Client::new().get(url); + + let mut event_source = EventSource::new(req)?; + while let Some(event) = event_source.next().await { + match event { + Ok(Event::Message(message)) => { + let data = &message.data; + + let parsed_data = match serde_json::from_str::< + Vec<(PreconfRequest, PreconfResponseData)>, + >(data) + { + Ok(data) => data, + Err(e) => { + error!("[Stream Ingestor]: Failed to parse preconf data: {}", e); + continue; + } + }; + + debug!("[Stream Ingestor]: Received {} preconfirmations", parsed_data.len()); + + for (preconf_request, preconf_response_data) in parsed_data.iter() { + let target_slot = preconf_request.target_slot(); + debug!( + "[Stream Ingestor]: Processing preconfirmation for slot {}", + target_slot + ); + + let preconf_request_data = PreconfRequestData { + preconf_type: match preconf_request { + PreconfRequest::TypeA(_) => 0, + PreconfRequest::TypeB(_) => 1, + }, + preconf_request: match preconf_request { + PreconfRequest::TypeA(preconf_request) => { + match serde_json::to_string(&preconf_request) { + Ok(s) => s, + Err(e) => { + error!("[Stream Ingestor]: Failed to serialize preconf request: {}", e); + continue; + } + } + } + PreconfRequest::TypeB(preconf_request) => { + match serde_json::to_string(&preconf_request) { + Ok(s) => s, + Err(e) => { + error!("[Stream Ingestor]: Failed to serialize preconf request: {}", e); + continue; + } + } + } + }, + preconf_request_signature: match &preconf_response_data.commitment { + Some(commitment) => hex::encode(commitment.as_bytes()), + None => { + error!("[Stream Ingestor]: Missing commitment in preconf response"); + continue; + } + }, + }; + + let read_tx = match preconf_db.begin_read() { + Ok(tx) => tx, + Err(e) => { + error!("[Stream Ingestor]: Failed to begin read transaction: {}", e); + continue; + } + }; + + let table = match read_tx.open_table(PRECONF_TABLE) { + Ok(table) => table, + Err(e) => { + error!("[Stream Ingestor]: Failed to open preconf table: {}", e); + continue; + } + }; + + let mut preconf_values = match table.get(&target_slot) { + Ok(Some(val)) => val.value(), + Ok(None) => Vec::new(), + Err(e) => { + error!( + "[Stream Ingestor]: Storage error for slot {}: {}", + target_slot, e + ); + continue; + } + }; + + preconf_values.push(preconf_request_data); + + let write_result = (|| -> Result<(), redb::Error> { + let write_tx = preconf_db.begin_write()?; + { + let mut table = write_tx.open_table(PRECONF_TABLE)?; + table.insert(&target_slot, preconf_values)?; + } + write_tx.commit()?; + Ok(()) + })(); + + if let Err(e) = write_result { + error!("[Stream Ingestor]: Failed to write preconf data: {}", e); + continue; + } + + debug!("[Stream Ingestor]: Stored preconfirmation for slot {}", target_slot); + } + } + Ok(Event::Open) => { + debug!("[Stream Ingestor]: SSE connection opened"); + } + Err(err) => { + error!("[Stream Ingestor]: Error receiving SSE event: {:?}", err); + } + } + } + + Ok(()) +} diff --git a/bin/taiyi-challenger/src/main.rs b/bin/taiyi-challenger/src/main.rs new file mode 100644 index 00000000..bc927ac6 --- /dev/null +++ b/bin/taiyi-challenger/src/main.rs @@ -0,0 +1,153 @@ +use std::sync::Arc; + +use alloy_eips::merge::SLOT_DURATION_SECS; +use alloy_primitives::Address; +use clap::Parser; +use ethereum_consensus::{deneb::Context, networks::Network}; +use futures_util::future::join_all; +use handle_challenge_creation::handle_challenge_creation; +use handle_challenge_submission::handle_challenge_submission; +use handle_underwriter_stream::handle_underwriter_stream; +use redb::Database; +use reqwest::Url; +use table_definitions::{CHALLENGE_TABLE, PRECONF_TABLE}; +use tracing::{debug, error, info, level_filters::LevelFilter}; + +mod handle_challenge_creation; +mod handle_challenge_submission; +mod handle_underwriter_stream; +mod preconf_request_data; +mod table_definitions; + +pub fn get_slot_from_timestamp(timestamp: u64, genesis_timestamp: u64) -> u64 { + (timestamp - genesis_timestamp) / SLOT_DURATION_SECS +} + +#[derive(Parser, Clone)] +struct Opts { + /// execution_client_ws_url + #[clap(long = "execution-client-ws-url")] + execution_client_ws_url: String, + /// network + #[clap(long = "network")] + network: String, + /// finalization_window + #[clap(long = "finalization-window")] + finalization_window: u64, + /// underwriter stream urls + #[clap(long = "underwriter-stream-urls")] + underwriter_stream_urls: Vec, + /// Private key to sign transactions + #[clap(long = "private-key")] + private_key: String, + /// Taiyi challenger contract address + #[clap(long = "taiyi-challenger-address")] + taiyi_challenger_address: Address, + /// Always open challenges + #[clap(long = "always-open-challenges", default_value = "false")] + always_open_challenges: bool, +} + +#[tokio::main] +async fn main() -> eyre::Result<()> { + // Read cli args + let opts = Opts::parse(); + let opts = Arc::new(opts); + + // Initialize tracing + tracing_subscriber::fmt().with_max_level(LevelFilter::DEBUG).init(); + + let preconf_db = Database::create("preconf.db")?; + let preconf_db = Arc::new(preconf_db); + + let challenge_db = Database::create("challenge.db")?; + let challenge_db = Arc::new(challenge_db); + + // Create tables if they don't exist + debug!("Creating tables..."); + + let create_preconf_table = || -> Result<(), redb::Error> { + let tx = preconf_db.begin_write()?; + tx.open_table(PRECONF_TABLE)?; + tx.commit()?; + Ok(()) + }; + + if let Err(e) = create_preconf_table() { + error!("Failed to create preconf table: {}", e); + return Err(eyre::eyre!("Failed to create preconf table: {}", e)); + } + + let create_challenge_table = || -> Result<(), redb::Error> { + let tx = challenge_db.begin_write()?; + tx.open_table(CHALLENGE_TABLE)?; + tx.commit()?; + Ok(()) + }; + + if let Err(e) = create_challenge_table() { + error!("Failed to create challenge table: {}", e); + return Err(eyre::eyre!("Failed to create challenge table: {}", e)); + } + + debug!("Tables created successfully"); + + // Genesis timestamp + let network: Network = opts.network.clone().into(); + let context: Context = network.try_into()?; + let genesis_timestamp = context.genesis_time()?; + + let mut handles = Vec::new(); + + // Handles for ingesting underwriter streams + let underwriter_stream_urls = opts + .underwriter_stream_urls + .clone() + .iter() + .filter_map(|url| match Url::parse(url) { + Ok(parsed_url) => Some(parsed_url), + Err(e) => { + error!("Failed to parse URL '{}': {}", url, e); + None + } + }) + .collect::>(); + + if underwriter_stream_urls.is_empty() { + error!("No valid underwriter stream URLs provided"); + return Err(eyre::eyre!("No valid underwriter stream URLs provided")); + } + + for url in underwriter_stream_urls { + let handle = tokio::spawn(handle_underwriter_stream(preconf_db.clone(), url)); + handles.push(handle); + } + + // Handle for creating challenges + let challenger_creator_handle = tokio::spawn(handle_challenge_creation( + preconf_db.clone(), + challenge_db.clone(), + opts.clone(), + genesis_timestamp, + )); + + handles.push(challenger_creator_handle); + + // Handle for submitting challenges + let challenger_submitter_handle = tokio::spawn(handle_challenge_submission( + challenge_db.clone(), + opts.clone(), + genesis_timestamp, + )); + + handles.push(challenger_submitter_handle); + + tokio::select! { + _ = join_all(handles) => {}, + _ = tokio::signal::ctrl_c() => { + info!("Shutdown signal received."); + } + } + + Ok(()) +} diff --git a/bin/taiyi-challenger/src/preconf_request_data.rs b/bin/taiyi-challenger/src/preconf_request_data.rs new file mode 100644 index 00000000..674fd1e2 --- /dev/null +++ b/bin/taiyi-challenger/src/preconf_request_data.rs @@ -0,0 +1,175 @@ +use std::{any::type_name, fmt::Debug}; + +use bincode::{decode_from_slice, encode_to_vec, Decode, Encode}; +use redb::{TypeName, Value}; + +#[derive(Debug, Clone, Decode, Encode, PartialEq)] +pub struct PreconfRequestData { + pub preconf_type: u8, // 0: Type A, 1: Type B + pub preconf_request: String, // Serde json string + pub preconf_request_signature: String, // Hex encoded signature +} + +/// Wrapper type to handle values using bincode serialization +#[derive(Debug)] +pub struct Bincode(pub T); + +impl Value for Bincode +where + T: Debug + Encode + Decode<()>, +{ + type SelfType<'a> + = T + where + Self: 'a; + + type AsBytes<'a> + = Vec + where + Self: 'a; + + fn fixed_width() -> Option { + None + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + decode_from_slice(data, bincode::config::standard()).expect("Failed to decode bincode").0 + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'a, + Self: 'b, + { + encode_to_vec(value, bincode::config::standard()).expect("Failed to encode bincode") + } + + fn type_name() -> TypeName { + TypeName::new(&format!("Bincode<{}>", type_name::())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Debug, Clone, PartialEq, Decode, Encode)] + struct TestStruct { + id: u64, + name: String, + data: Vec, + } + + #[test] + fn test_bincode_roundtrip() { + // Create test values of different types + let test_string = String::from("Hello, world!"); + let test_int = 42u64; + let test_struct = TestStruct { id: 1, name: "Test".to_string(), data: vec![1, 2, 3, 4, 5] }; + + // Test string roundtrip + let bytes = >::as_bytes(&test_string); + let decoded = >::from_bytes(&bytes); + assert_eq!(decoded, test_string); + + // Test integer roundtrip + let bytes = >::as_bytes(&test_int); + let decoded = >::from_bytes(&bytes); + assert_eq!(decoded, test_int); + + // Test struct roundtrip + let bytes = >::as_bytes(&test_struct); + let decoded = >::from_bytes(&bytes); + assert_eq!(decoded, test_struct); + } + + #[test] + fn test_bincode_type_name() { + assert_eq!( + >::type_name(), + TypeName::new(&format!("Bincode<{}>", type_name::())) + ); + assert_eq!( + >::type_name(), + TypeName::new(&format!("Bincode<{}>", type_name::())) + ); + assert_eq!( + >::type_name(), + TypeName::new(&format!("Bincode<{}>", type_name::())) + ); + } + + #[test] + fn test_bincode_fixed_width() { + assert_eq!(>::fixed_width(), None); + assert_eq!(>::fixed_width(), None); + assert_eq!(>::fixed_width(), None); + } + + #[test] + fn test_preconf_request_data_serialization() { + let original = PreconfRequestData { + preconf_type: 1, + preconf_request: r#"{"key": "value"}"#.to_string(), + preconf_request_signature: "abcdef1234567890".to_string(), + }; + + // Serialize and deserialize using Bincode + let bytes = >::as_bytes(&original); + let decoded = >::from_bytes(&bytes); + + assert_eq!(decoded, original); + } + + #[test] + fn test_bincode_with_vector_of_preconf_data() { + // Create a vector of PreconfRequestData + let data_vec = vec![ + PreconfRequestData { + preconf_type: 0, + preconf_request: r#"{"key": "value"}"#.to_string(), + preconf_request_signature: "signature1".to_string(), + }, + PreconfRequestData { + preconf_type: 1, + preconf_request: r#"{"key": "value"}"#.to_string(), + preconf_request_signature: "signature2".to_string(), + }, + PreconfRequestData { + preconf_type: 0, + preconf_request: r#"{"key": "value"}"#.to_string(), + preconf_request_signature: "signature3".to_string(), + }, + ]; + + // Test serialization and deserialization of the vector + let bytes = >>::as_bytes(&data_vec); + let decoded = >>::from_bytes(&bytes); + + assert_eq!(decoded.len(), data_vec.len()); + assert_eq!(decoded, data_vec); + } + + #[test] + fn test_bincode_with_empty_vector_of_preconf_data() { + // Test with an empty vector + let empty_vec: Vec = vec![]; + + let bytes = >>::as_bytes(&empty_vec); + let decoded = >>::from_bytes(&bytes); + + assert_eq!(decoded.len(), 0); + assert!(decoded.is_empty()); + } + + #[test] + #[should_panic(expected = "Failed to decode bincode")] + fn test_bincode_decode_invalid_data() { + // Test decoding invalid data + let invalid_data = "invalid data"; // Invalid data that can't be decoded + >::from_bytes(invalid_data.as_bytes()); // This should panic + } +} diff --git a/bin/taiyi-challenger/src/table_definitions.rs b/bin/taiyi-challenger/src/table_definitions.rs new file mode 100644 index 00000000..32dd7818 --- /dev/null +++ b/bin/taiyi-challenger/src/table_definitions.rs @@ -0,0 +1,222 @@ +use redb::TableDefinition; + +use crate::preconf_request_data::{Bincode, PreconfRequestData}; + +pub const PRECONF_TABLE: TableDefinition>> = + TableDefinition::new("preconf"); +pub const CHALLENGE_TABLE: TableDefinition>> = + TableDefinition::new("challenge"); + +#[cfg(test)] +mod tests { + use std::fs; + + use redb::Database; + + use super::*; + + // Helper function to create a test PreconfRequestData + fn create_test_data(preconf_type: u8, index: u64) -> PreconfRequestData { + PreconfRequestData { + preconf_type, + preconf_request: format!(r#"{{"id": {}}}"#, index), + preconf_request_signature: format!("signature{}", index), + } + } + + // Helper function to create a temporary database path + fn temp_db_path(test_name: &str) -> String { + format!("/tmp/taiyi_test_{}.db", test_name) + } + + // Helper function to cleanup database files + fn cleanup_db(path: &str) { + let _ = fs::remove_file(path); + } + + #[test] + fn test_preconf_table() { + let db_path = temp_db_path("preconf_table"); + cleanup_db(&db_path); // Ensure clean state + + // Create a database + let db = Database::create(&db_path).unwrap(); + + // Start a write transaction and insert data + let write_tx = db.begin_write().unwrap(); + { + let mut table = write_tx.open_table(PRECONF_TABLE).unwrap(); + + // Create test data + let slot = 42u64; + let data = vec![create_test_data(0, 1), create_test_data(1, 2)]; + + // Insert the data + table.insert(&slot, data.clone()).unwrap(); + } + + // Commit the transaction + write_tx.commit().unwrap(); + + // Start a read transaction and verify data + let read_tx = db.begin_read().unwrap(); + let table = read_tx.open_table(PRECONF_TABLE).unwrap(); + + let slot = 42u64; + let expected_data = vec![create_test_data(0, 1), create_test_data(1, 2)]; + + // Read the data + let read_data = table.get(&slot).unwrap().unwrap().value(); + + // Verify the data + assert_eq!(read_data.len(), expected_data.len()); + assert_eq!(read_data, expected_data); + + // Clean up + drop(db); + cleanup_db(&db_path); + } + + #[test] + fn test_challenge_table() { + let db_path = temp_db_path("challenge_table"); + cleanup_db(&db_path); // Ensure clean state + + // Create a database + let db = Database::create(&db_path).unwrap(); + + let slot = 100u64; + let data = vec![create_test_data(0, 3)]; + + // Write transaction + let write_tx = db.begin_write().unwrap(); + { + // Open the CHALLENGE_TABLE + let mut table = write_tx.open_table(CHALLENGE_TABLE).unwrap(); + + // Insert the data + table.insert(&slot, data.clone()).unwrap(); + } + + // Commit the transaction + write_tx.commit().unwrap(); + + // Read transaction + let read_tx = db.begin_read().unwrap(); + let table = read_tx.open_table(CHALLENGE_TABLE).unwrap(); + + // Read the data + let read_data = table.get(&slot).unwrap().unwrap().value(); + + // Verify the data + assert_eq!(read_data, data); + + // Clean up + drop(db); + cleanup_db(&db_path); + } + + #[test] + fn test_multiple_slots() { + let db_path = temp_db_path("multiple_slots"); + cleanup_db(&db_path); // Ensure clean state + + // Create a database + let db = Database::create(&db_path).unwrap(); + + // Write transaction + let write_tx = db.begin_write().unwrap(); + { + // Open both tables + let mut preconf_table = write_tx.open_table(PRECONF_TABLE).unwrap(); + let mut challenge_table = write_tx.open_table(CHALLENGE_TABLE).unwrap(); + + // Create and insert test data for multiple slots + for slot in 1..5 { + let preconf_data = + vec![create_test_data(0, slot * 10), create_test_data(1, slot * 10 + 1)]; + + let challenge_data = vec![create_test_data(1, slot * 100)]; + + preconf_table.insert(&slot, preconf_data).unwrap(); + challenge_table.insert(&slot, challenge_data).unwrap(); + } + } + + // Commit the transaction + write_tx.commit().unwrap(); + + // Read transaction + let read_tx = db.begin_read().unwrap(); + let preconf_table = read_tx.open_table(PRECONF_TABLE).unwrap(); + let challenge_table = read_tx.open_table(CHALLENGE_TABLE).unwrap(); + + // Verify the data for each slot + for slot in 1..5 { + let preconf_data = preconf_table.get(&slot).unwrap().unwrap().value(); + let challenge_data = challenge_table.get(&slot).unwrap().unwrap().value(); + + assert_eq!(preconf_data.len(), 2); + assert_eq!(preconf_data[0].preconf_type, 0); + assert_eq!(preconf_data[1].preconf_type, 1); + + assert_eq!(challenge_data.len(), 1); + assert_eq!(challenge_data[0].preconf_type, 1); + } + + // Clean up + drop(db); + cleanup_db(&db_path); + } + + #[test] + fn test_update_existing_data() { + let db_path = temp_db_path("update_existing"); + cleanup_db(&db_path); // Ensure clean state + + // Create a database + let db = Database::create(&db_path).unwrap(); + + let slot = 200u64; + let initial_data = vec![create_test_data(0, 1)]; + + // First write transaction - insert initial data + let write_tx = db.begin_write().unwrap(); + { + let mut table = write_tx.open_table(PRECONF_TABLE).unwrap(); + table.insert(&slot, initial_data.clone()).unwrap(); + } + write_tx.commit().unwrap(); + + // Read the data and create updated data + let updated_data = { + let read_tx = db.begin_read().unwrap(); + let table = read_tx.open_table(PRECONF_TABLE).unwrap(); + let mut data = table.get(&slot).unwrap().unwrap().value(); + + // Append new data + data.push(create_test_data(1, 2)); + data + }; + + // Second write transaction - update with new data + let write_tx = db.begin_write().unwrap(); + { + let mut table = write_tx.open_table(PRECONF_TABLE).unwrap(); + table.insert(&slot, updated_data.clone()).unwrap(); + } + write_tx.commit().unwrap(); + + // Verify the updated data + let read_tx = db.begin_read().unwrap(); + let table = read_tx.open_table(PRECONF_TABLE).unwrap(); + let read_data = table.get(&slot).unwrap().unwrap().value(); + + assert_eq!(read_data.len(), 2); + assert_eq!(read_data, updated_data); + + // Clean up + drop(db); + cleanup_db(&db_path); + } +} diff --git a/scripts/devnet/start-taiyi-challenger.sh b/scripts/devnet/start-taiyi-challenger.sh new file mode 100755 index 00000000..c5b8ca3a --- /dev/null +++ b/scripts/devnet/start-taiyi-challenger.sh @@ -0,0 +1,23 @@ +set -xe + +source "$(dirname "$0")/config.sh" + +if kurtosis enclave inspect $ENCLAVE_NAME >/dev/null 2>&1; then + export EXECUTION_CLIENT_WS_URL="ws://`kurtosis port print luban el-1-geth-lighthouse ws`" + export BEACON_URL="`kurtosis port print luban cl-1-lighthouse-geth http`" +fi + +# TAIYI_DEPLOYMENT_FILE="contracts/script/output/devnet/taiyiAddresses.json" +# if [ -f "$TAIYI_DEPLOYMENT_FILE" ]; then +# export TAIYI_CHALLENGER_ADDRESS=$(jq -r '.taiyiAddresses.taiyiChallengerProxy' "$TAIYI_DEPLOYMENT_FILE") +# fi + +export TAIYI_CHALLENGER_ADDRESS=0x0000000000000000000000000000000000000000 + +cargo run --bin taiyi-challenger -- \ + --execution-client-ws-url $EXECUTION_CLIENT_WS_URL \ + --network $NETWORK \ + --finalization-window 32 \ + --underwriter-stream-urls http://127.0.0.1:5656/commitments/v0/commitment_stream \ + --private-key 0xbf3beef3bd999ba9f2451e06936f0423cd62b815c9233dd3bc90f7e02a1e8673 \ + --taiyi-challenger-address $TAIYI_CHALLENGER_ADDRESS diff --git a/scripts/devnet/start-taiyi-preconfer.sh b/scripts/devnet/start-taiyi-preconfer.sh old mode 100644 new mode 100755 index 4f61432c..b24be3b0 --- a/scripts/devnet/start-taiyi-preconfer.sh +++ b/scripts/devnet/start-taiyi-preconfer.sh @@ -2,13 +2,17 @@ set -xe source "$(dirname "$0")/config.sh" -export EXECUTION_URL="http://`kurtosis port print luban el-1-geth-lighthouse rpc`" -export BEACON_URL="`kurtosis port print luban cl-1-lighthouse-geth http`" -export HELIX_URL="http://`kurtosis port print luban helix-relay api`" +if kurtosis enclave inspect $ENCLAVE_NAME >/dev/null 2>&1; then + export EXECUTION_URL="http://`kurtosis port print luban el-1-geth-lighthouse rpc`" + export BEACON_URL="`kurtosis port print luban cl-1-lighthouse-geth http`" + export HELIX_URL="http://`kurtosis port print luban helix-relay api`" +fi + cargo run --bin taiyi underwriter \ - --bls_sk 4942d3308d3fbfbdb977c0bf4c09cb6990aec9fd5ce24709eaf23d96dba71148 \ - --ecdsa_sk 0xc5114526e042343c6d1899cad05e1c00ba588314de9b96929914ee0df18d46b2 \ + --bls-sk 4942d3308d3fbfbdb977c0bf4c09cb6990aec9fd5ce24709eaf23d96dba71148 \ + --ecdsa-sk 0xc5114526e042343c6d1899cad05e1c00ba588314de9b96929914ee0df18d46b2 \ --network $WORKING_DIR/el_cl_genesis_data \ - --execution_client_url $EXECUTION_URL \ - --beacon_client_url $BEACON_URL \ - --relay_url $HELIX_URL + --execution-rpc-url $EXECUTION_URL \ + --beacon-rpc-url $BEACON_URL \ + --relay-url $HELIX_URL \ + --taiyi-escrow-address $TAIYI_CORE_ADDRESS diff --git a/scripts/devnet/submit-preconf-tx-type-a.sh b/scripts/devnet/submit-preconf-tx-type-a.sh new file mode 100755 index 00000000..ad50eab1 --- /dev/null +++ b/scripts/devnet/submit-preconf-tx-type-a.sh @@ -0,0 +1,14 @@ +set -xe + +source "$(dirname "$0")/config.sh" + +if kurtosis enclave inspect $ENCLAVE_NAME >/dev/null 2>&1; then + export EXECUTION_CLIENT_URL="http://$(kurtosis port print luban el-1-geth-lighthouse rpc)" + export BEACON_CLIENT_URL="$(kurtosis port print luban cl-1-lighthouse-geth http)" +fi + +export UNDERWRITER_URL="http://127.0.0.1:5656" +export PRIVATE_KEY="bf3beef3bd999ba9f2451e06936f0423cd62b815c9233dd3bc90f7e02a1e8673" +export UNDERWRITER_ADDRESS=0xD8F3183DEF51A987222D845be228e0Bbb932C222 + +cargo run -p type_a \ No newline at end of file diff --git a/scripts/devnet/submit-preconf-tx-type-b.sh b/scripts/devnet/submit-preconf-tx-type-b.sh new file mode 100755 index 00000000..a3481b16 --- /dev/null +++ b/scripts/devnet/submit-preconf-tx-type-b.sh @@ -0,0 +1,14 @@ +set -xe + +source "$(dirname "$0")/config.sh" + +if kurtosis enclave inspect $ENCLAVE_NAME >/dev/null 2>&1; then + export EXECUTION_CLIENT_URL="http://$(kurtosis port print luban el-1-geth-lighthouse rpc)" + export BEACON_CLIENT_URL="$(kurtosis port print luban cl-1-lighthouse-geth http)" +fi + +export UNDERWRITER_URL="http://127.0.0.1:5656" +export PRIVATE_KEY="bf3beef3bd999ba9f2451e06936f0423cd62b815c9233dd3bc90f7e02a1e8673" +export UNDERWRITER_ADDRESS=0xD8F3183DEF51A987222D845be228e0Bbb932C222 + +cargo run -p type_b \ No newline at end of file diff --git a/scripts/devnet/submit-preconf-tx.sh b/scripts/devnet/submit-preconf-tx.sh deleted file mode 100644 index 3692af88..00000000 --- a/scripts/devnet/submit-preconf-tx.sh +++ /dev/null @@ -1,7 +0,0 @@ -set -xe - -source "$(dirname "$0")/config.sh" - -export PRIVATE_KEY="bf3beef3bd999ba9f2451e06936f0423cd62b815c9233dd3bc90f7e02a1e8673" -export TAIYI_UNDERWRITER_URL="http://127.0.0.1:5656" -cargo run --example submit-preconf-request