Skip to content

Solana CT: SDK Sprint 1 — confidential mint + proof-account tx group - #9384

Draft
bhavidhingra with Copilot wants to merge 1 commit into
masterfrom
copilot/solana-ct-sdk-sprint-1-confidential-mint
Draft

Solana CT: SDK Sprint 1 — confidential mint + proof-account tx group#9384
bhavidhingra with Copilot wants to merge 1 commit into
masterfrom
copilot/solana-ct-sdk-sprint-1-confidential-mint

Conversation

Copilot AI commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Adds Token-2022 confidential transfer transaction construction in sdk-coin-sol using the proof-account approach.

  • New instruction types: ConfigureConfidentialTransferAccount, ConfidentialMint, CreateRecordAccount, WriteRecordData, VerifyEqualityProof, VerifyValidityProof, VerifyRangeProof, CloseRecordAccount, CloseContextState.
  • Raw instruction builders: hand-encoded Token-2022 extension layouts and zk-elgamal-proof verification instructions in solInstructionFactory.ts.
  • ConfidentialMintBuilder: groups the proof-account sequence (create-record → write → verify ×3 → confidentialMint) as one logical transaction, with optional cleanup closes.
  • Factory/export wiring: registered via TransactionBuilderFactory.getConfidentialMintBuilder() and exported from index.ts.
  • Parser round-trip: custom-transaction parser preserves confidential instruction metadata instead of falling back to opaque CustomInstruction.
  • Unit tests: cover ConfigureConfidentialTransferAccount layout, full proof-group ordering, multi-chunk record writes, and raw tx serialization.
const builder = factory.getConfidentialMintBuilder();
builder.nonce(recentBlockhash).sender(payer).confidentialMint({
  tokenAddress,
  mintAddress,
  authorityAddress,
  payerAddress,
  rangeRecordAddress,
  equalityContextStateAddress,
  validityContextStateAddress,
  contextStateAuthorityAddress,
  newDecryptableSupply: '00'.repeat(36),
  mintAmountAuditorCiphertextLo: '00'.repeat(64),
  mintAmountAuditorCiphertextHi: '00'.repeat(64),
  rangeRecordData: '00'.repeat(1000),
});
const tx = await builder.build();

@bhavidhingra

Copy link
Copy Markdown
Contributor

@copilot are you working on it?

Copilot AI changed the title [WIP] Build confidential mint transaction construction in sdk-coin-sol Solana CT: SDK Sprint 1 — confidential mint + proof-account tx group Jul 29, 2026
Copilot AI requested a review from bhavidhingra July 29, 2026 12:28

Copilot AI commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

@copilot are you working on it?

Yes, I’m still on it. I’ve addressed the prior review feedback in the latest commits and will keep an eye out for any additional comments.

@bhavidhingra
bhavidhingra force-pushed the copilot/solana-ct-sdk-sprint-1-confidential-mint branch from ff0c936 to 2349023 Compare July 29, 2026 13:31
@bhavidhingra

Copy link
Copy Markdown
Contributor

Review: Solana CT SDK Sprint 1 — confidential mint + proof-account tx group

Reviewed against Linear TDD CHALO-1090 requirements. Findings below.


✅ What's good

  • Discriminators are correct. Token-2022: ConfidentialTransferExtension = 27 (TokenInstruction.ConfidentialTransferExtension), ConfigureAccount = 2, ConfidentialMintBurn = 42, Mint = 3 — all verified by tests (solInstructionFactory.ts:444-447, test asserts data[0]=27, data[1]=2, data[0]=42, data[1]=3). zk-elgamal-proof: verify equality = 3, verify range U128 = 7, verify validity = 12 — all correct (solInstructionFactory.ts:452-454).
  • confidentialMintDataLayout encoding is correct. u8(1) + u8(1) + blob(36) + blob(64) + blob(64) + 3×u8(3) = 169 bytes — test asserts data.length === 169 ✓ (solInstructionFactory.ts:477-495).
  • Parser round-trip preserves metadata. parseConfidentialTransferInstruction in instructionParamsFactory.ts:329-351 returns stored metadata for all 9 confidential instruction types instead of falling back to opaque CustomInstruction.
  • Close instructions included for rent reclamation. CloseRecordAccount and CloseContextState are both supported with optional flags (confidentialMintBuilder.ts close flags, solInstructionFactory.ts close functions).
  • Builder validation. Rejects empty builder and invalid addresses. Multi-chunk record writes (1-2) supported via rangeRecordDataPart2.
  • Factory wiring. getConfidentialMintBuilder() added to TransactionBuilderFactory and exported from index.ts.

