Repository: ergoplatform/sigma-rust (language: rust)

Issue:
Add public API for Diffie-Hellman Tuples transaction signing
With the new privacy protocols like Stealth Addresses seems necessary to add to have a public API for DHT transaction signing so that wallets and dApps can work with such protocols.

I'm not sure about the API specification, but accepting params as UInt8Array would be great, as it can be compatible with keys generated from outside the ErgoLib.

Additionally, here is how node's sign endpoint requires DHT params:

```ts
dht: [
  {
    secret: string;
    g: string;
    h: string;
    u: string;
    v: string;
  }
]
```


Discussion / hints:

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index fd66611ac..379591666 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -72,6 +72,7 @@ proptest = { version = "1.0", default-features = false, features = ["std"] }
 proptest-derive = "0.3"
 pretty_assertions = "1.3"
 wasm-bindgen-test = "0.3.10"
+expect-test = "1.0.1"
 
 [profile.release]
 # Tell `rustc` to optimize for small code size.
diff --git a/bindings/ergo-lib-wasm/src/secret_key.rs b/bindings/ergo-lib-wasm/src/secret_key.rs
index c41db749f..8fb9419f9 100644
--- a/bindings/ergo-lib-wasm/src/secret_key.rs
+++ b/bindings/ergo-lib-wasm/src/secret_key.rs
@@ -1,6 +1,7 @@
 //! Secret key
 use std::convert::TryInto;
 
+use ergo_lib::ergo_chain_types::EcPoint;
 use ergo_lib::ergotree_interpreter::sigma_protocol::private_input::DlogProverInput;
 use ergo_lib::wallet;
 use wasm_bindgen::prelude::*;
@@ -36,15 +37,94 @@ impl SecretKey {
             .ok_or_else(|| JsValue::from_str("failed to parse scalar"))
     }
 
+    /// Parse Diffie-Hellman tuple secret key from bytes.
+    /// secret is expected as SEC-1-encoded scalar of 32 bytes,
+    /// g,h,u,v are expected as 33-byte compressed points
+    pub fn dht_from_bytes(
+        secret: &[u8],
+        g: &[u8],
+        h: &[u8],
+        u: &[u8],
+        v: &[u8],
+    ) -> Result<SecretKey, JsValue> {
+        let sized_secret: &[u8; DlogProverInput::SIZE_BYTES] = secret.try_into().map_err(|_| {
+            JsValue::from_str(&format!(
+                "expected secret byte array of size {}, found {}",
+                DlogProverInput::SIZE_BYTES,
+                secret.len()
+            ))
+        })?;
+        let sized_g: &[u8; EcPoint::GROUP_SIZE] = g.try_into().map_err(|_| {
+            JsValue::from_str(&format!(
+                "expected g byte array of size {}, found {}",
+                EcPoint::GROUP_SIZE,
+                g.len()
+            ))
+        })?;
+        let sized_h: &[u8; EcPoint::GROUP_SIZE] = h.try_into().map_err(|_| {
+            JsValue::from_str(&format!(
+                "expected h byte array of size {}, found {}",
+                EcPoint::GROUP_SIZE,
+                h.len()
+            ))
+        })?;
+        let sized_u: &[u8; EcPoint::GROUP_SIZE] = u.try_into().map_err(|_| {
+            JsValue::from_str(&format!(
+                "expected u byte array of size {}, found {}",
+                EcPoint::GROUP_SIZE,
+                u.len()
+            ))
+        })?;
+        let sized_v: &[u8; EcPoint::GROUP_SIZE] = v.try_into().map_err(|_| {
+            JsValue::from_str(&format!(
+                "expected v byte array of size {}, found {}",
+                EcPoint::GROUP_SIZE,
+                v.len()
+            ))
+        })?;
+        wallet::secret_key::SecretKey::dht_from_bytes_fields(
+            sized_secret,
+            sized_g,
+            sized_h,
+            sized_u,
+            sized_v,
+        )
+        .map(SecretKey::from)
+        .ok_or_else(|| JsValue::from_str("failed to parse Diffie-Hellman tuple"))
+    }
+
     /// Address (encoded public image)
     pub fn get_address(&self) -> Address {
         self.0.get_address_from_public_image().into()
     }
 
