Move.

Допис

Діліться своїми знаннями.

lite.vue.
Aug 29, 2025
Питання та відповіді експертів

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
0
2
Поділитися
Коментарі
.

Відповіді

2
Kurosakisui.
Aug 29 2025, 19:04

The 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

  1. Shard-by-key: store per-entity data in separate resources/objects keyed by id so unrelated transactions don’t contend.
  2. Use pure/readonly data for queries; write only the minimal shard.
  3. For global aggregates, use event emission + off-chain indexers, or batched reducers that run infrequently.
  4. 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.

2
Коментарі
.
Sato$hii.
Aug 29 2025, 19:12

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 new Receipt 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).

1
Коментарі
.

Ви знаєте відповідь?

Будь ласка, увійдіть та поділіться нею.

Move is an executable bytecode language used to implement custom transactions and smart contracts.

242Пости541Відповіді
Sui.X.Peera.

Зароби свою частку з 1000 Sui

Заробляй бали репутації та отримуй винагороди за допомогу в розвитку спільноти Sui.

Кампанія винагородСерпень