package domain import ( "crypto/sha256" "fmt" "strconv" "time" ) const ( ResultDeliveryAlertAfter = 5 * time.Minute ResultDeliveryReviewAfter = 30 * time.Minute ) type IntegrityState string const ( IntegrityCertified IntegrityState = "CERTIFIED" IntegritySuppressed IntegrityState = "SUPPRESSED" IntegrityReview IntegrityState = "REVIEW" ) type IntegrityEvidence struct { RosterAuthoritative bool SimulationAuthoritative bool ResultAuthoritative bool RegionalPlayFair bool DeliveryAvailable bool } // ClassifyIntegrity deliberately ignores DeliveryAvailable when deciding // rating eligibility: a healthy match remains rated while the control plane // is temporarily unable to acknowledge its result. func ClassifyIntegrity(evidence IntegrityEvidence) IntegrityState { if !evidence.RosterAuthoritative || !evidence.SimulationAuthoritative || !evidence.ResultAuthoritative || !evidence.RegionalPlayFair { return IntegritySuppressed } return IntegrityCertified } var ( ErrResultBinding = fmt.Errorf("result workload binding rejected") ErrResultConflict = fmt.Errorf("conflicting result") ErrResultInvalid = fmt.Errorf("invalid match result") ErrReceiptMissing = fmt.Errorf("result receipt not found") ) // WorkloadBinding is the identity extracted and validated by the secure // credential adapter. The domain compares every binding dimension recorded by // allocation; a shared service-account class is not sufficient on its own. type WorkloadBinding struct { Issuer string Audience string Namespace string ServiceAcct string PodUID string GameServerUID string MatchID string ServerID string } type MatchResult struct { MatchID string ServerID string ResultNonce string Team0Score int Team1Score int IntegrityState IntegrityState } type ResultReceipt struct { ResultID string MatchID string ResultNonce string PayloadDigest [32]byte IntegrityState IntegrityState ReceivedAt time.Time CommittedAt time.Time } type ResultStore struct { expected WorkloadBinding receipts map[string]ResultReceipt } func NewResultStore(expected WorkloadBinding) (*ResultStore, error) { if err := validateBinding(expected); err != nil { return nil, err } return &ResultStore{expected: expected, receipts: make(map[string]ResultReceipt)}, nil } // Submit is the durable-transaction boundary in miniature. Production code // must persist the receipt, match transition, participant penalties/ratings, // and outbox event atomically around this same decision. func (s *ResultStore) Submit(resultID string, result MatchResult, binding WorkloadBinding, now time.Time) (ResultReceipt, bool, error) { if resultID == "" || !sameBinding(s.expected, binding) { return ResultReceipt{}, false, ErrResultBinding } if err := validateResult(s.expected, result); err != nil { return ResultReceipt{}, false, err } digest := resultDigest(result) if prior, ok := s.receipts[result.MatchID]; ok { if prior.ResultID == resultID && prior.PayloadDigest == digest { return prior, false, nil } return prior, false, ErrResultConflict } receipt := ResultReceipt{ResultID: resultID, MatchID: result.MatchID, ResultNonce: result.ResultNonce, PayloadDigest: digest, IntegrityState: result.IntegrityState, ReceivedAt: now} s.receipts[result.MatchID] = receipt return receipt, true, nil } // ResultAnnotation is the non-secret Agones spool representation. Its // signature is checked by the workload-credential adapter before Reconcile; // the digest check here prevents annotation/payload drift even after trust // has been established. type ResultAnnotation struct { ResultID string Result MatchResult PayloadDigest [32]byte Signature []byte } func (s *ResultStore) Reconcile(annotation ResultAnnotation, verify func(ResultAnnotation) bool, binding WorkloadBinding, now time.Time) (ResultReceipt, bool, error) { if len(annotation.Signature) == 0 || verify == nil || !verify(annotation) || annotation.PayloadDigest != resultDigest(annotation.Result) { return ResultReceipt{}, false, ErrResultBinding } return s.Submit(annotation.ResultID, annotation.Result, binding, now) } func (s *ResultStore) Commit(resultID, matchID string, now time.Time) (ResultReceipt, error) { receipt, ok := s.receipts[matchID] if !ok || receipt.ResultID != resultID { return ResultReceipt{}, ErrReceiptMissing } if receipt.CommittedAt.IsZero() { receipt.CommittedAt = now s.receipts[matchID] = receipt } return receipt, nil } type DeliveryHealth string const ( DeliveryHealthy DeliveryHealth = "HEALTHY" DeliveryAlert DeliveryHealth = "ALERT" DeliveryReview DeliveryHealth = "REVIEW" ) func DeliveryStatus(receipt ResultReceipt, now time.Time) DeliveryHealth { if !receipt.CommittedAt.IsZero() { return DeliveryHealthy } age := now.Sub(receipt.ReceivedAt) if age >= ResultDeliveryReviewAfter { return DeliveryReview } if age >= ResultDeliveryAlertAfter { return DeliveryAlert } return DeliveryHealthy } func RatingEligible(receipt ResultReceipt) bool { return receipt.IntegrityState == IntegrityCertified } func validateBinding(binding WorkloadBinding) error { if binding.Issuer == "" || binding.Audience == "" || binding.Namespace == "" || binding.ServiceAcct == "" || binding.PodUID == "" || binding.GameServerUID == "" || binding.MatchID == "" || binding.ServerID == "" { return ErrResultBinding } return nil } func sameBinding(a, b WorkloadBinding) bool { return a == b } func validateResult(expected WorkloadBinding, result MatchResult) error { if result.MatchID != expected.MatchID || result.ServerID != expected.ServerID || len(result.ResultNonce) < 16 || len(result.ResultNonce) > 128 || result.Team0Score < 0 || result.Team1Score < 0 { return ErrResultInvalid } switch result.IntegrityState { case IntegrityCertified, IntegritySuppressed, IntegrityReview: return nil default: return ErrResultInvalid } } func resultDigest(result MatchResult) [32]byte { canonical := result.MatchID + "\x00" + result.ServerID + "\x00" + result.ResultNonce + "\x00" + strconv.Itoa(result.Team0Score) + "\x00" + strconv.Itoa(result.Team1Score) + "\x00" + string(result.IntegrityState) return sha256.Sum256([]byte(canonical)) }