···55description = "Elliptic Curve Multiset Hash - a homomorphic hash function for multisets"
66license = "MIT OR Apache-2.0"
7788+[lib]
99+crate-type = ["cdylib", "rlib"]
1010+811[dependencies]
912curve25519-dalek = { version = "4", features = ["digest"], optional = true }
1013sha2 = "0.10"
···1215k256 = { version = "0.13", features = ["hash2curve", "arithmetic"], optional = true }
1316elliptic-curve = { version = "0.13", features = ["hash2curve", "sec1"], optional = true }
1417serde = { version = "1", features = ["derive"], optional = true }
1515-1616-[dependencies.hex]
1717-version = "0.4"
1818+hex = "0.4"
1919+wasm-bindgen = { version = "0.2", optional = true }
2020+js-sys = { version = "0.3", optional = true }
18211922[dev-dependencies]
2023···2427p256 = ["dep:p256", "dep:elliptic-curve"]
2528k256 = ["dep:k256", "dep:elliptic-curve"]
2629serde = ["dep:serde"]
3030+wasm = ["dep:wasm-bindgen", "dep:js-sys", "ristretto"]
+63-2
README.md
···11# ecmh
2233-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).
33+Elliptic Curve Multiset Hash (ECMH) for Rust and WebAssembly — a homomorphic hash function for multisets based on [Maitin-Shepard et al. (2016)](https://arxiv.org/abs/1601.06502).
4455ECMH 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···19192020- **Operator overloading**: `+`, `-`, `+=`, `-=` for combining hashes
2121- **Custom curves**: implement the `CurveBackend` trait
2222+- **WebAssembly**: first-class wasm support via `wasm-pack`
22232324## Usage
2525+2626+### Rust
24272528```toml
2629[dependencies]
···3437ecmh = { version = "0.1", default-features = false, features = ["p256"] }
3538```
36393737-## Example
4040+### WebAssembly
4141+4242+Build the wasm package with `wasm-pack`:
4343+4444+```sh
4545+wasm-pack build --target web --features wasm
4646+```
4747+4848+This produces a `pkg/` directory containing the `.wasm` binary, JS glue, and TypeScript definitions ready for `npm publish` or direct use.
4949+5050+For additional curve backends in the wasm build:
5151+5252+```sh
5353+wasm-pack build --target web --features wasm,p256,k256
5454+```
5555+5656+## Example (Rust)
38573958```rust
4059use ecmh::RistrettoEcmh;
···7291let recovered = RistrettoEcmh::from_bytes(&bytes).unwrap();
7392assert_eq!(recovered, hash);
7493```
9494+9595+## Example (JavaScript / TypeScript)
9696+9797+```typescript
9898+import init, { EcmhRistretto } from './pkg/ecmh.js';
9999+100100+await init();
101101+102102+const enc = new TextEncoder();
103103+104104+// Build a multiset hash incrementally
105105+const hash = new EcmhRistretto();
106106+hash.insert(enc.encode("alice"));
107107+hash.insert(enc.encode("bob"));
108108+109109+// Order doesn't matter
110110+const hash2 = new EcmhRistretto();
111111+hash2.insert(enc.encode("bob"));
112112+hash2.insert(enc.encode("alice"));
113113+console.log(hash.equals(hash2)); // true
114114+115115+// Combine disjoint sets
116116+const a = EcmhRistretto.fromElement(enc.encode("alice"));
117117+const b = EcmhRistretto.fromElement(enc.encode("bob"));
118118+console.log(a.union(b).equals(hash)); // true
119119+120120+// Serialize
121121+console.log(hash.toHex()); // 64-char hex string
122122+console.log(hash.toBytes()); // Uint8Array(32)
123123+124124+// Deserialize
125125+const recovered = EcmhRistretto.fromBytes(hash.toBytes());
126126+console.log(recovered.equals(hash)); // true
127127+```
128128+129129+The wasm build exposes one class per enabled curve backend:
130130+131131+| JS class | Curve | Feature flags |
132132+|---|---|---|
133133+| `EcmhRistretto` | Ristretto255 | `wasm` |
134134+| `EcmhP256` | NIST P-256 | `wasm` + `p256` |
135135+| `EcmhK256` | secp256k1 | `wasm` + `k256` |
7513676137## Custom curve backend
77138
+194
USAGE.md
···11+# Permissioned Repo Commits with ECMH
22+33+This document describes how ECMH is used to build authenticated, per-reader commits for permissioned repositories. Each commit characterizes the current set of live records in a repo without exposing that information to other readers.
44+55+## Permissioned repo commits
66+77+Each user's permissioned repo within a space is represented by a cryptographic commit: a compact digest that characterizes the current set of live records, independent of history.
88+99+We use ECMH (Elliptic Curve Multiset Hash), a set hash where adding or removing an element is a single point operation rather than a full recompute. Two permissioned repos with the same live records produce the same ECMH digest regardless of the order records were written or deleted.
1010+1111+This commitment plays the same role as the MST root hash for public data: a single value that definitively characterizes what is in the repo. Unlike the MST, it does not support partial sync or single-record proofs. The tradeoff is a noticeably lower-overhead cryptographic structure and sync protocol.
1212+1313+## The problem with bare ECMH digests
1414+1515+ECMH is deterministic. If client-a and client-b happen to have permissioned repos with identical live records, their bare ECMH digests will be identical. A party who obtains both commits can tell that the two repos contain the same data, even without knowing what that data is.
1616+1717+Worse, a bare digest can be rebroadcast. If client-a receives a commit from the server, nothing stops client-a from forwarding that commit to a third party who can then verify it against data they obtain independently.
1818+1919+Both problems are solved by authenticating the ECMH digest with a randomly generated, transient HMAC key that is unique to each reader.
2020+2121+## Commit structure
2222+2323+A permissioned repo commit is composed of four fields:
2424+2525+```
2626+commit = {
2727+ ecmh: bytes, // the ECMH digest of the live record set
2828+ hmac: bytes, // HMAC-SHA256(hmac_key, ecmh)
2929+ hmac_key: bytes, // randomly generated, unique per reader
3030+ sig: bytes, // atproto signing key signature over (ecmh, hmac, hmac_key)
3131+}
3232+```
3333+3434+The server generates a fresh HMAC key for every reader on every read. The signature binds all fields together under the repo owner's signing key.
3535+3636+## Why this works
3737+3838+### Different commits for identical repos
3939+4040+Even when two readers have access to repos with the exact same live records, the ECMH digests are the same but the commits are different because each reader receives a distinct random HMAC key:
4141+4242+```
4343+Reader A receives:
4444+ ecmh = 0x7a3f... (deterministic, same for identical sets)
4545+ hmac_key = 0x91ab... (random, unique to this reader)
4646+ hmac = HMAC-SHA256(0x91ab..., 0x7a3f...) → 0xd4e7...
4747+ sig = sign(ecmh || hmac || hmac_key)
4848+4949+Reader B receives:
5050+ ecmh = 0x7a3f... (same ECMH — same records)
5151+ hmac_key = 0xc30f... (different random key)
5252+ hmac = HMAC-SHA256(0xc30f..., 0x7a3f...) → 0x18b2...
5353+ sig = sign(ecmh || hmac || hmac_key)
5454+```
5555+5656+The two commits share the same ECMH value, but the HMACs are completely different. A third party comparing the two commits cannot determine whether the underlying repos are identical without independently knowing the record sets.
5757+5858+### Deniability
5959+6060+The HMAC key is included in the commit. This is intentional. Because anyone who holds the key can produce a valid HMAC over any ECMH digest, a leaked commit does not serve as cryptographic proof that the signer sent that specific content to that specific reader. The signature covers the bundle, but the HMAC key's presence means the reader could have constructed the HMAC themselves.
6161+6262+This gives the signer plausible deniability: the commit authenticates the data for the intended reader, but is not a transferable proof to third parties.
6363+6464+### No rebroadcast
6565+6666+Each commit is authenticated for exactly one party. If client-a forwards their commit to client-b, client-b has no reason to trust it: the HMAC key was chosen for client-a, and client-b has their own key. The server will never produce the same HMAC key for two different readers.
6767+6868+## Demonstration
6969+7070+The following shows how the ECMH layer fits into the commit protocol. The HMAC and signature operations use standard cryptographic primitives alongside the `ecmh` crate.
7171+7272+```rust
7373+use ecmh::RistrettoEcmh;
7474+use hmac::{Hmac, Mac};
7575+use sha2::Sha256;
7676+7777+type HmacSha256 = Hmac<Sha256>;
7878+7979+/// A permissioned repo commit.
8080+struct Commit {
8181+ ecmh: Vec<u8>,
8282+ hmac: Vec<u8>,
8383+ hmac_key: Vec<u8>,
8484+ sig: Vec<u8>,
8585+}
8686+8787+// --- Server side ---
8888+8989+// The repo's live record set, maintained incrementally.
9090+let mut repo = RistrettoEcmh::new();
9191+repo.insert(b"at://did:plc:abc/app.bsky.feed.post/3abc");
9292+repo.insert(b"at://did:plc:abc/app.bsky.feed.post/3def");
9393+repo.insert(b"at://did:plc:abc/app.bsky.feed.like/3ghi");
9494+9595+let ecmh_bytes = repo.to_bytes();
9696+9797+// Generate a commit for reader A.
9898+let hmac_key_a: [u8; 32] = rand::random();
9999+let mut mac_a = HmacSha256::new_from_slice(&hmac_key_a).unwrap();
100100+mac_a.update(&ecmh_bytes);
101101+let hmac_a = mac_a.finalize().into_bytes();
102102+103103+// Generate a commit for reader B — same repo, different key.
104104+let hmac_key_b: [u8; 32] = rand::random();
105105+let mut mac_b = HmacSha256::new_from_slice(&hmac_key_b).unwrap();
106106+mac_b.update(&ecmh_bytes);
107107+let hmac_b = mac_b.finalize().into_bytes();
108108+109109+// The ECMH digests are identical (same live records)...
110110+assert_eq!(ecmh_bytes, repo.to_bytes());
111111+112112+// ...but the HMACs are different (different transient keys).
113113+assert_ne!(hmac_a.as_slice(), hmac_b.as_slice());
114114+115115+// Each commit is then signed by the repo owner's atproto signing key
116116+// and delivered to the respective reader.
117117+```
118118+119119+### Verification by the reader
120120+121121+The reader verifies the commit by:
122122+123123+1. Checking the signature against the repo owner's public key
124124+2. Recomputing the HMAC from the provided key and ECMH digest
125125+3. Comparing the recomputed HMAC to the one in the commit
126126+4. Optionally, recomputing the ECMH from the received records to verify consistency
127127+128128+```rust
129129+// --- Reader A side ---
130130+131131+// Reader A receives the commit and the record set from the server.
132132+let received_ecmh = &ecmh_bytes;
133133+let received_hmac = hmac_a.as_slice();
134134+let received_key = &hmac_key_a;
135135+136136+// Step 1: verify signature (omitted — use atproto signing key verification)
137137+138138+// Step 2: recompute HMAC
139139+let mut verifier = HmacSha256::new_from_slice(received_key).unwrap();
140140+verifier.update(received_ecmh);
141141+verifier.verify_slice(received_hmac).expect("HMAC mismatch");
142142+143143+// Step 3: recompute ECMH from the records to check consistency
144144+let mut local = RistrettoEcmh::new();
145145+local.insert(b"at://did:plc:abc/app.bsky.feed.post/3abc");
146146+local.insert(b"at://did:plc:abc/app.bsky.feed.post/3def");
147147+local.insert(b"at://did:plc:abc/app.bsky.feed.like/3ghi");
148148+assert_eq!(local.to_bytes(), *received_ecmh);
149149+```
150150+151151+### Incremental sync
152152+153153+When records are added or removed, the server updates the ECMH incrementally and issues a new commit with a fresh HMAC key:
154154+155155+```rust
156156+// Server adds a new record
157157+repo.insert(b"at://did:plc:abc/app.bsky.feed.post/3jkl");
158158+159159+// Server deletes an old record
160160+repo.remove(b"at://did:plc:abc/app.bsky.feed.like/3ghi");
161161+162162+// New commit for reader A with a fresh key
163163+let new_ecmh = repo.to_bytes();
164164+let new_hmac_key: [u8; 32] = rand::random();
165165+let mut mac = HmacSha256::new_from_slice(&new_hmac_key).unwrap();
166166+mac.update(&new_ecmh);
167167+let new_hmac = mac.finalize().into_bytes();
168168+169169+// The new ECMH reflects the updated record set.
170170+// The new HMAC key ensures this commit is distinct from all prior commits,
171171+// even if the record set happens to return to a previous state.
172172+```
173173+174174+## Security properties
175175+176176+| Property | Mechanism |
177177+|---|---|
178178+| **Integrity** | ECMH digest changes if any record is added or removed |
179179+| **Consistency** | Reader recomputes ECMH from received records and compares to commit |
180180+| **Confidentiality** | ECMH is one-way; digest reveals nothing about records |
181181+| **Reader isolation** | Transient HMAC key makes each commit unique per reader |
182182+| **Deniability** | Included HMAC key means anyone could have produced the HMAC |
183183+| **No rebroadcast** | Commit is meaningful only to the reader who holds the matching key |
184184+| **Authenticity** | Signature by the repo owner's atproto signing key binds the commit |
185185+186186+## Limitations
187187+188188+**ECMH equality across readers.** The raw ECMH field is the same for identical repos. The HMAC prevents trivial comparison of commits, but a reader who strips the HMAC and compares raw ECMH values with another reader can determine set equality. If this is a concern, the ECMH digest itself can be encrypted or omitted from the wire format, transmitting only the HMAC.
189189+190190+**Small record universes.** If the space of possible records is small enough to enumerate, an attacker can brute-force the ECMH by hashing all possible subsets. ECMH security relies on the record space being large.
191191+192192+**HMAC key lifecycle.** The HMAC key must be generated fresh for every read and never reused. Reusing a key across readers or across time would allow the same comparison attacks the HMAC is designed to prevent.
193193+194194+**No partial proofs.** Unlike a Merkle tree, ECMH does not support proving that a single record is or is not in the set without revealing the entire set. The tradeoff is significantly lower computational and storage overhead.
+3-1
src/lib.rs
···41414242pub mod backend;
4343pub mod curves;
4444+#[cfg(feature = "wasm")]
4545+pub mod wasm;
44464547pub use backend::CurveBackend;
4648#[cfg(feature = "p256")]
···52545355use core::fmt;
5456use core::marker::PhantomData;
5555-use std::ops::{Add, AddAssign, Sub, SubAssign};
5757+use core::ops::{Add, AddAssign, Sub, SubAssign};
56585759/// A homomorphic multiset hash value parameterized by a curve backend.
5860///
+279
src/wasm.rs
···11+//! WebAssembly bindings for ECMH via `wasm-bindgen`.
22+//!
33+//! Enabled by the `wasm` feature flag. Provides JS-friendly wrapper classes
44+//! for each curve backend.
55+66+use wasm_bindgen::prelude::*;
77+88+// ---------------------------------------------------------------------------
99+// Ristretto255 (always available when wasm feature is enabled)
1010+// ---------------------------------------------------------------------------
1111+1212+/// Elliptic Curve Multiset Hash using Ristretto255.
1313+///
1414+/// A homomorphic hash for multisets: the hash of the union of two sets
1515+/// equals the sum of their individual hashes.
1616+#[wasm_bindgen(js_name = "EcmhRistretto")]
1717+pub struct WasmEcmh {
1818+ inner: crate::RistrettoEcmh,
1919+}
2020+2121+impl Default for WasmEcmh {
2222+ fn default() -> Self {
2323+ Self::new()
2424+ }
2525+}
2626+2727+#[wasm_bindgen(js_class = "EcmhRistretto")]
2828+impl WasmEcmh {
2929+ /// Create a hash representing the empty multiset.
3030+ #[wasm_bindgen(constructor)]
3131+ pub fn new() -> Self {
3232+ Self {
3333+ inner: crate::RistrettoEcmh::new(),
3434+ }
3535+ }
3636+3737+ /// Create a hash from a single element.
3838+ #[wasm_bindgen(js_name = "fromElement")]
3939+ pub fn from_element(data: &[u8]) -> Self {
4040+ Self {
4141+ inner: crate::RistrettoEcmh::from_element(data),
4242+ }
4343+ }
4444+4545+ /// Insert an element into the multiset.
4646+ pub fn insert(&mut self, data: &[u8]) {
4747+ self.inner.insert(data);
4848+ }
4949+5050+ /// Remove an element from the multiset.
5151+ pub fn remove(&mut self, data: &[u8]) {
5252+ self.inner.remove(data);
5353+ }
5454+5555+ /// Return the union of this hash with another.
5656+ pub fn union(&self, other: &WasmEcmh) -> Self {
5757+ Self {
5858+ inner: self.inner.union(&other.inner),
5959+ }
6060+ }
6161+6262+ /// Return the difference of this hash minus another.
6363+ pub fn difference(&self, other: &WasmEcmh) -> Self {
6464+ Self {
6565+ inner: self.inner.difference(&other.inner),
6666+ }
6767+ }
6868+6969+ /// Returns `true` if this hash represents the empty multiset.
7070+ #[wasm_bindgen(js_name = "isEmpty")]
7171+ pub fn is_empty(&self) -> bool {
7272+ self.inner.is_empty()
7373+ }
7474+7575+ /// Serialize the hash to bytes (Uint8Array in JS).
7676+ #[wasm_bindgen(js_name = "toBytes")]
7777+ pub fn to_bytes(&self) -> Vec<u8> {
7878+ self.inner.to_bytes()
7979+ }
8080+8181+ /// Deserialize a hash from bytes. Returns `undefined` if invalid.
8282+ #[wasm_bindgen(js_name = "fromBytes")]
8383+ pub fn from_bytes(bytes: &[u8]) -> Option<WasmEcmh> {
8484+ crate::RistrettoEcmh::from_bytes(bytes).map(|inner| Self { inner })
8585+ }
8686+8787+ /// Return the hash as a hex string.
8888+ #[wasm_bindgen(js_name = "toHex")]
8989+ pub fn to_hex(&self) -> String {
9090+ hex::encode(self.inner.to_bytes())
9191+ }
9292+9393+ /// Check equality with another hash.
9494+ pub fn equals(&self, other: &WasmEcmh) -> bool {
9595+ self.inner == other.inner
9696+ }
9797+}
9898+9999+// ---------------------------------------------------------------------------
100100+// P-256
101101+// ---------------------------------------------------------------------------
102102+103103+#[cfg(feature = "p256")]
104104+/// Elliptic Curve Multiset Hash using NIST P-256.
105105+#[wasm_bindgen(js_name = "EcmhP256")]
106106+pub struct WasmEcmhP256 {
107107+ inner: crate::P256Ecmh,
108108+}
109109+110110+#[cfg(feature = "p256")]
111111+impl Default for WasmEcmhP256 {
112112+ fn default() -> Self {
113113+ Self::new()
114114+ }
115115+}
116116+117117+#[cfg(feature = "p256")]
118118+#[wasm_bindgen(js_class = "EcmhP256")]
119119+impl WasmEcmhP256 {
120120+ /// Create a hash representing the empty multiset.
121121+ #[wasm_bindgen(constructor)]
122122+ pub fn new() -> Self {
123123+ Self {
124124+ inner: crate::P256Ecmh::new(),
125125+ }
126126+ }
127127+128128+ /// Create a hash from a single element.
129129+ #[wasm_bindgen(js_name = "fromElement")]
130130+ pub fn from_element(data: &[u8]) -> Self {
131131+ Self {
132132+ inner: crate::P256Ecmh::from_element(data),
133133+ }
134134+ }
135135+136136+ /// Insert an element into the multiset.
137137+ pub fn insert(&mut self, data: &[u8]) {
138138+ self.inner.insert(data);
139139+ }
140140+141141+ /// Remove an element from the multiset.
142142+ pub fn remove(&mut self, data: &[u8]) {
143143+ self.inner.remove(data);
144144+ }
145145+146146+ /// Return the union of this hash with another.
147147+ pub fn union(&self, other: &WasmEcmhP256) -> Self {
148148+ Self {
149149+ inner: self.inner.union(&other.inner),
150150+ }
151151+ }
152152+153153+ /// Return the difference of this hash minus another.
154154+ pub fn difference(&self, other: &WasmEcmhP256) -> Self {
155155+ Self {
156156+ inner: self.inner.difference(&other.inner),
157157+ }
158158+ }
159159+160160+ /// Returns `true` if this hash represents the empty multiset.
161161+ #[wasm_bindgen(js_name = "isEmpty")]
162162+ pub fn is_empty(&self) -> bool {
163163+ self.inner.is_empty()
164164+ }
165165+166166+ /// Serialize the hash to bytes (Uint8Array in JS).
167167+ #[wasm_bindgen(js_name = "toBytes")]
168168+ pub fn to_bytes(&self) -> Vec<u8> {
169169+ self.inner.to_bytes()
170170+ }
171171+172172+ /// Deserialize a hash from bytes. Returns `undefined` if invalid.
173173+ #[wasm_bindgen(js_name = "fromBytes")]
174174+ pub fn from_bytes(bytes: &[u8]) -> Option<WasmEcmhP256> {
175175+ crate::P256Ecmh::from_bytes(bytes).map(|inner| Self { inner })
176176+ }
177177+178178+ /// Return the hash as a hex string.
179179+ #[wasm_bindgen(js_name = "toHex")]
180180+ pub fn to_hex(&self) -> String {
181181+ hex::encode(self.inner.to_bytes())
182182+ }
183183+184184+ /// Check equality with another hash.
185185+ pub fn equals(&self, other: &WasmEcmhP256) -> bool {
186186+ self.inner == other.inner
187187+ }
188188+}
189189+190190+// ---------------------------------------------------------------------------
191191+// secp256k1 (K-256)
192192+// ---------------------------------------------------------------------------
193193+194194+#[cfg(feature = "k256")]
195195+/// Elliptic Curve Multiset Hash using secp256k1.
196196+#[wasm_bindgen(js_name = "EcmhK256")]
197197+pub struct WasmEcmhK256 {
198198+ inner: crate::K256Ecmh,
199199+}
200200+201201+#[cfg(feature = "k256")]
202202+impl Default for WasmEcmhK256 {
203203+ fn default() -> Self {
204204+ Self::new()
205205+ }
206206+}
207207+208208+#[cfg(feature = "k256")]
209209+#[wasm_bindgen(js_class = "EcmhK256")]
210210+impl WasmEcmhK256 {
211211+ /// Create a hash representing the empty multiset.
212212+ #[wasm_bindgen(constructor)]
213213+ pub fn new() -> Self {
214214+ Self {
215215+ inner: crate::K256Ecmh::new(),
216216+ }
217217+ }
218218+219219+ /// Create a hash from a single element.
220220+ #[wasm_bindgen(js_name = "fromElement")]
221221+ pub fn from_element(data: &[u8]) -> Self {
222222+ Self {
223223+ inner: crate::K256Ecmh::from_element(data),
224224+ }
225225+ }
226226+227227+ /// Insert an element into the multiset.
228228+ pub fn insert(&mut self, data: &[u8]) {
229229+ self.inner.insert(data);
230230+ }
231231+232232+ /// Remove an element from the multiset.
233233+ pub fn remove(&mut self, data: &[u8]) {
234234+ self.inner.remove(data);
235235+ }
236236+237237+ /// Return the union of this hash with another.
238238+ pub fn union(&self, other: &WasmEcmhK256) -> Self {
239239+ Self {
240240+ inner: self.inner.union(&other.inner),
241241+ }
242242+ }
243243+244244+ /// Return the difference of this hash minus another.
245245+ pub fn difference(&self, other: &WasmEcmhK256) -> Self {
246246+ Self {
247247+ inner: self.inner.difference(&other.inner),
248248+ }
249249+ }
250250+251251+ /// Returns `true` if this hash represents the empty multiset.
252252+ #[wasm_bindgen(js_name = "isEmpty")]
253253+ pub fn is_empty(&self) -> bool {
254254+ self.inner.is_empty()
255255+ }
256256+257257+ /// Serialize the hash to bytes (Uint8Array in JS).
258258+ #[wasm_bindgen(js_name = "toBytes")]
259259+ pub fn to_bytes(&self) -> Vec<u8> {
260260+ self.inner.to_bytes()
261261+ }
262262+263263+ /// Deserialize a hash from bytes. Returns `undefined` if invalid.
264264+ #[wasm_bindgen(js_name = "fromBytes")]
265265+ pub fn from_bytes(bytes: &[u8]) -> Option<WasmEcmhK256> {
266266+ crate::K256Ecmh::from_bytes(bytes).map(|inner| Self { inner })
267267+ }
268268+269269+ /// Return the hash as a hex string.
270270+ #[wasm_bindgen(js_name = "toHex")]
271271+ pub fn to_hex(&self) -> String {
272272+ hex::encode(self.inner.to_bytes())
273273+ }
274274+275275+ /// Check equality with another hash.
276276+ pub fn equals(&self, other: &WasmEcmhK256) -> bool {
277277+ self.inner == other.inner
278278+ }
279279+}