mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
The two chains held the same algorithm written twice: insertion order, a forward and reverse conflict index, a byte budget, a bounded cache of refusals, and the rule that removing a transaction also removes its rivals. It is now in core, and it knows nothing about a chain — it can refuse four things it can see for itself (duplicate, too large, no room, conflict) and it cannot tell a valid transaction from an invalid one. It reaches a transaction through three functions declared beside the transaction rather than beside the pool: pool_id, pool_size, pool_inputs. That is the whole seam. Each chain says those four refusals in its own words at its own door. THE DOOR IS THE POINT. The P-chain had TWO ways into its pool — a submitter's, and the one that puts a rejected block's transactions back — and only the first ran the chain's rule that nobody may submit its reward transaction. A rule enforced at one of two doors is a rule an attacker uses the other door for. There is now one admit(), and both call it. The refusal cache moved to the gate on the X-Chain too, where the reasons actually come from: the pool has four answers, and everything else a submitter is told came from an execution the pool knows nothing about. A side effect is a fixed bug — a transaction put back after its block was rejected now goes through the gate, so the filter a peer samples says this node holds it again, where before it silently did not. 208 differential vectors still agree.
167 lines
5.8 KiB
C++
167 lines
5.8 KiB
C++
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
// SPDX-License-Identifier: BSD-3-Clause-Eco
|
|
//
|
|
// mempool_test.cpp — the pool's four refusals, its order, and the two rules
|
|
// that are easy to state and easy to get wrong: that removing a transaction
|
|
// also removes its rivals, and that the space it took comes back.
|
|
//
|
|
// The transaction here is a struct with three fields, because that is all the
|
|
// pool is allowed to know about one. If a test needed a real transaction to
|
|
// exercise the pool, the pool would know too much.
|
|
|
|
#include "lux/core/check.hpp"
|
|
#include "lux/core/mempool.hpp"
|
|
|
|
#include <cstdio>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
using namespace lux::core;
|
|
using namespace lux::core::test;
|
|
using namespace lux::core::mempool;
|
|
|
|
namespace {
|
|
|
|
Id id_of(std::uint8_t b) {
|
|
Id v{};
|
|
v[0] = b;
|
|
return v;
|
|
}
|
|
|
|
// Everything a pool may know about a transaction.
|
|
struct Tx {
|
|
Id id{};
|
|
std::size_t size = 100;
|
|
std::vector<Id> inputs;
|
|
};
|
|
|
|
Id pool_id(const Tx& t) { return t.id; }
|
|
std::size_t pool_size(const Tx& t) { return t.size; }
|
|
const std::vector<Id>& pool_inputs(const Tx& t) { return t.inputs; }
|
|
|
|
Tx tx(std::uint8_t name, std::vector<std::uint8_t> spends = {}, std::size_t size = 100) {
|
|
Tx t;
|
|
t.id = id_of(name);
|
|
t.size = size;
|
|
for (auto s : spends) t.inputs.push_back(id_of(s));
|
|
return t;
|
|
}
|
|
|
|
void the_oldest_goes_first() {
|
|
Pool<Tx> p;
|
|
check(p.add(tx(1)).has_value(), "one is admitted");
|
|
check(p.add(tx(2)).has_value(), "and another");
|
|
check(p.add(tx(3)).has_value(), "and a third");
|
|
check(p.size() == 3, "the pool holds three");
|
|
|
|
const Tx* first = p.peek();
|
|
check(first != nullptr && first->id == id_of(1), "the oldest is the one that arrived first");
|
|
|
|
std::vector<std::uint8_t> seen;
|
|
p.each([&](const Tx& t) {
|
|
seen.push_back(t.id[0]);
|
|
return true;
|
|
});
|
|
check(seen.size() == 3 && seen[0] == 1 && seen[1] == 2 && seen[2] == 3,
|
|
"and the walk is in arrival order — the order two nodes must agree on");
|
|
|
|
int count = 0;
|
|
p.each([&](const Tx&) {
|
|
++count;
|
|
return false;
|
|
});
|
|
check(count == 1, "returning false stops the walk");
|
|
}
|
|
|
|
void the_four_refusals() {
|
|
Pool<Tx> p(1000);
|
|
check(p.add(tx(1, {0x10})).has_value(), "the first is admitted");
|
|
|
|
auto dup = p.add(tx(1, {0x11}));
|
|
check(!dup && dup.error() == Refusal::Duplicate, "the same name twice is a duplicate");
|
|
|
|
auto big = p.add(tx(2, {}, kMaxTxSize + 1));
|
|
check(!big && big.error() == Refusal::TooLarge, "one over the transaction bound is too large");
|
|
|
|
auto conflict = p.add(tx(3, {0x10}));
|
|
check(!conflict && conflict.error() == Refusal::Conflict,
|
|
"one spending an input already spoken for conflicts");
|
|
|
|
auto full = p.add(tx(4, {}, 5000));
|
|
check(!full && full.error() == Refusal::Full, "one bigger than the room left does not fit");
|
|
|
|
check(p.size() == 1, "and none of the four is holding space");
|
|
check(p.bytes_available() == 900, "the space is exactly what the one admitted left");
|
|
}
|
|
|
|
void space_comes_back() {
|
|
Pool<Tx> p(1000);
|
|
check(p.add(tx(1, {}, 400)).has_value() && p.add(tx(2, {}, 400)).has_value(),
|
|
"two large transactions fill most of it");
|
|
check(p.bytes_available() == 200, "leaving what they did not take");
|
|
p.erase(id_of(1));
|
|
check(p.bytes_available() == 600, "removing one gives its bytes back");
|
|
check(p.add(tx(3, {}, 500)).has_value(), "so something that would not have fit now does");
|
|
}
|
|
|
|
void removing_a_transaction_removes_its_rivals() {
|
|
Pool<Tx> p;
|
|
check(p.add(tx(1, {0x10})).has_value(), "one spending an output is pooled");
|
|
check(p.add(tx(2, {0x20})).has_value(), "and one spending another");
|
|
|
|
// A transaction that was never pooled is accepted in a block. It spends the
|
|
// same output as tx 1, so tx 1 can NEVER be accepted now.
|
|
p.remove({tx(9, {0x10})});
|
|
check(p.get(id_of(1)) == nullptr, "the rival of an accepted transaction is dropped");
|
|
check(p.get(id_of(2)) != nullptr, "and the one that rivals nothing stays");
|
|
check(p.size() == 1, "the pool is one smaller");
|
|
|
|
// Its input is free again, so a new spender of it is admissible.
|
|
check(p.add(tx(3, {0x10})).has_value(), "and the input it held is free again");
|
|
}
|
|
|
|
void a_removed_transaction_frees_its_own_inputs() {
|
|
Pool<Tx> p;
|
|
check(p.add(tx(1, {0x10, 0x11})).has_value(), "one spending two outputs");
|
|
p.remove({tx(1, {0x10, 0x11})});
|
|
check(p.empty(), "removing it by name empties the pool");
|
|
check(p.add(tx(2, {0x10})).has_value(), "and both of its inputs are free");
|
|
check(p.add(tx(3, {0x11})).has_value(), "both of them");
|
|
}
|
|
|
|
void refusals_are_remembered_and_forgotten() {
|
|
Dropped<std::string> d(3);
|
|
d.mark(id_of(1), "because");
|
|
d.mark(id_of(2), "because");
|
|
d.mark(id_of(3), "because");
|
|
check(d.why(id_of(1)).value_or("") == "because", "a refusal is remembered");
|
|
|
|
d.mark(id_of(4), "because");
|
|
check(d.size() == 3, "only so many are remembered");
|
|
check(!d.why(id_of(1)).has_value(), "and the oldest is the one forgotten");
|
|
check(d.why(id_of(4)).has_value(), "the newest is kept");
|
|
|
|
d.mark(id_of(2), "a better reason");
|
|
check(d.why(id_of(2)).value_or("") == "a better reason",
|
|
"marking one twice replaces the reason rather than the slot");
|
|
check(d.size() == 3, "and does not cost a second slot");
|
|
|
|
d.forget(id_of(2));
|
|
check(!d.why(id_of(2)).has_value(),
|
|
"a transaction that arrives is no longer a transaction that was refused");
|
|
check(d.size() == 2, "and its slot is free");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
std::printf("mempool\n");
|
|
the_oldest_goes_first();
|
|
the_four_refusals();
|
|
space_comes_back();
|
|
removing_a_transaction_removes_its_rivals();
|
|
a_removed_transaction_frees_its_own_inputs();
|
|
refusals_are_remembered_and_forgotten();
|
|
return report("mempool");
|
|
}
|