Skip to content

refactor(crypto): expose BLS as free functions - #664

Open
emlautarom1-agent[bot] wants to merge 5 commits into
mainfrom
emlautarom1/tbls-free-functions
Open

refactor(crypto): expose BLS as free functions#664
emlautarom1-agent[bot] wants to merge 5 commits into
mainfrom
emlautarom1/tbls-free-functions

Conversation

@emlautarom1-agent

@emlautarom1-agent emlautarom1-agent Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Closes #602

Summary

pluto_crypto exposed BLS through a Tbls trait with one implementor, the zero-sized BlstImpl, so every call site had to materialize the ZST and import the trait. The trait was never used polymorphically. BLS is now plain pub fns, matching pluto-k1util.

This moves toward Charon, not away from it. Charon's tbls.Implementation exists to back SetImplementation, called from exactly one place in the Go tree — its own test file. Charon's callers only ever see the package-level tbls.Sign/tbls.Verify. Pluto never had that swap point.

Commits

Each is independently buildable; the branch bisects cleanly.

  1. expose BLS as free functions — the issue. Also moves the private blst arithmetic to tbls::math and tightens the crate lint to unsafe_code = "deny", with that module opting back in.
  2. move the eth2 conversions into types — folds tblsconv in next to the types it converts. tbls no longer depends on pluto-eth2api.
  3. reuse the crypto sig_to_eth2 helperpluto_core::signeddata carried its own copy of the same identity conversion over the same [u8; 96].
  4. prune the unreachable BLS error paths — an Error variant whose only constructor was never invoked, and a #[from] never exercised. See inline note.
  5. simplify the polynomial evaluation loop — the only arithmetic change. See inline note.

`pluto_crypto` exposes BLS operations as plain `pub fn`s in `tbls`,
matching the style `pluto-k1util` already uses. Call sites read
`tbls::verify(...)` instead of materializing a zero-sized receiver and
importing a trait.

The blst scalar and curve-point arithmetic moves to a private
`tbls::math`, which holds every `unsafe` block in the crate. The crate
lint is `unsafe_code = "deny"` so that adding `unsafe` anywhere else
fails the build, and `math` opts back in locally. It is `deny` rather
than the workspace `forbid` because `forbid` cannot be re-enabled for
`math`; a local `#[allow(unsafe_code)]` can therefore still defeat it.

Closes #602
The key and signature conversions live next to the types they operate
on, in `pluto_crypto::types`, rather than in a module of their own.
Call sites read `types::pubkey_from_bytes(...)`, and `tbls` is left
holding only the BLS operations.
`signeddata` calls `pluto_crypto::types::sig_to_eth2` instead of
carrying its own copy. `pluto_core` already depends on `pluto-crypto`
and re-exports `Signature` from it, so the two were the same identity
conversion over the same `[u8; 96]`.
`Error::BlsError` and the `From<BLST_ERROR> for Error` impl that was its
only constructor are gone: no fallible call returns `BLST_ERROR`, so the
variant could never be observed. Every real blst failure is already
mapped to a specific variant carrying a `BlsError`.

`InvalidSecretKey` also loses its `#[from]`. Nothing returns
`Result<_, BlsError>`, so the conversion was never exercised, and a
future `?` on a public-key or signature parse would have been labelled a
secret-key failure. It now matches its three sibling variants, which
carry a `BlsError` without a blanket conversion.
`evaluate_polynomial` updates `x_power` unconditionally and hoists the
`x` scalar out of the loop. The guard it replaces was expressed as a
property of the polynomial's degree rather than of the loop position, so
it read like a bounds bug when it was only avoiding one multiply on a
degree-1 polynomial. That multiply now happens and its result is
discarded; the value returned is unchanged.

@emlautarom1-agent emlautarom1-agent Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Three notes on the parts that aren't self-evident from the diff.

Comment on lines +52 to +61
// Horner-free evaluation: `x_power` holds x^i entering iteration i.
let x_scalar = scalar_from_u64(x);
let mut x_power = x_scalar.clone();

for coeff in poly.iter().skip(1) {
// result += coeff * x_power
let term = scalar_mult_secret(coeff, &x_power)?;
result = scalar_add_secret(&result, &term)?;

x_power = scalar_mult_scalars(&x_power, &x_scalar)?;

@emlautarom1-agent emlautarom1-agent Bot Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Only arithmetic change in the branch. The replaced guard (if poly.len() > 2) was loop-invariant, so for len >= 3 the old code already ran the trailing discarded multiply. The sole delta is threshold == 2, which now does one extra multiply whose result is never read.

It cannot fail: blst_sk_mul_n_check fails only when the product is zero mod r, and both operands are x, drawn from 1..=total with total <= 255.

/// BLST error.
#[error("BLST error: {0}")]
BlsError(BlsError),
InvalidSecretKey(BlsError),

@emlautarom1-agent emlautarom1-agent Bot Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Dropping #[from] also drops source(), since thiserror's #[from] implies #[source]. Display is unchanged.

The conversion was never exercised — nothing returns Result<_, BlsError> — but it was a footgun: a future ? on a public-key or signature parse would have been labelled a secret-key failure. The three error-chain consumers were checked; none is reachable with this variant.

#[source] in place of #[from] would keep the chain, if that beats sibling consistency.

Comment thread crates/crypto/Cargo.toml
unsafe_code = "allow"
# `deny` rather than the workspace `forbid` so `tbls::math` — the sole module
# wrapping the blst C bindings — can opt back in with `#![allow(unsafe_code)]`.
unsafe_code = "deny"

@emlautarom1-agent emlautarom1-agent Bot Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

deny, not forbid All unsafe now lives in the private tbls::math, which opts back in with #![allow(unsafe_code)], impossible under forbid.

@emlautarom1 emlautarom1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Checked manually, mostly mechanical changes due to dropping BlstImpl

The single simplification in the math module has been verified to be correct.

@emlautarom1
emlautarom1 marked this pull request as ready for review August 21, 2026 20:22
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.

Replace the Tbls trait + BlstImpl receiver with free functions

1 participant