3

Hi everyone I'm studying the signature verification process. I was reading solidity by example docs here and I've found this code:

/// signature methods.
    function splitSignature(bytes memory sig)
        internal
        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);
    }

Why does it work? Can someone explain the assembly part? Much appreciate it. Thanks.

1 Answer 1

2

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:

enter image description here

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.

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.