DAY 006 / GROWTH PROGRAM

Growth Program v2: replacing a live score contract without mutating it

How I turned a live-but-unsafe Solana growth contract into a hardened v2, an evidence-led website, and safety-gated browser-local and local-validator demos without mutating legacy accounts.

01 / THE PRODUCT PRIMITIVE

A growth score is useful only when its rubric, issuer, period, and limits travel with it.

Growth Program V2 is an undeployed, issuer-attested, multi-pillar scorecard and credential primitive designed for Solana. An organization defines a versioned rubric, a subject consents to participate, and each period becomes an inspectable assessment instead of an endlessly overwritten number.

The product idea can serve teams, projects, or contributor programs: turn several dimensions of progress into a comparable vector, retain the measurement policy that made it meaningful, and give the subject a dispute, correction, and exit path. It is not a token economy, a decentralized review network, or a universal reputation score.

This build began with a repository containing live legacy deployments, not a blank page. That changed the order of operations: understand what already existed, minimize the evidence retained, contain exposed development credentials without mutating live state, and build V2 as a separate replacement.

02 / WHAT WAS INHERITED

V1 was already live—and its data model carried prototype assumptions into public state.

The original Anchor program declared the same address on mainnet and devnet. A sanitized repository snapshot dated 30 August 2026 records different binaries on the two networks, both upgradeable through the same single-key authority. At that point, discriminator counting found 115 program-owned mainnet accounts—2 Org and 111 Score matches—and 94 devnet accounts—29 Org and 55 Score matches.

Those counts are a privacy-minimized historical observation, not a fresh RPC feed for this page. Names, subject keys, scores, reviews, and raw account payloads were deliberately excluded. The public note links the preserved hashes and methodology rather than republishing identity-linked records.

V1 account shape, selected fieldsRust · source
// Growth v1: selected fields from the deployed account format.
pub struct Org {
    pub name: String,
    pub min_reviews: u8,
    pub weights: Vec<f32>,
    pub ranges: Vec<u8>,
    pub levels: Vec<Vec<f32>>,
    pub mint: Pubkey,
    pub authority: Pubkey,
    // ...
}

pub struct Score {
    pub name: String,
    pub scores: Vec<f32>,
    pub scores_sum: Vec<f32>,
    pub applicant: Pubkey,
    pub mint: Pubkey,
    pub reviews_recieved: Vec<u16>,
    pub reviews_sent: u16,
    pub levels: Vec<u8>,
    // ...
}

V1 used floating-point weights and scores, accumulated mutable sums, advanced one level at a time, stored a name on the Score account, and attached NFT/Metaplex behavior. It had no durable reviewer identity, no per-period assessment record, no explicit subject consent lifecycle, and no way for the program to prove that an aggregate reflected honest or unique evidence.

ConcernV1 shapeV2 response
MathOn-chain f32 values and ad hoc averagesChecked 0–10,000 fixed-point integers
PolicyMutable vectors inside one organizationSequential, versioned, bounded rubrics
HistoryMutable accumulated Score stateMonotonic assessment accounts plus a current cache
ConsentUnchecked applicant registrationCo-signed enrollment and rubric adoption
SurfaceNFT metadata and third-party CPIsNo token, NFT, Metaplex, or external-CPI dependency

03 / CONTAINMENT FIRST

Removing a secret from HEAD does not revoke it from the world.

The repository had tracked development signer files and a provider credential. The current tree now generates ephemeral test keypairs, removes the fixed endpoint, ignores common secret paths, and preserves only sanitized binary, IDL, loader, authority, count, and commitment evidence. No transaction, upgrade, authority transfer, account mutation, or provider-side change was attempted during this work.

Contain before rebuildingThe safe sequence was observation, privacy-minimized preservation, containment, and only then a fresh replacement. Deleting secrets from the current tree did not complete the external incident response.

This is why V2 is a new program design rather than an in-place upgrade of either V1 deployment. Legacy state remains an evidence and retirement problem; new code does not retroactively make it trustworthy.

04 / THE V2 CONTRACT

Separate the issuer, policy, subject lifecycle, history, and provenance.

V2 replaces one mutable score object with five explicit account roles. Organization namespaces the policy and subject paths and stores current authority. Rubric freezes a version of the measurement policy. ScoreProfile stores the subject relationship and current cache. Assessment preserves one period. LegacyMigrationReceipt marks a canonical V1 relationship once, globally.

