Chain Message:利用区块链打造一个的无法被篡改的留言页面

前言

我对学习一项新技术的最低标准是:“入门”。在我看来,达到所谓的入门,需要两方面的能力

  1. 对该领域的的核心概念有一定的理解,至少说听业内人士提到某个概念,能大概明白是什么;
  2. 进行实践,利用自己掌握到的知识,能够自己动手完成一个像样的作品。

最近我对区块链技术比较感兴趣,学习了核心概念后,想着用它来做点什么。我在寻思用什么项目来检验我的入门水平的时候,突然想到了留言板这么一个idea。

传统的留言板大多都是基于中心化的服务来实现的,这意味着,控制这个中心化的人可以将其他用户的留言进行删除或者是篡改。但是如果说我们能够利用区块链几乎不可能被篡改的能力,将用户的留言发送到链上,并且将智能合约的代码公开,那么这将是一个完美的闭环,不管是留言的留言者,还是说这个合约的创建者,都无法篡改这些内容。然后再加上一个前端页面,当用户访问的时候获取链上的留言,这样大家就都能看得到了。哪怕将前端网页下线,在链上根据开源的合约代码和公开合约的ABI信息,也能够查看到留言信息。

说干就干!

项目技术栈

  • Solidity - 以太坊智能合约的编程语言
  • React - 前端框架
  • Tailwind CSS - 样式框架
  • Wagmi - Web3开发库
  • Cloudflare Workers - 后端代理服务

项目目录树(使用我写的开源CLI:Treex生成):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
📁 ./
├── 📝 README.md
├── 📁 cloudflare/
│ └── 📜 worker.js
├── 📜 eslint.config.js
├── 🌐 index.html
├── 📋 package.json
├── ⚙️ pnpm-lock.yaml
├── 📜 postcss.config.js
├── 📁 public/
│ ├── 📄 favicon.ico
│ └── 🖼️ icon.png
├── 📁 solidity/
│ ├── 📋 abi.json
│ └── 📄 message.sol
├── 📁 src/
│ ├── 🎨 App.css
│ ├── 📜 App.tsx
│ ├── 📁 assets/
│ │ └── 🖼️ react.svg
│ ├── 📁 components/
│ │ ├── 📜 MessageBoard.tsx
│ │ └── 📜 QA.tsx
│ ├── 📜 config.ts
│ ├── 🎨 index.css
│ ├── 📜 main.tsx
│ └── 📜 vite-env.d.ts
├── 📜 tailwind.config.js
├── 📋 tsconfig.app.json
├── 📋 tsconfig.json
├── 📋 tsconfig.node.json
└── 📜 vite.config.ts

正文

Github 开源地址:https://github.com/shiquda/chain-message

在线预览:https://msg.shiquda.link/

部署教程: https://github.com/shiquda/chain-message/blob/main/DEPLOY_GUIDE.md

