-
Notifications
You must be signed in to change notification settings - Fork 164
feat: add alt_bn128 syscalls #246
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't comment on all of the places where I saw this possible improvement, but I think using sized arrays will get rid of the need for runtime checks on the &[u8] in cases where it is defined as a constant. We should let the user decide when/if/how they want to handle this instead of forcing a runtime check. Overall, great work @LStan 🫡
| /// Note: This function checks if the input has the correct length, | ||
| /// returning an error without incurring the cost of the syscall. | ||
| pub fn checked_alt_bn128_g1_compress( | ||
| input: &[u8], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of taking in a &[u8], consider using a &[[u8;ALT_BN128_G1_SIZE]]. This will avoid the need for the length check at runtime for properly defined constants.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What will be the input type of alt_bn128_compression?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, will it be convenient from the user's perspective if they have just &[u8] input?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, why &[[u8;ALT_BN128_G1_SIZE]] if there is only one point in the input? Maybe &[u8;ALT_BN128_G1_SIZE]?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My bad, just do the one, you are right. For multiple we want the array, for single, we want just the one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, I can change input type to &[u8;SIZE] for all compressions, then checked_ variants will be unnecessary. But I'm still unsure about the convenience of usage for callers.
| /// It will return an error if the length is invalid, incurring the cost of the syscall. | ||
| #[inline(always)] | ||
| pub fn alt_bn128_addition( | ||
| input: &[u8], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider using a sized array here as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently, syscalls accept inputs that are equal or less than required and extend them with zeros.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Heh, ironically, on LE, this might actually save us some CUs. Maybe you're right.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunatelly no, it will fail.
Endianness::LE => {
if input.len() != ALT_BN128_ADDITION_INPUT_LEN {
return Err(AltBn128Error::InvalidInputData);
}
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What will fail?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The syscall will fail, but only for LE.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I don't understand. What syscall do you mean? Is there something wrong with LE implementation of addition in the SDK?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think there is anything wrong. I was just pointing out that while for BE you can pass a slice shorter than the required, for LE you cannot.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is exactly what Dean said, that for LE it makes sense to use a sized array as input, because it cannot be shorter than the required.
| if input.len() % ALT_BN128_PAIRING_ELEMENT_LEN != 0 { | ||
| return Err(ProgramError::InvalidArgument); | ||
| } | ||
| alt_bn128_group_op::<32>(input, ALT_BN128_PAIRING).map(|data| data[31]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
beautiful!
| /// Note: This function checks if the input has the correct length, | ||
| /// returning an error without incurring the cost of the syscall. | ||
| #[inline(always)] | ||
| pub fn checked_alt_bn128_pairing(input: &[u8]) -> Result<u8, ProgramError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure we need checked if we just use an array of correctly sized u8 arrays?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If input type for paring will be &[[u8;SIZE]] and for mult and add - &[u8;SIZE], what will be the input type for `alt_bn128_group_op?
| } | ||
|
|
||
| #[inline] | ||
| fn alt_bn128_group_op<const OUTPUT_DATA_LEN: usize>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This const generic is super nice and will save plenty of CUs vs the solana-program implementation!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall looks great! I have a couple of points for discussion:
- The "non-checked" variations could use an array with the expected size, as @deanmlittle suggested. This way you can get compile-time error if you are passing an invalid input. I expect that most of the time you will have an array as an input.
- We could prefix the ones accepting a slice with
slice_instead ofchecked_. This is similar to theinvokevariants. Since these ones take a slice, they perform the length check.
Another alternative is to not have checked_ (slice_) variants and let the syscall fail if the input has the incorrect length. In the majority of the cases, an error in the syscall is not recoverable, and we should be optimizing for the "success" case. Programs have the option to do the validation if needed. In this case we would keep the argument as a slice.
A separa point is about the organization. What do you think if we follow a similar structure as the SDK? That means separating the implementation into:
addition.rscompression.rsmultiplication.rspairing.rs
An array as an input will work for compress/decompress, because in that syscalls the length is checked for equality. But Also what to do for and also
This split in the SDK was done recently as a part of a large refactoring to add versioned functions for the validator here. There was a lot of code in one file and this split made sense. |
I am really tempted to say that we should just have one method taking a slice and don't do any validation. We document that the syscall will fail when the input is invalid and the program can decide to do the validation beforehand or not.
To complement my first point, I think it is confusing to have so many variations, in particular the
I think it will be clearer to have them separated in multiple file, with the benefit that this mirrors the SDK implementation. Any shared function can go in |
|
When I created |
Sounds good! |
|
I implemented the changes. |
yeah, tbh it's a good idea because it can't possibly result in a worse outcome than what happens now, and we can already use maybeuninit to save CUs on shorter numbers. |
Agree – makes sense. |
| /// After SIMD-0334 is implemented, it will return an error if the length is invalid, | ||
| /// incurring the cost of the syscall. | ||
| #[inline(always)] | ||
| pub fn alt_bn128_pairing_be(input: &[u8]) -> Result<u8, ProgramError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to require the pair G1 and G2 as parameters and then convert them to &[u8]? This seems to be the only one that is requiring a slice as parameter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is actually multi-pairing, so the input is an array of pairs of points [point_in_g1, point_in_g2, another_point_in_g1, another_point_in_g2....]. The only idea I have is what I suggested above:
pub fn alt_bn128_pairing_be(
input: &[([u8; ALT_BN128_G1_POINT_SIZE], [u8; ALT_BN128_G2_POINT_SIZE])],
) -> Result<u8, ProgramError> {
let len = input.len() * ALT_BN128_PAIRING_ELEMENT_SIZE;
// SAFETY: The tuples are contiguous, and the arrays inside are contiguous.
let input = unsafe { core::slice::from_raw_parts(input.as_ptr() as *const u8, len) };
alt_bn128_group_op::<32>(input, ALT_BN128_PAIRING_BE).map(|data| data[31])
}Also, the syscall returns only 0 or 1. So maybe the function should return bool. And have a different name, something like alt_bn128_is_multi_pairing_valid
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think a bool return with the alt_bn128_is_pairing_valid_be name looks better (do we need the "multi" in the name?). I also think that the array of tuples is clearer than the slice &[u8]. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Array of tuples? I think you mean slice of tuples. I don't know, it depends on a use case. What if you have a prepared slice in instruction data? Then you'll have to construct a slice of tuples from it. And also some CUs will be spent for len calculation, but considering that there is already an error in calculation of pairing syscall cost, that is not much.
I personally don't have any preference here, so I'll do what you and others decide.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if you have a prepared slice in instruction data?
This is a good point, let's leave it as it is. Can we just update the name to alt_bn128_is_pairing_valid_be and have a bool return?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also changed the docs, please check
|
Another thought about naming. The module is called |
I think it is ok. My understanding is that |
|
@LStan Just a final tweak on a function definition and then it is good to go. |
|
@febo What do you think about putting this into a separate crate, e.g. |
Good idea, let's do a |
|
It all depends on whether we would like this changes to be out before the refactoring or not. After the refactoring, these changes should ideally move to the SDK crate |
There is a dependency on |
No description provided.