Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,38 @@ export const envSchema = z.object({
.default('5')
.transform(Number)
.pipe(z.number().int().min(1)),

// Reputation module (snapshot/persisted) scoring weights
REPUTATION_BOND_MULTIPLIER: z
.string()
.default('0.01')
.transform(Number)
.pipe(z.number().min(0)),
REPUTATION_MAX_BOND_SCORE: z
.string()
.default('1000')
.transform(Number)
.pipe(z.number().min(0)),
REPUTATION_ATTESTATION_MULTIPLIER: z
.string()
.default('0.1')
.transform(Number)
.pipe(z.number().min(0)),
REPUTATION_MAX_ATTESTATION_WEIGHT: z
.string()
.default('100')
.transform(Number)
.pipe(z.number().min(0)),
REPUTATION_MAX_DURATION_MS: z
.string()
.default('31536000000')
.transform(Number)
.pipe(z.number().int().min(1)),
REPUTATION_TIME_DECAY_RATE: z
.string()
.default('0.5')
.transform(Number)
.pipe(z.number().min(0).max(10)),
SOROBAN_CIRCUIT_BREAKER_FAILURE_THRESHOLD: z
.string()
.default('5')
Expand Down Expand Up @@ -773,6 +805,12 @@ export interface Config {
oneEthWei: bigint
maxDurationDays: number
maxAttestationCount: number
bondMultiplier: number
maxBondScore: number
attestationMultiplier: number
maxAttestationWeight: number
maxDurationMs: number
decayRate: number
}
sorobanCircuitBreaker: {
failureThreshold: number
Expand Down Expand Up @@ -1036,6 +1074,12 @@ function mapEnvToConfig(env: Env): Config {
oneEthWei: BigInt(env.REPUTATION_ONE_ETH_WEI),
maxDurationDays: env.REPUTATION_MAX_DURATION_DAYS,
maxAttestationCount: env.REPUTATION_MAX_ATTESTATION_COUNT,
bondMultiplier: env.REPUTATION_BOND_MULTIPLIER,
maxBondScore: env.REPUTATION_MAX_BOND_SCORE,
attestationMultiplier: env.REPUTATION_ATTESTATION_MULTIPLIER,
maxAttestationWeight: env.REPUTATION_MAX_ATTESTATION_WEIGHT,
maxDurationMs: env.REPUTATION_MAX_DURATION_MS,
decayRate: env.REPUTATION_TIME_DECAY_RATE,
},
trustScoreCache: {
ttl: env.TRUST_SCORE_CACHE_TTL,
Expand Down
3 changes: 2 additions & 1 deletion src/jobs/scoreSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
} from './types.js'
import { trustScoreInvalidationHook } from '../cache/invalidationHooks.js'
import { logger } from '../utils/logger.js'
import { loadConfig } from '../config/index.js'

/**
* Options for score snapshot job.
Expand Down Expand Up @@ -54,7 +55,7 @@ export class ScoreSnapshotJob {
this.batchSize = options.batchSize ?? 100
this.continueOnError = options.continueOnError ?? true
this.logger = options.logger ?? (() => {})
this.scoringModelVersion = options.scoringModelVersion ?? '1.0.0'
this.scoringModelVersion = options.scoringModelVersion ?? loadConfig().reputation.scoringModelVersion
}

/**
Expand Down
49 changes: 32 additions & 17 deletions src/services/reputation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,22 @@ REPUTATION_ATTESTATION_SCORE_MAX=30
REPUTATION_ONE_ETH_WEI=1000000000000000000 # 1 ETH in wei
REPUTATION_MAX_DURATION_DAYS=365 # Days for full duration score
REPUTATION_MAX_ATTESTATION_COUNT=5 # Attestations for full score

# Module-level weight parameters
REPUTATION_BOND_MULTIPLIER=0.01 # Bond amount multiplier
REPUTATION_MAX_BOND_SCORE=1000 # Bond score cap
REPUTATION_ATTESTATION_MULTIPLIER=0.1 # Attestation weight multiplier
REPUTATION_MAX_ATTESTATION_WEIGHT=100 # Attestation score cap
REPUTATION_MAX_DURATION_MS=31536000000 # Max duration for full time weight (1 year in ms)
REPUTATION_TIME_DECAY_RATE=0.5 # Exponential decay rate for time weight
```

### Configuration Validation

- All score maxima are validated to be between 0 and 100
- `REPUTATION_ONE_ETH_WEI` must be a valid BigInt string
- Duration and attestation count must be positive integers
- Module weight parameters have sensible defaults and runtime validation
- Invalid configuration will cause the application to fail at startup with a clear error message

### Tuning the Model
Expand All @@ -48,6 +57,13 @@ REPUTATION_DURATION_SCORE_MAX=20
REPUTATION_ATTESTATION_SCORE_MAX=50
```

**Override module weight defaults via config:**
```bash
REPUTATION_BOND_MULTIPLIER=0.02
REPUTATION_MAX_BOND_SCORE=2000
REPUTATION_ATTESTATION_MULTIPLIER=0.2
```

## Formula

```
Expand Down Expand Up @@ -175,20 +191,19 @@ interface ReputationScore {
}
```

## Constants
## Default Constants

```typescript
// Bond Score
const BOND_MULTIPLIER = 0.01
const MAX_BOND_SCORE = 1000

// Attestation Score
const ATTESTATION_MULTIPLIER = 0.1
const MAX_ATTESTATION_WEIGHT = 100
The scoring functions have built-in defaults. All values can be overridden at runtime via `ReputationModuleConfig`:

// Time Weight
const DECAY_RATE = 0.5
const MAX_DURATION_MS = 365 * 24 * 60 * 60 * 1000 // 1 year
```typescript
const DEFAULT_CONFIG: ReputationModuleConfig = {
bondMultiplier: 0.01,
maxBondScore: 1000,
attestationMultiplier: 0.1,
maxAttestationWeight: 100,
maxDurationMs: 365 * 24 * 60 * 60 * 1000, // 1 year in ms
decayRate: 0.5,
}
```

## Examples
Expand Down Expand Up @@ -351,19 +366,19 @@ See [TEST_DOCUMENTATION.md](./TEST_DOCUMENTATION.md) for detailed test scenarios

### Functions

#### `calculateReputationScore(input: ReputationInput): ReputationScore`
#### `calculateReputationScore(input: ReputationInput, identityId?: string, config?: ReputationModuleConfig): ReputationScore`
Calculate comprehensive reputation score with all components.

#### `calculateReputationScoreWithCustomDuration(input: ReputationInput, maxDuration: number): ReputationScore`
#### `calculateReputationScoreWithCustomDuration(input: ReputationInput, maxDuration: number, config?: ReputationModuleConfig): ReputationScore`
Calculate reputation score with custom maximum duration for time weight.

#### `calculateBondScore(bond: BondData): number`
#### `calculateBondScore(bond: BondData, config?: ReputationModuleConfig): number`
Calculate bond score component only.

#### `calculateAttestationScore(attestations: Attestation[]): number`
#### `calculateAttestationScore(attestations: Attestation[], config?: ReputationModuleConfig): number`
Calculate attestation score component only.

#### `calculateTimeWeight(bondStart: number, currentTime: number, maxDuration?: number): number`
#### `calculateTimeWeight(bondStart: number, currentTime: number, maxDuration?: number, config?: ReputationModuleConfig): number`
Calculate time weight component only.

### Getters
Expand Down
43 changes: 43 additions & 0 deletions src/services/reputation/attestationScore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getAttestationMultiplier,
} from './attestationScore.js'
import type { Attestation } from './types.js'
import type { ReputationModuleConfig } from './types.js'

describe('attestationScore', () => {
describe('calculateAttestationScore', () => {
Expand Down Expand Up @@ -255,4 +256,46 @@ describe('attestationScore', () => {
expect(multiplier).toBe(0.1)
})
})

describe('config-driven scoring', () => {
it('should use custom multiplier from config', () => {
const config: ReputationModuleConfig = {
bondMultiplier: 0.01,
maxBondScore: 1000,
attestationMultiplier: 0.2,
maxAttestationWeight: 100,
maxDurationMs: 31536000000,
decayRate: 0.5,
}
const attestations: Attestation[] = [
{ weight: 100, timestamp: 1000, isValid: true },
]
const result = calculateAttestationScore(attestations, config)
expect(result).toBe(20) // 100 * 0.2
})

it('should use custom max weight from config', () => {
const config: ReputationModuleConfig = {
bondMultiplier: 0.01,
maxBondScore: 1000,
attestationMultiplier: 0.1,
maxAttestationWeight: 50,
maxDurationMs: 31536000000,
decayRate: 0.5,
}
const attestations: Attestation[] = [
{ weight: 1000, timestamp: 1000, isValid: true },
]
const result = calculateAttestationScore(attestations, config)
expect(result).toBe(50) // Capped at custom max
})

it('should default to module defaults when no config provided', () => {
const attestations: Attestation[] = [
{ weight: 100, timestamp: 1000, isValid: true },
]
const result = calculateAttestationScore(attestations)
expect(result).toBe(10) // 100 * 0.1
})
})
})
27 changes: 20 additions & 7 deletions src/services/reputation/attestationScore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,29 @@
*/

import type { Attestation } from './types.js'

const MAX_ATTESTATION_WEIGHT = 100
const ATTESTATION_MULTIPLIER = 0.1
import type { ReputationModuleConfig } from './types.js'

const DEFAULT_CONFIG: ReputationModuleConfig = {
bondMultiplier: 0.01,
maxBondScore: 1000,
attestationMultiplier: 0.1,
maxAttestationWeight: 100,
maxDurationMs: 365 * 24 * 60 * 60 * 1000,
decayRate: 0.5,
}

/**
* Calculate attestation score from attestations
* @param attestations - Array of attestations
* @param config - Optional scoring configuration (defaults to module defaults)
* @returns Attestation score
*/
export function calculateAttestationScore(attestations: Attestation[]): number {
export function calculateAttestationScore(
attestations: Attestation[],
config?: ReputationModuleConfig
): number {
const { attestationMultiplier, maxAttestationWeight } = config ?? DEFAULT_CONFIG

if (!attestations || attestations.length === 0) {
return 0
}
Expand All @@ -30,7 +43,7 @@ export function calculateAttestationScore(attestations: Attestation[]): number {
}, 0)

// Apply multiplier and cap at max
const score = Math.min(totalWeight * ATTESTATION_MULTIPLIER, MAX_ATTESTATION_WEIGHT)
const score = Math.min(totalWeight * attestationMultiplier, maxAttestationWeight)

return score
}
Expand All @@ -39,12 +52,12 @@ export function calculateAttestationScore(attestations: Attestation[]): number {
* Get the maximum attestation weight constant
*/
export function getMaxAttestationWeight(): number {
return MAX_ATTESTATION_WEIGHT
return DEFAULT_CONFIG.maxAttestationWeight
}

/**
* Get the attestation multiplier constant
*/
export function getAttestationMultiplier(): number {
return ATTESTATION_MULTIPLIER
return DEFAULT_CONFIG.attestationMultiplier
}
52 changes: 52 additions & 0 deletions src/services/reputation/bondScore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { describe, it, expect } from 'vitest'
import { calculateBondScore, getBondMultiplier, getMaxBondScore } from './bondScore.js'
import type { BondData } from './types.js'
import type { ReputationModuleConfig } from './types.js'

describe('bondScore', () => {
describe('calculateBondScore', () => {
Expand Down Expand Up @@ -253,4 +254,55 @@ describe('bondScore', () => {
expect(maxScore).toBe(1000)
})
})

describe('config-driven scoring', () => {
it('should use custom multiplier from config', () => {
const config: ReputationModuleConfig = {
bondMultiplier: 0.05,
maxBondScore: 1000,
attestationMultiplier: 0.1,
maxAttestationWeight: 100,
maxDurationMs: 31536000000,
decayRate: 0.5,
}
const bond: BondData = {
bondedAmount: 1000,
bondStart: 1000000,
bondDuration: 100000,
isSlashed: false,
}
const result = calculateBondScore(bond, config)
expect(result).toBe(50) // 1000 * 0.05
})

it('should use custom maxBondScore from config', () => {
const config: ReputationModuleConfig = {
bondMultiplier: 0.01,
maxBondScore: 500,
attestationMultiplier: 0.1,
maxAttestationWeight: 100,
maxDurationMs: 31536000000,
decayRate: 0.5,
}
const bond: BondData = {
bondedAmount: 100000,
bondStart: 1000000,
bondDuration: 100000,
isSlashed: false,
}
const result = calculateBondScore(bond, config)
expect(result).toBe(500) // Capped at custom max
})

it('should default to module defaults when no config provided', () => {
const bond: BondData = {
bondedAmount: 1000,
bondStart: 1000000,
bondDuration: 100000,
isSlashed: false,
}
const result = calculateBondScore(bond)
expect(result).toBe(10) // 1000 * 0.01
})
})
})
Loading
Loading