diff --git a/.github/workflows/notes.yml b/.github/workflows/notes.yml index bf006951c5..1ff3d07c2d 100644 --- a/.github/workflows/notes.yml +++ b/.github/workflows/notes.yml @@ -39,7 +39,7 @@ jobs: # - published crates are excluded # Doing this in one go is useful because the JSON file with search # indexes is overwritten on each cargo doc invocation. - cargo doc --no-deps -p tendermint -p ark-sponge -p decaf377 -p poseidon377 -p penumbra-proto -p penumbra + cargo doc --no-deps -p tendermint -p ark-sponge -p decaf377 -p poseidon377 -p penumbra-crypto -p penumbra-proto -p penumbra - name: Move API docs to subdirectory run: | if [ -d "firebase-tmp" ]; then rm -rf firebase-tmp; fi diff --git a/Cargo.toml b/Cargo.toml index 22bb1814d6..888dc69962 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,9 +2,14 @@ members = [ "proto", + "crypto", "penumbra", ] [patch.crates-io] tracing = { git = "https://github.com/tokio-rs/tracing/", branch = "v0.1.x" } tracing-subscriber = { git = "https://github.com/tokio-rs/tracing/", branch = "v0.1.x" } + +# The "ours" branch is based off of v0.3.0 +ark-ff = { git = "https://github.com/penumbra-zone/algebra", branch = "ours" } +ark-serialize = { git = "https://github.com/penumbra-zone/algebra", branch = "ours" } diff --git a/crypto/Cargo.toml b/crypto/Cargo.toml new file mode 100644 index 0000000000..ac9b4fa413 --- /dev/null +++ b/crypto/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "penumbra-crypto" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +decaf377 = { git = "https://github.com/penumbra-zone/decaf377" } +poseidon377 = { git = "https://github.com/penumbra-zone/poseidon377" } +hex = "0.4" +blake2b_simd = "0.5" +ark-ff = "0.3" +once_cell = "1.8" +# only needed because ark-ff doesn't display correctly +num-bigint = "0.4" diff --git a/crypto/src/asset.rs b/crypto/src/asset.rs new file mode 100644 index 0000000000..1fff953c9a --- /dev/null +++ b/crypto/src/asset.rs @@ -0,0 +1,87 @@ +//! Asset types and identifiers. + +use ark_ff::fields::PrimeField; +use once_cell::sync::Lazy; + +use crate::Fq; + +/// An identifier for an IBC asset type. +/// +/// This is similar to, but different from, the design in [ADR001]. As in +/// ADR001, a denomination trace is hashed to a fixed-size identifier, but +/// unlike ADR001, we hash to a field element rather than a byte string. +/// +/// A denomination trace looks like +/// +/// - `denom` (native chain A asset) +/// - `transfer/channelToA/denom` (chain B representation of chain A asset) +/// - `transfer/channelToB/transfer/channelToA/denom` (chain C representation of chain B representation of chain A asset) +/// +/// ADR001 defines the IBC asset ID as the SHA-256 hash of the denomination +/// trace. Instead, Penumbra hashes to a field element, so that asset IDs can +/// be more easily used inside of a circuit. +/// +/// [ADR001]: +/// https://github.com/cosmos/ibc-go/blob/main/docs/architecture/adr-001-coin-source-tracing.md +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct Id(pub Fq); + +impl std::fmt::Debug for Id { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use ark_ff::BigInteger; + let bytes = self.0.into_repr().to_bytes_le(); + f.write_fmt(format_args!("asset::Id({})", hex::encode(&bytes))) + } +} + +// XXX define a DenomTrace structure ? + +impl From<&[u8]> for Id { + fn from(slice: &[u8]) -> Id { + // Convert an asset name to an asset ID by hashing to a scalar + Id(Fq::from_le_bytes_mod_order( + // XXX choice of hash function? + blake2b_simd::Params::default() + .personal(b"penumbra.asset") + .hash(slice) + .as_bytes(), + )) + } +} + +/// The domain separator used to hash asset ids to value generators. +static VALUE_GENERATOR_DOMAIN_SEP: Lazy = Lazy::new(|| { + Fq::from_le_bytes_mod_order(blake2b_simd::blake2b(b"penumbra.value.generator").as_bytes()) +}); + +impl Id { + /// Compute the value commitment generator for this asset. + pub fn value_generator(&self) -> decaf377::Element { + use crate::poseidon_hash::hash_1; + let hash = hash_1(&VALUE_GENERATOR_DOMAIN_SEP, self.0); + decaf377::Element::map_to_group_cdh(&hash) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn make_up_some_fake_asset_ids() { + // marked for future deletion + // not really a test, just a way to exercise the code + + let pen_trace = b"pen"; + let atom_trace = b"HubPort/HubChannel/atom"; + + let pen_id = Id::from(&pen_trace[..]); + let atom_id = Id::from(&atom_trace[..]); + + dbg!(pen_id); + dbg!(atom_id); + + dbg!(pen_id.value_generator()); + dbg!(atom_id.value_generator()); + } +} diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs new file mode 100644 index 0000000000..c01ad8a8c9 --- /dev/null +++ b/crypto/src/lib.rs @@ -0,0 +1,6 @@ +pub use decaf377::{Fq, Fr}; + +pub mod asset; +pub mod value; + +mod poseidon_hash; diff --git a/crypto/src/poseidon_hash.rs b/crypto/src/poseidon_hash.rs new file mode 100644 index 0000000000..0e47b2c551 --- /dev/null +++ b/crypto/src/poseidon_hash.rs @@ -0,0 +1,37 @@ +// XXX move into poseidon377 crate? + +use crate::Fq; + +use poseidon377::ark_sponge::{ + poseidon::PoseidonSponge, CryptographicSponge, FieldBasedCryptographicSponge, +}; + +pub fn hash_1(domain_separator: &Fq, value: Fq) -> Fq { + // we want to set the capacity to domain_separator and the rate to value, + // then run the sponge and extract the rate. it's a bit hard to do this + // using the ark-sponge api, which is trying to do a higher-level duplex + // construction and doesn't allow access to the underlying sponge + + let mut sponge = PoseidonSponge::new(&poseidon377::params::rate_1()); + + // arkworks sponge api doesn't let us call permute + // + // best we can do now is to look in the source to see how the rate and + // capacity are arranged and try to plumb the functionality we want through + // the higher-level API + // + // arkworks uses (rate || capacity) instead of (capacity || rate) + // + // this also gives incompatible outputs, but let's deal with that later + + // set the capacity + assert_eq!(sponge.state.len(), 2); + sponge.state[1] = *domain_separator; + + // now use absorb to set the rate (hopefully) + sponge.absorb(&value); + // and squeeze an element + let out_vec = sponge.squeeze_native_field_elements(1); + + out_vec.into_iter().next().unwrap() +} diff --git a/crypto/src/value.rs b/crypto/src/value.rs new file mode 100644 index 0000000000..6078697007 --- /dev/null +++ b/crypto/src/value.rs @@ -0,0 +1,115 @@ +//! Values (?) + +use std::ops::Deref; + +use ark_ff::PrimeField; +use once_cell::sync::Lazy; + +use crate::{asset, Fq, Fr}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Value { + pub amount: u64, + pub asset_id: asset::Id, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Commitment(pub decaf377::Element); + +static VALUE_BLINDING_GENERATOR: Lazy = Lazy::new(|| { + let s = Fq::from_le_bytes_mod_order(blake2b_simd::blake2b(b"penumbra.val.blinding").as_bytes()); + decaf377::Element::map_to_group_cdh(&s) +}); + +impl Value { + #[allow(non_snake_case)] + pub fn commit(&self, blinding: Fr) -> Commitment { + let G_v = self.asset_id.value_generator(); + let H = VALUE_BLINDING_GENERATOR.deref(); + + let v = Fr::from(self.amount); + let C = v * G_v + blinding * H; + + Commitment(C) + } +} + +impl std::ops::Add for Commitment { + type Output = Commitment; + fn add(self, rhs: Commitment) -> Self::Output { + Commitment(self.0 + rhs.0) + } +} + +impl std::ops::Sub for Commitment { + type Output = Commitment; + fn sub(self, rhs: Commitment) -> Self::Output { + Commitment(self.0 - rhs.0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sum_value_commitments() { + use ark_ff::Field; + + let pen_trace = b"pen"; + let atom_trace = b"HubPort/HubChannel/atom"; + + let pen_id = asset::Id::from(&pen_trace[..]); + let atom_id = asset::Id::from(&atom_trace[..]); + + // some values of different types + let v1 = Value { + amount: 10, + asset_id: pen_id, + }; + let v2 = Value { + amount: 8, + asset_id: pen_id, + }; + let v3 = Value { + amount: 2, + asset_id: pen_id, + }; + let v4 = Value { + amount: 13, + asset_id: atom_id, + }; + let v5 = Value { + amount: 17, + asset_id: atom_id, + }; + let v6 = Value { + amount: 30, + asset_id: atom_id, + }; + + // some random-looking blinding factors + let b1 = Fr::from(-129).inverse().unwrap(); + let b2 = Fr::from(-199).inverse().unwrap(); + let b3 = Fr::from(-121).inverse().unwrap(); + let b4 = Fr::from(-179).inverse().unwrap(); + let b5 = Fr::from(-379).inverse().unwrap(); + let b6 = Fr::from(-879).inverse().unwrap(); + + // form commitments + let c1 = v1.commit(b1); + let c2 = v2.commit(b2); + let c3 = v3.commit(b3); + let c4 = v4.commit(b4); + let c5 = v5.commit(b5); + let c6 = v6.commit(b6); + + // values sum to 0, so this is a commitment to 0... + let c0 = c1 - c2 - c3 + c4 + c5 - c6; + // with the following synthetic blinding factor: + let b0 = b1 - b2 - b3 + b4 + b5 - b6; + + // so c0 = 0 * G_v1 + 0 * G_v2 + b0 * H + assert_eq!(c0.0, b0 * VALUE_BLINDING_GENERATOR.deref()); + } +} diff --git a/penumbra/Cargo.toml b/penumbra/Cargo.toml index 769960b3d5..2dd922dc49 100644 --- a/penumbra/Cargo.toml +++ b/penumbra/Cargo.toml @@ -15,8 +15,6 @@ publish = false # Workspace dependencies penumbra-proto = { path = "../proto" } # Penumbra dependencies -decaf377 = { git = "https://github.com/penumbra-zone/decaf377" } -poseidon377 = { git = "https://github.com/penumbra-zone/poseidon377" } tower-abci = { git = "https://github.com/penumbra-zone/tower-abci/" } tendermint-proto = { git = "https://github.com/penumbra-zone/tendermint-rs.git", branch = "abci-domain-types" } tendermint = { git = "https://github.com/penumbra-zone/tendermint-rs.git", branch = "abci-domain-types" }