Skip to content

fix: use proper serialization for versioned messages in get_fee_for_m…#7719

Merged
steveluscher merged 3 commits intoanza-xyz:masterfrom
swarna1101:fix/versioned-message-serialization-get-fee
Sep 22, 2025
Merged

fix: use proper serialization for versioned messages in get_fee_for_m…#7719
steveluscher merged 3 commits intoanza-xyz:masterfrom
swarna1101:fix/versioned-message-serialization-get-fee

Conversation

@swarna1101
Copy link
Copy Markdown

@swarna1101 swarna1101 commented Aug 26, 2025

Fix versioned message serialization in get_fee_for_message

Problem

get_fee_for_message was failing with "index out of bounds" errors when used with v0 messages containing Address Lookup Tables. The root cause was that bincode::serialize() doesn't include the MESSAGE_VERSION_PREFIX (0x80) required for v0 messages, causing the RPC endpoint to misinterpret the serialized message format.

Solution

Added a serialize_message() method to the SerializableMessage trait that uses the proper native serialization for each message type. For v0 messages, this includes the required version prefix, while legacy messages remain unchanged. Updated get_fee_for_message() to use this new serialization method instead of bincode::serialize().

The fix ensures that both legacy messages and v0 messages with Address Lookup Tables are properly serialized when calling get_fee_for_message(). Added comprehensive tests to verify the serialization behavior and ensure no regressions.

Closes #7563

Can you pls check this @KirillLykov @0xbrw

@mergify mergify Bot requested a review from a team August 26, 2025 07:27
@0xbrw
Copy link
Copy Markdown

0xbrw commented Aug 26, 2025

@KirillLykov would you know the right person to review this?

@KirillLykov
Copy link
Copy Markdown

@steveluscher probably is the best one

@KirillLykov
Copy link
Copy Markdown

Also this PR seems to fix the problem, it introduces some additional code. I wonder if it is possible to reuse serialize_and_encode: it calls let serialized = serialize(input) and this input should implement serde::Serialize. It seems that both are implementing serde::Serialize, so it should call correctly method serialize of each. But maybe I don't get something

@KirillLykov KirillLykov added the CI Pull Request is ready to enter CI label Aug 27, 2025
@anza-team anza-team removed the CI Pull Request is ready to enter CI label Aug 27, 2025
@swarna1101
Copy link
Copy Markdown
Author

Also this PR seems to fix the problem, it introduces some additional code. I wonder if it is possible to reuse serialize_and_encode: it calls let serialized = serialize(input) and this input should implement serde::Serialize. It seems that both are implementing serde::Serialize, so it should call correctly method serialize of each. But maybe I don't get something

Good point! The issue is that serialize_and_encode uses bincode::serialize(input) which calls the serde serialization trait, but for v0 messages we need the native serialize() method that includes the MESSAGE_VERSION_PREFIX.

The key difference:

  • bincode::serialize(&v0_message) → No version prefix ❌ (causes "index out of bounds")
  • v0_message.serialize() → Includes MESSAGE_VERSION_PREFIX ✅ (works correctly)

We could modify serialize_and_encode to detect message types, but this would break its generic nature and risk affecting other use cases. This approach keeps the existing function unchanged while being explicit about using the proper serialization method for each message type.

@KirillLykov
Copy link
Copy Markdown

KirillLykov commented Aug 28, 2025

@swarna1101 Hm, I got you.
I wonder if there is a way to reuse here the code from https://github.com/anza-xyz/solana-sdk/blob/master/message/src/versions/mod.rs#L179 instead of introducing the new one?

PS CI fails with extra whitespaces: git diff origin/master --check --oneline

@swarna1101
Copy link
Copy Markdown
Author

Hm, I got you. I wonder if there is a way to reuse here the code from https://github.com/anza-xyz/solana-sdk/blob/master/message/src/versions/mod.rs#L179 instead of introducing the new one?

PS CI fails with extra whitespaces: git diff origin/master --check --oneline

I've refactored to use the existing VersionedMessage from solana-sdk/message/src/versions/mod.rs#L179 instead of the custom trait. It uses proven serialization logic that handles MESSAGE_VERSION_PREFIX. Removed the unnecessary SerializableMessage trait entirely and updated get_fee_for_message() to work directly with VersionedMessage. Tests pass and whitespace issues fixed. 👍

Ok(encoded)
}

