Value Commitments and Balance
1. Why this chapter exists
Sapling hides amounts by committing to them with a Pedersen-style commitment
whose value slot uses one generator and whose randomness slot uses another.
Because the scheme is additively homomorphic, the sum of all spend value
commitments minus the sum of all output value commitments minus a valueBalance
offset must equal a commitment to zero. That fact is what the binding
signature asserts. Without the binding signature there is no inflation
protection, and that protection has to be implemented faithfully or Zcash is
broken at the protocol level.
By the end you should know exactly how the binding key bvk is constructed, why
the binding signature is over the same key the prover knows, and where the
value-balance integer-range bounds live.
2. Definitions
Definition 9.1 (value commitment). Given a non-negative note value and a trapdoor ,
where the two generators are
VALUE_COMMITMENT_VALUE_GENERATOR
and
VALUE_COMMITMENT_RANDOMNESS_GENERATOR.
The commitment lives in , not in , because the
generators are points in but the result is treated as an
ExtendedPoint so that subgroup tests can reject small-order commitments at
deserialisation. Code:
ValueCommitment::derive.
Definition 9.2 (value balance). The net value moved into / out of the shielded pool in a bundle is
The protocol bounds to a signed 63-bit integer (in Rust:
i64, but only values in are valid in the abstract
protocol). Code:
ValueSum.
Definition 9.3 (binding signing key bsk, binding verification key bvk). Set
and
Then , so
is the RedJubjub signing key whose verification key is . The
verifier reconstructs from the transaction's commitments and
value-balance, and checks the binding signature against it. Code:
TrapdoorSum::into_bsk,
CommitmentSum::into_bvk.
Theorem 9.4 (balance soundness, informal). If an adversary produces a Sapling bundle that verifies (proofs valid, binding signature valid, value commitments not small-order), then the sum equals in , provided the discrete log of with respect to is unknown. Proof sketch: a valid binding signature on the constructed requires knowledge of a such that . Plugging in the definitions: any attempt to inflate (e.g. claim a spend of while committing to ) forces to contain a nonzero multiple of , but extracting from such a would yield a discrete log relation between the two generators. Citation: Hopwood et al., Zcash Protocol Specification §4.13.
Invariant 9.5 (cv is not small order). A value commitment received from the wire must not be a small-order Jubjub point. The defence at the consensus level is at deserialisation:
loading...
A small-order would allow an attacker to manipulate the binding key in a way that bypasses the soundness reduction; the practical impact would be to commit to one value but produce a signature compatible with a different value.
3. The code
3.1 The three layers
value.rs exposes three types:
NoteValue: a transparentu64newtype with aZEROconstant.ValueCommitTrapdoor: a wrapper aroundjubjub::Scalar.ValueCommitment: a wrapper aroundjubjub::ExtendedPoint.
loading...
loading...
loading...
3.2 Sums
value/sums.rs is where the homomorphism lives. It is essentially a wrapper
around field/group sums plus a conversion to the binding key pair:
loading...
loading...
loading...
The Sub<&ValueCommitment> for ValueCommitment impls (lines 234-261) are how
spends contribute positively and outputs negatively. Look at how the
absolute-value handling works on lines 192-202: i64's range is asymmetric, so
the function explicitly handles i64::MIN.
3.3 The consistency test
There is one proptest that closes the loop:
loading...
It generates a vector of (value, trapdoor) pairs, computes bsk from the
trapdoors, bvk from the commitments and value balance, and asserts the
RedJubjub equation VerificationKey::from(&bsk) == bvk. This is the in-crate
proof that the homomorphism is correctly wired; if you change one of the
generators or break a sum direction, this test fails.
4. Failure modes
- Wrong sign in
into_bvk. The function flips signs of the value balance based on whethervalue_balance.is_negative(). If that branch is inverted, the binding signature still verifies for some bundles (where vbalance happens to be zero) but fails for others. Caught by: thebsk_consistent_with_bvkproptest with high probability. - Adding spends and outputs with the same sign. If
CommitmentSum::add_assignwere accidentally used in the output loop instead ofsub_assign, every output would inflate the binding sum by+cvinstead of-cv. Caught by: the same proptest. - A small-order
cvslipping past deserialisation. If a wire parser calledValueCommitment(jubjub::ExtendedPoint::from_bytes(b).unwrap())instead ofValueCommitment::from_bytes_not_small_order(b), a small-order commitment would be admitted, and an attacker could later exploit the cofactor 8 to claim a spend of while committing to a different value. Caught by:SaplingVerificationContextInner::check_spendhas the small-order check baked into the call sites; the comment on lines 45-48 documents the assumption. - Overflow in
vbalance.ValueSumis ani128, large enough to hold all sums up ton_spends + n_outputs <= 2^64. Casting it toi64forinto_bvkcan lose information; that is whatBalanceError::Overflowandtry_into()guard against. Caught by: tests invalue/sums.rsandvalue.rs.
5. Spec pointers
- Zcash Protocol Specification, §4.13 (Balance and Binding Signature)
is the authority. The range bounds (the
[-(r_J - 1)/2..(r_J - 1)/2]constraint cited invalue/sums.rs::ValueSum) are quoted from this section. - Zcash Protocol Specification, §5.4.8.3 (Homomorphic Pedersen commitments)
is the source of
ValueCommit^Saplingexactly as implemented inValueCommitment::derive.
6. Exercises
- Verify the homomorphism by hand. Take two spends with values ,
and one output with value . Sample three trapdoors.
Compute the three commitments. Compute
bsk = rcv_1 + rcv_2 - rcv. Computebvktwo ways: (a) directly as , (b) viaCommitmentSum::into_bvkwithvalue_balance = 20. Assert they match. - Add a test for the
i64::MINedge case. Theinto_bvkabsolute-value handler has a special case forvalue.checked_abs() == None, which only triggers atvalue = i64::MIN. Add a unit test that constructs aCommitmentSum(any sum) and callsinto_bvk(i64::MIN). The test should not panic. - Tighten the range comment. The
ValueSumdoc comment cites the abstract bound[-(r_J - 1)/2..(r_J - 1)/2]and the concrete-Zcash bound[−38913406623490299131842..104805176454780817500623]. Verify numerically that the concrete bound is what you get from21 * MAX_MONEY = 21 * 10^15(whereMAX_MONEYis the maximum per-output value Zcash allows). Comment on whether the asymmetry is intentional.
Answers in the code. For exercise 1, the homomorphism is the exact subject
of the
bsk_consistent_with_bvk
proptest. For exercise 3, the asymmetric bound comes from the fact that the
protocol uses i64 for vbalance but limits the Sapling-transactable supply to
zatoshi; the concrete upper bound is therefore approximately
for a known constant . See
Spec §4.13.