solana-labs / solana-web3.js

Solana JavaScript SDK
https://solana-labs.github.io/solana-web3.js
MIT License
2.2k stars 871 forks source link

Feature Request: Optional `feePayerVerify` simulation config param (similar to `sigVerify`) #3214

Closed jacksondoherty closed 2 weeks ago

jacksondoherty commented 2 months ago

Motivation

Returning account data using an emit instruction is a growing pattern on Solana that enhances composability by abstracting internal data layouts. For example, the TokenMetadata extension has an emit function.

Clients are expected to simulate emit and deserialize the return data. To do so, they need to provide a payer for the transaction, which must have enough lamports to process as if it were submitted. This payer requirement makes it difficult to create libraries that use the simulation workflow. For example, here is a utility function that extracts TokenMetadata using emit – we're required to add a payer parameter:

// Usage
const tokenMetadata = getEmittedTokenMetadata(connection, metadata, program, payer);

// Implementation
async function getEmittedTokenMetadata(
  connection: Connection,
  metadata: PublicKey,
  programId: PublicKey,
  payer: PublicKey
): Promise<TokenMetadata | null> {
  const emitIx = createEmitInstruction({
    programId,
    metadata,
  });
  const latestBlockhash = await connection.getLatestBlockhash();
  const txMsg = new TransactionMessage({
    payerKey: payer,
    recentBlockhash: latestBlockhash.blockhash,
    instructions: [emitIx],
  }).compileToV0Message();
  const v0Tx = new VersionedTransaction(txMsg);
  const res = (await connection.simulateTransaction(v0Tx)).value;

  if (!res.returnData?.data) {
    return null;
  }

  const data = Buffer.from(res.returnData.data[0], "base64");
  return unpack(data);
}

Here's how this might look if we had a feePayerVerify param:

// Usage
const tokenMetadata = getEmittedTokenMetadata(connection, metadata, program); // No payer required

// Implementation
async function getEmittedTokenMetadata(
  connection: Connection,
  metadata: PublicKey,
  programId: PublicKey,
): Promise<TokenMetadata | null> {
  const emitIx = createEmitInstruction({
    programId,
    metadata,
  });
  const latestBlockhash = await connection.getLatestBlockhash();
  const txMsg = new TransactionMessage({
    payerKey: Keypair.generate().publicKey,
    recentBlockhash: latestBlockhash.blockhash,
    instructions: [emitIx],
  }).compileToV0Message();
  const v0Tx = new VersionedTransaction(txMsg);
  const res = (await connection.simulateTransaction(v0Tx, { feePayerVerify: false })).value;

  if (!res.returnData?.data) {
    return null;
  }

  const data = Buffer.from(res.returnData.data[0], "base64");
  return unpack(data);
}

Optionally, we could use some sort of secure community public key as a payer, but this seems hacky.

buffalojoec commented 2 months ago

Hey @jacksondoherty thanks for the suggestion. This is actually something we'd have to change on the validator - specifically within the transaction processing pipeline - not Web3.js.

RPC's simulate_transaction will use a Bank instance to run a simulation against the transaction processing pipeline. Sigverify happens before this pipeline is invoked, so it's trivial to disable sigverify using the aptly-named RPC parameter.

https://github.com/anza-xyz/agave/blob/b1de2e0ce873c7aa2e8470a0a3e3e1ae785ca821/rpc/src/rpc.rs#L3827-L3834

Within Bank, this isn't really super configurable (at the moment).

https://github.com/anza-xyz/agave/blob/b1de2e0ce873c7aa2e8470a0a3e3e1ae785ca821/runtime/src/bank.rs#L3318

However, the SVM API does allow for quite a bit of customization. I wonder if we can kill two birds with one stone here.

We might be able to make fees completely configurable within the SVM (transaction processing pipeline), similar to rent collection. Then, we could add a handful of new configurations to the RPC's simulate method - one of which could be to disable fee payer checks completely, by configuring fees to be zero.

Alternatively, we can just jam a check_fee_payer into the SVM API's parameters, similar to your suggestion here.

Side note: I wonder how strongly RPC providers would throw their support behind no-fee program view functions through simulation... 😅

jacksondoherty commented 2 months ago

Hm it seems fairly involved to do it the "correct way."

The "hacky" solution makes more sense then since it still fully meets requirements. I imagine it's possible to create some sort of community public key that you can prove is randomly generated and nobody owns – perhaps inside a smart contract – and then send it a little dust. Use that inside the library functions.

Not sure if that's something you'd want in the SPL. Either way, thanks for the thoughts @buffalojoec !

buffalojoec commented 2 months ago

I imagine it's possible to create some sort of community public key that you can prove is randomly generated and nobody owns – perhaps inside a smart contract – and then send it a little dust. Use that inside the library functions.

I really don't think this is something we want to support in Web3.js or SPL, but I can leave this issue open for anyone else to weigh in. Thanks!

mcintyre94 commented 2 weeks ago

Agree with @buffalojoec - we're not going to create that account and hardcode it into the library. But you're right that simulating a transaction with sigVerify: false means that you can just use an arbitrary funded account as the fee payer.

If you want to propose an RPC change that would allow skipping the fee payer check then please open an issue against https://github.com/anza-xyz/agave. If such a change is made to the RPC then we would support that field in web3js, but it's out of scope for this repo to discuss RPC changes.

github-actions[bot] commented 1 week ago

Because there has been no activity on this issue for 7 days since it was closed, it has been automatically locked. Please open a new issue if it requires a follow up.