The service uses pure Rust implementation for Bulletproofs, a technique allowing to prove a range of a value hidden with the help of the Pedersen commitment scheme. Commitments are used instead of values both in account details and in transfer transactions.
The service uses account-based scheme similar to one used for ERC-20 tokens in Ethereum;
it is also used in simpler demo cryptocurrency services for Exonum.
Unlike other demos, each wallet contains a commitment to the current
balance Comm(bal; r)
instead of its plaintext value bal
. Only the owner of the account
knows the opening to this commitment.
Each transfer transaction contains a commitment to the transferred amount C_a = Comm(a; r)
.
It is supplied with two range proofs:
- The amount is positive:
a > 0
- The sender has sufficient balance on his account:
sender.bal >= a
The first proof is stateless, i.e., can be verified without consulting the blockchain state.
In order to verify the second proof, it’s necessary to know the commitment C_bal
to the sender’s current balance (which is stored in her wallet info). The proof is equivalent
to proving C_bal - C_a
opens to a value in the allowed range.
A natural question is how the receiver of the payment finds out about its amount a
;
by design, it is impossible using only blockchain information. We solve this
by asymmetrically encrypting the opening to C_a
– i.e., pair (a, r)
–
with the help of box
routine from libsodium
, so it can only be decrypted by the
receiver and sender of the transfer. For simplicity, we convert Ed25519 keys used
to sign transactions to Curve25519 keys required for box
; i.e., accounts are identified
by a single Ed25519 public key.
A sender may maliciously encrypt garbage. Thus, we give the receiver a certain amount of time after the transfer transaction is committed, to verify that she can decrypt it. To signal successful verification, the receiver creates and sends a separate acceptance transaction referencing the transfer.
The sender’s balance decreases immediately after the transfer transaction is committed (recall that it is stored as a commitment, so we perform arithmetic on commitments rather than plaintext values). The receiver’s balance is not changed immediately; it is only increased (again, using commitment arithmetic) only after her appropriate acceptance transaction is committed.
To prevent deadlocks, each transfer transaction specifies the timelock parameter
(in relative blockchain height, a la Bitcoin’s CSV
opcode). If this timelock expires
and the receiver of the transfer still hasn’t accepted it,
the transfer is automatically refunded to the sender.
The scheme described above is almost practical, except for one thing: the sender might not now her balance precisely at the moment of transfer! Indeed, it might happen that the sender’s stray accept transaction or a refund are processed just before the sender’s transfer (but after the transfer has been created, signed and sent to the network). Hence, if we simply retrieve the sender’s balance from the blockchain state during transaction execution, there is a good chance it will differ from the one the sender had in mind when creating the sufficient balance proof.
In order to alleviate this problem, we allow the sender to specify what she thinks
is the length of her wallet history history_len
. The proof of sufficient balance
is then checked against the balance commitment at this point in history.
For this scheme to be safe, we demand that history_len - 1 >= last_send_index
,
where last_send_index
is the index of the latest outgoing transfer in the sender’s history
(we track last_send_index
directly in the sender’s account details).
If this inequality holds, it’s safe to process the transfer; we know for sure that since
last_send_index
the sender’s balance can only increase (via incoming transfers
and/or refunds). Thus, if we subtract the transfer amount from the sender’s current balance,
we still end up with non-negative balance.
Even with heuristics described above, the scheme is limiting: before making a transfer, the sender needs to know that there are no other unconfirmed outgoing transfers. This problem could be solved with auto-increment counters a la Ethereum, or other means to order transactions originating from the same user. This is outside the scope of this PoC.