Skip to main content
All articles

How Does a Native Program Validate Proposed Transaction Changes?

· 12 min read
Bankware Global Engineering

A holds 100 units of an asset and has authorized S to spend up to 30 of them. S wants to use that permission to send 20 to B. After the transaction, A should have 80, B should have 20, and S's remaining allowance should be 10.

In Nigo's Native Program path, the party constructing the transaction prepares these proposed changes first. The program checks whether the proposal satisfies the spending conditions. One Ledger, Three Ways to Execute Transactions compared this approach with Direct Cell and the EVM. Here, we look inside Native Program validation.

Is it enough for the program to reply, “Allowed”? If the answer does not identify which of A's asset inputs may be used, or which approval record may be changed into what, the ledger cannot establish which inputs and state changes that authorization covers. This post asks how to connect a decision that spending conditions are satisfied to the records that actually change.

A proposal includes both asset and approval records

Nigo holds ledger state in small record units called StateCells. This example needs records holding the asset's owner and quantity, plus a spending approval record managed by a program. A is the owner, S is the approved spender and transaction signer, and B is the recipient.

RecordCurrentProposed
A's asset quantity10080
B's asset quantity020
Allowance A granted to S3010

The asset here is a Native Asset, whose type and quantity the ledger manages directly. There is no additional issuance or burning, and the fee amount is assumed to be 0. The table shows only the asset being transferred and the approval record. The allowance of 30 is not a separate deposit, so the total asset quantity is 100 both before and after the transaction. B's 0 means B has none of the asset yet; it does not mean preparing an input cell with a quantity of 0.

The transaction includes existing records to consume and new records to create. In this example, it identifies A's asset record of 100 and the allowance record of 30 as inputs. It proposes A's asset record of 80, B's asset record of 20, and the allowance record of 10 as outputs. It also includes signer S and program invocation information specifying “Transfer 20 of A's asset to B.”

Constructing this proposal requires finding records that are currently available to use. A wallet, SDK, or the node's request-building tools can help, but the transaction being signed must specify concrete inputs and outputs. The Native Program evaluates the proposal prepared this way.

Core assembles the data supplied to the program

S's claim in a request that “A holds 100 and my allowance is 30” does not make it true. Identifying an input to use and establishing its current contents are different tasks.

Core, which enforces Nigo's common ledger rules, retrieves the transaction's referenced inputs from the current ledger state. It checks that the input records are still available and gathers the information needed for validation, including the signer and the asset's policy. Outputs are new records proposed by the transaction. Checking their format and permitted scope of access does not make them part of the ledger state yet.

The data supplied to the program includes:

  • Current inputs: the actual records for A's asset quantity of 100 and the allowance of 30 that A granted to S.
  • Proposed outputs: A's asset quantity of 80, B's asset quantity of 20, and the remaining allowance of 10.
  • Invocation and execution context: signer S, the recipient and quantity, asset policy, chain and block information, fees, and other relevant details.

Even when Core presents asset information in a form that is easier for the program to read, it checks that the values come from the actual input and output cells. It does not pass a quantity separately claimed by the caller as though it were a quantity recorded in the ledger.

This collection of data, expressed in a prescribed format, is the canonical context. Its format and ordering ensure that nodes processing the same transaction against the same execution state construct the same byte sequence. The program can distinguish current records from proposed results as it checks them.

The result identifies authorized inputs and state changes

The validation program, or validator, receives this context and checks the spending conditions. In the example, it checks that the approval record concerns A's asset, that the approved spender is signer S, and that spending 20 does not exceed the allowance of 30. It also checks that the asset recipient and quantity match the invocation and that the remaining allowance is exactly 10.

The common entry point for this check is validate. The program is called once in an execution attempt, and its result identifies which inputs it authorizes for use and which state changes it permits. This does not mean a transaction is validated only once over its entire lifetime, including any re-execution.

Let us assign illustrative numbers to just the asset and approval records in this example. This list omits the full transaction format and shows how the validation data connects to the returned result.

Input list
0: A's asset 100
1: Existing allowance 30

Output list
0: A's asset 80
1: B's asset 20
2: New allowance 10

Returned result
Authorized inputs: [0, 1]
Approval record change:
input 1 → output 2

The actual return value refers to positions in the input and output lists already supplied to the program. When authorizing the use of an input the signer does not own, it returns that input's position. For a program state change, it returns a logical key identifying the state, together with the positions of the input to consume and the output to create. In this example, the logical key identifies the spending approval between A and S.

In the list above, input 0 is A's asset record, and input 1 is the approval record granted by A. The program authorizes S to use both and declares that the approval record changes from input 1 to output 2. The result therefore does not separately construct and return a cell holding the new allowance of 10. It points to the proposed output that represents the new approval record. The program has checked that output's contents, and Core can verify that the returned reference corresponds to that actual record.

A simplified view of the success path. The records supplied for validation are matched to those referenced by the returned result, and changes are applied only after they also pass the ledger's rules.

There are limits to the authority a program may exercise. To authorize the use of someone else's inputs or declare program state changes, an installed program must have permission to perform those functions. An arbitrary program cannot simply return “A's asset may be spent” and have that result accepted. Core also checks that the program is the one associated with the asset's policy.

Core matches the returned references to actual ledger state

Returning a logical key and input and output positions does not itself change the ledger state. Core checks which cells are actually at those positions and which record represents that state in the current ledger.

For example, if the program returns “Update the approval A granted to S,” the input identified for consumption must be the current record for that approval. The output must be the next record for the same state and belong to the state area managed by the invoked program. A result pointing to another approval's input or to an output that does not exist in the transaction cannot pass this check. The program state inputs and outputs in the transaction and the returned change declarations must correspond completely, with nothing left unmatched.

Two questions must be distinguished here. “Should the allowance be 10?” is a question about application rules. “Are we changing the authorized state through these inputs and outputs?” is a question about applying the change to the ledger. Core does not implement allowance subtraction on behalf of every program.

Three incorrect proposals make this boundary clearer. These examples explain who is responsible for each check; they are not a sequence of errors observed by actually running the transactions.

Spending 40 with an allowance of 30

Suppose S proposes reducing A's asset quantity to 60 and giving B 40. The total remains 100, but the amount exceeds the approved 30. Asset conservation alone cannot reveal the problem. The program that understands the approved spender and the meaning of the allowance must check this application condition.

Sending 20 while leaving the allowance at 30

A proposal could set A=80 and B=20 while leaving the output allowance at 30. Again, the asset total is correct. But accepting this change would allow the same approval to be used repeatedly. The program must check the relationship between the transferred quantity and the new allowance and reject the proposal.

Core, meanwhile, checks whether a returned approval update references a different state or fails to match the transaction's actual approval inputs and outputs. If the program implements allowance arithmetic incorrectly, approves an incorrect output, and returns references that match that output, Core's matching checks alone cannot detect the application error. A structure that binds validation results to actual changes still requires the program itself to be correct.

Creating 80 for A and 25 for B

The input asset quantity is 100, but the outputs total 105. Since this example involves no additional issuance, the change violates the per-asset conservation rules enforced by Core. Even if a program incorrectly approves it, that result alone cannot create an extra 5 units. A correct program may also reject such a proposal, but responsibility for the common ledger rules does not move to the program.

The two kinds of checks therefore answer different questions. The program decides whether the change is permitted by the application's rules. Core checks that this decision corresponds precisely to the actual records and respects the ledger's common rules. Inputs are consumed and outputs applied only after the checks pass. In the valid example, the asset transfer and allowance reduction are therefore recorded together.

Failures are not all handled in the same way. If the program rejects execution with a revert or exhausts gas, its execution budget, the application changes are not applied. Separate fee settlement and a failed execution result remain. A result that violates protocol rules—such as its return format, permitted authority, or correspondence with the actual state changes—makes the transaction itself invalid. This is distinct from the path that settles a program execution failure.

Validation stays within the state supplied to the program

What if the program could read arbitrary additional ledger state or call other contracts during validation? The data initially presented by the transaction would no longer be enough to identify all state used in the decision. If a call could also change other state, the scope of changes would extend beyond the declared inputs and outputs.

Native Programs therefore receive the ledger state they need through inputs, outputs, and the context assembled by Core. Instructions that read or write arbitrary contract storage and calls to external contracts are not permitted. If validating an approval requires a new policy record, a way to supply that record as validation data must first be established.

Native Programs reuse Solidity and a restricted EVM environment to express and execute application rules specific to each program. They do not adopt the storage access model of ordinary Ethereum contracts; the state to validate arrives in the data supplied above.

This choice requires preparation by the party constructing the transaction. It must find A's asset record and the approval record for S, then propose outputs with the appropriate asset quantities and remaining allowance. In return, the ledger gains a basis for identifying in advance which state validation depends on and which state will change.

From explicit changes to the next question

Return to the opening transaction. S proposed splitting the asset quantity of 100 into 80 and 20, and reducing the allowance from 30 to 10. Core assembles the actual current records as validation data, and the program checks the approval conditions and the new allowance. The result identifies authorized inputs and the approval record's change. Core matches these references to the actual records and checks common rules such as per-asset conservation.

This connection completes Native Program validation: state explicitly what has been authorized, and let the ledger ensure that the authorization covers the changes it will actually apply.

Knowing the scope of changes in advance raises the next question. If transactions use different asset inputs and approval records, can they be validated concurrently and still produce the same ledger? Explicit access boundaries are a starting point for that decision. They do not make every operation that changes system state, or every set of dependent transactions, independent. Next, we will examine which changes can execute together and how to preserve the same result even when execution finishes in a different order.