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

[Feature] Add arithmetic types, plaintext, and conversion of plaintext & records to JS objects #944

Open
wants to merge 5 commits into
base: mainnet
Choose a base branch
from

Conversation

iamalwaysuncomfortable
Copy link
Collaborator

@iamalwaysuncomfortable iamalwaysuncomfortable commented Nov 26, 2024

Motivation

From several requests and issues surrounding the SDK, the following features have been flagged as important to add.

Addition Field, Scalar, and Group Types

Addition of Field, Group, and Scalar types and core arithmetic methods on those types allowing useful work to be done using algebraic types.

Accessible Plaintext and Record Data + Conversion of Aleo types to JS type.

This PR adds the ability to access:

  • Methods to access the field of of a Record directly
  • Methods to access Struct fields
  • Methods for converting Plaintext types into JS objects. This includes Aleo Literal ( i.e. address, boolean, i8/i16/i32/i64/i128/u8/u16/u32/u64/u128, field, scalar, signature), Struct, and Array types) into JS types.

Transition and Transaction Data Accessor Methods

The ability to search for records, and get the inputs and outputs of Transitions and Transactions.

Addition of GraphKey and ComputeKey

This PR adds both the GraphKey and ComputeKey to the sdk, allowing GraphKeys to be used directly to determine ownership records.

Test Plan

  • This PR adds unit tests for all new features.

@iamalwaysuncomfortable iamalwaysuncomfortable marked this pull request as ready for review December 3, 2024 04:56
@iamalwaysuncomfortable iamalwaysuncomfortable changed the title [Feature] Add arithmetic types, plaintext, and conversion of Plaintext & Records to JS objects [Feature] Add arithmetic types, plaintext, and conversion of plaintext & records to JS objects Dec 3, 2024
Copy link
Collaborator

@Pauan Pauan left a comment

Choose a reason for hiding this comment

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

There's various small nits that need to be fixed, otherwise it looks good to me.

