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
63 changes: 63 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: CI

on:
push:
branches: [master]
pull_request:

jobs:
contracts:
name: Contracts (forge)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

# lib/ is gitignored (not vendored, not submodules), so CI fetches the
# Solidity deps at the versions foundry.toml's remappings expect.
# OZ is pinned to v5.6.1; forge-std tracks its default branch (test-only
# scaffolding, stable cheatcode API).
- name: Fetch Solidity dependencies
run: |
git clone --depth 1 --branch v5.6.1 https://github.com/OpenZeppelin/openzeppelin-contracts.git lib/openzeppelin-contracts
git clone --depth 1 --branch v5.6.1 https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable.git lib/openzeppelin-contracts-upgradeable
git clone --depth 1 https://github.com/foundry-rs/forge-std.git lib/forge-std

- name: Build
run: forge build --sizes

- name: Test (ci profile — 5000 fuzz runs)
env:
FOUNDRY_PROFILE: ci
run: forge test -vvv

sdk:
name: SDK (vitest)
runs-on: ubuntu-latest
defaults:
run:
working-directory: packages/sdk
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- run: npm test

oracle:
name: Oracle (node --test)
runs-on: ubuntu-latest
defaults:
run:
working-directory: oracle
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: node --test
26 changes: 15 additions & 11 deletions Countersig_Build_Document.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

The Countersig Network is a decentralized identity and trust protocol designed explicitly for autonomous AI agents. As AI agents evolve into independent economic actors, the lack of verifiable Non-Human Identity (NHI) presents a systemic risk, enabling Sybil attacks, reputation manipulation, and unaccountable execution.

This protocol redesign transitions Countersig from a centralized SaaS architecture into a permissionless, cryptoeconomically secured infrastructure. The design philosophy centers on **verifiability, interoperability, and economic accountability**. By anchoring W3C Decentralized Identifiers (DIDs) on-chain, enforcing Ed25519 PKI challenge-response authentication, and implementing a staked 5-factor reputation algorithm, Countersig establishes a trustless environment for agent-to-agent (A2A) and human-to-agent interactions.
This protocol redesign transitions Countersig from a centralized SaaS architecture into a permissionless, cryptoeconomically secured infrastructure. The design philosophy centers on **verifiability, interoperability, and economic accountability**. By anchoring W3C Decentralized Identifiers (DIDs) on-chain, enforcing Ed25519 PKI challenge-response authentication, and implementing a staked 6-factor reputation algorithm, Countersig establishes a trustless environment for agent-to-agent (A2A) and human-to-agent interactions.

