Transaction Validation when a Peer Committing a Block

KC Tam
9 min readJun 18, 2020

Introduction

This article is to make observations when more than one transaction acts on the same state in the same block. The result is different depending on whether the transaction reads and updates that state or not in the function. This has implications in our blockchain application design. For demonstration purposes we create a simple chaincode implementing two functions updating the state. One requires state reading and another does not. We will observe the result and make some discussion on it.

Overview

A client invoking a chaincode function in order to update a state in the ledger. A typical transaction flow is described in the documentation. Here is a quick summary.

  1. Client sends proposals to selected endorsers for endorsement.
  2. Endorsers return proposal responses with their own signature.
  3. Client checks endorsement. If the same result is received, and the endorsement meets the endorsing policy requirement, client constructs a transaction and sends it to the ordering service.
  4. Upon successful validation of the transaction, an orderer packs this transaction (and other transactions “not-yet-blocked” in this channel) into a block and sends this new block to all channel members (peers).
  5. All peers receiving this block will first validate the transactions, and then commit the block and update the worldstate based on the transactions, one-by-one, in the block.

Our interest in this article is on step 4 and 5. What happens if more than one transaction is updating the same state inside a block?

The documentation is clearly describing the behaviour. Here is a quick summary for our interest:

  • If the transaction does not involve reading the state, the transaction is valid and the worldstate is updated.
  • If the transaction reads the state and the state is updated once, all subsequent transactions in the block reading this state will be invalid.

Here we perform a test and see how it works.

Testing Chaincode

I wrote a quick chaincode to perform the test testblocktime.go. The chaincode is simply keeping a state value. It is initialized as zero.

Three functions are defined in this chaincode

  • get(): to return the value stored in value. This is how we get back the value in the tests.
  • set(x): to set value to the given value x. This function does not require reading the original value.
  • add(x): to add a given value x upon existing value of value. This function requires reading the existing state, and stores the result after adding that amount.

Testing environment

The following test is done on First Network of Fabric v1.4. First Network comes with a default Batch Timeout (in configtx.yaml) 2 second, which means that orderer processes a transaction and waits for 2 seconds before creating a new block. Therefore a new block is created almost after each transaction (as far as my new chaincode invoking is done after 2 seconds). We keep using First Network in One Transaction per Block case.

To make two transactions packed in a block, we change the Batch Timeout bigger. Let’s say 60 seconds. As far as my chaincode invoking is made within 60 seconds, they will be packed into the same block. To achieve this, we create another network environment. Here I name it kc-first-network, with everything identical to first-network, except that the configtx.yaml is modified.

cd fabric-samples
cp -r first-network kc-first-network
cd kc-first-network

And perform the modification of BatchTimeout (to 60s):

Change the BatchTimeout to 60s in kc-first-network/configtx.yaml

Now kc-first-network will be used in Two Transactions in One Block.

We will omit the steps of bringing up the network, chaincode installation and instantiation, and only show when we start invoking functions.

Bring Up Environment

We use First Network or modified First Network. The difference is the BatchTimeout in configtx.yaml. Also the chaincode testblocktime.go is placed in fabric-sample/chaincode/testeblocktime/ such that it is correctly mapped into the CLI container.

cd first-network/ OR cd kc-first-network/# bring up network components and channel
./byfn.sh up -n
# install chaincode to peer0.org1.example.com
docker exec cli peer chaincode install -n mycc -v 1.0 -p github.com/chaincode/testblocktime
# install chaincode to peer0.org2.example.com
docker exec -e CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp -e CORE_PEER_ADDRESS=peer0.org2.example.com:9051 -e CORE_PEER_LOCALMSPID="Org2MSP" -e CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt cli peer chaincode install -n mycc -v 1.0 -p github.com/chaincode/testblocktime
# instantiate chaincode to mychannel
docker exec cli peer chaincode instantiate -o orderer.example.com:7050 --tls true --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n mycc -v 1.0 -c '{"Args": []}' -P "AND('Org1MSP.member','Org2MSP.member')"

Note: In real life, different functions will be invoked and therefore packed in a block. For demonstration purpose, we only use one function, either set() or add() in each case. This shows the effect.

Invoke set(): State is not Read

One Transaction per Block

This serves as a baseline for comparison.

As BatchTimeout is 2 seconds, we see the state is updated almost immediately after invoking the function.

  • get value. result: 0
  • invoke set(100).
  • get value. result: 100
  • invoke set(200).
  • get value. result: 200

Here is what we observed.

Fast batch timeout makes one transaction per block.

Two Transactions in One Block

As BatchTimeout is 60 seconds, we see the state update is not done after we set(100), and set(200). After a while a new block is received and the state gets updated.

  • get value. result: 0
  • invoke set(100).
  • get value. result: 0
  • invoke set(200).
  • get value. result: 0
  • (after a block is received and committed) get value. result: 200

We fetch the block and check transactions in the block: we see two transactions in this block (two payload).

A block showing two transactions (two payloads) recorded

And take a look at the RWSet for each transaction.

RWSet for Transaction 1: set(100)

“MTAw” is 100 encoded in Base64

RWSet for Transaction 2: set(200)

“MjAw” is 200 encoded in Base64

Per our function design, no state is read in set(x). We can see empty ReadSet (line 68 and 196). In this case, all these two transactions are valid, and the final state is based on the last transaction (result is 200).

Function set() does not read the state: all transactions are valid and resulting state is the final transaction.

Invoke add(): State is Read

One Transaction per Block

This serves as a baseline for comparison.

As BatchTimeout is 2 seconds, we see the state is updated almost immediately after invoking the function.

  • get value. result: 0
  • invoke add(100).
  • get value. result: 100, which is (0+100)
  • invoke add(200).
  • get value. result: 300, which is (100+200)

Here is what we observed.

Fast batch timeout makes one transaction per block.

Two Transaction in One Block

As BatchTimeout is 60 seconds, we see the state update is not done after we add(100), and add(200). After a while a new block is received and the state gets updated.

  • get value. result: 0
  • invoke add(100).
  • get value. result: 0
  • invoke add(200).
  • get value. result: 0
  • (after a block is received and committed) get value. result: 100

We fetch the block and check transactions in the block: we see two transactions in this block (two payload).

A block showing two transactions (two payloads) recorded

And take a look at the RWSet for each transaction.

RWSet for Transaction 1: add(100)

“MTAw” is 100 encoded in Base64

RWSet for Transaction 2: add(200)

“MjAw” is 200 encoded in Base64

Note: the result of transaction is 200 instead of 300. It is because the state when this proposal is endorsed is still 0, and not 100 yet.

Per our function design, state of value is read and updated in add(x). We can see the ReadSet (line 68–76 and 204–212). As the state is updated in the first transaction, only the first transaction is valid. All subsequent transactions see the value state is modified by the first transaction and therefore not valid.

Function add() read a state which is modified: only the first transaction is valid and resulting state is the first transaction.

Summary of Testing Results

In case a function does not read the state to be updated, all transactions within a block are valid, and the final state is the last transaction (200 in this case).

In case a function reads the state and the state is updated, only the first transaction is valid and all others invalid. The final state is the result of the first transaction (100 in this case).

Here shows the two cases.

Discussion

We can imagine two scenarios when functions are invoked more than once within a block.

Scenario 1: A client invokes twice

It is the client’s intention to invoke a function twice. As we can see during the demonstration, if state is not read in the function, the newest transaction will override all previous. This is a desired result as the state keeps the latest chaincode invoking. However, if state reading is required in the function and the state is updated, only the first transaction is valid and all subsequent ones in the block are invalid. This is not the desired result. In this example, as the client really intends to add 100 and then add 200. He is expecting 300 but it turns out 100.

Scenario 2: Two clients invoke within a block time

In this case different clients are invoking functions within the same block time. Again, if no state is not read in the function, the newest transaction overrides all the previous. This is again a desired result. However, if state reading is required in the function and the state is updated, only the first transaction is valid. While it is not desired as it is a wrong result, even worse is that users have no idea that their function invoking is or is not accounted into the final state.

Implication in chaincode design

When we design a fabric application, one key element is to determine what is written into the ledger and how. The former is about the data structure, and the latter is about chaincode functions. Roughly there are two ways. Keep raw data written into the ledger with minimal data processing in chaincode. This is like what we did in set(). In such a case, raw data is written into the ledger, and later being processed either by on-chain functions (chaincode) or off-chain functions (by general application). To a certain extent this provides better results and should be used if your business case can adopt this.

If for whatever reason we need to keep reading and updating state, like what we did with add(x), make sure the block time is fast enough to reduce the chance of the invalid transactions. Also the overall throughput of the system may also be geared (e.g. reduced the rate) in order to keep transactions valid.

Summary

By tuning the batch timeout for new block creation, we have inspected what happens if a state is being updated by more than one transaction within a block. Whether a state is being read and modified in the function invoke has impact on the final state. Hope we can have a better understanding in the fabric system and fine-tune our blockchain application design.

--

--