Skip to content

Traffic-based App Rewards: Compute aggregateActivityTotals, add ComputeAppRewardTrigger, expose new endpoints#4420

Open
adetokunbo wants to merge 46 commits intoadetokunbo/feature-cip-104-scan-computationfrom
adetokunbo/cip-104-add-app-reward-computation-trigger
Open

Traffic-based App Rewards: Compute aggregateActivityTotals, add ComputeAppRewardTrigger, expose new endpoints#4420
adetokunbo wants to merge 46 commits intoadetokunbo/feature-cip-104-scan-computationfrom
adetokunbo/cip-104-add-app-reward-computation-trigger

Conversation

@adetokunbo
Copy link
Contributor

Fixes #4380

@meiersi-da: I replaced NoOpActivityComputation with FakeAppActivityComputation in 41b32f1. The Fake will be replaced on this feature branch once its rebased on the nearly-done TCS changes

Pull Request Checklist

Cluster Testing

  • If a cluster test is required, comment /cluster_test on this PR to request it, and ping someone with access to the DA-internal system to approve it.
  • If a hard-migration test is required (from the latest release), comment /hdm_test on this PR to request it, and ping someone with access to the DA-internal system to approve it.

PR Guidelines

  • Include any change that might be observable by our partners or affect their deployment in the release notes.
  • Specify fixed issues with Fixes #n, and mention issues worked on using #n
  • Include a screenshot for frontend-related PRs - see README or use your favorite screenshot tool

Merge Guidelines

  • Make the git commit message look sensible when squash-merging on GitHub (most likely: just copy your PR description).

Copy link
Contributor

@meiersi-da meiersi-da left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot. Sorry for the delay in the review.

There seems to have been a misunderstanding wrt featured app parties lookup. See my comments. Let's make another pass once the current comments have been addressed.

@adetokunbo adetokunbo changed the title Compute aggregateActivityTotals, add ComputeAppRewardTrigger, expose new endpoints Traffic-based App Rewards: Compute aggregateActivityTotals, add ComputeAppRewardTrigger, expose new endpoints Mar 18, 2026
@adetokunbo adetokunbo force-pushed the adetokunbo/cip-104-add-app-reward-computation-trigger branch 2 times, most recently from cac78b8 to bc955b6 Compare March 18, 2026 11:09
@adetokunbo
Copy link
Contributor Author

@meiersi-da PTAL, I've addressed the current set of comments

@adetokunbo adetokunbo requested a review from meiersi-da March 18, 2026 11:15
Copy link
Contributor

@meiersi-da meiersi-da left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot. We are getting closer. The code continues to require a high attention to detail. Not an easy one to write or review ;)

