1

The AZTEC Crypto Engine lets you validate proofs and cache them for future re-use, since they are fairly expensive (around 800,000 gas for a 4-note join-split proof before Istanbul, and now about 200,000 gas). This question refers to aztec.js 0.9.1.

The proofs contain a signer, the address of the owner of the notes to operate on, and a sender or validator, the address of the (possibly a contract) sender of the proofs to ACE.

When you validate the proofs and cache them, you provide one address, which is called the sender

https://github.com/AztecProtocol/AZTEC/blob/2e30619dd39182982a0d6cce57de2cd015c1e8d0/packages/protocol/contracts/ACE/ACE.sol#L179

This is a mutator method and I would expect an EVM revert if it fails.

When you want to read from the cache, you also provide an address called sender, which I expect to be the same as sender used in the call above to write to the cache, and the proofHash:

https://github.com/AztecProtocol/AZTEC/blob/2e30619dd39182982a0d6cce57de2cd015c1e8d0/packages/protocol/contracts/ACE/ACE.sol#L287

My question is: do the calls to either or both of ace.validateProof and ace.validateProofByHash have to come from the same address as the sender address parameter?

To test this, I first make a call from Javascript that relays a call to ace.validateProof via a contract called TradeValidator

      const sellerValidation = await result.minedTx(
        result.validator.validateAndGetFirstProofOutput,
        [result.seller.jsProofData, result.validator.address] )

The Solidity code of the method validateAndGetFirstProofOutput is

   function validateAndGetFirstProofOutput(
        bytes memory _proofData,
        address _proofSender
    ) public returns (bytes memory, bytes32) {
        bytes memory formattedProofOutput = ace.validateProof(JOIN_SPLIT_PROOF, _proofSender, _proofData).get(0);
        lastProofOutput = formattedProofOutput;
        lastProofHash = keccak256(formattedProofOutput);
        return (lastProofOutput, lastProofHash);
    }

I've verified that the proofHash is what I expect from Javascript, and then I also tried to verify that this proof is cached by calling from Javascript

      const validateResult2 = await result.ace.validateProofByHash(
        JOIN_SPLIT_PROOF, result.seller.jsProofHash, result.validator.address,
      )
      assert( Boolean(validateResult2['0']),
        'seller proof hash is not associated with transferer/sender, when sent from Javascript user'
      )

My expectation is that ace.validateProofByHash would succeed if proofHash matches JOIN_SPLIT_PROOF type and sender address, when called from anywhere, whether it's a Solidity contract, a Node command-line user, or webpacked JS code in a browser. However, this second call is currently failing.

Two ways I can test this:

  1. Does ace.validateProof revert if the sender parameter is incorrect, or if it comes from a different sender?
  2. Can I make a Solidity method in the validator contract that also calls ace.validateProofByHash, but coming from the same sender address as the parameter?
2
  • 1
    The answer to #1 above is that submitting an incorrect proof will indeed cause an EVM revert, and therefore not cost gas. (Possibly DoS attack vector). gist.github.com/cryptogoth/285d2e55f3818fad54072ca9f9f18f9a
    – Paul Pham
    Dec 22, 2019 at 21:19
  • Also yes to #2, such a call works. It turns out that the answer to this question is: * The address to a proof is the sender / validator, not the signer. * The call to both validateProof and validateProofByHash must come from the sender (whether it's a user address called from Javascript, or a Solidity contract)
    – Paul Pham
    Dec 23, 2019 at 0:56

1 Answer 1

0

The address needed for a proof is the sender / validator, not the signer / owner. The call to both validateProof and validateProofByHash must come from the same sender (whether it's a user address called from Javascript, or a Solidity contract).

For example, if senderAddress is a contract called TradeValidator, then in order to call ACE you need methods

    function validateProof(
        uint24 _proofType,
        address _sender,
        bytes memory _proofData
    ) public returns (bytes memory) {
        require(_sender == address(this), 'transferer address mismatch');
        return ace.validateProof(_proofType, _sender, _proofData);
    }

    // Same signature as ACE.validateProofByHash to allow swapping one for the other
    function validateProofByHash(
        uint24 _proofType,
        bytes32 _proofHash,
        address _sender
    ) public view returns (bool) {
        require(_sender == address(this), 'transferer address mismatch');
        return ace.validateProofByHash(_proofType, _proofHash, _sender);
    }

and then you may call both methods (validateProof first and validateProofByHash afterwards) from Javascript, or from Solidity.

The actual caller doesn't need to match the sender address for validateProof{,ByHash} to succeed, so in Solidity, your final call to validateProofByHash should use or address(this), in ZkAsset* in your confidential* methods, to require that only valid signed proofs previously validated can have ownership transferred.

    function confidentialTrade(
        bytes memory _proofOutputs,
        bytes memory _signatures,
        bytes memory _proofData
    ) public {

        bytes memory formattedProofOutput = ParamUtils.sliceBytes(_proofOutput, 32, _proofOutput.length);

        bytes32 proofHash = keccak256(formattedProofOutput);
        require( ace.validateProofByHash(
            JOIN_SPLIT_PROOF, proofHash, address(this)
            ), "proof output is invalid" );

        bytes memory proofOutput = TradeUtils.getFirstProofOutput(_proofOutputs);

        (,
        ,
        ,
        int256 publicValue) = proofOutput.extractProofOutput();

        (
            ,
            uint256 scalingFactor,
            uint256 totalSupply,
            ,
            ,
            ,
        ) = ace.getRegistry(address(this));
        if (publicValue > 0) {
            if (totalSupply < uint256(publicValue)) {
                uint256 supplementValue = uint256(publicValue).sub(totalSupply);
                ERC20Mintable(address(linkedToken)).mint(address(this), supplementValue.mul(scalingFactor));
                ERC20Mintable(address(linkedToken)).approve(address(ace), supplementValue.mul(scalingFactor));

                ace.supplementTokens(supplementValue);
            }
        }
        // It seems like there should be a confidentialBurn here too

        confidentialTransferInternal(_proofOutputs, _signatures, _proofData);
    }

Your Answer

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

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