Перейти к основному содержимому

Редакционная доработка ещё намеренно не завершена

Этот раздел скрыт из основной навигации. Перевод и редакторская полировка намеренно не считаются обязательными, пока раздел не станет публичным.

Асинхронная Model

The promise system is the foundation of NEAR's asynchronous execution model. When a smart контракта needs to вызов another контракта, it creates promises - abstract representations of future computations that will be converted to квитанции for actual execution.

Understanding Promises

In NEAR, a Promise is a placeholder for work that will happen in a future блока. Unlike JavaScript promises or Rust futures, NEAR promises are not in-memory objects that you await - they're instructions that get converted to квитанции when the текущий function returns.

The Key Principle

Promises are declarative, not imperative. You're not calling another контракта - you're declaring that you want to вызов it. The actual execution happens in future блоки, potentially on different shards.

This is why:

  • You can't get a return value immediately
  • You must use callbacks to process results
  • The callback might execute in a completely different блока/shard
  • NEAR is fundamentally asynchronous

The Promise DAG

Promises form a Directed Acyclic Graph (DAG) of dependencies:

Promise Creation Functions

promise_create()

Creates a new promise for calling another аккаунта's function.

pub fn promise_create(
    &mut self,
    account_id_len: u64,
    account_id_ptr: u64,
    function_name_len: u64,
    function_name_ptr: u64,
    arguments_len: u64,
    arguments_ptr: u64,
    amount: u128,
    gas: u64,
) -> Result<u64>

What it does:

  1. Reads target account_id and function_name from WASM memory
  2. Reads arguments (the вызов payload)
  3. Creates a new квитанция destined for the target аккаунта
  4. Attaches the specified amount of NEAR and prepays gas
  5. Returns a PromiseIndex that can be used in subsequent calls

Usage pattern:

// In contract code
let promise_id = env::promise_create(
    "other-contract.near",
    "some_method",
    b'{"arg": "value"}',
    0,                    // attached NEAR
    5_000_000_000_000,    // attached gas (5 TGas)
);

promise_then()

Creates a callback that executes after another promise completes.

pub fn promise_then(
    &mut self,
    promise_idx: u64,
    // ... target account, method, args ...
    amount: u128,
    gas: u64,
) -> Result<u64>

What it does:

  1. Takes an existing promise_idx as a dependency
  2. Creates a new квитанция that waits for the first promise to complete
  3. When the first promise finishes, its return value becomes available as PromiseResult in the callback
  4. Returns a new PromiseIndex for the callback

The Key Insight: The callback doesn't execute immediately. NEAR creates two квитанции:

  1. The original promise (вызов to other контракта)
  2. A callback квитанция with an input_data_id dependency

When the first квитанция executes and produces a result, that result becomes a DataReceipt that satisfies the callback's dependency.

promise_and()

Joins multiple promises - waits for ALL of them to complete.

pub fn promise_and(
    &mut self,
    promise_idx_ptr: u64,
    promise_idx_count: u64,
) -> Result<u64>

Use case: When you need to вызов multiple контракты and wait for all results:

let p1 = env::promise_create("contract_a", "get_data", ...);
let p2 = env::promise_create("contract_b", "get_data", ...);
let p3 = env::promise_create("contract_c", "get_data", ...);

let all = env::promise_and(&[p1, p2, p3]);
let callback = env::promise_then(all, "self", "process_all_results", ...);

promise_batch_create() and promise_batch_then()

Low-level functions for creating квитанции with multiple actions:

// Create an account with initial balance
let batch = env::promise_batch_create("new_account.near");
env::promise_batch_action_create_account(batch);
env::promise_batch_action_transfer(batch, deposit_amount);
env::promise_batch_action_add_full_access_key(batch, public_key);

Promise Results

When a callback executes, it can read the results of the promises it depends on.

pub enum PromiseResult {
    /// The promise has not yet been resolved
    Pending,
    /// The promise succeeded with a value
    Successful(Vec<u8>),
    /// The promise failed (no value)
    Failed,
}

In callback код:

fn callback() {
    let count = env::promise_results_count();
    for i in 0..count {
        match env::promise_result(i, 0) {
            PromiseResult::Successful(value) => {
                // Process successful result
            }
            PromiseResult::Failed => {
                // Handle failure
            }
            PromiseResult::Pending => unreachable!(),
        }
    }
}

Квитанция Types

Receipts are the fundamental unit of execution in NEAR. While транзакции are what users submit, квитанции are what the runtime actually processes.

Квитанция Hierarchy

pub enum ReceiptEnum {
    Action(ActionReceipt) = 0,                       // Execute actions
    Data(DataReceipt) = 1,                           // Deliver return data
    PromiseYield(ActionReceipt) = 2,                 // Suspend execution (NEP-519)
    PromiseResume(DataReceipt) = 3,                  // Resume suspended execution
    GlobalContractDistribution(...) = 4,             // Distribute shared contracts
    ActionV2(ActionReceiptV2) = 5,                   // Enhanced action receipt
    PromiseYieldV2(ActionReceiptV2) = 6,             // Yield with enhanced receipt
}

ActionReceipt: The Workhorse

ActionReceipts contain the actual work to be done.

pub struct ActionReceipt {
    /// Account that originally signed the transaction
    pub signer_id: AccountId,

    /// Public key used for signing (for gas refunds)
    pub signer_public_key: PublicKey,