**Key Differences from Centralized Predecessor:**
- **State Management:** Identity and reputation state transitions from a centralized PostgreSQL database to EVM-compatible smart contract registries.
Expand Down Expand Up @@ -49,7 +49,7 @@ The Countersig Network operates across three layers: the On-Chain State Layer, t
```

**Core Components:**
- **On-Chain State:** Three primary EVM registries manage the lifecycle of agent identities, compute their 5-factor reputation scores, and store verifiable credential hashes. The Staking Core enforces economic security.
- **On-Chain State:** Three primary EVM registries manage the lifecycle of agent identities, compute their 6-factor reputation scores, and store verifiable credential hashes. The Staking Core enforces economic security.
- **Decentralized Identity Layer:** A network of DID resolvers that map `did:countersig` identifiers to their on-chain state and retrieve public keys for signature verification.
- **Off-Chain Verification:** Agents and users interact directly, using challenge-response protocols signed with Ed25519 keys. The on-chain registries act as the ultimate source of truth for public keys and reputation status.

Expand All @@ -67,7 +67,7 @@ Anchors the agent's identity and maps it to the operator's address and the agent
- **Key Interface:** `registerAgent(bytes32 didHash, bytes32 ed25519PubKey)`

### Registry 2: Trust/Reputation Registry
Maintains the 5-factor reputation score for each agent.
Maintains the 6-factor reputation score for each agent.

- **Storage Layout:**
- `mapping(bytes32 => ReputationData) public reputations;`
Expand Down Expand Up @@ -123,21 +123,25 @@ Agents authenticate off-chain using their Ed25519 keys, verified against the on-
3. **Sign:** Agent A signs the payload using its Ed25519 private key.
4. **Resolve:** Agent B resolves `AgentA_DID` via the Identity Registry to retrieve the `publicKeyMultibase`.
5. **Verify:** Agent B verifies the signature using the retrieved public key. If valid, authentication succeeds.
## 5. Reputation System: 5-Factor Algorithm
## 5. Reputation System: 6-Factor Algorithm

The Countersig reputation system computes a deterministic score (0-100) using a 5-factor model. This score is stored in the Reputation Registry and determines the agent's trust tier.
The Countersig reputation system computes a deterministic score (0-100) using a 6-factor model. This score is stored in the Reputation Registry and determines the agent's trust tier.

### The 5 Factors and Weights
1. **Fee Activity (30%):** Measures economic utility. Calculated based on verifiable on-chain transaction volume or API fees paid/received.
### The 6 Factors and Weights
1. **Fee Activity (30%):** Measures economic utility. Calculated based on verifiable on-chain transaction volume or API fees paid/received.
- *Scoring:* `min(30, floor(totalFeesUSD / 100))`
2. **Success Rate (25%):** Measures operational reliability. Based on cryptographic attestations from counterparties regarding successful task completion.
- *Scoring:* `floor((successfulTasks / totalTasks) * 25)`
3. **Registration Age (20%):** Measures longevity and persistence.
- *Scoring:* `min(20, daysSinceRegistration)`
3. **Registration Age (20%):** Measures longevity and persistence. Logarithmic, so it can't be farmed simply by waiting.
- *Scoring:* `min(20, floor(log2(daysSinceRegistration + 1) * 4))`
4. **External Trust (15%):** Measures cross-platform validity. Integrates scores from external registries (e.g., SAID Protocol, Gitcoin Passport).
- *Scoring:* `floor((externalScore / 100) * 15)`
5. **Community Verification (10%):** Measures behavioral safety. Starts at 10, reduced by community flags or minor slashing events.
- *Scoring:* `10 - (activeFlags * 5)` (Min 0)
5. **Community Verification (5%):** Measures behavioral safety. Starts at 5, reduced by community flags.
- *Scoring:* `max(0, 5 - (activeFlags * 2))`
6. **Trust Propagation (5%):** Inherited trust from high-reputation agents that vouch for or delegate to this agent.
- *Scoring:* oracle-computed from the attestation/vouch graph.

> **Phase note:** External Trust (4) and Trust Propagation (6) are stubbed to 0 until the Phase 2 oracle network is live; the on-chain caps and the other four factors are active today. This matches `docs/reputation-model.md`, `CountersigReputation.sol`, and `oracle/scoring.js`.

### On-Chain Scoring Mechanics
To optimize gas, the Reputation Registry does not compute the score on every transaction. Instead, an off-chain decentralized oracle network aggregates the raw data, computes the score, and submits a state update to the registry periodically (e.g., every 24 hours) or upon a significant deviation threshold.
Expand Down
29 changes: 28 additions & 1 deletion oracle/http-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,31 @@ function parseScorePath(pathname) {
return match ? match[1] : null;
}

module.exports = { MAX_BODY_SIZE, json, readBody, isAuthorized, parseScorePath };
// Simple in-memory fixed-window rate limiter, keyed by an arbitrary string
// (caller passes the client IP). Testnet-grade: single-process and resets on
// restart, but it blunts bursts of writes to /attest, /flag, and /epoch on top
// of the bearer-token gate — a stolen token can no longer flood scores/gas.
const RATE_WINDOW_MS = 60_000;
const RATE_MAX = 60; // requests per key per window
const _rateBuckets = new Map();

function rateLimited(key, now = Date.now(), max = RATE_MAX, windowMs = RATE_WINDOW_MS) {
const bucket = _rateBuckets.get(key);
if (!bucket || now >= bucket.reset) {
_rateBuckets.set(key, { count: 1, reset: now + windowMs });
return false;
}
bucket.count++;
return bucket.count > max;
}

module.exports = {
MAX_BODY_SIZE,
RATE_WINDOW_MS,
RATE_MAX,
json,
readBody,
isAuthorized,
parseScorePath,
rateLimited,
};
30 changes: 29 additions & 1 deletion oracle/http-helpers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const { test } = require('node:test');
const assert = require('node:assert/strict');
const { EventEmitter } = require('node:events');
const { readBody, isAuthorized, parseScorePath } = require('./http-helpers');
const { readBody, isAuthorized, parseScorePath, rateLimited, RATE_MAX } = require('./http-helpers');

// Minimal fake matching the subset of http.IncomingMessage that readBody uses:
// an EventEmitter with data/end/error events plus a destroy() method.
Expand Down Expand Up @@ -113,3 +113,31 @@ test('parseScorePath: missing 0x prefix returns null', () => {
test('parseScorePath: unrelated path returns null', () => {
assert.equal(parseScorePath('/health'), null);
});

// -------------------------------------------------------------------------
// rateLimited
// -------------------------------------------------------------------------

test('rateLimited: allows up to the max, then blocks within the window', () => {
const key = 'ip-a';
const now = 1_000_000;
for (let i = 0; i < RATE_MAX; i++) {
assert.equal(rateLimited(key, now), false, `request ${i + 1} should pass`);
}
assert.equal(rateLimited(key, now), true, 'the (max+1)th request is blocked');
});

test('rateLimited: window reset clears the count', () => {
const key = 'ip-b';
const now = 2_000_000;
for (let i = 0; i < RATE_MAX; i++) rateLimited(key, now);
assert.equal(rateLimited(key, now), true, 'blocked at the cap');
assert.equal(rateLimited(key, now + 60_001), false, 'allowed again after the window');
});

test('rateLimited: separate keys have independent buckets', () => {
const now = 3_000_000;
for (let i = 0; i < RATE_MAX; i++) rateLimited('ip-c', now);
assert.equal(rateLimited('ip-c', now), true, 'ip-c is capped');
assert.equal(rateLimited('ip-d', now), false, 'ip-d is unaffected');
});
10 changes: 9 additions & 1 deletion oracle/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ const http = require('http');
const chain = require('./chain');
const { computeScore } = require('./scoring');
const { decideAction } = require('./epoch-policy');
const { json, readBody, isAuthorized, parseScorePath } = require('./http-helpers');
const { json, readBody, isAuthorized, parseScorePath, rateLimited } = require('./http-helpers');

// Per-client key for rate limiting. Behind the container's 127.0.0.1 port map all
// requests may share one source IP, so this degrades to a global cap — still a
// useful flood guard for the write endpoints.
const clientKey = req => req.socket?.remoteAddress || 'unknown';

// ---- Config ----------------------------------------------------------------

Expand Down Expand Up @@ -165,6 +170,7 @@ const server = http.createServer(async (req, res) => {
// Gated: a manual epoch submits on-chain tx's paid from the oracle wallet, so
// it must not be triggerable by anyone who can reach the port.
if (!isAuthorized(req.headers, cfg.adminToken)) return json(res, 401, { error: 'Unauthorized' });
if (rateLimited(clientKey(req))) return json(res, 429, { error: 'Rate limited' });
if (epochRunning) return json(res, 409, { error: 'Epoch already running' });
runEpoch().catch(err => console.error('[oracle] manual epoch error:', err.message));
return json(res, 202, { message: 'Epoch started' });
Expand All @@ -173,6 +179,7 @@ const server = http.createServer(async (req, res) => {
// POST /attest — body: { didHash, success }
if (req.method === 'POST' && pathname === '/attest') {
if (!isAuthorized(req.headers, cfg.adminToken)) return json(res, 401, { error: 'Unauthorized' });
if (rateLimited(clientKey(req))) return json(res, 429, { error: 'Rate limited' });
try {
const { didHash, success } = await readBody(req);
if (!didHash) return json(res, 400, { error: 'didHash required' });
Expand All @@ -189,6 +196,7 @@ const server = http.createServer(async (req, res) => {
// POST /flag — body: { didHash }
if (req.method === 'POST' && pathname === '/flag') {
if (!isAuthorized(req.headers, cfg.adminToken)) return json(res, 401, { error: 'Unauthorized' });
if (rateLimited(clientKey(req))) return json(res, 429, { error: 'Rate limited' });
try {
const { didHash } = await readBody(req);
if (!didHash) return json(res, 400, { error: 'didHash required' });
Expand Down
Loading