🔴 Issues found

1. CRITICAL — Builder produces ONE transaction, not 5-6 separate transactions

This is the most important architectural issue per the TDD. The TDD explicitly states:

The proof-account approach requires MULTIPLE SEPARATE TRANSACTIONS (5-6 per mint), not one transaction with multiple instructions. [...] A builder that puts all instructions in one transaction is INCORRECT.

ConfidentialMintBuilder.buildImplementation() (confidentialMintBuilder.ts) assembles all instructions into a single this._instructionsData array and calls super.buildImplementation(). The parent TransactionBuilder.buildLegacyTransaction() (transactionBuilder.ts) then does:

for (const instruction of this._instructionsData) {
  tx.add(...solInstructionFactory(instruction));
}

This puts all 6+ instructions into one SolTransaction. The test confirms: instructions.should.have.length(6) — 6 instructions in one tx.

This will not work on-chain for two reasons:

  1. Solana tx size limit (1232 bytes): The WriteRecordData instruction alone carries ~1000 bytes of range proof data. createAccount + write(1000B) + 3× verify + confidentialMint(169B) far exceeds 1232 bytes. The test uses '00'.repeat(1000) and asserts a single tx — this would never broadcast.
  2. Sequential confirmation requirement: The proof-account approach exists specifically because each verify step may need on-chain-confirmed state before the next. The TDD requires: tx1 (create-record + write) → confirm → tx2 (verify equality + validity) → confirm → tx3 (verify range) → confirm → tx4 (confidentialMint). The builder should produce a sequence of transactions or a SendQ group, not one transaction.

Recommendation: buildImplementation() should return an array/group of transactions (or the builder should expose a method that yields individual txs in sequence). The class docstring even says "groups the proof-account tx sequence as one logical transaction" — but "one logical transaction" must be multiple on-chain transactions sequenced as a unit, not one SolTransaction.

2. CRITICAL — Proof instruction offsets are wrong (positive instead of negative)

confidentialMintBuilder.ts defaults:

equalityProofInstructionOffset: params.equalityProofInstructionOffset ?? 5,
ciphertextValidityProofInstructionOffset: params.ciphertextValidityProofInstructionOffset ?? 4,
rangeProofInstructionOffset: params.rangeProofInstructionOffset ?? 3,

In the instruction order (indices 0-5):

0: CreateRecordAccount
1: WriteRecordData
2: VerifyEqualityProof
3: VerifyValidityProof
4: VerifyRangeProof
5: ConfidentialMint

The offsets are relative to the confidentialMint instruction (signed i8, negative = look backward). The correct values are:

  • equality: 2 - 5 = -3 (encoded as 253 in u8)
  • validity: 3 - 5 = -2 (encoded as 254)
  • range: 4 - 5 = -1 (encoded as 255)

The code defaults to 5, 4, 3 (positive — pointing forward to instructions that don't exist after confidentialMint). The test asserts data[166]=5, data[167]=4, data[168]=3the test asserts the wrong values too. On-chain, the program would look for proof instructions at positions 10, 9, 8 (which don't exist) and fail.

3. MAJOR — Record account owner defaults to a wallet address instead of ZK_ELGAMAL_PROOF_PROGRAM_ID

confidentialMintBuilder.ts:

const recordOwner = params.recordAccountOwnerAddress || params.contextStateAuthorityAddress;

Then createRecordAccountInstruction uses this as SystemProgram.createAccount({ programId: recordOwner }). For a zk-elgamal-proof record account, the owner must be ZK_ELGAMAL_PROOF_PROGRAM_ID — not a wallet address. With the default, the account is owned by a wallet, so the zk program cannot write to it and WriteRecordData / verify instructions will fail with "account owner mismatch."

