Skip to content

Feature/random sc#899

Open
double-k-3033 wants to merge 11 commits into
qubic:developfrom
double-k-3033:feature/randomSC
Open

Feature/random sc#899
double-k-3033 wants to merge 11 commits into
qubic:developfrom
double-k-3033:feature/randomSC

Conversation

@double-k-3033
Copy link
Copy Markdown
Contributor

No description provided.

Franziska-Mueller and others added 11 commits May 13, 2026 09:54
- BuyEntropy: add missing else branch so invalid inputs (bad tier,
  zero/oversize bits, underpaid fee) refund the full invocation reward
  instead of silently keeping it.
- END_TICK: move reveals.set(..., zeroReveal) out of the
  a no-show aren't permanently locked out.
Change BuyEntropy_output.entropy from Array<bit_4096,32> to a single
bit_4096, copy the first numberOfBits bits, and refund any payment
beyond numberOfBits*100 instead of silently keeping it.

Co-Authored-By: N-010 <N-010@users.noreply.github.com>
Rework the collateral lifecycle so a provider's stake stays locked
from commit until they reveal:
- refund the stake on a valid reveal, slash (burn) it on a no-show
- reject an empty commit before any state change to avoid double refund
- stop paying silent providers from the treasury at END_TICK

Adds lockedCollateralAmounts and revealedThisTickFlags state, and
gtests covering the collateral lifecycle.
@ThatsNotMySourceCode
Copy link
Copy Markdown