如果你想要充分理解这篇文章内容,最好需要有一定的区块链知识。没有也没关系,相信以读者聪明的脑袋瓜,可以随时利用大模型轻松理解。如果你对此感兴趣,想要动手实践的话,你还需要一个钱包插件。(我使用的是OKX的Web3钱包

智能合约

首先我们需要编写智能合约代码。智能合约可以简单理解为在区块链上运行的一串代码,而整个区块链可以看作是一个状态机,智能合约的交互使得状态机从一个状态变化到另一个状态。智能合约支持两类调用,一种是Read类型的,调用此类合约不需要花费Gas手续费;另一种是Write类型,由于涉及到写操作,这类开销是比较大的,需要花费Gas。

我编写的这个智能合约代码只允许两个接口,一个是 postMessage ,这是一个Write Contract,让用户传入他想要提交的留言。其中名字是可选的,如果不填就会视作是匿名留言。此外应该要支持markdown格式,这一点在前端处理即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
function postMessage(string memory _name, string memory _content) public {
unchecked {
// 安全计数(Solidity 0.8+默认检查算术溢出)
_messageCounter++;
}
emit MessagePosted(
_messageCounter,
msg.sender,
bytes(_name).length == 0 ? "Anonymous" : _name,
_content,
block.timestamp
);
}

PixPin_2025-04-21_00-02-02

在留言发送后,智能合约会自动保存前面的这几个参数之外,还会保存发送时的时间戳等信息。

1
2
3
4
5
6
7
event MessagePosted(
uint256 indexed messageId, // 留言ID(可索引)
address indexed sender, // 发送者地址(可索引)
string name, // 留言者名字(或"Anonymous")
string content, // Markdown内容
uint256 timestamp // 区块时间戳
);

还有一个接口,用于查询消息的数量。这个接口是只读的,进行合约调用的话,相当于只是在区块链上进行状态的查询,并不会消耗gas。这个接口其实项目暂时没有用到。

1
2
3
function getMessageCount() public view returns (uint256) {
return _messageCounter;
}

PixPin_2025-04-21_00-03-02

除此之外,就没有预留其他任何接口了,这意味着只要留言上链,哪怕是我也无法删除。

接下来讲讲我是如何部署智能合约。我使用的是以太坊基金会推荐的,对于新手比较友好的一个在线IDE:Remix

PixPin_2025-04-21_00-06-23

我们将本地写好合约的贴到这个remix上,然后然后选择编译。编译之后,在左侧选择部署选项卡。

PixPin_2025-04-21_00-06-57

选择Walletconnect,并且根据提示连接你的钱包插件。连接之后,点击部署,在钱包插件确认之后就能部署上链了。

部署合约是需要消耗比较高的Gas费的,所以说你可能需要从中心化的交易所中提取一些一些到你的钱包。我上次部署的时候大概是花了0.37刀。部署完成之后你还可以到Etherscan上添加你的合约源码,这样大家就可以公开审查这个合约代码,确保这个系统的不可篡改性。

这里有一点要注意的是,我使用的是智能合约的event log来进行留言的记录。之所以这么做而不选择使用storage直接存储到区块链上的原因,是因为那样会消耗更多的Gas费。使用Event log记录的方式就可以减少这个消耗,让用户提交留言的时候少花一些Gas费。在我测试的时候,提交一次留言需要使用0.05美元,大概折合人民币3毛多。

当然,其实测试的时候先上测试网进行测试是一个比较稳妥的方式,但是我这边太心急了,想马上能够把第一个属于自己的智能合约部署到主网上,大家最好不要学我这么做🤣

前端实现

前端实现,我使用的是React框架,Tailwind CSS对样式进行简单的美化。这边使用的对接web3的库是wagmi,感谢前人Web3开发者,已经帮我们普通开发者做了很多事情了。这个库可以自动处理和用户的钱包插件交互。

具体实现就不多赘述了,感兴趣的读者可以查看源码

在线预览:https://msg.shiquda.link/

PixPin_2025-04-21_00-16-23

Cloudflare Workers

前端的内容比较简单,但这时出现了一个新的问题:怎么样让用户能够获取区块链上的留言信息呢?

我首先尝试一种方案,利用公开的以太坊节点来进行日志的获取。但是我尝试几个后发现大多数都不支持无鉴权的调用。对于获取Event log信息,我找到一个免费的, https://ethereum.publicnode.com ,但也仅支持查询最多5万个区块的信息,这与我的预期显然是不符合的。因此我干脆使用需要鉴权的Etherscan的API。

但这里又有一个问题,如果说密钥直接存储到前端的话,泄露之后,不就会被他人滥用了?

于是我想到了Cloudflare Workers做一个简单的封装。当用户发起请求到Cloudflare Workers的时候,worker通过在CF配置的环境变量的APIkey,然后再将请求发往EtherScan,做一层转发。再根据传回的内容返回给用户,这样前端页面就可以进行实时的消息获取了。

但这样,还是有可能被人利用接口进行event log的查询。我们再设置一个环境变量,让接口只能用于我们部署的合约的查询,这样就大大降低了滥用者的收益。

这边还有一个坑点,就是说CF的Workers域名是被GFW封锁的。想要解决这个问题,可以使用自己的域名或者子域名做一个转发,这样就能绕过限制了,让用户直连也能够请求到所需的留言信息。

要进行Workers的配置,你需要在Cloudflare的左侧找到Workers 和Pages,然后创建 > Workers > Hello world > 填写名称并部署 > 进入修改源码,粘贴下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// worker.js
export default {
async fetch(request, env) {
// 处理预检请求
if (request.method === 'OPTIONS') {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
},
});
}

const { searchParams } = new URL(request.url);
const address = searchParams.get('address');
const startBlock = searchParams.get('startBlock');

if (!address || !startBlock) {
return new Response('Missing parameters', {
status: 400,
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
}
});
}

// 可选环境变量:合约地址,防止接口盗刷
const CONTRACT_ADDRESS = env.CONTRACT_ADDRESS?.toLowerCase();
if (CONTRACT_ADDRESS && address.toLowerCase() !== CONTRACT_ADDRESS) {
return new Response('Forbidden', {
status: 403,
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
}
});
}

const params = new URLSearchParams({
module: 'logs',
action: 'getLogs',
address: address,
fromBlock: startBlock,
apikey: env.ETHERSCAN_API_KEY
});
const response = await fetch(`https://api.etherscan.io/api?${params.toString()}`);

const data = await response.json();
return new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
},
});
},
};

然后找到Worker的设置 > 变量和机密 > 变量和机密 > 添加,添加CONTRACT_ADDRESSETHERSCAN_API_KEY这两个环境变量。

如果你需要绑定自己的域名,你需要在CF进入域名的管理页面,然后找到左侧的“Workers 路由”,选择添加路由并绑定前面创建的Worker。

PixPin_2025-04-21_00-29-46

静态网页生成

一切就绪后,我们完全可以使用生成一个静态页面,然后把这个静态页面上传到我们的博客或者是云托管服务上面,这样就能够实现独立的运行了。

推荐使用Cloudflare Pages的方式进行部署。使用这种方式的教程请见Github 上的部署教程

克隆项目,安装依赖并配置

1
2
3
git clone https://github.com/shiquda/chain-message.git
cd chain-message
pnpm install

然后参考.env.example填写需要的环境变量。一切就绪后,使用下面的命令进行构建:

1
pnpm build

dist/中的文件就是静态网页了,你可以把它放到托管服务上。


结语

说完这么多,真的是感慨万千。假如说一百年后,当我离开这个世界之后,这些留言可能还能存在区块链上,甚至别人还能够访问到、看到我自己的,或者是别人网友给我的留言,真的是一件非常的事情。

留言地址:https://msg.shiquda.link/

不管怎么样,先给10年后的自己留个言。(多打了一个字,已经无法挽回了😄)

PixPin_2025-04-21_00-58-32

PixPin_2025-04-21_00-59-03

实现这个idea的所有代码,包括智能合约代码,前端代码,还有cloudflare workers代码,我都放到github仓库上,要是你也对这个感兴趣,可以自己部署一个玩玩看。

我前几天申请了一个以太坊的地址域名:shiquda.eth。使用这个ENS域名,就可以找到我的钱包地址,也就是部署这个合约的地址。如果文章对您有所启发,可以给我这个地址打钱😄(真是太不要脸了)