Similarly, writeRecordDataInstruction uses recordAccountOwnerAddress as the programId:

programId: new PublicKey(recordAccountOwnerAddress || ZK_ELGAMAL_PROOF_PROGRAM_ID),

When recordAccountOwnerAddress is set to a wallet (the default path), the write instruction targets the wallet as program — wrong.

Fix: recordAccountOwnerAddress should default to ZK_ELGAMAL_PROOF_PROGRAM_ID, not contextStateAuthorityAddress. Or better: remove the overload — the record account owner is always the zk proof program.

4. MAJOR — createRecordAccount uses lamports: 0

solInstructionFactory.ts createRecordAccountInstruction:

lamports: 0,

Solana requires accounts to be rent-exempt. Creating an account with 0 lamports will fail on-chain. The builder needs to calculate rent for the account size (e.g., using getMinimumBalanceForRentExemption(space) or accepting a rent parameter).

5. MAJOR — ConfigureConfidentialTransferAccount is missing ElGamal pubkey + AES key

The TDD states: "ConfigureConfidentialTransferAccount instruction: one-time ATA setup — register ElGamal pubkey + AES key."

The current layout (solInstructionFactory.ts:460-472) is 47 bytes:

u8 instruction + u8 extension + blob(36) decryptable + blob(8) counter + u8 offset = 47

The actual Token-2022 ConfigureAccount instruction also requires:

  • ElGamal pubkey (32 bytes)
  • AES key (16 bytes)

The correct layout should be ~95 bytes: 1 + 1 + 32 + 16 + 36 + 8 + 1. The test asserts data.length === 47 — the test confirms the missing fields. Without the ElGamal pubkey and AES key, the token account is not properly configured for confidential transfers.

6. MAJOR — closeRecordAccountInstruction uses wrong program

solInstructionFactory.ts closeRecordAccountInstruction:

createCloseAccountInstruction(
  new PublicKey(recordAccountAddress),
  new PublicKey(destinationAddress),
  new PublicKey(authorityAddress),
  [],
  TOKEN_2022_PROGRAM_ID
)

The record account is owned by the zk-elgamal-proof program, not Token-2022. createCloseAccountInstruction from @solana/spl-token constructs a Token program close instruction — this will fail with "program owner mismatch." Should use the zk-elgamal-proof program's close instruction (or a raw TransactionInstruction targeting ZK_ELGAMAL_PROOF_PROGRAM_ID).

7. MAJOR — VerifyRangeProof account layout is wrong

confidentialMintBuilder.ts:

instructions.push({
  type: InstructionBuilderTypes.VerifyRangeProof,
  params: {
    proofAccountAddress: params.rangeRecordAddress,
    contextStateAccountAddress: params.rangeRecordAddress,  // SAME account
    contextStateAuthorityAddress: params.contextStateAuthorityAddress,
  },
});

The VerifyBatchedRangeProofU128 instruction reads the proof from the record account and does not use a context state account. Passing the same address as both proofAccountAddress (read-only) and contextStateAccountAddress (writable) is incorrect — the runtime will merge them into one writable entry, but the zk program expects a different account layout for range proofs vs. equality/validity proofs. The buildVerifyProofInstruction helper always creates 3 keys (proof, context state, authority) which doesn't match the range proof's actual account requirements.

8. MINOR — TransactionBuilderFactory.from() doesn't route to ConfidentialMintBuilder

transactionBuilderFactory.ts: ConfidentialMintBuilder returns TransactionType.CustomTx, and from() routes CustomTx to getCustomInstructionBuilder(), not getConfidentialMintBuilder(). So when parsing a raw confidential mint transaction, the caller gets a CustomInstructionBuilder instead of a ConfidentialMintBuilder. The parseConfidentialTransferInstruction metadata preservation mitigates data loss, but the specialized confidentialMint() / configureConfidentialTransferAccount() methods are unavailable on the parsed builder. Consider adding a routing path or a discriminator check.

9. MINOR — Confidential instructions added to VALID_SYSTEM_INSTRUCTION_TYPES

constants.ts: The 9 new instruction types are added to VALID_SYSTEM_INSTRUCTION_TYPES. These are Token-2022 and zk-elgamal-proof program instructions, not System program instructions. If this array is used for System program validation, this could cause misclassification.

