Files
MeshCore/test/fixtures/ota_source_proofs/test.cpp
T

289 lines
13 KiB
C++

using Workspace = SourceMerkleProof<4096, OTA_LEAVES_FRAG>;
static_assert(sizeof(Workspace) <= 1536, "bounded allocation includes getter scratch");
class LeafSource : public MotaSource {
public:
std::vector<uint8_t> bytes;
uint16_t quantum = 128;
unsigned calls = 0, fail_at = 0;
uint8_t root[4];
explicit LeafSource(uint32_t count) : bytes(17 + count * 4) {
for (uint32_t i = 0; i < count; ++i) {
uint8_t value[4]; wr_u32le(value, i * 8191u + count);
merkle_leaf(bytes.data() + 17 + i * 4, value, 4);
}
merkle_root(root, bytes.data() + 17, count);
}
uint8_t count() override { return 1; }
bool describe(uint8_t, MotaDesc&) override { return false; }
uint16_t read_quantum() const override { return quantum; }
bool read(uint8_t index, uint32_t offset, uint8_t* output, uint32_t length) override {
++calls;
assert(length <= quantum && length <= 128);
if (fail_at == calls) { if (length) output[0] = 0xEF; return false; }
if (index || uint64_t(offset) + length > bytes.size()) return false;
memcpy(output, bytes.data() + offset, length); return true;
}
};
static std::vector<std::vector<uint8_t>> reference_levels(const uint8_t* leaves, uint32_t count) {
std::vector<std::vector<uint8_t>> levels{{leaves, leaves + count * 4}};
while (count > 1) {
std::vector<uint8_t> next(((count + 1) / 2) * 4);
const auto& previous = levels.back();
for (uint32_t i = 0; i < count; i += 2) {
if (i + 1 < count) merkle_combine(next.data() + (i / 2) * 4,
previous.data() + i * 4, previous.data() + (i + 1) * 4);
else memcpy(next.data() + (i / 2) * 4, previous.data() + i * 4, 4);
}
levels.push_back(std::move(next)); count = (count + 1) / 2;
}
return levels;
}
static void grouped_equivalence() {
std::vector<uint32_t> counts;
for (uint32_t count = 1; count <= 65; ++count) counts.push_back(count);
for (uint32_t count : {127,128,129,255,256,257,511,512,513,1023,1024,1025,
2047,2048,2049,4095,4096}) counts.push_back(count);
size_t checked = 0;
for (uint32_t count : counts) {
LeafSource source(count);
source.quantum = count <= 65 ? uint16_t(std::array<int, 5>{1,3,17,127,128}[count % 5]) : 127;
Workspace workspace;
assert(workspace.bind(&source, 0, 17, count, source.root));
const auto expected = reference_levels(source.bytes.data() + 17, count);
for (uint32_t block = 0; block < count; ++block) {
uint8_t output[128]; uint8_t length = 99;
Workspace::Step step = Workspace::Step::Pending;
for (unsigned turn = 0; step == Workspace::Step::Pending && turn < 20000; ++turn) {
const auto before = source.calls;
step = workspace.proof(block, output, length);
assert(source.calls - before <= 1);
}
assert(step == Workspace::Step::Ready);
std::vector<uint8_t> proof;
uint32_t index = block;
for (const auto& row : expected) {
const uint32_t sibling = index ^ 1u;
if (sibling < row.size() / 4)
proof.insert(proof.end(), row.begin() + sibling * 4, row.begin() + sibling * 4 + 4);
index /= 2;
}
assert(proof.size() == size_t(length) * 4);
assert(proof.empty() || !memcmp(output, proof.data(), proof.size()));
assert(merkle_verify_from_leaf(source.bytes.data() + 17 + block * 4, block,
output, length, source.root, count));
const auto before = source.calls;
assert(workspace.proof(block, output, length) == Workspace::Step::Ready);
assert(source.calls == before); // exact retry never re-reads a pinned group
++checked;
}
}
printf("PASS: byte-identical grouped proofs (%zu blocks, max4096; workspace=%zu)\n", checked, sizeof(Workspace));
}
static void helper_faults() {
LeafSource source(65), other(65); source.quantum = 3; other.quantum = 17;
Workspace workspace;
assert(!workspace.bind(nullptr, 0, 17, 65, source.root));
assert(!workspace.bind(&source, 0, 17, 0, source.root));
assert(!workspace.bind(&source, 0, 17, 4097, source.root));
assert(!workspace.bind(&source, 0, UINT32_MAX, 65, source.root));
assert(workspace.bind(&source, 0, 17, 65, source.root));
assert(workspace.prepare() == Workspace::Step::Pending);
const auto warmed = source.calls;
const uint8_t* fragment = nullptr;
while (workspace.fragment(&other, 0, 17, 101, fragment) == Workspace::Step::Pending) {}
assert(fragment && !memcmp(fragment, other.bytes.data() + 17, 101));
const auto read = other.calls;
assert(workspace.fragment(&other, 0, 17, 101, fragment) == Workspace::Step::Ready);
assert(other.calls == read && source.calls == warmed);
workspace.releaseFragment();
source.fail_at = source.calls + 1;
assert(workspace.prepare() == Workspace::Step::Failed); // partial writes never authenticated
source.fail_at = 0;
assert(workspace.bind(&source, 0, 17, 65, source.root));
while (workspace.prepare() == Workspace::Step::Pending) {}
source.bytes[17] ^= 1; // source changed after the authenticated root cache was built
uint8_t proof[128], length = 0;
Workspace::Step step;
do { step = workspace.proof(0, proof, length); } while (step == Workspace::Step::Pending);
assert(step == Workspace::Step::Failed && length == 0);
source.bytes[17] ^= 1;
uint8_t wrong_root[4]; memcpy(wrong_root, source.root, 4); wrong_root[0] ^= 1;
assert(workspace.bind(&source, 0, 17, 65, wrong_root));
do { step = workspace.prepare(); } while (step == Workspace::Step::Pending);
assert(step == Workspace::Step::Failed);
source.quantum = 0;
assert(workspace.bind(&source, 0, 17, 65, source.root));
assert(workspace.prepare() == Workspace::Step::Failed);
workspace.reset();
assert(workspace.proof(0, proof, length) == Workspace::Step::Failed);
puts("PASS: partial reads and source changes");
}
class TrackedSource : public Source {
public:
using Source::Source;
uint16_t quantum = 17;
uint32_t leaf_bytes = 0, leaf_calls = 0;
bool fail_leaves = false;
uint16_t read_quantum() const override { return quantum; }
bool read(uint8_t index, uint32_t offset, uint8_t* output, uint32_t length) override {
const uint32_t first = 8 + MOTA_MFL, last = first + manifest.block_count * 4;
if (offset >= first && offset < last) {
assert(length <= quantum && length <= 128 && offset + length <= last);
++leaf_calls; leaf_bytes += length;
if (fail_leaves) { if (length) output[0] = 0xEE; return false; }
}
return Source::read(index, offset, output, length);
}
};
static void manager_lifetimes() {
TrackedSource a(4096, 0x73), b(65, 0x56);
a.host_encoding = b.host_encoding = Source::HostEncoding::Good;
a.fail_payload = true;
Sent sent;
{
OtaManager manager; manager.begin(123, send, &sent);
assert(manager.add_source(&a) && manager.add_source(&b));
assert(request(manager, a, 4095));
assert(allocated == 0); // receiving a request never allocates a full leaf table
sent.accept_data = sent.accept_proof = false;
manager.serviceEgress(); // cache host-generated DATA
assert(a.host_calls == 1);
for (unsigned i = 0; i < 20; ++i) {
const auto before = a.leaf_calls;
manager.serviceEgress(); assert(a.leaf_calls - before <= 1);
}
assert(allocated == sizeof(Workspace) && peak <= 1536);
// A different MID's manifest and getter must not restart A's warming.
assert(manifest_request(manager, b));
for (unsigned i = 0; i < 20 && manager.pendingManifestJobs(); ++i) {
manager.set_clock(100000 + i * 10000); manager.serviceEgress();
}
assert(!manager.pendingManifestJobs());
GetLeavesMsg get{}; memcpy(get.manifest_id, b.manifest.merkle_root, 4); get.want_mask = 1;
uint8_t wire[MAX_PACKET_PAYLOAD];
assert(manager.on_message(wire, encode_get_leaves(wire, sizeof(wire), get)));
sent.accept_manifest = false;
for (unsigned i = 0; i < 30 && manager.pendingManifestJobs(); ++i) {
const auto before = b.leaf_calls;
manager.set_clock(400000 + i * 10000); manager.serviceEgress();
assert(b.leaf_calls - before <= 1);
}
assert(manager.pendingManifestJobs());
const auto getter_reads = b.leaf_calls;
for (unsigned i = 0; i < 10; ++i) manager.serviceEgress();
assert(b.leaf_calls == getter_reads); // a rejected getter emit pins its exact fragment
sent.accept_manifest = true;
manager.serviceEgress();
assert(!manager.pendingManifestJobs());
bool getter_seen = false;
for (const auto& packet : sent.packets) {
LeavesMsg leaves{};
if (decode_leaves(packet.data(), packet.size(), leaves)) {
assert(leaves.len == OTA_LEAVES_FRAG &&
!memcmp(leaves.bytes, b.manifest.leaves, leaves.len));
getter_seen = true;
}
}
assert(getter_seen);
sent.accept_data = true;
for (unsigned i = 0; i < 2000; ++i) {
const auto before = a.leaf_calls;
manager.set_clock(800000 + i * 1000); manager.serviceEgress();
assert(a.leaf_calls - before <= 1);
}
assert(manager.pendingServeJobs() && a.host_calls == 1);
const auto before = a.leaf_calls;
for (unsigned i = 0; i < 10; ++i) manager.serviceEgress();
assert(a.leaf_calls == before); // ready proof held under emit backpressure
sent.accept_proof = true;
manager.serviceEgress();
assert(!manager.pendingServeJobs());
verify_v2(sent, a, 4095, true);
manager.set_clock(5000000); manager.loop();
assert(allocated == 0); // idle timer releases the only workspace without an encoder scratch
sent.accept_data = false;
assert(request(manager, a, 4095));
fail_next = true;
manager.serviceEgress(); manager.serviceEgress();
assert(!manager.pendingServeJobs() && !fail_next && allocated == 0);
a.fail_leaves = true;
assert(request(manager, a, 4095));
manager.serviceEgress(); manager.serviceEgress();
assert(!manager.pendingServeJobs());
manager.clear_sources(); assert(allocated == 0);
a.fail_leaves = false;
assert(manager.add_source(&a) && request(manager, a, 4095));
manager.serviceEgress(); manager.serviceEgress();
assert(allocated == sizeof(Workspace));
assert(manager.remove_source(&a)); assert(allocated == 0 && !manager.pendingServeJobs());
}
assert(allocated == 0);
puts("PASS: bounded manager ownership and backpressure");
}
static void simultaneous_capture() {
for (bool host : {false, true}) {
TrackedSource outgoing(5, 0x40);
Source incoming(3, 0x70);
if (host) {
outgoing.host_encoding = Source::HostEncoding::Good;
outgoing.host_encoded_size = 400;
}
Sent sent; OtaManager manager; OtaStoreRam<16384> store;
manager.begin(123, send, &sent); manager.set_fetch_store(&store);
manager.set_transport_deflate_encoder(encode); manager.set_transport_deflate_decoder(decode);
assert(manager.add_source(&outgoing) && request(manager, outgoing));
sent.accept_data = false;
manager.serviceEgress(); manager.serviceEgress();
const size_t expected_allocation = sizeof(Workspace) + (host ? 0 : OTA_MAX_BLOCK);
assert(allocated == expected_allocation);
assert(manager.pull_archive(incoming.manifest.merkle_root, 123, false) == OtaManager::PULL_STARTED);
for (unsigned fragment = 0; fragment < 2; ++fragment) {
ManifestMsg mf{}; memcpy(mf.manifest_id, incoming.manifest.merkle_root, 4);
mf.frag_idx = fragment; mf.frag_total = 2;
mf.bytes = incoming.manifest.manifest_start + fragment * OTA_MF_FRAG;
mf.len = fragment == 0 ? OTA_MF_FRAG : MOTA_MFL - OTA_MF_FRAG;
uint8_t wire[MAX_PACKET_PAYLOAD];
manager.on_message(wire, encode_manifest(wire, sizeof(wire), mf));
}
assert(manager.fetchState() == OtaManager::FETCHING);
for (uint16_t block = 0; block < 3; ++block) {
uint8_t representation[12]; memset(representation + 4, uint8_t(0x70 + block), 8);
mh4(representation, representation + 4, 8);
DataMsg data{}; memcpy(data.manifest_id, incoming.manifest.merkle_root, 4); data.block_idx = block;
assert(ota_data_v2_pack_extended(0, 8, data.frag_off));
data.data = representation; data.data_len = sizeof(representation);
uint8_t wire[MAX_PACKET_PAYLOAD];
assert(manager.on_message(wire, encode_data(wire, sizeof(wire), data)));
uint8_t siblings[128]; ProofMsg proof{};
memcpy(proof.manifest_id, incoming.manifest.merkle_root, 4); proof.block_idx = block;
proof.n_proof = merkle_gen_proof(incoming.manifest.leaves, 3, block, nullptr, siblings);
proof.proof = siblings;
assert(manager.on_message(wire, encode_proof(wire, sizeof(wire), proof)));
manager.set_clock(100000 + block * 10000); manager.loop();
assert(allocated == expected_allocation && manager.pendingServeJobs());
}
assert(manager.fetchState() == OtaManager::COMPLETE);
MotaManifest staged; assert(mota_parse(store.data(), store.staged_size(), staged));
assert(!memcmp(staged.payload, incoming.manifest.payload, incoming.manifest.payload_size));
sent.accept_data = true; drain(manager, 200000);
if (host) verify_v2(sent, outgoing, 0, true, 400);
else verify(sent, outgoing, true);
manager.clear_sources(); assert(allocated == 0);
}
puts("PASS: simultaneous capture and serving");
}
int main() {
grouped_equivalence(); helper_faults(); manager_lifetimes(); simultaneous_capture();
assert(allocated == 0);
return 0;
}