用于数字签名与验签的dApp
只有前端与链上合约两个组成部分的小dApp,其中前端使用ethers.js与Metamask钱包进行交互、以及提供hash和签名功能;链端是一个Solidity合约,提供验签功能。
前端
用ChatGPT辅助生成的代码
app.html
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Verifying Signature on Ethereum</title>
<link rel="stylesheet" href="styles.css">
<script src="app.js" type="module"></script>
</head>
<body>
<div class="container">
<h1>Verifying Signature on Ethereum</h1>
<button id="connectButton" class="button">Connect Wallet</button>
<div id="signPage" class="card">
<h2>Sign Message</h2>
<input type="text" id="to" placeholder="To Address" class="input">
<input type="number" id="amount" placeholder="Amount" class="input">
<input type="text" id="message" placeholder="Message" class="input">
<input type="number" id="nonce" placeholder="Nonce" class="input">
<button id="signButton" class="button">Sign Message</button>
<p id="signature" class="output"></p>
<button id="goToVerify" class="button">Go to Verify</button>
</div>
<div id="verifyPage" class="card hidden">
<h2>Verify Signature</h2>
<input type="text" id="verifyTo" placeholder="To Address" class="input">
<input type="number" id="verifyAmount" placeholder="Amount" class="input">
<input type="text" id="verifyMessage" placeholder="Message" class="input">
<input type="number" id="verifyNonce" placeholder="Nonce" class="input">
<input type="text" id="signatureInput" placeholder="Signature" class="input">
<input type="text" id="claimedSigner" placeholder="Claimed Signer Address" class="input">
<button id="verifyButton" class="button">Verify Signature</button>
<p id="verificationResult" class="output"></p>
<button id="goToSign" class="button">Go to Sign</button>
</div>
</div>
</body>
</html>
type="module"说明文件里的js变量和函数都是模块私有的,不是window全局的,在页面上直接访问不了。这里需要注意一下。
样式type.css
body {
font-family: Arial, sans-serif;
background: url('background.jpg') no-repeat center center fixed;
background-size: cover;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
background: rgba(255, 255, 255, 0.9);
border-radius: 10px;
padding: 20px;
width: 300px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
text-align: center;
}
h1 {
margin-bottom: 20px;
color: #333;
}
.card {
background: #f9f9f9;
border-radius: 10px;
padding: 15px;
margin-bottom: 20px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
.hidden {
display: none;
}
.input {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ccc;
border-radius: 5px;
box-sizing: border-box;
}
.button {
background-color: #007bff;
color: white;
padding: 10px 15px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
.button:hover {
background-color: #0056b3;
}
.output {
word-break: break-all;
background: #e9ecef;
padding: 10px;
border-radius: 5px;
margin-top: 10px;
font-size: 14px;
color: #333;
}
app.js
import { ethers } from "https://cdn.jsdelivr.net/npm/ethers/dist/ethers.esm.min.js";
let signer;
let contract;
const connectButton = document.getElementById('connectButton');
const signButton = document.getElementById('signButton');
const verifyButton = document.getElementById('verifyButton');
const goToVerifyButton = document.getElementById('goToVerify');
const goToSignButton = document.getElementById('goToSign');
connectButton.onclick = connectWallet;
signButton.onclick = signMessage;
verifyButton.onclick = verifySignature;
goToVerifyButton.onclick = showVerifyPage;
goToSignButton.onclick = showSignPage;
const abi = [
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "string",
"name": "message",
"type": "string"
},
{
"internalType": "uint256",
"name": "nonce",
"type": "uint256"
}
],
"name": "getMessageHash",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "messageHash",
"type": "bytes32"
}
],
"name": "getEthSignedMessageHash",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "signer",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "string",
"name": "message",
"type": "string"
},
{
"internalType": "uint256",
"name": "nonce",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "signature",
"type": "bytes"
}
],
"name": "verify",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "pure",
"type": "function"
}
];
//连接钱包
async function connectWallet() {
if (typeof window.ethereum !== 'undefined') {
try {
const accounts = await ethereum.request({ method: 'eth_requestAccounts' });
const provider = new ethers.providers.Web3Provider(window.ethereum);
signer = provider.getSigner();
console.log('Wallet connected: ', accounts[0]);
alert('Wallet connected: ' + accounts[0]);
const contractAddress = "0x262D7b573e1c1e964A8FbcD1375Acfd97168Ac5F";
contract = new ethers.Contract(contractAddress, abi, signer);
} catch (error) {
console.error('Error connecting to wallet: ', error);
}
} else {
alert('MetaMask is not installed!');
}
}
//签名
async function signMessage() {
const to = document.getElementById('to').value;
const amount = document.getElementById('amount').value;
const message = document.getElementById('message').value;
const nonce = document.getElementById('nonce').value;
const messageHash = ethers.utils.solidityKeccak256(['address', 'uint256', 'string', 'uint256'], [to, amount, message, nonce]);
const signature = await signer.signMessage(ethers.utils.arrayify(messageHash));
document.getElementById('signature').innerText = 'Signature: ' + signature;
console.log('Signature: ', signature);
}
//验签
async function verifySignature() {
const to = document.getElementById('verifyTo').value;
const amount = document.getElementById('verifyAmount').value;
const message = document.getElementById('verifyMessage').value;
const nonce = document.getElementById('verifyNonce').value;
const signature = document.getElementById('signatureInput').value;
const claimedSigner = document.getElementById('claimedSigner').value;
const isValid = await contract.verify(claimedSigner, to, amount, message, nonce, signature);
document.getElementById('verificationResult').innerText = 'Signature is valid: ' + isValid;
console.log('Signature is valid: ', isValid);
}
//签名和验签页切换
function showVerifyPage() {
document.getElementById('signPage').classList.add('hidden');
document.getElementById('verifyPage').classList.remove('hidden');
}
function showSignPage() {
document.getElementById('verifyPage').classList.add('hidden');
document.getElementById('signPage').classList.remove('hidden');
}
签名与验签的关键代码:
把to, amount, message, nonce拼接进行Keccak256得到hash,然后对hash进行签名
const messageHash = ethers.utils.solidityKeccak256(['address', 'uint256', 'string', 'uint256'], [to, amount, message, nonce]);
const signature = await signer.signMessage(ethers.utils.arrayify(messageHash));
调用合约的verify方法,因为这个方法是pure的,pure/view的方法可以直接用合约对象调用,否则需要用contractSigned = contract.connect(signer)进行调用
const isValid = await contract.verify(claimedSigner, to, amount, message, nonce, signature);
链端Solidity合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "hardhat/console.sol";
contract VerifySignature {
function getMessageHash(
address _to,
uint256 _amount,
string memory _message,
uint256 _nonce
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_to, _amount, _message, _nonce));
}
function getEthSignedMessageHash(bytes32 _messageHash)
public
pure
returns (bytes32)
{
return keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash)
);
}
/* 4. 验证签名 Verify signature
signer = 0xB273216C05A8c0D4F0a4Dd0d7Bae1D2EfFE636dd
to = 0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C
amount = 123
message = "coffee and donuts"
nonce = 1
signature =
0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b
*/
function verify(
address _signer,
address _to,
uint256 _amount,
string memory _message,
uint256 _nonce,
bytes memory signature
) public pure returns (bool) {
bytes32 messageHash = getMessageHash(_to, _amount, _message, _nonce);
bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash);
console.log("result: %s", (recoverSigner(ethSignedMessageHash, signature) == _signer) );
return recoverSigner(ethSignedMessageHash, signature) == _signer;
}
//从签名和摘要中恢复签名者
function recoverSigner(
bytes32 _ethSignedMessageHash,
bytes memory _signature
) public pure returns (address) {
(bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature);
return ecrecover(_ethSignedMessageHash, v, r, s);
}
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)
}
}
合约的作用就是验签,核心代码是ecrecover(_ethSignedMessageHash, v, r, s)
,这是以太坊solidity特有的内置方法,作用是从签了名的消息中恢复签名者signer address,入参v r s代表签名,上面的splitSignature是从签名串中分离出这三个参数。
然后ethSignedMessageHash则是在原来keccak256 hash值的基础上加了特定前缀,使之符合以太坊的规范签名标准,"\x19Ethereum Signed Message:\n32" + messageHash
, getEthSignedMessageHash
就是用于生成以太坊标准的签名消息哈希。以太坊签名标准要求在原始消息前面添加特定的前缀,以防止重放攻击和确保签名的唯一性。
合约部署和测试脚本: 用的hardhat + ganache
const { ethers } = require("hardhat");
const { boolean } = require("hardhat/internal/core/params/argumentTypes");
async function main () {
const [deployer] = await ethers.getSigners();
//const accountBalance = await ethers.provider.getBalance(deployer.address);
console.log("使用如下账户部署合约:", await deployer.getAddress());
//console.log("账户余额:", accountBalance);
const factory = await ethers.getContractFactory("VerifySignature");
const contract = await factory.deploy();
console.log("合约地址: ", await contract.getAddress());
console.log("-----------------");
const contractSigned = contract.connect(deployer);
let bool = await contractSigned.verify("0x4953bc5fc7d5abd0cd86a198a4a34eb2e3b8dc1d",
"0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C",
123,
"coffee and donuts",
1,
"0x823f00ad2f7e992f42284c9903fc511ab729b3f771f04622e0fede0f6c478d51456249f905bfafd42b3c340d5326820d79dff2dde570b585b0cc1ada32bc04531b"
);
console.log("验签结果:", bool);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
合约部署到ganache之后,把前端程序部署到nginx上(只放在本机文件系统上用浏览器打开无法唤起Metamask插件), 然后访问app.html,在Metamask中添加ganache本地网络,HTTP://127.0.0.1:7545, chain ID 1337 ,然后就可以使用这个dApp了。