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
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.
-
1hmm was thinking it would be possible for; Bob to partially sign and Alice signs the transaction.– C.OG ♦Jul 22, 2022 at 9:39
-
1Do you mean for Alice to sign with
sign
or withpartialSign
?sign
will invalidate Bob's signature (even if Bob usedpartialSign
), but Alice could usepartialSign
and that'd work. See this fork of the REPL where Bob usespartialSign
and Alice usessign
, it has the same signature verification error: replit.com/@mcintyre94/partial-sign-sign#index.js– Callum MJul 22, 2022 at 10:03 -
1Yup, I wasn't sure why Alice calling
sign
would invalidate Bob's signature if Bob signed withpartialSign
– C.OG ♦Jul 22, 2022 at 10:07 -
1I think it's because Alice's call to
sign
setsthis.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 usingpartialSign
which doesn't updatethis.signers
and doesn't invalidate the previous signatures.– Callum MJul 22, 2022 at 10:10 -
1ahh ofcourse, I blocked out on the
this.signers
being updated. Thanks Callum!– C.OG ♦Jul 22, 2022 at 10:12
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
-
2I 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 MJul 21, 2022 at 15:08
-
1That's awesome. Could you clarify; In the example you mentioned, is it possible for the merchant to
sign
the transaction AND have the user eventuallysign
? or can it only be partialSign + sign?– C.OG ♦Jul 21, 2022 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
orpartialSign
, the user mustpartialSign
regardless which one the merchant did.– Callum MJul 21, 2022 at 18:05 -
2
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());