Browse Source

feat!: derive CMZ generator A automatically

Derive and memoize the auxiliary generator with RFC 9380 hash-to-group, while retaining explicit initialization as a pre-first-use compatibility override.

BREAKING CHANGE: CMZ credential point types must implement the encoding and SHA-512 hash-to-group bounds captured by CMZGroup.

(cherry picked from commit bbe7a7948c1aac9feba4d2505cdd95d65167adea)
Michele Orrù 2 months ago
parent
commit
5e7b23a75b
11 changed files with 213 additions and 55 deletions
  1. 16 0
      CHANGELOG.md
  2. 73 0
      Cargo.lock
  3. 13 2
      Cargo.toml
  4. 17 3
      README.md
  5. 55 18
      src/lib.rs
  6. 0 4
      tests/basic.rs
  7. 0 7
      tests/priv_serialize.rs
  8. 17 5
      tests/rate_limiting.rs
  9. 0 4
      tests/submodule.rs
  10. 22 9
      tests/two_groups.rs
  11. 0 3
      tests/wallet.rs

+ 16 - 0
CHANGELOG.md

@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [Unreleased]
+
+### Added
+
+- Derive the CMZ generator `A` automatically via a RFC 9380 hash-to-group of the generator.
+  Applications no longer need to call `cmz_group_init` at startup.
+- Add `CMZGroup`, `cmz_generator_a`, and curve features for the automatic
+  derivation. Ristretto support is enabled by default; P-256 and secp256k1 are
+  available through the `p256` and `k256` features.
+
+### Changes
+
+- CMZ credential point types must now implement
+  `group::prime::PrimeGroup + group::GroupEncoding`, and
+  `hash2group::FromHash<hash2group::rfc9380::ExpandMsgXmd<sha2::Sha512>>`.
+
 ## [0.3.0] - 2026-08-04
 
 ### Changes

+ 73 - 0
Cargo.lock

@@ -161,6 +161,12 @@ version = "3.20.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
 
+[[package]]
+name = "bytemuck"
+version = "1.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
+
 [[package]]
 name = "byteorder"
 version = "1.5.0"
