feat: include source and destination in path data where available

realted to #85
This commit is contained in:
Enot (ded) Skelly
2026-07-23 09:44:27 -07:00
parent 7a58a07bbd
commit f07edf9fb6
4 changed files with 127 additions and 0 deletions
+61
View File
@@ -337,6 +337,48 @@ func (s *Store) GetPacket(ctx context.Context, packetHash []byte) (*api.Packet,
}
}
}
// 1-byte source/destination hashes for the payload types that carry a resolvable
// endpoint (REQUEST, RESPONSE, TEXT_MESSAGE, PATH, ANON_REQ's destination). Constant
// for this packet hash, so parsed once; resolution itself still happens per-observation
// below since candidates depend on the observation's IATA, same as the intermediate
// hop resolution already does.
var sourceHashByte, destHashByte []byte
switch row.PayloadType {
case int16(meshcore.PayloadTypeAnonReq):
if anonReq, err := meshcore.AnonReqFromBytes(row.RawPayload); err == nil {
destHashByte = []byte{anonReq.Destination}
}
case int16(meshcore.PayloadTypeReq):
if req, err := meshcore.RequestFromBytes(row.RawPayload); err == nil {
sourceHashByte = []byte{req.Source}
destHashByte = []byte{req.Destination}
}
case int16(meshcore.PayloadTypeResponse):
if resp, err := meshcore.ResponseFromBytes(row.RawPayload); err == nil {
sourceHashByte = []byte{resp.Source}
destHashByte = []byte{resp.Destination}
}
case int16(meshcore.PayloadTypeTxtMsg):
if txt, err := meshcore.TextMessageFromBytes(row.RawPayload); err == nil {
sourceHashByte = []byte{txt.Source}
destHashByte = []byte{txt.Destination}
}
case int16(meshcore.PayloadTypePath):
if path, err := meshcore.PathFromBytes(row.RawPayload); err == nil {
sourceHashByte = []byte{path.Source}
destHashByte = []byte{path.Destination}
}
}
// ADVERT's source is an exact pubkey match, not ambiguous like the above -- and unlike
// them it doesn't depend on IATA, so resolve it once here rather than per observation.
var resolvedAdvertSource *api.ResolvedNode
if row.PayloadType == int16(meshcore.PayloadTypeAdvert) && row.OriginPubkey != nil {
if nodeID, err := s.GetNodeByPubkey(ctx, row.OriginPubkey); err == nil {
if nodes, err := s.GetNodesByIDs(ctx, []uuid.UUID{nodeID}); err == nil {
resolvedAdvertSource = nodes[nodeID]
}
}
}
for _, v := range obsRows {
obs := api.PacketObservationDetail{
ID: v.ID,
@@ -379,6 +421,25 @@ func (s *Store) GetPacket(ctx context.Context, packetHash []byte) (*api.Packet,
}
}
obs.ResolvedPath = resolvedPath
if row.PayloadType == int16(meshcore.PayloadTypeAdvert) {
hop := api.ResolveExactNode(resolvedAdvertSource)
obs.ResolvedSource = &hop
} else if len(sourceHashByte) == 1 {
if r, err := s.ResolvePathHashes(ctx, v.Iata, [][]byte{sourceHashByte}); err != nil {
log.Printf("store: source resolution failed for observation %d: %v", v.ID, err)
} else {
hop := api.BuildResolvedPath([][]byte{sourceHashByte}, r)[0]
obs.ResolvedSource = &hop
}
}
if len(destHashByte) == 1 {
if r, err := s.ResolvePathHashes(ctx, v.Iata, [][]byte{destHashByte}); err != nil {
log.Printf("store: destination resolution failed for observation %d: %v", v.ID, err)
} else {
hop := api.BuildResolvedPath([][]byte{destHashByte}, r)[0]
obs.ResolvedDestination = &hop
}
}
if row.PayloadType == int16(meshcore.PayloadTypeTrace) && len(traceRawHashes) > 0 {
// Swap in the trace's own path hashes so PathData's hop-block split (driven by
// pathBytes + hashSize) lines up 1:1 with resolvedPath -- the raw SNR bytes
+19
View File
@@ -58,6 +58,14 @@ type PacketObservationDetail struct {
Radio *PacketRadio `json:"radio,omitempty"`
SourceBroker string `json:"sourceBroker"`
ResolvedPath []ResolvedHop `json:"resolvedPath"` // per-observation resolved path hashes
// ResolvedSource/ResolvedDestination are the packet's endpoints, when the payload type
// carries a resolvable one: an exact match for ADVERT's full pubkey, an ambiguous
// hash-prefix match (like intermediate hops) for TEXT_MESSAGE/PATH/ANON_REQ's 1-byte
// source/destination hashes. Nil when the payload type doesn't carry one at all (e.g.
// GRP_TXT/GRP_DATA/TRACE aren't node-to-node addressed) -- see BuildResolvedPath and
// ResolveExactNode for how each is built.
ResolvedSource *ResolvedHop `json:"resolvedSource,omitempty"`
ResolvedDestination *ResolvedHop `json:"resolvedDestination,omitempty"`
}
// PacketRadio holds the radio settings copied from the observer at observation time.
@@ -278,3 +286,14 @@ func BuildResolvedPath(hashes [][]byte, resolved map[string][]ResolvedPathEntry)
}
return path
}
// ResolveExactNode builds a ResolvedHop for an endpoint resolved by an exact, unambiguous
// key -- e.g. an ADVERT's full public key already resolved to a single node -- as opposed
// to BuildResolvedPath's hash-prefix matching, which can be ambiguous. Confidence is always
// "high" when a node was found, "none" when it wasn't (unknown node, or lookup failed).
func ResolveExactNode(node *ResolvedNode) ResolvedHop {
if node == nil {
return ResolvedHop{Confidence: "none", Nodes: []ResolvedNode{}}
}
return ResolvedHop{Confidence: "high", Nodes: []ResolvedNode{*node}}
}
+4
View File
@@ -105,6 +105,10 @@ type DB interface {
// the observer's own node may not exist until it has advertised.
GetNodeByPubkey(ctx context.Context, pubkey []byte) (uuid.UUID, error)
// GetNodesByIDs returns resolved node details (name, pubkey, coords) for a set of node
// IDs. Used with GetNodeByPubkey to resolve an ADVERT's exact-match source endpoint.
GetNodesByIDs(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID]*api.ResolvedNode, error)
// UpsertNodeIATA upserts a node_iatas row.
UpsertNodeIATA(ctx context.Context, nodeID uuid.UUID, iata string) error
+43
View File
@@ -94,6 +94,10 @@ type packetObservationEvent struct {
} `json:"pathLength"`
PropagationTimeMs int32 `json:"propagationTimeMs"`
ResolvedPath []api.ResolvedHop `json:"resolvedPath"` // only present in the resolvePath-opted-in variant; see hub.Event.PayloadResolved
// ResolvedSource/ResolvedDestination mirror api.PacketObservationDetail's fields of
// the same name -- nil when this payload type has no resolvable endpoint.
ResolvedSource *api.ResolvedHop `json:"resolvedSource,omitempty"`
ResolvedDestination *api.ResolvedHop `json:"resolvedDestination,omitempty"`
} `json:"observation"`
}
@@ -340,6 +344,10 @@ func (w *Worker) handlePacket(ctx context.Context, iata, pubkeyHex string, raw [
// below), so the "physical route" hashes for resolvedPath/known-route purposes come
// instead from the TRACE payload's own embedded PathHashes (the path being probed).
var traceRawHashes [][]byte
// 1-byte source/destination hashes for ambiguous prefix resolution (REQUEST, RESPONSE,
// TEXT_MESSAGE, PATH, ANON_REQ's destination). GRP_TXT/GRP_DATA/TRACE have no such
// fields and are left nil.
var sourceHashByte, destHashByte []byte
switch packet.PayloadType() {
case meshcore.PayloadTypeGrpTxt:
@@ -438,6 +446,7 @@ func (w *Worker) handlePacket(ctx context.Context, iata, pubkeyHex string, raw [
anonReq, err := meshcore.AnonReqFromBytes(packet.Payload)
if err == nil {
originPubkey = anonReq.EphemeralPubKey[:]
destHashByte = []byte{anonReq.Destination}
par := parsedAnonReq{
Raw: hex.EncodeToString(packet.Payload),
Type: "ANON_REQUEST",
@@ -450,6 +459,8 @@ func (w *Worker) handlePacket(ctx context.Context, iata, pubkeyHex string, raw [
case meshcore.PayloadTypeReq:
req, err := meshcore.RequestFromBytes(packet.Payload)
if err == nil {
sourceHashByte = []byte{req.Source}
destHashByte = []byte{req.Destination}
pe := parsedEnvelope{
Raw: hex.EncodeToString(packet.Payload),
Type: "REQUEST",
@@ -465,6 +476,8 @@ func (w *Worker) handlePacket(ctx context.Context, iata, pubkeyHex string, raw [
case meshcore.PayloadTypeResponse:
resp, err := meshcore.ResponseFromBytes(packet.Payload)
if err == nil {
sourceHashByte = []byte{resp.Source}
destHashByte = []byte{resp.Destination}
pe := parsedEnvelope{
Raw: hex.EncodeToString(packet.Payload),
Type: "RESPONSE",
@@ -480,6 +493,8 @@ func (w *Worker) handlePacket(ctx context.Context, iata, pubkeyHex string, raw [
case meshcore.PayloadTypeTxtMsg:
txt, err := meshcore.TextMessageFromBytes(packet.Payload)
if err == nil {
sourceHashByte = []byte{txt.Source}
destHashByte = []byte{txt.Destination}
pe := parsedEnvelope{
Raw: hex.EncodeToString(packet.Payload),
Type: "TEXT_MESSAGE",
@@ -495,6 +510,8 @@ func (w *Worker) handlePacket(ctx context.Context, iata, pubkeyHex string, raw [
case meshcore.PayloadTypePath:
path, err := meshcore.PathFromBytes(packet.Payload)
if err == nil {
sourceHashByte = []byte{path.Source}
destHashByte = []byte{path.Destination}
pe := parsedEnvelope{
Raw: hex.EncodeToString(packet.Payload),
Type: "PATH",
@@ -805,6 +822,30 @@ func (w *Worker) handlePacket(ctx context.Context, iata, pubkeyHex string, raw [
}
}
w.runCapabilityDetection(ctx, packet.PayloadType(), packet.PathHashSize(), resolvedIDs)
var resolvedSource, resolvedDestination *api.ResolvedHop
if packet.PayloadType() == meshcore.PayloadTypeAdvert && originPubkey != nil {
// Exact match: ADVERT carries the sender's real identity pubkey, not a
// short ambiguous hash prefix like the other resolvable payload types.
if nodeID, err := w.db.GetNodeByPubkey(ctx, originPubkey); err == nil {
if nodes, err := w.db.GetNodesByIDs(ctx, []uuid.UUID{nodeID}); err == nil {
hop := api.ResolveExactNode(nodes[nodeID])
resolvedSource = &hop
}
}
} else if len(sourceHashByte) == 1 {
if r, err := w.db.ResolvePathHashes(ctx, iata, [][]byte{sourceHashByte}); err == nil {
hop := api.BuildResolvedPath([][]byte{sourceHashByte}, r)[0]
resolvedSource = &hop
}
}
if len(destHashByte) == 1 {
if r, err := w.db.ResolvePathHashes(ctx, iata, [][]byte{destHashByte}); err == nil {
hop := api.BuildResolvedPath([][]byte{destHashByte}, r)[0]
resolvedDestination = &hop
}
}
if inserted {
w.handlePayloadTypeSideEffects(ctx, packet, iata, packetHash[:], radio, scopeID, matchedScope, pubkeyBytes, float32(parseNumber(envelope.SNR)))
evt := packetObservationEvent{}
@@ -836,6 +877,8 @@ func (w *Worker) handlePacket(ctx context.Context, iata, pubkeyHex string, raw [
evt.Packet.Scope = matchedScope
}
resolvedPath := api.BuildResolvedPath(hashes, resolved)
evt.Observation.ResolvedSource = resolvedSource
evt.Observation.ResolvedDestination = resolvedDestination
w.broadcastPacketObservation(iata, packet.PayloadType(), evt, resolvedPath)
}
}