The v2 account graphEvery relationship is explicit and indexable. Rubric policy and assessment result payloads are immutable; assessment lifecycle status and the profile's current cache can change.
Canonical V2 relationshipstext · source
Organization           creator + org_seed
├── Rubric             organization + sequential version
└── ScoreProfile       organization + subject
    └── Assessment     profile + monotonic sequence

LegacyMigrationReceipt
                        canonical legacy Score account; globally one-use

Organization: namespace, authority, status, active rubric
Rubric:       immutable scoring policy after activation
ScoreProfile: current cache and lifecycle pointer
Assessment:   append-style period result and commitments
Receipt:      one-time provenance marker; no score import

Organization PDAs are namespaced by immutable creator plus a 32-byte organization seed, so authority rotation cannot move the namespace. Rubrics are sequential and immutable after creation except for one-time activation, then immutable thereafter. Assessments use the profile and monotonic sequence in their address; a correction therefore cannot occupy or rewrite the original result payload, although it marks the old lifecycle status as corrected.

05 / SCORES AND RUBRICS

Integer arithmetic makes the policy bounded, deterministic, and testable.

Every score, weight, coverage value, and threshold uses basis points from 0 to 10,000. A rubric can contain at most 16 unique pillars and eight strictly increasing thresholds per pillar. Weights must be positive and total exactly 10,000; samples, freshness, and coverage are bounded before a result is accepted.

Rubric validation, condensedRust · source
pub const SCORE_SCALE: u16 = 10_000;
pub const WEIGHT_SCALE: u16 = 10_000;
pub const MAX_PILLARS: usize = 16;
pub const MAX_LEVEL_THRESHOLDS: usize = 8;

require!(!pillars.is_empty() && pillars.len() <= MAX_PILLARS, ...);

for pillar in pillars {
    require!(pillar.weight_bps > 0, ...);
    require!(pillar.min_samples > 0, ...);
    require!(pillar.min_coverage_bps <= SCORE_SCALE, ...);
    // IDs are unique and thresholds are strictly increasing.
    weight_sum = weight_sum.checked_add(u32::from(pillar.weight_bps))?;
}

require!(weight_sum == u32::from(WEIGHT_SCALE), ...);
Checked weighted scoring, condensedRust · source
score_numerator += u64::from(input.score_bps)
    .checked_mul(u64::from(pillar.weight_bps))?;

coverage_numerator += u64::from(input.coverage_bps)
    .checked_mul(u64::from(pillar.weight_bps))?;

// Round half-up exactly once after all weighted terms are added.
let weighted_score = score_numerator
    .checked_add(u64::from(WEIGHT_SCALE) / 2)?
    / u64::from(WEIGHT_SCALE);

let level = pillar.level_thresholds
    .iter()
    .take_while(|threshold| input.score_bps >= **threshold)
    .count();

The final weighted score and coverage round half-up once after all products are added. Each pillar level is simply the number of thresholds met. The website's “balanced level”—the weakest pillar—is a fixture presentation convention, not a field or rule in the on-chain program.

06 / CONSENT AND CORRECTIONS

Consent is placed at relationship changes—not falsely attached to every score.

Issuer and subject co-sign enrollment. If the organization activates a new rubric, both sign adoption and the profile's rubric-relative score cache is cleared. Ordinary periodic assessment is then issuer-signed only. Saying that every assessment is subject-approved would overstate the contract.

Signer boundaries, selected instruction contextsRust · source
// Enrollment and rubric adoption require both signers.
pub struct EnrollSubject<'info> {
    #[account(mut)] pub authority: Signer<'info>,
    pub subject: Signer<'info>,
    // ...
}

pub struct AdoptRubric<'info> {
    pub authority: Signer<'info>,
    pub subject: Signer<'info>,
    // ...
}

// Routine periodic assessment is issuer-signed—not co-signed.
pub struct SubmitAssessment<'info> {
    #[account(mut)] pub authority: Signer<'info>,
    // no subject signer
}
Monotonic period, freshness, and expiry guards, condensedRust · source
let expected_sequence = score_profile.latest_sequence.checked_add(1)?;
require!(args.sequence == expected_sequence, InvalidAssessmentSequence);