-    /// Encode from a serialized key
+    /// Parse secret key from bytes (expected 32 bytes for Dlog, 32(secret)+33(g)+33(h)+33(u)+33(v)=164 bytes for DHT)
+    /// secret is expected as SEC-1-encoded scalar of 32 bytes,
+    /// g,h,u,v are expected as 33-byte compressed points
+    pub fn from_bytes(bytes: &[u8]) -> Result<SecretKey, JsValue> {
+        wallet::secret_key::SecretKey::from_bytes(bytes)
+            .map(SecretKey)
+            .map_err(|e| JsValue::from_str(&format!("failed to parse SecretKey from bytes: {}", e)))
+    }
+
+    /// Serialized secret key (32 bytes for Dlog, 32(secret)+33(g)+33(h)+33(u)+33(v)=164 bytes for DHT)
+    /// DHT format is the same as in from_bytes
     pub fn to_bytes(&self) -> Vec<u8> {
         self.0.to_bytes()
     }
+
+    /// Parse secret key from JSON string (Dlog expected as base16-encoded bytes, DHT in node REST API format)
+    pub fn from_json(json_str: &str) -> Result<SecretKey, JsValue> {
+        serde_json::from_str(json_str)
+            .map(SecretKey)
+            .map_err(|e| JsValue::from_str(&format!("failed to parse SecretKey from JSON: {}", e)))
+    }
+
+    /// Encode secret key to JSON string (Dlog as base16-encoded bytes, DHT in node REST API format)
+    pub fn to_json(&self) -> Result<String, JsValue> {
+        serde_json::to_string(&self.0)
+            .map_err(|e| JsValue::from_str(&format!("failed to encode SecretKey to JSON: {}", e)))
+    }
 }
 
 /// SecretKey collection
diff --git a/ergo-lib/Cargo.toml b/ergo-lib/Cargo.toml
index 464991347..7c2b15732 100644
--- a/ergo-lib/Cargo.toml
+++ b/ergo-lib/Cargo.toml
@@ -61,3 +61,4 @@ sigma-test-util = { workspace = true }
 pretty_assertions = { workspace = true }
 bs58 = { workspace = true }
 byteorder = { workspace = true }
+expect-test = { workspace = true }
diff --git a/ergo-lib/src/wallet/secret_key.rs b/ergo-lib/src/wallet/secret_key.rs
index 2383d92d6..54473f8e5 100644
--- a/ergo-lib/src/wallet/secret_key.rs
+++ b/ergo-lib/src/wallet/secret_key.rs
@@ -1,13 +1,30 @@
 //! Secret types
+
+use derive_more::From;
+use ergo_chain_types::EcPoint;
+use ergotree_interpreter::sigma_protocol::private_input::DhTupleProverInput;
 use ergotree_interpreter::sigma_protocol::private_input::DlogProverInput;
 use ergotree_interpreter::sigma_protocol::private_input::PrivateInput;
 use ergotree_ir::chain::address::Address;
+use ergotree_ir::ergo_tree::ErgoTree;
+use ergotree_ir::mir::constant::Constant;
+use ergotree_ir::mir::expr::Expr;
+use ergotree_ir::serialization::SigmaSerializable;
+use thiserror::Error;
 
 /// Types of secrets
