Introduction
For oasis p4w3 hackathon, I was working on concept where we can store full game state on chain. In the game player1 hides a treasure on a grid and player2 has to find it. Here is the link to the game and its full source code.
In sapphire paratime, we can store private fields on chain which cannot be seen by anyone outside as payload is obfuscated. During the game, we need player1 to be able to see the treasure only. In the end, everyone can see the location. To enable this, we need to verify that the get call to a public view function in contract is from player1. For this, player1 signs a message which we pass a parameter and contract verifies if the signature matches the sender as player1. Then only it returns treasure location.
Generate Signed Message on Frontend
In our frontend we are creating and signing the message as below
//Timestamp in number format
const timestamp=""+(new Date()/1) ;
//The game identifier for which we are fetching the information
const game_index=0;
//Keccak256 hash of the message to keep it fixed in length
const message=ethers.solidityPackedKeccak256(['string','uint256'],[timestamp,ethers.toBigInt(gameIndex)]);
//Sign message using user's wallet
const signature=await provider?.getSigner().signMessage(hashMsg);
Verify Signed Message on Chain
For on chain verification, we create a function in contract which takes address, message inputs and signature as parameters
function verifySignature(address signer,uint256 game_index, string calldata data, bytes calldata signature) public pure returns(bool)
In solidity, we use game_index and data(timestamp) to generate the message using keccak256. Then we hash this message again with prefix to generate the final message. Below are the steps
//For helper functions check here
//This generates the hash as above in frontend
string memory data_hash=_toLower(toHex(keccak256(abi.encodePacked(data,game_index))));
//When we send a message to be signed by a wallet in ethersjs , it appends below prefix before signing happens
bytes memory prefix = "\x19Ethereum Signed Message:\n";
//Hash length + '0x'
uint length=66;
//This is the final message which is signed by
bytes32 prefixedHashMessage = keccak256(abi.encodePacked(prefix,length, data_hash));
After generating the message, we use signature to recover the signer address and match it with the expected signer
function splitSignature(
bytes memory sig
) internal pure returns (bytes32 r, bytes32 s, uint8 v) {
require(sig.length == 65, "invalid signature length");
assembly {
// first 32 bytes, after the length prefix
r := mload(add(sig, 32))
// second 32 bytes
s := mload(add(sig, 64))
// final byte (first byte of the next 32 bytes)
v := byte(0, mload(add(sig, 96)))
}
// implicitly return (r, s, v)
}
(bytes32 _r, bytes32 _s, uint8 _v) = splitSignature(signature);
address signature_signer = ecrecover(signature_hash, _v, _r, _s);
//Check if msg.sender matches the signer of the message
//signer can be msg.sender or some other address which we wish to verify
return signature_signer==signer;
This is required so that same signed message cannot be used to verify data which user may not know. In our case, we are checking if the signed message is of particular gameIndex. This works in sapphire paratime as no one can see the parameters in payload. On other hand, if parameters were visible, it will not make sense to send the raw contents of the message if they need to be private as everyone can see the parameters which make the hash