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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---
Expand All @@ -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

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:

Expand Down
36 changes: 24 additions & 12 deletions destinations.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,87 +7,98 @@
"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",
"type": "account",
"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",
"type": "account",
"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",
"type": "account",
"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",
"type": "account",
"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",
"type": "account",
"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",
"type": "account",
"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",
"type": "account",
"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",
"type": "account",
"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",
"type": "account",
"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",
"type": "account",
"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",
Expand All @@ -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"
}
]
}
11 changes: 11 additions & 0 deletions scripts/validate-fixtures.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 + '"');
}
Expand Down
17 changes: 17 additions & 0 deletions tests/backward-compat.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"'
);
});
Loading