I replicated the logic in Solidity so you can understand it better when comparing it.
Take a look at the function splitSignatureWithSlicing
, it does the same thing the splitSignature
function does, but without assembly. It's easier to understand it.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;
contract Contract {
function splitSignature(bytes memory sig)
public
pure
returns (uint8 v, bytes32 r, bytes32 s)
{
require(sig.length == 65);
assembly {
// first 32 bytes, after the length prefix.
r := mload(add(sig, 32))
// second 32 bytes.
s := mload(add(sig, 64))
// final byte (first byte of the next 32 bytes).
v := byte(0, mload(add(sig, 96)))
}
return (v, r, s);
}
function splitSignatureWithSlicing(bytes calldata sig) public pure returns (uint8 v, bytes32 r, bytes32 s){
r = bytes32(sig[0:32]); // Copy first 32 bytes
s = bytes32(sig[32:64]); // Copy 32 more bytes
v = uint8(bytes1(sig[64:65])); // Copy last byte
}
}
Trying it out with bytes like: 0x997997d543f68c7b77e62a13efc6e546bd2a81c2aa8769c3354422ebbbb4fba4001c4a94f7f79e3a29652092d905b5ce47e84d6c0dd13d0ef0dd063abc149dffa7
,
They both return the same:

Keep in mind that we can only use array slicing with calldata
arrays, not with memory
or storage
arrays. That's why I declared the sig
as bytes calldata sig
.
What it is doing is copying the first 32 bytes, then copying 32 more bytes after the first 32 bytes, and then copying the last byte.
The r := mload(add(sig, 32))
assembly part just means to add 32 to the starting location in memory of the sig
bytes, and then mload
will read 32 bytes from that point.
mload
reads 32 bytes at a time.
Then, s := mload(add(sig, 64))
reads 32 more bytes from it left off in the previous step.
Then v := byte(0, mload(add(sig, 96)))
reads 32 more bytes, but we know that there's only one byte with value in these 32 bytes now. So we just need that one byte, and that's why is cast it to a single byte, which is equivalent to uint8
.
Try it yourself in Remix.