1use thiserror::Error;
13
14#[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
28pub trait VrfProvider: Send + Sync {
38 fn derive_member_key(&self) -> Result<[u8; 32], VrfError>;
41
42 fn sign(&self, message: &[u8]) -> Result<Vec<u8>, VrfError>;
45
46 fn create_proof(
56 &self,
57 members: &[[u8; 32]],
58 context: &[u8; 32],
59 message: &[u8],
60 ) -> Result<Vec<u8>, VrfError>;
61
62 fn alias_in_context(&self, context: &[u8; 32]) -> Result<[u8; 32], VrfError>;
66}
67
68pub 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 ";
74pub 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}