-#[derive(PartialEq, Eq, Debug, Clone)]
+#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "json", serde(untagged))]
+#[derive(PartialEq, Eq, Debug, Clone, From)]
 pub enum SecretKey {
-    /// Secret exponent of a group element, i.e. secret w such as h = g^^w, where g is group generator, h is a public key.
+    /// Secret exponent of a group element, i.e. secret w such as h = g^^w, where g is group generator,
+    /// h is a public key.
     DlogSecretKey(DlogProverInput),
+    /// Diffie-Hellman tuple and secret
+    /// Used in a proof that of equality of discrete logarithms (i.e., a proof of a Diffie-Hellman tuple):
+    /// given group elements g, h, u, v, the proof convinces a verifier that the prover knows `w` such
+    /// that `u = g^w` and `v = h^w`, without revealing `w`
+    DhtSecretKey(DhTupleProverInput),
 }
 
 impl SecretKey {
@@ -16,40 +33,105 @@ impl SecretKey {
         SecretKey::DlogSecretKey(DlogProverInput::random())
     }
 
+    /// Generates random DhTupleProverInput
+    pub fn random_dht() -> SecretKey {
+        SecretKey::DhtSecretKey(DhTupleProverInput::random())
+    }
+
     /// Parse DlogSecretKey from bytes (SEC-1-encoded scalar)
     pub fn dlog_from_bytes(bytes: &[u8; DlogProverInput::SIZE_BYTES]) -> Option<SecretKey> {
         DlogProverInput::from_bytes(bytes).map(SecretKey::DlogSecretKey)
     }
 
-    /// Address (encoded public image)
+    /// Parse DhtSecretKey  from bytes
+    /// expected 32(secret)+33(g)+33(h)+33(u)+33(v)=164 bytes
+    /// secret is expected as SEC-1-encoded scalar of 32 bytes,
+    /// g,h,u,v are expected as 33-byte compressed points
+    pub fn dht_from_bytes(bytes: &[u8; DhTupleProverInput::SIZE_BYTES]) -> Option<SecretKey> {
+        DhTupleProverInput::from_bytes(bytes).map(SecretKey::DhtSecretKey)
+    }
+
+    /// Parse from bytes
+    /// secret is expected as SEC-1-encoded scalar of 32 bytes,
+    /// g,h,u,v are expected as 33-byte compressed points
+    pub fn dht_from_bytes_fields(
+        w_bytes: &[u8; DlogProverInput::SIZE_BYTES],
+        g_bytes: &[u8; EcPoint::GROUP_SIZE],
+        h_bytes: &[u8; EcPoint::GROUP_SIZE],
+        u_bytes: &[u8; EcPoint::GROUP_SIZE],
+        v_bytes: &[u8; EcPoint::GROUP_SIZE],
+    ) -> Option<SecretKey> {
+        DhTupleProverInput::from_bytes_fields(w_bytes, g_bytes, h_bytes, u_bytes, v_bytes)
+            .map(SecretKey::DhtSecretKey)
+    }
+
+    /// Address from public image:
+    /// P2PK address for DlogSecretKey
+    /// P2S address with ProveDhTuple ergo tree for DhtSecretKey
+    #[allow(clippy::unwrap_used)]
     pub fn get_address_from_public_image(&self) -> Address {
         match self {
-            SecretKey::DlogSecretKey(dpi) => Address::P2Pk(dpi.public_image()),
+            SecretKey::DlogSecretKey(dlog) => Address::P2Pk(dlog.public_image()),
+            SecretKey::DhtSecretKey(dht) => {
+                let expr: Expr = Constant::from(dht.public_image().clone()).into();
+                let ergo_tree: ErgoTree = expr.try_into().unwrap();
+                Address::P2S(ergo_tree.sigma_serialize_bytes().unwrap())
+            }
         }
     }
 
     /// Encode from a serialized key
     pub fn to_bytes(&self) -> Vec<u8> {
         match self {
-            SecretKey::DlogSecretKey(key) => key.to_bytes().to_vec(),
+            SecretKey::DlogSecretKey(dlog) => dlog.to_bytes().to_vec(),
+            SecretKey::DhtSecretKey(dht) => dht.to_bytes().to_vec(),
+        }
+    }
+
+    /// Parse secret key from bytes
+    /// expected 32 bytes for Dlog, 32(secret)+33(g)+33(h)+33(u)+33(v)=164 bytes for DHT
+    /// secret is expected as SEC-1-encoded scalar of 32 bytes,
+    /// g,h,u,v are expected as 33-byte compressed points
+    pub fn from_bytes(bytes: &[u8]) -> Result<SecretKey, SecretKeyParsingError> {
+        if bytes.len() == DlogProverInput::SIZE_BYTES {
+            let mut dlog_bytes = [0u8; DlogProverInput::SIZE_BYTES];
+            dlog_bytes.copy_from_slice(bytes);
+            Ok(SecretKey::dlog_from_bytes(&dlog_bytes)
+                .ok_or(SecretKeyParsingError::DlogParsingError)?)
+        } else if bytes.len() == DhTupleProverInput::SIZE_BYTES {
+            let mut dht_bytes = [0u8; DhTupleProverInput::SIZE_BYTES];
+            dht_bytes.copy_from_slice(bytes);
+            Ok(SecretKey::DhtSecretKey(
+                DhTupleProverInput::from_bytes(&dht_bytes)
+                    .ok_or(SecretKeyParsingError::DhtParsingError)?,
+            ))
+        } else {
+            Err(SecretKeyParsingError::InvalidLength)
         }
     }
 }
 