    /// Gas price when this receipt was created
    pub gas_price: Balance,

    /// Where to send the result of this execution
    pub output_data_receivers: Vec<DataReceiver>,

    /// What this receipt is waiting for (callback dependencies)
    pub input_data_ids: Vec<CryptoHash>,

    /// The actual operations to perform
    pub actions: Vec<Action>,
}

Key Fields:

FieldPurpose
signer_idTrack back to original tx signer for газ refunds
gas_priceГаз price at creation time (for deterministic refunds)
output_data_receiversReceipts waiting for this execution's result
input_data_idsDependencies this квитанция must wait for
actionsОперацияs to perform: FunctionCall, Transfer, etc.

DataReceipt: The Messenger

DataReceipts carry return values between квитанции.

pub struct DataReceipt {
    /// Unique identifier matching an input_data_id somewhere
    pub data_id: CryptoHash,

    /// The actual return value (None means execution failed)
    pub data: Option<Vec<u8>>,
}

The Data Flow Dance

Квитанция Lifecycle

Stage 1: Creation

Receipts are created in two ways:

  • From транзакции: The first квитанция for any транзакции
  • From execution: When a контракта creates promises

Stage 2: Routing

Each квитанция routes to its receiver's shard:

pub fn receiver_shard_id(&self, shard_layout: &ShardLayout) -> ShardId {
    shard_layout.account_id_to_shard_id(self.receiver_id())
}

Stage 3: Waiting (if needed)

If a квитанция has input_data_ids, it cannot execute until ALL dependencies are satisfied. These become delayed квитанции.

Stage 4: Execution

When all dependencies are satisfied:

  1. Load input data from состояние as PromiseResults
  2. Delete input data from состояние (one-time consumption)
  3. Execute actions sequentially
  4. Produce outputs (DataReceipts, new ActionReceipts, refunds)

Cross-Shard Communication

NEAR's sharding model means different аккаунтов live on different shards. Cross-shard communication uses asynchronous квитанция passing.

Аккаунт-to-Shard Mapping

Every аккаунта belongs to exactly one shard, determined by alphabetical ordering against boundary аккаунтов:

boundary_accounts: ["aurora", "aurora-0", "kkuuue2akv_1630967379.near"]

Shard 0: accounts < "aurora"
Shard 1: "aurora" <= accounts < "aurora-0"
Shard 2: "aurora-0" <= accounts < "kkuuue2akv_1630967379.near"
Shard 3: accounts >= "kkuuue2akv_1630967379.near"
примечание

Alphabetical ordering uses the full аккаунта name. So a.near and z.a.near may be on different shards!

Cross-Shard Квитанция Propagation

Execution Order Guarantees

Within a Shard:

  1. Транзакции execute in chunk order
  2. Receipts execute deterministically (local first, then by receipt_id)

Across Shards:

  • No global ordering guarantee across shards
  • Causal ordering: If квитанция A creates квитанция B, A always executes before B
  • Data dependencies: A квитанция waits until ALL its input_data_ids are satisfied
  • No double execution: Each квитанция executes exactly once

Delayed Receipts

When a квитанция arrives but its dependencies aren't ready, it becomes a delayed квитанция.

Why Delayed?

  • Cross-shard calls take time (at least 1 блока per hop)
  • Callback can't execute until source вызов completes
  • Сеть latency and shard processing order affect arrival time

Processing (each блока):

  1. Check delayed квитанции queue
  2. For each, check if dependencies arrived
  3. If satisfied, promote to execution
  4. If not, leave in queue

Congestion Control

Too many cross-shard квитанции can overwhelm a shard. NEAR implements backpressure (NEP-539):

  • Memory-based limiting: Reject new транзакции when shard memory exceeds threshold (~500MB)
  • Receiver-specific backpressure: Stop accepting транзакции to congested аккаунтов
  • Deadlock prevention: Always allow minimum throughput to drain queues

Practical Example: Cross-Контракт Вызов with Callback

Step 1: Контракт A Creates Promises

Contract A executes transaction:
├── promise_create("B", "getData", ...) → Promise 0
└── promise_then(0, "A", "callback", ...) → Promise 1

Step 2: ReceiptManager Generates Receipts

Receipt R1 (for Promise 0):
├── receiver_id: "B"
├── actions: [FunctionCall("getData")]
├── input_data_ids: []  (no dependencies)
└── output_data_receivers: [{data_id: D1, receiver_id: "A"}]

Receipt R2 (for Promise 1):
├── receiver_id: "A"
├── actions: [FunctionCall("callback")]
├── input_data_ids: [D1]  (waits for R1's output)
└── output_data_receivers: []

Step 3: Execution Timeline

Block N (Shard A):
  └── Transaction executes → creates R1, R2
  └── R1 sent to Shard B, R2 stored as delayed (waiting for D1)

Block N+1 (Shard B):
  └── R1 executes → produces DataReceipt D1 with result
  └── D1 sent to Shard A

Block N+2 (Shard A):
  └── D1 arrives → satisfies R2's dependency
  └── R2 executes with D1's data as PromiseResult::Successful

Газ Weight Distribution

When you don't know exactly how much газ to allocate, use газ weights:

env::promise_batch_action_function_call_weight(
    promise_id,
    "method",
    args,
    deposit,
    Gas::ZERO,        // minimum gas
    GasWeight(1),     // weight for distributing remaining gas
);

The ReceiptManager distributes unused газ proportionally based on weights.