用于数字签名与验签的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" + messageHashgetEthSignedMessageHash 就是用于生成以太坊标准的签名消息哈希。以太坊签名标准要求在原始消息前面添加特定的前缀,以防止重放攻击和确保签名的唯一性。

合约部署和测试脚本: 用的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了。

posted on 2024-07-05 10:14  肥兔子爱豆畜子  阅读(7)  评论(0编辑  收藏  举报

导航