Negative Transfer Protection
The chain rejects it. No DERO transaction can transfer a negative amount or create coins from nothing — the network refuses to accept it. The protection isn't a check inside the wallet that a malicious sender could remove; it's enforced by every node, by recomputing the math on every transfer (formally: range-proof soundness + value conservation).
The claim
DERO consensus cannot accept a transaction that transfers a negative amount or mints value from nothing.
This is a mathematical guarantee, enforced independently by every validating node. Crucially, it does not depend on the sender's wallet behaving honestly. A malicious sender controls their own client, so any protection that lived only in proof generation could be patched out. The protection lives in proof verification, where it cannot.
What every transfer must prove
DERO (Stargate) holds account balances as homomorphic ElGamal ciphertexts. A transfer never reveals amounts; instead it carries zero-knowledge proofs that constrain what could have happened:
- A Sigma proof binds the transaction together: the sender controls their key, and the encrypted amounts conserve value across the statement — what leaves one account enters others, nothing is created.
- A Bulletproof range proof proves the hidden quantities lie in a valid non-negative range.
The range proof is the one that closes the door on negative transfers. DERO packs two quantities into a single 128-bit value and range-proves them together (cryptography/crypto/proof_generate.go:468-471):
// transfer amount in the low 64 bits, sender's remaining balance in the high 64 bits
btransfer := new(big.Int).SetInt64(int64(witness.TransferAmount)) // this should be reduced
bdiff := new(big.Int).SetInt64(int64(witness.Balance)) // this should be reduced
number := btransfer.Add(btransfer, bdiff.Lsh(bdiff, 64)) // 128-bit value to range-proveThe trailing // this should be reduced comments are the DERO author's own forensic note in source — flagging that reduction modulo the bn256 group order should be applied to these inputs. They are preserved verbatim here because they carry that authorial signal.
So one range proof establishes both:
- the amount sent is a valid non-negative 64-bit number, and
- the sender's remaining balance is a valid non-negative 64-bit number.
Why both matter: proving only the amount is non-negative would not stop an overspend. By also proving the remaining balance is non-negative, the same proof guarantees you cannot send more than you hold — and cannot manufacture a balance by sending a "negative" amount.
Why a negative transfer cannot produce a valid proof
A "negative" amount is just the two's-complement uint64 wraparound of a value near 2^64. Fed into the construction, it fails the range proof from both directions:
| Attack framing | Why the range proof rejects it |
|---|---|
| Treat the wraparound as the transfer amount | A value near 2^64 lies outside [0, 2^64) — the low-64 range bound fails. |
| Treat it as receiving (so your own balance jumps) | The conservation proof forces a matching decrease; the sender's remaining balance goes negative → wraps → the high-64 range bound fails. |
The decisive property is computational soundness — under the discrete-log assumption on bn256, in the random oracle model (Fiat-Shamir). A Bulletproof convinces the verifier that the committed value is in range without revealing it, and soundness means a prover cannot construct a proof that verifies for a value outside the range, except with negligible probability bounded by the underlying hardness. There is no "almost in range," no rounding, no edge case: either a verifying proof exists (value in range) or — modulo that negligible cheating probability — it does not. See Cryptographic Assumptions for the full stack.
Common misconception — it is not the minus sign. A claim circulates that protection comes from Go's BigInt.Text(2) emitting a '-' that "breaks the bit loop." That is not the mechanism:
- That loop is proof generation (the sender's wallet), not node verification — an attacker controls it and could change it.
- For a real transaction
number = transfer + (balance << 64)is positive, soText(2)produces no'-'at all. - The loop is an
if bit == '1' { … } else { … }; it does not detect or reject anything — a stray character would simply be treated as a0bit.
The protection is the verifier's Bulletproof check plus homomorphic conservation. The bit decomposition below is just how an honest prover builds the proof — it is not the safeguard.
How the bit decomposition actually works
For completeness, lines 473–487 of proof_generate.go turn number into the two vectors a Bulletproof commits to:
number_string := reverse("000…000" + number.Text(2)) // binary digits, zero-padded
bits := number_string[0:128] // low 128 bits
for _, b := range []byte(bits) {
if b == '1' { aL = 1; aR = 0 } // aL[i] = the i-th bit
else { aL = 0; aR = -1 } // aR[i] = aL[i] − 1 (mod group order)
}aL is the bit vector of number; aR = aL − 1. Note: in the actual source at proof_generate.go:485, the -1 is stored as new(big.Int).Mod(new(big.Int).SetInt64(-1), bn256.Order) — i.e. the field-order representative of -1 (a 256-bit positive integer congruent to -1 mod bn256.Order). A literal -1 in Go's big.Int would carry the negative sign and would not be equivalent in the bulletproof's polynomial relations; the Mod(..., bn256.Order) is load-bearing. The proof then demonstrates, in zero knowledge, that for every position aL ∘ aR = 0 and aL − aR = 1 — i.e. each component really is a single bit (0 or 1) — and that those bits reconstruct the committed value. A value that is not a genuine 128-bit non-negative integer cannot satisfy those constraints, so the inner-product argument the verifier checks will not close.
This is generation code, shown for understanding. What enforces the rule is cryptography/crypto/proof_verify.go, run by every node: it re-derives the commitments and checks the inner-product relation. A malformed or out-of-range number yields a proof that simply fails that check.
Verify it yourself
git clone https://github.com/deroproject/derohe.git
cd derohe
# The packing + bit decomposition (proof GENERATION)
sed -n '466,490p' cryptography/crypto/proof_generate.go
# The verification every node runs (the actual ENFORCEMENT)
grep -n "func.*Verify" cryptography/crypto/proof_verify.goLook for the transfer + (balance << 64) packing and the 128-bit decomposition in proof_generate.go, then the inner-product / range check the verifier performs in proof_verify.go. The safeguard is on the verify side — that is the half an attacker cannot control.
Why this cannot be bypassed
| Layer | Guarantee |
|---|---|
| Range proof (Bulletproof) | Committed transfer and remaining balance are provably in [0, 2^64). Soundness ⇒ no verifying proof exists for an out-of-range value. |
| Homomorphic conservation (Sigma) | Encrypted inputs and outputs are bound so value is conserved — coins cannot be created, only moved. |
| Independent verification | Every node re-verifies both proofs before accepting a block. There is no trusted client to subvert. |
Bottom line: Negative transfers are not blocked by a policy or a parser that could be patched around. They are blocked because no prover can produce a range proof that verifies for a negative or out-of-range amount, and because the homomorphic statement conserves value. Minting coins this way is not "hard" — it is cryptographically impossible.
Related Pages
Security suite:
- Range Proof Integrity — how the range proof binds amounts
- Transaction Proofs — the full proof set every node checks
- Proof Verification Flow — end-to-end validation
- 2022 Inflation Claim — point-by-point rebuttal of the circulated negative-transfer report
Privacy suite:
- Bulletproofs — zero-knowledge range proofs