mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-13 21:12:03 +00:00
feat: add durable result policy core
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
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"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user