#ifdef TEST_PSRAM #define BOARD_HAS_PSRAM 1 #endif #define MAX_NEIGHBOURS 254 #define PUB_KEY_SIZE 32 #define MAX_CLIENTS 32 #define PATH_HASH_SIZE 1 #define WITH_MQTT_NEIGHBORS 1 #define MESH_DEBUG_PRINTLN(...) ((void)0) #define PERM_ACL_ROLE_MASK 3 #define PERM_ACL_GUEST 0 #include #include #include #include #include #include #include #include #include #include #include namespace mesh { struct Identity { uint8_t pub_key[32]; @HASH_MATCH@ }; struct LocalIdentity : Identity { mutable unsigned derivations=0; // A deterministic crypto dependency records which immutable key the real // getPeerSharedSecret body selected; this is a routing test, not an ECDH test. void calcSharedSecret(uint8_t* out,const Identity& other) const { ++derivations; for(size_t i=0;i<32;++i) out[i]=uint8_t(other.pub_key[i]^0xA5); } }; struct Packet {}; struct Utils { static void toHex(char* out, const uint8_t* key, size_t size) { for (size_t i=0;i clients; ClientInfo* getClient(const uint8_t* key,size_t size) { for(auto& client:clients) if(!memcmp(key,client.id.pub_key,size)) return &client; return nullptr; } int getNumClients() const { return int(clients.size()); } ClientInfo* getClientByIdx(int i) { return &clients.at(size_t(i)); } }; struct GuestLogin { bool value=false; bool enabled() const { return value; } }; struct NeighbourInfo { mesh::Identity id; uint32_t heard_timestamp; int8_t snr; int16_t rssi; }; static bool fail_scratch=false, fail_json=false; static unsigned allocation=0; static void* neighborsAlloc(size_t size) { ++allocation; if (!size || (allocation==1 && fail_scratch) || (allocation==2 && fail_json)) return nullptr; return malloc(size); } static void neighborsFree(void* ptr) { free(ptr); } static uint32_t futureMillis(uint32_t value) { return value+10; } static bool millisHasNowPassed(uint32_t) { return true; } static const uint32_t NEIGHBOR_DISCOVER_QUEUE_TIMEOUT_MS=100; @HELPERS@ struct MyMesh { enum { ND_UNSENT, ND_QUEUED, ND_PENDING, ND_RESPONDED, ND_TIMEOUT, ND_SEND_FAILED }; @LAYOUT@ @STATE@ }; struct LegacyMesh { enum { ND_UNSENT, ND_QUEUED, ND_PENDING, ND_RESPONDED, ND_TIMEOUT, ND_SEND_FAILED }; @LEGACY_LAYOUT@ @STATE@ }; @METHODS@ @LEGACY_METHODS@ template std::string runPass(Mesh& m,int count,int result,size_t scope,bool admission=true) { m.admit=admission; m.requested=0; m.touched=0; m.mock_bridge.last_json.clear(); memset(m.neighbours,0,sizeof(m.neighbours)); for (int i=0;i(m.neighbor_discover_scopes)[i]==0); } int steps=0; while(m.neighbor_discover_active) { assert(++steps<1024); auto& entry=m.neighbor_discover_metadata[m.neighbor_discover_next]; if(entry.status==Mesh::ND_QUEUED) { // The real TX hook starts the reply timeout, not queue admission. if(result==Mesh::ND_SEND_FAILED) m.markFailed(); else m.markTransmitted(); if(result==Mesh::ND_RESPONDED) { std::vector response(8+scope,'x'); memcpy(response.data(),&entry.tag,4); assert(m.handleNeighborDiscoverResponse(m.neighbor_discover_next, response.data(),response.size(),5.0f,-99)); } } m.loopNeighborDiscover(); } assert(m.neighbor_discover_count==0 && m.neighbor_discover_next==0); assert(m.neighbor_discover_publish_count==0 && m.neighbor_discover_queried_count==0); assert(!m.neighbor_discover_request && m.neighbor_discover_until==0); assert(m.next_neighbors_publish==60010); assert(m.mock_bridge.publishes==old_publishes+1); JsonDocument document; assert(!deserializeJson(document,m.mock_bridge.last_json)); assert(document["total_neighbors"].template as()==count); if(count>0) assert(document["neighbors"][0]["pubkey"].template as()!=nullptr); assert(document["neighbors"].template as().size()<=size_t(count)); if(count>MQTTBridge::NEIGHBORS_MAX_PUBLISH_ENTRIES) assert(document["truncated"].template as()); return m.mock_bridge.last_json; } struct Snapshot { mesh::Identity id; uint32_t heard; int8_t snr; int16_t rssi; }; // The old full-entry model snapshots every admitted row, then stable-sorts the // complete rows. Compare the new indexed snapshot against an independent // standard-library implementation rather than a second copy of its loop. template void checkOverlay(bool collision,bool guests) { Mesh m={}; m.guest_login.value=guests; std::vector previous; for(int i=0;i<254;++i) { auto& row=m.neighbours[i]; for(size_t j=0;j<32;++j) row.id.pub_key[j]=uint8_t(i+j*13); row.id.pub_key[0]=collision ? 0x42 : uint8_t(i%16); row.heard_timestamp=1799999900+uint32_t((i*37)%11); row.snr=int8_t((i*7)%8); row.rssi=int16_t(-80-i%20); previous.push_back({row.id,row.heard_timestamp,row.snr,row.rssi}); } // Add unheard rows too: only the positive-time source rows enter discovery. if(collision) { m.neighbours[42].heard_timestamp=0; previous.erase(previous.begin()+42); } std::stable_sort(previous.begin(),previous.end(),[](const Snapshot& lhs,const Snapshot& rhs) { if(lhs.heard!=rhs.heard) return lhs.heard>rhs.heard; if(lhs.snr!=rhs.snr) return lhs.snr>rhs.snr; return memcmp(lhs.id.pub_key,rhs.id.pub_key,32)<0; }); for(size_t i : {size_t(0),size_t(17),size_t(73),size_t(200),size_t(252)}) { ClientInfo client={};client.id=previous[i].id; client.permissions=i%2 ? PERM_ACL_GUEST : 1; memset(client.shared_secret,uint8_t(0x30+i%16),32); m.acl.clients.push_back(client); } char reply[160]={};allocation=0; assert(m.startNeighborDiscover(reply)); assert(m.neighbor_discover_count==previous.size()); #ifdef TEST_LOST_KEY_TAIL for(size_t i=Mesh::NEIGHBOR_SCOPE_RESULTS;i old_matches; if(active) for(size_t i=0;i=Mesh::NEIGHBOR_DISCOVER_PEER_BASE) { const auto& id=previous[size_t(match-Mesh::NEIGHBOR_DISCOVER_PEER_BASE)].id; for(size_t b=0;b<32;++b) expected[b]=uint8_t(id.pub_key[b]^0xA5); } else memcpy(expected,m.acl.clients[size_t(match)].shared_secret,32); assert(!memcmp(actual,expected,32)); } } } m.neighbor_discover_active=true; // Exercise every sorted identity directly, including keys beyond the // publishable/query prefix and keys hidden behind the collision-result cap. const unsigned before=m.self_id.derivations; for(size_t i=0;i(collision,guests); checkOverlay(collision,guests); } for(int count : {0,1,19,20,21,30,71,72,73,253,254}) for(int result : {MyMesh::ND_RESPONDED,MyMesh::ND_TIMEOUT,MyMesh::ND_SEND_FAILED}) for(size_t scope : {size_t(0),size_t(95),size_t(180)}) { MyMesh current={}; LegacyMesh baseline={}; for(bool admitted : {true,false}) { auto actual=runPass(current,count,result,scope,admitted); auto previous=runPass(baseline,count,result,scope,admitted); assert(actual==previous && current.requested==baseline.requested); assert(current.touched==baseline.touched); } } // Failed publish allocation still resets the complete discovery lifetime; // the next pass starts with empty scopes and cannot retain stale replies. for(bool scratch : {true,false}) { MyMesh current={}; current.neighbours[0].heard_timestamp=1799999999; fail_scratch=scratch; fail_json=!scratch; allocation=0; char reply[160]={}; assert(current.startNeighborDiscover(reply)); current.markTransmitted(); for (int i=0; current.neighbor_discover_active && i<4; ++i) current.loopNeighborDiscover(); assert(!current.neighbor_discover_active && current.mock_bridge.publishes==0); assert(current.neighbor_discover_count==0 && !current.neighbor_discover_request); fail_scratch=fail_json=false; runPass(current,1,MyMesh::ND_TIMEOUT,0); } puts("real neighbor serialization and lifecycle checks passed"); }