Add more SFPP config values

This commit is contained in:
Jonathan Bennett
2025-12-27 21:21:51 -06:00
parent 428b839254
commit a8a5086b6d
4 changed files with 143 additions and 102 deletions

View File

@@ -1,14 +1,9 @@
// I've done a lot of this in SQLite for now, but honestly it needs to happen in memory, and get saved to sqlite during downtime // I've done a lot of this in SQLite for now, but honestly it needs to happen in memory, and get saved to sqlite during downtime
// TODO: Message asking for the message x spots from the tip of the chain.
// This would allow individual nodes to apt in, grab the latest fe messages, and not need to sync the entire chain
// TODO: custom hops. 1 maybe 0. Configurable? // TODO: custom hops. 1 maybe 0. Configurable?
// TODO: non-stratum0 nodes need to be pointed at their upstream source? Maybe // TODO: non-stratum0 nodes need to be pointed at their upstream source? Maybe
// TODO: There will come a point where the chain is too big to sync
// TODO: evict messages from scratch after a timeout // TODO: evict messages from scratch after a timeout
// things may get weird if there are multiple stratum-0 nodes on a single mesh. Come up with mitigations // things may get weird if there are multiple stratum-0 nodes on a single mesh. Come up with mitigations
@@ -29,8 +24,16 @@ StoreForwardPlusPlusModule::StoreForwardPlusPlusModule()
if (portduino_config.sfpp_stratum0) if (portduino_config.sfpp_stratum0)
LOG_WARN("SF++ stratum0"); LOG_WARN("SF++ stratum0");
std::string db_path = portduino_config.sfpp_db_path + "storeforwardpp.db";
LOG_WARN("Opening SF++ DB at %s", db_path.c_str());
int res = sqlite3_open("test.db", &ppDb); int res = sqlite3_open(db_path.c_str(), &ppDb);
if (res != SQLITE_OK) {
LOG_ERROR("Cannot open database: %s", sqlite3_errmsg(ppDb));
sqlite3_close(ppDb);
ppDb = nullptr;
exit(EXIT_FAILURE);
}
LOG_WARN("Result1 %u", res); LOG_WARN("Result1 %u", res);
char *err = nullptr; char *err = nullptr;
@@ -166,30 +169,35 @@ StoreForwardPlusPlusModule::StoreForwardPlusPlusModule()
encryptedOk = true; encryptedOk = true;
// wait about 15 seconds after boot for the first runOnce() this->setInterval(portduino_config.sfpp_announce_interval * 60 * 1000);
// TODO: When not doing active development, adjust this to a longer time
this->setInterval(15 * 1000);
} }
int32_t StoreForwardPlusPlusModule::runOnce() int32_t StoreForwardPlusPlusModule::runOnce()
{ {
// get number of links on chain
// if more than max_chain, evict oldest
LOG_WARN("StoreForward++ runONce"); LOG_WARN("StoreForward++ runONce");
pendingRun = false; pendingRun = false;
if (getRTCQuality() < RTCQualityNTP) { if (getRTCQuality() < RTCQualityNTP) {
LOG_WARN("StoreForward++ deferred due to time quality %u", getRTCQuality()); LOG_WARN("StoreForward++ deferred due to time quality %u", getRTCQuality());
return 5 * 60 * 1000; return portduino_config.sfpp_announce_interval * 60 * 1000;
} }
uint8_t root_hash_bytes[32] = {0}; uint8_t root_hash_bytes[SFPP_HASH_SIZE] = {0};
ChannelHash hash = channels.getHash(0); ChannelHash hash = channels.getHash(0);
getOrAddRootFromChannelHash(hash, root_hash_bytes); getOrAddRootFromChannelHash(hash, root_hash_bytes);
uint32_t chain_count = getChainCount(root_hash_bytes, SFPP_HASH_SIZE);
LOG_WARN("Chain count is %u", chain_count);
if (chain_count > portduino_config.sfpp_max_chain) {
LOG_WARN("Chain length %u exceeds max %u, evicting oldest", chain_count, portduino_config.sfpp_max_chain);
}
if (memfll(root_hash_bytes, '\0', 32)) { if (memfll(root_hash_bytes, '\0', SFPP_HASH_SIZE)) {
LOG_WARN("No root hash found, not sending"); LOG_WARN("No root hash found, not sending");
return 5 * 60 * 1000; return portduino_config.sfpp_announce_interval * 60 * 1000;
} }
// get tip of chain for this channel // get tip of chain for this channel
link_object chain_end = getLinkFromCount(0, root_hash_bytes, 32); link_object chain_end = getLinkFromCount(0, root_hash_bytes, SFPP_HASH_SIZE);
LOG_WARN("latest payload %s", chain_end.payload.c_str()); LOG_WARN("latest payload %s", chain_end.payload.c_str());
if (chain_end.rx_time == 0) { if (chain_end.rx_time == 0) {
@@ -198,8 +206,8 @@ int32_t StoreForwardPlusPlusModule::runOnce()
// first attempt at a chain-only announce with no messages // first attempt at a chain-only announce with no messages
meshtastic_StoreForwardPlusPlus storeforward = meshtastic_StoreForwardPlusPlus_init_zero; meshtastic_StoreForwardPlusPlus storeforward = meshtastic_StoreForwardPlusPlus_init_zero;
storeforward.sfpp_message_type = meshtastic_StoreForwardPlusPlus_SFPP_message_type_CANON_ANNOUNCE; storeforward.sfpp_message_type = meshtastic_StoreForwardPlusPlus_SFPP_message_type_CANON_ANNOUNCE;
storeforward.root_hash.size = 32; storeforward.root_hash.size = SFPP_HASH_SIZE;
memcpy(storeforward.root_hash.bytes, root_hash_bytes, 32); memcpy(storeforward.root_hash.bytes, root_hash_bytes, SFPP_HASH_SIZE);
storeforward.encapsulated_rxtime = 0; storeforward.encapsulated_rxtime = 0;
// storeforward. // storeforward.
@@ -208,17 +216,19 @@ int32_t StoreForwardPlusPlusModule::runOnce()
p->decoded.want_response = false; p->decoded.want_response = false;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
p->channel = 0; p->channel = 0;
p->hop_limit = portduino_config.sfpp_hops;
p->hop_start = portduino_config.sfpp_hops;
LOG_INFO("Send packet to mesh"); LOG_INFO("Send packet to mesh");
service->sendToMesh(p, RX_SRC_LOCAL, true); service->sendToMesh(p, RX_SRC_LOCAL, true);
return 5 * 60 * 1000; return portduino_config.sfpp_announce_interval * 60 * 1000;
} }
// broadcast the tip of the chain // broadcast the tip of the chain
canonAnnounce(chain_end.message_hash, chain_end.commit_hash, root_hash_bytes, chain_end.rx_time); canonAnnounce(chain_end.message_hash, chain_end.commit_hash, root_hash_bytes, chain_end.rx_time);
// eventually timeout things on the scratch queue // eventually timeout things on the scratch queue
return 5 * 60 * 1000; return portduino_config.sfpp_announce_interval * 60 * 1000;
} }
ProcessMessage StoreForwardPlusPlusModule::handleReceived(const meshtastic_MeshPacket &mp) ProcessMessage StoreForwardPlusPlusModule::handleReceived(const meshtastic_MeshPacket &mp)
@@ -296,27 +306,25 @@ bool StoreForwardPlusPlusModule::handleReceivedProtobuf(const meshtastic_MeshPac
// TODO: Regardless of where we are in the chain, if we have a newer message, send it back. // TODO: Regardless of where we are in the chain, if we have a newer message, send it back.
if (portduino_config.sfpp_stratum0) { if (portduino_config.sfpp_stratum0) {
LOG_WARN("Received a CANON_ANNOUNCE while stratum 0"); LOG_WARN("Received a CANON_ANNOUNCE while stratum 0");
uint8_t next_commit_hash[32] = {0}; uint8_t next_commit_hash[SFPP_HASH_SIZE] = {0};
if (getNextHash(t->root_hash.bytes, t->root_hash.size, t->commit_hash.bytes, t->commit_hash.size, next_commit_hash)) { if (getNextHash(t->root_hash.bytes, t->root_hash.size, t->commit_hash.bytes, t->commit_hash.size, next_commit_hash)) {
printBytes("next chain hash: ", next_commit_hash, 32); printBytes("next chain hash: ", next_commit_hash, SFPP_HASH_SIZE);
if (airTime->isTxAllowedChannelUtil(true)) { if (airTime->isTxAllowedChannelUtil(true)) {
broadcastLink(next_commit_hash, 32); broadcastLink(next_commit_hash, SFPP_HASH_SIZE);
} }
} }
} else { } else {
uint8_t tmp_root_hash_bytes[32] = {0}; uint8_t tmp_root_hash_bytes[SFPP_HASH_SIZE] = {0};
LOG_WARN("Received a CANON_ANNOUNCE"); LOG_WARN("Received a CANON_ANNOUNCE");
if (getRootFromChannelHash(router->p_encrypted->channel, tmp_root_hash_bytes)) { if (getRootFromChannelHash(router->p_encrypted->channel, tmp_root_hash_bytes)) {
// we found the hash, check if it's the right one // we found the hash, check if it's the right one
// TODO handle oddball sizes here
if (memcmp(tmp_root_hash_bytes, t->root_hash.bytes, t->root_hash.size) != 0) { if (memcmp(tmp_root_hash_bytes, t->root_hash.bytes, t->root_hash.size) != 0) {
LOG_WARN("Found root hash, and it doesn't match!"); LOG_WARN("Found root hash, and it doesn't match!");
return true; return true;
} }
} else { } else {
// TODO: size check // TODO: size check
// TODO: make sure we don't already have a root hash for this 1-byte channel hash
addRootToMappings(router->p_encrypted->channel, t->root_hash.bytes); addRootToMappings(router->p_encrypted->channel, t->root_hash.bytes);
LOG_WARN("Adding root hash to mappings"); LOG_WARN("Adding root hash to mappings");
} }
@@ -354,19 +362,19 @@ bool StoreForwardPlusPlusModule::handleReceivedProtobuf(const meshtastic_MeshPac
} }
} }
if (airTime->isTxAllowedChannelUtil(true)) { if (airTime->isTxAllowedChannelUtil(true)) {
requestNextMessage(t->root_hash.bytes, t->root_hash.size, chain_end.commit_hash, 32); requestNextMessage(t->root_hash.bytes, t->root_hash.size, chain_end.commit_hash, SFPP_HASH_SIZE);
} }
} }
} else { // if chainEnd() } else { // if chainEnd()
LOG_WARN("No Messages on this chain, request!"); LOG_WARN("No Messages on this chain, request!");
// todo request using portduino config initial_sync // todo request using portduino config initial_sync
if (airTime->isTxAllowedChannelUtil(true)) { if (airTime->isTxAllowedChannelUtil(true)) {
requestMessageCount(t->root_hash.bytes, t->root_hash.size, portduino_config.initial_sync); requestMessageCount(t->root_hash.bytes, t->root_hash.size, portduino_config.sfpp_initial_sync);
} }
} }
} }
} else if (t->sfpp_message_type == meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_REQUEST) { } else if (t->sfpp_message_type == meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_REQUEST) {
uint8_t next_commit_hash[32] = {0}; uint8_t next_commit_hash[SFPP_HASH_SIZE] = {0};
LOG_WARN("Received link request"); LOG_WARN("Received link request");
@@ -379,9 +387,9 @@ bool StoreForwardPlusPlusModule::handleReceivedProtobuf(const meshtastic_MeshPac
} else if (getNextHash(t->root_hash.bytes, t->root_hash.size, t->commit_hash.bytes, t->commit_hash.size, } else if (getNextHash(t->root_hash.bytes, t->root_hash.size, t->commit_hash.bytes, t->commit_hash.size,
next_commit_hash)) { next_commit_hash)) {
printBytes("next chain hash: ", next_commit_hash, 32); printBytes("next chain hash: ", next_commit_hash, SFPP_HASH_SIZE);
broadcastLink(next_commit_hash, 32); broadcastLink(next_commit_hash, SFPP_HASH_SIZE);
} }
// if root and chain hashes are the same, grab the first message on the chain // if root and chain hashes are the same, grab the first message on the chain
@@ -422,7 +430,7 @@ bool StoreForwardPlusPlusModule::handleReceivedProtobuf(const meshtastic_MeshPac
} }
} else { } else {
if (incoming_link.commit_hash_len == 32) { if (incoming_link.commit_hash_len == SFPP_HASH_SIZE) {
addToChain(incoming_link); addToChain(incoming_link);
if (isInScratch(incoming_link.message_hash, incoming_link.message_hash_len)) { if (isInScratch(incoming_link.message_hash, incoming_link.message_hash_len)) {
link_object scratch_object = getFromScratch(incoming_link.message_hash, incoming_link.message_hash_len); link_object scratch_object = getFromScratch(incoming_link.message_hash, incoming_link.message_hash_len);
@@ -462,7 +470,7 @@ bool StoreForwardPlusPlusModule::getRootFromChannelHash(ChannelHash _ch_hash, ui
uint8_t *tmp_root_hash = (uint8_t *)sqlite3_column_blob(getRootFromChannelHashStmt, 0); uint8_t *tmp_root_hash = (uint8_t *)sqlite3_column_blob(getRootFromChannelHashStmt, 0);
if (tmp_root_hash) { if (tmp_root_hash) {
LOG_WARN("Found root hash!"); LOG_WARN("Found root hash!");
memcpy(_root_hash, tmp_root_hash, 32); memcpy(_root_hash, tmp_root_hash, SFPP_HASH_SIZE);
found = true; found = true;
} }
sqlite3_reset(getRootFromChannelHashStmt); sqlite3_reset(getRootFromChannelHashStmt);
@@ -479,13 +487,13 @@ ChannelHash StoreForwardPlusPlusModule::getChannelHashFromRoot(uint8_t *_root_ha
return tmp_hash; return tmp_hash;
} }
// return code indicates newly created chain // return code indicates bytes in root hash, or 0 if not found/added
bool StoreForwardPlusPlusModule::getOrAddRootFromChannelHash(ChannelHash _ch_hash, uint8_t *_root_hash) size_t StoreForwardPlusPlusModule::getOrAddRootFromChannelHash(ChannelHash _ch_hash, uint8_t *_root_hash)
{ {
LOG_WARN("getOrAddRootFromChannelHash()"); LOG_WARN("getOrAddRootFromChannelHash()");
bool isNew = !getRootFromChannelHash(_ch_hash, _root_hash); bool wasFound = getRootFromChannelHash(_ch_hash, _root_hash);
if (isNew) { if (!wasFound) {
if (portduino_config.sfpp_stratum0) { if (portduino_config.sfpp_stratum0) {
LOG_WARN("Generating Root hash!"); LOG_WARN("Generating Root hash!");
// generate root hash // generate root hash
@@ -495,17 +503,21 @@ bool StoreForwardPlusPlusModule::getOrAddRootFromChannelHash(ChannelHash _ch_has
root_hash.update(&ourNode, sizeof(ourNode)); root_hash.update(&ourNode, sizeof(ourNode));
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true);
root_hash.update(&rtc_sec, sizeof(rtc_sec)); root_hash.update(&rtc_sec, sizeof(rtc_sec));
root_hash.finalize(_root_hash, 32); root_hash.finalize(_root_hash, SFPP_HASH_SIZE);
addRootToMappings(_ch_hash, _root_hash); addRootToMappings(_ch_hash, _root_hash);
wasFound = true;
} }
} }
return isNew; if (wasFound)
return SFPP_HASH_SIZE;
else
return 0;
} }
void StoreForwardPlusPlusModule::addRootToMappings(ChannelHash _ch_hash, uint8_t *_root_hash) void StoreForwardPlusPlusModule::addRootToMappings(ChannelHash _ch_hash, uint8_t *_root_hash)
{ {
LOG_WARN("addRootToMappings()"); LOG_WARN("addRootToMappings()");
printBytes("_root_hash", _root_hash, 32); printBytes("_root_hash", _root_hash, SFPP_HASH_SIZE);
// write to the table // write to the table
int type = chain_types::channel_chain; int type = chain_types::channel_chain;
@@ -513,7 +525,7 @@ void StoreForwardPlusPlusModule::addRootToMappings(ChannelHash _ch_hash, uint8_t
sqlite3_bind_int(addRootToMappingsStmt, 1, type); sqlite3_bind_int(addRootToMappingsStmt, 1, type);
sqlite3_bind_int(addRootToMappingsStmt, 2, _ch_hash); sqlite3_bind_int(addRootToMappingsStmt, 2, _ch_hash);
sqlite3_bind_blob(addRootToMappingsStmt, 3, _root_hash, 32, NULL); sqlite3_bind_blob(addRootToMappingsStmt, 3, _root_hash, SFPP_HASH_SIZE, NULL);
auto rc = sqlite3_step(addRootToMappingsStmt); auto rc = sqlite3_step(addRootToMappingsStmt);
LOG_WARN("result %u, %s", rc, sqlite3_errmsg(ppDb)); LOG_WARN("result %u, %s", rc, sqlite3_errmsg(ppDb));
sqlite3_reset(addRootToMappingsStmt); sqlite3_reset(addRootToMappingsStmt);
@@ -542,6 +554,8 @@ void StoreForwardPlusPlusModule::requestNextMessage(uint8_t *_root_hash, size_t
p->decoded.want_response = false; p->decoded.want_response = false;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
p->channel = 0; p->channel = 0;
p->hop_limit = portduino_config.sfpp_hops;
p->hop_start = portduino_config.sfpp_hops;
LOG_INFO("Send packet to mesh"); LOG_INFO("Send packet to mesh");
service->sendToMesh(p, RX_SRC_LOCAL, true); service->sendToMesh(p, RX_SRC_LOCAL, true);
} }
@@ -566,6 +580,8 @@ void StoreForwardPlusPlusModule::requestMessageCount(uint8_t *_root_hash, size_t
p->decoded.want_response = false; p->decoded.want_response = false;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
p->channel = 0; p->channel = 0;
p->hop_limit = portduino_config.sfpp_hops;
p->hop_start = portduino_config.sfpp_hops;
LOG_INFO("Send packet to mesh"); LOG_INFO("Send packet to mesh");
service->sendToMesh(p, RX_SRC_LOCAL, true); service->sendToMesh(p, RX_SRC_LOCAL, true);
} }
@@ -595,8 +611,8 @@ bool StoreForwardPlusPlusModule::getNextHash(uint8_t *_root_hash, size_t _root_h
sqlite3_reset(getNextHashStmt); sqlite3_reset(getNextHashStmt);
return false; return false;
} }
printBytes("commit_hash", tmp_commit_hash, 32); printBytes("commit_hash", tmp_commit_hash, SFPP_HASH_SIZE);
memcpy(next_commit_hash, tmp_commit_hash, 32); memcpy(next_commit_hash, tmp_commit_hash, SFPP_HASH_SIZE);
next_hash = true; next_hash = true;
} else { } else {
bool found_hash = false; bool found_hash = false;
@@ -608,7 +624,7 @@ bool StoreForwardPlusPlusModule::getNextHash(uint8_t *_root_hash, size_t _root_h
if (found_hash) { if (found_hash) {
LOG_WARN("Found hash"); LOG_WARN("Found hash");
memcpy(next_commit_hash, tmp_commit_hash, 32); memcpy(next_commit_hash, tmp_commit_hash, SFPP_HASH_SIZE);
next_hash = true; next_hash = true;
break; break;
} }
@@ -660,10 +676,13 @@ void StoreForwardPlusPlusModule::broadcastLink(uint8_t *_commit_hash, size_t _co
p->decoded.want_response = false; p->decoded.want_response = false;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
p->channel = 0; p->channel = 0;
p->hop_limit = portduino_config.sfpp_hops;
p->hop_start = portduino_config.sfpp_hops;
LOG_INFO("Send link to mesh"); LOG_INFO("Send link to mesh");
service->sendToMesh(p, RX_SRC_LOCAL, true); service->sendToMesh(p, RX_SRC_LOCAL, true);
} }
// TODO if stratum0, send the chain count
void StoreForwardPlusPlusModule::broadcastLink(link_object &lo, bool full_commit_hash) void StoreForwardPlusPlusModule::broadcastLink(link_object &lo, bool full_commit_hash)
{ {
meshtastic_StoreForwardPlusPlus storeforward = meshtastic_StoreForwardPlusPlus_init_zero; meshtastic_StoreForwardPlusPlus storeforward = meshtastic_StoreForwardPlusPlus_init_zero;
@@ -698,6 +717,8 @@ void StoreForwardPlusPlusModule::broadcastLink(link_object &lo, bool full_commit
p->decoded.want_response = false; p->decoded.want_response = false;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
p->channel = 0; p->channel = 0;
p->hop_limit = portduino_config.sfpp_hops;
p->hop_start = portduino_config.sfpp_hops;
LOG_INFO("Send link to mesh"); LOG_INFO("Send link to mesh");
service->sendToMesh(p, RX_SRC_LOCAL, true); service->sendToMesh(p, RX_SRC_LOCAL, true);
} }
@@ -720,17 +741,17 @@ StoreForwardPlusPlusModule::link_object StoreForwardPlusPlusModule::getLink(uint
memcpy(lo.encrypted_bytes, _payload, lo.encrypted_len); memcpy(lo.encrypted_bytes, _payload, lo.encrypted_len);
uint8_t *_message_hash = (uint8_t *)sqlite3_column_blob(getLinkStmt, 4); uint8_t *_message_hash = (uint8_t *)sqlite3_column_blob(getLinkStmt, 4);
lo.message_hash_len = 32; lo.message_hash_len = SFPP_HASH_SIZE;
memcpy(lo.message_hash, _message_hash, lo.message_hash_len); memcpy(lo.message_hash, _message_hash, lo.message_hash_len);
lo.rx_time = sqlite3_column_int(getLinkStmt, 5); lo.rx_time = sqlite3_column_int(getLinkStmt, 5);
uint8_t *_tmp_commit_hash = (uint8_t *)sqlite3_column_blob(getLinkStmt, 6); uint8_t *_tmp_commit_hash = (uint8_t *)sqlite3_column_blob(getLinkStmt, 6);
lo.commit_hash_len = 32; lo.commit_hash_len = SFPP_HASH_SIZE;
memcpy(lo.commit_hash, _tmp_commit_hash, lo.commit_hash_len); memcpy(lo.commit_hash, _tmp_commit_hash, lo.commit_hash_len);
uint8_t *_root_hash = (uint8_t *)sqlite3_column_blob(getLinkStmt, 7); uint8_t *_root_hash = (uint8_t *)sqlite3_column_blob(getLinkStmt, 7);
lo.root_hash_len = 32; lo.root_hash_len = SFPP_HASH_SIZE;
memcpy(lo.root_hash, _root_hash, lo.root_hash_len); memcpy(lo.root_hash, _root_hash, lo.root_hash_len);
lo.counter = sqlite3_column_int(getLinkStmt, 8); lo.counter = sqlite3_column_int(getLinkStmt, 8);
@@ -749,7 +770,7 @@ bool StoreForwardPlusPlusModule::sendFromScratch(uint8_t *root_hash)
LOG_WARN("sendFromScratch"); LOG_WARN("sendFromScratch");
// "select destination, sender, packet_id, channel_hash, encrypted_bytes, message_hash, rx_time \ // "select destination, sender, packet_id, channel_hash, encrypted_bytes, message_hash, rx_time \
// from local_messages order by rx_time desc LIMIT 1;" // from local_messages order by rx_time desc LIMIT 1;"
sqlite3_bind_blob(fromScratchStmt, 1, root_hash, 32, NULL); sqlite3_bind_blob(fromScratchStmt, 1, root_hash, SFPP_HASH_SIZE, NULL);
if (sqlite3_step(fromScratchStmt) == SQLITE_DONE) { if (sqlite3_step(fromScratchStmt) == SQLITE_DONE) {
LOG_WARN("No messages in scratch to forward"); LOG_WARN("No messages in scratch to forward");
return false; return false;
@@ -767,13 +788,13 @@ bool StoreForwardPlusPlusModule::sendFromScratch(uint8_t *root_hash)
memcpy(storeforward.message.bytes, _encrypted, storeforward.message.size); memcpy(storeforward.message.bytes, _encrypted, storeforward.message.size);
uint8_t *_message_hash = (uint8_t *)sqlite3_column_blob(fromScratchStmt, 4); uint8_t *_message_hash = (uint8_t *)sqlite3_column_blob(fromScratchStmt, 4);
storeforward.message_hash.size = 32; storeforward.message_hash.size = SFPP_HASH_SIZE;
memcpy(storeforward.message_hash.bytes, _message_hash, storeforward.message_hash.size); memcpy(storeforward.message_hash.bytes, _message_hash, storeforward.message_hash.size);
storeforward.encapsulated_rxtime = sqlite3_column_int(fromScratchStmt, 5); storeforward.encapsulated_rxtime = sqlite3_column_int(fromScratchStmt, 5);
storeforward.root_hash.size = 32; storeforward.root_hash.size = SFPP_HASH_SIZE;
memcpy(storeforward.root_hash.bytes, root_hash, 32); memcpy(storeforward.root_hash.bytes, root_hash, SFPP_HASH_SIZE);
sqlite3_reset(fromScratchStmt); sqlite3_reset(fromScratchStmt);
@@ -782,6 +803,8 @@ bool StoreForwardPlusPlusModule::sendFromScratch(uint8_t *root_hash)
p->decoded.want_response = false; p->decoded.want_response = false;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
p->channel = 0; p->channel = 0;
p->hop_limit = portduino_config.sfpp_hops;
p->hop_start = portduino_config.sfpp_hops;
LOG_INFO("Send link to mesh"); LOG_INFO("Send link to mesh");
service->sendToMesh(p, RX_SRC_LOCAL, true); service->sendToMesh(p, RX_SRC_LOCAL, true);
return true; return true;
@@ -793,22 +816,22 @@ bool StoreForwardPlusPlusModule::addToChain(link_object &lo)
link_object chain_end = getLinkFromCount(0, lo.root_hash, lo.root_hash_len); link_object chain_end = getLinkFromCount(0, lo.root_hash, lo.root_hash_len);
// we may need to calculate the full commit hash at this point // we may need to calculate the full commit hash at this point
if (lo.commit_hash_len != 32) { if (lo.commit_hash_len != SFPP_HASH_SIZE) {
SHA256 commit_hash; SHA256 commit_hash;
commit_hash.reset(); commit_hash.reset();
if (chain_end.commit_hash_len == 32) { if (chain_end.commit_hash_len == SFPP_HASH_SIZE) {
printBytes("last message: 0x", chain_end.commit_hash, 32); printBytes("last message: 0x", chain_end.commit_hash, SFPP_HASH_SIZE);
commit_hash.update(chain_end.commit_hash, 32); commit_hash.update(chain_end.commit_hash, SFPP_HASH_SIZE);
} else { } else {
printBytes("new chain root: 0x", lo.root_hash, 32); printBytes("new chain root: 0x", lo.root_hash, SFPP_HASH_SIZE);
commit_hash.update(lo.root_hash, 32); commit_hash.update(lo.root_hash, SFPP_HASH_SIZE);
} }
commit_hash.update(lo.message_hash, 32); commit_hash.update(lo.message_hash, SFPP_HASH_SIZE);
// message_hash.update(&mp.rx_time, sizeof(mp.rx_time)); // message_hash.update(&mp.rx_time, sizeof(mp.rx_time));
commit_hash.finalize(lo.commit_hash, 32); commit_hash.finalize(lo.commit_hash, SFPP_HASH_SIZE);
} }
lo.counter = chain_end.counter + 1; lo.counter = chain_end.counter + 1;
// push a message into the local chain DB // push a message into the local chain DB
@@ -819,22 +842,22 @@ bool StoreForwardPlusPlusModule::addToChain(link_object &lo)
// packet_id // packet_id
sqlite3_bind_int(chain_insert_stmt, 3, lo.id); sqlite3_bind_int(chain_insert_stmt, 3, lo.id);
// root_hash // root_hash
sqlite3_bind_blob(chain_insert_stmt, 4, lo.root_hash, 32, NULL); sqlite3_bind_blob(chain_insert_stmt, 4, lo.root_hash, SFPP_HASH_SIZE, NULL);
// encrypted_bytes // encrypted_bytes
sqlite3_bind_blob(chain_insert_stmt, 5, lo.encrypted_bytes, lo.encrypted_len, NULL); sqlite3_bind_blob(chain_insert_stmt, 5, lo.encrypted_bytes, lo.encrypted_len, NULL);
// message_hash // message_hash
sqlite3_bind_blob(chain_insert_stmt, 6, lo.message_hash, 32, NULL); sqlite3_bind_blob(chain_insert_stmt, 6, lo.message_hash, SFPP_HASH_SIZE, NULL);
// rx_time // rx_time
sqlite3_bind_int(chain_insert_stmt, 7, lo.rx_time); sqlite3_bind_int(chain_insert_stmt, 7, lo.rx_time);
// commit_hash // commit_hash
sqlite3_bind_blob(chain_insert_stmt, 8, lo.commit_hash, 32, NULL); sqlite3_bind_blob(chain_insert_stmt, 8, lo.commit_hash, SFPP_HASH_SIZE, NULL);
// payload // payload
sqlite3_bind_text(chain_insert_stmt, 9, lo.payload.c_str(), lo.payload.length(), NULL); sqlite3_bind_text(chain_insert_stmt, 9, lo.payload.c_str(), lo.payload.length(), NULL);
sqlite3_bind_int(chain_insert_stmt, 10, lo.counter); sqlite3_bind_int(chain_insert_stmt, 10, lo.counter);
sqlite3_step(chain_insert_stmt); sqlite3_step(chain_insert_stmt);
sqlite3_reset(chain_insert_stmt); sqlite3_reset(chain_insert_stmt);
setChainCount(lo.root_hash, 32, lo.counter); setChainCount(lo.root_hash, SFPP_HASH_SIZE, lo.counter);
return true; return true;
} }
@@ -848,11 +871,11 @@ bool StoreForwardPlusPlusModule::addToScratch(link_object &lo)
// packet_id // packet_id
sqlite3_bind_int(scratch_insert_stmt, 3, lo.id); sqlite3_bind_int(scratch_insert_stmt, 3, lo.id);
// root_hash // root_hash
sqlite3_bind_blob(scratch_insert_stmt, 4, lo.root_hash, 32, NULL); sqlite3_bind_blob(scratch_insert_stmt, 4, lo.root_hash, SFPP_HASH_SIZE, NULL);
// encrypted_bytes // encrypted_bytes
sqlite3_bind_blob(scratch_insert_stmt, 5, lo.encrypted_bytes, lo.encrypted_len, NULL); sqlite3_bind_blob(scratch_insert_stmt, 5, lo.encrypted_bytes, lo.encrypted_len, NULL);
// message_hash // message_hash
sqlite3_bind_blob(scratch_insert_stmt, 6, lo.message_hash, 32, NULL); sqlite3_bind_blob(scratch_insert_stmt, 6, lo.message_hash, SFPP_HASH_SIZE, NULL);
// rx_time // rx_time
sqlite3_bind_int(scratch_insert_stmt, 7, lo.rx_time); sqlite3_bind_int(scratch_insert_stmt, 7, lo.rx_time);
// payload // payload
@@ -882,8 +905,8 @@ void StoreForwardPlusPlusModule::canonAnnounce(uint8_t *_message_hash, uint8_t *
// set root hash // set root hash
// needs to be the full hash to bootstrap // needs to be the full hash to bootstrap
storeforward.root_hash.size = 32; storeforward.root_hash.size = SFPP_HASH_SIZE;
memcpy(storeforward.root_hash.bytes, _root_hash, 32); memcpy(storeforward.root_hash.bytes, _root_hash, SFPP_HASH_SIZE);
storeforward.encapsulated_rxtime = _rx_time; storeforward.encapsulated_rxtime = _rx_time;
// storeforward. // storeforward.
@@ -892,6 +915,8 @@ void StoreForwardPlusPlusModule::canonAnnounce(uint8_t *_message_hash, uint8_t *
p->decoded.want_response = false; p->decoded.want_response = false;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
p->channel = 0; p->channel = 0;
p->hop_limit = portduino_config.sfpp_hops;
p->hop_start = portduino_config.sfpp_hops;
LOG_INFO("Send packet to mesh"); LOG_INFO("Send packet to mesh");
service->sendToMesh(p, RX_SRC_LOCAL, true); service->sendToMesh(p, RX_SRC_LOCAL, true);
} }
@@ -965,10 +990,10 @@ StoreForwardPlusPlusModule::link_object StoreForwardPlusPlusModule::getFromScrat
lo.encrypted_len = sqlite3_column_bytes(fromScratchByHashStmt, 3); lo.encrypted_len = sqlite3_column_bytes(fromScratchByHashStmt, 3);
memcpy(lo.encrypted_bytes, encrypted_bytes, lo.encrypted_len); memcpy(lo.encrypted_bytes, encrypted_bytes, lo.encrypted_len);
uint8_t *message_hash = (uint8_t *)sqlite3_column_blob(fromScratchByHashStmt, 4); uint8_t *message_hash = (uint8_t *)sqlite3_column_blob(fromScratchByHashStmt, 4);
memcpy(lo.message_hash, message_hash, 32); memcpy(lo.message_hash, message_hash, SFPP_HASH_SIZE);
lo.rx_time = sqlite3_column_int(fromScratchByHashStmt, 5); lo.rx_time = sqlite3_column_int(fromScratchByHashStmt, 5);
uint8_t *root_hash = (uint8_t *)sqlite3_column_blob(fromScratchByHashStmt, 6); uint8_t *root_hash = (uint8_t *)sqlite3_column_blob(fromScratchByHashStmt, 6);
memcpy(lo.root_hash, root_hash, 32); memcpy(lo.root_hash, root_hash, SFPP_HASH_SIZE);
lo.payload = lo.payload =
std::string((char *)sqlite3_column_text(fromScratchByHashStmt, 7), sqlite3_column_bytes(fromScratchByHashStmt, 7)); std::string((char *)sqlite3_column_text(fromScratchByHashStmt, 7), sqlite3_column_bytes(fromScratchByHashStmt, 7));
lo.message_hash_len = hash_len; lo.message_hash_len = hash_len;
@@ -998,15 +1023,16 @@ StoreForwardPlusPlusModule::ingestTextPacket(const meshtastic_MeshPacket &mp, co
message_hash.update(&mp.to, sizeof(mp.to)); message_hash.update(&mp.to, sizeof(mp.to));
message_hash.update(&mp.from, sizeof(mp.from)); message_hash.update(&mp.from, sizeof(mp.from));
message_hash.update(&mp.id, sizeof(mp.id)); message_hash.update(&mp.id, sizeof(mp.id));
message_hash.finalize(lo.message_hash, 32); message_hash.finalize(lo.message_hash, SFPP_HASH_SIZE);
lo.message_hash_len = 32; lo.message_hash_len = SFPP_HASH_SIZE;
getOrAddRootFromChannelHash(encrypted_meshpacket->channel, lo.root_hash); lo.root_hash_len = getOrAddRootFromChannelHash(encrypted_meshpacket->channel, lo.root_hash);
return lo; return lo;
} }
StoreForwardPlusPlusModule::link_object StoreForwardPlusPlusModule::ingestLinkMessage(meshtastic_StoreForwardPlusPlus *t) StoreForwardPlusPlusModule::link_object StoreForwardPlusPlusModule::ingestLinkMessage(meshtastic_StoreForwardPlusPlus *t)
{ {
// TODO: If not stratum0, injest the chain count
link_object lo; link_object lo;
lo.to = t->encapsulated_to; lo.to = t->encapsulated_to;
@@ -1026,13 +1052,13 @@ StoreForwardPlusPlusModule::link_object StoreForwardPlusPlusModule::ingestLinkMe
message_hash.update(&lo.to, sizeof(lo.to)); message_hash.update(&lo.to, sizeof(lo.to));
message_hash.update(&lo.from, sizeof(lo.from)); message_hash.update(&lo.from, sizeof(lo.from));
message_hash.update(&lo.id, sizeof(lo.id)); message_hash.update(&lo.id, sizeof(lo.id));
message_hash.finalize(lo.message_hash, 32); message_hash.finalize(lo.message_hash, SFPP_HASH_SIZE);
lo.message_hash_len = 32; lo.message_hash_len = SFPP_HASH_SIZE;
// look up full root hash and copy over the partial if it matches // look up full root hash and copy over the partial if it matches
if (lookUpFullRootHash(t->root_hash.bytes, t->root_hash.size, lo.root_hash)) { if (lookUpFullRootHash(t->root_hash.bytes, t->root_hash.size, lo.root_hash)) {
printBytes("Found full root hash: 0x", lo.root_hash, 32); printBytes("Found full root hash: 0x", lo.root_hash, SFPP_HASH_SIZE);
lo.root_hash_len = 32; lo.root_hash_len = SFPP_HASH_SIZE;
} else { } else {
LOG_WARN("root hash does not match %d bytes", t->root_hash.size); LOG_WARN("root hash does not match %d bytes", t->root_hash.size);
lo.root_hash_len = 0; lo.root_hash_len = 0;
@@ -1040,11 +1066,11 @@ StoreForwardPlusPlusModule::link_object StoreForwardPlusPlusModule::ingestLinkMe
return lo; return lo;
} }
if (t->commit_hash.size == 32 && getChainCount(t->root_hash.bytes, t->root_hash.size) == 0 && if (t->commit_hash.size == SFPP_HASH_SIZE && getChainCount(t->root_hash.bytes, t->root_hash.size) == 0 &&
portduino_config.initial_sync != 0 && !portduino_config.sfpp_stratum0) { portduino_config.sfpp_initial_sync != 0 && !portduino_config.sfpp_stratum0) {
LOG_WARN("Accepting SF++ ch "); LOG_WARN("Accepting SF++ ch ");
lo.commit_hash_len = 32; lo.commit_hash_len = SFPP_HASH_SIZE;
memcpy(lo.commit_hash, t->commit_hash.bytes, 32); memcpy(lo.commit_hash, t->commit_hash.bytes, SFPP_HASH_SIZE);
} else if (t->commit_hash.size > 0) { } else if (t->commit_hash.size > 0) {
// calculate the full commit hash and replace the partial if it matches // calculate the full commit hash and replace the partial if it matches
@@ -1089,21 +1115,21 @@ bool StoreForwardPlusPlusModule::checkCommitHash(StoreForwardPlusPlusModule::lin
commit_hash.reset(); commit_hash.reset();
if (chain_end.commit_hash_len == 32) { if (chain_end.commit_hash_len == SFPP_HASH_SIZE) {
printBytes("last message: 0x", chain_end.commit_hash, 32); printBytes("last message: 0x", chain_end.commit_hash, SFPP_HASH_SIZE);
commit_hash.update(chain_end.commit_hash, 32); commit_hash.update(chain_end.commit_hash, SFPP_HASH_SIZE);
} else { } else {
if (lo.root_hash_len != 32) { if (lo.root_hash_len != SFPP_HASH_SIZE) {
LOG_WARN("Short root hash in link object, cannot create new chain"); LOG_WARN("Short root hash in link object, cannot create new chain");
return false; return false;
} }
printBytes("new chain root: 0x", lo.root_hash, 32); printBytes("new chain root: 0x", lo.root_hash, SFPP_HASH_SIZE);
commit_hash.update(lo.root_hash, 32); commit_hash.update(lo.root_hash, SFPP_HASH_SIZE);
} }
commit_hash.update(lo.message_hash, 32); commit_hash.update(lo.message_hash, SFPP_HASH_SIZE);
commit_hash.finalize(lo.commit_hash, 32); commit_hash.finalize(lo.commit_hash, SFPP_HASH_SIZE);
lo.commit_hash_len = 32; lo.commit_hash_len = SFPP_HASH_SIZE;
if (hash_len == 0 || memcmp(commit_hash_bytes, lo.commit_hash, hash_len) == 0) { if (hash_len == 0 || memcmp(commit_hash_bytes, lo.commit_hash, hash_len) == 0) {
return true; return true;
@@ -1122,7 +1148,7 @@ bool StoreForwardPlusPlusModule::lookUpFullRootHash(uint8_t *partial_root_hash,
uint8_t *tmp_root_hash = (uint8_t *)sqlite3_column_blob(getFullRootHashStmt, 0); uint8_t *tmp_root_hash = (uint8_t *)sqlite3_column_blob(getFullRootHashStmt, 0);
if (tmp_root_hash) { if (tmp_root_hash) {
LOG_WARN("Found full root hash!"); LOG_WARN("Found full root hash!");
memcpy(full_root_hash, tmp_root_hash, 32); memcpy(full_root_hash, tmp_root_hash, SFPP_HASH_SIZE);
sqlite3_reset(getFullRootHashStmt); sqlite3_reset(getFullRootHashStmt);
return true; return true;
} }
@@ -1158,8 +1184,8 @@ StoreForwardPlusPlusModule::link_object StoreForwardPlusPlusModule::getLinkFromC
int step = 0; int step = 0;
uint32_t _rx_time = 0; uint32_t _rx_time = 0;
uint8_t last_message_commit_hash[32] = {0}; uint8_t last_message_commit_hash[SFPP_HASH_SIZE] = {0};
uint8_t last_message_hash[32] = {0}; uint8_t last_message_hash[SFPP_HASH_SIZE] = {0};
sqlite3_bind_int(getChainEndStmt, 1, _root_hash_len); sqlite3_bind_int(getChainEndStmt, 1, _root_hash_len);
sqlite3_bind_blob(getChainEndStmt, 2, _root_hash, _root_hash_len, NULL); sqlite3_bind_blob(getChainEndStmt, 2, _root_hash, _root_hash_len, NULL);
@@ -1169,8 +1195,8 @@ StoreForwardPlusPlusModule::link_object StoreForwardPlusPlusModule::getLinkFromC
uint8_t *last_message_commit_hash_ptr = (uint8_t *)sqlite3_column_blob(getChainEndStmt, 0); uint8_t *last_message_commit_hash_ptr = (uint8_t *)sqlite3_column_blob(getChainEndStmt, 0);
uint8_t *last_message_hash_ptr = (uint8_t *)sqlite3_column_blob(getChainEndStmt, 1); uint8_t *last_message_hash_ptr = (uint8_t *)sqlite3_column_blob(getChainEndStmt, 1);
_rx_time = sqlite3_column_int(getChainEndStmt, 2); _rx_time = sqlite3_column_int(getChainEndStmt, 2);
memcpy(last_message_commit_hash, last_message_commit_hash_ptr, 32); memcpy(last_message_commit_hash, last_message_commit_hash_ptr, SFPP_HASH_SIZE);
memcpy(last_message_hash, last_message_hash_ptr, 32); memcpy(last_message_hash, last_message_hash_ptr, SFPP_HASH_SIZE);
if (_count == step) if (_count == step)
break; break;
@@ -1178,7 +1204,7 @@ StoreForwardPlusPlusModule::link_object StoreForwardPlusPlusModule::getLinkFromC
} }
LOG_WARN("step %d", step); LOG_WARN("step %d", step);
if (last_message_commit_hash != nullptr && _rx_time != 0) { if (last_message_commit_hash != nullptr && _rx_time != 0) {
lo = getLink(last_message_commit_hash, 32); lo = getLink(last_message_commit_hash, SFPP_HASH_SIZE);
} else { } else {
LOG_WARN("Failed to get link from count"); LOG_WARN("Failed to get link from count");
lo.validObject = false; lo.validObject = false;

View File

@@ -7,6 +7,8 @@
#include "SinglePortModule.h" #include "SinglePortModule.h"
#include "sqlite3.h" #include "sqlite3.h"
#define SFPP_HASH_SIZE 32
/** /**
* Store and forward ++ module * Store and forward ++ module
* There's an obvious need for a store-and-forward mechanism in Meshtastic. * There's an obvious need for a store-and-forward mechanism in Meshtastic.
@@ -53,11 +55,11 @@ class StoreForwardPlusPlusModule : public ProtobufModule<meshtastic_StoreForward
ChannelHash channel_hash; ChannelHash channel_hash;
uint8_t encrypted_bytes[256] = {0}; uint8_t encrypted_bytes[256] = {0};
size_t encrypted_len; size_t encrypted_len;
uint8_t message_hash[32] = {0}; uint8_t message_hash[SFPP_HASH_SIZE] = {0};
size_t message_hash_len = 0; size_t message_hash_len = 0;
uint8_t root_hash[32] = {0}; uint8_t root_hash[SFPP_HASH_SIZE] = {0};
size_t root_hash_len = 0; size_t root_hash_len = 0;
uint8_t commit_hash[32] = {0}; uint8_t commit_hash[SFPP_HASH_SIZE] = {0};
size_t commit_hash_len = 0; size_t commit_hash_len = 0;
uint32_t counter = 0; uint32_t counter = 0;
std::string payload; std::string payload;
@@ -130,8 +132,8 @@ class StoreForwardPlusPlusModule : public ProtobufModule<meshtastic_StoreForward
// For a given Meshtastic ChannelHash, fills the root_hash buffer with a 32-byte root hash // For a given Meshtastic ChannelHash, fills the root_hash buffer with a 32-byte root hash
// but this function will add the root hash if it is not already present // but this function will add the root hash if it is not already present
// returns true if the hash is new // returns hash size or 0 if not found/added
bool getOrAddRootFromChannelHash(ChannelHash, uint8_t *); size_t getOrAddRootFromChannelHash(ChannelHash, uint8_t *);
// adds the ChannelHash and root_hash to the mappings table // adds the ChannelHash and root_hash to the mappings table
void addRootToMappings(ChannelHash, uint8_t *); void addRootToMappings(ChannelHash, uint8_t *);

View File

@@ -790,7 +790,8 @@ bool loadConfig(const char *configPath)
if (yamlConfig["StoreAndForward"]) { if (yamlConfig["StoreAndForward"]) {
portduino_config.sfpp_stratum0 = (yamlConfig["StoreAndForward"]["Stratum0"]).as<bool>(false); portduino_config.sfpp_stratum0 = (yamlConfig["StoreAndForward"]["Stratum0"]).as<bool>(false);
portduino_config.initial_sync = (yamlConfig["StoreAndForward"]["InitialSync"]).as<int>(10); portduino_config.sfpp_initial_sync = (yamlConfig["StoreAndForward"]["InitialSync"]).as<int>(10);
portduino_config.sfpp_hops = (yamlConfig["StoreAndForward"]["Hops"]).as<int>(3);
} }
if (yamlConfig["General"]) { if (yamlConfig["General"]) {

View File

@@ -170,8 +170,17 @@ extern struct portduino_config_struct {
bool has_configDisplayMode = false; bool has_configDisplayMode = false;
// Store and Forward++ // Store and Forward++
// DB location /var/lib/meshtasticd/
std::string sfpp_db_path = "/var/lib/meshtasticd/";
bool sfpp_stratum0 = false; bool sfpp_stratum0 = false;
int initial_sync = 10; int sfpp_initial_sync = 10;
int sfpp_hops = 3;
int sfpp_announce_interval = 5; // minutes
uint32_t sfpp_max_chain = 1000;
// allowed root hashes
// upstream node
// Are we allowing unknown channel hashes? Does this even make sense?
// Allow DMs
// General // General
std::string mac_address = ""; std::string mac_address = "";
@@ -493,11 +502,14 @@ extern struct portduino_config_struct {
} }
// StoreAndForward // StoreAndForward
if (sfpp_stratum0 || initial_sync != 10) { if (sfpp_stratum0 || sfpp_initial_sync != 10 || sfpp_hops != 3 || sfpp_announce_interval != 5 || sfpp_max_chain != 1000) {
out << YAML::Key << "StoreAndForward" << YAML::Value << YAML::BeginMap; out << YAML::Key << "StoreAndForward" << YAML::Value << YAML::BeginMap;
out << YAML::Key << "Stratum0" << YAML::Value << sfpp_stratum0; out << YAML::Key << "Stratum0" << YAML::Value << sfpp_stratum0;
out << YAML::Key << "InitialSync" << YAML::Value << initial_sync; out << YAML::Key << "InitialSync" << YAML::Value << sfpp_initial_sync;
out << YAML::Key << "Hops" << YAML::Value << sfpp_hops;
out << YAML::Key << "AnnounceInterval" << YAML::Value << sfpp_announce_interval;
out << YAML::Key << "MaxChainLength" << YAML::Value << sfpp_max_chain;
out << YAML::EndMap; // StoreAndForward out << YAML::EndMap; // StoreAndForward
} }