Skip to content

Commit ff9e203

Browse files
authored
feat(svm): allow Phantom and Solflare Lighthouse instructions in transaction verification (#991)
Phantom and Solflare wallets inject Lighthouse program instructions for user protection on Solana mainnet transactions. This causes x402 verification to fail because the scheme expects exactly 3 instructions. This change allows optional 4th and 5th instructions specifically from the Lighthouse program (L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95): - 4th instruction: Phantom wallet protection (1 Lighthouse instruction) - 5th instruction: Solflare wallet protection (adds 2 Lighthouse instructions) Changes: - Add LIGHTHOUSE_PROGRAM_ADDRESS constant (TypeScript and Go) - Update ExactSvmScheme verify to allow 3-5 instructions - Update ExactSvmSchemeV1 verify to allow 3-5 instructions - Reject any 4th/5th instruction that is not from Lighthouse program - Add error constants for unknown 4th/5th instructions - Update scheme_exact_svm.md spec to reflect changes Fixes: #828
1 parent 7527f95 commit ff9e203

File tree

9 files changed

+157
-16
lines changed

9 files changed

+157
-16
lines changed

go/mechanisms/svm/constants.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ const (
2323
// DefaultComputeUnitLimit is the default compute unit limit for transactions
2424
DefaultComputeUnitLimit uint32 = 8000
2525

26+
// LighthouseProgramAddress is the Phantom/Solflare Lighthouse program address
27+
// Phantom and Solflare wallets inject Lighthouse instructions for user protection on mainnet transactions.
28+
// - Phantom adds 1 Lighthouse instruction (4th instruction)
29+
// - Solflare adds 2 Lighthouse instructions (4th and 5th instructions)
30+
// We allow these as optional instructions to support these wallets.
31+
// See: https://github.com/coinbase/x402/issues/828
32+
LighthouseProgramAddress = "L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95"
33+
2634
// DefaultCommitment is the default commitment level for transactions
2735
DefaultCommitment = rpc.CommitmentConfirmed
2836

go/mechanisms/svm/exact/facilitator/errors.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ const (
1010
ErrInvalidPayloadTransaction = "invalid_exact_solana_payload_transaction"
1111
ErrTransactionCouldNotBeDecoded = "invalid_exact_solana_payload_transaction_could_not_be_decoded"
1212
ErrTransactionInstructionsLength = "invalid_exact_solana_payload_transaction_instructions_length"
13+
ErrUnknownFourthInstruction = "invalid_exact_solana_payload_unknown_fourth_instruction"
14+
ErrUnknownFifthInstruction = "invalid_exact_solana_payload_unknown_fifth_instruction"
1315
ErrComputeLimitInstruction = "invalid_exact_solana_payload_transaction_instructions_compute_limit_instruction"
1416
ErrComputePriceInstruction = "invalid_exact_solana_payload_transaction_instructions_compute_price_instruction"
1517
ErrComputePriceInstructionTooHigh = "invalid_exact_solana_payload_transaction_instructions_compute_price_instruction_too_high"

go/mechanisms/svm/exact/facilitator/scheme.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,13 @@ func (f *ExactSvmScheme) Verify(
120120
return nil, x402.NewVerifyError(ErrTransactionCouldNotBeDecoded, "", network, err)
121121
}
122122

123-
// 3 instructions: ComputeLimit + ComputePrice + TransferChecked
124-
if len(tx.Message.Instructions) != 3 {
123+
// Allow 3, 4, or 5 instructions:
124+
// - 3 instructions: ComputeLimit + ComputePrice + TransferChecked
125+
// - 4 instructions: ComputeLimit + ComputePrice + TransferChecked + Lighthouse (Phantom wallet protection)
126+
// - 5 instructions: ComputeLimit + ComputePrice + TransferChecked + Lighthouse + Lighthouse (Solflare wallet protection)
127+
// See: https://github.com/coinbase/x402/issues/828
128+
numInstructions := len(tx.Message.Instructions)
129+
if numInstructions < 3 || numInstructions > 5 {
125130
return nil, x402.NewVerifyError(ErrTransactionInstructionsLength, "", network, nil)
126131
}
127132

@@ -158,7 +163,26 @@ func (f *ExactSvmScheme) Verify(
158163
return nil, x402.NewVerifyError(err.Error(), payer, network, err)
159164
}
160165

161-
// Step 5: Sign and Simulate Transaction
166+
// Step 5: Verify Lighthouse Instructions (if present)
167+
// - 4th instruction: Lighthouse program (Phantom wallet protection)
168+
// - 5th instruction: Lighthouse program (Solflare wallet adds 2 Lighthouse instructions)
169+
if numInstructions >= 4 {
170+
fourthProgID := tx.Message.AccountKeys[tx.Message.Instructions[3].ProgramIDIndex]
171+
lighthousePubkey := solana.MustPublicKeyFromBase58(svm.LighthouseProgramAddress)
172+
if !fourthProgID.Equals(lighthousePubkey) {
173+
return nil, x402.NewVerifyError(ErrUnknownFourthInstruction, payer, network, nil)
174+
}
175+
}
176+
177+
if numInstructions == 5 {
178+
fifthProgID := tx.Message.AccountKeys[tx.Message.Instructions[4].ProgramIDIndex]
179+
lighthousePubkey := solana.MustPublicKeyFromBase58(svm.LighthouseProgramAddress)
180+
if !fifthProgID.Equals(lighthousePubkey) {
181+
return nil, x402.NewVerifyError(ErrUnknownFifthInstruction, payer, network, nil)
182+
}
183+
}
184+
185+
// Step 6: Sign and Simulate Transaction
162186
// CRITICAL: Simulation proves transaction will succeed (catches insufficient balance, invalid accounts, etc)
163187

164188
// feePayer already validated in Step 1

go/mechanisms/svm/exact/v1/facilitator/errors.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ const (
1111
ErrInvalidPayloadTransaction = "invalid_exact_solana_payload_transaction"
1212
ErrTransactionCouldNotBeDecoded = "invalid_exact_solana_payload_transaction_could_not_be_decoded"
1313
ErrTransactionInstructionsLength = "invalid_exact_solana_payload_transaction_instructions_length"
14+
ErrUnknownFourthInstruction = "invalid_exact_solana_payload_unknown_fourth_instruction"
15+
ErrUnknownFifthInstruction = "invalid_exact_solana_payload_unknown_fifth_instruction"
1416
ErrComputeLimitInstruction = "invalid_exact_solana_payload_transaction_instructions_compute_limit_instruction"
1517
ErrComputePriceInstruction = "invalid_exact_solana_payload_transaction_instructions_compute_price_instruction"
1618
ErrComputePriceInstructionTooHigh = "invalid_exact_solana_payload_transaction_instructions_compute_price_instruction_too_high"

go/mechanisms/svm/exact/v1/facilitator/scheme.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,13 @@ func (f *ExactSvmSchemeV1) Verify(
130130
return nil, x402.NewVerifyError(ErrTransactionCouldNotBeDecoded, "", network, err)
131131
}
132132

133-
// 3 instructions: ComputeLimit + ComputePrice + TransferChecked
134-
if len(tx.Message.Instructions) != 3 {
133+
// Allow 3, 4, or 5 instructions:
134+
// - 3 instructions: ComputeLimit + ComputePrice + TransferChecked
135+
// - 4 instructions: ComputeLimit + ComputePrice + TransferChecked + Lighthouse (Phantom wallet protection)
136+
// - 5 instructions: ComputeLimit + ComputePrice + TransferChecked + Lighthouse + Lighthouse (Solflare wallet protection)
137+
// See: https://github.com/coinbase/x402/issues/828
138+
numInstructions := len(tx.Message.Instructions)
139+
if numInstructions < 3 || numInstructions > 5 {
135140
return nil, x402.NewVerifyError(ErrTransactionInstructionsLength, "", network, nil)
136141
}
137142

@@ -155,7 +160,26 @@ func (f *ExactSvmSchemeV1) Verify(
155160
return nil, x402.NewVerifyError(err.Error(), payer, network, err)
156161
}
157162

158-
// Step 5: Sign and Simulate Transaction
163+
// Step 5: Verify Lighthouse Instructions (if present)
164+
// - 4th instruction: Lighthouse program (Phantom wallet protection)
165+
// - 5th instruction: Lighthouse program (Solflare wallet adds 2 Lighthouse instructions)
166+
if numInstructions >= 4 {
167+
fourthProgID := tx.Message.AccountKeys[tx.Message.Instructions[3].ProgramIDIndex]
168+
lighthousePubkey := solana.MustPublicKeyFromBase58(svm.LighthouseProgramAddress)
169+
if !fourthProgID.Equals(lighthousePubkey) {
170+
return nil, x402.NewVerifyError(ErrUnknownFourthInstruction, payer, network, nil)
171+
}
172+
}
173+
174+
if numInstructions == 5 {
175+
fifthProgID := tx.Message.AccountKeys[tx.Message.Instructions[4].ProgramIDIndex]
176+
lighthousePubkey := solana.MustPublicKeyFromBase58(svm.LighthouseProgramAddress)
177+
if !fifthProgID.Equals(lighthousePubkey) {
178+
return nil, x402.NewVerifyError(ErrUnknownFifthInstruction, payer, network, nil)
179+
}
180+
}
181+
182+
// Step 6: Sign and Simulate Transaction
159183
// CRITICAL: Simulation proves transaction will succeed (catches insufficient balance, invalid accounts, etc)
160184

161185
// feePayer already validated in Step 1

specs/schemes/exact/scheme_exact_svm.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,16 @@ A facilitator verifying an `exact`-scheme SVM payment MUST enforce all of the fo
106106

107107
1. Instruction layout
108108

109-
- The decompiled transaction MUST contain 3 instructions in this exact order:
109+
- The decompiled transaction MUST contain 3 to 5 instructions in this order:
110110
1. Compute Budget: Set Compute Unit Limit
111111
2. Compute Budget: Set Compute Unit Price
112-
4. SPL Token or Token-2022 TransferChecked
112+
3. SPL Token or Token-2022 TransferChecked
113+
4. (Optional) Lighthouse program instruction (Phantom wallet protection)
114+
5. (Optional) Lighthouse program instruction (Solflare wallet protection)
115+
116+
- If a 4th or 5th instruction is present, the program MUST be the Lighthouse program (`L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95`).
117+
- Phantom wallet injects 1 Lighthouse instruction; Solflare injects 2.
118+
- These Lighthouse instructions are wallet-injected user protection mechanisms and MUST be allowed to support these wallets.
113119

114120
2. Fee payer (facilitator) safety
115121

typescript/packages/mechanisms/svm/src/constants.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ export const TOKEN_PROGRAM_ADDRESS = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5D
66
export const TOKEN_2022_PROGRAM_ADDRESS = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
77
export const COMPUTE_BUDGET_PROGRAM_ADDRESS = "ComputeBudget111111111111111111111111111111";
88

9+
/**
10+
* Phantom/Solflare Lighthouse program address
11+
* Phantom and Solflare wallets inject Lighthouse instructions for user protection on mainnet transactions.
12+
* - Phantom adds 1 Lighthouse instruction (4th instruction)
13+
* - Solflare adds 2 Lighthouse instructions (4th and 5th instructions)
14+
* We allow these as optional instructions to support these wallets.
15+
* See: https://github.com/coinbase/x402/issues/828
16+
*/
17+
export const LIGHTHOUSE_PROGRAM_ADDRESS = "L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95";
18+
919
/**
1020
* Default RPC URLs for Solana networks
1121
*/

typescript/packages/mechanisms/svm/src/exact/facilitator/scheme.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import type {
2424
SettleResponse,
2525
VerifyResponse,
2626
} from "@x402/core/types";
27-
import { MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS } from "../../constants";
27+
import { LIGHTHOUSE_PROGRAM_ADDRESS, MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS } from "../../constants";
2828
import type { FacilitatorSvmSigner } from "../../signer";
2929
import type { ExactSvmPayloadV2 } from "../../types";
3030
import { decodeTransactionFromPayload, getTokenPayerFromTransaction } from "../../utils";
@@ -137,8 +137,12 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator {
137137
const decompiled = decompileTransactionMessage(compiled);
138138
const instructions = decompiled.instructions ?? [];
139139

140-
// 3 instructions: ComputeLimit + ComputePrice + TransferChecked
141-
if (instructions.length !== 3) {
140+
// Allow 3, 4, or 5 instructions:
141+
// - 3 instructions: ComputeLimit + ComputePrice + TransferChecked
142+
// - 4 instructions: ComputeLimit + ComputePrice + TransferChecked + Lighthouse (Phantom wallet protection)
143+
// - 5 instructions: ComputeLimit + ComputePrice + TransferChecked + Lighthouse + Lighthouse (Solflare wallet protection)
144+
// See: https://github.com/coinbase/x402/issues/828
145+
if (instructions.length < 3 || instructions.length > 5) {
142146
return {
143147
isValid: false,
144148
invalidReason: "invalid_exact_svm_payload_transaction_instructions_length",
@@ -257,7 +261,34 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator {
257261
};
258262
}
259263

260-
// Step 5: Sign and Simulate Transaction
264+
// Step 5: Verify Lighthouse Instructions (if present)
265+
// - 4th instruction: Lighthouse program (Phantom wallet protection)
266+
// - 5th instruction: Lighthouse program (Solflare wallet adds 2 Lighthouse instructions)
267+
if (instructions.length >= 4) {
268+
const fourthInstruction = instructions[3];
269+
const fourthProgramAddress = fourthInstruction.programAddress.toString();
270+
if (fourthProgramAddress !== LIGHTHOUSE_PROGRAM_ADDRESS) {
271+
return {
272+
isValid: false,
273+
invalidReason: "invalid_exact_svm_payload_unknown_fourth_instruction",
274+
payer,
275+
};
276+
}
277+
}
278+
279+
if (instructions.length === 5) {
280+
const fifthInstruction = instructions[4];
281+
const fifthProgramAddress = fifthInstruction.programAddress.toString();
282+
if (fifthProgramAddress !== LIGHTHOUSE_PROGRAM_ADDRESS) {
283+
return {
284+
isValid: false,
285+
invalidReason: "invalid_exact_svm_payload_unknown_fifth_instruction",
286+
payer,
287+
};
288+
}
289+
}
290+
291+
// Step 6: Sign and Simulate Transaction
261292
// CRITICAL: Simulation proves transaction will succeed (catches insufficient balance, invalid accounts, etc)
262293
try {
263294
const feePayer = requirements.extra.feePayer as Address;

typescript/packages/mechanisms/svm/src/exact/v1/facilitator/scheme.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ import type {
2525
VerifyResponse,
2626
} from "@x402/core/types";
2727
import type { PaymentPayloadV1, PaymentRequirementsV1 } from "@x402/core/types/v1";
28-
import { MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS } from "../../../constants";
28+
import {
29+
LIGHTHOUSE_PROGRAM_ADDRESS,
30+
MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS,
31+
} from "../../../constants";
2932
import type { FacilitatorSvmSigner } from "../../../signer";
3033
import type { ExactSvmPayloadV1 } from "../../../types";
3134
import { decodeTransactionFromPayload, getTokenPayerFromTransaction } from "../../../utils";
@@ -140,8 +143,12 @@ export class ExactSvmSchemeV1 implements SchemeNetworkFacilitator {
140143
const decompiled = decompileTransactionMessage(compiled);
141144
const instructions = decompiled.instructions ?? [];
142145

143-
// 3 instructions: ComputeLimit + ComputePrice + TransferChecked
144-
if (instructions.length !== 3) {
146+
// Allow 3, 4, or 5 instructions:
147+
// - 3 instructions: ComputeLimit + ComputePrice + TransferChecked
148+
// - 4 instructions: ComputeLimit + ComputePrice + TransferChecked + Lighthouse (Phantom wallet protection)
149+
// - 5 instructions: ComputeLimit + ComputePrice + TransferChecked + Lighthouse + Lighthouse (Solflare wallet protection)
150+
// See: https://github.com/coinbase/x402/issues/828
151+
if (instructions.length < 3 || instructions.length > 5) {
145152
return {
146153
isValid: false,
147154
invalidReason: "invalid_exact_svm_payload_transaction_instructions_length",
@@ -260,7 +267,34 @@ export class ExactSvmSchemeV1 implements SchemeNetworkFacilitator {
260267
};
261268
}
262269

263-
// Step 5: Sign and Simulate Transaction
270+
// Step 5: Verify Lighthouse Instructions (if present)
271+
// - 4th instruction: Lighthouse program (Phantom wallet protection)
272+
// - 5th instruction: Lighthouse program (Solflare wallet adds 2 Lighthouse instructions)
273+
if (instructions.length >= 4) {
274+
const fourthInstruction = instructions[3];
275+
const fourthProgramAddress = fourthInstruction.programAddress.toString();
276+
if (fourthProgramAddress !== LIGHTHOUSE_PROGRAM_ADDRESS) {
277+
return {
278+
isValid: false,
279+
invalidReason: "invalid_exact_svm_payload_unknown_fourth_instruction",
280+
payer,
281+
};
282+
}
283+
}
284+
285+
if (instructions.length === 5) {
286+
const fifthInstruction = instructions[4];
287+
const fifthProgramAddress = fifthInstruction.programAddress.toString();
288+
if (fifthProgramAddress !== LIGHTHOUSE_PROGRAM_ADDRESS) {
289+
return {
290+
isValid: false,
291+
invalidReason: "invalid_exact_svm_payload_unknown_fifth_instruction",
292+
payer,
293+
};
294+
}
295+
}
296+
297+
// Step 6: Sign and Simulate Transaction
264298
// CRITICAL: Simulation proves transaction will succeed (catches insufficient balance, invalid accounts, etc)
265299
try {
266300
const feePayer = requirementsV1.extra.feePayer as Address;

0 commit comments

Comments
 (0)