let expected_period_id = score_profile.latest_period_id.checked_add(1)?;
require!(args.period_id == expected_period_id, InvalidPeriodId);

require!(
    args.period_end > args.period_start
        && args.period_start >= score_profile.last_period_end
        && args.period_end <= now + MAX_FUTURE_CLOCK_SKEW_SECONDS,
    InvalidPeriod
);

require!(now - args.period_end <= rubric.max_submission_delay_seconds, ...);
require!(args.period_end + rubric.assessment_ttl_seconds > now, ...);
Consent, history, and correctionThe issuer can attest, but it cannot silently enroll a subject or replace an old result payload. Dispute and correction update the old record's lifecycle status; the corrected result is a new account. Revocation and retirement are terminal.

A subject may dispute only the latest final assessment. The profile then blocks later periods until the issue is resolved. Correction requires both issuer and subject, keeps the original period and rubric, marks the target corrected, and writes a new sequence that names what it supersedes. Subject revocation and organization retirement stop future issuance permanently; neither deletes history.

A correction is a new co-signed record, condensedRust · source
pub struct SubmitCorrection<'info> {
    #[account(mut)] pub authority: Signer<'info>,
    pub subject: Signer<'info>,
    // target_assessment and new correction are distinct accounts
}

target.status = AssessmentStatus::Corrected;

correction.set_inner(Assessment {
    status: AssessmentStatus::Final,
    sequence: args.sequence,
    period_id: target.period_id,
    rubric_version: target.rubric_version,
    supersedes: target.key(),
    correction_reason_commitment: args.reason_commitment,
    // ...new aggregates and evidence commitment...
});

07 / AUTHORITY MODEL

The program proves which issuer key attested; it cannot make the attestation true.

An active organization authority creates and activates rubrics, submits assessments, administers lifecycle state, and proposes a replacement authority. The recipient key must explicitly accept before rotation completes. Pause blocks issuance and allows controlled migration work; retirement is terminal.

Two-step rotation prevents accidental transfer to a key that cannot sign, but it is not a timelock. A compromised current authority can propose and accept an attacker-controlled key immediately. Production still needs hardware-backed multisig custody, role policy, incident handling, and independent post-change verification.

08 / LEGACY MIGRATION

A receipt proves continuity, not historical truth.

V2 can create one globally unique receipt for a canonical V1 Score account. The instruction verifies the legacy program owner, Org and Score discriminators, canonical PDAs, stored legacy authority, applicant/subject relationship, current V2 organization, active subject, signatures, and nonzero snapshot commitment.

The parser skips V1 scores by designRust · source
/// Validates only the stable v1 prefix needed for migration provenance.
/// No legacy floating-point score is interpreted or imported.
pub fn validate_legacy_score_data(
    data: &[u8],
    expected_subject: &Pubkey,
) -> Result<()> {
    let expected = &hash(b"account:Score").to_bytes()[..8];
    require!(&data[..8] == expected, ...);
    skip_legacy_dynamic_field(data, &mut cursor, 1)?; // name
    skip_legacy_dynamic_field(data, &mut cursor, 4)?; // Vec<f32> scores
    skip_legacy_dynamic_field(data, &mut cursor, 4)?; // Vec<f32> sums
    require!(&data[cursor..cursor + 32] == expected_subject.as_ref(), ...);
    Ok(())
}

The parser reads only the stable prefix needed to prove provenance. It explicitly skips V1's floating-point scores and sums; none becomes a V2 score. A receipt says the consenting subject and issuer continuity were checked against one legacy account and one snapshot commitment. It does not certify that the old reviews were accurate.

Commitments also need precision. On-chain, V2 rejects only an all-zero 32-byte value. A useful confidential commitment needs a canonical per-kind domain, serialization, private random salt, storage policy, and verification procedure. Hashing a predictable name or score without a salt is dictionary-guessable.

09 / THE EVIDENCE WEBSITE

Three product stories make the contract legible without pretending to be live data.

The companion website frames Team Health, Project Maturity, and Contributor Journey as synthetic applications of the same contract. Its explorer is fixture-only. It does not ingest either V1 deployment, expose legacy identities, connect a wallet, or present a V2 account as deployed.

