This tutorial compares three Rust serialization libraries for Solana programs: Borsh (the original library with clear patterns but less efficient), Bincode (more efficient but unmaintained due to a doxing incident), and Wincode (developed by Anza, the Solana validator team, which offers zero-copy support and better performance). The video demonstrates through a Solana program that Wincode can reduce compute unit costs by approximately 70% compared to Borsh when using zero-copy techniques, making it the recommended choice for production Solana programs.
Deep Dive
Prerequisite Knowledge
- No data available.
Where to go next
- No data available.
Deep Dive
Borsh vs Bincode vs Wincode [Solana Tutorial] - May 6th '25Added:
Hello and welcome to another Solana tutorial. Today we're going to talk about serialization and deserialization libraries because that's been on my list for a while and I really want to dig deeper into this and understand the differences between Borsh, Bincode, and WinCode. Cuz Solana programs have to do a lot of deserialization and serialization and so understanding how to work with those libraries and what the advantages of the individual libraries are can be really helpful. So yeah, let's compare Borsh, Bincode, and WinCode. And my idea for this is to create a Solana program, serialization [clears throat] demo. By the way, we are talking about Rust libraries. I should have mentioned that in the intro because on TypeScript we do serialization with other libraries, but we're specifically talking about Rust and I specifically want to do it in the context of Solana.
So we are going to write an actual Solana program. Could of course play with the libraries without Solana, but I want to do a Solana program just so we can compare CUs directly in it. Let's go. So this is not a Solana program yet and I'm going to try to not vibe code it today. Good old times for not vibe coding. Deploy your first Solana program. Now this is Anchor. I also specifically don't want to do Anchor.
Fine, I'll just take one of the program examples as a template and we're going to do native. Not Pinocchio, not Anchor.
So first of all, the line that we always keep forgetting to add. We got to put that. We got to put that here and then I'm a one-file maxi today. I'm going to put it all into one file. We're going to use the Solana entry point and we're going to have this process instruction here and in state, yeah, it's already using Borsh, which is going to copy that. Okay, and for that to work, we need to cargo add Solana program and Borsh is enabled as a feature. It is adding WinCode though. So, WinCode is also a dependency of Solana program.
Cool. And we will see that differences.
I guess we also need to add Porsche directly, though. So, so sure it also has this. Okay. Cool. So, now I just copied the example, and I'm going to write the instruction a bit. Actually, before we rewrite this, I'm going to be Okay. Let's define our own struct, cuz I don't want to work with this. Let's call it account data. That's what I put on the account. That's a fair name. So, what would I want to put on the account?
How about discriminator? Is that how you spell it?
And I'm going to do a U64 like Anchor would do. That's 8 bytes, right? And for fun, another number, a counter. I don't know. I'm not creative.
Another U64. And then, maybe something bigger like an address, which is a pubkey.
But, can I get that from address? Can you just get address instead? In Solana program 4.0, you should have address.
Otherwise, I'll just add Solana address.
I'm not going to let you annoy me here.
Solana address, address. Here. Yes, take that. And I can use address. Perfect.
And then, maybe a more complicated type like a vector or something of first, an optional address. First, an option for an address. Then, a vector of addresses.
Yes, now I'm not being creative anymore.
And to round it off, a string, cuz that ain't got fixed length. Just so we have different types in here. Oh, and let's YOLO it and put a U8, another byte, at the very end.
Random account data, because I can. I need something to compare. I actually While we're here, >> [laughter] >> can also have While we're doing so much with address, can also have an array of two addresses. Now that I managed to import address, I want to use it everywhere. This is just for an example.
I need something in there that we can deserialize and serialize that stuff.
So, yeah. And we have debug. So, this should be easily printable. So, let's really just start with this. I'm not even going to write something to an account yet, but I will create new account data. Let's do default in it, and I will give it an address and some text. And then it should return me me some account data with discriminator one because I decide that's what it is. The address I set to the address. The array of addresses I set to twice the address.
I'm being so creative here. The counter I set to zero because obviously that's where it starts. Also, the byte uh I set to 255 such that we have something else.
And then the option I set to some with the address. And the vector I do a vector with also twice the address. Why not? And then what's remaining? The text. Oh, yeah. Have some text. Okay, that's some default in it. So, I can create that here. Um literally just going to get some account data. I'm going to type it as well. And I'm going to do a Nope, not initializing it like this, but account data default in it.
And I'll default in it it with my program ID. And this is the first example.
Going to own it as a string. It's going to I I again, am I trying to be efficient here? No. I could work with a reference here such that you don't need to copy that, but do I need that later?
Nah, I'll just give it to you here. Here you go. I don't need the program ID anymore. Good.
And then now that we have that data, I want to somehow log it. So, I'm going to use message cuz again, here I'm not trying to be efficient. I just want something and I'm going to How did that go? Something like this.
That it debug prints me my data. Cuz obviously I could do the individual I could read the individual things from here. But this is the rust data. This is what will be the deserialized one just that we didn't do any deserialization here yet. We just created it in that format and we're just going to log it just to see how much compute it takes to just log this entire thing. So we have a baseline that we can then compare against. that's my Solana program. All it does is log this data struct thingy here. Let's see if we can cargo build SPF this. Nope. Pubkey not found in this scope. Hold on. Where am I still using pubkey? Here. Ah, yeah, you're an address. Trust me, bro. You're an address. I'll use those later. Okay.
Now, there we go. We do have warnings, but meh, whatever. All right, good enough. Let's build and deploy. Solana deploy our target deploy SOF file. Oop.
Oh, I keep doing this. Program.
Okay. Program deployed. Now we just need to run this. And I'm going to borrow the script from last video. And for now, all we're doing is one instruction to this program that we just deployed. There you go. No data, no accounts. Let's go. Now, in this transaction, it's just calling the serialization program. And all it does, it logs the account data struct with all its fields. That's the address.
That's the array of addresses. That's the optional address. That's the vector of addresses. And that's some text.
>> [laughter] >> I like this. And the thing I'm interested in is the CU profiling, or more specifically the one instruction which has 21,293 compute units. So in the ballpark of, you know, 21K. And this is just for the entry point creating that struct and logging it as a Solana message. That's it. So, that logging probably takes quite a few compute units. Let's see, without the logging, it would be Yeah, 185 compute units.
But, I'm not sure if it then completely optimized that stuff away, cuz if you're not using that, then maybe we don't need that at all. So, let's make sure we use it and just assert that the data byte is 255 or something. Just so we're using this. And that way we're still at still at 185. Yeah, interesting. But, maybe the compiler is again too smart for this.
Um just log the data byte.
Then we see what it costs us to log one simple message. Then we're at 600 compute units. Okay, 650 for logging this. Again, logging is expensive, but by far not as expensive as debug printing this entire thing. So, yeah, of course I could do that more efficiently.
Another reason why that is is so inefficient is because I have so many addresses in this thing. And for every address, it needs to convert the bytes to base 58. That also makes that super inefficient. That's why we end up with 20K compute. So, yeah, this certainly is just for debug purposes. Cuz if we log an address, then we're at 2,300 compute units.
And we were logging a lot of addresses.
So, yeah, that's where all that compute comes from. Maybe I shouldn't do that.
But, also no, it it doesn't matter as long as when I'm comparing, I serialize and deserialize the exact same amount of addresses. Otherwise, it's unfair comparison. I mean, we talked about that. You shouldn't log pub keys like that. But again, this is just for demo purposes. Anyway, it doesn't really matter. Cuz the thing we're really interested in is how much the serialization and deserialization costs us. So, let's work with actual accounts.
Let's say we're going to take one account and going to do it the not-so-clean way and call it my data account and just get it from the first one. A clean way would be to get an iterator and then next account and then throw up insufficient accounts if we don't have But Andy, you're setting a bad example. Okay, fine.
I'll do it that way then. Fine. I'll get that account iterator and I'll use that account iterator here and I'll raise that error if it appears. And then I have my data account. And as a step one, let's just do the serialization into that account. So, I'm going to get the same data [clears throat] still that I default in it myself and then I try to write that onto the data account. So, that's the serialization step where data account data I try to borrow mutably and and then I want to now write this. How did this example do it? It says serialize a mutable mutable account info data borrow mutably. Okay, I can do that. I will do this. So, that part that I had here, I do in there and I don't try so I don't need to unwrap. Okay.
Yeah, sure. So, I just use the data account, borrow the data, all of it and then serialize we have because we say borsh serialize we derive this. So, then my data will have a serialize and also the other way around the account data also has a deserialize, but we'll get there in a second. Let's first try the serialization. This data here we serialize onto the data account, which means that the data account must exist and have the right size and I'll refrain from logging again. Build and deploy. Oh oh. Oh yeah, of course that won't come back. Insufficient account keys for instruction. We even get that nice little program error because we were using this next account info. What we're missing, of course, are the accounts here. So, we need to give you a data account that you can write to. And in order to have a data account that you can write to, we need to create an account and assign it to the program.
So, data account.
Let's generate a key pair sign. So, then we put the data account address in here as writable. But, that alone won't work because the program can't access that.
Obviously, we need to first get a create account instruction from the system program where we actually create this data account and assign it to our program. You know what? I'm going to create the program address here like this. Boom. Then, I can use it here directly. Okay. Space in lamports? Well, lamports depend on space. Well, let's call it account size. That's the right word. That now depends on Well, what do we put in here? So, what's the size of this account? And that's variable size.
But, let's do the old-school counting.
We have 8 bytes, 8 bytes, 32 bytes, twice 32 bytes. And here, the option I think takes 4 bytes for Or is it 1 byte?
It's not a C option, so it should be 1 + 32. Although, I'm not sure actually how borsh really deserializes that. And the vector is also I think this one I think is 4 + the amount of addresses * 32. And in our case, X is 2. And also here, the string we have I think 4 + the length.
And this is simply again 1 byte. So, let's do the math. 218 + the string length. And the string length we used in this case + 55. So, that's our length, 273. And dum dum dum, send it. Okay, I'm actually not even sure if we could have a larger size here. I think when we borsh serialize, it expects it to fit exactly into this data, but I'm not sure. We can also try that. So, we created a create account instruction, and we created a called a program instruction, and put that in this transaction. Send it. Let's send it.
Voila! Look at that.
That works. And all of it just took 1,000 compute units. That's not too bad, actually. Especially cuz 150 is from the system program. So, it's just 870, not so bad. And let's check that account that we created, this one, and see what we find here. So, this is now the serialized version of our account data struct. So, first 8 bytes is the discriminator, then we have the counter that is zero. Then, we have the first address, and then we have the array of addresses, which again, twice the same address. Here, we don't have a length specifier, cuz it's an array. We know the length, it's fixed size two. Then, we have an option, so that's why we have this one here for hey, the option is present. And then, we once again have the same address. Then, we have a length specifier for the vector, saying hey, two times there's an address now, so that's again, twice that address. And then, comes the string with 37, so that's 3 * 16 + 7, which is 48 + 7, which is 55, which is exactly what I counted for the actual string length.
And then, that's the string, or better I should show it here, that's the string that ends here. And then, we have one more byte at 255 at the end. Nice. So, the serialization worked and it wasn't even that expensive. Just what was it?
Roughly 600 compute units. And now let's quickly let me do that test with I'm going to add an additional byte and see if that would still work or if Looks like that would still work and it would still just leave that byte empty. Yeah, it will just keep an empty byte. Okay, so we don't need to fit the size exactly with this. Perfect. But if we had not enough space, then it would complain.
Yeah, then we get failed to serialize or deserialize account data. Cool. Nice. I like it. And that by the way comes from this error here. So, if we if the serializer that gives me a result, that comes back with an error, I will just raise it. And when we looked at the errors, I made a video on that once, we saw a lot of the Borsh errors also being Solana errors. So, a lot of serialization deserialization errors that there were. But anyway, hey, that works. Great. It's still a bit inconvenient that I need to know the account size up front. Ideally, my program would create the account itself, so do it the CPI in the system program.
But that's not what I'm analyzing today.
I'm really just interested in comparing the serialization libraries. So, I'm fine with that. But, let's also look at deserialization. Cuz so far we created this and then we serialized it into the account. Let's do deserialization of that account as well. So, instead of creating the data myself, I deserialize it from the data account. So, my data will again be account data, but this time from >> [snorts] >> account data deserialize. And we're going to get the data account data borrowed. Why does this need to be mutable? Updates the buffer to point at remaining bytes. Oh, so you change the pointer of those expected mutable reference. Found mutable reference.
Fine.
This? No. This?
Rust. Okay. Let's see. So, we deserialize all of the data. Maybe we should also do something with the data like the counter we could increase by one such that it's an actual counter.
Oh, for that that needs to be mutable though. There we go. Make it mutable.
That thing lives on the stack in my program. That thing lives on the account and serialize it back onto the account.
Not super efficient, but should work.
Let's build and deploy and then we're not going to create a new account. We're just going to hardcode an address from before. This one. Here you go. So, we're going to recycle this account for now and there you go. Transaction went through. Now with a total of 1,400 compute units. So, that's an additional 600 compute units roughly for the deserialization step. And if we check the same account, it should have the same data except the counter is up one.
If we call it again, the counter should obviously be at two. Voilà. We built yet another counter program. Wow. Okay. But cool. The deserialization and serialization works and in a proper program you would do something similar for the instruction data. Should we do that as well really quick? So, that would look something like this. We would do another struct instruction data and we would also derive deserialize and serialize on the instruction data or really we would just need to derive the deserialize and our instruction would have also a discriminator maybe that is just a U8 here. Why not? Just so we can do different instructions like the init or the update whatever. And what would we take as instruction data? Maybe so we don't always write the program ID, but we take another address and then we take the text. How about that? We do address here, which is an address, and we do text, which is a string.
And then, we would use that instruction data and deserialize the instruction data the same way we would do with the account data. So, instruction data deserialize from this one as immutable reference. Can I borrow as mutable? Do I need to copy that input buffer then? That would be weird. Send example for that. Try from slice. Ah, I could try from slice. Okay, that makes sense. Let's try from slice cuz we have a slice here. This is slice. Check Check this out. And if we get a portion error, fire it. There we go. Then we have the instruction data, and then I could match based on the discriminator. I could say, "Match input discriminator for the case of, let's say, zero, we do the initialize. So, we're doing this. And one, we're doing the deserialize. So, we're doing this. And then, I set the data like that directly." And here it still complains that not all paths have been explored because it could also be another byte than any of those two, in which case I will error invalid instruction data.
For instance. Cool.
So, now this is the deserialization. And then we keep incrementing the counter, why not? And we do the serialization.
Great.
Or maybe we also want to use the address from the input. So, take this address and take the input text for the default in it. And do we also want to change the address when we deserialize it, or do we just change the counter? Cuz it's a bit weird if I would also do the other thing that I need to provide the address and the text, and then it's not written anyway. But yeah, whatever. Obviously, I could also just check for the first byte of the instruction data and then only in here try from slice the init instruction data for this part, but I'll just provide the address and the text every time and I ignored in this case.
Whatever. It doesn't need to be a super great program. I just want to compare serialization strategies. And the whole point of this was also for the instruction data we can do something like that. So, in that way it's good. I want it to deserialize the instruction data fully even if I don't use it just for my benchmark comparison. So, let's do both of that in the same transaction.
Let's go. Create an account. I need to adjust the length. This is using Porsche now. And that was 218 plus the length of this. Then we're working with big ends, but I we need a big end cuz this thing expects a big end, which is annoying. Still one of my favorite things to get annoyed by with a kid. Yeah. So, that's the account size.
That's just some text. And now we also need to when we initialize it actually provide data here. So, let's put data new uint 8 array. First byte is the discriminator. Then we encode an address. So, encode let's say we encode our payer address. Why not? And can I flatten this into here? Would that work?
I guess so. And then I also flatten uh text encoder to encode this text. Voilà.
So, that's the other side. I mean, I could do this with a struct even nicer, but we're not analyzing the TypeScript side of serialization. We just analyze the Rust side today. I just put the address, the 32 bytes for the address, and then the text. And then I'm going to do the same thing again, but this time I'm going to say one for update, so it should serialize the account. Oh, and this time we're going to use the actual data account here again, not the hard-coded address like this. There we go. So, we create a new account, then initialize it, and then deserialize it, and serialize it again. Just to see what the computed size of that is. Let's go.
Data build and deploy. Do it again just to be sure. And let's go. No, it doesn't work. Out of memory. What? We are out of memory? Well, that's inconvenient. Are we putting too much stuff on our stack?
Maybe.
>> [laughter] >> But why am I out of memory? Where am I out of memory? Memory allocation failed.
I want to know when that happens. So, I'm going to log some stuff. Even before. So, we get to our start, and then when we're doing this, we're getting out of memory. Instruction data try from slice. But it's not like that needs so much memory. It's just an address and some text. Oh, I see. Just some text, he says.
Possibly, my text and my UTF-8 encoder don't have the Oh, yeah. Yeah, I'm not even using a size prefix with this UTF-8 encoder. The UTF-8 encoder really just serializes the string and doesn't add the four bytes for length. So, I would need to get How was that? It's a variable size encoder. Finally, NPM has a dark mode. It's not very readable, but it has a dark mode. Size at codec size prefix. There we go. We need to add size prefix to the codec. And we want four bytes, so we get a U32 encoder. Maybe we need to use the codec entirely. Yeah, okay. This way we will add the four bytes for the length of the text. The problem was that it read the first four letters of the string and interpreted that as the size of the text. So, it allocated a lot of memory to be able to read that, which obviously doesn't work. So, yeah, that's my bad for calling it with the wrong data, but like this, it should work now. Yeah.
Okay, let's look at this. We should have three instructions now. One to create the account, one to initialize it.
That's the data, and one to deserialize and serialize again. Let's see what those things are in terms of compute. We have 1,163 and 1,600.
In both cases, we are deserializing the instruction data, and in this case, we're also deserializing the account data, which is taking more compute than if we just default in it such a struct.
Yeah, but I guess that's a good baseline now, a good benchmark this kind of a program, and the video is long enough.
It's time to move on to compare with other libraries. Cuz Borsh, we know Borsh. Borsh is what we've been looking at back in the days when I made videos about the Sol Dev course, and that's been, I don't know, 3 years ago, long time ago, where we were getting started with Solana development, where we were using Anchor and Borsh, and we looked at that deserialization and deserialization as one of the first videos in that series. So, we know this. This is nothing really new. It was just a refresher on how to work with it today, but let's actually compare it with a different library. We want to do the same thing now with that account data, just not use Borsh anymore. So, screw that Instead, we want to use BinCode. BinCode is now unmaintained due to a doxing harassment incident.
Development on BinCode has ceased.
Interesting. What people do to other people. They have listed WinCode though as an alternative, but WinCode was developed based on BinCode. So, I do want to look at VIN code first. So, cargo add VIN code and then we use VIN code. Let's see what that has. Also, let's remove Porsche here, please. Oh, no. Oh, no. They completely killed VIN code. Well played. This is VIN code.
Everything breaks now. There was a really I thought there was apparently this is a serious thing that really they got annoyed by that. Okay, then I cannot or if they don't want me to, I will not use VIN code and I will instead proceed right over to VIN code because that stuff is actually maintained by Anza and we know those guys. They developed the Solana validator client. So, we trust in them. Then, let's just move over to VIN code. Fine. I would have liked to do a VIN code versus VIN code comparison, but I guess I don't get to do that anymore.
Fine. I'm late to the game. Okay, VIN code it is. Fine. Screw VIN code. Let's go VIN code. Goodbye, VIN code. So, VIN code. There we go. This thing actually still has stuff to use. Great. So, we have schema read, schema write. I think those are the important ones. So, we instead of the Porsche deserialize, we'll do schema read and instead of the serialize, we'll do write. Schema write.
Okay, you're still complaining though.
Let's see. Why are you complaining here?
Cannot find derive macro.
Docs. Derive attributes. Enable the derive macros for schema read, schema write. Derive is disabled. Ah, so I guess I want to have VIN code with derive. How did they call? Minus features. Okay.
There we go. Now, we have derive. Now, we should be able to do this. Ha, nice.
No? Still not? VIN code is a bit more strict, I think, in what we can put here. It does not seem to like address cuz it doesn't implement a schema read and write. Hold on. Just testing.
If I were to use pub key, same same.
Okay. Okay.
And here the same, it doesn't like address. Maybe that's because it's an actual address and not a reference. Let me quickly check if that was having a lifetime and we are just using that.
Look, that changes things. So, we can't use address directly because that would do the copying copying, but we can use references. Okay. But then I probably also want to do that with ticks, no? I'm not sure. We'll need to still understand this, but for now let's make it work.
So, okay. What if we added a lifetime, a lifetime, and made this references? Now you're still complaining here though.
Wait, for read we are okay now, but for write, does that then need to be immutable? I don't know. Okay, let's try something.
Let's do like they have here with a signature, just a my address that is 32 bytes. Could I use the my address here?
No, doesn't implement debug. Well, screw debug. I don't need debug. But same thing with schema write, not implemented. Wait, but they derive that for that other thing as well. Hold on, let me do that. Oh there, if I say this thing has read and write my address, then I could use my address. Okay. Okay, so address would need to have this schema, but I am sure that there's a way to work with an actual address from Solana because Anchor developed wing code, it will work with address. Solana address.
In the long list of features of Solana address, where it doesn't even support borsh, or byte macro, or copy, or decode per default, there is also wing code.
Can I this with a feature WinCode?
Now feature WinCode is active.
Look, we just added with feature WinCode and WinCode with feature derive.
Look, look, now that works. And does the rest here also work? Look, now all of that works. Wait, do I even need the lifetime then? No, now I don't even need the lifetime anymore. I wouldn't have known what that's good for anyway. I'll just take it back.
Address, apparently, now derives Look, if we have the feature WinCode, then we derive schema right and schema read, yeah. And we do have that feature now.
Love it. That's good. That's good.
That's good. Great. So basically, I just replaced the borsh stuff with a schema read schema right from WinCode. Now those functions still change. We don't have a try from slice anymore. How does WinCode work? We have a WinCode serialize and the WinCode deserialize.
All right.
That seems simple enough. So, WinCode deserialize does deserialize exact What does that do? And reject trailing bytes.
Okay, cool. So that it must match exact with the size. But I think I'm fine to just serialize and the source will be my instruction data. Then I get a result.
Could I theoretically also throw this?
No, that I would need to unwrap. Okay, I'll just let it crash if that doesn't work. And you, I want you to be instruction data cuz it doesn't know what the type is. Wait, how do you know what the type is? T instruction data.
Oh, if I don't specify that here, then types must be known at this point. Then it doesn't know what to deserialize because it needs that T. Okay, so I need to specify that here that WinCode knows how to deserialize it. Okay, cool. I can do that. And then also here with a deserialize, I'm going to do the same thing. I'm going to not do count data deserialize, but this deserialize from the same source buffer, except this time I don't need this to be mutable.
Deserialize just needs a U8 slice. So, like this. Couldn't convert to program error. Yeah, okay, fine. Then we unwrap again and just fail if that doesn't work. Okay? Also looks a bit cleaner actually. And the serialization step, this would now be Win code serialize or serialize into Wait. Hold on though, because that literally just serializes it and then we have a bunch of bytes as a result. And then I manually write the bytes into the data account. Is that what we're doing? So, we're serializing the data. Will that work? Yep. And then we write that into the data account. I still think we're doing this wrong. I could of course somehow then make it zero copy, but for now let's let's copy the data over. How would I get this in there? Okay. We're going to do this inefficiently first just to get it to work. I'm going to save this. Serialized data. It's a vector even, okay? And then the data account, we can copy from slice. And we're just going to take the serialized vector as a slice. That should work. It's not going to be efficient, but it should work. Let's just try. Does it build?
Let's see. There we go. It builds with warnings, but I don't care. Good. And then Solana program deploy target deploy. Let's go. Okay. Now in our front end, we don't change anything except we're using Win code now. Call it.
Nope, doesn't work. Oh, panic. So, some of the one of the unwrap probably doesn't work at 57 57.
We have an unwrap. Indeed. Okay, so the Win code deserialize of instruction data is not happy. I maybe WinCode / WinCode does not have the same way of encoding a string. What if I wouldn't let you encode? What if I would let you ignore the text and put a dummy here? Let's do that. Would that fail in the same?
It would No, this would fail somewhere else. This would fail at 69. So, that would fail here at this unwrap, probably, or at the copy from slice.
Somewhere here it would fail. Okay, one after the other. Let's try and fix this thing. Why does it not like the text?
U64 by default. Okay, so it seems like WinCode is expecting not four but eight bytes. I can give you eight bytes. Just going to change this to a U64 the codec then instead of 32, 64. Let's go. Does that fix it? Oh, maybe also redeploy this. Cool. Does that fix it? No. Well, yes, because we're still dying at 69.
This is fine now, but here we're still dying. Why? I'll just quickly split it between the borrowed data if I can unwrap this or if it's a copying from the slice that is the problem. And 70. It seems to be the copying from slice that is the problem. So, it seems to be a problem with this. So, well, I don't know. Let's just Let's just look at this. Can I look at the serialized bytes? Cuz it gets the account data, so it must know what type it is. Yeah, I mean, let's see. Yes, so the serialized bytes. But that looks good. Look, that's about what I would expect. We have that and we end with the last byte. Okay, I don't see the issue with that. So, why can I not copy from slice? When does this panic? This function will panic if the two slices have different lengths.
Oh, is my length wrong again? Ah, there we go. So, WinCode is very precise on needs to have the right length. And oh, and we know why why wait wait yeah yeah yeah yeah yeah yeah yeah. Same as above, same error or the or same issue essentially because the default serialization strategy of a string seems to be eight plus the length, not four plus the length. And I don't know about vectors, maybe that's also eight, who knows. So, I will need to add at least four bytes here. Let's see if that's enough. Nope. Actually, you know what I could do? Instead of logging the actual bytes, I can log the length. How about this, yo? Then we can cross-check. So, the length would be 252?
Wait, my actual length is My length would be 248. So, another four bytes.
Okay. So, most likely the vector will also come with eight plus the actual data. So, that's why I need to add another four here. All right. So, we see there are differences between Porsche and WinCode deserialization, different default lengths. And now now it will work. Look at this. Great. There we go.
We could could run this deserialize this input data and deserialize the account data and serialize it back into the account into this account. And there we go. Here, it does look different because both the vector and the string both have eight bytes of length discriminators.
Cool. Just got to know if you're using WinCode. Okay, fair enough. Before we look at compute units, we are still doing logging. I'm going to remove that.
I am not going to log this anymore.
Bye-bye. And rebuild and redeploy and send another transaction. and this time we look at compute. That looks very similar though. We are at 1,100 compute units here and 1,600 compute units here compared to when we did it with Porsche where it was 1163 and 58. So, yeah, 60 compute units here, 20 compute units here. So, we do see there is a difference. Win code is already more efficient, but I think I can make it more efficient especially here here cuz I'm literally deserializing into a vector on the heap. That must be this step. I'm sure I can somehow save and let it serialize directly into the data and and onto the account. Can I somehow have a writer something or can I give you a destination serialize into?
How about this? What does that take? A writer and a source. Okay, cool. Then I just need to get to a writer from this.
Fine. I'll write code a little with a cursor. So, I'll do a cursor. So, my inner would just be the borrowed data, right? So, my buffer would be this. Oh, because that's a reference. That's my issue here. Is that my issue here? Or do I like with Porsche like this? Then I just have the byte. Can I put this as a as an inner? Yeah. Look, and that cursor I put here. Oh, from mutable U8 because I just have U8 and you need mutable U8 to have a writer. Okay, so you need to be mutable such that it's a mutable U8 and then Oh, now we used it twice.
That's why it's already moved. That's why. Ha, nailed it. So, we're wrapping this into a cursor such that we have a writer and then we directly serialize that data into that buffer which is the account data. So, this way we save the step of copying it once on the stack, and that should be more efficient. Let's see if it compiles. It does compile. It does deploy. And it does simulate.
Awesome. Let's see how it compares.
There we go. We saved another 20 or so compute to this one. This versus this.
22 and 23. Yeah, nice. There we go. It's more efficient. Can we do the same or is there also a possibility to improve this up here, the deserialization? Because when I deserialize it into a instruction data object here, that copies it onto the stack then, right? I'm going to ask Claude. I already opened the box. The inefficiencies including heap allocated string and the copied address. Yeah, read only the discriminator by directly.
I was talking about that. I would do that. Yeah, fair enough, but you would still deserialize it, right? I would love to use zero copy instead. Because I would love to just have instruction data still living here, but be able to reference it like with a proper zero copy. I don't know if WinCode supports that. But I read something, didn't I?
WinCode. Zero copy and config zero copy methods. Zero copy traits provide some convenience methods for working with zero copy types. WinCode supports in-place mutation of zero copy types.
Get a mutable reference to a type from given bytes. And it wouldn't even need to be mutable. I can just do from bytes.
And then I would do data ref. So I would just use a reference and not serialize it. Yeah, that How about that? Let's do that. So, how about instead of deserialize, we're going to do from bytes of this schema. So we're going to do instruction data. This doesn't have a from bytes. Has a from Has a from, but not a from bytes. You have rep C. Maybe we need that. And that is the also the thing that we discussed when we talked about alignment. Watch that video cuz I think if you rep C, then we guarantee alignment safe structs. Let's see if it complains here anything. We have a U8, we have an address which should have alignment one and the string probably also has alignment one. Although with the eight bytes it with a U64 maybe not. I'm just going to try. Does it change anything now? It has a deserialize from but will still deserialize. What I wanted was a from bytes cuz I want to treat this as already deserialize bytes and now I just want to from bytes mute this. What if I add the right schema as well? Do I get that then? Any type that is composed entirely of the above primitives is eligible for zero copy. This includes arrow slices and structs are annotated with transparent or rep C and have no implicit padding. Okay. Would transparent change something?
Transparent struct needs at most one field with non-trivial size or align but has a three. Nice. Okay, we have a transparency issue here. What if we just had this then it would be fine and then I would get my from bytes and from bytes mute. Nice. Okay, so let's do this and that would then be a reference of instruction data. Nice and the source would be the input data better. Would that work? Yes, but we can't convert this result directly. Okay, fine. Unrep this and we panic again. But yeah, that should work if we actually have a transparent instruction data. Also, I added the right. I don't need the right.
Okay, so as soon as we add address now, we get a problem with the transparency though. Needs at most one field with non-trivial size or alignment. Wait, what does transparent actually mean then though? At least one with non-trivial size or alignment. What's the definition of trivial size? It's written on the following primitives, U8 and I8 and all of those. If I wrapped C, then it would be fine again. Do I still have the from bytes here then? Yeah, and that would then also still work. But as soon as I add the address and the string, oh, with the address and the string, the from Okay, so the string is messing with the from bytes. Okay, so how about we don't do that and we don't need that either.
So the string, cuz that's variable length, we can't rep C. That makes sense. C would represents send strings differently anyway. Okay, and if I still wanted the text in there somehow, I could instead do a U64. Nope, already doesn't work anymore. Zero copy, which is required instruction. See, we can't do zero copy with a U64. This is probably because of alignment. If that was here, nope, also not.
We can't do a U64 at all. No, one U64 would be fine, but as soon as I add another byte, it complains. Yeah, that's I think that's an alignment problem.
What if I had I'm just playing now, padding of seven bytes. Then it would work again. Look at that.
I can't do a single byte, but if I add padding of another seven to get to eight bytes again, then that's fine and I have that. But if I don't have that padding, it doesn't work. Fun. Okay, so we're learning something, which means to read that thing back here like this, I would need to first have a seven-byte padding, right? Then it would work again as well, cuz then we're aligned to the correct U64. Nice. Yeah, watch the video on alignment if you don't understand what's going on here, but that's actually pretty pretty cool. The alternative would be to use pod types. Can we use pod types with wing code? Pod wrapper.
Wing code pod wrapper. Anyway, we're losing ourselves here. Should I just ditch the string? I will need to put it in here anyway as a string or re-implement this to take a reference as well. Okay, fine. You know what? I will use it like this without the C rep and then I will actually match just the discriminator, so the instruction data zero. And for the initialize, I will do like we had before, so we will do an instruction data deserialize from instruction data with actual copying to the heap such that we have an actual input. We're double deserializing the first byte now, but I don't mind. And the address that we have now on the heap, we put in there the same way as before because I just implemented it like this that it actually takes the owned things and not references and gives me another owned thing back, which bit inefficient, but let's do the deserialization efficiently therefore.
Represent this as C. So, we have 64 64 address address optional address. I think here we get the issue. Oh, no.
That's why per default it has eight bytes for the option to have the same alignment. Now it makes sense. That's why we will have Wait, no. The option will still have just one. Damn it. And then we're off alignment. Not sure if that's going to work. Nope, this won't work. Let's prefer Let's get rid of bunch of that stuff and see if it still works. Yeah, here I have a from bytes mutably even. That's what I want.
The account data borrow mutable reference of U8. There you go. Hehe.
Okay, so it will work like this with just U64s and an address. With the array, it will still work. That will still be fine. Scroll up, scroll down.
But the option, I think, is going to be a problem. This is going to be Yep, this is the option is killing me. Cuz then we're out of alignment. I would need to add padding if I really wanted that option, which is stupid. But I could add padding of seven bytes. And then No, it also doesn't work. Okay, I will just ignore my option for now. But the vector should be fine.
Without the padding, of course. Nope, still not. Honestly, I don't fully get that. I don't fully understand when it is zero copy and when not. Structs deriving are eligible as long as they're composed entirely of the above types, annotated, and have no implicit padding.
Tuples are not eligible. Oh, yeah.
Obviously, a vector cannot have for zero copy because I don't know the length of it. The same with a string. I can't have not even one tailing one. So, that That stuff will just not work. My one byte is the only thing that I can still get in there. And also, only if I would add that padding. Yeah. Oh, man.
You can add it in front or in the back.
Doesn't really matter. But it for the one byte, we would need that padding, yeah. Okay, so we now basically killed our entire struct. If you want to use zero copy, we can only do certain things. But hey, if all we're doing is increase the counter, we can do that with zero copy. We wouldn't even need to deserialize the entire thing. I could even, you know, have a an actual account data with the actual representation of things like this. And a zero copy version where I just go so far as to where I can comfortably zero copy. And then here with a zero copy cannot borrow as mutable. Oh, yeah, because I didn't say mute. So, that way I would get data like this. So, So a mutable account data zero copy. And then from that data I could increase the counter and then I wouldn't even need to serialize it back at all. So we can save that entire thing. We will just need to do that in the init here to get that data, to serialize the data we create like this.
So this is the still the inefficient part or especially this one, but the deserialize is we actually don't even need to do this. Now instead of the deserialization, we do this. We do zero copy reading. There we have a temporary value dropped while borrowing. Okay, fine.
There you go. So now we take the account data directly and just zero copy read like we get a zero copy thing and then we change the counter and that, I think, should increase the counter because we're writing this directly there where the bytes are, so directly where the account lives. Let's test this. Does it build? Does build. Wonderful. Can we call it? Yeah. Create account. We do the inefficient stuff and then we do efficient stuff. So now, look, even this one we're also saving a bit here. And here, a lot more actually. We are not deserializing the input data anymore here. So that's one saving. So we're doing too many things at the same time.
Let's backtrack. Instead of deserializing, so that was just a step one.
Instead of deserialization deserialization and serialization, we're doing zero copy. So just to compare directly, if we deserialize, oh wait, we're missing the thing. We're missing the doing the thing. We deserialize, change the counter, serialize again without deserializing the input data anymore because we're now just checking the first byte, then we would be at, yeah, 1339 for this instruction. And so interesting. And if we move instead of this stuff to zero copy, then we end up with just 380. So, that's a thousand compute units that we save.
Interestingly, the other one also goes down. I'm not entirely sure why. So, let's do check the accounts if they're the same. So, we were writing to this account. That's the reference. That's with a deserialization serialization.
And that's with zero copy. And it's the same data. Look, even here the counter got increased by one. All just using this zero copy. Nice. Win code with zero copy.
There you go. Win code with zero copy.
There you go. That's proof. Win code with zero copy. Nice. So, obviously you can't do everything with zero copy. As soon as you're using strings or types that need padding or vectors or options, you know, then you can't do zero copy anymore. But if you just have stuff that is neatly aligned, then it's fine. And then we can do zero copy. Obviously, you can build on that and you can improve that. Like you can use pod types like Quasar does or like Anchor V2 does cuz they're building on Win code. Not sure about Quasar, but I'm pretty sure that Anchor on the hood uses Win code, Anchor V2.
And yeah, I mean, that's pretty cool.
There's more to optimize, of course. We could get rid of that with that text.
That was one of the selling points that sadly we didn't get into in depth. The one selling point from Win code over Bin code is that we could write that stuff directly into the serialization and we don't need to first put it somewhere on the stack or the heap. It's literally one of the selling types here. When you have a my struct new box, then the my struct is constructed before it's moved into the box. And Win code makes that more efficient. But this video is already pretty long and I think I'm going to stop here. We did see a meaningful reduction in compute units used compared to when we were using Borsh, and yes, most of that is due to zero copy, but even without zero copy, we were doing better. So, that's my conclusion. Let's conclude. I hope you learned something about deserialization and serialization in Solana programs.
Borsh is the library that has been around for the longest and that has a very clear deserialization and serialization pattern. It's very deterministic and we know how it's done, but it's also not the most efficient library for serialization. Bin code is a bit more efficient, but does do serialization a bit differently for some types, different length prefixes for instance, and Win code was then just an optimization over Bin code where they tried to avoid this double copy, and also they support zero copy types. So, you can even just work with the references directly, and that will then make it most efficient. It's been a hell of a video. Let me know if you need more info on Win code, but I think this could like my my main point I was trying to make is use Win code instead of Borsh, and I wanted to have a look at Win code, how we can use it, and that we saw. We saw the serialization, deserialization, and we saw how it's done with zero copy, even if that's pretty complicated cuz you need to understand when the type is zero copyable. But anyway, I'm going to leave you with this today. Here's some other videos that you can watch. Give this one a like if you enjoyed it.
Subscribe if you have not done that already, and I will see you in the next deep dive. Looking at some libraries or some other Solana programming stuff.
Yeah.
>> [laughter] >> Uh, Win code, man. It did get a bit complicated, but I think it's worth it to really understand Win code cuz that is the library that I would recommend for you when you're doing serialization on Solana.
Related Videos
Are our DeFi tools becoming too easy to exploit?
saidotfun
228 views•2026-05-30
Solana Unchained ($UCHN) Explained: Solana’s Next Big Utility Project?
CryptoVlogOfficial
339 views•2026-05-30
🚨 Access Network App FREE Withdrawal to MetaMask?! Only 25M Supply 🔥
Airdrop26Alpha
459 views•2026-05-28
Free TON in 2026? How I Tested This Reddit TON Tool
SirenHead-z9y
2K views•2026-05-28
⚠️ALGO Has a Very Bright Future! ✅ One #Crypto Everyone Should Own!
MetaShackle
184 views•2026-05-30
BingX EventX: Trade Sports, Crypto & Global Events With One Click
AidenCryptox
311 views•2026-05-31
XRP IS GOING TO VANISH! A SUPPLY SHOCK IS INEVITABLE! (THIS IS THE PROOF!)
NCash
2K views•2026-05-31
AI Predicts What XRP Looks Like If Ripple Gets A Fed Master Account
CryptoBlazon
422 views•2026-05-30











