diff --git a/CHANGELOG.md b/CHANGELOG.md index 424286f..e558794 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ Version numbers track the fixture dataset, not a software release. ## [Unreleased] +### Changed — Destinations +- Added the required per-fixture `fixture_status` field; all existing destinations are marked `synthetic-only`. + + ### Added — Scenario Bundles (`scenarios/`) - Introduced versioned scenario bundles (`schema_version` `1.0`) for deterministic, offline, multi-transaction attack workflows. A scenario groups participants, destination/transaction references, and ordered steps with machine-checkable expected warnings and state transitions. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 68e6f31..f2e18f5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,8 +91,11 @@ Every new entry in `destinations.json` must include: | `asset_code` | ✅ (assets) | The asset code | | `asset_issuer` | ✅ (assets) | The issuing account's public key | | `label` | ✅ | One of `clean`, `suspicious`, `malicious` | +| `fixture_status` | ✅ | Lifecycle status; currently must be `"synthetic-only"` | | `notes` | ✅ | A one-sentence rationale explaining *why* this label was chosen, referencing the specific pattern. This is what downstream reviewers and evaluators read to verify the label is defensible. | +Every fixture is synthetic-only: it is not a record of live Horizon state and must not be used to infer network activity. The validator rejects missing or unknown `fixture_status` values. Future network-visible fixtures must be introduced with an explicitly supported status and coordinated adapter-schema change. + Every new entry must have a matching entry in `scores.json` with an integer score in 0–100. --- @@ -109,6 +112,7 @@ npm test - Every destination has a matching entry in `scores.json` - Every score is an integer in 0–100 - Every label is one of `clean`, `suspicious`, or `malicious` +- Every destination has `fixture_status: "synthetic-only"` (unknown or missing statuses fail validation) - No extra entries in `scores.json` without a matching destination - Every scenario in `scenarios/` passes schema, reference, and ordering validation diff --git a/README.md b/README.md index 6261d3f..f8fd8bd 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Gryd Lock's warning layer is only useful if it can be measured. This repo provid ## Features +- Per-fixture lifecycle status: every destination declares `fixture_status: "synthetic-only"` - Labelled Destinations: Stellar testnet addresses and assets tagged clean, suspicious, or malicious - Sample Transactions: unsigned transaction XDRs covering payment, path payment, change trust - Score Stubs: lookup table mapping each destination to a 0-100 score for deterministic offline scoring @@ -49,7 +50,7 @@ grydlock-testkit/ npm run validate -Checks that every destination in destinations.json has a matching entry in scores.json, every score is an integer in 0-100, and every label is one of clean, suspicious, or malicious. It also validates every scenario bundle under scenarios/ (schema version, references, and step ordering). +Checks that every destination in destinations.json has a matching entry in scores.json, every score is an integer in 0-100, and every label is one of clean, suspicious, or malicious, and every destination declares the supported `fixture_status` (`"synthetic-only"`). It also validates every scenario bundle under scenarios/ (schema version, references, and step ordering). Other commands: diff --git a/destinations.json b/destinations.json index 407e07a..c8f9180 100644 --- a/destinations.json +++ b/destinations.json @@ -7,7 +7,8 @@ "address": "GCRRYBV5IY7DSI54DKW33ZELC2LWYCAHC43TXAM2A2HTFN5GWOFWXPC2", "label": "clean", "risk_pattern": "none", - "notes": "Established testnet wallet with regular payment history; no red flags." + "notes": "Established testnet wallet with regular payment history; no red flags.", + "fixture_status": "synthetic-only" }, { "id": "GA4HFFSHZ7PADQWOFCZGYV2HE437LQ2WDILWCGC33BMJUQ6OBO5HKI5D", @@ -15,7 +16,8 @@ "address": "GA4HFFSHZ7PADQWOFCZGYV2HE437LQ2WDILWCGC33BMJUQ6OBO5HKI5D", "label": "clean", "risk_pattern": "none", - "notes": "Long-lived wallet, low transaction velocity, no shared funding source with flagged accounts." + "notes": "Long-lived wallet, low transaction velocity, no shared funding source with flagged accounts.", + "fixture_status": "synthetic-only" }, { "id": "GCER76PMMAKI2S6UUCPOFXC4SMY3IEILVE5MKSX4CTNDXXIZMFDXJJVS", @@ -23,7 +25,8 @@ "address": "GCER76PMMAKI2S6UUCPOFXC4SMY3IEILVE5MKSX4CTNDXXIZMFDXJJVS", "label": "clean", "risk_pattern": "none", - "notes": "Holds a USDC trustline against usdc_issuer below; normal trading activity." + "notes": "Holds a USDC trustline against usdc_issuer below; normal trading activity.", + "fixture_status": "synthetic-only" }, { "id": "GBWPMNXNRLA3VMBTWZHWN7VLUZPXBZUEOMPD6OIQKYCZNDJ2SY4ACC5B", @@ -31,7 +34,8 @@ "address": "GBWPMNXNRLA3VMBTWZHWN7VLUZPXBZUEOMPD6OIQKYCZNDJ2SY4ACC5B", "label": "clean", "risk_pattern": "none", - "notes": "Fixture stand-in for a well-known, long-lived asset issuer (issues USDC below)." + "notes": "Fixture stand-in for a well-known, long-lived asset issuer (issues USDC below).", + "fixture_status": "synthetic-only" }, { "id": "GCRNKXJJLZNDLK2EWPX25JISTORCXCF2HYUXMYKF7XWKHMEOHCXVGP4J", @@ -39,7 +43,8 @@ "address": "GCRNKXJJLZNDLK2EWPX25JISTORCXCF2HYUXMYKF7XWKHMEOHCXVGP4J", "label": "suspicious", "risk_pattern": "pass-through", - "notes": "Funded seconds before a large outgoing payment burst; pattern consistent with a pass-through wallet." + "notes": "Funded seconds before a large outgoing payment burst; pattern consistent with a pass-through wallet.", + "fixture_status": "synthetic-only" }, { "id": "GDL4JRQOHLJARTSQMZCBANXJ6GI6BN2QG2TX6ZPC5KSHKJCDEM735BYJ", @@ -47,7 +52,8 @@ "address": "GDL4JRQOHLJARTSQMZCBANXJ6GI6BN2QG2TX6ZPC5KSHKJCDEM735BYJ", "label": "suspicious", "risk_pattern": "pass-through", - "notes": "Shares a funding source with malicious_wallet_1 and malicious_wallet_2 below." + "notes": "Shares a funding source with malicious_wallet_1 and malicious_wallet_2 below.", + "fixture_status": "synthetic-only" }, { "id": "GCHYSQ57SVW6LFLGLQ4P77ZDQJ7BPQIM3QOCPIBIZKXGZGAQMJQZRFMS", @@ -55,7 +61,8 @@ "address": "GCHYSQ57SVW6LFLGLQ4P77ZDQJ7BPQIM3QOCPIBIZKXGZGAQMJQZRFMS", "label": "suspicious", "risk_pattern": "scam-trustline", - "notes": "Holds a trustline to the SCAM asset listed below; no other activity." + "notes": "Holds a trustline to the SCAM asset listed below; no other activity.", + "fixture_status": "synthetic-only" }, { "id": "GD7XPB2A7CG5Z4ICV24B3LXCRHAEJRFEK4OEW3ZIOQAPJOHAXBB7QHGE", @@ -63,7 +70,8 @@ "address": "GD7XPB2A7CG5Z4ICV24B3LXCRHAEJRFEK4OEW3ZIOQAPJOHAXBB7QHGE", "label": "malicious", "risk_pattern": "sweep", - "notes": "Path-payment sweep pattern: drains newly funded wallets within seconds of receiving deposits." + "notes": "Path-payment sweep pattern: drains newly funded wallets within seconds of receiving deposits.", + "fixture_status": "synthetic-only" }, { "id": "GCAMGLMB5EN55ICM26RYDGZSE5P4GKMK6TD6ZB7LACMVE7IEDTWVYZDF", @@ -71,7 +79,8 @@ "address": "GCAMGLMB5EN55ICM26RYDGZSE5P4GKMK6TD6ZB7LACMVE7IEDTWVYZDF", "label": "malicious", "risk_pattern": "phishing-drainer", - "notes": "Repeated small 'test' payments precede a large drain; matches a known phishing-drainer shape." + "notes": "Repeated small 'test' payments precede a large drain; matches a known phishing-drainer shape.", + "fixture_status": "synthetic-only" }, { "id": "GDQPZVGOJY6Q4PPASHZBIFN3PTIBD6WCRDCCAIFIPHZIYKSDQ7PZWPNJ", @@ -79,7 +88,8 @@ "address": "GDQPZVGOJY6Q4PPASHZBIFN3PTIBD6WCRDCCAIFIPHZIYKSDQ7PZWPNJ", "label": "malicious", "risk_pattern": "rug-pull", - "notes": "Destination of multiple change-trust + immediate max-sell patterns typical of rug-pull collection wallets." + "notes": "Destination of multiple change-trust + immediate max-sell patterns typical of rug-pull collection wallets.", + "fixture_status": "synthetic-only" }, { "id": "GAJLLIIPHII6OCG4KQJIGPCHVN6DNCRBXHX6DEUTPE7MQ6OONAYBRLET", @@ -87,7 +97,8 @@ "address": "GAJLLIIPHII6OCG4KQJIGPCHVN6DNCRBXHX6DEUTPE7MQ6OONAYBRLET", "label": "malicious", "risk_pattern": "rug-pull", - "notes": "Issuer of the SCAM asset below; no distributed supply outside the issuer's own control." + "notes": "Issuer of the SCAM asset below; no distributed supply outside the issuer's own control.", + "fixture_status": "synthetic-only" }, { "id": "SCAM:GAJLLIIPHII6OCG4KQJIGPCHVN6DNCRBXHX6DEUTPE7MQ6OONAYBRLET", @@ -96,7 +107,8 @@ "asset_issuer": "GAJLLIIPHII6OCG4KQJIGPCHVN6DNCRBXHX6DEUTPE7MQ6OONAYBRLET", "label": "malicious", "risk_pattern": "rug-pull", - "notes": "Newly issued token with no liquidity outside the issuer; classic rug-pull setup." + "notes": "Newly issued token with no liquidity outside the issuer; classic rug-pull setup.", + "fixture_status": "synthetic-only" } ] } diff --git a/scripts/validate-fixtures.mjs b/scripts/validate-fixtures.mjs index 045a8bb..90ea1ab 100644 --- a/scripts/validate-fixtures.mjs +++ b/scripts/validate-fixtures.mjs @@ -8,9 +8,20 @@ const scores = JSON.parse(readFileSync(root + '/scores.json', 'utf-8')); const VALID_LABEL_SET = new Set(VALID_LABELS); const VALID_RISK_PATTERN_SET = new Set(VALID_RISK_PATTERNS); +const VALID_FIXTURE_STATUSES = new Set(['synthetic-only']); const errors = []; +export function validateFixtureStatus(destination) { + if (!Object.hasOwn(destination, 'fixture_status')) return 'missing fixture_status'; + if (!VALID_FIXTURE_STATUSES.has(destination.fixture_status)) { + return `invalid fixture_status \"${destination.fixture_status}\"`; + } + return null; +} + for (const d of destinations) { + const fixtureStatusError = validateFixtureStatus(d); + if (fixtureStatusError) errors.push(d.id + ': ' + fixtureStatusError); if (!VALID_LABEL_SET.has(d.label)) { errors.push(d.id + ': invalid label "' + d.label + '"'); } diff --git a/tests/backward-compat.test.mjs b/tests/backward-compat.test.mjs index 6b313c5..9c431d5 100644 --- a/tests/backward-compat.test.mjs +++ b/tests/backward-compat.test.mjs @@ -62,3 +62,20 @@ test('backward compat: the scenario example only references existing fixtures', assert.ok(transactions.has('payment')); assert.ok(transactions.has('path_payment')); }); + +import { validateFixtureStatus } from '../scripts/validate-fixtures.mjs'; + +test('fixture status validation: accepts the supported synthetic status', () => { + assert.equal(validateFixtureStatus({ fixture_status: 'synthetic-only' }), null); +}); + +test('fixture status validation: rejects a missing status', () => { + assert.equal(validateFixtureStatus({}), 'missing fixture_status'); +}); + +test('fixture status validation: rejects an unknown status', () => { + assert.equal( + validateFixtureStatus({ fixture_status: 'network-visible' }), + 'invalid fixture_status "network-visible"' + ); +});