One way you might consider reading the rest of the data from the transaction is to use various ranges. For example, consider the following code:
let transaction_bytes = hex::decode(transaction_hex).unwrap();
let version = u32::from_le_bytes(&transaction_bytes[0..4]);
let number_of_inputs = u32::from_le_bytes(&transaction_bytes[5..6]);
Notice how we're grabbing different ranges of transaction_bytes
.
We have to repeatedly reference transaction_bytes
and we have to keep track of the start and end indexes for each component.
This is not ideal because we can easily make mistakes.
Note: there's an indexing mistake in the code above, can you see what it is?
Transactions are presented in hex format for a reason.
They are designed to be serialized as byte streams that can be transmitted over the network and read one byte at a time in order.
A better solution would be to use a function that keeps track of the indices and allows us to request the number of bytes we require.
Rust's standard library's Read
trait allows for exactly this.
The slice data type in Rust implements the Read
trait.
What does this mean? It gives us a method, read
, which will read some bytes from the slice and return that data in an array.
When we call read
again, it will start from where it left off.
In other words, the read
trait includes the machinery to keep track of the current position we are reading in the stream and to manage the pointer as it proceeds.
This means we don't need to keep track of any indexes ourselves.
Let's walk through how this works at a high level with a quick example and then dive deeper into what traits are and how they work.
In order to use a trait method we have to first bring it into scope with a use
statement.
In this case, we want to bring the Read
trait into scope with use std::io::Read
.
The next thing we want to do is use the read
method as intended based on the example from the documentation.
You can follow along with this example in Rust Playground.
use std::io::Read;
fn main() {
let mut bytes_slice: &[u8] = [1, 0, 0, 0, 2].as_slice();
let mut buffer = [0; 4];
bytes_slice.read(&mut buffer).unwrap();
let version = u32::from_le_bytes(buffer);
println!("Version: {}", version);
println!("Bytes slice: {:?}", bytes_slice);
}
The mut
keyword before bytes_slice
tells Rust the variable is mutable.
If we don't provide that keyword in a variable declaration, then the compiler will complain that we're attempting to change the value of an immutable variable, which is not allowed.
You might also notice the &mut
keyword in the argument to the read
method.
This indicates that we're passing in buffer
as a mutable reference.
We'll talk more about this means in the next chapter so for now let's not worry about that nuance.
When we run this, it will print the following:
Version: 1
Bytes slice: [2]
And this is what we'd expect.
The Version is 1
and the bytes_slice
variable has been updated and no longer contains the first 4 bytes.
You may notice that the way this works is that you have to first create an array with a fixed size.
Calling read
will then extract the number of bytes equal to the size of the array, store that into a buffer and then update our slice.
Traits are a way to define shared behavior.
You can think of them as a template for a particular set of behaviors.
For example, the Read
trait provides a template for types that want to "read data".
It lays out an abstract interface for a type: what kind of behavior is expected from the type and which functions are available to exercise that behavior.
Let's take a closer look at the documentation for the Read
trait.
It defines a required method, read
, which has the following function signature: fn read(&mut self, buf: &mut [u8]) -> Result<usize>;
.
...
pub trait Read {
// Required method
fn read(&mut self, buf: &mut [u8]) -> Result<usize>;
...
You'll notice there's no function body, just the signature.
It means the read
method itself is not actually implemented with any logic in the trait declaration.
We expect the types that "implement" this trait to actually provide the function logic for any required method, or trait methods, that have no implementation.
A trait can also provide other methods that a type can get access to once it has implemented the trait.
These are known as provided methods and are considered default implementations since they can also be overwritten.
You'll notice, for example, that there is a read_exact
method which is implemented with a call to the default_read_exact
method.
default_read_exact
by itself is implemented with a call the the read
method.
As long as a type implements the Read
trait by providing a read
method, it will have access to these other provided methods.
A type can also choose to override some or all of these provided methods as well and have its own custom implementations (e.g. for performance reasons).
Now if we look at the slice
type documentation, we can see that it implements the Read
trait and provides the function logic for the read
method.
Let's take a look at the source code:
...
#[stable(feature = "rust1", since = "1.0.0")]
impl Read for &[u8] {
#[inline]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let amt = cmp::min(buf.len(), self.len());
let (a, b) = self.split_at(amt);
// First check if the amount of bytes we want to read is small:
// `copy_from_slice` will generally expand to a call to `memcpy`, and
// for a single byte the overhead is significant.
if amt == 1 {
buf[0] = a[0];
} else {
buf[..amt].copy_from_slice(a);
}
*self = b;
Ok(amt)
}
...
Don't worry if you don't understand what all of this means just yet!
Simply notice how we implement a trait with the impl
keyword.
So impl Read for &[u8]
is the code block that provides the function logic for the trait.
The other thing to notice is how the function signature for read
matches the trait's function signature.
The idea here is that different types, not just the &[u8]
type can implement the Read
trait by providing the function logic for any required method and then be expected to have similar behavior and get access to the trait's provided methods.
The function logic itself for each type might differ, but given the template they are expected to take in the same arguments, return the same type and generally do the same thing, which in this case is to read some data and modify self
and the buffer.
Again, you might notice some patterns in the code above that you are not yet familiar with, such as the &mut
keyword and asterisk *
before self
at the bottom of the function.
We'll go into more detail about what these mean in the next lesson.
Let's now update our program to print out the version number leveraging the Read
trait.
We can convert the transaction_bytes
Vec
to a slice
type using the as_slice
method.
Here is the modified read_version
function.
use std::io::Read;
fn read_version(transaction_hex: &str) -> u32 {
let transaction_bytes = hex::decode(transaction_hex).unwrap();
let mut bytes_slice = transaction_bytes.as_slice();
// Read contents of bytes_slice into a buffer
let mut buffer = [0; 4];
bytes_slice.read(&mut buffer).unwrap();
u32::from_le_bytes(buffer)
}
fn main() {
let version = read_version("010000000242d5c1d6f7308bbe95c0f6e1301dd73a8da77d2155b0773bc297ac47f9cd7380010000006a4730440220771361aae55e84496b9e7b06e0a53dd122a1425f85840af7a52b20fa329816070220221dd92132e82ef9c133cb1a106b64893892a11acf2cfa1adb7698dcdc02f01b0121030077be25dc482e7f4abad60115416881fe4ef98af33c924cd8b20ca4e57e8bd5feffffff75c87cc5f3150eefc1c04c0246e7e0b370e64b17d6226c44b333a6f4ca14b49c000000006b483045022100e0d85fece671d367c8d442a96230954cdda4b9cf95e9edc763616d05d93e944302202330d520408d909575c5f6976cc405b3042673b601f4f2140b2e4d447e671c47012103c43afccd37aae7107f5a43f5b7b223d034e7583b77c8cd1084d86895a7341abffeffffff02ebb10f00000000001976a9144ef88a0b04e3ad6d1888da4be260d6735e0d308488ac508c1e000000000017a91476c0c8f2fc403c5edaea365f6a284317b9cdf7258700000000");
println!("Version: {}", version);
}
And voila, this will print Version: 1
as expected!
Great job so far!
How do we grab the modified bytes_slice
and continue decoding the transaction?
What we probably want to do is pass in the bytes_slice
into this function as an argument and continue using it in the main
function.
We'll talk more about that and associated Rust concepts of references and borrowing in the next section.
- Take another look at the
Read
trait and the implementation of theRead
trait for a slice in the documentation. What are the required and provided methods for the trait? What provided methods are being overwritten by the slice? - Consider the following block of code in which we create a Vec and then attempt to print it out:
fn main() {
let vec: Vec::<u8> = vec![0, 0, 0, 0, 0];
println!("Vec: {}", vec);
}
The compiler will return an error that the Vec cannot be formatted with the default formatter. 1. Which trait is not implemented for the Vec that is required for it to be printed? 2. How else can you print out the vector for debugging purposes? 3. Try and implement the correct trait for Vec so that it can be printed for standard display purposes.
- The Rust Book on Traits: https://doc.rust-lang.org/book/ch10-02-traits.html