📄 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 otherwise
1/*
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