From 6fb1759e84f227317818e6656d4da0e8350ab9c3 Mon Sep 17 00:00:00 2001 From: fnordspace Date: Tue, 16 Dec 2025 17:10:43 +0100 Subject: [PATCH 1/3] Add stable computation of futureComputor Current implementation sorts future computor based on their score. This change will keep the computor list position for requalifying computors. The stable sorting only takes place befor system is saved. --- doc/stable_computor_index_diagram.svg | 233 ++++++++++++++++++++++++++ src/Qubic.vcxproj | 1 + src/Qubic.vcxproj.filters | 3 + src/qubic.cpp | 6 + src/ticking/stable_computor_index.h | 74 ++++++++ test/stable_computor_index.cpp | 214 +++++++++++++++++++++++ test/test.vcxproj | 1 + test/test.vcxproj.filters | 1 + 8 files changed, 533 insertions(+) create mode 100644 doc/stable_computor_index_diagram.svg create mode 100644 src/ticking/stable_computor_index.h create mode 100644 test/stable_computor_index.cpp diff --git a/doc/stable_computor_index_diagram.svg b/doc/stable_computor_index_diagram.svg new file mode 100644 index 000000000..5cffdd46c --- /dev/null +++ b/doc/stable_computor_index_diagram.svg @@ -0,0 +1,233 @@ + + + + + + + + + Stable Computor Index Algorithm + + + Memory Layout in tempBuffer: + + + + + + + tempComputorList[676] + (m256i array: 676 × 32 = 21,632 bytes) + + + + isIndexTaken[676] + (bool array: 676 bytes) + + + + isFutureComputorUsed[676] + (bool array: 676 bytes) + + + m256i* tempComputorList = (m256i*)tempBuffer; + bool* isIndexTaken = (bool*)(tempComputorList + NUMBER_OF_COMPUTORS); // advances by 676 × sizeof(m256i) bytes + bool* isFutureComputorUsed = isIndexTaken + NUMBER_OF_COMPUTORS; // advances by 676 bytes + + + Example (simplified to 6 computors): + + + Current Computors (epoch N): + + + idx 0 + ID_A + + + idx 1 + ID_B + + + idx 2 + ID_C + + + idx 3 + ID_D + + + idx 4 + ID_E + + + idx 5 + ID_F + + + + Future Computors BEFORE (sorted by mining score): + + + idx 0 + ID_C + + + idx 1 + ID_X + + + idx 2 + ID_A + + + idx 3 + ID_Y + + + idx 4 + ID_E + + + idx 5 + ID_B + + ← ID_C moved from idx 2→0, ID_A from idx 0→2, etc. + Problem: Requalifying IDs have different indices! + + + Step 1: Find requalifying computors, assign to their ORIGINAL index + + + + + idx 0 + ID_A + + + idx 1 + ID_B + + + idx 2 + ID_C + + + idx 3 + empty + + + idx 4 + ID_E + + + idx 5 + empty + + + + + isIndexTaken: + + T + + T + + T + + F + + T + + F + + isFutureUsed: + + T + + F + + T + + F + + T + + T + ← ID_X, ID_Y unused + + + + Step 2: Fill empty slots with new computors (ID_X, ID_Y) + + + + idx 0 + ID_A + + + idx 1 + ID_B + + + idx 2 + ID_C + + + idx 3 + ID_X + + + idx 4 + ID_E + + + idx 5 + ID_Y + + ← New computors fill vacated slots (D→X, F→Y) + + + Result: Future Computors AFTER (stable indices): + + + idx 0 + ID_A + + + idx 1 + ID_B + + + idx 2 + ID_C + + + idx 3 + ID_X + + + idx 4 + ID_E + + + idx 5 + ID_Y + + + + + + Requalifying (kept same index) + + + New computor (fills vacated slot) + + + + + Key Benefit for Execution Fee Reporting: + ID_A reports at ticks where tick % 676 == 0 in BOTH epochs + ID_B reports at ticks where tick % 676 == 1 in BOTH epochs → No gaps, no duplicates! + + diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj index 0a441c3b6..4c8e2180f 100644 --- a/src/Qubic.vcxproj +++ b/src/Qubic.vcxproj @@ -129,6 +129,7 @@ + diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters index 8886b6994..451cc3931 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -294,6 +294,9 @@ ticking + + ticking + contracts diff --git a/src/qubic.cpp b/src/qubic.cpp index 57c165b50..704d33d30 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -61,6 +61,7 @@ #include "contract_core/qpi_ticking_impl.h" #include "vote_counter.h" #include "ticking/execution_fee_report_collector.h" +#include "ticking/stable_computor_index.h" #include "network_messages/execution_fees.h" #include "contract_core/ipo.h" @@ -5248,6 +5249,11 @@ static void tickProcessor(void*) // Save the file of revenue. This blocking save can be called from any thread saveRevenueComponents(NULL); + // Reorder futureComputors so requalifying computors keep their index + // This is needed for correct execution fee reporting across epoch boundaries + static_assert(reorgBufferSize >= stableComputorIndexBufferSize(), "reorgBuffer too small for stable computor index"); + calculateStableComputorIndex(system.futureComputors, broadcastedComputors.computors.publicKeys, reorgBuffer); + // instruct main loop to save system and wait until it is done systemMustBeSaved = true; WAIT_WHILE(systemMustBeSaved); diff --git a/src/ticking/stable_computor_index.h b/src/ticking/stable_computor_index.h new file mode 100644 index 000000000..e69ff372f --- /dev/null +++ b/src/ticking/stable_computor_index.h @@ -0,0 +1,74 @@ +#pragma once + +#include "platform/m256.h" +#include "public_settings.h" + +// Minimum buffer size: NUMBER_OF_COMPUTORS * sizeof(m256i) + 2 * NUMBER_OF_COMPUTORS bytes (~23KB) +constexpr unsigned long long stableComputorIndexBufferSize() +{ + return NUMBER_OF_COMPUTORS * sizeof(m256i) + 2 * NUMBER_OF_COMPUTORS; +} + +// Reorders futureComputors so requalifying computors keep their current index. +// New computors fill remaining slots. See doc/stable_computor_index_diagram.svg +// Returns false if there aren't enough computors to fill all slots. +static bool calculateStableComputorIndex( + m256i* futureComputors, + const m256i* currentComputors, + void* tempBuffer) +{ + m256i* tempComputorList = (m256i*)tempBuffer; + bool* isIndexTaken = (bool*)(tempComputorList + NUMBER_OF_COMPUTORS); + bool* isFutureComputorUsed = isIndexTaken + NUMBER_OF_COMPUTORS; + + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + tempComputorList[i] = m256i::zero(); + isIndexTaken[i] = false; + isFutureComputorUsed[i] = false; + } + + // Step 1: Requalifying computors keep their current index + for (unsigned int futureIdx = 0; futureIdx < NUMBER_OF_COMPUTORS; futureIdx++) + { + for (unsigned int currentIdx = 0; currentIdx < NUMBER_OF_COMPUTORS; currentIdx++) + { + if (futureComputors[futureIdx] == currentComputors[currentIdx]) + { + tempComputorList[currentIdx] = futureComputors[futureIdx]; + isIndexTaken[currentIdx] = true; + isFutureComputorUsed[futureIdx] = true; + break; + } + } + } + + // Step 2: New computors fill remaining slots + unsigned int nextNewComputorIdx = 0; + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + if (!isIndexTaken[i]) + { + while (nextNewComputorIdx < NUMBER_OF_COMPUTORS && isFutureComputorUsed[nextNewComputorIdx]) + { + nextNewComputorIdx++; + } + + if (nextNewComputorIdx >= NUMBER_OF_COMPUTORS) + { + return false; + } + + tempComputorList[i] = futureComputors[nextNewComputorIdx]; + isFutureComputorUsed[nextNewComputorIdx] = true; + nextNewComputorIdx++; + } + } + + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + futureComputors[i] = tempComputorList[i]; + } + + return true; +} diff --git a/test/stable_computor_index.cpp b/test/stable_computor_index.cpp new file mode 100644 index 000000000..e4e6309be --- /dev/null +++ b/test/stable_computor_index.cpp @@ -0,0 +1,214 @@ +#define NO_UEFI + +#include "gtest/gtest.h" +#include "../src/ticking/stable_computor_index.h" + +class StableComputorIndexTest : public ::testing::Test +{ +protected: + m256i futureComputors[NUMBER_OF_COMPUTORS]; + m256i currentComputors[NUMBER_OF_COMPUTORS]; + unsigned char tempBuffer[stableComputorIndexBufferSize()]; + + void SetUp() override + { + memset(futureComputors, 0, sizeof(futureComputors)); + memset(currentComputors, 0, sizeof(currentComputors)); + memset(tempBuffer, 0, sizeof(tempBuffer)); + } + + m256i makeId(int n) + { + m256i id = m256i::zero(); + id.m256i_u64[0] = n; + return id; + } +}; + +// Test: All computors requalify - all should keep their indices +TEST_F(StableComputorIndexTest, AllRequalify) +{ + // Set up current computors with IDs 1 to NUMBER_OF_COMPUTORS + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + currentComputors[i] = makeId(i + 1); + } + + // Future: Same IDs but reversed order (simulating score reordering) + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + futureComputors[i] = makeId(NUMBER_OF_COMPUTORS - i); + } + + bool result = calculateStableComputorIndex(futureComputors, currentComputors, tempBuffer); + ASSERT_TRUE(result); + + // All should be back to their original indices + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + EXPECT_EQ(futureComputors[i], makeId(i + 1)) << "Index " << i << " mismatch"; + } +} + +// Test: Half computors replaced - requalifying keep index, new fill gaps +TEST_F(StableComputorIndexTest, PartialRequalify) +{ + // Current: ID 1 at idx 0, ID 2 at idx 1, ..., ID 676 at idx 675 + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + currentComputors[i] = makeId(i + 1); + } + + // Future input (scrambled): odd IDs requalify, even IDs replaced by new (1001+) + unsigned int requalifyingId = 1; + unsigned int newId = 1001; + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + if (i % 2 == 0 && requalifyingId <= NUMBER_OF_COMPUTORS) + { + futureComputors[i] = makeId(requalifyingId); + requalifyingId += 2; + } + else + { + futureComputors[i] = makeId(newId++); + } + } + + bool result = calculateStableComputorIndex(futureComputors, currentComputors, tempBuffer); + ASSERT_TRUE(result); + + // Odd IDs (1,3,5,...) should be at original indices (0,2,4,...) + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i += 2) + { + EXPECT_EQ(futureComputors[i], makeId(i + 1)); + } + + // New IDs (1001,1002,...) should fill gaps at indices (1,3,5,...) + unsigned int expectedNewId = 1001; + for (unsigned int i = 1; i < NUMBER_OF_COMPUTORS; i += 2) + { + EXPECT_EQ(futureComputors[i], makeId(expectedNewId++)); + } +} + +// Test: All computors are new - order preserved +TEST_F(StableComputorIndexTest, AllNew) +{ + // Current: IDs 1 to 676 + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + currentComputors[i] = makeId(i + 1); + } + + // Future: Completely new set (IDs 1000 to 1675) + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + futureComputors[i] = makeId(i + 1000); + } + + bool result = calculateStableComputorIndex(futureComputors, currentComputors, tempBuffer); + ASSERT_TRUE(result); + + // New computors should fill slots in order + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + EXPECT_EQ(futureComputors[i], makeId(i + 1000)); + } +} + +// Test: Single computor requalifies +TEST_F(StableComputorIndexTest, SingleRequalify) +{ + // Current: IDs 1 to 676 + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + currentComputors[i] = makeId(i + 1); + } + + // Future: Only ID 100 requalifies (at position 0), rest are new + futureComputors[0] = makeId(100); // Requalifying, was at idx 99 + for (unsigned int i = 1; i < NUMBER_OF_COMPUTORS; i++) + { + futureComputors[i] = makeId(i + 1000); // New IDs + } + + bool result = calculateStableComputorIndex(futureComputors, currentComputors, tempBuffer); + ASSERT_TRUE(result); + + // ID 100 should be at its original index 99 + EXPECT_EQ(futureComputors[99], makeId(100)); + + // New computors fill remaining slots (0-98, 100-675) + unsigned int newIdx = 0; + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + if (i == 99) continue; // Skip the requalifying slot + EXPECT_EQ(futureComputors[i], makeId(newIdx + 1001)) << "New computor at index " << i; + newIdx++; + } +} + + +// Test: First and last computor swap positions in input +TEST_F(StableComputorIndexTest, FirstLastSwap) +{ + // Current: IDs 1 to 676 + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + currentComputors[i] = makeId(i + 1); + } + + // Future: All same IDs, but first and last swapped in input order + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + futureComputors[i] = makeId(i + 1); + } + futureComputors[0] = makeId(NUMBER_OF_COMPUTORS); // Last ID at first position + futureComputors[NUMBER_OF_COMPUTORS - 1] = makeId(1); // First ID at last position + + bool result = calculateStableComputorIndex(futureComputors, currentComputors, tempBuffer); + ASSERT_TRUE(result); + + // All should be at their original indices regardless of input order + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + EXPECT_EQ(futureComputors[i], makeId(i + 1)) << "Index " << i << " mismatch"; + } +} + +// Test: Realistic scenario - 225 computors change (max allowed) +TEST_F(StableComputorIndexTest, MaxChange225) +{ + // Current: IDs 1 to 676 + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + currentComputors[i] = makeId(i + 1); + } + + // Future: First 451 (QUORUM) stay, last 225 are replaced with new IDs + for (unsigned int i = 0; i < 451; i++) + { + futureComputors[i] = makeId(i + 1); // Same IDs, possibly different order + } + for (unsigned int i = 451; i < NUMBER_OF_COMPUTORS; i++) + { + futureComputors[i] = makeId(i + 1000); // New IDs + } + + bool result = calculateStableComputorIndex(futureComputors, currentComputors, tempBuffer); + ASSERT_TRUE(result); + + // First 451 should keep their indices + for (unsigned int i = 0; i < 451; i++) + { + EXPECT_EQ(futureComputors[i], makeId(i + 1)); + } + + // Last 225 slots should have the new IDs + for (unsigned int i = 451; i < NUMBER_OF_COMPUTORS; i++) + { + EXPECT_EQ(futureComputors[i], makeId(i + 1000)); + } +} + diff --git a/test/test.vcxproj b/test/test.vcxproj index 5979b640b..7eda0af5b 100644 --- a/test/test.vcxproj +++ b/test/test.vcxproj @@ -144,6 +144,7 @@ + diff --git a/test/test.vcxproj.filters b/test/test.vcxproj.filters index 05f9b9622..391098eb9 100644 --- a/test/test.vcxproj.filters +++ b/test/test.vcxproj.filters @@ -35,6 +35,7 @@ + From 4684af7fd40a19feda6ac9cd8487b740a1e61d0b Mon Sep 17 00:00:00 2001 From: fnordspace Date: Fri, 19 Dec 2025 11:47:36 +0100 Subject: [PATCH 2/3] Repalce loops with set/copy mem --- src/ticking/stable_computor_index.h | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/ticking/stable_computor_index.h b/src/ticking/stable_computor_index.h index e69ff372f..1b4c0ee9d 100644 --- a/src/ticking/stable_computor_index.h +++ b/src/ticking/stable_computor_index.h @@ -1,6 +1,7 @@ #pragma once #include "platform/m256.h" +#include "platform/memory.h" #include "public_settings.h" // Minimum buffer size: NUMBER_OF_COMPUTORS * sizeof(m256i) + 2 * NUMBER_OF_COMPUTORS bytes (~23KB) @@ -21,12 +22,9 @@ static bool calculateStableComputorIndex( bool* isIndexTaken = (bool*)(tempComputorList + NUMBER_OF_COMPUTORS); bool* isFutureComputorUsed = isIndexTaken + NUMBER_OF_COMPUTORS; - for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) - { - tempComputorList[i] = m256i::zero(); - isIndexTaken[i] = false; - isFutureComputorUsed[i] = false; - } + setMem(tempComputorList, NUMBER_OF_COMPUTORS * sizeof(m256i), 0); + setMem(isIndexTaken, NUMBER_OF_COMPUTORS, 0); + setMem(isFutureComputorUsed, NUMBER_OF_COMPUTORS, 0); // Step 1: Requalifying computors keep their current index for (unsigned int futureIdx = 0; futureIdx < NUMBER_OF_COMPUTORS; futureIdx++) @@ -65,10 +63,7 @@ static bool calculateStableComputorIndex( } } - for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) - { - futureComputors[i] = tempComputorList[i]; - } + copyMem(futureComputors, tempComputorList, NUMBER_OF_COMPUTORS * sizeof(m256i)); return true; } From 6365c8ad34d3727f95e8f8c694633b3c0c137a8c Mon Sep 17 00:00:00 2001 From: fnordspace Date: Fri, 19 Dec 2025 12:01:29 +0100 Subject: [PATCH 3/3] Change test ids to non-contract ids --- test/stable_computor_index.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/stable_computor_index.cpp b/test/stable_computor_index.cpp index e4e6309be..3eaab702c 100644 --- a/test/stable_computor_index.cpp +++ b/test/stable_computor_index.cpp @@ -20,7 +20,7 @@ class StableComputorIndexTest : public ::testing::Test m256i makeId(int n) { m256i id = m256i::zero(); - id.m256i_u64[0] = n; + id.m256i_u64[1] = n; return id; } };