10. MINOR — _extractMintParams loses rangeRecordData on round-trip

confidentialMintBuilder.ts _extractMintParams:

rangeRecordData: '',

When reconstructing from a parsed transaction, the range proof data is set to empty string. If someone tries to rebuild from the parsed tx, the write instruction would have empty data. Consider storing/recovering the record data or documenting that round-trip rebuild is not supported for the proof-account group.

11. MINOR — Verify proof instruction authority isSigner: false

buildVerifyProofInstruction sets contextStateAuthorityAddress with isSigner: false. For context-state verify instructions, the authority may need to sign to authorize writes to the context state account. Verify against the zk-elgamal-proof program's actual account requirements.


❓ Questions

  1. ZK_ELGAMAL_PROOF_PROGRAM_IDconstants.ts sets it to ZkE1G11tXDEfKBqrPK5G1iFbnW4mdeQTw48MYUb9Gkp. The commonly-cited mainnet zk-token-proof program ID is ZkTokenProof1111111111111111111111111111111111. Is ZkE1G11tXDEfKBqrPK5G1iFbnW4mdeQTw48MYUb9Gkp a devnet/testnet address or a newer program deployment? Please confirm this matches the target environment.

  2. SendQ integration — The TDD mentions "SendQ must sequence the 5-6 txs for a single mint/burn as one logical unit." Is there a SendQ interface this builder should integrate with, or is that planned for a follow-up PR? The current builder has no SendQ awareness.

  3. confidential_supply on fresh mint — The TDD notes confidential_supply on fresh mint = identity ciphertext (64 zero bytes), NOT encryptU64(0). The builder accepts newDecryptableSupply as a param (36 bytes). Is the caller (KMS worker) responsible for providing the correct identity ciphertext, or should the SDK encode this default for fresh mints?

  4. Multi-tx architecture — What is the planned interface for the multi-tx output? Options: (a) build() returns Transaction[], (b) a buildGroup() method returns a sequenced group, (c) the builder yields individual txs via an async iterator. This affects downstream signing and SendQ integration.

  5. ConfigureConfidentialTransferAccount proof approach — The current implementation uses INSTRUCTIONS_SYSVAR_ADDRESS as the default for instructionsSysvarOrContextStateAddress, implying in-tx proof verification. But the TDD's proof-account approach uses context state accounts. Should configure also support the context-state path, or is configure always in-tx proof?

@bhavidhingra
bhavidhingra force-pushed the copilot/solana-ct-sdk-sprint-1-confidential-mint branch 5 times, most recently from c863ba0 to 06edfad Compare July 30, 2026 11:23
…group

- Fix proof instruction offsets: -3, -2, -1 (signed i8 relative to
  confidentialMint) instead of positive 5, 4, 3. Encoded as u8
  two's complement: 253, 254, 255
- Record account owner defaults to ZK_ELGAMAL_PROOF_PROGRAM_ID
  instead of contextStateAuthorityAddress (wallet)
- Add lamports parameter to CreateRecordAccount; calculate
  rent-exempt default: (space + 128) * 6960 * 2
- Fix VerifyRangeProof: only proof account key (no context state)
  — VerifyBatchedRangeProofU128 does not use context state
- Fix closeRecordAccount: use ZK_ELGAMAL_PROOF_PROGRAM_ID
  instead of TOKEN_2022_PROGRAM_ID (record owned by zk program)
- Remove confidential instructions from VALID_SYSTEM_INSTRUCTION_TYPES
  (they are Token-2022/zk-proof program, not System program)
- Add instruction-level test assertions for coverage target

Remaining (follow-up):
- Critical 1: multi-tx architecture (builder produces single tx,
  TDD requires 5-6 separate txs for 1232B limit)
- Major 5: ConfigureAccount missing ElGamal pubkey (32B) + AES key (16B)

Ticket: CHALO-1090
@bhavidhingra
bhavidhingra force-pushed the copilot/solana-ct-sdk-sprint-1-confidential-mint branch from 06edfad to b383cf2 Compare July 30, 2026 11:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Solana CT: SDK — confidential mint transaction building + proof-account approach (Sprint 1)

2 participants