I have a contract that requires two witnesses to sign. Signatures are both being generated using the same code, but I am only able to validate Witness A. Witness B is never found, always just get the zero address for some reason.

JavaScript that generates signatures (Witness A as example):

const hash = "0x" + ethereumjsabi
    ["address", "uint256", "string", "address"],

const signature = await this.web3.eth.personal.sign(hash, "0x8809465617E09405a1af06C2d93C2bcE0Ce5Ac80");

Solidity to validate signatures:

// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;
import "remix_tests.sol"; // this import is automatically injected by Remix.

contract VerificationTest {

    function prefixed(bytes32 hash) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));

    function checkIt () public {
        bytes memory witnessASignature = hex"8ee674e7c0105a914066d3554ee62a89f07a7a56ad5697695969b3219d8dc9da459d6f290db10b27a374010fb0abc5984dfeb8b380aa8f3564d4361217535e461b";
        bytes memory witnessBSignature = hex"8e474293656af3b93f36169062adeee942d0f2a5450d80110dcb8943863570a96312cdb64030ad3b12203755ac6a597798bede06667e95a7d3d5b3f3bed15b5e01";

        require(witnessASignature.length == 65, "invalid signature A length");
        require(witnessBSignature.length == 65, "invalid signature B length");

        bytes32 _ar;
        bytes32 _as;
        uint8 _av;

        assembly {
            _ar := mload(add(witnessASignature, 32))
            _as := mload(add(witnessASignature, 64))
            _av := byte(0, mload(add(witnessASignature, 96)))

        bytes32 _br;
        bytes32 _bs;
        uint8 _bv;

        assembly {
            _br := mload(add(witnessBSignature, 32))
            _bs := mload(add(witnessBSignature, 64))
            _bv := byte(0, mload(add(witnessBSignature, 96)))

        address witnessAAddress = 0x8809465617E09405a1af06C2d93C2bcE0Ce5Ac80;
        address witnessBAddress = 0x71eb10a6566cCA81Dd2892d36c55359AfedD3CFA;

        uint256 _amount = 10000000;
        string memory _externalAddress = "mi4VcxS4dsw9PzFAeTFJD17v2B7i24eMfQ";

        Assert.equal(ecrecover(prefixed(keccak256(abi.encodePacked(0xfCab1090e039D4B44c3862F46b7d55bC256AB041, _amount, _externalAddress, witnessAAddress))), _av, _ar, _as), witnessAAddress, "A not equal!");
        Assert.equal(ecrecover(prefixed(keccak256(abi.encodePacked(0xfCab1090e039D4B44c3862F46b7d55bC256AB041, _amount, _externalAddress, witnessBAddress))), _bv, _br, _bs), witnessBAddress, "B not equal!");

Result from test when run on Remix IDE

  • 1
    How did you get the second signature? It has an incorrect v value of 01. Possible values are 1b or 1c. Some signing libraries use 00 and 01 for the signature to work you have to replace them by 1b or 1c.
    – Ismael
    Nov 27 '21 at 23:39
  • Both signatures are generated using the same method. We both sign via the same UI using MetaMask. We have tested it with a third witness, which worked just fine, but as you said, the other witness signature has a 01 v value. If I manually change it to 1c in my Remix test, then it passes. Idk why it would mess up that value though.
    – Severin
    Nov 28 '21 at 12:02
  • We figured out what was going on based on your comment @Ismael. Feel free to post an answer, which I will then accept. Otherwise I'll accept my own answer in a few days.
    – Severin
    Nov 28 '21 at 12:47

Okay, with the comment from Ismael about the incorrect v value, the first B witness realised what was going on. He was using a ledger, which was set to Ethereum mainnet. Someone we were discussing the problem with then remembered that the v value is based on network, so we quickly saw the issue.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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