Transaction Validity
- Transaction Lifecycle
- Access Lists
- VM Precondition Validity Rules
- Predicate Verification
- Script Execution
- VM Postcondition Validity Rules
Transaction Lifecycle
Once a transaction is seen, it goes through several stages of validation, in this order:
Access Lists
The validity rules below assume sequential transaction validation for side effects (i.e. state changes). However, by construction, transactions with disjoint write access lists can be validated in parallel, including with overlapping read-only access lists. Transactions with overlapping write access lists must be validated and placed in blocks in topological order.
UTXOs and contracts in the read-only and write-destroy access lists must exist (i.e. have been created previously) in order for a transaction to be valid. In other words, for a unique state element ID, the write-create must precede the write-destroy.
Read-only access list:
Write-destroy access list:
- For each input
InputType.Coin
- The UTXO ID
(txID, outputIndex)
- The UTXO ID
- For each input
InputType.Contract
- The UTXO ID
(txID, outputIndex)
- The UTXO ID
- For each input
InputType.Message
- The message ID
messageID
- The message ID
Write-create access list:
- For each output
OutputType.ContractCreated
- The contract ID
contractID
- The contract ID
- For each output
- The created UTXO ID
Note that block proposers use the contract ID contractID
for inputs and outputs of type InputType.Contract
and OutputType.Contract
rather than the pair of txID
and outputIndex
.
VM Precondition Validity Rules
This section defines VM precondition validity rules for transactions: the bare minimum required to accept an unconfirmed transaction into a mempool, and preconditions that the VM assumes to hold prior to execution. Chains of unconfirmed transactions are omitted.
For a transaction tx
, UTXO set state
, contract set contracts
, and message set messages
, the following checks must pass.
Note: InputMessages where
input.dataLength > 0
are not dropped from themessages
message set until they are included in a transaction of typeTransactionType.Script
with aScriptResult
receipt whereresult
is equal to0
indicating a successful script exit.
Base Sanity Checks
Base sanity checks are defined in the transaction format.
Spending UTXOs and Created Contracts
for input in tx.inputs:
if input.type == InputType.Contract:
if not input.contractID in contracts:
return False
elif input.type == InputType.Message:
if not input.nonce in messages:
return False
else:
if not (input.txID, input.outputIndex) in state:
return False
return True
If this check passes, the UTXO ID (txID, outputIndex)
fields of each contract input is set to the UTXO ID of the respective contract. The txPointer
of each input is also set to the TX pointer of the UTXO with ID utxoID
.
Sufficient Balance
For each asset ID asset_id
in the input and output set:
def sum_data_messages(tx, asset_id) -> int:
"""
Returns the total balance available from messages containing data
"""
total: int = 0
if asset_id == 0:
for input in tx.inputs:
if input.type == InputType.Message and input.dataLength > 0:
total += input.amount
return total
def sum_inputs(tx, asset_id) -> int:
total: int = 0
for input in tx.inputs:
if input.type == InputType.Coin and input.asset_id == asset_id:
total += input.amount
elif input.type == InputType.Message and asset_id == 0 and input.dataLength == 0:
total += input.amount
return total
"""
Returns any minted amounts by the transaction
"""
def minted(tx, asset_id) -> int:
if tx.type != TransactionType.Mint or asset_id != tx.mint_asset_id:
return 0
return tx.mint_amount
def sum_outputs(tx, asset_id) -> int:
total: int = 0
for output in tx.outputs:
if output.type == OutputType.Coin and output.asset_id == asset_id:
total += output.amount
return total
def available_balance(tx, asset_id) -> int:
"""
Make the data message balance available to the script
"""
availableBalance = sum_inputs(tx, asset_id) + sum_data_messages(tx, asset_id) + minted(tx, asset_id)
return availableBalance
def unavailable_balance(tx, asset_id) -> int:
sentBalance = sum_outputs(tx, asset_id)
gasBalance = gasPrice * gasLimit / GAS_PRICE_FACTOR
# Size excludes witness data as it is malleable (even by third parties!)
bytesBalance = size(tx) * GAS_PER_BYTE * gasPrice / GAS_PRICE_FACTOR
# Total fee balance
feeBalance = ceiling(gasBalance + bytesBalance)
# Only base asset can be used to pay for gas
if asset_id == 0:
return sentBalance + feeBalance
return sentBalance
# The sum_data_messages total is not included in the unavailable_balance since it is spendable as long as there
# is enough base asset amount to cover gas costs without using data messages. Messages containing data can't
# cover gas costs since they are retryable.
return available_balance(tx, asset_id) >= (unavailable_balance(tx, asset_id) + sum_data_messages(tx, asset_id))
Valid Signatures
def address_from(pubkey: bytes) -> bytes:
return sha256(pubkey)[0:32]
for input in tx.inputs:
if (input.type == InputType.Coin || input.type == InputType.Message) and input.predicateLength == 0:
# ECDSA signatures must be 64 bytes
if tx.witnesses[input.witnessIndex].dataLength != 64:
return False
# Signature must be from owner
if address_from(ecrecover_k1(txhash(), tx.witnesses[input.witnessIndex].data)) != input.owner:
return False
return True
Signatures and signature verification are specified here.
The transaction hash is computed as defined here.
Predicate Verification
For each input of type InputType.Coin
or InputType.Message
, and predicateLength > 0
, verify its predicate.
Script Execution
Given transaction tx
, the following checks must pass:
If tx.scriptLength == 0
, there is no script and the transaction defines a simple balance transfer, so no further checks are required.
If tx.scriptLength > 0
, the script must be executed. For each asset ID asset_id
in the input set, the free balance available to be moved around by the script and called contracts is freeBalance[asset_id]
. The initial message balance available to be moved around by the script and called contracts is messageBalance
:
freeBalance[asset_id] = available_balance(tx, asset_id) - unavailable_balance(tx, asset_id)
messageBalance = sum_data_messages(tx, 0)
Once the free balances are computed, the script is executed. After execution, the following is extracted:
- The transaction in-memory on VM termination is used as the final transaction which is included in the block.
- The unspent free balance
unspentBalance
for each asset ID. - The unspent gas
unspentGas
from the$ggas
register.
The fees incurred for a transaction are ceiling(((size(tx) * GAS_PER_BYTE) + (tx.gasLimit - unspentGas)) * tx.gasPrice / GAS_PRICE_FACTOR)
.
If the transaction as included in a block does not match this final transaction, the block is invalid.
VM Postcondition Validity Rules
This section defines VM postcondition validity rules for transactions: the requirements for a transaction to be valid after it has been executed.
Given transaction tx
, state state
, and contract set contracts
, the following checks must pass.
Correct Change
If change outputs are present, they must have:
- if the transaction does not revert;
- if the asset ID is
0
; anamount
ofunspentBalance + floor((unspentGas * tx.gasPrice) / GAS_PRICE_FACTOR)
- otherwise; an
amount
of the unspent free balance for that asset ID after VM execution is complete
- if the asset ID is
- if the transaction reverts;
- if the asset ID is
0
; anamount
of the initial free balance plus(unspentGas * tx.gasPrice) - messageBalance
- otherwise; an
amount
of the initial free balance for that asset ID.
- if the asset ID is
State Changes
Transaction processing is completed by removing spent UTXOs from the state and adding created UTXOs to the state.
Coinbase Transaction
The coinbase transaction is a mechanism for block creators to collect transaction fees.
In order for a coinbase transaction to be valid:
- It must be a Mint transaction.
- The coinbase transaction must be the last transaction within a block, even if there are no other transactions in the block and the fee is zero.
- The
mintAmount
doesn't exceed the total amount of fees processed from all other transactions within the same block. - The
mintAssetId
matches theasset_id
that fees are paid in (asset_id == 0
).
The minted amount of the coinbase transaction intrinsically increases the balance corresponding to the inputContract
.
This means the balance of mintAssetId
is directly increased by mintAmount
on the input contract,
without requiring any VM execution. Compared to coin outputs, intrinsically increasing a contract balance to collect
coinbase amounts prevents the accumulation of dust during low-usage periods.