3

The Transaction class has a sign and partialSign method. What are the differences between them and what are the different use cases of using partialSign over sign

3 Answers 3

4

I did a bunch more digging, and I think the real difference is that sign updates this.signers on the transaction, while partialSign does not. In practice that means that calling sign invalidates previous signatures, while calling partialSign does not.

You can see this in the source code: https://github.com/solana-labs/solana-web3.js/blob/194d2d1/src/transaction.ts#L620

The sign and partialSign functions are the same, except that sign contains:

this.signatures = uniqueSigners.map(signer => ({
  signature: null,
  publicKey: signer.publicKey,
}));

I've made an example which is based on the cookbook example, but hopefully a bit clearer: https://replit.com/@mcintyre94/sign-partial-sign#index.js

If you run this as-is it should work, with output similar to:

Setup complete!
Created the transaction, signed by Bob
Recovered the transaction as Alice
Signed the transaction as Alice
serialized te transaction
Sent transaction 5x25WynLSyN7Dyufj7FgkK7jkvLjf2L4FJpvwkQK38gNiRU4HmY4ZrtE2ZwNEt7CfAsa9tLoccbiMk19wEJY2ho1
Confirmed transaction 5x25WynLSyN7Dyufj7FgkK7jkvLjf2L4FJpvwkQK38gNiRU4HmY4ZrtE2ZwNEt7CfAsa9tLoccbiMk19wEJY2ho1

It uses the same pattern: Bob signs first, then Alice, then the transaction is sent.

If you look at the api function you'll see that Bob is using sign:

  // Partial sign as Bob
  transaction.sign(bobKeypair);

  // Serialize the transaction and convert to base64 to return it
  const serializedTransaction = transaction.serialize({
    // We will need Alice to deserialize and sign the transaction
    requireAllSignatures: false,
  });

  const transactionBase64 = serializedTransaction.toString("base64");
  return transactionBase64;

It also works if that's changed to partialSign.

But Alice uses partialSign:

  // Recover the transaction by de-serializing it
  const recoveredTransaction = Transaction.from(
    Buffer.from(transactionBase64, "base64")
  );
  console.log('Recovered the transaction as Alice')

  // Partial sign it as Alice
  recoveredTransaction.partialSign(aliceKeypair)
  console.log('Signed the transaction as Alice')

If you update it so that Alice uses sign instead you'll get an error:

Setup complete!
Created the transaction, signed by Bob
Recovered the transaction as Alice
Signed the transaction as Alice
/home/runner/sign-partial-sign/node_modules/@solana/web3.js/lib/index.cjs.js:3146
      throw new Error('Signature verification failed');
            ^

Error: Signature verification failed
    at Transaction.serialize (/home/runner/sign-partial-sign/node_modules/@solana/web3.js/lib/index.cjs.js:3146:13)
    at file:///home/runner/sign-partial-sign/index.js:132:43
repl process died unexpectedly: exit status 1

I think what's happening here is that sign updated this.signers and that invalidated Bob's signature.

This is also why the code Alice uses to send the transaction is really weird. You can't call connection.sendTransaction(recoveredTransaction, [aliceKeypair]) because you'll get the verification error. I'm guessing this is because sendTransaction uses sign and thus invalidates Bob's signature.

I think this example is better than my cookbook one because it shows where you must use partialSign. Bob doesn't need to, Alice does.

6
  • 1
    Thank you for the detail and the repl!!
    – C.OG
    Jul 22 at 9:24
  • 1
    hmm was thinking it would be possible for; Bob to partially sign and Alice signs the transaction.
    – C.OG
    Jul 22 at 9:39
  • 1
    Do you mean for Alice to sign with sign or with partialSign? sign will invalidate Bob's signature (even if Bob used partialSign), but Alice could use partialSign and that'd work. See this fork of the REPL where Bob uses partialSign and Alice uses sign, it has the same signature verification error: replit.com/@mcintyre94/partial-sign-sign#index.js
    – Callum M
    Jul 22 at 10:03
  • 1
    Yup, I wasn't sure why Alice calling sign would invalidate Bob's signature if Bob signed with partialSign
    – C.OG
    Jul 22 at 10:07
  • 1
    I think it's because Alice's call to sign sets this.signatures on the transaction, and updating any field on the transaction invalidates previous signatures. In many cases that's ideal - you don't want someone changing the feePayer or fiddling with instructions after you've signed it. But in this use case you do want someone else to be able to sign it afterward, which they can do using partialSign which doesn't update this.signers and doesn't invalidate the previous signatures.
    – Callum M
    Jul 22 at 10:10
4

To discuss this, this we first need to talk about offline transactions.

There are 3 steps to submitting a transaction: 1) create the transaction, 2) sign it, and 3) broadcast it to the chain. In most cases, all three happen immediately one after another but strictly speaking, it does not have to!

For example, you can create a transaction and sign it, then send it to someone else to verify and submit it. A more interesting scenario is where you create and sign the transaction, serialise it and send it to a second party, who also has to sign it and then finally submit it to the chain.

Solana Cookbook does mention some scenarios where this is useful:

  • Send an SPL token in return for payment
  • Sign a transaction so that you can later verify its authenticity
  • Call custom programs in a transaction that require your signature
4
  • 2
    I wrote that cookbook example and can elaborate a bit on the motivating use case for me. With Solana Pay transaction requests, you need to write an API that returns a serialized transaction that can be signed and broadcast by the user('s wallet). If you want that transaction to send something back to the user (like an NFT or an SPL token) then you need to partially sign it with the authority to do that, before returning it from that API.
    – Callum M
    Jul 21 at 15:08
  • 1
    That's awesome. Could you clarify; In the example you mentioned, is it possible for the merchant to sign the transaction AND have the user eventually sign? or can it only be partialSign + sign?
    – C.OG
    Jul 21 at 15:27
  • I've posted a separate answer, it ended up being way too detailed for a comment and includes a better example than the cookbook one. In short though, the merchant can sign or partialSign, the user must partialSign regardless which one the merchant did.
    – Callum M
    Jul 21 at 18:05
  • 2
    Thanks for the deep dive, @CallumM. I upvoted yours!
    – sohrab
    Jul 21 at 23:41
3

Both sign and partialSign are used to add signatures to a transaction.

A transaction needs someone to pay the fee. In case the feePayer is not provided for a transaction then the first signer will be used for paying the fee.

partialSign is used to add signatures to a transaction. For instance if you are using createAccount then you need to make sure the new account is also a signer.

Here's an example of using partialSign


let randomKey = SolanaKeypair.generate();

let programId = TOKEN_PROGRAM_ID;

let associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID;

const associatedToken = await getAssociatedTokenAddress(randomKey.publicKey,phantomPublicKey,false,programId,associatedTokenProgramId);
    const recentBH = await connection?.getRecentBlockhash();

let t = new Transaction({
      feePayer: phantomPublicKey,
      recentBlockhash: recentBH.blockhash
    })
    .add(
      SystemProgram.createAccount({
      fromPubkey: phantomPublicKey,
      newAccountPubkey: randomKey.publicKey,
      space: MintLayout.span,
      lamports: await getMinimumBalanceForRentExemptMint(connection),
      programId: TOKEN_PROGRAM_ID,
    })
    ,
    createInitializeMintInstruction(
      randomKey.publicKey,
      7,
      phantomPublicKey,
      phantomPublicKey,
      TOKEN_PROGRAM_ID
    )
    )
    .add(
      createAssociatedTokenAccountInstruction(
        phantomPublicKey,
          associatedToken,
          phantomPublicKey,
          randomKey.publicKey,
          programId,
          associatedTokenProgramId
      )
  ).add(
    createMintToInstruction(
      randomKey.publicKey,
        associatedToken,
        phantomPublicKey,
        67000000000,
        [],
        programId
    ));
    
    
    const sendT = await providerSolana.signTransaction(t); // This triggers Phantom wallet so that user can sign transaction

sendT.partialSign(randomKey);  // This adds the randomKey signature which will be the new Mint

    const fsignature = await connection.sendRawTransaction(sendT.serialize());

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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