ThatsNotMySourceCode commented May 27, 2026

    struct END_TICK_locals
    {
        bit_4096 zeroReveal; // TODO: Use a constant from either QPI or global state
        bit_4096 entropy;
        uint32 stream;
        uint32 i, j;
        uint32 index;
        uint32 lastIndex;
        uint32 tier;
        uint16 collateralTierFlags;
        uint64 lockedAmount;
    };

    END_TICK_WITH_LOCALS()
    {
        locals.stream = mod<uint32>(qpi.tick(), 3);

        // Zero entropy for this stream
        for (locals.i = 0; locals.i < 10; locals.i++)
        {
            state.mut().entropy.set(locals.stream * 10 + locals.i, locals.zeroReveal);
        }

        // === Empty tick detection using revealedThisTickFlags ===
        // If nobody actually revealed this tick, treat it as an empty tick
        // and refund providers instead of burning them.
        bool was_empty_tick = true;
        for (locals.i = 0; locals.i < state.get().populations.get(locals.stream); locals.i++)
        {
            if (state.get().revealedThisTickFlags.get(locals.stream * RANDOM_STREAM_CAPACITY + locals.i))
            {
                was_empty_tick = false;
                break;
            }
        }

        if (was_empty_tick)
        {
            // Empty tick → refund everyone who has locked collateral
            for (locals.i = state.get().populations.get(locals.stream); locals.i--;)
            {
                locals.index = locals.stream * RANDOM_STREAM_CAPACITY + locals.i;
                locals.lockedAmount = state.get().lockedCollateralAmounts.get(locals.index);

                if (locals.lockedAmount > 0)
                {
                    qpi.transfer(state.get().providers.get(locals.index),
                                 static_cast<sint64>(locals.lockedAmount));
                }

                // Clear the provider slot
                state.mut().providers.set(locals.index, id::zero());
                state.mut().collateralTiers.set(locals.index, 0);
                state.mut().commits.set(locals.index, id::zero());
                state.mut().reveals.set(locals.index, locals.zeroReveal);
                state.mut().lockedCollateralAmounts.set(locals.index, 0);
                state.mut().revealOrCommitFlags.set(locals.index, 0);
                state.mut().revealedThisTickFlags.set(locals.index, 0);
            }

            state.mut().populations.set(locals.stream, 0);
            return;
        }

        // === Normal tick: at least one provider revealed ===
        for (locals.i = state.get().populations.get(locals.stream); locals.i--;)
        {
            locals.index = locals.stream * RANDOM_STREAM_CAPACITY + locals.i;
            locals.tier = static_cast<uint32>(state.get().collateralTiers.get(locals.index));

            if (state.get().revealOrCommitFlags.get(locals.index))
            {
                // Provider participated this tick
                if (state.get().revealedThisTickFlags.get(locals.index))
                {
                    // Valid reveal → mix into entropy (unless tier already poisoned)
                    if (!(locals.collateralTierFlags & (1 << locals.tier)))
                    {
                        locals.entropy = state.get().entropy.get(locals.stream * 10 + locals.tier);
                        for (locals.j = 0; locals.j < 4096; locals.j++)
                        {
                            locals.entropy.set(locals.j,
                                               locals.entropy.get(locals.j) ^ state.get().reveals.get(locals.index).get(locals.j));
                        }
                        state.mut().entropy.set(locals.stream * 10 + locals.tier, locals.entropy);
                    }
                    state.mut().reveals.set(locals.index, locals.zeroReveal);
                }

                state.mut().revealOrCommitFlags.set(locals.index, 0);
                state.mut().revealedThisTickFlags.set(locals.index, 0);
            }
            else
            {
                // Provider did not reveal this tick → burn collateral
                locals.lockedAmount = state.get().lockedCollateralAmounts.get(locals.index);
                if (locals.lockedAmount > 0)
                {
                    qpi.burn(static_cast<sint64>(locals.lockedAmount));
                    state.mut().burnedAmount += locals.lockedAmount;
                }

                locals.collateralTierFlags |= (1 << locals.tier);

                // Swap-delete
                locals.lastIndex = locals.stream * RANDOM_STREAM_CAPACITY +
                                   state.get().populations.get(locals.stream) - 1;

                if (locals.index != locals.lastIndex)
                {
                    state.mut().providers.set(locals.index, state.get().providers.get(locals.lastIndex));
                    state.mut().collateralTiers.set(locals.index, state.get().collateralTiers.get(locals.lastIndex));
                    state.mut().commits.set(locals.index, state.get().commits.get(locals.lastIndex));
                    state.mut().reveals.set(locals.index, state.get().reveals.get(locals.lastIndex));
                    state.mut().lockedCollateralAmounts.set(locals.index, state.get().lockedCollateralAmounts.get(locals.lastIndex));
                    state.mut().revealOrCommitFlags.set(locals.index, state.get().revealOrCommitFlags.get(locals.lastIndex));
                    state.mut().revealedThisTickFlags.set(locals.index, state.get().revealedThisTickFlags.get(locals.lastIndex));
                }

                state.mut().providers.set(locals.lastIndex, id::zero());
                state.mut().collateralTiers.set(locals.lastIndex, 0);
                state.mut().commits.set(locals.lastIndex, id::zero());
                state.mut().reveals.set(locals.lastIndex, locals.zeroReveal);
                state.mut().lockedCollateralAmounts.set(locals.lastIndex, 0);
                state.mut().revealOrCommitFlags.set(locals.lastIndex, 0);
                state.mut().revealedThisTickFlags.set(locals.lastIndex, 0);

                state.mut().populations.set(locals.stream, state.get().populations.get(locals.stream) - 1);
            }
        }

        // Zero entropy for any tier that had a no-show this round
        for (locals.i = 0; locals.i < 10; locals.i++)
        {
            if (locals.collateralTierFlags & (1 << locals.i))
            {
                state.mut().entropy.set(locals.stream * 10 + locals.i, locals.zeroReveal);
            }
        }
    }

edit: this version refunds everyone on empty tick.

@double-k-3033
Copy link
Copy Markdown
Contributor Author

