Mix.install([
{:bitcoinex, "~> 0.1.7"}
])
The Cashu
module implements Blind Diffie Hellman Key Exchange according to the Cashu spec, and leaning on the Python implementation, itself inspired by Ruben Somsen's writeup of the method.
This approach gives us blind Chaumian ecash, where the a bank (or "mint") can issue a bearer token, and the user can redeem it without the bank knowing who the redeemer is (hence "blind"). In this sense, Chaumian ecash preserves user privacy while requiring full trust in the bank to honor deposits.
Feel free to use the module as reference, and skip to the guided walkthrough below it.
defmodule Cashu do
@moduledoc """
An implementation of Blind Diffie Hellman Key Exchange according to the Cashu spec.
"""
alias Bitcoinex.Secp256k1
alias Bitcoinex.Secp256k1.{Math, Params, Point, PrivateKey}
@n Params.curve().n
@max_privkey @n - 1
@doc """
Map a hash to the Secp256k1 elliptic curve.
"""
def hash_to_curve(msg) do
hash = sha256_hash(msg)
x = "02" <> Base.encode16(hash, case: :lower)
iterate_to_valid_point(x)
end
defp iterate_to_valid_point(x) do
case Secp256k1.get_y(x, false) do
{:error, "invalid sq root"} ->
iterate_to_valid_point(sha256_hash(x))
{:ok, y} ->
{:ok, %Point{x: :binary.decode_unsigned(x), y: y}}
end
end
@doc """
In the first step of the exchange, Alice has a secret message, and a secret number to blind this message.
If the number (the "blinding factor") doesn't exist we create it.
She provides Bob with a blinded point.
"""
def step1_alice(secret_msg) do
{:ok, blinding_factor} = random_number() |> PrivateKey.new()
step1_alice(secret_msg, blinding_factor)
end
def step1_alice(secret_msg, blinding_factor) do
# Get a point on the curve from the secret message
{:ok, point} = hash_to_curve(secret_msg)
# Get the public key from the blinding factor
blinding_point = PrivateKey.to_point(blinding_factor)
# Add public keys to get B_
blinded_point = Math.add(point, blinding_point)
# Return B_ and the blinding factor
{blinded_point, blinding_factor.d}
end
@doc """
In step 2, Bob (the mint) uses this blinded point, and his private key "a",
to sign this blinded point. Effectively "committing" to this blinded point.
He returns this signature-on-blinded-point "c_" .
"""
def step2_bob(b_point, a_privkey) do
c_ = Math.multiply(b_point, a_privkey.d)
{:ok, e, s} = step2_bob_dleq(b_point, a_privkey)
{:ok, c_, e, s}
end
@doc """
In step 3, Alice takes this signed commitment "c_", Bob's public key "a_point",
and her blinding factor from step 1.
By substracting her blinding factor from c_ , she "unblinds" the signature on her secret message.
This is your ecash token.
"""
def step3_alice(c_, r, a_point) do
{:ok, a_negated} =
a_point
|> Math.multiply(r)
|> negate()
Math.add(c_, a_negated)
end
@doc """
A discrete log equality proof (DLEQ)
r = random nonce
R1 = r*G
R2 = r*B'
e = hash(R1,R2,A,C')
s = r + e*a
"""
def step2_bob_dleq(b_point, a_privkey) do
# Generate a random PrivateKey p for the nonce
{:ok, p_priv} = random_number() |> PrivateKey.new()
step2_bob_dleq(b_point, a_privkey, p_priv)
end
def step2_bob_dleq(b_point, a_privkey, p_priv) do
r1 = PrivateKey.to_point(p_priv)
r2 = Math.multiply(b_point, p_priv.d)
a_point = PrivateKey.to_point(a_privkey)
c_ = Math.multiply(b_point, a_privkey.d)
e = hash_pubkeys([r1, r2, a_point, c_]) |> :binary.decode_unsigned()
# scalar multiplication here, therefore modulo the curve order.
multiplied = Math.modulo(a_privkey.d * e, @n)
s = Math.modulo(p_priv.d + multiplied, @n)
{:ok, e, s}
end
@doc """
To verify the DLEQ, Alice recreates r1 and r2 from e,s provided by Bob.
"""
def alice_verify_dleq(b_, c_, e, s, a_point) do
{:ok, a_negated} = a_point |> Math.multiply(e) |> negate()
r1 = s |> PrivateKey.to_point() |> Math.add(a_negated)
{:ok, c_negated} = c_ |> Math.multiply(e) |> negate()
r2 = b_ |> Math.multiply(s) |> Math.add(c_negated)
hash_k = [r1, r2, a_point, c_] |> hash_pubkeys() |> :binary.decode_unsigned()
e == hash_k
end
@doc """
To verify that the user/client (Alice) has the right message, the mint (Bob) takes the unblinded sig from Alice,
and the original secret message, and checks it against their private key.
If multiplying the message hashed to curve by Bob's private key is equal to the unblinded signature,
then the user's unblinded signature is valid for the message.
"""
def is_valid?(a_privkey, c, msg) do
{:ok, y} = hash_to_curve(msg)
c == Math.multiply(y, a_privkey.d)
end
def sha256_hash(msg), do: :crypto.hash(:sha256, msg)
def random_number(), do: :rand.uniform(@max_privkey)
def negate(point_a) do
pubkey = Point.serialize_public_key(point_a)
case negate_hex(pubkey) do
{:ok, a_negated} -> Point.parse_public_key(a_negated)
{:error, reason} -> raise(reason)
end
end
def negate_hex("02" <> rest), do: {:ok, "03" <> rest}
def negate_hex("03" <> rest), do: {:ok, "02" <> rest}
def negate_hex(pubkey), do: {:error, "pubkey prefix did not match 02 or 03, got #{pubkey}"}
def hash_pubkeys(pubkeys) do
pubkeys
|> Enum.map(&Point.serialize_public_key(&1))
|> Enum.join()
|> sha256_hash()
end
end
The image below describes the flow (from Cashu docs). The purple cash note would be the ecash token.
Step 1 is for the user/customer to have two secret values:
- a secret message (a binary)
- a random number we call a blinding factor (an integer)
From the blinding factor and the secret message, the step1_alice
function creates a blinded point and returns the blinding factor. The blinded_point
is a point on the secp256k1 curve of type %Point{x: x, y: y}
.
{blinded_point, blinding_fact} = Cashu.step1_alice("my message")
Bob has a private key too.
{:ok, bob_privkey} = Cashu.random_number() |> Bitcoinex.Secp256k1.PrivateKey.new()
After receiving Alice's blinded_point
, Bob signs it, or "commits" to it with his private key. The point he returns we call c_
.
Bob can't know what went into creating the blinded_point
, so Alice's secrets are safe. After Bob commits to this point, Alice can check that he indeed created c_
by using Bob's public key and checking that the signature is valid.
Bob also creates DLEQ proofs for his signature: scalars e
and s
(we'll cover later).
{:ok, c_, e, s} = Cashu.step2_bob(blinded_point, bob_privkey)
This point c_
is the crux of the whole key exchange.
What Alice can do is "unblind" this point to reveal a point only she can know, if she is actually the one who created the original blinded_point
. We call this unblinded_point
; it's sometimes called c
since it's the unblinded version of c_
.
Below Alice takes a_point
, i.e. Bob's public key, and unblinds the c_
point with her blinding_factor
from earlier.
a_point = Bitcoinex.Secp256k1.PrivateKey.to_point(bob_privkey)
unblinded_point = Cashu.step3_alice(c_, blinding_fact, a_point)
This unblinded_point
point can be used as an ecash token in Cashu. For Alice to redeem this unblinded_point
, she presents it to Bob along with her secret message.
Bob uses his private key to check that the unblinded_point
corresponds to the secret message. He never learns the blinding_factor
, and so all he can know is that the note is valid for something he himself issued.
Revealing the secret message doesn't reveal the blinding_factor
thanks to elliptic curve addition being non reversible.
Cashu.is_valid?(bob_privkey, unblinded_point, "my message")
Cashu.alice_verify_dleq(blinded_point, c_, e, s, a_point)