tasks <- (lastClosedO, earliestCompleteO) match {
case (Some((lastClosed, _)), Some(earliestComplete)) =>
appRewardsStore
.getNextRoundWithoutRootHash(updateHistory.historyId, lastClosed)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels off as a function. It seems to do too much.

Why not query the latest round for which the computation completed; and also query the latest round with complete data. Then the tasks available are all the round from max(earliestComplete, latestRoundWithRootHash + 1) to min(lastClosed, latestComplete) (with some adjustments for the optionals).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can then just pick the first 4 round numbers from this interval.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

"return activity totals after aggregation trigger runs" in { implicit _env =>
// Advance enough rounds so that at least one round is fully closed and aggregated.
// Round 0 requires multiple ticks to close.
for (_ <- 1 to 7) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need 7 rounds?

I'd expect that closing rounds 0 and 1 should be sufficient; i.e, you need to advance rounds two times.

Copy link
Contributor Author

@adetokunbo adetokunbo Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed the comment, that function increments by ticks not rounds.

@adetokunbo adetokunbo force-pushed the adetokunbo/cip-104-add-app-reward-computation-trigger branch from 066c3c1 to 6c01e33 Compare March 19, 2026 06:10
@adetokunbo adetokunbo requested a review from meiersi-da March 19, 2026 07:28
@adetokunbo
Copy link
Contributor Author

@meiersi-da: PTAL, most comments are addressed. I plan at least one more change; #4322 is now merged, so I will rebase on main and implement TODOs that can be now be resolved

Copy link
Contributor

@meiersi-da meiersi-da left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot. Please review the bugs and address them with tests that exhibit them before fixing them.

It's OK to not build a test for rewards being computed sequentially.

Comment on lines +25 to +26
* A round is complete if the prior round also has activity records,
* proving ingestion was running continuously through it.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copy-pasta

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

from #${Tables.appActivityRecords} aar
where exists(
select 1 from #${Tables.appActivityRecords}
where round_number = aar.round_number - 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check the query plan for this. This might be expensive to do that way.

If you do want to check the existence, then first query for the max and do a join against that single value, instead of potentially forcing an expensive join.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something along the lines of

select max_round
from
  (select max(round) - 1 
   from app_activity_records
   where history_id = $historyId
  )
where exists (
  select 1 
  from app_activity_records
  where history_id = $history_id and round = max_round
)

also: please add tests that surface the problem wrt missing check of history_id, and check all your queries wrt the missing comparison of history_id

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed; added, verifying with store tests that would have otherwise failed due to the missing historyId

override protected def isStaleTask(
task: RewardComputationTrigger.Task
)(implicit tc: TraceContext): Future[Boolean] =
Future.successful(false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is stale if the latest completed round is larger than the task round. Should not happen, but seems like the better definition.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented this.

lastClosedO <- store.lookupRoundOfLatestData()
earliestCompleteO <- appActivityStore.earliestRoundWithCompleteAppActivity()
latestCompleteO <- appActivityStore.latestRoundWithCompleteAppActivity()
latestComputedO <- appRewardsStore.lookupLatestRoundWithRewardComputation()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually turns out to be wrong when the trigger executes tasks in parallel and the backend gets restarted: round 2 might complete before round 1, and thus round 1 will never be completed.

So what I'd suggest to do for now is:

  1. return at most one task and thus ensure that they get solved one after the other
  2. create a TODO for an issue to compute tasks in parallel, which will require introducing a watermark table to remember the round number up to which all rewards have been computed. We might not need that. It will depend on how fast catchup works for the round trigger.

FYI: @rautenrieth-da -- catchup speed of reward computation is something to add to the test plan

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Opened #4570

def computeRewards(
roundNumber: Long
)(implicit tc: TraceContext): Future[Unit] =
aggregateActivityTotals(roundNumber)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prudent engineering: add safety check that all activity records have been ingested for the round in question. Better safe than sorry.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: added an assertion; verified with a store test

] = {
implicit val tc = extracted
withSpan(s"$workflowId.getRewardAccountingEarliestAvailableRound") { _ => _ =>
appRewardsStore.getEarliestRoundWithActivityTotals().map {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIR, the goal of the HTTP function is to enable other SVs to determine whether the earliest round for which this SV will eventually have the activity totals available. So we should return the earliest round for which there is a complete set of activity records and not the earliest one for which activity totals have been computed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect we actually have no need to retrieve the earliest round with activity totals, so we can remove that function from the ScanAppRewardsStore.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@adetokunbo adetokunbo force-pushed the adetokunbo/cip-104-add-app-reward-computation-trigger branch from 751ea69 to 8f12e5d Compare March 19, 2026 11:22
@adetokunbo adetokunbo changed the base branch from feature-cip-104-scan-computation to feature-cip-104-scan-computation-2 March 19, 2026 11:23
@adetokunbo adetokunbo changed the base branch from feature-cip-104-scan-computation-2 to adetokunbo/feature-cip-104-scan-computation March 23, 2026 03:05
@adetokunbo adetokunbo requested a review from meiersi-da March 23, 2026 10:31
@adetokunbo
Copy link
Contributor Author

@meiersi-da PTAL.

FYI: In 69a15c3, ScanStore.lookupRoundOfLatestData is removed as it is not not necessary due to the way the synchronization logic currently works; it is not replaced with any equivalent from ScanRewardsReferenceStore as there is nothing it uses from that store at the moment.

In the next PR of #4384 it will need obtain data from the appropriate OpenMiningRound, and this w

@adetokunbo adetokunbo force-pushed the adetokunbo/feature-cip-104-scan-computation branch from 1369090 to 0effc2c Compare March 23, 2026 10:48
@adetokunbo adetokunbo force-pushed the adetokunbo/cip-104-add-app-reward-computation-trigger branch 2 times, most recently from cad918f to 2ee47df Compare March 23, 2026 12:24
@adetokunbo adetokunbo force-pushed the adetokunbo/feature-cip-104-scan-computation branch from 0effc2c to 79c3627 Compare March 23, 2026 12:46
…tory_for_hash

Signed-off-by: Tim Emiola <adetokunbo@emio.la>
Signed-off-by: Tim Emiola <adetokunbo@emio.la>
Signed-off-by: Tim Emiola <adetokunbo@emio.la>
adetokunbo and others added 28 commits March 23, 2026 12:58
Co-authored-by: Simon Meier <simon@digitalasset.com>
Signed-off-by: Timothy Emiola <adetokunbo@users.noreply.github.com>
Signed-off-by: Tim Emiola <adetokunbo@emio.la>
Signed-off-by: Tim Emiola <adetokunbo@emio.la>
Signed-off-by: Tim Emiola <adetokunbo@emio.la>
Signed-off-by: Tim Emiola <adetokunbo@emio.la>
Signed-off-by: Tim Emiola <adetokunbo@emio.la>
Signed-off-by: Tim Emiola <adetokunbo@emio.la>
Co-authored-by: Simon Meier <simon@digitalasset.com>
Signed-off-by: Timothy Emiola <adetokunbo@users.noreply.github.com>
- change the condition so that two consecutive rounds have to have activity

Signed-off-by: Tim Emiola <adetokunbo@emio.la>
Signed-off-by: Tim Emiola <adetokunbo@emio.la>
Signed-off-by: Tim Emiola <adetokunbo@emio.la>
Signed-off-by: Tim Emiola <adetokunbo@emio.la>
Signed-off-by: Tim Emiola <adetokunbo@emio.la>
Co-authored-by: Simon Meier <simon@digitalasset.com>
Signed-off-by: Timothy Emiola <adetokunbo@users.noreply.github.com>
Co-authored-by: Simon Meier <simon@digitalasset.com>
Signed-off-by: Timothy Emiola <adetokunbo@users.noreply.github.com>
Co-authored-by: Simon Meier <simon@digitalasset.com>
Signed-off-by: Timothy Emiola <adetokunbo@users.noreply.github.com>
Co-authored-by: Simon Meier <simon@digitalasset.com>
Signed-off-by: Timothy Emiola <adetokunbo@users.noreply.github.com>
Signed-off-by: Tim Emiola <adetokunbo@emio.la>
Signed-off-by: Tim Emiola <adetokunbo@emio.la>
Signed-off-by: Tim Emiola <adetokunbo@emio.la>
Signed-off-by: Tim Emiola <adetokunbo@emio.la>
- use the historyId and add unittests that verify historyId isolation
- use a more efficient query, uses the index better

Signed-off-by: Tim Emiola <adetokunbo@emio.la>
Signed-off-by: Tim Emiola <adetokunbo@emio.la>
Signed-off-by: Tim Emiola <adetokunbo@emio.la>
Signed-off-by: Tim Emiola <adetokunbo@emio.la>
Signed-off-by: Tim Emiola <adetokunbo@emio.la>
…ctivity records

Signed-off-by: Tim Emiola <adetokunbo@emio.la>
@adetokunbo adetokunbo force-pushed the adetokunbo/cip-104-add-app-reward-computation-trigger branch from 2ee47df to 94e18ca Compare March 23, 2026 13:02
Copy link
Contributor

@meiersi-da meiersi-da left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot. It looks like only some small polish is required.

}

"return activity totals after aggregation trigger runs" in { implicit _env =>
// TODO(#4118): Update the time management here
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This issue is closed: #4118

Seems like the wrong reference.

case None =>
ScanResource.GetRewardAccountingActivityTotalsResponse.NotFound(
ErrorResponse(
s"Activity totals not yet computed for round $roundNumber"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
s"Activity totals not yet computed for round $roundNumber"
s"Activity totals not (yet) computed for round $roundNumber"

it might be never


/** Trigger that drives the CIP-0104 reward computation pipeline via
* ScanAppRewardsStore.computeRewards, which will eventually run three
* idempotent steps in one transaction:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* idempotent steps in one transaction:
* computation steps in one transaction:

): Future[Option[Long]]

/** Find the latest round for which all app activity records have been ingested.
* Returns None if fewer than two consecutive rounds have been ingested.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Returns None if fewer than two consecutive rounds have been ingested.

Let's not leak implementation details on the API. We don't want callers to rely on it, and break when the implementation changes.

* MUST only be called on rounds for which all app activity records have
* been ingested and for which the reward information has not yet been computed.
*/
def computeRewards(roundNumber: Long)(implicit
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def computeRewards(roundNumber: Long)(implicit
def computeAndStoreRewards(roundNumber: Long)(implicit

required:
- round_number
- total_app_activity_weight
- active_parties_count
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rautenrieth-da : wdyt about also exposing the number of activity records in the round? Seems like very useful debugging info.

def computeRewards(
roundNumber: Long
)(implicit tc: TraceContext): Future[Unit] =
aggregateActivityTotals(roundNumber)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rautenrieth-da : wdyt about adding counters for:

  • number of parties with activity
  • number of activity records summarized
  • number of parties with rewards
  • number of batches created

tagged with synchronizer-id and migration-id.

It seems that having dashboards for these would seem valuable to observe the proper working of reward computation. However, I'm lacking the ops experience to judge their usefulness.

@adetokunbo : If we do so, then I'd still do that in a separate PR. Just create an issue from this comment.

} yield ()

/** Unnest per-verdict activity arrays and aggregate weights by party. */
private def unnestAndAggregate(historyId: Long, roundNumber: Long) =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rautenrieth-da : could I ask you to review these DB queries from a maintainers perspective? They look fine to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scan app: Traffic-based App Rewards: compute aggregateActivityTotals; add access via a new endpoint

2 participants