diff --git a/.gitignore b/.gitignore index 088ba6b..7b3afb9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk + +.vscode diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e69de29 diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..391d32d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "mina-signer" +version = "0.1.0" +authors = ["Joseph Spadavecchia "] +edition = "2018" + +[lib] +path = "src/lib.rs" + +[dependencies] +oracle = { git = "https://github.com/o1-labs/proof-systems.git", rev = "902f3f7bbdb55edc979af4b3a7e2b3f0c5c76cc3" } +mina-curves = { git = "https://github.com/o1-labs/proof-systems.git", rev = "902f3f7bbdb55edc979af4b3a7e2b3f0c5c76cc3" } +commitment_dlog = { git = "https://github.com/o1-labs/proof-systems.git", rev = "902f3f7bbdb55edc979af4b3a7e2b3f0c5c76cc3" } + +ark-ec = { version = "0.3.0", features = [ "parallel" ] } +ark-ff = { version = "0.3.0", features = [ "parallel", "asm" ] } +ark-serialize = { version = "0.3.0" } + +rand = { version = "0.8.0" } +array-init = { version = "0.1.1" } +blake2 = { version = "0.9.1" } +hex = { version = "0.4" } +bitvec = { version = "0.22.3" } +sha2 = { version = "0.9.6" } +bs58 = { version = "0.4.0" } +byteordered = { version = "0.6.0" } +byteorder = { version = "1.4.3" } diff --git a/README.md b/README.md index 1288d1a..2ece892 100644 --- a/README.md +++ b/README.md @@ -1 +1,82 @@ -# mina-signer \ No newline at end of file +# Mina signer + +This crate provides an API and framework for Mina signing. It follows the algorithm outlined in the [Mina Signature Specification](https://github.com/MinaProtocol/mina/blob/develop/docs/specs/signatures/description.md). + +## Simple interface + +The simple interface uses the default signer configuration compatible with mainnet and testnet transaction signatures. + +```rust +use rand; +use mina_signer::{NetworkId, Keypair, Signer}; +use mina_signer::NetworkId; + +let mut ctx = mina_signer::create(NetworkId::TESTNET); +let sig = ctx.sign(key_pair, transaction); + +assert_eq!(ctx.verify(sig, key_pair.public, transaction), true); +``` + +## Advanced interface + +The advanced interface allows specification of an alternative cryptographic sponge and parameters, for example, in order to create signatures that can be verified more efficiently using the Kimchi proof system. + +```rust +use rand; +use mina_signer::{NetworkId, Keypair, Signer}; +use oracle::{pasta, poseidon}; + +let mut ctx = mina_signer::custom::( + pasta::fp5::params(), + NetworkId::TESTNET, +); + +let sig = ctx.sign(key_pair, transaction); +assert_eq!(ctx.verify(sig, key_pair.public, transaction), true); +``` + +## Framework + +The framework allows you to easily define a new signature type simply by implementing the `Hashable` and `Signable` traits. + +For example, if you wanted to create Mina signatures for a `Foo` structure you would do the following. + +```rust +use mina_signer::{Hashable, NetworkId, ROInput, Signable}; + +#[derive(Clone, Copy)] +struct Foo { + foo: u32, + bar: u64, +} + +impl Hashable for Foo { + fn to_roinput(self) -> ROInput { + let mut roi = ROInput::new(); + + roi.append_u32(self.foo); + roi.append_u64(self.bar); + + roi + } +} + +impl Signable for Foo { + fn domain_string(network_id: NetworkId) -> &'static str { + match network_id { + NetworkId::MAINNET => "FooSigMainnet", + NetworkId::TESTNET => "FooSigTestnet", + } + } +} +``` + +For more details please see the rustdoc mina-signer documentation. + +# Unit tests + +There is a standard set of signature unit tests in the `./tests` directory. + +These can be run with + +`cargo test --test tests ` \ No newline at end of file diff --git a/src/domain.rs b/src/domain.rs new file mode 100644 index 0000000..63c534c --- /dev/null +++ b/src/domain.rs @@ -0,0 +1,134 @@ +//! Signer domain and helpers +//! +//! Shorthands and helpers for base and scalar field elements + +use ark_ec::AffineCurve; +use ark_ff::PrimeField; // for into_repr() + +use mina_curves::pasta::pallas as Pallas; + +/// Affine curve point type +pub use Pallas::Affine as CurvePoint; +/// Base field element type +pub type BaseField = ::BaseField; +/// Scalar field element type +pub type ScalarField = ::ScalarField; + +use ark_serialize::CanonicalSerialize; + +/// Field element helpers +pub trait FieldHelpers { + /// Deserialize from bytes + fn from_bytes(bytes: &[u8]) -> Result; + + /// Deserialize from hex + fn from_hex(hex: &str) -> Result; + + /// Serialize to bytes + fn to_bytes(self) -> Vec; + + /// Serialize to hex + fn to_hex(self) -> String; +} + +impl FieldHelpers for F { + fn from_bytes(bytes: &[u8]) -> Result { + F::deserialize(&mut &*bytes).map_err(|_| "failed to deserialize field bytes") + } + + fn from_hex(hex: &str) -> Result { + let bytes: Vec = hex::decode(hex).map_err(|_| "Failed to decode field hex")?; + + F::deserialize(&mut &bytes[..]).map_err(|_| "Failed to deserialize field bytes") + } + + fn to_bytes(self) -> Vec { + let mut bytes: Vec = vec![]; + self.into_repr() + .serialize(&mut bytes) + .expect("Failed to serialize field"); + + bytes + } + + fn to_hex(self) -> String { + hex::encode(self.to_bytes()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn field_from_hex() { + assert_eq!( + BaseField::from_hex(""), + Err("Failed to deserialize field bytes") + ); + assert_eq!( + BaseField::from_hex("1428fadcf0c02396e620f14f176fddb5d769b7de2027469d027a80142ef8f07"), + Err("Failed to decode field hex") + ); + assert_eq!( + BaseField::from_hex( + "0f5314f176fddb5d769b7de2027469d027ad428fadcf0c02396e6280142efb7d8" + ), + Err("Failed to decode field hex") + ); + assert_eq!( + BaseField::from_hex("g64244176fddb5d769b7de2027469d027ad428fadcf0c02396e6280142efb7d8"), + Err("Failed to decode field hex") + ); + assert_eq!( + BaseField::from_hex("0cdaf334e9632268a5aa959c2781fb32bf45565fe244ae42c849d3fdc7c644fd"), + Err("Failed to deserialize field bytes") + ); + + assert_eq!( + BaseField::from_hex("2eaedae42a7461d5952d27b97ecad068b698ebb94e8a0e4c45388bb613de7e08") + .is_ok(), + true + ); + } + + #[test] + fn scalar_from_hex() { + assert_eq!( + ScalarField::from_hex(""), + Err("Failed to deserialize field bytes") + ); + assert_eq!( + ScalarField::from_hex( + "1428fadcf0c02396e620f14f176fddb5d769b7de2027469d027a80142ef8f07" + ), + Err("Failed to decode field hex") + ); + assert_eq!( + ScalarField::from_hex( + "0f5314f176fddb5d769b7de2027469d027ad428fadcf0c02396e6280142efb7d8" + ), + Err("Failed to decode field hex") + ); + assert_eq!( + ScalarField::from_hex( + "g64244176fddb5d769b7de2027469d027ad428fadcf0c02396e6280142efb7d8" + ), + Err("Failed to decode field hex") + ); + assert_eq!( + ScalarField::from_hex( + "817bfe2410826e69320c0ccdaf824da720d9647202ed7b967d5bddf6714424dd" + ), + Err("Failed to deserialize field bytes") + ); + + assert_eq!( + ScalarField::from_hex( + "2cc3342ad3cd516175b8f0d0189bc3bdcb7947a4cc96c7cfc8d5df10cc443832" + ) + .is_ok(), + true + ); + } +} diff --git a/src/keypair.rs b/src/keypair.rs new file mode 100644 index 0000000..b6c14f6 --- /dev/null +++ b/src/keypair.rs @@ -0,0 +1,138 @@ +//! Keypair structures and algorithms +//! +//! Definition of secret key, keypairs and related helpers + +use core::fmt; + +use crate::{CurvePoint, FieldHelpers, PubKey, ScalarField, SecKey}; +use ark_ec::{AffineCurve, ProjectiveCurve}; +use ark_ff::UniformRand; +use rand::{self, CryptoRng, RngCore}; + +/// Keypair structure +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct Keypair { + /// Secret key + pub secret: SecKey, + /// Public key + pub public: PubKey, +} + +impl Keypair { + /// Create a keypair from scalar field `secret` element and curve point `public` + pub fn new(secret: ScalarField, public: CurvePoint) -> Self { + Self { + secret: SecKey::new(secret), + public: PubKey::new(public), + } + } + + /// Generate a random keypair + pub fn rand(rng: &mut (impl RngCore + CryptoRng)) -> Self { + let secret: ScalarField = ScalarField::rand(rng); + let public: CurvePoint = CurvePoint::prime_subgroup_generator() + .mul(secret) + .into_affine(); + + Keypair { + secret: SecKey::new(secret), + public: PubKey::new(public), + } + } + + /// Deserialize a keypair from secret key hex + pub fn from_hex(secret_hex: &str) -> Result { + let mut bytes: Vec = hex::decode(secret_hex).map_err(|_| "Invalid secret key hex")?; + bytes.reverse(); // mina scalars hex format is in big-endian order + + let secret = ScalarField::from_bytes(&bytes).map_err(|_| "Invalid secret key hex")?; + let public: CurvePoint = CurvePoint::prime_subgroup_generator() + .mul(secret) + .into_affine(); + + Ok(Keypair::new(secret, public)) + } + + /// Obtain the Mina address corresponding to the keypair's public key + pub fn get_address(self) -> String { + self.public.to_address() + } +} + +impl fmt::Debug for Keypair { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Omit the secret key for security + write!(f, "{:?}", self.public) + } +} + +impl fmt::Display for Keypair { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Omit the secret key for security + write!(f, "{}", self.public) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_hex() { + assert_eq!(Keypair::from_hex(""), Err("Invalid secret key hex")); + assert_eq!( + Keypair::from_hex("1428fadcf0c02396e620f14f176fddb5d769b7de2027469d027a80142ef8f07"), + Err("Invalid secret key hex") + ); + assert_eq!( + Keypair::from_hex("0f5314f176fddb5d769b7de2027469d027ad428fadcf0c02396e6280142efb7d8"), + Err("Invalid secret key hex") + ); + assert_eq!( + Keypair::from_hex("g64244176fddb5d769b7de2027469d027ad428fadcf0c02396e6280142efb7d8"), + Err("Invalid secret key hex") + ); + assert_eq!( + Keypair::from_hex("dd4244176fddb5d769b7de2027469d027ad428fadcc0c02396e6280142efb718"), + Err("Invalid secret key hex") + ); + + Keypair::from_hex("164244176fddb5d769b7de2027469d027ad428fadcc0c02396e6280142efb718") + .expect("failed to decode keypair secret key"); + } + + #[test] + fn get_address() { + macro_rules! assert_get_address_eq { + ($sec_key_hex:expr, $target_address:expr) => { + let kp = Keypair::from_hex($sec_key_hex).expect("failed to create keypair"); + assert_eq!(kp.get_address(), $target_address); + }; + } + + assert_get_address_eq!( + "164244176fddb5d769b7de2027469d027ad428fadcc0c02396e6280142efb718", + "B62qnzbXmRNo9q32n4SNu2mpB8e7FYYLH8NmaX6oFCBYjjQ8SbD7uzV" + ); + assert_get_address_eq!( + "3ca187a58f09da346844964310c7e0dd948a9105702b716f4d732e042e0c172e", + "B62qicipYxyEHu7QjUqS7QvBipTs5CzgkYZZZkPoKVYBu6tnDUcE9Zt" + ); + assert_get_address_eq!( + "336eb4a19b3d8905824b0f2254fb495573be302c17582748bf7e101965aa4774", + "B62qrKG4Z8hnzZqp1AL8WsQhQYah3quN1qUj3SyfJA8Lw135qWWg1mi" + ); + assert_get_address_eq!( + "1dee867358d4000f1dafa5978341fb515f89eeddbe450bd57df091f1e63d4444", + "B62qoqiAgERjCjXhofXiD7cMLJSKD8hE8ZtMh4jX5MPNgKB4CFxxm1N" + ); + assert_get_address_eq!( + "20f84123a26e58dd32b0ea3c80381f35cd01bc22a20346cc65b0a67ae48532ba", + "B62qkiT4kgCawkSEF84ga5kP9QnhmTJEYzcfgGuk6okAJtSBfVcjm1M" + ); + assert_get_address_eq!( + "3414fc16e86e6ac272fda03cf8dcb4d7d47af91b4b726494dab43bf773ce1779", + "B62qoG5Yk4iVxpyczUrBNpwtx2xunhL48dydN53A2VjoRwF8NUTbVr4" + ); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d00a0bd --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,173 @@ +#![warn(missing_docs)] + +//! Mina signer library for verification and signing +//! +//! **Example** +//! +//! ``` +//! use rand; +//! use mina_signer::{Hashable, Keypair, NetworkId, ROInput, Signable, Signer}; +//! +//! #[derive(Clone, Copy)] +//! struct Thing { +//! foo: u32, +//! bar: u64, +//! } +//! +//! impl Hashable for Thing { +//! fn to_roinput(self) -> ROInput { +//! let mut roi = ROInput::new(); +//! +//! roi.append_u32(self.foo); +//! roi.append_u64(self.bar); +//! +//! roi +//! } +//! } +//! +//! impl Signable for Thing { +//! fn domain_string(network_id: NetworkId) -> &'static str { +//! match network_id { +//! NetworkId::MAINNET => "ThingSigMainnet", +//! NetworkId::TESTNET => "ThingSigTestnet", +//! } +//! } +//! } +//! +//! let kp = Keypair::rand(&mut rand::rngs::OsRng); +//! let thang = Thing { foo: 31, bar: 45 }; +//! +//! let mut ctx = mina_signer::create(NetworkId::TESTNET); +//! let sig = ctx.sign(kp, thang); +//! assert_eq!(ctx.verify(sig, kp.public, thang), true); +//! ``` + +pub mod domain; +pub mod keypair; +pub mod pubkey; +pub mod roinput; +pub mod schnorr; +pub mod seckey; +pub mod signature; + +pub use domain::{BaseField, CurvePoint, FieldHelpers, ScalarField}; +pub use keypair::Keypair; +pub use pubkey::{CompressedPubKey, PubKey}; +pub use roinput::ROInput; +pub use schnorr::Schnorr; +pub use seckey::SecKey; +pub use signature::Signature; + +use oracle::{ + pasta, + poseidon::{ + ArithmeticSponge, ArithmeticSpongeParams, PlonkSpongeConstantsBasic, Sponge, + SpongeConstants, + }, +}; + +/// Mina network (or blockchain) identifier +#[derive(Copy, Clone)] +pub enum NetworkId { + /// Id for all testnets + TESTNET = 0x00, + + /// Id for mainnet + MAINNET = 0x01, +} + +impl From for u8 { + fn from(id: NetworkId) -> u8 { + id as u8 + } +} + +/// Interface for hashable objects +/// +/// See example in [ROInput] documentation +pub trait Hashable: Copy { + /// Serialization to random oracle input + fn to_roinput(self) -> ROInput; +} + +/// Interface for signed objects +/// +/// **Example** +/// +/// ``` +/// use mina_signer::{Hashable, NetworkId, ROInput, Signable}; +/// +/// #[derive(Clone, Copy)] +/// struct Example; +/// +/// impl Hashable for Example { +/// fn to_roinput(self) -> ROInput { +/// let roi = ROInput::new(); +/// // Serialize example members +/// roi +/// } +/// } +/// +/// impl Signable for Example { +/// fn domain_string(network_id: NetworkId) -> &'static str { +/// match network_id { +/// NetworkId::MAINNET => "ExampleSigMainnet", +/// NetworkId::TESTNET => "ExampleSigTestnet", +/// } +/// } +/// } +/// ``` +/// +/// Please see [here](crate) for a more complete example. +pub trait Signable: Hashable { + /// Returns the unique domain string for this input type on network specified by `network_id`. + /// + /// The domain string length must be `<= 20`. + fn domain_string(network_id: NetworkId) -> &'static str; +} + +/// Signer interface for signing [Signable] inputs and verifying [Signatures](Signature) using [Keypairs](Keypair) and [PubKeys](PubKey) +pub trait Signer { + /// Sign `input` (see [Signable]) using keypair `kp` and return the corresponding signature. + fn sign(&mut self, kp: Keypair, input: S) -> Signature; + + /// Verify that the signature `sig` on `input` (see [Signable]) is signed with the secret key corresponding to `pub_key`. + /// Return `true` if the signature is valid and `false` otherwise. + fn verify(&mut self, sig: Signature, pub_key: PubKey, input: S) -> bool; +} + +/// Create a default signer context for network instance identified by `network_id` +/// +/// **Example** +/// +/// ``` +/// use mina_signer::NetworkId; +/// +/// let mut ctx = mina_signer::create(NetworkId::MAINNET); +/// ``` +pub fn create(network_id: NetworkId) -> impl Signer { + Schnorr::::new( + ArithmeticSponge::::new(pasta::fp::params()), + network_id, + ) +} + +/// Create a custom signer context for network instance identified by `network_id` using custom sponge parameters `params` +/// +/// **Example** +/// +/// ``` +/// use mina_signer::NetworkId; +/// use oracle::{pasta, poseidon}; +/// +/// let mut ctx = mina_signer::custom::( +/// pasta::fp5::params(), +/// NetworkId::TESTNET, +/// ); +/// ``` +pub fn custom( + params: ArithmeticSpongeParams, + network_id: NetworkId, +) -> impl Signer { + Schnorr::::new(ArithmeticSponge::::new(params), network_id) +} diff --git a/src/pubkey.rs b/src/pubkey.rs new file mode 100644 index 0000000..1123335 --- /dev/null +++ b/src/pubkey.rs @@ -0,0 +1,152 @@ +//! Public key structures and algorithms +//! +//! Definition of public key structure and helpers + +use ark_ff::{BigInteger, PrimeField}; +use bs58; +use core::fmt; +use sha2::{Digest, Sha256}; +use std::ops::Neg; + +use crate::{BaseField, CurvePoint, FieldHelpers}; + +/// Length of Mina addresses +pub const MINA_ADDRESS_LEN: usize = 55; + +/// Public key +#[derive(Copy, Clone, fmt::Debug, PartialEq, Eq)] +pub struct PubKey(CurvePoint); + +impl PubKey { + /// Create a public key from curve point + pub fn new(point: CurvePoint) -> Self { + Self(point) + } + + /// Deserialize Mina address into public key + pub fn from_address(address: &str) -> Result { + if address.len() != MINA_ADDRESS_LEN { + return Err("Invalid address length"); + } + + let bytes = bs58::decode(address) + .into_vec() + .map_err(|_| "Invalid address encoding")?; + + let (raw, checksum) = (&bytes[..bytes.len() - 4], &bytes[bytes.len() - 4..]); + let hash = Sha256::digest(&Sha256::digest(raw)[..]); + if checksum != &hash[..4] { + return Err("Invalid address checksum"); + } + + let (version, x_bytes, y_parity) = ( + &raw[..3], + &raw[3..bytes.len() - 5], + raw[bytes.len() - 5] == 0x01, + ); + if version != [0xcb, 0x01, 0x01] { + return Err("Invalid address version info"); + } + + let x = BaseField::from_bytes(x_bytes).map_err(|_| "invalid x-coordinate bytes")?; + let mut pt = + CurvePoint::get_point_from_x(x, y_parity).ok_or("Invalid address x-coordinate")?; + + if pt.y.into_repr().is_even() == y_parity { + pt.y = pt.y.neg(); + } + + Ok(PubKey::new(pt)) + } + + /// Convert public key into curve point + pub fn to_point(self) -> CurvePoint { + self.0 + } + + /// Convert public key into compressed public key + pub fn to_compressed(self) -> CompressedPubKey { + let point = self.to_point(); + CompressedPubKey { + x: point.x, + is_odd: !point.y.into_repr().is_even(), + } + } + + /// Serialize public key into corresponding Mina address + pub fn to_address(self) -> String { + let point = self.to_point(); + to_address(point.x, point.y.into_repr().is_odd()) + } +} + +impl fmt::Display for PubKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let point = self.to_point(); + let mut x_bytes = point.x.to_bytes(); + let mut y_bytes = point.y.to_bytes(); + x_bytes.reverse(); + y_bytes.reverse(); + + write!(f, "{}{}", hex::encode(x_bytes), hex::encode(y_bytes)) + } +} + +/// Compressed public keys consist of x-coordinate and y-coordinate parity. +#[derive(Clone, Copy)] +pub struct CompressedPubKey { + /// X-coordinate + pub x: BaseField, + + /// Parity of y-coordinate + pub is_odd: bool, +} + +fn to_address(x: BaseField, is_odd: bool) -> String { + let mut raw: Vec = vec![ + 0xcb, // version for base58 check + 0x01, // non_zero_curve_point version + 0x01, // compressed_poly version + ]; + + // pub key x-coordinate + raw.extend(x.to_bytes()); + + // pub key y-coordinate parity + raw.push(is_odd as u8); + + // 4-byte checksum + let hash = Sha256::digest(&Sha256::digest(&raw[..])[..]); + raw.extend(&hash[..4]); + + bs58::encode(raw).into_string() +} + +impl CompressedPubKey { + /// Serialize compressed public key into corresponding Mina address + pub fn to_address(self) -> String { + to_address(self.x, self.is_odd) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_address() { + macro_rules! assert_from_address_check { + ($address:expr) => { + let pk = PubKey::from_address($address).expect("failed to create pubkey"); + assert_eq!(pk.to_address(), $address); + }; + } + + assert_from_address_check!("B62qnzbXmRNo9q32n4SNu2mpB8e7FYYLH8NmaX6oFCBYjjQ8SbD7uzV"); + assert_from_address_check!("B62qicipYxyEHu7QjUqS7QvBipTs5CzgkYZZZkPoKVYBu6tnDUcE9Zt"); + assert_from_address_check!("B62qoG5Yk4iVxpyczUrBNpwtx2xunhL48dydN53A2VjoRwF8NUTbVr4"); + assert_from_address_check!("B62qrKG4Z8hnzZqp1AL8WsQhQYah3quN1qUj3SyfJA8Lw135qWWg1mi"); + assert_from_address_check!("B62qoqiAgERjCjXhofXiD7cMLJSKD8hE8ZtMh4jX5MPNgKB4CFxxm1N"); + assert_from_address_check!("B62qkiT4kgCawkSEF84ga5kP9QnhmTJEYzcfgGuk6okAJtSBfVcjm1M"); + } +} diff --git a/src/roinput.rs b/src/roinput.rs new file mode 100644 index 0000000..b725b8a --- /dev/null +++ b/src/roinput.rs @@ -0,0 +1,867 @@ +//! Random oracle input structures and algorithms +//! +//! Definition of random oracle input structure and +//! methods for serializing into bytes and field elements + +use crate::{BaseField, FieldHelpers, ScalarField}; +use ark_ff::PrimeField; +use bitvec::{prelude::*, view::AsBits}; + +/// Random oracle input structure +/// +/// The random oracle input encapsulates the serialization format and methods using during signing. +/// +/// When implementing the [crate::Hashable] trait in order to enable signing for a type, you must implement +/// its `to_roinput()` serialization method using the [ROInput] functions below. +/// +/// For example, +/// +/// ```rust +/// use mina_signer::{CompressedPubKey, Hashable, NetworkId, ROInput, Signable}; +/// +/// #[derive(Clone, Copy)] +/// pub struct MyExample { +/// pub account: CompressedPubKey, +/// pub amount: u64, +/// pub nonce: u32, +/// } +/// +/// impl Hashable for MyExample { +/// fn to_roinput(self) -> ROInput { +/// let mut roi = ROInput::new(); +/// +/// roi.append_field(self.account.x); +/// roi.append_bit(self.account.is_odd); +/// roi.append_u64(self.amount); +/// roi.append_u32(self.nonce); +/// +/// roi +/// } +/// } +/// ``` +/// **Details:** For technical reasons related to our proof system and performance, fields are +/// serialized for signing differently than other types. Additionally, during signing all members +/// of the random oracle input get serialized together in two different ways: both as *bytes* and +/// as a vector of *field elements*. The random oracle input encapsulates and automates this +/// complexity. + +#[derive(Default)] +pub struct ROInput { + fields: Vec, + bits: BitVec, +} + +impl ROInput { + /// Create a new empty random oracle input + pub fn new() -> Self { + ROInput { + fields: vec![], + bits: BitVec::new(), + } + } + + /// Append a base field element + pub fn append_field(&mut self, f: BaseField) { + self.fields.push(f); + } + + /// Append a scalar field element + pub fn append_scalar(&mut self, s: ScalarField) { + // mina scalars are 255 bytes + let bytes = s.to_bytes(); // TODO: Combine these two into one-liner + let bits = &bytes.as_bits::()[..ScalarField::size_in_bits()]; + self.bits.extend(bits); + } + + /// Append a single bit + pub fn append_bit(&mut self, b: bool) { + self.bits.push(b); + } + + /// Append bytes + pub fn append_bytes(&mut self, bytes: &[u8]) { + self.bits.extend_from_bitslice(bytes.as_bits::()); + } + + /// Append a 32-bit unsigned integer + pub fn append_u32(&mut self, x: u32) { + self.append_bytes(&x.to_le_bytes()); + } + + /// Append a 64-bit unsigned integer + pub fn append_u64(&mut self, x: u64) { + self.append_bytes(&x.to_le_bytes()); + } + + /// Serialize random oracle input to bytes + pub fn to_bytes(&self) -> Vec { + let mut bits: BitVec = self.fields.iter().fold(BitVec::new(), |mut acc, fe| { + acc.extend_from_bitslice(&fe.to_bytes().as_bits::()[..BaseField::size_in_bits()]); + + acc + }); + + bits.extend(&self.bits); + + bits.into() + } + + /// Serialize random oracle input to vector of base field elements + pub fn to_fields(&self) -> Vec { + let mut fields: Vec = self.fields.clone(); + + let bits_as_fields = self + .bits + .chunks(BaseField::size_in_bits() - 1) + .into_iter() + .fold(vec![], |mut acc, chunk| { + // Workaround: chunk.clone() does not appear to respect + // the chunk's boundaries when it's not byte-aligned. + // + // That is, + // + // let mut bv = chunk.clone().to_bitvec(); + // bv.resize(BaseField::size_in_bits(), false); + // fields.push(BaseField::from_bytes(bv.into())); + // + // doesn't work. + // + // Instead we must do + + let mut bv = BitVec::::new(); + bv.resize(chunk.len(), false); + bv.clone_from_bitslice(chunk); + + // extend to the size of a field; + bv.resize(BaseField::size_in_bits(), false); + + acc.push( + BaseField::from_bytes(&bv.into_vec()) + .expect("failed to create base field element"), + ); + + acc + }); + + fields.extend(bits_as_fields); + + fields + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn append_bit() { + let mut roi: ROInput = ROInput::new(); + roi.append_bit(true); + assert!(roi.bits.len() == 1); + assert!(roi.bits.as_raw_slice() == [0x01]); + } + + #[test] + fn append_two_bits() { + let mut roi: ROInput = ROInput::new(); + roi.append_bit(false); + roi.append_bit(true); + assert!(roi.bits.len() == 2); + assert!(roi.bits.as_raw_slice() == [0x02]); + } + + #[test] + fn append_five_bits() { + let mut roi: ROInput = ROInput::new(); + roi.append_bit(false); + roi.append_bit(true); + roi.append_bit(false); + roi.append_bit(false); + roi.append_bit(true); + assert!(roi.bits.len() == 5); + assert!(roi.bits.as_raw_slice() == [0x12]); + } + + #[test] + fn append_byte() { + let mut roi: ROInput = ROInput::new(); + roi.append_bytes(&vec![0x01]); + assert!(roi.bits.len() == 8); + assert!(roi.bits.as_raw_slice() == [0x01]); + } + + #[test] + fn append_two_bytes() { + let mut roi: ROInput = ROInput::new(); + roi.append_bytes(&vec![0x10, 0xac]); + assert!(roi.bits.len() == 16); + assert!(roi.bits.as_raw_slice() == [0x10, 0xac]); + } + + #[test] + fn append_five_bytes() { + let mut roi: ROInput = ROInput::new(); + roi.append_bytes(&vec![0x10, 0xac, 0x01, 0xeb, 0xca]); + assert!(roi.bits.len() == 40); + assert!(roi.bits.as_raw_slice() == [0x10, 0xac, 0x01, 0xeb, 0xca]); + } + + #[test] + fn append_scalar() { + let scalar = ScalarField::from_hex( + "18b7ef420128e69623c0c0dcfa28d47a029d462720deb769d7b5dd6f17444216", + ) + .expect("failed to create scalar"); + let mut roi: ROInput = ROInput::new(); + roi.append_scalar(scalar); + assert_eq!(roi.bits.len(), 255); + assert_eq!( + roi.bits.as_raw_slice(), + [ + 0x18, 0xb7, 0xef, 0x42, 0x01, 0x28, 0xe6, 0x96, 0x23, 0xc0, 0xc0, 0xdc, 0xfa, 0x28, + 0xd4, 0x7a, 0x02, 0x9d, 0x46, 0x27, 0x20, 0xde, 0xb7, 0x69, 0xd7, 0xb5, 0xdd, 0x6f, + 0x17, 0x44, 0x42, 0x16 + ] + ); + assert_eq!( + roi.to_bytes(), + [ + 0x18, 0xb7, 0xef, 0x42, 0x01, 0x28, 0xe6, 0x96, 0x23, 0xc0, 0xc0, 0xdc, 0xfa, 0x28, + 0xd4, 0x7a, 0x02, 0x9d, 0x46, 0x27, 0x20, 0xde, 0xb7, 0x69, 0xd7, 0xb5, 0xdd, 0x6f, + 0x17, 0x44, 0x42, 0x16 + ] + ); + } + + #[test] + fn append_scalar_and_byte() { + let scalar = ScalarField::from_hex( + "18b7ef420128e69623c0c0dcfa28d47a029d462720deb769d7b5dd6f17444216", + ) + .expect("failed to create scalar"); + let mut roi: ROInput = ROInput::new(); + roi.append_scalar(scalar); + roi.append_bytes(&vec![0x01]); + assert!(roi.bits.len() == 263); + assert!( + roi.bits.as_raw_slice() + == [ + 0x18, 0xb7, 0xef, 0x42, 0x01, 0x28, 0xe6, 0x96, 0x23, 0xc0, 0xc0, 0xdc, 0xfa, + 0x28, 0xd4, 0x7a, 0x02, 0x9d, 0x46, 0x27, 0x20, 0xde, 0xb7, 0x69, 0xd7, 0xb5, + 0xdd, 0x6f, 0x17, 0x44, 0x42, 0x96, 0x00 + ] + ); + } + + #[test] + fn append_two_scalars() { + let scalar1 = ScalarField::from_hex( + "18b7ef420128e69623c0c0dcfa28d47a029d462720deb769d7b5dd6f17444216", + ) + .expect("failed to create scalar"); + let scalar2 = ScalarField::from_hex( + "a1b1e948835be341277548134e0effabdbcb95b742e8c5e967e9bf13eb4ae805", + ) + .expect("failed to create scalar"); + let mut roi: ROInput = ROInput::new(); + roi.append_scalar(scalar1); + roi.append_scalar(scalar2); + assert!(roi.bits.len() == 510); + assert!( + roi.bits.as_raw_slice() + == [ + 0x18, 0xb7, 0xef, 0x42, 0x01, 0x28, 0xe6, 0x96, 0x23, 0xc0, 0xc0, 0xdc, 0xfa, + 0x28, 0xd4, 0x7a, 0x02, 0x9d, 0x46, 0x27, 0x20, 0xde, 0xb7, 0x69, 0xd7, 0xb5, + 0xdd, 0x6f, 0x17, 0x44, 0x42, 0x96, 0xd0, 0xd8, 0x74, 0xa4, 0xc1, 0xad, 0xf1, + 0xa0, 0x93, 0x3a, 0xa4, 0x09, 0x27, 0x87, 0xff, 0xd5, 0xed, 0xe5, 0xca, 0x5b, + 0x21, 0xf4, 0xe2, 0xf4, 0xb3, 0xf4, 0xdf, 0x89, 0x75, 0x25, 0xf4, 0x02 + ] + ); + } + + #[test] + fn append_two_scalars_and_byte() { + let scalar1 = ScalarField::from_hex( + "60db6f4f5b8ce1c7cb747fba9e324cc3268c7a6e3f43cd82d451ae99a7b2bd1f", + ) + .expect("failed to create scalar"); + let scalar2 = ScalarField::from_hex( + "fe7775b106bceb58f3e23e5a4eb99f404b8ed8cf2afeef9c9d1800f12138cd07", + ) + .expect("failed to create scalar"); + let mut roi: ROInput = ROInput::new(); + roi.append_scalar(scalar1); + roi.append_bytes(&vec![0x2a]); + roi.append_scalar(scalar2); + assert!(roi.bits.len() == 518); + assert!( + roi.bits.as_raw_slice() + == [ + 0x60, 0xdb, 0x6f, 0x4f, 0x5b, 0x8c, 0xe1, 0xc7, 0xcb, 0x74, 0x7f, 0xba, 0x9e, + 0x32, 0x4c, 0xc3, 0x26, 0x8c, 0x7a, 0x6e, 0x3f, 0x43, 0xcd, 0x82, 0xd4, 0x51, + 0xae, 0x99, 0xa7, 0xb2, 0xbd, 0x1f, 0x15, 0xff, 0xbb, 0xba, 0x58, 0x03, 0xde, + 0x75, 0xac, 0x79, 0x71, 0x1f, 0x2d, 0xa7, 0xdc, 0x4f, 0xa0, 0x25, 0x47, 0xec, + 0x67, 0x15, 0xff, 0x77, 0xce, 0x4e, 0x0c, 0x80, 0xf8, 0x10, 0x9c, 0xe6, 0x03 + ] + ); + } + + #[test] + fn append_u32() { + let mut roi: ROInput = ROInput::new(); + roi.append_u32(1984u32); + assert!(roi.bits.len() == 32); + assert!(roi.bits.as_raw_slice() == [0xc0, 0x07, 0x00, 0x00]); + } + + #[test] + fn append_two_u32_and_bit() { + let mut roi: ROInput = ROInput::new(); + roi.append_u32(1729u32); + roi.append_bit(false); + roi.append_u32(u32::MAX); + assert!(roi.bits.len() == 65); + assert!(roi.bits.as_raw_slice() == [0xc1, 0x06, 0x00, 0x00, 0xfe, 0xff, 0xff, 0xff, 0x01]); + } + + #[test] + fn append_u64() { + let mut roi: ROInput = ROInput::new(); + roi.append_u64(6174u64); + assert!(roi.bits.len() == 64); + assert!(roi.bits.as_raw_slice() == [0x1e, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); + } + + #[test] + fn append_two_u64_and_bits() { + let mut roi: ROInput = ROInput::new(); + roi.append_bit(true); + roi.append_u64(u64::MAX / 6174u64); + roi.append_bit(false); + roi.append_u64(u64::MAX / 1111u64); + assert!(roi.bits.len() == 130); + assert!( + roi.bits.as_raw_slice() + == [ + 0xe1, 0x29, 0x89, 0xd6, 0xcb, 0x3a, 0x15, 0x00, 0x08, 0x17, 0xc4, 0x9b, 0x04, + 0xf4, 0xeb, 0x00, 0x00 + ] + ); + } + + #[test] + fn all_1() { + let mut roi: ROInput = ROInput::new(); + roi.append_bit(true); + roi.append_scalar( + ScalarField::from_hex( + "01d1755db21c8cd2a9cf5a3436178da3d70f484cd4b4c8834b799921e7d7a102", + ) + .expect("failed to create scalar"), + ); + roi.append_u64(18446744073709551557); + roi.append_bytes(&vec![0xba, 0xdc, 0x0f, 0xfe]); + roi.append_scalar( + ScalarField::from_hex( + "e70187e9b125524489d0433da76fd8287fa652eaebde147b45fa0cd86f171810", + ) + .expect("failed to create scalar"), + ); + roi.append_bit(false); + roi.append_u32(2147483647); + roi.append_bit(true); + + assert!(roi.bits.len() == 641); + assert!( + roi.bits.as_raw_slice() + == [ + 0x03, 0xa2, 0xeb, 0xba, 0x64, 0x39, 0x18, 0xa5, 0x53, 0x9f, 0xb5, 0x68, 0x6c, + 0x2e, 0x1a, 0x47, 0xaf, 0x1f, 0x90, 0x98, 0xa8, 0x69, 0x91, 0x07, 0x97, 0xf2, + 0x32, 0x43, 0xce, 0xaf, 0x43, 0x05, 0xc5, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xba, 0xdc, 0x0f, 0xfe, 0xe7, 0x01, 0x87, 0xe9, 0xb1, 0x25, 0x52, 0x44, + 0x89, 0xd0, 0x43, 0x3d, 0xa7, 0x6f, 0xd8, 0x28, 0x7f, 0xa6, 0x52, 0xea, 0xeb, + 0xde, 0x14, 0x7b, 0x45, 0xfa, 0x0c, 0xd8, 0x6f, 0x17, 0x18, 0x10, 0xff, 0xff, + 0xff, 0x7f, 0x01 + ] + ); + } + + #[test] + fn transaction_bits() { + let mut roi = ROInput::new(); + roi.append_u64(1000000); // fee + roi.append_u64(1); // fee token + roi.append_bit(true); // fee payer pk odd + roi.append_u32(0); // nonce + roi.append_u32(u32::MAX); // valid_until + roi.append_bytes(&vec![0; 34]); // memo + roi.append_bit(false); // tags[0] + roi.append_bit(false); // tags[1] + roi.append_bit(false); // tags[2] + roi.append_bit(true); // sender pk odd + roi.append_bit(false); // receiver pk odd + roi.append_u64(1); // token_id + roi.append_u64(10000000000); // amount + roi.append_bit(false); // token_locked + roi.append_scalar( + ScalarField::from_hex( + "de217a3017ca0b7a278e75f63c09890e3894be532d8dbadd30a7d450055f6d2d", + ) + .expect("failed to create scalar"), + ); + roi.append_bytes(&vec![0x01]); + assert_eq!(roi.bits.len(), 862); + assert_eq!( + roi.bits.as_raw_slice(), + [ + 0x40, 0x42, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xfe, 0xff, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf9, 0x02, + 0x95, 0x00, 0x00, 0x00, 0x00, 0xef, 0x10, 0x3d, 0x98, 0x0b, 0xe5, 0x05, 0xbd, 0x13, + 0xc7, 0x3a, 0x7b, 0x9e, 0x84, 0x44, 0x07, 0x1c, 0x4a, 0xdf, 0xa9, 0x96, 0x46, 0xdd, + 0x6e, 0x98, 0x53, 0x6a, 0xa8, 0x82, 0xaf, 0xb6, 0x56, 0x00 + ] + ) + } + + #[test] + fn append_field() { + let mut roi = ROInput::new(); + roi.append_field( + BaseField::from_hex("2eaedae42a7461d5952d27b97ecad068b698ebb94e8a0e4c45388bb613de7e08") + .expect("failed to create field"), + ); + + assert_eq!( + roi.to_bytes(), + [ + 0x2e, 0xae, 0xda, 0xe4, 0x2a, 0x74, 0x61, 0xd5, 0x95, 0x2d, 0x27, 0xb9, 0x7e, 0xca, + 0xd0, 0x68, 0xb6, 0x98, 0xeb, 0xb9, 0x4e, 0x8a, 0x0e, 0x4c, 0x45, 0x38, 0x8b, 0xb6, + 0x13, 0xde, 0x7e, 0x08 + ] + ); + } + + #[test] + fn append_two_fields() { + let mut roi = ROInput::new(); + roi.append_field( + BaseField::from_hex("0cdaf334e9632268a5aa959c2781fb32bf45565fe244ae42c849d3fdc7c6441d") + .expect("failed to create field"), + ); + roi.append_field( + BaseField::from_hex("2eaedae42a7461d5952d27b97ecad068b698ebb94e8a0e4c45388bb613de7e08") + .expect("failed to create field"), + ); + + assert_eq!( + roi.to_bytes(), + [ + 0x0c, 0xda, 0xf3, 0x34, 0xe9, 0x63, 0x22, 0x68, 0xa5, 0xaa, 0x95, 0x9c, 0x27, 0x81, + 0xfb, 0x32, 0xbf, 0x45, 0x56, 0x5f, 0xe2, 0x44, 0xae, 0x42, 0xc8, 0x49, 0xd3, 0xfd, + 0xc7, 0xc6, 0x44, 0x1d, 0x17, 0x57, 0x6d, 0x72, 0x15, 0xba, 0xb0, 0xea, 0xca, 0x96, + 0x93, 0x5c, 0x3f, 0x65, 0x68, 0x34, 0x5b, 0xcc, 0xf5, 0x5c, 0x27, 0x45, 0x07, 0xa6, + 0x22, 0x9c, 0x45, 0xdb, 0x09, 0x6f, 0x3f, 0x04 + ] + ); + } + + #[test] + fn append_three_fields() { + let mut roi = ROInput::new(); + roi.append_field( + BaseField::from_hex("1f3f142986041b54427aa2032632e34df2fa9bde9bce70c04c5034266619e529") + .expect("failed to create field"), + ); + roi.append_field( + BaseField::from_hex("37f4433b85e753a91a1d79751645f1448954c433f9492e36a933ca7f3df61a04") + .expect("failed to create field"), + ); + roi.append_field( + BaseField::from_hex("6cf4772d3e1aab98a2b514b73a4f6e0df1fb4f703ecfa762196b22c26da4341c") + .expect("failed to create field"), + ); + + assert_eq!( + roi.to_bytes(), + [ + 0x1f, 0x3f, 0x14, 0x29, 0x86, 0x04, 0x1b, 0x54, 0x42, 0x7a, 0xa2, 0x03, 0x26, 0x32, + 0xe3, 0x4d, 0xf2, 0xfa, 0x9b, 0xde, 0x9b, 0xce, 0x70, 0xc0, 0x4c, 0x50, 0x34, 0x26, + 0x66, 0x19, 0xe5, 0xa9, 0x1b, 0xfa, 0xa1, 0x9d, 0xc2, 0xf3, 0xa9, 0x54, 0x8d, 0x8e, + 0xbc, 0x3a, 0x8b, 0xa2, 0x78, 0xa2, 0x44, 0x2a, 0xe2, 0x99, 0xfc, 0x24, 0x17, 0x9b, + 0xd4, 0x19, 0xe5, 0xbf, 0x1e, 0x7b, 0x0d, 0x02, 0x1b, 0xfd, 0x5d, 0x8b, 0x8f, 0xc6, + 0x2a, 0xa6, 0x68, 0x2d, 0xc5, 0xad, 0xce, 0x93, 0x5b, 0x43, 0xfc, 0xfe, 0x13, 0x9c, + 0xcf, 0xf3, 0xa9, 0x58, 0xc6, 0x9a, 0x88, 0x70, 0x1b, 0x29, 0x0d, 0x07 + ] + ); + } + + #[test] + fn append_field_and_scalar() { + let mut roi = ROInput::new(); + roi.append_field( + BaseField::from_hex("64cde530327a36fcb88b6d769adca9b7c5d266e7d0042482203f3fd3a0d71721") + .expect("failed to create field"), + ); + roi.append_scalar( + ScalarField::from_hex( + "604355d0daa455db783fd7ee11c5bd9b04d67ba64c27c95bef95e379f98c6432", + ) + .expect("failed to create scalar"), + ); + + assert_eq!( + roi.to_bytes(), + [ + 0x64, 0xcd, 0xe5, 0x30, 0x32, 0x7a, 0x36, 0xfc, 0xb8, 0x8b, 0x6d, 0x76, 0x9a, 0xdc, + 0xa9, 0xb7, 0xc5, 0xd2, 0x66, 0xe7, 0xd0, 0x04, 0x24, 0x82, 0x20, 0x3f, 0x3f, 0xd3, + 0xa0, 0xd7, 0x17, 0x21, 0xb0, 0xa1, 0x2a, 0x68, 0x6d, 0xd2, 0xaa, 0x6d, 0xbc, 0x9f, + 0x6b, 0xf7, 0x88, 0xe2, 0xde, 0x4d, 0x02, 0xeb, 0x3d, 0x53, 0xa6, 0x93, 0xe4, 0xad, + 0xf7, 0xca, 0xf1, 0xbc, 0x7c, 0x46, 0x32, 0x19 + ] + ); + } + + #[test] + fn append_field_bit_and_scalar() { + let mut roi = ROInput::new(); + roi.append_field( + BaseField::from_hex("d897c7a8b811d8acd3eeaa4adf42292802eed80031c2ad7c8989aea1fe94322c") + .expect("failed to create field"), + ); + roi.append_bit(false); + roi.append_scalar( + ScalarField::from_hex( + "79586cc6b8b53c8991b2abe0ca76508f056ca50f06836ce4d818c2ff73d42b28", + ) + .expect("failed to create scalar"), + ); + + assert_eq!( + roi.to_bytes(), + [ + 0xd8, 0x97, 0xc7, 0xa8, 0xb8, 0x11, 0xd8, 0xac, 0xd3, 0xee, 0xaa, 0x4a, 0xdf, 0x42, + 0x29, 0x28, 0x02, 0xee, 0xd8, 0x00, 0x31, 0xc2, 0xad, 0x7c, 0x89, 0x89, 0xae, 0xa1, + 0xfe, 0x94, 0x32, 0x2c, 0x79, 0x58, 0x6c, 0xc6, 0xb8, 0xb5, 0x3c, 0x89, 0x91, 0xb2, + 0xab, 0xe0, 0xca, 0x76, 0x50, 0x8f, 0x05, 0x6c, 0xa5, 0x0f, 0x06, 0x83, 0x6c, 0xe4, + 0xd8, 0x18, 0xc2, 0xff, 0x73, 0xd4, 0x2b, 0x28 + ] + ); + } + + #[test] + fn to_bytes() { + let mut roi = ROInput::new(); + roi.append_field( + BaseField::from_hex("a5984f2bd00906f9a86e75bfb4b2c3625f1a0d1cfacc1501e8e82ae7041efc14") + .expect("failed to create field"), + ); + roi.append_field( + BaseField::from_hex("8af0bc770d49a5b9fcabfcdd033bab470b2a211ef80b710efe71315cfa818c0a") + .expect("failed to create field"), + ); + roi.append_bit(false); + roi.append_u32(314u32); + roi.append_scalar( + ScalarField::from_hex( + "c23c43a23ddc1516578b0f0d81b93cdbbc97744acc697cfc8c5dfd01cc448323", + ) + .expect("failed to create scalar"), + ); + + assert_eq!( + roi.to_bytes(), + [ + 0xa5, 0x98, 0x4f, 0x2b, 0xd0, 0x09, 0x06, 0xf9, 0xa8, 0x6e, 0x75, 0xbf, 0xb4, 0xb2, + 0xc3, 0x62, 0x5f, 0x1a, 0x0d, 0x1c, 0xfa, 0xcc, 0x15, 0x01, 0xe8, 0xe8, 0x2a, 0xe7, + 0x04, 0x1e, 0xfc, 0x14, 0x45, 0x78, 0xde, 0xbb, 0x86, 0xa4, 0xd2, 0x5c, 0xfe, 0x55, + 0xfe, 0xee, 0x81, 0x9d, 0xd5, 0xa3, 0x05, 0x95, 0x10, 0x0f, 0xfc, 0x85, 0x38, 0x07, + 0xff, 0xb8, 0x18, 0x2e, 0xfd, 0x40, 0x46, 0x05, 0x9d, 0x00, 0x00, 0x00, 0x61, 0x9e, + 0x21, 0xd1, 0x1e, 0xee, 0x0a, 0x8b, 0xab, 0xc5, 0x87, 0x86, 0xc0, 0x5c, 0x9e, 0x6d, + 0xde, 0x4b, 0x3a, 0x25, 0xe6, 0x34, 0x3e, 0x7e, 0xc6, 0xae, 0xfe, 0x00, 0x66, 0xa2, + 0xc1, 0x11 + ] + ); + } + + #[test] + fn to_fields_1_scalar() { + let mut roi = ROInput::new(); + roi.append_scalar( + ScalarField::from_hex( + "5d496dd8ff63f640c006887098092b16bc8c78504f84fa1ee3a0b54f85f0a625", + ) + .expect("failed to create scalar"), + ); + + assert_eq!( + roi.to_bytes(), + [ + 0x5d, 0x49, 0x6d, 0xd8, 0xff, 0x63, 0xf6, 0x40, 0xc0, 0x06, 0x88, 0x70, 0x98, 0x09, + 0x2b, 0x16, 0xbc, 0x8c, 0x78, 0x50, 0x4f, 0x84, 0xfa, 0x1e, 0xe3, 0xa0, 0xb5, 0x4f, + 0x85, 0xf0, 0xa6, 0x25 + ] + ); + + assert_eq!( + roi.to_fields(), + [ + BaseField::from_hex( + "5d496dd8ff63f640c006887098092b16bc8c78504f84fa1ee3a0b54f85f0a625" + ) + .expect("failed to create field"), + BaseField::from_hex( + "0000000000000000000000000000000000000000000000000000000000000000" + ) + .expect("failed to create field"), + ] + ); + } + + #[test] + fn to_fields_1_scalar_2_bits() { + let mut roi = ROInput::new(); + roi.append_scalar( + ScalarField::from_hex( + "e8a9961c8c417b0d0e3d7366f6b0e6ef90a6dad123070f715e8a9eaa02e47330", + ) + .expect("failed to create scalar"), + ); + roi.append_bit(false); + roi.append_bit(true); + + assert_eq!( + roi.to_bytes(), + [ + 0xe8, 0xa9, 0x96, 0x1c, 0x8c, 0x41, 0x7b, 0x0d, 0x0e, 0x3d, 0x73, 0x66, 0xf6, 0xb0, + 0xe6, 0xef, 0x90, 0xa6, 0xda, 0xd1, 0x23, 0x07, 0x0f, 0x71, 0x5e, 0x8a, 0x9e, 0xaa, + 0x02, 0xe4, 0x73, 0x30, 0x01 + ] + ); + + assert_eq!( + roi.to_fields(), + [ + BaseField::from_hex( + "e8a9961c8c417b0d0e3d7366f6b0e6ef90a6dad123070f715e8a9eaa02e47330" + ) + .expect("failed to create field"), + BaseField::from_hex( + "0400000000000000000000000000000000000000000000000000000000000000" + ) + .expect("failed to create field"), + ] + ); + } + + #[test] + fn to_fields_2_scalars() { + let mut roi = ROInput::new(); + roi.append_scalar( + ScalarField::from_hex( + "e05c25d2c17ec20d6bc8fd21204af52808451076cff687407164a21d352ddd22", + ) + .expect("failed to create scalar"), + ); + roi.append_scalar( + ScalarField::from_hex( + "c356dbb39478508818e0320dffa6c1ef512564366ec885ee2fc4d385dd36df0f", + ) + .expect("failed to create scalar"), + ); + + assert_eq!( + roi.to_bytes(), + [ + 0xe0, 0x5c, 0x25, 0xd2, 0xc1, 0x7e, 0xc2, 0x0d, 0x6b, 0xc8, 0xfd, 0x21, 0x20, 0x4a, + 0xf5, 0x28, 0x08, 0x45, 0x10, 0x76, 0xcf, 0xf6, 0x87, 0x40, 0x71, 0x64, 0xa2, 0x1d, + 0x35, 0x2d, 0xdd, 0xa2, 0x61, 0xab, 0xed, 0x59, 0x4a, 0x3c, 0x28, 0x44, 0x0c, 0x70, + 0x99, 0x86, 0x7f, 0xd3, 0xe0, 0xf7, 0xa8, 0x12, 0x32, 0x1b, 0x37, 0xe4, 0x42, 0xf7, + 0x17, 0xe2, 0xe9, 0xc2, 0x6e, 0x9b, 0xef, 0x07 + ] + ); + + assert_eq!( + roi.to_fields(), + [ + BaseField::from_hex( + "e05c25d2c17ec20d6bc8fd21204af52808451076cff687407164a21d352ddd22" + ) + .expect("failed to create field"), + BaseField::from_hex( + "86adb66729f1a01031c0651afe4d83dfa34ac86cdc900bdd5f88a70bbb6dbe1f" + ) + .expect("failed to create field"), + BaseField::from_hex( + "0000000000000000000000000000000000000000000000000000000000000000" + ) + .expect("failed to create field"), + ] + ); + } + + #[test] + fn to_fields_2_bits_scalar_u32() { + let mut roi = ROInput::new(); + roi.append_bit(true); + roi.append_bit(false); + roi.append_scalar( + ScalarField::from_hex( + "689634de233b06251a80ac7df64483922727757eea1adc6f0c8f184441cfe10d", + ) + .expect("failed to create scalar"), + ); + roi.append_u32(834803); + + assert_eq!( + roi.to_bytes(), + [ + 0xa1, 0x59, 0xd2, 0x78, 0x8f, 0xec, 0x18, 0x94, 0x68, 0x00, 0xb2, 0xf6, 0xd9, 0x13, + 0x0d, 0x4a, 0x9e, 0x9c, 0xd4, 0xf9, 0xa9, 0x6b, 0x70, 0xbf, 0x31, 0x3c, 0x62, 0x10, + 0x05, 0x3d, 0x87, 0x37, 0xe6, 0x79, 0x19, 0x00, 0x00 + ] + ); + + assert_eq!( + roi.to_fields(), + [ + BaseField::from_hex( + "a159d2788fec18946800b2f6d9130d4a9e9cd4f9a96b70bf313c6210053d8737" + ) + .expect("failed to create field"), + BaseField::from_hex( + "98e7650000000000000000000000000000000000000000000000000000000000" + ) + .expect("failed to create field"), + ] + ); + } + + #[test] + fn to_fields_2_bits_field_scalar() { + let mut roi = ROInput::new(); + roi.append_bit(false); + roi.append_bit(true); + roi.append_field( + BaseField::from_hex("90926b620ad09ed616d5df158504faed42928719c58ae619d9eccc062f920411") + .expect("failed to create field"), + ); + roi.append_scalar( + ScalarField::from_hex( + "689634de233b06251a80ac7df64483922727757eea1adc6f0c8f184441cfe10d", + ) + .expect("failed to create scalar"), + ); + + assert_eq!( + roi.to_bytes(), + [ + 0x90, 0x92, 0x6b, 0x62, 0x0a, 0xd0, 0x9e, 0xd6, 0x16, 0xd5, 0xdf, 0x15, 0x85, 0x04, + 0xfa, 0xed, 0x42, 0x92, 0x87, 0x19, 0xc5, 0x8a, 0xe6, 0x19, 0xd9, 0xec, 0xcc, 0x06, + 0x2f, 0x92, 0x04, 0x11, 0xd1, 0x2c, 0x69, 0xbc, 0x47, 0x76, 0x0c, 0x4a, 0x34, 0x00, + 0x59, 0xfb, 0xec, 0x89, 0x06, 0x25, 0x4f, 0x4e, 0xea, 0xfc, 0xd4, 0x35, 0xb8, 0xdf, + 0x18, 0x1e, 0x31, 0x88, 0x82, 0x9e, 0xc3, 0x1b + ] + ); + + assert_eq!( + roi.to_fields(), + [ + BaseField::from_hex( + "90926b620ad09ed616d5df158504faed42928719c58ae619d9eccc062f920411" + ) + .expect("failed to create field"), + BaseField::from_hex( + "a259d2788fec18946800b2f6d9130d4a9e9cd4f9a96b70bf313c6210053d8737" + ) + .expect("failed to create field"), + BaseField::from_hex( + "0000000000000000000000000000000000000000000000000000000000000000" + ) + .expect("failed to create field"), + ] + ); + } + + #[test] + fn transaction_test_1() { + let mut roi = ROInput::new(); + roi.append_field( + BaseField::from_hex("41203c6bbac14b357301e1f386d80f52123fd00f02197491b690bddfa742ca22") + .expect("failed to create field"), + ); // fee payer + roi.append_field( + BaseField::from_hex("992cdaf29ffe15b2bcea5d00e498ed4fffd117c197f0f98586e405f72ef88e00") + .expect("failed to create field"), + ); // source + roi.append_field( + BaseField::from_hex("3fba4fa71bce0dfdf709d827463036d6291458dfef772ff65e87bd6d1b1e062a") + .expect("failed to create field"), + ); // receiver + roi.append_u64(1000000); // fee + roi.append_u64(1); // fee token + roi.append_bit(true); // fee payer pk odd + roi.append_u32(0); // nonce + roi.append_u32(u32::MAX); // valid_until + roi.append_bytes(&vec![0; 34]); // memo + roi.append_bit(false); // tags[0] + roi.append_bit(false); // tags[1] + roi.append_bit(false); // tags[2] + roi.append_bit(true); // sender pk odd + roi.append_bit(false); // receiver pk odd + roi.append_u64(1); // token_id + roi.append_u64(10000000000); // amount + roi.append_bit(false); // token_locked + assert_eq!(roi.bits.len() + roi.fields.len() * 255, 1364); + assert_eq!( + roi.to_bytes(), + [ + 0x41, 0x20, 0x3c, 0x6b, 0xba, 0xc1, 0x4b, 0x35, 0x73, 0x01, 0xe1, 0xf3, 0x86, 0xd8, + 0x0f, 0x52, 0x12, 0x3f, 0xd0, 0x0f, 0x02, 0x19, 0x74, 0x91, 0xb6, 0x90, 0xbd, 0xdf, + 0xa7, 0x42, 0xca, 0xa2, 0x4c, 0x16, 0x6d, 0xf9, 0x4f, 0xff, 0x0a, 0x59, 0x5e, 0xf5, + 0x2e, 0x00, 0x72, 0xcc, 0xf6, 0xa7, 0xff, 0xe8, 0x8b, 0xe0, 0x4b, 0xf8, 0xfc, 0x42, + 0x43, 0xf2, 0x82, 0x7b, 0x17, 0x7c, 0x47, 0xc0, 0x8f, 0xee, 0xd3, 0xe9, 0x86, 0x73, + 0x43, 0xff, 0x7d, 0x02, 0xf6, 0x89, 0x11, 0x8c, 0x8d, 0x75, 0x0a, 0x05, 0xd6, 0xf7, + 0xfb, 0xdd, 0x8b, 0xbd, 0xd7, 0x61, 0x6f, 0xdb, 0x86, 0x87, 0x81, 0x0a, 0x48, 0xe8, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, + 0x00, 0x00, 0x00, 0xc0, 0xff, 0xff, 0xff, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x5f, 0xa0, 0x12, 0x00, + 0x00, 0x00, 0x00 + ] + ); + + assert_eq!( + roi.to_fields(), + [ + BaseField::from_hex( + "41203c6bbac14b357301e1f386d80f52123fd00f02197491b690bddfa742ca22" + ) + .expect("failed to create field"), + BaseField::from_hex( + "992cdaf29ffe15b2bcea5d00e498ed4fffd117c197f0f98586e405f72ef88e00" + ) + .expect("failed to create field"), + BaseField::from_hex( + "3fba4fa71bce0dfdf709d827463036d6291458dfef772ff65e87bd6d1b1e062a" + ) + .expect("failed to create field"), + BaseField::from_hex( + "40420f0000000000010000000000000001000000feffffff0100000000000000" + ) + .expect("failed to create field"), + BaseField::from_hex( + "0000000000000000000000000000000000000000000000000000400100000000" + ) + .expect("failed to create field"), + BaseField::from_hex( + "00000000902f5009000000000000000000000000000000000000000000000000" + ) + .expect("failed to create field"), + ] + ); + } +} diff --git a/src/schnorr.rs b/src/schnorr.rs new file mode 100644 index 0000000..840bf50 --- /dev/null +++ b/src/schnorr.rs @@ -0,0 +1,156 @@ +//! Mina Schnorr signature scheme +//! +//! An implementation of the singer interface for the Mina signature algorithm +//! +//! Details: + +use ark_ec::{ + AffineCurve, // for prime_subgroup_generator() + ProjectiveCurve, // for into_affine() +}; +use ark_ff::{ + BigInteger, // for is_even() + Field, // for from_random_bytes() + PrimeField, // for from_repr() + Zero, +}; +use blake2::{ + digest::{Update, VariableOutput}, + VarBlake2b, +}; +use oracle::{ + poseidon::{SpongeConstants, SpongeState}, + rndoracle::{ArithmeticSponge, Sponge}, +}; +use std::ops::Neg; + +use crate::{ + BaseField, CurvePoint, FieldHelpers, Hashable, Keypair, NetworkId, PubKey, ROInput, + ScalarField, Signable, Signature, Signer, +}; + +/// Schnorr signer context for the Mina signature algorithm +/// +/// For details about the signature algorithm please see [crate::schnorr] +pub struct Schnorr { + sponge: ArithmeticSponge, + network_id: NetworkId, +} + +impl Signer for Schnorr { + fn sign(&mut self, kp: Keypair, input: S) -> Signature + where + S: Signable, + { + let k: ScalarField = self.blinding_hash(&kp, input); + let r: CurvePoint = CurvePoint::prime_subgroup_generator().mul(k).into_affine(); + let k: ScalarField = if r.y.into_repr().is_even() { k } else { -k }; + + let e: ScalarField = self.message_hash(&kp.public, r.x, input); + let s: ScalarField = k + e * kp.secret.to_scalar(); + + Signature::new(r.x, s) + } + + fn verify(&mut self, sig: Signature, public: PubKey, input: S) -> bool + where + S: Signable, + { + let ev: ScalarField = self.message_hash(&public, sig.rx, input); + + let sv: CurvePoint = CurvePoint::prime_subgroup_generator() + .mul(sig.s) + .into_affine(); + // Perform addition and infinity check in projective coordinates for performance + let rv = public.to_point().mul(ev).neg().add_mixed(&sv); + if rv.is_zero() { + return false; + } + let rv = rv.into_affine(); + + rv.y.into_repr().is_even() && rv.x == sig.rx + } +} + +impl Schnorr { + /// Create a new Schnorr signer context for network instance `network_id` using arithmetic sponge defined by `sponge`. + pub fn new(sponge: ArithmeticSponge, network_id: NetworkId) -> Schnorr { + Schnorr:: { sponge, network_id } + } + + fn domain_bytes(network_id: NetworkId) -> Vec + where + S: Signable, + { + let mut domain_string = S::domain_string(network_id); + // Domain prefixes have a max length of 20 and are padded with '*' + assert!(domain_string.len() <= 20); + domain_string = &domain_string[..std::cmp::min(domain_string.len(), 20)]; + let mut bytes = format!("{:*<20}", domain_string).as_bytes().to_vec(); + bytes.resize(32, 0); + + bytes + } + + // This function uses a cryptographic hash function to create a uniformly and + // randomly distributed nonce. It is crucial for security that no two different + // messages share the same nonce. + fn blinding_hash(&self, kp: &Keypair, input: H) -> ScalarField + where + H: Hashable, + { + let mut hasher = VarBlake2b::new(32).unwrap(); + + let mut roi: ROInput = input.to_roinput(); + roi.append_field(kp.public.to_point().x); + roi.append_field(kp.public.to_point().y); + roi.append_scalar(kp.secret.to_scalar()); + roi.append_bytes(&[self.network_id.into()]); + + hasher.update(roi.to_bytes()); + + let mut bytes = [0; 32]; + hasher.finalize_variable(|out| bytes.copy_from_slice(out)); + // Drop the top two bits to convert into a scalar field element + // N.B. Since the order p is very close to 2^m for some m, truncating only creates + // a tiny amount of bias that should be insignificant and keeps the implementation + // simple by avoiding reduction modulo p. + bytes[bytes.len() - 1] &= 0b0011_1111; + + ScalarField::from_random_bytes(&bytes[..]).expect("failed to create scalar from bytes") + } + + // This function uses a cryptographic hash function (based on a sponge construction) to + // convert the message to be signed (and some other information) into a uniformly and + // randomly distributed scalar field element. It uses Mina's variant of the Poseidon + // SNARK-friendly cryptographic hash function. + // Details: + fn message_hash(&mut self, pub_key: &PubKey, rx: BaseField, input: S) -> ScalarField + where + S: Signable, + { + let mut roi: ROInput = input.to_roinput(); + roi.append_field(pub_key.to_point().x); + roi.append_field(pub_key.to_point().y); + roi.append_field(rx); + + // Set sponge initial state (explicitly init state so signer context can be reused) + // N.B. Mina sets the sponge's initial state by hashing the input's domain bytes + self.sponge.state = vec![BaseField::zero(); self.sponge.state.len()]; + self.sponge.sponge_state = SpongeState::Absorbed(0); + self.sponge + .absorb(&[ + BaseField::from_bytes(&Schnorr::::domain_bytes::(self.network_id)) + .expect("invalid domain bytes"), + ]); + self.sponge.squeeze(); + + // Absorb random oracle input + self.sponge.absorb(&roi.to_fields()); + + // Squeeze and convert from base field element to scalar field element + // Since the difference in modulus between the two fields is < 2^125, w.h.p., a + // random value from one field will fit in the other field. + ScalarField::from_repr(self.sponge.squeeze().into_repr()).expect("failed to create scalar") + } +} diff --git a/src/seckey.rs b/src/seckey.rs new file mode 100644 index 0000000..b1810e3 --- /dev/null +++ b/src/seckey.rs @@ -0,0 +1,19 @@ +//! Secret key structures and helpers + +use crate::ScalarField; + +/// Secret key +#[derive(Clone, Copy, PartialEq, Eq)] // No Debug nor Display +pub struct SecKey(ScalarField); + +impl SecKey { + /// Create a secret key from scalar field element + pub fn new(scalar: ScalarField) -> Self { + Self(scalar) + } + + /// Convert secret key into scalar field element + pub fn to_scalar(self) -> ScalarField { + self.0 + } +} diff --git a/src/signature.rs b/src/signature.rs new file mode 100644 index 0000000..a7b8dcc --- /dev/null +++ b/src/signature.rs @@ -0,0 +1,33 @@ +//! Mina signature structure and associated helpers + +use std::fmt; + +use crate::{BaseField, FieldHelpers, ScalarField}; + +/// Signature structure +#[derive(Clone, Copy, Eq, fmt::Debug, PartialEq)] +pub struct Signature { + /// Field component + pub rx: BaseField, + + /// Scalar component + pub s: ScalarField, +} + +impl Signature { + /// Create a new signature + pub fn new(rx: BaseField, s: ScalarField) -> Self { + Self { rx, s } + } +} + +impl fmt::Display for Signature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut rx_bytes = self.rx.to_bytes(); + let mut s_bytes = self.s.to_bytes(); + rx_bytes.reverse(); + s_bytes.reverse(); + + write!(f, "{}{}", hex::encode(rx_bytes), hex::encode(s_bytes)) + } +} diff --git a/tests/notarization.rs b/tests/notarization.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/notarization.rs @@ -0,0 +1 @@ + diff --git a/tests/tests.rs b/tests/tests.rs new file mode 100644 index 0000000..0b0baf0 --- /dev/null +++ b/tests/tests.rs @@ -0,0 +1,271 @@ +pub mod transaction; + +use ark_ff::Zero; +use mina_signer::{BaseField, Keypair, NetworkId, PubKey, ScalarField, Signer}; +use rand; +pub use transaction::Transaction; + +enum TransactionType { + PaymentTx, + DelegationTx, +} + +macro_rules! assert_sign_verify_tx { + ($tx_type:expr, $sec_key:expr, $source_address:expr, $receiver_address:expr, $amount:expr, $fee:expr, + $nonce:expr, $valid_until:expr, $memo:expr, $testnet_target:expr, $mainnet_target:expr) => { + let kp = Keypair::from_hex($sec_key).expect("failed to create keypair"); + assert_eq!( + kp.public, + PubKey::from_address($source_address).expect("invalid source address") + ); + let mut tx = match $tx_type { + TransactionType::PaymentTx => Transaction::new_payment( + PubKey::from_address($source_address).expect("invalid source address"), + PubKey::from_address($receiver_address).expect("invalid receiver address"), + $amount, + $fee, + $nonce, + ), + TransactionType::DelegationTx => Transaction::new_delegation( + PubKey::from_address($source_address).expect("invalid source address"), + PubKey::from_address($receiver_address).expect("invalid receiver address"), + $fee, + $nonce, + ), + }; + + tx = tx.set_valid_until($valid_until).set_memo_str($memo); + + let mut testnet_ctx = mina_signer::create(NetworkId::TESTNET); + let testnet_sig = testnet_ctx.sign(kp, tx); + + let mut mainnet_ctx = mina_signer::create(NetworkId::MAINNET); + let mainnet_sig = mainnet_ctx.sign(kp, tx); + + // Signing checks + assert_ne!(testnet_sig, mainnet_sig); // Testnet and mainnet sigs are not equal + assert_eq!(testnet_sig.to_string(), $testnet_target); // Testnet target check + assert_eq!(mainnet_sig.to_string(), $mainnet_target); // Mainnet target check + + // Verification checks + assert_eq!(testnet_ctx.verify(testnet_sig, kp.public, tx), true); + assert_eq!(mainnet_ctx.verify(mainnet_sig, kp.public, tx), true); + + assert_eq!(mainnet_ctx.verify(testnet_sig, kp.public, tx), false); + assert_eq!(testnet_ctx.verify(mainnet_sig, kp.public, tx), false); + + tx.valid_until = !tx.valid_until; + assert_eq!(testnet_ctx.verify(testnet_sig, kp.public, tx), false); + assert_eq!(mainnet_ctx.verify(mainnet_sig, kp.public, tx), false); + }; +} + +#[test] +fn signer_test_raw() { + let kp = Keypair::from_hex("164244176fddb5d769b7de2027469d027ad428fadcc0c02396e6280142efb718") + .expect("failed to create keypair"); + let tx = Transaction::new_payment( + kp.public, + PubKey::from_address("B62qicipYxyEHu7QjUqS7QvBipTs5CzgkYZZZkPoKVYBu6tnDUcE9Zt") + .expect("invalid address"), + 1729000000000, + 2000000000, + 16, + ) + .set_valid_until(271828) + .set_memo_str("Hello Mina!"); + + assert_eq!(tx.valid_until, 271828); + assert_eq!( + tx.memo, + [ + 0x01, 0x0b, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x4d, 0x69, 0x6e, 0x61, 0x21, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ] + ); + + let mut ctx = mina_signer::create(NetworkId::TESTNET); + let sig = ctx.sign(kp, tx); + + assert_eq!(sig.to_string(), + "11a36a8dfe5b857b95a2a7b7b17c62c3ea33411ae6f4eb3a907064aecae353c60794f1d0288322fe3f8bb69d6fabd4fd7c15f8d09f8783b2f087a80407e299af"); +} + +#[test] +fn signer_zero_test() { + let kp = Keypair::from_hex("164244176fddb5d769b7de2027469d027ad428fadcc0c02396e6280142efb718") + .expect("failed to create keypair"); + let tx = Transaction::new_payment( + kp.public, + PubKey::from_address("B62qicipYxyEHu7QjUqS7QvBipTs5CzgkYZZZkPoKVYBu6tnDUcE9Zt") + .expect("invalid address"), + 1729000000000, + 2000000000, + 16, + ); + + let mut ctx = mina_signer::create(NetworkId::TESTNET); + let sig = ctx.sign(kp, tx); + + assert_eq!(ctx.verify(sig, kp.public, tx), true); + + // Zero some things + let mut sig2 = sig; + sig2.rx = BaseField::zero(); + assert_eq!(ctx.verify(sig2, kp.public, tx), false); + let mut sig3 = sig; + sig3.s = ScalarField::zero(); + assert_eq!(ctx.verify(sig3, kp.public, tx), false); + sig3.rx = BaseField::zero(); + assert_eq!(ctx.verify(sig3, kp.public, tx), false); +} + +#[test] +fn sign_payment_test_1() { + assert_sign_verify_tx!( + /* Transaction type */ TransactionType::PaymentTx, + /* sender secret key */ "164244176fddb5d769b7de2027469d027ad428fadcc0c02396e6280142efb718", + /* source address */ "B62qnzbXmRNo9q32n4SNu2mpB8e7FYYLH8NmaX6oFCBYjjQ8SbD7uzV", + /* receiver address */ "B62qicipYxyEHu7QjUqS7QvBipTs5CzgkYZZZkPoKVYBu6tnDUcE9Zt", + /* amount */ 1729000000000, + /* fee */ 2000000000, + /* nonce */ 16, + /* valid until */ 271828, + /* memo */ "Hello Mina!", + /* testntet signature */ "11a36a8dfe5b857b95a2a7b7b17c62c3ea33411ae6f4eb3a907064aecae353c60794f1d0288322fe3f8bb69d6fabd4fd7c15f8d09f8783b2f087a80407e299af", + /* mainnet signature */ "124c592178ed380cdffb11a9f8e1521bf940e39c13f37ba4c55bb4454ea69fba3c3595a55b06dac86261bb8ab97126bf3f7fff70270300cb97ff41401a5ef789" + ); +} + +#[test] +fn sign_payment_test_2() { + assert_sign_verify_tx!( + /* Transaction type */ TransactionType::PaymentTx, + /* sender secret key */ "3414fc16e86e6ac272fda03cf8dcb4d7d47af91b4b726494dab43bf773ce1779", + /* source address */ "B62qoG5Yk4iVxpyczUrBNpwtx2xunhL48dydN53A2VjoRwF8NUTbVr4", + /* receiver address */ "B62qrKG4Z8hnzZqp1AL8WsQhQYah3quN1qUj3SyfJA8Lw135qWWg1mi", + /* amount */ 314159265359, + /* fee */ 1618033988, + /* nonce */ 0, + /* valid until */ 4294967295, + /* memo */ "", + /* testnet signature */ "23a9e2375dd3d0cd061e05c33361e0ba270bf689c4945262abdcc81d7083d8c311ae46b8bebfc98c584e2fb54566851919b58cf0917a256d2c1113daa1ccb27f", + /* mainnet signature */ "204eb1a37e56d0255921edd5a7903c210730b289a622d45ed63a52d9e3e461d13dfcf301da98e218563893e6b30fa327600c5ff0788108652a06b970823a4124" + ); +} + +#[test] +fn sign_payment_test_3() { + assert_sign_verify_tx!( + /* Transaction type */ TransactionType::PaymentTx, + /* sender secret key */ "3414fc16e86e6ac272fda03cf8dcb4d7d47af91b4b726494dab43bf773ce1779", + /* source address */ "B62qoG5Yk4iVxpyczUrBNpwtx2xunhL48dydN53A2VjoRwF8NUTbVr4", + /* receiver address */ "B62qoqiAgERjCjXhofXiD7cMLJSKD8hE8ZtMh4jX5MPNgKB4CFxxm1N", + /* amount */ 271828182845904, + /* fee */ 100000, + /* nonce */ 5687, + /* valid until */ 4294967295, + /* memo */ "01234567890123456789012345678901", + /* testnet signature */ "2b4d0bffcb57981d11a93c05b17672b7be700d42af8496e1ba344394da5d0b0b0432c1e8a77ee1bd4b8ef6449297f7ed4956b81df95bdc6ac95d128984f77205", + /* mainnet signature */ "076d8ebca8ccbfd9c8297a768f756ff9d08c049e585c12c636d57ffcee7f6b3b1bd4b9bd42cc2cbee34b329adbfc5127fe5a2ceea45b7f55a1048b7f1a9f7559" + ); +} + +#[test] +fn sign_payment_test_4() { + assert_sign_verify_tx!( + /* Transaction type */ TransactionType::PaymentTx, + /* sender secret key */ "1dee867358d4000f1dafa5978341fb515f89eeddbe450bd57df091f1e63d4444", + /* source address */ "B62qoqiAgERjCjXhofXiD7cMLJSKD8hE8ZtMh4jX5MPNgKB4CFxxm1N", + /* receiver address */ "B62qnzbXmRNo9q32n4SNu2mpB8e7FYYLH8NmaX6oFCBYjjQ8SbD7uzV", + /* amount */ 0, + /* fee */ 2000000000, + /* nonce */ 0, + /* valid until */ 1982, + /* memo */ "", + /* testnet signature */ "25bb730a25ce7180b1e5766ff8cc67452631ee46e2d255bccab8662e5f1f0c850a4bb90b3e7399e935fff7f1a06195c6ef89891c0260331b9f381a13e5507a4c", + /* mainnet signature */ "058ed7fb4e17d9d400acca06fe20ca8efca2af4ac9a3ed279911b0bf93c45eea0e8961519b703c2fd0e431061d8997cac4a7574e622c0675227d27ce2ff357d9" + ); +} + +#[test] +fn sign_delegation_test_1() { + assert_sign_verify_tx!( + /* Transaction type */ TransactionType::DelegationTx, + /* sender secret key */ "164244176fddb5d769b7de2027469d027ad428fadcc0c02396e6280142efb718", + /* source address */ "B62qnzbXmRNo9q32n4SNu2mpB8e7FYYLH8NmaX6oFCBYjjQ8SbD7uzV", + /* receiver address */ "B62qicipYxyEHu7QjUqS7QvBipTs5CzgkYZZZkPoKVYBu6tnDUcE9Zt", + /* amount */ 0, + /* fee */ 2000000000, + /* nonce */ 16, + /* valid until */ 1337, + /* memo */ "Delewho?", + /* testnet signature */ "30797d7d0426e54ff195d1f94dc412300f900cc9e84990603939a77b3a4d2fc11ebab12857b47c481c182abe147279732549f0fd49e68d5541f825e9d1e6fa04", + /* mainnet signature */ "0904e9521a95334e3f6757cb0007ec8af3322421954255e8d263d0616910b04d213344f8ec020a4b873747d1cbb07296510315a2ec76e52150a4c765520d387f" + ); +} + +#[test] +fn sign_delegation_test_2() { + assert_sign_verify_tx!( + /* Transaction type */ TransactionType::DelegationTx, + /* sender secret key */ "20f84123a26e58dd32b0ea3c80381f35cd01bc22a20346cc65b0a67ae48532ba", + /* source address */ "B62qkiT4kgCawkSEF84ga5kP9QnhmTJEYzcfgGuk6okAJtSBfVcjm1M", + /* receiver address */ "B62qnzbXmRNo9q32n4SNu2mpB8e7FYYLH8NmaX6oFCBYjjQ8SbD7uzV", + /* amount */ 0, + /* fee */ 2000000000, + /* nonce */ 0, + /* valid until */ 4294967295, + /* memo */ "", + /* testnet signature */ "07e9f88fc671ed06781f9edb233fdbdee20fa32303015e795747ad9e43fcb47b3ce34e27e31f7c667756403df3eb4ce670d9175dd0ae8490b273485b71c56066", + /* mainnet signature */ "2406ab43f8201bd32bdd81b361fdb7871979c0eec4e3b7a91edf87473963c8a4069f4811ebc5a0e85cbb4951bffe93b638e230ce5a250cb08d2c250113a1967c" + ); +} + +#[test] +fn sign_delegation_test_3() { + assert_sign_verify_tx!( + /* Transaction type */ TransactionType::DelegationTx, + /* sender secret key */ "3414fc16e86e6ac272fda03cf8dcb4d7d47af91b4b726494dab43bf773ce1779", + /* source address */ "B62qoG5Yk4iVxpyczUrBNpwtx2xunhL48dydN53A2VjoRwF8NUTbVr4", + /* receiver address */ "B62qkiT4kgCawkSEF84ga5kP9QnhmTJEYzcfgGuk6okAJtSBfVcjm1M", + /* amount */ 0, + /* fee */ 42000000000, + /* nonce */ 1, + /* valid until */ 4294967295, + /* memo */ "more delegates, more fun........", + /* testnet signature */ "1ff9f77fed4711e0ebe2a7a46a7b1988d1b62a850774bf299ec71a24d5ebfdd81d04a570e4811efe867adefe3491ba8b210f24bd0ec8577df72212d61b569b15", + /* mainnet signature */ "36a80d0421b9c0cbfa08ea95b27f401df108b30213ae138f1f5978ffc59606cf2b64758db9d26bd9c5b908423338f7445c8f0a07520f2154bbb62926aa0cb8fa" + ); +} + +#[test] +fn sign_delegation_test_4() { + assert_sign_verify_tx!( + /* Transaction type */ TransactionType::DelegationTx, + /* sender secret key */ "336eb4a19b3d8905824b0f2254fb495573be302c17582748bf7e101965aa4774", + /* source address */ "B62qrKG4Z8hnzZqp1AL8WsQhQYah3quN1qUj3SyfJA8Lw135qWWg1mi", + /* receiver address */ "B62qicipYxyEHu7QjUqS7QvBipTs5CzgkYZZZkPoKVYBu6tnDUcE9Zt", + /* amount */ 0, + /* fee */ 1202056900, + /* nonce */ 0, + /* valid until */ 577216, + /* memo */ "", + /* testnet signature */ "26ca6b95dee29d956b813afa642a6a62cd89b1929320ed6b099fd191a217b08d2c9a54ba1c95e5000b44b93cfbd3b625e20e95636f1929311473c10858a27f09", + /* mainnet signature */ "093f9ef0e4e051279da0a3ded85553847590ab739ee1bfd59e5bb30f98ed8a001a7a60d8506e2572164b7a525617a09f17e1756ac37555b72e01b90f37271595" + ); +} + +#[test] +fn custom_signer_test() { + use oracle::{pasta, poseidon}; + + let kp = Keypair::rand(&mut rand::rngs::OsRng); + let mut ctx = mina_signer::custom::( + pasta::fp_3::params(), + NetworkId::MAINNET, + ); + let tx = Transaction::new_payment(kp.public, kp.public, 2049, 1, 0); + ctx.sign(kp, tx); +} diff --git a/tests/transaction.rs b/tests/transaction.rs new file mode 100644 index 0000000..8b9be17 --- /dev/null +++ b/tests/transaction.rs @@ -0,0 +1,214 @@ +use mina_signer::{CompressedPubKey, Hashable, NetworkId, PubKey, ROInput, Signable}; + +const MEMO_BYTES: usize = 34; +const TAG_BITS: usize = 3; +const PAYMENT_TX_TAG: [bool; TAG_BITS] = [false, false, false]; +const DELEGATION_TX_TAG: [bool; TAG_BITS] = [false, false, true]; + +#[derive(Clone, Copy)] +pub struct Transaction { + // Common + pub fee: u64, + pub fee_token: u64, + pub fee_payer_pk: CompressedPubKey, + pub nonce: u32, + pub valid_until: u32, + pub memo: [u8; MEMO_BYTES], + // Body + pub tag: [bool; TAG_BITS], + pub source_pk: CompressedPubKey, + pub receiver_pk: CompressedPubKey, + pub token_id: u64, + pub amount: u64, + pub token_locked: bool, +} + +impl Hashable for Transaction { + fn to_roinput(self) -> ROInput { + let mut roi = ROInput::new(); + + roi.append_field(self.fee_payer_pk.x); + roi.append_field(self.source_pk.x); + roi.append_field(self.receiver_pk.x); + + roi.append_u64(self.fee); + roi.append_u64(self.fee_token); + roi.append_bit(self.fee_payer_pk.is_odd); + roi.append_u32(self.nonce); + roi.append_u32(self.valid_until); + roi.append_bytes(&self.memo); + + for tag_bit in self.tag { + roi.append_bit(tag_bit); + } + + roi.append_bit(self.source_pk.is_odd); + roi.append_bit(self.receiver_pk.is_odd); + roi.append_u64(self.token_id); + roi.append_u64(self.amount); + roi.append_bit(self.token_locked); + + roi + } +} + +impl Signable for Transaction { + fn domain_string(network_id: NetworkId) -> &'static str { + // Domain strings must have length <= 20 + match network_id { + NetworkId::MAINNET => "MinaSignatureMainnet", + NetworkId::TESTNET => "CodaSignature", + } + } +} + +impl Transaction { + pub fn new_payment(from: PubKey, to: PubKey, amount: u64, fee: u64, nonce: u32) -> Self { + Transaction { + fee: fee, + fee_token: 1, + fee_payer_pk: from.to_compressed(), + nonce: nonce, + valid_until: u32::MAX, + memo: array_init::array_init(|i| (i == 0) as u8), + tag: PAYMENT_TX_TAG, + source_pk: from.to_compressed(), + receiver_pk: to.to_compressed(), + token_id: 1, + amount: amount, + token_locked: false, + } + } + + pub fn new_delegation(from: PubKey, to: PubKey, fee: u64, nonce: u32) -> Self { + Transaction { + fee: fee, + fee_token: 1, + fee_payer_pk: from.to_compressed(), + nonce: nonce, + valid_until: u32::MAX, + memo: array_init::array_init(|i| (i == 0) as u8), + tag: DELEGATION_TX_TAG, + source_pk: from.to_compressed(), + receiver_pk: to.to_compressed(), + token_id: 1, + amount: 0, + token_locked: false, + } + } + + pub fn set_valid_until(mut self, global_slot: u32) -> Self { + self.valid_until = global_slot; + + self + } + + pub fn set_memo(mut self, memo: [u8; MEMO_BYTES - 2]) -> Self { + self.memo[0] = 0x01; + self.memo[1] = (MEMO_BYTES - 2) as u8; + self.memo[2..].copy_from_slice(&memo[..]); + + self + } + + pub fn set_memo_str(mut self, memo: &str) -> Self { + self.memo[0] = 0x01; + self.memo[1] = std::cmp::min(memo.len(), MEMO_BYTES - 2) as u8; + let memo = format!("{:\0<32}", memo); // Pad user-supplied memo with zeros + self.memo[2..] + .copy_from_slice(&memo.as_bytes()[..std::cmp::min(memo.len(), MEMO_BYTES - 2)]); + // Anything beyond MEMO_BYTES is truncated + + self + } +} + +use mina_signer::Keypair; + +#[test] +fn transaction_domain() { + assert_eq!( + Transaction::domain_string(NetworkId::MAINNET), + "MinaSignatureMainnet" + ); + assert_eq!( + Transaction::domain_string(NetworkId::TESTNET), + "CodaSignature" + ); +} + +#[test] +fn transaction_memo() { + let kp = Keypair::from_hex("164244176fddb5d769b7de2027469d027ad428fadcc0c02396e6280142efb718") + .expect("failed to create keypair"); + + let tx = Transaction::new_payment(kp.public, kp.public, 0, 0, 0); + assert_eq!( + tx.memo, + [ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0 + ] + ); + + // Memo length < max memo length + let tx = tx.set_memo([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]); + assert_eq!( + tx.memo, + [ + 1, 32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + ] + ); + + // Memo > max memo length (truncate) + let tx = tx.set_memo([ + 8, 92, 15, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 2, 31, 54, 55, 4, 57, 48, 49, 50, + 51, 52, 53, 54, 55, 6, 71, 48, 49, + ]); + assert_eq!( + tx.memo, + [ + 1, 32, 8, 92, 15, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 2, 31, 54, 55, 4, 57, 48, + 49, 50, 51, 52, 53, 54, 55, 6, 71, 48, 49 + ] + ); +} + +#[test] +fn transaction_memo_str() { + let kp = Keypair::from_hex("164244176fddb5d769b7de2027469d027ad428fadcc0c02396e6280142efb718") + .expect("failed to create keypair"); + + let tx = Transaction::new_payment(kp.public, kp.public, 0, 0, 0); + assert_eq!( + tx.memo, + [ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0 + ] + ); + + // Memo length < max memo length + let tx = tx.set_memo_str("Hello Mina!"); + assert_eq!( + tx.memo, + [ + 1, 11, 72, 101, 108, 108, 111, 32, 77, 105, 110, 97, 33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + + // Memo > max memo length (truncate) + let tx = tx.set_memo_str("012345678901234567890123456789012345"); + assert_eq!( + tx.memo, + [ + 1, 32, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49 + ] + ); +}