Publication
Partagez vos connaissances.
How do I optimize for parallel execution while preserving deterministic outcomes?
How do I optimize for parallel execution while preserving deterministic outcomes?
- Move CLI
- Move
- Smart Contract
- Move Script
Réponses
2The kind of complicated aspect of this is ;
- Maximizing throughput on networks that analyze object/state conflicts (e.g., Sui’s object model or Aptos’ resource access lists).
- Avoiding cross-transaction contention by designing sharded state and disjoint access.
- Keeping global aggregates correct without serial bottlenecks.
Strategy
- Shard-by-key: store per-entity data in separate resources/objects keyed by
id
so unrelated transactions don’t contend. - Use pure/readonly data for queries; write only the minimal shard.
- For global aggregates, use event emission + off-chain indexers, or batched reducers that run infrequently.
- Encode conflict keys explicitly (Sui: distinct object IDs; Aptos: separate resources at distinct addresses).
Example: High-concurrency ledger with per-account shards
module 0xA::sharded_ledger {
use std::signer;
use std::assert;
const EINSUFFICIENT_BAL: u64 = 1;
const EZERO: u64 = 2;
/// Each account stores its own resource (disjoint address = disjoint shard).
struct Balance has key {
amount: u64,
}
/// Initialize caller’s shard
public fun init_account(user: &signer) {
if (!exists<Balance>(signer::address_of(user))) {
move_to(user, Balance { amount: 0 });
}
}
public fun mint(admin: &signer, recipient: address, amt: u64) acquires Balance {
assert!(amt > 0, EZERO);
let b = borrow_or_create<Balance>(recipient);
b.amount = b.amount + amt;
}
/// Pure intra-shard debit/credit to minimize cross-conflicts:
/// - debit sender shard
/// - credit receiver shard
public fun transfer(sender: &signer, to: address, amt: u64) acquires Balance {
assert!(amt > 0, EZERO);
let s_addr = signer::address_of(sender);
let s = borrow_global_mut<Balance>(s_addr);
assert!(s.amount >= amt, EINSUFFICIENT_BAL);
s.amount = s.amount - amt;
let r = borrow_or_create<Balance>(to);
r.amount = r.amount + amt;
}
public fun balance_of(owner: address): u64 acquires Balance {
if (exists<Balance>(owner)) {
borrow_global<Balance>(owner).amount
} else { 0 }
}
fun borrow_or_create<T: key>(addr: address): &mut T {
// Generic helper isn’t allowed; we specialize for Balance.
// Included here conceptually; write a Balance-specific version:
abort 999
}
}
Let’s fix the helper and make it concrete:
module 0xA::sharded_ledger2 {
use std::signer;
use std::assert;
const EINSUFFICIENT_BAL: u64 = 1;
const EZERO: u64 = 2;
struct Balance has key { amount: u64 }
public fun init_account(user: &signer) {
if (!exists<Balance>(signer::address_of(user))) {
move_to(user, Balance { amount: 0 });
}
}
public fun mint(admin: &signer, recipient: address, amt: u64) acquires Balance {
assert!(amt > 0, EZERO);
let b = borrow_or_create_balance(recipient);
b.amount = b.amount + amt;
}
public fun transfer(sender: &signer, to: address, amt: u64) acquires Balance {
assert!(amt > 0, EZERO);
let s_addr = signer::address_of(sender);
let s = borrow_global_mut<Balance>(s_addr);
assert!(s.amount >= amt, EINSUFFICIENT_BAL);
s.amount = s.amount - amt;
let r = borrow_or_create_balance(to);
r.amount = r.amount + amt;
}
public fun balance_of(owner: address): u64 acquires Balance {
if (exists<Balance>(owner)) {
borrow_global<Balance>(owner).amount
} else { 0 }
}
fun borrow_or_create_balance(addr: address): &mut Balance acquires Balance {
if (!exists<Balance>(addr)) {
// In generic Move, only the signer at `addr` can publish at `addr`.
// On Sui, creation would be via a new object rather than at `addr`.
// For demo, we assume a privileged admin can create on behalf (pattern varies by chain).
// Replace with chain-appropriate factory pattern.
abort 777;
};
borrow_global_mut<Balance>(addr)
}
////////////////////////
// Spec highlights
////////////////////////
spec transfer {
aborts_if amt <= 0 with EZERO;
aborts_if !exists<Balance>(signer::address_of(sender));
aborts_if borrow_global<Balance>(signer::address_of(sender)).amount < amt with EINSUFFICIENT_BAL;
ensures balance_of(to) >= old(balance_of(to));
ensures balance_of(signer::address_of(sender)) + balance_of(to)
== old(balance_of(signer::address_of(sender))) + old(balance_of(to));
}
}
Why this scales
- Each account’s
Balance
lives under a unique address (or, on Sui, as a distinct object ID). Two transfers touching disjoint(sender, receiver)
pairs don’t conflict and can run in parallel. - Global sums are derived via events/indexers, or computed off-chain, avoiding hot-spot writes.
To maximize parallelism while guaranteeing deterministic final state, design per-actor objects (shards) so most txns touch disjoint object IDs, and implement a deterministic reducer that consumes events in a canonical order (epoch + sequence number) to update global aggregates. Use write-once receipts/events to avoid conflicting writes.
Key ideas
- Make each account/object the canonical owner of its shard; transfers touch only two shards (sender & receiver), minimizing conflicts.
- Replace hot global counters with event emission + off-chain aggregator OR a deterministic on-chain reducer that processes event objects sequentially but in batched background transactions (so main txs are conflict-free).
- Use monotonic sequence numbers or epoch markers to ensure deterministic reduce ordering.
Working pattern (generic Move notion; Sui notes after):
module 0xA::parallel_tokens {
use std::signer;
use std::vector;
struct Balance has key { amount: u128 }
// A write-once Receipt object created on each transfer to record observed change.
struct Receipt has key {
from: address,
to: address,
amount: u128,
epoch: u64,
seq: u64, // per-epoch sequence to deterministically order receipts
}
// A lightweight per-account sequence so transfers from same account are ordered locally.
struct AccountMeta has key { seq: u64 }
public fun init_account(user: &signer) {
let addr = signer::address_of(user);
if (!exists<Balance>(addr)) { move_to(user, Balance { amount: 0u128 }); }
if (!exists<AccountMeta>(addr)) { move_to(user, AccountMeta { seq: 0u64 }); }
}
// Transfer: touch only sender shard (AccountMeta) and create a Receipt object (new object id),
// then credit receiver's Balance object. Creating new Receipt does not conflict with other sender shards.
public fun transfer(sender: &signer, to: address, amt: u128) acquires Balance, AccountMeta, Receipt {
assert!(amt > 0u128, 1);
let saddr = signer::address_of(sender);
let s_bal = borrow_global_mut<Balance>(saddr);
assert!(s_bal.amount >= amt, 2);
s_bal.amount = s_bal.amount - amt;
// increment local seq (touches only sender's AccountMeta)
let meta = borrow_global_mut<AccountMeta>(saddr);
meta.seq = meta.seq + 1u64;
// create receipt
let r = Receipt { from: saddr, to, amount: amt, epoch: current_epoch(), seq: meta.seq };
create_receipt_object(sender, r);
// credit receiver (touches receiver only)
// borrow_or_create_balance(to) pattern assumed
credit_balance(to, amt);
}
fun current_epoch(): u64 {
// implement epoch source (e.g. block height / time). For determinism, derive from chain-provided height.
0u64
}
fun create_receipt_object(user: &signer, r: Receipt) {
// publishing receipts as discrete objects (Sui object) ensures different receipts don't conflict.
// Implementation depends on chain primitives; conceptually: move_to(user, r) or create_object.
// On Sui: create a new object for each receipt; objects with different IDs do not conflict.
}
fun credit_balance(to: address, amt: u128) acquires Balance {
if (!exists<Balance>(to)) { /* create via authorized factory */ abort 777; }
let b = borrow_global_mut<Balance>(to);
b.amount = b.amount + amt;
}
///////////////////////
// Deterministic aggregator (reducer)
///////////////////////
// An on-chain reducer can be an authority that processes receipts in canonical order (epoch, seq).
// Off-chain indexers can also process receipts deterministically and publish snapshots back on-chain.
}
Why this scales & stays deterministic
- Most transactions only touch the sender and receiver shards (two object addresses) — if unrelated accounts are used, transactions run in parallel.
- Receipts are write-once objects: creating many receipts with distinct IDs avoids lock contention on a single global log.
- Deterministic ordering is achieved via
(epoch, seq)
numbers embedded in receipts. A reducer processes receipts in canonical lexicographic order. If the reducer runs on-chain, it must be a single authoritative actor (so it serializes processing) — but main token transfers remain parallel because they don't write the global aggregator directly. - Off-chain indexers can re-compose final totals deterministically and publish checkpoint objects (snapshots) that the chain can reference.
Sui-specific notes
- Sui’s parallel execution model is object-based. Model each
Balance
as an object ID. Creating a newReceipt
object for every transfer is cheap and prevents conflicts — different receipts have different object IDs. - Avoid putting frequently-updated global counters in a single shared object; instead, either shard them or move them off-chain and store checkpoints as objects.
Practical tips
- Use batching: clients accumulate receipts locally and submit a single batched transaction that touches fewer objects (but batching should avoid creating contention).
- For strong on-chain global invariants you must have a deterministic reducer on-chain; otherwise rely on off-chain indexers and periodic on-chain checkpoints.
- Use
spec
to prove that per-account balance changes + sum(receipts) = snapshot totals (ghost state / spec-only invariants).
Connaissez-vous la réponse ?
Veuillez vous connecter et la partager.
Move is an executable bytecode language used to implement custom transactions and smart contracts.
Gagne ta part de 1000 Sui
Gagne des points de réputation et obtiens des récompenses pour avoir aidé la communauté Sui à se développer.

- ... SUIMatthardy+2095
- ... SUIacher+1666
- ... SUIjakodelarin+1092
- ... SUIChubbycheeks +1081
- ... SUITucker+1047
- ... SUIKurosakisui+1034
- ... SUIzerus+890