diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bfac7db..c27ff4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: uses: actions/checkout@v4 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@1.98.1 with: components: clippy, rustfmt @@ -101,7 +101,7 @@ jobs: uses: actions/checkout@v4 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@1.98.1 - name: Cache cargo registry uses: actions/cache@v4 diff --git a/crates/randscan-api/src/handlers/bridge.rs b/crates/randscan-api/src/handlers/bridge.rs index 90663d4..abf56fe 100644 --- a/crates/randscan-api/src/handlers/bridge.rs +++ b/crates/randscan-api/src/handlers/bridge.rs @@ -1,6 +1,9 @@ use crate::{error::AppError, state::AppState, ApiResult}; use axum::{extract::State, Json}; -use randscan_core::{approved_token, ApprovedToken, BridgeAsset, BridgeAssetActivity, BridgeState, Supply, APPROVED_TOKENS}; +use randscan_core::{ + approved_token, ApprovedToken, BridgeAsset, BridgeAssetActivity, BridgeState, Supply, + APPROVED_TOKENS, +}; use randscan_db::{BridgeAssetFlowRow, BridgeBackingBurnRow}; /// GET /api/v1/bridge — the bridge's public state as last read from the node, plus `endpoints`: @@ -17,7 +20,9 @@ pub async fn get_bridge(State(state): State) -> ApiResult) -> ApiResult>> { +pub async fn bridge_assets( + State(state): State, +) -> ApiResult>> { let Some(bridge) = state.indexer.bridge().await.filter(|b| b.enabled) else { return Ok(Json(vec![])); }; @@ -48,11 +53,14 @@ fn backing_rows( let token = flows.iter().find(|f| f.asset_index == a.index); let token_deposited = token.map_or_else(|| "0".to_string(), |f| f.deposited.clone()); let token_burned = token.map_or_else(|| "0".to_string(), |f| f.burned.clone()); - let token_deposit_fees = token.map_or_else(|| "0".to_string(), |f| f.deposit_fees.clone()); + let token_deposit_fees = + token.map_or_else(|| "0".to_string(), |f| f.deposit_fees.clone()); let token_burn_fees = token.map_or_else(|| "0".to_string(), |f| f.burn_fees.clone()); - let own = burns - .iter() - .find(|b| b.asset_index == a.index && b.chain == a.chain && b.token.eq_ignore_ascii_case(&a.token)); + let own = burns.iter().find(|b| { + b.asset_index == a.index + && b.chain == a.chain + && b.token.eq_ignore_ascii_case(&a.token) + }); let burned = own.map_or_else(|| "0".to_string(), |b| b.burned.clone()); let burn_fees = own.map_or_else(|| "0".to_string(), |b| b.burn_fees.clone()); let sole = backings == 1; @@ -69,7 +77,10 @@ fn backing_rows( deposits: sole.then(|| token.map_or(0, |f| f.deposits)), deposited: sole.then(|| token_deposited.clone()), burns: own.map_or(0, |b| b.burns), - outstanding: a.locked.clone().unwrap_or_else(|| units_sub(&token_deposited, &burned)), + outstanding: a + .locked + .clone() + .unwrap_or_else(|| units_sub(&token_deposited, &burned)), burned, burn_fees, token_deposit_fees, @@ -128,7 +139,13 @@ mod tests { .unwrap() } - fn token_flow(index: i64, deposits: i64, deposited: &str, burns: i64, burned: &str) -> BridgeAssetFlowRow { + fn token_flow( + index: i64, + deposits: i64, + deposited: &str, + burns: i64, + burned: &str, + ) -> BridgeAssetFlowRow { BridgeAssetFlowRow { asset_index: index, deposits, @@ -161,18 +178,36 @@ mod tests { assert_eq!(rows.len(), 7); for (row, a) in rows.iter().zip(&assets) { assert_eq!(row.backings, 7); - assert_eq!((row.deposits, row.deposited.as_deref()), (None, None), "not attributable to one coin"); - assert_eq!(Some(&row.outstanding), a.locked.as_ref(), "a backing holds what the registry says"); - assert_eq!((row.token_deposits, row.token_deposited.as_str()), (3, "300000000")); + assert_eq!( + (row.deposits, row.deposited.as_deref()), + (None, None), + "not attributable to one coin" + ); + assert_eq!( + Some(&row.outstanding), + a.locked.as_ref(), + "a backing holds what the registry says" + ); + assert_eq!( + (row.token_deposits, row.token_deposited.as_str()), + (3, "300000000") + ); } // What the rows hold adds up to what the token's deposits brought in, once. - let held: u128 = rows.iter().map(|r| r.outstanding.parse::().unwrap()).sum(); + let held: u128 = rows + .iter() + .map(|r| r.outstanding.parse::().unwrap()) + .sum(); assert_eq!(held, 300_000_000); } #[test] fn a_burn_is_laid_on_the_backing_it_names_and_on_no_other() { - let assets = vec![backing(1, 2, "AA", "600"), backing(1, 3, "aa", "0"), backing(2, 2, "aa", "0")]; + let assets = vec![ + backing(1, 2, "AA", "600"), + backing(1, 3, "aa", "0"), + backing(2, 2, "aa", "0"), + ]; let burns = vec![BridgeBackingBurnRow { asset_index: 1, chain: 2, @@ -196,12 +231,30 @@ mod tests { fn a_sole_backing_owns_its_tokens_deposits() { let mut old_node = backing(1, 2, "cc", "0"); old_node.locked = None; // a node before chain 14 serves no `locked` - let rows = backing_rows(&[old_node, backing(2, 2, "dd", "0")], &[token_flow(1, 1, "1000", 1, "400")], &[ - BridgeBackingBurnRow { asset_index: 1, chain: 2, token: "cc".into(), burns: 1, burned: "400".into(), burn_fees: "0".into() }, - ]); - assert_eq!((rows[0].deposits, rows[0].deposited.as_deref()), (Some(1), Some("1000"))); - assert_eq!(rows[0].outstanding, "600", "rebuilt from the flows only without the registry's figure"); - assert_eq!((rows[1].deposits, rows[1].deposited.as_deref()), (Some(0), Some("0"))); + let rows = backing_rows( + &[old_node, backing(2, 2, "dd", "0")], + &[token_flow(1, 1, "1000", 1, "400")], + &[BridgeBackingBurnRow { + asset_index: 1, + chain: 2, + token: "cc".into(), + burns: 1, + burned: "400".into(), + burn_fees: "0".into(), + }], + ); + assert_eq!( + (rows[0].deposits, rows[0].deposited.as_deref()), + (Some(1), Some("1000")) + ); + assert_eq!( + rows[0].outstanding, "600", + "rebuilt from the flows only without the registry's figure" + ); + assert_eq!( + (rows[1].deposits, rows[1].deposited.as_deref()), + (Some(0), Some("0")) + ); } /// Chain 20 (`bridge.fees` 10/10 bps): one 1 zUSD deposit through Ethereum USDT and a 0.5 @@ -223,11 +276,24 @@ mod tests { }]; let rows = backing_rows(&assets, &[flow], &burns); let r = &rows[0]; - assert_eq!(r.deposited.as_deref(), Some("100000000"), "the gross the guardians signed"); - assert_eq!((r.burned.as_str(), r.burn_fees.as_str()), ("49950000", "50000")); - assert_eq!((r.token_deposit_fees.as_str(), r.token_burn_fees.as_str()), ("100000", "50000")); + assert_eq!( + r.deposited.as_deref(), + Some("100000000"), + "the gross the guardians signed" + ); + assert_eq!( + (r.burned.as_str(), r.burn_fees.as_str()), + ("49950000", "50000") + ); + assert_eq!( + (r.token_deposit_fees.as_str(), r.token_burn_fees.as_str()), + ("100000", "50000") + ); assert_eq!(r.outstanding, "50050000"); // deposited − released == locked, and supply (net notes + fee notes) is the same figure. - assert_eq!(units_sub(r.deposited.as_deref().unwrap(), &r.burned), r.outstanding); + assert_eq!( + units_sub(r.deposited.as_deref().unwrap(), &r.burned), + r.outstanding + ); } } diff --git a/crates/randscan-api/src/handlers/envelopes.rs b/crates/randscan-api/src/handlers/envelopes.rs index facf4f0..959c120 100644 --- a/crates/randscan-api/src/handlers/envelopes.rs +++ b/crates/randscan-api/src/handlers/envelopes.rs @@ -33,7 +33,11 @@ pub async fn transaction_envelopes( ) -> ApiResult> { let hash = match classify_query(&hash) { QueryKind::Hash(h) => h, - _ => return Err(AppError::BadRequest("transaction hash must be 64 hex characters".into())), + _ => { + return Err(AppError::BadRequest( + "transaction hash must be 64 hex characters".into(), + )) + } }; let pool = state.db.inner(); let tx = db::get_transaction_summary(pool, &hash) diff --git a/crates/randscan-api/src/handlers/nodes.rs b/crates/randscan-api/src/handlers/nodes.rs index 18586a0..1ab9f35 100644 --- a/crates/randscan-api/src/handlers/nodes.rs +++ b/crates/randscan-api/src/handlers/nodes.rs @@ -10,6 +10,8 @@ pub async fn list_nodes(State(state): State) -> ApiResult) -> ApiResult>> { +pub async fn list_provers( + State(state): State, +) -> ApiResult>> { Ok(Json(state.indexer.provers().await)) } diff --git a/crates/randscan-api/src/handlers/programs.rs b/crates/randscan-api/src/handlers/programs.rs index 37abf48..1fa3ce9 100644 --- a/crates/randscan-api/src/handlers/programs.rs +++ b/crates/randscan-api/src/handlers/programs.rs @@ -51,8 +51,15 @@ pub async fn get_program( // order. `None` on a chain without the section (and on an older node); a failed read is // logged and served as `None` rather than failing the page. let rpc = state.indexer.rpc(); - let program_state = match (rpc.program_vault(&id).await, rpc.program_cells(&id, None, CELLS_FIRST_PAGE).await) { - (Ok(Some(vault)), Ok(Some(page))) => Some(ProgramState { vault, cells: page.cells, cells_next: page.next }), + let program_state = match ( + rpc.program_vault(&id).await, + rpc.program_cells(&id, None, CELLS_FIRST_PAGE).await, + ) { + (Ok(Some(vault)), Ok(Some(page))) => Some(ProgramState { + vault, + cells: page.cells, + cells_next: page.next, + }), (Ok(_), Ok(_)) => None, (Err(e), _) | (_, Err(e)) => { warn!("program state read for {id} failed: {e:#}"); @@ -92,7 +99,11 @@ pub async fn program_cells( let after = match q.after.as_deref().filter(|a| !a.is_empty()) { Some(a) => match classify_query(a) { QueryKind::Hash(h) => Some(h), - _ => return Err(AppError::BadRequest("after must be a 64 hex cell key".into())), + _ => { + return Err(AppError::BadRequest( + "after must be a 64 hex cell key".into(), + )) + } }, None => None, }; @@ -103,6 +114,8 @@ pub async fn program_cells( .program_cells(&id, after.as_deref(), limit) .await .map_err(|e| AppError::Internal(format!("program cells: {e:#}")))? - .ok_or_else(|| AppError::NotFound("program state (this chain has no program_state section)".into()))?; + .ok_or_else(|| { + AppError::NotFound("program state (this chain has no program_state section)".into()) + })?; Ok(Json(page)) } diff --git a/crates/randscan-api/src/handlers/tokens.rs b/crates/randscan-api/src/handlers/tokens.rs index fdc28c9..6cb2046 100644 --- a/crates/randscan-api/src/handlers/tokens.rs +++ b/crates/randscan-api/src/handlers/tokens.rs @@ -27,7 +27,10 @@ fn find_token<'a>(tokens: &'a [TokenInfo], key: &str) -> Option<&'a TokenInfo> { return Some(t); } } - let hex_key = key.strip_prefix("0x").or_else(|| key.strip_prefix("0X")).unwrap_or(key); + let hex_key = key + .strip_prefix("0x") + .or_else(|| key.strip_prefix("0X")) + .unwrap_or(key); tokens .iter() .find(|t| t.id.eq_ignore_ascii_case(hex_key) || t.id_text.eq_ignore_ascii_case(key)) @@ -36,7 +39,10 @@ fn find_token<'a>(tokens: &'a [TokenInfo], key: &str) -> Option<&'a TokenInfo> { /// GET /api/v1/tokens/:id — one token by index, 64-hex id or `rpl1…` text form, its deploy /// transaction and its public supply history (every `register_token`/`token_mint`/`token_burn` /// naming it, oldest first). -pub async fn get_token(State(state): State, Path(id): Path) -> ApiResult> { +pub async fn get_token( + State(state): State, + Path(id): Path, +) -> ApiResult> { let list = state.indexer.tokens().await.unwrap_or_default(); let info = find_token(&list.tokens, &id) .cloned() @@ -62,11 +68,21 @@ pub async fn get_token(State(state): State, Path(id): Path) -> "token_burn" => e.amount.as_deref().map(|a| format!("-{a}")), _ => None, }?; - Some(TokenSupplyEvent { tx_hash: e.tx_hash, height: e.height, timestamp_ms: e.timestamp_ms, kind: e.kind, delta }) + Some(TokenSupplyEvent { + tx_hash: e.tx_hash, + height: e.height, + timestamp_ms: e.timestamp_ms, + kind: e.kind, + delta, + }) }) .collect(); - Ok(Json(TokenDetail { info, deploy_tx, supply_history })) + Ok(Json(TokenDetail { + info, + deploy_tx, + supply_history, + })) } #[cfg(test)] @@ -93,7 +109,10 @@ mod tests { let id = "aa".repeat(32); let tokens = vec![token(3, &id, "rpl1zusd")]; assert_eq!(find_token(&tokens, "3").unwrap().symbol, "zUSD"); - assert_eq!(find_token(&tokens, &id.to_ascii_uppercase()).unwrap().index, 3); + assert_eq!( + find_token(&tokens, &id.to_ascii_uppercase()).unwrap().index, + 3 + ); assert_eq!(find_token(&tokens, &format!("0x{id}")).unwrap().index, 3); assert_eq!(find_token(&tokens, "RPL1ZUSD").unwrap().index, 3); assert!(find_token(&tokens, "9").is_none()); diff --git a/crates/randscan-api/tests/common/mock_node.rs b/crates/randscan-api/tests/common/mock_node.rs index 5d2bc9e..28cf533 100644 --- a/crates/randscan-api/tests/common/mock_node.rs +++ b/crates/randscan-api/tests/common/mock_node.rs @@ -44,7 +44,8 @@ pub fn h(seed: &str) -> String { /// realistic `token_mint`/`register_token` fixture and, by the indexer under test, to link that /// leaf back to its transaction (`randscan_core::notecommit::mint_commitment`). pub fn mint_note_cm(recipient: &str, amount: u64, index: u32, time: u32, r_hex: &str) -> String { - notecommit::mint_commitment_hex(recipient, amount, index, time, r_hex).expect("a well-formed shielded address") + notecommit::mint_commitment_hex(recipient, amount, index, time, r_hex) + .expect("a well-formed shielded address") } /// A register entry as the S2 node serves it. @@ -192,7 +193,8 @@ impl MockChain { Some("invoke") => { for side in ["pays", "mints"] { for p in a["transition"][side].as_array().into_iter().flatten() { - self.leaves.push((p["cm"].as_str().unwrap().to_string(), height)); + self.leaves + .push((p["cm"].as_str().unwrap().to_string(), height)); } } } @@ -227,7 +229,12 @@ impl MockChain { fn committed(&self) -> impl Iterator + '_ { self.blocks.iter().flat_map(|b| { let height = b["height"].as_u64().unwrap_or(0); - b["transactions"].as_array().into_iter().flatten().enumerate().map(move |(i, t)| (height, i, t)) + b["transactions"] + .as_array() + .into_iter() + .flatten() + .enumerate() + .map(move |(i, t)| (height, i, t)) }) } @@ -527,9 +534,16 @@ fn matches_token_key(row: &Value, key: &Value) -> bool { return true; } } - let hex_key = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")).unwrap_or(s); - row["id"].as_str().is_some_and(|id| id.eq_ignore_ascii_case(hex_key)) - || row["id_text"].as_str().is_some_and(|t| t.eq_ignore_ascii_case(s)) + let hex_key = s + .strip_prefix("0x") + .or_else(|| s.strip_prefix("0X")) + .unwrap_or(s); + row["id"] + .as_str() + .is_some_and(|id| id.eq_ignore_ascii_case(hex_key)) + || row["id_text"] + .as_str() + .is_some_and(|t| t.eq_ignore_ascii_case(s)) } /// The public fields of the chain-14 hidden-asset bundle, unique per `seed`: four slots, no diff --git a/crates/randscan-api/tests/common/mod.rs b/crates/randscan-api/tests/common/mod.rs index 26fc1bb..5d3947c 100644 --- a/crates/randscan-api/tests/common/mod.rs +++ b/crates/randscan-api/tests/common/mod.rs @@ -143,7 +143,12 @@ impl LiveApp { } /// Poll `GET path` until `pred(body)` holds; panics with the last body after `timeout`. - pub async fn wait_for(&self, path: &str, timeout: Duration, pred: impl Fn(&Value) -> bool) -> Value { + pub async fn wait_for( + &self, + path: &str, + timeout: Duration, + pred: impl Fn(&Value) -> bool, + ) -> Value { let start = std::time::Instant::now(); let mut last = Value::Null; while start.elapsed() < timeout { @@ -189,5 +194,10 @@ pub async fn live_app(cfg: ApiConfig, rpc_url: &str) -> Option { limiter: Arc::new(RateLimiter::new()), mailer: None, }; - Some(LiveApp { app: create_router(state), pool, indexer, broadcaster }) + Some(LiveApp { + app: create_router(state), + pool, + indexer, + broadcaster, + }) } diff --git a/crates/randscan-api/tests/mock_node.rs b/crates/randscan-api/tests/mock_node.rs index a53efef..1105787 100644 --- a/crates/randscan-api/tests/mock_node.rs +++ b/crates/randscan-api/tests/mock_node.rs @@ -20,30 +20,90 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { // ----- phase 1: chain 7 with every action the node can serve --------------------------- let mut chain = MockChain::new(7, "genesis-a"); let transfer = tx(7, "transfer", Some(bundle("t")), json!({ "kind": "none" })); - let mint = tx(7, "mint", None, json!({ "kind": "mint", "cm": h("cm-mint"), "amount": 100000000000u64, "minter": VALIDATOR })); + let mint = tx( + 7, + "mint", + None, + json!({ "kind": "mint", "cm": h("cm-mint"), "amount": 100000000000u64, "minter": VALIDATOR }), + ); chain.push_block(vec![transfer.clone(), mint.clone()]); let program = h("program-1"); - let deploy = tx(7, "deploy", Some(bundle("d")), json!({ "kind": "deploy", "program": program, "words": 412, "public_words_len": 27151 })); - let call_tx = tx(7, "call", Some(bundle("c")), json!({ "kind": "call", "program": program, "proof_len": 1202416, "input_envelope_len": 1280 })); - let bond = tx(7, "bond", Some(bundle("b")), json!({ "kind": "bond", "validator": VALIDATOR_B, "amount": 1000000000000u64, "registered": true })); - let unbond = tx(7, "unbond", None, json!({ "kind": "unbond", "validator": VALIDATOR_B, "amount": 5000000000u64, "nonce": 1 })); - let withdraw = tx(7, "withdraw", None, json!({ "kind": "withdraw", "validator": VALIDATOR, "amount": "9000000", "nonce": 3, "time": 1994 })); - chain.push_block(vec![deploy.clone(), call_tx.clone(), bond.clone(), unbond.clone(), withdraw.clone()]); - - let attest = tx(7, "attest", Some(bundle("a")), json!({ - "kind": "bridge_attest", "attestation_len": 520, "recipient": SHIELDED_ADDR, "asset": 1, "asset_index": 1, - "amount": 1000, "time": 2, "r": "aa".repeat(32), "commitment": h("deposit-attest"), "pq_signers": [0, 1] - })); - let rotation = tx(7, "rotation", Some(bundle("r")), json!({ - "kind": "bridge_attest", "attestation_len": 700, "recipient": SHIELDED_ADDR, "asset": null, "asset_index": null, - "amount": null, "time": 2, "r": "00".repeat(32), "commitment": null, "pq_signers": [0, 1] - })); - let evm_to = format!("{}{}", "0".repeat(24), "f10befe1e0794722d3baf8bfd5bdac47b2a33148"); - let burn = tx(7, "burn", Some(bundle("bb")), json!({ - "kind": "bridge_burn", "asset": 1, "amount": 400, "relayer_fee": 100, "to_chain": 2, "token": "cc".repeat(32), "to": evm_to - })); - let future = tx(7, "future", Some(bundle("f")), json!({ "kind": "slash", "evidence": "opaque" })); + let deploy = tx( + 7, + "deploy", + Some(bundle("d")), + json!({ "kind": "deploy", "program": program, "words": 412, "public_words_len": 27151 }), + ); + let call_tx = tx( + 7, + "call", + Some(bundle("c")), + json!({ "kind": "call", "program": program, "proof_len": 1202416, "input_envelope_len": 1280 }), + ); + let bond = tx( + 7, + "bond", + Some(bundle("b")), + json!({ "kind": "bond", "validator": VALIDATOR_B, "amount": 1000000000000u64, "registered": true }), + ); + let unbond = tx( + 7, + "unbond", + None, + json!({ "kind": "unbond", "validator": VALIDATOR_B, "amount": 5000000000u64, "nonce": 1 }), + ); + let withdraw = tx( + 7, + "withdraw", + None, + json!({ "kind": "withdraw", "validator": VALIDATOR, "amount": "9000000", "nonce": 3, "time": 1994 }), + ); + chain.push_block(vec![ + deploy.clone(), + call_tx.clone(), + bond.clone(), + unbond.clone(), + withdraw.clone(), + ]); + + let attest = tx( + 7, + "attest", + Some(bundle("a")), + json!({ + "kind": "bridge_attest", "attestation_len": 520, "recipient": SHIELDED_ADDR, "asset": 1, "asset_index": 1, + "amount": 1000, "time": 2, "r": "aa".repeat(32), "commitment": h("deposit-attest"), "pq_signers": [0, 1] + }), + ); + let rotation = tx( + 7, + "rotation", + Some(bundle("r")), + json!({ + "kind": "bridge_attest", "attestation_len": 700, "recipient": SHIELDED_ADDR, "asset": null, "asset_index": null, + "amount": null, "time": 2, "r": "00".repeat(32), "commitment": null, "pq_signers": [0, 1] + }), + ); + let evm_to = format!( + "{}{}", + "0".repeat(24), + "f10befe1e0794722d3baf8bfd5bdac47b2a33148" + ); + let burn = tx( + 7, + "burn", + Some(bundle("bb")), + json!({ + "kind": "bridge_burn", "asset": 1, "amount": 400, "relayer_fee": 100, "to_chain": 2, "token": "cc".repeat(32), "to": evm_to + }), + ); + let future = tx( + 7, + "future", + Some(bundle("f")), + json!({ "kind": "slash", "evidence": "opaque" }), + ); // RPL-2: an invoke of the deployed program — the shape of the node's own pinned test // (`rpc.rs`, "invoke"): one cell read as absent and written, 1 000 RAND units and 500 of // token 3 deposited through the bundle, 300 units of RAND paid out of the vault and 40 of @@ -55,16 +115,21 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { invoke_bundle["burn_a"] = json!("500"); invoke_bundle["burn_asset"] = json!(3); let invoke_recipient = shielded_address("invoke-payee"); - let invoke = tx(7, "invoke", Some(invoke_bundle), json!({ - "kind": "invoke", "program": program, "proof_len": 268123, "input_envelope_len": null, - "transition": { - "reads": [{ "key": cell_key, "value": "00".repeat(32) }], - "writes": [{ "key": cell_key, "value": format!("05{}", "00".repeat(31)) }], - "inflow": "deposit", - "pays": [{ "asset": 0, "amount": "300", "recipient": invoke_recipient, "time": 2, "r": "a1".repeat(32), "cm": h("payout-0") }], - "mints": [{ "asset": 3, "amount": 40, "recipient": invoke_recipient, "time": 2, "r": "a2".repeat(32), "cm": h("payout-1") }], - } - })); + let invoke = tx( + 7, + "invoke", + Some(invoke_bundle), + json!({ + "kind": "invoke", "program": program, "proof_len": 268123, "input_envelope_len": null, + "transition": { + "reads": [{ "key": cell_key, "value": "00".repeat(32) }], + "writes": [{ "key": cell_key, "value": format!("05{}", "00".repeat(31)) }], + "inflow": "deposit", + "pays": [{ "asset": 0, "amount": "300", "recipient": invoke_recipient, "time": 2, "r": "a1".repeat(32), "cm": h("payout-0") }], + "mints": [{ "asset": 3, "amount": 40, "recipient": invoke_recipient, "time": 2, "r": "a2".repeat(32), "cm": h("payout-1") }], + } + }), + ); // The RPL token standard (spec §4/§6) and bridge hardening's governance actions (B1/B4): a // token registered with an initial mint, a later mint, an authority change, a holder burn, @@ -84,39 +149,96 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { })); let zusd_register_recipient = shielded_address("zusd-register-recipient"); let zusd_mint_recipient = shielded_address("zusd-mint-recipient"); - let register_token = tx(7, "register-zusd", Some(bundle("rt")), json!({ - "kind": "register_token", "name": "zUSD", "symbol": "zUSD", "decimals": 6, "authority": "bridge", - "index": 3, "initial_amount": 5000, - "initial": { "amount": 5000, "recipient": zusd_register_recipient, "time": 3, "r": "bb".repeat(32) } - })); - let token_mint = tx(7, "mint-zusd", Some(bundle("tm")), json!({ - "kind": "token_mint", "asset": 3, "amount": 700, "recipient": zusd_mint_recipient, "time": 3, "r": "cc".repeat(32), "nonce": 0 - })); - let set_authority = tx(7, "set-auth-zusd", Some(bundle("sa")), json!({ - "kind": "set_authority", "asset": 3, "nonce": 1, "new_authority": VALIDATOR - })); + let register_token = tx( + 7, + "register-zusd", + Some(bundle("rt")), + json!({ + "kind": "register_token", "name": "zUSD", "symbol": "zUSD", "decimals": 6, "authority": "bridge", + "index": 3, "initial_amount": 5000, + "initial": { "amount": 5000, "recipient": zusd_register_recipient, "time": 3, "r": "bb".repeat(32) } + }), + ); + let token_mint = tx( + 7, + "mint-zusd", + Some(bundle("tm")), + json!({ + "kind": "token_mint", "asset": 3, "amount": 700, "recipient": zusd_mint_recipient, "time": 3, "r": "cc".repeat(32), "nonce": 0 + }), + ); + let set_authority = tx( + 7, + "set-auth-zusd", + Some(bundle("sa")), + json!({ + "kind": "set_authority", "asset": 3, "nonce": 1, "new_authority": VALIDATOR + }), + ); let mut token_burn_bundle = bundle("tb"); token_burn_bundle["burn_a"] = json!(400); token_burn_bundle["burn_asset"] = json!(3); - let token_burn = tx(7, "burn-zusd", Some(token_burn_bundle), json!({ "kind": "token_burn", "asset": 3, "amount": 400 })); - let pause = tx(7, "pause", None, json!({ "kind": "pause_mints", "nonce": 4 })); - let unpause = tx(7, "unpause", None, json!({ "kind": "unpause_mints", "nonce": 5, "pq_signers": [0, 2] })); - let register_bridged = tx(7, "register-bridged-2", Some(bundle("rb")), json!({ - "kind": "register_bridged_token", "name": "zUSD2", "symbol": "zUSD2", "salt": "ee".repeat(4), - "chain": 3, "token": "ff".repeat(32), "decimals": 6, "nonce": 6, "asset_id": h("zusd2-asset"), "pq_signers": [1] - })); - let list_backing = tx(7, "list-backing", Some(bundle("lb")), json!({ - "kind": "list_backing", "token_index": 3, "chain": 4, "token": "11".repeat(32), "decimals": 6, "nonce": 7, "pq_signers": [2] - })); + let token_burn = tx( + 7, + "burn-zusd", + Some(token_burn_bundle), + json!({ "kind": "token_burn", "asset": 3, "amount": 400 }), + ); + let pause = tx( + 7, + "pause", + None, + json!({ "kind": "pause_mints", "nonce": 4 }), + ); + let unpause = tx( + 7, + "unpause", + None, + json!({ "kind": "unpause_mints", "nonce": 5, "pq_signers": [0, 2] }), + ); + let register_bridged = tx( + 7, + "register-bridged-2", + Some(bundle("rb")), + json!({ + "kind": "register_bridged_token", "name": "zUSD2", "symbol": "zUSD2", "salt": "ee".repeat(4), + "chain": 3, "token": "ff".repeat(32), "decimals": 6, "nonce": 6, "asset_id": h("zusd2-asset"), "pq_signers": [1] + }), + ); + let list_backing = tx( + 7, + "list-backing", + Some(bundle("lb")), + json!({ + "kind": "list_backing", "token_index": 3, "chain": 4, "token": "11".repeat(32), "decimals": 6, "nonce": 7, "pq_signers": [2] + }), + ); chain.push_block(vec![ - attest.clone(), rotation.clone(), burn.clone(), future.clone(), - register_token.clone(), token_mint.clone(), set_authority.clone(), token_burn.clone(), - pause.clone(), unpause.clone(), register_bridged.clone(), list_backing.clone(), invoke.clone(), + attest.clone(), + rotation.clone(), + burn.clone(), + future.clone(), + register_token.clone(), + token_mint.clone(), + set_authority.clone(), + token_burn.clone(), + pause.clone(), + unpause.clone(), + register_bridged.clone(), + list_backing.clone(), + invoke.clone(), ]); // 2000 transactions per block is a consensus rule; the explorer must take a full block. let fat: Vec = (0..2000) - .map(|i| tx(7, &format!("fat-{i}"), Some(bundle(&format!("fat-{i}"))), json!({ "kind": "none" }))) + .map(|i| { + tx( + 7, + &format!("fat-{i}"), + Some(bundle(&format!("fat-{i}"))), + json!({ "kind": "none" }), + ) + }) .collect(); chain.push_block(fat); let expected_leaves = chain.leaves.len(); @@ -159,11 +281,15 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { } live.start(); - live.wait_for("/api/v1/health", WAIT, |b| b["indexer"]["current_height"] == 4).await; + live.wait_for("/api/v1/health", WAIT, |b| { + b["indexer"]["current_height"] == 4 + }) + .await; // A plain transfer: a bundle and nothing else. let transfer_hash = transfer["hash"].as_str().unwrap(); - let (status, _, d) = call_api(&live.app, &format!("/api/v1/transactions/{transfer_hash}")).await; + let (status, _, d) = + call_api(&live.app, &format!("/api/v1/transactions/{transfer_hash}")).await; assert_eq!(status, 200, "{d}"); assert_eq!(d["kind"], "transfer"); assert_eq!(d["has_bundle"], true); @@ -172,7 +298,10 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { let (status, _, alias) = call_api(&live.app, &format!("/api/v1/tx/0x{transfer_hash}")).await; assert_eq!(status, 200, "{alias}"); assert_eq!(alias, d); - assert_eq!(d["bundle"]["nullifiers"], json!([h("nf1-t"), h("nf2-t"), h("nf3-t"), h("nf4-t")])); + assert_eq!( + d["bundle"]["nullifiers"], + json!([h("nf1-t"), h("nf2-t"), h("nf3-t"), h("nf4-t")]) + ); assert_eq!(d["bundle"]["commitments"][0], h("cm1-t")); assert_eq!(d["bundle"]["commitments"][3], h("cm4-t")); assert_eq!(d["bundle"]["proof_len"], 302857); @@ -180,9 +309,18 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { // Chain 17+: split authorisation's commitment and the auth proof, by length. assert_eq!(d["bundle"]["auth_commit"], h("auth-t")); assert_eq!(d["bundle"]["auth_proof_len"], 1360512); - assert!(d["bundle"].get("asset").is_none(), "a transfer's asset must never be served: {d}"); - assert!(d["amount"].is_null() && d["validator"].is_null() && d["program"].is_null(), "{d}"); - assert!(d.get("sender").is_none() && d.get("nonce").is_none() && d.get("to").is_none(), "no account fields: {d}"); + assert!( + d["bundle"].get("asset").is_none(), + "a transfer's asset must never be served: {d}" + ); + assert!( + d["amount"].is_null() && d["validator"].is_null() && d["program"].is_null(), + "{d}" + ); + assert!( + d.get("sender").is_none() && d.get("nonce").is_none() && d.get("to").is_none(), + "no account fields: {d}" + ); assert_eq!(d["chain_id"], 7); // A mint: no bundle, a public amount, the minting validator. @@ -196,36 +334,69 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { assert_eq!(d["cm"], h("cm-mint")); assert_eq!(d["validator"], VALIDATOR); - let (_, _, d) = call_api(&live.app, &format!("/api/v1/transactions/{}", deploy["hash"].as_str().unwrap())).await; + let (_, _, d) = call_api( + &live.app, + &format!("/api/v1/transactions/{}", deploy["hash"].as_str().unwrap()), + ) + .await; assert_eq!(d["kind"], "deploy"); assert_eq!(d["program"], program); assert_eq!(d["words_len"], 412); - let (_, _, d) = call_api(&live.app, &format!("/api/v1/transactions/{}", call_tx["hash"].as_str().unwrap())).await; + let (_, _, d) = call_api( + &live.app, + &format!("/api/v1/transactions/{}", call_tx["hash"].as_str().unwrap()), + ) + .await; assert_eq!(d["kind"], "call"); assert_eq!(d["program"], program); - assert_eq!(d["call_proof_len"], 1202416, "a constraint-set-5 proof size is stored as reported"); + assert_eq!( + d["call_proof_len"], 1202416, + "a constraint-set-5 proof size is stored as reported" + ); assert_eq!(d["input_envelope_len"], 1280); // The receipt is the node's, with the public digest the proof was checked against (v0.4). assert_eq!(d["receipt"]["tier"], 14, "{d}"); assert_eq!(d["receipt"]["h_in"], h("h_in")); assert_eq!(d["receipt"]["h_pub"], public_digest(&program)); - let (_, _, d) = call_api(&live.app, &format!("/api/v1/transactions/{}", bond["hash"].as_str().unwrap())).await; + let (_, _, d) = call_api( + &live.app, + &format!("/api/v1/transactions/{}", bond["hash"].as_str().unwrap()), + ) + .await; assert_eq!(d["kind"], "bond"); assert_eq!(d["validator"], VALIDATOR_B); assert_eq!(d["amount"], "1000000000000"); assert_eq!(d["registered"], true); - let (_, _, d) = call_api(&live.app, &format!("/api/v1/transactions/{}", unbond["hash"].as_str().unwrap())).await; + let (_, _, d) = call_api( + &live.app, + &format!("/api/v1/transactions/{}", unbond["hash"].as_str().unwrap()), + ) + .await; assert_eq!(d["kind"], "unbond"); assert_eq!(d["has_bundle"], false); assert_eq!(d["action_nonce"], 1); - let (_, _, d) = call_api(&live.app, &format!("/api/v1/transactions/{}", withdraw["hash"].as_str().unwrap())).await; + let (_, _, d) = call_api( + &live.app, + &format!( + "/api/v1/transactions/{}", + withdraw["hash"].as_str().unwrap() + ), + ) + .await; assert_eq!(d["kind"], "withdraw"); assert_eq!(d["amount"], "9000000"); assert_eq!(d["action_nonce"], 3); - assert_eq!(d["note_time"], 1994, "a withdraw's deposit note publishes its time word"); + assert_eq!( + d["note_time"], 1994, + "a withdraw's deposit note publishes its time word" + ); - let (_, _, d) = call_api(&live.app, &format!("/api/v1/transactions/{}", attest["hash"].as_str().unwrap())).await; + let (_, _, d) = call_api( + &live.app, + &format!("/api/v1/transactions/{}", attest["hash"].as_str().unwrap()), + ) + .await; assert_eq!(d["kind"], "bridge_attest"); assert_eq!(d["attestation_len"], 520); assert_eq!(d["recipient"], SHIELDED_ADDR); @@ -234,10 +405,23 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { assert_eq!(d["note_time"], 2); assert_eq!(d["commitment"], h("deposit-attest")); assert_eq!(d["pq_signers"], json!([0, 1])); - let (_, _, d) = call_api(&live.app, &format!("/api/v1/transactions/{}", rotation["hash"].as_str().unwrap())).await; + let (_, _, d) = call_api( + &live.app, + &format!( + "/api/v1/transactions/{}", + rotation["hash"].as_str().unwrap() + ), + ) + .await; assert_eq!(d["kind"], "bridge_attest"); - assert!(d["asset_index"].is_null() && d["amount"].is_null(), "a rotation deposits nothing: {d}"); - assert!(d["commitment"].is_null(), "a rotation deposits nothing: {d}"); + assert!( + d["asset_index"].is_null() && d["amount"].is_null(), + "a rotation deposits nothing: {d}" + ); + assert!( + d["commitment"].is_null(), + "a rotation deposits nothing: {d}" + ); let burn_hash = burn["hash"].as_str().unwrap(); let (_, _, d) = call_api(&live.app, &format!("/api/v1/transactions/{burn_hash}")).await; @@ -247,14 +431,28 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { assert_eq!(d["relayer_fee"], "100"); assert_eq!(d["to_chain"], 2); assert_eq!(d["bridge_to"], evm_to); - assert_eq!(d["bridge_token"], "cc".repeat(32), "the coin being redeemed, chain 14's single-bundle burn"); - assert!(d.get("asset_bundle").is_none(), "chain 14 has one bundle per transaction, no more asset_bundle"); + assert_eq!( + d["bridge_token"], + "cc".repeat(32), + "the coin being redeemed, chain 14's single-bundle burn" + ); + assert!( + d.get("asset_bundle").is_none(), + "chain 14 has one bundle per transaction, no more asset_bundle" + ); assert_eq!(d["bundle"]["nullifiers"][0], h("nf1-bb")); assert_eq!(d["bundle"]["nullifiers"].as_array().unwrap().len(), 4); // The RPL token standard (spec §4/§6): registration + initial mint, a later mint, an // authority change, a holder burn — all public by design, none of it a "transfer". - let (_, _, d) = call_api(&live.app, &format!("/api/v1/transactions/{}", register_token["hash"].as_str().unwrap())).await; + let (_, _, d) = call_api( + &live.app, + &format!( + "/api/v1/transactions/{}", + register_token["hash"].as_str().unwrap() + ), + ) + .await; assert_eq!(d["kind"], "register_token"); assert_eq!(d["asset_index"], 3); assert_eq!(d["amount"], "5000", "the initial mint's amount"); @@ -262,19 +460,40 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { assert_eq!(d["token_action"]["kind"], "register_token"); assert_eq!(d["token_action"]["name"], "zUSD"); assert_eq!(d["token_action"]["initial"]["amount"], "5000"); - let (_, _, d) = call_api(&live.app, &format!("/api/v1/transactions/{}", token_mint["hash"].as_str().unwrap())).await; + let (_, _, d) = call_api( + &live.app, + &format!( + "/api/v1/transactions/{}", + token_mint["hash"].as_str().unwrap() + ), + ) + .await; assert_eq!(d["kind"], "token_mint"); assert_eq!(d["asset_index"], 3); assert_eq!(d["amount"], "700"); assert_eq!(d["recipient"], zusd_mint_recipient); assert_eq!(d["action_nonce"], 0); assert_eq!(d["token_action"]["r"], "cc".repeat(32)); - let (_, _, d) = call_api(&live.app, &format!("/api/v1/transactions/{}", set_authority["hash"].as_str().unwrap())).await; + let (_, _, d) = call_api( + &live.app, + &format!( + "/api/v1/transactions/{}", + set_authority["hash"].as_str().unwrap() + ), + ) + .await; assert_eq!(d["kind"], "set_authority"); assert_eq!(d["asset_index"], 3); assert_eq!(d["action_nonce"], 1); assert_eq!(d["token_action"]["new_authority"], VALIDATOR); - let (_, _, d) = call_api(&live.app, &format!("/api/v1/transactions/{}", token_burn["hash"].as_str().unwrap())).await; + let (_, _, d) = call_api( + &live.app, + &format!( + "/api/v1/transactions/{}", + token_burn["hash"].as_str().unwrap() + ), + ) + .await; assert_eq!(d["kind"], "token_burn"); assert_eq!(d["asset_index"], 3); assert_eq!(d["amount"], "400"); @@ -284,45 +503,92 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { // Bridge hardening B1/B4: bundle-less pause/unpause, and a second bridged token + backing // listed after genesis by the PQ guardian quorum. - let (_, _, d) = call_api(&live.app, &format!("/api/v1/transactions/{}", pause["hash"].as_str().unwrap())).await; + let (_, _, d) = call_api( + &live.app, + &format!("/api/v1/transactions/{}", pause["hash"].as_str().unwrap()), + ) + .await; assert_eq!(d["kind"], "pause_mints"); assert_eq!(d["has_bundle"], false); assert_eq!(d["bridge_governance"]["nonce"], 4); - let (_, _, d) = call_api(&live.app, &format!("/api/v1/transactions/{}", unpause["hash"].as_str().unwrap())).await; + let (_, _, d) = call_api( + &live.app, + &format!("/api/v1/transactions/{}", unpause["hash"].as_str().unwrap()), + ) + .await; assert_eq!(d["kind"], "unpause_mints"); assert_eq!(d["pq_signers"], json!([0, 2])); - let (_, _, d) = call_api(&live.app, &format!("/api/v1/transactions/{}", register_bridged["hash"].as_str().unwrap())).await; + let (_, _, d) = call_api( + &live.app, + &format!( + "/api/v1/transactions/{}", + register_bridged["hash"].as_str().unwrap() + ), + ) + .await; assert_eq!(d["kind"], "register_bridged_token"); assert_eq!(d["bridge_governance"]["name"], "zUSD2"); assert_eq!(d["pq_signers"], json!([1])); - let (_, _, d) = call_api(&live.app, &format!("/api/v1/transactions/{}", list_backing["hash"].as_str().unwrap())).await; + let (_, _, d) = call_api( + &live.app, + &format!( + "/api/v1/transactions/{}", + list_backing["hash"].as_str().unwrap() + ), + ) + .await; assert_eq!(d["kind"], "list_backing"); assert_eq!(d["asset_index"], 3); assert_eq!(d["bridge_governance"]["chain"], 4); // Privacy (task S1 item 3): a plain transfer's whole JSON never says which asset moved. - let dump = serde_json::to_string(&call_api(&live.app, &format!("/api/v1/transactions/{transfer_hash}")).await.2).unwrap(); - assert!(!dump.contains("\"asset\""), "a transfer must never carry an asset field anywhere: {dump}"); + let dump = serde_json::to_string( + &call_api(&live.app, &format!("/api/v1/transactions/{transfer_hash}")) + .await + .2, + ) + .unwrap(); + assert!( + !dump.contains("\"asset\""), + "a transfer must never carry an asset field anywhere: {dump}" + ); let future_hash = future["hash"].as_str().unwrap(); let (_, _, d) = call_api(&live.app, &format!("/api/v1/transactions/{future_hash}")).await; - assert_eq!(d["kind"], "other", "unknown node kinds are indexed, not dropped: {d}"); + assert_eq!( + d["kind"], "other", + "unknown node kinds are indexed, not dropped: {d}" + ); assert_eq!(d["has_bundle"], true); let raw: String = sqlx::query_scalar("SELECT kind FROM transactions WHERE hash = $1") .bind(future_hash) .fetch_one(&live.pool) .await .unwrap(); - assert_eq!(raw, "slash", "the node's own tag is kept for a later backfill"); + assert_eq!( + raw, "slash", + "the node's own tag is kept for a later backfill" + ); // Filters: kind, validator, program; `sender` is gone and `none` is not a filter. let (_, _, list) = call_api(&live.app, "/api/v1/transactions?kind=bridge_burn").await; assert_eq!(list["pagination"]["total"], 1); assert_eq!(list["data"][0]["hash"], burn_hash); - let (_, _, list) = call_api(&live.app, &format!("/api/v1/transactions?validator={VALIDATOR_B}")).await; + let (_, _, list) = call_api( + &live.app, + &format!("/api/v1/transactions?validator={VALIDATOR_B}"), + ) + .await; assert_eq!(list["pagination"]["total"], 2, "{list}"); - let (_, _, list) = call_api(&live.app, &format!("/api/v1/transactions?program={program}")).await; - assert_eq!(list["pagination"]["total"], 3, "the deploy, the call and the invoke: {list}"); + let (_, _, list) = call_api( + &live.app, + &format!("/api/v1/transactions?program={program}"), + ) + .await; + assert_eq!( + list["pagination"]["total"], 3, + "the deploy, the call and the invoke: {list}" + ); let (_, _, list) = call_api(&live.app, "/api/v1/transactions?kind=transfer").await; assert_eq!(list["pagination"]["total"], 2001); for bad in ["none", "slash", "other"] { @@ -342,7 +608,8 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { assert_eq!(status, 200, "{nf}"); assert_eq!(nf["tx_hash"], burn_hash); assert_eq!(nf["height"], 3); - let (status, _, nf4) = call_api(&live.app, &format!("/api/v1/nullifiers/{}", h("nf4-bb"))).await; + let (status, _, nf4) = + call_api(&live.app, &format!("/api/v1/nullifiers/{}", h("nf4-bb"))).await; assert_eq!(status, 200, "{nf4}"); assert_eq!(nf4["tx_hash"], burn_hash); let (status, _, _) = call_api(&live.app, &format!("/api/v1/nullifiers/{}", h("never"))).await; @@ -350,72 +617,144 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { // The batch lookup answers for a whole history in one request: the published ones come // back, the unpublished one is just absent. let lookup = json!({ "nullifiers": [h("nf2-bb"), h("never"), h("nf4-bb")] }); - let (status, _, found) = - call(&live.app, json_req("POST", "/api/v1/nullifiers/lookup", Some(lookup), None)).await; + let (status, _, found) = call( + &live.app, + json_req("POST", "/api/v1/nullifiers/lookup", Some(lookup), None), + ) + .await; assert_eq!(status, 200, "{found}"); - let mut spent: Vec<&str> = - found["spent"].as_array().unwrap().iter().map(|n| n["nullifier"].as_str().unwrap()).collect(); + let mut spent: Vec<&str> = found["spent"] + .as_array() + .unwrap() + .iter() + .map(|n| n["nullifier"].as_str().unwrap()) + .collect(); spent.sort(); let mut want = vec![h("nf2-bb"), h("nf4-bb")]; want.sort(); assert_eq!(spent, want); assert_eq!(found["spent"][0]["tx_hash"], burn_hash); let too_many = json!({ "nullifiers": vec![h("never"); 1001] }); - let (status, _, _) = - call(&live.app, json_req("POST", "/api/v1/nullifiers/lookup", Some(too_many), None)).await; + let (status, _, _) = call( + &live.app, + json_req("POST", "/api/v1/nullifiers/lookup", Some(too_many), None), + ) + .await; assert_eq!(status, 400); // The commitment tree was paged in (1000 rows per page) and linked to the transactions. let notes = live - .wait_for("/api/v1/notes?limit=5", WAIT, |n| n["pagination"]["total"] == expected_leaves) + .wait_for("/api/v1/notes?limit=5", WAIT, |n| { + n["pagination"]["total"] == expected_leaves + }) .await; - assert_eq!(notes["data"][0]["leaf_index"], expected_leaves as i64 - 1, "newest first: {notes}"); + assert_eq!( + notes["data"][0]["leaf_index"], + expected_leaves as i64 - 1, + "newest first: {notes}" + ); let (status, _, n) = call_api(&live.app, &format!("/api/v1/notes/{}", h("cm-mint"))).await; assert_eq!(status, 200, "{n}"); assert_eq!(n["tx_hash"], mint_hash); assert_eq!(n["height"], 1); let (_, _, n0) = call_api(&live.app, "/api/v1/notes/0").await; assert_eq!(n0["height"], 0); - assert!(n0["tx_hash"].is_null(), "a genesis deposit note has no transaction: {n0}"); + assert!( + n0["tx_hash"].is_null(), + "a genesis deposit note has no transaction: {n0}" + ); let (_, _, n) = call_api(&live.app, &format!("/api/v1/notes/{}", h("cm2-bb"))).await; - assert_eq!(n["tx_hash"], burn_hash, "the bundle's four output slots are all leaves"); + assert_eq!( + n["tx_hash"], burn_hash, + "the bundle's four output slots are all leaves" + ); // The chain-computed notes (task S1 item 1/3): a register_token's initial mint and a // token_mint each add ONE note whose envelope rides in the action, not the bundle — but it // is still a real tree leaf, linked back to its transaction by the indexer's own recomputed // commitment (there is no wire field for it, unlike a bridge_attest's). let register_hash = register_token["hash"].as_str().unwrap(); - let (_, _, env) = call_api(&live.app, &format!("/api/v1/transactions/{register_hash}/envelopes")).await; - assert_eq!(env["notes"].as_array().unwrap().len(), 5, "4 bundle slots + the initial mint: {env}"); + let (_, _, env) = call_api( + &live.app, + &format!("/api/v1/transactions/{register_hash}/envelopes"), + ) + .await; + assert_eq!( + env["notes"].as_array().unwrap().len(), + 5, + "4 bundle slots + the initial mint: {env}" + ); let mint_zusd_hash = token_mint["hash"].as_str().unwrap(); - let (_, _, env) = call_api(&live.app, &format!("/api/v1/transactions/{mint_zusd_hash}/envelopes")).await; - assert_eq!(env["notes"].as_array().unwrap().len(), 5, "4 bundle slots + the minted note: {env}"); + let (_, _, env) = call_api( + &live.app, + &format!("/api/v1/transactions/{mint_zusd_hash}/envelopes"), + ) + .await; + assert_eq!( + env["notes"].as_array().unwrap().len(), + 5, + "4 bundle slots + the minted note: {env}" + ); let derived_cm = mint_note_cm(&zusd_mint_recipient, 700, 3, 3, &"cc".repeat(32)); assert!( - env["notes"].as_array().unwrap().iter().any(|n| n["cm"] == derived_cm), + env["notes"] + .as_array() + .unwrap() + .iter() + .any(|n| n["cm"] == derived_cm), "the minted note's own commitment must be among the transaction's notes: {env}" ); // Envelopes: every leaf carries the node's envelope, a transaction lists the leaves it // created, and a call gets its sealed transcript from the node on demand. - let (status, _, env) = call_api(&live.app, &format!("/api/v1/transactions/{mint_hash}/envelopes")).await; + let (status, _, env) = call_api( + &live.app, + &format!("/api/v1/transactions/{mint_hash}/envelopes"), + ) + .await; assert_eq!(status, 200, "{env}"); assert_eq!(env["kind"], "mint"); assert_eq!(env["notes"].as_array().unwrap().len(), 1); assert_eq!(env["notes"][0]["cm"], h("cm-mint")); assert_eq!(env["notes"][0]["envelope"]["kem_ct"], "00"); assert!(env["call_envelope"].is_null() && env["h_in"].is_null()); - let (_, _, env) = call_api(&live.app, &format!("/api/v1/transactions/{burn_hash}/envelopes")).await; - assert_eq!(env["notes"].as_array().unwrap().len(), 4, "the one hidden-asset bundle's four slots: {env}"); - let (_, _, env) = call_api(&live.app, &format!("/api/v1/transactions/{}/envelopes", call_tx["hash"].as_str().unwrap())).await; + let (_, _, env) = call_api( + &live.app, + &format!("/api/v1/transactions/{burn_hash}/envelopes"), + ) + .await; + assert_eq!( + env["notes"].as_array().unwrap().len(), + 4, + "the one hidden-asset bundle's four slots: {env}" + ); + let (_, _, env) = call_api( + &live.app, + &format!( + "/api/v1/transactions/{}/envelopes", + call_tx["hash"].as_str().unwrap() + ), + ) + .await; assert_eq!(env["kind"], "call"); assert_eq!(env["call_envelope"]["body"], "0b".repeat(60)); - assert_eq!(env["h_in"], h("h_in"), "the transcript opens against the receipt's h_in"); + assert_eq!( + env["h_in"], + h("h_in"), + "the transcript opens against the receipt's h_in" + ); let (_, _, page) = call_api(&live.app, "/api/v1/envelopes?from_leaf=0&limit=1000").await; assert_eq!(page["total_leaves"], expected_leaves); assert_eq!(page["notes"].as_array().unwrap().len(), 1000); assert_eq!(page["next_leaf"], 1000); - let (_, _, page) = call_api(&live.app, &format!("/api/v1/envelopes?from_leaf={}&limit=1000", expected_leaves - 5)).await; + let (_, _, page) = call_api( + &live.app, + &format!( + "/api/v1/envelopes?from_leaf={}&limit=1000", + expected_leaves - 5 + ), + ) + .await; assert_eq!(page["notes"].as_array().unwrap().len(), 5); assert!(page["next_leaf"].is_null()); @@ -430,11 +769,19 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { let (status, _, gone) = call_api(&live.app, &format!("/api/v1/accounts/{VALIDATOR}")).await; assert_eq!(status, 410, "{gone}"); assert_eq!(gone["error"], "no_accounts"); - let (status, _, _) = call_api(&live.app, &format!("/api/v1/accounts/{VALIDATOR}/transactions")).await; + let (status, _, _) = call_api( + &live.app, + &format!("/api/v1/accounts/{VALIDATOR}/transactions"), + ) + .await; assert_eq!(status, 410); // Stats carry the tree, the register and the supply audit. - let stats = live.wait_for("/api/v1/stats", WAIT, |s| s["chain_id"] == 7 && s["height"] == 4 && s["validator_count"] == 2).await; + let stats = live + .wait_for("/api/v1/stats", WAIT, |s| { + s["chain_id"] == 7 && s["height"] == 4 && s["validator_count"] == 2 + }) + .await; assert_eq!(stats["total_transactions"], 2020, "{stats}"); // The chain's identity and limits, and the node's build (v0.3 / v0.4). let (_, _, genesis) = call_api(&live.app, "/api/v1/blocks/0").await; @@ -445,7 +792,9 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { assert_eq!(stats["fri_profile"], "test"); // Chain 18's `rand_getLimits`, served whole: the caps, the memo envelope, the v0.6 switch, the // auth guest and the gas section's genesis parameters (prices as decimal strings). - assert_eq!(stats["limits"], json!({ "max_program_words": 65535, "max_proof_bytes": 8388608, + assert_eq!( + stats["limits"], + json!({ "max_program_words": 65535, "max_proof_bytes": 8388608, "max_block_bytes": 20971520, "max_call_envelope_bytes": 65536, "max_program_public_words": 32768, "envelope_bytes": 1860, "hardening_v6": true, "hc_auth": h("hc_auth"), "gas_price": "100", "byte_price": "800", "gas_metering": "circuit", @@ -455,13 +804,20 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { "max_gas_price": null, "max_byte_price": null, "byte_load": null, "admission_by_vote": false, "testnet": false, "slashing": null, "binding_domain": null, "proof_window_blocks": null, // Fee feedback: the mock serves `null`, a chain without a `fees` section. - "fee_rules": null })); + "fee_rules": null }) + ); // Off `rand_status`, refreshed every commit: the auth guest, and the tip's live prices (one // 12.5% step above the genesis prices the limits report — a dynamic chain moves them). assert_eq!(stats["hc_auth"], h("hc_auth")); - assert_eq!(stats["gas_prices"], json!({ "gas_price": "112", "byte_price": "900" })); + assert_eq!( + stats["gas_prices"], + json!({ "gas_price": "112", "byte_price": "900" }) + ); // RPL-2: the `program_state` group rides inside the stored limits. - assert_eq!(stats["limits"]["program_state"], json!({ "cell_fee": "10000000", "max_reads": 8, "max_writes": 8, "max_payouts": 4 })); + assert_eq!( + stats["limits"]["program_state"], + json!({ "cell_fee": "10000000", "max_reads": 8, "max_writes": 8, "max_payouts": 4 }) + ); assert_eq!(stats["notes"], expected_leaves); // 2015 bundle-carrying transactions (2000 transfers, 1 mint's bundle-less action aside, plus // deploy/call/bond/attest/rotation/burn/future/register_token/token_mint/set_authority/ @@ -469,7 +825,10 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { // and unpause_mints carry none. assert_eq!(stats["nullifiers"], 2015 * 4, "{stats}"); assert_eq!(stats["active_validator_count"], 1); - assert_eq!(stats["total_stake"], "100000000000000", "only the active set counts"); + assert_eq!( + stats["total_stake"], "100000000000000", + "only the active set counts" + ); assert_eq!(stats["total_supply"], "101100000000000"); assert_eq!(stats["pool_value"], "1099997000000"); assert_eq!(stats["epoch"], 0); @@ -487,13 +846,23 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { assert_eq!(v[0]["share_percent"], 100.0); assert_eq!(v[1]["address"], VALIDATOR_B); assert_eq!(v[1]["active"], false); - assert_eq!(v[1]["pending"], json!([{ "release_epoch": 3, "amount": "5000000000" }])); + assert_eq!( + v[1]["pending"], + json!([{ "release_epoch": 3, "amount": "5000000000" }]) + ); assert_eq!(v[1]["payout"], SHIELDED_ADDR); assert_eq!(v[1]["share_percent"], 0.0); let (_, _, supply) = call_api(&live.app, "/api/v1/supply").await; assert_eq!(supply["invariant_holds"], true); - assert_eq!((&supply["program_rand_out"], &supply["program_rand_held"]), (&json!("300"), &json!("700")), "RPL-2 vault counters: {supply}"); - assert_eq!(supply["base_fees_burned"], "0", "fee feedback's burn counter passes through: {supply}"); + assert_eq!( + (&supply["program_rand_out"], &supply["program_rand_held"]), + (&json!("300"), &json!("700")), + "RPL-2 vault counters: {supply}" + ); + assert_eq!( + supply["base_fees_burned"], "0", + "fee feedback's burn counter passes through: {supply}" + ); let (_, _, bridge) = call_api(&live.app, "/api/v1/bridge").await; assert_eq!(bridge["enabled"], true); assert_eq!(bridge["assets"][0]["index"], 1); @@ -509,12 +878,25 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { // redeployed Ethereum/BSC/Tron endpoints and the unchanged Solana program, in chain order, // each with its floor. assert_eq!(bridge["rotation_nonce"], 0); - assert_eq!(bridge["rules_v2"], json!({ "global_mint_cap_per_window": "400000000000", "cap_window_secs": 86400, - "global_minted_in_window": "1000", "global_mint_headroom": "399999999000" })); + assert_eq!( + bridge["rules_v2"], + json!({ "global_mint_cap_per_window": "400000000000", "cap_window_secs": 86400, + "global_minted_in_window": "1000", "global_mint_headroom": "399999999000" }) + ); assert_eq!(bridge["assets"][0]["mint_headroom"], "999000"); - assert_eq!(bridge["assets"][1]["mint_headroom"], Value::Null, "an older row has no headroom"); - assert_eq!(bridge["min_inbound_sequence"], json!({ "2": 1, "3": 1, "4": 1, "5": 4 })); - assert_eq!(bridge["emitters"]["4"], "0000000000000000000000006410797df959987a5baf65b5fab97edeb34d5163"); + assert_eq!( + bridge["assets"][1]["mint_headroom"], + Value::Null, + "an older row has no headroom" + ); + assert_eq!( + bridge["min_inbound_sequence"], + json!({ "2": 1, "3": 1, "4": 1, "5": 4 }) + ); + assert_eq!( + bridge["emitters"]["4"], + "0000000000000000000000006410797df959987a5baf65b5fab97edeb34d5163" + ); assert_eq!( bridge["endpoints"], json!([ @@ -547,7 +929,10 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { // node encoded it (`randscan_core::amount`'s tolerant deserializer). assert_eq!(bridge["assets"][0]["decimals"], 8); assert_eq!(bridge["assets"][0]["locked"], "600"); - assert_eq!(bridge["assets"][0]["mint_cap_per_day"], (100_000u64 * 100_000_000).to_string()); + assert_eq!( + bridge["assets"][0]["mint_cap_per_day"], + (100_000u64 * 100_000_000).to_string() + ); assert_eq!(bridge["assets"][0]["minted_today"], "1000"); // Registry rows joined with the indexed flows: index 1 saw one 1000-unit deposit and one // 400-unit burn (the rotation carries no asset and is not counted); index 2 is Ethereum @@ -558,19 +943,39 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { // Token 1 has two backings (chain 14: one zUSD, many coins). The burn named (chain 2, cc…), // so it is that backing's alone; the other coin of the same token saw nothing. A row must // never repeat the whole token's tally as its own. - assert_eq!((&assets[0]["index"], &assets[0]["chain"]), (&json!(1), &json!(2))); - assert_eq!((&assets[1]["index"], &assets[1]["chain"]), (&json!(1), &json!(3))); + assert_eq!( + (&assets[0]["index"], &assets[0]["chain"]), + (&json!(1), &json!(2)) + ); + assert_eq!( + (&assets[1]["index"], &assets[1]["chain"]), + (&json!(1), &json!(3)) + ); assert_eq!(assets[0]["burns"], 1); assert_eq!(assets[0]["burned"], "400"); - assert_eq!(assets[1]["burns"], 0, "a burn is its own backing's: {:?}", assets[1]); + assert_eq!( + assets[1]["burns"], 0, + "a burn is its own backing's: {:?}", + assets[1] + ); assert_eq!(assets[1]["burned"], "0"); // What a backing holds is the registry's `locked`, not a figure rebuilt from token-wide sums. assert_eq!(assets[0]["outstanding"], "600"); assert_eq!(assets[1]["outstanding"], "0"); // Audit v6: the window's count and the headroom ride along per backing where the node // serves them, and are null where it does not. - assert_eq!((&assets[0]["minted_in_window"], &assets[0]["mint_window_secs"], &assets[0]["mint_headroom"]), (&json!("1000"), &json!(86_400), &json!("999000"))); - assert_eq!((&assets[1]["minted_in_window"], &assets[1]["mint_headroom"]), (&Value::Null, &Value::Null)); + assert_eq!( + ( + &assets[0]["minted_in_window"], + &assets[0]["mint_window_secs"], + &assets[0]["mint_headroom"] + ), + (&json!("1000"), &json!(86_400), &json!("999000")) + ); + assert_eq!( + (&assets[1]["minted_in_window"], &assets[1]["mint_headroom"]), + (&Value::Null, &Value::Null) + ); // A deposit publishes the token it minted, not the coin that was locked for it, so with // several backings it cannot be laid at one of them: per backing it is null, and the whole // token's figure is served under its own name, the same on each of the token's rows. @@ -585,9 +990,15 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { } assert_eq!(assets[0]["symbol"], Value::Null); assert_eq!(assets[0]["first_height"], assets[0]["last_height"]); - assert_eq!(assets[0]["locked"], "600", "the registry's own figure, alongside the indexed flows"); + assert_eq!( + assets[0]["locked"], "600", + "the registry's own figure, alongside the indexed flows" + ); assert_eq!(assets[0]["minted_today"], "1000"); - assert_eq!(assets[0]["mint_cap_per_day"], (100_000u64 * 100_000_000).to_string()); + assert_eq!( + assets[0]["mint_cap_per_day"], + (100_000u64 * 100_000_000).to_string() + ); // Token 2 has one backing, so the token's deposits are that backing's. assert_eq!(assets[2]["index"], 2); assert_eq!(assets[2]["symbol"], "USDT"); @@ -608,8 +1019,14 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { assert_eq!(tokens_list["tokens"][0]["symbol"], "zUSD"); assert_eq!(tokens_list["tokens"][0]["decimals"], 6); assert_eq!(tokens_list["tokens"][0]["authority"]["kind"], "bridge"); - assert_eq!(tokens_list["tokens"][0]["authority"]["backings"][0]["locked"], "600"); - assert_eq!(tokens_list["tokens"][0]["authority"]["backings"][0]["minted_today"], "700"); + assert_eq!( + tokens_list["tokens"][0]["authority"]["backings"][0]["locked"], + "600" + ); + assert_eq!( + tokens_list["tokens"][0]["authority"]["backings"][0]["minted_today"], + "700" + ); // The node sent this one as a bare number (unlike locked/minted_today, strings, on the same // backing row) — this API's output still normalises it to a decimal string regardless. assert_eq!( @@ -617,13 +1034,24 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { (100_000u64 * 100_000_000).to_string() ); - for key in ["3", &h("zusd-id"), "rpl1zusdexampleexampleexampleexampleexampleexampleexampleeez"] { + for key in [ + "3", + &h("zusd-id"), + "rpl1zusdexampleexampleexampleexampleexampleexampleexampleeez", + ] { let (status, _, d) = call_api(&live.app, &format!("/api/v1/tokens/{key}")).await; assert_eq!(status, 200, "token lookup by {key}: {d}"); assert_eq!(d["symbol"], "zUSD", "{key}"); - assert_eq!(d["deploy_tx"], register_hash, "the register_token transaction, by asset_index: {d}"); + assert_eq!( + d["deploy_tx"], register_hash, + "the register_token transaction, by asset_index: {d}" + ); let history = d["supply_history"].as_array().unwrap(); - assert_eq!(history.len(), 3, "register (initial 5000) + mint (700) + burn (400): {d}"); + assert_eq!( + history.len(), + 3, + "register (initial 5000) + mint (700) + burn (400): {d}" + ); assert_eq!(history[0]["kind"], "register_token"); assert_eq!(history[0]["delta"], "5000"); assert_eq!(history[1]["kind"], "token_mint"); @@ -642,14 +1070,23 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { let (_, _, tokens) = call_api(&live.app, "/api/v1/bridge/tokens").await; let tokens = tokens.as_array().expect("array"); assert_eq!(tokens.len(), 8); - assert_eq!(tokens[0], json!({ - "symbol": "USDT", "name": "Tether USD", "chain": 2, "chain_name": "Ethereum", "standard": "ERC-20", - "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7", - "token": format!("{}dac17f958d2ee523a2206206994597c13d831ec7", "0".repeat(24)), - "decimals": 6, "status": "allowed", - "explorer_url": "https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7" - })); - assert_eq!(tokens.iter().filter(|t| t["status"] == "discontinued").count(), 1); + assert_eq!( + tokens[0], + json!({ + "symbol": "USDT", "name": "Tether USD", "chain": 2, "chain_name": "Ethereum", "standard": "ERC-20", + "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "token": format!("{}dac17f958d2ee523a2206206994597c13d831ec7", "0".repeat(24)), + "decimals": 6, "status": "allowed", + "explorer_url": "https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7" + }) + ); + assert_eq!( + tokens + .iter() + .filter(|t| t["status"] == "discontinued") + .count(), + 1 + ); let (_, _, p) = call_api(&live.app, &format!("/api/v1/programs/{program}")).await; assert_eq!(p["call_count"], 1); assert!(p.get("deployer").is_none(), "{p}"); @@ -659,16 +1096,38 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { assert_eq!(p["last_invoked_height"], 3); assert_eq!(p["recent_calls"].as_array().unwrap().len(), 2, "{p}"); assert_eq!(p["recent_calls"][0]["kind"], "invoke"); - assert_eq!(p["program_state"]["vault"], json!([{ "asset": 0, "amount": "700" }, { "asset": 3, "amount": "500" }])); - assert_eq!(p["program_state"]["cells"], json!([{ "key": cell_key, "value": format!("05{}", "00".repeat(31)) }])); + assert_eq!( + p["program_state"]["vault"], + json!([{ "asset": 0, "amount": "700" }, { "asset": 3, "amount": "500" }]) + ); + assert_eq!( + p["program_state"]["cells"], + json!([{ "key": cell_key, "value": format!("05{}", "00".repeat(31)) }]) + ); assert!(p["program_state"]["cells_next"].is_null()); - let (status, _, cells) = call_api(&live.app, &format!("/api/v1/programs/{program}/cells?limit=1")).await; + let (status, _, cells) = call_api( + &live.app, + &format!("/api/v1/programs/{program}/cells?limit=1"), + ) + .await; assert_eq!(status, 200, "{cells}"); assert_eq!(cells["cells"][0]["key"], cell_key); assert!(cells["next"].is_null()); - let (status, _, cells) = call_api(&live.app, &format!("/api/v1/programs/{program}/cells?after={cell_key}")).await; - assert_eq!((status.as_u16(), cells["cells"].as_array().map(Vec::len)), (200, Some(0)), "{cells}"); - let (status, _, _) = call_api(&live.app, &format!("/api/v1/programs/{program}/cells?after=nothex")).await; + let (status, _, cells) = call_api( + &live.app, + &format!("/api/v1/programs/{program}/cells?after={cell_key}"), + ) + .await; + assert_eq!( + (status.as_u16(), cells["cells"].as_array().map(Vec::len)), + (200, Some(0)), + "{cells}" + ); + let (status, _, _) = call_api( + &live.app, + &format!("/api/v1/programs/{program}/cells?after=nothex"), + ) + .await; assert_eq!(status, 400); // The invoke itself: a call's fields plus the transition, served as the node sent it (amounts @@ -681,22 +1140,52 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { assert_eq!(d["call_proof_len"], 268123); assert!(d["input_envelope_len"].is_null()); assert_eq!(d["bundle"]["burn_r"], "1000"); - assert_eq!((&d["bundle"]["burn_a"], &d["bundle"]["burn_asset"]), (&json!("500"), &json!(3))); + assert_eq!( + (&d["bundle"]["burn_a"], &d["bundle"]["burn_asset"]), + (&json!("500"), &json!(3)) + ); assert_eq!(d["transition"]["inflow"], "deposit"); - assert_eq!(d["transition"]["reads"], json!([{ "key": cell_key, "value": "00".repeat(32) }])); - assert_eq!(d["transition"]["writes"][0]["value"], format!("05{}", "00".repeat(31))); - assert_eq!(d["transition"]["pays"], json!([{ "asset": 0, "amount": "300", "recipient": invoke_recipient, "time": 2, "r": "a1".repeat(32), "cm": h("payout-0") }])); - assert_eq!(d["transition"]["mints"][0]["amount"], "40", "a numeric amount on the wire is a decimal string here"); + assert_eq!( + d["transition"]["reads"], + json!([{ "key": cell_key, "value": "00".repeat(32) }]) + ); + assert_eq!( + d["transition"]["writes"][0]["value"], + format!("05{}", "00".repeat(31)) + ); + assert_eq!( + d["transition"]["pays"], + json!([{ "asset": 0, "amount": "300", "recipient": invoke_recipient, "time": 2, "r": "a1".repeat(32), "cm": h("payout-0") }]) + ); + assert_eq!( + d["transition"]["mints"][0]["amount"], "40", + "a numeric amount on the wire is a decimal string here" + ); assert_eq!(d["transition"]["mints"][0]["cm"], h("payout-1")); - assert_eq!(d["receipt"]["tier"], 14, "an invoke has a call's receipt: {d}"); + assert_eq!( + d["receipt"]["tier"], 14, + "an invoke has a call's receipt: {d}" + ); assert_eq!(d["receipt"]["program"], program); for cm in [h("payout-0"), h("payout-1")] { let (_, _, note) = call_api(&live.app, &format!("/api/v1/notes/{cm}")).await; - assert_eq!(note["tx_hash"], invoke_hash, "a payout leaf links back to its invoke: {note}"); + assert_eq!( + note["tx_hash"], invoke_hash, + "a payout leaf links back to its invoke: {note}" + ); } - let (_, _, env) = call_api(&live.app, &format!("/api/v1/transactions/{invoke_hash}/envelopes")).await; + let (_, _, env) = call_api( + &live.app, + &format!("/api/v1/transactions/{invoke_hash}/envelopes"), + ) + .await; assert_eq!(env["kind"], "invoke"); - let cms: Vec<&str> = env["notes"].as_array().unwrap().iter().map(|n| n["cm"].as_str().unwrap()).collect(); + let cms: Vec<&str> = env["notes"] + .as_array() + .unwrap() + .iter() + .map(|n| n["cm"].as_str().unwrap()) + .collect(); assert_eq!(cms.len(), 6, "four bundle slots and two payouts: {env}"); assert!(cms.contains(&h("payout-0").as_str()) && cms.contains(&h("payout-1").as_str())); let (_, _, list) = call_api(&live.app, "/api/v1/transactions?kind=invoke").await; @@ -719,10 +1208,25 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { kinds }; for k in [ - TxKind::Transfer, TxKind::Mint, TxKind::Deploy, TxKind::Call, TxKind::Bond, TxKind::Unbond, - TxKind::Withdraw, TxKind::BridgeAttest, TxKind::BridgeBurn, TxKind::RegisterToken, TxKind::TokenMint, - TxKind::SetAuthority, TxKind::TokenBurn, TxKind::PauseMints, TxKind::UnpauseMints, - TxKind::RegisterBridgedToken, TxKind::ListBacking, TxKind::Invoke, TxKind::Other, + TxKind::Transfer, + TxKind::Mint, + TxKind::Deploy, + TxKind::Call, + TxKind::Bond, + TxKind::Unbond, + TxKind::Withdraw, + TxKind::BridgeAttest, + TxKind::BridgeBurn, + TxKind::RegisterToken, + TxKind::TokenMint, + TxKind::SetAuthority, + TxKind::TokenBurn, + TxKind::PauseMints, + TxKind::UnpauseMints, + TxKind::RegisterBridgedToken, + TxKind::ListBacking, + TxKind::Invoke, + TxKind::Other, ] { assert!(got.contains(&k), "broadcast kinds miss {k}: {got:?}"); } @@ -730,30 +1234,72 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { // ----- phase 2: a user and an API key exist; the fleet hard-forks to chain 8 (an S3-only // node: register without pending/active, no epoch, no supply) ---------------------------- let body = json!({ "email": unique_email(), "password": "correct horse battery" }); - let (status, headers, _) = call(&live.app, json_req("POST", "/api/v1/auth/signup", Some(body), None)).await; + let (status, headers, _) = call( + &live.app, + json_req("POST", "/api/v1/auth/signup", Some(body), None), + ) + .await; assert_eq!(status, 201); let cookie = session_cookie_from(&headers); - let (status, _, created) = call(&live.app, json_req("POST", "/api/v1/keys", Some(json!({ "name": "bot" })), Some(&cookie))).await; + let (status, _, created) = call( + &live.app, + json_req( + "POST", + "/api/v1/keys", + Some(json!({ "name": "bot" })), + Some(&cookie), + ), + ) + .await; assert_eq!(status, 201, "{created}"); let key = created["key"].as_str().unwrap().to_string(); node.with_chain(|c| { *c = MockChain::new(8, "genesis-b"); c.pre_s2 = true; - c.push_block(vec![tx(8, "t", Some(bundle("t8")), json!({ "kind": "none" }))]); + c.push_block(vec![tx( + 8, + "t", + Some(bundle("t8")), + json!({ "kind": "none" }), + )]); }); - let stats = live.wait_for("/api/v1/stats", WAIT, |s| s["chain_id"] == 8 && s["height"] == 1).await; - assert_eq!(stats["total_transactions"], 1, "old chain data gone: {stats}"); - assert_eq!(stats["notes"], 5, "the genesis note plus the one bundle's four output slots"); - assert!(stats["epoch"].is_null() && stats["pool_value"].is_null(), "S2 fields absent on an S3 node: {stats}"); + let stats = live + .wait_for("/api/v1/stats", WAIT, |s| { + s["chain_id"] == 8 && s["height"] == 1 + }) + .await; + assert_eq!( + stats["total_transactions"], 1, + "old chain data gone: {stats}" + ); + assert_eq!( + stats["notes"], 5, + "the genesis note plus the one bundle's four output slots" + ); + assert!( + stats["epoch"].is_null() && stats["pool_value"].is_null(), + "S2 fields absent on an S3 node: {stats}" + ); // A node older than v0.3 / v0.4 has none of the three methods; the old chain's answers must // not linger. The FRI profile still comes from `rand_status`. - assert!(stats["limits"].is_null() && stats["genesis_hash"].is_null() && stats["node_git_sha"].is_null(), "{stats}"); - assert!(stats["hc_auth"].is_null() && stats["gas_prices"].is_null(), "no auth guest, no gas section: {stats}"); + assert!( + stats["limits"].is_null() + && stats["genesis_hash"].is_null() + && stats["node_git_sha"].is_null(), + "{stats}" + ); + assert!( + stats["hc_auth"].is_null() && stats["gas_prices"].is_null(), + "no auth guest, no gas section: {stats}" + ); assert_eq!(stats["fri_profile"], "test"); assert_eq!(stats["total_supply"], "0"); - assert_eq!(stats["active_validator_count"], 1, "every register entry is active before S2"); + assert_eq!( + stats["active_validator_count"], 1, + "every register entry is active before S2" + ); let (status, _, _) = call_api(&live.app, &format!("/api/v1/transactions/{burn_hash}")).await; assert_eq!(status, 404); let (status, _, _) = call_api(&live.app, &format!("/api/v1/notes/{}", h("cm-mint"))).await; @@ -772,9 +1318,17 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { assert_eq!(next_leaf, 5); // Users, sessions and keys survived the fork. - let (status, _, me) = call(&live.app, json_req("GET", "/api/v1/auth/me", None, Some(&cookie))).await; + let (status, _, me) = call( + &live.app, + json_req("GET", "/api/v1/auth/me", None, Some(&cookie)), + ) + .await; assert_eq!(status, 200, "{me}"); - let (_, _, keys) = call(&live.app, json_req("GET", "/api/v1/keys", None, Some(&cookie))).await; + let (_, _, keys) = call( + &live.app, + json_req("GET", "/api/v1/keys", None, Some(&cookie)), + ) + .await; assert_eq!(keys.as_array().unwrap().len(), 1); let req = axum::http::Request::builder() .uri("/api/v1/stats") @@ -789,21 +1343,39 @@ async fn indexes_every_shielded_kind_and_survives_hard_forks() { c.push_block(vec![]); c.push_block(vec![]); }); - live.wait_for("/api/v1/health", WAIT, |b| b["indexer"]["current_height"] == 3).await; + live.wait_for("/api/v1/health", WAIT, |b| { + b["indexer"]["current_height"] == 3 + }) + .await; node.with_chain(|c| *c = MockChain::new(8, "genesis-c")); // The forced re-check is rate limited (10 s), so allow for that. let b0 = live - .wait_for("/api/v1/blocks/0", WAIT, |b| b["hash"] == h("block-8-genesis-c-0")) + .wait_for("/api/v1/blocks/0", WAIT, |b| { + b["hash"] == h("block-8-genesis-c-0") + }) .await; assert_eq!(b0["height"], 0); let (status, _, _) = call_api(&live.app, "/api/v1/blocks/3").await; assert_eq!(status, 404); - let notes = live.wait_for("/api/v1/notes", WAIT, |n| n["pagination"]["total"] == 1).await; - assert_eq!(notes["data"][0]["cm"], h("genesis-note-8-genesis-c"), "the tree was re-read from leaf 0"); - let (status, _, _) = call(&live.app, json_req("GET", "/api/v1/auth/me", None, Some(&cookie))).await; + let notes = live + .wait_for("/api/v1/notes", WAIT, |n| n["pagination"]["total"] == 1) + .await; + assert_eq!( + notes["data"][0]["cm"], + h("genesis-note-8-genesis-c"), + "the tree was re-read from leaf 0" + ); + let (status, _, _) = call( + &live.app, + json_req("GET", "/api/v1/auth/me", None, Some(&cookie)), + ) + .await; assert_eq!(status, 200); } -async fn call_api(app: &axum::Router, path: &str) -> (axum::http::StatusCode, axum::http::HeaderMap, Value) { +async fn call_api( + app: &axum::Router, + path: &str, +) -> (axum::http::StatusCode, axum::http::HeaderMap, Value) { call(app, json_req("GET", path, None, None)).await } diff --git a/crates/randscan-api/tests/real_node.rs b/crates/randscan-api/tests/real_node.rs index cd74743..8492a80 100644 --- a/crates/randscan-api/tests/real_node.rs +++ b/crates/randscan-api/tests/real_node.rs @@ -154,11 +154,24 @@ async fn start_node(bin: &PathBuf, cli: &PathBuf) -> (Node, String, String) { if help.contains("--gas-price") && help.contains("--auth-guest") { args.extend( [ - "--hardening-v6", "--bundle-guest", "v3", "--auth-guest", - "--max-proof-bytes", "4194304", "--max-block-bytes", "20971520", - "--envelope-bytes", "1860", - "--gas-price", "100", "--byte-price", "800", "--bundle-gas-limit", "20479", - "--gas-dynamic", "10485760,262144,1250", + "--hardening-v6", + "--bundle-guest", + "v3", + "--auth-guest", + "--max-proof-bytes", + "4194304", + "--max-block-bytes", + "20971520", + "--envelope-bytes", + "1860", + "--gas-price", + "100", + "--byte-price", + "800", + "--bundle-gas-limit", + "20479", + "--gas-dynamic", + "10485760,262144,1250", ] .into_iter() .map(str::to_string), @@ -172,7 +185,11 @@ async fn start_node(bin: &PathBuf, cli: &PathBuf) -> (Node, String, String) { args.extend(["--binding-domain", "1"].into_iter().map(str::to_string)); } if help.contains("--proof-window-blocks") { - args.extend(["--proof-window-blocks", "1024"].into_iter().map(str::to_string)); + args.extend( + ["--proof-window-blocks", "1024"] + .into_iter() + .map(str::to_string), + ); } // RPL-2 (fullnode v0.6.8, `feat/rpl2`): a build that knows the `program_state` section gets // one, with the token registry it stands on, so the explorer meets an `invoke`, a program's @@ -186,9 +203,14 @@ async fn start_node(bin: &PathBuf, cli: &PathBuf) -> (Node, String, String) { ) .unwrap(); args.extend( - ["--tokens", tokens_json.to_str().unwrap(), "--program-state-cell-fee", "10000000"] - .into_iter() - .map(str::to_string), + [ + "--tokens", + tokens_json.to_str().unwrap(), + "--program-state-cell-fee", + "10000000", + ] + .into_iter() + .map(str::to_string), ); } run(bin, &args.iter().map(String::as_str).collect::>()); @@ -265,12 +287,18 @@ async fn explorer_agrees_with_a_real_shielded_node() { // instead of passing it as a skip. let required = std::env::var_os("RANDSCAN_REQUIRE_REAL_NODE").is_some(); let Ok(bin) = std::env::var("RAND_NODE_BIN") else { - assert!(!required, "RANDSCAN_REQUIRE_REAL_NODE is set but RAND_NODE_BIN is not"); + assert!( + !required, + "RANDSCAN_REQUIRE_REAL_NODE is set but RAND_NODE_BIN is not" + ); eprintln!("skipping: RAND_NODE_BIN unset"); return; }; if std::env::var("DATABASE_URL").is_err() { - assert!(!required, "RANDSCAN_REQUIRE_REAL_NODE is set but DATABASE_URL is not"); + assert!( + !required, + "RANDSCAN_REQUIRE_REAL_NODE is set but DATABASE_URL is not" + ); eprintln!("skipping: DATABASE_URL unset"); return; } @@ -318,7 +346,15 @@ async fn explorer_agrees_with_a_real_shielded_node() { .trim() .to_string(); // Since v0.5.10 `rand send` confirms first and refuses a non-terminal stdin without `--yes`. - let mut send = vec!["send", &wallet2_addr, "1.25", "--key", wallet, "--rpc", &node.url]; + let mut send = vec![ + "send", + &wallet2_addr, + "1.25", + "--key", + wallet, + "--rpc", + &node.url, + ]; if run(&cli, &["send", "--help"]).contains("--yes") { send.push("--yes"); } @@ -463,8 +499,16 @@ async fn explorer_agrees_with_a_real_shielded_node() { // node serves them (`null` on a node or chain without), and the limits carry the genesis' // envelope format, auth guest and gas section. A chain-18 build got that genesis above. let null = json!(null); - assert_eq!(stats["hc_auth"], *node_status.get("hc_auth").unwrap_or(&null), "{stats}"); - assert_eq!(stats["gas_prices"], *node_status.get("gas_prices").unwrap_or(&null), "{stats}"); + assert_eq!( + stats["hc_auth"], + *node_status.get("hc_auth").unwrap_or(&null), + "{stats}" + ); + assert_eq!( + stats["gas_prices"], + *node_status.get("gas_prices").unwrap_or(&null), + "{stats}" + ); let node_limits = rpc(&client, &node.url, "rand_getLimits", json!([])).await; if node_limits["gas_metering"] == "circuit" { let l = &stats["limits"]; @@ -474,17 +518,27 @@ async fn explorer_agrees_with_a_real_shielded_node() { assert_eq!(l["envelope_bytes"], 1860); assert_eq!(l["hardening_v6"], true); assert_eq!(l["hc_auth"], node_status["hc_auth"]); - assert!(l["hc_auth"].is_string(), "a v3 chain pins an auth guest: {l}"); + assert!( + l["hc_auth"].is_string(), + "a v3 chain pins an auth guest: {l}" + ); assert!(stats["gas_prices"]["gas_price"].is_string(), "{stats}"); // The genesis prices are the floors; the tip's cannot be below them. let floor: u64 = node_limits["gas_price"].as_str().unwrap().parse().unwrap(); - let tip: u64 = stats["gas_prices"]["gas_price"].as_str().unwrap().parse().unwrap(); + let tip: u64 = stats["gas_prices"]["gas_price"] + .as_str() + .unwrap() + .parse() + .unwrap(); assert!(tip >= floor, "{tip} < {floor}"); // Under `hc_auth` every bundle carries an auth proof and every envelope is 1 860 B. assert_eq!(transfer["bundle"]["auth_commit"], nb["auth_commit"]); assert!(nb["auth_proof_bytes"].as_u64().unwrap() > 0, "{nb}"); assert_eq!(transfer["bundle"]["auth_proof_len"], nb["auth_proof_bytes"]); - assert_eq!(transfer["bundle"]["envelope_len"], json!([1860, 1860, 1860, 1860])); + assert_eq!( + transfer["bundle"]["envelope_len"], + json!([1860, 1860, 1860, 1860]) + ); } assert_eq!(stats["validator_count"], 1); assert_eq!(stats["faucet"], true); @@ -498,7 +552,10 @@ async fn explorer_agrees_with_a_real_shielded_node() { assert_eq!(list[0]["active"], true); // The proposer earned the transfer's fee (and the deploy/call fees below, later). let rewards: u128 = list[0]["rewards"].as_str().unwrap().parse().unwrap(); - assert!(rewards >= units(&nb["fee"]).parse::().unwrap(), "{list:?}"); + assert!( + rewards >= units(&nb["fee"]).parse::().unwrap(), + "{list:?}" + ); let (status, _, found) = call( &live.app, @@ -668,11 +725,39 @@ async fn explorer_agrees_with_a_real_shielded_node() { /// the program token — three proofs. Then the explorer's view against the node's: the invoke /// with its transition, a call's receipt, both payout leaves linked to it, the program's cells /// and vault, the limits' `program_state` group and the supply's vault counters. -async fn invoke_round_trip(live: &LiveApp, client: &reqwest::Client, node: &Node, cli: &PathBuf, wallet: &str, payee: &str) -> i64 { +async fn invoke_round_trip( + live: &LiveApp, + client: &reqwest::Client, + node: &Node, + cli: &PathBuf, + wallet: &str, + payee: &str, +) -> i64 { let dir = node.dir.path(); let counter_json = dir.join("counter.json"); - run(cli, &["program", "build", "--guest", "rpl2_counter", "--out", counter_json.to_str().unwrap()]); - let out = run(cli, &["program", "deploy", counter_json.to_str().unwrap(), "--rpc", &node.url, "--key", wallet]); + run( + cli, + &[ + "program", + "build", + "--guest", + "rpl2_counter", + "--out", + counter_json.to_str().unwrap(), + ], + ); + let out = run( + cli, + &[ + "program", + "deploy", + counter_json.to_str().unwrap(), + "--rpc", + &node.url, + "--key", + wallet, + ], + ); let counter = out .lines() .find_map(|l| l.strip_prefix("program id: ")) @@ -681,15 +766,39 @@ async fn invoke_round_trip(live: &LiveApp, client: &reqwest::Client, node: &Node .to_string(); let out = run( cli, - &["token", "create", "--name", "Counter Share", "--symbol", "CTR", "--decimals", "0", "--program", &counter, "--rpc", &node.url, "--key", wallet], + &[ + "token", + "create", + "--name", + "Counter Share", + "--symbol", + "CTR", + "--decimals", + "0", + "--program", + &counter, + "--rpc", + &node.url, + "--key", + wallet, + ], ); // The wallet reports it as "submitted token registration ". let register_hash = submitted_hash(&out, "token registration"); let registered = live - .wait_for(&format!("/api/v1/transactions/{register_hash}"), WAIT, |t| t["kind"] == "register_token") + .wait_for( + &format!("/api/v1/transactions/{register_hash}"), + WAIT, + |t| t["kind"] == "register_token", + ) .await; - let share_index = registered["asset_index"].as_i64().expect("the registration's index"); - assert_eq!(registered["token_action"]["authority"], "program", "{registered}"); + let share_index = registered["asset_index"] + .as_i64() + .expect("the registration's index"); + assert_eq!( + registered["token_action"]["authority"], "program", + "{registered}" + ); // The counter accepts exactly a transition that reads one cell and writes its first word // plus one; everything else rides along (`docs/cli.md`, "A stateful program"). @@ -707,20 +816,47 @@ async fn invoke_round_trip(live: &LiveApp, client: &reqwest::Client, node: &Node .to_string(), ) .unwrap(); - let out = run(cli, &["program", "invoke", &counter, "--transition", transition.to_str().unwrap(), "--rpc", &node.url, "--key", wallet]); + let out = run( + cli, + &[ + "program", + "invoke", + &counter, + "--transition", + transition.to_str().unwrap(), + "--rpc", + &node.url, + "--key", + wallet, + ], + ); let invoke_hash = submitted_hash(&out, "invoke"); - let node_tx = rpc(client, &node.url, "rand_getTransaction", json!([invoke_hash])).await; + let node_tx = rpc( + client, + &node.url, + "rand_getTransaction", + json!([invoke_hash]), + ) + .await; assert_eq!(node_tx["tx"]["action"]["kind"], "invoke", "{node_tx}"); let node_receipt = rpc(client, &node.url, "rand_getReceipt", json!([invoke_hash])).await; - assert!(node_receipt.is_object(), "node has no receipt for {invoke_hash}: {node_receipt}"); + assert!( + node_receipt.is_object(), + "node has no receipt for {invoke_hash}: {node_receipt}" + ); let tx = live - .wait_for(&format!("/api/v1/transactions/{invoke_hash}"), WAIT, |t| t["receipt"].is_object()) + .wait_for(&format!("/api/v1/transactions/{invoke_hash}"), WAIT, |t| { + t["receipt"].is_object() + }) .await; assert_eq!(tx["kind"], "invoke"); assert_eq!(tx["program"], counter); assert_eq!(tx["call_proof_len"], node_tx["tx"]["action"]["proof_len"]); - assert_eq!(tx["bundle"]["burn_r"], "500000000", "the RAND the transition deposited is the bundle's burn: {tx}"); + assert_eq!( + tx["bundle"]["burn_r"], "500000000", + "the RAND the transition deposited is the bundle's burn: {tx}" + ); let t = &tx["transition"]; let nt = &node_tx["tx"]["action"]["transition"]; assert_eq!(t["inflow"], "none"); @@ -734,7 +870,10 @@ async fn invoke_round_trip(live: &LiveApp, client: &reqwest::Client, node: &Node assert_eq!(ours["cm"], theirs["cm"], "{side}: {tx}"); assert_eq!(ours["recipient"], theirs["recipient"]); assert_eq!(ours["amount"], units(&theirs["amount"])); - assert_eq!(ours["time"], node_tx["tx"]["bundle"]["time"], "a payout note's time is the bundle's"); + assert_eq!( + ours["time"], node_tx["tx"]["bundle"]["time"], + "a payout note's time is the bundle's" + ); } assert_eq!(t["pays"][0]["recipient"], payee); assert_eq!(t["mints"][0]["asset"], share_index); @@ -742,42 +881,106 @@ async fn invoke_round_trip(live: &LiveApp, client: &reqwest::Client, node: &Node assert_eq!(tx["receipt"]["outputs"][0], 1, "the counter's new count"); // Both payout leaves are the chain's, linked to the invoke, among its notes. - let head = rpc(client, &node.url, "rand_getHead", json!([])).await["height"].as_i64().unwrap(); - live.wait_for("/api/v1/health", WAIT, |h| h["indexer"]["current_height"].as_i64().unwrap_or(-1) >= head).await; + let head = rpc(client, &node.url, "rand_getHead", json!([])).await["height"] + .as_i64() + .unwrap(); + live.wait_for("/api/v1/health", WAIT, |h| { + h["indexer"]["current_height"].as_i64().unwrap_or(-1) >= head + }) + .await; let tree = rpc(client, &node.url, "rand_getTreeInfo", json!([])).await; - live.wait_for("/api/v1/stats", WAIT, |s| s["notes"] == tree["next_index"]).await; - for cm in [t["pays"][0]["cm"].as_str().unwrap(), t["mints"][0]["cm"].as_str().unwrap()] { - let (status, _, n) = call(&live.app, json_req("GET", &format!("/api/v1/notes/{cm}"), None, None)).await; + live.wait_for("/api/v1/stats", WAIT, |s| s["notes"] == tree["next_index"]) + .await; + for cm in [ + t["pays"][0]["cm"].as_str().unwrap(), + t["mints"][0]["cm"].as_str().unwrap(), + ] { + let (status, _, n) = call( + &live.app, + json_req("GET", &format!("/api/v1/notes/{cm}"), None, None), + ) + .await; assert_eq!(status, 200, "{n}"); assert_eq!(n["tx_hash"], invoke_hash, "{n}"); } - let (_, _, env) = call(&live.app, json_req("GET", &format!("/api/v1/transactions/{invoke_hash}/envelopes"), None, None)).await; - assert_eq!(env["notes"].as_array().map(Vec::len), Some(6), "four slots and two payouts: {env}"); + let (_, _, env) = call( + &live.app, + json_req( + "GET", + &format!("/api/v1/transactions/{invoke_hash}/envelopes"), + None, + None, + ), + ) + .await; + assert_eq!( + env["notes"].as_array().map(Vec::len), + Some(6), + "four slots and two payouts: {env}" + ); // The program: counted as an invoke, its cell and its vault as the node serves them. let program = live - .wait_for(&format!("/api/v1/programs/{counter}"), WAIT, |p| p["invoke_count"] == 1) + .wait_for(&format!("/api/v1/programs/{counter}"), WAIT, |p| { + p["invoke_count"] == 1 + }) .await; assert_eq!(program["call_count"], 0); assert_eq!(program["recent_calls"][0]["hash"], invoke_hash); - let node_cells = rpc(client, &node.url, "rand_getProgramCells", json!([counter, { "limit": 10 }])).await; + let node_cells = rpc( + client, + &node.url, + "rand_getProgramCells", + json!([counter, { "limit": 10 }]), + ) + .await; let node_vault = rpc(client, &node.url, "rand_getProgramVault", json!([counter])).await; assert_eq!(program["program_state"]["cells"], node_cells["cells"]); - assert_eq!(program["program_state"]["cells"][0], json!({ "key": cell_key, "value": cell_key })); + assert_eq!( + program["program_state"]["cells"][0], + json!({ "key": cell_key, "value": cell_key }) + ); assert_eq!(program["program_state"]["vault"], node_vault); - assert_eq!(program["program_state"]["vault"][0], json!({ "asset": 0, "amount": "300000000" }), "0.5 in, 0.2 out"); - let (status, _, page) = call(&live.app, json_req("GET", &format!("/api/v1/programs/{counter}/cells?limit=1"), None, None)).await; + assert_eq!( + program["program_state"]["vault"][0], + json!({ "asset": 0, "amount": "300000000" }), + "0.5 in, 0.2 out" + ); + let (status, _, page) = call( + &live.app, + json_req( + "GET", + &format!("/api/v1/programs/{counter}/cells?limit=1"), + None, + None, + ), + ) + .await; assert_eq!(status, 200, "{page}"); assert_eq!(page["cells"], node_cells["cells"]); // The limits' group and the supply's vault counters, both the node's own figures. let node_limits = rpc(client, &node.url, "rand_getLimits", json!([])).await; - let stats = live.wait_for("/api/v1/stats", WAIT, |s| s["limits"]["program_state"].is_object()).await; - assert_eq!(stats["limits"]["program_state"], node_limits["program_state"]); + let stats = live + .wait_for("/api/v1/stats", WAIT, |s| { + s["limits"]["program_state"].is_object() + }) + .await; + assert_eq!( + stats["limits"]["program_state"], + node_limits["program_state"] + ); assert_eq!(stats["limits"]["program_state"]["cell_fee"], "10000000"); let node_supply = rpc(client, &node.url, "rand_getSupply", json!([])).await; - let supply = live.wait_for("/api/v1/supply", WAIT, |s| s["program_rand_held"] == json!("300000000")).await; - assert_eq!(supply["program_rand_out"], units(&node_supply["program_rand_out"])); + let supply = live + .wait_for("/api/v1/supply", WAIT, |s| { + s["program_rand_held"] == json!("300000000") + }) + .await; + assert_eq!( + supply["program_rand_out"], + units(&node_supply["program_rand_out"]) + ); assert_eq!(supply["program_rand_out"], "200000000"); assert_eq!(supply["invariant_holds"], true, "{supply}"); share_index @@ -809,40 +1012,78 @@ async fn consumer_contract( const ZUSD: &str = "zusd.money src/lib/balance-sheet.mjs (via BalanceSheet.astro)"; const RESERVES: &str = "randprotocol.org src/components/BridgeReserves.astro"; const BALANCE: &str = "randprotocol.org src/scripts/balance.js"; - let get = |path: String| async move { call(&live.app, json_req("GET", &path, None, None)).await }; - let is_units = |v: &Value| v.as_str().is_some_and(|s| !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())); + let get = + |path: String| async move { call(&live.app, json_req("GET", &path, None, None)).await }; + let is_units = |v: &Value| { + v.as_str() + .is_some_and(|s| !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())) + }; // Settle on the node's head first, so the bridge/token/supply caches have been refreshed. - let head = rpc(client, &node.url, "rand_getHead", json!([])).await["height"].as_i64().unwrap(); - live.wait_for("/api/v1/health", WAIT, |h| h["indexer"]["current_height"].as_i64().unwrap_or(-1) >= head).await; + let head = rpc(client, &node.url, "rand_getHead", json!([])).await["height"] + .as_i64() + .unwrap(); + live.wait_for("/api/v1/health", WAIT, |h| { + h["indexer"]["current_height"].as_i64().unwrap_or(-1) >= head + }) + .await; // --- /bridge --------------------------------------------------------------------------- let node_bridge = rpc(client, &node.url, "rand_getBridgeState", json!([])).await; let (status, _, bridge) = get("/api/v1/bridge".into()).await; assert_eq!(status, 200, "{ZUSD} and {RESERVES} fetch /bridge: {bridge}"); - assert!(bridge["enabled"].is_boolean(), "{ZUSD} reads bridge.enabled as a boolean: {bridge}"); - assert!(bridge["mint_paused"].is_boolean(), "{ZUSD} reads bridge.mint_paused as a boolean: {bridge}"); + assert!( + bridge["enabled"].is_boolean(), + "{ZUSD} reads bridge.enabled as a boolean: {bridge}" + ); + assert!( + bridge["mint_paused"].is_boolean(), + "{ZUSD} reads bridge.mint_paused as a boolean: {bridge}" + ); let enabled = node_bridge["enabled"].as_bool().unwrap_or(false); - assert_eq!(bridge["enabled"], enabled, "{ZUSD}: /bridge.enabled must be the node's: node {node_bridge}"); + assert_eq!( + bridge["enabled"], enabled, + "{ZUSD}: /bridge.enabled must be the node's: node {node_bridge}" + ); if !enabled { - assert_eq!(bridge["mint_paused"], false, "{ZUSD}: a chain without a bridge reports no pause: {bridge}"); + assert_eq!( + bridge["mint_paused"], false, + "{ZUSD}: a chain without a bridge reports no pause: {bridge}" + ); } // --- /bridge/assets ---------------------------------------------------------------------- let (status, _, assets) = get("/api/v1/bridge/assets".into()).await; - assert_eq!(status, 200, "{ZUSD} and {RESERVES} fetch /bridge/assets: {assets}"); - let rows = assets.as_array().unwrap_or_else(|| panic!("{ZUSD} iterates /bridge/assets as an array: {assets}")); + assert_eq!( + status, 200, + "{ZUSD} and {RESERVES} fetch /bridge/assets: {assets}" + ); + let rows = assets + .as_array() + .unwrap_or_else(|| panic!("{ZUSD} iterates /bridge/assets as an array: {assets}")); if !enabled { - assert!(rows.is_empty(), "{ZUSD}: a chain without a bridge lists no backings ([]): {assets}"); + assert!( + rows.is_empty(), + "{ZUSD}: a chain without a bridge lists no backings ([]): {assets}" + ); } for a in rows { assert!(a["index"].is_i64(), "{ZUSD} groups by row.index: {a}"); assert!(a["chain"].is_i64(), "{ZUSD} keys rows by row.chain: {a}"); - assert!(a["symbol"].is_string() || a["symbol"].is_null(), "{ZUSD} filters on row.symbol (string or null): {a}"); + assert!( + a["symbol"].is_string() || a["symbol"].is_null(), + "{ZUSD} filters on row.symbol (string or null): {a}" + ); if a["symbol"].is_string() { // Only symbol'd rows are summed, with BigInt(row.locked): null there would throw. - assert!(is_units(&a["locked"]), "{ZUSD} sums BigInt(row.locked): {a}"); - assert!(is_units(&a["minted_today"]) && is_units(&a["mint_cap_per_day"]), "{RESERVES} renders minted_today / mint_cap_per_day: {a}"); + assert!( + is_units(&a["locked"]), + "{ZUSD} sums BigInt(row.locked): {a}" + ); + assert!( + is_units(&a["minted_today"]) && is_units(&a["mint_cap_per_day"]), + "{RESERVES} renders minted_today / mint_cap_per_day: {a}" + ); } } @@ -850,7 +1091,10 @@ async fn consumer_contract( // The sites ask for each index the bridge rows name and treat a non-2xx as "unknown"; an // index nobody registered must be a 404, never a 200 with a made-up supply. let (status, _, missing) = get("/api/v1/tokens/987654".into()).await; - assert_eq!(status, 404, "{ZUSD} rejects a non-ok /tokens/{{index}}: {missing}"); + assert_eq!( + status, 404, + "{ZUSD} rejects a non-ok /tokens/{{index}}: {missing}" + ); match token_index { Some(index) => { let node_token = rpc(client, &node.url, "rand_getToken", json!([index])).await; @@ -871,53 +1115,102 @@ async fn consumer_contract( let tree = rpc(client, &node.url, "rand_getTreeInfo", json!([])).await; let leaves = tree["next_index"].as_i64().unwrap(); let page = live - .wait_for("/api/v1/envelopes?from_leaf=0&limit=1000", WAIT, |p| p["total_leaves"] == leaves) + .wait_for("/api/v1/envelopes?from_leaf=0&limit=1000", WAIT, |p| { + p["total_leaves"] == leaves + }) .await; - assert!(page["next_leaf"].is_null(), "{BALANCE} stops when next_leaf is null (every leaf on one page): {page}"); - let notes = page["notes"].as_array().unwrap_or_else(|| panic!("{BALANCE} iterates page.notes: {page}")); - assert_eq!(notes.len() as i64, leaves, "{BALANCE}: one row per leaf: {page}"); + assert!( + page["next_leaf"].is_null(), + "{BALANCE} stops when next_leaf is null (every leaf on one page): {page}" + ); + let notes = page["notes"] + .as_array() + .unwrap_or_else(|| panic!("{BALANCE} iterates page.notes: {page}")); + assert_eq!( + notes.len() as i64, + leaves, + "{BALANCE}: one row per leaf: {page}" + ); for (i, n) in notes.iter().enumerate() { - assert_eq!(n["leaf_index"], i as i64, "{BALANCE} sorts on n.leaf_index, oldest first: {n}"); + assert_eq!( + n["leaf_index"], i as i64, + "{BALANCE} sorts on n.leaf_index, oldest first: {n}" + ); assert!(n["cm"].is_string(), "{BALANCE} opens n.cm: {n}"); assert!(n["height"].is_i64(), "{BALANCE} shows n.height: {n}"); let o = n.as_object().unwrap(); for key in ["tx_hash", "envelope", "public"] { - assert!(o.contains_key(key), "{BALANCE} destructures n.{key} (null allowed, never missing): {n}"); + assert!( + o.contains_key(key), + "{BALANCE} destructures n.{key} (null allowed, never missing): {n}" + ); } - assert!(n["tx_hash"].is_string() || n["tx_hash"].is_null(), "{BALANCE}: n.tx_hash: {n}"); - assert!(n["envelope"].is_object() || n["envelope"].is_null(), "{BALANCE} passes n.envelope to the opener: {n}"); + assert!( + n["tx_hash"].is_string() || n["tx_hash"].is_null(), + "{BALANCE}: n.tx_hash: {n}" + ); + assert!( + n["envelope"].is_object() || n["envelope"].is_null(), + "{BALANCE} passes n.envelope to the opener: {n}" + ); } let mint_row = notes .iter() .find(|n| n["cm"] == *mint_cm) .unwrap_or_else(|| panic!("{BALANCE}: the faucet mint's leaf is served: {page}")); - assert_eq!(mint_row["tx_hash"], mint_hash, "{BALANCE}: the mint's leaf names its transaction: {mint_row}"); + assert_eq!( + mint_row["tx_hash"], mint_hash, + "{BALANCE}: the mint's leaf names its transaction: {mint_row}" + ); assert!( - notes.iter().any(|n| n["envelope"].is_object() && n["tx_hash"].is_string()), + notes + .iter() + .any(|n| n["envelope"].is_object() && n["tx_hash"].is_string()), "{BALANCE}: at least one leaf carries an envelope to open: {page}" ); // Paging: a short page points at the next leaf, and that page starts there (needs a third leaf). if leaves > 2 { let (status, _, first) = get("/api/v1/envelopes?from_leaf=0&limit=2".into()).await; assert_eq!(status, 200, "{BALANCE}: {first}"); - assert_eq!(first["next_leaf"], 2, "{BALANCE} follows page.next_leaf: {first}"); + assert_eq!( + first["next_leaf"], 2, + "{BALANCE} follows page.next_leaf: {first}" + ); let (_, _, second) = get("/api/v1/envelopes?from_leaf=2&limit=2".into()).await; - assert_eq!(second["notes"][0]["leaf_index"], 2, "{BALANCE}: the next page starts at next_leaf: {second}"); - assert_eq!(second["total_leaves"], leaves, "{BALANCE} reports progress against total_leaves: {second}"); + assert_eq!( + second["notes"][0]["leaf_index"], 2, + "{BALANCE}: the next page starts at next_leaf: {second}" + ); + assert_eq!( + second["total_leaves"], leaves, + "{BALANCE} reports progress against total_leaves: {second}" + ); } // --- /stats -------------------------------------------------------------------------------- let node_limits = rpc(client, &node.url, "rand_getLimits", json!([])).await; - let stats = live.wait_for("/api/v1/stats", WAIT, |s| s["limits"].is_object()).await; + let stats = live + .wait_for("/api/v1/stats", WAIT, |s| s["limits"].is_object()) + .await; let so = stats.as_object().unwrap(); - assert!(so.contains_key("gas_prices"), "stats.gas_prices is served (null allowed): {stats}"); + assert!( + so.contains_key("gas_prices"), + "stats.gas_prices is served (null allowed): {stats}" + ); if node_limits["gas_metering"] == "circuit" { - assert!(is_units(&stats["gas_prices"]["gas_price"]) && is_units(&stats["gas_prices"]["byte_price"]), "a gas-section chain's tip prices: {stats}"); + assert!( + is_units(&stats["gas_prices"]["gas_price"]) + && is_units(&stats["gas_prices"]["byte_price"]), + "a gas-section chain's tip prices: {stats}" + ); } match node_limits.get("fee_rules") { // Only the flags the explorer models (`FeeRules`) are compared, so a flag a later node // adds beside them does not fail the gate; a `null` group must stay `null`. - Some(Value::Null) => assert!(stats["limits"]["fee_rules"].is_null(), "limits.fee_rules is null as the node serves it: {stats}"), + Some(Value::Null) => assert!( + stats["limits"]["fee_rules"].is_null(), + "limits.fee_rules is null as the node serves it: {stats}" + ), Some(rules) => { for flag in ["burn_base", "subsidy_net_of_fees", "burn_floor"] { assert_eq!( @@ -946,7 +1239,10 @@ async fn consumer_contract( if status == 200 && ours["height"] == theirs["height"] { break (ours, theirs); } - assert!(start.elapsed() < WAIT, "/api/v1/supply never caught the node's height: {ours} vs {theirs}"); + assert!( + start.elapsed() < WAIT, + "/api/v1/supply never caught the node's height: {ours} vs {theirs}" + ); tokio::time::sleep(Duration::from_millis(50)).await; }; for (k, v) in supply.as_object().unwrap() { @@ -954,10 +1250,17 @@ async fn consumer_contract( Value::String(_) => { assert!(is_units(v), "supply.{k} is a decimal string: {supply}"); if let Some(theirs) = node_supply.get(k) { - assert_eq!(*v, json!(units(theirs)), "supply.{k} is the node's figure: node {node_supply}"); + assert_eq!( + *v, + json!(units(theirs)), + "supply.{k} is the node's figure: node {node_supply}" + ); } } - Value::Null => assert!(node_supply.get(k).is_none_or(Value::is_null), "supply.{k} dropped: node {node_supply}"), + Value::Null => assert!( + node_supply.get(k).is_none_or(Value::is_null), + "supply.{k} dropped: node {node_supply}" + ), _ => {} } } diff --git a/crates/randscan-core/src/amount.rs b/crates/randscan-core/src/amount.rs index d3bf0ef..4e94623 100644 --- a/crates/randscan-core/src/amount.rs +++ b/crates/randscan-core/src/amount.rs @@ -29,7 +29,9 @@ pub fn amount<'de, D: Deserializer<'de>>(d: D) -> Result { pub fn amount_opt<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { match Option::::deserialize(d)? { None | Some(Value::Null) => Ok(None), - Some(v) => from_value(&v).map(Some).ok_or_else(|| D::Error::custom(format!("not an amount: {v}"))), + Some(v) => from_value(&v) + .map(Some) + .ok_or_else(|| D::Error::custom(format!("not an amount: {v}"))), } } @@ -39,8 +41,15 @@ pub fn amount_opt<'de, D: Deserializer<'de>>(d: D) -> Result, D:: pub fn int_opt<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { match Option::::deserialize(d)? { None | Some(Value::Null) => Ok(None), - Some(Value::Number(n)) => n.as_i64().map(Some).ok_or_else(|| D::Error::custom(format!("integer out of range: {n}"))), - Some(Value::String(s)) => s.trim().parse::().map(Some).map_err(|_| D::Error::custom(format!("not an integer: {s:?}"))), + Some(Value::Number(n)) => n + .as_i64() + .map(Some) + .ok_or_else(|| D::Error::custom(format!("integer out of range: {n}"))), + Some(Value::String(s)) => s + .trim() + .parse::() + .map(Some) + .map_err(|_| D::Error::custom(format!("not an integer: {s:?}"))), Some(other) => Err(D::Error::custom(format!("not an integer: {other}"))), } } @@ -55,7 +64,9 @@ fn from_value(v: &Value) -> Option { // that fits u64/i64 — the same assumption the rest of this API already makes for every // other amount field (e.g. the indexer's own `Units` wrapper does the same). Value::Number(n) if n.is_u64() || n.is_i64() => Some(n.to_string()), - Value::String(s) if !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) => Some(s.clone()), + Value::String(s) if !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) => { + Some(s.clone()) + } _ => None, } } @@ -85,10 +96,22 @@ mod tests { #[test] fn amount_accepts_a_number_or_a_string() { - assert_eq!(serde_json::from_value::(serde_json::json!({ "a": 600 })).unwrap().a, "600"); - assert_eq!(serde_json::from_value::(serde_json::json!({ "a": "600" })).unwrap().a, "600"); assert_eq!( - serde_json::from_value::(serde_json::json!({ "a": 100_000u64 * 100_000_000 })).unwrap().a, + serde_json::from_value::(serde_json::json!({ "a": 600 })) + .unwrap() + .a, + "600" + ); + assert_eq!( + serde_json::from_value::(serde_json::json!({ "a": "600" })) + .unwrap() + .a, + "600" + ); + assert_eq!( + serde_json::from_value::(serde_json::json!({ "a": 100_000u64 * 100_000_000 })) + .unwrap() + .a, "10000000000000" ); assert!(serde_json::from_value::(serde_json::json!({ "a": "not-a-number" })).is_err()); @@ -98,17 +121,52 @@ mod tests { #[test] fn amount_opt_defaults_on_absence_or_null() { - assert_eq!(serde_json::from_value::(serde_json::json!({})).unwrap().a, None); - assert_eq!(serde_json::from_value::(serde_json::json!({ "a": null })).unwrap().a, None); - assert_eq!(serde_json::from_value::(serde_json::json!({ "a": 600 })).unwrap().a, Some("600".into())); - assert_eq!(serde_json::from_value::(serde_json::json!({ "a": "600" })).unwrap().a, Some("600".into())); + assert_eq!( + serde_json::from_value::(serde_json::json!({})) + .unwrap() + .a, + None + ); + assert_eq!( + serde_json::from_value::(serde_json::json!({ "a": null })) + .unwrap() + .a, + None + ); + assert_eq!( + serde_json::from_value::(serde_json::json!({ "a": 600 })) + .unwrap() + .a, + Some("600".into()) + ); + assert_eq!( + serde_json::from_value::(serde_json::json!({ "a": "600" })) + .unwrap() + .a, + Some("600".into()) + ); } #[test] fn int_opt_accepts_a_number_or_a_numeral_string() { - assert_eq!(serde_json::from_value::(serde_json::json!({})).unwrap().a, None); - assert_eq!(serde_json::from_value::(serde_json::json!({ "a": 1_000_000_000u64 })).unwrap().a, Some(1_000_000_000)); - assert_eq!(serde_json::from_value::(serde_json::json!({ "a": "1000000000" })).unwrap().a, Some(1_000_000_000)); + assert_eq!( + serde_json::from_value::(serde_json::json!({})) + .unwrap() + .a, + None + ); + assert_eq!( + serde_json::from_value::(serde_json::json!({ "a": 1_000_000_000u64 })) + .unwrap() + .a, + Some(1_000_000_000) + ); + assert_eq!( + serde_json::from_value::(serde_json::json!({ "a": "1000000000" })) + .unwrap() + .a, + Some(1_000_000_000) + ); assert!(serde_json::from_value::(serde_json::json!({ "a": "not-a-number" })).is_err()); } } diff --git a/crates/randscan-core/src/notecommit.rs b/crates/randscan-core/src/notecommit.rs index b5d2eae..3920e64 100644 --- a/crates/randscan-core/src/notecommit.rs +++ b/crates/randscan-core/src/notecommit.rs @@ -140,7 +140,13 @@ pub fn pk_from_address(address: &str) -> Option { /// indexer) rebuild the leaf the chain appended without opening any envelope. `None` when /// `recipient` does not parse as a shielded address. #[allow(clippy::too_many_arguments)] -pub fn mint_commitment(recipient: &str, amount: u64, index: u32, time: u32, r: &Word8) -> Option { +pub fn mint_commitment( + recipient: &str, + amount: u64, + index: u32, + time: u32, + r: &Word8, +) -> Option { let pk = pk_from_address(recipient)?; Some( Note { @@ -156,7 +162,13 @@ pub fn mint_commitment(recipient: &str, amount: u64, index: u32, time: u32, r: & } /// Convenience: the hex commitment, or `None` on a bad address. -pub fn mint_commitment_hex(recipient: &str, amount: u64, index: u32, time: u32, r_hex: &str) -> Option { +pub fn mint_commitment_hex( + recipient: &str, + amount: u64, + index: u32, + time: u32, + r_hex: &str, +) -> Option { let r = word8_from_hex(r_hex)?; mint_commitment(recipient, amount, index, time, &r).map(|cm| word8_to_hex(&cm)) } @@ -172,12 +184,17 @@ mod tests { #[test] fn note_commitment_matches_the_shared_test_vector() { let note = Note { - pk: word8_from_hex("53f57f874e8fee7ad3a5e5be820a12ab7836d5b642631c974ed0b5057f9c60aa").unwrap(), - from: word8_from_hex("c37ed965c936aa116c7184b4ae1d285c0a1882813e375a00456ea35df1feb955").unwrap(), + pk: word8_from_hex("53f57f874e8fee7ad3a5e5be820a12ab7836d5b642631c974ed0b5057f9c60aa") + .unwrap(), + from: word8_from_hex( + "c37ed965c936aa116c7184b4ae1d285c0a1882813e375a00456ea35df1feb955", + ) + .unwrap(), amount: 1_500_000_000, asset: 0, time: 66, - r: word8_from_hex("aa000000bb000000cc000000dd000000ee000000ff0000001100000022000000").unwrap(), + r: word8_from_hex("aa000000bb000000cc000000dd000000ee000000ff0000001100000022000000") + .unwrap(), }; assert_eq!( word8_to_hex(¬e.commitment()), @@ -190,8 +207,15 @@ mod tests { #[test] fn notecommit_draws_no_constants_at_runtime() { let src = include_str!("notecommit.rs"); - for pattern in [concat!("new_from", "_rng"), concat!("Std", "Rng"), concat!("seed_from", "_u64")] { - assert!(!src.contains(pattern), "notecommit.rs still derives constants: {pattern}"); + for pattern in [ + concat!("new_from", "_rng"), + concat!("Std", "Rng"), + concat!("seed_from", "_u64"), + ] { + assert!( + !src.contains(pattern), + "notecommit.rs still derives constants: {pattern}" + ); } } @@ -204,19 +228,50 @@ mod tests { let vectors: [([u64; 8], [u64; 8]); 3] = [ ( [0; 8], - [0x1e4b2c9eebb442b0, 0x2fbb0154ab9d22da, 0x9c397e8d1b856b3d, 0x3900699f4fe93a6e, 0xcb37891674d3ad6b, 0x9b530f7ac1ef1f56, 0x0510bc15edfecf33, 0xf9caffe23a93cd28], + [ + 0x1e4b2c9eebb442b0, + 0x2fbb0154ab9d22da, + 0x9c397e8d1b856b3d, + 0x3900699f4fe93a6e, + 0xcb37891674d3ad6b, + 0x9b530f7ac1ef1f56, + 0x0510bc15edfecf33, + 0xf9caffe23a93cd28, + ], ), ( [0, 1, 2, 3, 4, 5, 6, 7], - [0x682c703ce406cd60, 0x35fe4cacd5147b44, 0xf0b819068ae2838e, 0xde5f0a9ba791a8f4, 0xcf8ee9826729b322, 0x38e89ce1e7fcb535, 0xf58f43c0801e00db, 0x694ec48edbb331fa], + [ + 0x682c703ce406cd60, + 0x35fe4cacd5147b44, + 0xf0b819068ae2838e, + 0xde5f0a9ba791a8f4, + 0xcf8ee9826729b322, + 0x38e89ce1e7fcb535, + 0xf58f43c0801e00db, + 0x694ec48edbb331fa, + ], ), ( [0xffff_ffff_0000_0000; 8], - [0xdfebf956a8205183, 0xbacca056a5ba1b75, 0xdc24d665e3f9864b, 0x1ad86aab4b3e131a, 0xc68f628d6f8833e6, 0x0b913e0aa0757959, 0x248d88435c62b658, 0x7bc6fc5500a81dbc], + [ + 0xdfebf956a8205183, + 0xbacca056a5ba1b75, + 0xdc24d665e3f9864b, + 0x1ad86aab4b3e131a, + 0xc68f628d6f8833e6, + 0x0b913e0aa0757959, + 0x248d88435c62b658, + 0x7bc6fc5500a81dbc, + ], ), ]; for (input, want) in vectors { - assert_eq!(perm().permute(Val::new_array(input)), Val::new_array(want), "permute({input:x?})"); + assert_eq!( + perm().permute(Val::new_array(input)), + Val::new_array(want), + "permute({input:x?})" + ); } } @@ -261,18 +316,30 @@ mod tests { fn the_constants_are_the_circuits_table() { use crate::poseidon2_constants::{INITIAL, INTERNAL, TERMINAL}; use sha2::{Digest, Sha256}; - let values: Vec = INITIAL.iter().flatten().chain(INTERNAL.iter()).chain(TERMINAL.iter().flatten()).copied().collect(); + let values: Vec = INITIAL + .iter() + .flatten() + .chain(INTERNAL.iter()) + .chain(TERMINAL.iter().flatten()) + .copied() + .collect(); assert_eq!(values.len(), 86); let mut h = Sha256::new(); for v in values { h.update(v.to_le_bytes()); } - assert_eq!(hex::encode(h.finalize()), "e551d3944d88218c8c4cacb6cb6d8ae4bc2034adc155a6139b0ac77fcc5c24ac"); + assert_eq!( + hex::encode(h.finalize()), + "e551d3944d88218c8c4cacb6cb6d8ae4bc2034adc155a6139b0ac77fcc5c24ac" + ); } #[test] fn mint_from_is_the_ascii_tag_rpl_dash_mint() { - assert_eq!(word8_to_hex(&MINT_FROM), hex::encode(b"rpl-mint\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0")); + assert_eq!( + word8_to_hex(&MINT_FROM), + hex::encode(b"rpl-mint\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0") + ); assert_ne!(MINT_FROM, [0u32; 8]); } diff --git a/crates/randscan-core/src/poseidon2_constants.rs b/crates/randscan-core/src/poseidon2_constants.rs index f9212ae..0262034 100644 --- a/crates/randscan-core/src/poseidon2_constants.rs +++ b/crates/randscan-core/src/poseidon2_constants.rs @@ -50,33 +50,124 @@ pub const PERM_SEED: u64 = 0x5261_6e64_5a4b; /// The 4 initial full rounds' constants (the first half of `ExternalLayerConstants`). pub const INITIAL: [[u64; 8]; 4] = [ - [0xee75a7f2107126c1, 0xd57d10c167af2323, 0x4229e088ee92d68a, 0xe1614b8522f9edfd, 0xaab92b6d01582b3b, 0x97d27cc9bab4251c, 0x98f51ee076c58f7b, 0x1318292a9d21dc29], - [0x23ef19745f84736c, 0x433b3bb1130d4f75, 0x3391c4f3e01e4a98, 0x44eb89e1e2bcdf1c, 0x4c4eda2a35accf9d, 0x28e11d56b36e45e5, 0x4da623de96c338b1, 0xba4dbc2bce4c8a0f], - [0xbc85eed58a9b7745, 0x23e79833d8e14a2d, 0xab21189e14e49139, 0x56f9b94ec3779c7d, 0x299e9a969c442ec7, 0x554b2db9a60a9ed6, 0xd4749224c7f4b4c1, 0x148bbafd9a5eda40], - [0x0df862d37fcb4bdb, 0xcdbea10868b506bc, 0x467a7199b3f2b56f, 0x3abd55c99c4e8fd6, 0xcc144ce8d7401d8a, 0x08bd5f9b9b27e2a6, 0x567998b7de34f2bf, 0x6a3b9291a5ce797b], + [ + 0xee75a7f2107126c1, + 0xd57d10c167af2323, + 0x4229e088ee92d68a, + 0xe1614b8522f9edfd, + 0xaab92b6d01582b3b, + 0x97d27cc9bab4251c, + 0x98f51ee076c58f7b, + 0x1318292a9d21dc29, + ], + [ + 0x23ef19745f84736c, + 0x433b3bb1130d4f75, + 0x3391c4f3e01e4a98, + 0x44eb89e1e2bcdf1c, + 0x4c4eda2a35accf9d, + 0x28e11d56b36e45e5, + 0x4da623de96c338b1, + 0xba4dbc2bce4c8a0f, + ], + [ + 0xbc85eed58a9b7745, + 0x23e79833d8e14a2d, + 0xab21189e14e49139, + 0x56f9b94ec3779c7d, + 0x299e9a969c442ec7, + 0x554b2db9a60a9ed6, + 0xd4749224c7f4b4c1, + 0x148bbafd9a5eda40, + ], + [ + 0x0df862d37fcb4bdb, + 0xcdbea10868b506bc, + 0x467a7199b3f2b56f, + 0x3abd55c99c4e8fd6, + 0xcc144ce8d7401d8a, + 0x08bd5f9b9b27e2a6, + 0x567998b7de34f2bf, + 0x6a3b9291a5ce797b, + ], ]; /// The 22 partial rounds' constants, one scalar each (added to lane 0 only). pub const INTERNAL: [u64; 22] = [ - 0xb4a835a8cf918c3f, 0xfc8e76ddb453dce0, 0x8c482d91150cbbb3, 0x1647c2a95052a02e, - 0x229410bcacea59aa, 0x95b3a2391389bc0d, 0xd8e94d4c2993e464, 0x7f38bf21c1521128, - 0x1f4d8ea8df153211, 0x6c3108043a789868, 0xb43065f502f235c3, 0xa350c3ac510528cd, - 0x7a9e0efb9a8dec74, 0x49efc1d4bcad501a, 0xaadce63e6421ce48, 0xa7cf9e4ee247eab6, - 0xc13bd889a7b0bb77, 0x3214d332ecc1f5a6, 0x44e9d60696221771, 0xcdc1d0e4a7c2d60c, - 0x23e11c18f32582c0, 0x91f0f07e761248ad, + 0xb4a835a8cf918c3f, + 0xfc8e76ddb453dce0, + 0x8c482d91150cbbb3, + 0x1647c2a95052a02e, + 0x229410bcacea59aa, + 0x95b3a2391389bc0d, + 0xd8e94d4c2993e464, + 0x7f38bf21c1521128, + 0x1f4d8ea8df153211, + 0x6c3108043a789868, + 0xb43065f502f235c3, + 0xa350c3ac510528cd, + 0x7a9e0efb9a8dec74, + 0x49efc1d4bcad501a, + 0xaadce63e6421ce48, + 0xa7cf9e4ee247eab6, + 0xc13bd889a7b0bb77, + 0x3214d332ecc1f5a6, + 0x44e9d60696221771, + 0xcdc1d0e4a7c2d60c, + 0x23e11c18f32582c0, + 0x91f0f07e761248ad, ]; /// The 4 terminal full rounds' constants (the second half of `ExternalLayerConstants`). pub const TERMINAL: [[u64; 8]; 4] = [ - [0x8d8b439f471d3ed8, 0x6f306da8e7460bd1, 0x232495c2971b558b, 0xf5fac7d16c4bd3e4, 0x2afafe770334cb30, 0xeb207e7e4c8d9fc6, 0x8a96037107bc1178, 0xf1f324d786de2550], - [0x19187837e670ae50, 0x1960443a687a8d8c, 0xf63d378c5cc93b62, 0x687ae437f5a304ad, 0x69bff4277cf0e371, 0x93fa2bddac4ec4db, 0x0b15e520e37982a6, 0xb3acaa5e2aa2755e], - [0xdd6f2419a8e88629, 0x7aca3b451c80cfc6, 0x4ef863d6f17d4ea1, 0x7f9fe7304c74e395, 0x1b7a60765cc0afc8, 0x894938b3e73d019c, 0x614d2e03db7f63a7, 0xd43e0575caaa4b7c], - [0x2f9df0ebffe678a3, 0x641d03c34d5055d9, 0x87b22e63efbc1bca, 0x1b495476ff8afc22, 0x538eac6fda3a7632, 0xe18814f30a053036, 0x544bd2c06d7d6a60, 0x30ca8f9f4bdaebde], + [ + 0x8d8b439f471d3ed8, + 0x6f306da8e7460bd1, + 0x232495c2971b558b, + 0xf5fac7d16c4bd3e4, + 0x2afafe770334cb30, + 0xeb207e7e4c8d9fc6, + 0x8a96037107bc1178, + 0xf1f324d786de2550, + ], + [ + 0x19187837e670ae50, + 0x1960443a687a8d8c, + 0xf63d378c5cc93b62, + 0x687ae437f5a304ad, + 0x69bff4277cf0e371, + 0x93fa2bddac4ec4db, + 0x0b15e520e37982a6, + 0xb3acaa5e2aa2755e, + ], + [ + 0xdd6f2419a8e88629, + 0x7aca3b451c80cfc6, + 0x4ef863d6f17d4ea1, + 0x7f9fe7304c74e395, + 0x1b7a60765cc0afc8, + 0x894938b3e73d019c, + 0x614d2e03db7f63a7, + 0xd43e0575caaa4b7c, + ], + [ + 0x2f9df0ebffe678a3, + 0x641d03c34d5055d9, + 0x87b22e63efbc1bca, + 0x1b495476ff8afc22, + 0x538eac6fda3a7632, + 0xe18814f30a053036, + 0x544bd2c06d7d6a60, + 0x30ca8f9f4bdaebde, + ], ]; /// [`INITIAL`] and [`TERMINAL`] as the `ExternalLayerConstants` `p3_poseidon2` builds from. pub fn external() -> ExternalLayerConstants { - ExternalLayerConstants::new(INITIAL.map(Goldilocks::new_array).to_vec(), TERMINAL.map(Goldilocks::new_array).to_vec()) + ExternalLayerConstants::new( + INITIAL.map(Goldilocks::new_array).to_vec(), + TERMINAL.map(Goldilocks::new_array).to_vec(), + ) } /// [`INTERNAL`] as field elements. @@ -115,11 +206,20 @@ mod tests { let mut rng = StdRng::seed_from_u64(PERM_SEED); let ext = ExternalLayerConstants::::new_from_rng(8, &mut rng); let internal: Vec = (&mut rng).sample_iter(StandardUniform).take(22).collect(); - assert_eq!(ext.get_initial_constants(), &INITIAL.map(Goldilocks::new_array)[..], "initial rows"); - assert_eq!(ext.get_terminal_constants(), &TERMINAL.map(Goldilocks::new_array)[..], "terminal rows"); + assert_eq!( + ext.get_initial_constants(), + &INITIAL.map(Goldilocks::new_array)[..], + "initial rows" + ); + assert_eq!( + ext.get_terminal_constants(), + &TERMINAL.map(Goldilocks::new_array)[..], + "terminal rows" + ); assert_eq!(internal, super::internal().to_vec(), "internal scalars"); // And the whole permutation, as `p3` would have built it from the seed. - let seeded = Poseidon2Goldilocks::<8>::new_from_rng_128(&mut StdRng::seed_from_u64(PERM_SEED)); + let seeded = + Poseidon2Goldilocks::<8>::new_from_rng_128(&mut StdRng::seed_from_u64(PERM_SEED)); let table = permutation(); let mut s = StdRng::seed_from_u64(0x2b5f); for _ in 0..64 { @@ -135,20 +235,51 @@ mod tests { let vectors: [([u64; 8], [u64; 8]); 3] = [ ( [0; 8], - [0x1e4b2c9eebb442b0, 0x2fbb0154ab9d22da, 0x9c397e8d1b856b3d, 0x3900699f4fe93a6e, 0xcb37891674d3ad6b, 0x9b530f7ac1ef1f56, 0x0510bc15edfecf33, 0xf9caffe23a93cd28], + [ + 0x1e4b2c9eebb442b0, + 0x2fbb0154ab9d22da, + 0x9c397e8d1b856b3d, + 0x3900699f4fe93a6e, + 0xcb37891674d3ad6b, + 0x9b530f7ac1ef1f56, + 0x0510bc15edfecf33, + 0xf9caffe23a93cd28, + ], ), ( [0, 1, 2, 3, 4, 5, 6, 7], - [0x682c703ce406cd60, 0x35fe4cacd5147b44, 0xf0b819068ae2838e, 0xde5f0a9ba791a8f4, 0xcf8ee9826729b322, 0x38e89ce1e7fcb535, 0xf58f43c0801e00db, 0x694ec48edbb331fa], + [ + 0x682c703ce406cd60, + 0x35fe4cacd5147b44, + 0xf0b819068ae2838e, + 0xde5f0a9ba791a8f4, + 0xcf8ee9826729b322, + 0x38e89ce1e7fcb535, + 0xf58f43c0801e00db, + 0x694ec48edbb331fa, + ], ), ( [0xffff_ffff_0000_0000; 8], - [0xdfebf956a8205183, 0xbacca056a5ba1b75, 0xdc24d665e3f9864b, 0x1ad86aab4b3e131a, 0xc68f628d6f8833e6, 0x0b913e0aa0757959, 0x248d88435c62b658, 0x7bc6fc5500a81dbc], + [ + 0xdfebf956a8205183, + 0xbacca056a5ba1b75, + 0xdc24d665e3f9864b, + 0x1ad86aab4b3e131a, + 0xc68f628d6f8833e6, + 0x0b913e0aa0757959, + 0x248d88435c62b658, + 0x7bc6fc5500a81dbc, + ], ), ]; let perm = permutation(); for (input, want) in vectors { - assert_eq!(perm.permute(Goldilocks::new_array(input)), Goldilocks::new_array(want), "permute({input:x?})"); + assert_eq!( + perm.permute(Goldilocks::new_array(input)), + Goldilocks::new_array(want), + "permute({input:x?})" + ); } } } diff --git a/crates/randscan-core/src/types/bridge.rs b/crates/randscan-core/src/types/bridge.rs index 3cb5605..06b0184 100644 --- a/crates/randscan-core/src/types/bridge.rs +++ b/crates/randscan-core/src/types/bridge.rs @@ -200,7 +200,10 @@ pub fn bridge_chain_name(chain: i64) -> Option<&'static str> { /// are not zero. pub fn emitter_address(chain: i64, emitter: &str) -> Option { use sha2::{Digest, Sha256}; - let word: [u8; 32] = hex::decode(emitter.trim_start_matches("0x")).ok()?.try_into().ok()?; + let word: [u8; 32] = hex::decode(emitter.trim_start_matches("0x")) + .ok()? + .try_into() + .ok()?; let padded = || word[..12].iter().all(|&b| b == 0).then(|| &word[12..]); match chain { 2 | 3 => Some(format!("0x{}", hex::encode(padded()?))), @@ -241,9 +244,14 @@ impl BridgeState { chain, chain_name: bridge_chain_name(chain).map(str::to_string), emitter: emitter.clone(), - explorer_url: address.as_deref().and_then(|a| emitter_explorer_url(chain, a)), + explorer_url: address + .as_deref() + .and_then(|a| emitter_explorer_url(chain, a)), address, - min_inbound_sequence: self.min_inbound_sequence.as_ref().and_then(|f| f.get(id).copied()), + min_inbound_sequence: self + .min_inbound_sequence + .as_ref() + .and_then(|f| f.get(id).copied()), }) }) .collect(); @@ -333,7 +341,8 @@ mod tests { "decimals": 8, "locked": 600, "mint_cap_per_day": 100_000u64 * 100_000_000, "minted_today": 1_000, "mint_day": 0, }); - let asset: BridgeAsset = serde_json::from_value(v).expect("the node's own numeric encoding must parse"); + let asset: BridgeAsset = + serde_json::from_value(v).expect("the node's own numeric encoding must parse"); assert_eq!(asset.locked.as_deref(), Some("600")); assert_eq!(asset.minted_today.as_deref(), Some("1000")); assert_eq!(asset.mint_cap_per_day.as_deref(), Some("10000000000000")); @@ -372,12 +381,20 @@ mod tests { "mint_cap_per_day": 100_000u64 * 100_000_000, "minted_today": 1_000, "mint_day": 0, }], }); - let state: BridgeState = serde_json::from_value(v).expect("must parse without next_index and with numeric amounts"); + let state: BridgeState = serde_json::from_value(v) + .expect("must parse without next_index and with numeric amounts"); assert_eq!(state.registration_fee, Some(1_000_000_000)); assert_eq!(state.next_index, None); assert_eq!(state.assets[0].locked.as_deref(), Some("600")); // A reply this old has no rules v2, no replay floor, and nothing of the explorer's own. - assert_eq!((state.rotation_nonce, state.rules_v2, state.min_inbound_sequence), (None, None, None)); + assert_eq!( + ( + state.rotation_nonce, + state.rules_v2, + state.min_inbound_sequence + ), + (None, None, None) + ); assert!(state.endpoints.is_empty()); } @@ -412,19 +429,44 @@ mod tests { assert_eq!(state.burn_sequence, Some(8)); assert_eq!(state.rotation_nonce, Some(0)); let rules = state.rules_v2.as_ref().unwrap(); - assert_eq!((rules.global_mint_cap_per_window.as_str(), rules.cap_window_secs), ("400000000000", 86400)); + assert_eq!( + ( + rules.global_mint_cap_per_window.as_str(), + rules.cap_window_secs + ), + ("400000000000", 86400) + ); let floor = state.min_inbound_sequence.as_ref().unwrap(); - assert_eq!(floor.iter().map(|(c, s)| (c.as_str(), *s)).collect::>(), [("2", 1), ("3", 1), ("4", 1), ("5", 4)]); + assert_eq!( + floor + .iter() + .map(|(c, s)| (c.as_str(), *s)) + .collect::>(), + [("2", 1), ("3", 1), ("4", 1), ("5", 4)] + ); // A chain without either group says `null`, and a number where the node sends a string. let bare: BridgeState = serde_json::from_value(serde_json::json!({ "enabled": true, "rotation_nonce": 0, "rules_v2": null, "min_inbound_sequence": null, })) .unwrap(); - assert_eq!((bare.rotation_nonce, bare.rules_v2, bare.min_inbound_sequence), (Some(0), None, None)); + assert_eq!( + ( + bare.rotation_nonce, + bare.rules_v2, + bare.min_inbound_sequence + ), + (Some(0), None, None) + ); let numeric: BridgeRulesV2 = serde_json::from_value(serde_json::json!({ "global_mint_cap_per_window": 50_000_000_000_000u64, "cap_window_secs": 3600 })).unwrap(); assert_eq!(numeric.global_mint_cap_per_window, "50000000000000"); - assert_eq!((numeric.global_minted_in_window, numeric.global_mint_headroom), (None, None)); + assert_eq!( + ( + numeric.global_minted_in_window, + numeric.global_mint_headroom + ), + (None, None) + ); } /// The same reply from a node after v0.6.7 (audit v6, BRG-19): rules v2 says what the whole @@ -444,9 +486,22 @@ mod tests { })) .unwrap(); let rules = state.rules_v2.unwrap(); - assert_eq!((rules.global_minted_in_window.as_deref(), rules.global_mint_headroom.as_deref()), (Some("1000"), Some("999000"))); + assert_eq!( + ( + rules.global_minted_in_window.as_deref(), + rules.global_mint_headroom.as_deref() + ), + (Some("1000"), Some("999000")) + ); let a = &state.assets[0]; - assert_eq!((a.minted_in_window.as_deref(), a.mint_window_secs, a.mint_headroom.as_deref()), (Some("1000"), Some(86_400), Some("999000"))); + assert_eq!( + ( + a.minted_in_window.as_deref(), + a.mint_window_secs, + a.mint_headroom.as_deref() + ), + (Some("1000"), Some(86_400), Some("999000")) + ); } /// Every address is the bridge repository's own record of the deployment @@ -456,23 +511,50 @@ mod tests { fn an_emitter_reads_as_its_chain_prints_it() { let pad = |a: &str| format!("{}{a}", "0".repeat(24)); // chain 19 - assert_eq!(emitter_address(2, &pad("7af6b17047c1db6cb54347fdea45cf9179075bfa")).unwrap(), "0x7af6b17047c1db6cb54347fdea45cf9179075bfa"); - assert_eq!(emitter_address(3, &pad("7af6b17047c1db6cb54347fdea45cf9179075bfa")).unwrap(), "0x7af6b17047c1db6cb54347fdea45cf9179075bfa"); - assert_eq!(emitter_address(4, &pad("6410797df959987a5baf65b5fab97edeb34d5163")).unwrap(), "TK6JJv55CCkFjNHq7WwoU91GKaZEiC93me"); + assert_eq!( + emitter_address(2, &pad("7af6b17047c1db6cb54347fdea45cf9179075bfa")).unwrap(), + "0x7af6b17047c1db6cb54347fdea45cf9179075bfa" + ); + assert_eq!( + emitter_address(3, &pad("7af6b17047c1db6cb54347fdea45cf9179075bfa")).unwrap(), + "0x7af6b17047c1db6cb54347fdea45cf9179075bfa" + ); + assert_eq!( + emitter_address(4, &pad("6410797df959987a5baf65b5fab97edeb34d5163")).unwrap(), + "TK6JJv55CCkFjNHq7WwoU91GKaZEiC93me" + ); // chains 14–18 - assert_eq!(emitter_address(2, &pad("d6ebd21c3df90c9175ebdc8d6b377a9361604892")).unwrap(), "0xd6ebd21c3df90c9175ebdc8d6b377a9361604892"); - assert_eq!(emitter_address(4, &pad("0992df85dcce77ded2c0387f1fa9cf98ac859700")).unwrap(), "TAqq2i8KfYpACPUc9f5e2gAjdSgXqmPpkU"); + assert_eq!( + emitter_address(2, &pad("d6ebd21c3df90c9175ebdc8d6b377a9361604892")).unwrap(), + "0xd6ebd21c3df90c9175ebdc8d6b377a9361604892" + ); + assert_eq!( + emitter_address(4, &pad("0992df85dcce77ded2c0387f1fa9cf98ac859700")).unwrap(), + "TAqq2i8KfYpACPUc9f5e2gAjdSgXqmPpkU" + ); // Solana, not redeployed: the program id is the word itself let solana = "d3e58f1e9317bbc3c69b63fadff558ea82ba5d00765f1f1e483d705d209b413a"; - assert_eq!(emitter_address(5, solana).unwrap(), "FGA3kY3RjfDKjUszJESMYtYXAbsnkFhhoxM3Mb34vycu"); - assert_eq!(emitter_address(5, &format!("0x{}", solana.to_uppercase())).unwrap(), "FGA3kY3RjfDKjUszJESMYtYXAbsnkFhhoxM3Mb34vycu"); + assert_eq!( + emitter_address(5, solana).unwrap(), + "FGA3kY3RjfDKjUszJESMYtYXAbsnkFhhoxM3Mb34vycu" + ); + assert_eq!( + emitter_address(5, &format!("0x{}", solana.to_uppercase())).unwrap(), + "FGA3kY3RjfDKjUszJESMYtYXAbsnkFhhoxM3Mb34vycu" + ); // Not an address of that chain: a 20-byte chain's word with padding that is not zero, a // word of the wrong length, something that is not hex, a chain the explorer cannot name. assert_eq!(emitter_address(2, solana), None); assert_eq!(emitter_address(4, solana), None); - assert_eq!(emitter_address(2, "7af6b17047c1db6cb54347fdea45cf9179075bfa"), None); + assert_eq!( + emitter_address(2, "7af6b17047c1db6cb54347fdea45cf9179075bfa"), + None + ); assert_eq!(emitter_address(5, "not hex"), None); - assert_eq!(emitter_address(9, &pad("7af6b17047c1db6cb54347fdea45cf9179075bfa")), None); + assert_eq!( + emitter_address(9, &pad("7af6b17047c1db6cb54347fdea45cf9179075bfa")), + None + ); } #[test] @@ -482,21 +564,56 @@ mod tests { state.emitters.insert("10".into(), "ee".repeat(32)); state.emitters.insert("not a chain".into(), "ee".repeat(32)); let endpoints = state.derive_endpoints(); - let row = |e: &BridgeEndpoint| (e.chain, e.chain_name.clone(), e.address.clone(), e.explorer_url.clone(), e.min_inbound_sequence); + let row = |e: &BridgeEndpoint| { + ( + e.chain, + e.chain_name.clone(), + e.address.clone(), + e.explorer_url.clone(), + e.min_inbound_sequence, + ) + }; let some = |s: &str| Some(s.to_string()); assert_eq!( endpoints.iter().map(row).collect::>(), [ - (2, some("Ethereum"), some("0x7af6b17047c1db6cb54347fdea45cf9179075bfa"), some("https://etherscan.io/address/0x7af6b17047c1db6cb54347fdea45cf9179075bfa"), Some(1)), - (3, some("BSC"), some("0x7af6b17047c1db6cb54347fdea45cf9179075bfa"), some("https://bscscan.com/address/0x7af6b17047c1db6cb54347fdea45cf9179075bfa"), Some(1)), - (4, some("Tron"), some("TK6JJv55CCkFjNHq7WwoU91GKaZEiC93me"), some("https://tronscan.org/#/contract/TK6JJv55CCkFjNHq7WwoU91GKaZEiC93me"), Some(1)), - (5, some("Solana"), some("FGA3kY3RjfDKjUszJESMYtYXAbsnkFhhoxM3Mb34vycu"), some("https://solscan.io/account/FGA3kY3RjfDKjUszJESMYtYXAbsnkFhhoxM3Mb34vycu"), Some(4)), + ( + 2, + some("Ethereum"), + some("0x7af6b17047c1db6cb54347fdea45cf9179075bfa"), + some("https://etherscan.io/address/0x7af6b17047c1db6cb54347fdea45cf9179075bfa"), + Some(1) + ), + ( + 3, + some("BSC"), + some("0x7af6b17047c1db6cb54347fdea45cf9179075bfa"), + some("https://bscscan.com/address/0x7af6b17047c1db6cb54347fdea45cf9179075bfa"), + Some(1) + ), + ( + 4, + some("Tron"), + some("TK6JJv55CCkFjNHq7WwoU91GKaZEiC93me"), + some("https://tronscan.org/#/contract/TK6JJv55CCkFjNHq7WwoU91GKaZEiC93me"), + Some(1) + ), + ( + 5, + some("Solana"), + some("FGA3kY3RjfDKjUszJESMYtYXAbsnkFhhoxM3Mb34vycu"), + some("https://solscan.io/account/FGA3kY3RjfDKjUszJESMYtYXAbsnkFhhoxM3Mb34vycu"), + Some(4) + ), (10, None, None, None, None), ] ); assert_eq!(endpoints[0].emitter, state.emitters["2"]); // No floor in the genesis: every endpoint is there, none has one. state.min_inbound_sequence = None; - assert!(state.derive_endpoints().iter().all(|e| e.min_inbound_sequence.is_none())); + assert!(state + .derive_endpoints() + .iter() + .all(|e| e.min_inbound_sequence.is_none())); } } diff --git a/crates/randscan-core/src/types/bridge_tokens.rs b/crates/randscan-core/src/types/bridge_tokens.rs index 053fc38..f099c4a 100644 --- a/crates/randscan-core/src/types/bridge_tokens.rs +++ b/crates/randscan-core/src/types/bridge_tokens.rs @@ -54,7 +54,19 @@ const fn evm( decimals: u8, explorer_url: &'static str, ) -> ApprovedToken { - ApprovedToken { symbol, name, chain, chain_name, standard, address, token, decimals, status: TokenStatus::Allowed, explorer_url, note: None } + ApprovedToken { + symbol, + name, + chain, + chain_name, + standard, + address, + token, + decimals, + status: TokenStatus::Allowed, + explorer_url, + note: None, + } } /// The allowlist, grouped by home chain in bridge chain id order. @@ -106,7 +118,9 @@ pub const APPROVED_TOKENS: &[ApprovedToken] = &[ /// registry stores it (32 bytes hex), case-insensitively and with an optional `0x`. pub fn approved_token(chain: i64, token: &str) -> Option<&'static ApprovedToken> { let token = token.trim_start_matches("0x").to_ascii_lowercase(); - APPROVED_TOKENS.iter().find(|t| t.chain == chain && t.token == token) + APPROVED_TOKENS + .iter() + .find(|t| t.chain == chain && t.token == token) } #[cfg(test)] @@ -146,10 +160,19 @@ mod tests { fn wire_form_follows_from_the_printed_address() { for t in APPROVED_TOKENS { let expected = match t.chain { - 2 | 3 => format!("{}{}", "0".repeat(24), t.address.trim_start_matches("0x").to_ascii_lowercase()), + 2 | 3 => format!( + "{}{}", + "0".repeat(24), + t.address.trim_start_matches("0x").to_ascii_lowercase() + ), 4 => { let raw = base58(t.address); - assert_eq!(raw.len(), 25, "{}: 21-byte body + 4-byte checksum", t.address); + assert_eq!( + raw.len(), + 25, + "{}: 21-byte body + 4-byte checksum", + t.address + ); assert_eq!(raw[0], 0x41, "{}: Tron mainnet prefix", t.address); format!("{}{}", "0".repeat(24), hex(&raw[1..21])) } @@ -168,7 +191,11 @@ mod tests { /// The Tron README's worked example (tron/README.md §3) is the USDT entry. #[test] fn tron_usdt_matches_the_bridge_readme() { - let t = approved_token(4, "0x000000000000000000000000A614F803B6FD780986A42C78EC9C7F77E6DED13C").unwrap(); + let t = approved_token( + 4, + "0x000000000000000000000000A614F803B6FD780986A42C78EC9C7F77E6DED13C", + ) + .unwrap(); assert_eq!(t.symbol, "USDT"); assert_eq!(t.decimals, 6); } @@ -177,15 +204,38 @@ mod tests { fn every_pair_is_listed_once_and_only_tron_usdc_is_discontinued() { let mut seen = std::collections::HashSet::new(); for t in APPROVED_TOKENS { - assert!(seen.insert((t.chain, t.symbol)), "duplicate {} on {}", t.symbol, t.chain_name); + assert!( + seen.insert((t.chain, t.symbol)), + "duplicate {} on {}", + t.symbol, + t.chain_name + ); let discontinued = t.status == TokenStatus::Discontinued; - assert_eq!(discontinued, t.chain == 4 && t.symbol == "USDC", "{} on {}", t.symbol, t.chain_name); + assert_eq!( + discontinued, + t.chain == 4 && t.symbol == "USDC", + "{} on {}", + t.symbol, + t.chain_name + ); assert_eq!(discontinued, t.note.is_some()); } for chain in 2..=5 { - assert_eq!(APPROVED_TOKENS.iter().filter(|t| t.chain == chain).count(), 2, "USDT and USDC on chain {chain}"); + assert_eq!( + APPROVED_TOKENS.iter().filter(|t| t.chain == chain).count(), + 2, + "USDT and USDC on chain {chain}" + ); } assert!(approved_token(2, "cc".repeat(32).as_str()).is_none()); - assert_eq!(approved_token(5, "ce010e60afedb22717bd63192f54145a3f965a33bb82d2c7029eb2ce1e208264").unwrap().symbol, "USDT"); + assert_eq!( + approved_token( + 5, + "ce010e60afedb22717bd63192f54145a3f965a33bb82d2c7029eb2ce1e208264" + ) + .unwrap() + .symbol, + "USDT" + ); } } diff --git a/crates/randscan-core/src/types/note.rs b/crates/randscan-core/src/types/note.rs index 611651a..aa2c7b1 100644 --- a/crates/randscan-core/src/types/note.rs +++ b/crates/randscan-core/src/types/note.rs @@ -99,7 +99,11 @@ pub fn public_note_for(cm: &str, tx: &LeafTx<'_>) -> Option { _ => return None, }; // A deposit's note carries the net under `bridge.fees`; `amount` stays the gross. - let amount = if tx.kind == "bridge_attest" { tx.deposit_amount.or(tx.amount) } else { tx.amount }; + let amount = if tx.kind == "bridge_attest" { + tx.deposit_amount.or(tx.amount) + } else { + tx.amount + }; return Some(PublicNote { source: source.into(), amount: amount?.to_string(), @@ -110,7 +114,10 @@ pub fn public_note_for(cm: &str, tx: &LeafTx<'_>) -> Option { } if tx.kind == "invoke" { let t = tx.transition?; - let payouts = ["pays", "mints"].into_iter().filter_map(|k| t.get(k)?.as_array()).flatten(); + let payouts = ["pays", "mints"] + .into_iter() + .filter_map(|k| t.get(k)?.as_array()) + .flatten(); for p in payouts { if eq(p.get("cm").and_then(|c| c.as_str())) { return from_json("payout", p); @@ -163,11 +170,28 @@ mod public_note_tests { deposit_r: Some("63a44e149332d851011f20ac2c2622e814297177c8b18f7447fe9a0484edb7a0"), ..Default::default() }; - let d = public_note_for("d8f146fd390c839e5cc084a2c560c42f1901968864b834062e8aa28972ace5e2", &tx).unwrap(); - assert_eq!((d.source.as_str(), d.amount.as_str(), d.asset, d.time), ("bridge_deposit", "99900000", 1, 1599)); - let f = public_note_for("47c3ad478fa92b55348204ac0d9884500466aba53d19875b782ada67d2ca6ee4", &tx).unwrap(); - assert_eq!((f.source.as_str(), f.amount.as_str()), ("bridge_fee", "100000")); - assert!(public_note_for(&"00".repeat(32), &tx).is_none(), "a bundle slot has no public opening"); + let d = public_note_for( + "d8f146fd390c839e5cc084a2c560c42f1901968864b834062e8aa28972ace5e2", + &tx, + ) + .unwrap(); + assert_eq!( + (d.source.as_str(), d.amount.as_str(), d.asset, d.time), + ("bridge_deposit", "99900000", 1, 1599) + ); + let f = public_note_for( + "47c3ad478fa92b55348204ac0d9884500466aba53d19875b782ada67d2ca6ee4", + &tx, + ) + .unwrap(); + assert_eq!( + (f.source.as_str(), f.amount.as_str()), + ("bridge_fee", "100000") + ); + assert!( + public_note_for(&"00".repeat(32), &tx).is_none(), + "a bundle slot has no public opening" + ); } #[test] @@ -175,8 +199,15 @@ mod public_note_tests { let t = serde_json::json!({ "reads": [], "writes": [], "inflow": "none", "pays": [{ "asset": 0, "amount": "300", "recipient": "rand1x", "time": 41, "r": "aa", "cm": "bb" }], "mints": [{ "asset": 2, "amount": "40", "recipient": "rand1x", "time": 41, "r": "cc", "cm": "dd" }] }); - let tx = LeafTx { kind: "invoke", transition: Some(&t), ..Default::default() }; + let tx = LeafTx { + kind: "invoke", + transition: Some(&t), + ..Default::default() + }; let p = public_note_for("dd", &tx).unwrap(); - assert_eq!((p.source.as_str(), p.amount.as_str(), p.asset, p.r.as_str()), ("payout", "40", 2, "cc")); + assert_eq!( + (p.source.as_str(), p.amount.as_str(), p.asset, p.r.as_str()), + ("payout", "40", 2, "cc") + ); } } diff --git a/crates/randscan-core/src/types/prover.rs b/crates/randscan-core/src/types/prover.rs index 71e45f0..05f7fbb 100644 --- a/crates/randscan-core/src/types/prover.rs +++ b/crates/randscan-core/src/types/prover.rs @@ -45,11 +45,26 @@ pub const KNOWN_PROVERS: &[KnownProver] = &[KnownProver { fingerprint: "RGTF-7HKJ-XZFV-GQ1J", pairing_url: Some("https://prover.randprotocol.org/.well-known/rand-prover.json"), members: &[ - KnownProverMember { label: "rand-node-a", ip: "139.59.238.151" }, - KnownProverMember { label: "rand-archive-2", ip: "206.81.29.236" }, - KnownProverMember { label: "rand-guardian-1", ip: "104.248.5.129" }, - KnownProverMember { label: "rand-guardian-2", ip: "137.184.12.108" }, - KnownProverMember { label: "rand-guardian-5", ip: "209.97.179.217" }, + KnownProverMember { + label: "rand-node-a", + ip: "139.59.238.151", + }, + KnownProverMember { + label: "rand-archive-2", + ip: "206.81.29.236", + }, + KnownProverMember { + label: "rand-guardian-1", + ip: "104.248.5.129", + }, + KnownProverMember { + label: "rand-guardian-2", + ip: "137.184.12.108", + }, + KnownProverMember { + label: "rand-guardian-5", + ip: "209.97.179.217", + }, ], }]; @@ -134,7 +149,14 @@ impl ProverView { error: None, checked_at_ms: None, last_up_ms: None, - members: p.members.iter().map(|m| ProverMember { label: m.label.into(), geo: None }).collect(), + members: p + .members + .iter() + .map(|m| ProverMember { + label: m.label.into(), + geo: None, + }) + .collect(), } } } @@ -154,10 +176,18 @@ mod tests { "fee": { "amount": "1000000", "address": "rand1abc" }, "allowed_origins": ["*"], })) .unwrap(); - assert_eq!(r.queue, Some(ProverQueue { depth: 0, max: 5, proving: 1 })); + assert_eq!( + r.queue, + Some(ProverQueue { + depth: 0, + max: 5, + proving: 1 + }) + ); assert_eq!(r.fee.as_ref().map(|f| f.amount.as_str()), Some("1000000")); assert!(serde_json::to_value(&r).unwrap().get("kem_ek").is_none()); - let free: ProverInfoReply = serde_json::from_value(serde_json::json!({ "fee": null })).unwrap(); + let free: ProverInfoReply = + serde_json::from_value(serde_json::json!({ "fee": null })).unwrap(); assert!(free.fee.is_none() && free.queue.is_none()); } @@ -167,11 +197,20 @@ mod tests { assert!(p.url.starts_with("https://"), "{}", p.name); assert!(!p.members.is_empty(), "{}", p.name); for m in p.members { - assert!(m.ip.parse::().is_ok(), "{}: {}", m.label, m.ip); + assert!( + m.ip.parse::().is_ok(), + "{}: {}", + m.label, + m.ip + ); } let v = serde_json::to_value(ProverView::unpolled(p)).unwrap(); for m in p.members { - assert!(!v.to_string().contains(m.ip), "{} must not be served", m.label); + assert!( + !v.to_string().contains(m.ip), + "{} must not be served", + m.label + ); } } } diff --git a/crates/randscan-core/src/types/token.rs b/crates/randscan-core/src/types/token.rs index 57beb9b..64b8927 100644 --- a/crates/randscan-core/src/types/token.rs +++ b/crates/randscan-core/src/types/token.rs @@ -52,9 +52,16 @@ pub struct TokenBacking { pub enum TokenAuthority { /// Fixed supply, or renounced: this token can never be minted again. None, - Key { key: String, address: String }, - Bridge { backings: Vec }, - Program { program: String }, + Key { + key: String, + address: String, + }, + Bridge { + backings: Vec, + }, + Program { + program: String, + }, } impl TokenAuthority { @@ -155,11 +162,21 @@ mod tests { fn authority_tag_matches_the_variant() { assert_eq!(TokenAuthority::None.tag(), "none"); assert_eq!( - TokenAuthority::Key { key: "aa".into(), address: "2nRd".into() }.tag(), + TokenAuthority::Key { + key: "aa".into(), + address: "2nRd".into() + } + .tag(), "key" ); assert_eq!(TokenAuthority::Bridge { backings: vec![] }.tag(), "bridge"); - assert_eq!(TokenAuthority::Program { program: "aa".into() }.tag(), "program"); + assert_eq!( + TokenAuthority::Program { + program: "aa".into() + } + .tag(), + "program" + ); } fn zusd() -> TokenInfo { @@ -192,7 +209,10 @@ mod tests { #[test] fn backings_is_empty_for_a_native_token_and_populated_for_a_bridged_one() { - let native = TokenInfo { authority: TokenAuthority::None, ..zusd() }; + let native = TokenInfo { + authority: TokenAuthority::None, + ..zusd() + }; assert!(native.backings().is_empty()); assert_eq!(zusd().backings().len(), 1); assert_eq!(zusd().backings()[0].minted_today.as_deref(), Some("100")); @@ -216,7 +236,8 @@ mod tests { #[test] fn a_disabled_chain_reports_no_tokens() { - let v: TokenList = serde_json::from_value(serde_json::json!({ "enabled": false, "tokens": [] })).unwrap(); + let v: TokenList = + serde_json::from_value(serde_json::json!({ "enabled": false, "tokens": [] })).unwrap(); assert!(!v.enabled); assert!(v.tokens.is_empty()); assert_eq!(v.registration_fee, None); @@ -233,11 +254,19 @@ mod tests { "chain": 2, "token": "aa".repeat(32), "decimals": 8, "locked": "600", "mint_cap_per_day": 100_000u64 * 100_000_000, "minted_today": "1000", "mint_day": 0, }); - let backing: TokenBacking = serde_json::from_value(v).expect("the node's own mixed encoding must parse"); + let backing: TokenBacking = + serde_json::from_value(v).expect("the node's own mixed encoding must parse"); assert_eq!(backing.locked, "600"); assert_eq!(backing.minted_today.as_deref(), Some("1000")); assert_eq!(backing.mint_cap_per_day.as_deref(), Some("10000000000000")); - assert_eq!((backing.minted_in_window, backing.mint_window_secs, backing.mint_headroom), (None, None, None)); + assert_eq!( + ( + backing.minted_in_window, + backing.mint_window_secs, + backing.mint_headroom + ), + (None, None, None) + ); } /// A backing row from a node after v0.6.7 (audit v6, BRG-19), the literals of the node's own @@ -259,7 +288,10 @@ mod tests { "minted_in_window": null, "mint_window_secs": null, "mint_headroom": (100_000u64 * 100_000_000 - 1_000).to_string(), })) .unwrap(); - assert_eq!((day_counter.minted_in_window, day_counter.mint_window_secs), (None, None)); + assert_eq!( + (day_counter.minted_in_window, day_counter.mint_window_secs), + (None, None) + ); assert_eq!(day_counter.mint_headroom.as_deref(), Some("9999999999000")); } @@ -276,7 +308,8 @@ mod tests { "mint_nonce": 1, "total_supply": "5700", "registered_at": 3, }], }); - let list: TokenList = serde_json::from_value(v).expect("must parse the node's own pinned shape"); + let list: TokenList = + serde_json::from_value(v).expect("must parse the node's own pinned shape"); assert_eq!(list.registration_fee, Some(1_000_000_000)); assert_eq!(list.tokens[0].total_supply, "5700"); } diff --git a/crates/randscan-core/src/types/transaction.rs b/crates/randscan-core/src/types/transaction.rs index 37b0f76..4c9c033 100644 --- a/crates/randscan-core/src/types/transaction.rs +++ b/crates/randscan-core/src/types/transaction.rs @@ -312,8 +312,13 @@ pub enum TokenAction { #[serde(tag = "kind", rename_all = "snake_case")] pub enum BridgeGovernanceAction { /// The genesis pause key's own signature; no PQ quorum (only *lifting* the pause needs one). - PauseMints { nonce: i64 }, - UnpauseMints { nonce: i64, pq_signers: Vec }, + PauseMints { + nonce: i64, + }, + UnpauseMints { + nonce: i64, + pq_signers: Vec, + }, RegisterBridgedToken { name: String, symbol: String, @@ -334,16 +339,36 @@ pub enum BridgeGovernanceAction { pq_signers: Vec, }, /// Bridge rules v2: a new PQ guardian set (Dilithium2 keys, hex), signed by the set before. - RotatePqGuardians { new_pq_guardians: Vec, nonce: i64, pq_signers: Vec }, + RotatePqGuardians { + new_pq_guardians: Vec, + nonce: i64, + pq_signers: Vec, + }, /// Bridge rules v2: a new pause key, signed by the PQ quorum. - RotatePauseKey { new_pause_key: String, nonce: i64, pq_signers: Vec }, + RotatePauseKey { + new_pause_key: String, + nonce: i64, + pq_signers: Vec, + }, /// Audit v6 (BRG-14): the rotation with every new holder's proof of possession (counted, not /// shown); it takes effect after the genesis `bridge.rotation.delay_secs` unless cancelled. - RotatePqGuardiansV2 { new_pq_guardians: Vec, possession_signatures: i64, nonce: i64, pq_signers: Vec }, - RotatePauseKeyV2 { new_pause_key: String, nonce: i64, pq_signers: Vec }, + RotatePqGuardiansV2 { + new_pq_guardians: Vec, + possession_signatures: i64, + nonce: i64, + pq_signers: Vec, + }, + RotatePauseKeyV2 { + new_pause_key: String, + nonce: i64, + pq_signers: Vec, + }, /// Audit v6 (BRG-14): a pending rotation withdrawn before it took effect; `rotation_kind` is /// `pq_guardians` or `pause_key`. - CancelRotation { rotation_kind: String, nonce: i64 }, + CancelRotation { + rotation_kind: String, + nonce: i64, + }, } /// Audit v6's validator-set actions (fullnode v0.6.8): bundle-less and fee-less. @@ -353,9 +378,18 @@ pub enum StakingAction { /// STAKE-2: the validator set's vote admitting `candidate` (an address; `candidate_key` the /// Dilithium2 key, hex) to register with a `bond`, on a chain whose genesis sets /// `staking.admission_by_vote`. `voters` are the signing validators, by address. - AdmitValidator { candidate: String, candidate_key: String, voters: Vec }, + AdmitValidator { + candidate: String, + candidate_key: String, + voters: Vec, + }, /// STAKE-1: the evidence of a leader equivocation — two headers one key signed for one view. - SlashEquivocation { offender: String, view: i64, first: HeaderRef, second: HeaderRef }, + SlashEquivocation { + offender: String, + view: i64, + first: HeaderRef, + second: HeaderRef, + }, } /// A block header named by hash and height (a `slash_equivocation`'s two headers). @@ -515,7 +549,10 @@ mod tests { ); assert_eq!(TxKind::Transfer.node_tag(), "none"); assert_eq!(TxKind::Withdraw.node_tag(), "withdraw"); - assert_eq!(TxKind::RegisterBridgedToken.as_str(), "register_bridged_token"); + assert_eq!( + TxKind::RegisterBridgedToken.as_str(), + "register_bridged_token" + ); } #[test] @@ -534,9 +571,18 @@ mod tests { "mints": [{ "asset": 2, "amount": "40", "recipient": "rand1abc", "time": 41, "r": "cc".repeat(32), "cm": "dd".repeat(32) }], })) .unwrap(); - assert_eq!((t.reads.len(), t.writes.len(), t.inflow.as_str()), (1, 1, "deposit")); - assert_eq!((t.pays[0].asset, t.pays[0].amount.as_str(), t.mints[0].asset), (0, "300", 2)); - assert_eq!(serde_json::to_value(&t).unwrap()["pays"][0]["cm"], "bb".repeat(32)); + assert_eq!( + (t.reads.len(), t.writes.len(), t.inflow.as_str()), + (1, 1, "deposit") + ); + assert_eq!( + (t.pays[0].asset, t.pays[0].amount.as_str(), t.mints[0].asset), + (0, "300", 2) + ); + assert_eq!( + serde_json::to_value(&t).unwrap()["pays"][0]["cm"], + "bb".repeat(32) + ); } #[test] @@ -574,7 +620,10 @@ mod tests { assert!(TxKind::UnpauseMints.has_pq_signers()); assert!(TxKind::RegisterBridgedToken.has_pq_signers()); assert!(TxKind::ListBacking.has_pq_signers()); - assert!(!TxKind::PauseMints.has_pq_signers(), "the pause key alone, no quorum"); + assert!( + !TxKind::PauseMints.has_pq_signers(), + "the pause key alone, no quorum" + ); assert!(!TxKind::TokenMint.has_pq_signers()); } @@ -648,7 +697,10 @@ mod tests { assert_eq!(v["kind"], "transfer"); assert_eq!(v["bundle"]["nullifiers"].as_array().unwrap().len(), 4); assert_eq!(v["bundle"]["commitments"][3], "08"); - assert!(v["bundle"].get("asset").is_none(), "no public asset field on the bundle"); + assert!( + v["bundle"].get("asset").is_none(), + "no public asset field on the bundle" + ); assert!(v.get("sender").is_none() && v.get("nonce").is_none()); } @@ -673,7 +725,10 @@ mod tests { }); let v = serde_json::to_value(&d).unwrap(); let dump = v.to_string(); - assert!(!dump.contains("\"asset\":"), "a transfer's asset must never be serialised: {dump}"); + assert!( + !dump.contains("\"asset\":"), + "a transfer's asset must never be serialised: {dump}" + ); assert_eq!(v["asset_index"], serde_json::Value::Null); } @@ -726,7 +781,10 @@ mod tests { pq_signers: vec![0, 2], }); let v = serde_json::to_value(&d).unwrap(); - assert_eq!(v["bridge_governance"]["pq_signers"], serde_json::json!([0, 2])); + assert_eq!( + v["bridge_governance"]["pq_signers"], + serde_json::json!([0, 2]) + ); } /// Audit v6's kinds are filterable, their tags are the node's (`tx_json` in fullnode @@ -746,7 +804,10 @@ mod tests { assert_eq!(TxKind::parse(tag), Some(k)); assert_eq!(serde_json::to_string(&k).unwrap(), format!("\"{tag}\"")); } - assert!(TxKind::RotatePqGuardiansV2.has_pq_signers() && !TxKind::CancelRotation.has_pq_signers()); + assert!( + TxKind::RotatePqGuardiansV2.has_pq_signers() + && !TxKind::CancelRotation.has_pq_signers() + ); assert!(!TxKind::AdmitValidator.has_pq_signers()); // The governance enum's own tags agree with the kinds'. let g = BridgeGovernanceAction::RotatePqGuardiansV2 { @@ -755,15 +816,24 @@ mod tests { nonce: 9, pq_signers: vec![2], }; - assert_eq!(serde_json::to_value(&g).unwrap()["kind"], "rotate_pq_guardians_v2"); + assert_eq!( + serde_json::to_value(&g).unwrap()["kind"], + "rotate_pq_guardians_v2" + ); let c: BridgeGovernanceAction = serde_json::from_value(serde_json::json!({ "kind": "cancel_rotation", "rotation_kind": "pause_key", "nonce": 11 })).unwrap(); - assert!(matches!(c, BridgeGovernanceAction::CancelRotation { nonce: 11, .. })); + assert!(matches!( + c, + BridgeGovernanceAction::CancelRotation { nonce: 11, .. } + )); let s: StakingAction = serde_json::from_value(serde_json::json!({ "kind": "slash_equivocation", "offender": "2nRd", "view": 7, "first": { "hash": "aa", "height": 5 }, "second": { "hash": "bb", "height": 5 } })) .unwrap(); - assert!(matches!(s, StakingAction::SlashEquivocation { view: 7, .. })); + assert!(matches!( + s, + StakingAction::SlashEquivocation { view: 7, .. } + )); } } diff --git a/crates/randscan-db/src/models.rs b/crates/randscan-db/src/models.rs index 2c3797f..5b223f4 100644 --- a/crates/randscan-db/src/models.rs +++ b/crates/randscan-db/src/models.rs @@ -2,9 +2,9 @@ use chrono::{DateTime, Utc}; use randscan_core::{ - BlockSummary, BridgeFeeNote, BridgeGovernanceAction, Bundle, StakingAction, GeoInfo, NetworkStats, Note, Nullifier, - PendingStake, ProgramSummary, Receipt, TokenAction, TransactionDetail, TransactionSummary, Transition, - TxKind, Validator, + BlockSummary, BridgeFeeNote, BridgeGovernanceAction, Bundle, GeoInfo, NetworkStats, Note, + Nullifier, PendingStake, ProgramSummary, Receipt, StakingAction, TokenAction, + TransactionDetail, TransactionSummary, Transition, TxKind, Validator, }; use sqlx::FromRow; @@ -189,9 +189,13 @@ impl TxDetailRow { let pq_signers = self .pq_signers .map(|v| v.into_iter().map(i64::from).collect()); - let transition: Option = self.transition.and_then(|v| serde_json::from_value(v).ok()); - let fee_note: Option = self.fee_note.and_then(|v| serde_json::from_value(v).ok()); - let staking_action: Option = self.staking_action.and_then(|v| serde_json::from_value(v).ok()); + let transition: Option = + self.transition.and_then(|v| serde_json::from_value(v).ok()); + let fee_note: Option = + self.fee_note.and_then(|v| serde_json::from_value(v).ok()); + let staking_action: Option = self + .staking_action + .and_then(|v| serde_json::from_value(v).ok()); TransactionDetail { chain_id: self.chain_id, bundle, diff --git a/crates/randscan-db/src/queries/notes.rs b/crates/randscan-db/src/queries/notes.rs index 4426088..2e450dc 100644 --- a/crates/randscan-db/src/queries/notes.rs +++ b/crates/randscan-db/src/queries/notes.rs @@ -107,22 +107,26 @@ pub async fn get_note_envelopes_for_tx(pool: &PgPool, hash: &str) -> Result(&sql) - .bind(hash) - .fetch_all(pool) - .await?) + .bind(hash) + .fetch_all(pool) + .await?) } /// A page of leaves with envelopes from `from_leaf` upwards (for a history scan in the browser). -pub async fn list_note_envelopes(pool: &PgPool, from_leaf: i64, limit: i64) -> Result> { +pub async fn list_note_envelopes( + pool: &PgPool, + from_leaf: i64, + limit: i64, +) -> Result> { let sql = format!( "SELECT {ENVELOPE_COLS} FROM notes n LEFT JOIN transactions t ON t.hash = n.tx_hash WHERE n.leaf_index >= $1 ORDER BY n.leaf_index LIMIT $2" ); Ok(sqlx::query_as::<_, NoteEnvelopeRow>(&sql) - .bind(from_leaf) - .bind(limit) - .fetch_all(pool) - .await?) + .bind(from_leaf) + .bind(limit) + .fetch_all(pool) + .await?) } /// Link notes of `height` to the transactions that created them (for leaves fetched before the diff --git a/crates/randscan-db/src/queries/transactions.rs b/crates/randscan-db/src/queries/transactions.rs index 2fcabfe..e18442a 100644 --- a/crates/randscan-db/src/queries/transactions.rs +++ b/crates/randscan-db/src/queries/transactions.rs @@ -356,7 +356,11 @@ pub struct TokenEventRow { /// The transaction that registered a token: a `register_token` naming this registry index, or a /// `register_bridged_token` naming this asset id (that action's `tx_json` carries no registry /// index — only the id the chain will derive it under — so it is matched by id instead). -pub async fn find_token_deploy_tx(pool: &PgPool, asset_index: i64, asset_id_hex: &str) -> Result> { +pub async fn find_token_deploy_tx( + pool: &PgPool, + asset_index: i64, + asset_id_hex: &str, +) -> Result> { Ok(sqlx::query_scalar( "SELECT hash FROM transactions WHERE (kind = 'register_token' AND asset_index = $1) @@ -369,7 +373,11 @@ pub async fn find_token_deploy_tx(pool: &PgPool, asset_index: i64, asset_id_hex: .await?) } -pub async fn list_token_events(pool: &PgPool, asset_index: i64, limit: i64) -> Result> { +pub async fn list_token_events( + pool: &PgPool, + asset_index: i64, + limit: i64, +) -> Result> { Ok(sqlx::query_as::<_, TokenEventRow>( "SELECT hash AS tx_hash, height, timestamp_ms, kind, amount::text AS amount, token_action FROM transactions diff --git a/crates/randscan-db/tests/chain_reset.rs b/crates/randscan-db/tests/chain_reset.rs index 5893cff..12439cf 100644 --- a/crates/randscan-db/tests/chain_reset.rs +++ b/crates/randscan-db/tests/chain_reset.rs @@ -2,8 +2,8 @@ use randscan_db::{ get_indexer_state, insert_block, insert_notes, insert_transaction, reset_chain_data, - run_migrations, set_chain_id, set_next_height, set_next_leaf, NewBlock, NewBundle, NewNote, NewTx, - CHAIN_TABLES, + run_migrations, set_chain_id, set_next_height, set_next_leaf, NewBlock, NewBundle, NewNote, + NewTx, CHAIN_TABLES, }; use sqlx::postgres::PgPoolOptions; @@ -13,14 +13,21 @@ async fn reset_chain_data_truncates_chain_tables_and_keeps_users() { eprintln!("skipping: DATABASE_URL unset"); return; }; - let pool = PgPoolOptions::new().max_connections(2).connect(&url).await.unwrap(); + let pool = PgPoolOptions::new() + .max_connections(2) + .connect(&url) + .await + .unwrap(); run_migrations(&pool).await.unwrap(); let mut conn = pool.acquire().await.unwrap(); // One block with a bridge_burn (the chain-14 single hidden-asset bundle: four slots, its // `burn_a`/`burn_asset` carrying the redemption), one tree leaf, and a user that must survive. for t in CHAIN_TABLES { - sqlx::query(&format!("TRUNCATE {t} CASCADE")).execute(&mut *conn).await.unwrap(); + sqlx::query(&format!("TRUNCATE {t} CASCADE")) + .execute(&mut *conn) + .await + .unwrap(); } insert_block( &mut conn, @@ -41,8 +48,18 @@ async fn reset_chain_data_truncates_chain_tables_and_keeps_users() { .unwrap(); let bundle = |tag: &str| NewBundle { anchor: "aa".repeat(32), - nullifiers: [format!("{tag}1").repeat(32), format!("{tag}2").repeat(32), format!("{tag}3").repeat(32), format!("{tag}4").repeat(32)], - commitments: [format!("{tag}5").repeat(32), format!("{tag}6").repeat(32), format!("{tag}7").repeat(32), format!("{tag}8").repeat(32)], + nullifiers: [ + format!("{tag}1").repeat(32), + format!("{tag}2").repeat(32), + format!("{tag}3").repeat(32), + format!("{tag}4").repeat(32), + ], + commitments: [ + format!("{tag}5").repeat(32), + format!("{tag}6").repeat(32), + format!("{tag}7").repeat(32), + format!("{tag}8").repeat(32), + ], fee: "2000000".into(), burn_a: "500".into(), burn_r: "0".into(), @@ -75,13 +92,28 @@ async fn reset_chain_data_truncates_chain_tables_and_keeps_users() { ) .await .unwrap(); - insert_notes(&mut conn, &[NewNote { leaf_index: 0, cm: "d5".repeat(32), height: 0, envelope: None }]).await.unwrap(); + insert_notes( + &mut conn, + &[NewNote { + leaf_index: 0, + cm: "d5".repeat(32), + height: 0, + envelope: None, + }], + ) + .await + .unwrap(); set_next_leaf(&mut conn, 1).await.unwrap(); - set_next_height(&mut conn, 1, Some(&"ab".repeat(32))).await.unwrap(); + set_next_height(&mut conn, 1, Some(&"ab".repeat(32))) + .await + .unwrap(); set_chain_id(&mut conn, 4).await.unwrap(); // The database is shared across runs and users survive the reset by design, so drop any // copy of this user a previous run left behind before inserting it again. - sqlx::query("DELETE FROM users WHERE email = 'reset@example.com'").execute(&mut *conn).await.unwrap(); + sqlx::query("DELETE FROM users WHERE email = 'reset@example.com'") + .execute(&mut *conn) + .await + .unwrap(); sqlx::query("INSERT INTO users (email, password_hash) VALUES ('reset@example.com', 'x')") .execute(&mut *conn) .await @@ -94,9 +126,18 @@ async fn reset_chain_data_truncates_chain_tables_and_keeps_users() { .fetch_one(&mut *conn) .await .unwrap(); - assert_eq!(stored, ("bridge_burn".into(), Some("400".into()), Some(2), true)); - let nfs: i64 = sqlx::query_scalar("SELECT count(*) FROM nullifiers").fetch_one(&mut *conn).await.unwrap(); - assert_eq!(nfs, 4, "the one hidden-asset bundle's four slots are recorded"); + assert_eq!( + stored, + ("bridge_burn".into(), Some("400".into()), Some(2), true) + ); + let nfs: i64 = sqlx::query_scalar("SELECT count(*) FROM nullifiers") + .fetch_one(&mut *conn) + .await + .unwrap(); + assert_eq!( + nfs, 4, + "the one hidden-asset bundle's four slots are recorded" + ); let (burn_a, burn_asset): (String, i64) = sqlx::query_as("SELECT burn_a::text, burn_asset FROM transactions WHERE hash = $1") .bind("cd".repeat(32)) @@ -104,11 +145,16 @@ async fn reset_chain_data_truncates_chain_tables_and_keeps_users() { .await .unwrap(); assert_eq!((burn_a, burn_asset), ("500".into(), 2)); - let linked: Option = sqlx::query_scalar("SELECT tx_hash FROM notes WHERE leaf_index = 0") - .fetch_one(&mut *conn) - .await - .unwrap(); - assert_eq!(linked.as_deref(), Some("cd".repeat(32).as_str()), "a leaf is linked to the tx that carried its commitment"); + let linked: Option = + sqlx::query_scalar("SELECT tx_hash FROM notes WHERE leaf_index = 0") + .fetch_one(&mut *conn) + .await + .unwrap(); + assert_eq!( + linked.as_deref(), + Some("cd".repeat(32).as_str()), + "a leaf is linked to the tx that carried its commitment" + ); reset_chain_data(&mut conn, 5).await.unwrap(); @@ -124,10 +170,11 @@ async fn reset_chain_data_truncates_chain_tables_and_keeps_users() { assert_eq!(st.next_leaf, 0); assert_eq!(st.last_indexed_hash, None); assert_eq!(st.chain_id, Some(5)); - let users: i64 = sqlx::query_scalar("SELECT count(*) FROM users WHERE email = 'reset@example.com'") - .fetch_one(&mut *conn) - .await - .unwrap(); + let users: i64 = + sqlx::query_scalar("SELECT count(*) FROM users WHERE email = 'reset@example.com'") + .fetch_one(&mut *conn) + .await + .unwrap(); assert_eq!(users, 1); let (chain_id, height, notes): (i64, i64, i64) = sqlx::query_as("SELECT chain_id, height, notes FROM network_stats WHERE id = 1") diff --git a/crates/randscan-indexer/src/nodes.rs b/crates/randscan-indexer/src/nodes.rs index 23abd3f..46f2292 100644 --- a/crates/randscan-indexer/src/nodes.rs +++ b/crates/randscan-indexer/src/nodes.rs @@ -2,7 +2,10 @@ use crate::rpc::RpcClient; use anyhow::Result; -use randscan_core::{is_private_ip, parse_multiaddr, NodeInfo, ProverInfoReply, ProverMember, ProverView, KNOWN_PROVERS}; +use randscan_core::{ + is_private_ip, parse_multiaddr, NodeInfo, ProverInfoReply, ProverMember, ProverView, + KNOWN_PROVERS, +}; use randscan_db::{self as db, DbPool}; use serde::Deserialize; use std::collections::HashMap; @@ -73,7 +76,9 @@ impl NodeTracker { .filter(|s| !s.is_empty()), ), nodes: Arc::new(RwLock::new(Vec::new())), - provers: Arc::new(RwLock::new(KNOWN_PROVERS.iter().map(ProverView::unpolled).collect())), + provers: Arc::new(RwLock::new( + KNOWN_PROVERS.iter().map(ProverView::unpolled).collect(), + )), } } @@ -203,22 +208,33 @@ impl NodeTracker { /// Poll every known prover's `prover_info` and place its members. A prover that does not /// answer keeps its last reply (and `last_up_ms`), marked down with the error. async fn refresh_provers(&self) -> Result<()> { - let ips: Vec = KNOWN_PROVERS.iter().flat_map(|p| p.members.iter().map(|m| m.ip.to_string())).collect(); + let ips: Vec = KNOWN_PROVERS + .iter() + .flat_map(|p| p.members.iter().map(|m| m.ip.to_string())) + .collect(); let cache = self.geolocate(&ips).await?; let previous = self.provers().await; let now = chrono_now().timestamp_millis(); let mut out = Vec::with_capacity(KNOWN_PROVERS.len()); for p in KNOWN_PROVERS { - let mut v = previous.iter().find(|v| v.url == p.url).cloned().unwrap_or_else(|| ProverView::unpolled(p)); + let mut v = previous + .iter() + .find(|v| v.url == p.url) + .cloned() + .unwrap_or_else(|| ProverView::unpolled(p)); v.members = p .members .iter() - .map(|m| ProverMember { label: m.label.into(), geo: cache.get(m.ip).and_then(|r| r.geo()) }) + .map(|m| ProverMember { + label: m.label.into(), + geo: cache.get(m.ip).and_then(|r| r.geo()), + }) .collect(); v.checked_at_ms = Some(now); match self.prover_info(p.url).await { Ok(info) => { - v.fingerprint_matches = info.kem_fingerprint.as_deref().map(|f| f == p.fingerprint); + v.fingerprint_matches = + info.kem_fingerprint.as_deref().map(|f| f == p.fingerprint); v.up = true; v.error = None; v.last_up_ms = Some(now); @@ -250,7 +266,9 @@ impl NodeTracker { if let Some(err) = reply.get("error") { anyhow::bail!("prover_info: {err}"); } - Ok(serde_json::from_value(reply.get("result").cloned().unwrap_or_default())?) + Ok(serde_json::from_value( + reply.get("result").cloned().unwrap_or_default(), + )?) } async fn discover_public_ip(&self) -> Option { diff --git a/crates/randscan-indexer/src/processor.rs b/crates/randscan-indexer/src/processor.rs index 8411bde..3f91db7 100644 --- a/crates/randscan-indexer/src/processor.rs +++ b/crates/randscan-indexer/src/processor.rs @@ -518,7 +518,9 @@ impl<'a> TxFields<'a> { derived_cm: commitment.clone(), pq_signers: Some(pq_signers.iter().map(|&i| i as i32).collect()), deposit_amount: deposit_amount.as_ref().map(|a| a.0.clone()), - fee_note: fee_note.as_ref().and_then(|n| serde_json::to_value(n.to_core()).ok()), + fee_note: fee_note + .as_ref() + .and_then(|n| serde_json::to_value(n.to_core()).ok()), fee_cm: fee_note.as_ref().map(|n| n.commitment.clone()), ..Self::empty(TxKind::BridgeAttest) }, @@ -539,7 +541,9 @@ impl<'a> TxFields<'a> { bridge_to: Some(to), bridge_token: Some(token), release_amount: release_amount.as_ref().map(|a| a.0.clone()), - fee_note: fee_note.as_ref().and_then(|n| serde_json::to_value(n.to_core()).ok()), + fee_note: fee_note + .as_ref() + .and_then(|n| serde_json::to_value(n.to_core()).ok()), fee_cm: fee_note.as_ref().map(|n| n.commitment.clone()), ..Self::empty(TxKind::BridgeBurn) }, @@ -569,7 +573,13 @@ impl<'a> TxFields<'a> { // The registration's initial mint is the one derived note register_token appends // (register_token itself creates no other leaf). let derived_cm = initial.as_ref().and_then(|m| { - randscan_core::notecommit::mint_commitment_hex(&m.recipient, m.amount.0.parse().unwrap_or(0), *index, m.time as u32, &m.r) + randscan_core::notecommit::mint_commitment_hex( + &m.recipient, + m.amount.0.parse().unwrap_or(0), + *index, + m.time as u32, + &m.r, + ) }); TxFields { asset_index: Some(*index as i64), @@ -598,8 +608,13 @@ impl<'a> TxFields<'a> { r: r.clone(), nonce: *nonce as i64, }; - let derived_cm = - randscan_core::notecommit::mint_commitment_hex(recipient, amount.0.parse().unwrap_or(0), *asset, *time as u32, r); + let derived_cm = randscan_core::notecommit::mint_commitment_hex( + recipient, + amount.0.parse().unwrap_or(0), + *asset, + *time as u32, + r, + ); TxFields { asset_index: Some(*asset as i64), amount: Some(amount.0.clone()), @@ -612,7 +627,11 @@ impl<'a> TxFields<'a> { ..Self::empty(TxKind::TokenMint) } } - RpcAction::SetAuthority { asset, nonce, new_authority } => { + RpcAction::SetAuthority { + asset, + nonce, + new_authority, + } => { let action = randscan_core::TokenAction::SetAuthority { asset: *asset as i64, nonce: *nonce as i64, @@ -626,7 +645,10 @@ impl<'a> TxFields<'a> { } } RpcAction::TokenBurn { asset, amount } => { - let action = randscan_core::TokenAction::TokenBurn { asset: *asset as i64, amount: amount.0.clone() }; + let action = randscan_core::TokenAction::TokenBurn { + asset: *asset as i64, + amount: amount.0.clone(), + }; TxFields { asset_index: Some(*asset as i64), amount: Some(amount.0.clone()), @@ -635,7 +657,9 @@ impl<'a> TxFields<'a> { } } RpcAction::PauseMints { nonce } => { - let action = randscan_core::BridgeGovernanceAction::PauseMints { nonce: *nonce as i64 }; + let action = randscan_core::BridgeGovernanceAction::PauseMints { + nonce: *nonce as i64, + }; TxFields { action_nonce: Some(*nonce as i64), bridge_governance: serde_json::to_value(&action).ok(), @@ -707,7 +731,11 @@ impl<'a> TxFields<'a> { ..Self::empty(TxKind::ListBacking) } } - RpcAction::AdmitValidator { candidate, candidate_key, voters } => { + RpcAction::AdmitValidator { + candidate, + candidate_key, + voters, + } => { let action = randscan_core::StakingAction::AdmitValidator { candidate: candidate.clone(), candidate_key: candidate_key.clone(), @@ -719,8 +747,16 @@ impl<'a> TxFields<'a> { ..Self::empty(TxKind::AdmitValidator) } } - RpcAction::SlashEquivocation { offender, view, first, second } => { - let r = |h: &crate::rpc::RpcHeaderRef| randscan_core::HeaderRef { hash: h.hash.clone(), height: h.height as i64 }; + RpcAction::SlashEquivocation { + offender, + view, + first, + second, + } => { + let r = |h: &crate::rpc::RpcHeaderRef| randscan_core::HeaderRef { + hash: h.hash.clone(), + height: h.height as i64, + }; let action = randscan_core::StakingAction::SlashEquivocation { offender: offender.clone(), view: *view as i64, @@ -733,7 +769,13 @@ impl<'a> TxFields<'a> { ..Self::empty(TxKind::SlashEquivocation) } } - RpcAction::RotatePqGuardians { new_pq_guardians, possession_signatures, nonce, pq_signers, v2 } => { + RpcAction::RotatePqGuardians { + new_pq_guardians, + possession_signatures, + nonce, + pq_signers, + v2, + } => { let (action, kind) = if *v2 { ( randscan_core::BridgeGovernanceAction::RotatePqGuardiansV2 { @@ -761,16 +803,30 @@ impl<'a> TxFields<'a> { ..Self::empty(kind) } } - RpcAction::RotatePauseKey { new_pause_key, nonce, pq_signers, v2 } => { - let (new_pause_key, nonce_i, signers) = (new_pause_key.clone(), *nonce as i64, pq_signers.clone()); + RpcAction::RotatePauseKey { + new_pause_key, + nonce, + pq_signers, + v2, + } => { + let (new_pause_key, nonce_i, signers) = + (new_pause_key.clone(), *nonce as i64, pq_signers.clone()); let (action, kind) = if *v2 { ( - randscan_core::BridgeGovernanceAction::RotatePauseKeyV2 { new_pause_key, nonce: nonce_i, pq_signers: signers }, + randscan_core::BridgeGovernanceAction::RotatePauseKeyV2 { + new_pause_key, + nonce: nonce_i, + pq_signers: signers, + }, TxKind::RotatePauseKeyV2, ) } else { ( - randscan_core::BridgeGovernanceAction::RotatePauseKey { new_pause_key, nonce: nonce_i, pq_signers: signers }, + randscan_core::BridgeGovernanceAction::RotatePauseKey { + new_pause_key, + nonce: nonce_i, + pq_signers: signers, + }, TxKind::RotatePauseKey, ) }; @@ -781,7 +837,10 @@ impl<'a> TxFields<'a> { ..Self::empty(kind) } } - RpcAction::CancelRotation { rotation_kind, nonce } => { + RpcAction::CancelRotation { + rotation_kind, + nonce, + } => { let action = randscan_core::BridgeGovernanceAction::CancelRotation { rotation_kind: rotation_kind.clone(), nonce: *nonce as i64, @@ -891,12 +950,16 @@ mod tests { assert_eq!(f.asset_index, Some(3)); assert_eq!(f.amount.as_deref(), Some("700")); assert_eq!(f.recipient, Some(recipient.as_str())); - let cm = f.derived_cm.expect("a token_mint's note commitment must be computed"); + let cm = f + .derived_cm + .expect("a token_mint's note commitment must be computed"); assert_eq!(cm.len(), 64); // Deterministic: recomputing from the same fields gives the same commitment. let f2 = TxFields::from_action(&k); assert_eq!(f2.derived_cm, Some(cm)); - let action = f.token_action.expect("token_action carries the full payload"); + let action = f + .token_action + .expect("token_action carries the full payload"); assert_eq!(action["kind"], "token_mint"); assert_eq!(action["asset"], 3); } @@ -914,7 +977,12 @@ mod tests { authority: "bridge".into(), index: 3, initial_amount: Some(Units("5000".into())), - initial: Some(RpcInitialMint { amount: Units("5000".into()), recipient: recipient.clone(), time: 40, r: "bb".repeat(32) }), + initial: Some(RpcInitialMint { + amount: Units("5000".into()), + recipient: recipient.clone(), + time: 40, + r: "bb".repeat(32), + }), }; let f = TxFields::from_action(&with_initial); assert_eq!(f.kind, TxKind::RegisterToken); @@ -985,7 +1053,11 @@ mod tests { let f = TxFields::from_action(&k); assert_eq!(f.amount.as_deref(), Some("100000000")); assert_eq!(f.deposit_amount.as_deref(), Some("99900000")); - assert_eq!(f.derived_cm.as_deref(), Some("d8".repeat(32).as_str()), "the net deposit's leaf"); + assert_eq!( + f.derived_cm.as_deref(), + Some("d8".repeat(32).as_str()), + "the net deposit's leaf" + ); assert_eq!(f.fee_cm.as_deref(), Some("47".repeat(32).as_str())); assert_eq!(f.fee_note.as_ref().unwrap()["amount"], "100000"); @@ -1000,15 +1072,25 @@ mod tests { fee_note: Some(fee), }; let f = TxFields::from_action(&burn); - assert_eq!((f.amount.as_deref(), f.release_amount.as_deref()), (Some("500000"), Some("499500"))); + assert_eq!( + (f.amount.as_deref(), f.release_amount.as_deref()), + (Some("500000"), Some("499500")) + ); assert!(f.fee_cm.is_some()); } #[test] fn the_audit_v6_actions_are_stored_with_their_kinds() { - let a = RpcAction::AdmitValidator { candidate: "2nRd".into(), candidate_key: "ab".into(), voters: vec!["v1".into()] }; + let a = RpcAction::AdmitValidator { + candidate: "2nRd".into(), + candidate_key: "ab".into(), + voters: vec!["v1".into()], + }; let f = TxFields::from_action(&a); - assert_eq!((f.kind, f.validator), (TxKind::AdmitValidator, Some("2nRd"))); + assert_eq!( + (f.kind, f.validator), + (TxKind::AdmitValidator, Some("2nRd")) + ); assert_eq!(f.staking_action.unwrap()["kind"], "admit_validator"); let r = RpcAction::RotatePqGuardians { new_pq_guardians: vec!["k".into()], @@ -1018,9 +1100,15 @@ mod tests { v2: true, }; let f = TxFields::from_action(&r); - assert_eq!((f.kind, f.kind_tag), (TxKind::RotatePqGuardiansV2, "rotate_pq_guardians_v2")); + assert_eq!( + (f.kind, f.kind_tag), + (TxKind::RotatePqGuardiansV2, "rotate_pq_guardians_v2") + ); assert_eq!(f.bridge_governance.unwrap()["possession_signatures"], 1); - let c = RpcAction::CancelRotation { rotation_kind: "pq_guardians".into(), nonce: 4 }; + let c = RpcAction::CancelRotation { + rotation_kind: "pq_guardians".into(), + nonce: 4, + }; let f = TxFields::from_action(&c); assert_eq!((f.kind, f.action_nonce), (TxKind::CancelRotation, Some(4))); } @@ -1032,11 +1120,17 @@ mod tests { assert_eq!(f.action_nonce, Some(4)); assert_eq!(f.bridge_governance.unwrap()["kind"], "pause_mints"); - let unpause = RpcAction::UnpauseMints { nonce: 5, pq_signers: vec![0, 2] }; + let unpause = RpcAction::UnpauseMints { + nonce: 5, + pq_signers: vec![0, 2], + }; let f = TxFields::from_action(&unpause); assert_eq!(f.kind, TxKind::UnpauseMints); assert_eq!(f.pq_signers, Some(vec![0, 2])); - assert_eq!(f.bridge_governance.unwrap()["pq_signers"], serde_json::json!([0, 2])); + assert_eq!( + f.bridge_governance.unwrap()["pq_signers"], + serde_json::json!([0, 2]) + ); } #[test] diff --git a/crates/randscan-indexer/src/rpc.rs b/crates/randscan-indexer/src/rpc.rs index 1c7fadb..4d9bb2c 100644 --- a/crates/randscan-indexer/src/rpc.rs +++ b/crates/randscan-indexer/src/rpc.rs @@ -181,14 +181,15 @@ impl RpcClient { } pub async fn program(&self, id: &str) -> Result> { - self.call("rand_getProgram", serde_json::json!([id])) - .await + self.call("rand_getProgram", serde_json::json!([id])).await } /// RPL-2: a program's vault. `None` on a node without the method and on a chain without a /// `program_state` section (`{"enabled": false}`); an empty vault is `Some(vec![])`. pub async fn program_vault(&self, id: &str) -> Result>> { - let v: Option = self.call_optional_method("rand_getProgramVault", serde_json::json!([id])).await?; + let v: Option = self + .call_optional_method("rand_getProgramVault", serde_json::json!([id])) + .await?; match v { Some(rows @ serde_json::Value::Array(_)) => Ok(Some(serde_json::from_value(rows)?)), _ => Ok(None), @@ -197,9 +198,17 @@ impl RpcClient { /// RPL-2: a page of a program's cells in key order, after `after` (a 64-hex key) when given, /// at most `limit` (the node clamps to 1 000). `None` as [`Self::program_vault`]. - pub async fn program_cells(&self, id: &str, after: Option<&str>, limit: u64) -> Result> { + pub async fn program_cells( + &self, + id: &str, + after: Option<&str>, + limit: u64, + ) -> Result> { let v: Option = self - .call_optional_method("rand_getProgramCells", serde_json::json!([id, { "after": after, "limit": limit }])) + .call_optional_method( + "rand_getProgramCells", + serde_json::json!([id, { "after": after, "limit": limit }]), + ) .await?; match v { Some(v) if v.get("cells").is_some() => Ok(Some(serde_json::from_value(v)?)), @@ -234,7 +243,11 @@ impl RpcClient { /// One page of the RPL token registry, `[from_index, limit]`. `None` on a node without the /// method (a chain older than the token RPC task). - pub async fn tokens_page(&self, from_index: u64, limit: u64) -> Result> { + pub async fn tokens_page( + &self, + from_index: u64, + limit: u64, + ) -> Result> { self.call_optional_method("rand_getTokens", serde_json::json!([from_index, limit])) .await } @@ -579,10 +592,7 @@ pub enum RpcAction { }, /// A holder burn: public by design (it audits `total_supply`); a transfer of the same token /// is a plain `none` bundle, its asset private. - TokenBurn { - asset: u32, - amount: Units, - }, + TokenBurn { asset: u32, amount: Units }, /// Bridge hardening B1: the genesis pause key's own signature, no PQ quorum. PauseMints { nonce: u64 }, /// Bridge hardening B1: lifting the pause needs the PQ guardian quorum. @@ -618,19 +628,37 @@ pub enum RpcAction { transition: RpcTransition, }, /// Audit v6, STAKE-2: the validator set's vote admitting a key. - AdmitValidator { candidate: String, candidate_key: String, voters: Vec }, + AdmitValidator { + candidate: String, + candidate_key: String, + voters: Vec, + }, /// Audit v6, STAKE-1: two headers of one key for one view. - SlashEquivocation { offender: String, view: u64, first: RpcHeaderRef, second: RpcHeaderRef }, + SlashEquivocation { + offender: String, + view: u64, + first: RpcHeaderRef, + second: RpcHeaderRef, + }, /// Bridge rules v2 (22) and audit v6's possession-carrying twin (30, `v2`). - RotatePqGuardians { new_pq_guardians: Vec, possession_signatures: Option, nonce: u64, pq_signers: Vec, v2: bool }, + RotatePqGuardians { + new_pq_guardians: Vec, + possession_signatures: Option, + nonce: u64, + pq_signers: Vec, + v2: bool, + }, /// Bridge rules v2 (23) and audit v6's twin (31, `v2`). - RotatePauseKey { new_pause_key: String, nonce: u64, pq_signers: Vec, v2: bool }, + RotatePauseKey { + new_pause_key: String, + nonce: u64, + pq_signers: Vec, + v2: bool, + }, /// Audit v6, BRG-14 (32). CancelRotation { rotation_kind: String, nonce: u64 }, /// A kind this build does not decode; `kind` is the node's tag. - Unknown { - kind: String, - }, + Unknown { kind: String }, } /// An invoke's transition as the node renders it — the shape `randscan_core::Transition` keeps, @@ -667,7 +695,14 @@ pub struct RpcPayout { impl RpcTransition { /// The explorer's own type, amounts normalised to decimal strings. pub fn to_core(&self) -> randscan_core::Transition { - let cells = |v: &[RpcCell]| v.iter().map(|c| randscan_core::ProgramCell { key: c.key.clone(), value: c.value.clone() }).collect(); + let cells = |v: &[RpcCell]| { + v.iter() + .map(|c| randscan_core::ProgramCell { + key: c.key.clone(), + value: c.value.clone(), + }) + .collect() + }; let payouts = |v: &[RpcPayout]| { v.iter() .map(|p| randscan_core::Payout { @@ -691,7 +726,11 @@ impl RpcTransition { /// Every payout note's commitment, pays then mints — the order the chain appended them in. pub fn payout_cms(&self) -> Vec { - self.pays.iter().chain(&self.mints).map(|p| p.cm.clone()).collect() + self.pays + .iter() + .chain(&self.mints) + .map(|p| p.cm.clone()) + .collect() } } @@ -1082,12 +1121,30 @@ impl<'de> Deserialize<'de> for RpcAction { initial_amount, initial, }, - KnownAction::TokenMint { asset, amount, recipient, time, r, nonce } => { - RpcAction::TokenMint { asset, amount, recipient, time, r, nonce } - } - KnownAction::SetAuthority { asset, nonce, new_authority } => { - RpcAction::SetAuthority { asset, nonce, new_authority } - } + KnownAction::TokenMint { + asset, + amount, + recipient, + time, + r, + nonce, + } => RpcAction::TokenMint { + asset, + amount, + recipient, + time, + r, + nonce, + }, + KnownAction::SetAuthority { + asset, + nonce, + new_authority, + } => RpcAction::SetAuthority { + asset, + nonce, + new_authority, + }, KnownAction::TokenBurn { asset, amount } => RpcAction::TokenBurn { asset, amount }, KnownAction::PauseMints { nonce } => RpcAction::PauseMints { nonce }, KnownAction::UnpauseMints { nonce, pq_signers } => { @@ -1114,34 +1171,91 @@ impl<'de> Deserialize<'de> for RpcAction { asset_id, pq_signers, }, - KnownAction::ListBacking { token_index, chain, token, decimals, nonce, pq_signers } => { - RpcAction::ListBacking { token_index, chain, token, decimals, nonce, pq_signers } - } - KnownAction::AdmitValidator { candidate, candidate_key, voters } => { - RpcAction::AdmitValidator { candidate, candidate_key, voters } - } - KnownAction::SlashEquivocation { offender, view, first, second } => { - RpcAction::SlashEquivocation { offender, view, first, second } - } - KnownAction::RotatePqGuardians { new_pq_guardians, nonce, pq_signers } => { - RpcAction::RotatePqGuardians { new_pq_guardians, possession_signatures: None, nonce, pq_signers, v2: false } - } - KnownAction::RotatePqGuardiansV2 { new_pq_guardians, possession_signatures, nonce, pq_signers } => { - RpcAction::RotatePqGuardians { - new_pq_guardians, - possession_signatures: Some(possession_signatures), - nonce, - pq_signers, - v2: true, - } - } - KnownAction::RotatePauseKey { new_pause_key, nonce, pq_signers } => { - RpcAction::RotatePauseKey { new_pause_key, nonce, pq_signers, v2: false } - } - KnownAction::RotatePauseKeyV2 { new_pause_key, nonce, pq_signers } => { - RpcAction::RotatePauseKey { new_pause_key, nonce, pq_signers, v2: true } - } - KnownAction::CancelRotation { rotation_kind, nonce } => RpcAction::CancelRotation { rotation_kind, nonce }, + KnownAction::ListBacking { + token_index, + chain, + token, + decimals, + nonce, + pq_signers, + } => RpcAction::ListBacking { + token_index, + chain, + token, + decimals, + nonce, + pq_signers, + }, + KnownAction::AdmitValidator { + candidate, + candidate_key, + voters, + } => RpcAction::AdmitValidator { + candidate, + candidate_key, + voters, + }, + KnownAction::SlashEquivocation { + offender, + view, + first, + second, + } => RpcAction::SlashEquivocation { + offender, + view, + first, + second, + }, + KnownAction::RotatePqGuardians { + new_pq_guardians, + nonce, + pq_signers, + } => RpcAction::RotatePqGuardians { + new_pq_guardians, + possession_signatures: None, + nonce, + pq_signers, + v2: false, + }, + KnownAction::RotatePqGuardiansV2 { + new_pq_guardians, + possession_signatures, + nonce, + pq_signers, + } => RpcAction::RotatePqGuardians { + new_pq_guardians, + possession_signatures: Some(possession_signatures), + nonce, + pq_signers, + v2: true, + }, + KnownAction::RotatePauseKey { + new_pause_key, + nonce, + pq_signers, + } => RpcAction::RotatePauseKey { + new_pause_key, + nonce, + pq_signers, + v2: false, + }, + KnownAction::RotatePauseKeyV2 { + new_pause_key, + nonce, + pq_signers, + } => RpcAction::RotatePauseKey { + new_pause_key, + nonce, + pq_signers, + v2: true, + }, + KnownAction::CancelRotation { + rotation_kind, + nonce, + } => RpcAction::CancelRotation { + rotation_kind, + nonce, + }, }), // A known tag with a malformed body is a real error; an unknown tag is tolerated. Err(e) => { @@ -1310,9 +1424,15 @@ mod tests { assert_eq!(l.envelope_bytes, Some(1860)); assert!(l.hardening_v6); assert_eq!(l.hc_auth.as_deref().map(str::len), Some(64)); - assert_eq!((l.gas_price.as_deref(), l.byte_price.as_deref()), (Some("100"), Some("800"))); + assert_eq!( + (l.gas_price.as_deref(), l.byte_price.as_deref()), + (Some("100"), Some("800")) + ); assert!(l.has_gas_section()); - assert_eq!((l.bundle_gas_limit, l.adjust_bps), (Some(20479), Some(1250))); + assert_eq!( + (l.bundle_gas_limit, l.adjust_bps), + (Some(20479), Some(1250)) + ); // A node's own policy (Phase 0, no section) and a chain with none: both read as no section. let l: randscan_core::ChainLimits = serde_json::from_str( r#"{ "max_program_words": 4096, "max_proof_bytes": 2097152, "max_block_bytes": 4194304, @@ -1335,11 +1455,15 @@ mod tests { assert_eq!(s.hc_auth.as_deref(), Some("1a2b")); assert_eq!( s.gas_prices, - Some(RpcGasPrices { gas_price: Units("112".into()), byte_price: Units("900".into()) }) + Some(RpcGasPrices { + gas_price: Units("112".into()), + byte_price: Units("900".into()) + }) ); - let s: NodeStatus = - serde_json::from_str(r#"{ "height": 1, "hc_bundle": "60af", "hc_auth": null, "gas_prices": null }"#) - .unwrap(); + let s: NodeStatus = serde_json::from_str( + r#"{ "height": 1, "hc_bundle": "60af", "hc_auth": null, "gas_prices": null }"#, + ) + .unwrap(); assert!(s.hc_auth.is_none() && s.gas_prices.is_none()); let mut b = bundle_json(); @@ -1393,8 +1517,14 @@ mod tests { let b: RpcBlock = serde_json::from_value(json).unwrap(); assert_eq!(b.transactions.len(), 17); assert!(matches!(b.transactions[0].action, RpcAction::None)); - assert_eq!(b.transactions[0].bundle.as_ref().unwrap().nullifiers.len(), 4); - assert_eq!(b.transactions[0].bundle.as_ref().unwrap().commitments.len(), 4); + assert_eq!( + b.transactions[0].bundle.as_ref().unwrap().nullifiers.len(), + 4 + ); + assert_eq!( + b.transactions[0].bundle.as_ref().unwrap().commitments.len(), + 4 + ); assert!(b.transactions[1].bundle.is_none()); match &b.transactions[1].action { RpcAction::Mint { amount, .. } => assert_eq!(amount.0, "100000000000"), @@ -1435,14 +1565,21 @@ mod tests { other => panic!("{other:?}"), } match &b.transactions[8].action { - RpcAction::BridgeBurn { token, relayer_fee, .. } => { + RpcAction::BridgeBurn { + token, relayer_fee, .. + } => { assert_eq!(token, "cdcd"); assert_eq!(relayer_fee.0, "100"); } other => panic!("{other:?}"), } match &b.transactions[9].action { - RpcAction::RegisterToken { name, index, initial, .. } => { + RpcAction::RegisterToken { + name, + index, + initial, + .. + } => { assert_eq!(name, "zUSD"); assert_eq!(*index, 3); assert_eq!(initial.as_ref().unwrap().amount.0, "5000"); @@ -1450,14 +1587,26 @@ mod tests { other => panic!("{other:?}"), } match &b.transactions[10].action { - RpcAction::TokenMint { asset, amount, recipient, .. } => { + RpcAction::TokenMint { + asset, + amount, + recipient, + .. + } => { assert_eq!(*asset, 3); assert_eq!(amount.0, "700"); assert_eq!(recipient, "rand1abc"); } other => panic!("{other:?}"), } - assert!(matches!(&b.transactions[11].action, RpcAction::SetAuthority { asset: 3, nonce: 1, .. })); + assert!(matches!( + &b.transactions[11].action, + RpcAction::SetAuthority { + asset: 3, + nonce: 1, + .. + } + )); match &b.transactions[12].action { RpcAction::TokenBurn { asset, amount } => { assert_eq!(*asset, 3); @@ -1465,7 +1614,10 @@ mod tests { } other => panic!("{other:?}"), } - assert!(matches!(&b.transactions[13].action, RpcAction::PauseMints { nonce: 4 })); + assert!(matches!( + &b.transactions[13].action, + RpcAction::PauseMints { nonce: 4 } + )); assert!(b.transactions[13].bundle.is_none()); match &b.transactions[14].action { RpcAction::UnpauseMints { nonce, pq_signers } => { @@ -1475,7 +1627,12 @@ mod tests { other => panic!("{other:?}"), } match &b.transactions[15].action { - RpcAction::RegisterBridgedToken { name, chain, pq_signers, .. } => { + RpcAction::RegisterBridgedToken { + name, + chain, + pq_signers, + .. + } => { assert_eq!(name, "zUSD"); assert_eq!(*chain, 3); assert_eq!(pq_signers, &vec![1]); @@ -1483,7 +1640,12 @@ mod tests { other => panic!("{other:?}"), } match &b.transactions[16].action { - RpcAction::ListBacking { token_index, chain, pq_signers, .. } => { + RpcAction::ListBacking { + token_index, + chain, + pq_signers, + .. + } => { assert_eq!(*token_index, 3); assert_eq!(*chain, 4); assert_eq!(pq_signers, &vec![2]); @@ -1506,12 +1668,25 @@ mod tests { })) .unwrap(); match a { - RpcAction::BridgeAttest { amount, deposit_amount, fee_note, commitment, .. } => { + RpcAction::BridgeAttest { + amount, + deposit_amount, + fee_note, + commitment, + .. + } => { assert_eq!(amount.unwrap().0, "100000000"); assert_eq!(deposit_amount.unwrap().0, "99900000"); let f = fee_note.unwrap().to_core(); - assert_eq!((f.amount.as_str(), f.asset, f.commitment.as_str()), ("100000", 1, "47c3")); - assert_eq!(commitment.as_deref(), Some("d8f1"), "the net deposit's leaf"); + assert_eq!( + (f.amount.as_str(), f.asset, f.commitment.as_str()), + ("100000", 1, "47c3") + ); + assert_eq!( + commitment.as_deref(), + Some("d8f1"), + "the net deposit's leaf" + ); } other => panic!("{other:?}"), } @@ -1521,7 +1696,9 @@ mod tests { "fee_note": { "amount": "500", "asset": 1, "commitment": "ee", "r": "ff", "time": 9 } })) .unwrap(); - assert!(matches!(b, RpcAction::BridgeBurn { release_amount: Some(ref r), fee_note: Some(_), .. } if r.0 == "499500")); + assert!( + matches!(b, RpcAction::BridgeBurn { release_amount: Some(ref r), fee_note: Some(_), .. } if r.0 == "499500") + ); // A chain without the group (and an older node): no split. let b: RpcAction = serde_json::from_value(serde_json::json!({ "kind": "bridge_burn", "asset": 1, "amount": 400, "relayer_fee": 100, "to_chain": 5, "token": "cd", "to": "ab", @@ -1536,24 +1713,48 @@ mod tests { RpcAction::AdmitValidator { ref voters, .. } if voters.len() == 2 )); assert!(matches!( - k(serde_json::json!({ "kind": "slash_equivocation", "offender": "2nRd", "view": 1, - "first": { "hash": "aa", "height": 3 }, "second": { "hash": "bb", "height": 3 } })), + k( + serde_json::json!({ "kind": "slash_equivocation", "offender": "2nRd", "view": 1, + "first": { "hash": "aa", "height": 3 }, "second": { "hash": "bb", "height": 3 } }) + ), RpcAction::SlashEquivocation { view: 1, .. } )); assert!(matches!( - k(serde_json::json!({ "kind": "rotate_pq_guardians", "new_pq_guardians": ["k0", "k1"], "nonce": 7, "pq_signers": [1, 4] })), - RpcAction::RotatePqGuardians { v2: false, possession_signatures: None, nonce: 7, .. } + k( + serde_json::json!({ "kind": "rotate_pq_guardians", "new_pq_guardians": ["k0", "k1"], "nonce": 7, "pq_signers": [1, 4] }) + ), + RpcAction::RotatePqGuardians { + v2: false, + possession_signatures: None, + nonce: 7, + .. + } )); assert!(matches!( - k(serde_json::json!({ "kind": "rotate_pq_guardians_v2", "new_pq_guardians": ["k0", "k1"], "possession_signatures": 2, "nonce": 9, "pq_signers": [2] })), - RpcAction::RotatePqGuardians { v2: true, possession_signatures: Some(2), nonce: 9, .. } + k( + serde_json::json!({ "kind": "rotate_pq_guardians_v2", "new_pq_guardians": ["k0", "k1"], "possession_signatures": 2, "nonce": 9, "pq_signers": [2] }) + ), + RpcAction::RotatePqGuardians { + v2: true, + possession_signatures: Some(2), + nonce: 9, + .. + } )); assert!(matches!( - k(serde_json::json!({ "kind": "rotate_pause_key_v2", "new_pause_key": "k0", "nonce": 10, "pq_signers": [1] })), - RpcAction::RotatePauseKey { v2: true, nonce: 10, .. } + k( + serde_json::json!({ "kind": "rotate_pause_key_v2", "new_pause_key": "k0", "nonce": 10, "pq_signers": [1] }) + ), + RpcAction::RotatePauseKey { + v2: true, + nonce: 10, + .. + } )); assert!(matches!( - k(serde_json::json!({ "kind": "cancel_rotation", "rotation_kind": "pause_key", "nonce": 11 })), + k( + serde_json::json!({ "kind": "cancel_rotation", "rotation_kind": "pause_key", "nonce": 11 }) + ), RpcAction::CancelRotation { nonce: 11, .. } )); } @@ -1566,10 +1767,17 @@ mod tests { ) .unwrap(); assert!(l.testnet && l.admission_by_vote && l.slashing.is_none()); - assert_eq!((l.binding_domain, l.proof_window_blocks), (Some(1), Some(1024))); - assert_eq!((l.max_gas_price.as_deref(), l.max_byte_price.as_deref()), (Some("10000"), Some("80000"))); + assert_eq!( + (l.binding_domain, l.proof_window_blocks), + (Some(1), Some(1024)) + ); + assert_eq!( + (l.max_gas_price.as_deref(), l.max_byte_price.as_deref()), + (Some("10000"), Some("80000")) + ); assert_eq!(l.byte_load.as_deref(), Some("paying")); - let back: randscan_core::ChainLimits = serde_json::from_value(serde_json::to_value(&l).unwrap()).unwrap(); + let back: randscan_core::ChainLimits = + serde_json::from_value(serde_json::to_value(&l).unwrap()).unwrap(); assert_eq!(back, l); } @@ -1668,7 +1876,10 @@ mod tests { #[test] fn there_is_no_token_transfer_kind_a_transfer_is_none() { let json = r#"{"kind":"none"}"#; - assert!(matches!(serde_json::from_str::(json).unwrap(), RpcAction::None)); + assert!(matches!( + serde_json::from_str::(json).unwrap(), + RpcAction::None + )); // A node tag this build has never heard of (e.g. a future kind) is tolerated, not fatal. assert!(serde_json::from_str::(r#"{"kind":"token_transfer"}"#).is_ok()); }