Introduce WifKey for private keys with network
What changed, and why it matters
This commit is a routine API refactor in the rust-bitcoin library. It splits the old PrivateKey type into two parts: a simpler PrivateKey that only stores the secret key and compression flag, and a new WifKey type that pairs a PrivateKey with a Bitcoin network for WIF import/export. The change removes the network field from PrivateKey and moves WIF-specific parsing, serialization, and formatting onto WifKey. There is no security fix here; it is a design cleanup to make the key types more general-purpose.
No security action required. Downstream users should update code that constructs PrivateKey with a network argument or relies on PrivateKey for WIF parsing/serialization; use WifKey instead. Review the release notes for migration guidance.
Security signals we found
No security-relevant signal: this is an API refactor separating network metadata from the core private key type.
WIF parsing/serialization behavior is preserved; only the owning type changes.
No new validation, no bounds/secret-handling changes, no deprecation of unsafe APIs.
Evidence from the diff
The patch refactors the key module (bitcoin/src/crypto/key.rs) by introducing WifKey { private_key: PrivateKey, network_kind: NetworkKind } and removing the network field from PrivateKey. Constructors such as PrivateKey::from_secp, from_secp_uncompressed, from_secret_bytes, and generate no longer take a network argument. WIF logic (fmt_wif, to_wif, from_wif, FromStr, serde Serialize/Deserialize) is moved from PrivateKey to WifKey. Call sites in bip32.rs, psbt/mod.rs, tests, and the embedded example are updated accordingly. The change is purely structural and does not alter cryptographic behavior, validation, or WIF encoding rules.
Changed components
bitcoin/src/crypto/key.rsbitcoin/src/bip32.rsbitcoin/src/psbt/mod.rsbitcoin/src/lib.rsbitcoin/embedded/src/main.rsbitcoin/tests/bip_174.rsbitcoin/tests/psbt-sign-taproot.rsbitcoin/tests/serde.rsInspect captured patch +91 / −97
diff --git a/bitcoin/embedded/src/main.rs b/bitcoin/embedded/src/main.rs
index f27a1127..ae884d5e 100644
--- a/bitcoin/embedded/src/main.rs
+++ b/bitcoin/embedded/src/main.rs
@@ -11,7 +11,7 @@ use core::panic::PanicInfo;
use alloc_cortex_m::CortexMHeap;
// use panic_halt as _;
-use bitcoin::{Address, Network, PrivateKey};
+use bitcoin::{Address, Network, WifKey};
use cortex_m_rt::entry;
use cortex_m_semihosting::{debug, hprintln};
@@ -29,11 +29,11 @@ fn main() -> ! {
// Load a private key
let raw = "L1HKVVLHXiUhecWnwFYF6L3shkf1E12HUmuZTESvBXUdx3yqVP1D";
- let pk = PrivateKey::from_wif(raw).unwrap();
- hprintln!("Seed WIF: {}", pk.to_wif()).unwrap();
+ let wk = WifKey::from_wif(raw).unwrap();
+ hprintln!("Seed WIF: {}", wk.to_wif()).unwrap();
// Derive address
- let pubkey = pk.public_key().try_into().unwrap();
+ let pubkey = wk.private_key.public_key().try_into().unwrap();
let address = Address::p2wpkh(pubkey, Network::Bitcoin);
hprintln!("Address: {}", address).unwrap();
diff --git a/bitcoin/src/bip32.rs b/bitcoin/src/bip32.rs
index d0b501b3..f2e1c516 100644
--- a/bitcoin/src/bip32.rs
+++ b/bitcoin/src/bip32.rs
@@ -736,7 +736,7 @@ impl Xpriv {
/// Constructs a new ECDSA compressed private key matching internal secret key representation.
pub fn to_private_key(self) -> PrivateKey {
- PrivateKey::from_secp(self.private_key, self.network)
+ PrivateKey::from_secp(self.private_key)
}
/// Constructs a new extended public key from this extended private key.
diff --git a/bitcoin/src/crypto/key.rs b/bitcoin/src/crypto/key.rs
index 372bc483..72315d63 100644
--- a/bitcoin/src/crypto/key.rs
+++ b/bitcoin/src/crypto/key.rs
@@ -40,8 +40,6 @@ pub use secp256k1::rand;
mod encapsulate {
use secp256k1::Parity;
- use crate::network::NetworkKind;
-
/// A Bitcoin Schnorr X-only public key used for BIP-0340 signatures.
///
/// This type also holds the parity of the full public key.
@@ -146,26 +144,20 @@ mod encapsulate {
pub struct PrivateKey {
/// Whether this private key should be serialized as compressed.
compressed: bool,
- /// The network kind on which this key should be used.
- network: NetworkKind,
/// The actual ECDSA key.
inner: secp256k1::SecretKey,
}
impl PrivateKey {
- /// Constructs a new compressed ECDSA private key from the provided secp256k1 private
- /// key and the specified network.
- pub fn from_secp(key: secp256k1::SecretKey, network: impl Into<NetworkKind>) -> Self {
- Self { compressed: true, network: network.into(), inner: key }
+ /// Constructs a new compressed ECDSA private key from the provided secp256k1 private key.
+ pub fn from_secp(key: secp256k1::SecretKey) -> Self {
+ Self { compressed: true, inner: key }
}
/// Constructs a new uncompressed (legacy) ECDSA private key from the provided secp256k1
- /// private key and the specified network.
- pub fn from_secp_uncompressed(
- key: secp256k1::SecretKey,
- network: impl Into<NetworkKind>,
- ) -> Self {
- Self { compressed: false, network: network.into(), inner: key }
+ /// private key.
+ pub fn from_secp_uncompressed(key: secp256k1::SecretKey) -> Self {
+ Self { compressed: false, inner: key }
}
/// Returns a reference to the inner secp256k1 secret key.
@@ -175,10 +167,6 @@ mod encapsulate {
/// Returns whether this private key should be serialized as compressed.
#[inline]
pub fn compressed(&self) -> bool { self.compressed }
-
- /// Returns the [`NetworkKind`] of this key.
- #[inline]
- pub fn network(&self) -> NetworkKind { self.network }
}
/// Tweaked BIP-0340 X-coord-only public key.
@@ -911,9 +899,9 @@ impl PrivateKey {
/// Constructs a new compressed ECDSA private key using the secp256k1 algorithm and
/// a secure random number generator.
#[cfg(all(feature = "rand", feature = "std"))]
- pub fn generate(network: impl Into<NetworkKind>) -> Self {
+ pub fn generate() -> Self {
let secret_key = secp256k1::SecretKey::new(&mut rand::rng());
- Self::from_secp(secret_key, network.into())
+ Self::from_secp(secret_key)
}
/// Constructs a new public key from this private key.
@@ -939,11 +927,8 @@ impl PrivateKey {
///
/// Errors when the secret key is invalid: when it is all-zeros or would exceed
/// the curve order when interpreted as a big-endian unsigned integer.
- pub fn from_secret_bytes(
- data: [u8; 32],
- network: impl Into<NetworkKind>,
- ) -> Result<Self, secp256k1::Error> {
- Ok(Self::from_secp(secp256k1::SecretKey::from_secret_bytes(data)?, network))
+ pub fn from_secret_bytes(data: [u8; 32]) -> Result<Self, secp256k1::Error> {
+ Ok(Self::from_secp(secp256k1::SecretKey::from_secret_bytes(data)?))
}
/// Deserializes a private key from a slice.
@@ -957,10 +942,40 @@ impl PrivateKey {
#[deprecated(since = "TBD", note = "use from_secret_bytes instead")]
pub fn from_slice(
data: &[u8],
- network: impl Into<NetworkKind>,
+ _network: impl Into<NetworkKind>,
) -> Result<Self, secp256k1::Error> {
let array = data.try_into().map_err(|_| secp256k1::Error::InvalidSecretKey)?;
- Self::from_secret_bytes(array, network)
+ Self::from_secret_bytes(array)
+ }
+
+ /// Returns a new private key with the negated secret value.
+ ///
+ /// The resulting key corresponds to the same x-only public key (identical x-coordinate)
+ /// but with the opposite y-coordinate parity. This is useful for ensuring compatibility
+ /// with specific public key formats and BIP-0340 requirements.
+ #[inline]
+ pub fn negate(&self) -> Self {
+ match self.compressed() {
+ true => Self::from_secp(self.as_inner().negate()),
+ false => Self::from_secp_uncompressed(self.as_inner().negate()),
+ }
+ }
+}
+
+/// A Bitcoin ECDSA private key with known network for WIF.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct WifKey {
+ /// The actual key
+ pub private_key: PrivateKey,
+ /// The network kind on which this key should be used.
+ pub network_kind: NetworkKind,
+}
+
+impl WifKey {
+ /// Constructs a new WIF private key from the provided [`PrivateKey`] and the
+ /// specified network.
+ pub fn new(key: PrivateKey, network: impl Into<NetworkKind>) -> Self {
+ Self { network_kind: network.into(), private_key: key }
}
/// Formats the private key to WIF format.
@@ -971,10 +986,10 @@ impl PrivateKey {
#[rustfmt::skip]
pub fn fmt_wif(&self, fmt: &mut dyn fmt::Write) -> fmt::Result {
let mut ret = [0; 34];
- ret[0] = if self.network().is_mainnet() { 128 } else { 239 };
+ ret[0] = if self.network_kind.is_mainnet() { 128 } else { 239 };
- ret[1..33].copy_from_slice(&self.as_inner()[..]);
- let privkey = if self.compressed() {
+ ret[1..33].copy_from_slice(&self.private_key.as_inner()[..]);
+ let privkey = if self.private_key.compressed() {
ret[33] = 1;
base58::encode_check(&ret[..])
} else {
@@ -985,7 +1000,7 @@ impl PrivateKey {
/// Gets the WIF encoding of this private key.
#[allow(clippy::missing_panics_doc)]
- pub fn to_wif(self) -> String {
+ pub fn to_wif(&self) -> String {
let mut buf = String::new();
let _ = self.fmt_wif(&mut buf);
buf.shrink_to_fit();
@@ -1027,50 +1042,36 @@ impl PrivateKey {
}
};
- Ok(match compressed {
- true => Self::from_secp(secp256k1::SecretKey::from_secret_bytes(*key)?, network),
- false => Self::from_secp_uncompressed(
- secp256k1::SecretKey::from_secret_bytes(*key)?,
- network,
- ),
- })
- }
-
- /// Returns a new private key with the negated secret value.
- ///
- /// The resulting key corresponds to the same x-only public key (identical x-coordinate)
- /// but with the opposite y-coordinate parity. This is useful for ensuring compatibility
- /// with specific public key formats and BIP-0340 requirements.
- #[inline]
- pub fn negate(&self) -> Self {
- match self.compressed() {
- true => Self::from_secp(self.as_inner().negate(), self.network()),
- false => Self::from_secp_uncompressed(self.as_inner().negate(), self.network()),
- }
+ let sec_key = secp256k1::SecretKey::from_secret_bytes(*key)?;
+ let priv_key = match compressed {
+ true => PrivateKey::from_secp(sec_key),
+ false => PrivateKey::from_secp_uncompressed(sec_key),
+ };
+ Ok(Self::new(priv_key, network))
}
}
-// [`PrivateKey`] intentionally has a `FromStr` without a reciprocal `Display`.
+// [`WifKey`] intentionally has a `FromStr` without a reciprocal `Display`.
// Parsing from a WIF string should be convenient, printing secret data should not.
-impl FromStr for PrivateKey {
+impl FromStr for WifKey {
type Err = FromWifError;
fn from_str(s: &str) -> Result<Self, FromWifError> { Self::from_wif(s) }
}
#[cfg(feature = "serde")]
-impl serde::Serialize for PrivateKey {
+impl serde::Serialize for WifKey {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&self.to_wif())
}
}
#[cfg(feature = "serde")]
-impl<'de> serde::Deserialize<'de> for PrivateKey {
+impl<'de> serde::Deserialize<'de> for WifKey {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
struct WifVisitor;
impl serde::de::Visitor<'_> for WifVisitor {
- type Value = PrivateKey;
+ type Value = WifKey;
fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
formatter.write_str("an ASCII WIF string")
@@ -1081,7 +1082,7 @@ impl<'de> serde::Deserialize<'de> for PrivateKey {
E: serde::de::Error,
{
if let Ok(s) = core::str::from_utf8(v) {
- s.parse::<PrivateKey>().map_err(E::custom)
+ s.parse::<WifKey>().map_err(E::custom)
} else {
Err(E::invalid_value(::serde::de::Unexpected::Bytes(v), &self))
}
@@ -1091,7 +1092,7 @@ impl<'de> serde::Deserialize<'de> for PrivateKey {
where
E: serde::de::Error,
{
- v.parse::<PrivateKey>().map_err(E::custom)
+ v.parse::<WifKey>().map_err(E::custom)
}
}
@@ -1760,7 +1761,7 @@ mod tests {
#[test]
fn key_derivation() {
// mainnet compressed WIF with invalid compression flag.
- let sk = PrivateKey::from_wif("L2x4uC2YgfFWZm9tF4pjDnVR6nJkheizFhEr2KvDNnTEmEqVzPJY");
+ let sk = WifKey::from_wif("L2x4uC2YgfFWZm9tF4pjDnVR6nJkheizFhEr2KvDNnTEmEqVzPJY");
assert!(matches!(
sk,
Err(FromWifError::InvalidWifCompressionFlag(InvalidWifCompressionFlagError {
@@ -1770,33 +1771,33 @@ mod tests {
// testnet compressed
let sk =
- PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy").unwrap();
- assert_eq!(sk.network(), NetworkKind::Test);
- assert!(sk.compressed());
+ WifKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy").unwrap();
+ assert_eq!(sk.network_kind, NetworkKind::Test);
+ assert!(sk.private_key.compressed());
assert_eq!(&sk.to_wif(), "cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy");
- let pk = Address::p2pkh(sk.public_key(), sk.network());
+ let pk = Address::p2pkh(sk.private_key.public_key(), sk.network_kind);
assert_eq!(&pk.to_string(), "mqwpxxvfv3QbM8PU8uBx2jaNt9btQqvQNx");
// test string conversion
assert_eq!(&sk.to_wif(), "cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy");
let sk_str =
- "cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy".parse::<PrivateKey>().unwrap();
+ "cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy".parse::<WifKey>().unwrap();
assert_eq!(&sk.to_wif(), &sk_str.to_wif());
// mainnet uncompressed
let sk =
- PrivateKey::from_wif("5JYkZjmN7PVMjJUfJWfRFwtuXTGB439XV6faajeHPAM9Z2PT2R3").unwrap();
- assert_eq!(sk.network(), NetworkKind::Main);
- assert!(!sk.compressed());
+ WifKey::from_wif("5JYkZjmN7PVMjJUfJWfRFwtuXTGB439XV6faajeHPAM9Z2PT2R3").unwrap();
+ assert_eq!(sk.network_kind, NetworkKind::Main);
+ assert!(!sk.private_key.compressed());
assert_eq!(&sk.to_wif(), "5JYkZjmN7PVMjJUfJWfRFwtuXTGB439XV6faajeHPAM9Z2PT2R3");
- let mut pk = sk.public_key();
+ let mut pk = sk.private_key.public_key();
assert!(!pk.compressed());
assert_eq!(&pk.to_string(), "042e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af191923a2964c177f5b5923ae500fca49e99492d534aa3759d6b25a8bc971b133");
assert_eq!(pk, "042e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af191923a2964c177f5b5923ae500fca49e99492d534aa3759d6b25a8bc971b133"
.parse::<PublicKey>().unwrap());
- let addr = Address::p2pkh(pk, sk.network());
+ let addr = Address::p2pkh(pk, sk.network_kind);
assert_eq!(&addr.to_string(), "1GhQvF6dL8xa6wBxLnWmHcQsurx9RxiMc8");
pk = PublicKey::from_secp(pk.to_inner());
assert_eq!(
@@ -1868,11 +1869,11 @@ mod tests {
0xe9, 0x71, 0xd8, 0x6b, 0x5e, 0x61, 0x87, 0x5d,
];
- let sk = KEY_WIF.parse::<PrivateKey>().unwrap();
- let pk = PublicKey::from_private_key(sk);
+ let wk = KEY_WIF.parse::<WifKey>().unwrap();
+ let pk = PublicKey::from_private_key(wk.private_key);
let pk_u = PublicKey::from_secp_uncompressed(pk.to_inner());
- assert_tokens(&sk, &[Token::BorrowedStr(KEY_WIF)]);
+ assert_tokens(&wk, &[Token::BorrowedStr(KEY_WIF)]);
assert_tokens(&pk.compact(), &[Token::BorrowedBytes(&PK_BYTES[..])]);
assert_tokens(&pk.readable(), &[Token::BorrowedStr(PK_STR)]);
assert_tokens(&pk_u.compact(), &[Token::BorrowedBytes(&PK_BYTES_U[..])]);
@@ -2185,7 +2186,7 @@ mod tests {
"1ede31b0e7e47c2afc65ffd158b1b1b9d3b752bba8fd117dc8b9e944a390e8d9",
)
.unwrap();
- let sk = PrivateKey::from_secret_bytes(bytes, NetworkKind::Test).unwrap();
+ let sk = PrivateKey::from_secret_bytes(bytes).unwrap();
Keypair::from_secret_key(sk.as_inner())
};
diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs
index 491142ec..f182f1fb 100644
--- a/bitcoin/src/lib.rs
+++ b/bitcoin/src/lib.rs
@@ -185,7 +185,7 @@ pub use crate::{
address::{Address, AddressType, KnownHrp},
bip32::XKeyIdentifier,
crypto::ecdsa,
- crypto::key::{self, CompressedPublicKey, Keypair, PrivateKey, PublicKey, XOnlyPublicKey},
+ crypto::key::{self, CompressedPublicKey, Keypair, PrivateKey, PublicKey, WifKey, XOnlyPublicKey},
crypto::sighash::{self, LegacySighash, SegwitV0Sighash, TapSighash, TapSighashTag},
network::params::{self, Params},
network::{Network, NetworkKind, TestnetVersion},
diff --git a/bitcoin/src/psbt/mod.rs b/bitcoin/src/psbt/mod.rs
index a54efea8..b3716aaf 100644
--- a/bitcoin/src/psbt/mod.rs
+++ b/bitcoin/src/psbt/mod.rs
@@ -2353,7 +2353,7 @@ mod tests {
use secp256k1::rand;
let sk = SecretKey::new(&mut rand::rng());
- let priv_key = PrivateKey::from_secp(sk, NetworkKind::Test);
+ let priv_key = PrivateKey::from_secp(sk);
let pk = PublicKey::from_private_key(priv_key);
(priv_key, pk)
diff --git a/bitcoin/tests/bip_174.rs b/bitcoin/tests/bip_174.rs
index 2ed96275..8781d574 100644
--- a/bitcoin/tests/bip_174.rs
+++ b/bitcoin/tests/bip_174.rs
@@ -12,7 +12,7 @@ use bitcoin::psbt::{Psbt, PsbtSighashType};
use bitcoin::script::{PushBytes, ScriptBuf};
use bitcoin::{
absolute, script, transaction, NetworkKind, OutPoint, PrivateKey, PublicKey, ScriptPubKeyBuf,
- ScriptSigBuf, Sequence, Transaction, TxIn, TxOut, Witness,
+ ScriptSigBuf, Sequence, Transaction, TxIn, TxOut, Witness, WifKey,
};
#[track_caller]
@@ -119,8 +119,8 @@ fn build_extended_private_key() -> Xpriv {
let xpriv = extended_private_key.parse::<Xpriv>().unwrap();
- let sk = PrivateKey::from_wif(seed).unwrap();
- let seeded = Xpriv::new_master(NetworkKind::Test, &sk.as_inner().to_secret_bytes());
+ let sk = WifKey::from_wif(seed).unwrap();
+ let seeded = Xpriv::new_master(NetworkKind::Test, &sk.private_key.to_bytes());
assert_eq!(xpriv, seeded);
xpriv
@@ -309,13 +309,13 @@ fn parse_and_verify_keys(
) -> BTreeMap<PublicKey, PrivateKey> {
let mut key_map = BTreeMap::new();
for (secret_key, derivation_path) in sk_path.iter() {
- let wif_priv = PrivateKey::from_wif(secret_key).expect("failed to parse key");
+ let wif_priv = WifKey::from_wif(secret_key).expect("failed to parse key");
let path =
derivation_path.into_derivation_path().expect("failed to convert derivation path");
let derived_priv =
ext_priv.derive_xpriv(&path).expect("derivation path too long").to_private_key();
- assert_eq!(wif_priv, derived_priv);
+ assert_eq!(wif_priv.private_key, derived_priv);
let derived_pub = derived_priv.public_key();
key_map.insert(derived_pub, derived_priv);
}
diff --git a/bitcoin/tests/psbt-sign-taproot.rs b/bitcoin/tests/psbt-sign-taproot.rs
index 06e6f34c..89112e9e 100644
--- a/bitcoin/tests/psbt-sign-taproot.rs
+++ b/bitcoin/tests/psbt-sign-taproot.rs
@@ -84,10 +84,7 @@ fn psbt_sign_taproot() {
//
let keystore = Keystore {
mfp: mfp.parse::<Fingerprint>().unwrap(),
- sk: PrivateKey::from_secp(
- kp.to_secret_key(),
- Network::Testnet(bitcoin::TestnetVersion::V3),
- ),
+ sk: PrivateKey::from_secp(kp.to_secret_key()),
};
let _ = psbt_key_path_spend.sign(&keystore);
@@ -117,10 +114,7 @@ fn psbt_sign_taproot() {
let keystore = Keystore {
mfp: mfp.parse::<Fingerprint>().unwrap(),
- sk: PrivateKey::from_secp(
- kp.to_secret_key(),
- Network::Testnet(bitcoin::TestnetVersion::V3),
- ),
+ sk: PrivateKey::from_secp(kp.to_secret_key()),
};
//
diff --git a/bitcoin/tests/serde.rs b/bitcoin/tests/serde.rs
index 79a610ff..d2244318 100644
--- a/bitcoin/tests/serde.rs
+++ b/bitcoin/tests/serde.rs
@@ -31,9 +31,8 @@ use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
use bitcoin::taproot::{self, ControlBlock, LeafVersion, TapTree, TaprootBuilder};
use bitcoin::witness::Witness;
use bitcoin::{
- ecdsa, transaction, Address, Amount, NetworkKind, OutPoint, PrivateKey, PublicKey,
- ScriptPubKeyBuf, ScriptSigBuf, Sequence, TapScriptBuf, Target, Transaction, TxIn, TxOut, Txid,
- Work,
+ ecdsa, transaction, Address, Amount, NetworkKind, OutPoint, PublicKey, ScriptPubKeyBuf,
+ ScriptSigBuf, Sequence, TapScriptBuf, Target, Transaction, TxIn, TxOut, Txid, Work, WifKey,
};
#[test]
@@ -174,7 +173,7 @@ fn serde_regression_child_number() {
#[test]
fn serde_regression_private_key() {
- let sk = PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy").unwrap();
+ let sk = WifKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy").unwrap();
let got = serialize(&sk).unwrap();
let want = include_bytes!("data/serde/private_key_bincode") as &[_];
Why this scored 17/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.