Skip to content

Commit 6664717

Browse files
jeswrclaude
andauthored
feat(zk): route all six DualLeafV2 value lanes and build every lane's inputs (#7099)
* feat(zk): route all six DualLeafV2 value lanes and build every lane's inputs Two host-side gaps in the DualLeafV2 value lanes. Neither opens a verifier gate: the stage-0 ValueLaneSubProofNotAccepted refusal, the stage-2 operand match, filter_edge_true and encode_pattern_slot are untouched, and a test asserts per lane that a manifest assembled entirely by the new builders is still refused. Routing. dual_leaf_v2::is_hookable_datatype and encode_literal now route xsd:boolean, xsd:dateTime and xsd:date as well as xsd:integer, xsd:decimal and xsd:double, so a graph committed under DualLeafV2 carries a real value hook, under that datatype's own lane constant, for every literal on them. Before this, a value-lane FILTER on one of those three bound an operand_enc no committed leaf of the graph was, which is why value_pin refused them as vacuous. Two deliberate consequences: the three newly routed lanes inherit the fail-closed ingest rule, so an XSD-legal but non-canonical lexical on them ("1"^^xsd:boolean, a bare or non-Z xsd:dateTime, a 24:00:00 instant) now fails the whole commitment instead of taking the degenerate string lane; and a V2 commitment over a graph that CONTAINS such a literal is re-based. Every leaf on the three already-routed lanes, every degenerate string-lane leaf, every IRI and blank-node leaf, and the whole-graph commitment over a graph of only those are byte-identical, pinned by a fixed vector captured before the change. DualLeafV1's routing is unchanged, so an already-issued V1 graph is unaffected. Build helpers. build::build_filter_value_dl_{int, f64, decimal} complete the V2 builder set beside the existing _bool / _datetime / _date entries, over one shared assembly rather than three copies: both operands go through the lane's own dual_leaf_v2 encoder, the committed literal's lane constant is carried, and the disclosed verdict is computed by that member's host verdict mirror (integer_verdict, double_bits_verdict, the shared signed_epoch_verdict) rather than taken from the caller. The decimal lane rescales the FILTER constant to the committed literal's scale exactly, never rounded, refusing an inexact or overflowing rescale, through the same two functions value_pin pins with, so the builder's scale and the pin's cannot disagree. BuiltFilterValueDlNumeric::lane_proof feeds the checked typed prove entry, so the Prover.toml is rendered from validated typed fields and never from caller TOML. Tested as a composition: every one of the six lanes' builder output satisfies value_pin::check_value_lane_pin for the matching typed FILTER, and changing any single pinned field of it (member, lane constant, op, bound, verdict) is refused with that field's own PinError. Live nargo+bb prove+verify covers one helper per lane family. Host-side Rust only; no .nr source changed. Research grade, NOT externally audited (sq-qhy4); the value lanes carry the #769-accepted INV-VL downgrade (CR-G8). No soundness or privacy claim. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015CB7XiW3NDsggNCuDcuJJo * zk: pin the routed V2 commitment, tidy test docs and the decimal scale message Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015CB7XiW3NDsggNCuDcuJJo * zk: state the V2 date/time value domain precisely and the member-pinning dependency Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015CB7XiW3NDsggNCuDcuJJo --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 35a8d59 commit 6664717

9 files changed

