Problemas con ecrecover()

Tengo una rutina de prueba en Truffle que procesa los datos, los firma y pasa los datos de la firma a un contrato para verificar. La primera prueba procesa una sola cadena y su firma se verifica con éxito mediante el contrato. El segundo cifra un número y una dirección. Esta segunda prueba falla ya ecrecover()que no regresa msg.sender.

El contrato que estoy probando es:

pragma solidity ^0.4.24;

contract ContractAuth {
    function getPrefixedHash(bytes32 messageHash) internal pure returns (bytes32) {
        bytes memory hashPrefix = "\x19Ethereum Signed Message:\n32";
        return keccak256(abi.encodePacked(hashPrefix, messageHash));
    } 

    // https://ethereum.stackexchange.com/a/15911
    function verifyMessageHash(bytes32 messageHash, uint8 v, bytes32 r, bytes32 s) internal view returns (bool) {
        bytes32 prefixedHash = getPrefixedHash(messageHash);
        return ecrecover(prefixedHash, v, r, s) == msg.sender;
    }
}

Para mantener internas las funciones anteriores, he creado un contrato de envoltura de prueba:

pragma solidity ^0.4.24;

import "./ContractAuth .sol";

// TestContractAuth acts as a wrapper contract, allowing internal and
// private functions to be accessed without modifying the scope of the actual functions.
contract TestContractAuth is ContractAuth {

    // Test access to ContractAuth::getPrefixedHash()
    function getPrefixedHashTest(bytes32 messageHash) public pure returns (bytes32) {
        return getPrefixedHash(messageHash);
    }

    // Test access to ContractAuth::verifyMessageHash()
    function verifyMessageHashTest(bytes32 messageHash, uint8 v, bytes32 r, bytes32 s) public view returns (bool) {
        return verifyMessageHash(messageHash, v, r, s);
    }

    function verifyMultipleInputs(uint256 inputNumber, address inputAddress, uint8 v, bytes32 r, bytes32 s) public view returns (bool) {
        bytes32 messageHash = keccak256(abi.encodePacked(inputNumber, inputAddress));
        return verifyMessageHash(messageHash, v, r, s);
    }
}

La rutina de pruebas para verificar los datos firmados es la siguiente:

it("verify signed data", async () => {
    // getInstance() deploys the test contract above and returns
    // an instance to it for testing
    const contractAuth = getInstance("TestContractAuth");
    const testAddr = await web3.eth.getCoinbase();
    const msgPrefix = "\x19Ethereum Signed Message:\n32";

    {
        let hashedMessage = web3.utils.soliditySha3("Hello, World!");
        const prefixedHash = web3.utils.soliditySha3(msgPrefix, hashedMessage);

        let rawSig = await web3.eth.sign(prefixedHash, testAddr);
        const sig = parseSignature(rawSig);

        let validSig =
            await contractAuth.methods.verifyMessageHashTest(prefixedHash, sig.v, sig.r, sig.s).call();

        assert.equal(validSig, true, "Expected valid signature returned by verifyMessageHashTest()");
    }
    {   // Test currently fails...
        // TODO: ask question on StackExchange

        let price = 100000000;
        let contractAddr = "0x856F6BD97c2e74F1089a9e7827586a8E3447400b";

        let hashedMessage = web3.utils.soliditySha3(price, contractAddr);
        const prefixedHash = web3.utils.soliditySha3(msgPrefix, hashedMessage);

        const rawSig = await web3.eth.sign(prefixedHash, testAddr);
        const sig = parseSignature(rawSig);

        let validSig =
            await contractAuth.methods.verifyMultipleInputs(price, contractAddr, sig.v, sig.r, sig.s).call();

        assert.equal(validSig, true, "expected valid sig from verifyMultipleInputs()");
    }
});

parseSignature()es una función auxiliar para realizar el corte de la firma sin formato devuelta por web3.eth.sign().

La primera assert.equal()llamada pasa cuando la firma se verifica con éxito en el contrato. El segundo falla y actualmente no sé por qué.

