Skip to content

Commit 76e6a31

Browse files
authored
Merge pull request #451 from OriginTrail/RFC-26-update
feat: Implement RFC-26 Stake-Adjusted Multi-Epoch Node Score Formula
2 parents ae0c23b + a8d0cb7 commit 76e6a31

15 files changed

Lines changed: 491 additions & 346 deletions

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,32 @@
77

88
This repository contains the smart contracts for OriginTrail V8, which serves as the core module for the Decentralized Knowledge Graph (DKG). The module handles various aspects, such as DKG Node profile management, Knowledge Assets ownership, consensus mechanisms, and others, in order to ensure the secure and efficient operation of the network. The contracts are written in Solidity and can be deployed on Ethereum and compatible networks.
99

10+
## Node Scoring Mechanism (RFC-26)
11+
12+
The DKG network uses a node scoring formula to determine reward distribution among participating nodes. The current implementation follows [RFC-26: Stake-Adjusted, Multi-Epoch Node Score Formula](https://github.com/OriginTrail/OT-RFC-repository/tree/main/RFCs/OT-RFC-26_Stake_Adjusted_Multi_Epoch_Node_Score_Formula).
13+
14+
### Formula
15+
16+
```
17+
nodeScore(t) = 0.04 × S(t) + 0.86 × P(t) + 0.6 × A(t) × P(t)
18+
```
19+
20+
Where:
21+
22+
| Factor | Description | Weight |
23+
|--------|-------------|--------|
24+
| **S(t)** - Stake Factor | `√(nodeStake / STAKE_CAP)` — Sublinear scaling using square root to reduce stake dominance | 4% |
25+
| **P(t)** - Publishing Factor | `K_n / K_total` — Node's share of knowledge production over a rolling 4-epoch window (epochs t-3, t-2, t-1, t) | 86% |
26+
| **A(t)** - Ask Alignment Factor | `1 - |nodeAsk - networkPrice| / networkPrice` — Rewards nodes whose ask price is close to the network reference price | 60% (multiplied with P(t)) |
27+
28+
### Key Design Principles
29+
30+
1. **Sublinear Stake Scaling**: Using square root reduces the dominance of large stakers while still rewarding stake commitment
31+
2. **Multi-Epoch Publishing**: Smooths publishing factor over 4 epochs to reduce gaming and reward consistent knowledge production
32+
3. **Ask Alignment**: Incentivizes competitive pricing by rewarding nodes whose ask prices align with network demand
33+
34+
For detailed rationale and mathematical derivations, see the [RFC-26 specification](https://github.com/OriginTrail/OT-RFC-repository/tree/main/RFCs/OT-RFC-26_Stake_Adjusted_Multi_Epoch_Node_Score_Formula).
35+
1036
## Repository Structure
1137

1238
This repository contains the following main components:

contracts/RandomSampling.sol

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {ParametersStorage} from "./storage/ParametersStorage.sol";
2121
import {ShardingTableStorage} from "./storage/ShardingTableStorage.sol";
2222
import {ICustodian} from "./interfaces/ICustodian.sol";
2323
import {HubLib} from "./libraries/HubLib.sol";
24+
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
2425

2526
contract RandomSampling is INamed, IVersioned, ContractStatus, IInitializable {
2627
string private constant _NAME = "RandomSampling";
@@ -410,47 +411,64 @@ contract RandomSampling is INamed, IVersioned, ContractStatus, IInitializable {
410411
}
411412

412413
/**
413-
* @dev Calculates the node score based on stake, ask price, and publishing activity
414-
* Score = nodeStakeFactor + nodeAskFactor + nodePublishingFactor
414+
* @dev Calculates the node score based on stake, publishing activity, and ask alignment
415+
* Implements RFC-26: Stake-Adjusted, Multi-Epoch Node Score Formula
415416
*
416-
* nodeStakeFactor: 2 * (nodeStake / maxStake)^2 - rewards higher stake
417-
* nodeAskFactor: (nodeStake/maxStake) * ((upperBound - nodeAsk) / (upperBound - lowerBound))^2 - rewards lower ask prices
418-
* nodePublishingFactor: nodeStakeFactor * (nodePublishing / maxNodePublishing) - rewards active publishers
417+
* Formula: nodeScore(t) = 0.04 * S(t) + 0.86 * P(t) + 0.6 * A(t) * P(t)
418+
*
419+
* Where:
420+
* - S(t) = sqrt(nodeStake / STAKE_CAP) - sublinear stake scaling
421+
* - P(t) = K_n / K_total - publishing share over 4 epochs (t-3, t-2, t-1, t)
422+
* - A(t) = 1 - |nodeAsk - networkPrice| / networkPrice - ask alignment factor
419423
*
420424
* All calculations use 18-decimal precision for accuracy
421425
* @param identityId The node identity to calculate score for
422426
* @return score18 The calculated node score scaled by 18-decimal for precision
423427
*/
424428
function calculateNodeScore(uint72 identityId) public view returns (uint256) {
425-
// 1. Node stake factor calculation
426-
// Formula: nodeStakeFactor = 2 * (nodeStake / maximumStake)^2
427-
uint256 maximumStake = uint256(parametersStorage.maximumStake());
429+
uint256 currentEpoch = chronos.getCurrentEpoch();
430+
431+
// 1. Stake factor S(t) = sqrt(nodeStake / stakeCap)
432+
// Using sublinear scaling to reduce stake dominance (RFC-26 Section 4.1)
433+
uint256 stakeCap = uint256(parametersStorage.maximumStake());
428434
uint256 nodeStake = uint256(stakingStorage.getNodeStake(identityId));
429-
nodeStake = nodeStake > maximumStake ? maximumStake : nodeStake;
430-
uint256 stakeRatio18 = (nodeStake * SCALE18) / maximumStake;
431-
uint256 nodeStakeFactor18 = (2 * stakeRatio18 * stakeRatio18) / SCALE18;
432-
433-
// 2. Node ask factor calculation
434-
// Formula: nodeStake * ((upperAskBound - nodeAsk) / (upperAskBound - lowerAskBound))^2 / maximumStake
435-
uint256 nodeAsk18 = uint256(profileStorage.getAsk(identityId)) * SCALE18;
436-
(uint256 askLowerBound18, uint256 askUpperBound18) = askStorage.getAskBounds();
437-
uint256 nodeAskFactor18;
438-
if (askUpperBound18 > askLowerBound18 && nodeAsk18 >= askLowerBound18 && nodeAsk18 <= askUpperBound18) {
439-
uint256 askDiffRatio18 = ((askUpperBound18 - nodeAsk18) * SCALE18) / (askUpperBound18 - askLowerBound18);
440-
nodeAskFactor18 = (stakeRatio18 * (askDiffRatio18 ** 2)) / (SCALE18 ** 2);
435+
nodeStake = nodeStake > stakeCap ? stakeCap : nodeStake;
436+
// S18 = sqrt((nodeStake / stakeCap) * SCALE18) * sqrt(SCALE18)
437+
uint256 stakeRatio18 = (nodeStake * SCALE18) / stakeCap;
438+
uint256 stakeFactor18 = Math.sqrt(stakeRatio18 * SCALE18);
439+
440+
// 2. Publishing factor P(t) = K_n / K_total over 4 epochs (RFC-26 Section 4.2)
441+
// Sum knowledge value over epochs (t-3, t-2, t-1, t)
442+
uint256 nodeKnowledgeValue = 0;
443+
uint256 totalKnowledgeValue = 0;
444+
uint256 startEpoch = currentEpoch >= 3 ? currentEpoch - 3 : 0;
445+
for (uint256 e = startEpoch; e <= currentEpoch; e++) {
446+
nodeKnowledgeValue += uint256(epochStorage.getNodeEpochProducedKnowledgeValue(identityId, e));
447+
totalKnowledgeValue += uint256(epochStorage.getEpochProducedKnowledgeValue(e));
441448
}
442-
443-
// 3. Node publishing factor calculation
444-
// Original: nodeStakeFactor * (nodePublishingFactor / MAX(allNodesPublishingFactors))
445-
uint256 maxNodePub = uint256(epochStorage.getCurrentEpochNodeMaxProducedKnowledgeValue());
446-
uint256 nodePublishingFactor18 = 0;
447-
if (maxNodePub > 0) {
448-
uint256 nodePub = uint256(epochStorage.getNodeCurrentEpochProducedKnowledgeValue(identityId));
449-
uint256 pubRatio18 = (nodePub * SCALE18) / maxNodePub;
450-
nodePublishingFactor18 = (nodeStakeFactor18 * pubRatio18) / SCALE18;
449+
uint256 publishingFactor18 = totalKnowledgeValue > 0 ? (nodeKnowledgeValue * SCALE18) / totalKnowledgeValue : 0;
450+
451+
// 3. Ask alignment factor A(t) = 1 - |nodeAsk - networkPrice| / networkPrice (RFC-26 Section 4.3)
452+
// Rewards nodes whose ask is close to the network reference price:
453+
// - Perfect alignment (deviation = 0): A(t) = 1.0 (maximum bonus)
454+
// - 50% deviation: A(t) = 0.5
455+
// - 100%+ deviation: A(t) = 0.0 (no bonus, capped to avoid negative values)
456+
uint256 nodeAsk = uint256(profileStorage.getAsk(identityId));
457+
uint256 networkPrice = askStorage.getPricePerKbEpoch();
458+
uint256 askAlignmentFactor18 = 0;
459+
if (networkPrice > 0) {
460+
uint256 deviation = nodeAsk > networkPrice ? nodeAsk - networkPrice : networkPrice - nodeAsk;
461+
uint256 deviationRatio18 = (deviation * SCALE18) / networkPrice;
462+
askAlignmentFactor18 = deviationRatio18 >= SCALE18 ? 0 : SCALE18 - deviationRatio18;
451463
}
452464

453-
return nodeStakeFactor18 + nodeAskFactor18 / 10 + nodePublishingFactor18 * 15;
465+
// nodeScore(t) = 0.04 * S(t) + 0.86 * P(t) + 0.6 * A(t) * P(t)
466+
// Coefficients: 0.04 = 4/100, 0.86 = 86/100, 0.6 = 60/100
467+
uint256 stakeComponent18 = (4 * stakeFactor18) / 100;
468+
uint256 publishingComponent18 = (86 * publishingFactor18) / 100;
469+
uint256 askPublishingComponent18 = (60 * askAlignmentFactor18 * publishingFactor18) / (100 * SCALE18);
470+
471+
return stakeComponent18 + publishingComponent18 + askPublishingComponent18;
454472
}
455473

456474
/**

contracts/storage/ParametersStorage.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ contract ParametersStorage is INamed, IVersioned, HubDependent {
4040

4141
constructor(address hubAddress, uint256 _v81ReleaseEpoch) HubDependent(hubAddress) {
4242
minimumStake = 50_000 ether;
43-
maximumStake = 5_000_000 ether;
43+
maximumStake = 10_000_000 ether;
4444

4545
stakeWithdrawalDelay = 28 days;
4646
nodeAskUpdateDelay = 1 days;

deployments/base_mainnet_contracts.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@
2424
"deployed": true
2525
},
2626
"ParametersStorage": {
27-
"evmAddress": "0x806A3376666D75b01B076DA85617f19b57e583F8",
27+
"evmAddress": "0x5a8263F9f65dB112e16243698DEF36ab744e19fe",
2828
"version": "1.0.0",
29-
"gitBranch": "main",
30-
"gitCommitHash": "2758c75e77dff9d160e30aa80bc5f383b8e4c5e0",
31-
"deploymentBlock": 35664091,
32-
"deploymentTimestamp": 1758117531764,
29+
"gitBranch": "RFC-26-update",
30+
"gitCommitHash": "90b2d522e4cc50b106487462ded7fa9e6a767c4b",
31+
"deploymentBlock": 41853387,
32+
"deploymentTimestamp": 1770496121800,
3333
"deployed": true
3434
},
3535
"WhitelistStorage": {
@@ -312,12 +312,12 @@
312312
"deployed": true
313313
},
314314
"RandomSampling": {
315-
"evmAddress": "0x46219162DFB560fb3B56d64D8cc01C550079eA7B",
315+
"evmAddress": "0x4B20D581695fb96db8FE78D8b12994fa43946eFA",
316316
"version": "1.0.0",
317-
"gitBranch": "main",
318-
"gitCommitHash": "2758c75e77dff9d160e30aa80bc5f383b8e4c5e0",
319-
"deploymentBlock": 35664094,
320-
"deploymentTimestamp": 1758117538445,
317+
"gitBranch": "RFC-26-update",
318+
"gitCommitHash": "90b2d522e4cc50b106487462ded7fa9e6a767c4b",
319+
"deploymentBlock": 41853400,
320+
"deploymentTimestamp": 1770496150937,
321321
"deployed": true
322322
},
323323
"MigratorV8TuningPeriodRewards": {

deployments/base_sepolia_test_contracts.json

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@
2020
"deployed": true
2121
},
2222
"ParametersStorage": {
23-
"evmAddress": "0x7ad79B9A2607Fd15c4fEf85412684B83473C7dBa",
23+
"evmAddress": "0xe772349234f405147875a5bc28D12D56375441BE",
2424
"version": "1.0.0",
25-
"gitBranch": "improvement/parameters-storage-access-policy",
26-
"gitCommitHash": "ca8b8ffaaf1961fa270fb7a1a5f187fcc5d45ff4",
27-
"deploymentBlock": 31132834,
28-
"deploymentTimestamp": 1758033973092,
25+
"gitBranch": "RFC-26-update",
26+
"gitCommitHash": "90b2d522e4cc50b106487462ded7fa9e6a767c4b",
27+
"deploymentBlock": 37268596,
28+
"deploymentTimestamp": 1770305485224,
2929
"deployed": true
3030
},
3131
"WhitelistStorage": {
@@ -281,12 +281,12 @@
281281
"deployed": true
282282
},
283283
"RandomSampling": {
284-
"evmAddress": "0x09553F8aDACf76D76Da49678555240cb614c36AF",
284+
"evmAddress": "0x95CF1b97f62F268804F4d82Bf1c98dEEFA8AF5A3",
285285
"version": "1.0.0",
286-
"gitBranch": "improvement/parameters-storage-access-policy",
287-
"gitCommitHash": "ca8b8ffaaf1961fa270fb7a1a5f187fcc5d45ff4",
288-
"deploymentBlock": 31132846,
289-
"deploymentTimestamp": 1758033980703,
286+
"gitBranch": "RFC-26-update",
287+
"gitCommitHash": "90b2d522e4cc50b106487462ded7fa9e6a767c4b",
288+
"deploymentBlock": 37268602,
289+
"deploymentTimestamp": 1770305493101,
290290
"deployed": true
291291
},
292292
"StakingKPI": {
@@ -297,6 +297,24 @@
297297
"deploymentBlock": 27547204,
298298
"deploymentTimestamp": 1750862701223,
299299
"deployed": true
300+
},
301+
"MigratorV8TuningPeriodRewards": {
302+
"evmAddress": "0x2823186A6BA9df3Dc788F6b1b73ec185a9895108",
303+
"version": "1.0.0",
304+
"gitBranch": "RFC-26-update",
305+
"gitCommitHash": "90b2d522e4cc50b106487462ded7fa9e6a767c4b",
306+
"deploymentBlock": 37268392,
307+
"deploymentTimestamp": 1770305073410,
308+
"deployed": true
309+
},
310+
"MigratorV6TuningPeriodRewards": {
311+
"evmAddress": "0xe4fF01a0cEC8172BC5f087655cC8a54aCFB1FDb1",
312+
"version": "1.0.0",
313+
"gitBranch": "RFC-26-update",
314+
"gitCommitHash": "90b2d522e4cc50b106487462ded7fa9e6a767c4b",
315+
"deploymentBlock": 37268394,
316+
"deploymentTimestamp": 1770305077619,
317+
"deployed": true
300318
}
301319
}
302320
}

deployments/gnosis_chiado_test_contracts.json

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@
2020
"deployed": true
2121
},
2222
"ParametersStorage": {
23-
"evmAddress": "0x4a652D864148f0e533247Fcfb7B7dC92c7B55e6e",
23+
"evmAddress": "0x30F5380BcC9443DCd219BE99906b04Fea23CA918",
2424
"version": "1.0.0",
25-
"gitBranch": "improvement/parameters-storage-access-policy",
26-
"gitCommitHash": "88b220af8f4ff70118a7eb1574d60458cf318b0f",
27-
"deploymentBlock": 17817446,
28-
"deploymentTimestamp": 1758033679341,
25+
"gitBranch": "RFC-26-update",
26+
"gitCommitHash": "90b2d522e4cc50b106487462ded7fa9e6a767c4b",
27+
"deploymentBlock": 19690390,
28+
"deploymentTimestamp": 1770306175937,
2929
"deployed": true
3030
},
3131
"WhitelistStorage": {
@@ -281,12 +281,12 @@
281281
"deployed": true
282282
},
283283
"RandomSampling": {
284-
"evmAddress": "0x3B39e292c3EC9088ceEf6629663857a679249ac4",
284+
"evmAddress": "0xe772349234f405147875a5bc28D12D56375441BE",
285285
"version": "1.0.0",
286-
"gitBranch": "improvement/parameters-storage-access-policy",
287-
"gitCommitHash": "88b220af8f4ff70118a7eb1574d60458cf318b0f",
288-
"deploymentBlock": 17817448,
289-
"deploymentTimestamp": 1758033688668,
286+
"gitBranch": "RFC-26-update",
287+
"gitCommitHash": "90b2d522e4cc50b106487462ded7fa9e6a767c4b",
288+
"deploymentBlock": 19690391,
289+
"deploymentTimestamp": 1770306185640,
290290
"deployed": true
291291
},
292292
"StakingKPI": {
@@ -306,6 +306,24 @@
306306
"deploymentBlock": 16357950,
307307
"deploymentTimestamp": 1750427976240,
308308
"deployed": true
309+
},
310+
"MigratorV8TuningPeriodRewards": {
311+
"evmAddress": "0x1ec0510C3b001396A275FeF00edcA96815B6F737",
312+
"version": "1.0.0",
313+
"gitBranch": "RFC-26-update",
314+
"gitCommitHash": "90b2d522e4cc50b106487462ded7fa9e6a767c4b",
315+
"deploymentBlock": 19690368,
316+
"deploymentTimestamp": 1770306011091,
317+
"deployed": true
318+
},
319+
"MigratorV6TuningPeriodRewards": {
320+
"evmAddress": "0x87Ffd0f8f2739644483A8B91b92A225CC555c852",
321+
"version": "1.0.0",
322+
"gitBranch": "RFC-26-update",
323+
"gitCommitHash": "90b2d522e4cc50b106487462ded7fa9e6a767c4b",
324+
"deploymentBlock": 19690369,
325+
"deploymentTimestamp": 1770306024120,
326+
"deployed": true
309327
}
310328
}
311329
}

deployments/gnosis_mainnet_contracts.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@
2020
"deployed": true
2121
},
2222
"ParametersStorage": {
23-
"evmAddress": "0x806A3376666D75b01B076DA85617f19b57e583F8",
23+
"evmAddress": "0x5a8263F9f65dB112e16243698DEF36ab744e19fe",
2424
"version": "1.0.0",
25-
"gitBranch": "main",
26-
"gitCommitHash": "2758c75e77dff9d160e30aa80bc5f383b8e4c5e0",
27-
"deploymentBlock": 42168992,
28-
"deploymentTimestamp": 1758117438273,
25+
"gitBranch": "RFC-26-update",
26+
"gitCommitHash": "90b2d522e4cc50b106487462ded7fa9e6a767c4b",
27+
"deploymentBlock": 44563699,
28+
"deploymentTimestamp": 1770496906829,
2929
"deployed": true
3030
},
3131
"WhitelistStorage": {
@@ -308,12 +308,12 @@
308308
"deployed": true
309309
},
310310
"RandomSampling": {
311-
"evmAddress": "0x46219162DFB560fb3B56d64D8cc01C550079eA7B",
311+
"evmAddress": "0x4B20D581695fb96db8FE78D8b12994fa43946eFA",
312312
"version": "1.0.0",
313-
"gitBranch": "main",
314-
"gitCommitHash": "2758c75e77dff9d160e30aa80bc5f383b8e4c5e0",
315-
"deploymentBlock": 42168994,
316-
"deploymentTimestamp": 1758117448794,
313+
"gitBranch": "RFC-26-update",
314+
"gitCommitHash": "90b2d522e4cc50b106487462ded7fa9e6a767c4b",
315+
"deploymentBlock": 44563701,
316+
"deploymentTimestamp": 1770496917390,
317317
"deployed": true
318318
},
319319
"MigratorV8TuningPeriodRewards": {

deployments/neuroweb_mainnet_contracts.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@
2222
"deployed": true
2323
},
2424
"ParametersStorage": {
25-
"evmAddress": "0x806A3376666D75b01B076DA85617f19b57e583F8",
26-
"substrateAddress": "5EMjsczmuX972YCQS442gWQeBunSVaeqJnrEv3a6u7LhZQtm",
25+
"evmAddress": "0x5a8263F9f65dB112e16243698DEF36ab744e19fe",
26+
"substrateAddress": "5EMjsczeJzkKuUJGtXcfA8suT4jKvT6kNfLRSAdmLvtn8wdc",
2727
"version": "1.0.0",
28-
"gitBranch": "main",
29-
"gitCommitHash": "2758c75e77dff9d160e30aa80bc5f383b8e4c5e0",
30-
"deploymentBlock": 11000160,
31-
"deploymentTimestamp": 1758117483074,
28+
"gitBranch": "RFC-26-update",
29+
"gitCommitHash": "90b2d522e4cc50b106487462ded7fa9e6a767c4b",
30+
"deploymentBlock": 12866728,
31+
"deploymentTimestamp": 1770497516969,
3232
"deployed": true
3333
},
3434
"WhitelistStorage": {
@@ -342,13 +342,13 @@
342342
"deployed": true
343343
},
344344
"RandomSampling": {
345-
"evmAddress": "0x46219162DFB560fb3B56d64D8cc01C550079eA7B",
346-
"substrateAddress": "5EMjsczaEAiNz51dxpTtYxtTrpzNQZT2xvbGLB2wUAqQH2JC",
345+
"evmAddress": "0x4B20D581695fb96db8FE78D8b12994fa43946eFA",
346+
"substrateAddress": "5EMjsczbEF4CypoB45vfPE9PK8Zb8A5CCa9uE2A4Jyq219s4",
347347
"version": "1.0.0",
348-
"gitBranch": "main",
349-
"gitCommitHash": "2758c75e77dff9d160e30aa80bc5f383b8e4c5e0",
350-
"deploymentBlock": 11000161,
351-
"deploymentTimestamp": 1758117488860,
348+
"gitBranch": "RFC-26-update",
349+
"gitCommitHash": "90b2d522e4cc50b106487462ded7fa9e6a767c4b",
350+
"deploymentBlock": 12866729,
351+
"deploymentTimestamp": 1770497522817,
352352
"deployed": true
353353
},
354354
"MigratorV8TuningPeriodRewards": {

0 commit comments

Comments
 (0)