Lines changed: 1722 additions & 66 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- **sparq-zk / sparq-zk-compose (opt-in `dual-leaf`):** `DualLeafV2` routes all six value lanes,
13+
and the host builds every lane's value-lane sub-proof inputs from typed operands.
14+
`dual_leaf_v2::is_hookable_datatype` and `encode_literal` now route `xsd:boolean`,
15+
`xsd:dateTime` and `xsd:date` as well as `xsd:integer`, `xsd:decimal` and `xsd:double`, so a
16+
graph committed under `DualLeafV2` carries a real value hook, under that datatype's own lane
17+
constant, for every literal on them — which is what a value-lane FILTER member needs the
18+
committed leaf to be. Two deliberate consequences. The three newly routed lanes inherit the
19+
fail-closed ingest rule, so any lexical outside their §13.2 value domain (non-canonical spellings such as
20+
`"1"^^xsd:boolean` or a `24:00:00` instant, and also XSD-canonical forms the domain excludes:
21+
an untimezoned or non-`Z` `xsd:dateTime`/`xsd:date`, or more than three fraction digits) now fails the WHOLE commitment instead
22+
of taking the degenerate string lane. And **a V2 commitment over a graph that CONTAINS a
23+
literal on one of those three lanes changes**; every leaf on the three already-routed lanes,
24+
every degenerate string-lane leaf, every IRI and blank-node leaf, and the whole-graph
25+
commitment over a graph of only those are BYTE-IDENTICAL, pinned by a fixed vector captured
26+
before the change. `DualLeafV1`'s routing is unchanged, so an already-issued V1 graph is
27+
unaffected. Beside this, `build::build_filter_value_dl_{int, f64, decimal}(&Literal, FilterOp,
28+
bound: &Literal)` complete the V2 builder set next to the existing `_bool` / `_datetime` /
29+
`_date` entries, over ONE shared assembly: both operands go through the lane's own
30+
`dual_leaf_v2` encoder, the COMMITTED literal's lane constant is carried, and the disclosed
31+
verdict is COMPUTED by that member's host verdict mirror (`build::integer_verdict`,
32+
`build::double_bits_verdict`, the shared `build::signed_epoch_verdict`) rather than taken from
33+
the caller. The decimal lane rescales the FILTER constant to the committed literal's scale
34+
EXACTLY — never rounded — refusing an inexact or overflowing rescale with
35+
`BuildValueDlV2Error::DecimalScaleNotRepresentable`, through the same two functions
36+
`value_pin` pins with. `BuiltFilterValueDlNumeric::lane_proof` feeds the checked typed prove
37+
entry, so the `Prover.toml` is rendered from validated typed fields and never from caller TOML.
38+
Every lane's builder output satisfies `value_pin::check_value_lane_pin` for the matching typed
39+
FILTER, and changing any single pinned field of it — member, lane constant, op, bound, verdict
40+
— is refused with that field's own `PinError`. **The value-lane verifier gates are UNCHANGED
41+
and still CLOSED:** `verifier::prefilter_manifest_structure` refuses every value-lane sub-proof
42+
at stage 0 with `CheckError::ValueLaneSubProofNotAccepted`, the stage-2 operand match,
43+
`filter_edge_true` and `encode_pattern_slot` are untouched, and a test asserts per lane that a
44+
manifest assembled entirely by these builders is still refused. Host-side Rust only; no `.nr`
45+
source changed. Research grade, NOT externally audited (`sq-qhy4`); the value lanes carry the
46+
#769-accepted INV-VL downgrade (CR-G8). No soundness or privacy claim.
47+
<!-- privacy-claims-allow: opt-in host routing + build helpers; the INV-VL downgrade and the external audit are framed as OPEN obligations; the verifier gates are stated as unchanged and closed; asserts no soundness/privacy property; sq-qhy4 / CR-G8 -->
48+
1249
- **sparq-zk-compose:** verifier-side **authenticated commitment method**. The verifier no longer has
1350
to assume which commitment method a committed graph was made under, and it does not read one from
1451
the manifest either — it RECOVERS it from the issuer signature. `method_auth` builds one candidate

‎crates/sparq-zk-compose/src/build.rs‎

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,319 @@ pub(crate) fn assemble_dl_datetime(
775775
})
776776
}
777777