#[wasm_bindgen(js_name = toBytesLe)]
pub fn to_bytes_le(&self) -> Result<Uint8Array, String> {
let bytes = self.0.to_bytes_le().map_err(|e| e.to_string())?;
let array = Uint8Array::new_with_length(bytes.len() as u32);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should instead use Uint8Array::from, which is simpler and faster.

///
/// @returns {Array<RecordPlaintext>} Array of record plaintext objects
pub fn owned_records(&self, view_key: &ViewKey) -> Array {
let array = Array::new();
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should instead use iterators and collect:

self.0.records()
    .into_iter()
    .filter_map(|(_, record_ciphertext)| {
        ...
    })
    .collect()

///
/// @returns {Array<{commitment: Field, record: RecordCiphertext}>} Array of record ciphertext objects
pub fn records(&self) -> Array {
let array = Array::new();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same as above, this should use map + collect.

pub fn records(&self) -> Array {
let array = Array::new();
self.0.records().for_each(|(commitment, record_ciphertext)| {
let object = Object::new();
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should use the object! utility macro.

/// @returns {Object} Transaction summary
pub fn summary(&self, convert_to_js: bool) -> Result<Object, String> {
// Create a transaction object.
let transaction = Object::new();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same as above, it would be nice to use the object! utility macro.

#[wasm_bindgen(js_name = "toBytesLe")]
pub fn to_bytes_le(&self) -> Result<Uint8Array, String> {
let rust_bytes = self.0.to_bytes_le().map_err(|e| e.to_string())?;
let array = Uint8Array::new_with_length(rust_bytes.len() as u32);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should use Uint8Array::from.

wasm/src/programs/program.rs Show resolved Hide resolved
}

/// Invert the field element.
pub fn inverse(&self) -> Field {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should be called negate.

Copy link
Collaborator Author

@iamalwaysuncomfortable iamalwaysuncomfortable Dec 5, 2024

Choose a reason for hiding this comment

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

Negate isn't the right terminology in Field Theory given there's no general concept of negative as there is in the real or complex fields. In field theory inverse has very specific meaning and it's the corresponding member of the field that when added together produces the additive identity element (i.e. 0). If you look in SnarkVM Field traits, inverse is the terminology used for this as it matches the formal definitions in theory, so this should stay inverse.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmmmm, in that case I don't think it should be using the - operator... since - should have the meaning of "negate", not "invert".

Copy link
Collaborator Author

@iamalwaysuncomfortable iamalwaysuncomfortable Dec 6, 2024

Choose a reason for hiding this comment

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

We're generally used to a negation being defined for signed numeric types, but theoretically it is
creating the inverse element to the positive number, so it is indeed referring to an inversion operation.

A finite field is different however in that there are no negative numbers, and thus no signed bit to denote the sign. The confusion here stems from the fact that in unsigned integers, there's no inverse element (and thus it's not a field, ring, or even a group by definition) but since a finite field is indeed a field, it follows field axioms and thus has an additive inverse even though it has no negative numbers. However in theoretical notation (and in SnarkVM) the negative sign is used to denote the inverse element, so it's consistent with common notation in Field theory. The number it ends up though is usually a large number.

For instance, in the ed25519 field (integers mod 2^255 − 19)

You can write (-2) + 2 = 0 to say "element 2 + its additive inverse equals zero", but it is shorthand notation for (2^255 - 21) + 2 = 0. Thus (-2) + 2 = (2^255 - 21) + 2 = 0. This is why using the Neg trait in SnarkVM is valid, and using it here is valid as well.

Because we are defining a finite field, there are no negatives and negate isn't a standard term in Field arithmetic, but using the negative operator (as defined in SnarkVM) is consistent. Calling this negate here is incorrect because it may cause conflation of concepts and lead people to believe this is a signed object and potentially make mistakes in cryptographic code. Inverse is the operation defined here and that's the name that needs to stay on it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for the explanation, that helps a lot.

}

/// Invert the field element.
pub fn inverse(&self) -> Scalar {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should be called negate.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This should stay inverse same justification as Field (Scalar is also a finite field). Inverse is the mathematical definition of what's happening here and there's no standard negation in finite fields.

@@ -418,5 +418,7 @@ self.addEventListener("message", (ev) => {
programManager.setHost(defaultHost);
}
})();
} else if (ev.data.type === "PROPOSE_GAME") {
Copy link
Collaborator

Choose a reason for hiding this comment

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

What's the purpose of this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is a mistake, I shall remove.

Copy link
Collaborator Author

@iamalwaysuncomfortable iamalwaysuncomfortable left a comment

Choose a reason for hiding this comment

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

Feedback addressed, @Pauan - when there's time to double check, let me know.

}

/// Invert the field element.
pub fn inverse(&self) -> Field {
Copy link
Collaborator Author

@iamalwaysuncomfortable iamalwaysuncomfortable Dec 5, 2024

Choose a reason for hiding this comment

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

Negate isn't the right terminology in Field Theory given there's no general concept of negative as there is in the real or complex fields. In field theory inverse has very specific meaning and it's the corresponding member of the field that when added together produces the additive identity element (i.e. 0). If you look in SnarkVM Field traits, inverse is the terminology used for this as it matches the formal definitions in theory, so this should stay inverse.

Comment on lines +90 to +92
pub fn negate(&self) -> Group {
Group(-self.0)
}
Copy link
Collaborator Author

@iamalwaysuncomfortable iamalwaysuncomfortable Dec 5, 2024

Choose a reason for hiding this comment

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

@Pauan the reason this is called negate is because describes a different operation than the inverse operation of a field. A group element is an eliptic curve element and a negation is the definition given to the operation of flipping a point about it axis of symmetry, which is what -self.0 encapsulates. So that's why the difference here.

}

/// Invert the field element.
pub fn inverse(&self) -> Scalar {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This should stay inverse same justification as Field (Scalar is also a finite field). Inverse is the mathematical definition of what's happening here and there's no standard negation in finite fields.

@@ -418,5 +418,7 @@ self.addEventListener("message", (ev) => {
programManager.setHost(defaultHost);
}
})();
} else if (ev.data.type === "PROPOSE_GAME") {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is a mistake, I shall remove.

let tpk = Group::from(transition.tpk());
let tcm = Field::from(transition.tcm());
let scm = Field::from(transition.scm());
if convert_to_js {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This will only work if its first wrapped in JsValue, which seems acceptable.

Copy link
Collaborator

@Pauan Pauan left a comment

Choose a reason for hiding this comment

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

Couple more minor issues to fix, otherwise it looks good to me.

($($key:literal: $value:expr,)*) => {{
let object = ::js_sys::Object::new();

$(Reflect::set(&object, &::wasm_bindgen::JsValue::from_str($key), &::wasm_bindgen::JsValue::from($value)).unwrap();)*
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor nit: this should be ::js_sys::Reflect::set

"programId" : argument.program_id().to_string(),
"functionName" : argument.function_name().to_string(),
};
let arguments = argument
Copy link
Collaborator

Choose a reason for hiding this comment

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

This can be moved into the object! so it can avoid the Reflect::set.

let record = object! {
"type": "record",
};
if convert_to_js {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it's cleaner to do something like this:

if convert_to_js {
    object! {
        "type": "record",
        ...
    }
} else {
    object! {
        "type": "record",
        ...
    }
}

let external_record = object! {
"type": "extneralRecord",
};
if convert_to_js {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same as above, better to use object! instead of Reflect::set

};

// Create a record object.
if convert_to_js {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same as above, better to use object! instead of Reflect::set

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.

2 participants