Trustus EIP712 based solution for ingestion off-chain data on-chain
ilija
Posted on June 22, 2022
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;
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);
}
}
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)
)
);
}
}
Posted on June 22, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.