2

I'm writing an Anchor program which has an instruction that initializes a PDA account using some seeds. I want to write a test that verifies that my implementation ensures uniqueness: I can't call my create instruction with the same arguments twice. I'm mostly using it as a motivating example for using the PDA, but I think it's a valid test case regardless.

This is the test I've written:

it("Does not allow creating the same pixel twice", async () => {
  const x = 20
  const y = 20

  const [pixelPublicKey] = web3.PublicKey.findProgramAddressSync(
    [Buffer.from("pixel"), Buffer.from([x, y])],
    program.programId,
  )

  // Create the pixel: this should pass
  await program.methods
    .createPixel(x, y, 0, 0, 255)
    .accounts({
      pixel: pixelPublicKey,
      user: anchorProvider.wallet.publicKey,
      systemProgram: web3.SystemProgram.programId,
    })
    .rpc()

  // Create the same pixel: this should fail
  await program.methods
    .createPixel(x, y, 0, 0, 255)
    .accounts({
      pixel: pixelPublicKey,
      user: anchorProvider.wallet.publicKey,
      systemProgram: web3.SystemProgram.programId,
    })
    .rpc()
    .then(
      () => Promise.reject(new Error('Expected to error!')),
      (e: any) => {
        console.log(e)
        assert.ok(e instanceof AnchorError)
        // TODO: improve assertion using anchor logs
      }
    )
})

This test should pass, with an AnchorError being logged that says the address is already in use.

Instead it fails, with the actual error thrown by that second transaction looking like this:

SendTransactionError: failed to send transaction: Transaction simulation failed: This transaction has already been processed

It seems that something is considering the two transactions to be the same transaction, and refusing to send the second one.

One workaround is to send the transaction as another user, so it's meaningfully different. But is there a better/more standard workaround for this problem? I'd expect it to be possible to send the same transaction as the same user twice, and for the program to be responsible for deciding what should happen in that case.

2 Answers 2

4

I'd expect it to be possible to send the same transaction as the same user twice, and for the program to be responsible for deciding what should happen in that case.

This is what's known as a "double-spend" and one of the core problems that blockchains solve. The behavior you're observing is expected. I suspect that the transactions are being executed so quickly that both get the same recent_blockhash with high probability.

You need to manipulate the transaction such that the message body and hence the signature change. Typically this is done by ensuring the recent_blockhash is different for otherwise identical transactions. Another trick is to append some other instruction to use as a nonce, system self-transfers of monotonically increasing lamports size are common.

Sorry, I don't know enough about anchor to supply examples of how to modify your code. Hopefully someone else will come along who can.

2
  • 2
    Damnit, Trent. Sniped me again. Commented Jul 29, 2022 at 22:52
  • 1
    Relevant section from the docs, Callum: “A transaction includes a recent blockhash to prevent duplication and to give transactions lifetimes. Any transaction that is completely identical to a previous one is rejected, so adding a newer blockhash allows multiple transactions to repeat the exact same action.” Commented Jul 29, 2022 at 22:54
2

Trent's answer explains exactly what's going on so I've accepted it.

In case it's helpful, this is how to modify the code to add instructions as mentioned in that response:

await program.methods
  .createPixel(x, y, 0, 0, 255)
  .accounts({
    pixel: pixelPublicKey,
    user: anchorProvider.wallet.publicKey,
    systemProgram: web3.SystemProgram.programId,
  })
  .postInstructions([
    web3.SystemProgram.transfer({
      fromPubkey: anchorProvider.wallet.publicKey,
      toPubkey: web3.Keypair.generate().publicKey,
      lamports: 1,
    })
  ])
  .rpc()
  .then(
    // ...
  )

We can also use preInstructions to put the instruction before the Anchor one.

As an alternative, you can use .transaction() instead of .rpc() to return the transaction. However, this gets a bit fiddly because anchorProvider.sendAndConfirm(tx) overwrites the recentBlockhash using getRecentBlockhash: https://github.dev/coral-xyz/anchor/blob/205e9d875f9abf1fc379cfc8114c5c53f0207141/ts/src/provider.ts#L136 - which can be the same multiple transactions in a row. This is I think the root cause of the issue: rpc() uses sendAndConfirm and sets the same recentBlockhash on both transactions. But it also means you can't set a later blockhash manually, sendAndConfirm will reset it. I'm going to look at whether I can get a fix for this into Anchor.

Also if you do use transaction() and then sendAndConfirm, you'll want to make sure to translate the errors the same way Anchor does to get a much more helpful error message: https://github.dev/coral-xyz/anchor/blob/205e9d875f9abf1fc379cfc8114c5c53f0207141/ts/src/program/namespace/rpc.ts#L28-L29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.