Skip to content

Feat/attestor api#820

Open
Trantorian1 wants to merge 6 commits intousc-devfrom
feat/attestor_api
Open

Feat/attestor api#820
Trantorian1 wants to merge 6 commits intousc-devfrom
feat/attestor_api

Conversation

@Trantorian1
Copy link
Copy Markdown
Contributor

@Trantorian1 Trantorian1 commented Apr 14, 2026

Description of proposed changes

Rework to allow simpler attestor parametrization for #819

Features

@Trantorian1 Trantorian1 self-assigned this Apr 14, 2026
@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 14, 2026

PR Summary

Medium Risk
Touches core attestation production/validation and CC3 transaction submission paths while changing how keys are derived and passed around; mistakes could cause invalid signatures, mis-registered BLS keys, or failed submissions.

Overview
Refactors the attestor stack to centralize identity/secret handling in cc-client via new attestor::Attestor and secret::{Secret,RpcUrl} types, replacing the old CC3Signer and stream_legacy::secret implementations and adding Substrate // URI support.

Decouples signing identity from cc_client::Client creation (client becomes URL-only) and updates attestor runtime, validation, production, zombienet tooling, continuity builder, proof-gen server, and query CLI to pass an Attestor explicitly for transactions, VRF signing, P2P keys, and BLS registration. The attestation stream is also changed to emit a Permit and require an Attestor to generate/sign attestations, enabling stream reuse across identities.

Reviewed by Cursor Bugbot for commit e1bce3b. Bugbot is set up for automated code reviews on this repo. Configure here.

let pair_vrf = sp_core::sr25519::Pair::from_string(seed.expose_secret(), None)?;

let mut seed: [u8; 32] = secret.try_into()?;
let bls_key = bls_signatures::PrivateKey::new(seed);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLS key derivation uses different seed than before

High Severity

The BLS private key derivation changed incompatibly. Previously, PrivateKey::new was seeded with the mnemonic phrase string as UTF-8 bytes (via to_bls_seed_bytes()to_secret_uri_string().as_bytes()), which was a variable-length byte vector. Now it's seeded with 32 bytes from Secret::try_into::<[u8; 32]>(), which for mnemonics returns mnemonic.to_seed_normalized("")[..32] — a completely different value. This silently changes the BLS keypair for all existing attestors, breaking on-chain BLS registration and attestation signature verification.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit df7ca07. Configure here.

Comment thread common/cc-client/src/attestor.rs
}
}

impl zeroize::ZeroizeOnDrop for Secret {}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ZeroizeOnDrop marker won't zeroize on drop

Medium Severity

impl zeroize::ZeroizeOnDrop for Secret {} is only a marker trait implementation — it does not generate a Drop impl that calls zeroize(). Unlike the old code which used #[derive(ZeroizeOnDrop)] (which generates a proper Drop), this manual impl means sensitive mnemonic/seed data won't be cleared from memory when Secret is dropped, despite the type claiming to be ZeroizeOnDrop.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit df7ca07. Configure here.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 4 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Zombienet uses sudo nonce for attestor-signed transactions
    • Chill and unregister now fetch and use each attestor account’s own nonce before signing with that attestor key, with retries incrementing that local nonce.
  • ✅ Fixed: Secret Deserialize borrows str, fails with YAML
    • Secret deserialization now uses String::deserialize instead of &str, making it compatible with YAML deserializers that provide owned strings.

Create PR

Or push these changes by commenting:

@cursor push abf38c1b30
Preview (abf38c1b30)
diff --git a/attestor/attestor_zombienet/src/main.rs b/attestor/attestor_zombienet/src/main.rs
--- a/attestor/attestor_zombienet/src/main.rs
+++ b/attestor/attestor_zombienet/src/main.rs
@@ -454,12 +454,6 @@
 
     // ------------------------------------* Attestor chilling *-----------------------------------
 
-    let nonce = cc3
-        .get_account_nonce(&sudo)
-        .await
-        .context("Failed to get funding address nonce")?;
-    let nonce = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(nonce));
-
     tracing::info!("❄️ Chilling attestors");
 
     let blocks = cc3.api().blocks().subscribe_finalized().await.unwrap();
