DEV Community

ilija
ilija

Posted on • Updated on

Trustus EIP712 based solution for ingestion off-chain data on-chain

Trustus smart contract

Trustus is "trust-minimized method for accessing off-chain data on-chain". And you can find more about project on https://github.com/ZeframLou/trustus

Trustus is basically one smart contract written in Solidity. Main part of contract is implementation of EIP712 standard for hashing and signing of typed structured date. Trustus allow us to 1) white list address from which is allowed to send data to contract and 2) EIP712 part. This means that we can take off-chain data (like for example NFT price feed) put in specific and predefined data structure and sign. Using then V, R, S output values and additional raw data we can call smart contract function in our Main contract (on which we previously apply verifyPacket modifier from imported Trustus abstract contract).

Solidity code from Trusts contract (_verifyPacket function) will re-play hashing operation based on V,R,S values and raw data that we pass and recover Ethereum address with ecrecover. Then it will compare to white-listed addresses (setted previously with _setIsTrusted function) and allow further ingestion of data or revert in case that recovered address is not white-listed.

"Caviat" is that JavaScript part that we need (if we want to formulate proper data structure) is not given in documentation neither some example. Only one sentence: "The server must implement the specific standard used by Trustus to format the data packet, as well as provide an ECDSA signature that verifies the data packet originated from the trusted server."

That is why in this post I will give JS part to complement Trustus contract. Only difference between original Trustus contract and one I'm using here is that in my version payload defined in struct is not bytes but uint.

All this code is developed for community banking and asset management fluidNFT project supported by ConsenSys Mesh & Protocol Labs. If you are intrested you can visit our project on ipns://beta.fluidnft.org/#/

plus Tnx @apurbapokharel from Medium for all hints in this process!

import React, { useState, useEffect } from "react";
import Web3 from "web3";
import Main from "./contracts/Main.json";
var ethUtil = require('ethereumjs-util');
var sigUtil = require('eth-sig-util');


const App = () => {
  const [storageValue, setStorageValue] = useState("");
  const [myWeb3, setMyWeb3] = useState(null);
  const [accounts, setAccounts] = useState(null);
  const [contract, setContract] = useState(null); 


  const init= async () => {
    const web3 = new Web3(window.ethereum);    
    setMyWeb3(web3);

    const _accounts = await web3.eth.getAccounts();
    setAccounts(_accounts[0]); 

    const instance = new web3.eth.Contract(
      Main.abi,
      "0x3779277C9EE5f957fE90027E37bf60828c028ecF"
    );
    setContract(instance);
    const response = await instance.methods.get_recovered().call();
    setStorageValue(response);
  };

  const signData = async () => {
    var milsec_deadline = Date.now() / 1000 + 100;
    var deadline = parseInt(String(milsec_deadline).slice(0, 10));
    console.log(deadline);
    var request =
      "0x0000000000000000000000000000000000000000000000000000000000000001";
    var payload = 122;

    myWeb3.currentProvider.sendAsync(
      {
        method: "net_version",
        params: [],
        jsonrpc: "2.0",
      },
      function (err, result) {
        const netId = result.result;

        const msgParams = JSON.stringify({
          types: {
            EIP712Domain: [
              { name: "name", type: "string" },
              { name: "version", type: "string" },
              { name: "chainId", type: "uint256" },
              { name: "verifyingContract", type: "address" },
            ],
            VerifyPacket: [
              { name: "request", type: "bytes32" },
              { name: "deadline", type: "uint256" },
              { name: "payload", type: "uint256" },
            ],
          },
          primaryType: "VerifyPacket",
          domain: {
            name: "Trustus",
            version: "1",
            chainId: netId,
            verifyingContract: "0x3779277C9EE5f957fE90027E37bf60828c028ecF",
          },
          message: {
            request: request,
            deadline: deadline,
            payload: payload,
          },
        });

        var params = [accounts, msgParams];
        console.dir(params);
        var method = "eth_signTypedData_v3";

        myWeb3.currentProvider.sendAsync(
          {
            method,
            params,
            accounts,
          },
          async function (err, result) {
            if (err) return console.dir(err);
            if (result.error) {
              alert(result.error.message);
            }
            if (result.error) return console.error("ERROR", result);            

            const recovered = sigUtil.recoverTypedSignature({
              data: JSON.parse(msgParams),
              sig: result.result,
            });

            if (
              ethUtil.toChecksumAddress(recovered) ===
              ethUtil.toChecksumAddress(accounts)
            ) {
              alert("Successfully ecRecovered signer as " + accounts);
            } else {
              alert(
                "Failed to verify signer when comparing " +
                  result +
                  " to " +
                  accounts
              );
            }

            //getting r s v from a signature
            const signature = result.result.substring(2);
            const r = "0x" + signature.substring(0, 64);
            const s = "0x" + signature.substring(64, 128);
            const v = parseInt(signature.substring(128, 130), 16);
            console.log("r:", r);
            console.log("s:", s);
            console.log("v:", v);
            console.log("signer:", accounts);

            await contract.methods
              .proba(request, [v, r, s, request, deadline, payload])
              .send({ from: accounts });
          }
        );
      }
    );
  };

  const setRendered = async () => {
    // Get the value from the contract to prove it worked.
    const response = await contract.methods.get_recovered().call();
    console.log(response);
    setStorageValue(response);
  };

  const setUser = async () => {
    await contract.methods
      .setTrusted(accounts, true)
      .send({ from: accounts });
  };

  useEffect(() => {  
    init();
  }, []);


  return (
    <div className="App">
      <h1>Implementation of EIP 712 standard</h1>
      <h2>The recovred address is: {storageValue}</h2>
      <button className={style.universalBtn} onClick={() => signData()}>
        Press to sign
      </button>
      <button className={style.universalBtn} onClick={() => setRendered()}>
        Set rendered address
      </button>
      <button className={style.universalBtn} onClick={() => setUser()}>
        Add trusted address
      </button>
    </div>
  );
}