fn serialize_and_encode_versioned_message(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure that we need a new serialize_and_encode_versioned_message function? It looks to me serialize_and_encode will work, won't it?

Comment thread rpc-client/src/rpc_client.rs Outdated

#[test]
fn test_versioned_message_serialization_includes_prefix() {
use solana_message::{v0, Message as LegacyMessage, compiled_instruction::CompiledInstruction, v0::MessageAddressTableLookup, MessageHeader};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we typically avoid using function-level imports.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed all function-level use statements from the test functions since the required types are already imported in the test module's use block

Comment thread rpc-client/src/rpc_client.rs Outdated
fn test_versioned_message_serialization_includes_prefix() {
use solana_message::{v0, Message as LegacyMessage, compiled_instruction::CompiledInstruction, v0::MessageAddressTableLookup, MessageHeader};

// Test that VersionedMessage serialization includes MESSAGE_VERSION_PREFIX
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would move this comment to be for the whole test, so above #[test]

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved

message: &VersionedMessage,
) -> ClientResult<u64> {
let serialized_encoded = serialize_and_encode(message, UiTransactionEncoding::Base64)?;
let serialized_encoded = serialize_and_encode_versioned_message(message, UiTransactionEncoding::Base64)?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if there are tests for this get_fee_for_message function? If they exist and easy to add a new one, I think it might be valuable to have tests for this function instead of serialization of VersionedMessage because the later lives in sdk.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that we should test the actual get_fee_for_message function rather than just serialization since VersionedMessage is already well-tested in the SDK.

Currently there aren't specific tests for get_fee_for_message - adding proper ones would need either integration tests with a real RPC server or mock-based tests.

Since this PR focuses on the serialization bug fix and our tests do validate the fix works (v0 messages get proper version prefix), I think they serve their purpose here. But I agree comprehensive function tests would be valuable as a follow-up.

Would you prefer adding mock tests in this PR or keeping this focused and doing function tests separately?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add a unit test with mock rpc as part of the bug fix (this one) but for backporting it to 2.3.x, it is might be better to make a follow up so this PR is without tests.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, will be great if we can create an issue to track it, happy to do that

@mergify
Copy link
Copy Markdown

mergify Bot commented Aug 29, 2025

Backports to the stable branch are to be avoided unless absolutely necessary for fixing bugs, security issues, and perf regressions. Changes intended for backport should be structured such that a minimum effective diff can be committed separately from any refactoring, plumbing, cleanup, etc that are not strictly necessary to achieve the goal. Any of the latter should go only into master and ride the normal stabilization schedule.

@steveluscher
Copy link
Copy Markdown

@jstarry, do you still have the versioned message stuff loaded into your head enough that this is a quick review for you?

@steveluscher steveluscher requested a review from jstarry August 29, 2025 20:36
@KirillLykov KirillLykov added the CI Pull Request is ready to enter CI label Sep 3, 2025
@anza-team anza-team removed the CI Pull Request is ready to enter CI label Sep 3, 2025
@KirillLykov KirillLykov added the CI Pull Request is ready to enter CI label Sep 3, 2025
@anza-team anza-team removed the CI Pull Request is ready to enter CI label Sep 3, 2025
@mergify
Copy link
Copy Markdown

mergify Bot commented Sep 3, 2025

Backports to the beta branch are to be avoided unless absolutely necessary for fixing bugs, security issues, and perf regressions. Changes intended for backport should be structured such that a minimum effective diff can be committed separately from any refactoring, plumbing, cleanup, etc that are not strictly necessary to achieve the goal. Any of the latter should go only into master and ride the normal stabilization schedule. Exceptions include CI/metrics changes, CLI improvements and documentation updates on a case by case basis.

Copy link
Copy Markdown

@steveluscher steveluscher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your original approach of trying to teach SerializableMessage to do the right thing was less intrusive, and required fewer changes. I think the current version of this PR where you force folks to use get_fee_for_message_with_legacy sometimes and get_fee_for_message other times is worse.

With a few tweaks, wouldn't your original change work?

diff --git a/rpc-client/src/nonblocking/rpc_client.rs b/rpc-client/src/nonblocking/rpc_client.rs
index e4e1396abc..293228b07b 100644
--- a/rpc-client/src/nonblocking/rpc_client.rs
+++ b/rpc-client/src/nonblocking/rpc_client.rs
@@ -4671,7 +4671,8 @@ impl RpcClient {
         &self,
         message: &impl SerializableMessage,
     ) -> ClientResult<u64> {
-        let serialized_encoded = serialize_and_encode(message, UiTransactionEncoding::Base64)?;
+        let serialized = message.serialize();
+        let serialized_encoded = BASE64_STANDARD.encode(serialized);
         let result = self
             .send::<Response<Option<u64>>>(
                 RpcRequest::GetFeeForMessage,
diff --git a/rpc-client/src/rpc_client.rs b/rpc-client/src/rpc_client.rs
index bf42e2fb06..567fc44ba0 100644
--- a/rpc-client/src/rpc_client.rs
+++ b/rpc-client/src/rpc_client.rs
@@ -62,9 +62,19 @@ impl RpcClientConfig {
 
 /// Trait used to add support for versioned messages to RPC APIs while
 /// retaining backwards compatibility
-pub trait SerializableMessage: Serialize {}
-impl SerializableMessage for LegacyMessage {}
-impl SerializableMessage for v0::Message {}
+pub trait SerializableMessage {
+    fn serialize(&self) -> Vec<u8>;
+}
+impl SerializableMessage for LegacyMessage {
+    fn serialize(&self) -> Vec<u8> {
+        self.serialize()
+    }
+}
+impl SerializableMessage for v0::Message {
+    fn serialize(&self) -> Vec<u8> {
+        self.serialize()
+    }
+}
 
 /// Trait used to add support for versioned transactions to RPC APIs while
 /// retaining backwards compatibility
@@ -3803,6 +3813,7 @@ mod tests {
         serde_json::{json, Number},
         solana_account_decoder::encode_ui_account,
         solana_account_decoder_client_types::UiAccountEncoding,
+        solana_hash::Hash,
         solana_instruction::error::InstructionError,
         solana_keypair::Keypair,
         solana_rpc_client_api::client_error::ErrorKind,

@jstarry
Copy link
Copy Markdown

jstarry commented Sep 4, 2025

Ugh, looks like I introduced this bug in solana-labs#28217 because I didn't add proper tests, apologies!

I think that we should do a few things:

  1. impl SerializableMessage for VersionedMessage
  2. Add fn version_prefix() -> Option<u8> to SerializableMessage and impl it for v0::Message
  3. Update fn get_fee_for_message to first serialize the version prefix if is_some

@jstarry
Copy link
Copy Markdown

jstarry commented Sep 4, 2025

@steveluscher I don't really understand how your suggestion would fix the issue. Where is the version prefix handled?

@steveluscher
Copy link
Copy Markdown

@steveluscher I don't really understand how your suggestion would fix the issue. Where is the version prefix handled?

Here's a one-liner that applies the patch, after which you can hyperclick around the calls to serialize() in the SerializableMessage part. That'll lead you right to it.

cat << EOF | git apply
diff --git a/rpc-client/src/nonblocking/rpc_client.rs b/rpc-client/src/nonblocking/rpc_client.rs
index e4e1396abc..293228b07b 100644
--- a/rpc-client/src/nonblocking/rpc_client.rs
+++ b/rpc-client/src/nonblocking/rpc_client.rs
@@ -4671,7 +4671,8 @@ impl RpcClient {
         &self,
         message: &impl SerializableMessage,
     ) -> ClientResult<u64> {
-        let serialized_encoded = serialize_and_encode(message, UiTransactionEncoding::Base64)?;
+        let serialized = message.serialize();
+        let serialized_encoded = BASE64_STANDARD.encode(serialized);
         let result = self
             .send::<Response<Option<u64>>>(
                 RpcRequest::GetFeeForMessage,
diff --git a/rpc-client/src/rpc_client.rs b/rpc-client/src/rpc_client.rs
index bf42e2fb06..567fc44ba0 100644
--- a/rpc-client/src/rpc_client.rs
+++ b/rpc-client/src/rpc_client.rs
@@ -62,9 +62,19 @@ impl RpcClientConfig {
 
 /// Trait used to add support for versioned messages to RPC APIs while
 /// retaining backwards compatibility
-pub trait SerializableMessage: Serialize {}
-impl SerializableMessage for LegacyMessage {}
-impl SerializableMessage for v0::Message {}
+pub trait SerializableMessage {
+    fn serialize(&self) -> Vec<u8>;
+}
+impl SerializableMessage for LegacyMessage {
+    fn serialize(&self) -> Vec<u8> {
+        self.serialize()
+    }
+}
+impl SerializableMessage for v0::Message {
+    fn serialize(&self) -> Vec<u8> {
+        self.serialize()
+    }
+}
 
 /// Trait used to add support for versioned transactions to RPC APIs while
 /// retaining backwards compatibility
@@ -3803,6 +3813,7 @@ mod tests {
         serde_json::{json, Number},
         solana_account_decoder::encode_ui_account,
         solana_account_decoder_client_types::UiAccountEncoding,
+        solana_hash::Hash,
         solana_instruction::error::InstructionError,
         solana_keypair::Keypair,
         solana_rpc_client_api::client_error::ErrorKind,
EOF

@swarna1101
Copy link
Copy Markdown
Author

Thanks a lot for all this discussions, I am checking them, let me get back

@steveluscher steveluscher self-assigned this Sep 16, 2025
@steveluscher steveluscher force-pushed the fix/versioned-message-serialization-get-fee branch 2 times, most recently from 9904aa8 to 5c486b2 Compare September 17, 2025 22:49
@steveluscher
Copy link
Copy Markdown

This PR has been rewritten, and a tight test case added to assert that what we're sending to getFeeForMessage is the correct bytes. Ready for review.

@steveluscher steveluscher force-pushed the fix/versioned-message-serialization-get-fee branch from 5c486b2 to cb41b5c Compare September 17, 2025 22:50
KirillLykov
KirillLykov previously approved these changes Sep 18, 2025
@KirillLykov
Copy link
Copy Markdown

But it fails CI

Comment thread rpc-client/src/rpc_client.rs Outdated
Co-authored-by: kirill lykov <lykov.kirill@gmail.com>
Comment thread rpc-client/src/rpc_client.rs Outdated
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Sep 18, 2025

Codecov Report

❌ Patch coverage is 92.85714% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.9%. Comparing base (b984691) to head (ae09fcc).
⚠️ Report is 2170 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #7719   +/-   ##
=======================================
  Coverage    82.9%    82.9%           
=======================================
  Files         823      823           
  Lines      360694   360763   +69     
=======================================
+ Hits       299159   299237   +78     
+ Misses      61535    61526    -9     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

&self,
message: &impl SerializableMessage,
) -> ClientResult<u64> {
let serialized_encoded = serialize_and_encode(message, UiTransactionEncoding::Base64)?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't new code does the same as the function serialize_and_encode but just in place? I wonder if we even need this function if it is a 2 lines of code.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not exactly. serialize_and_encode does bincode::serialize(thing) whereas the change does thing.serialize(). I'm always happy inlining code, but that's a matter for a different PR.

Copy link
Copy Markdown

@KirillLykov KirillLykov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the change itself looks good to me, left comment about the function serialize_and_encode.

@steveluscher steveluscher self-requested a review September 22, 2025 18:34
@steveluscher steveluscher dismissed their stale review September 22, 2025 18:34

I am steveluscher.

@steveluscher steveluscher merged commit 3f25767 into anza-xyz:master Sep 22, 2025
54 checks passed
mergify Bot pushed a commit that referenced this pull request Sep 22, 2025
#7719)

* fix: use proper serialization for versioned messages in get_fee_for_message

* Clippy satiated

Co-authored-by: kirill lykov <lykov.kirill@gmail.com>

* Try harder to satiate clippy

---------

Co-authored-by: Steven Luscher <steveluscher@users.noreply.github.com>
Co-authored-by: kirill lykov <lykov.kirill@gmail.com>
(cherry picked from commit 3f25767)
mergify Bot pushed a commit that referenced this pull request Sep 22, 2025
#7719)

* fix: use proper serialization for versioned messages in get_fee_for_message

* Clippy satiated

Co-authored-by: kirill lykov <lykov.kirill@gmail.com>

* Try harder to satiate clippy

---------

Co-authored-by: Steven Luscher <steveluscher@users.noreply.github.com>
Co-authored-by: kirill lykov <lykov.kirill@gmail.com>
(cherry picked from commit 3f25767)
steveluscher added a commit that referenced this pull request Sep 23, 2025
…_for_m… (backport of #7719) (#8138)

fix: use proper serialization for versioned messages in get_fee_for_m… (#7719)

* fix: use proper serialization for versioned messages in get_fee_for_message

* Clippy satiated



* Try harder to satiate clippy

---------



(cherry picked from commit 3f25767)

Co-authored-by: Swarna <swarnabhasinha@gmail.com>
Co-authored-by: Steven Luscher <steveluscher@users.noreply.github.com>
Co-authored-by: kirill lykov <lykov.kirill@gmail.com>
steveluscher added a commit that referenced this pull request Sep 23, 2025
…_for_m… (backport of #7719) (#8139)

fix: use proper serialization for versioned messages in get_fee_for_m… (#7719)

* fix: use proper serialization for versioned messages in get_fee_for_message

* Clippy satiated



* Try harder to satiate clippy

---------



(cherry picked from commit 3f25767)

Co-authored-by: Swarna <swarnabhasinha@gmail.com>
Co-authored-by: Steven Luscher <steveluscher@users.noreply.github.com>
Co-authored-by: kirill lykov <lykov.kirill@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RPC: Invalid serialiation for versioned messages in get_fee_for_message

8 participants