+/// Error type for SecretKey parsing
+#[allow(missing_docs)]
+#[derive(Error, Eq, PartialEq, Debug, Clone)]
+pub enum SecretKeyParsingError {
+    #[error("Dlog parsing error")]
+    DlogParsingError,
+    #[error("DHT secret parsing error")]
+    DhtParsingError,
+    #[error("Invalid length, expected either 32(Dlog) or 164(DHT) bytes")]
+    InvalidLength,
+}
+
 impl From<SecretKey> for PrivateInput {
     fn from(s: SecretKey) -> Self {
         match s {
-            SecretKey::DlogSecretKey(dpi) => PrivateInput::DlogProverInput(dpi),
+            SecretKey::DlogSecretKey(dlog) => PrivateInput::DlogProverInput(dlog),
+            SecretKey::DhtSecretKey(dht) => PrivateInput::DhTupleProverInput(dht),
         }
     }
 }
 
-impl From<DlogProverInput> for SecretKey {
-    fn from(pi: DlogProverInput) -> Self {
-        SecretKey::DlogSecretKey(pi)
-    }
-}
-
 #[cfg(test)]
 #[allow(clippy::unwrap_used)]
 mod tests {
@@ -59,8 +141,71 @@ mod tests {
     #[test]
     fn dlog_roundtrip() {
         let sk = SecretKey::random_dlog();
-        let sk_copy =
+        let sk_copy1 =
             SecretKey::dlog_from_bytes(&sk.to_bytes().as_slice().try_into().unwrap()).unwrap();
+        let sk_copy2 = SecretKey::from_bytes(sk.to_bytes().as_slice()).unwrap();
+        assert_eq!(sk, sk_copy1);
+        assert_eq!(sk, sk_copy2);
+    }
+
+    #[test]
+    fn dht_roundtrip() {
+        let sk = SecretKey::random_dht();
+        let sk_copy1 =
+            SecretKey::dht_from_bytes(&sk.to_bytes().as_slice().try_into().unwrap()).unwrap();
+        let sk_copy2 = SecretKey::from_bytes(sk.to_bytes().as_slice()).unwrap();
+        assert_eq!(sk, sk_copy1);
+        assert_eq!(sk, sk_copy2);
+    }
+}
+
+#[cfg(test)]
+#[allow(clippy::unwrap_used)]
+#[cfg(feature = "json")]
+mod json_tests {
+    use crate::wallet::secret_key::SecretKey;
+    use pretty_assertions::assert_eq;
+
+    #[test]
+    fn json_dlog_roundtrip() {
+        let sk = SecretKey::random_dlog();
+        let sk_json = serde_json::to_string(&sk).unwrap();
+        dbg!(&sk_json);
+        let sk_copy: SecretKey = serde_json::from_str(&sk_json).unwrap();
+        assert_eq!(sk, sk_copy);
+    }
+
+    #[test]
+    fn json_dht_roundtrip() {
+        let sk = SecretKey::random_dht();
+        let sk_json = serde_json::to_string(&sk).unwrap();
+        dbg!(&sk_json);
+        let sk_copy: SecretKey = serde_json::from_str(&sk_json).unwrap();
         assert_eq!(sk, sk_copy);
     }
+
+    #[test]
+    fn json_dht_golden() {
+        let sk_json = r#"{
+  "secret": "b2a93a9a37b4656c7abf4e259b9c066cd8bf4e02449d5956aaf453a73764bfeb",
+  "g": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+  "h": "0288a812f57b66b4c68fd9e097c79a6e2847013fa4112a43c45cd41c9ba8c79b69",
+  "u": "0381fff110959bc06d99c5580c462c0196d8434bc5d470fb10a160019276e648c2",
+  "v": "02c6626ad387bb6b2eccc2fdd238c97ee4a81bfded401843bc8bb71f1cc7269924"
+}"#;
+        let sk: SecretKey = serde_json::from_str(sk_json).unwrap();
+        assert!(matches!(sk, SecretKey::DhtSecretKey(_)));
+        let sk_json_copy = serde_json::to_string_pretty(&sk).unwrap();
+        assert_eq!(sk_json, sk_json_copy);
+    }
+
+    #[test]
+    fn json_dlog_golden() {
+        let sk_json = r#""0cd81ce156fed4017520e561e9c492222027751ed0dd71b5a9b3a61da68b5850""#;
+        dbg!(&sk_json);
+        let sk: SecretKey = serde_json::from_str(sk_json).unwrap();
+        assert!(matches!(sk, SecretKey::DlogSecretKey(_)));
+        let sk_json_copy = serde_json::to_string_pretty(&sk).unwrap();
+        assert_eq!(sk_json, sk_json_copy);
+    }
 }
