IDENTITY AND SIGNING POWER
One player may use two keys, but the working key remains an ordinary Solana signer
Start with one player and two account roles. The owner is the long-lived identity behind the PlayerProfile. The sessionAuthority is the key presented to a consumer for repeated work. In plugin-wallet mode these roles normally use distinct keys: the wallet keeps the durable owner key, while the browser stores a separate session-purpose Keypair. That does not create a second player, transfer the Backpack, or make the girl in the teaching scene an authority; both roles still refer back to the same owner.
The record address is P_session = PDA(PlayerProgram, ["session", owner, sessionAuthority]). The literal session seed and both public keys are ordered inputs under the Player Program ID. Reusing the same owner and sessionAuthority selects the same PDA, although matching an address alone does not guarantee that refresh will pass its stored relationship checks. Changing only the session authority selects a different PDA for the same player.
The important boundary is outside that PDA. A session Keypair is an ordinary Solana signer, not a NiceChunk-only key. It can create a fresh signature for an independent System Program transfer without referencing PlayerSession and without invoking the Player Program. Session expiry, allowedActions, maxActions, and actionCount are not a key sandbox and do not protect the SOL held at the session address; those fields matter only to code that deliberately reads them. The PlayerSession record expires, but the Keypair, address, private bytes, and ordinary signing ability do not.
Local Game Wallet changes the role arrangement. Its same long-lived key fills both roles: owner = sessionAuthority, so the same key acts in both account positions and derives PDA(PlayerProgram, ["session", owner, owner]). Successful setup can create or refresh that on-chain PDA without a separate session-purpose signing secret. The client flag usesGameplaySession: false only says there is no separate gameplay Keypair; by itself it does not prove that the PDA exists or that setup succeeded.
The public identity checked against PlayerProfile and stored at byte offset 12 in PlayerSession.
A plugin browser Keypair or, in Local Game Wallet mode, the same long-lived owner key.
Both public keys affect the address; private bytes are never PDA seeds.
The reviewed repository plugin flow uses a durable wallet identity plus a separate browser-held session signer.
The same key fills both roles and is long-lived rather than a separate session-purpose wallet.
The deterministic audit constructed and verified an ordinary transfer without outputting a private key, referencing PlayerSession, or contacting a network.
ONE APPROVAL TRANSACTION
A 15-byte request names the limits, and both signer roles must approve it
The Player Program dispatches instruction tag 4 to create_or_refresh_player_session. The reviewed repository and SDK builders serialize 15 bytes in total: one tag byte plus a 14-byte payload made from an eight-byte expiresAt, a two-byte allowedActions mask, and a four-byte maxActions. In the full data, tag starts at offset 0, expiresAt at offset 1, allowedActions at offset 9, and maxActions at offset 11. Six accounts accompany it: owner, sessionAuthority, PlayerProfile, PlayerSession, GlobalConfig, and System Program.
The owner must be a writable signer, and the session authority must be a signer. In the reviewed direct browser transaction, distinct keys normally produce two transaction signatures: one from the wallet owner and one from the browser Keypair. When the same public key occupies both roles, one transaction signature can satisfy both account-role checks. The contract itself observes AccountInfo.is_signer; a CPI PDA signer can have different mechanics, so the two-private-key explanation is specific to this direct client flow.
The program rejects expiresAt unless it is in the future, meaning expiresAt > chain Clock time. It also rejects a zero mask, any bit outside the two declared bits, and maxActions = 0. The reviewed repository and retained live chain asset use an eight-hour request, allowedActions mask 6 (0b0110), and maxActions 10,000. Those are client defaults: the contract does not impose an eight-hour maximum.
If the canonical PDA already belongs to Player Program, tag 4 can refresh it only after the existing owner, authority, playerProfile, and globalConfig all match the current inputs and the current player-v7 Profile precondition passes. Otherwise the owner funds allocation of a new canonical PDA. Selecting the same address is therefore not a guarantee of rewrite success. Repository Session builders request PlayerSession writable for setup; their later Session-aware consumers request it read-only.
The first byte selects create_or_refresh_player_session.
1 tag + 8 expiresAt + 2 allowedActions + 4 maxActions.
The handler receives only the final 14 bytes after dispatch removes the tag.
The owner also pays for setup fees, optional funding, profile creation, and Session allocation.
Possession of the public address alone cannot create or refresh its Session.
The plugin wallet and session-purpose Keypair approve the same setup transaction.
Both signer checks observe the same signed public key in Local Game Wallet mode.
The mask combines BREAK_BLOCK value 2 and PLACE_BLOCK value 4.
A production browser choice, not a maximum accepted duration in the Player Program.
184 PUBLIC BYTES, PRIVATE SIGNING MATERIAL
The chain publishes the pass record; the browser keeps the secret that can use it
PlayerSession is a fixed 184-byte public record beginning with NCKSES01. Anyone reading the account can see its public owner, session authority, linked profile and configuration, world, mask, timestamps, and stored counters. Public keys identify accounts; they are not private signing bytes. The record cannot reveal the secret half of the session Keypair.
Plugin-wallet mode stores that private half in localStorage under nicechunk.session.v1.<owner>. The JSON contains the public key, secretKey encoded as Base64, expiresAt, and savedAt. Base64 is not encryption or protection; it is a reversible text encoding. Browser code or an extension able to read this origin's storage may copy the secret. The public account and private secret therefore have different visibility and different risks.
The table below follows the Rust pack order and matching SDK decoder. Offsets count from the first byte, so owner starts at byte 12 after eight magic bytes, two version bytes, one bump byte, and one active byte. The Devnet capture calls byte 11 initialized; the SDK calls it active; every audited Session consumer requires that byte to equal 1 and the version to equal 1. Multi-byte integers use the implementation's little-endian readers and writers. Refresh changes mask, expiry, updatedSlot, and maxActions and resets actionCount, while createdSlot and createdAt remain creation metadata.
A read-only capture on 2026-07-18 retained 10 PlayerSession records for 8 unique owners at confirmed Devnet context slot 477236696. RPC getBlockTime for that context slot returned 2026-07-18T21:56:51Z, and all 10 records were expired relative to that retained block time. The capture did not directly read the Clock sysvar used by a consumer. Five records had owner = sessionAuthority and five used distinct authorities, while one owner had three Sessions; public equality does not prove which client created them.
The referenced Profiles were six current v7 records, three historical v2 records, and one historical v3 record. Current Chunk, Building, and Backpack Profile views require the final 773-byte NCKPLY01 layout, version 7, initialized state, and nine equipment slots before owner and configuration relationships can pass. The 473-byte v2 records fail length and the 773-byte v3 record fails version. Those retained references show that old Session accounts remain visible, not that a historical Profile can authorize a current action. Repository-default builders likewise derive the current player-v7 Profile.
The Session, Profile, and GlobalConfig program-account reads share context slot 477236696. Player Program and Player ProgramData used supplemental nearby context slots at +1 and +2, not the exact shared account context. Their hashes bind only the observed Player deployment bytes—not Chunk, Building, unified Game, Backpack, or Guardian—and do not prove this dirty source tree was reproducibly compiled to the deployment.
This is a single confirmed Devnet snapshot, not a Mainnet claim, finalized guarantee, permanent population total, or proof that anyone possessed a corresponding secret. It did not read session-authority balances or browser storage. The retained context-slot RPC block time supports the expiry classification, while live consumers independently read Clock.unix_timestamp when an instruction executes.
The account-family marker checked before the consumer reads public fields.
Packed as version 1 and required to equal 1 by every audited Session consumer.
The canonical PDA bump stored during setup; current consumers do not read it.
Creation writes 1, and every audited Session consumer rejects any other value.
The persistent player identity resolved from the Session.
The signer public key that a consumer compares with the presented authority.
The Profile account expected by the current action.
Checked by Chunk and Building; Backpack's Session view does not read it.
Stored from GlobalConfig at creation; current audited consumers do not read it.
A consumer checks whether its requested bit is present.
A consumer rejects the record when this is less than or equal to chain time.
The creation slot; refresh leaves it unchanged.
Creation copies createdSlot; refresh replaces it with the current slot.
The chain timestamp recorded when the account was first packed.
Must be nonzero at setup but is not read by current audited consumers.
Created and refreshed as zero, then left unchanged by current read-only consumers.
WHAT ACTUALLY READS THE PASS
Fifteen audited contract Session branches share BREAK_BLOCK; default Play reachability is separate
The source-derived capability matrix identifies 15 audited contract handlers with a PlayerSession-aware branch: five Chunk handlers, seven Building handlers, one Player equipment-durability handler, and two Backpack handlers. All 15 such branches request BREAK_BLOCK bit index 1, whose value or permission mask is 2. Fifteen does not mean or prove that one default Play bundle reaches, exports, or calls every handler; actual reachability depends on the runtime-resolved asset and call graph.
The five Chunk handlers are mine_block, mine_block_with_rewards, fell_tree_with_rewards, batch_mine_with_rewards, and range_mine_with_rewards. Current repository source defines both batch tag 20 and range tag 21 builders but its default bulk call points to range. The retained 2026-07-19 default live chain candidate exposes no recordBulkMineOnChain export and produced zero exact writeUInt8(20, 0) / writeUInt8(21, 0) literal matches; that exact-string search does not exclude equivalent encodings such as variables, hexadecimal bytes, or Buffer constructors. The one versioned dedicated bulk candidate named by the runtime returned 404. A separately probed unversioned bulk URL also returned 404 but was not the nonempty-version runtime's default. These byte observations separate contract capability from default live Play reachability and remain override-dependent.
The seven Building handlers are create_build_site, register_build_site_chunks, begin_building_upload, write_building_shard, finalize_building, cancel_building_upload, and begin_build_site_resize. They use Building instruction tags 0 through 5 and tag 7, and every one uses BREAK_BLOCK action index 1. Tag 6 is invalid in the final program. Building tag 8 is not an eighth Session flow: it publishes a Guardian blueprint through a separate fixed-publisher path. cancel_building_upload can return manifest and shard rent to sessionAuthority, so this bit can authorize a SOL destination as well as construction state.
The Player Program adds one Session-aware equipment path: consume_equipment_durability at tag 15. Mining transactions append its repository builder when equipped forged tools take durability damage. The handler requests BREAK_BLOCK, keeps PlayerSession read-only, derives and checks PDA(PlayerProgram, ["session", owner, sessionAuthority]), and mutates only the canonical PlayerEquipment record. This makes it stricter about the Session address than the audited Chunk, Building, and Backpack validators.
Backpack routing has two layers. The repository-default gameContext and the retained live chain candidate target the unified Game Program 6CurnvneezBuHwPUnrCiFg1QMWeUF67ufQxYebyr2UP7 for Backpack. Namespace byte 1 wraps the inner Backpack data: single removal is [1, 2, index], and batch removal begins [1, 4, count, ...]. The unified Game Program strips namespace one and delegates the remaining payload to the shared nicechunk_backpack handler.
Each Backpack handler also has two account shapes. Its four-account Session-aware branch reads authority, PlayerProfile, PlayerSession, and Backpack, then requests BREAK_BLOCK bit 1. Its two-account owner-direct branch uses owner signer plus Backpack and bypasses PlayerSession entirely. Repository and retained live chain-client discard builders use the four-account unified-Game form, but the contract's owner-direct form means it would be wrong to say every Backpack removal requires a Session.
Both forms remove a dense packed Backpack slot or reference. remove_resource shifts later fixed-size records left; remove_resources rejects empty, duplicate, or out-of-range indexes and removes valid choices high to low. Neither checks slot kind, type, or category, so material/resource, block, smelting, blueprint, and forged equipment/item references are structurally in scope. The removal only shifts and clears the packed Backpack record; it does not close, modify, or refund an underlying item PDA that the record may reference.
Repository Session-aware builders request all 15 PlayerSession metas read-only, but read-only is not the sole reason actionCount does not change. The handlers borrow or inspect Session data immutably, never read or compare actionCount with maxActions, and contain no increment path. The external Chunk, Building, and Backpack programs cannot write Player-owned Session data; the Player durability handler owns that account domain but still borrows Session immutably and mutates only PlayerEquipment. Therefore actionCount is not incremented or updated and 10,000 is not a current limit, even though a different transaction could merge account privileges more broadly.
Four stored control fields are ignored: the validators do not read or check bump, worldId, maxActions, or actionCount; createdSlot, updatedSlot, and createdAt also do not authorize actions. Every audited Session consumer now requires version 1 and active byte 1. Chunk, Building, and Player durability read globalConfig while Backpack's Session view omits it. Chunk and Building require sessionAuthority signer+writable; Player durability also requires signer+writable; Backpack's Session branch requires signer but not writable. All require 184 bytes, NCKSES01, and Player Program ownership. Chunk, Building, and Backpack do not derive the canonical PlayerSession PDA, while Player durability does derive and validate it. This external-consumer defense-in-depth gap is not proof that an attacker can freely create arbitrary Player-owned accounts.
Historical Profiles are rejected by current consumer layout validation. Chunk, Building, and Backpack require Profile length 773, NCKPLY01, version 7, initialized byte 1, and nine equipment slots before checking owner relationships. A v2 Profile fails length and a same-length v3 Profile fails version, even when a Session stores its exact pointer. Repository-default builders also derive current player-v7. A retained historical reference therefore documents old state that can remain allocated, not a compatibility route for current actions.
PLACE_BLOCK is bit index 2 with value 4, but it has zero audited contract consumers. The reviewed repository method and retained live chain candidate return the literal reason chain-placement-disabled before submitting ordinary placement. Storing mask 6 therefore does not make PLACE_BLOCK an on-chain placement permission.
Normal mining, rewarded mining, tree felling, batch mining, and range mining all request action index 1.
Building tags 0–5 and 7 share BREAK_BLOCK rather than a dedicated building permission; tag 6 is invalid.
Tag 15 requests BREAK_BLOCK, validates the canonical Session PDA, and writes only the canonical PlayerEquipment account.
Each four-account Session-aware branch requests BREAK_BLOCK; each handler also has a two-account owner-direct branch that does not read Session.
15 of 15 audited consumers use this one bit.
The current ordinary placement method reports chain-placement-disabled.
Builders request read-only and handlers only inspect Session; none reads the counter or invokes Player Program to increment it.
createdSlot, updatedSlot, and createdAt are also not authorization inputs; their presence does not make any of these fields an implemented gate.
Dense removal validates owner and indexes, shifts or clears only the packed reference, and neither restricts slot kind nor closes an item PDA.
Outer forms begin [1, 2, index] and [1, 4, count, ...], then unified Game delegates the remaining payload to shared Backpack code.
The separately fetched unversioned bulk URL was only a deployment-boundary probe, not the nonempty-version runtime's selected default candidate.
CLIENT CONVENIENCE VERSUS CHAIN AUTHORITY
Browser reuse is a heuristic; refresh is conditional, and saving the secret is a second commit
Plugin-wallet mode saves the session-purpose Keypair under nicechunk.session.v1.<owner wallet>. Reuse begins with the copied browser expiresAt and requires localExpiresAt > now + 900 seconds, a 15-minute skew. It derives that authority's PDA, but its on-chain Session test is only that the account has nonempty data. It does not decode or validate Session length, magic, stored owner, authority, Profile pointer, mask, or chain expiresAt before the branch proceeds.
The separately derived canonical-address Profile must belong to Player Program and pass the strict decoder: 773-byte NCKPLY01 shape, version 7, initialized byte 1, nine equipment slots, stored owner, and GlobalConfig. Reuse is still a browser convenience heuristic because the plugin branch checks only nonempty Session data rather than decoding that Session; a consumer can still reject the key for its actual Session, Profile relationship, expiry, permission, or action rules.
Reuse may still ask the owner wallet to sign. The branch calls funding before a complete on-chain Session validation; when balance is below 0.1 SOL, it builds an owner-to-authority System transfer and requests wallet approval. Only a fresh-enough, adequately funded stored key can avoid a new owner transaction. Approving a top-up is not proof that the pass is usable, because a later consumer can reject the Session after the SOL transfer has confirmed.
The same owner and same authority select the same PDA, but address selection does not guarantee a rewrite. Refresh requires the existing record's stored owner, sessionAuthority, playerProfile, and globalConfig to match the tag-4 inputs, requires the old authority to sign, and first requires the current player-v7 Profile PDA. Only then can it replace allowedActions, expiresAt, updatedSlot, and maxActions and reset actionCount. The contract accepts any still-future expiry, so a correctly approved refresh can shorten as well as lengthen time; the repository client normally asks for now + 8 hours.
That pointer rule matters to old accounts. A Session that still stores a historical v2 or v3 Profile pointer hits the same PDA for the same authority but cannot refresh in place against the required current player-v7 pointer. Plugin mode can choose a new authority and create a new PDA pointing at the current Profile, while the old PDA and rent remain. Local Game Wallet fixes authority = owner, so the same-address historical-pointer case has no automatic key-rotation escape in this flow. This is a source-level compatibility boundary, not an observed failure in the retained snapshot, whose five owner-equals-authority records all pointed at current v7.
The repository and SDK tag-4 builders mark PlayerProfile read-only because tag 4 does not migrate an allocated Profile. Before calling ensure_player_profile_current, the handler requires Player Program ownership. Final-layout validation then requires length 773, NCKPLY01, version 7, initialized byte 1, nine equipment slots, owner, and GlobalConfig. A valid Profile returns without mutation; an invalid allocated Profile is rejected. The helper's empty System-account allocation branch cannot be reached through tag 4 after the ownership precondition. In repository flows, a missing Profile first receives a tag-0 initialization instruction that marks it writable in the same transaction; tag 4 follows and sees a newly packed final Profile rather than repairing an old one.
Rotation is not revocation. Missing or damaged plugin storage leads to a new Keypair and different PDA. Creating that new PDA does not stop, deactivate, close, or refund the old one, and it locks another allocation's rent. The old record can remain usable with its old secret until its own expiry and gates pass. One Devnet owner having three Sessions is only a coexistence snapshot, not proof that all three were simultaneously usable.
Chain setup and browser secret persistence are non-atomic. The client broadcasts and waits for the owner setup transaction before calling storeGameplaySession; then localStorage.setItem performs a separate browser write. RPC confirmation ambiguity, page closure, unavailable storage, quota or permission errors, or a thrown setItem can leave a funded authority and PlayerSession on chain without a durable local secret. A later retry may generate another authority, transfer another target balance, and allocate another rent-funded PDA. No one outcome is guaranteed, but the two commits can strand repeated balances and rent.
Local Game Wallet has a different reuse shortcut. A five-minute in-memory ready cache can skip RPC and report a newly recomputed now + 8 hours instead of the record's real expiresAt. Five minutes bounds only the no-RPC cache window, not the expiry overstatement: a cached existing Session could have been just above the 15-minute freshness floor, so the synthetic local time may materially exceed the chain record. Outside the cache, its RPC freshness helper checks Player ownership, 184-byte magic, version 1, active byte 1, stored owner and authority, and expiresAt > now + 900. It still does not cross-check Session playerProfile, globalConfig, allowedActions, bump, worldId, maxActions, or actionCount. The consumer remains final.
Finally, play-chain-adapter.js can dynamically import a global override or localStorage nicechunk.chainModuleUrl before its normal candidates. Dynamic import executes and evaluates module code before the adapter checks whether expected exports exist. Never paste or configure an untrusted module URL: same-origin-accessible browser secrets may be exposed to code that runs. This is a capability and trust warning, not evidence that an override exploit or compromise has occurred.
The wallet address selects one browser record, but logout later scans the entire prefix across owners.
The current 15-minute client skew prompts refresh before the copied local expiry.
Rewrite occurs only after both signer roles, stored owner/authority/Profile/Config relationships, and current Profile preconditions pass.
The address changes because authority is a seed; another account allocation locks another rent balance.
The old authority may still use its old record until expiry; new-key creation writes no old account.
A reused browser key can still be rejected by chain-time expiry, missing bits, identity mismatches, or action rules.
Reuse does not first decode Session magic, length, stored relationships, mask, or chain expiry.
The canonical-address query uses the strict final-layout decoder before comparing owner and GlobalConfig.
Funding happens before complete Session authorization; successful funding does not prove later use will pass.
Failure between them can leave chain state and funds without a durable browser copy of the key.
Five minutes bounds RPC bypass, not how far the displayed local expiry may overstate the actual record.
FEES, BALANCES, AND PERSISTENT LOCAL KEYS
The 0.1 SOL policy is a refill trigger, not a spending cap or key restriction
A separate session-purpose Keypair needs SOL in its ordinary sessionAuthority System account to pay transaction fees and, where an action creates accounts, rent funding. The reviewed repository source and retained live chain candidate use a minimum of 100,000,000 lamports, or 0.1 SOL, and clamp the configured target to at least that value. Below 0.1 SOL the owner may be asked to transfer toward the target; at or above the threshold, a higher target is not automatically filled.
That top-up target is not a cap, limit, budget, escrow, or on-chain authorization rule. PlayerSession stores neither target nor balance. The Session record can expire, but the Keypair, address, and private signing bytes do not expire, erase, or revoke themselves. Expiry, allowedActions, maxActions, and actionCount do not protect or restrict the address's SOL and cannot stop the same key from signing an ordinary SystemProgram.transfer that never consults Player Program.
Repository chain code and the retained live candidate use the session Keypair as fee payer for supported later calls. Fees and account rent can reduce its balance, while any controller of the private key can create a different ordinary transfer signature. The offline one-lamport System Program proof verifies that signing shape without a real balance, live blockhash, broadcast, or fund movement, so it proves ordinary signing capability rather than successful execution.
Do not combine authority balance with PlayerSession PDA rent. The retained Devnet accounts each held 2,171,520 lamports as the observed rent-exempt minimum for a separate 184-byte PDA; the snapshot did not query authority balances. A new authority creates another PDA and locks another rent allocation. Because the reviewed Player Program has no Session close path, expiry, rotation, and logout do not refund that rent.
Logout also does not sweep the authority's SOL. If best-effort cleanup successfully deletes the only nicechunk.session.v1.<owner> copy and no backup exists, normal UI flow no longer has the private bytes needed to move the old address's remaining SOL. Reconnecting can create and fund a new key instead. This loss is not guaranteed—copies may exist or deletion may fail—but there is no automatic recovery or pre-logout drain, so 0.1 SOL is not a guaranteed maximum loss.
Local Game Wallet is a different and larger risk shape. Its owner = sessionAuthority, so one persistent local key can satisfy both setup roles and later wallet authority. The browser stores that long-lived private key as an unencrypted plain Base58 string under separate Local Game Wallet keys. Its full wallet balance is exposed to that key, and plugin-session logout does not delete it. usesGameplaySession: false says only that there is no separate gameplay Keypair; it does not prove an on-chain PDA exists. After successful setup, the flow can use PDA(PlayerProgram, ["session", owner, owner]).
If only an independent plugin session key leaks, the old record's chain expiry eventually blocks its audited NiceChunk branches. If the owner wallet secret or Local Game Wallet key leaks, the attacker can occupy both required setup roles or choose a new authority and create or refresh a future Session. Session expiry is therefore not final containment for owner-key compromise, and short expiries cannot repair loss of the long-lived key.
Present in reviewed repository and retained live chain bytes; it is not a field in the 184-byte Session.
Used to calculate an owner transfer only when the address is below the minimum threshold.
Observed on each 184-byte PlayerSession PDA; a new authority allocates another PDA, and no reviewed Session close refunds it.
The chain client sets transaction.feePayer to the Keypair public key and signs with that Keypair.
No PlayerSession account or Player Program invocation is required by that separate transaction.
The owner funds a separate, limited-balance address but PlayerSession still does not enforce that balance.
One persistent key fills both roles; the status flag does not itself prove Session setup or PDA existence.
This long-lived Local Game Wallet secret is separate from nicechunk.session.v1.<owner>.
The Keypair, address, SOL control, and ability to sign another message do not expire with that record.
Without another copy, the normal UI cannot recover that old authority's SOL; the PDA rent also remains allocated.
HOW AUTHORITY REALLY ENDS
Expiry closes checked gates; logout, key rotation, and Guardian do not revoke the account
A Chunk, Building, or Backpack Session branch rejects when expiresAt <= Clock.unix_timestamp. That closes this old public record's checked gate; it does not expire, erase, revoke, or zero the Keypair, address, private bytes, or SOL. The copied browser time is only a hint and cannot make a chain-expired record valid.
The reviewed Player Program has no close, revoke, or deactivate instruction for PlayerSession and no owner-only kill switch. Refresh of a distinct old authority requires that old authority to sign; replacement authority creates another PDA without changing or refunding the old one. The owner therefore cannot immediately close a stolen independent Session through this interface.
Expiry is meaningful only after naming which secret leaked. If an attacker holds only an independent plugin session key, expiry is the cutoff for that old record's audited NiceChunk branches, although the key can still sign ordinary transactions and control remaining SOL. If the owner wallet or Local Game Wallet key is stolen or compromised, that key can approve a new authority or, when owner = sessionAuthority, satisfy both setup roles and create or refresh another future Session. Expiry is not a final or complete compromise stop-loss for the long-lived owner key.
Logout is best-effort browser cleanup, not a verified deletion. clearWalletSession first attempts four wallet-identity removals, then tries to enumerate localStorage. If storage.length or storage.key enumeration throws, it returns and skips the runtime-prefix pass. When enumeration succeeds, it attempts every key beginning nicechunk.session.v1., nicechunk.equippedBackpack.v1., nicechunk.sessionFundingLamports.v1., or nicechunk.sessionFundingAcknowledged.v1. Each removeItem error is caught and swallowed, and the helper does not read back or confirm success.
The prefix pass is not limited to the connected wallet. A successful scan can delete another owner's only or last browser copy of a plugin session secret, while an error can leave a supposedly cleared secret behind. Logout therefore does not guarantee deletion, and its cross-owner reach is a risk rather than proof that every matching key was removed.
Logout does not alter on-chain PlayerSession state, send a revoke, close or refund the Session PDA, or drain, recover, return, or refund the authority address's SOL. It also does not delete the separately stored Local Game Wallet private key. If the only plugin secret is successfully removed before its balance is swept, the normal UI may lose access to that SOL even while the public Session and rent remain. A copied key elsewhere may keep using an unexpired old record if its gates pass.
Guardian is independent of this lifecycle. Realtime HELLO sends an unsigned public wallet hint plus nonce—not possession proof, PlayerSession, or wallet signature. Guardian Program manages registry, operators, endpoints, and blueprints; Guardian does not issue, validate, refresh, or revoke PlayerSession. Building tag 8 is a separate fixed-publisher Guardian blueprint path, not one of Building's seven Session-aware tags.
At confirmed Devnet context slot 477236696, the retained 2026-07-18 RPC getBlockTime value classified all 10 Session records as expired. This was not a direct Clock sysvar read. Session/Profile/Config reads shared that slot, while Player Program and ProgramData hash reads used supplemental nearby +1 and +2 contexts and cover only Player deployment bytes. Expired records remaining visible show allocation can outlive authorization, but this one confirmed Devnet snapshot is not Mainnet, an ongoing monitor, or a permanent population count.
The decision uses Clock.unix_timestamp inside each audited consumer.
The reviewed Player Program dispatch exposes create or refresh but no Session lifecycle kill handler.
Owner consent alone cannot extend or overwrite a distinct old authority's canonical Session.
Enumeration failure skips the prefix pass; each removeItem error is swallowed and no success read-back occurs.
No revoke, Session PDA close or rent refund, session-authority SOL drain, or other on-chain Session transaction is sent.
Plugin-session cleanup does not clear the separate Base58 secret.
No PlayerSession, session authority, session secret, or wallet signature is sent in HELLO.
Expiry used context-slot RPC getBlockTime rather than a direct Clock sysvar read; Player deployment hashes used nearby +1/+2 contexts.
That deletion still does not sweep its SOL, refund its Session rent, or remove copies outside this browser origin.
Expiry contains only the old record used by an attacker who lacks the owner key; it is not complete owner-key recovery.
VERIFY THE BOUNDARY
Separate the account predicate, the ordinary key, and the browser lifecycle
The formulas translate signer roles, byte layout, permission bits, consumer reads, reuse, funding, and expiry into plain tests. Literal excerpts separate the repository production /play/ source route, the runtime-resolved live Nginx assets retained on 2026-07-19 UTC, contract capability, SDK serialization, offline derivation, and Devnet account snapshot.
Two roles describe one player's session
roles = { owner: long-lived identity, sessionAuthority: signer presented to consumers }The public keys may be different in plugin-wallet mode or identical in Local Game Wallet mode. Account roles are logical positions; they do not guarantee two different physical secrets.
- owner
- The persistent public key checked by PlayerProfile.
- sessionAuthority
- The signer public key compared by a session-aware consumer.
Canonical PlayerSession address
P_session = PDA(PlayerProgram, ["session", owner, sessionAuthority])The two public keys are address inputs. Keeping both identical inputs returns the same PDA; replacing the authority returns a different PDA and does not write the earlier account.
- P_session
- The deterministic 184-byte public account address.
- PlayerProgram
- The NiceChunk Player Program whose ID defines the PDA domain.
Direct browser signer count follows distinct public keys
setupSigners = { owner, sessionAuthority }; owner != sessionAuthority ⇒ 2 signatures; owner = sessionAuthority ⇒ 1 signature satisfies both rolesThis describes the reviewed direct browser transaction. The contract actually checks is_signer on both account positions; equal metas can rely on one transaction signature, while a CPI PDA signer may use invoke_signed instead of a private key.
- setupSigners
- The public keys whose account roles must be marked as signers.
Reviewed repository setup instruction size
setupDataBytes = 1 tag + 8 expiresAt + 2 allowedActions + 4 maxActions = 15 bytesTag 4 selects the handler. The contract receives the remaining fourteen bytes as payload and reads the timestamp, mask, and stored maximum from fixed offsets.
- tag
- The one-byte instruction selector whose current value is 4.
Core create-or-refresh acceptance
setupCoreOK = ownerSigned AND sessionSigned AND expiresAt > Clock.unix_timestamp AND allowedActions != 0 AND (allowedActions & ~6) = 0 AND maxActions > 0PDA, ownership, Profile, GlobalConfig, System Program, and writable-account checks also run. The expression isolates the signer and payload requirements most visible to players.
- 6
- The union of the only two declared setup permission bits.
Default permission mask
defaultAllowedActions = (1 << 1) | (1 << 2) = 2 | 4 = 6 = 0b0110A mask records which bits are present. It does not prove that a corresponding consumer exists, and current PLACE_BLOCK has no audited consumer.
- 1 << n
- Move one binary bit left by n positions.
Fixed record length
184 = 8 + 2 + 1 + 1 + 32 + 32 + 32 + 32 + 2 + 2 + 8 + 8 + 8 + 8 + 4 + 4 bytesThe sum follows the literal Rust writer and the SDK decoder order. Offsets are byte positions inside this exact fixed-length public account.
- 32
- The byte length of each Solana public key field.
Chunk and Building consumer gate
gateCB(a,t) = ownedByPlayerProgram AND length=184 AND magic=NCKSES01 AND version=1 AND active=1 AND sessionSignedWritable AND authorityMatch AND profileMatch AND globalConfigMatch AND expiresAt > t AND (allowedActions & (1 << a)) != 0Their helpers then validate the owner-backed final-layout Profile and continue into action-specific rules. This expression includes fixed Session shape and lifecycle predicates but not every downstream rule; they do not derive the canonical Session PDA or read bump, worldId, maxActions, or actionCount.
- a
- The requested action-bit index, currently fixed to 1 in these consumers.
- t
- Clock.unix_timestamp read during the instruction.
Backpack consumer gate
gateBackpack(a,t) = ownedByPlayerProgram AND length=184 AND magic=NCKSES01 AND version=1 AND active=1 AND sessionSigned AND authorityMatch AND profileMatch AND expiresAt > t AND (allowedActions & (1 << a)) != 0Unlike Chunk and Building, Backpack requires the authority to sign but not to be writable, and its Session view does not compare GlobalConfig. It later validates Profile owner and the owner's Backpack PDA; this expression is the shared Session gate rather than every deletion rule.
- gateBackpack
- The shared PlayerSession part of either dense removal handler.
Highlighted control fields omitted by every audited consumer
unreadControlFields = { bump, worldId, maxActions, actionCount }; alsoNotConsumed = { createdSlot, updatedSlot, createdAt }A stored field has no enforcement effect unless a consumer reads and applies it. Backpack additionally omits globalConfig, while Chunk and Building compare that field.
- unreadControlFields
- The four control fields explicitly tracked as ignored by every audited consumer view.
- alsoNotConsumed
- Creation and update metadata retained in the record but not used by the consumer gates.
Audited contract Session-branch total
auditedSessionBranches = 5 Chunk + 7 Building + 1 Player durability + 2 Backpack = 15This is a contract and repository-builder capability surface, not proof that one default Play runtime reaches all fifteen. Every counted Session branch requests BREAK_BLOCK; Backpack also keeps owner-direct branches outside this count.
- auditedSessionBranches
- Handlers with a traced PlayerSession-aware account branch, independent of default live client reachability.
Actual permission consumption
BREAK_BLOCK = 1 << 1 = 2 → 15 audited contract Session branches; PLACE_BLOCK = 1 << 2 = 4 → 0 audited contract consumersThe contract matrix, not the friendly name, defines capability. Default runtime reachability remains asset-dependent, and reviewed ordinary placement returns chain-placement-disabled before a Session consumer is submitted.
- →
- Maps a stored bit to the number of audited consumer paths using it.
No implemented action-count ceiling
builders request read-only AND handlers only inspect Session AND actionCount is unread AND no Player increment CPI ⇒ actionCountAfter = actionCountBefore; actionCount < maxActions predicate = absentRead-only account meta is not the sole barrier because transaction privileges can merge. The independent handler behavior, Player ownership boundary, missing comparison, and missing increment instruction establish the current non-enforcement.
- absent
- No such comparison exists in the audited authorization paths.
Unified Game Backpack envelope
single outer data = [namespace 1, inner tag 2, index]; batch outer data = [namespace 1, inner tag 4, count, indexes...]Game Program removes the first namespace byte and delegates the remaining payload to shared Backpack code. This explains why outer live bytes and inner Backpack handler tags are both correct descriptions of one route.
- namespace 1
- The unified Game selector for Backpack instructions.
Stored ten thousand is not a cap
stored maxActions = 10,000 != enforced successful-action limitThe value is visible public data and must be nonzero at setup. Without a consumer read, comparison, and counter update, it remains descriptive storage rather than a ceiling.
- !=
- Means the two ideas must not be treated as equivalent.
Plugin browser reuse heuristic
reuseClient = storedKey AND localExpiresAt > now + 900 AND sessionPdaNonemptyData AND canonicalProfileSelectedGateSession data is not decoded here. The Profile predicate uses the strict final decoder for length, magic, version 7, initialized state, and nine equipment slots before checking stored owner and GlobalConfig. Consumers remain final for the Session and action.
- 900
- The current fifteen-minute refresh skew in seconds.
Refresh and rotation address outcomes
same owner + same authority ⇒ same PDA selection; rewrite only if stored owner, authority, Profile, Config and current player-v7 preconditions match; new authority ⇒ different PDA AND old PDA unchangedThe repository normally asks for now plus eight hours, but any still-future expiry may shorten or lengthen time. A historical Profile pointer can make same-PDA refresh fail; a new authority adds another rent-funded account.
- unchanged
- The replacement-key transaction does not pass the older account as writable.
Chain setup and secret storage are separate commits
wallet broadcast/confirmation → PlayerSession and funding may exist; then localStorage.setItem(secret) → browser recovery copy may existThere is no atomic rollback joining these two systems. Confirmation ambiguity, page closure, unavailable storage, quota failure, or setItem exception can leave chain state and funds without a saved browser secret; retries may create another key and PDA.
- →
- Execution order, not a guarantee that the later step succeeds.
Local Game Wallet cache reports synthetic expiry
readyCacheAge < 300 seconds ⇒ skip RPC and report expiresAt = now + 28,800; on-chain expiresAt remains unchangedFive minutes limits only how long RPC is bypassed. A cached existing record may have been near the fifteen-minute freshness floor, so the synthetic reported expiry can materially overstate chain expiry by far more than five minutes.
- 300
- The in-memory cache lifetime in seconds, not an expiry-error bound.
Current top-up policy
topUp = balance < 0.1 SOL ? max(0, configuredTarget - balance) : 0The minimum and target are browser policy, not an escrow or cap. During reuse, this top-up can request another owner-wallet approval before the Session has been fully validated, so funding does not prove usability.
- balance
- The confirmed SOL balance of the separate plugin session address.
Ordinary transfer bypasses the pass record
ordinaryTransferSignatureValid = sessionKeySignature AND SystemProgram.transfer; PlayerSessionReferenced = false; PlayerProgramInvoked = falseThe retained proof uses a synthetic unfunded key, stays offline, sends no transaction, and exposes no private key. It proves signature capability, not executable balance or real fund movement.
- ordinaryTransferSignatureValid
- Cryptographic construction and signature verification, not a broadcast result.
Rotation allocates rent without closing the old account
new sessionAuthority ⇒ new PlayerSession PDA + another rent allocation; reviewed close/refund instruction = absentThe retained Devnet minimum was 2,171,520 lamports for each 184-byte Session. Expiry, logout, and key replacement do not reclaim an older PDA's lamports under the reviewed lifecycle.
- another rent allocation
- A separate account balance, not the temporary authority's 0.1 SOL refill target.
Logout is a best-effort attempt
logout = attempt identity removals; if enumeration succeeds, attempt every matching prefix removal; per-key exceptions are swallowed; success is not verifiedEven a successful local secret deletion sends no chain instruction, drains no authority SOL, closes no Session PDA, and leaves the separate Local Game Wallet secret untouched.
- attempt
- A requested local operation whose success is neither guaranteed nor verified.
Consumer expiry rule
authorizedAtUse = expiresAt > Clock.unix_timestampWhen expiresAt is equal to or below chain time, the checked Session path rejects. The public account may remain allocated after this authority ends.
- Clock.unix_timestamp
- The chain-provided Unix timestamp inside the program.
Retained Devnet observation
snapshot(2026-07-18, confirmed slot 477236696, RPC getBlockTime) = 10 PlayerSessions, 8 unique owners, 10 expired relative to retained block timeRPC getBlockTime supplied 2026-07-18T21:56:51Z; the capture did not directly read Clock sysvar. Session/Profile/Config shared the context slot, while Player Program and ProgramData hashes used supplemental +1/+2 slots and cover only Player deployment bytes.
- snapshot
- One shared RPC context whose retained bytes can be checked offline.
Repository Play source declares the modular entry
HTMLplay/index.html <script type="module" src="./main.js"></script>
This repository source tag declares the intended production /play/ source route through /play/main.js and play/*. It is not the retained live served byte route: the 2026-07-19 Nginx snapshot returned a versioned bundle from /play/index.html.
- Line 1
Load main.js as an ES module relative to /play/index.html.
The repository modular entry wires player sync, chain loading, and wallet-session control
JavaScriptplay/main.jsimport { createPlayChainPlayerSync } from "./play-chain-player.js";
import { loadPlayChainModule, mineShouldSavePlayerPosition } from "./play-chain-adapter.js";
import { createPlayChainSession, WALLET_SESSION_CHANGED_EVENT } from "./play-chain-session.js";
The repository working-tree entry imports PlayerProfile synchronization, the chain-module loader and mining position-save predicate, and the wallet-session controller; the older /src/main.js is outside this route. This is source evidence, not the retained live served module: direct /play/main.js returned index HTML in the snapshot.
- Line 1
Import the PlayerProfile synchronization module declared by this repository source-tree route, not retained live served bytes.
- Lines 2–3
Import the repository chain-module loader, mining position-save predicate, and wallet-session lifecycle; the retained live served bytes came from a versioned bundle instead.
Player state declares the Session family and PDA seed
Rustprograms/nicechunk_player/src/state.rspub const PLAYER_SESSION_MAGIC: [u8; 8] = *b"NCKSES01";
pub const PLAYER_SESSION_VERSION: u16 = 1;
pub const PLAYER_SESSION_SEED: &[u8] = b"session";
These constants identify the 184-byte public record family and the literal first seed used by the canonical PDA.
- Lines 1–2
Name the account family and current packed version written during setup.
- Line 3
Fix the UTF-8 seed word used before owner and session authority.
Rust packs all sixteen public fields in order
Rustprograms/nicechunk_player/src/state.rs writer.bytes(&PLAYER_SESSION_MAGIC)?;
writer.u16(PLAYER_SESSION_VERSION)?;
writer.u8(args.bump)?;
writer.u8(1)?;
writer.pubkey(args.owner)?;
writer.pubkey(args.session_authority)?;
writer.pubkey(args.player_profile)?;
writer.pubkey(args.global_config)?;
writer.u16(args.world_id)?;
writer.u16(args.allowed_actions)?;
writer.i64(args.expires_at)?;
writer.u64(args.created_slot)?;
writer.u64(args.created_slot)?;
writer.i64(args.created_at)?;
writer.u32(args.max_actions)?;
writer.u32(0)?;
The sequential writers produce the offset table shown above. updatedSlot begins as createdSlot, and actionCount begins as zero.
- Lines 1–4
Write magic, version, bump, and the initialized or active byte.
- Lines 5–8
Write the four public keys for owner, authority, Profile, and GlobalConfig.
- Lines 9–16
Write world, mask, expiry, slots, creation time, maximum, and zero action count.
Refresh resets the stored counter but does not revoke
Rustprograms/nicechunk_player/src/state.rs dst[Self::UPDATED_SLOT_OFFSET..Self::UPDATED_SLOT_OFFSET + 8]
.copy_from_slice(&updated_slot.to_le_bytes());
dst[Self::MAX_ACTIONS_OFFSET..Self::MAX_ACTIONS_OFFSET + 4]
.copy_from_slice(&max_actions.to_le_bytes());
dst[Self::ACTION_COUNT_OFFSET..Self::ACTION_COUNT_OFFSET + 4]
.copy_from_slice(&0_u32.to_le_bytes());
Ok(())
A refresh updates the slot and stored maximum and writes actionCount back to zero. No close, revoke, or deactivate behavior appears in this method.
- Lines 1–4
Replace updatedSlot and maxActions on the same validated account.
- Lines 5–7
Reset actionCount to zero and finish without closing the account.
Setup requires both signer account roles
Rustprograms/nicechunk_player/src/lib.rs if !owner.is_signer || !owner.is_writable {
return Err(NicechunkPlayerError::InvalidPayer.into());
}
if !session_authority.is_signer {
return Err(NicechunkPlayerError::InvalidSessionAuthority.into());
}
if !player_session.is_writable {
return Err(NicechunkPlayerError::InvalidWritableAccount.into());
}
Distinct owner and session keys both have to sign the setup transaction. If their public keys match, the same transaction signature satisfies both signer metas.
- Lines 1–3
Require the owner to sign and remain writable as setup payer.
- Lines 4–6
Require possession of the session-authority secret through its signature.
- Lines 7–9
Allow create or refresh to write the canonical Session account.
The Player Program restricts setup payload and derives the canonical PDA
Rustprograms/nicechunk_player/src/lib.rs let expires_at = read_i64(payload, 0);
let allowed_actions = read_u16(payload, 8);
let max_actions = read_u32(payload, 10);
let allowed_mask = SESSION_ACTION_BREAK_BLOCK | SESSION_ACTION_PLACE_BLOCK;
if allowed_actions == 0 || allowed_actions & !allowed_mask != 0 || max_actions == 0 {
return Err(NicechunkPlayerError::InvalidInstruction.into());
}
let clock = Clock::get()?;
if expires_at <= clock.unix_timestamp {
return Err(NicechunkPlayerError::InvalidInstruction.into());
}
let (expected_player_profile, profile_bump) =
Pubkey::find_program_address(&[PLAYER_PROFILE_SEED, owner.key.as_ref()], program_id);
require_key_eq(
player_profile.key,
&expected_player_profile,
NicechunkPlayerError::InvalidPlayerProfilePda,
)?;
let (expected_player_session, bump) = Pubkey::find_program_address(
&[
PLAYER_SESSION_SEED,
owner.key.as_ref(),
session_authority.key.as_ref(),
],
program_id,
);
The payload must use only declared bits, a nonzero maximum, and future expiry. PDA derivation then binds the literal seed, owner, and session authority under Player Program.
- Lines 1–7
Decode the fourteen-byte payload and reject zero or unknown permission values.
- Lines 9–12
Compare expiresAt with the chain Clock rather than the browser clock.
- Lines 14–28
Derive the canonical Profile and Session addresses from their ordered public seeds.
The SDK uses the same Session seed tuple
TypeScriptsdk/nicechunk-player.ts return PublicKey.findProgramAddressSync(
[Buffer.from(PLAYER_SESSION_SEED), owner.toBuffer(), sessionAuthority.toBuffer()],
programId,
);
Independent clients can reproduce the Player Program's address because the SDK keeps the identical seed order.
- Lines 1–4
Derive one PDA from the session literal, owner bytes, authority bytes, and Player Program ID.
The SDK serializes tag four and fifteen bytes
TypeScriptsdk/nicechunk-player.ts const data = Buffer.alloc(15);
data.writeUInt8(4, 0);
data.writeBigInt64LE(BigInt(expiresAt), 1);
data.writeUInt16LE(allowedActions, 9);
data.writeUInt32LE(maxActions, 11);
This mirrors the production builder: one selector byte precedes the fourteen bytes parsed by the Rust handler.
- Lines 1–2
Allocate fifteen bytes and write instruction tag four first.
- Lines 3–5
Write expiry, action mask, and maximum in little-endian form.
The SDK decoder follows the same field order
TypeScriptsdk/nicechunk-player.ts const decoded: DecodedPlayerSession = {
magic: bytes(8).toString("utf8"),
version: u16(),
bump: u8(),
active: u8() === 1,
owner: pubkey(),
sessionAuthority: pubkey(),
playerProfile: pubkey(),
globalConfig: pubkey(),
worldId: u16(),
allowedActions: u16(),
expiresAt: i64(),
createdSlot: u64(),
updatedSlot: u64(),
createdAt: i64(),
maxActions: u32(),
actionCount: u32(),
};
The decoder consumes the same sixteen fields as Rust pack. The derived audit checks that both orders and byte widths still agree.
- Lines 1–9
Decode the header and four public keys before reaching world state.
- Lines 10–18
Decode mask, time, slots, creation time, maximum, and action count.
Chunk reads expiry and the requested action bit
Rustprograms/nicechunk_chunk/src/state.rs if read_i64(data, PLAYER_SESSION_EXPIRES_AT_OFFSET) <= now {
return Err(NicechunkChunkError::PlayerSessionExpired);
}
let allowed_actions = read_u16(data, PLAYER_SESSION_ALLOWED_ACTIONS_OFFSET);
if action >= 16 || allowed_actions & (1_u16 << action) == 0 {
return Err(NicechunkChunkError::SessionActionNotAllowed);
}
After identity comparisons, Chunk rejects expiry at or before chain time and a missing requested bit. This range contains no maxActions or actionCount read.
- Lines 1–3
Reject a Session whose stored expiry is no longer in the future.
- Lines 4–7
Read allowedActions and require the consumer's bit to be present.
Chunk dispatch names all five audited Session consumers
Rustprograms/nicechunk_chunk/src/lib.rs match tag {
5 => mine_block(program_id, accounts, payload),
6 => initialize_chunk_broken(program_id, accounts, payload),
7 => initialize_resource_drop_table(program_id, accounts, payload),
8 => mine_block_with_rewards(program_id, accounts, payload),
9 => fell_tree_with_rewards(program_id, accounts, payload),
10 => apply_civilization_resource_drop_receipt(program_id, accounts, payload),
11 => initialize_surface_decoration_table(program_id, accounts, payload),
12 => verify_surface_decoration(program_id, accounts, payload),
13 => apply_civilization_surface_decoration_receipt(program_id, accounts, payload),
15 => register_build_site_chunk(program_id, accounts, payload),
20 => batch_mine_with_rewards(program_id, accounts, payload),
21 => range_mine_with_rewards(program_id, accounts, payload),
_ => Err(NicechunkChunkError::InvalidInstruction.into()),
}
The audit traces Session metas from production builders to five handlers in this dispatch: tags 5, 8, 9, 20, and 21.
- Lines 1–13
Map Chunk instruction tags to handlers, including the five Session-aware mining and tree paths.
- Line 14
Reject any tag outside this explicit dispatch table.
Building mirrors the expiry and permission gate
Rustprograms/nicechunk_building/src/state.rs if read_i64(data, PLAYER_SESSION_EXPIRES_AT_OFFSET) <= now {
return Err(NicechunkBuildingError::PlayerSessionExpired);
}
let allowed_actions = read_u16(data, PLAYER_SESSION_ALLOWED_ACTIONS_OFFSET);
if action >= 16 || allowed_actions & (1_u16 << action) == 0 {
return Err(NicechunkBuildingError::SessionActionNotAllowed);
}
Building uses the same fixed offsets for expiry and allowedActions. Its current shared helper passes action index 1 for tags 0–5 and 7.
- Lines 1–3
Reject when the public record has reached its chain-time expiry.
- Lines 4–7
Require the Building helper's requested bit in allowedActions.
Building dispatch separates seven Session flows from Guardian publish
Rustprograms/nicechunk_building/src/lib.rs match tag {
0 => create_build_site(program_id, accounts, payload),
1 => register_build_site_chunks(program_id, accounts, payload),
2 => begin_building_upload(program_id, accounts, payload),
3 => write_building_shard(program_id, accounts, payload),
4 => finalize_building(program_id, accounts, payload),
5 => cancel_building_upload(program_id, accounts, payload),
7 => begin_build_site_resize(program_id, accounts, payload),
8 => publish_guardian_blueprint(program_id, accounts, payload),
_ => Err(NicechunkBuildingError::InvalidInstruction.into()),
}
The audited matrix finds PlayerSession on all seven handlers at tags 0–5 and 7. Tag 6 is invalid, while tag 8 is a separate fixed-publisher Guardian CPI and does not use PlayerSession.
- Lines 1–9
Dispatch the seven construction lifecycle handlers that share action index one.
- Lines 10–12
Keep Guardian blueprint publishing and invalid tags outside that Session consumer set.
Single Backpack removal shifts the dense array without a type check
Rustprograms/nicechunk_backpack/src/state.rs pub fn remove_resource_at(
data: &mut [u8],
owner: &Pubkey,
index: u8,
updated_slot: u64,
) -> ProgramResult {
Self::validate_owner(data, owner)?;
let item_count = data[Self::ITEM_COUNT_OFFSET];
if index >= item_count {
return Err(NicechunkBackpackError::InvalidResourceIndex.into());
}
let start = Self::RECORDS_OFFSET + index as usize * BACKPACK_SLOT_RECORD_LEN;
let removed = BackpackSlotRecord::unpack(&data[start..start + BACKPACK_SLOT_RECORD_LEN])?;
Self::subtract_total_mass(data, removed.mass_grams()?)?;
let end = Self::RECORDS_OFFSET + item_count as usize * BACKPACK_SLOT_RECORD_LEN;
if start + BACKPACK_SLOT_RECORD_LEN < end {
data.copy_within(start + BACKPACK_SLOT_RECORD_LEN..end, start);
}
let last_start =
Self::RECORDS_OFFSET + (item_count - 1) as usize * BACKPACK_SLOT_RECORD_LEN;
data[last_start..last_start + BACKPACK_SLOT_RECORD_LEN].fill(0);
data[Self::ITEM_COUNT_OFFSET] = item_count.saturating_sub(1);
The method validates owner and index, shifts every later fixed-size record left, clears the old last slot, and reduces count. It never reads a slot kind or category.
- Lines 1–11
Validate Backpack ownership and ensure the dense index exists.
- Lines 13–17
Copy later packed records left over the selected record.
- Lines 18–21
Clear the final duplicate and reduce the packed item count.
Backpack Session removal asks for BREAK_BLOCK
Rustprograms/nicechunk_backpack/src/lib.rs let player_session_data = player_session.try_borrow_data()?;
let session = PlayerSessionView::validate(
&player_session_data,
session_authority.key,
player_profile.key,
SESSION_ACTION_BREAK_BLOCK,
clock.unix_timestamp,
)?;
drop(player_session_data);
let player_profile_data = player_profile.try_borrow_data()?;
PlayerProfileView::validate_owner(&player_profile_data, &session.owner)?;
drop(player_profile_data);
validate_existing_backpack_pda(program_id, backpack, &session.owner)?;
let mut backpack_data = backpack.try_borrow_mut_data()?;
BackpackAccount::remove_resource_at(&mut backpack_data, &session.owner, index, clock.slot)
The single-removal session path borrows PlayerSession read-only, requests BREAK_BLOCK, resolves its owner, and mutates the owner-backed Backpack rather than Session.
- Lines 1–9
Read and validate PlayerSession using the BREAK_BLOCK permission constant.
- Lines 11–14
Bind the resolved owner to PlayerProfile and the canonical Backpack.
- Lines 16–17
Borrow only Backpack mutably and perform dense-index removal.
Repository browser policy sets time and funding defaults
JavaScriptsrc/chain/nicechunkChain.jsconst sessionDurationSeconds = 8 * 60 * 60;
const sessionRefreshSkewSeconds = 15 * 60;
const lamportsPerSol = 1_000_000_000;
const minimumSessionFundingLamports = 100_000_000;
These repository source constants define duration, skew, conversion, and minimum funding. They are not by themselves the retained live served bytes; the separate production snapshot hashes the live candidate and confirms equivalent literals.
- Lines 1–2
Set the requested duration and early-refresh window in seconds.
- Lines 3–4
Define SOL conversion and the browser minimum funding threshold.
Plugin reuse trusts a local window and account presence
JavaScriptsrc/chain/nicechunkChain.js if (stored && stored.expiresAt > nowSeconds + sessionRefreshSkewSeconds) {
const [playerSession] = derivePlayerSessionPda(owner, stored.keypair.publicKey);
const [playerProfile] = derivePlayerProfilePda(owner);
const [account, sessionAccount, profileAccount] = await conn.getMultipleAccountsInfo(
[playerSession, stored.keypair.publicKey, playerProfile],
"confirmed",
);
const sessionBalance = accountLamports(sessionAccount);
if (account?.data?.length && isCurrentPlayerProfileAccount(profileAccount, owner)) {
await fundGameplaySessionIfNeeded(provider, stored.keypair.publicKey, sessionBalance, conn);
This repository source branch uses copied time, nonempty Session data, selected Profile checks, and funding; it is not the retained live served excerpt. It does not fully decode the Session, and low balance can prompt an owner top-up before consumer validation.
- Lines 1–3
Require the local expiry window and derive addresses from the stored authority.
- Lines 4–7
Read Session, session-address balance, and Profile together at confirmed commitment.
- Lines 8–10
Accept nonempty Session data plus the selected Profile gate and top up the stored authority if needed.
Local Game Wallet puts the owner in both Session roles
JavaScriptsrc/chain/nicechunkChain.js tx.add(createOrRefreshPlayerSessionInstruction({
owner,
sessionAuthority: owner,
playerProfile,
playerSession,
expiresAt,
}));
await signAndSendKeypairTransaction(keypair, tx, conn);
In repository source, the same owner key occupies both Session roles and signs the direct transaction. This excerpt is not the retained live served asset, and successful execution—not usesGameplaySession:false—would establish the PDA.
- Lines 1–7
Build tag four with the owner public key in both account roles.
- Line 9
Sign and submit with the one persistent Local Game Wallet Keypair.
Ordinary placement is currently disabled before submission
JavaScriptsrc/chain/nicechunkChain.jsexport async function recordBlockPlacementOnChain(_target, _renderType, _toolSlot = 0) {
return { submitted: false, reason: "chain-placement-disabled" };
}
This repository source method returns chain-placement-disabled without submission; it is not the retained live served excerpt. The separately hashed live chain candidate contains the same literal behavior at capture time.
- Lines 1–3
Return the literal disabled reason without constructing or sending a chain placement transaction.
Plugin mode stores private session bytes as Base64
JavaScriptsrc/chain/nicechunkChain.jsfunction storeGameplaySession(owner, session) {
if (!hasLocalStorage()) return;
localStorage.setItem(sessionStorageKey(owner), JSON.stringify({
publicKey: session.keypair.publicKey.toBase58(),
secretKey: bytesToBase64(session.keypair.secretKey),
expiresAt: session.expiresAt,
savedAt: Date.now(),
}));
}
This repository source stores Base64 rather than encrypted private bytes under an owner suffix. It is not the retained live served byte evidence, and localStorage access remains a browser trust boundary.
- Lines 1–3
Require browser storage and select the owner-scoped Session record.
- Lines 4–8
Save public key, Base64 secret bytes, copied expiry, and save time.
- Line 9
Complete the local-only storage helper without any chain call.
The session Keypair becomes an ordinary transaction fee payer
JavaScriptsrc/chain/nicechunkChain.jsasync function signAndSendKeypairTransaction(signer, transaction, conn = getNicechunkConnection()) {
transaction.feePayer = signer.publicKey;
const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash("confirmed");
transaction.recentBlockhash = blockhash;
transaction.lastValidBlockHeight = lastValidBlockHeight;
transaction.sign(signer);
This repository source helper makes an ordinary Keypair fee payer and signer without a Session wrapper. It is not the retained live served excerpt; the offline audit separately verifies the cryptographic System-transfer shape.
- Lines 1–2
Use the supplied Keypair's public key as transaction fee payer.
- Lines 3–5
Fetch a recent confirmed blockhash and its validity boundary.
- Line 6
Apply the ordinary Keypair signature to the constructed transaction.
Repository Play adapter forwards placement into a resolved module
JavaScriptplay/play-chain-adapter.js async function submitPlace(pending) {
if (!pending) return skipped("missing-pending");
if (!isEnabled()) return skipped("chain-sync-disabled");
if (!getWalletAddress()) return skipped("wallet-unavailable");
const module = await loadPlayChainModule();
if (typeof module.recordBlockPlacementOnChain !== "function") return skipped("record-placement-unavailable");
const renderType = module.renderTypeForBlockId?.(pending.blockId) ?? null;
const result = await module.recordBlockPlacementOnChain(blockFromPending(pending), renderType, pending.hotbarSlotIndex ?? 0);
return normalizeChainResult(result);
}
This repository adapter source resolves a module and then calls recordBlockPlacementOnChain only if that loaded module exports it. It is not the retained live served adapter bytes; direct /play/play-chain-adapter.js returned HTML, and actual behavior depends on the resolved asset or trusted override.
- Lines 1–4
Require a pending placement, enabled adapter, and connected wallet.
- Lines 5–8
Load the runtime-resolved module and call its ordinary placement method; this repository source tree is not retained live served bytes.
- Lines 9–10
Normalize the non-submitted disabled result for the Play controller.
Logout attempts all matching runtime prefixes
JavaScriptplay/play-auth-session.jsexport function clearWalletSession(storage = globalThis.localStorage) {
for (const key of Object.values(walletSessionKeys)) remove(storage, key);
const keys = [];
try {
for (let index = 0; index < storage.length; index += 1) {
const key = storage.key(index);
if (key) keys.push(key);
}
} catch {
return;
}
for (const key of keys) {
if (runtimeCachePrefixes.some((prefix) => key.startsWith(prefix))) remove(storage, key);
}
}
The function does not filter runtime caches to the connected owner. It attempts every matching removal, but enumeration or per-key errors can prevent deletion, so this is best-effort cleanup rather than a success guarantee.
- Lines 1–2
Attempt removal of the four wallet-identity fields from this browser origin.
- Lines 3–11
Try to enumerate localStorage keys; an exception returns before the runtime-prefix pass.
- Lines 12–15
Attempt each matching prefix removal regardless of owner suffix; this range does not verify success.
Play logout performs local cleanup and best-effort plugin disconnect
JavaScriptplay/play-chain-session.js function disconnectWallet() {
const provider = detectWalletProvider();
try {
if (provider?.disconnect) {
Promise.resolve(provider.disconnect()).catch(() => {
// The local session is already cleared; plugin disconnect is best effort.
});
}
} catch {
// Some wallet plugins throw when already disconnected.
}
clearWalletSession();
state.walletAddress = "";
state.sessionLamports = getStoredSessionLamports("");
resetWalletBalance();
dispatchWalletChanged("");
This handler attempts provider disconnect and browser cleanup on a best-effort basis, then resets in-memory presentation. It contains no System transfer, PlayerSession close, revoke, or refund transaction and does not verify storage deletion.
- Lines 1–10
Ask the wallet provider to disconnect but tolerate rejection or an already-disconnected state.
- Lines 11–15
Call best-effort local cleanup, then reset in-memory wallet and balance presentation.
- Line 16
Notify Play that the visible wallet address is now empty.
Local Game Wallet stores a persistent Base58 secret
JavaScriptsrc/localGameWallet.jsfunction storeLocalGameWalletKeypair(keypair, source = "created") {
const address = keypair.publicKey.toBase58();
const secretKey = bs58.encode(keypair.secretKey);
const createdAt = String(Date.now());
if (!hasLocalStorage()) throw new Error("Browser local storage is unavailable.");
localStorage.setItem(localGameWalletKeys.address, address);
localStorage.setItem(localGameWalletKeys.secretKey, secretKey);
The persistent owner's private bytes are turned into a plain Base58 string and stored under Local Game Wallet keys, separate from plugin-session cleanup.
- Lines 1–4
Prepare the public address, Base58 secret text, and creation time.
- Lines 5–7
Require localStorage and save both public address and unencrypted secret string.
Guardian HELLO contains a wallet hint and random nonce
JavaScriptplay/play-guardian-client.js encodeHello(position) {
const chunk = this.worldToChunk(position?.x ?? 0, position?.z ?? 0);
const bytes = new Uint8Array(HELLO_SIZE);
const view = new DataView(bytes.buffer);
view.setUint8(0, MSG_HELLO);
view.setUint8(1, PROTOCOL_VERSION);
view.setUint16(2, 0, true);
this.writeWalletHint(bytes, 4);
view.setInt32(36, chunk.x, true);
view.setInt32(40, chunk.z, true);
const nonce = randomNonceWords();
view.setUint32(44, nonce.low, true);
view.setUint32(48, nonce.high, true);
return bytes.buffer;
}
The realtime greeting writes a public wallet hint, chunk coordinates, and nonce. It does not insert PlayerSession, session authority, private bytes, or wallet signature.
- Lines 1–7
Allocate the HELLO frame and write its message and protocol header.
- Lines 8–10
Write the wallet public-key hint and current chunk coordinates.
- Lines 11–15
Add random nonce words and return the unsigned message bytes.
Guardian runtime receives walletAddress rather than PlayerSession
JavaScriptplay/play-guardian.js function createGuardianClient(wallet) {
return createNiceChunkGuardianClient({
walletAddress: wallet,
url: state.guardianUrl,
playerName: String(getPlayerName?.() || "Local Miner"),
chunkSize: chunks?.chunkSize || 16,
onReady: (info) => {
state.connected = true;
The runtime constructs its separate realtime client from the wallet public address and world presentation data, not from a PlayerSession input.
- Lines 1–3
Create the Guardian client using the current wallet address as an identity hint.
- Lines 4–8
Provide endpoint and presentation context, then register an independent onReady callback.
Guardian Program dispatch has its own lifecycle
Rustprograms/nicechunk_guardian/src/lib.rs match tag {
0 => initialize_registry(program_id, accounts),
1 => register_genesis_guardian(program_id, accounts, payload),
2 => register_guardian(program_id, accounts, payload),
5 => update_guardian_endpoint(program_id, accounts, payload),
6 => update_guardian_blueprint(program_id, accounts, payload),
8 => update_guardian_operator(program_id, accounts, payload),
_ => Err(NicechunkGuardianError::InvalidInstruction.into()),
}
The current Guardian dispatch manages registry, enrollment, endpoint, blueprint, and operator state. The full audited contract contains no PlayerSession reference or session lifecycle handler.
- Lines 1–7
Map Guardian tags only to Guardian registry and region-management handlers.
- Lines 8–9
Reject unsupported tags rather than accepting a Session revoke operation.
Offline audit proves ordinary transfer signing without a network
JavaScriptscripts/derive-player-sessions-audit.mjs const instruction = SystemProgram.transfer({
fromPubkey: sessionAuthority.publicKey,
toPubkey: recipient,
lamports: 1,
});
const transaction = new Transaction({
feePayer: sessionAuthority.publicKey,
recentBlockhash: Keypair.generate().publicKey.toBase58(),
}).add(instruction);
transaction.sign(sessionAuthority);
const playerSessionReferenced = instruction.keys.some(({ pubkey }) => pubkey.equals(playerSession));
const playerProgramInvoked = instruction.programId.equals(new PublicKey(playerProgramId));
const source = instruction.keys[0];
const signaturePresent = transaction.signatures.length === 1 && transaction.signatures[0].signature !== null;
const signatureValid = transaction.verifySignatures();
assert(instruction.programId.equals(SystemProgram.programId), "Offline transfer did not target System Program.");
assert(source.pubkey.equals(sessionAuthority.publicKey), "Offline transfer source differs from the session authority.");
assert(source.isSigner && source.isWritable, "Offline transfer source is not a writable signer.");
assert(instruction.keys.length === 2, "System transfer account list drifted from two keys.");
assert(!playerSessionReferenced, "Offline System transfer unexpectedly references PlayerSession.");
assert(!playerProgramInvoked, "Offline System transfer unexpectedly invokes Player Program.");
assert(signaturePresent && signatureValid, "Temporary session Keypair could not sign an ordinary System transfer offline.");
Fresh test keys sign one unbroadcast lamport transfer entirely offline. Assertions confirm System Program, a writable signer, no Session account, no Player Program, and a valid signature.
- Lines 1–10
Construct a one-lamport System transfer and sign it with a generated session-authority Keypair.
- Lines 11–15
Measure Session references, invoked program, signer meta, and signature validity.
- Lines 16–22
Fail the audit if the ordinary transfer starts depending on PlayerSession or cannot verify.
The retained derived report records the transfer boundary
JSONdocs/audits/player-sessions-derived-2026-07-18.json "systemTransferProbe": {
"networkRequests": 0,
"broadcasts": 0,
"lamports": 1,
"instructionProgramId": "11111111111111111111111111111111",
"instructionAccountCount": 2,
"sourceIsSigner": true,
"sourceIsWritable": true,
"playerSessionAccountReferenced": false,
"playerProgramInvoked": false,
"signaturePresent": true,
"signatureValid": true,
"privateKeyOutput": false
},
The saved output exposes the verifiable result without retaining private key bytes and without claiming a broadcast or real fund movement.
- Lines 1–6
Record zero network activity and the native System Program instruction shape.
- Lines 7–10
Record signer requirements and absence of PlayerSession and Player Program.
- Lines 11–13
Record signature presence, successful verification, and no private-key output.
Devnet check mode protects the snapshot privacy boundary
JavaScriptscripts/capture-player-sessions-devnet.mjs assert(report.privacyBoundary.sessionSigningSecretBytesRetained === false, "Privacy boundary drifted.");
assert(report.privacyBoundary.walletFilesRead === false, "Wallet-file boundary drifted.");
assert(report.privacyBoundary.browserStorageRead === false, "Browser-storage boundary drifted.");
assert(report.reproduction.checkModeUsesNetwork === false, "Local check unexpectedly claims network use.");
assertNoSecretFields(report);
Offline report checking explicitly rejects retained session secrets, wallet-file reads, browser-storage reads, network use, or secret-shaped report fields.
- Lines 1–3
Require that no session secret, wallet file, or browser storage entered the capture.
- Lines 4–5
Keep check mode offline and scan the retained report for forbidden secret fields.
One retained Devnet summary shows expired accounts can remain
JSONdocs/audits/player-sessions-devnet-2026-07-18.json "summary": {
"retainedPlayerSessions": 10,
"currentAtContextBlockTime": 0,
"expiredAtContextBlockTime": 10,
"uniqueOwners": 8,
"uniqueSessionAuthorities": 10,
"ownerEqualsAuthoritySessions": 5,
"distinctAuthoritySessions": 5,
"ownersWithMultipleSessions": 1,
"maximumSessionsForOneOwner": 3,
"sessionsForOwnersWithMultipleSessions": 3,
"canonicalSessionPdas": 10,
At confirmed context slot 477236696, all ten retained public Session records were expired, and one owner had three records. Presence is not current authorization.
- Lines 1–4
Count ten retained Sessions, zero current records, and ten expired records at context time.
- Lines 5–8
Count eight owners and split equal-key versus distinct-authority arrangements.
- Lines 9–12
Record multi-Session coexistence and canonical-PDA observations in this one snapshot.
The SDK tag-four builder keeps PlayerProfile read-only
TypeScriptsdk/nicechunk-player.ts const [globalConfig] = deriveGlobalConfigPda(coreProgramId);
const data = Buffer.alloc(15);
data.writeUInt8(4, 0);
data.writeBigInt64LE(BigInt(expiresAt), 1);
data.writeUInt16LE(allowedActions, 9);
data.writeUInt32LE(maxActions, 11);
return new TransactionInstruction({
programId: playerProgramId,
keys: [
{ pubkey: owner, isSigner: true, isWritable: true },
{ pubkey: sessionAuthority, isSigner: true, isWritable: false },
{ pubkey: playerProfile, isSigner: false, isWritable: false },
{ pubkey: playerSession, isSigner: false, isWritable: true },
{ pubkey: globalConfig, isSigner: false, isWritable: false },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
],
data,
});
The independent SDK writes tag 4 and marks PlayerProfile read-only while PlayerSession is writable. The handler accepts an already valid final-layout Profile without mutation and rejects an invalid allocated one; this builder does not grant an in-place migration path.
- Lines 1–6
Derive GlobalConfig and serialize the fifteen-byte tag-four instruction data.
- Lines 7–16
Declare both signer roles, a read-only PlayerProfile, a writable Session, and the remaining read-only accounts.
- Lines 17–18
Attach the serialized data and finish the SDK instruction object.
Repository tag-four builder also keeps PlayerProfile read-only
JavaScriptsrc/chain/nicechunkChain.js const data = Buffer.alloc(15);
data.writeUInt8(4, 0);
data.writeBigInt64LE(BigInt(expiresAt), 1);
data.writeUInt16LE(sessionAllowedActions, 9);
data.writeUInt32LE(sessionMaxActions, 11);
return new TransactionInstruction({
programId: playerProgramId,
keys: [
{ pubkey: owner, isSigner: true, isWritable: true },
{ pubkey: sessionAuthority, isSigner: true, isWritable: false },
{ pubkey: playerProfile, isSigner: false, isWritable: false },
{ pubkey: playerSession, isSigner: false, isWritable: true },
{ pubkey: deriveGlobalConfigPda(), isSigner: false, isWritable: false },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
],
data,
});
This repository source-tree builder repeats the SDK's read-only PlayerProfile and writable PlayerSession metas. It is not retained live served bytes; the production snapshot separately pins the fetched chain candidate.
- Lines 1–5
Serialize repository defaults into tag-four data; this source tree is not retained live served bytes.
- Lines 7–16
Request owner and authority signers, keep Profile read-only, and make only PlayerSession writable for setup.
- Lines 17–18
Finish this repository instruction object without proving what live bytes a browser loaded.
Player Profile validation failure is rejected instead of migrated
Rustprograms/nicechunk_player/src/lib.rsfn ensure_player_profile_current<'a>(
payer: &AccountInfo<'a>,
player_profile: &AccountInfo<'a>,
global_config: &AccountInfo<'a>,
system_program_account: &AccountInfo<'a>,
program_id: &Pubkey,
owner: &Pubkey,
bump: u8,
global_config_view: &GlobalConfigView,
clock: &Clock,
) -> ProgramResult {
{
let data = player_profile.try_borrow_data()?;
if PlayerProfile::validate_owner_and_config(&data, owner, global_config.key).is_ok() {
return Ok(());
}
}
if player_profile.owner != &system_program::ID || player_profile.data_len() != 0 {
return Err(NicechunkPlayerError::InvalidSystemAccount.into());
}
create_or_allocate_player_profile_pda(
payer,
player_profile,
system_program_account,
program_id,
owner,
bump,
)?;
let mut data = player_profile.try_borrow_mut_data()?;
PlayerProfile::pack_default(
&mut data,
bump,
owner,
global_config.key,
global_config_view.world_id,
"",
clock.slot,
clock.unix_timestamp,
)
}
A valid final-layout Profile returns immediately. Any existing invalid account is rejected. Only an empty System-owned PDA can be allocated, so Session refresh cannot silently resize or migrate old layouts.
- Lines 1–14
Borrow the supplied Profile and return only when final-layout validation confirms owner, GlobalConfig, version, initialized state, and slot-count invariants.
- Lines 16–18
Reject every invalid account that is already allocated or owned by a program instead of attempting an in-place migration.
- Lines 19–37
Allocate only a missing System-owned PDA and pack a new default Profile with the current canonical layout.
Unified Game namespace one delegates to Backpack
Rustprograms/nicechunk_game/src/lib.rsconst NS_BACKPACK: u8 = 1;
const NS_CHUNK: u8 = 2;
const NS_SMELTING: u8 = 3;
const NS_MARKET: u8 = 4;
pub fn process_instruction(
program_id: &solana_program::pubkey::Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let (tag, payload) = instruction_data
.split_first()
.ok_or(NicechunkGameError::InvalidInstruction)?;
match *tag {
NS_BACKPACK => nicechunk_backpack::process_instruction(program_id, accounts, payload),
NS_CHUNK => nicechunk_chunk::process_instruction(program_id, accounts, payload),
NS_SMELTING => nicechunk_smelting::process_instruction(program_id, accounts, payload),
NS_MARKET => nicechunk_market::process_instruction(program_id, accounts, payload),
_ => Err(NicechunkGameError::InvalidInstruction.into()),
}
}
The unified Game Program treats the first byte as a namespace. Namespace 1 removes that outer byte and passes the remaining inner tag and payload to the shared Backpack instruction handler.
- Lines 1–4
Assign namespace byte one to Backpack and distinct values to the other game families.
- Lines 6–13
Split the first byte from the remaining payload and reject empty instruction data.
- Lines 15–22
Delegate namespace one to nicechunk_backpack and reject unknown namespaces.
Backpack's two-account branch is owner-direct
Rustprograms/nicechunk_backpack/src/lib.rs if payload.len() != 1 || (accounts.len() != 2 && accounts.len() != 4) {
return Err(NicechunkBackpackError::InvalidInstruction.into());
}
let index = payload[0];
let account_info_iter = &mut accounts.iter();
if accounts.len() == 2 {
let owner = next_account_info(account_info_iter)?;
let backpack = next_account_info(account_info_iter)?;
if !owner.is_signer || !owner.is_writable {
return Err(NicechunkBackpackError::InvalidPayer.into());
}
if !backpack.is_writable {
return Err(NicechunkBackpackError::InvalidWritableAccount.into());
}
require_key_eq(
backpack.owner,
program_id,
NicechunkBackpackError::InvalidBackpackOwner,
)?;
validate_existing_backpack_pda(program_id, backpack, owner.key)?;
let clock = Clock::get()?;
let mut backpack_data = backpack.try_borrow_mut_data()?;
return BackpackAccount::remove_resource_at(
&mut backpack_data,
owner.key,
index,
clock.slot,
);
}
The handler explicitly accepts either two or four accounts. With two, it requires the owner signer and writable Backpack and returns after direct dense-index removal without reading PlayerSession.
- Lines 1–6
Accept exactly the single-index payload and either the two-account or four-account shape.
- Lines 8–23
In the two-account shape, validate the owner signer and canonical writable Backpack.
- Lines 25–33
Mutate the owner's Backpack directly and return before any Session-aware branch.
Backpack's four-account branch validates a Session
Rustprograms/nicechunk_backpack/src/lib.rs let session_authority = next_account_info(account_info_iter)?;
let player_profile = next_account_info(account_info_iter)?;
let player_session = next_account_info(account_info_iter)?;
let backpack = next_account_info(account_info_iter)?;
if !session_authority.is_signer {
return Err(NicechunkBackpackError::InvalidSessionAuthority.into());
}
if !backpack.is_writable {
return Err(NicechunkBackpackError::InvalidWritableAccount.into());
}
require_key_eq(
backpack.owner,
program_id,
NicechunkBackpackError::InvalidBackpackOwner,
)?;
require_key_eq(
player_profile.owner,
&NICECHUNK_PLAYER_PROGRAM_ID,
NicechunkBackpackError::InvalidPlayerProgram,
)?;
require_key_eq(
player_session.owner,
&NICECHUNK_PLAYER_PROGRAM_ID,
NicechunkBackpackError::InvalidPlayerProgram,
)?;
let clock = Clock::get()?;
let player_session_data = player_session.try_borrow_data()?;
let session = PlayerSessionView::validate(
&player_session_data,
session_authority.key,
player_profile.key,
SESSION_ACTION_BREAK_BLOCK,
clock.unix_timestamp,
)?;
After the two-account early return, the remaining four accounts are authority, Profile, Session, and Backpack. This branch checks the authority signature, Player Program ownership, chain time, and BREAK_BLOCK permission before later removal.
- Lines 1–4
Consume the four Session-aware account roles in their contract order.
- Lines 6–26
Require the authority signature, writable Backpack, and expected owning programs.
- Lines 28–36
Read chain time and ask PlayerSessionView for BREAK_BLOCK authorization.
Repository mask and maximum defaults are separate constants
JavaScriptsrc/chain/nicechunkChain.jsconst sessionAllowedActions = (1 << 1) | (1 << 2);
const sessionMaxActions = 10_000;
These adjacent repository source-tree constants combine BREAK_BLOCK and PLACE_BLOCK into mask 6 and request maxActions 10,000. They are not retained live served bytes and do not prove either field is enforced by consumers.
- Line 1
Combine bit indexes one and two into repository mask value six.
- Line 2
Choose the stored maximum of ten thousand in source, not an enforced live-byte limit.
Repository wallet helper waits for confirmed before returning
JavaScriptsrc/chain/nicechunkChain.jsasync function signAndSendWalletTransaction(provider, transaction, conn = getNicechunkConnection(), extraSigners = []) {
transaction.feePayer = provider.publicKey;
if (typeof conn.prepareTransaction === "function") {
await conn.prepareTransaction(transaction, { commitment: "confirmed" });
} else {
const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash("confirmed");
transaction.recentBlockhash = blockhash;
transaction.lastValidBlockHeight = lastValidBlockHeight;
}
for (const signer of extraSigners) transaction.partialSign(signer);
if (typeof provider.signTransaction === "function") {
const signed = await provider.signTransaction(transaction);
const signature = await sendRawTransactionWithLogs(conn, signed.serialize(), "wallet");
await confirmTransactionByHttpPolling(conn, {
signature,
blockhash: transaction.recentBlockhash,
lastValidBlockHeight: transaction.lastValidBlockHeight,
}, "confirmed");
return signature;
}
if (typeof provider.signAndSendTransaction !== "function") {
throw new Error("Wallet does not support transaction signing.");
}
const result = await provider.signAndSendTransaction(transaction);
const signature = typeof result === "string" ? result : result?.signature;
if (!signature) throw new Error("Wallet did not return a transaction signature.");
await confirmTransactionByHttpPolling(conn, {
signature,
blockhash: transaction.recentBlockhash,
lastValidBlockHeight: transaction.lastValidBlockHeight,
}, "confirmed");
return signature;
}
This repository source-tree helper broadcasts through one of two wallet APIs and awaits HTTP polling at confirmed commitment before it returns. It is not retained live served bytes and does not make the following browser storage write part of the chain transaction.
- Lines 1–12
Prepare the repository transaction and apply any extra signer signatures; these are not retained live served bytes.
- Lines 14–23
With signTransaction, broadcast serialized bytes and wait for confirmed polling before return.
- Lines 25–37
With signAndSendTransaction, require a signature and likewise await confirmed polling before return.
Repository setup stores the secret only after the wallet helper returns
JavaScriptsrc/chain/nicechunkChain.js await signAndSendWalletTransaction(provider, tx, conn, [keypair]);
const session = { keypair, expiresAt };
storeGameplaySession(owner, session);
markGameplaySessionReady(owner, keypair.publicKey, targetLamports);
In this repository source tree, setup awaits the broadcasting-and-confirming wallet helper before calling storeGameplaySession. It is not retained live served bytes, and the later localStorage write is a second, non-atomic commit that can still fail.
- Line 1
Await the repository wallet helper's confirmed return; this source is not retained live served bytes.
- Lines 2–3
Only afterward assemble the local record and ask localStorage to persist the secret.
- Line 4
Mark the in-memory ready cache after the separate persistence call completes.
Repository ready cache has a five-minute TTL
JavaScriptsrc/chain/nicechunkChain.jsconst chunkDeltaCacheTtlMs = 60_000;
const gameplaySessionStatusCacheTtlMs = 60_000;
const gameplaySessionReadyCacheTtlMs = 5 * 60_000;
const resourceDropTableCacheTtlMs = 10 * 60_000;
const chunkDeltaCache = new Map();
const initializedChunkBrokenCache = new Set();
const gameplaySessionStatusCache = new Map();
const gameplaySessionReadyCache = new Map();
The repository source-tree ready-cache TTL is five minutes and has its own Map. It is not retained live served bytes; the duration limits cache age, not Session record expiry or possible synthetic-time overstatement.
- Lines 1–4
Declare separate repository cache lifetimes, including five minutes for gameplay readiness.
- Lines 5–8
Allocate the independent cache collections; these declarations are not retained live served bytes.
Local Game Wallet cache hit returns synthetic now plus duration before RPC
JavaScriptsrc/chain/nicechunkChain.js const nowSeconds = Math.floor(Date.now() / 1000);
const expiresAt = nowSeconds + sessionDurationSeconds;
if (isGameplaySessionReadyCached(owner, owner)) {
return { keypair, expiresAt, localGameWallet: true };
}
const conn = getNicechunkConnection();
const [playerProfile] = derivePlayerProfilePda(owner);
const [playerSession] = derivePlayerSessionPda(owner, owner);
This repository source-tree Local Game Wallet branch computes now plus the eight-hour sessionDurationSeconds and returns it on a ready-cache hit before creating the connection used for RPC reads. It is not retained live served bytes, and this expiresAt is synthetic rather than the confirmed record value.
- Lines 1–2
Compute a fresh repository-local now-plus-eight-hours candidate, not an on-chain expiry read.
- Lines 3–5
Return that synthetic expiry immediately when the five-minute ready cache still matches.
- Lines 7–9
Only a cache miss reaches connection and PDA setup for later RPC checks; this source is not retained live served bytes.
Per-key logout removal swallows storage exceptions
JavaScriptplay/play-auth-session.jsfunction remove(storage, key) {
try {
storage?.removeItem?.(key);
} catch {
// Logout still redirects even when browser storage is unavailable.
}
}
Each identity or prefix deletion uses this helper. A removeItem exception is caught and ignored, so the caller continues without proving that the browser actually deleted that key.
- Lines 1–3
Attempt one optional removeItem call for the supplied storage key.
- Lines 4–7
Swallow an exception so logout can continue and redirect without a deletion guarantee.
Devnet capture classifies expiry with context-slot block time
JavaScriptscripts/capture-player-sessions-devnet.mjs const [blockTimeUnix, rentMinimumLamports, playerProgram] = await Promise.all([
connection.getBlockTime(snapshot.contextSlot),
connection.getMinimumBalanceForRentExemption(lengths.playerSession, commitment),
inspectUpgradeableProgram(connection, ids.playerProgram, snapshot.contextSlot),
]);
if (blockTimeUnix === null) {
throw new Error(`No block time is available for confirmed context slot ${snapshot.contextSlot}.`);
}
The capture requests RPC getBlockTime for the shared confirmed context slot and refuses to classify records without it. This is not a direct read of the Clock sysvar that on-chain consumers use during execution.
- Lines 1–5
Read the context-slot block time alongside rent and supplemental Player deployment information.
- Lines 6–8
Stop the capture if RPC cannot supply a timestamp for the confirmed context slot.
Devnet report records shared and supplemental contexts separately
JavaScriptscripts/capture-player-sessions-devnet.mjs capture: {
endpoint: redactEndpoint(endpoint),
endpointSource: process.env.NICECHUNK_DEVNET_RPC ? "NICECHUNK_DEVNET_RPC" : "runtime-public-default",
endpointFingerprintSha256: sha256(Buffer.from(redactEndpoint(endpoint), "utf8")),
endpointCredentialMaterialRetained: false,
commitment,
rpcVersion,
genesisHash,
minimumRequestedSlot: snapshot.minimumSlot,
programAccountsContextSlot: snapshot.contextSlot,
allProgramAccountQueriesShareOneContext: true,
contextSlots: snapshot.contexts,
contextBlockTimeUnix: blockTimeUnix,
contextBlockTime: new Date(blockTimeUnix * 1_000).toISOString(),
sharedContextAttempt: snapshot.attempt,
supplementalAccountContextSlots: {
playerProgram: playerProgram.contextSlot,
playerProgramData: playerProgram.programData.contextSlot,
},
maximumSupplementalContextSlotDelta: Math.max(
playerProgram.contextSlot,
playerProgram.programData.contextSlot,
) - snapshot.contextSlot,
The script records the common program-account slot separately from Player Program and ProgramData supplemental slots. In the retained report those latter contexts are +1 and +2, so their hashes are nearby deployment observations rather than the exact shared Session/Profile/Config context.
- Lines 1–10
Retain redacted endpoint metadata, commitment, and the shared program-account context slot.
- Lines 11–15
Record that program-account queries share one context and attach its RPC block time.
- Lines 16–23
Keep Player Program and ProgramData supplemental slots and their maximum delta distinct.
Retained production report states its date and static privacy boundary
JSONdocs/audits/player-sessions-production-2026-07-18.json "schemaVersion": 1,
"kind": "nicechunk-player-sessions-production-runtime-snapshot",
"auditDate": "2026-07-18",
"snapshotDate": "2026-07-19",
"captureStartedAt": "2026-07-19T00:44:27.337Z",
"capturedAt": "2026-07-19T00:44:32.257Z",
"productionOrigin": "https://nicechunk.com",
"scope": {
"statement": "A byte-pinned snapshot of the public NiceChunk production entry, runtime bundle, and runtime-resolved chain-module candidates.",
"productionSnapshotNotPermanentFact": true,
"repositorySourceUsedAsProductionEvidence": false,
"rawResponseBodiesRetained": false,
"retainedInstead": "Full response SHA-256/length/status metadata plus byte offsets and hashed short literal contexts.",
"dateSemantics": "auditDate names the documentation audit batch and output filename; snapshotDate is the real UTC date of the HTTPS observation."
},
"privacyAndNetworkBoundary": {
"transport": "TLS HTTPS only",
"requestMethod": "GET",
"fixedPublicHost": "nicechunk.com",
"requestsMade": 10,
"requestBodiesSent": 0,
"authorizationHeadersSent": false,
"cookiesSent": false,
"queryCredentialsSent": false,
"browserStorageRead": false,
"walletKeyMaterialRead": false,
"rpcMethodsCalled": [],
"transactionsConstructed": 0,
"transactionsSent": 0,
"note": "Static JavaScript contains storage-key and secretKey field names; this capture reads code bytes only and never reads any player's stored values."
},
This retained artifact calls 2026-07-18 the documentation audit batch and 2026-07-19 the actual UTC HTTPS observation date. It describes a fixed public GET byte audit—not a browser HAR—and says no browser storage, wallet keys, RPC accounts, or player overrides were read.
- Lines 1–15
Separate auditDate from snapshotDate and state that the point-in-time byte snapshot is not permanent.
- Lines 16–24
Record TLS GET transport without bodies, authorization, cookies, or query credentials.
- Lines 25–31
Record zero browser-storage, key, RPC, and transaction access; only static code bytes were inspected.
Retained production entry binds served path to body hash
JSONdocs/audits/player-sessions-production-2026-07-18.json {
"id": "playIndex",
"requestUrl": "https://nicechunk.com/play/index.html",
"finalUrl": "https://nicechunk.com/play/index.html",
"redirected": false,
"status": 200,
"ok": true,
"contentType": "text/html",
"contentEncoding": null,
"contentLengthHeader": 55636,
"bodyLength": 55636,
"contentLengthHeaderMatchesBody": true,
"sha256": "9caed4fdcbd50702fbae99068771ccf9ce108046f918251996bc2e85363ae48f",
"etag": "\"6a5ba988-d954\"",
"lastModified": "Sat, 18 Jul 2026 16:27:52 GMT",
"cacheControl": "no-store, no-cache, must-revalidate",
"hashBoundary": "sha256 covers the decoded HTTP entity body bytes in [0, bodyLength); Accept-Encoding was identity."
},
The retained live served entry tuple is path, 200 HTML status, body length, and SHA-256. The hash covers the decoded HTTP entity body, which makes this static response auditable without claiming browser execution.
- Lines 1–8
Identify the served production index URL, final URL, HTTP status, and content type.
- Lines 9–18
Bind its decoded body length and SHA-256 to the explicit identity-encoding hash boundary.
Retained runtime and default chain candidate have distinct byte pins
JSONdocs/audits/player-sessions-production-2026-07-18.json {
"id": "versionedRuntime",
"requestUrl": "https://nicechunk.com/runtime/play-bundle-de6020cca220427a/assets/index-BPPMv-5b.js",
"finalUrl": "https://nicechunk.com/runtime/play-bundle-de6020cca220427a/assets/index-BPPMv-5b.js",
"redirected": false,
"status": 200,
"ok": true,
"contentType": "application/javascript",
"contentEncoding": null,
"contentLengthHeader": 989679,
"bodyLength": 989679,
"contentLengthHeaderMatchesBody": true,
"sha256": "594a23c33a6bc37a73064f3631cf69bbd8f6a24e7c0168bf829b11bc0a9036c6",
"etag": "\"6a5ba404-f19ef\"",
"lastModified": "Sat, 18 Jul 2026 16:04:20 GMT",
"cacheControl": "max-age=31536000, public, max-age=31536000, immutable",
"hashBoundary": "sha256 covers the decoded HTTP entity body bytes in [0, bodyLength); Accept-Encoding was identity."
},
{
"id": "versionedChain",
"requestUrl": "https://nicechunk.com/assets/nicechunkChain.play-bundle-de6020cca220427a.js",
"finalUrl": "https://nicechunk.com/assets/nicechunkChain.play-bundle-de6020cca220427a.js",
"redirected": false,
"status": 200,
"ok": true,
"contentType": "application/javascript",
"contentEncoding": null,
"contentLengthHeader": 112446,
"bodyLength": 112446,
"contentLengthHeaderMatchesBody": true,
"sha256": "2fd4d2f3a36e67a97db59e8b6c8263fbeb2753e5eff6df3d8814f7a6a42ce9f4",
"etag": "\"6a5ba455-1b73e\"",
"lastModified": "Sat, 18 Jul 2026 16:05:41 GMT",
"cacheControl": "max-age=31536000, public, immutable",
"hashBoundary": "sha256 covers the decoded HTTP entity body bytes in [0, bodyLength); Accept-Encoding was identity."
},
The runtime bundle and separately fetched default chain candidate are different served resources with different hashes. Static fetching proves their observed bytes, but not which candidate an individual browser actually imported after override resolution.
- Lines 1–18
Bind the live versioned runtime URL, JavaScript response, length, and decoded-body hash.
- Lines 19–36
Bind the separate versioned default chain candidate URL, response metadata, and hash boundary.
Production capture checker pins entry, runtime, and chain roles
JavaScriptscripts/capture-player-sessions-production.mjsconst expectedBuildVersion = "play-bundle-de6020cca220427a";
const expectedRuntimePath = "/runtime/play-bundle-de6020cca220427a/assets/index-BPPMv-5b.js";
const expectedChainPath = "/assets/nicechunkChain.play-bundle-de6020cca220427a.js";
const expectedResources = Object.freeze({
playIndex: {
requestUrl: "https://nicechunk.com/play/index.html",
status: 200,
contentType: "text/html",
bodyLength: 55_636,
sha256: "9caed4fdcbd50702fbae99068771ccf9ce108046f918251996bc2e85363ae48f",
},
playMainFallback: {
requestUrl: "https://nicechunk.com/play/main.js",
status: 200,
contentType: "text/html",
bodyLength: 55_636,
sha256: "9caed4fdcbd50702fbae99068771ccf9ce108046f918251996bc2e85363ae48f",
},
playAdapterFallback: {
requestUrl: "https://nicechunk.com/play/play-chain-adapter.js",
status: 200,
contentType: "text/html",
bodyLength: 55_636,
sha256: "9caed4fdcbd50702fbae99068771ccf9ce108046f918251996bc2e85363ae48f",
},
versionedRuntime: {
requestUrl: `https://nicechunk.com${expectedRuntimePath}`,
status: 200,
contentType: "application/javascript",
bodyLength: 989_679,
sha256: "594a23c33a6bc37a73064f3631cf69bbd8f6a24e7c0168bf829b11bc0a9036c6",
},
versionedChain: {
requestUrl: `https://nicechunk.com${expectedChainPath}`,
status: 200,
contentType: "application/javascript",
bodyLength: 112_446,
sha256: "2fd4d2f3a36e67a97db59e8b6c8263fbeb2753e5eff6df3d8814f7a6a42ce9f4",
},
The capture checker assigns distinct expected resource IDs to the live entry, two HTML fallbacks, versioned runtime, and versioned chain candidate, then pins each path, response shape, length, and SHA-256.
- Lines 1–5
Pin the build label and distinct runtime and default chain candidate paths.
- Lines 6–25
Pin the served index and two direct repository-module URL HTML fallback responses.
- Lines 26–40
Pin the versioned runtime and separately fetched versioned chain candidate bytes.
Production capture records a static GET-only boundary
JavaScriptscripts/capture-player-sessions-production.mjs const unsignedReport = {
schemaVersion: 1,
kind: "nicechunk-player-sessions-production-runtime-snapshot",
auditDate,
snapshotDate: captureStartedAt.slice(0, 10),
captureStartedAt,
capturedAt: new Date().toISOString(),
productionOrigin,
scope: {
statement: "A byte-pinned snapshot of the public NiceChunk production entry, runtime bundle, and runtime-resolved chain-module candidates.",
productionSnapshotNotPermanentFact: true,
repositorySourceUsedAsProductionEvidence: false,
rawResponseBodiesRetained: false,
retainedInstead: "Full response SHA-256/length/status metadata plus byte offsets and hashed short literal contexts.",
dateSemantics: "auditDate names the documentation audit batch and output filename; snapshotDate is the real UTC date of the HTTPS observation.",
},
privacyAndNetworkBoundary: {
transport: "TLS HTTPS only",
requestMethod: "GET",
fixedPublicHost: "nicechunk.com",
requestsMade: resources.length,
requestBodiesSent: 0,
authorizationHeadersSent: false,
cookiesSent: false,
queryCredentialsSent: false,
browserStorageRead: false,
walletKeyMaterialRead: false,
rpcMethodsCalled: [],
transactionsConstructed: 0,
transactionsSent: 0,
note: "Static JavaScript contains storage-key and secretKey field names; this capture reads code bytes only and never reads any player's stored values.",
},
The capture script labels repository source as non-production evidence and records fixed-host TLS GETs with no browser storage, wallet key, RPC, or transaction access. This is a static byte snapshot, not a HAR, override read, or execution trace.
- Lines 1–16
Record the point-in-time byte scope, date semantics, and repository-versus-served evidence boundary.
- Lines 17–25
Record fixed-host TLS GET transport without request bodies, credentials, or cookies.
- Lines 26–32
Record no browser values, wallet keys, RPC calls, transaction construction, or sends.
IMPLEMENTATION EVIDENCE
Where these claims come from
Each claim is intentionally scoped to a concrete implementation path. These references are for verification, not decoration.
play/index.html
Loads ./main.js as the production module entry for /play/ and contains the current player-facing session-funding explanation.
play/main.js
Imports the modular Play chain adapter, wallet-session controller, player synchronization, placement, mining, and Guardian runtime used by the production route.
play/play-chain-adapter.js
Forwards ordinary placement to recordBlockPlacementOnChain and normalizes its current non-submission result.
play/play-chain-session.js
Implements the visible wallet lifecycle, funding presentation, best-effort provider disconnect, local cleanup call, and login redirect.
play/play-auth-session.js
Defines the four wallet identity keys, four runtime cache prefixes, and prefix-wide logout scan across all owner suffixes.
play/play-guardian-client.js
Encodes Guardian HELLO with a public wallet hint, chunk coordinates, and nonce but no PlayerSession or wallet signature.
play/play-guardian.js
Creates the Guardian realtime client from walletAddress and presentation context without accepting PlayerSession input.
src/chain/nicechunkChain.js
Implements current eight-hour duration, fifteen-minute skew, mask 6, stored maximum 10,000, 0.1 SOL policy, owner-scoped secret storage, reuse, setup, disabled placement, and ordinary Keypair signing.
src/localGameWallet.js
Stores the long-lived Local Game Wallet secret as Base58 in browser storage and provides the Keypair that fills owner and sessionAuthority roles.
sdk/nicechunk-player.ts
Publishes matching Session constants, PDA derivation, 15-byte tag-four instruction builder, signer metas, and the complete fixed-layout decoder.
programs/nicechunk_player/src/state.rs
Defines Session magic, version, seed, permission bits, 184-byte pack order, refresh mutation, counter reset, and stored relationship checks.
programs/nicechunk_player/src/lib.rs
Dispatches Session setup at tag 4 and equipment-durability consumption at tag 15; the latter validates the canonical Session PDA and BREAK_BLOCK before mutating PlayerEquipment, while no Session close, revoke, or deactivate handler exists.
programs/nicechunk_chunk/src/state.rs
Defines Chunk's selected-field PlayerSession view and validates authority, Profile, GlobalConfig, expiry, and action bit without reading six stored fields.
programs/nicechunk_chunk/src/lib.rs
Dispatches the five audited Session-aware mining and tree handlers and passes action index 1 through the shared read-only Session validation path.
programs/nicechunk_building/src/state.rs
Defines Building's selected-field PlayerSession view with the same authority, Profile, GlobalConfig, expiry, and action-bit checks.
programs/nicechunk_building/src/lib.rs
Dispatches Building tags 0–5 and 7 through the shared action-index-1 path, rejects tag 6, and uses tag 8 to publish Guardian blueprint state through a separate fixed-publisher CPI.
programs/nicechunk_backpack/src/state.rs
Defines Backpack's Session view plus dense single and batch removal that shift records without checking slot kind, type, or category.
programs/nicechunk_backpack/src/lib.rs
Dispatches session-aware remove_resource and remove_resources using BREAK_BLOCK and mutates the owner-backed Backpack while keeping PlayerSession read-only.
programs/nicechunk_game/src/lib.rs
Defines unified Game namespace byte one and delegates its remaining instruction payload to the shared NiceChunk Backpack handler.
programs/nicechunk_guardian/src/lib.rs
Implements Guardian registry, endpoint, blueprint, and operator handlers without referencing PlayerSession or providing its lifecycle operations.
scripts/derive-player-sessions-audit.mjs
Deterministically derives byte layout, setup defaults, selected consumer reads, the 15-handler matrix including Player durability, lifecycle boundaries, Guardian independence, and an offline ordinary System transfer proof.
docs/audits/player-sessions-derived-2026-07-18.json
Retains the zero-network derived results, source hashes, 15 read-only consumers, no implemented action ceiling, no PLACE consumer, no revoke path, and valid ordinary transfer signature.
scripts/capture-player-sessions-devnet.mjs
Captures read-only Devnet program accounts at a shared context and checks retained bytes, PDAs, distributions, privacy boundaries, and offline reproduction.
docs/audits/player-sessions-devnet-2026-07-18.json
Retains one confirmed Devnet snapshot at context slot 477236696 with ten canonical Session records for eight owners, all expired at that chain time.
scripts/capture-player-sessions-production.mjs
Captures fixed public NiceChunk HTTPS resources, hashes decoded response bodies, and checks the retained entry, runtime, fallback, candidate, and privacy boundaries offline.
docs/audits/player-sessions-production-2026-07-18.json
Retains the 2026-07-19 UTC static production observation inside the 2026-07-18 documentation audit batch with path, response, hash, literal, and non-execution boundaries.
public/mainnet.json
Configures the current runtime chain cluster as Devnet and records the Player, Chunk, Building, Backpack, and Core program IDs used by the capture.
programs/nicechunk_player/src/cluster_config.rs
Pins the Player Program's expected Core and Backpack program IDs used in owner and configuration validation.
programs/nicechunk_core/src/state.rs
Defines the canonical GlobalConfig layout and world fields that setup reads before packing worldId and GlobalConfig into PlayerSession.
src/rpcConfig.js
Resolves the runtime Solana cluster and endpoint configuration used by the production chain client and retained Devnet capture.
play/tests/play-auth-session.test.mjs
Exercises wallet logout cleanup behavior, including removal of scoped runtime caches while preserving unrelated browser storage.
tests/nicechunk_player_chunk.ts
Checks PlayerSession PDA derivation, serialized mask and expiry, account ordering, and session-aware Chunk mining instruction behavior.
play/placement-controller.js
Coordinates local placement prediction, chain-adapter submission, confirmation, and rollback around the currently disabled chain placement result.
play/mining-controller.js
Coordinates the player-facing mining action that can eventually call one of the session-aware Chunk consumer paths.
play/play-chain-backpack.js
Synchronizes the owner-backed Backpack visible to Play, whose dense records are changed by the audited Backpack removal handlers.
play/tests/play-chain-adapter.test.mjs
Exercises production adapter normalization for submitted and skipped chain-action results, including placement delegation.