Skip to main content

Editorial polish is still intentionally in progress

This section is hidden from primary navigation. Translation and editorial polish are intentionally out of scope until the section is public.

Async Model

The promise system is the foundation of NEAR's asynchronous execution model. When a smart contract needs to call another contract, it creates promises - abstract representations of future computations that will be converted to receipts for actual execution.

Understanding Promises

In NEAR, a Promise is a placeholder for work that will happen in a future block. Unlike JavaScript promises or Rust futures, NEAR promises are not in-memory objects that you await - they're instructions that get converted to receipts when the current function returns.

The Key Principle

Promises are declarative, not imperative. You're not calling another contract - you're declaring that you want to call it. The actual execution happens in future blocks, 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 block/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 account'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 call payload)
  3. Creates a new receipt destined for the target account
  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 receipt 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 receipts:

  1. The original promise (call to other contract)
  2. A callback receipt with an input_data_id dependency

When the first receipt 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 call multiple contracts 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 receipts 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 code:

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!(),
        }
    }
}

Receipt Types

Receipts are the fundamental unit of execution in NEAR. While transactions are what users submit, receipts are what the runtime actually processes.

Receipt 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 gas refunds
gas_priceGas price at creation time (for deterministic refunds)
output_data_receiversReceipts waiting for this execution's result
input_data_idsDependencies this receipt must wait for
actionsOperations to perform: FunctionCall, Transfer, etc.

DataReceipt: The Messenger

DataReceipts carry return values between receipts.

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

Receipt Lifecycle

Stage 1: Creation

Receipts are created in two ways:

  • From transactions: The first receipt for any transaction
  • From execution: When a contract creates promises

Stage 2: Routing

Each receipt 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 receipt has input_data_ids, it cannot execute until ALL dependencies are satisfied. These become delayed receipts.

Stage 4: Execution

When all dependencies are satisfied:

  1. Load input data from state as PromiseResults
  2. Delete input data from state (one-time consumption)
  3. Execute actions sequentially
  4. Produce outputs (DataReceipts, new ActionReceipts, refunds)

Cross-Shard Communication

NEAR's sharding model means different accounts live on different shards. Cross-shard communication uses asynchronous receipt passing.

Account-to-Shard Mapping

Every account belongs to exactly one shard, determined by alphabetical ordering against boundary accounts:

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"
note

Alphabetical ordering uses the full account name. So a.near and z.a.near may be on different shards!

Cross-Shard Receipt Propagation

Execution Order Guarantees

Within a Shard:

  1. Transactions 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 receipt A creates receipt B, A always executes before B
  • Data dependencies: A receipt waits until ALL its input_data_ids are satisfied
  • No double execution: Each receipt executes exactly once

Delayed Receipts

When a receipt arrives but its dependencies aren't ready, it becomes a delayed receipt.

Why Delayed?

  • Cross-shard calls take time (at least 1 block per hop)
  • Callback can't execute until source call completes
  • Network latency and shard processing order affect arrival time

Processing (each block):

  1. Check delayed receipts 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 receipts can overwhelm a shard. NEAR implements backpressure (NEP-539):

  • Memory-based limiting: Reject new transactions when shard memory exceeds threshold (~500MB)
  • Receiver-specific backpressure: Stop accepting transactions to congested accounts
  • Deadlock prevention: Always allow minimum throughput to drain queues

Practical Example: Cross-Contract Call with Callback

Step 1: Contract 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

Gas Weight Distribution

When you don't know exactly how much gas to allocate, use gas 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 gas proportionally based on weights.