Fixture storyWhat it demonstratesBoundary
Team healthDelivery, quality, collaboration, and sustainability measured under one versioned rubric.An issuer-attested periodic scorecard—not an anonymous employee-review network.
Project maturityA project key carries comparable readiness or operating pillars across periods.Comparison is meaningful only inside the same issuer, rubric version, and period policy.
Contributor journeyA consenting subject can inspect history, challenge a latest result, and leave future issuance.Revocation cannot erase a public chain history or make a wallet identity private.

The UI can rank fixtures only when issuer, rubric, rubric version, and measurement period are genuinely comparable. Ranking is a website computation, not an instruction or protocol guarantee. The hosted build is currently owner-only; an anonymous request returns 401, so it is recorded as a build artifact rather than offered as a public demo.

10 / BROWSER-LOCAL PLAYGROUND

The first demo models every state transition without creating a network path.

The normal /playground route is an in-memory simulator. It derives synthetic addresses, models required signers, computes the same fixed-point result, records synthetic receipts and events, and lets the visitor inspect before/after state. It has no RPC, wallet, signing, transaction sending, automatic persistence, analytics, remote storage, or import path.

Hosted and localhost network policies, condensedTypeScript · source
const isLocalValidatorLab =
  isLoopback && pathname.startsWith('/playground/localnet');

const policy = isLocalValidatorLab
  ? "connect-src 'self' http://127.0.0.1:18999; ..."
  : pathname.startsWith('/playground')
    ? "connect-src 'none'; ..."
    : "connect-src 'self'; ...";

// The hosted simulator cannot gain a quiet RPC fallback.
response.headers.set('Content-Security-Policy', policy);

The only export is an explicit, warned JSON download. The hosted route receives aconnect-src 'none' Content Security Policy, so a future frontend mistake cannot quietly reach devnet, mainnet, or a remote API from that simulator. A devnet option exists only as a locked, unsigned plan.

11 / LOCAL-VALIDATOR LAB

The second demo crosses the real program boundary—and nowhere beyond localhost.

The /playground/localnet route connects to a loopback bridge at127.0.0.1:18999. That bridge owns disposable in-memory issuer and subject signers and submits real instructions to a resettable validator at127.0.0.1:18899. Hosted use remains locked; there is no remote-RPC override or fallback.

Run the complete local proofshell · source
# From the repository root: start the complete local lab.
npm run demo:localnet

# Or run the browser-to-validator proof and clean up afterward.
npm run test:localnet

# Lower-level v2 controls, from v2/:
yarn local:start      # terminal 1: isolated validator
yarn local:bridge     # terminal 2: loopback transaction bridge
yarn local:status
yarn local:stop

Startup builds the current SBF program, checks the generated IDL byte-for-byte against the committed canonical copy and hash, resets a dedicated ledger, genesis-loads the exact binary through Solana's upgradeable loader, then exports a manifest. Health checks and every write re-verify loader state, ProgramData pointer and authority, deployed bytes, program hash, IDL, and local run identity.

Artifact identity returned by the bridge, condensedJavaScript · source
const identity = await verifyLocalIdentity();

return {
  artifactIdlSha256: identity.idlSha256,
  artifactProgramSha256: identity.programSha256,
  identityVerified: true,
  rpcUrl: "http://127.0.0.1:18899",
};

// Every write re-checks loader ownership, ProgramData pointer,
// upgrade authority, deployed bytes, committed IDL, and checksums.

The declared program address is intentionally test-only. No matching deployment key is assumed to exist, so genesis loading demonstrates real local execution—not the normalsolana program deploy path. The deterministic payer is public and unsafe anywhere outside this disposable ledger.

12 / DEMO BOUNDARIES

The demo fails closed at network, identity, and packet-size boundaries.

Two demos, two different guaranteesThe hosted surface is intentionally unable to touch a chain. Real instructions exist only in the separate loopback lab, where disposable signers and an isolated validator are started and stopped together.

Organization creation, rubric creation, and activation are one atomic transaction in the browser lab. The bridge builds, signs, serializes, size-checks, and simulates it before sending. That creates a tighter demo limit than the program itself.

Why the browser bridge caps a rubric at six pillarstext · source
create organization
+ create rubric
+ activate rubric
= one signed, simulated, atomic transaction

6 maximum-length pillars → 1,167 serialized bytes → allowed
7 maximum-length pillars → 1,263 serialized bytes → rejected before send
Solana packet limit          → 1,232 bytes

