Skip to main content
Tessellation distributes rewards at the end of each consensus round triggered by a TimeTrigger. By default, currency metagraphs inherit the global L0 reward schedule. You can replace this with custom logic by implementing the Rewards trait.

The Rewards trait

// node-shared/domain/rewards/Rewards.scala
trait Rewards[F[_], P <: StateProof, S <: IncrementalSnapshot[P], E] {
  def distribute(
    lastArtifact: Signed[S],
    lastBalances: SortedMap[Address, Balance],
    acceptedTransactions: SortedSet[Signed[Transaction]],
    trigger: ConsensusTrigger,
    events: Set[E],
    maybeCalculatedState: Option[DataCalculatedState] = None
  ): F[SortedSet[RewardTransaction]]
}
For a currency metagraph, the concrete type parameters are:
  • PCurrencySnapshotStateProof
  • SCurrencyIncrementalSnapshot
  • ECurrencySnapshotEvent

Consensus triggers

Rewards are only meaningful on TimeTrigger rounds. EventTrigger rounds happen when there are pending events but no epoch boundary — returning an empty set on those is the standard pattern:
import io.constellationnetwork.node.shared.infrastructure.consensus.trigger.{ConsensusTrigger, EventTrigger, TimeTrigger}

override def distribute(
  lastArtifact: Signed[CurrencyIncrementalSnapshot],
  lastBalances: SortedMap[Address, Balance],
  acceptedTransactions: SortedSet[Signed[Transaction]],
  trigger: ConsensusTrigger,
  events: Set[CurrencySnapshotEvent],
  maybeCalculatedState: Option[DataCalculatedState] = None
): IO[SortedSet[RewardTransaction]] =
  trigger match {
    case EventTrigger => IO.pure(SortedSet.empty[RewardTransaction])
    case TimeTrigger  => computeRewards(lastArtifact, lastBalances)
  }
This mirrors the global L0 reward implementation:
// dag-l0/infrastructure/rewards/Rewards.scala
def distribute(
  lastArtifact: Signed[GlobalIncrementalSnapshot],
  lastBalances: SortedMap[Address, Balance],
  acceptedTransactions: SortedSet[Signed[Transaction]],
  trigger: ConsensusTrigger,
  events: Set[GlobalSnapshotEvent],
  maybeCalculatedState: Option[DataCalculatedState] = None
): F[SortedSet[RewardTransaction]] = {
  val facilitators = lastArtifact.proofs.map(_.id)
  trigger match {
    case EventTrigger => SortedSet.empty[RewardTransaction].pure[F]
    case TimeTrigger  => mintedDistribution(lastArtifact.epochProgress, facilitators)
  }
}

Epoch progress

The lastArtifact.epochProgress field tells you which epoch this round belongs to. Use it to implement epoch-based reward schedules:
import io.constellationnetwork.schema.epoch.EpochProgress
import io.constellationnetwork.schema.transaction.{RewardTransaction, TransactionAmount}
import io.constellationnetwork.schema.balance.Amount

def rewardAmountForEpoch(epoch: EpochProgress): Amount = {
  // Halve rewards every 1,000,000 epochs
  val halvings = (epoch.value / 1_000_000L).toInt
  Amount(initialReward.value >> halvings)
}

Accessing data application state

If your metagraph has a data application, the current DataCalculatedState is passed as maybeCalculatedState. Use it to weight rewards based on participation:
override def distribute(
  lastArtifact: Signed[CurrencyIncrementalSnapshot],
  lastBalances: SortedMap[Address, Balance],
  acceptedTransactions: SortedSet[Signed[Transaction]],
  trigger: ConsensusTrigger,
  events: Set[CurrencySnapshotEvent],
  maybeCalculatedState: Option[DataCalculatedState] = None
): IO[SortedSet[RewardTransaction]] =
  trigger match {
    case EventTrigger => IO.pure(SortedSet.empty)
    case TimeTrigger =>
      maybeCalculatedState match {
        case Some(state: MyCalculatedState) => distributeByParticipation(state, lastBalances)
        case _                              => IO.pure(SortedSet.empty)
      }
  }

Implementing a custom distributor

import cats.effect.IO
import cats.syntax.all._

import scala.collection.immutable.{SortedMap, SortedSet}

import io.constellationnetwork.currency.dataApplication.DataCalculatedState
import io.constellationnetwork.currency.schema.currency.{CurrencyIncrementalSnapshot, CurrencySnapshotEvent, CurrencySnapshotStateProof}
import io.constellationnetwork.node.shared.domain.rewards.Rewards
import io.constellationnetwork.node.shared.infrastructure.consensus.trigger.{ConsensusTrigger, EventTrigger, TimeTrigger}
import io.constellationnetwork.schema.address.Address
import io.constellationnetwork.schema.balance.{Amount, Balance}
import io.constellationnetwork.schema.transaction.{RewardTransaction, Transaction, TransactionAmount}
import io.constellationnetwork.security.signature.Signed

import eu.timepit.refined.refineV
import eu.timepit.refined.numeric.Positive

object MyRewards {
  def make[F[_]: Async]: Rewards[F, CurrencySnapshotStateProof, CurrencyIncrementalSnapshot, CurrencySnapshotEvent] =
    new Rewards[F, CurrencySnapshotStateProof, CurrencyIncrementalSnapshot, CurrencySnapshotEvent] {

      private val rewardPerRound: Long = 100_000_000L // in datum units

      override def distribute(
        lastArtifact: Signed[CurrencyIncrementalSnapshot],
        lastBalances: SortedMap[Address, Balance],
        acceptedTransactions: SortedSet[Signed[Transaction]],
        trigger: ConsensusTrigger,
        events: Set[CurrencySnapshotEvent],
        maybeCalculatedState: Option[DataCalculatedState] = None
      ): F[SortedSet[RewardTransaction]] =
        trigger match {
          case EventTrigger => SortedSet.empty[RewardTransaction].pure[F]
          case TimeTrigger =>
            // Distribute equally to all facilitators that signed the last snapshot
            val facilitators: List[Address] = lastArtifact.proofs.map(_.id).toList.map(_.toAddress)
            val perFacilitator = rewardPerRound / Math.max(facilitators.size.toLong, 1L)

            facilitators
              .flatMap { address =>
                refineV[Positive](perFacilitator).toOption.map { amount =>
                  RewardTransaction(address, TransactionAmount(amount))
                }
              }
              .toSortedSet
              .pure[F]
        }
    }
}

Registering the rewards implementation

Return your implementation from the rewards extension point in your CurrencyL0App:
object Main extends CurrencyL0App(...) {
  override def rewards(
    implicit sp: SecurityProvider[IO]
  ): Option[Rewards[IO, CurrencySnapshotStateProof, CurrencyIncrementalSnapshot, CurrencySnapshotEvent]] =
    Some(MyRewards.make[IO])
}
The total amount of RewardTransaction values you return must not exceed the epoch’s minted amount. If your distribution mints tokens that don’t exist in the supply schedule, the global L0 snapshot validation will reject the currency snapshot.

Reward transaction type

case class RewardTransaction(destination: Address, amount: TransactionAmount)
Reward transactions are included directly in the currency snapshot and credited to the destination addresses when the snapshot is accepted by global L0.

Build docs developers (and LLMs) love