778+
// === The three NUMERIC DualLeafV2 value lanes, from typed operands ===========
779+
//
780+
// `build_filter_value_dl_bool` / `_datetime` / `_date` above build the boolean,
781+
// dateTime and date lanes from typed `oxrdf::Literal` operands. The integer,
782+
// double and decimal lanes had no such entry: a caller had to assemble the
783+
// `ProofInputs` variant by hand, which meant deciding the lane constant, the
784+
// bound's encoding and (for decimal) the SCALE the bound is carried at outside
785+
// any checked path. The entries below close that, through ONE shared assembly.
786+
787+
/// The `DualLeafV2` value lane a [`build_numeric_value_lane`] call is on.
788+
#[cfg(feature = "dual-leaf")]
789+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
790+
enum NumericValueLane {
791+
/// `xsd:integer` — [`CircuitId::FilterValueDl`], a non-negative `u64` bound.
792+
Integer,
793+
/// `xsd:double` — [`CircuitId::FilterValueDlF64`], the bound's canonical
794+
/// IEEE-754 bit pattern.
795+
Double,
796+
/// `xsd:decimal` — [`CircuitId::FilterValueDlDecimal`], a sign-split
797+
/// magnitude at the COMMITTED literal's scale.
798+
Decimal,
799+
}
800+
801+
/// Why a numeric `DualLeafV2` value-lane builder refused. Every variant is
802+
/// fail-closed: nothing is rounded, promoted or repaired.
803+
#[cfg(feature = "dual-leaf")]
804+
#[derive(Debug, PartialEq, Eq)]
805+
pub enum BuildValueDlV2Error {
806+
/// A host encoder refused an operand (the §6 fail-closed co-binding): the
807+
/// wrong datatype, or a lexical form outside the lane's canonical domain.
808+
Encode(sparq_zk::dual_leaf::DualLeafError),
809+
/// An operand's encoded value handle is outside its lane's handle domain:
810+
/// not a `u64` (nor, on the sign-folded decimal lane, the field negation of
811+
/// one). No shipped encoder produces such a handle — the integer lane's is a
812+
/// non-negative `u64`, the double lane's a 64-bit pattern, and the decimal
813+
/// lane's a sign-folded `u64` magnitude — so this is surfaced fail-closed
814+
/// rather than unwrapping an invariant the type system does not carry, the
815+
/// same way `assemble_dl_datetime` treats its own split.
816+
HandleOutsideLaneDomain { lexical: String },
817+
/// The decimal lane: one of the two operands' lane constants names no
818+
/// admitted scale, or the FILTER constant cannot be rescaled to the
819+
/// COMMITTED literal's scale exactly (it would need rounding, or the
820+
/// rescaled magnitude overflows a `u64`).
821+
DecimalScaleNotRepresentable { value: String, bound: String },
822+
}
823+
824+
#[cfg(feature = "dual-leaf")]
825+
impl From<sparq_zk::dual_leaf::DualLeafError> for BuildValueDlV2Error {
826+
fn from(e: sparq_zk::dual_leaf::DualLeafError) -> Self {
827+
BuildValueDlV2Error::Encode(e)
828+
}
829+
}
830+
831+
#[cfg(feature = "dual-leaf")]
832+
impl std::fmt::Display for BuildValueDlV2Error {
833+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
834+
match self {
835+
BuildValueDlV2Error::Encode(e) => write!(f, "{}", e),
836+
BuildValueDlV2Error::HandleOutsideLaneDomain { lexical } => write!(
837+
f,
838+
"the value handle of {} is outside its value lane's handle domain",
839+
lexical
840+
),
841+
BuildValueDlV2Error::DecimalScaleNotRepresentable { value, bound } => write!(
842+
f,
843+
"the FILTER constant {} and the committed literal {} have no admitted scale at which both are carried exactly",
844+
bound, value
845+
),
846+
}
847+
}
848+
}
849+
850+
#[cfg(feature = "dual-leaf")]
851+
impl std::error::Error for BuildValueDlV2Error {}
852+
853+
/// A built `DualLeafV2` value-lane FILTER on one of the three NUMERIC lanes: the
854+
/// public [`ProofInputs`] plus that member's PRIVATE witness, in the shape
855+
/// `crate::value_lane` takes.
856+
///
857+
/// The witness is the [`sparq_zk::dual_leaf::DualLeafComponents`] the lane
858+
/// encoder produced for the COMMITTED literal, so `inputs.operand_enc` is by
859+
/// construction the leaf it rebinds to in-circuit.
860+
#[cfg(feature = "dual-leaf")]
861+
#[derive(Debug, Clone, PartialEq, Eq)]
862+
pub struct BuiltFilterValueDlNumeric {
863+
/// Public inputs for the lane's member, carrying the COMMITTED literal's own
864+
/// V2 lane constant.
865+
pub inputs: ProofInputs,
866+
/// PRIVATE: the member's witness — [`crate::value_lane::LaneWitness::Hook`]
867+
/// for the integer and double lanes, `Signed` for the decimal lane.
868+
pub witness: crate::value_lane::LaneWitness,
869+
}
870+
871+
#[cfg(feature = "dual-leaf")]
872+
impl BuiltFilterValueDlNumeric {
873+
/// The typed inputs of the checked prove entry
874+
/// (`crate::driver::CircuitProver::prove_value_lane`), assembled by
875+
/// [`crate::value_lane::ValueLaneProof::from_inputs`] — so a public field
876+
/// edited after the build is re-checked rather than trusted, and the
877+
/// `Prover.toml` is rendered from the validated typed fields rather than from
878+
/// caller text.
879+
pub fn lane_proof(
880+
&self,
881+
) -> Result<crate::value_lane::ValueLaneProof, crate::dispatch::DispatchError> {
882+
crate::value_lane::ValueLaneProof::from_inputs(&self.inputs, self.witness.clone())
883+
}
884+
}
885+
886+
/// The INTEGER-lane verdict for `value <op> bound` over `u64` — the host mirror
887+
/// of the `filter_value_dl_int` member's `integer_verdict`
888+
/// (`zk/compose/compose_core/src/filter_value.nr`).
889+
#[cfg(feature = "dual-leaf")]
890+
pub fn integer_verdict(value: u64, op: FilterOp, bound: u64) -> bool {
891+
match op {
892+
FilterOp::Lt => value < bound,
893+
FilterOp::Le => value <= bound,
894+
FilterOp::Gt => value > bound,
895+
FilterOp::Ge => value >= bound,
896+
FilterOp::Eq => value == bound,
897+
FilterOp::Ne => value != bound,
898+
}
899+
}
900+
901+
/// The DOUBLE-lane verdict for `value <op> bound` over two IEEE-754 bit patterns
902+
/// — the host mirror of the `filter_value_dl_f64` member's `f64_verdict`, which
903+
/// compares the two patterns AS doubles (so a NaN operand is unordered and only
904+
/// `NE` holds of it, and `-0.0` compares equal to `+0.0`).
905+
///
906+
/// The member folds its witnessed handle to the SPARQL-numeric canonical bits
907+
/// before comparing; the host encoder already emits those canonical bits, so the
908+
/// two agree on the handle this is called with.
909+
#[cfg(feature = "dual-leaf")]
910+
pub fn double_bits_verdict(value_bits: u64, op: FilterOp, bound_bits: u64) -> bool {
911+
let a = f64::from_bits(value_bits);
912+
let b = f64::from_bits(bound_bits);
913+
match op {
914+
FilterOp::Lt => a < b,
915+
FilterOp::Le => a <= b,
916+
FilterOp::Gt => a > b,
917+
FilterOp::Ge => a >= b,
918+
FilterOp::Eq => a == b,
919+
FilterOp::Ne => a != b,
920+
}
921+
}
922+
923+
/// Build a `DualLeafV2` `xsd:integer` value-lane FILTER over the committed
924+
/// literal `literal` under `op` against the constant `bound`.
925+
///
926+
/// Both operands go through `sparq_zk::dual_leaf_v2::encode_integer`, so the
927+
/// disclosed `bound` is the same `u64` handle the committer would have folded,
928+
/// derived by the same fail-closed canonical predicate. A negative or
929+
/// non-canonically spelled integer on either side returns the encoder's error.
930+
///
931+
/// The disclosed verdict is COMPUTED ([`integer_verdict`]) rather than taken from
932+
/// the caller. The resulting public inputs are exactly what
933+
/// [`crate::value_pin::check_value_lane_pin`] requires of this lane for the
934+
/// matching typed FILTER; [`BuiltFilterValueDlNumeric::lane_proof`] is the
935+
/// validated typed path to a `Prover.toml`.
936+
///
937+
/// DOCUMENTED RISK: inherits the value lane's INV-VL downgrade (#769 accepted,
938+
/// CR-G8 / sq-qhy4). NOT externally audited; no soundness / privacy claim.
939+
#[cfg(feature = "dual-leaf")]
940+
pub fn build_filter_value_dl_int(
941+
literal: &oxrdf::Literal,
942+
op: FilterOp,
943+
bound: &oxrdf::Literal,
944+
) -> Result<BuiltFilterValueDlNumeric, BuildValueDlV2Error> {
945+
build_numeric_value_lane(NumericValueLane::Integer, literal, op, bound)
946+
}
947+
948+
/// Build a `DualLeafV2` `xsd:double` value-lane FILTER — the same shared path as
949+
/// [`build_filter_value_dl_int`], with `b_bits` the bound's CANONICAL IEEE-754
950+
/// bit pattern (both operands through `sparq_zk::dual_leaf_v2::encode_double`,
951+
/// which folds `-0.0` and every NaN payload to one handle) and the verdict from
952+
/// [`double_bits_verdict`].
953+
///
954+
/// DOCUMENTED RISK: as [`build_filter_value_dl_int`].
955+
#[cfg(feature = "dual-leaf")]
956+
pub fn build_filter_value_dl_f64(
957+
literal: &oxrdf::Literal,
958+
op: FilterOp,
959+
bound: &oxrdf::Literal,
960+
) -> Result<BuiltFilterValueDlNumeric, BuildValueDlV2Error> {
961+
build_numeric_value_lane(NumericValueLane::Double, literal, op, bound)
962+
}
963+
964+
/// Build a `DualLeafV2` `xsd:decimal` value-lane FILTER — the same shared path as
965+
/// [`build_filter_value_dl_int`], on the SIGN-FOLDED decimal member.
966+
///
967+
/// The decimal lane folds its SCALE into the lane constant, and the member
968+
/// compares the two magnitudes at one scale. The scale is therefore the COMMITTED
969+
/// literal's — it is what that literal's leaf was built with — so the bound is
970+
/// rescaled to it EXACTLY, never rounded: `"1.5"` against a literal written
971+
/// `"2.50"` is carried as `150` at scale 2, and a bound that would need rounding
972+
/// (or whose rescaled magnitude overflows a `u64`) is
973+
/// [`BuildValueDlV2Error::DecimalScaleNotRepresentable`]. That is the same exact
974+
/// rescale `crate::value_pin` pins, through the same two functions.
975+
///
976+
/// DOCUMENTED RISK: as [`build_filter_value_dl_int`].
977+
#[cfg(feature = "dual-leaf")]
978+
pub fn build_filter_value_dl_decimal(
979+
literal: &oxrdf::Literal,
980+
op: FilterOp,
981+
bound: &oxrdf::Literal,
982+
) -> Result<BuiltFilterValueDlNumeric, BuildValueDlV2Error> {
983+
build_numeric_value_lane(NumericValueLane::Decimal, literal, op, bound)
984+
}
985+
986+
/// The ONE assembly behind the three numeric-lane entries: encode BOTH operands
987+
/// with the lane's own V2 encoder, split each handle into `(neg, magnitude)`,
988+
/// carry the committed literal's lane constant, compute the verdict with that
989+
/// member's host verdict mirror, and self-check the pairing.
990+
#[cfg(feature = "dual-leaf")]
991+
fn build_numeric_value_lane(
992+
lane: NumericValueLane,
993+
literal: &oxrdf::Literal,
994+
op: FilterOp,
995+
bound: &oxrdf::Literal,
996+
) -> Result<BuiltFilterValueDlNumeric, BuildValueDlV2Error> {
997+
use crate::value_lane::{HookWitness, LaneWitness, SignedWitness};
998+
let encode = match lane {
999+
NumericValueLane::Integer => sparq_zk::dual_leaf_v2::encode_integer,
1000+
NumericValueLane::Double => sparq_zk::dual_leaf_v2::encode_double,
1001+
NumericValueLane::Decimal => sparq_zk::dual_leaf_v2::encode_decimal,
1002+
};
1003+
let value = encode(literal)?;
1004+
let bound_components = encode(bound)?;
1005+
let outside = |l: &oxrdf::Literal| BuildValueDlV2Error::HandleOutsideLaneDomain {
1006+
lexical: l.to_string(),
1007+
};
1008+
// The two HOOK lanes take a NON-NEGATIVE handle (the integer lane's value,
1009+
// the double lane's 64-bit pattern); the decimal lane takes a sign-folded
1010+
// one. Each operand's handle is read in its lane's own domain, so a handle
1011+
// outside it is refused rather than having its sign discarded.
1012+
let split = |hook: &Fr, l: &oxrdf::Literal| -> Result<(bool, u64), BuildValueDlV2Error> {
1013+
match (lane, split_signed_hook(hook)) {
1014+
(NumericValueLane::Decimal, Some(parts)) => Ok(parts),
1015+
(_, Some((false, mag))) => Ok((false, mag)),
1016+
_ => Err(outside(l)),
1017+
}
1018+
};
1019+
let (value_neg, value_mag) = split(&value.value_hook, literal)?;
1020+
let (bound_neg, bound_mag) = split(&bound_components.value_hook, bound)?;
1021+
1022+
let operand_enc = hexf(&value.leaf());
1023+
let datatype_const = hexf(&value.datatype_const);
1024+
let inputs = match lane {
1025+
NumericValueLane::Integer | NumericValueLane::Double => {
1026+
debug_assert!(!value_neg && !bound_neg, "the hook lanes split unsigned");
1027+
if lane == NumericValueLane::Integer {
1028+
ProofInputs::FilterValueDl {
1029+
id: CircuitId::FilterValueDl,
1030+
operand_enc,
1031+
op,
1032+
bound: bound_mag,
1033+
datatype_const,
1034+
expected: integer_verdict(value_mag, op, bound_mag),
1035+
}
1036+
} else {
1037+
ProofInputs::FilterValueDlF64 {
1038+
id: CircuitId::FilterValueDlF64,
1039+
operand_enc,
1040+
op,
1041+
b_bits: bound_mag,
1042+
datatype_const,
1043+
expected: double_bits_verdict(value_mag, op, bound_mag),
1044+
}
1045+
}
1046+
}
1047+
NumericValueLane::Decimal => {
1048+
let scale_error = || BuildValueDlV2Error::DecimalScaleNotRepresentable {
1049+
value: literal.to_string(),
1050+
bound: bound.to_string(),
1051+
};
1052+
// The member works at ONE scale, and that scale is the committed
1053+
// literal's: it is folded into the lane constant its leaf carries.
1054+
let value_fd = crate::value_pin::fraction_digits_of_decoded(&value.datatype_const)
1055+
.ok_or_else(scale_error)?;
1056+
let bound_fd =
1057+
crate::value_pin::fraction_digits_of_decoded(&bound_components.datatype_const)
1058+
.ok_or_else(scale_error)?;
1059+
let bound_scaled = crate::value_pin::rescale_exact(bound_mag, bound_fd, value_fd)
1060+
.ok_or_else(scale_error)?;
1061+
// No `-0`: the encoder rejects a negative zero, and an EXACT rescale
1062+
// of a non-zero magnitude is non-zero (scaling down requires exact
1063+
// divisibility), so `bound_neg` cannot pair with a zero magnitude.
1064+
ProofInputs::FilterValueDlDecimal {
1065+
id: CircuitId::FilterValueDlDecimal,
1066+
operand_enc,
1067+
op,
1068+
bound_neg,
1069+
bound_scaled,
1070+
datatype_const,
1071+
expected: signed_epoch_verdict(value_neg, value_mag, bound_neg, bound_scaled, op),
1072+
}
1073+
}
1074+
};
1075+
v2_self_check(&inputs);
1076+
let witness = match lane {
1077+
NumericValueLane::Integer | NumericValueLane::Double => LaneWitness::Hook(HookWitness {
1078+
value_hook: hexf(&value.value_hook),
1079+
lexical_component: hexf(&value.lexical_component),
1080+
}),
1081+
// The member takes the MAGNITUDE privately and re-folds the sign itself.
1082+
NumericValueLane::Decimal => LaneWitness::Signed(SignedWitness {
1083+
value_neg,
1084+
value_hook_scaled: hexf(&Fr::from(value_mag)),
1085+
lexical_component: hexf(&value.lexical_component),
1086+
}),
1087+
};
1088+
Ok(BuiltFilterValueDlNumeric { inputs, witness })
1089+
}
1090+
7781091
/// Encode an xsd:integer literal `value` to its term encoding under `salt`
7791092
/// (salt-independent for literals; convenience for callers wiring a filter to
7801093
/// a known constant).

0 commit comments

Comments
 (0)