DERIVED OWNERSHIP, PUBLIC BYTES
One owner can derive several backpacks, and every account remains publicly readable
A version-4 Backpack address is derived from the literal backpack seed, the owner's public key, and one little-endian 64-bit backpack ID. Initialization also requires the owner's PlayerProfile, then stores the owner and ID in the 128-byte header. Every later Backpack write validates the stored owner and re-derives the account address, so substituting another address or owner fails even when its bytes look structurally valid.
That ownership boundary controls mutation, not observation. Solana account bytes are public and unencrypted. A reader with the address can fetch owner, capacity, item count, update slot, packed block coordinates and identity, item codes and PDAs, quantity, volume, durability, grade, quality, and metadata without an owner signature. Separating inventory from PlayerProfile keeps the profile smaller and lets clients load the account on demand; it does not create private storage.
PlayerProfile exposes one equippedBackpack public key. Setting it requires the owner signature and a validated owner-derived Backpack PDA, but the value is a replaceable client default. Mining, Backpack removal, Market, Smelting, and forge paths validate the Backpack explicitly supplied to that instruction; they do not require it to equal the equipped pointer.
Changing the ID derives another address for the same owner.
No wallet signature, encryption key, or read ACL is part of the format.
Consumers authorize the Backpack account passed to them rather than comparing this field.
8048 BYTES, TWO SLOT KINDS
Fifty active slots each hold one item or up to ninety-nine matching block resources
The account always allocates a 128-byte header followed by ninety-nine 80-byte records. Capacity and itemCount are one-byte header fields, and validation requires itemCount not to exceed capacity. The general layout accepts capacity one through ninety-nine, but initialize clamps the requested value to one through fifty. No current instruction expands an initialized account, so ninety-nine is a format reserve rather than a capacity a new player can request today.
Header state is currently carried, the mass-valid flag is required, and the placed X, Y, and Z fields initialize to zero. The Backpack Program has no instruction that updates those placement fields or changes the carried state. Their presence in the layout must not be presented as a deployed placeable-backpack feature.
A block record stores kind one, quantity from one through ninety-nine, one representative world X/Z pair, Packed-Y block identity, aggregate volume, aggregate mass, and metadata. New matching block rewards fill the same record, then open another record after ninety-nine. An item record stores kind two plus category, itemCode, itemId, itemPda, producer-defined quantity, volume, durability, grade, level, quality, and metadata. Smelting can partially consume a quantity-bearing input; generic discard, Market listing, and forge selection still operate on complete selected records.
The only initialize path clamps every new capacity request to at most fifty active records.
All accounts allocate the fixed version-4 8,048-byte length without extra stack rent.
Block ID, category, flags, and metadata must match. Aggregate volume and mass are added during the merge; they do not have to be equal beforehand.
PROGRAM-DERIVED APPEND AUTHORITY
Records enter through trusted producer PDAs, and capacity failure depends on the path
The owner does not have a generic sign-and-append instruction. Mined block records arrive from a ChunkBroken PDA that signs a cross-program invocation. The Backpack Program checks that signer is owned by the configured Chunk Program, re-derives it from global config and chunk coordinates, validates its magic, version, and bump, binds PlayerProfile to the same global config, and finally validates the destination owner-derived Backpack PDA.
Market and Smelting item appends use the same pattern with narrower authority accounts. The signer must equal the Market Program's market-authority PDA or the Smelting Program's smelting-authority PDA. Backpack validates the incoming slot envelope and destination ownership, while the producer program remains responsible for sale or recipe rules. Exact PDA plus signer status is the trust boundary: an ordinary wallet cannot sign as those program-derived addresses.
The required base block from ordinary tag 8 mining and a Market-returned block stack first use compatible block headroom; they return BackpackFull only when the matching stacks and all active slots are full. Non-block Market and Smelting outputs require a free active slot. A failed strict cross-program append rolls back its Solana transaction. Ordinary tag 8 secondaries are lossy and keep decoration before the exploration extra. Tree tag 9 groups its trunk and every-fifth-leaf candidates by Chunk, while tags 20 and 21 combine their bounded base and secondary candidates; all three multi-block paths send those complete candidate lists through the ordered lossy batch append, so accepted geometry and XP do not guarantee matching stored resource counts.
Bound to global config and the chunk containing every supplied reward coordinate.
Only their owning programs can invoke Backpack with the required PDA signature.
A successful tree, batch, or range transaction can omit even a base-resource candidate that did not fit.
TRANSIENT INDEXES, BROAD DELETE BIT
Removal shifts every later slot, and a break-block session can delete any record kind
Active records occupy the dense prefix from zero through itemCount minus one. Single removal copies the complete trailing byte range left by eighty bytes, clears the old last record, decrements itemCount, and updates the slot timestamp. Batch removal first rejects duplicates and out-of-range indexes, then deletes from high to low so every submitted index still refers to the original snapshot.
The direct path accepts the owner signature. The session path instead checks a signer against a live PlayerSession, binds that session to PlayerProfile, requires expiry in the future, requires the break-block action bit, derives the owner from the session, and validates the Backpack PDA for that owner. The deletion routine sees only an index after authorization; it does not restrict kind or category. A session allowed to break blocks can therefore discard block, material, or forged records.
PlayerProfile.equippedBackpack does not prevent that operation and is not consulted. Likewise, an item's chain index is not a persistent identity: every lower deletion changes it. Clients should key durable item restoration by itemId, itemPda, design hash, or another stored identity where applicable, and reload authoritative slots after removal, recipe execution, listing, purchase, or forging. Current Player instructions refresh sessions but do not close them; clearing browser storage removes the local key only, while the on-chain session remains valid until expiry.
Dense compaction changes all later chainIndex values.
No slot-kind filter narrows deletion after the session passes validation.
The session expires on chain; local logout alone does not revoke it.
ONE SIGNED FIELD, TWO VALUES
Packed-Y carries current block identity only inside the signed decoder envelope
BackpackResourceRecord is an older ten-byte coordinate shape: i32 X, i16 Y, and i32 Z. Current mining preserves that size by placing a nine-bit offset from canonical minBuildY in the low bits of Y and block ID in the upper bits. Block slots leave itemCode zero, so this packed field is the authoritative block identity that recipe, fuel, inventory, and rendering consumers need to recover.
The Rust encoder accepts height offsets zero through 511 and block IDs one through 127, falling back to raw world Y outside that envelope. Current canonical height is minus thirty-two through 320, so its offset reaches only 352. Current Chunk.js registers block IDs zero through fifty-three. Those values produce nonnegative signed i16 results and round-trip through the production decoder.
The nominal seven-bit ID range is not fully interoperable. An ID of sixty-four or more sets the sign bit. The JavaScript decoder treats negative i16 values as legacy raw height, and the Smelting decoder returns block ID zero for a negative value. The current effective cross-consumer ceiling is therefore sixty-three. Adding a future ID above it requires a coordinated codec or field migration. The standalone TypeScript Backpack SDK also exposes this field as raw signed worldY; consumers needing actual height and block identity must apply the production Packed-Y codec rather than treating that raw number as a coordinate.
Current world offsets use only zero through 352.
This nominal range exceeds what current signed consumers can reconstruct.
No current registered block crosses the sign boundary.
AUTHORITATIVE RECORD, CLIENT-BUILT APPEARANCE
Chunk.js reconstructs the inventory view, while ordinary block placement remains local-only
The production client fetches the public Backpack account, validates its program owner and version, decodes only the dense readable prefix, and turns each record into an application slot. Packed block identity selects the canonical Chunk.js block definition and resource class. Decoration metadata can select the matching surface model. Item categories select material or forged-item paths, while volume, durability, quality, item ID, item PDA, and design hash remain available for verification and detail views.
The inventory icon system does not download a thumbnail from the chain. Ordinary solid blocks reuse face-specific material IDs and deterministically baked TextureArray tiles. Plants and special drops reuse low-cost Chunk.js resource geometry; smelted materials use their material preview models; restored NCF1 equipment can rebuild its encoded geometry through the forge cache. These are client reconstruction products, so a different renderer may present the same authoritative record differently without changing ownership.
Equipping a Backpack record to the hotbar is a local shortcut. The ordinary placement controller captures a local slot snapshot and applies a pending client delta. It does not decrement the chain-backed slot in local game state; rollback removes the world delta, while the current restoreBackpackSlotSnapshot hook is a no-op. recordBlockPlacementOnChain currently returns chain-placement-disabled, and Backpack header placement coordinates have no writer. This guide therefore makes no claim that moving a mined resource into the world is an accepted on-chain placement or that the Backpack PDA records a deployed container position. On-chain foundations and NCM3 buildings are separate systems with their own accounts and validators.
Ownership, slot parameters, identity, and ordering come from the chain account.
Icons and meshes are derived locally and are not stored in the Backpack account.
Local pending and confirmation UI is not proof of a chain placement instruction.
ADDRESS, CAPACITY, PACKING, AND INDEX MATH
Five equations separate stable ownership from transient slots and reconstructed pixels
The derivation fixes which account belongs to an owner and ID. The layout equation distinguishes fifty active slots from reserved format space. Stack-aware append and removal equations expose headroom, ordered rewards, and index shifts. Packed-Y then shows exactly where current block identity fits—and where a future codec change would be required.
Owner-and-ID Backpack PDA
P = findProgramAddress([UTF8("backpack"), owner_32, LE64(backpackId)], backpackProgramId)The owner may derive more than one Backpack PDA by choosing different 64-bit IDs. Every current write path reloads the stored owner and ID and checks this derivation. PlayerProfile.equippedBackpack may point to one validated account for client discovery, but changing that public pointer does not change P and other Backpack consumers do not compare against it as an exclusive authorization gate.
- P
- Program-derived Backpack account address and bump under the active Backpack or unified game program.
- owner_32
- The owner's 32-byte public key, also stored in the Backpack header.
- LE64(backpackId)
- The selected unsigned 64-bit backpack ID encoded little-endian.
Fixed layout versus current creation capacity
L = H + S_layout*R = 128 + 99*80 = 8048 bytes; c_init = min(50,max(1,c_request)); 0 <= n <= c_init <= 50; 1 <= q_block <= 99; B_max = 50*99 = 4950Version 4 keeps the fixed 8,048-byte account and ninety-nine-record format reserve, while the only initialize instruction clamps active capacity to at most fifty. n counts occupied slot records. Matching block resources accumulate quantity, volume, and mass in one record up to ninety-nine units, so fifty full block stacks hold 4,950 units without increasing account rent.
- H, R
- The 128-byte header and one 80-byte slot record.
- S_layout, c_init, c_format
- Reserved record count, current initialized capacity, and format-level capacity ceiling.
- n, q_block, B_max
- Dense active slot count, one block stack's quantity, and the block-only backpack capacity.
Stack-aware strict append versus ordered lossy batches
k=(blockId,category,flags,metadata); h_k = sum_i max(0,99-q_i) for stacks with identity k; r = ceil(max(0,q_in-h_k)/99); strictAccepted = (n+r<=c); lossyAccepted_j = canStore(S_(j-1),candidate_j)A block append first fills headroom in records with the same block ID, category, flags, and metadata, then opens only the additional slots required by its remainder. Stored coordinates, aggregate volume, and aggregate mass are not equality keys; accepted merges add proportional volume and mass to the destination stack. The required base block from ordinary reward mining and Market returns fail atomically when neither compatible headroom nor a free slot exists. Ordinary mining secondaries and the complete tree, tag 20, and tag 21 candidate lists use the ordered lossy batch path: every candidate is attempted against the state left by earlier candidates, and an unfittable candidate is skipped without preventing a later compatible candidate from using stack headroom. Non-block items use one active slot and retain their producer-defined quantity semantics.
- c, n, h_k
- The account's active capacity, occupied slot count, and remaining units in block stacks with the same chain merge identity k.
- q_in, r
- Incoming block quantity and the number of additional slots it requires after using matching headroom.
- lossyAccepted_j
- Whether ordered lossy candidate j was stored after all earlier accepted candidates changed capacity.
Dense deletion and session authorization
R subseteq {0,...,n-1}; n_after=n-|R|; newIndex(i)=i-|{r in R | r<i}| for i notin R; removeAllowed = ownerSigned OR (sessionSigned AND expiresAt>now AND BREAK_BLOCK_bit=1 AND owner/profile/PDA match)Single deletion copies every later 80-byte record one position left. Batch deletion validates distinct original indexes, then removes from high to low. The session branch reuses the break-block action bit and does not filter slot kind, so a valid session with that permission can delete block, material, or forged records. It cannot close the Backpack account or revoke its own PlayerSession; the current Player Program has no session-close instruction, so a locally forgotten session remains governed by its on-chain expiry.
- R
- Set of distinct, valid record indexes selected for removal.
- newIndex(i)
- New dense position of a surviving record after every lower removed index is counted.
- BREAK_BLOCK_bit
- The PlayerSession action permission currently reused by Backpack tag 2 and tag 4 deletion.
Packed-Y block identity and the signed decoder boundary
o=y-y_min; p=(b<<9)|o for 0<=o<=511 and 1<=b<=127 in the Rust encoder; b=p>>9 and y=y_min+(p&511); current signed-i16 decode additionally requires p>=0, therefore b<=63; today y_min=-32, y<=320, b<=53Nine low bits carry height offset and the remaining seven encoder bits carry block ID. The canonical height span uses offsets zero through 352, and every currently registered block ID is at most fifty-three, so present rewards round-trip. IDs sixty-four through 127 would set the i16 sign bit; the current JavaScript and Smelting decoders treat a negative value as legacy raw Y and lose block identity. Extending the registry beyond sixty-three therefore requires an explicit storage or decoder revision, not only a new block definition.
- y, y_min, o
- World height, canonical minimum build height, and its nine-bit nonnegative offset.
- b
- Canonical block ID stored in the high bits; current registered values are zero through fifty-three.
- p
- The bit pattern written into BackpackResourceRecord.world_y as a signed 16-bit field.
A known Backpack address is readable without an owner signature
JavaScriptsrc/chain/nicechunkChain.jsexport async function fetchBackpack(backpackAddress) {
if (!backpackAddress) return null;
const publicKey = typeof backpackAddress === "string" ? new PublicKey(backpackAddress) : backpackAddress;
const account = await getNicechunkConnection().getAccountInfo(publicKey, "confirmed");
if (!account?.data?.length) return null;
if (!account.owner.equals(gameProgramId)) return null;
const decoded = decodeBackpack(account.data);
The stable read boundary needs an address, not the owner's signature. Program ownership and binary decoding protect type integrity rather than privacy; the full client then hydrates any valid immutable ForgedItem references before returning the Backpack.
Each slot stores quantity and aggregate physical values together
Rustprograms/nicechunk_backpack/src/state.rspub struct BackpackSlotRecord {
pub kind: u8,
pub category: u8,
pub flags: u16,
pub quantity: u32,
pub resource: BackpackResourceRecord,
pub item_code: u16,
pub item_id: u64,
pub item_pda: Pubkey,
pub volume_mm3: u32,
pub durability_current: u32,
pub durability_max: u32,
pub grade: u8,
pub item_level: u8,
pub quality_bps: u16,
pub metadata: u32,
}
A block stack remains one 80-byte record: quantity, volume, and mass represent the aggregate, while block identity and metadata determine whether another reward may merge into it. The stack-aware append routine updates these fields together and never exceeds ninety-nine block units.
Deleting one record shifts the dense tail left
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;
A chain index resolves a current dense record position, not a permanent item identity. The full removal routine subtracts that record's mass, shifts the tail left, and updates the slot; clients must reload after every mutation.
The Chunk Program packs block ID above a nine-bit height offset
Rustprograms/nicechunk_chunk/src/state.rspub fn pack_backpack_resource_y(world_y: i16, block_id: u16, min_y: i16) -> i16 {
let y_offset = world_y as i32 - min_y as i32;
if (0..=BACKPACK_PACKED_Y_MASK).contains(&y_offset)
&& block_id > 0
&& block_id < (1_u16 << (16 - BACKPACK_PACKED_Y_BITS))
{
((block_id as i32) << BACKPACK_PACKED_Y_BITS | y_offset) as i16
} else {
world_y
}
}
This encoder exposes the nominal 1-through-127 ID range and raw-Y fallback. The current consumers add a stricter signed-value condition, making sixty-three the actual end-to-end ceiling. Present IDs stop at fifty-three, so current data remains inside both envelopes.
Chunk.js reconstructs ordinary block icons from the canonical world preview mesh
JavaScriptchunk.js/renderer/item-preview.js const blockId = Number.isFinite(item.blockId) ? item.blockId : null;
const resourceId = Number.isFinite(item.resourceId) ? item.resourceId : blockId !== null ? blockDef(blockId).resourceId : RESOURCE_ID.stone;
const previewBlockId = blockId ?? resourcePreviewBlockId(resourceId);
if (previewBlockId !== null && hasWorldBlockPreviewModel(previewBlockId)) {
const mesh = createWorldBlockPreviewMesh({ blockId: previewBlockId });
bindVoxelPreviewMesh(canvas, ctx, size, mesh, options);
canvas.dataset.worldBlockModelBlockId = String(previewBlockId);
if (blockId === null) canvas.dataset.worldBlockModelSource = "resource";
return canvas;
}
After the account decoder recovers a block or resource ID, the current icon path resolves a canonical preview block, rebuilds the same world-preview mesh, and hands that mesh to the shared texture-array rasterizer. Surface decorations, smelted materials, equipment, and forged items use their own reconstruction branches; the PDA still stores identities and properties rather than a raster image.
IMPLEMENTATION EVIDENCE
Where these claims come from
Each claim is intentionally scoped to a concrete implementation path. These references are for verification, not decoration.
programs/nicechunk_backpack/src/state.rs
Defines the 8,048-byte Backpack v4 layout, fifty-slot creation policy, 99-unit block stacks, stack-aware strict and lossy appends, aggregate volume and mass, dense deletion, forge consumption, and session validation view.
programs/nicechunk_backpack/src/lib.rs
Clamps current initialization to fifty, dispatches the Backpack instructions, authenticates ChunkBroken, Market, and Smelting PDA producers, validates owner-derived Backpack accounts, and exposes owner or break-block-session deletion plus Smelting-authority quantity consumption.
sdk/nicechunk-backpack.ts
Mirrors PDA seeds, version, 50-slot and 99-unit stack constants, account length, record offsets, and TypeScript block-stack validation, including its raw signed worldY boundary.
programs/nicechunk_player/src/state.rs
Defines the public PlayerProfile equippedBackpack field and writes it as a public key rather than embedding inventory contents in the profile.
programs/nicechunk_player/src/lib.rs
Validates owner, PlayerProfile PDA, Backpack program owner, and Backpack owner-derived PDA before replacing the equipped pointer; its dispatch has no PlayerSession close instruction.
programs/nicechunk_chunk/src/state.rs
Defines the nine-bit Packed-Y offset, current canonical height envelope, current world block constants used by canonical mining, and the nominal one-through-127 Rust packing condition.
programs/nicechunk_chunk/src/lib.rs
Shows ordinary tag 8's strict base-block CPI, its ordered lossy secondaries, and the fully lossy reward-candidate batches used by tree, two-block batch, and compressed range mining.
programs/nicechunk_smelting/src/state.rs
Reads block identity from Packed-Y, treats negative values as no packed block, requires unique recipe indexes, allocates template quantities from matching stacks, and projects capacity after partial consumption.
programs/nicechunk_market/src/lib.rs
Moves complete 80-byte source slots through Market custody; returned block stacks can merge into compatible Backpack headroom while listings remain whole-slot transfers.
src/chain/nicechunkChain.js
Fetches public Backpack accounts without a read signature, validates 99-unit block stacks, calculates stack-aware capacity, decodes Packed-Y, treats equippedBackpack as the default loader pointer, and submits session-key deletion.
play/play-chain-backpack.js
Aggregates matching chain resources into 99-unit visible stacks, counts occupied capacity by those stacks, preserves authoritative chain indexes, and reloads after mutations.
play/game-state.js
Treats PDA slot order as non-reorderable locally and uses Backpack-backed hotbar entries as client shortcuts rather than additional ownership records.
play/placement-controller.js
Captures a local slot snapshot, applies and rolls back pending world deltas, and calls a currently no-op Backpack restoration hook without supplying an on-chain placement validator.
chunk.js/world/block-registry.js
Defines the current zero-through-fifty-three block identities, material mapping, resource classes, flags, and face-specific grass material rule used after Packed-Y decoding.
chunk.js/renderer/item-preview.js
Reconstructs Backpack-facing icons from baked block faces, surface decorations, resource models, smelted-material models, and equipment models rather than chain-stored images.
chunk.js/renderer/texture-array-manager.js
Deterministically bakes material tiles from code, seed, and schema with a bounded cache, providing the same material language used by inventory block previews.
