Skip to main content

host_vrf/
lib.rs

1//! Bandersnatch ring-VRF provider trait.
2//!
3//! Defines the `VrfProvider` interface for ring-VRF operations used in
4//! Polkadot lite-person attestation. The trait carries no cryptographic
5//! dependencies — implementations live in separate backend crates
6//! (e.g., `host-vrf-native` for arkworks, or platform-specific bindings).
7//!
8//! The attestation flow itself (extrinsic construction, People chain
9//! submission) is out of scope for this crate. See the backlog for
10//! tracking.
11
12use thiserror::Error;
13
14/// Errors from VRF operations.
15#[derive(Debug, Clone, PartialEq, Eq, Error)]
16#[non_exhaustive]
17pub enum VrfError {
18    #[error("invalid entropy: {0}")]
19    InvalidEntropy(String),
20    #[error("proof creation failed: {0}")]
21    ProofCreationFailed(String),
22    #[error("signing failed: {0}")]
23    SigningFailed(String),
24    #[error("alias derivation failed: {0}")]
25    AliasFailed(String),
26}
27
28/// Provider for Bandersnatch ring-VRF operations.
29///
30/// Constructed with entropy baked in — the secret never crosses this
31/// interface after construction. This matches the `chat_sign_key()`
32/// pattern in `host-wallet` where key material stays inside the provider.
33///
34/// Implementations:
35/// - `host-vrf-native`: Rust-native via the `verifiable` crate (arkworks)
36/// - Platform bindings: `verifiable-swift` (iOS), JNI (Android), `verifiablejs` (web)
37pub trait VrfProvider: Send + Sync {
38    /// Derive the Bandersnatch public member key.
39    /// Returns the 32-byte compressed curve point.
40    fn derive_member_key(&self) -> Result<[u8; 32], VrfError>;
41
42    /// Sign a message with the Bandersnatch key (proof of ownership).
43    /// Returns the signature bytes (variable length, implementation-dependent).
44    fn sign(&self, message: &[u8]) -> Result<Vec<u8>, VrfError>;
45
46    /// Create a ring-VRF proof proving membership in a set without
47    /// revealing which member you are.
48    ///
49    /// - `members`: the ring of public member keys (each 32 bytes)
50    /// - `context`: 32-byte context identifier (e.g., `CONTEXT_IDENTITY`)
51    /// - `message`: the message to prove against
52    ///
53    /// Returns the proof bytes. CPU-intensive — callers should offload
54    /// to a thread pool (e.g., `tokio::task::spawn_blocking`) before calling.
55    fn create_proof(
56        &self,
57        members: &[[u8; 32]],
58        context: &[u8; 32],
59        message: &[u8],
60    ) -> Result<Vec<u8>, VrfError>;
61
62    /// Derive a context-specific alias (pseudonymous identity).
63    /// The same key produces different aliases in different contexts,
64    /// enabling unlinkable per-context identities.
65    fn alias_in_context(&self, context: &[u8; 32]) -> Result<[u8; 32], VrfError>;
66}
67
68// Known Polkadot context identifiers (32 bytes each, space-padded).
69// Verified against Android JNI binding constants in KnownBandersnatchContext.kt.
70pub const CONTEXT_MOB_RULE: &[u8; 32] = b"pop:polkadot.network/mob-rule   ";
71pub const CONTEXT_IDENTITY: &[u8; 32] = b"pop:polkadot.network/identity   ";
72pub const CONTEXT_SCORE: &[u8; 32] = b"pop:polkadot.network/score      ";
73pub const CONTEXT_RESOURCES: &[u8; 32] = b"pop:polkadot.network/resources  ";
74// "priv-vouchr" is intentionally truncated (not "privacy-voucher") to fit 32 bytes
75// without padding. Matches KnownBandersnatchContext.kt byte-for-byte.
76pub const CONTEXT_PRIVACY_VOUCHER: &[u8; 32] = b"pop:polkadot.network/priv-vouchr";
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    struct OkVrf;
83    impl VrfProvider for OkVrf {
84        fn derive_member_key(&self) -> Result<[u8; 32], VrfError> {
85            Ok([0xAA; 32])
86        }
87        fn sign(&self, _msg: &[u8]) -> Result<Vec<u8>, VrfError> {
88            Ok(vec![0u8; 64])
89        }
90        fn create_proof(
91            &self,
92            _m: &[[u8; 32]],
93            _c: &[u8; 32],
94            _msg: &[u8],
95        ) -> Result<Vec<u8>, VrfError> {
96            Ok(vec![0u8; 96])
97        }
98        fn alias_in_context(&self, _c: &[u8; 32]) -> Result<[u8; 32], VrfError> {
99            Ok([0xBB; 32])
100        }
101    }
102
103    struct FailVrf;
104    impl VrfProvider for FailVrf {
105        fn derive_member_key(&self) -> Result<[u8; 32], VrfError> {
106            Err(VrfError::InvalidEntropy("zeroed".into()))
107        }
108        fn sign(&self, _msg: &[u8]) -> Result<Vec<u8>, VrfError> {
109            Err(VrfError::SigningFailed("no key".into()))
110        }
111        fn create_proof(
112            &self,
113            _m: &[[u8; 32]],
114            _c: &[u8; 32],
115            _msg: &[u8],
116        ) -> Result<Vec<u8>, VrfError> {
117            Err(VrfError::ProofCreationFailed("ring empty".into()))
118        }
119        fn alias_in_context(&self, _c: &[u8; 32]) -> Result<[u8; 32], VrfError> {
120            Err(VrfError::AliasFailed("bad context".into()))
121        }
122    }
123
124    #[test]
125    fn test_trait_is_object_safe() {
126        let _: Box<dyn VrfProvider> = Box::new(OkVrf);
127    }
128
129    #[test]
130    fn test_trait_is_send_sync() {
131        fn assert_send_sync<T: Send + Sync>() {}
132        assert_send_sync::<OkVrf>();
133    }
134
135    #[test]
136    fn test_ok_stub_returns_values() {
137        let vrf = OkVrf;
138        assert_eq!(vrf.derive_member_key().unwrap(), [0xAA; 32]);
139        assert_eq!(vrf.sign(b"msg").unwrap().len(), 64);
140        assert!(!vrf
141            .create_proof(&[[0u8; 32]], CONTEXT_IDENTITY, b"msg")
142            .unwrap()
143            .is_empty());
144        assert_eq!(vrf.alias_in_context(CONTEXT_IDENTITY).unwrap(), [0xBB; 32]);
145    }
146
147    #[test]
148    fn test_fail_stub_returns_errors() {
149        let vrf = FailVrf;
150        assert!(matches!(
151            vrf.derive_member_key(),
152            Err(VrfError::InvalidEntropy(_))
153        ));
154        assert!(matches!(vrf.sign(b"msg"), Err(VrfError::SigningFailed(_))));
155        assert!(matches!(
156            vrf.create_proof(&[], CONTEXT_IDENTITY, b"msg"),
157            Err(VrfError::ProofCreationFailed(_))
158        ));
159        assert!(matches!(
160            vrf.alias_in_context(CONTEXT_IDENTITY),
161            Err(VrfError::AliasFailed(_))
162        ));
163    }
164
165    #[test]
166    fn test_error_display_contains_message() {
167        let e = VrfError::InvalidEntropy("bad seed".into());
168        assert!(e.to_string().contains("bad seed"));
169        let e = VrfError::ProofCreationFailed("ring too small".into());
170        assert!(e.to_string().contains("ring too small"));
171    }
172
173    #[test]
174    fn test_context_constants_contain_expected_prefix() {
175        assert!(CONTEXT_MOB_RULE.starts_with(b"pop:polkadot.network/mob-rule"));
176        assert!(CONTEXT_IDENTITY.starts_with(b"pop:polkadot.network/identity"));
177        assert!(CONTEXT_SCORE.starts_with(b"pop:polkadot.network/score"));
178        assert!(CONTEXT_RESOURCES.starts_with(b"pop:polkadot.network/resources"));
179        assert!(CONTEXT_PRIVACY_VOUCHER.starts_with(b"pop:polkadot.network/priv-vouchr"));
180    }
181}