// SPDX-License-Identifier: 0BSD // Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io package packet import ( "fmt" "sync" "time" "git.quad4.io/Networks/Reticulum-Go/pkg/debug" "git.quad4.io/Networks/Reticulum-Go/pkg/identity" ) const ( RECEIPT_FAILED = 0x00 RECEIPT_SENT = 0x01 RECEIPT_DELIVERED = 0x02 RECEIPT_CULLED = 0xFF EXPL_LENGTH = (identity.HASHLENGTH + identity.SIGLENGTH) / 8 IMPL_LENGTH = identity.SIGLENGTH / 8 ) type PacketReceipt struct { mutex sync.RWMutex hash []byte truncatedHash []byte sent bool sentAt time.Time proved bool status byte destination interface{} timeout time.Duration concludedAt time.Time proofPacket *Packet deliveryCallback func(*PacketReceipt) timeoutCallback func(*PacketReceipt) link interface{} destinationHash []byte destinationIdent *identity.Identity timeoutCheckDone chan bool } func NewPacketReceipt(pkt *Packet) *PacketReceipt { hash := pkt.Hash() receipt := &PacketReceipt{ hash: hash, truncatedHash: pkt.TruncatedHash(), sent: true, sentAt: time.Now(), proved: false, status: RECEIPT_SENT, destination: pkt.Destination, timeout: calculateTimeout(pkt), timeoutCheckDone: make(chan bool, 1), } go receipt.timeoutWatchdog() debug.Log(debug.DEBUG_PACKETS, "Created packet receipt", "hash", fmt.Sprintf("%x", receipt.truncatedHash)) return receipt } func calculateTimeout(pkt *Packet) time.Duration { baseTimeout := 15 * time.Second if pkt.Hops > 0 { baseTimeout += time.Duration(pkt.Hops) * (3 * time.Second) } return baseTimeout } func (pr *PacketReceipt) GetStatus() byte { pr.mutex.RLock() defer pr.mutex.RUnlock() return pr.status } func (pr *PacketReceipt) GetHash() []byte { pr.mutex.RLock() defer pr.mutex.RUnlock() return pr.hash } func (pr *PacketReceipt) IsDelivered() bool { pr.mutex.RLock() defer pr.mutex.RUnlock() return pr.status == RECEIPT_DELIVERED } func (pr *PacketReceipt) IsFailed() bool { pr.mutex.RLock() defer pr.mutex.RUnlock() return pr.status == RECEIPT_FAILED } func (pr *PacketReceipt) ValidateProofPacket(proofPacket *Packet) bool { if proofPacket.Link != nil { return pr.ValidateLinkProof(proofPacket.Data, proofPacket.Link, proofPacket) } return pr.ValidateProof(proofPacket.Data, proofPacket) } func (pr *PacketReceipt) ValidateLinkProof(proof []byte, link interface{}, proofPacket *Packet) bool { if len(proof) == EXPL_LENGTH { proofHash := proof[:identity.HASHLENGTH/8] signature := proof[identity.HASHLENGTH/8 : identity.HASHLENGTH/8+identity.SIGLENGTH/8] pr.mutex.RLock() hashMatch := string(proofHash) == string(pr.hash) pr.mutex.RUnlock() if !hashMatch { return false } proofValid := pr.validateLinkSignature(signature, link) if proofValid { pr.mutex.Lock() pr.status = RECEIPT_DELIVERED pr.proved = true pr.concludedAt = time.Now() pr.proofPacket = proofPacket callback := pr.deliveryCallback pr.mutex.Unlock() if callback != nil { go callback(pr) } debug.Log(debug.DEBUG_PACKETS, "Link proof validated", "hash", fmt.Sprintf("%x", pr.truncatedHash)) return true } } else if len(proof) == IMPL_LENGTH { debug.Log(debug.DEBUG_TRACE, "Implicit link proof not yet implemented") } return false } func (pr *PacketReceipt) ValidateProof(proof []byte, proofPacket *Packet) bool { if len(proof) == EXPL_LENGTH { proofHash := proof[:identity.HASHLENGTH/8] signature := proof[identity.HASHLENGTH/8 : identity.HASHLENGTH/8+identity.SIGLENGTH/8] pr.mutex.RLock() hashMatch := string(proofHash) == string(pr.hash) ident := pr.destinationIdent pr.mutex.RUnlock() debug.Log(debug.DEBUG_PACKETS, "Explicit proof validation", "len", len(proof), "hashMatch", hashMatch, "hasIdent", ident != nil) if !hashMatch { debug.Log(debug.DEBUG_PACKETS, "Proof hash mismatch") return false } if ident == nil { debug.Log(debug.DEBUG_VERBOSE, "Cannot validate proof without destination identity") return false } proofValid := ident.Verify(pr.hash, signature) debug.Log(debug.DEBUG_PACKETS, "Signature verification result", "valid", proofValid) if proofValid { pr.mutex.Lock() pr.status = RECEIPT_DELIVERED pr.proved = true pr.concludedAt = time.Now() pr.proofPacket = proofPacket callback := pr.deliveryCallback pr.mutex.Unlock() if callback != nil { go callback(pr) } debug.Log(debug.DEBUG_PACKETS, "Proof validated", "hash", fmt.Sprintf("%x", pr.truncatedHash)) return true } } else if len(proof) == IMPL_LENGTH { signature := proof[:identity.SIGLENGTH/8] pr.mutex.RLock() ident := pr.destinationIdent pr.mutex.RUnlock() if ident == nil { return false } proofValid := ident.Verify(pr.hash, signature) if proofValid { pr.mutex.Lock() pr.status = RECEIPT_DELIVERED pr.proved = true pr.concludedAt = time.Now() pr.proofPacket = proofPacket callback := pr.deliveryCallback pr.mutex.Unlock() if callback != nil { go callback(pr) } debug.Log(debug.DEBUG_PACKETS, "Implicit proof validated", "hash", fmt.Sprintf("%x", pr.truncatedHash)) return true } } return false } func (pr *PacketReceipt) validateLinkSignature(signature []byte, link interface{}) bool { type linkValidator interface { Validate(signature, message []byte) bool } if validator, ok := link.(linkValidator); ok { return validator.Validate(signature, pr.hash) } debug.Log(debug.DEBUG_TRACE, "Link does not implement Validate method") return false } func (pr *PacketReceipt) GetRTT() time.Duration { pr.mutex.RLock() defer pr.mutex.RUnlock() if pr.concludedAt.IsZero() { return 0 } return pr.concludedAt.Sub(pr.sentAt) } func (pr *PacketReceipt) IsTimedOut() bool { pr.mutex.RLock() defer pr.mutex.RUnlock() return time.Since(pr.sentAt) > pr.timeout } func (pr *PacketReceipt) checkTimeout() { pr.mutex.Lock() if pr.status != RECEIPT_SENT { pr.mutex.Unlock() return } if time.Since(pr.sentAt) <= pr.timeout { pr.mutex.Unlock() return } if pr.timeout < 0 { pr.status = RECEIPT_CULLED } else { pr.status = RECEIPT_FAILED } pr.concludedAt = time.Now() callback := pr.timeoutCallback pr.mutex.Unlock() debug.Log(debug.DEBUG_VERBOSE, "Packet receipt timed out", "hash", fmt.Sprintf("%x", pr.truncatedHash)) if callback != nil { go callback(pr) } } func (pr *PacketReceipt) timeoutWatchdog() { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: pr.checkTimeout() pr.mutex.RLock() status := pr.status pr.mutex.RUnlock() if status != RECEIPT_SENT { return } case <-pr.timeoutCheckDone: return } } } func (pr *PacketReceipt) SetTimeout(timeout time.Duration) { pr.mutex.Lock() defer pr.mutex.Unlock() pr.timeout = timeout } func (pr *PacketReceipt) SetDeliveryCallback(callback func(*PacketReceipt)) { pr.mutex.Lock() defer pr.mutex.Unlock() pr.deliveryCallback = callback } func (pr *PacketReceipt) SetTimeoutCallback(callback func(*PacketReceipt)) { pr.mutex.Lock() defer pr.mutex.Unlock() pr.timeoutCallback = callback } func (pr *PacketReceipt) SetDestinationIdentity(ident *identity.Identity) { pr.mutex.Lock() defer pr.mutex.Unlock() pr.destinationIdent = ident } func (pr *PacketReceipt) SetLink(link interface{}) { pr.mutex.Lock() defer pr.mutex.Unlock() pr.link = link } func (pr *PacketReceipt) Cancel() { pr.mutex.Lock() defer pr.mutex.Unlock() if pr.status == RECEIPT_SENT { pr.status = RECEIPT_CULLED pr.concludedAt = time.Now() } select { case pr.timeoutCheckDone <- true: default: } }