I tested Elements (part 2) — Confidentials transactions & Zero-knowledge proof

Louis Singer
ON-X Blockchain (Chain-Accelerator)
14 min readMar 19, 2020

In the first part, we saw how to make a transaction. Here we will see a specific feature of Elements: confidential transactions.

In reality, we already did a confidential transaction during part 1. By default in Elements, all the transactions are confidential. But here we’ll see how to audit a confidential transaction. That’s the strength of Elements: mixing the public with the possibility of confidentiality.

Today, we’ll create three Elements nodes and imagine a scenario where the first node (SENDER) sends bitcoins to the second node (RECEIVER). The third one will be the AUDITOR: We’ll see how he can audit the confidential transaction without being one of his stakeholders.

Part 2 scenario

This part is a continuation of part 1. It is necessary to have Elements and Bitcoin installed on your computer. The installation part is described in Part 1: https://medium.com/chain-accelerator/i-tested-elements-part-1-setup-and-first-transactions-604c4e83cac3

1. Create our working environment

Fetch the starter pack from git

We have already seen in the first part how to configure a node. In this part everything is configured, you just have to clone the starter pack:

git clone https://github.com/louisinger/elements-confidential-transaction.git

After cloning the repository, you’ll see a elements-confidential-transaction folder. This folder contains four directories:

  • auditor-node/ : the auditor’s Elements node.
  • receiver-node/ : the receiver’s Elements node.
  • sender-node/ : the sender’s Elements node.
  • bitcoin-node/ : the bitcoin node.

In each folder, there is a configuration file using to configure nodes. The nodes use default ports to communicate, you can change the value of these ports in the configuration files.

Generate aliases

In the elements-confidential-transaction folder, there is a script named getalias.sh. It generates the bash alias to make it easier for clients to use our nodes data. Let’s launch the script:

cd elements-confidential-transaction
chmod +x getalias.sh
./getalias.sh

Depending on the location of the directory, the script will return the correct aliases. Personally, I cloned the repository in my home so the script returns:

# What I get from getalias.sh# I use 'grep' to select only aliases
# ./getalias.sh | grep '^alias'
alias senderd="elementsd -datadir=/home/louisinger/sender-node"
alias sender-cli="elements-cli -datadir=/home/louisinger/sender-node"
alias receiverd="elementsd -datadir=/home/louisinger/receiver-node"
alias receiver-cli="elements-cli -datadir=/home/louisinger/receiver-node"
alias auditord="elementsd -datadir=/home/louisinger/auditor-node"
alias auditor-cli="elements-cli -datadir=/home/louisinger/auditor-node"
alias btcd="bitcoind -datadir=/home/louisinger/bitcoin-node"
alias btc-cli="bitcoin-cli -datadir=/home/louisinger/bitcoin-node"

⚠ DO NOT COPY my aliases, you need to run the script on your computer. Then use your own aliases.

Use nano to edit ~/.bashrc file and add the aliases.

nano ~/.bashrc

Then, update with the following command:

source ~/.bashrc

Now, you can launch the daemons:

# Launch bitcoin node
btcd
# Launch the sender node
senderd
# Launch the receiver node
receiverd
# Launch the auditor node
auditord

Transfer the “everyone-can-spend” bitcoins to the sender

The initial coinbase is 20 BTC (defined in the elements.conf files). Everyone can spend these bitcoins. So let’s transfer the entire coinbase to the sender wallet.

Send 20 BTC to a sender’s node address:

SENDER_ADDRESS=$(sender-cli getnewaddress)
sender-cli sendtoaddress $SENDER_ADDRESS 20 "" "" true

Then, generate 102 blocks to confirm the transaction:

sender-cli generatetoaddress 102 $SENDER_ADDRESS

Finally, verify if the sender has 20 BTC:

sender-cli getwalletinfo | jq ".balance.bitcoin"
# > 20

And verify that auditor and receiver have no bitcoins:

receiver-cli getwalletinfo | jq ".balance.bitcoin"
# > 0
auditor-cli getwalletinfo | jq ".balance.bitcoin"
# > 0

Congratulations! The setup is complete.

2. Send our transaction (confidential)

This part is no different from what we saw in part 1 because, as I said earlier, all transactions are confidential by default on an Elements network.

Let’s send 1 BTC from the sender to the receiver (and store the transaction ID in a variable). Before sending it, we just create an address on the receiver wallet and store it inside a variable. We’ll need it later.

RECEIVER_ADDR=$(receiver-cli getnewaddress)
TXID=$(sender-cli sendtoaddress $RECEIVER_ADDR 1 "" "" false)

And generate blocks to confirm the transaction:

sender-cli generatetoaddress 102 $SENDER_ADDRESS

3. Examine the transaction

So now we have a transaction and we have stored his ID inside $TXID variable. Let’s use some new commands to examine the transaction.

From the sender point of view

sender-cli gettransaction $TXID

The gettransaction command inspects the wallet to find the transaction ID given as a parameter. If the transaction exists within the wallet, it returns a JSON with the transaction details. You can use jq to just fetch the details attribute of the JSON:

sender-cli gettransaction $TXID | jq ".details"

And the result:

[{
"address": "XZXJHG89TL8htjmUaYrwQGbABywZ1KQVfU",
"category": "send",
"amount": -1,
"amountblinder": "a03a45bd2e8618585d8935a85a42069cd974a30a770de625def2c625f34a65b1",
"asset": "b2e15d0d7a0c94e4e2ce0fe6e8691b9e451377f6e46e8045a86f7c4b5d4f0f23",
"assetblinder": "8bbe7435e9a3897fc4cfc28c80de725bbe3e9674c44cb21a1d67ec3ab369d16a",
"vout": 0,
"fee": 0.000375,
"abandoned": false
}]

The sender sends 1 BTC, that’s why the transaction’s amount is negative.

From the receiver point of view

receiver-cli gettransaction $TXID | jq ".details"

We get a different result than the sender one.

[{
"address": "XZXJHG89TL8htjmUaYrwQGbABywZ1KQVfU",
"category": "receive",
"amount": 1,
"amountblinder": "a03a45bd2e8618585d8935a85a42069cd974a30a770de625def2c625f34a65b1",
"asset": "b2e15d0d7a0c94e4e2ce0fe6e8691b9e451377f6e46e8045a86f7c4b5d4f0f23",
"assetblinder": "8bbe7435e9a3897fc4cfc28c80de725bbe3e9674c44cb21a1d67ec3ab369d16a",
"label": "",
"vout": 0
}]

The receiver receives 1 BTC so the transaction’s amount is positive.

From the auditor point of view

The auditor isn’t a stakeholder of the transaction. Which means the transaction script pattern's not being imported into his wallet. That’s why the gettransaction command won’t work. We’ll use getrawtransaction. Because gettransaction is a wallet RPC whereas getrawtransaction is a blockchain RCP.

Let’s launch the command and store the result inside a new variable.

RAWTX=$(auditor-cli getrawtransaction $TXID)

getrawtransaction returns a hex-encoded JSON object describing the transaction. We can decode the raw transaction with the command decoderawtransaction.

auditor-cli decoderawtransaction $RAWTX

Now, you are able to read the JSON object. The interesting data is vout (transaction’s outputs vector). Select it with jq .

auditor-cli decoderawtransaction $RAWTX | jq ".vout"

vout is an array of three objects. The first two objects correspond to the “send” and “receive” parts of the transaction while the third object represents the “generate” part of the transaction. Let’s look at the last one.

{
"value": 0.000375,
"asset": "b2e15d0d7a0c94e4e2ce0fe6e8691b9e451377f6e46e8045a86f7c4b5d4f0f23",
"commitmentnonce": "",
"commitmentnonce_fully_valid": false,
"n": 2,
"scriptPubKey": {
"asm": "",
"hex": "",
"type": "fee"
}
}

The auditor can see the fees amount and the type of fee asset. However, for the “send” and the “receive” parts, things are different.

{
"value-minimum": 1e-08,
"value-maximum": 687.19476736,
"ct-exponent": 0,
"ct-bits": 36,
"surjectionproof": "0100016850e131051736d7457f46b05cb13df4fe95bc0535125d4140e9a4d8ba08e7487099a2d3627ee1113f28b23262b24bc17d8555b6906e857ca80cf3ca4f960fd8",
"valuecommitment": "09edd6c7810d33107dff091043fc26ca9a09eefc8750bf06304a419108b5987980",
"assetcommitment": "0a11c80311133baa82f50dbb0e3fa637c144251a76282b5e8674046bc359af72bc",
"commitmentnonce": "02b35b2b484b84b9f14bd6b47facad4aa1550f29d35a9caf1cc338000cd10cb8fd",
"commitmentnonce_fully_valid": true,
"n": 0,communicate
"scriptPubKey": {
"asm": "OP_HASH160 f339cae9642acd3cb5b692a18f1aaf55fa9644b0 OP_EQUAL",
"hex": "a914f339cae9642acd3cb5b692a18f1aaf55fa9644b087",
"reqSigs": 1,
"type": "scripthash",
"addresses": [
"XZXJHG89TL8htjmUaYrwQGbABywZ1KQVfU"
]
}
},
{
"value-minimum": 1e-08,
"value-maximum": 687.19476736,
"ct-exponent": 0,
"ct-bits": 36,
"surjectionproof": "010001c7b423595ca5208bb730b5f18a6608a0a22d9e1fa2a72939897c50aacf967a72d1af4170955fbcc9789ad70bdb45cb399f1b11f60ebeb084e50fe0afde28175b",
"valuecommitment": "097d59d77f10e8241387a3ea594ae2cb018ec6779a3ba4a7db4d071a0cb453bf09",
"assetcommitment": "0a988966c03d9734b6a666fae7e408adcbc0132c14e145cdfe05a39b62fe4474b2",
"commitmentnonce": "03ee7001d48bfe0742f1c26bdff5a95dd705f9638b209a50b5da58a8b556c2d94d",
"commitmentnonce_fully_valid": true,
"n": 1,
"scriptPubKey": {
"asm": "OP_HASH160 c50592ffb077fc45fef8adbcff97b42efb74134f OP_EQUAL",
"hex": "a914c50592ffb077fc45fef8adbcff97b42efb74134f87",
"reqSigs": 1,
"type": "scripthash",
"addresses": [
"XVJzeEo4HF3EnVZBSfZJpiYSR4pBQZAi4v"
]
}
}

This is the time to describe the composition of a transaction in more detail:

  • scriptPubKey contains the address and the condition saying that the bitcoins of the address can be spent only if the address owner validates it with his private key. The asm member is the explicit Bitcoin script which lock the output. hex is the hexadecimal encoded version of the script. reqSigs is the number of required signature. type is the script type -it exists several script patterns-.
  • surjectionproof is a Zero knowledge proof of the blinding factor. The blinding factor is a value using to implement confidential transactions. It is a secret of the address’ owner. The Bonus part describes his role more in detail.
  • commitmentnonce : a nonce used to send and receive the commitments.

The object doesn’t contain the exact value of the transaction’s amount. Instead, we have a range of values: value-minimum and value-maximum . Other interesting things are commitments. There are a valuecommitment proving the value data and the assetcommitment proving the type of asset transferred (a bitcoin or an asset). We’ll go further about commitments in the Bonus part.

In conclusion, the raw transaction does not permit to get the transaction details. The auditor knows that the transaction exists but he cannot know what asset is concerned as well as the amount exchanged.

4. Audit the confidential transaction

To audit the transaction (= access his value and his type of asset), the auditor needs to do two things.

First, import the public address of the receiver. We had stored it inside RECEIVER_ADDR :

auditor-cli importaddress $RECEIVER_ADDR

The importaddress import the transactions of the address given as parameters inside the auditor’s wallet. Thus, we can use gettransaction .

auditor-cli gettransaction $TXID true

This time, the command returns the transaction object but the details array is empty and the amount equals zero. That’s why without knowledge of the Blinding Key, the amount and type of asset being transacted are hidden.

Each address has an associated blinding key. This key is used in the cryptographic process to hide the type of asset and the amount of the transaction.

The receiver can get the blinding key of his addresses with the dumpblindingkey . Let’s export it inside a variable:

BLINDING_KEY=$(receiver-cli dumpblindingkey $RECEIVER_ADDR)

So the blindingkey can only be obtained by the owner of the address.

Let’s imagine that the receiver agrees to reveal the details of the transaction to the auditor. He can send the blinding key to the auditor. Then, the auditor can import the blinding key:

auditor-cli importblindingkey $RECEIVER_ADDR $BLINDING_KEY

Finally, we can retry the gettransaction RPC.

auditor-cli gettransaction $TXID true | jq ".details"

And this time we get the transaction’s details:

[{
"involvesWatchonly": true,
"address": "XZXJHG89TL8htjmUaYrwQGbABywZ1KQVfU",
"category": "receive",
"amount": 1.00000000,
"amountblinder": "a03a45bd2e8618585d8935a85a42069cd974a30a770de625def2c625f34a65b1",
"asset": "b2e15d0d7a0c94e4e2ce0fe6e8691b9e451377f6e46e8045a86f7c4b5d4f0f23",
"assetblinder": "8bbe7435e9a3897fc4cfc28c80de725bbe3e9674c44cb21a1d67ec3ab369d16a",
"label": "",
"vout": 0
}]

In our scenario, the listener uses the binding key of the receiver but we could have used the binding key of the sender’s address.

In conclusion, in order for a third party to have access to the details of the transaction, it is necessary for one of the stakeholders to send him his blinding key.

5. Send an unconfidential transaction

It is also possible to send non-confidential transactions. Let’s see how it works with Elements. This will be an opportunity to learn more about addresses.

Now let’s say that our SENDER wants to send 2 BTC to the RECEIVER using an unconfidential transaction.

The sender only knows the value of $RECEIVER_ADDR . To send assets using a non-confidential transaction, the sender needs to use getaddressinfo :

sender-cli getaddressinfo $RECEIVER_ADDR

You’ll get something like this:

{
"address": "AzpxBswbqHzbaV7bYiTRjY7F19tMSYUnKNFVLPdqznz1X15YFFiStPpPfTpRTCqg1eZ9cFwyQk4Bn1UT",
"scriptPubKey": "a914f339cae9642acd3cb5b692a18f1aaf55fa9644b087",
"ismine": false,
"solvable": false,
"iswatchonly": false,
"isscript": true,
"iswitness": false,
"confidential": "AzpxBswbqHzbaV7bYiTRjY7F19tMSYUnKNFVLPdqznz1X15YFFiStPpPfTpRTCqg1eZ9cFwyQk4Bn1UT",
"confidential_key": "03e813219a9a9abc91109db43f412ea862f8530a8d96b9a07a43a46bd910cc1805",
"unconfidential": "XZXJHG89TL8htjmUaYrwQGbABywZ1KQVfU",
"ischange": false,
"labels": []
}

⚠ If you do a getaddressinfo on a wallet’s address. For instance:sender-cli getaddressinfo $SENDER_ADDR , you’ll get more informations. At the moment we are focusing on the public information of an address. Do not hesitate to explore the command reference: https://chainquery.com/bitcoin-cli/getaddressinfo.

We can see that an address is made up of several things:

  • address : the public address (here, our $RECEIVER_ADDR ).
  • scriptPubKey : a Bitcoin script that provides the conditions that must be met. When they are, the transaction’s outputs can be used (the BTC becomes spendable).
  • ismine : a boolean indicating if the address is yours or not. If you use getaddressinfo one of your addresses, the response will contain more information.
  • solvable : a boolean indicating if we (here the sender) know how to spend the address’s bitcoin. If false we cannot use the outputs of the address’s transactions.
  • iswatchonly : a watch-only address is a public address imported in your wallet. As it is not your address, you’re not able to use it for transactions. That’s why we called them watch-only addresses.
  • isscript : a boolean specifying if the key is a script or not.
  • iswitness : if true , the address is a witness = an address used to be a witness in a transaction.
  • confidential (only Elements): the confidential address. As confidential transactions are the default transaction type in Elements, the confidential address is the address . More about confidential assets here: https://elementsproject.org/features/confidential-transactions/addresses
  • confidential_key (only Elements): the blinding public key associated with the private blinding key.
  • unconfidential (only Elements): the unconfidential public address. We’ll use it to send our bitcoin in a non-confidential way.
  • ischange : specify if it is a change address. The following link explains in detail what is a change address: https://coinguides.org/bitcoin-change-address-output/.
  • labels : the labels attached to the address. A label is used to describe an address. It could be the name of the address’s owner for example.

Let’s store the unconfidential public address in a variable:

RECEIVER_UNCONF=$(sender-cli getaddressinfo $RECEIVER_ADDR| jq ".unconfidential"| tr -d '"')

Then, sends 2 BTC to the address. We’ll also store the transaction ID. Like the precedents transactions, we also need to generate 102 blocks:

UTXID=$(sender-cli sendtoaddress $RECEIVER_UNCONF 2)
sender-cli generatetoaddress 102 $SENDER_ADDRESS

Check the receiver balance:

receiver-cli getwalletinfo |jq ".balance.bitcoin"
# > 3

and verify if the auditor can see the transaction details.

⚠ The auditor is able to use the gettransaction because we import the receiver public address before. https://elementsproject.org/features/confidential-transactions/addresses

auditor-cli gettransaction $UTXID true| jq ".details"

Result:

[{
"involvesWatchonly": true,
"address": "XZXJHG89TL8htjmUaYrwQGbABywZ1KQVfU",
"category": "receive",
"amount": 2,
"amountblinder": "0000000000000000000000000000000000000000000000000000000000000000",
"asset": "b2e15d0d7a0c94e4e2ce0fe6e8691b9e451377f6e46e8045a86f7c4b5d4f0f23",
"assetblinder": "0000000000000000000000000000000000000000000000000000000000000000",
"label": "",
"vout": 1
}]

The transaction details are public! We have sent a non-confidential transaction.

6. Stop the daemons

When you’re done, stop the daemons: Let’s describe how it works:

sender-cli stop
receiver-cli stop
auditor-cli stop
btc-cli stop

Bonus: how do confidential transactions work?

It is beyond the scope of this section to describe in detail how confidential transactions work. it is rather a popularization of the general functioning in order to have a better understanding of Elements.

The bonus part is based on the Confidential assets white paper, and the confidential Transactions investigation.

In a Bitcoin blockchain, everybody verifies the transaction. That’s why the data about the transaction are public. As we have just seen, this is not the case with Elements blockchain through confidential transactions.

The biggest challenge of confidential transactions is to hide the details of the transactions while allowing their verification.

To accomplish this, confidential transactions use a set of cryptographic tools but rely primarily on commitment schemes. Especially the Pederson commitment. Let’s describe how it works.

Commitment scheme

A commitment is a value proving the integrity of another value. A simple commitment scheme could be the MD5 sum.

DATA="100 BTC"
COMMITMENT=$(echo $DATA| md5sum)
> 054372c6011ea3bfb2520d693e25db76

I can store $COMMITMENT in a public area. Later, if I want to prove that I sent 100 BTC, I need to reveal the $DATA value. Then, auditors could calculate the md5sum and verify that $DATA equals 100 BTC.

We often add a blinding factor (= a random value) to the value-to-blind before calculating his commitment. This prevents an attacker from guessing the value. For instance, if I want to blind the asset name, the attacker could know that it only exists 3 assets type: BTC, TOKEN1, BEER_TOKEN. So he just needs to calculate the commitments of these three values and then compare the results with my commitment. However, if I previously add a random blinding factor, the attacker needs to know it. Without it, his calculated commitments will be different than mine.

commitment = commitmentAlgorithm(blinding_factor, data)

blinding_factor and data are secret whereas the commitment and the commitmentAlgorithm are public.

Pederson commitment

For clarity, we define PC as the Pederson commitment function and BF as blinding factor.

A Pedersen commitment works like the above but has two additional mathematical properties:

# BF = blinding factors
# PC = Perderson commitment function
# Commutative property
> PC(BF, data) - PC(BF, data) = 0
# Preserve addition
> PC(BF1, data1) + PC(BF2, data2) = PC(BF1 + BF2, data1 + data2)

The Elements’ Pederson commitment

Elements use the following function as the commitment scheme.

PC(BF, data) = BF * p + data * q 

Where p is a prime number and q a number that does not belong to the same linear group than p. Both numbers are public data.

Elements choose the numbers p and q using Elliptic Curves. If you want to learn more about this process: https://fangpenlin.com/posts/2019/10/07/elliptic-curve-cryptography-explained/

We can generate p and q with a simple algorithm:

  1. Take a prime number. It will be p.
  2. Generate a random integer k.
  3. Calculate q = p * k + 1.

Now, let’s imagine two commitments:

C0 = PC(BF0, data0) = p * BF0 + q * data0
C1 = PC(BF1, data1) = p * BF1 + q * data1

And calculate the difference between C0 and C1:

C0 - C1 = p * BF0 + q * data0 - p * BF1 - q *data1
= (BF0 - BF1)p + (data0 - data1)q

Then apply a modulo p on the expression:

(C0 - C1) % p = ((BF0 - BF1)p + (data0 - data1)q) % p
= (BF0 - BF1)p % p + (data0 - data1)q % p
= (data0 - data1)q % p
= (data0 - data1)(p * k + 1) % p
= ((data0 - data1)pk + (data0 - data1)) % p
= (data0 - data1)k % p + (data0 - data1) % p
= (data0 - data1)(k + 1) % p

k-1 is not equal to zero so we can assert that:

if (C0 — C1)%p = 0 then (data0 — data1)=0 so data0 = data1 .

This is the homomorphic property of the Pederson commitment. Thanks to it, we can verify that data0 = data1 without knowing their values.

Let’s try it ourselves

Our scenario is simple: Alice has sent 5 BTC to Bob. Both want to hide the amount of the transaction but Eve (another person on the network) would like to verify the transaction.

Verify the transaction means that the amount sends by Alice is equals to the amount received by Bob.

Alice and Bob both have their own blinding factor only known by themselves. We’ll call it BF_alice = 92 and BF_bob = 9.

Then generate our p and q:

# 7 is a prime number
p = 7
# k = 4 the
q = k * p + 1 = 4 * 7 + 1 = 29

So our commitment function is:

PC(BF, data) = BF * 7 + data * 29

Let’s resume the scenario with a simple scheme:

Let’s calculate Alice’s commitment. She sends 5 BTC.

Ca = PC(BF_alice, amount_alice) = BF_alice * 7 + amount_alice * 29
= 92 * 7 + 5 * 29
= 789

And then Bob’s commitment. He receives 5 BTC.

Cb = PC(BF_bob, amount_bob) = BF_bob * 7 + amount_bob * 29
= 9 * 7 + 5 * 29
= 208

Finally, Eve can calculate the difference between Ca and Cb :

SeeCa - Cb = 789 - 208 = 581See

And then apply a modulo p = 7 .

581 % 7 = 0

Eve is seeing that the result is equal to 0. She can conclude that Alice and Bob committed the same amount. It means that the transaction is valid.

Let’s imagine that Bob lies and commit an amount of 8 BTC. So:

Cb = PC(BF_bob, amount_bob) = BF_bob * 7 + amount_bob * 29
= 9 * 7 + 8 * 29
= 295

And when Eve will try to verify the transaction:

(Ca - Cb) % p = (789 - 295) % 7 = 494 % 7 = 4 

As (Ca — Cb)%p is not equal to zero, so the transaction is not confirmed. Eve knows that somebody lied.

There is a javascript implementation of the algorithm used in this part. Take a look at the bonus folder of the cloned repository at the beginning of the article if you want to play with it.

Conclusion

Obviously, Elements is not implemented with the same algorithm. I assure you it’s far more secure and reliable than mine. If you wish to go further, there is the official Blockstream white paper: Confidential assets white paper.

--

--