diff --git a/ergotree-interpreter/src/sigma_protocol/private_input.rs b/ergotree-interpreter/src/sigma_protocol/private_input.rs
index d5fd6f78d..7f8a1beed 100644
--- a/ergotree-interpreter/src/sigma_protocol/private_input.rs
+++ b/ergotree-interpreter/src/sigma_protocol/private_input.rs
@@ -3,6 +3,8 @@ use std::convert::TryInto;
 use std::fmt::Formatter;
 
 use elliptic_curve::group::ff::PrimeField;
+use ergo_chain_types::EcPoint;
+use ergotree_ir::serialization::SigmaSerializable;
 use ergotree_ir::sigma_protocol::dlog_group;
 use ergotree_ir::sigma_protocol::sigma_boolean::ProveDhTuple;
 use ergotree_ir::sigma_protocol::sigma_boolean::ProveDlog;
@@ -18,6 +20,8 @@ use super::crypto_utils;
 use super::wscalar::Wscalar;
 
 /// Secret key of discrete logarithm signature protocol
+#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "json", serde(transparent))]
 #[derive(PartialEq, Eq, Clone, derive_more::From)]
 pub struct DlogProverInput {
     /// secret key value
@@ -102,10 +106,13 @@ impl DlogProverInput {
 /// given group elements g, h, u, v, the proof convinces a verifier that the prover knows `w` such
 /// that `u = g^w` and `v = h^w`, without revealing `w`
 #[derive(PartialEq, Eq, Clone)]
+#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))]
 pub struct DhTupleProverInput {
     /// Diffie-Hellman tuple's secret
+    #[cfg_attr(feature = "json", serde(rename = "secret"))]
     pub w: Wscalar,
     /// Diffie-Hellman tuple
+    #[cfg_attr(feature = "json", serde(flatten))]
     pub common_input: ProveDhTuple,
 }
 
@@ -117,6 +124,9 @@ impl std::fmt::Debug for DhTupleProverInput {
 }
 
 impl DhTupleProverInput {
+    /// Size in bytes: 32(secret)+33(g)+33(h)+33(u)+33(v)=164 bytes
+    pub const SIZE_BYTES: usize = DlogProverInput::SIZE_BYTES + EcPoint::GROUP_SIZE * 4;
+
     /// Create random secret and Diffie-Hellman tuple
     #[allow(clippy::many_single_char_names)]
     pub fn random() -> DhTupleProverInput {
@@ -140,6 +150,70 @@ impl DhTupleProverInput {
     pub fn public_image(&self) -> &ProveDhTuple {
         &self.common_input
     }
+
+    /// 32(secret)+33(g)+33(h)+33(u)+33(v)=164 bytes
+    #[allow(clippy::unwrap_used)]
+    pub fn to_bytes(&self) -> [u8; DhTupleProverInput::SIZE_BYTES] {
+        let mut bytes = Vec::with_capacity(DhTupleProverInput::SIZE_BYTES);
+        bytes.extend_from_slice(&self.w.as_scalar_ref().to_bytes());
+        bytes.extend_from_slice(&self.common_input.g.sigma_serialize_bytes().unwrap());
+        bytes.extend_from_slice(&self.common_input.h.sigma_serialize_bytes().unwrap());
+        bytes.extend_from_slice(&self.common_input.u.sigma_serialize_bytes().unwrap());
+        bytes.extend_from_slice(&self.common_input.v.sigma_serialize_bytes().unwrap());
+        bytes.try_into().unwrap()
+    }
+
+    /// Parse from bytes (32(secret)+33(g)+33(h)+33(u)+33(v)=164 bytes)
+    /// secret is expected as SEC-1-encoded scalar of 32 bytes,
+    /// g,h,u,v are expected as 33-byte compressed points
+    #[allow(clippy::unwrap_used)]
+    pub fn from_bytes(bytes: &[u8; DhTupleProverInput::SIZE_BYTES]) -> Option<DhTupleProverInput> {
+        let w_bytes: &[u8; DlogProverInput::SIZE_BYTES] =
+            &bytes[..DlogProverInput::SIZE_BYTES].try_into().unwrap();
+        let g_bytes: &[u8; EcPoint::GROUP_SIZE] = &bytes
+            [DlogProverInput::SIZE_BYTES..DlogProverInput::SIZE_BYTES + EcPoint::GROUP_SIZE]
+            .try_into()
+            .unwrap();
+        let h_bytes: &[u8; EcPoint::GROUP_SIZE] = &bytes[DlogProverInput::SIZE_BYTES
+            + EcPoint::GROUP_SIZE
+            ..DlogProverInput::SIZE_BYTES + EcPoint::GROUP_SIZE * 2]
+            .try_into()
+            .unwrap();
+        let u_bytes: &[u8; EcPoint::GROUP_SIZE] = &bytes[DlogProverInput::SIZE_BYTES
+            + EcPoint::GROUP_SIZE * 2
+            ..DlogProverInput::SIZE_BYTES + EcPoint::GROUP_SIZE * 3]
+            .try_into()
+            .unwrap();
+        let v_bytes: &[u8; EcPoint::GROUP_SIZE] = &bytes[DlogProverInput::SIZE_BYTES
+            + EcPoint::GROUP_SIZE * 3
+            ..DlogProverInput::SIZE_BYTES + EcPoint::GROUP_SIZE * 4]
+            .try_into()
+            .unwrap();
+        Self::from_bytes_fields(w_bytes, g_bytes, h_bytes, u_bytes, v_bytes)
+    }
+
+    /// Parse from bytes
+    /// secret is expected as SEC-1-encoded scalar of 32 bytes,
+    /// g,h,u,v are expected as 33-byte compressed points
+    pub fn from_bytes_fields(
+        w_bytes: &[u8; DlogProverInput::SIZE_BYTES],
+        g_bytes: &[u8; EcPoint::GROUP_SIZE],
+        h_bytes: &[u8; EcPoint::GROUP_SIZE],
+        u_bytes: &[u8; EcPoint::GROUP_SIZE],
+        v_bytes: &[u8; EcPoint::GROUP_SIZE],
+    ) -> Option<DhTupleProverInput> {
+        let w: Option<Wscalar> = k256::Scalar::from_repr((*w_bytes).into())
+            .map(Wscalar::from)
+            .into();
+        let g = EcPoint::sigma_parse_bytes(&g_bytes[..EcPoint::GROUP_SIZE]).ok()?;
+        let h = EcPoint::sigma_parse_bytes(&h_bytes[..EcPoint::GROUP_SIZE]).ok()?;
+        let u = EcPoint::sigma_parse_bytes(&u_bytes[..EcPoint::GROUP_SIZE]).ok()?;
+        let v = EcPoint::sigma_parse_bytes(&v_bytes[..EcPoint::GROUP_SIZE]).ok()?;
+        w.map(|w| DhTupleProverInput {
+            w,
+            common_input: ProveDhTuple::new(g, h, u, v),
+        })
+    }
 }
 
 /// Private inputs (secrets)
diff --git a/ergotree-ir/src/sigma_protocol/sigma_boolean.rs b/ergotree-ir/src/sigma_protocol/sigma_boolean.rs
index 8208e0ab0..fe0f71866 100644
--- a/ergotree-ir/src/sigma_protocol/sigma_boolean.rs
+++ b/ergotree-ir/src/sigma_protocol/sigma_boolean.rs
@@ -57,6 +57,7 @@ impl From<EcPoint> for ProveDlog {
 /// Used in a proof that of equality of discrete logarithms (i.e., a proof of a Diffie-Hellman tuple):
 /// given group elements g, h, u, v, the proof convinces a verifier that the prover knows `w` such
 /// that `u = g^w` and `v = h^w`, without revealing `w`
+#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))]
 #[derive(PartialEq, Eq, Debug, Clone)]
 pub struct ProveDhTuple {
     /// Generator g


Test patch: