Protocol Integrity
Negative Transfer Protection

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:

  1. 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.
  2. 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-prove

The 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 framingWhy the range proof rejects it
Treat the wraparound as the transfer amountA 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, so Text(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 a 0 bit.

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.go

Look 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

LayerGuarantee
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 verificationEvery 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:

Privacy suite: