Why Tokens Don't Work using Private Transactions

Update
Since this post was written, Hyperledger FireFly has reached 1.0. Learn more here!

A token economy with privacy is a popular use case in the enterprise space. For CBDCs (Central Bank based Digital Currency), carbon trading, and many other scenarios involving payments or settlements, token is a very useful construct. Most of these use cases involve participants that represent organizations and commercial entities that compete with each other in their industries. As a result, not disclosing each other’s balances and trading patterns is a fundamental requirement.

Properly addressing privacy in a token implementation is a topic that requires in-depth discussions, which is beyond the scope of this blog. What we’d like to focus on is how NOT to implement a token contract.

Private transaction is a concept that has been implemented by multiple blockchain protocols. Quorum and Hyperledger Besu are able to work with a private transaction manager called Tessera. Hyperledger Fabric supports the same private transaction pattern with chaincode private collections.

Private Transaction in a Token Economy

Private transaction allows a subset of participants in a blockchain network to maintain a separate world state from the world state that is globally shared among all the participants of the blockchain. Only the participants privy to the transaction are able to execute the transaction and update the state, while those not privy to the transaction do not update the state.

This approach has usage in many scenarios, mainly when transactions are by and large independent of each other. For instance, in a global trade network, one trade is independent from another. One private transaction can be carried out between party A and party B, while another private transaction can be carried out between party A and party C. The result is that at the end of the two trades, party B and party C will have different views to the current state that party A is in.

Because there are no interdependencies between these trades, the fact that party B and party C will have different views to the state of party A is not an issue. Using private transactions in such scenarios is a great approach to achieve privacy.

On the other hand, private transactions do not work in a token economy.

All token implementations require a shared global view. For fungible tokens, such as ERC20 tokens, this means all parties must be able to verify that mass conservation is always achieved with every transaction. For non-fungible tokens, such as ERC721 tokens, a shared global view is required so that all parties are able to verify that each token is globally unique.

This runs contrary to the fact that private transactions result in different parties having different views of the global state.

Let's look at an example

The following example will illustrate the problem with using private transactions to implement a token economy.

Imagine a blockchain network of 3 parties: A, B and C. A fungible token has been implemented with a standard ERC20 contract. The illustration is based on Ethereum. Hyperledger Fabric will be slightly different in the exact mechanism with chaincode private collections but the concept is identical.

Party A deploys the contract and designates all 3 parties as private members of the private contract, by including the Tessera private identities for the 3 parties in the privateFor list.

The initial supply is set to 1,000 by the contract’s constructor. As a result, the initial balances look like the following:

  • Party A: 1,000
  • Party B: 0
  • Party C: 0

Let’s designate this state as version 0. We also use a designation to indicate that this state is shared among all 3 parties. Symbol v0[A,B,C] is used for the initial state:

v0[A,B,C]: A=1000, B=0, C=0

Now let’s use a private transaction between A and B where A transfers 100 tokens to B. We designate the resulting state as v1[A,B]:

v1[A,B]: A=900, B=100, C=0

Note that at this point, party A and party B both have updated their state from v0[A,B,C] to v1[A,B]. Party C still has the initial state v0[A,B,C]. As a result, B’s balance according to party C’s world view is still 0. We will see at a later step that this will cause problems.

Let’s send another private transaction from B to C, with the naive thinking that this will allow B to transfer 50 tokens to C. Things will get a bit complicated at this point because the two parties involved, B and C will evolve their states separately. This is because before the transaction, B is at v1[A,B], while C is still at v0[A,B,C].

So, the private transaction gets executed by B, as follows:

v1[A,B]: A=900, B=100, C=0
          |
          | B transfers 50 to C
          |
v2[B,C]: A=900, B=50, C=50

So far so good, everything gets updated as expected.

However, when the same transaction gets executed by C, the following happens and the transaction fails as a result:

v0[A,B,C]: A=1000, B=0, C=0
          |
          | B transfers 50 to C
          |
Failed with error: transfer amount exceeds balance

This transaction failed on party C’s node because B had a starting balance of 0, and attempted to transfer 50.

The entire flow is illustrated in the following diagram:

This flow can be further observed by querying the Party B node and Party C node of the private transaction #2 receipt, given the transaction hash 0xa30e4bca7cfb2586504535afb159bf4a74e834e45509240eac42e6fbad0fd048:

Against party B, notice the transaction was successful with status “0x1”:


Against party C, notice the transaction was failing with status “0x0”:

Finally, the transaction failure can be queried by sending the same transaction input to party C node again, using the eth_call method (instead of eth_sendTransaction):

INFO [06-16|03:41:33.497] VM returned with error                   err="execution reverted"
DEBUG[06-16|03:41:33.497] Executing EVM call finished              runtime="268.317 µs"
WARN [06-16|03:41:33.497] Served eth_call                          conn=127.0.0.1:60506    reqid=9         t="403.363 µs" err="execution reverted: ERC20: transfer amount exceeds balance" errdata=0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002645524332303a207472616e7366657220616d6f756e7420657863656564732062616c616e63650000000000000000000000000000000000000000000000000000

The above error message confirms that the reason the same transaction succeeded on party B node but failed on party C was because party C’s starting state has balance 0, as illustrated above.

Protection Against Private State Divergence

Private state divergence as demonstrated above can cause many types of issues. To protect against this, the developer can make use of these features:

  • privacy groups, which ensures that private transactions targeting a group are always sent to all the members of the group. Both Quorum and Hyperledger Besu support privacy groups.
  • private state protection, which requires all private transactions targeting a private contract to always include all the original members that the contract deployment specifies

Note that in both cases, while the private state integrity is maintained within the subset of the participants, the end result is that all the members of the private contract share the same private state (as in, everybody knows everybody’s balances and transactions). In the case of a token implementation that requires privacy, the privacy requirement is not fulfilled.

Summary

In summary, while private transactions work well for use cases that involve “one-off” transactions, or transactions that do not modify any globally shared states, they can not be used to implement tokens.

Tokens that need privacy must utilize advanced technologies such as zero knowledge proof. This is still an actively advancing technology, with promising implementations including EY’s Nightfall 3 confidential tokens. As with any technologies involving advanced cryptography, it takes time and effort to prove the correctness of the algorithm and security of the implementation.

On the other hand, if your blockchain project requires privacy, check out Hyperledger FireFly. It takes inspiration from the private transactions pattern to provide privacy while giving a whole lot more flexibility to developers in terms of how to implement the transaction processing logic.

Interested in Blockchain?

Start learning blockchain and creating enterprise solutions today with a free Kaleido account!

Create Free Account
Don't Forget to share this article!
Interested in Blockchain?

Start learning blockchain and creating enterprise solutions today with a free Kaleido account!

Create Free Account

Related Posts

App Chains: What They Are and Why Enterprises Should Care

Jim Zhang
Co-Founder & Head of Protocol
Kaleido Product Updates for May 2022

Product Updates May 2022

Ray Chen
Product Manager