Mi creencia es que hay un problema en el formato inputNumbery inputAddresscuando lo hago hash en el cliente. ¿Hay alguna manera de formatear estos datos antes de que se conviertan en hash?


Editar0:

Para verificar el hashing, he agregado la siguiente función a mi contrato de prueba:

function hashMultipleValues(uint256 inputNumber, address inputAddress) public pure returns (bytes32) {
    return keccak256(abi.encodePacked(inputNumber, inputAddress));
}

Que simplemente devuelve la combinación hash de las dos entradas.

He agregado lo siguiente a una prueba para verificar los hashes uno al lado del otro:

let hashedMessage = web3.utils.soliditySha3(price, contractAddr);
let solHashedMessage =
    await contractAuth.methods.hashMultipleValues(price, contractAddr).call();
console.log("Solidity:\t" + solHashedMessage);
console.log("Web3:\t\t" + hashedMessage);

Con los valores de entrada anteriores, obtengo el siguiente resultado en la consola durante la prueba:

Solidity:       0xdce71017994de76c5339d7b083bcbe948e6e75786de1faeb971c2cb369913535
Web3:           0xdce71017994de76c5339d7b083bcbe948e6e75786de1faeb971c2cb369913535

Esto prueba que hay paridad entre los métodos de hashing que estoy usando.

Respuestas (2)

Bien, parece que me he topado con la respuesta. El problema que tuve fue que estaba firmando el mensaje prefijado, en lugar de solo el hash sin el prefijo.

La prueba modificada, que recurre verifyMultipleInputs()a mi contrato de prueba, aparece de la siguiente manera:

const testAddr = await web3.eth.getCoinbase();
let price = 100000000;
let contractAddr = "0x856F6BD97c2e74F1089a9e7827586a8E3447400b";

let hashedMessage = web3.utils.soliditySha3(price, contractAddr);

// Sign the hashedMessage, not a prefixedHash
const rawSig = await web3.eth.sign(hashedMessage, testAddr);
const sig = parseSignature(rawSig);

let validSig =
    await CentraDEX.methods.verifyMultipleInputs(price, contractAddr, sig.v, sig.r, sig.s).call({from: testAddr});

assert.equal(validSig, true, "expected valid sig from verifyMultipleInputs()");

La función en el contrato ahora está volviendo verdadero y la prueba pasa. ¿Alguien puede explicar por qué no debería estar firmando datos prefijados antes de llamar al contrato?

Editar0:

gethantepone el Ethereum Signed Messageinternamente cuando se llama el signo.

https://github.com/ethereum/go-ethereum/commit/b59c8399fbe42390a3d41e945d03b1f21c1a9b8d

por cierto, debe editar su pregunta además de poner el contenido en la respuesta

¿Puede probar esto para asegurarse de que msg.senderse espera la función de contrato inteligente al llamar?

cambiar:

let validSig =
    await contractAuth.methods.verifyMultipleInputs(price, contractAddr, sig.v, sig.r, sig.s).call();

a:

let validSig =
    await contractAuth.methods.verifyMultipleInputs(price, contractAddr, sig.v, sig.r, sig.s).call({from: testAddr});

Y el problema puede venir de abi.encodePacked; entonces estos dos a continuación son diferentes

keccak256(abi.encodePacked(inputNumber, inputAddress)

y

web3.utils.soliditySha3(price, contractAddr);

Puede dar una advertencia, pero puede intentar usarlo keccak256(inputNumber, inputAddress)para confirmar el problema.

¡Espero que esto ayude!

Gracias por la rápida respuesta, pero esa adición no ha solucionado el problema para mí. Es una adición que mantendré en las pruebas para asegurarme de que estoy enviando desde esa dirección de prueba .
bien, tenga en cuenta que keccak256y sha3no es el mismo algoritmo hash
Pensé que el objetivo soliditySha3()era imitar el comportamiento de keccak256()?
¡ah, okey! culpa mía. Entonces el problema puede venir delabi.encodePacked