Validator Attestation
After a proposer produces block N, validators verify the block independently and broadcast an attestation — an Ed25519 signature over the block's canonical hash. Once a quorum of attestations is collected, the block is finalized and validators receive ATTESTATION_REWARD txs.
Validator set
The active validator set = addresses that:
- Have attested to the previous K blocks (defaults: K = 50)
- Hold a balance ≥ minimum stake (currently 1 OMBRA)
- Have not been slashed for double-signing
Selection is round-robin ordered by attestation count then by address (lexicographic tiebreak) — fully deterministic.
Attestation message
attestationMessage = sha256(blockHash)
signature = Ed25519.sign(validator.privateKey, attestationMessage)
The attestation is broadcast as a P2P message (not an on-chain tx). The proposer of block N+1 aggregates them and includes the list in the block header.
Pseudo-code: validator loop
function validatorLoop():
while True:
block = waitForNewBlock()
// 1. Verify proposer signature
if not Ed25519.verify(block.proposerPublicKey, block.hash, block.signature):
continue
// 2. Verify every tx signature + state transition
for tx in block.transactions:
assert Ed25519.verify(tx.publicKey, tx.hash, tx.signature)
assert deriveAddress(tx.publicKey) == tx.from
simulateApplyTx(tx, state) // would-throw if invalid
// 3. Verify BURN tx matches formula
validateBlockBurn(block, state)
// 4. Sign + broadcast attestation
attestation = sign(myWallet.privateKey, sha256(block.hash))
broadcastP2P({
type: "ATTESTATION",
blockIndex: block.index,
blockHash: block.hash,
validatorAddress: myWallet.address,
signature: attestation,
})
Pseudo-code: reward distribution
When block N+1 is proposed, the proposer includes one ATTESTATION_REWARD tx per validator who attested to block N:
function buildAttestationRewards(attestations, blockIndex, proposerWallet):
rewards = []
for att in attestations:
rewards.append(buildAttestationRewardTx(
from = proposerWallet.address,
validatorAddress = att.validatorAddress,
amount = VALIDATOR_BASE_REWARD, // 100 micro
blockIndex = blockIndex,
attestationHash = sha256(att.blockHash),
nonce = nextNonce(proposerWallet),
privateKey = proposerWallet.privateKey,
))
return rewards
Constants
| Constant | Value | Meaning |
|---|---|---|
VALIDATOR_BASE_REWARD | 100 micro | 0.0001 OMBRA per attestation |
ATTESTATION_QUORUM | > 50% of active validators | Required for block finality |
K_RECENT_BLOCKS | 50 | Window for validator eligibility |
MIN_VALIDATOR_STAKE | 1_000_000 micro | 1 OMBRA |
Slashing
A validator that signs two conflicting attestations for the same block index is slashed:
slashAmount = validator.balance * 0.10 // 10%
state.accounts[validator.address].balance -= slashAmount
state.totalBurned += slashAmount
state.validators[validator.address].slashedAt = blockIndex
// validator removed from active set for next K blocks
Detection happens when both attestation messages are observed in P2P gossip; any node can submit a SLASH evidence tx (though typically the next proposer includes it).
Why not BFT consensus
OmbraChain uses optimistic finality: once attestation quorum is reached, the block is treated as final by all honest nodes. There's no explicit 2-round vote (pre-vote + commit) like Tendermint. The trade-off:
- ✅ Simpler implementation, lower latency
- ⚠ Requires honest majority — a 51% attack of validators could finalize a fork
- ✅ Slashing makes the attack expensive (10% of total validator stake)
For production threat models requiring full BFT, future forks may introduce explicit voting rounds.
Next: Tokenomics →