Допис
Діліться своїми знаннями.
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
Відповіді
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).
Ви знаєте відповідь?
Будь ласка, увійдіть та поділіться нею.
Move is an executable bytecode language used to implement custom transactions and smart contracts.
Зароби свою частку з 1000 Sui
Заробляй бали репутації та отримуй винагороди за допомогу в розвитку спільноти Sui.

- ... SUIMatthardy+2095
- ... SUIacher+1666
- ... SUIChubbycheeks +1091
- ... SUIjakodelarin+1060
- ... SUITucker+1047
- ... SUIKurosakisui+1034
- ... SUIOpiiii+861