The on-chain contract supports 16 pillars.
The bridge's tighter six-pillar ceiling belongs only to this atomic demo.

The on-chain model still accepts up to 16 pillars; the six-pillar bridge cap only protects the single atomic setup packet. Solana test-validator's auxiliary faucet may bind0.0.0.0:19001 even while RPC is loopback-only, so the runbook also calls for a host firewall on untrusted networks.

13 / WHAT BROKE WHILE BUILDING

The important failures turned into explicit gates.

01

Legacy credentials were source problems and operator problems

Current-tree deletion and ephemeral fixtures reduced future exposure, but they could not revoke a provider key, recover live authority, erase forks, or approve chain writes. Those steps remain in a separate containment checklist.

02

The final program identity was not a deployable identity

The placeholder address is suitable for deterministic genesis loading but has no matching release key. Instead of manufacturing credentials in Git, the release path now stops until an approved key ceremony and governance plan exist.

03

A verifiable build attempt did not produce evidence

A normal pinned SBF build passed. A bounded container-based attempt was stopped while pulling its image and produced no reproducible artifact, so the note reports one local hash rather than calling the binary reproducible.

04

Seven pillars crossed the transaction packet limit

The program's 16-pillar limit was valid at the account layer, but atomic setup with seven maximum-shaped pillars serialized to 1,263 bytes. The bridge now rejects above six before any partial organization can be created.

05

A running local lab blocked the one-shot runner

Startup correctly refused to reuse occupied ports. The existing stack's health endpoint first proved the exact current binary, IDL, and run identity; the validator suite and browser proof then ran manually against that verified isolated stack.

06

Status documentation carried an older IDL hash

Two review documents still list the pre-local-lab 0c25… digest. The canonical newline-terminated IDL at the pinned commit hashes to 9e658…; the build note uses that current artifact and leaves the discrepancy visible.

14 / VERIFIED RESULT

The pinned commit runs from scoring logic through a real local browser transaction.

10Rust tests
12validator + planner cases
15website boundary tests
1end-to-end local proof
Evidence surfaceResultWhat was checked
Rust scoring and serialization10 / 10Fresh pass at the pinned commit
Isolated validator + migration planner12 / 12Current verification session on the exact-health-verified isolated stack
Loopback bridge unit/boundary4 / 4Artifact and maximum-packet guards
Website fixture + simulator boundaries15 / 15One fixture test and fourteen playground tests
Browser → bridge → validator proof1 / 1Same verified stack; confirmed signatures, account owners, exact commitment bytes
Format, lint, types, Clippy, SBF, prerenderPassedSix application routes in seven prerender results
Repository verification commandsshell · source
$ cd v2

$ cargo +1.92.0 test --lib
# 10 passed

$ anchor build && yarn idl:check && yarn typecheck
# SBF build, canonical IDL, and TypeScript checks passed

$ yarn test:local
# Anchor's tests/**/*.ts glob runs 8 lifecycle + 4 planner cases: 12 passed

$ cd ..

$ npm --prefix website run test:fixtures
# 1 passed

$ npm --prefix website run test:playground
# 14 simulator and boundary cases passed

$ npm run test:localnet
# 1 browser-to-validator proof passed
Current local-validation program artifacttext · source
Program identity (test-only)
  GrWthV2cQ4GVjzQF8X2VXcZ6HC8ZwHfEEw5fm7cMx99

growth_v2.so
  size    511,336 bytes
  sha256  668da8ea743ca339e83019433904513ecfce3d722f8dc9c63d7a8aee5140639b
Current canonical IDL artifacttext · source
growth_v2.json (canonical, newline-terminated IDL)
  sha256  9e65824eb077b8e87921541426063baf0744f1eadc2de0245a024f6cbd414d0d

The browser proof created a maximum-six-pillar organization and rubric, enrolled a subject, submitted an assessment, and verified confirmed signatures, four program-owned accounts, and exact evidence-commitment bytes. The development-session walkthrough separately recorded a four-pillar fixture—94%, 87%, 91%, and 82%—which computed to 88.50% weighted score and 91.50% coverage. Those particular values are session evidence, not a committed benchmark or public-chain result.

15 / CURRENT TRUTH

V2 is implemented and locally validated. It is not deployed or release-ready.

