Some checks failed
Bearer / scan (push) Successful in 9s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 42s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 44s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 41s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 39s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 1m8s
Go Build Multi-Platform / build (wasm, js) (push) Successful in 1m6s
TinyGo Build / tinygo-build (tinygo-wasm, tinygo-wasm, reticulum-go.wasm, wasm) (pull_request) Failing after 1m2s
TinyGo Build / tinygo-build (tinygo-build, tinygo-default, reticulum-go-tinygo, ) (pull_request) Failing after 1m4s
Go Revive Lint / lint (push) Successful in 1m4s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m24s
Run Gosec / tests (push) Successful in 1m29s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 2m31s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 9m28s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 9m28s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 9m30s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m27s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 9m26s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m29s
Go Build Multi-Platform / Create Release (push) Has been skipped
345 lines
7.6 KiB
Go
345 lines
7.6 KiB
Go
// 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:
|
|
}
|
|
}
|