Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proc macro to generate memory-efficient gate #695

Merged
merged 6 commits into from
Jun 16, 2023

Conversation

taikiy
Copy link
Contributor

@taikiy taikiy commented Jun 9, 2023

Procedural macro to implement the Step and StepNarrow traits and generate a memory-efficient gate.

The goal is to generate a state transition graph and the corresponding StepNarrow implementations for the IPA protocol. This macro assumes that a complete IPA steps file exists in the repo at the location specified by STEPS_FILE. The steps file can be generated by running collect_steps.py.

The steps file contains a list of narrowed steps, where each line represents a hierarchy of narrowed steps delimited by "/". For example, the following lines represent a hierarchy of narrowed steps ("=> x" parts are not in the file. They are added in here as comments to indicate their potential IDs when the macro parses the file):

    RootStep                                => 0
    RootStep/StepA::A1                      => 1
    RootStep/StepA::A1/StepB::B1            => 2
    RootStep/StepA::A1/StepB::B2            => 3
    RootStep/StepC::C1                      => 4
    RootStep/StepC::C1/StepD::D1            => 5
    RootStep/StepC::C1/StepD::D1/StepA::A2  => 6
    RootStep/StepC::C2                      => 7

From these lines, we want to generate StepNarrow implementations for each step.

    impl StepNarrow<StepA> for Compact {
        fn narrow(&self, step: &StepA) -> Self {
            Self(match (self.0, step.as_ref()) {
                (0, "A1") => 1,
                (5, "A2") => 6,
                _ => panic!("invalid state transition"),
            })
        }
    }
    impl StepNarrow<StepB> for Compact {
        fn narrow(&self, step: &StepB) -> Self {
            Self(match (self.0, step.as_ref()) {
                (1, "B1") => 2,
                (1, "B2") => 3,
                _ => panic!("invalid state transition"),
            })
        }
    }
    ...

Currently, this derive macro assumes it annotates the Compact struct defined in src/protocol/step/compact.rs. The Compact struct is a wrapper around a u16 value that represents the current state of the IPA protocol.

In the future, we might change the macro to annotate each step in the IPA protocol. The macro will then generate both Descriptive and Compact implementations for the step. However, that kind of derive macro needs to be able to analyze the fully qualified module path of the step at runtime. This is because there are many locally-defined Step enums in IPA, and we need to disambiguate them. There are two ways to solve this. One is to use a feature to resolve the fully qualified module path of a Step enum/struct, but the feature is in nightly as of June 2023. Another way is to define globally unique step names.

@taikiy taikiy requested review from martinthomson and akoshelev June 9, 2023 08:53
@martinthomson
Copy link
Member

One comment, just based on your commentary (not the patch, I am out of energy for the week).

    impl StepNarrow<StepA> for Compact {
        fn narrow(&self, step: &StepA) -> Self {
            Self(match (self.0, step.as_ref()) {
                (0, "A1") => 1,

This would be better as:

    impl StepNarrow<StepA> for Compact {
        fn narrow(&self, step: &StepA) -> Self {
            Self(match (self.0, step) {

And, to confirm. Do you have annotations for enum arms that take integers, like this:

enum StepC {
  DoX,
  DoY(u32),
}

It would be OK if you only have code for BitOpStep and friends.

Comment on lines 29 to 30
StepB::Bar => "bar",
StepB::Baz => "baz",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
StepB::Bar => "bar",
StepB::Baz => "baz",
Self::Bar => "bar",
Self::Baz => "baz",

// config file, and generate the steps file accordingly. However, the value could change after the steps
// file is generated, so we need to make sure that the steps file is always up to date somehow.
//
// 6. Root step
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need a separate type and proc macro for that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alternatively we could come up with a oneshot run of IPA that initializes TestWorld in a way that does not allow to use it in more than one run

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was more about whether we need to dynamically generate a string representation of root step(s), i.e., does query ID "X" must be generated by Compact gate's AsRef<str> implementation? If we do not require the root step in a real world scenario, we don't need to worry about this. Descriptive gate has id field which embeds protocol/run-0 as the root step, so I was wondering if that was also needed for Compact gate.

@taikiy
Copy link
Contributor Author

taikiy commented Jun 12, 2023

And, to confirm. Do you have annotations for enum arms that take integers, like this:

enum StepC {
  DoX,
  DoY(u32),
}

We don't have macros or annotations for a macro to deal with this yet, but I cover these cases in #695. That said, we should create an improved version of the macro with the annotation for this. I could do this together with what we discussed about using strum to directly use enum arms as step representations.

@@ -0,0 +1,23 @@
[package]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are a lot of benefits using workspaces, we should consider using it at some point: https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html

};
use syn::{parse_macro_input, punctuated::Punctuated, DeriveInput, PathArguments, PathSegment};

// TODOs:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lots of good stuff here, lets make sure we have a tracking issue for those. Likely this code will not go through a lot of changes so we may just forget about them

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed #704, #705, #706, #707, #708. Deleted the comments in the source file.

// - We could use https://doc.rust-lang.org/proc_macro/struct.Span.html#method.source_file, but it's in nightly.
// - This will also allow us to generate state transition match statements from int to int rather than str to int.
//
// 2. Enable compile-time detection of steps that are `narrow`ed but never trigger `send` in the protocol.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: steps need to be inert in order to be "invisible". If they are created to generate shared randomness, that's fine

// a case where there are child steps branching off from the conditional step.
// (b) Forcibly execute conditional steps when `step-trace` feature is on to generate the steps file.
// Not sure what is the best way to do this, or whether this will work though.
// - Currently, we are doing (a-1). It could let unexpected state transitions happen without panicking, but
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if it is a great option, but we could annotate such steps with a macro that must enumerate all the valid "parent" steps for conditionally executed one. Then use that information to generate "narrow"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we could attribute such a step with #[conditional_step] (name TBD). This assumes we have a solution to #704.

// we could apply the same conditions used to call `multiply` using these contexts to where we `narrow`
// the context. The code becomes a bit messy, but it's doable.
// - crate::protocol::context::UpgradeStep
// This steps is executed in both malicious and semi-honest contexts, but the `narrow` call in semi-honest
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible to solve it with putting the step narrow inside the upgrade?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is basically what I did. Instead of the template implementation in UpgradedContext trait, I created specific implementations of upgrade for semi-honest and malicious.

Comment on lines 66 to 72
if let Some(my_parent_ref) = my_parent_weak.upgrade() {
Some(Node {
data: my_parent_ref,
})
} else {
None
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be wrong but I think this should work

Suggested change
if let Some(my_parent_ref) = my_parent_weak.upgrade() {
Some(Node {
data: my_parent_ref,
})
} else {
None
}
my_parent_weak.upgrade().clone()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference here is that my_parent_ref encloses InnerNode but the return type is Node. I could implement From<> but this is the only place we would use that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

new_child
}

pub fn get_children(&self) -> Vec<Node<T>> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

returning borrowed data may be nicer, but not sure if it is easy to do.

   pub fn get_children(&self) -> impl Iterator<Item = Node<T>>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me take this as a homework. Not sure how to return an iterator while keeping borrow().iter().

}

impl<T> Node<T> {
pub fn new(value: T) -> Node<T> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub fn new(value: T) -> Node<T> {
pub fn new(value: T)-> Self {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!


// We want to make sure that once a parent node is dropped, all its children are dropped as well.
// To do that, we use `Rc<>` for children, and `Weak<>` for parent. (`Rc` because it's single-threaded)
type InnerNodeRef<T> = Rc<InnerNode<T>>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it feels that it should be reversed, that is: Rc<RefCell<Node<T>>> for strong reference and Weak<RefCell<Node<T>>> for weak

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's a bit confusing...

The type of Node::data::parent is

Rc<RefCell<Weak<InnerNode<T>>>>

and Node::data::children is

Rc<RefCell<Vec<Rc<InnerNode<T>>>>>,

but Node::data::value is

Rc<T>.

We can keep the value immutable while able to mutate parent/child edges via add_child.

Rc::clone(&self.data)
}

pub fn add_child(&self, value: T) -> Node<T> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could make it a method on Rc and make a Node constructor that takes Weak as an argument for parent (pass Weak::new() for none).

    pub fn add_child(self: Rc<Self>, value: T) -> Self {
        let child = Rc::new(Node::new(value, self.downgrade());
        self.children.push(Rc::clone(child));
 
        child
    } 

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that would make this part of the code a bit more readable. I think hiding all Rc/Weak stuff from the users of Node is a pretty nice feature though.

@akoshelev akoshelev merged commit f078c9c into private-attribution:main Jun 16, 2023
@taikiy taikiy deleted the ipa-macros branch June 16, 2023 10:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants