···11+[package]
22+name = "ecmh"
33+version = "0.1.0"
44+edition = "2024"
55+description = "Elliptic Curve Multiset Hash - a homomorphic hash function for multisets"
66+license = "MIT OR Apache-2.0"
77+88+[dependencies]
99+curve25519-dalek = { version = "4", features = ["digest"], optional = true }
1010+sha2 = "0.10"
1111+p256 = { version = "0.13", features = ["hash2curve", "arithmetic"], optional = true }
1212+k256 = { version = "0.13", features = ["hash2curve", "arithmetic"], optional = true }
1313+elliptic-curve = { version = "0.13", features = ["hash2curve", "sec1"], optional = true }
1414+serde = { version = "1", features = ["derive"], optional = true }
1515+1616+[dependencies.hex]
1717+version = "0.4"
1818+1919+[dev-dependencies]
2020+2121+[features]
2222+default = ["ristretto"]
2323+ristretto = ["dep:curve25519-dalek"]
2424+p256 = ["dep:p256", "dep:elliptic-curve"]
2525+k256 = ["dep:k256", "dep:elliptic-curve"]
2626+serde = ["dep:serde"]
+109
README.md
···11+# ecmh
22+33+Elliptic Curve Multiset Hash (ECMH) for Rust — a homomorphic hash function for multisets based on [Maitin-Shepard et al. (2016)](https://arxiv.org/abs/1601.06502).
44+55+ECMH maps each set element to an elliptic curve point and sums them. The hash of the union of two multisets is simply the sum of their individual hashes, enabling efficient incremental updates without rehashing the entire set.
66+77+## Features
88+99+- **Incremental**: insert and remove elements without recomputing from scratch
1010+- **Order-independent**: `H({a, b}) == H({b, a})`
1111+- **Homomorphic**: `H(A ∪ B) == H(A) + H(B)`
1212+- **Multiple curve backends** behind feature flags:
1313+1414+| Type alias | Curve | Output size | Feature |
1515+|---|---|---|---|
1616+| `RistrettoEcmh` | Ristretto255 | 32 bytes | `ristretto` (default) |
1717+| `P256Ecmh` | NIST P-256 | 33 bytes | `p256` |
1818+| `K256Ecmh` | secp256k1 | 33 bytes | `k256` |
1919+2020+- **Operator overloading**: `+`, `-`, `+=`, `-=` for combining hashes
2121+- **Custom curves**: implement the `CurveBackend` trait
2222+2323+## Usage
2424+2525+```toml
2626+[dependencies]
2727+ecmh = "0.1"
2828+```
2929+3030+For a specific curve backend:
3131+3232+```toml
3333+[dependencies]
3434+ecmh = { version = "0.1", default-features = false, features = ["p256"] }
3535+```
3636+3737+## Example
3838+3939+```rust
4040+use ecmh::RistrettoEcmh;
4141+4242+// Build a multiset hash incrementally
4343+let mut hash = RistrettoEcmh::new();
4444+hash.insert(b"alice");
4545+hash.insert(b"bob");
4646+4747+// Order doesn't matter
4848+let mut hash2 = RistrettoEcmh::new();
4949+hash2.insert(b"bob");
5050+hash2.insert(b"alice");
5151+assert_eq!(hash, hash2);
5252+5353+// Combine disjoint sets
5454+let a = RistrettoEcmh::from_element(b"alice");
5555+let b = RistrettoEcmh::from_element(b"bob");
5656+assert_eq!(hash, a + b);
5757+5858+// Remove an element
5959+let mut h = hash.clone();
6060+h.remove(b"bob");
6161+assert_eq!(h, RistrettoEcmh::from_element(b"alice"));
6262+6363+// Build from an iterator
6464+let from_iter = RistrettoEcmh::from_elements([
6565+ b"alice".as_slice(),
6666+ b"bob".as_slice(),
6767+]);
6868+assert_eq!(from_iter, hash);
6969+7070+// Serialize / deserialize
7171+let bytes = hash.to_bytes();
7272+let recovered = RistrettoEcmh::from_bytes(&bytes).unwrap();
7373+assert_eq!(recovered, hash);
7474+```
7575+7676+## Custom curve backend
7777+7878+Implement the `CurveBackend` trait to use any elliptic curve:
7979+8080+```rust
8181+use ecmh::CurveBackend;
8282+8383+#[derive(Clone, Default, Debug)]
8484+struct MyCurve;
8585+8686+impl CurveBackend for MyCurve {
8787+ type Point = /* your point type */;
8888+ const POINT_SIZE: usize = /* compressed size */;
8989+9090+ fn identity() -> Self::Point { /* ... */ }
9191+ fn hash_to_point(data: &[u8]) -> Self::Point { /* ... */ }
9292+ fn add(a: &Self::Point, b: &Self::Point) -> Self::Point { /* ... */ }
9393+ fn sub(a: &Self::Point, b: &Self::Point) -> Self::Point { /* ... */ }
9494+ fn is_identity(p: &Self::Point) -> bool { /* ... */ }
9595+ fn to_bytes(p: &Self::Point) -> Vec<u8> { /* ... */ }
9696+ fn from_bytes(bytes: &[u8]) -> Option<Self::Point> { /* ... */ }
9797+}
9898+```
9999+100100+## References
101101+102102+- [Elliptic Curve Multiset Hash](https://arxiv.org/abs/1601.06502) — Maitin-Shepard, Tibouchi, Aranha (2016)
103103+- [jbms/ecmh](https://github.com/jbms/ecmh) — C++ reference implementation
104104+- [cronokirby/multiset-hash](https://github.com/cronokirby/multiset-hash) — earlier Rust crate (Ristretto only, outdated deps)
105105+- [ECMH BIP proposal](https://github.com/tomasvdw/bips/blob/master/ecmh.mediawiki) — Bitcoin specification using secp256k1
106106+107107+## License
108108+109109+MIT OR Apache-2.0
+36
src/backend.rs
···11+use core::fmt::Debug;
22+33+/// Trait abstracting the elliptic curve operations needed for ECMH.
44+///
55+/// Implementors provide a specific curve with hash-to-curve, point arithmetic,
66+/// and serialization. The crate ships with backends for Ristretto255, NIST P-256,
77+/// and secp256k1 behind feature flags.
88+pub trait CurveBackend: Clone + Default + Debug {
99+ /// The elliptic curve point type.
1010+ type Point: Clone + Debug + PartialEq + Eq;
1111+1212+ /// Size in bytes of the compressed/serialized point.
1313+ const POINT_SIZE: usize;
1414+1515+ /// Return the identity (neutral) element of the curve group.
1616+ fn identity() -> Self::Point;
1717+1818+ /// Deterministically hash arbitrary data to a curve point.
1919+ fn hash_to_point(data: &[u8]) -> Self::Point;
2020+2121+ /// Elliptic curve point addition.
2222+ fn add(a: &Self::Point, b: &Self::Point) -> Self::Point;
2323+2424+ /// Elliptic curve point subtraction.
2525+ fn sub(a: &Self::Point, b: &Self::Point) -> Self::Point;
2626+2727+ /// Check whether a point is the identity element.
2828+ fn is_identity(p: &Self::Point) -> bool;
2929+3030+ /// Serialize a point to its compressed byte representation.
3131+ fn to_bytes(p: &Self::Point) -> Vec<u8>;
3232+3333+ /// Deserialize a point from its compressed byte representation.
3434+ /// Returns `None` if the bytes do not represent a valid point.
3535+ fn from_bytes(bytes: &[u8]) -> Option<Self::Point>;
3636+}