@@ -467,7 +461,6 @@
     let mut futures_chill = tokio::task::JoinSet::new();
     for (attestor, name, ..) in attestor_info.iter() {
         let cc3 = std::sync::Arc::clone(&cc3);
-        let nonce = std::sync::Arc::clone(&nonce);
 
         let name = name.clone();
         let account_id = attestor.account_id();
@@ -476,14 +469,17 @@
         let mut attempt = 0;
 
         futures_chill.spawn(async move {
-            let mut nonce_local = nonce.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
+            let mut nonce_local = cc3
+                .get_account_nonce(&attestor)
+                .await
+                .context("Failed to get attestor nonce")?;
             while let Err(err) = cc3.attestor_chill(&attestor, Some(nonce_local)).await {
                 attempt += 1;
                 if attempt >= MAX_ATTEMPTS {
                     anyhow::bail!("Failed to chill attestor {name} - {account_id}: {err}");
                 }
 
-                nonce_local = nonce.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
+                nonce_local = nonce_local.saturating_add(1);
             }
 
             tracing::debug!(nonce_local, "OK - chill");
@@ -517,7 +513,6 @@
     let mut futures_unregister = tokio::task::JoinSet::new();
     for (attestor, name, ..) in attestor_info.iter() {
         let cc3 = std::sync::Arc::clone(&cc3);
-        let nonce = std::sync::Arc::clone(&nonce);
 
         let name = name.clone();
         let account_id = attestor.account_id();
@@ -526,14 +521,17 @@
         let mut attempt = 0;
 
         futures_unregister.spawn(async move {
-            let mut nonce_local = nonce.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
+            let mut nonce_local = cc3
+                .get_account_nonce(&attestor)
+                .await
+                .context("Failed to get attestor nonce")?;
             while let Err(err) = cc3.attestor_unregister(&attestor, Some(nonce_local)).await {
                 attempt += 1;
                 if attempt >= MAX_ATTEMPTS {
                     anyhow::bail!("Failed to un-register attestor {name} - {account_id}: {err}");
                 }
 
-                nonce_local = nonce.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
+                nonce_local = nonce_local.saturating_add(1);
             }
 
             tracing::debug!(nonce_local, "OK - unregister");

diff --git a/common/cc-client/src/secret.rs b/common/cc-client/src/secret.rs
--- a/common/cc-client/src/secret.rs
+++ b/common/cc-client/src/secret.rs
@@ -66,7 +66,7 @@
     where
         D: serde::Deserializer<'de>,
     {
-        let s = <&str>::deserialize(deserializer)?;
+        let s = String::deserialize(deserializer)?;
         s.parse().map_err(serde::de::Error::custom)
     }
 }

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e1bce3b. Configure here.

.attestor_chill(args.chain_key, account_id.clone(), Some(nonce_local))
.await
{
while let Err(err) = cc3.attestor_chill(&attestor, Some(nonce_local)).await {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zombienet uses sudo nonce for attestor-signed transactions

High Severity

The nonce counter is initialized from cc3.get_account_nonce(&sudo) (the sudo/funding account's nonce), but attestor_chill and attestor_unregister now sign with the individual attestor.keypair_subxt. The nonce from the sudo account has no relation to each attestor's actual on-chain nonce, causing every chill and unregister transaction to fail with an invalid nonce error.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e1bce3b. Configure here.

let s = <&str>::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Secret Deserialize borrows str, fails with YAML

Medium Severity

The Deserialize impl uses <&str>::deserialize(deserializer) which requires the deserializer to support zero-copy borrowed strings. YAML deserializers (like serde_yaml) produce owned strings and call visit_str/visit_string, not visit_borrowed_str, causing deserialization to fail with "expected a borrowed string" when a secret is specified in a YAML config file. Using String::deserialize instead would work with all deserializers.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e1bce3b. Configure here.

@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 14, 2026

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: BLS key derivation uses different seed than before
    • Attestor::new now derives the BLS key from secret.private_key().as_bytes() again, restoring the prior secret-URI-byte derivation behavior.
  • ✅ Fixed: BLS pubkey serialization method changed from as_bytes
    • bls_pubkey() now uses bls_signatures::Serialize::as_bytes() and converts to [u8; 48], restoring the crate-defined serialization format used previously.
  • ✅ Fixed: ZeroizeOnDrop marker won't zeroize on drop
    • Secret now has an explicit Drop impl that calls zeroize, and zeroize clears each variant’s underlying sensitive data in place.

Create PR

Or push these changes by commenting:

@cursor push 6f6516fb47
Preview (6f6516fb47)
diff --git a/Cargo.lock b/Cargo.lock
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1572,7 +1572,6 @@
  "url",
  "usc-abi-encoding",
  "user",
- "zeroize",
 ]
 
 [[package]]
@@ -1606,7 +1605,6 @@
  "chrono",
  "clap",
  "eth",
- "rand 0.8.5",
  "subxt",
  "subxt-signer",
  "tokio",
@@ -2221,6 +2219,12 @@
 dependencies = [
  "anyhow",
  "attestor-primitives",
+ "bip39",
+ "bls-signatures",
+ "bls12_381",
+ "hex",
+ "libp2p 0.56.0",
+ "secrecy 0.10.3",
  "serde",
  "sp-core",
  "subxt",
@@ -2229,8 +2233,10 @@
  "thiserror 1.0.69",
  "tokio",
  "tracing",
+ "url",
  "usc-abi-encoding",
  "vrf",
+ "zeroize",
 ]
 
 [[package]]
@@ -14790,7 +14796,6 @@
 dependencies = [
  "attestor-primitives",
  "bip39",
- "bls-signatures",
  "builder",
  "cc-client",
  "clap",

diff --git a/Cargo.toml b/Cargo.toml
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -59,6 +59,7 @@
 async-trait = { version = "0.1.42" }
 axum = { version = "0.8" }
 bip32 = { version = "0.5.1", default-features = false, features = ["bip39"] }
+bip39 = { version = "2.2.0", features = ["rand", "serde", "zeroize"] }
 clap = { version = "4.5.3", features = ["derive", "env"] }
 derive_more = "0.99.17"
 env_logger = "0.11"

diff --git a/attestor/attestor/Cargo.toml b/attestor/attestor/Cargo.toml
--- a/attestor/attestor/Cargo.toml
+++ b/attestor/attestor/Cargo.toml
@@ -23,7 +23,7 @@
 
 # Creditcoin
 attestor-primitives = { workspace = true }
-bip39 = { version = "2.2.0", features = ["rand", "serde", "zeroize"] }
+bip39 = { workspace = true }
 bls-signatures = { workspace = true }
 builder = { workspace = true }
 cc-client = { workspace = true }
@@ -85,7 +85,6 @@
 parking_lot = { workspace = true }
 rand = { workspace = true, features = ["std_rng"] }
 user = { workspace = true }
-zeroize = { workspace = true }
 
 [dev-dependencies]
 assert_matches = { workspace = true }

diff --git a/attestor/attestor/src/lib.rs b/attestor/attestor/src/lib.rs
--- a/attestor/attestor/src/lib.rs
+++ b/attestor/attestor/src/lib.rs
@@ -22,6 +22,7 @@
 pub struct Config {
     name: String,
     chain_key: attestor_primitives::ChainKey,
+    secret: cc_client::secret::Secret,
 
     stream: stream_legacy::Config,
     attestation: attestation::Config,
@@ -50,23 +51,17 @@
     )]
     pub async fn run(self) -> Result<(), Error> {
         use anyhow::Context as _;
-        use bls_signatures::Serialize as _;
         use stream::util::ChainExt as _;
 
         // --------------------------------------* Identity *--------------------------------------
 
         let chain_key = self.config.chain_key;
+        let attestor = cc_client::attestor::Attestor::new(self.config.secret, chain_key)
+            .map_err(Error::InitError)?;
 
-        let secret_str = self.config.stream.secret.to_secret_uri_string();
-        let signer =
-            cc_client::signer::CC3Signer::new(secret_str.as_str()).map_err(Error::InitError)?;
-        let account_id = signer.account_id();
+        let account_id = attestor.account_id();
+        let peer_id = attestor.peer_id();
 
-        let mut seed = self.config.stream.secret.to_seed_bytes_32();
-        let keypair_p2p = libp2p::identity::Keypair::ed25519_from_bytes(&mut *seed)
-            .expect("Failed to create ed25519 keypair");
-        let peer_id = libp2p::PeerId::from_public_key(&keypair_p2p.public());
-
         tracing::info!(name = self.config.name, %account_id, chain_key, "🙋‍♀️ Starting attestor");
 
         let monitor = worker::CancellationMonitor::new();
@@ -85,12 +80,9 @@
             }
         }
 
-        let client_cc3 = cc_client::Client::new(
-            self.config.stream.url_cc3.as_ref().as_ref(),
-            secret_str.as_str(),
-        )
-        .await
-        .map_err(Error::InitError)?;
+        let client_cc3 = cc_client::Client::new(self.config.stream.url_cc3.as_ref().as_ref())
+            .await
+            .map_err(Error::InitError)?;
 
         let client_eth = eth::Client::new(self.config.stream.url_eth.as_ref().as_ref(), None)
             .await
@@ -159,27 +151,12 @@
 
         // ------------------------------------* Registration *------------------------------------
 
-        let bls_seed = self.config.stream.secret.to_bls_seed_bytes();
-        let bls_key = bls_signatures::PrivateKey::new(bls_seed.as_slice());
-
-        let bls_public_key_bytes = bls_key.public_key().as_bytes();
-        let bls_pubkey_hex = format!("0x{}", hex::encode(&bls_public_key_bytes));
         tracing::info!(
-            bls_public_key_hex = %bls_pubkey_hex,
+            bls_public_key = format!("0x{}", hex::encode(attestor.bls_pubkey())),
             "🔑 BLS public key (set this in fork genesis Attestors if needed)"
         );
 
-        // ------------------------------------* Start Attesting *------------------------------------
-
-        match register_bls(
-            chain_key,
-            &client_cc3,
-            &account_id,
-            &bls_key,
-            &bls_public_key_bytes,
-        )
-        .await
-        {
+        match register_bls(&client_cc3, &attestor).await {
             Ok(()) => {}
             Err(Interrupt::Stop) => {
                 tracing::info!("🔌 Received shutdown signal");
@@ -304,9 +281,7 @@
         let stream_tip = stream::eth::StreamTip::new(config).await;
 
         let config = stream::attestation::ConfigBuilder::new()
-            .with_signer(signer.clone())
             .with_chain_key(self.config.chain_key)
-            .with_bls_key(bls_key)
             .with_stream_roots(stream_roots.boxed_data())
             .with_stream_tip(stream_tip.boxed_data())
             .with_attestation_interval(interval_attestation)
@@ -373,7 +348,7 @@
         let config = worker::validation::ConfigBuilder::new()
             .with_stream_cc3(stream_cc3_validation)
             .with_cc3(client_cc3.clone())
-            .with_signer(signer)
+            .with_attestor(attestor.clone())
             .with_validation_receiver(receiver_validation)
             .with_validation_sender(sender_validation.clone())
             .with_api_calls(cc_client::Client::runtime_api())
@@ -391,7 +366,7 @@
         let config = self
             .config
             .p2p
-            .with_keypair(keypair_p2p)
+            .with_keypair(attestor.peer_keypair())
             .with_receiver_p2p(receiver_p2p)
             .with_sender_validation(sender_validation.clone())
             .with_chain_key(chain_key)
@@ -410,7 +385,7 @@
                 match wait_for_genesis(
                     genesis,
                     &client_eth,
-                    &account_id,
+                    &attestor,
                     &mut stream_cc3_genesis,
                     &mut stream_attestation,
                     &mut sender_validation,
@@ -436,6 +411,7 @@
         tracing::info!("⏳ [4/4] Starting attestation production worker");
 
         let config = worker::production::ConfigBuilder::new()
+            .with_attestor(attestor)
             .with_stream_attestation(stream_attestation)
             .with_stream_cc3(stream_cc3_production)
             .with_sender_p2p(sender_p2p)
@@ -503,8 +479,8 @@
 }
 
 async fn wait_for_endpoints(
-    url_eth: &stream_legacy::RpcSecret,
-    url_cc3: &stream_legacy::RpcSecret,
+    url_eth: &cc_client::secret::RpcUrl,
+    url_cc3: &cc_client::secret::RpcUrl,
 ) -> Result<(), Interrupt<Error>> {
     loop {
         tokio::select! {
@@ -551,42 +527,22 @@
 }
 
 async fn register_bls(
-    chain_key: attestor_primitives::ChainKey,
     client_cc3: &cc_client::Client,
-    account_id: &cc_client::AccountId32,
-    bls_key: &bls_signatures::PrivateKey,
-    bls_public_key_bytes: &[u8],
+    attestor: &cc_client::attestor::Attestor,
 ) -> Result<(), Interrupt<Error>> {
-    use anyhow::Context as _;
-    use bls_signatures::Serialize as _;
-
     let status = client_cc3
-        .get_attestor_status(chain_key)
+        .get_attestor_status(attestor)
         .await
         .map_interrupt(Error::RpcError)?;
 
     if status == Some(attestor_primitives::AttestorStatus::Idle) {
         tracing::info!(
-            attestor_id = %account_id,
+            attestor_id = %attestor.account_id(),
             "📝 Submitting attest() extrinsic to transition from Idle to Waiting"
         );
 
-        let bls_public_key = bls_public_key_bytes[..]
-            .try_into()
-            .context("BLS public key has unexpected length")
-            .map_interrupt(Error::InitError)?;
-
-        let proof_of_possession = bls_key.sign(bls_public_key).as_bytes()[..]
-            .try_into()
-            .context("BLS signature has unexpected length")
-            .map_interrupt(Error::InitError)?;
-
         tokio::select! {
-            res = client_cc3.start_attesting(
-                chain_key,
-                bls_public_key,
-                proof_of_possession,
-            ) => {
+            res = client_cc3.start_attesting(attestor) => {
                 res.map_interrupt(Error::RpcError)?;
             }
             _ = tokio::signal::ctrl_c() => {
@@ -595,12 +551,12 @@
         }
 
         tracing::info!(
-            attestor_id = %account_id,
+            attestor_id = %attestor.account_id(),
             "✅ Successfully submitted attest() - now Waiting for election"
         );
     } else {
         tracing::info!(
-            attestor_id = %account_id,
+            attestor_id = %attestor.account_id(),
             ?status,
             "ℹ️ Attestor status is already {:?}, skipping attest()", status
         );
@@ -680,7 +636,7 @@
 async fn wait_for_genesis(
     genesis: common::types::Height,
     client_eth: &eth::Client,
-    account_id: &cc_client::AccountId32,
+    attestor: &cc_client::attestor::Attestor,
     stream_cc3: &mut stream_legacy::cc3::StreamCC3,
     stream_attestation: &mut stream::attestation::StreamAttestation,
     sender_validation: &mut worker::validation::pool::AttestationPoolSender,
@@ -702,10 +658,11 @@
         hash: attestor_primitives::Digest::from(*block.hash()),
     };
 
-    let attestation_genesis = stream_attestation.generate_attestation_genesis(info);
+    let attestation_genesis = stream_attestation.generate_attestation_genesis(attestor, info);
 
     let height = attestation_genesis.header_number();
     let digest = attestation_genesis.digest();
+
     // No previous digest means we will log `0x000...000` as the previous digest
     let digest_prev = attestation_genesis
         .prev_digest()
@@ -754,7 +711,7 @@
             _ = interval.tick() => {
                 tracing::info!(
                     height,
-                    attestor_id = %account_id,
+                    attestor_id = %attestor.account_id(),
                     "⏲️  waiting on submission..."
                 );
             }

diff --git a/attestor/attestor/src/main.rs b/attestor/attestor/src/main.rs
--- a/attestor/attestor/src/main.rs
+++ b/attestor/attestor/src/main.rs
@@ -1,5 +1,4 @@
 use attestor::prelude::*;
-use std::str::FromStr;
 
 // -------------------------------------- [ Configuration ] ------------------------------------ //
 
@@ -11,14 +10,14 @@
 struct Config {
     name: String,
     logs: std::path::PathBuf,
-    secret: attestor::stream_legacy::AttestorSecret,
+    secret: cc_client::secret::Secret,
     chain_key: attestor_primitives::ChainKey,
     public_addr: Option<String>,
     api_port: u16,
     boot_nodes: Vec<libp2p::Multiaddr>,
     p2p_port: u16, // Defaults to 9000 if not specified
-    eth_url: attestor::stream_legacy::RpcSecret,
-    cc3_url: attestor::stream_legacy::RpcSecret,
+    eth_url: cc_client::secret::RpcUrl,
+    cc3_url: cc_client::secret::RpcUrl,
     pool_capacity: std::num::NonZeroUsize,
     start_height: Option<common::types::Height>,
     attestation_interval: Option<std::num::NonZero<common::types::Height>>,
@@ -48,7 +47,7 @@
     name: Option<String>,
     chain_key: Option<attestor_primitives::ChainKey>,
     /// BIP39 mnemonic or raw 32-byte seed as hex (e.g. 0x398f...)
-    secret: Option<String>,
+    secret: Option<cc_client::secret::Secret>,
     public_addr: Option<String>,
     #[serde(default = "default_logs")]
     logs: std::path::PathBuf,
@@ -149,7 +148,7 @@
                     )
                     .env("ATTESTOR_SECRET")
                     .required(false)
-                    .value_parser(clap::value_parser!(attestor::stream_legacy::AttestorSecret)),
+                    .value_parser(clap::value_parser!(cc_client::secret::Secret)),
             )
             .arg(
                 clap::arg!(--"public-addr" <PORT>)
@@ -329,15 +328,13 @@
                 .expect("Chain key is set either in config or by clap"),
         };
 
-        let secret = match matches.get_one::<attestor::stream_legacy::AttestorSecret>("secret") {
+        let secret = match matches.get_one::<cc_client::secret::Secret>("secret") {
             Some(secret) => secret.clone(),
-            None => match &config_file.attestor.secret {
-                Some(s) => attestor::stream_legacy::AttestorSecret::from_str(s)
-                    .map_err(|e| anyhow::anyhow!("invalid attestor secret in config file: {e}"))?,
-                None => attestor::stream_legacy::AttestorSecret::Mnemonic(
-                    bip39::Mnemonic::generate(12).expect("Failed to generate attestor secret"),
-                ),
-            },
+            None => config_file.attestor.secret.unwrap_or(
+                bip39::Mnemonic::generate(12)
+                    .expect("Failed to generate attestor secret")
+                    .into(),
+            ),
         };
 
         let api_port = matches
@@ -373,9 +370,9 @@
                 .expect("Eth url is set either in config or by clap"),
         };
         let eth_url = if expose_url {
-            attestor::stream_legacy::RpcSecret::new_exposed(eth_url)
+            cc_client::secret::RpcUrl::new_exposed(eth_url)
         } else {
-            attestor::stream_legacy::RpcSecret::new_opaque(eth_url)
+            cc_client::secret::RpcUrl::new_opaque(eth_url)
         };
 
         let cc3_url = match matches.get_one::<url::Url>("cc3-url") {
@@ -386,9 +383,9 @@
                 .expect("CC3 url is set either in config or by clap"),
         };
         let cc3_url = if expose_url {
-            attestor::stream_legacy::RpcSecret::new_exposed(cc3_url)
+            cc_client::secret::RpcUrl::new_exposed(cc3_url)
         } else {
-            attestor::stream_legacy::RpcSecret::new_opaque(cc3_url)
+            cc_client::secret::RpcUrl::new_opaque(cc3_url)
         };
 
         let pool_capacity = match matches.get_one::<std::num::NonZeroUsize>("pool-capacity") {
@@ -494,11 +491,11 @@
     let config = attestor::ConfigBuilder::new()
         .with_name(args.name)
         .with_chain_key(args.chain_key)
+        .with_secret(args.secret)
         .with_stream(
             attestor::stream_legacy::ConfigBuilder::new()
                 .with_url_eth(args.eth_url)
                 .with_url_cc3(args.cc3_url)
-                .with_secret(args.secret)
                 .build(),
         )
         .with_p2p(

diff --git a/attestor/attestor/src/stream_legacy/mod.rs b/attestor/attestor/src/stream_legacy/mod.rs
--- a/attestor/attestor/src/stream_legacy/mod.rs
+++ b/attestor/attestor/src/stream_legacy/mod.rs
@@ -1,29 +1,7 @@
-//! Data [`Stream`]s used to react to [source chain] and [execution chain] progress.
-//!
-//! # What is the difference between a channel and a stream?
-//!
-//! A channel is a passive transport mechanism — it does not schedule or delay emissions. A stream
-//! on the other hand has full control over the way in which it is paused/resumed, allowing for the
-//! implementation of much more complex and fine-grained waiting logic.
-//!
-//! Under the hood all asynchronous code in Rust ends up calling some low-level manual [`Future`]
-//! implementation. Streams work at that level to allow full control over the async execution model
-//! of your code. In contrast, channels are much higher-level constructs which do not allow for
-//! manual fine-tuning.
-//!
-//! [`Stream`]: futures::Stream
-//! [source chain]: attestation
-//! [execution chain]: cc3
-//! [`Future`]: std::future::Future
-
 pub mod cc3;
-pub mod secret;
 
-pub use secret::*;
-
 #[derive(Debug, builder::Builder)]
 pub struct Config {
-    pub(crate) url_eth: RpcSecret,
-    pub(crate) url_cc3: RpcSecret,
-    pub(crate) secret: AttestorSecret,
+    pub(crate) url_eth: cc_client::secret::RpcUrl,
+    pub(crate) url_cc3: cc_client::secret::RpcUrl,
 }

diff --git a/attestor/attestor/src/stream_legacy/secret.rs b/attestor/attestor/src/stream_legacy/secret.rs
deleted file mode 100644
--- a/attestor/attestor/src/stream_legacy/secret.rs
+++ /dev/null
@@ -1,177 +1,0 @@
-//! Attestor secret: either a BIP39 mnemonic or a raw 32-byte hex seed (e.g. `0x398f...`).
-
-use std::str::FromStr;
-use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
-
-/// Secret used for the attestor identity: BIP39 mnemonic or raw 32-byte seed as hex.
-/// Implements [`Zeroize`] and [`ZeroizeOnDrop`] so sensitive data is cleared on drop.
-/// [`Debug`] and [`Display`] are redacted so the secret is never logged or printed.
-#[derive(Clone, Zeroize, ZeroizeOnDrop)]
-pub enum AttestorSecret {
-    Mnemonic(bip39::Mnemonic),
-    RawSeed([u8; 32]),
-}
-
-impl AttestorSecret {
-    /// String suitable for Substrate `SecretUri::from_str` (mnemonic phrase or `0x` + 64 hex).
-    /// Returned in [`Zeroizing`] so the string is zeroized when dropped.
-    pub fn to_secret_uri_string(&self) -> Zeroizing<String> {
-        let s = match self {
-            AttestorSecret::Mnemonic(m) => m.to_string(),
-            AttestorSecret::RawSeed(bytes) => format!("0x{}", hex::encode(bytes)),
-        };
-        Zeroizing::new(s)
-    }
-
-    /// First 32 bytes of the seed (for P2P keypair: mnemonic seed or raw bytes).
-    /// Returned in [`Zeroizing`] so the bytes are zeroized when dropped.
-    pub fn to_seed_bytes_32(&self) -> Zeroizing<[u8; 32]> {
-        match self {
-            AttestorSecret::Mnemonic(m) => {
-                let full_seed = m.to_seed_normalized("");
-                let full_seed_zeroizing = Zeroizing::new({
-                    let mut arr = [0u8; 64];
-                    arr.copy_from_slice(full_seed.as_ref());
-                    arr
-                });
-                let mut out = [0u8; 32];
-                out.copy_from_slice(&full_seed_zeroizing[..32]);
-                Zeroizing::new(out)
-            }
-            AttestorSecret::RawSeed(bytes) => Zeroizing::new(*bytes),
-        }
-    }
-
-    /// Bytes used for BLS key derivation (same as secret URI string as bytes).
-    /// Returned in [`Zeroizing`] so the bytes are zeroized when dropped.
-    pub fn to_bls_seed_bytes(&self) -> Zeroizing<Vec<u8>> {
-        Zeroizing::new(self.to_secret_uri_string().as_bytes().to_vec())
-    }
-}
-
-impl FromStr for AttestorSecret {
-    type Err = anyhow::Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let s = s.trim();
-        if s.starts_with("0x") {
-            let hex = s.strip_prefix("0x").unwrap();
-            if hex.len() != 64 {
-                return Err(anyhow::anyhow!(
-                    "invalid hex seed: expected 0x followed by 64 hex digits, got {} characters",
-                    hex.len()
-                ));
-            }
-            if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
-                return Err(anyhow::anyhow!(
-                    "invalid hex seed: 0x prefix must be followed by 64 hex digits (0-9, a-f, A-F)"
-                ));
-            }
-            let mut bytes = [0u8; 32];
-            hex::decode_to_slice(hex, &mut bytes).map_err(anyhow::Error::msg)?;
-            return Ok(AttestorSecret::RawSeed(bytes));
-        }
-        bip39::Mnemonic::from_str(s)
-            .map(AttestorSecret::Mnemonic)
-            .map_err(|e| anyhow::anyhow!("invalid mnemonic or hex seed: {e}"))
-    }
-}
-
-impl std::fmt::Debug for AttestorSecret {
-    /// Redacted so {:?}, dbg!(), and Debug in tracing never expose the secret.
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            AttestorSecret::Mnemonic(_) => f.write_str("AttestorSecret(Mnemonic(***))"),
-            AttestorSecret::RawSeed(_) => f.write_str("AttestorSecret(RawSeed(***))"),
-        }
-    }
-}
-
-impl std::fmt::Display for AttestorSecret {
-    /// Redacted to avoid leaking the secret via e.g. `format!("{}", secret)` or logs.
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.write_str("AttestorSecret(***)")
-    }
-}
-
-/// Wrapper struct around [`Url`] which avoids leaking RPC api keys through logs.
-///
-/// [`Url`]: url::Url
-pub enum RpcSecret {
-    /// Hides the RPC url on calls to [`Debug`] or [`Display`].
-    ///
-    /// [`Debug`]: std::fmt::Debug
-    /// [`Display`]: std::fmt::Display
-    Opaque(url::Url),
-    /// Exposes the RPC url on calls to [`Debug`] or [`Display`].
-    ///
-    /// <div class="warning">
-    ///
-    /// Use this for testing purposes only! This option should not be used in environment where
-    /// logs are publicly accessible, such as Github actions or other CI.
-    ///
-    /// </div>
-    ///
-    /// [`Debug`]: std::fmt::Debug
-    /// [`Display`]: std::fmt::Display
-    Exposed(url::Url),
-}
-
-impl RpcSecret {
-    /// Creates a new masked [`RpcSecret`].
-    pub fn new_opaque(url: url::Url) -> Self {
-        Self::Opaque(url)
-    }
-
-    /// Creates a new [`RpcSecret`] **which exposes the underlying RPC url**.
-    pub fn new_exposed(url: url::Url) -> Self {
-        Self::Exposed(url)
-    }
-}
-
-impl From<RpcSecret> for url::Url {
-    fn from(value: RpcSecret) -> Self {
-        match value {
-            RpcSecret::Opaque(url) => url,
-            RpcSecret::Exposed(url) => url,
-        }
-    }
-}
-
-impl AsRef<url::Url> for RpcSecret {
-    fn as_ref(&self) -> &url::Url {
-        match self {
-            Self::Opaque(url) => url,
-            Self::Exposed(url) => url,
-        }
-    }
-}
-
-impl std::ops::Deref for RpcSecret {
-    type Target = url::Url;
-
-    fn deref(&self) -> &Self::Target {
-        match self {
-            Self::Opaque(url) => url,
-            Self::Exposed(url) => url,
-        }
-    }
-}
-
-impl std::fmt::Debug for RpcSecret {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            Self::Opaque(_) => f.debug_tuple("RpcSecret").field(&"***").finish(),
-            Self::Exposed(url) => f.debug_tuple("RpcSecret").field(url).finish(),
-        }
-    }
-}
-
-impl std::fmt::Display for RpcSecret {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            Self::Opaque(_) => write!(f, "***"),
-            Self::Exposed(url) => write!(f, "{url}"),
-        }
-    }
-}
\ No newline at end of file

diff --git a/attestor/attestor/src/worker/production/mod.rs b/attestor/attestor/src/worker/production/mod.rs
--- a/attestor/attestor/src/worker/production/mod.rs
+++ b/attestor/attestor/src/worker/production/mod.rs
@@ -78,7 +78,7 @@
 //! events. These events are then forwarded for further handling.
 //!
 //! [`Worker`]: crate::worker::Worker
-//! [attestation stream]: crate::stream::attestation
+//! [attestation stream]: stream::attestation::StreamAttestation
 //! [attestation pool]: crate::worker::validation::pool
 //! [`Attestation`]: crate::common::types::Attestation
 //! [p2p worker]: crate::worker::p2p
@@ -97,6 +97,8 @@
 /// attestor, such as its account id.
 #[derive(builder::Builder)]
 pub struct Config {
+    attestor: cc_client::attestor::Attestor,
+
     stream_attestation: stream::attestation::StreamAttestation,
     stream_cc3: crate::stream_legacy::cc3::StreamCC3,
 
@@ -114,6 +116,8 @@
 // ----------------------------------------- [ Worker ] ---------------------------------------- //
 
 pub(crate) struct WorkerAttestationProduction {
+    attestor: cc_client::attestor::Attestor,
+
     // CHAIN LISTENERS
     stream_attestation: stream::attestation::StreamAttestation,
     stream_cc3: crate::stream_legacy::cc3::StreamCC3,
@@ -138,6 +142,8 @@
 impl WorkerAttestationProduction {
     pub(crate) fn new(config: Config) -> anyhow::Result<Self> {
         Ok(Self {
+            attestor: config.attestor,
+
             stream_attestation: config.stream_attestation,
             stream_cc3: config.stream_cc3,
 
@@ -178,8 +184,8 @@
                 Some(events) = self.stream_cc3.next() => {
                     self.handle_event_cc3(events).await?;
                 }
-                Some(attestation) = self.stream_attestation.next(), if self.can_attest => {
-                    self.handle_event_attestation(attestation).await?;
+                Some(permit) = self.stream_attestation.next(), if self.can_attest => {
+                    self.handle_event_attestation(permit).await?;
                 }
             }
         }
@@ -193,10 +199,14 @@
 
     async fn handle_event_attestation(
         &mut self,
-        attestation: stream::attestation::Attestation,
+        permit: stream::attestation::Permit,
     ) -> Result<(), Interrupt<Error>> {
         let now = std::time::Instant::now();
 
+        let attestation = self
+            .stream_attestation
+            .generate_attestation(&self.attestor, permit);
+
         let height = attestation.header_number();
         let digest = attestation.digest();
         // No previous digest means we will log `0x000...000` as the previous digest

diff --git a/attestor/attestor/src/worker/validation/mod.rs b/attestor/attestor/src/worker/validation/mod.rs
--- a/attestor/attestor/src/worker/validation/mod.rs
+++ b/attestor/attestor/src/worker/validation/mod.rs
@@ -103,7 +103,7 @@
 pub struct Config {
     stream_cc3: crate::stream_legacy::cc3::StreamCC3,
     cc3: cc_client::Client,
-    signer: cc_client::signer::CC3Signer,
+    attestor: cc_client::attestor::Attestor,
 
     validation_sender: pool::AttestationPoolSender,
     validation_receiver: pool::AttestationPoolReceiver,
@@ -123,7 +123,7 @@
     cc3: cc_client::Client,
 
     // ATTESTATIONS
-    signer: cc_client::signer::CC3Signer,
+    attestor: cc_client::attestor::Attestor,
     watch_submission: future::OptionFuture<(AttestationSubmission, common::types::Height)>,
     validation_sender: pool::AttestationPoolSender,
     validation_receiver: pool::AttestationPoolReceiver,
@@ -143,7 +143,7 @@
             stream_cc3: config.stream_cc3,
             cc3: config.cc3,
 
-            signer: config.signer,
+            attestor: config.attestor,
             watch_submission: future::OptionFuture::default(),
             validation_receiver: config.validation_receiver,
             validation_sender: config.validation_sender,
@@ -922,12 +922,7 @@
             let vrf = loop {
                 match self
                     .cc3
-                    .sign_vrf_submission(
-                        attestation.attestation.chain_key,
-                        height,
-                        randomness,
-                        epoch_index,
-                    )
+                    .sign_vrf_submission(&self.attestor, height, randomness, epoch_index)
                     .await
                 {
                     Ok(vrf) => break vrf,
@@ -1085,17 +1080,11 @@
             .commit_attestation(attestation);
 
         let submit = loop {
-            match self
-                .cc3
-                .api()
-                .tx()
-                .sign_and_submit_then_watch_default(&call, self.signer.keypair())
-                .await
-            {
+            match self.cc3.submit(&self.attestor, &call).await {
                 Ok(submit) => break submit,
                 Err(err) => {
                     tracing::error!(height, ?err, "⛔ Failed to submit attestation");
-                    self.reconnect(Error::Subxt(err)).await?;
+                    self.reconnect(Error::Client(err)).await?;
                 }
             }
         };

diff --git a/attestor/attestor_zombienet/Cargo.toml b/attestor/attestor_zombienet/Cargo.toml
--- a/attestor/attestor_zombienet/Cargo.toml
+++ b/attestor/attestor_zombienet/Cargo.toml
@@ -12,7 +12,7 @@
 # Creditcoin
 attestor = { workspace = true }
 attestor-primitives = { workspace = true }
-bip39 = { version = "2.2.0", features = ["rand", "serde"] }
+bip39 = { version = "2.2.0", features = ["serde"] }
 cc-client = { workspace = true }
 eth = { workspace = true }
 subxt = { workspace = true }
@@ -35,4 +35,3 @@
 
 # Other
 chrono = "0.4.42"
-rand = { version = "0.8.5" }

diff --git a/attestor/attestor_zombienet/src/main.rs b/attestor/attestor_zombienet/src/main.rs
--- a/attestor/attestor_zombienet/src/main.rs
+++ b/attestor/attestor_zombienet/src/main.rs
@@ -89,7 +89,6 @@
     use anyhow::Context as _;
... diff truncated: showing 800 of 2633 lines

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants