Skip to main content

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:

  1. Have attested to the previous K blocks (defaults: K = 50)
  2. Hold a balance ≥ minimum stake (currently 1 OMBRA)
  3. 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

ConstantValueMeaning
VALIDATOR_BASE_REWARD100 micro0.0001 OMBRA per attestation
ATTESTATION_QUORUM> 50% of active validatorsRequired for block finality
K_RECENT_BLOCKS50Window for validator eligibility
MIN_VALIDATOR_STAKE1_000_000 micro1 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 →