solana: Web3.js transaction serialize => deserialize => serialize can produce "Signature verification failed" due to bug in Message serialization
Problem
The logic here https://github.com/solana-labs/solana-web3.js/blob/74007a55afa29b36e0535bb9bcff3b64dda9b19e/src/transaction.ts#L261-L281
Can lead to different orderings to accountMetas after serializing and deserializing. The “content” of the transaction is the same, however the accountKeys flipped around. So long as the indices still hold true for writable accounts, the transaction will function the same.
The issue is that this is then serialized and used to create a transaction signature. So this non-determinism can mean that a previously correct signature is now incorrect.
Code to reproduce:
const signer = Keypair.generate();
const acc0Writable = Keypair.generate();
const acc1Writable = Keypair.generate();
const acc2Writable = Keypair.generate();
const t0 = new Transaction({
recentBlockhash: "HZaTsZuhN1aaz9WuuimCFMyH7wJ5xiyMUHFCnZSMyguH",
feePayer: signer.publicKey
});
t0.add(new TransactionInstruction({
keys: [{
pubkey: signer.publicKey,
isWritable: true,
isSigner: true
}, {
pubkey: acc0Writable.publicKey,
isWritable: true,
isSigner: false
}],
programId: Keypair.generate().publicKey
}))
t0.add(new TransactionInstruction({
keys: [{
pubkey: acc1Writable.publicKey,
isWritable: false,
isSigner: false
}],
programId: Keypair.generate().publicKey
}))
t0.add(new TransactionInstruction({
keys: [{
pubkey: acc2Writable.publicKey,
isWritable: true,
isSigner: false
}],
programId: Keypair.generate().publicKey
}))
t0.add(new TransactionInstruction({
keys: [{
pubkey: signer.publicKey,
isWritable: true,
isSigner: true
},{
pubkey: acc0Writable.publicKey,
isWritable: false,
isSigner: false
}, {
pubkey: acc2Writable.publicKey,
isWritable: false,
isSigner: false
}, {
pubkey: acc1Writable.publicKey,
isWritable: true,
isSigner: false
}],
programId: Keypair.generate().publicKey
}))
t0.partialSign(signer);
const t1 = Transaction.from(t0.serialize());
t1.serialize()
A workaround is to, before pre-signing the transaction, run Transaction.from(tx.serialize({ requireAllSignatures: false })). Then sign the transaction and serialize. This ensures the ordering doesn’t get jumbled.
Proposed Solution
Add a default sorting here based on the alphabetical (or just numerical) sorting of the public keys so that this function is deterministic.
About this issue
- Original URL
- State: closed
- Created 3 years ago
- Reactions: 2
- Comments: 23 (11 by maintainers)
Commits related to this issue
- [#21722] Fix non-deterministic writeable account order — committed to ChewingGlass/solana by ChewingGlass 3 years ago
I dug into the code and came up with what I think is causing this. Related: #23641, #23610, https://github.com/solana-labs/wallet-adapter/issues/350
https://github.com/solana-labs/solana/blob/17cc095d2892654225b5efe91b55c19eb932bfd3/web3.js/src/message.ts#L12-L25
Message, and the wire format of the message, does not store information that allowsAccountMetato be recreated per instruction. Only the number of signatures required, and the numbers of read-only accounts (signed and unsigned) are stored in the message header.https://github.com/solana-labs/solana/blob/17cc095d2892654225b5efe91b55c19eb932bfd3/web3.js/src/transaction.ts#L705-L709 Because of 1), when a Transaction is populated from a
Message, if an account hasisSignerorisWritableflags for any instruction, these flags will be propagated to the account for every instruction in the transaction.https://github.com/solana-labs/solana/blob/17cc095d2892654225b5efe91b55c19eb932bfd3/web3.js/src/transaction.ts#L261-L270 Because of 2), when
AccountMetaare sorted byisSignerandisWritable, accounts that didn’t haveisSignerorisWritableflags before serialization may have them after deserialization. The order of these is likely to change.https://github.com/solana-labs/solana/blob/17cc095d2892654225b5efe91b55c19eb932bfd3/web3.js/src/transaction.ts#L272-L285 Because of 3),
AccountMetawill be iterated over in a different order to gather the unique keys.https://github.com/solana-labs/solana/blob/17cc095d2892654225b5efe91b55c19eb932bfd3/web3.js/src/transaction.ts#L327-L343 Because of 4), the signed and unsigned keys will be collected in a different order and concatenated.
https://github.com/solana-labs/solana/blob/17cc095d2892654225b5efe91b55c19eb932bfd3/web3.js/src/message.ts#L191-L196 Because of 5), the keys will be serialized as bytes in a different order. When these bytes are signed, this will result in a different signature.
In short, because of 1),
TransactiontoMessageperforms lossy compression. You can’t deterministically serialize aTransactionto aMessageand deserialize thisMessageback to the originalTransaction.Fix should be released in
@solana/web3.js@1.39.1PR attempt @ #23720, please review!
No. Wrong direction.
Transactionis a container for aMessageand its signatures, nothing moreI don’t think it’s useful to enforce or follow any kind of canonical account key sorting method. The way
Transaction.populateis decompiling the message account keys is losing the ordering used by the processed transaction so I think we have to get rid of that.Hi @jstarry! Is the order of serialization part of the Solana spec officially? Should the Solana Documentation be updated? We have some problem here when we need to communicate between Dart Library and Js Library: https://github.com/cryptoplease/cryptoplease-dart/pull/184
Hey, do you think you can help me? https://github.com/solana-labs/solana/issues/22391 I think you have described my problem. But Im using offline signing, so I cant user partialSign because I dont have signer object. If you will refactor my code to needed or just give advice I would be really happy!