New Native Operation PublicKeyOf (#355)

* wrote some initial code

* added way to input private key into circuit

* TypedValue::SecretKey hashed as 10 32-bit limbs

* Check PublicKeyOf in Frontend and Middleware

* Diff review

* PR review

* Finish utest

* Fix bounds check

* added giving secret key witness to circuit

* Test & doc improvements

* added private key comparison to circuit and added test cases

* cargo fmt

* Add frontend tests for PublicKeyOf

* Add public_key_of and hash_of to op! macro

* Add ownership check to ticket example

* Group order checking in tests

* More negative test cases at circuit level

* Cleanups after self review

* clippy fixes

* Fixes after merge.  Temporarily remove plonky2 commit hash

* Add a nullifier to the ticket test example

* Test PublicKeyOf with a real prover (not mock)

* plonky-u32 dependency

* feat: optimize operation checks

Skip the circuits that verify operation checks other than None, Copy or
NewEntry for the public statements.  This works because public
statements are created by copying private statements, so we never use
the other operation checks in those slots.

---------

Co-authored-by: Andrew Twyman <artwyman@gmail.com>
Co-authored-by: Eduard S. <eduardsanou@posteo.net>
This commit is contained in:
brian6l 2025-07-28 15:53:01 -07:00 committed by GitHub
parent 9f8335756c
commit 5b04b2a360
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 958 additions and 76 deletions

View file

@ -34,7 +34,8 @@ use serialization::*;
pub use statement::*;
use crate::backends::plonky2::primitives::{
ec::curve::Point as PublicKey, merkletree::MerkleProof,
ec::{curve::Point as PublicKey, schnorr::SecretKey},
merkletree::MerkleProof,
};
pub const SELF: PodId = PodId(SELF_ID_HASH);
@ -62,8 +63,10 @@ pub enum TypedValue {
),
// Uses the serialization for middleware::Value:
Raw(RawValue),
// Public key variant
// Schnorr public key variant (EC point)
PublicKey(PublicKey),
// Schnorr secret key variant (scalar)
SecretKey(SecretKey),
PodId(PodId),
// UNTAGGED TYPES:
#[serde(untagged)]
@ -114,6 +117,12 @@ impl From<PublicKey> for TypedValue {
}
}
impl From<SecretKey> for TypedValue {
fn from(sk: SecretKey) -> Self {
TypedValue::SecretKey(sk)
}
}
impl From<PodId> for TypedValue {
fn from(id: PodId) -> Self {
TypedValue::PodId(id)
@ -188,6 +197,28 @@ impl TryFrom<&TypedValue> for PodId {
}
}
impl TryFrom<&TypedValue> for PublicKey {
type Error = Error;
fn try_from(v: &TypedValue) -> std::result::Result<Self, Self::Error> {
if let TypedValue::PublicKey(pk) = v {
Ok(*pk)
} else {
Err(Error::custom("Value not a public key".to_string()))
}
}
}
impl TryFrom<&TypedValue> for SecretKey {
type Error = Error;
fn try_from(v: &TypedValue) -> std::result::Result<Self, Self::Error> {
if let TypedValue::SecretKey(sk) = v {
Ok(sk.clone())
} else {
Err(Error::custom("Value not a secret key".to_string()))
}
}
}
impl fmt::Display for TypedValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
@ -233,6 +264,7 @@ impl fmt::Display for TypedValue {
write!(f, "]")
}
TypedValue::PublicKey(p) => write!(f, "PublicKey({})", p),
TypedValue::SecretKey(p) => write!(f, "SecretKey({})", p),
TypedValue::PodId(p) => {
write!(f, "0x{}", p.0.encode_hex::<String>())
}
@ -254,6 +286,7 @@ impl From<&TypedValue> for RawValue {
TypedValue::Array(a) => RawValue::from(a.commitment()),
TypedValue::Raw(v) => *v,
TypedValue::PublicKey(p) => RawValue::from(hash_fields(&p.as_fields())),
TypedValue::SecretKey(sk) => RawValue::from(hash_fields(&sk.to_limbs())),
TypedValue::PodId(id) => RawValue::from(id.0),
}
}
@ -321,6 +354,19 @@ impl JsonSchema for TypedValue {
..Default::default()
};
let secret_key_schema = schemars::schema::SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
object: Some(Box::new(schemars::schema::ObjectValidation {
// SecretKey is serialized as a string
properties: [("SecretKey".to_string(), gen.subschema_for::<String>())]
.into_iter()
.collect(),
required: ["SecretKey".to_string()].into_iter().collect(),
..Default::default()
})),
..Default::default()
};
// This is the part that Schemars can't generate automatically:
let untagged_array_schema = gen.subschema_for::<Array>();
let untagged_set_schema = gen.subschema_for::<Set>();
@ -334,6 +380,7 @@ impl JsonSchema for TypedValue {
Schema::Object(int_schema),
Schema::Object(raw_schema),
Schema::Object(public_key_schema),
Schema::Object(secret_key_schema),
untagged_array_schema,
untagged_dictionary_schema,
untagged_string_schema,

View file

@ -5,7 +5,13 @@ use plonky2::field::types::Field;
use serde::{Deserialize, Serialize};
use crate::{
backends::plonky2::primitives::merkletree::{MerkleProof, MerkleTree},
backends::plonky2::primitives::{
ec::{
curve::{Point as PublicKey, GROUP_ORDER},
schnorr::SecretKey,
},
merkletree::{MerkleProof, MerkleTree},
},
middleware::{
hash_values, AnchoredKey, CustomPredicate, CustomPredicateRef, Error, NativePredicate,
Params, Predicate, Result, Statement, StatementArg, StatementTmplArg, ToFields, Value,
@ -73,6 +79,7 @@ pub enum NativeOperation {
ProductOf = 12,
MaxOf = 13,
HashOf = 14,
PublicKeyOf = 15,
// Syntactic sugar operations. These operations are not supported by the backend. The
// frontend compiler is responsible of translating these operations into the operations above.
@ -130,6 +137,9 @@ impl OperationType {
NativeOperation::ProductOf => Some(Predicate::Native(NativePredicate::ProductOf)),
NativeOperation::MaxOf => Some(Predicate::Native(NativePredicate::MaxOf)),
NativeOperation::HashOf => Some(Predicate::Native(NativePredicate::HashOf)),
NativeOperation::PublicKeyOf => {
Some(Predicate::Native(NativePredicate::PublicKeyOf))
}
no => unreachable!("Unexpected syntactic sugar op {:?}", no),
},
OperationType::Custom(cpr) => Some(Predicate::Custom(cpr.clone())),
@ -164,6 +174,7 @@ pub enum Operation {
ProductOf(Statement, Statement, Statement),
MaxOf(Statement, Statement, Statement),
HashOf(Statement, Statement, Statement),
PublicKeyOf(Statement, Statement),
Custom(CustomPredicateRef, Vec<Statement>),
}
@ -203,6 +214,7 @@ impl Operation {
Self::ProductOf(_, _, _) => OT::Native(ProductOf),
Self::MaxOf(_, _, _) => OT::Native(MaxOf),
Self::HashOf(_, _, _) => OT::Native(HashOf),
Self::PublicKeyOf(_, _) => OT::Native(PublicKeyOf),
Self::Custom(cpr, _) => OT::Custom(cpr.clone()),
}
}
@ -224,6 +236,7 @@ impl Operation {
Self::ProductOf(s1, s2, s3) => vec![s1, s2, s3],
Self::MaxOf(s1, s2, s3) => vec![s1, s2, s3],
Self::HashOf(s1, s2, s3) => vec![s1, s2, s3],
Self::PublicKeyOf(s1, s2) => vec![s1, s2],
Self::Custom(_, args) => args,
}
}
@ -276,6 +289,7 @@ impl Operation {
(NO::HashOf, &[s1, s2, s3], OA::None) => {
Self::HashOf(s1.clone(), s2.clone(), s3.clone())
}
(NO::PublicKeyOf, &[s1, s2], OA::None) => Self::PublicKeyOf(s1.clone(), s2.clone()),
_ => Err(Error::custom(format!(
"Ill-formed operation {:?} with {} arguments {:?} and aux {:?}.",
op_code,
@ -310,6 +324,12 @@ impl Operation {
Ok(i1 == f(i2, i3))
}
pub(crate) fn check_public_key(v1: &Value, v2: &Value) -> Result<bool> {
let pk: PublicKey = v1.typed().try_into()?;
let sk: SecretKey = v2.typed().try_into()?;
Ok(sk.0 < *GROUP_ORDER && pk == sk.public_key())
}
/// Checks the given operation against a statement.
pub fn check(&self, params: &Params, output_statement: &Statement) -> Result<bool> {
use Statement::*;
@ -369,6 +389,9 @@ impl Operation {
(Self::HashOf(s1, s2, s3), HashOf(v4, v5, v6)) => {
val(v4, s1)? == hash_op(val(v5, s2)?, val(v6, s3)?)
}
(Self::PublicKeyOf(s1, s2), PublicKeyOf(v3, v4)) => {
Self::check_public_key(&val(v3, s1)?, &val(v4, s2)?)?
}
(Self::Custom(CustomPredicateRef { batch, index }, args), Custom(cpr, s_args))
if batch == &cpr.batch && index == &cpr.index =>
{
@ -564,8 +587,13 @@ pub(crate) fn value_from_op(input_st: &Statement, output_ref: &ValueRef) -> Opti
mod tests {
use std::collections::HashMap;
use num::BigUint;
use crate::{
backends::plonky2::primitives::merkletree::MerkleTree,
backends::plonky2::primitives::{
ec::{curve::GROUP_ORDER, schnorr::SecretKey},
merkletree::MerkleTree,
},
middleware::{
hash_value, AnchoredKey, Error, Key, Operation, Params, PodId, Result, Statement,
},
@ -635,4 +663,85 @@ mod tests {
})
})
}
#[test]
fn check_public_key_of_op() -> Result<()> {
let fixed_sk = SecretKey(BigUint::from(0x1234567890abcdefu64));
let fixed_pk = fixed_sk.public_key();
let rand_sk = SecretKey::new_rand();
let rand_pk = rand_sk.public_key();
let small_sk = SecretKey(BigUint::from(0x1u32));
let small_pk = small_sk.public_key();
let too_large_sk = SecretKey(small_sk.0.clone() + GROUP_ORDER.clone());
assert_eq!(small_pk, too_large_sk.public_key());
let test_cases = [
// Valid pairs
(fixed_pk, fixed_sk.clone(), true),
(rand_pk, rand_sk.clone(), true),
// Mismatched pairs
(fixed_pk, rand_sk.clone(), false),
(rand_pk, fixed_sk.clone(), false),
// Above group order
(small_pk, small_sk.clone(), true),
(small_pk, too_large_sk.clone(), false),
];
let params = Params::default();
let pod_id = PodId::default();
let pk_ak = AnchoredKey::new(pod_id, Key::new("pubkey".into()));
let sk_ak = AnchoredKey::new(pod_id, Key::new("secret".into()));
test_cases.iter().try_for_each(|(pk, sk, expect_good)| {
// Form op args
let pk_s = Statement::Equal(pk_ak.clone().into(), (*pk).into());
let sk_s = Statement::Equal(sk_ak.clone().into(), sk.clone().into());
// Form op
let op = Operation::PublicKeyOf(pk_s.clone(), sk_s.clone());
// Form output statement
let st = Statement::PublicKeyOf(pk_ak.clone().into(), sk_ak.clone().into());
// Check
op.check(&params, &st).map(|is_good| {
assert_eq!(
is_good, *expect_good,
"PublicKeyOf({}, {}) => {}",
pk, sk, is_good
);
})
})
}
#[test]
fn check_public_key_of_op_arg_types() -> Result<()> {
let fixed_sk = SecretKey(BigUint::from(0x1234567890abcdefu64));
let fixed_pk = fixed_sk.public_key();
let params = Params::default();
let pod_id = PodId::default();
let pk_ak = AnchoredKey::new(pod_id, Key::new("pubkey".into()));
let sk_ak = AnchoredKey::new(pod_id, Key::new("secret".into()));
// Form op args
let pk_s = Statement::Equal(pk_ak.clone().into(), fixed_pk.into());
let sk_s = Statement::Equal(sk_ak.clone().into(), fixed_sk.clone().into());
// Bad op and statement with bad first args
let op = Operation::PublicKeyOf(pk_s.clone(), pk_s.clone());
let st = Statement::PublicKeyOf(pk_ak.clone().into(), pk_ak.clone().into());
// Check
assert!(op.check(&params, &st).is_err());
// Bad op and statement with bad second args
let op = Operation::PublicKeyOf(sk_s.clone(), sk_s.clone());
let st = Statement::PublicKeyOf(sk_ak.clone().into(), sk_ak.clone().into());
// Check
assert!(op.check(&params, &st).is_err());
Ok(())
}
}

View file

@ -35,6 +35,7 @@ pub enum NativePredicate {
ProductOf = 9,
MaxOf = 10,
HashOf = 11,
PublicKeyOf = 12,
// Syntactic sugar predicates. These predicates are not supported by the backend. The
// frontend compiler is responsible of translating these predicates into the predicates above.
@ -64,6 +65,7 @@ impl Display for NativePredicate {
NativePredicate::ProductOf => "ProductOf",
NativePredicate::MaxOf => "MaxOf",
NativePredicate::HashOf => "HashOf",
NativePredicate::PublicKeyOf => "PublicKeyOf",
NativePredicate::DictContains => "DictContains",
NativePredicate::DictNotContains => "DictNotContains",
NativePredicate::ArrayContains => "ArrayContains",
@ -177,6 +179,7 @@ pub enum Statement {
ProductOf(ValueRef, ValueRef, ValueRef),
MaxOf(ValueRef, ValueRef, ValueRef),
HashOf(ValueRef, ValueRef, ValueRef),
PublicKeyOf(ValueRef, ValueRef),
Custom(CustomPredicateRef, Vec<Value>),
}
@ -211,6 +214,7 @@ impl Statement {
statement_constructor!(product_of, ProductOf, 3);
statement_constructor!(max_of, MaxOf, 3);
statement_constructor!(hash_of, HashOf, 3);
statement_constructor!(public_key_of, PublicKeyOf, 2);
pub fn predicate(&self) -> Predicate {
use Predicate::*;
match self {
@ -225,6 +229,7 @@ impl Statement {
Self::ProductOf(_, _, _) => Native(NativePredicate::ProductOf),
Self::MaxOf(_, _, _) => Native(NativePredicate::MaxOf),
Self::HashOf(_, _, _) => Native(NativePredicate::HashOf),
Self::PublicKeyOf(_, _) => Native(NativePredicate::PublicKeyOf),
Self::Custom(cpr, _) => Custom(cpr.clone()),
}
}
@ -242,6 +247,7 @@ impl Statement {
Self::ProductOf(ak1, ak2, ak3) => vec![ak1.into(), ak2.into(), ak3.into()],
Self::MaxOf(ak1, ak2, ak3) => vec![ak1.into(), ak2.into(), ak3.into()],
Self::HashOf(ak1, ak2, ak3) => vec![ak1.into(), ak2.into(), ak3.into()],
Self::PublicKeyOf(ak1, ak2) => vec![ak1.into(), ak2.into()],
Self::Custom(_, args) => Vec::from_iter(args.into_iter().map(Literal)),
}
}
@ -286,7 +292,9 @@ impl Statement {
(Native(NativePredicate::HashOf), &[a1, a2, a3]) => {
Self::HashOf(a1.try_into()?, a2.try_into()?, a3.try_into()?)
}
(Native(NativePredicate::PublicKeyOf), &[a1, a2]) => {
Self::PublicKeyOf(a1.try_into()?, a2.try_into()?)
}
(Native(np), _) => {
return Err(Error::custom(format!("Predicate {:?} is syntax sugar", np)))
}