Feat/attestor api#820
Conversation
PR SummaryMedium Risk Overview Decouples signing identity from 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); |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit df7ca07. Configure here.
| } | ||
| } | ||
|
|
||
| impl zeroize::ZeroizeOnDrop for Secret {} |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit df7ca07. Configure here.
df7ca07 to
e1bce3b
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 4 total unresolved issues (including 2 from previous reviews).
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::deserializeinstead of&str, making it compatible with YAML deserializers that provide owned strings.
- Secret deserialization now uses
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 { |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit e1bce3b. Configure here.
| let s = <&str>::deserialize(deserializer)?; | ||
| s.parse().map_err(serde::de::Error::custom) | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit e1bce3b. Configure here.
|
Bugbot Autofix prepared fixes for all 3 issues found in the latest run.
Or push these changes by commenting: 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: ð::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 linesThis Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard. |



Description of proposed changes
Rework to allow simpler attestor parametrization for #819
Features
Attestorinterface.