Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug] Transaction signature verification failure #459

Open
robertvo opened this issue Jun 19, 2024 · 4 comments
Open

[Bug] Transaction signature verification failure #459

robertvo opened this issue Jun 19, 2024 · 4 comments
Assignees
Labels
bug Something isn't working

Comments

@robertvo
Copy link

robertvo commented Jun 19, 2024

Describe the bug
Get error from Solana network "Transaction signature verification failure"

To Reproduce
Create a consolidation transaction with 4 transfers from 4 input addresses to 1 output

There's some kind of signature mismatch. It always works with 1 input, it SOMETIMES works with 3 and it never works with 4.

@robertvo robertvo added the bug Something isn't working label Jun 19, 2024
@AbanoubNassem
Copy link

any updates ? have you figured a work around ?

@BifrostTitan
Copy link
Contributor

This is from Solnet.Serum. Maybe it will help bypass the verification issue.
When certain transactions are deserialized the public keys for the signatures are pulled from the instructions which can be incorrect.

Create a TransactionBuilder instance and create your transaction by adding the instructions etc then obtain signatures from all the signers and add them to the signature list. Try populating the signatures manually.

I will look into the web3.js source code and take a look at the typescript implementation to see if I can find a better way to serialize and break down transactions.

   byte[] txBytes = txBuilder.CompileMessage();

   byte[] signatureBytes = TraderWallet.Sign(txBytes);

   List<byte[]> signatures = new() { signatureBytes };
   signatures.AddRange(_signers.Select(signer => signer.Sign(txBytes)));
   _signers.Clear();

   Transaction tx = Transaction.Populate(Message.Deserialize(txBytes), signatures);
    return await rpc.SendTransactionAsync(tx.Serialize());

@robertvo
Copy link
Author

robertvo commented Sep 7, 2024

I did not figure this out. Instead of making 4 transfers in one transactions, I'm sending 4 separate transactions :(

@pfleeter
Copy link

pfleeter commented Feb 13, 2025

I believe I have a clue for this.

I have found that when I add signers, success becomes less likely. I further demonstrated in my code that when serializing a transaction, the order of the accounts and the order of the signatures are not matched, which is required.

I wrote the following unit test, which just creates a transaction with increasing numbers of signers and does a "waterfall" transfer of 1 lamport between them. So, if there is 1 transfer, there are two signers and 3 accounts. signers: fee payer, a sender; non-signer a receiver. If there were two transfers there is fee payer A, B -> C, then C->D (so 3 signers: A, B and C) and so on.

This is repeated from 1 transfer to 7. At 1 transfer, success is about 100% (1000 iterations). By the time you get to 7 it will sometimes succeed 1 / 1000 times.

When printing the success cases, it appears that if the non-fee paying signers happen to be in alphabetical order, it works.

Here are the logs from this test code:

Transfers: 1, Success Count: 1000 Alphabetical Non-Fee Payer Signers Count: 1000
Transfers: 2, Success Count: 497 Alphabetical Non-Fee Payer Signers Count: 497
Transfers: 3, Success Count: 176 Alphabetical Non-Fee Payer Signers Count: 176
Transfers: 4, Success Count: 37 Alphabetical Non-Fee Payer Signers Count: 37
Transfers: 5, Success Count: 8 Alphabetical Non-Fee Payer Signers Count: 8
Transfers: 6, Success Count: 1 Alphabetical Non-Fee Payer Signers Count: 1
Transfers: 7, Success Count: 0 Alphabetical Non-Fee Payer Signers Count: 0

record IterationStats(int SuccessCount, int AlphabetizedCount);

[Fact]
public async Task TestSolnetTransactionSigningSerialization()
{
    var successCounts = new Dictionary<int, IterationStats>();
    var attemptCount = 1000;
    var minTransfers = 1;
    var maxTransfers = 7;

    for (var transfers = minTransfers; transfers <= maxTransfers; transfers++)
    {
        var successCount = 0;
        var alphabetizedCount = 0;

        for (var i = 0; i < attemptCount; i++)
        {
            var numberOfAccounts = transfers + 2; // fee payer, sender, receiver
            var accountList = new List<Account>(numberOfAccounts);
            for (var j = 0; j <= numberOfAccounts; j++)
            {
                accountList.Add(new Account());
            }
        
            var transaction = new Transaction
            {
                FeePayer = accountList[0].PublicKey
            };

            for (var j = 1; j <= transfers; j++)
            {
                transaction.Add(SystemProgram.Transfer(accountList[j], accountList[j+1], 1));
            }
          
            // just something
            transaction.RecentBlockHash = "BtQ4EEbRrNbFiMf3g16hdhkN9VLSwBtYjbdrW5rDar5A";

            var signersList = new List<Account>(transfers + 1);
            for (var j = 0; j <= transfers; j++)
            {
                signersList.Add(accountList[j]);
            }
            transaction.Sign(signersList);

            // In this case, things always succeed because the signers are pulled from the signers list as they are added
            // above
            Assert.True(transaction.VerifySignatures());

            // Hypothesis: serialization only creates an accounts list that matches the order of the serialized signers
            // when the non-fee payer signers are in alphabetical order
   
            var signersPublicKeys = signersList.Select(s => s.PublicKey.ToString()).ToList();
            // Get all but the first signers (ignore the fee payer)
            var nonFeePayerSignerPublicKey = signersPublicKeys.Slice(1, signersPublicKeys.Count - 1).ToList();
            var sortedSignersList = nonFeePayerSignerPublicKey.Select(s => s).OrderBy(x => x).ToList();
            var signersListWasAlphabetical = nonFeePayerSignerPublicKey.SequenceEqual(sortedSignersList);
            if (signersListWasAlphabetical)
            {
                // The non-fee payer signers happened to be in alphabetical order
                alphabetizedCount++;
            }
            
            var bytes = transaction.Serialize();

            var deserializedTransaction = Transaction.Deserialize(bytes);

            // This may fail, because the signers have to be loaded in from the serialized message, in which the 
            // signers header may be in a different order than the accounts list
            if (deserializedTransaction.VerifySignatures())
            {
                successCount++;
                
                if (transfers == maxTransfers)
                {
                    _testOutputHelper.WriteLine($"Success w/ transfer count: {transfers}.  " +
                                                $"Signer keys in order were: {string.Join("\n", signersPublicKeys)}");
                }
            }
        }
        
        successCounts.Add(transfers, new IterationStats(successCount, alphabetizedCount));
    
        // Assert.Equal(attemptCount, successCount);
    }
    
    foreach (var (transfers, iterationStats) in successCounts)
    {
        _testOutputHelper.WriteLine(
            $"Transfers: {transfers}, " +
            $"Success Count: {iterationStats.SuccessCount} " +
            $"Alphabetical Non-Fee Payer Signers Count: {iterationStats.AlphabetizedCount}");
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

5 participants