From d0f6e8c8a61d58ae836c6c2fe1196336013c315c Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Fri, 1 Apr 2022 14:43:34 +1030 Subject: [PATCH] cln-rpc: Add Sha256 and Secret types --- cln-rpc/src/primitives.rs | 88 ++++++++++++++++++- contrib/msggen/msggen/grpc.py | 15 +++- contrib/msggen/msggen/model.py | 3 + contrib/msggen/msggen/rust.py | 2 + contrib/pyln-testing/pyln/testing/fixtures.py | 9 ++ 5 files changed, 112 insertions(+), 5 deletions(-) diff --git a/cln-rpc/src/primitives.rs b/cln-rpc/src/primitives.rs index 155bb675f..0631e2b2f 100644 --- a/cln-rpc/src/primitives.rs +++ b/cln-rpc/src/primitives.rs @@ -180,10 +180,90 @@ impl ShortChannelId { } } -pub type Secret = [u8; 32]; -pub type Txid = [u8; 32]; -pub type Hash = [u8; 32]; -pub type NodeId = Pubkey; +#[derive(Clone, Debug)] +pub struct Secret([u8; 32]); + +impl TryFrom> for Secret { + type Error = crate::Error; + fn try_from(v: Vec) -> Result { + if v.len() != 32 { + Err(anyhow!("Unexpected secret length: {}", hex::encode(v))) + } else { + Ok(Secret(v.try_into().unwrap())) + } + } +} + +impl Serialize for Secret { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&hex::encode(&self.0)) + } +} + +impl<'de> Deserialize<'de> for Secret { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + use serde::de::Error; + let s: String = Deserialize::deserialize(deserializer)?; + let h = hex::decode(s).map_err(|_| Error::custom("not a valid hex string"))?; + Ok(Secret(h.try_into().map_err(|_| { + Error::custom("not a valid hex-encoded hash") + })?)) + } +} + +impl Secret { + pub fn to_vec(self) -> Vec { + self.0.to_vec() + } +} + +#[derive(Clone, Debug)] +pub struct Sha256([u8; 32]); +impl Sha256 { + pub fn to_vec(self) -> Vec { + self.0.to_vec() + } +} + +impl TryFrom> for Sha256 { + type Error = crate::Error; + fn try_from(v: Vec) -> Result { + if v.len() != 32 { + Err(anyhow!("Unexpected hash length: {}", hex::encode(v))) + } else { + Ok(Sha256(v.try_into().unwrap())) + } + } +} + +impl Serialize for Sha256 { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&hex::encode(&self.0)) + } +} + +impl<'de> Deserialize<'de> for Sha256 { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + use serde::de::Error; + let s: String = Deserialize::deserialize(deserializer)?; + let h = hex::decode(s).map_err(|_| Error::custom("not a valid hex string"))?; + Ok(Sha256(h.try_into().map_err(|_| { + Error::custom("not a valid hex-encoded hash") + })?)) + } +} #[derive(Clone, Debug, PartialEq)] pub struct Outpoint { diff --git a/contrib/msggen/msggen/grpc.py b/contrib/msggen/msggen/grpc.py index d3369570c..56317f71e 100644 --- a/contrib/msggen/msggen/grpc.py +++ b/contrib/msggen/msggen/grpc.py @@ -27,6 +27,8 @@ typemap = { "outpoint": "Outpoint", "feerate": "Feerate", "outputdesc": "OutputDesc", + "secret": "bytes", + "hash": "bytes", } @@ -274,10 +276,11 @@ class GrpcConverterGenerator: # array. The current item is called `i` mapping = { 'hex': f'hex::decode(i).unwrap()', + 'secret': f'i.clone().to_vec()', }.get(typ, f'i.into()') if f.required: - self.write(f"{name}: c.{name}.iter().map(|i| {mapping}).collect(), // Rule #3 \n", numindent=3) + self.write(f"{name}: c.{name}.iter().map(|i| {mapping}).collect(), // Rule #3 for type {typ} \n", numindent=3) else: self.write(f"{name}: c.{name}.as_ref().map(|arr| arr.iter().map(|i| {mapping}).collect()).unwrap_or(vec![]), // Rule #3 \n", numindent=3) elif isinstance(f, EnumField): @@ -306,6 +309,10 @@ class GrpcConverterGenerator: 'txid?': f'c.{name}.as_ref().map(|v| hex::decode(&v).unwrap())', 'short_channel_id': f'c.{name}.to_string()', 'short_channel_id?': f'c.{name}.as_ref().map(|v| v.to_string())', + 'hash': f'c.{name}.clone().to_vec()', + 'hash?': f'c.{name}.clone().map(|v| v.to_vec())', + 'secret': f'c.{name}.clone().to_vec()', + 'secret?': f'c.{name}.clone().map(|v| v.to_vec())', }.get( typ, f'c.{name}.clone()' # default to just assignment @@ -385,6 +392,7 @@ class GrpcUnconverterGenerator(GrpcConverterGenerator): mapping = { 'hex': f'hex::encode(s)', 'u32': f's.clone()', + 'secret': f's.clone().try_into().unwrap()' }.get(typ, f's.into()') if f.required: self.write(f"{name}: c.{name}.iter().map(|s| {mapping}).collect(), // Rule #4\n", numindent=3) @@ -422,6 +430,11 @@ class GrpcUnconverterGenerator(GrpcConverterGenerator): 'RoutehintList?': f'c.{name}.clone().map(|rl| rl.into())', 'short_channel_id': f'cln_rpc::primitives::ShortChannelId::from_str(&c.{name}).unwrap()', 'short_channel_id?': f'c.{name}.as_ref().map(|v| cln_rpc::primitives::ShortChannelId::from_str(&v).unwrap())', + 'secret': f'c.{name}.clone().try_into().unwrap()', + 'secret?': f'c.{name}.clone().map(|v| v.try_into().unwrap())', + 'hash': f'c.{name}.clone().try_into().unwrap()', + 'hash?': f'c.{name}.clone().map(|v| v.try_into().unwrap())', + 'txid': f'hex::encode(&c.{name})', }.get( typ, f'c.{name}.clone()' # default to just assignment diff --git a/contrib/msggen/msggen/model.py b/contrib/msggen/msggen/model.py index d26ea5eee..4c8902bee 100644 --- a/contrib/msggen/msggen/model.py +++ b/contrib/msggen/msggen/model.py @@ -218,6 +218,7 @@ class EnumField(Field): values = ",".join([v for v in self.values if v is not None]) return f"Enum[path={self.path}, required={self.required}, values=[{values}]]" + class UnionField(Field): """A type that can be one of a number of types. @@ -278,6 +279,8 @@ class PrimitiveField(Field): "feerate", "utxo", # A string representing the tuple (txid, outnum) "outputdesc", # A dict that maps an address to an amount (bitcoind style) + "secret", + "hash", ] def __init__(self, typename, path, description): diff --git a/contrib/msggen/msggen/rust.py b/contrib/msggen/msggen/rust.py index 915bb9507..ebb005c09 100644 --- a/contrib/msggen/msggen/rust.py +++ b/contrib/msggen/msggen/rust.py @@ -47,6 +47,8 @@ typemap = { 'feerate': 'Feerate', 'outpoint': 'Outpoint', 'outputdesc': 'OutputDesc', + 'hash': 'Sha256', + 'secret': 'Secret', } header = f"""#![allow(non_camel_case_types)] diff --git a/contrib/pyln-testing/pyln/testing/fixtures.py b/contrib/pyln-testing/pyln/testing/fixtures.py index ec6271bbb..4cbb63ab0 100644 --- a/contrib/pyln-testing/pyln/testing/fixtures.py +++ b/contrib/pyln-testing/pyln/testing/fixtures.py @@ -316,6 +316,13 @@ def _extra_validator(is_request: bool): return False return instance[0:2] == "02" or instance[0:2] == "03" + def is_32byte_hex(self, instance): + """Fixed size 32 byte hex string + + This matches a variety of hex types: secrets, hashes, txid + """ + return self.is_type(instance, "hex") and len(instance) == 64 + def is_point32(checker, instance): """x-only BIP-340 public key""" if not checker.is_type(instance, "hex"): @@ -389,6 +396,8 @@ def _extra_validator(is_request: bool): is_msat = is_msat_response type_checker = jsonschema.Draft7Validator.TYPE_CHECKER.redefine_many({ "hex": is_hex, + "hash": is_32byte_hex, + "secret": is_32byte_hex, "u64": is_u64, "u32": is_u32, "u16": is_u16,