Skip to content

Conversation

@LStan
Copy link
Contributor

@LStan LStan commented Sep 8, 2025

No description provided.

Copy link
Contributor

@deanmlittle deanmlittle left a 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],
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor Author

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?

Copy link
Contributor Author

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]?

Copy link
Contributor

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.

Copy link
Contributor Author

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],
Copy link
Contributor

@deanmlittle deanmlittle Sep 9, 2025

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Collaborator

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);
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What will fail?

Copy link
Collaborator

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.

Copy link
Contributor Author

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?

Copy link
Collaborator

@febo febo Sep 15, 2025

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.

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 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])
Copy link
Contributor

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> {
Copy link
Contributor

@deanmlittle deanmlittle Sep 9, 2025

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?

Copy link
Contributor Author

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>(
Copy link
Contributor

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!

Copy link
Collaborator

@febo febo left a 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 of checked_. This is similar to the invoke variants. 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.rs
  • compression.rs
  • multiplication.rs
  • pairing.rs

@LStan
Copy link
Contributor Author

LStan commented Sep 14, 2025

  • 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 of checked_. This is similar to the invoke variants. 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.

An array as an input will work for compress/decompress, because in that syscalls the length is checked for equality. But add and mult allow inputs less than required and then extend them with zeros. What to do with them?
For compress/decompress I suggest three functions: alt_bn128_g1_compress with array input, alt_bn128_g1_compress_slice (I prefer slice as suffix like sol_log_slice) for slice input as it done currently, and checked_alt_bn128_g1_compress_slice as current checked functions.
For add and mult there will be only ..._slice and checked_..._slice variants.

Also what to do for pairing? I suggest this:

pub fn alt_bn128_pairing(
    input: &[([u8; ALT_BN128_G1_SIZE], [u8; ALT_BN128_G2_SIZE])],
) -> Result<u8, ProgramError> {
    let len = input.len() * (ALT_BN128_G1_SIZE + ALT_BN128_G2_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).map(|data| data[31])
}

and also ..._slice and checked_..._slice variants as they implemented now.

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.rs`
* `compression.rs`
* `multiplication.rs`
* `pairing.rs`

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 don't think it is needed here, because there is not much code. Also, it is unclear where to put alt_bn128_group_op.

@febo
Copy link
Collaborator

febo commented Sep 15, 2025

  • 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 of checked_. This is similar to the invoke variants. 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.

An array as an input will work for compress/decompress, because in that syscalls the length is checked for equality. But add and mult allow inputs less than required and then extend them with zeros. What to do with them?

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.

For compress/decompress I suggest three functions: alt_bn128_g1_compress with array input, alt_bn128_g1_compress_slice (I prefer slice as suffix like sol_log_slice) for slice input as it done currently, and checked_alt_bn128_g1_compress_slice as current checked functions. For add and mult there will be only ..._slice and checked_..._slice variants.

To complement my first point, I think it is confusing to have so many variations, in particular the checked_..._slice variant is a mouthful. So we could have a single one with slice as parameter and no validation. You can either pass an array or slice, and any validation is done by the syscall or the program. What do you think?

Also what to do for pairing? I suggest this:

pub fn alt_bn128_pairing(
    input: &[([u8; ALT_BN128_G1_SIZE], [u8; ALT_BN128_G2_SIZE])],
) -> Result<u8, ProgramError> {
    let len = input.len() * (ALT_BN128_G1_SIZE + ALT_BN128_G2_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).map(|data| data[31])
}

and also ..._slice and checked_..._slice variants as they implemented now.

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.rs`
* `compression.rs`
* `multiplication.rs`
* `pairing.rs`

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 don't think it is needed here, because there is not much code. Also, it is unclear where to put alt_bn128_group_op.

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 mod.rs. In a not so distant future, we should add this optimization to the SDK.

@LStan
Copy link
Contributor Author

LStan commented Sep 16, 2025

When I created checked_ versions, I followed the pattern with (checked_)create_program_address. But probably you are right, this is unnecessary.
About the input. What if for compress/decompress the input will be a sized array, and for add, mult, pairing - slice? As Dean said, array input may save CUs, but I haven't checked it.
I noticed that in the SDK alt_bn128_g2_decompress has a sized array as input, while the others - slice.

@febo
Copy link
Collaborator

febo commented Sep 16, 2025

About the input. What if for compress/decompress the input will be a sized array, and for add, mult, pairing - slice? As Dean said, array input may save CUs, but I haven't checked it.
I noticed that in the SDK alt_bn128_g2_decompress has a sized array as input, while the others - slice.

Sounds good!

@LStan
Copy link
Contributor Author

LStan commented Sep 20, 2025

I implemented the changes.
I now think that maybe it is worth to make a sized array input also for addition and multiplication, even though it is possible to pass a shorter slice. As far as I understand, there is a plan to change it. What do you think?

@deanmlittle
Copy link
Contributor

I implemented the changes.

I now think that maybe it is worth to make a sized array input also for addition and multiplication, even though it is possible to pass a shorter slice. As far as I understand, there is a plan to change it. What do you think?

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.

@febo
Copy link
Collaborator

febo commented Sep 21, 2025

I implemented the changes. I now think that maybe it is worth to make a sized array input also for addition and multiplication, even though it is possible to pass a shorter slice. As far as I understand, there is a plan to change it. What do you think?

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> {
Copy link
Collaborator

@febo febo Oct 3, 2025

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.

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 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

Copy link
Collaborator

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?

Copy link
Contributor Author

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.

Copy link
Collaborator

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?

Copy link
Contributor Author

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

@LStan
Copy link
Contributor Author

LStan commented Oct 3, 2025

Another thought about naming. The module is called bn254, but the functions have alt_bn128 prefix. I tried to mimic the original SDK, but was it the right choice?

@febo
Copy link
Collaborator

febo commented Oct 3, 2025

Another thought about naming. The module is called bn254, but the functions have alt_bn128 prefix. I tried to mimic the original SDK, but was it the right choice?

I think it is ok. My understanding is that bn254 is the precise name for the curve family.

@febo
Copy link
Collaborator

febo commented Oct 24, 2025

@LStan Just a final tweak on a function definition and then it is good to go.

@LStan
Copy link
Contributor Author

LStan commented Oct 25, 2025

@febo What do you think about putting this into a separate crate, e.g. pinocchio-bn254 or even pinocchio-curve(s)?

@febo
Copy link
Collaborator

febo commented Oct 25, 2025

@febo What do you think about putting this into a separate crate, e.g. pinocchio-bn254 or even pinocchio-curve(s)?

Good idea, let's do a pinocchio-bn254.

@LStan
Copy link
Contributor Author

LStan commented Oct 26, 2025

@febo

  1. I saw you plan to remove syscalls and errors and use solana-define-syscall and solana-program-error. Should I wait for that changes to happen or use the whole pinocchio crate as a dependency for now?
  2. Also in log: Remove crate #264 you plan to use sdk directory only for pinocchio crate. Where should I put this one?

@febo
Copy link
Collaborator

febo commented Oct 26, 2025

@febo

  1. I saw you plan to remove syscalls and errors and use solana-define-syscall and solana-program-error. Should I wait for that changes to happen or use the whole pinocchio crate as a dependency for now?
  2. Also in log: Remove crate #264 you plan to use sdk directory only for pinocchio crate. Where should I put this one?

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 solana-bn254. Since this new crate is actually independent of pinocchio, we could publish it based on the current version. Then after the refactoring, we make changes to the SDK one. What do you think?

@LStan
Copy link
Contributor Author

LStan commented Oct 26, 2025

Since this new crate is actually independent of pinocchio, we could publish it based on the current version

There is a dependency on syscalls and ProgramError. I can implement syscalls inside the crate (as in log). But what to do with ProgramError?

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