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..1b4c0ee9d --- /dev/null +++ b/src/ticking/stable_computor_index.h @@ -0,0 +1,69 @@ +#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) +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; + + 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++) + { + 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++; + } + } + + copyMem(futureComputors, tempComputorList, NUMBER_OF_COMPUTORS * sizeof(m256i)); + + return true; +} diff --git a/test/stable_computor_index.cpp b/test/stable_computor_index.cpp new file mode 100644 index 000000000..3eaab702c --- /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[1] = 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 @@ +