END_TICK_WITH_LOCALS()
{
    locals.stream = mod<uint32>(qpi.tick(), 3);

    // Zero entropy for this stream
    for (locals.i = 0; locals.i < 10; locals.i++)
    {
        state.mut().entropy.set(locals.stream * 10 + locals.i, locals.zeroReveal);
    }

    // === Check if this was a completely empty tick ===
    bool empty_tick = true;
    for (locals.i = 0; locals.i < state.get().populations.get(locals.stream); locals.i++)
    {
        if (state.get().revealOrCommitFlags.get(locals.stream * RANDOM_STREAM_CAPACITY + locals.i))
        {
            empty_tick = false;
            break;
        }
    }

    if (empty_tick)
    {
        // Full empty tick → refund everyone and clear the stream
        for (locals.i = state.get().populations.get(locals.stream); locals.i--;)
        {
            locals.index = locals.stream * RANDOM_STREAM_CAPACITY + locals.i;
            locals.lockedAmount = state.get().lockedCollateralAmounts.get(locals.index);

            if (locals.lockedAmount > 0)
            {
                qpi.transfer(state.get().providers.get(locals.index),
                             static_cast<sint64>(locals.lockedAmount));
            }

            // Clear slot
            state.mut().providers.set(locals.index, id::zero());
            state.mut().collateralTiers.set(locals.index, 0);
            state.mut().commits.set(locals.index, id::zero());
            state.mut().reveals.set(locals.index, locals.zeroReveal);
            state.mut().lockedCollateralAmounts.set(locals.index, 0);
            state.mut().revealOrCommitFlags.set(locals.index, 0);
            state.mut().revealedThisTickFlags.set(locals.index, 0);
        }
        state.mut().populations.set(locals.stream, 0);
        return;
    }

    // === Normal tick: at least one provider participated ===
    for (locals.i = state.get().populations.get(locals.stream); locals.i--;)
    {
        locals.index = locals.stream * RANDOM_STREAM_CAPACITY + locals.i;
        locals.tier = static_cast<uint32>(state.get().collateralTiers.get(locals.index));

        if (state.get().revealOrCommitFlags.get(locals.index))
        {
            if (state.get().revealedThisTickFlags.get(locals.index))
            {
                if (!(locals.collateralTierFlags & (1 << locals.tier)))
                {
                    locals.entropy = state.get().entropy.get(locals.stream * 10 + locals.tier);
                    for (locals.j = 0; locals.j < 4096; locals.j++)
                    {
                        locals.entropy.set(locals.j,
                            locals.entropy.get(locals.j) ^ state.get().reveals.get(locals.index).get(locals.j));
                    }
                    state.mut().entropy.set(locals.stream * 10 + locals.tier, locals.entropy);
                }
                state.mut().reveals.set(locals.index, locals.zeroReveal);
            }

            state.mut().revealOrCommitFlags.set(locals.index, 0);
            state.mut().revealedThisTickFlags.set(locals.index, 0);
        }
        else
        {
            // No-show → burn
            locals.lockedAmount = state.get().lockedCollateralAmounts.get(locals.index);
            if (locals.lockedAmount > 0)
            {
                qpi.burn(static_cast<sint64>(locals.lockedAmount));
                state.mut().burnedAmount += locals.lockedAmount;
            }

            locals.collateralTierFlags |= (1 << locals.tier);

            // Swap-delete
            locals.lastIndex = locals.stream * RANDOM_STREAM_CAPACITY +
                               state.get().populations.get(locals.stream) - 1;
            if (locals.index != locals.lastIndex)
            {
                state.mut().providers.set(locals.index, state.get().providers.get(locals.lastIndex));
                state.mut().collateralTiers.set(locals.index, state.get().collateralTiers.get(locals.lastIndex));
                state.mut().commits.set(locals.index, state.get().commits.get(locals.lastIndex));
                state.mut().reveals.set(locals.index, state.get().reveals.get(locals.lastIndex));
                state.mut().lockedCollateralAmounts.set(locals.index, state.get().lockedCollateralAmounts.get(locals.lastIndex));
                state.mut().revealOrCommitFlags.set(locals.index, state.get().revealOrCommitFlags.get(locals.lastIndex));
                state.mut().revealedThisTickFlags.set(locals.index, state.get().revealedThisTickFlags.get(locals.lastIndex));
            }
            state.mut().providers.set(locals.lastIndex, id::zero());
            state.mut().collateralTiers.set(locals.lastIndex, 0);
            state.mut().commits.set(locals.lastIndex, id::zero());
            state.mut().reveals.set(locals.lastIndex, locals.zeroReveal);
            state.mut().lockedCollateralAmounts.set(locals.lastIndex, 0);
            state.mut().revealOrCommitFlags.set(locals.lastIndex, 0);
            state.mut().revealedThisTickFlags.set(locals.lastIndex, 0);

            state.mut().populations.set(locals.stream, state.get().populations.get(locals.stream) - 1);
        }
    }

    // Zero entropy for any tier that had a no-show
    for (locals.i = 0; locals.i < 10; locals.i++)
    {
        if (locals.collateralTierFlags & (1 << locals.i))
        {
            state.mut().entropy.set(locals.stream * 10 + locals.i, locals.zeroReveal);
        }
    }
}