export default App;
Enter fullscreen mode Exit fullscreen mode

And here is Main smart contract where we import Trustus abstract contract

// SPDX-License-Identifier: MIT

import "./Trustus.sol";

pragma solidity ^0.8.4;

contract Main is Trustus {



    function proba(bytes32 _request, TrustusPacket calldata _packet) public verifyPacket(_request, _packet) returns (bool) {
        return true;
    }


    function setTrusted(address _signer, bool _isTrusted) public {
        _setIsTrusted (_signer, _isTrusted);
    }

}
Enter fullscreen mode Exit fullscreen mode

And here is slightly modified version of Trustus contract

// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.4;
/// @title Trustus
/// @author zefram.eth
/// @notice Trust-minimized method for accessing offchain data onchain

abstract contract Trustus {

    /// -----------------------------------------------------------------------
    /// Structs
    /// -----------------------------------------------------------------------
     /// @param v Part of the ECDSA signature
    /// @param r Part of the ECDSA signature
    /// @param s Part of the ECDSA signature
    /// @param request Identifier for verifying the packet is what is desired
    /// , rather than a packet for some other function/contract
    /// @param deadline The Unix timestamp (in seconds) after which the packet
    /// should be rejected by the contract
    /// @param payload The payload of the packet

    struct TrustusPacket {
        uint8 v;
        bytes32 r;
        bytes32 s;
        bytes32 request;
        uint256 deadline;
        uint256 payload;
    }

    // ADDED (erase on the end of testing)
    address recovered;  

    /// -----------------------------------------------------------------------
    /// Errors
    /// -----------------------------------------------------------------------
    error Trustus__InvalidPacket();

    /// -----------------------------------------------------------------------
    /// Immutable parameters
    /// -----------------------------------------------------------------------

    /// @notice The chain ID used by EIP-712
    uint256 internal immutable INITIAL_CHAIN_ID;

    /// @notice The domain separator used by EIP-712
    bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR;

    /// -----------------------------------------------------------------------
    /// Storage variables
    /// -----------------------------------------------------------------------

    /// @notice Records whether an address is trusted as a packet provider
    /// @dev provider => value    
    mapping(address => bool) internal isTrusted;

    /// -----------------------------------------------------------------------
    /// Modifiers
    /// -----------------------------------------------------------------------
    /// @notice Verifies whether a packet is valid and returns the result.
    /// Will revert if the packet is invalid.
    /// @dev The deadline, request, and signature are verified.
    /// @param request The identifier for the requested payload
    /// @param packet The packet provided by the offchain data provider
    modifier verifyPacket(bytes32 request, TrustusPacket calldata packet) {
        if (!_verifyPacket(request, packet)) revert Trustus__InvalidPacket();
        _;
    }

    /// -----------------------------------------------------------------------
    /// Constructor
    /// -----------------------------------------------------------------------

    constructor() {
        INITIAL_CHAIN_ID = block.chainid;
        INITIAL_DOMAIN_SEPARATOR = _computeDomainSeparator();
    }

    // ADDED (erase on the end of testing)
    function get_recovered() public view returns (address) {
        return recovered;
    }

    /// -----------------------------------------------------------------------
    /// Packet verification
    /// -----------------------------------------------------------------------
    /// @notice Verifies whether a packet is valid and returns the result.
    /// @dev The deadline, request, and signature are verified.
    /// @param request The identifier for the requested payload
    /// @param packet The packet provided by the offchain data provider
    /// @return success True if the packet is valid, false otherwise

    function _verifyPacket(bytes32 request, TrustusPacket calldata packet)
        internal
        virtual
        returns (bool success)
    {
        // verify deadline
        if (block.timestamp > packet.deadline) return false;

        // verify request
        if (request != packet.request) return false;

        // verify signature
        address recoveredAddress = ecrecover(
            keccak256(
                abi.encodePacked(
                    "\x19\x01",
                    DOMAIN_SEPARATOR(),
                    keccak256(
                        abi.encode(
                            keccak256(
                                "VerifyPacket(bytes32 request,uint256 deadline,uint256 payload)"
                            ),
                            packet.request,
                            packet.deadline,
                            packet.payload
                        )
                    )
                )
            ),
            packet.v,
            packet.r,
            packet.s
        );


        /// Added to original Trustus for test
        recovered =  recoveredAddress;

        return (recoveredAddress != address(0)) && isTrusted[recoveredAddress];
    } 


    /// @notice Sets the trusted status of an offchain data provider.
    /// @param signer The data provider's ECDSA public key as an Ethereum address
    /// @param isTrusted_ The desired trusted status to set
    function _setIsTrusted(address signer, bool isTrusted_) internal virtual {
            isTrusted[signer] = isTrusted_;
        }

    /// -----------------------------------------------------------------------
    /// EIP-712 compliance
    /// -----------------------------------------------------------------------

    /// @notice The domain separator used by EIP-712
    function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
            return
            block.chainid == INITIAL_CHAIN_ID
                ? INITIAL_DOMAIN_SEPARATOR
                : _computeDomainSeparator();
    }
    /// @notice Computes the domain separator used by EIP-712
    function _computeDomainSeparator() internal view virtual returns (bytes32) {
        return
            keccak256(
                abi.encode(
                    keccak256(
                        "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
                    ),
                    keccak256("Trustus"),
                    keccak256("1"),
                    block.chainid,
                    address(this)
                )
            );
    }

}


Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
ilija profile image
ilija

Here is ZeframLou newly published solution with ethers.js from [github.com/ZeframLou/trustus]

const ethers = require("ethers");

const signer = ethers.Wallet.createRandom();

const domain = {
  name: "Trustus",
  version: "1",
  chainId: 1, // ethereum mainnet
  verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", // change to your Trustus contract's address
};

const types = {
  VerifyPacket: [
    { name: "request", type: "bytes32" },
    { name: "deadline", type: "uint256" },
    { name: "payload", type: "bytes" },
  ],
};

// The data to sign
const value = {
  request: ethers.utils.keccak256(
    ethers.utils.toUtf8Bytes("MyRequest(string)")
  ), // identifier for different types of requests your Trustus contract specifies
  deadline: Math.floor(Date.now() + 600), // 10 minutes in the future
  payload: ethers.utils.toUtf8Bytes("Hello world!"),
};

const signature = await signer._signTypedData(domain, types, value);

console.log(signature);
Enter fullscreen mode Exit fullscreen mode