# Waves 1.5: Light Node
# Overview
Waves protocol version 1.5 introduces a mechanism for the generating node to produce and distribute final state snapshots for the blocks it forges. Other nodes can rely on these state snapshots to speed up transaction processing under heavy load. Other nodes can challenge state snapshot calculation results in cases when a malicious generator distributes invalid state snapshots to peers.
# Transaction State Snapshot
Blockchain state can be described as a mapping key => value
which is needed to validate the subsequent transactions, including final account balances of WAVES, assets and leases, data entries, exchange order statuses, etc. Each transaction, in turn, changes the state somehow by creating, altering or removing some values from the state. Transaction state snapshot (TSS) is the set of all (key, value)
pairs affected by the transaction. TSSs are encoded as TransactionStateSnapshot
protobuf message at the network level.
The generator produces a state snapshot for every transaction it includes in the block. Instead of validating transactions, nodes can sequentially apply transaction state snapshots to their local state, replacing local values for the given key with the new ones. Applying state snapshots instead of performing full transaction validation may significantly decrease block processing time. The state built by sequentially applying transaction state snapshots is identical to the one built by validating all the transactions.
# Block State Hash
A generator calculates the transaction state snapshot hash (TH) by building the list of binary strings si representing the state changes. These binary strings are sorted lexicographically and fed into Blake2b hash function. For the purpose of sorting, bytes are assumed to be unsigned. The resulting hash would be the transaction state snapshot hash.
The following binary strings are built for the snapshot (||
is the string concatenation operator):
- Waves balances:
address || balance
- Asset balances:
address || asset_id || balance
- Data entries:
address || key || data_entry
- Account script:
sender_public_key || script || verifier_complexity)
. If the script is removed from the account, then onlysender_public_key
is hashed. - Asset script:
asset_id || script
- Lease balance:
address || lease_in || lease_out
- Lease details:
lease_id || amount || sender_public_key || recipient
- Lease status:
lease_id || is_active
- Sponsorship:
asset_id || min_sponsored_fee
- Alias:
address || alias
- Filled volume and fee:
order_id || filled_volume || filled_fee
- Static asset info:
asset_id || issuer_public_key || decimals || is_nft
.decimals
is one byte. - Asset reissuability:
asset_id || is_reissuable || total_quantity
- Asset name and description:
asset_id || name || description || change_height
- Non-successful transaction application status:
tx_id || application_status
. Application status is one byte, either 0x01 (script execution failed) or 0x02 (elided; see below).
Boolean flags (is_active
, is_nft
, is_reissuable
) are encoded as one byte with 0x01 signifying true
and 0x00 signifying false
. All numeric fields are 8-byte big-endian values, unless otherwise specified.
H0 is defined as the hash of an empty string, and Hi as a hash of the previous hash Hi-1 concatenated with THi. Hi is the hash of the blockchain state after applying the i-th transaction to the state:
- TH = Blake2b(s1||s2||...||sm),
- H0 = Blake2b('')
- Hn = Blake2b(Hn–1 || THn)
The generator does not include state snapshots in the block. To attest the authenticity of TSS, the generator includes the final Hn into the block header, where n is the sequence number of the last transaction in the block. Hn is called the block state hash.
This algorithm works well for Waves-NG: no hashes need to be recalculated when the new microblock is appended to the state. Just like other mutable liquid block fields (e.g. transactions root), block state hash changes when new microblocks are appended.
When receiving the state snapshot over the network, the nodes working in state snapshot application mode compute block state hash from the contents of the snapshot and check if the computed hash matches the hash from the block header. If the hashes don’t match, the node considers the state snapshot invalid and ignores it.
Even though the nodes working in such mode still perform block consensus validation, they end up trusting state snapshot producers as they don’t check if the state snapshot does indeed correspond to the block contents.
# Challenging State Hash
When a node receives a block from the generator, it validates all the transactions from the block and computes the state hash of its own. If the resulting hash does not match the one calculated by the original generator, the node can challenge the results, if block generation is enabled in node’s settings, and the configured generating accounts have sufficient generating balance.
- Challenger’s generating balance is combined with the original generator’s generating balance, and then block consensus parameters are recalculated. If the node has several generating accounts configured, consensus parameters (including block timestamp) are calculated for all accounts. The block is forged with the account which has the lowest block timestamp.
- The node forges a new block with the transactions from the original block and the same reference.
- Challenged block header is included in the new block header.
- The block is signed and broadcast to other peers. Since block timestamp is also recalculated and is likely to change, the node might have to wait before broadcasting the block.
This mechanism works for both ordinary blocks and microblocks: if state hash discrepancy is discovered in one of the microblocks, the node stops accepting subsequent microblocks from and immediately challenges this generator.
When a node receives a block with a challenge, it validates both the challenged block and the challenge consensus. If the challenge is valid, the malicious generator’s effective balance (and hence the generating balance for the following 1000 blocks) is considered to be 0. Instead of the original generator, the successful challenger receives the block reward.
Reduction of malicious generator’s generating balance helps to prevent them from re-attempting an attack. Resetting effective balance to 0 on the main chain will effectively disable the generator for the following 1000 blocks.
# Counteracting Malicious Challenges
Malicious generators may distribute invalid challenging blocks to their peers in several ways: broadcasting an alternative block at the top of the blockchain intending to replace a valid liquid block or providing a block in response to GetBlock
request during synchronization. In both cases such invalid blocks will not be challenged by the generator and will be discarded instead. Only an original block (containing no challenge of its own) can be challenged.
# Elided Transactions
Since challenging block consensus information is different from the original block, some transactions from the original block may become invalid.
- Suppose the original block generator’s account has the regular balance of 0 (i.e. all the generating balance comes from the incoming lease). The first transaction of the original block transfers the block reward from the generating account to a “safe”. Since the generator of the challenging block changes, this transaction will no longer be valid.
- Block timestamp and generator address are accessible from Ride scripts. Changes to these fields may alter script execution results and either cause the Invoke Script transaction to fail, or even invalidate the entire transaction due to a verifier script returning
false
.
In order to validate the transactions merkle root of the original block, the challenging block needs to include all of the transactions from the original block, so no transactions can be dropped, even though they may be invalid in the challenging block. Such invalid transactions are elided: they are included in the block, but produce an empty state snapshot. Elided transactions have the following properties:
- may only appear in the challenging block (transactions in ordinary blocks can never be elided)
- transaction ID uniqueness is still required
- elided transactions have an
applicationStatus
ofelided
/transactions/merkleProof
API endpoint returns proofs for elided transactionstransactionHeightById()
,transferTransactionById()
, andtransactionById()
Ride built-in functions returnunit
for elided transactions.
# VRF
VRF plays a key role in both consensus and application layer. Since VRF is re-calculated in a challenging block, it’s important to ensure the challenger is unable to exploit the challenge mechanism. As mentioned above, only an account with a minimum of 1000 WAVES of generating balance is allowed to challenge invalid state hashes. This requirement makes it impossible to “mine” the specific VRF value which would be used to game a dApp.
# Schema Changes
Two new fields are added to the block header schema:
bytes state_hash = 11;
ChallengedHeader challenged_header = 12;
The state_hash
field contains the root state hash for the current block.
The ChallengedHeader
message contains values from the block header which is being challenged:
message ChallengedHeader {
int64 base_target = 1;
bytes generation_signature = 2;
repeated uint32 feature_votes = 3;
int64 timestamp = 4;
int32 version = 5;
bytes generator = 6;
int64 reward_vote = 7;
bytes header_signature = 8;
}
The intention is to include all the fields required to reconstruct the original header, so that the signature could be validated and the authenticity of the header be established.
# Activation
The block schema has been changed after activation of feature #22 “Light Node”. Older nodes can not verify signatures of the newer blocks containing state hashes, and obviously can not append them. 1.4 nodes check for feature activation after the block had been appended. Since the new blocks are not appended, the nodes do not know the Light Node feature had already been activated, and can not shut down because of unsupported feature activation. This obvious problem could result in the 1.4 nodes being silently disconnected from the main chain.
To ensure that 1.4 nodes indeed stop once the feature is activated, altering block structure was delayed until 1000 blocks after the activation: generators began including state hashes at (activation height + 1000). 1000 is larger than maximum rollback depth (100), so a node running in the sync mode can still append the entire extension and see that unsupported feature had been activated.