📄 ScientistStorage.sol
D-OPEN SOVEREIGN
Documentation & Insights
@dev Structure to store each entry in the heap
We pack both hash and score together for efficient storage@dev Structure to hold heap data for each stats category
Each combination of (ActionType, objectType, monthId) has its own heap@dev Returns the number of months elapsed since contract deployment.@dev Returns the number of months elapsed since contract deployment.@dev Updates or inserts a score for a given hash in a specific stats category
This is the core heap management function
Time Complexity: O(log n) where n <= 500
Gas Cost: ~15,000-25,000 gas depending on operation
@param statsKey The stats category key (shortHash)
@param hash The unique identifier for this entry
@param newScore The new score to set@dev Inserts a new entry when heap is not full
@param statsKey The stats category key
@param hash The hash to insert
@param score The score to insert@dev Replaces the minimum element (root) with a new entry
This is called when heap is full and new score beats minimum
@param statsKey The stats category key
@param hash The new hash to insert
@param score The new score to insert@dev Moves an element up the heap until heap property is restored
Used when an element's score increases (becomes smaller in min-heap context)
In a min-heap, parent must be <= children
We bubble up when current element is smaller than its parent
@param statsKey The stats category key
@param index The index of element to bubble up@dev Moves an element down the heap until heap property is restored
Used when an element's score decreases (becomes larger in min-heap context)
We bubble down when current element is larger than its children
@param statsKey The stats category key
@param index The index of element to bubble down@dev Swaps two elements in the heap and updates the hash-to-index mapping
@param statsKey The stats category key
@param i First index
@param j Second index@dev Main function to process batch of compressed actions and update statistics
This is called by the Factory contract when users submit data@dev Check if a specific hash is in the top-500 for a stats category
@param action The action type
@param objectType The object type
@param monthId The month ID
@param hash The hash to check
@return true if hash is in top-500, false otherwise1/*
2 * Copyright (c) 2026 DataPond D-Library Pty Ltd. ("The Sanctuary")
3 * Non-Profit Public Library — The World Library
4 *
5 * D-SAFE Certification issued by POND Enterprise.
6 * Published under the D-Open Code Sovereign Licence v1.0 framework
7 * DataPond D-Library All exclusive Right reserved on modifiying the code - CC Attribution to data pond, Non modifiable, Non Commercial.
8 * https://registry.world.bibliotech.com/licence
9 * Source code donated to DataPond D-Library by it's author: data pond.
10 *
11 * Technical Guardian ("The Shield"): Pond Enterprise Pty Ltd. (ACN 694 747 987)
12 * All liability claims about D-SAFE certification issues coming from the published D-Safe direction must be addressed with Pond Enterprise Pty Ltd.
13 * Code Modification automatically voids the certified liability protection provided by Pond Enterprise.
14 */
15
16// SPDX-License-Identifier: Apache-2.0
17pragma solidity ^0.8.24;
18
19import {DateUtils} from "./DateUtils.sol";
20import {ErrorLibrary} from "./ErrorLibrary.sol";
21import {IScientistStorage} from "./IScientistStorage.sol";
22
23contract ScientistStorage is IScientistStorage {
24 enum ActionType {
25 Visit,
26 VisitPart, // partId
27 Download,
28 Rate, // rateId
29 RatePart, // partId + rateId
30 Share,
31 SharePart, // partId
32 EditNumberProp, // propId, propValue
33 EditTextProp, // propId, propValue
34 EditBooleanProp,
35 CreateRecord,
36 RelationAddTo,
37 RelationRemoveFrom,
38 LocationWrite
39 }
40
41 mapping(address => bool) public _isAuthorized; // contracts allowed to call write methods
42 address public admin; // single admin or multi-sig if you choose
43
44 event AuthorizedAdded(address indexed caller);
45 event AuthorizedRemoved(address indexed caller);
46
47 modifier onlyAdmin() {
48 require(msg.sender == admin, "not admin");
49 _;
50 }
51
52 modifier onlyAuthorized() {
53 require(_isAuthorized[msg.sender], "not authorized");
54 _;
55 }
56
57 modifier only() {
58 require(
59 msg.sender == admin || _isAuthorized[msg.sender],
60 "not admin and not authorized"
61 );
62 _;
63 }
64
65 function isAuthorized(address caller) external view returns (bool) {
66 require(caller != address(0), "zero addr");
67 return _isAuthorized[caller];
68 }
69
70 function addAuthorized(address caller) external onlyAdmin {
71 require(caller != address(0), "zero addr");
72 _isAuthorized[caller] = true;
73 emit AuthorizedAdded(caller);
74 }
75
76 function removeAuthorized(address caller) external onlyAdmin {
77 _isAuthorized[caller] = false;
78 emit AuthorizedRemoved(caller);
79 }
80
81 // ========================== TOP500 HEAP IMPLEMENTATION ==========================
82
83 // Maximum size of our leaderboard
84 uint16 private constant MAX_SIZE = 500;
85
86 /**
87 * @dev Structure to store each entry in the heap
88 * We pack both hash and score together for efficient storage
89 */
90 struct HeapNode {
91 uint64 hash; // The unique identifier/hash
92 uint32 score; // The score associated with this hash
93 }
94
95 /**
96 * @dev Structure to hold heap data for each stats category
97 * Each combination of (ActionType, objectType, monthId) has its own heap
98 */
99 struct Top500Data {
100 HeapNode[MAX_SIZE] heap; // The actual heap array
101 uint16 heapSize; // Current number of elements
102 mapping(uint64 => uint16) hashToIndex; // Hash to heap index mapping
103 }
104
105 // ========================== SCIENTIST STORAGE ==========================
106
107 // Main mapping that stores all scores (not just top 500)
108 mapping(uint64 => uint32) public counters;
109
110 // Each stats category gets its own Top500 heap
111 // A key is defined by ActionType, ObjectType and MonthId
112 // Key: shortHash = paramsToHashShort(action, objectType, monthId)
113 // Value: Top500Data containing the heap for that category
114 mapping(uint32 => Top500Data) private stats;
115
116 uint16 public deploymentYear;
117 uint8 public deploymentMonth;
118
119 // Constructor to capture deployment year/month (year is last two digits)
120 constructor() {
121 admin = msg.sender;
122 (uint16 y, uint8 m) = DateUtils.getCurrentYearAndMonth();
123 deploymentYear = y;
124 deploymentMonth = m;
125 }
126
127 /**
128 * @dev Returns the number of months elapsed since contract deployment.
129 */
130 function getLastMonthId() public view returns (uint16) {
131 (uint16 curYear3, uint8 curMonth) = DateUtils.getCurrentYearAndMonth();
132 return getNbMonthsSince(curYear3, curMonth);
133 }
134
135 function getDeploymentDate() public view returns (uint16, uint8) {
136 return (deploymentYear, deploymentMonth);
137 }
138
139 /**
140 * @dev Returns the number of months elapsed since contract deployment.
141 */
142 function getNbMonthsSince(
143 uint16 year,
144 uint8 months
145 ) internal view returns (uint16) {
146 uint16 start = deploymentYear * 12 + deploymentMonth; // deploymentYear already stores last two digits
147 uint16 nowM = year * 12 + months;
148 if (nowM < start) {
149 revert ErrorLibrary.NbMonthsSinceOverflow();
150 }
151 return nowM - start;
152 }
153 // ========================== HASH FUNCTIONS ==========================
154 function paramsToHashShort(
155 ActionType action,
156 uint8 objectType,
157 uint16 monthId
158 ) internal pure returns (uint32) {
159 // Pack parameters into a single 32-bit hash for stats categorization
160 return (uint32(uint8(action)) |
161 (uint32(objectType) << 8) |
162 (uint32(monthId) << 16));
163 }
164 function hashToParamsShort(
165 uint32 hash
166 )
167 internal
168 pure
169 returns (ActionType action, uint8 objectType, uint16 objectId)
170 {
171 // Extract the parameters from the hash using bit masking and shifting
172 action = ActionType(uint8(hash & 0xFF)); // Extract the lowest 8 bits
173 objectType = uint8((hash >> 8) & 0xFF); // Extract bits 8-15
174 objectId = uint16((hash >> 16) & 0xFFFF); // Extract bits 16-31
175 return (action, objectType, objectId);
176 }
177 function hashToParams(
178 uint64 hash
179 )
180 internal
181 pure
182 returns (
183 ActionType action,
184 uint8 objectType,
185 uint16 monthId,
186 uint16 objectId
187 )
188 {
189 // Extract the parameters from the hash using bit masking and shifting
190 action = ActionType(uint8(hash & 0xFF)); // Extract the lowest 8 bits
191 objectType = uint8((hash >> 8) & 0xFF); // Extract bits 8-15
192 monthId = uint16((hash >> 16) & 0xFFFF); // Extract bits 16-31
193 objectId = uint16((hash >> 32) & 0xFFFF); // Extract bits 32-47
194 return (action, objectType, monthId, objectId);
195 }
196 function paramsToHash(
197 ActionType action,
198 uint8 objectType,
199 uint16 monthId,
200 uint16 objectId
201 ) internal pure returns (uint64) {
202 // Pack all parameters into a single 64-bit hash for unique identification
203 return (uint64(uint8(action)) |
204 (uint64(objectType) << 8) |
205 (uint64(monthId) << 16) |
206 (uint64(objectId) << 32));
207 }
208 // ========================== TOP500 HEAP INTERNAL FUNCTIONS ==========================
209 /**
210 * @dev Updates or inserts a score for a given hash in a specific stats category
211 * This is the core heap management function
212 *
213 * Time Complexity: O(log n) where n <= 500
214 * Gas Cost: ~15,000-25,000 gas depending on operation
215 *
216 * @param statsKey The stats category key (shortHash)
217 * @param hash The unique identifier for this entry
218 * @param newScore The new score to set
219 */
220 function _updateScore(
221 uint32 statsKey,
222 uint64 hash,
223 uint32 newScore
224 ) internal {
225 Top500Data storage heapData = stats[statsKey];
226 // Check if this hash already exists in our top-500 heap
227 uint16 existingIndex = heapData.hashToIndex[hash];
228 bool existsInHeap = (existingIndex < heapData.heapSize &&
229 heapData.heap[existingIndex].hash == hash);
230 if (existsInHeap) {
231 // Case 1: Hash already in top-500, update its score
232 uint32 oldScore = heapData.heap[existingIndex].score;
233 heapData.heap[existingIndex].score = newScore;
234 if (newScore > oldScore) {
235 // Score increased - bubble up towards root (decrease key)
236 _bubbleUp(statsKey, existingIndex);
237 } else if (newScore < oldScore) {
238 // Score decreased - bubble down towards leaves (increase key)
239 _bubbleDown(statsKey, existingIndex);
240 }
241 // If scores are equal, no heap property violation
242 } else {
243 // Case 2: Hash not in top-500
244 if (heapData.heapSize < MAX_SIZE) {
245 // Heap not full - simply add the new entry
246 _insertNewEntry(statsKey, hash, newScore);
247 } else {
248 // Heap is full - check if new score beats the minimum (root)
249 if (newScore > heapData.heap[0].score) {
250 // New score qualifies for top-500
251 // Remove the old minimum and insert new entry
252 _replaceMinimum(statsKey, hash, newScore);
253 }
254 // If newScore <= heap[0].score, it doesn't qualify, do nothing
255 }
256 }
257 }
258
259 /**
260 * @dev Inserts a new entry when heap is not full
261 * @param statsKey The stats category key
262 * @param hash The hash to insert
263 * @param score The score to insert
264 */
265 function _insertNewEntry(
266 uint32 statsKey,
267 uint64 hash,
268 uint32 score
269 ) internal {
270 Top500Data storage heapData = stats[statsKey];
271 // Add new element at the end of heap
272 uint16 newIndex = heapData.heapSize;
273 heapData.heap[newIndex] = HeapNode(hash, score);
274 heapData.hashToIndex[hash] = newIndex;
275 heapData.heapSize++;
276 // Restore heap property by bubbling up
277 _bubbleUp(statsKey, newIndex);
278 }
279
280 /**
281 * @dev Replaces the minimum element (root) with a new entry
282 * This is called when heap is full and new score beats minimum
283 * @param statsKey The stats category key
284 * @param hash The new hash to insert
285 * @param score The new score to insert
286 */
287 function _replaceMinimum(
288 uint32 statsKey,
289 uint64 hash,
290 uint32 score
291 ) internal {
292 Top500Data storage heapData = stats[statsKey];
293 // Remove the old minimum from hashToIndex mapping
294 delete heapData.hashToIndex[heapData.heap[0].hash];
295 // Replace root with new entry
296 heapData.heap[0] = HeapNode(hash, score);
297 heapData.hashToIndex[hash] = 0;
298 // Restore heap property by bubbling down from root
299 _bubbleDown(statsKey, 0);
300 }
301
302 /**
303 * @dev Moves an element up the heap until heap property is restored
304 * Used when an element's score increases (becomes smaller in min-heap context)
305 *
306 * In a min-heap, parent must be <= children
307 * We bubble up when current element is smaller than its parent
308 *
309 * @param statsKey The stats category key
310 * @param index The index of element to bubble up
311 */
312 function _bubbleUp(uint32 statsKey, uint16 index) internal {
313 Top500Data storage heapData = stats[statsKey];
314 // Continue until we reach root or heap property is satisfied
315 while (index > 0) {
316 uint16 parentIndex = (index - 1) / 2; // Parent is at (i-1)/2
317 // If current element >= parent, heap property is satisfied
318 if (
319 heapData.heap[index].score >= heapData.heap[parentIndex].score
320 ) {
321 break;
322 }
323 // Swap current element with parent
324 _swap(statsKey, index, parentIndex);
325 // Move up to parent position
326 index = parentIndex;
327 }
328 }
329
330 /**
331 * @dev Moves an element down the heap until heap property is restored
332 * Used when an element's score decreases (becomes larger in min-heap context)
333 *
334 * We bubble down when current element is larger than its children
335 *
336 * @param statsKey The stats category key
337 * @param index The index of element to bubble down
338 */
339 function _bubbleDown(uint32 statsKey, uint16 index) internal {
340 Top500Data storage heapData = stats[statsKey];
341 while (true) {
342 uint16 leftChild = 2 * index + 1; // Left child at 2*i + 1
343 uint16 rightChild = 2 * index + 2; // Right child at 2*i + 2
344 uint16 smallest = index; // Assume current is smallest
345 // Check if left child exists and is smaller than current smallest
346 if (
347 leftChild < heapData.heapSize &&
348 heapData.heap[leftChild].score < heapData.heap[smallest].score
349 ) {
350 smallest = leftChild;
351 }
352 // Check if right child exists and is smaller than current smallest
353 if (
354 rightChild < heapData.heapSize &&
355 heapData.heap[rightChild].score < heapData.heap[smallest].score
356 ) {
357 smallest = rightChild;
358 }
359 // If current element is already the smallest, heap property is satisfied
360 if (smallest == index) {
361 break;
362 }
363 // Swap current element with the smallest child
364 _swap(statsKey, index, smallest);
365 // Move down to the child position
366 index = smallest;
367 }
368 }
369
370 /**
371 * @dev Swaps two elements in the heap and updates the hash-to-index mapping
372 * @param statsKey The stats category key
373 * @param i First index
374 * @param j Second index
375 */
376 function _swap(uint32 statsKey, uint16 i, uint16 j) internal {
377 Top500Data storage heapData = stats[statsKey];
378 // Swap the actual heap nodes
379 HeapNode memory temp = heapData.heap[i];
380 heapData.heap[i] = heapData.heap[j];
381 heapData.heap[j] = temp;
382 // Update the hash-to-index mapping
383 heapData.hashToIndex[heapData.heap[i].hash] = i;
384 heapData.hashToIndex[heapData.heap[j].hash] = j;
385 }
386
387 // ========================== PUBLIC INTERFACE ==========================
388
389 // require(countryCode < 10000, "ObjectId must be lower than 10000");
390
391 function calculateMonthId(
392 uint32 ts
393 ) internal pure returns (uint16 monthId) {
394 return 0;
395 }
396 /**
397 * @dev Main function to process batch of compressed actions and update statistics
398 * This is called by the Factory contract when users submit data
399 */
400 function storeBatchStats(
401 uint256[] calldata patches,
402 uint16[] calldata textIndex,
403 string[] calldata textData
404 ) external override onlyAuthorized {
405 uint16 monthId;
406 uint256 encoded;
407 ActionType action;
408 uint8 objectTypeId;
409 uint16 targetId;
410 // Process each action in the batch
411 for (uint256 i = 0; i < patches.length; i++) {
412 encoded = patches[i];
413 // Decode the compressed action data
414 // uint8 action = ActionType(uint8(encoded & 0xF));
415 // uint8 objectType = uint8(encoded >> 4 & 0xFF);
416 // uint32 objectId = uint32(encoded >> 10 & 0xFFFFFFFF);
417 // uint32 ts = uint32(encoded >> 28 & 0xFFFFFFFF);
418 // uint16 monthId = calculateMonthId(ts);
419
420 // if (action == ActionType.Visit || action == ActionType.Download || action== ActionType.Share) {
421 // uint64 longHash = paramsToHash(action, objectType, monthId, objectId);
422 // uint32 shortHash = paramsToHashShort(action, objectType, monthId);
423 // // Increment the counter for this specific action
424 // counters[longHash]++;
425 // // Update the leaderboard for this stats category
426 // _updateScore(shortHash, longHash, counters[longHash]);
427 // }
428
429 // monthId = uint16((encoded >> 8) & 0xFFFF);
430 // objectTypeId = uint8((encoded >> 24) & 0xFF);
431 // targetId = uint16((encoded >> 32) & 0xFFFF);
432 // // Validate target ID (business rule)
433 // if (targetId >=10000) {
434 // revert ErrorLibrary.ObjectIdOverflow();
435 // }
436 // // Create unique hashes for this action
437 // uint64 longHash = paramsToHash(action, objectTypeId, monthId, targetId);
438 // uint32 shortHash = paramsToHashShort(action, objectTypeId, monthId);
439 // // Increment the counter for this specific action
440 // counters[longHash]++;
441 // // Update the leaderboard for this stats category
442 // _updateScore(shortHash, longHash, counters[longHash]);
443 }
444 }
445
446 function storeNewRegister(
447 uint16 countryCode
448 ) external override onlyAuthorized {
449 // Decode the compressed action data
450 ActionType action = ActionType.CreateRecord;
451 uint16 monthId = getLastMonthId();
452 uint8 objectTypeId = 5; // "Member in Typescript"
453 // Validate target ID (business rule)
454 if (countryCode >= 10000) {
455 revert ErrorLibrary.ObjectIdOverflow();
456 }
457 // Create unique hashes for this action
458 uint64 longHash = paramsToHash(
459 action,
460 objectTypeId,
461 monthId,
462 countryCode
463 );
464 uint32 shortHash = paramsToHashShort(action, objectTypeId, monthId);
465 // Increment the counter for this specific action
466 counters[longHash]++;
467 // Update the leaderboard for this stats category
468 _updateScore(shortHash, longHash, counters[longHash]);
469 }
470
471 function storeNewWrite(
472 uint16 countryCode
473 ) external override onlyAuthorized {
474 // Decode the compressed action data
475 ActionType action = ActionType.LocationWrite;
476 uint16 monthId = getLastMonthId();
477 uint8 objectTypeId = 5; // "Member in Typescript"
478 // Validate target ID (business rule)
479 if (countryCode >= 10000) {
480 revert ErrorLibrary.ObjectIdOverflow();
481 }
482 // Create unique hashes for this action
483 uint64 longHash = paramsToHash(
484 action,
485 objectTypeId,
486 monthId,
487 countryCode
488 );
489 uint32 shortHash = paramsToHashShort(action, objectTypeId, monthId);
490 // Increment the counter for this specific action
491 counters[longHash]++;
492 // Update the leaderboard for this stats category
493 _updateScore(shortHash, longHash, counters[longHash]);
494 }
495 // ========================== VIEW FUNCTIONS (GAS OPTIMIZED) ==========================
496
497 function getStats(
498 uint8 objectType,
499 uint16 monthId
500 )
501 public
502 view
503 override
504 returns (
505 uint8[11] memory actions,
506 uint64[][11] memory hashes,
507 uint32[][11] memory scores
508 )
509 {
510 actions[0] = uint8(ActionType.Visit);
511 actions[1] = uint8(ActionType.Download);
512 actions[2] = uint8(ActionType.Share);
513 actions[3] = uint8(ActionType.Rate);
514 actions[4] = uint8(ActionType.VisitPart);
515 actions[5] = uint8(ActionType.CreateRecord);
516 actions[6] = uint8(ActionType.SharePart);
517 actions[7] = uint8(ActionType.RatePart);
518 actions[8] = uint8(ActionType.EditTextProp);
519 actions[9] = uint8(ActionType.EditBooleanProp);
520 actions[10] = uint8(ActionType.EditNumberProp);
521
522 for (uint256 i = 0; i < 11; i++) {
523 (uint64[] memory hs, uint32[] memory sc) = _getStatsFor(
524 ActionType(actions[i]),
525 objectType,
526 monthId
527 );
528 hashes[i] = hs;
529 scores[i] = sc;
530 }
531 return (actions, hashes, scores);
532 }
533
534 function _getStatsFor(
535 ActionType action,
536 uint8 objectType,
537 uint16 monthId
538 ) internal view returns (uint64[] memory hashes, uint32[] memory scores) {
539 uint32 statsKey = paramsToHashShort(action, objectType, monthId);
540 Top500Data storage heapData = stats[statsKey];
541 hashes = new uint64[](heapData.heapSize);
542 scores = new uint32[](heapData.heapSize);
543 for (uint16 j = 0; j < heapData.heapSize; j++) {
544 hashes[j] = heapData.heap[j].hash;
545 scores[j] = heapData.heap[j].score;
546 }
547 return (hashes, scores);
548 }
549
550 /**
551 * @dev Check if a specific hash is in the top-500 for a stats category
552 * @param action The action type
553 * @param objectType The object type
554 * @param monthId The month ID
555 * @param hash The hash to check
556 * @return true if hash is in top-500, false otherwise
557 */
558 function isInTop500(
559 ActionType action,
560 uint8 objectType,
561 uint16 monthId,
562 uint64 hash
563 ) external view returns (bool) {
564 uint32 statsKey = paramsToHashShort(action, objectType, monthId);
565 Top500Data storage heapData = stats[statsKey];
566 uint16 index = heapData.hashToIndex[hash];
567 return (index < heapData.heapSize && heapData.heap[index].hash == hash);
568 }
569
570 // Replace the public enum-typed version with external uint8 version (interface)
571 function getStatsRaw(
572 uint8 action,
573 uint8 objectType,
574 uint16 monthId
575 )
576 external
577 view
578 override
579 returns (uint64[] memory hashes, uint32[] memory scores)
580 {
581 uint32 statsKey = paramsToHashShort(
582 ActionType(action),
583 objectType,
584 monthId
585 );
586 Top500Data storage heapData = stats[statsKey];
587 hashes = new uint64[](heapData.heapSize);
588 scores = new uint32[](heapData.heapSize);
589 for (uint16 i = 0; i < heapData.heapSize; i++) {
590 hashes[i] = heapData.heap[i].hash;
591 scores[i] = heapData.heap[i].score;
592 }
593 return (hashes, scores);
594 }
595
596 function getStatsSize(
597 uint8 action,
598 uint8 objectType,
599 uint16 monthId
600 ) external view override returns (uint16) {
601 uint32 statsKey = paramsToHashShort(
602 ActionType(action),
603 objectType,
604 monthId
605 );
606 return stats[statsKey].heapSize;
607 }
608
609 function getMinimumScore(
610 uint8 action,
611 uint8 objectType,
612 uint16 monthId
613 ) external view override returns (uint32) {
614 uint32 statsKey = paramsToHashShort(
615 ActionType(action),
616 objectType,
617 monthId
618 );
619 Top500Data storage heapData = stats[statsKey];
620 if (heapData.heapSize == 0) return 0;
621 return heapData.heap[0].score;
622 }
623
624 function loadMonthStat(
625 uint8 action,
626 uint8 objectType,
627 uint16 monthId
628 ) public view returns (uint64[] memory) {
629 uint32 statsKey = paramsToHashShort(
630 ActionType(action),
631 objectType,
632 monthId
633 );
634 Top500Data storage heapData = stats[statsKey];
635 if (heapData.heapSize == 0) {
636 return new uint64[](0);
637 }
638 uint64[] memory result = new uint64[](heapData.heapSize);
639 for (uint16 i = 0; i < heapData.heapSize; i++) {
640 result[i] = heapData.heap[i].hash;
641 }
642 return result;
643 }
644}
645