GateCurrent stateRequired next evidence
Release identityThe declared address is a test-only placeholder with no matching deploy key.Create a governed program identity outside Git, synchronize IDs, and review again.
Reproducible artifactsOne normal SBF build passed; a two-operator verifiable build is not established.Produce matching clean builds and compare deployed bytes with the approved hash.
Migration adversarial coverageParser/planner checks exist; malicious legacy accounts are not fully exercised on-validator.Test wrong owners, discriminators, PDAs, signers, duplicate receipts, and failure invariants.
Governance and containmentLegacy provider revocation, authority custody, funds, and Git-history decisions remain operator work.Complete the incident checklist and put program/issuer control behind approved custody.
Indexer and reconciliationThe website intentionally has no production chain indexer.Build finalized account reconciliation; treat events as hints, not the source of truth.
Privacy and evidence truthWallets, aggregates, counts, dates, disputes, and history would be public; issuer data can be false.Define privacy policy, cohort safeguards, canonical salted commitments, and evidence verification.

V2 has not received a formal external smart-contract audit, privacy/legal review, or independent reproducible-build attestation. There is no final program identity, governed upgrade authority, production indexer, reconciler, or public live V2 explorer. Mainnet is outside this prototype milestone, and V1 must not be overwritten by V2.

Public-chain use would make subject wallet keys, profile relationships, timestamps, per-pillar aggregates, sample counts, coverage, disputes, revocations, and history permanently linkable. Revocation stops future issuance; it does not erase past state. Commitments reduce raw-data exposure only when producers use a strong, salted canonical scheme.

16 / FILE GUIDE

The repository keeps archaeology, replacement code, demos, and release policy apart.

Repository maptext · source
growth-program/
├── programs/growth/              # preserved v1 source
├── snapshots/2026-08-30/         # sanitized live-state evidence
├── docs/
│   ├── USE_CASES.md
│   └── CONTAINMENT_STATUS.md
├── security/V2_SECURITY_REVIEW.md
├── v2/
│   ├── programs/growth_v2/src/   # replacement Anchor program
│   ├── idl/                      # canonical public interface + hash
│   ├── tests/                    # lifecycle and migration planner
│   ├── scripts/                  # build, identity, bridge, local ledger
│   └── docs/                     # security, migration, release runbooks
├── website/                      # fixtures + two separated playgrounds
└── scripts/localnet-demo.sh      # complete browser lab orchestrator

LEGACY EVIDENCE

Sanitized snapshot

Network binaries, IDLs, hashes, loader facts, safe counts, and omissions.

CONTRACT

Growth V2

Account model, validation commands, local identity, and safety boundary.

17 / WHAT IS NEXT

The next milestone is release evidence, not more polish on synthetic data.

  1. Close the legacy incident boundary.Record provider revocation, signer and authority disposition, custody, and Git-history decisions.
  2. Create a governed release identity.Generate it outside Git, synchronize every declared ID, and define program and issuer multisig policy.
  3. Produce independent reproducible artifacts.Match binary, IDL, and client hashes from two clean operators, then compare deployed bytes.
  4. Finish adversarial validator coverage.Exercise malformed migration accounts, lifecycle boundaries, terminal states, events, rollback, and size limits.
  5. Specify privacy and evidence policy.Define issuer accountability, cohort thresholds, consent copy, canonical salted commitments, and retention.
  6. Build reconciliation before a live explorer.Index finalized canonical accounts, survive forks and missed logs, and expose provenance without overstating truth.

18 / EVIDENCE LEDGER

Repository state is primary; the development chat is supplementary.

All implementation claims and code excerpts above point to public commit d944ee75cbb06d6eabdbd7075a88a15bb15e5936. The shared development session explains the requested sequence and records the interactive walkthrough; where its result is not a committed fixture, this note labels it as session evidence.

SUPPLEMENTARY

Development session

Build intent, implementation chronology, corrections, and interactive walkthrough.

OWNER-ONLY ARTIFACT

Companion website

Currently requires owner authentication; explorer content is synthetic.

LEGACY NETWORKS

Mainnet · Devnet

Legacy V1 address only. This note relies on the dated sanitized snapshot, not a live data feed.

CONTINUE EXPLORING

Inspect the replacement—and the gates that keep it off public networks.

The pinned repository separates sanitized live-state evidence, an undeployed v2 program, a no-network browser playground, and a real loopback-only validator lab.