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.
// 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.
| Concern | V1 shape | V2 response |
|---|---|---|
| Math | On-chain f32 values and ad hoc averages | Checked 0–10,000 fixed-point integers |
| Policy | Mutable vectors inside one organization | Sequential, versioned, bounded rubrics |
| History | Mutable accumulated Score state | Monotonic assessment accounts plus a current cache |
| Consent | Unchecked applicant registration | Co-signed enrollment and rubric adoption |
| Surface | NFT metadata and third-party CPIs | No 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.
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.
ORGANIZATION + SEQUENTIAL VERSION
ORGANIZATION + SUBJECT
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 importOrganization 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.
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), ...);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.
// 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
}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, ...);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.
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...
});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.
/// 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 story | What it demonstrates | Boundary |
|---|---|---|
| Team health | Delivery, quality, collaboration, and sustainability measured under one versioned rubric. | An issuer-attested periodic scorecard—not an anonymous employee-review network. |
| Project maturity | A project key carries comparable readiness or operating pillars across periods. | Comparison is meaningful only inside the same issuer, rubric version, and period policy. |
| Contributor journey | A 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.
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.
# 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:stopStartup 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.
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.
HOSTED / OWNER-ONLY
LOCALHOST / REAL WRITES
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.
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.
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.
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.
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.
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.
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.
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.
| Evidence surface | Result | What was checked |
|---|---|---|
| Rust scoring and serialization | 10 / 10 | Fresh pass at the pinned commit |
| Isolated validator + migration planner | 12 / 12 | Current verification session on the exact-health-verified isolated stack |
| Loopback bridge unit/boundary | 4 / 4 | Artifact and maximum-packet guards |
| Website fixture + simulator boundaries | 15 / 15 | One fixture test and fourteen playground tests |
| Browser → bridge → validator proof | 1 / 1 | Same verified stack; confirmed signatures, account owners, exact commitment bytes |
| Format, lint, types, Clippy, SBF, prerender | Passed | Six application routes in seven prerender results |
$ 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 passedProgram identity (test-only)
GrWthV2cQ4GVjzQF8X2VXcZ6HC8ZwHfEEw5fm7cMx99
growth_v2.so
size 511,336 bytes
sha256 668da8ea743ca339e83019433904513ecfce3d722f8dc9c63d7a8aee5140639bgrowth_v2.json (canonical, newline-terminated IDL)
sha256 9e65824eb077b8e87921541426063baf0744f1eadc2de0245a024f6cbd414d0dThe 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.
| Gate | Current state | Required next evidence |
|---|---|---|
| Release identity | The 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 artifacts | One 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 coverage | Parser/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 containment | Legacy 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 reconciliation | The website intentionally has no production chain indexer. | Build finalized account reconciliation; treat events as hints, not the source of truth. |
| Privacy and evidence truth | Wallets, 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.
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 orchestratorLEGACY 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.
REVIEW
Internal security review
Threat model, present controls, release blockers, and residual risks.
LOCAL PROOF
Validator lab runbook
Exact local identity, artifact gates, loopback bridge, limits, and teardown.
17 / WHAT IS NEXT
The next milestone is release evidence, not more polish on synthetic data.
- Close the legacy incident boundary.Record provider revocation, signer and authority disposition, custody, and Git-history decisions.
- Create a governed release identity.Generate it outside Git, synchronize every declared ID, and define program and issuer multisig policy.
- Produce independent reproducible artifacts.Match binary, IDL, and client hashes from two clean operators, then compare deployed bytes.
- Finish adversarial validator coverage.Exercise malformed migration accounts, lifecycle boundaries, terminal states, events, rollback, and size limits.
- Specify privacy and evidence policy.Define issuer accountability, cohort thresholds, consent copy, canonical salted commitments, and retention.
- 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.
PRIMARY
Pinned Growth repository
Source, tests, sanitized snapshots, security review, website, and local-validator lab.
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.