Elliptic Curve Multiset Hash (ECMH) for Rust and WebAssembly
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Rust 100.0%
2 1 0

Clone this repository

https://tangled.org/ngerakines.me/ecmh-rs https://tangled.org/did:plc:cbkjy5n7bk3ax2wplmtjofq2/ecmh-rs
git@tangled.org:ngerakines.me/ecmh-rs git@tangled.org:did:plc:cbkjy5n7bk3ax2wplmtjofq2/ecmh-rs

For self-hosted knots, clone URLs may differ based on your setup.

Download tar.gz
README.md

ecmh#

Elliptic Curve Multiset Hash (ECMH) for Rust and WebAssembly — a homomorphic hash function for multisets based on Maitin-Shepard et al. (2016).

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.

Features#

  • Incremental: insert and remove elements without recomputing from scratch
  • Order-independent: H({a, b}) == H({b, a})
  • Homomorphic: H(A ∪ B) == H(A) + H(B)
  • Multiple curve backends behind feature flags:
Type alias Curve Output size Feature
RistrettoEcmh Ristretto255 32 bytes ristretto (default)
P256Ecmh NIST P-256 33 bytes p256
K256Ecmh secp256k1 33 bytes k256
  • Operator overloading: +, -, +=, -= for combining hashes
  • Custom curves: implement the CurveBackend trait
  • WebAssembly: first-class wasm support via wasm-pack

Usage#

Rust#

[dependencies]
ecmh = "0.1"

For a specific curve backend:

[dependencies]
ecmh = { version = "0.1", default-features = false, features = ["p256"] }

WebAssembly#

Build the wasm package with wasm-pack:

wasm-pack build --target web --features wasm

This produces a pkg/ directory containing the .wasm binary, JS glue, and TypeScript definitions ready for npm publish or direct use.

For additional curve backends in the wasm build:

wasm-pack build --target web --features wasm,p256,k256

Example (Rust)#

use ecmh::RistrettoEcmh;

// Build a multiset hash incrementally
let mut hash = RistrettoEcmh::new();
hash.insert(b"alice");
hash.insert(b"bob");

// Order doesn't matter
let mut hash2 = RistrettoEcmh::new();
hash2.insert(b"bob");
hash2.insert(b"alice");
assert_eq!(hash, hash2);

// Combine disjoint sets
let a = RistrettoEcmh::from_element(b"alice");
let b = RistrettoEcmh::from_element(b"bob");
assert_eq!(hash, a + b);

// Remove an element
let mut h = hash.clone();
h.remove(b"bob");
assert_eq!(h, RistrettoEcmh::from_element(b"alice"));

// Build from an iterator
let from_iter = RistrettoEcmh::from_elements([
    b"alice".as_slice(),
    b"bob".as_slice(),
]);
assert_eq!(from_iter, hash);

// Serialize / deserialize
let bytes = hash.to_bytes();
let recovered = RistrettoEcmh::from_bytes(&bytes).unwrap();
assert_eq!(recovered, hash);

Example (JavaScript / TypeScript)#

import init, { EcmhRistretto } from './pkg/ecmh.js';

await init();

const enc = new TextEncoder();

// Build a multiset hash incrementally
const hash = new EcmhRistretto();
hash.insert(enc.encode("alice"));
hash.insert(enc.encode("bob"));

// Order doesn't matter
const hash2 = new EcmhRistretto();
hash2.insert(enc.encode("bob"));
hash2.insert(enc.encode("alice"));
console.log(hash.equals(hash2)); // true

// Combine disjoint sets
const a = EcmhRistretto.fromElement(enc.encode("alice"));
const b = EcmhRistretto.fromElement(enc.encode("bob"));
console.log(a.union(b).equals(hash)); // true

// Serialize
console.log(hash.toHex());   // 64-char hex string
console.log(hash.toBytes()); // Uint8Array(32)

// Deserialize
const recovered = EcmhRistretto.fromBytes(hash.toBytes());
console.log(recovered.equals(hash)); // true

The wasm build exposes one class per enabled curve backend:

JS class Curve Feature flags
EcmhRistretto Ristretto255 wasm
EcmhP256 NIST P-256 wasm + p256
EcmhK256 secp256k1 wasm + k256

Custom curve backend#

Implement the CurveBackend trait to use any elliptic curve:

use ecmh::CurveBackend;

#[derive(Clone, Default, Debug)]
struct MyCurve;

impl CurveBackend for MyCurve {
    type Point = /* your point type */;
    const POINT_SIZE: usize = /* compressed size */;

    fn identity() -> Self::Point { /* ... */ }
    fn hash_to_point(data: &[u8]) -> Self::Point { /* ... */ }
    fn add(a: &Self::Point, b: &Self::Point) -> Self::Point { /* ... */ }
    fn sub(a: &Self::Point, b: &Self::Point) -> Self::Point { /* ... */ }
    fn is_identity(p: &Self::Point) -> bool { /* ... */ }
    fn to_bytes(p: &Self::Point) -> Vec<u8> { /* ... */ }
    fn from_bytes(bytes: &[u8]) -> Option<Self::Point> { /* ... */ }
}

References#

License#

MIT OR Apache-2.0