@@ -264,6 +270,7 @@ dependencies = [
  "ff",
  "generic_static",
  "group",
+ "hash2group",
  "hex",
  "lazy_static",
  "p256",
@@ -356,6 +363,7 @@ checksum = "6bb08299a6046833a879ace115697f12fd2bd6c8d3e93c909ffc9e0738e7cae0"
 dependencies = [
  "cpubits",
  "ctutils",
+ "getrandom",
  "hybrid-array",
  "num-traits",
  "rand_core",
@@ -369,6 +377,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
 dependencies = [
+ "getrandom",
  "hybrid-array",
  "rand_core",
 ]
@@ -519,6 +528,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a"
 dependencies = [
  "const-oid",
+ "pem-rfc7468",
  "zeroize",
 ]
 
@@ -540,6 +550,7 @@ dependencies = [
  "block-buffer",
  "const-oid",
  "crypto-common",
+ "ctutils",
  "zeroize",
 ]
 
@@ -556,7 +567,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0"
 dependencies = [
  "der",
+ "digest",
  "elliptic-curve",
+ "rfc6979",
  "signature",
  "spki",
  "zeroize",
@@ -577,9 +590,11 @@ dependencies = [
  "base16ct",
  "crypto-bigint",
  "crypto-common",
+ "digest",
  "ff",
  "group",
  "hybrid-array",
+ "pem-rfc7468",
  "pkcs8",
  "rand_core",
  "sec1",
@@ -712,6 +727,31 @@ dependencies = [
  "subtle",
 ]
 
+[[package]]
+name = "hash2curve"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1eaf40612d7d854743e7189228a6d528f0f6e8502cf6a0cb831d28a218b7f3f6"
+dependencies = [
+ "digest",
+ "elliptic-curve",
+]
+
+[[package]]
+name = "hash2group"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39c137c8355a760f7444e8b87aa30b3af24b4ecc37c8b82ef8b63be16d1267b"
+dependencies = [
+ "bytemuck",
+ "curve25519-dalek",
+ "digest",
+ "elliptic-curve",
+ "hash2curve",
+ "k256",
+ "p256",
+]
+
 [[package]]
 name = "hash32"
 version = "0.2.1"
@@ -762,6 +802,15 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "hmac"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
+dependencies = [
+ "digest",
+]
+
 [[package]]
 name = "hybrid-array"
 version = "0.4.15"
@@ -921,6 +970,7 @@ dependencies = [
  "cpubits",
  "ecdsa",
  "elliptic-curve",
+ "hash2curve",
  "primeorder",
  "wnaf",
 ]
@@ -1022,8 +1072,19 @@ checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a"
 dependencies = [
  "ecdsa",
  "elliptic-curve",
+ "hash2curve",
  "primefield",
  "primeorder",
+ "sha2",
+]
+
+[[package]]
+name = "pem-rfc7468"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9"
+dependencies = [
+ "base64ct",
 ]
 
 [[package]]
@@ -1107,6 +1168,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06"
 dependencies = [
  "elliptic-curve",
+ "once_cell",
  "primefield",
  "serdect",
  "wnaf",
@@ -1179,6 +1241,16 @@ dependencies = [
  "syn 3.0.9",
 ]
 
+[[package]]
+name = "rfc6979"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b"
+dependencies = [
+ "crypto-bigint",
+ "hmac",
+]
+
 [[package]]
 name = "rustc_version"
 version = "0.4.1"
@@ -1420,6 +1492,7 @@ version = "3.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5"
 dependencies = [
+ "digest",
  "rand_core",
 ]
 

+ 13 - 2
Cargo.toml

@@ -11,6 +11,7 @@ cmz-derive = "=0.3.0"
 ff = "0.14"
 generic_static = "0.2"
 group = "0.14"
+hash2group = { version = "0.1.2", features = [ "rfc9380" ] }
 hex = { version = "0.4", features = [ "serde" ] }
 lazy_static = "1"
 postcard = { version = "1", features = [ "alloc" ] }
@@ -18,15 +19,17 @@ rand = "0.10"
 serde = { version = "1", features = [ "derive" ] }
 serde_bytes = "0.11"
 serde_with = "3"
+sha2 = "0.11"
 sigma-compiler = "0.2.3"
 thiserror = "2"
 
 [dev-dependencies]
 chrono = "0.4"
-curve25519-dalek = { version = "5", features = [ "group", "rand_core", "digest" ] }
+curve25519-dalek = { version = "5", features = [ "group", "rand_core" ] }
 # Used for testing co-habitating with a second PrimeGroup.
 p256 = { version = "0.14", features = [ "arithmetic" ] }
-sha2 = "0.11"
+# Enable both test group implementations regardless of the cmz feature set.
+hash2group = { version = "0.1.2", features = [ "curve25519-dalek", "p256" ] }
 
 [patch.crates-io]
 cmz-derive = { path = "cmz-derive" }
@@ -37,6 +40,14 @@ sigma-compiler-core = { path = "../sigma-compiler/sigma-compiler-core" }
 sigma-proofs = { path = "../sigma-proofs" }
 
 [features]
+# Hash-to-group implementations available to the automatic CMZ generator
+# derivation. Ristretto remains the default group used by the examples and
+# downstream applications; other groups are opt-in to avoid pulling every
+# curve implementation into every build.
+default = [ "curve25519-dalek" ]
+curve25519-dalek = [ "hash2group/curve25519-dalek" ]
+k256 = [ "hash2group/k256" ]
+p256 = [ "hash2group/p256" ]
 # Dump (to stdout or to a string) the value of the instance on both the
 # prover's and verifier's side.  They should match.
 dump = [ "sigma-compiler/dump" ]

+ 17 - 3
README.md

@@ -17,10 +17,24 @@ to create the credential.)
 Your application can have multiple different kinds of credentials, each
 with its own set of attributes.  All of the credentials in your
 application should use the _same_ `Scalar` and `Point` types.  You get
-these from a mathematical _group_, which must satisfy the trait
-[group::prime::PrimeGroup](https://docs.rs/group/0.14.0/group/prime/trait.PrimeGroup.html).
-A typical such group would be
+these from a mathematical _group_. The `Point` type must implement all of
+the following bounds:
+
+  - `group::prime::PrimeGroup`
+  - `group::GroupEncoding`
+  - `hash2group::FromHash<hash2group::rfc9380::ExpandMsgXmd<sha2::Sha512>>`
+
+`CMZGroup` is a blanket marker trait for exactly this combination of bounds.
+A typical point type satisfying them is
 [curve25519\_dalek::ristretto::RistrettoPoint](https://docs.rs/curve25519-dalek/5.0.0/curve25519_dalek/ristretto/struct.RistrettoPoint.html).
+The default `curve25519-dalek` feature supplies the hash-to-group
+implementation for `RistrettoPoint`; the optional `p256` and `k256` features
+supply it for `p256::ProjectivePoint` and `k256::ProjectivePoint`, respectively.
+
+CMZ requires a second generator `A`, which is derived automatically on first use
+by hashing the canonical encoding of `B` and caching it. Callers may override it
+by calling `cmz_group_init` or `cmz_group_init_with` before the first CMZ
+operation.
 
 To declare a credential type, use the `CMZ!` macro at the top level of
 your crate or module (outside of any function):

+ 55 - 18
src/lib.rs

@@ -11,11 +11,13 @@ use group::prime::PrimeGroup;
 use group::{Group, GroupEncoding};
 #[cfg(feature = "wnaf_is_constant_time")]
 use group::{WnafBase, WnafScalar};
+use hash2group::{rfc9380::ExpandMsgXmd, FromHash};
 use lazy_static::lazy_static;
 use rand::Rng;
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
 pub use serde_with::serde_as;
 use serde_with::{DeserializeAs, SerializeAs};
+use sha2::Sha512;
 use sigma_compiler::*;
 pub use sigma_compiler::{self};
 use thiserror::Error;
@@ -111,6 +113,36 @@ pub struct CMZPubkey<G: PrimeGroup> {
     pub X: Vec<G>,
 }
 
+/// Domain separator for deriving the CMZ generator `A`.
+///
+/// This value is a public protocol parameter, it is the computationally-independent
+/// auxiliary group generator.
+/// Changing it changes `A`, and thus it is a breaking change for existing CMZ keys
+/// and credentials.
+pub const CMZ_GENERATOR_A_DST: &[u8] = b"CMZ_GENERATOR_A_XMD:SHA-512_RO_V1_";
+
+/// A prime-order group supporting encoding and hash into the group.
+///
+/// The blanket implementation covers groups supported by [`hash2group`].
+pub trait CMZGroup: PrimeGroup + GroupEncoding + FromHash<ExpandMsgXmd<Sha512>> {}
+
+impl<G> CMZGroup for G where G: PrimeGroup + GroupEncoding + FromHash<ExpandMsgXmd<Sha512>> {}
+
+/// Deterministically derive the CMZ generator `A` from the standard generator
+/// `B` using hash-to-group.
+pub fn cmz_generator_a<G: CMZGroup>() -> G {
+    let generator_b = G::generator();
+    let encoded_b = generator_b.to_bytes();
+    let generator_a =
+        <G as FromHash<ExpandMsgXmd<Sha512>>>::from_hash(CMZ_GENERATOR_A_DST, encoded_b.as_ref());
+
+    assert!(
+        !bool::from(generator_a.is_identity()) && generator_a != generator_b,
+        "hash-to-group produced an invalid CMZ generator A"
+    );
+    generator_a
+}
+
 // The size of the WNAF windows.  Larger sizes take more memory, but
 // result in faster multiplications.
 #[cfg(feature = "wnaf_is_constant_time")]
@@ -255,34 +287,38 @@ fn load_bp_with<G: Group>(init: impl FnOnce() -> CMZBasepoints<G>) -> &'static C
         .unwrap()
 }
 
-/// The already-loaded `CMZBasepoints<G>`; panics if `cmz_group_init` has
-/// not been called for `G`.
-fn load_bp<G: Group>() -> &'static CMZBasepoints<G> {
-    load_bp_with::<G>(|| panic!("basepoints uninitialized"))
+/// Return the basepoints for `G`, deriving and memoizing `A` on first use.
+fn load_bp<G: CMZGroup>() -> &'static CMZBasepoints<G> {
+    load_bp_with::<G>(|| CMZBasepoints::init(cmz_generator_a::<G>()))
 }
 
-/// Initialize the required second generator for a `PrimeGroup`.
+/// Override the automatically derived second generator for a `PrimeGroup`.
 ///
 /// CMZ credentials require two generators, `A` and `B`.  `B` is the
 /// "standard" generator.  A can be any other generator (that is, any
 /// other non-identity point in a prime-order group), but it is required
 /// that no one know the discrete log between `A` and `B`.  So you can't
 /// generate `A` by multiplying `B` by some scalar, for example.  If your
-/// group has a hash_from_bytes function, then you can use that to generate
-/// `A`. For example, if your group is a curve25519 group, you can
+/// group has a hash-to-group function, then you can use that to generate
+/// `A`. For the standard CMZ derivation, use [`cmz_generator_a`]:
 ///
 /// ```
-/// use curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT as B;
+/// use cmz::cmz_generator_a;
 /// use curve25519_dalek::ristretto::RistrettoPoint as G;
-/// use sha2::Sha512;
-/// let A = G::hash_from_bytes::<Sha512>(b"CMZ Generator A");
-/// assert_ne!(A, B);
+/// use group::Group;
+/// let A = cmz_generator_a::<G>();
+/// assert_ne!(A, G::generator());
 /// ```
 ///
 /// Otherwise, you're possibly on your own to generate an appropriate
-/// generator `A`.  Everyone who uses a given credential type with a
-/// given group will need to use the same `A`.  You need to call this
-/// before doing any operations with a credential.
+/// generator `A`. Everyone who uses a given credential type with a given
+/// group will need to use the same `A`.
+///
+/// CMZ automatically derives `A` using [`cmz_generator_a`], so normal callers
+/// do not need this function. This override exists for compatibility with
+/// deployments using older or application-specific public parameters. It must
+/// be called before the first CMZ operation for `G`; later calls retain the
+/// already-loaded value.
 pub fn cmz_group_init<G: PrimeGroup>(generator_A: G) {
     load_bp_with(|| CMZBasepoints::<G>::init(generator_A));
 }
@@ -296,12 +332,12 @@ pub fn cmz_group_init_with<G: PrimeGroup>(generator_A: impl FnOnce() -> G) {
 }
 
 /// Get the loaded CMZBasepoints for the given group
-pub fn cmz_basepoints<G: PrimeGroup>() -> &'static CMZBasepoints<G> {
+pub fn cmz_basepoints<G: CMZGroup>() -> &'static CMZBasepoints<G> {
     load_bp()
 }
 
 /// Compute a public key from a private key
-pub fn cmz_privkey_to_pubkey<G: PrimeGroup>(privkey: &CMZPrivkey<G>) -> CMZPubkey<G> {
+pub fn cmz_privkey_to_pubkey<G: CMZGroup>(privkey: &CMZPrivkey<G>) -> CMZPubkey<G> {
     let bp = load_bp::<G>();
     let X0: Option<G> = if privkey.muCMZ {
         Some(bp.mulB(&privkey.x0))
@@ -326,7 +362,7 @@ where
     type Scalar: PrimeField;
 
     /// The type of the coordinates of the MAC for this credential
-    type Point: PrimeGroup;
+    type Point: CMZGroup;
 
     /// Produce a vector of strings containing the names of the
     /// attributes of this credential.  (The MAC is not included.)
@@ -476,7 +512,8 @@ where
 ///     use curve25519_dalek::ristretto::RistrettoPoint;
 ///     type G = RistrettoPoint;
 ///
-/// The group must implement the trait [`PrimeGroup`](https://docs.rs/group/latest/group/prime/trait.PrimeGroup.html).
+/// The group must implement [`CMZGroup`]. Implementations are available for
+/// supported hash-to-group curves through the corresponding crate features.
 #[macro_export]
 macro_rules! CMZ {
     ( $name: ident < $G: ident > : $( $id: ident ),+ ) => {

+ 0 - 4
tests/basic.rs

@@ -3,7 +3,6 @@ use curve25519_dalek::ristretto::RistrettoPoint;
 use curve25519_dalek::scalar::Scalar;
 use group::Group;
 use rand::{CryptoRng, Rng};
-use sha2::Sha512;
 
 CMZ! { Basic<RistrettoPoint> :
     attr1,
@@ -19,9 +18,6 @@ A: Basic {
 #[test]
 fn test_basic() {
     let mut rng = rand::rng();
-    cmz_group_init(RistrettoPoint::hash_from_bytes::<Sha512>(
-        b"CMZ Generator A",
-    ));
 
     let (privkey, pubkey) = Basic::cmz14_gen_keys(&mut rng);
 

+ 0 - 7
tests/priv_serialize.rs

@@ -3,7 +3,6 @@ use curve25519_dalek::ristretto::RistrettoPoint;
 use curve25519_dalek::scalar::Scalar;
 use group::Group;
 use rand::{CryptoRng, Rng};
-use sha2::Sha512;
 
 CMZ! { Basic<RistrettoPoint> :
     attr1,
@@ -22,9 +21,6 @@ A: Basic {
 #[test]
 fn test_default_serialize() {
     let mut rng = rand::rng();
-    cmz_group_init(RistrettoPoint::hash_from_bytes::<Sha512>(
-        b"CMZ Generator A",
-    ));
 
     let (privkey, pubkey) = Basic::cmz14_gen_keys(&mut rng);
 
@@ -50,9 +46,6 @@ fn test_default_serialize() {
 #[test]
 fn test_priv_serialize() {
     let mut rng = rand::rng();
-    cmz_group_init(RistrettoPoint::hash_from_bytes::<Sha512>(
-        b"CMZ Generator A",
-    ));
 
     let (privkey, pubkey) = Basic::cmz14_gen_keys(&mut rng);
 

+ 17 - 5
tests/rate_limiting.rs

@@ -4,12 +4,19 @@ use chrono::Utc;
 use cmz::*;
 use curve25519_dalek::ristretto::RistrettoPoint;
 use group::{Group, GroupEncoding};
+use hash2group::{rfc9380::ExpandMsgXmd, FromHash};
 use rand::{CryptoRng, Rng};
 use sha2::Sha512;
 use std::collections::HashSet;
 
 type G = RistrettoPoint;
 
+const EPOCH_BASE_DST: &[u8] = b"CMZ_RATE_LIMIT_EPOCH_XMD:SHA-512_RO_V1_";
+
+fn epoch_base(epoch: &[u8]) -> G {
+    <G as FromHash<ExpandMsgXmd<Sha512>>>::from_hash(EPOCH_BASE_DST, epoch)
+}
+
 CMZ! { Cred: key }
 CMZ! { PresNum: pres_num }
 
@@ -49,7 +56,7 @@ impl RateLimitClient {
         let mut P = PresNum::using_pubkey(&self.presnum_pubkey);
         P.pres_num = Some(pres_num.into());
         P.fake_MAC(rng);
-        let Epoch_base = RistrettoPoint::hash_from_bytes::<Sha512>(epoch);
+        let Epoch_base = epoch_base(epoch);
         let VRF_output = (self.cred.key.unwrap() + P.pres_num.unwrap()).invert() * Epoch_base;
         let params = pres_cred::Params {
             max_pres: 5u32.into(),
@@ -87,7 +94,7 @@ impl RateLimitServer {
         epoch: &[u8],
         msg: &[u8],
     ) -> Result<(), CMZError> {
-        let Epoch_base = RistrettoPoint::hash_from_bytes::<Sha512>(epoch);
+        let Epoch_base = epoch_base(epoch);
         // Separate the message into the VRF output and the request
         let VRF_output = G::from_bytes(&msg[..32].try_into().unwrap()).unwrap();
 
@@ -125,9 +132,6 @@ impl RateLimitServer {
 #[test]
 fn test_rate_limiting() -> Result<(), CMZError> {
     let mut rng = rand::rng();
-    cmz_group_init(RistrettoPoint::hash_from_bytes::<Sha512>(
-        b"CMZ Generator A",
-    ));
 
     let (privkey, pubkey) = Cred::mucmz_gen_keys(&mut rng);
 
@@ -191,3 +195,11 @@ fn test_rate_limiting() -> Result<(), CMZError> {
 
     Ok(())
 }
+
+#[test]
+fn epoch_base_derivation_is_stable() {
+    assert_eq!(
+        hex::encode(epoch_base(b"Epoch 2026-08-16").to_bytes()),
+        "cabdff610ae6898b12cc86be131d101913b807180f8ffe741319a065d3c2d576"
+    );
+}

+ 0 - 4
tests/submodule.rs

@@ -5,7 +5,6 @@ use curve25519_dalek::ristretto::RistrettoPoint;
 use curve25519_dalek::scalar::Scalar;
 use group::Group;
 use rand::{CryptoRng, Rng};
-use sha2::Sha512;
 
 pub mod cred {
     use super::*;
@@ -30,9 +29,6 @@ pub mod submod {
     #[test]
     fn test_submodule() -> Result<(), CMZError> {
         let mut rng = rand::rng();
-        cmz_group_init(RistrettoPoint::hash_from_bytes::<Sha512>(
-            b"CMZ Generator A",
-        ));
 
         let (privkey, pubkey) = Basic::cmz14_gen_keys(&mut rng);
 

+ 22 - 9
tests/two_groups.rs

@@ -1,21 +1,34 @@
 //! Basepoint derivation is keyed by group type, so two groups can coexist.
 
-use cmz::{cmz_basepoints, cmz_group_init};
+use cmz::{cmz_basepoints, cmz_generator_a, cmz_group_init};
 use curve25519_dalek::ristretto::RistrettoPoint;
-use group::Group;
-use sha2::Sha512;
+use group::{Group, GroupEncoding};
+
+const RISTRETTO_A_ENCODING: &[u8] = &[
+    0x8e, 0x65, 0xa3, 0x1d, 0xc2, 0x60, 0x08, 0xdf, 0xbc, 0xd9, 0xf6, 0xef, 0xbd, 0x57, 0xf7, 0xcb,
+    0xb2, 0xb3, 0x98, 0x72, 0xf6, 0xfd, 0x32, 0x73, 0x6a, 0x00, 0x7a, 0x08, 0xc2, 0x1e, 0x47, 0x18,
+];
+const P256_A_ENCODING: &[u8] = &[
+    0x02, 0x73, 0xe5, 0x71, 0x95, 0x24, 0x34, 0xf0, 0xd5, 0xac, 0xaa, 0xe0, 0xfc, 0xe7, 0x35, 0x7c,
+    0x45, 0xb2, 0x73, 0xbd, 0x8e, 0x30, 0x4e, 0xe1, 0xac, 0xd9, 0xf2, 0x43, 0xf3, 0x0b, 0x96, 0x18,
+    0x9e,
+];
 
 #[test]
 fn two_groups_coexist() {
-    let ristretto_a = RistrettoPoint::hash_from_bytes::<Sha512>(b"CMZ Generator A");
-    let p256_a = p256::ProjectivePoint::generator() * p256::Scalar::from(42u64);
+    let ristretto_a = cmz_generator_a::<RistrettoPoint>();
+    let p256_a = cmz_generator_a::<p256::ProjectivePoint>();
 
-    cmz_group_init::<RistrettoPoint>(ristretto_a);
-    cmz_group_init::<p256::ProjectivePoint>(p256_a);
+    assert_eq!(&ristretto_a.to_bytes()[..], RISTRETTO_A_ENCODING);
+    assert_eq!(&p256_a.to_bytes()[..], P256_A_ENCODING);
 
-    // Each group must read back its own basepoints, not the other's.
+    // Each group is initialized automatically and reads back its own
+    // basepoints, not the other's.
     assert_eq!(cmz_basepoints::<RistrettoPoint>().A(), ristretto_a);
-    assert_eq!(cmz_basepoints::<RistrettoPoint>().B(), RistrettoPoint::generator());
+    assert_eq!(
+        cmz_basepoints::<RistrettoPoint>().B(),
+        RistrettoPoint::generator()
+    );
     assert_eq!(cmz_basepoints::<p256::ProjectivePoint>().A(), p256_a);
     assert_eq!(
         cmz_basepoints::<p256::ProjectivePoint>().B(),

+ 0 - 3
tests/wallet.rs

@@ -6,7 +6,6 @@ use cmz::*;
 use curve25519_dalek::ristretto::RistrettoPoint as G;
 use group::Group;
 use rand::{CryptoRng, Rng};
-use sha2::Sha512;
 
 CMZ! { Wallet: randid, balance }
 CMZ! { Item: serialno, price }
@@ -107,9 +106,7 @@ macro_rules! protos_def {
                 }
             }
 
-            // Initialization
             let mut rng = rand::rng();
-            cmz_group_init(G::hash_from_bytes::<Sha512>(b"CMZ Generator A"));
 
             // Issuer: generate private and public keys for each type of
             // credential.  (The client gets a copy of the public keys.)