Skip to content

Prove sub#44

Merged
TheodoreEhrenborg merged 152 commits intomainfrom
prove_sub
Aug 11, 2025
Merged

Prove sub#44
TheodoreEhrenborg merged 152 commits intomainfrom
prove_sub

Conversation

@TheodoreEhrenborg
Copy link
Copy Markdown
Collaborator

No description provided.


#[verifier::external_body]
pub fn select(x: &u64, y: &u64, c: Choice) -> (res: u64)
ensures boolify(c) ==> res == x,
Copy link
Copy Markdown
Collaborator Author

@TheodoreEhrenborg TheodoreEhrenborg Aug 11, 2025

Choose a reason for hiding this comment

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

The version of this on main (which I wrote) is wrong 🤦‍♂️

I've flipped it to be correct. And I've changed the bool to be an enum with similar names to https://docs.rs/subtle/latest/subtle/trait.ConditionallySelectable.html#tymethod.conditional_select, so now it's easier to compare to that doc and see that it's the right way around

// subtract l if the sum is >= l
proof { lemma_l_value_properties(&constants::L, &sum); }
assert(to_nat(&sum.limbs) >= 0);
assume(to_nat(&sum.limbs) < 2 * group_order());
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I've added this assume in add so it can call sub. I think this can be propagated back to become another precondition for add

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

assert(to_nat(&sum.limbs) >= 0);

Isn't this a tautology based on types? to_nat returns a nat, which is always >= 0

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Removed


// a - b
let mut borrow: u64 = 0;
assert(seq_u64_to_nat(a.limbs@.subrange(0, 0 as int)) - seq_u64_to_nat(b.limbs@.subrange(0, 0 as int )) ==
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I've moved most of the proof of sub into lemmas, but I've left in the invariants and a few asserts. I could probably squish a few more asserts into lemmas, although some of them are needed to get Verus to recognize that a lemma has the right preconditions

seq_to_nat(limbs@.map(|i, x| x as nat))
}

pub open spec fn seq_u64_to_nat(limbs: Seq<u64>) -> nat
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Just another wrapper around seq_to_nat

assert(0x000d63c715bea69fu64 < (1u64 << 52)) by (bit_vector);
}

pub proof fn lemma_seq_u64_to_nat_subrange_extend(seq: Seq<u64>, i: int)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I asked Claude to do this and fed it a very similar proof from https://github.com/Beneficial-AI-Foundation/dafny-bignums, but it couldn't---maybe it didn't realize how similar Dafny and Verus are? I did get it to do most of the base case though

assert(a as nat == (a >> 52) as nat * pow2(52) + (a & mask) as nat);
}

pub proof fn lemma_sub_loop1_invariant(difference: Scalar52, borrow: u64, i: usize, a: &Scalar52, b: &Scalar52, old_borrow: u64, mask: u64, difference_loop1_start: Scalar52)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I haven't put much effort into cutting unnecessary asserts from these lemmas

// Without the following condition, all we can prove is something like:
// to_nat(&a.limbs) >= to_nat(&b.limbs) ==> to_nat(&s.limbs) == to_nat(&a.limbs) - to_nat(&b.limbs),
// to_nat(&a.limbs) < to_nat(&b.limbs) ==> to_nat(&s.limbs) == (to_nat(&a.limbs) - to_nat(&b.limbs) + pow2(260) + group_order()) % (pow2(260) as int),
// In the 2nd case, `sub` doesn't always do subtraction mod group_order
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It took me several tries to get the spec right

@TheodoreEhrenborg TheodoreEhrenborg marked this pull request as ready for review August 11, 2025 10:55
@TheodoreEhrenborg TheodoreEhrenborg requested a review from a team August 11, 2025 10:55
Copy link
Copy Markdown

@Kukovec Kukovec left a comment

Choose a reason for hiding this comment

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

LGMT, though I'm curious if Claude can comment its code if asked; some of the lemmas would be a bit tough to read through manually if uncommented.

// subtract l if the sum is >= l
proof { lemma_l_value_properties(&constants::L, &sum); }
assert(to_nat(&sum.limbs) >= 0);
assume(to_nat(&sum.limbs) < 2 * group_order());
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

assert(to_nat(&sum.limbs) >= 0);

Isn't this a tautology based on types? to_nat returns a nat, which is always >= 0

// to_nat(&a.limbs) >= to_nat(&b.limbs) ==> to_nat(&s.limbs) == to_nat(&a.limbs) - to_nat(&b.limbs),
// to_nat(&a.limbs) < to_nat(&b.limbs) ==> to_nat(&s.limbs) == (to_nat(&a.limbs) - to_nat(&b.limbs) + pow2(260) + group_order()) % (pow2(260) as int),
// In the 2nd case, `sub` doesn't always do subtraction mod group_order
-group_order() <= to_nat(&a.limbs) - to_nat(&b.limbs) < group_order(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

How come this is necessary though?
If a and b are bounded, then to_nat(a) < order and a - b <= a, similar for b and -order.

Copy link
Copy Markdown
Collaborator Author

@TheodoreEhrenborg TheodoreEhrenborg Aug 11, 2025

Choose a reason for hiding this comment

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

limbs_bounded(a) guarantees that to_nat(a) < 2^260, but group_order ~ 2^252. So I tried requiring 0 <= a < group_order && 0 <= b < group_order, but add passes in values sometimes larger than group_order. I think add's call to sub satisfies 0 <= a < 2 * group_order && 0 <= b < 2 * group_order, which is simpler than the precondition I went with, but that precondition is too loose for sub to always be correct

Also montgomery_reduce calls sub, and I don't yet understand it well enough to know if it satisfies sub's preconditions.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In that case, I'd consider relaxing this requirement, but changing ensures to

        to_nat(&s.limbs) % (group_order() as int) == (to_nat(&a.limbs) - to_nat(&b.limbs)) % (group_order() as int),

result limbs are bounded, but may represent a value greater than the order, in which case you have to take mod p on the left too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The weird thing about sub is that

to_nat(&s.limbs) % (group_order() as int) == (to_nat(&a.limbs) - to_nat(&b.limbs)) % (group_order() as int),

isn't always true. If to_nat(&a.limbs) < to_nat(&b.limbs), we get

to_nat(&s.limbs) == (to_nat(&a.limbs) - to_nat(&b.limbs) + pow2(260) + group_order()) % (pow2(260) as int),

And then if to_nat(&a.limbs) - to_nat(&b.limbs) < -group_order, this simplifies to

to_nat(&s.limbs) == to_nat(&a.limbs) - to_nat(&b.limbs) + pow2(260) + group_order(),

And mod group_order, we get

s % group_order == (a - b + pow(260)) % group order  != (a - b) % group order

Here's a test demonstrating this: https://github.com/Beneficial-AI-Foundation/curve25519-dalek/tree/sub_test

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems like there are a lot of spurious asserts, but if it's correct it's correct.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

At some point I should run verus_cleaner.py, but that script takes a while and doesn't catch everything

@TheodoreEhrenborg
Copy link
Copy Markdown
Collaborator Author

some of the lemmas would be a bit tough to read through manually if uncommented

That's my fault; I've added some comments

@TheodoreEhrenborg TheodoreEhrenborg merged commit bc45994 into main Aug 11, 2025
41 checks passed
@TheodoreEhrenborg TheodoreEhrenborg deleted the prove_sub branch August 11, 2025 12:33
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