ship(P06): merge Identity/Standing into milestone v0.1
---ci--- project: oy phase: 6 milestone: v0.1 status: complete ship: v0.0.6 ---/ci---
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
ModuleName = "identity"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
)
|
||||
|
||||
// Reach is the mesh-native identity layer (§8, §9.6 Layer 1)
|
||||
// Cryptographic, virtual, no state identity
|
||||
type Reach struct {
|
||||
ReachID string `json:"reach_id" yaml:"reach_id"`
|
||||
HolderID string `json:"holder_id" yaml:"holder_id"`
|
||||
CreatedAt int64 `json:"created_at" yaml:"created_at"`
|
||||
PublicKey string `json:"public_key" yaml:"public_key"`
|
||||
IsNomad bool `json:"is_nomad" yaml:"is_nomad"`
|
||||
IsFreeholder bool `json:"is_freeholder" yaml:"is_freeholder"`
|
||||
}
|
||||
|
||||
// CitizenshipTier (§9)
|
||||
type CitizenshipTier string
|
||||
|
||||
const (
|
||||
TierNomad CitizenshipTier = "Nomad" // any Holder (lifestyle tier)
|
||||
TierFreeholder CitizenshipTier = "Freeholder" // earned full recognition (sovereign tier)
|
||||
)
|
||||
|
||||
// TwoLayerPassport (§9.6)
|
||||
// Layer 1: Reach (mesh-native, virtual, cryptographic)
|
||||
// Layer 2: Pier-routed real-world credentials (e-Residency, biometric IDs)
|
||||
// OY does not issue passports; OY routes to Piers that do.
|
||||
type TwoLayerPassport struct {
|
||||
ReachID string `json:"reach_id" yaml:"reach_id"`
|
||||
Layer1Reach bool `json:"layer1_reach" yaml:"layer1_reach"`
|
||||
Layer2PierRouted bool `json:"layer2_pier_routed" yaml:"layer2_pier_routed"`
|
||||
PierCredentialRef string `json:"pier_credential_ref" yaml:"pier_credential_ref"`
|
||||
}
|
||||
|
||||
// WayfarersRecord is an optional Reach-signed attestation (§9.5)
|
||||
// Credential of belonging, not state identity
|
||||
type WayfarersRecord struct {
|
||||
ReachID string `json:"reach_id" yaml:"reach_id"`
|
||||
FreeholderStatus bool `json:"freeholder_status" yaml:"freeholder_status"`
|
||||
StandingScore float64 `json:"standing_score" yaml:"standing_score"`
|
||||
GuildAffiliations []string `json:"guild_affiliations" yaml:"guild_affiliations"`
|
||||
BloomHistory string `json:"bloom_history" yaml:"bloom_history"`
|
||||
IssuedAt int64 `json:"issued_at" yaml:"issued_at"`
|
||||
}
|
||||
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Reaches []Reach `json:"reaches" yaml:"reaches"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{Params: DefaultParams(), Reaches: []Reach{}}
|
||||
}
|
||||
|
||||
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||
@@ -0,0 +1,160 @@
|
||||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
ModuleName = "standing"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// Standing formula parameters (§9.2 — LOCKED)
|
||||
PriorMean = 4.0 // Bayesian prior mean
|
||||
PriorWeight = 10 // Bayesian prior weight
|
||||
DecayBucket6mo = 1.0 // 6mo: 100pct
|
||||
DecayBucket12mo = 0.5 // 12mo: 50pct
|
||||
DecayBucket24mo = 0.25 // 24mo: 25pct
|
||||
DecayBucket24plus = 0.0 // 24mo+: 0pct
|
||||
|
||||
// Diversity bonus (§9.2)
|
||||
DiversityBonus3Cats = 0.05
|
||||
DiversityBonus4Cats = 0.10
|
||||
DiversityBonus5Cats = 0.15
|
||||
|
||||
// Voucher weights (§9.2)
|
||||
VoucherWeightFreeholder = 1.5
|
||||
VoucherWeight45Plus = 1.2
|
||||
VoucherWeight40To45 = 1.0
|
||||
VoucherWeightBelow40 = 0.5
|
||||
VoucherWeightBelow10Ratings = 0.3
|
||||
|
||||
// Minimum counterparties (§9.2)
|
||||
MinCounterpartiesValid = 3 // to be valid
|
||||
MinCounterpartiesFreeholderEligible = 10 // to be Freeholder-eligible
|
||||
MinCounterpartiesFreeholderStatus = 30 // across 3 categories for Freeholder status
|
||||
|
||||
// Freeholder Four Signals (§9.1 — LOCKED)
|
||||
FreeholderStashMaturityDays = 90 // 90 days Stash activity
|
||||
FreeholderStashMaxGapDays = 30 // no gap > 30 days
|
||||
FreeholderMinStandingScore = 4.5 // 4.5+ in at least 3 service categories
|
||||
FreeholderMinCategories = 3 // at least 3 service categories
|
||||
)
|
||||
|
||||
// Rating is a single rating event (§9.2)
|
||||
type Rating struct {
|
||||
RaterID string `json:"rater_id" yaml:"rater_id"`
|
||||
RateeID string `json:"ratee_id" yaml:"ratee_id"`
|
||||
Category string `json:"category" yaml:"category"`
|
||||
Score float64 `json:"score" yaml:"score"` // 0.0-5.0
|
||||
Weight float64 `json:"weight" yaml:"weight"` // derived from rater Voucher_Weight
|
||||
TxRef string `json:"tx_ref" yaml:"tx_ref"`
|
||||
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||
DecayBucket uint8 `json:"decay_bucket" yaml:"decay_bucket"`
|
||||
}
|
||||
|
||||
// Vouch is a Freeholder vouch with skin-in-the-game (§9.1)
|
||||
type Vouch struct {
|
||||
VoucherID string `json:"voucher_id" yaml:"voucher_id"`
|
||||
VoucheeID string `json:"vouchee_id" yaml:"vouchee_id"`
|
||||
Category string `json:"category" yaml:"category"`
|
||||
BondAmount int64 `json:"bond_amount" yaml:"bond_amount"` // voucher skin-in-the-game
|
||||
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||
}
|
||||
|
||||
// Slash penalizes a Holder (§9.4)
|
||||
type Slash struct {
|
||||
ReachID string `json:"reach_id" yaml:"reach_id"`
|
||||
Amount float64 `json:"amount" yaml:"amount"`
|
||||
Reason string `json:"reason" yaml:"reason"` // Crack, FraudulentCoverCall, InactivityTimeout
|
||||
Attester string `json:"attester" yaml:"attester"` // Watcher ID
|
||||
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||
}
|
||||
|
||||
// StandingBucket for display (§9.2)
|
||||
type StandingBucket string
|
||||
|
||||
const (
|
||||
BucketNew StandingBucket = "New" // <10 ratings
|
||||
BucketTrusted StandingBucket = "Trusted" // 4.0-4.4
|
||||
BucketPreferred StandingBucket = "Preferred" // 4.5-4.7
|
||||
BucketTop StandingBucket = "Top" // 4.8+
|
||||
BucketSlashed StandingBucket = "Slashed" // penalized
|
||||
)
|
||||
|
||||
// FreeholderSignals — all four must be present (§9.1 LOCKED)
|
||||
type FreeholderSignals struct {
|
||||
StashMaturity bool `json:"stash_maturity" yaml:"stash_maturity"`
|
||||
MultiDomainStanding bool `json:"multi_domain_standing" yaml:"multi_domain_standing"`
|
||||
CommittedCapital bool `json:"committed_capital" yaml:"committed_capital"`
|
||||
CommunityEndorsement bool `json:"community_endorsement" yaml:"community_endorsement"`
|
||||
}
|
||||
|
||||
// IsFreeholderEligible checks all four signals (§9.1)
|
||||
// No application, no committee, no form. All four must be present.
|
||||
func (s FreeholderSignals) IsFreeholderEligible() bool {
|
||||
return s.StashMaturity && s.MultiDomainStanding && s.CommittedCapital && s.CommunityEndorsement
|
||||
}
|
||||
|
||||
// ComputeDiversityBonus returns the bonus for category count (§9.2)
|
||||
func ComputeDiversityBonus(categoryCount int) float64 {
|
||||
switch {
|
||||
case categoryCount >= 5:
|
||||
return DiversityBonus5Cats
|
||||
case categoryCount == 4:
|
||||
return DiversityBonus4Cats
|
||||
case categoryCount == 3:
|
||||
return DiversityBonus3Cats
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// GetVoucherWeight returns the weight for a given rater profile (§9.2)
|
||||
func GetVoucherWeight(isFreeholder bool, standingScore float64, ratingCount int) float64 {
|
||||
if isFreeholder {
|
||||
return VoucherWeightFreeholder
|
||||
}
|
||||
if ratingCount < 10 {
|
||||
return VoucherWeightBelow10Ratings
|
||||
}
|
||||
if standingScore >= 4.5 {
|
||||
return VoucherWeight45Plus
|
||||
}
|
||||
if standingScore >= 4.0 {
|
||||
return VoucherWeight40To45
|
||||
}
|
||||
return VoucherWeightBelow40
|
||||
}
|
||||
|
||||
// GetStandingBucket returns the display bucket for a score (§9.2)
|
||||
func GetStandingBucket(score float64, ratingCount int, isSlashed bool) StandingBucket {
|
||||
if isSlashed {
|
||||
return BucketSlashed
|
||||
}
|
||||
if ratingCount < 10 {
|
||||
return BucketNew
|
||||
}
|
||||
switch {
|
||||
case score >= 4.8:
|
||||
return BucketTop
|
||||
case score >= 4.5:
|
||||
return BucketPreferred
|
||||
case score >= 4.0:
|
||||
return BucketTrusted
|
||||
}
|
||||
return BucketNew
|
||||
}
|
||||
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Ratings []Rating `json:"ratings" yaml:"ratings"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{Params: DefaultParams(), Ratings: []Rating{}}
|
||||
}
|
||||
|
||||
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||
@@ -0,0 +1,100 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/x/standing/types"
|
||||
)
|
||||
|
||||
func TestFreeholderSignalsAllPresent(t *testing.T) {
|
||||
signals := types.FreeholderSignals{
|
||||
StashMaturity: true,
|
||||
MultiDomainStanding: true,
|
||||
CommittedCapital: true,
|
||||
CommunityEndorsement: true,
|
||||
}
|
||||
if !signals.IsFreeholderEligible() {
|
||||
t.Error("All four signals present should be Freeholder eligible (§9.1)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFreeholderSignalsMissing(t *testing.T) {
|
||||
signals := types.FreeholderSignals{
|
||||
StashMaturity: true,
|
||||
MultiDomainStanding: true,
|
||||
CommittedCapital: true,
|
||||
CommunityEndorsement: false, // missing
|
||||
}
|
||||
if signals.IsFreeholderEligible() {
|
||||
t.Error("Missing one signal should NOT be Freeholder eligible (§9.1: all four must be present)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiversityBonus(t *testing.T) {
|
||||
if types.ComputeDiversityBonus(3) != 0.05 {
|
||||
t.Error("3 categories should give +0.05 bonus (§9.2)")
|
||||
}
|
||||
if types.ComputeDiversityBonus(4) != 0.10 {
|
||||
t.Error("4 categories should give +0.10 bonus (§9.2)")
|
||||
}
|
||||
if types.ComputeDiversityBonus(5) != 0.15 {
|
||||
t.Error("5+ categories should give +0.15 bonus (§9.2)")
|
||||
}
|
||||
if types.ComputeDiversityBonus(2) != 0.0 {
|
||||
t.Error("2 categories should give 0 bonus (§9.2)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVoucherWeights(t *testing.T) {
|
||||
if types.GetVoucherWeight(true, 4.0, 100) != 1.5 {
|
||||
t.Error("Freeholder weight should be 1.5x (§9.2)")
|
||||
}
|
||||
if types.GetVoucherWeight(false, 4.6, 100) != 1.2 {
|
||||
t.Error("4.5+ with 1-2 cats should be 1.2x (§9.2)")
|
||||
}
|
||||
if types.GetVoucherWeight(false, 4.2, 100) != 1.0 {
|
||||
t.Error("4.0-4.5 should be 1.0x (§9.2)")
|
||||
}
|
||||
if types.GetVoucherWeight(false, 3.5, 100) != 0.5 {
|
||||
t.Error("Below 4.0 should be 0.5x (§9.2)")
|
||||
}
|
||||
if types.GetVoucherWeight(false, 4.0, 5) != 0.3 {
|
||||
t.Error("Below 10 ratings should be 0.3x (§9.2)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStandingBuckets(t *testing.T) {
|
||||
if types.GetStandingBucket(4.9, 100, false) != types.BucketTop {
|
||||
t.Error("4.8+ should be Top (§9.2)")
|
||||
}
|
||||
if types.GetStandingBucket(4.6, 100, false) != types.BucketPreferred {
|
||||
t.Error("4.5-4.7 should be Preferred (§9.2)")
|
||||
}
|
||||
if types.GetStandingBucket(4.2, 100, false) != types.BucketTrusted {
|
||||
t.Error("4.0-4.4 should be Trusted (§9.2)")
|
||||
}
|
||||
if types.GetStandingBucket(3.5, 100, false) != types.BucketNew {
|
||||
t.Error("Below 4.0 should be New (§9.2)")
|
||||
}
|
||||
if types.GetStandingBucket(4.5, 5, false) != types.BucketNew {
|
||||
t.Error("Below 10 ratings should be New (§9.2)")
|
||||
}
|
||||
if types.GetStandingBucket(4.5, 100, true) != types.BucketSlashed {
|
||||
t.Error("Slashed should be Slashed bucket (§9.2)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLockedConstants(t *testing.T) {
|
||||
if types.FreeholderStashMaturityDays != 90 {
|
||||
t.Error("Stash maturity should be 90 days (§9.1 LOCKED)")
|
||||
}
|
||||
if types.FreeholderMinStandingScore != 4.5 {
|
||||
t.Error("Min standing score should be 4.5 (§9.1 LOCKED)")
|
||||
}
|
||||
if types.FreeholderMinCategories != 3 {
|
||||
t.Error("Min categories should be 3 (§9.1 LOCKED)")
|
||||
}
|
||||
if types.MinCounterpartiesFreeholderStatus != 30 {
|
||||
t.Error("Min counterparties for Freeholder status should be 30 (§9.2)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user