@ThatsNotMyCode Thx for your alternative.
I want to push back on empty-tick signal specifically using revealOrCommitFlags here would re-introduce the bug I just fixed.
revealOrCommitFlags is set on the first-commit path too. Concrete Scenario where this break:

  • Tick T: miners M1..M5 commit
  • Tick T+3(their reveal round): network is degraded, none of their reveal txs land. One brand-new miner M6 manages to first-commit (a single tx slips through).
  • With revealOrCommitFlags : empty_tick = false (M6's flag is set) -> normal walk -> M1..M5's revealOrCommitFlag = 0 -> all 5 burned for a network failure they had no agency over.
  • With revealedThisTickFlags(current code) : empty_tick = true (no reveal landed) -> refund branch -> M1..M5 made whole, M6 kept registered for next round.

The whole reason for adding the second flag in this commit was precisely to distinguish "did someone commit this tick" from "did someone reveal this tick". @come_from_beyond called out that they are not the same signal.
On the refund-everyone vs preserve-fresh-committers split that is a defensible design choice either way. This way happy to go with refund-everyone if you would prefer, but signal must be revealThisTickFlags.

@ThatsNotMySourceCode
Copy link
Copy Markdown

ThatsNotMySourceCode commented May 27, 2026

Alternative that preserves fresh committers during empty tick...

    struct END_TICK_locals
    {
        bit_4096 zeroReveal;
        bit_4096 entropy;
        uint32 stream;
        uint32 i, j;
        uint32 index;
        uint32 lastIndex;
        uint32 tier;
        uint16 collateralTierFlags;
        uint64 lockedAmount;
        bool   was_empty_tick;
    };

    END_TICK_WITH_LOCALS()
    {
        locals.stream = mod<uint32>(qpi.tick(), 3);

        // Zero entropy for this stream
        for (locals.i = 0; locals.i < 10; locals.i++)
        {
            state.mut().entropy.set(locals.stream * 10 + locals.i, locals.zeroReveal);
        }

        // === Empty tick detection (using revealedThisTickFlags) ===
        locals.was_empty_tick = true;
        for (locals.i = 0; locals.i < state.get().populations.get(locals.stream); locals.i++)
        {
            if (state.get().revealedThisTickFlags.get(locals.stream * RANDOM_STREAM_CAPACITY + locals.i))
            {
                locals.was_empty_tick = false;
                break;
            }
        }

        if (locals.was_empty_tick)
        {
            // Empty tick → refund providers who failed to reveal,
            // but preserve fresh committers who just joined this tick.
            for (locals.i = state.get().populations.get(locals.stream); locals.i--;)
            {
                locals.index = locals.stream * RANDOM_STREAM_CAPACITY + locals.i;

                if (state.get().revealOrCommitFlags.get(locals.index))
                {
                    // Fresh committer this tick → preserve them
                    state.mut().revealOrCommitFlags.set(locals.index, 0);
                    state.mut().revealedThisTickFlags.set(locals.index, 0);
                    continue;
                }

                // Had a pending reveal obligation but didn't reveal → refund + remove
                locals.lockedAmount = state.get().lockedCollateralAmounts.get(locals.index);
                if (locals.lockedAmount > 0)
                {
                    qpi.transfer(state.get().providers.get(locals.index),
                                 static_cast<sint64>(locals.lockedAmount));
                }

                // Swap-delete
                locals.lastIndex = locals.stream * RANDOM_STREAM_CAPACITY +
                                   state.get().populations.get(locals.stream) - 1;

                if (locals.index != locals.lastIndex)
                {
                    state.mut().providers.set(locals.index, state.get().providers.get(locals.lastIndex));
                    state.mut().collateralTiers.set(locals.index, state.get().collateralTiers.get(locals.lastIndex));
                    state.mut().commits.set(locals.index, state.get().commits.get(locals.lastIndex));
                    state.mut().reveals.set(locals.index, state.get().reveals.get(locals.lastIndex));
                    state.mut().lockedCollateralAmounts.set(locals.index, state.get().lockedCollateralAmounts.get(locals.lastIndex));
                    state.mut().revealOrCommitFlags.set(locals.index, state.get().revealOrCommitFlags.get(locals.lastIndex));
                    state.mut().revealedThisTickFlags.set(locals.index, state.get().revealedThisTickFlags.get(locals.lastIndex));
                }

                state.mut().providers.set(locals.lastIndex, id::zero());
                state.mut().collateralTiers.set(locals.lastIndex, 0);
                state.mut().commits.set(locals.lastIndex, id::zero());
                state.mut().reveals.set(locals.lastIndex, locals.zeroReveal);
                state.mut().lockedCollateralAmounts.set(locals.lastIndex, 0);
                state.mut().revealOrCommitFlags.set(locals.lastIndex, 0);
                state.mut().revealedThisTickFlags.set(locals.lastIndex, 0);

                state.mut().populations.set(locals.stream, state.get().populations.get(locals.stream) - 1);
            }
            return;
        }

        // === Normal tick: at least one provider revealed ===
        for (locals.i = state.get().populations.get(locals.stream); locals.i--;)
        {
            locals.index = locals.stream * RANDOM_STREAM_CAPACITY + locals.i;
            locals.tier = static_cast<uint32>(state.get().collateralTiers.get(locals.index));

            if (state.get().revealOrCommitFlags.get(locals.index))
            {
                if (state.get().revealedThisTickFlags.get(locals.index))
                {
                    if (!(locals.collateralTierFlags & (1 << locals.tier)))
                    {
                        locals.entropy = state.get().entropy.get(locals.stream * 10 + locals.tier);
                        for (locals.j = 0; locals.j < 4096; locals.j++)
                        {
                            locals.entropy.set(locals.j,
                                               locals.entropy.get(locals.j) ^ state.get().reveals.get(locals.index).get(locals.j));
                        }
                        state.mut().entropy.set(locals.stream * 10 + locals.tier, locals.entropy);
                    }
                    state.mut().reveals.set(locals.index, locals.zeroReveal);
                }

                state.mut().revealOrCommitFlags.set(locals.index, 0);
                state.mut().revealedThisTickFlags.set(locals.index, 0);
            }
            else
            {
                // No-show → burn collateral
                locals.lockedAmount = state.get().lockedCollateralAmounts.get(locals.index);
                if (locals.lockedAmount > 0)
                {
                    qpi.burn(static_cast<sint64>(locals.lockedAmount));
                    state.mut().burnedAmount += locals.lockedAmount;
                }

                locals.collateralTierFlags |= (1 << locals.tier);

                locals.lastIndex = locals.stream * RANDOM_STREAM_CAPACITY +
                                   state.get().populations.get(locals.stream) - 1;

                if (locals.index != locals.lastIndex)
                {
                    state.mut().providers.set(locals.index, state.get().providers.get(locals.lastIndex));
                    state.mut().collateralTiers.set(locals.index, state.get().collateralTiers.get(locals.lastIndex));
                    state.mut().commits.set(locals.index, state.get().commits.get(locals.lastIndex));
                    state.mut().reveals.set(locals.index, state.get().reveals.get(locals.lastIndex));
                    state.mut().lockedCollateralAmounts.set(locals.index, state.get().lockedCollateralAmounts.get(locals.lastIndex));
                    state.mut().revealOrCommitFlags.set(locals.index, state.get().revealOrCommitFlags.get(locals.lastIndex));
                    state.mut().revealedThisTickFlags.set(locals.index, state.get().revealedThisTickFlags.get(locals.lastIndex));
                }

                state.mut().providers.set(locals.lastIndex, id::zero());
                state.mut().collateralTiers.set(locals.lastIndex, 0);
                state.mut().commits.set(locals.lastIndex, id::zero());
                state.mut().reveals.set(locals.lastIndex, locals.zeroReveal);
                state.mut().lockedCollateralAmounts.set(locals.lastIndex, 0);
                state.mut().revealOrCommitFlags.set(locals.lastIndex, 0);
                state.mut().revealedThisTickFlags.set(locals.lastIndex, 0);

                state.mut().populations.set(locals.stream, state.get().populations.get(locals.stream) - 1);
            }
        }

        // Drop entropy for any tier that had a no-show
        for (locals.i = 0; locals.i < 10; locals.i++)
        {
            if (locals.collateralTierFlags & (1 << locals.i))
            {
                state.mut().entropy.set(locals.stream * 10 + locals.i, locals.zeroReveal);
            }
        }
    }

Edit: updated based on your comment

@double-k-3033
Copy link
Copy Markdown
Contributor Author

Alternative that preserves fresh committers during empty tick...

    struct END_TICK_locals
    {
        bit_4096 zeroReveal;
        bit_4096 entropy;
        uint32 stream;
        uint32 i, j;
        uint32 index;
        uint32 lastIndex;
        uint32 tier;
        uint16 collateralTierFlags;
        uint64 lockedAmount;
    };

    END_TICK_WITH_LOCALS()
    {
        locals.stream = mod<uint32>(qpi.tick(), 3);

        // Zero entropy for this stream
        for (locals.i = 0; locals.i < 10; locals.i++)
        {
            state.mut().entropy.set(locals.stream * 10 + locals.i, locals.zeroReveal);
        }

        // === Empty tick detection ===
        bool was_empty_tick = true;
        for (locals.i = 0; locals.i < state.get().populations.get(locals.stream); locals.i++)
        {
            if (state.get().revealedThisTickFlags.get(locals.stream * RANDOM_STREAM_CAPACITY + locals.i))
            {
                was_empty_tick = false;
                break;
            }
        }

        if (was_empty_tick)
        {
            // Empty tick → refund providers who failed to reveal,
            // but preserve fresh committers who just joined this tick.
            for (locals.i = state.get().populations.get(locals.stream); locals.i--;)
            {
                locals.index = locals.stream * RANDOM_STREAM_CAPACITY + locals.i;

                // If they had a pending obligation (locked collateral) and did not reveal → refund them
                if (state.get().lockedCollateralAmounts.get(locals.index) > 0 &&
                    !state.get().revealedThisTickFlags.get(locals.index))
                {
                    qpi.transfer(state.get().providers.get(locals.index),
                                 static_cast<sint64>(state.get().lockedCollateralAmounts.get(locals.index)));
                }

                // Clear the slot only for those who had a pending reveal obligation.
                // Fresh committers (who just committed this tick) are kept.
                if (state.get().lockedCollateralAmounts.get(locals.index) > 0 &&
                    !state.get().revealedThisTickFlags.get(locals.index))
                {
                    // This provider had a pending reveal → remove them after refund
                    locals.lastIndex = locals.stream * RANDOM_STREAM_CAPACITY +
                                       state.get().populations.get(locals.stream) - 1;

                    if (locals.index != locals.lastIndex)
                    {
                        state.mut().providers.set(locals.index, state.get().providers.get(locals.lastIndex));
                        state.mut().collateralTiers.set(locals.index, state.get().collateralTiers.get(locals.lastIndex));
                        state.mut().commits.set(locals.index, state.get().commits.get(locals.lastIndex));
                        state.mut().reveals.set(locals.index, state.get().reveals.get(locals.lastIndex));
                        state.mut().lockedCollateralAmounts.set(locals.index, state.get().lockedCollateralAmounts.get(locals.lastIndex));
                        state.mut().revealOrCommitFlags.set(locals.index, state.get().revealOrCommitFlags.get(locals.lastIndex));
                        state.mut().revealedThisTickFlags.set(locals.index, state.get().revealedThisTickFlags.get(locals.lastIndex));
                    }

                    state.mut().providers.set(locals.lastIndex, id::zero());
                    state.mut().collateralTiers.set(locals.lastIndex, 0);
                    state.mut().commits.set(locals.lastIndex, id::zero());
                    state.mut().reveals.set(locals.lastIndex, locals.zeroReveal);
                    state.mut().lockedCollateralAmounts.set(locals.lastIndex, 0);
                    state.mut().revealOrCommitFlags.set(locals.lastIndex, 0);
                    state.mut().revealedThisTickFlags.set(locals.lastIndex, 0);

                    state.mut().populations.set(locals.stream, state.get().populations.get(locals.stream) - 1);
                }
                else
                {
                    // Fresh committer or already handled → just clear flags for this tick
                    state.mut().revealOrCommitFlags.set(locals.index, 0);
                    state.mut().revealedThisTickFlags.set(locals.index, 0);
                }
            }
            return;
        }

        // === Normal tick ===
        for (locals.i = state.get().populations.get(locals.stream); locals.i--;)
        {
            locals.index = locals.stream * RANDOM_STREAM_CAPACITY + locals.i;
            locals.tier = static_cast<uint32>(state.get().collateralTiers.get(locals.index));

            if (state.get().revealOrCommitFlags.get(locals.index))
            {
                if (state.get().revealedThisTickFlags.get(locals.index))
                {
                    if (!(locals.collateralTierFlags & (1 << locals.tier)))
                    {
                        locals.entropy = state.get().entropy.get(locals.stream * 10 + locals.tier);
                        for (locals.j = 0; locals.j < 4096; locals.j++)
                        {
                            locals.entropy.set(locals.j,
                                               locals.entropy.get(locals.j) ^ state.get().reveals.get(locals.index).get(locals.j));
                        }
                        state.mut().entropy.set(locals.stream * 10 + locals.tier, locals.entropy);
                    }
                    state.mut().reveals.set(locals.index, locals.zeroReveal);
                }

                state.mut().revealOrCommitFlags.set(locals.index, 0);
                state.mut().revealedThisTickFlags.set(locals.index, 0);
            }
            else
            {
                // No-show → burn
                locals.lockedAmount = state.get().lockedCollateralAmounts.get(locals.index);
                if (locals.lockedAmount > 0)
                {
                    qpi.burn(static_cast<sint64>(locals.lockedAmount));
                    state.mut().burnedAmount += locals.lockedAmount;
                }

                locals.collateralTierFlags |= (1 << locals.tier);

                locals.lastIndex = locals.stream * RANDOM_STREAM_CAPACITY +
                                   state.get().populations.get(locals.stream) - 1;

                if (locals.index != locals.lastIndex)
                {
                    state.mut().providers.set(locals.index, state.get().providers.get(locals.lastIndex));
                    state.mut().collateralTiers.set(locals.index, state.get().collateralTiers.get(locals.lastIndex));
                    state.mut().commits.set(locals.index, state.get().commits.get(locals.lastIndex));
                    state.mut().reveals.set(locals.index, state.get().reveals.get(locals.lastIndex));
                    state.mut().lockedCollateralAmounts.set(locals.index, state.get().lockedCollateralAmounts.get(locals.lastIndex));
                    state.mut().revealOrCommitFlags.set(locals.index, state.get().revealOrCommitFlags.get(locals.lastIndex));
                    state.mut().revealedThisTickFlags.set(locals.index, state.get().revealedThisTickFlags.get(locals.lastIndex));
                }

                state.mut().providers.set(locals.lastIndex, id::zero());
                state.mut().collateralTiers.set(locals.lastIndex, 0);
                state.mut().commits.set(locals.lastIndex, id::zero());
                state.mut().reveals.set(locals.lastIndex, locals.zeroReveal);
                state.mut().lockedCollateralAmounts.set(locals.lastIndex, 0);
                state.mut().revealOrCommitFlags.set(locals.lastIndex, 0);
                state.mut().revealedThisTickFlags.set(locals.lastIndex, 0);

                state.mut().populations.set(locals.stream, state.get().populations.get(locals.stream) - 1);
            }
        }

        // Drop entropy for poisoned tiers
        for (locals.i = 0; locals.i < 10; locals.i++)
        {
            if (locals.collateralTierFlags & (1 << locals.i))
            {
                state.mut().entropy.set(locals.stream * 10 + locals.i, locals.zeroReveal);
            }
        }
    }

Thanks for iterating. Two issues with this version:

  1. It doesn't actually preserve fresh committers, despite the comment. Your predicate is lockedCollateralAmounts > 0 && !revealedThisTickFlags — but a fresh first-committer in the empty-tick branch satisfies both (their stake is locked from THIS tick's commit, and they obviously didn't reveal). So they fall into the refund+remove branch alongside stale committers. The else only triggers when lockedCollateralAmounts == 0, which is never true for a populated slot in our model.

The correct discriminator is revealOrCommitFlags — that flag is set on the RAC path (both first-commit and reveal+commit) and cleared by END_TICK, so within the empty-tick branch revealOrCommitFlags == 1 uniquely identifies "fresh first-commit this tick". That's what the current code uses (Random.h:303).

  1. bool was_empty_tick outside END_TICK_locals is a style break — every other QPI procedure puts scratch locals in the _locals struct.

Empty-tick detection on revealedThisTickFlags is correct in your version — that part matches the current code. The bug is only in the empty-tick body (the discriminator inside the loop).

If you actually want refund-everyone semantics, that's defensible too, but it's the opposite of preserving fresh committers — we should agree on the design intent first, then write code that matches it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants