Cumulative Integer
Concurrent Delta Friendly Data Types
In optimistic parallel processing, a fundamental assumption is that multiple transactions will not touch the same state. If they do, a conflict occurs, and the transactions will be reverted to protect state consistency. However, it is a common use case in many contracts to keep track of some form of cumulative values, such as counters, total supply, and similar metrics. This creates a serious bottleneck that requires special handling.
Design Goal
Cumulative variables are specifically designed to address concurrent counting. A cumulative variable allows multiple transactions to add their updates to the same variable without causing conflicts or reversions. Once initialized, a cumulative variable only accepts delta updates.
A cumulative variable has an internal buffer to store all delta updates and applies them only after the current generation of transactions has been executed. With this property, multiple transactions can add delta changes to the buffer first, instead of applying them immediately. Because these delta changes are both cumulative and commutative, the order in which they are added does not affect the final result.
Key Features
A cumulative variable has the following features:
Initialized once with a value of
0
Commutative and associative; accepts only delta changes
Supports upper and lower bounds to prevent over/underflow
Concurrent updates do not conflict
Applies deltas lazily after each block or on read
Conflict Model
Transactions with out-of-limit delta changes will fail, ensuring that the variable remains within its prescribed bounds. Attempting to invoke timing-dependent operations, such as read()
, on the variable while it is being updated by other threads will result in a rollback of all but one transaction.
Note: Arcology's parallel execution model is centered around preserving commutativity, any TX that violate the rule will be reverted.
Usage
A typical application is implementing numerical variables that need to be incremented or decremented by multiple transactions. Examples include counters, tallies, total supply, or any situation where a count needs to be updated frequently and concurrently.
To better understand how cumulative variables work, consider the following examples .
Example: Concurrent Like
The contracts below define a simple counter that tracks how many times the like()
function is called. Each call increments the likes
count by 1. In the original implementation, when called in parallel, the line likes += 1;
is a read-modify-write operation on shared state (likes
), which causes conflicts if multiple transactions try to call like()
in parallel.
The parallel version defines a Like
counter using Arcology’s U256Cumulative
, with a lower bound of 0 and higher bound of uint256
's maximum value. It is a parallel-safe, deterministic data structure. When like()
is called in parallel, it adds the delta value to its internal buffer, rather than directly updating the state.
Calling the Parallelized Version
In the diagram below, three users, Alice, Bob, Charlie, each submit a transaction (TX 0, TX 1 and TX2) that calls the function like()
and get()
.
Alice (TX 0) and Bob (TX 1) both call like()
, which increments a shared cumulative integer. These operations are commutative and associative, so they can be safely executed in parallel.
Charlie (TX 2) calls get()
, which reads the current value of the like counter. But it reads the aggregate value of like. If TX 2 executes:
Before TX 0 and TX 1 → it sees
0
After one → it sees
1
After both → it sees
2
Thus, TX 2 (get()
) does not commute with the writes. This violates Arcology’s commutativity-based conflict model, that requires the final result must be the same regardless of execution order.
Arcology's conflict detector identifies this read-write conflict after execution. To preserve correctness and determinism, the system reverts TX 2. TX 0 and TX 1 proceed and produce a final like count of 2
.
Note: the scheduler learns from this conflict pattern. In future blocks, it will automatically place transactions that call non-commutative functions into separate generations to prevent the same conflict from reoccurring.
Example: Vending Machine
The refill()
function allows the contract owner to increase the cupcake inventory. It checks that the caller is the owner and then adds the specified amount
to the contract’s cupcake balance.
The purchase()
function allows anyone to buy cupcakes by sending ETH. It verifies that the buyer has sent enough ETH (1 ETH per cupcake) and that the contract has enough cupcakes in stock. If both conditions are met, it deducts the requested amount from the contract’s balance and adds the corresponding amount to the buyer’s balance.
Conflict Analysis
Below is a table highlighting potential conflicts when the same or different functions are called simultaneously. We can conclude that since these functions modify the same variable concurrently, transactions calling to refill()
and/or purchase()
cannot be processed in parallel.
refill()
/ refill()
cupcakeBalances[address(this)]
Two concurrent refills modify the inventory simultaneously
refill()
/ purchase()
cupcakeBalances[address(this)]
Refill and purchase modify the sender's balance and the inventory.
purchase()
/ purchase()
cupcakeBalances[address(this)]
cupcakeBalances[msg.sender]
Two concurrent purchases modify the sender's balance and the inventory.
Conflict Example
Let's say there are two transactions calling refill(10)
and purchase(5)
. The refill transaction will add 10
cupcakes to the contracts inventory by executing cupcakeBalances[address(this)] += 10
, while the purchase transaction is try to move 5
cakes from contract's inventory to user's account.
System tries to run the transactions in full parallel.
Transactions read and write the same memory slot; the conflict detector will detect a write-write conflict after execution. One transaction is rolled back.
Only the surviving one proceeds.
Parallelized Version
Upon examining the cupcakeBalances
mapping, we can immediately see that these variables are never directly modified. Instead, they apply a difference—a delta value—to the original balance, either by adding or subtracting an amount. There is no read involved, meaning the final state isn't needed immediately. This makes it a perfect use case for cumulative variables. In the parallelized version:
We replaced the original
uint
used incupcakeBalances
mapping with acumulative u256
provided by Arcology's concurrent library.
Conflict Free Execution
Let's rerun two transactions calling refill(10)
and purchase(5)
. The refill transaction will add 10
cupcakes to the contracts inventory by executing cupcakeBalances[address(this)].add(10)
, while the purchase transaction is try to move 5
cakes from contract's inventory to user's account.
System tries to run the transactions in full parallel.
Transactions read and write the same concurrent variable that is commutative and associate. There is no conflict. Both transactions will through.
Alice successfully fill the inventory and Bob gets his cakes.
Last updated