第1章:基石篇——区块链与Web3.0核心概念通解
欢迎来到《PHP区块链与Web3.0开发》的第一章。本章是你的“创世区块”,是我们构建一切知识体系的起点。无论你是经验丰富的PHP开发者,希望探索下一代互联网技术,还是对区块链充满好奇的新手,本章都将为你夯实不可动摇的理论基础。
本章学习目标:
完成本章后,你将能够清晰地阐述区块链与Web3.0的核心思想及其技术架构,理解诸如去中心化、共识机制、智能合约和去中心化身份(DID)等关键概念。更重要的是,你将建立起一个“PHP开发者思维”与这些新概念的桥梁,初步构想如何在熟悉的Laravel、ThinkPHP等框架语境下理解和对接这些去中心化组件,为后续的实战编码做好准备。
在全教程中的定位:
本章属于基石篇,是全教程的“总纲”和“地图”。我们不会在此处编写具体的PHP连接代码,而是专注于“认知对齐”。就像在开发一个大型PHP项目前,你必须先理解业务逻辑和系统架构一样,本章旨在确保我们在同一种“语境”下对话。它为后续所有章节——无论是使用PHP库与以太坊交互,还是构建去中心化应用(DApp)的后端API——提供了统一的概念框架和思维模型。
主要内容概述:
我们将首先追溯价值互联网的演变,揭示Web3.0作为“可读+可写+拥有”网络的本质。接着,我们会深入区块链的核心,像解析一个分布式数据库一样,剖析其数据结构(区块、链、哈希)、关键机制(共识、加密)以及智能合约如何作为“部署在链上的、自动执行的PHP脚本逻辑”。我们还将探讨与PHP后端开发密切相关的Web3.0核心组件,例如去中心化存储(想象成不可篡改的云存储/uploads/目录)和DID(取代传统中心化用户表users的认证方式)。所有解释都将力求与你已有的PHP和Web开发知识体系产生关联和类比。
与前后章节的衔接:
作为开篇第一章,它没有前序技术章节,但承接着你对PHP和传统Web开发的全部经验。从本章出发,你将自然地过渡到第2章:环境搭建与工具链,在那里,我们将把这里的理论知识落地,开始配置你的PHP开发环境,引入web3.php、ethereum-php等关键库,并连接至测试网络。本章建立的概念将成为你理解后续每一行PHP代码意图的基石——当你编写一个调用智能合约的PHP方法时,你将清楚地知道它在整个去中心化网络中扮演的角色。
现在,让我们开启这段旅程,重新定义你作为PHP开发者的边界。
从你熟悉的领域出发,想象一个特殊的、公开的“数据库”。这个数据库不由任何一家公司(如阿里云、AWS)或单个服务器托管,而是由全球成千上万的计算机共同维护一份完全相同的数据副本。这就是区块链最基础的隐喻——一个分布式、不可篡改的账本。理解它,我们需要从其最基本的数据结构开始。
区块与链:不可篡改的数据结构
区块链由一个个“区块”串联而成。每个区块就像一个数据包裹,主要包含两部分:区块头(元数据)和区块体(实际数据)。区块头中至关重要的信息是“父区块哈希值”,它如同一个指向上一个区块的、独一无二的指纹链接。在PHP中,我们可以用一个类来模拟这种结构,它虽然极度简化,但能揭示核心逻辑:
<?php
/**
* 模拟一个简单的区块结构
* 真实的区块链区块远比此复杂,包含时间戳、随机数(Nonce)、梅克尔根等。
*/
class SimpleBlock {
public $index; // 区块高度(位置)
public $timestamp; // 区块生成时间
public $data; // 区块存储的数据(例如交易信息)
public $previousHash; // 上一个区块的哈希值,形成“链”的关键
public $hash; // 当前区块自身的哈希值
/**
* 构造函数,初始化一个区块
* @param int $index
* @param string $data
* @param string $previousHash
*/
public function __construct($index, $data, $previousHash = '') {
$this->index = $index;
$this->timestamp = time();
$this->data = $data;
$this->previousHash = $previousHash;
$this->hash = $this->calculateHash(); // 生成当前区块的哈希
}
/**
* 计算当前区块的哈希值
* 哈希函数将任意长度数据转换为固定长度的唯一字符串(指纹)。
* 这里使用PHP内置的sha256进行模拟。
* @return string
*/
public function calculateHash() {
// 将区块的核心属性拼接成一个字符串,然后计算其哈希
$stringToHash = $this->index . $this->timestamp . $this->data . $this->previousHash;
return hash('sha256', $stringToHash);
}
/**
* 验证区块的完整性
* 规则1:存储的哈希值必须与计算出的哈希值一致
* 规则2:指向的上一个区块哈希必须正确(在链式验证中体现)
* @return bool
*/
public function isValid() {
return $this->hash === $this->calculateHash();
}
}
// --- 演示:创建一条简单的链 ---
// 创世区块(第一个区块),没有上一个区块,previousHash为空或特定值
$genesisBlock = new SimpleBlock(0, '这是创世区块的数据', '0');
echo "创世区块哈希: " . $genesisBlock->hash . "\n";
// 第二个区块,链接到创世区块
$secondBlock = new SimpleBlock(1, '这是第二笔交易数据', $genesisBlock->hash);
echo "第二区块哈希: " . $secondBlock->hash . "\n";
echo "第二区块验证结果: " . ($secondBlock->isValid() ? '有效' : '无效') . "\n";
// 尝试篡改第二个区块的数据
$secondBlock->data = '被恶意修改的数据';
echo "篡改后,第二区块验证结果: " . ($secondBlock->isValid() ? '有效' : '无效') . "\n"; // 输出:无效
?>
这个示例揭示了区块链防篡改的核心:任何对区块内数据(如$data)的修改,都会导致其$hash发生巨变。由于下一个区块的$previousHash记录的是修改前的正确哈希,篡改行为会立即导致链条断裂,被网络轻易发现。哈希指针形成的链条,是“不可篡改”属性的技术基石。
去中心化与共识机制:无需信任的协作
既然没有中心服务器,网络中的节点(参与者)如何对新区块的内容达成一致?这就是共识机制要解决的问题,它是去中心化系统正常运转的“规则引擎”。最常见的PoW(工作量证明)机制要求节点通过消耗算力解决一个数学难题来竞争记账权。你可以将其理解为一种需要付出现实世界成本(电力)来获取系统内投票权的游戏规则,确保了作恶的成本极高。在PHP后端开发中,你通常不会直接实现共识算法,但必须理解它的结果:你的应用所读取的区块链数据,是成百上千个独立节点根据共识规则共同确认的“真相”,而不是来自单个可被贿赂或攻击的数据库管理员。
智能合约:部署在链上的业务逻辑
这是将PHP开发者思维引入区块链的关键桥梁。智能合约是一段存储在区块链上的、在满足条件时自动执行的代码。你可以将其类比为一个部署在不可篡改环境中的、永远在线且强制按规则运行的“PHP脚本”。它定义了数字资产(如代币)的流转规则或复杂的业务逻辑(如拍卖、众筹)。与PHP脚本运行在你的服务器上不同,智能合约在以太坊虚拟机(EVM)这样的全球性分布式计算机上运行。以下是一个模拟用PHP与智能合约交互的示例,它展示了后端如何“调用”链上逻辑:
<?php
// 假设我们已经通过web3.php库连接到了一个以太坊节点
// use Web3\Web3;
/**
* 模拟一个用户向智能合约发起调用的过程
* 场景:一个简单的“存证合约”,允许用户存储一段文本的哈希到区块链。
*/
class NotaryService {
private $contractAddress = '0x742d35Cc6634C0532925a3b844Bc9e...'; // 智能合约部署地址
private $contractABI; // 合约的ABI接口定义,描述了可调用的函数
public function __construct() {
// 这里应加载真实的合约ABI JSON文件
// $this->contractABI = json_decode(file_get_contents('NotaryContract.abi'), true);
// 为示例简化,我们进行模拟
}
/**
* 调用智能合约的 `storeProof` 方法,将信息摘要上链
* @param string $documentHash 文档的哈希值(如sha256结果)
* @param string $userAddress 调用者的以太坊地址
* @return string 模拟返回的交易哈希
*/
public function storeOnBlockchain($documentHash, $userAddress) {
// 实际代码可能类似于:
// $web3 = new Web3('https://rinkeby.infura.io/v3/YOUR_PROJECT_ID');
// $contract = new Contract($web3->provider, $this->contractABI);
// $tx = $contract->at($this->contractAddress)->send('storeProof', $documentHash, [
// 'from' => $userAddress,
// 'gas' => 200000
// ]);
// return $tx;
// 模拟调用成功,返回一个假的交易哈希
echo "【智能合约调用模拟】\n";
echo "调用者: $userAddress\n";
echo "调用合约: {$this->contractAddress}\n";
echo "函数: storeProof('$documentHash')\n";
echo "状态: 交易已发送,等待网络确认(共识)...\n";
// 模拟一个交易哈希
$fakeTxHash = '0x' . bin2hex(random_bytes(32));
return $fakeTxHash;
}
/**
* 从智能合约中读取数据(不消耗Gas,无需签名)
* @param string $documentHash
* @return array 模拟返回的存证信息(时间戳、存证者)
*/
public function verifyFromBlockchain($documentHash) {
// 实际调用合约的只读函数 `getProof`
// $proofInfo = $contract->at($this->contractAddress)->call('getProof', $documentHash);
// 模拟返回数据
return [
'existed' => true,
'timestamp' => 1640995200, // 2022-01-01 00:00:00 UTC
'notarizedBy' => '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B'
];
}
}
// --- 应用场景演示:电子存证 ---
$notary = new NotaryService();
$userEthAddress = '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1';
// 1. 用户上传文件,后端计算哈希
$documentContent = "这是一份重要的合同文件内容。";
$documentHash = hash('sha256', $documentContent);
echo "文档哈希: $documentHash\n";
// 2. 后端调用智能合约,将哈希存储上链(需要支付Gas费)
$txHash = $notary->storeOnBlockchain($documentHash, $userEthAddress);
echo "交易哈希: $txHash\n\n";
// 3. 任何人(例如验证方)都可以随时独立验证该存证
$proof = $notary->verifyFromBlockchain($documentHash);
if ($proof['existed']) {
echo "【存证验证成功】\n";
echo "该文档哈希于 " . date('Y-m-d H:i:s', $proof['timestamp']) . " 被存证。\n";
echo "存证者以太坊地址: " . $proof['notarizedBy'] . "\n";
}
?>
在这个场景中,智能合约代替了传统PHP应用中“将文件哈希和时间戳写入中心化数据库”的逻辑。它的优势在于,存证时间和存证者身份信息一经上链,便无法被任何单方(包括应用开发者)事后修改,提供了极强的公信力。
去中心化身份:用户主权回归
在传统Web2.0应用中,你的身份是存储在Facebook、腾讯等公司数据库中的一条记录(users表)。去中心化身份颠覆了这一模式。DID允许用户拥有一个基于区块链加密技术生成的、自我主权控制的全局唯一标识符。它不依赖于任何中心化注册机构。在PHP应用中,这意味著用户的登录凭证不再是“用户名+密码”或“OAuth令牌”,而是一把由用户自己保管的私钥对应的公钥地址(如以太坊地址0x...)。用户通过使用私钥对一段随机字符串(挑战码)进行签名来证明身份,你的PHP后端只需使用对应的公钥验证签名即可。这直接将身份的控制权和数据所有权交还给了用户,是构建真正用户中心化应用的基石。
逻辑关系与PHP开发者的视角
这四个概念构成了一个渐进的理解体系:区块与链提供了底层可信的数据结构;共识机制保障了在去中心化环境中对这套数据结构状态的全局一致性;在此坚实、可信的数据与计算基础层之上,智能合约得以运行,封装各种复杂的业务逻辑,成为DApp的“后台”;而DID则是用户与这个新世界交互的统一、自主的身份入口。对于PHP开发者而言,你的角色正在从“中心化业务逻辑和数据的管理者”转变为“去中心化生态的接入者和服务构建者”。你不再需要独自处理用户认证、支付对账或数据防伪的所有细节,而是可以通过PHP代码与这些已经由区块链网络保障的、可靠的去中心化组件(合约、身份、存储)进行交互,构建出更加透明、可信且抗审查的应用。
接下来,我们将通过三个具体的实践案例,将前述的核心概念转化为可运行的PHP代码。这些案例将展示PHP开发者如何与区块链世界进行交互,从数据验证到身份管理,逐步深入。
案例一:文件存证验证(与链上智能合约交互)
此案例模拟一个常见场景:你的PHP应用允许用户上传重要文件(如合同、证书),并将其哈希值存证到以太坊区块链上。随后,任何人都可以调用智能合约的验证功能,证明该文件在特定时间点的存在性。这里,PHP的角色是连接用户与区块链网络的桥梁。
我们将使用一个模拟的区块链交互类来演示,避免复杂的节点部署。在实际生产中,你需要使用如 web3.php 或直接通过 JSON-RPC 与以太坊节点(如 Infura)通信。
PHP实现代码 (FileNotarizationService.php):
<?php
/**
* 文件存证验证服务类 (模拟实现)
* 实际应用中,$contractClient 应替换为真实的Web3客户端,如web3.php
*/
class FileNotarizationService
{
private $contractClient;
/**
* 构造函数,初始化(模拟)合约客户端
*/
public function __construct()
{
// 此处仅为模拟。真实情况应配置节点RPC URL和合约ABI、地址。
$this->contractClient = new class {
// 模拟一个存储了存证记录的“链上”数据
private $mockChainStorage = [];
public function notarize(string $fileHash, string $senderAddress): array
{
// 模拟交易上链
$txHash = '0x' . bin2hex(random_bytes(32));
$timestamp = time();
$blockNumber = rand(15000000, 16000000);
// 将存证信息存入“链上”存储
$this->mockChainStorage[$fileHash] = [
'existed' => true,
'timestamp' => $timestamp,
'notarizedBy' => $senderAddress,
'txHash' => $txHash,
'blockNumber' => $blockNumber,
];
return [
'success' => true,
'txHash' => $txHash,
'blockNumber' => $blockNumber,
];
}
public function verify(string $fileHash): ?array
{
// 从“链上”存储查询
return $this->mockChainStorage[$fileHash] ?? ['existed' => false];
}
};
}
/**
* 计算文件哈希并调用合约进行存证
*
* @param string $filePath 本地文件路径
* @param string $senderAddress 执行存证的以太坊地址(私钥签名过程在前端完成,此处只传地址)
* @return array 存证交易结果
* @throws Exception 当文件不存在或存证失败时抛出异常
*/
public function notarizeFile(string $filePath, string $senderAddress): array
{
// 1. 输入验证
if (!file_exists($filePath)) {
throw new InvalidArgumentException("文件不存在: {$filePath}");
}
if (!preg_match('/^0x[a-fA-F0-9]{40}$/', $senderAddress)) {
throw new InvalidArgumentException("无效的以太坊地址格式");
}
// 2. 计算文件哈希 (使用SHA-256,确保唯一性)
$fileContent = file_get_contents($filePath);
if ($fileContent === false) {
throw new RuntimeException("无法读取文件: {$filePath}");
}
$documentHash = hash('sha256', $fileContent);
// 通常合约处理的是0x开头的十六进制字符串
$documentHashFormatted = '0x' . $documentHash;
// 3. 调用模拟的智能合约存证方法
try {
$result = $this->contractClient->notarize($documentHashFormatted, $senderAddress);
} catch (Exception $e) {
// 捕获可能的网络、Gas不足、合约执行失败等错误
throw new RuntimeException("调用存证合约失败: " . $e->getMessage(), 0, $e);
}
if (!$result['success']) {
throw new RuntimeException("存证交易未成功上链。");
}
// 4. 返回成功结果
return [
'document_hash' => $documentHash,
'document_hash_formatted' => $documentHashFormatted,
'transaction_hash' => $result['txHash'],
'block_number' => $result['blockNumber'],
'message' => '文件哈希已成功提交至区块链网络,等待区块确认。',
];
}
/**
* 根据文件哈希向合约查询存证信息
*
* @param string $documentHash 文件的SHA-256哈希值(带或不带0x前缀)
* @return array 存证验证结果
*/
public function verifyDocument(string $documentHash): array
{
// 格式化哈希
if (strpos($documentHash, '0x') !== 0) {
$documentHash = '0x' . $documentHash;
}
// 调用模拟的智能合约验证方法
try {
$proof = $this->contractClient->verify($documentHash);
} catch (Exception $e) {
// 捕获查询过程中的错误
throw new RuntimeException("查询链上存证信息失败: " . $e->getMessage(), 0, $e);
}
if (!$proof['existed']) {
return [
'existed' => false,
'message' => '该文件哈希未在区块链上找到存证记录。',
];
}
// 返回详细的存证信息
return [
'existed' => true,
'message' => '【存证验证成功】',
'timestamp' => $proof['timestamp'],
'notarized_by' => $proof['notarizedBy'],
'transaction_hash' => $proof['txHash'],
'block_number' => $proof['blockNumber'],
'human_time' => date('Y-m-d H:i:s', $proof['timestamp']),
];
}
}
// === 示例用法 ===
try {
$notaryService = new FileNotarizationService();
echo "=== 案例1:文件存证验证 ===\n\n";
// 模拟一次存证过程
$exampleFilePath = __DIR__ . '/example_contract.pdf'; // 假设此文件存在
$userAddress = '0x742d35Cc6634C0532925a3b844Bc9e90F90a7e11';
echo "1. 执行文件存证:\n";
$notarizeResult = $notaryService->notarizeFile($exampleFilePath, $userAddress);
echo " 文件哈希: {$notarizeResult['document_hash']}\n";
echo " 交易哈希: {$notarizeResult['transaction_hash']}\n";
echo " 区块号: {$notarizeResult['block_number']}\n";
echo " 信息: {$notarizeResult['message']}\n\n";
// 模拟一次验证过程
echo "2. 验证存证:\n";
$verifyResult = $notaryService->verifyDocument($notarizeResult['document_hash']);
if ($verifyResult['existed']) {
echo " " . $verifyResult['message'] . "\n";
echo " 存证时间: {$verifyResult['human_time']}\n";
echo " 存证者地址: {$verifyResult['notarized_by']}\n";
echo " 交易哈希: {$verifyResult['transaction_hash']}\n";
} else {
echo " " . $verifyResult['message'] . "\n";
}
} catch (Exception $e) {
echo "【出错】: " . $e->getMessage() . "\n";
// 在实际应用中,错误应被记录到日志系统,如Monolog
// error_log($e->getMessage());
}
?>
输入输出示例:
=== 案例1:文件存证验证 ===
1. 执行文件存证:
文件哈希: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
交易哈希: 0x8b7a5c9d3f1a2b4c6e8f0a9b3c5d7e1f2a4b6c8d0e9f1a3b5c7d9e2f4a6b8c0d
区块号: 15543210
信息: 文件哈希已成功提交至区块链网络,等待区块确认。
2. 验证存证:
【存证验证成功】
存证时间: 2023-10-27 14:30:25
存证者地址: 0x742d35Cc6634C0532925a3b844Bc9e90F90a7e11
交易哈希: 0x8b7a5c9d3f1a2b4c6e8f0a9b3c5d7e1f2a4b6c8d0e9f1a3b5c7d9e2f4a6b8c0d
常见问题与解决方案:
- 问题:如何连接真实的以太坊网络?
- 方案:使用
web3.php、ethereum-php等库,或直接使用cURL通过 JSON-RPC 与节点提供商(如 Infura、Alchemy)或自建节点通信。需要配置 RPC 端点 URL、合约 ABI 和地址。
- 方案:使用
- 问题:用户如何支付存证的 Gas 费?
- 方案:存证交易需由某个以太坊地址发起并支付 Gas。通常模式是:前端(如 MetaMask)让用户签名并发送交易;或由应用服务器使用一个付费地址(需妥善保管私钥)代付,但这涉及成本和管理。示例中
$senderAddress即为付费者。
- 方案:存证交易需由某个以太坊地址发起并支付 Gas。通常模式是:前端(如 MetaMask)让用户签名并发送交易;或由应用服务器使用一个付费地址(需妥善保管私钥)代付,但这涉及成本和管理。示例中
- 问题:文件哈希在链上,原文件泄露怎么办?
- 方案:存证的是哈希,而非文件内容。哈希是单向的,无法反推原文件。它的作用是“证明你有某个文件”,而非“共享文件”。对于隐私文件,存证前可先加密,存证加密后的哈希。
- 问题:交易上链需要时间,如何获取确认?
- 方案:
notarize方法返回的txHash可用于在 Etherscan 查询状态,或通过节点的eth_getTransactionReceiptRPC 方法轮询,直到status为0x1(成功)且有一定数量的区块确认(如12个区块)。
- 方案:
案例二:去中心化身份(DID)登录
此案例演示如何使用以太坊地址作为用户标识,通过签名验证实现无需密码的登录。这是 Web3.0 应用的标准登录方式。
PHP实现代码 (DIDAuthService.php):
<?php
/**
* 去中心化身份(DID)认证服务
* 使用以太坊风格的签名验证
*/
class DIDAuthService
{
/**
* 生成一个随机挑战码(Nonce),防止重放攻击
*
* @param string $address 用户地址
* @return string 生成的挑战码
*/
public function generateChallenge(string $address): string
{
// 通常将挑战码与地址、时间戳、随机数绑定后存储于Session或缓存,并设置有效期
$nonce = bin2hex(random_bytes(32));
$challenge = "Sign this message to login. Nonce: {$nonce}";
// 模拟存储,键名可为 `auth_challenge:` . strtolower($address)
$_SESSION['auth_challenge'][strtolower($address)] = [
'challenge' => $challenge,
'expires' => time() + 300, // 5分钟有效期
];
return $challenge;
}
/**
* 验证用户签名
*
* @param string $address 声称的以太坊地址
* @param string $signature 对挑战码的签名(十六进制,带0x)
* @return bool 验证是否通过
* @throws Exception
*/
public function verifySignature(string $address, string $signature): bool
{
// 1. 参数基础验证
if (!preg_match('/^0x[a-fA-F0-9]{40}$/', $address)) {
throw new InvalidArgumentException("无效的地址格式");
}
if (!preg_match('/^0x[a-fA-F0-9]{130}$/', $signature)) { // 典型签名长度
throw new InvalidArgumentException("无效的签名格式");
}
$addressLower = strtolower($address);
// 2. 取出之前生成的挑战码
$storedChallenge = $_SESSION['auth_challenge'][$addressLower] ?? null;
if (!$storedChallenge) {
throw new RuntimeException("未找到或已过期的挑战码,请重新请求登录。");
}
if (time() > $storedChallenge['expires']) {
unset($_SESSION['auth_challenge'][$addressLower]);
throw new RuntimeException("挑战码已过期,请重新请求登录。");
}
$message = $storedChallenge['challenge'];
// 3. 验证签名
// 以太坊签名是在消息前加了特定前缀 "\x19Ethereum Signed Message:\n" + 消息长度 + 消息
// 然后对这条前缀消息进行 Keccak-256 哈希,最后用 ECDSA 签名。
// 这里使用 simplito/elliptic-php 库进行验证(需通过Composer安装)
// 为了示例的完整性,我们使用一个简化的模拟验证。
// 实际项目请使用可靠的库,如 web3.php 中的签名验证功能。
$isValid = $this->simulateSignatureVerification($message, $signature, $address);
// 4. 验证成功后,清理本次挑战码,防止重用
if ($isValid) {
unset($_SESSION['auth_challenge'][$addressLower]);
// 在此处创建用户会话,$address 即为用户唯一ID
$_SESSION['logged_in_did'] = $addressLower;
$_SESSION['login_time'] = time();
}
return $isValid;
}
/**
* 模拟签名验证过程。
* 真实环境中,应替换为 `kornrunner/keccak` 和 `simplito/elliptic-php` 等库的实际计算。
*
* @param string $message 原始消息
* @param string $signature 签名
* @param string $claimedAddress 声称的地址
* @return bool
*/
private function simulateSignatureVerification(string $message, string $signature, string $claimedAddress): bool
{
// 警告:此为极度简化的模拟,仅用于演示逻辑流程。
// 实际算法步骤:
// 1. 计算以太坊签名消息哈希: keccak256("\x19Ethereum Signed Message:\n" . len(message) . message))
// 2. 从签名中提取 recovery id `v`,和 `r`, `s` 值。
// 3. 使用 ECDSA 恢复公钥。
// 4. 从公钥推导出以太坊地址。
// 5. 比较推导出的地址与 $claimedAddress。
echo " [模拟] 正在验证消息 '{$message}' 的签名 {$signature} 是否来自地址 {$claimedAddress}\n";
// 模拟一个成功的验证
return true; // 在实际中,这里应是 true 或 false
}
/**
* 检查用户是否已通过DID登录
*
* @return bool
*/
public function isAuthenticated(): bool
{
return isset($_SESSION['logged_in_did']);
}
/**
* 获取当前登录的DID
*
* @return string|null
*/
public function getCurrentDID(): ?string
{
return $_SESSION['logged_in_did'] ?? null;
}
/**
* 退出登录
*/
public function logout(): void
{
unset($_SESSION['logged_in_did'], $_SESSION['login_time']);
session_destroy();
}
}
// === 示例用法 ===
session_start();
try {
$authService = new DIDAuthService();
echo "\n=== 案例2:去中心化身份(DID)登录 ===\n\n";
// 模拟用户前端地址
$userAddress = '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B';
// 步骤1: 用户请求登录,后端生成挑战码
echo "1. 生成登录挑战码:\n";
$challenge = $authService->generateChallenge($userAddress);
echo " 请让地址 {$userAddress} 对以下消息签名:\n";
echo " \"" . $challenge . "\"\n\n";
// 步骤2: 假设用户已在前端(如MetaMask)签名,得到了签名结果
// 这是前端通过 web3.eth.personal.sign 方法得到的签名
$simulatedUserSignature = '0x7182d3518c45d255e248e...(很长的十六进制签名)'; // 模拟数据
echo "2. 验证签名:\n";
$isValid = $authService->verifySignature($userAddress, $simulatedUserSignature);
if ($isValid) {
echo " 【登录成功】\n";
echo " 当前登录的DID: " . $authService->getCurrentDID() . "\n";
echo " 会话状态: " . ($authService->isAuthenticated() ? '已认证' : '未认证') . "\n";
// 步骤3: 演示受保护资源访问
echo "\n3. 访问受保护资源:\n";
if ($authService->isAuthenticated()) {
echo " 欢迎回来,用户 " . $authService->getCurrentDID() . "!您可以查看您的个人仪表盘。\n";
} else {
echo " 请先登录。\n";
}
// 步骤4: 退出
// $authService->logout();
// echo "\n已退出登录。\n";
} else {
echo " 【登录失败】签名验证未通过。\n";
}
} catch (Exception $e) {
echo "【出错】: " . $e->getMessage() . "\n";
}
?>
输入输出示例:
=== 案例2:去中心化身份(DID)登录 ===
1. 生成登录挑战码:
请让地址 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B 对以下消息签名:
"Sign this message to login. Nonce: 4e2f1a8c5b9d3e7f6a0c2b8d5e1f3a9c7b6d4e8f0a2c1b3d5e7f9a1c3b5e8d0f2"
2. 验证签名:
[模拟] 正在验证消息 'Sign this message to login. Nonce: 4e2f1a8c5b9d3e7f6a0c2b8d5e1f3a9c7b6d4e8f0a2c1b3d5e7f9a1c3b5e8d0f2' 的签名 0x7182d3518c45d255e248e... 是否来自地址 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B
【登录成功】
当前登录的DID: 0xab5801a7d398351b8be11c439e05c5b3259aec9b
会话状态: 已认证
3. 访问受保护资源:
欢迎回来,用户 0xab5801a7d398351b8be11c439e05c5b3259aec9b!您可以查看您的个人仪表盘。
常见问题与解决方案:
- 问题:PHP后端如何实现精确的签名验证?
- 方案:不要自己实现加密算法! 使用成熟的库。通过 Composer 安装
kornrunner/keccak和simplito/elliptic-php,或直接使用web3.php提供的Web3\Utils::verifySignature()方法(如果可用)。
- 方案:不要自己实现加密算法! 使用成熟的库。通过 Composer 安装
- 问题:挑战码(Nonce)为什么重要?如何管理?
- 方案:Nonce 防止同一签名被重复使用(重放攻击)。它必须是随机、唯一且有时效的(如5分钟)。可以将其与用户地址关联存储在缓存(如 Redis)或数据库会话表中,验证后立即删除。
- 问题:用户首次用某个地址登录,我的系统需要创建本地用户记录吗?
- 方案:通常需要。虽然地址本身就是ID,但你可能还需要存储昵称、邮箱(可选)等元数据。可以在首次成功验证签名后,在本地
users表中创建或查找一条以地址为主键的记录。
- 方案:通常需要。虽然地址本身就是ID,但你可能还需要存储昵称、邮箱(可选)等元数据。可以在首次成功验证签名后,在本地
- 问题:前端如何生成签名?
- 方案:在浏览器中,通过 Web3 注入对象(如 MetaMask)调用
window.ethereum.request({ method: 'personal_sign', params: [message, address] })。在 Node.js 后端,可以使用ethers.js或web3.js库。
- 方案:在浏览器中,通过 Web3 注入对象(如 MetaMask)调用
案例三:查询区块链上的代币转账记录
许多DApp需要显示用户的代币余额或交易历史。此案例展示PHP后端如何通过JSON-RPC接口(这里用模拟)查询以太坊上标准ERC-20代币的转账记录。
PHP实现代码 (TokenTransactionQuery.php):
<?php
/**
* ERC-20代币交易查询服务 (模拟JSON-RPC交互)
*/
class TokenTransactionQuery
{
private $rpcClient;
public function __construct(string $nodeUrl = '')
{
// 模拟一个JSON-RPC客户端
$this->rpcClient = new class {
private $mockTransfers = [
// 格式: from, to, value (wei), transactionHash, blockNumber
['0xABC...', '0xUSER1', '1000000000000000000', '0xtx123...', 15543000],
['0xUSER1', '0xDEF...', '500000000000000000', '0xtx456...', 15543100],
['0xUSER1', '0xUSER1', '2000000000000000000', '0xtx789...', 15543200], // 自转账?
];
public function eth_getLogs(array $filter): array
{
// 模拟根据地址过滤转账日志
// 真实filter结构更复杂,包含合约地址、事件主题等
$targetAddress = $filter['address'] ?? null;
$results = [];
foreach ($this->mockTransfers as $transfer) {
list($from, $to, $value, $txHash, $block) = $transfer;
if ($targetAddress === null || $from === $targetAddress || $to === $targetAddress) {
$results[] = [
'transactionHash' => $txHash,
'blockNumber' => '0x' . dechex($block),
'topics' => [
'0xddf...(Transfer事件签名哈希)',
'0x' . str_pad(substr($from, 2), 64, '0', STR_PAD_LEFT),
'0x' . str_pad(substr($to, 2), 64, '0', STR_PAD_LEFT),
],
'data' => '0x' . str_pad(substr($value, 2), 64, '0', STR_PAD_LEFT),
];
}
}
return $results;
}
public function eth_getTransactionReceipt(string $txHash): ?array
{
// 模拟根据交易哈希查询收据
foreach ($this->mockTransfers as $transfer) {
if ($transfer[3] === $txHash) {
return [
'status' => '0x1',
'logs' => [/* 包含事件日志的数组 */],
];
}
}
return null;
}
};
}
/**
* 查询与某个地址相关的所有ERC-20代币转账记录
*
* @param string $walletAddress 要查询的以太坊钱包地址
* @param string $tokenContractAddress ERC-20代币合约地址(可选,模拟中忽略)
* @return array 格式化后的转账记录列表
*/
public function getTransfersByAddress(string $walletAddress, string $tokenContractAddress = ''): array
{
if (!preg_match('/^0x[a-fA-F0-9]{40}$/', $walletAddress)) {
throw new InvalidArgumentException("无效的钱包地址格式");
}
try {
// 构建过滤器:查询涉及该地址的 Transfer 事件日志
// 真实调用示例: $filter = [
// 'fromBlock' => '0x0',
// 'toBlock' => 'latest',
// 'address' => $tokenContractAddress,
// 'topics' => [
// '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', // Transfer事件签名
// null, // 来自任何地址
// '0x'.str_pad(substr($walletAddress,2), 64, '0', STR_PAD_LEFT), // 目标地址
// ],
// ];
// $logs = $this->rpcClient->eth_getLogs($filter);
$logs = $this->rpcClient->eth_getLogs(['address' => $walletAddress]);
$transfers = [];
foreach ($logs as $log) {
// 解析日志数据
$from = '0x' . substr($log['topics'][1], -40);
$to = '0x' . substr($log['topics'][2], -40);
// 数据字段是转账金额(十六进制,以wei为单位)
$valueWei = hexdec($log['data']);
$valueEth = $valueWei / 1e18; // 假设代币精度为18
$txHash = $log['transactionHash'];
$blockNumber = hexdec($log['blockNumber']);
// 可选:获取交易收据确认状态
$receipt = $this->rpcClient->eth_getTransactionReceipt($txHash);
$status = ($receipt['status'] ?? '0x0') === '0x1' ? '成功' : '失败';
$transfers[] = [
'transaction_hash' => $txHash,
'block_number' => $blockNumber,
'from' => $from,
'to' => $to,
'value_wei' => $valueWei,
'value_formatted' => number_format($valueEth, 4), // 格式化为4位小数
'status' => $status,
'direction_relative_to_me' => ($to === $walletAddress) ? '接收' : '转出',
];
}
// 按区块号倒序排列
usort($transfers, function ($a, $b) {
return $b['block_number'] <=> $a['block_number'];
});
return $transfers;
} catch (Exception $e) {
throw new RuntimeException("查询区块链转账记录失败: " . $e->getMessage(), 0, $e);
}
}
}
// === 示例用法 ===
try {
$queryService = new TokenTransactionQuery();
echo "\n=== 案例3:查询代币转账记录 ===\n\n";
$myWalletAddress = '0xUSER1';
$tokenAddress = '0xContractAddress'; // 特定代币合约,模拟中未使用
echo "查询地址 {$myWalletAddress} 的转账历史...\n\n";
$transfers = $queryService->getTransfersByAddress($myWalletAddress, $tokenAddress);
if (empty($transfers)) {
echo "未找到任何转账记录。\n";
} else {
echo "找到 " . count($transfers) . " 笔交易:\n";
echo str_repeat("-", 120) . "\n";
printf("%-15s %-66s %-10s %-10s %-20s\n", '方向', '交易哈希', '区块', '状态', '金额');
echo str_repeat("-", 120) . "\n";
foreach ($transfers as $tx) {
printf(
"%-15s %-66s %-10d %-10s %-20s\n",
$tx['direction_relative_to_me'],
$tx['transaction_hash'],
$tx['block_number'],
$tx['status'],
$tx['value_formatted']
);
}
echo str_repeat("-", 120) . "\n";
// 简单统计
$received = array_filter($transfers, fn($t) => $t['direction_relative_to_me'] === '接收');
$sent = array_filter($transfers, fn($t) => $t['direction_relative_to_me'] === '转出');
echo "统计:接收 " . count($received) . " 笔,转出 " . count($sent) . " 笔。\n";
}
} catch (Exception $e) {
echo "【出错】: " . $e->getMessage() . "\n";
}
?>
输入输出示例:
=== 案例3:查询代币转账记录 ===
查询地址 0xUSER1 的转账历史...
找到 3 笔交易:
------------------------------------------------------------------------------------------------------------------------
方向 交易哈希 区块 状态 金额
------------------------------------------------------------------------------------------------------------------------
转出 0xtx789... 15543200 成功 2.0000
转出 0xtx456... 15543100 成功 0.5000
接收 0xtx123... 15543000 成功 1.0000
------------------------------------------------------------------------------------------------------------------------
统计:接收 1 笔,转出 2 笔。
常见问题与解决方案:
- 问题:如何连接真实的节点进行查询?
- 方案:使用
cURL或 Guzzle 库直接发送 JSON-RPC POST 请求。请求体包含jsonrpc: "2.0",method(如eth_getLogs),params,id。将返回的 JSON 数据解析为数组。
- 方案:使用
- 问题:
eth_getLogs查询大量数据时很慢或超时怎么办?- 方案:1) 增加 RPC 提供商的超时设置。2) 分页查询:使用
fromBlock和toBlock参数分段查询,例如每次查询 10000 个区块。3) 对于需要频繁查询的数据,考虑使用索引服务(如 The Graph)或由节点提供商增强的 API(如 Infura 的增强 API),它们对事件日志进行了索引,查询更快。
- 方案:1) 增加 RPC 提供商的超时设置。2) 分页查询:使用
- 问题:如何区分 ETH 转账和 ERC-20 代币转账?
- 方案:ETH 是原生货币,转账记录在普通交易 (
value > 0) 中,查询eth_getTransactionByAddress。ERC-20 转账是通过调用合约触发的Transfer事件,必须通过eth_getLogs查询事件日志。两者需要分别处理。
- 方案:ETH 是原生货币,转账记录在普通交易 (
- 问题:查询到的金额为什么是很大的整数?
- 方案:区块链上存储的是代币的最小单位(如 wei for ETH)。需要根据代币的
decimals属性(通常是18)进行换算:实际数量 = 链上整数 / 10^decimals。可以通过代币合约的decimals()函数查询精度。
- 方案:区块链上存储的是代币的最小单位(如 wei for ETH)。需要根据代币的
通过以上三个案例,我们演示了PHP在Web3.0开发中的典型作用:作为可靠的后端,处理业务逻辑、会话管理、安全验证,并通过标准接口与去中心化的区块链网络和智能合约进行安全交互。这标志着PHP开发者从“一切的中心”转变为“可信生态的集成者”,利用区块链提供的不可篡改、可验证和去信任化组件,构建新一代应用。
本章的核心目标,是为传统的PHP开发者构建通往Web3.0世界的认知桥梁和技术基石。我们认识到,Web3.0并非要淘汰PHP这类成熟的后端技术,而是为其引入了新的、可信的数据源和交互范式。PHP的角色从“唯一的数据中心与逻辑处理器”演变为“传统业务逻辑与去中心化可信组件的集成中枢”。
本章PHP核心知识点总结:
作为集成者,PHP在本阶段的核心任务是与区块链节点进行安全、可靠的通信。这主要通过JSON-RPC协议实现。你掌握了使用cURL或GuzzleHttp库构建标准的HTTP POST请求,其请求体是一个遵循JSON-RPC 2.0规范的JSON对象,包含jsonrpc、method、params和id关键字段。同时,你也学会了如何处理节点返回的JSON数据,并将其解析为PHP数组或对象进行后续业务处理。此外,理解区块链数据格式(如十六进制的区块号、哈希值和金额)并将其转换为PHP可处理的十进制数字或字符串,是一项基础且关键的技能。
重点内容与关键技能梳理:
- 节点连接与交互:这是所有Web3.0 PHP开发的起点。重点是理解并熟练构建JSON-RPC请求,调用如
eth_blockNumber、eth_getBalance、eth_getTransactionReceipt等核心方法。 - 数据查询与解析:重点区分两类核心数据获取方式:交易查询与事件日志查询。普通ETH转账通过交易查询(
value字段),而ERC-20等智能合约行为必须通过eth_getLogs查询特定事件(如Transfer)。这是理解链上活动的关键。 - 数据格式化与换算:关键技能在于正确处理链上原始数据。你必须掌握将十六进制字符串(如
0x...)转换为十进制数的能力,并深刻理解“最小单位”概念,能根据代币精度(decimals)将大整数(如1000000000000000000)转换为用户可读的金额(如1.0)。
实践应用建议与最佳实践:
- 开发环境:强烈建议从以太坊的测试网络(如Sepolia)开始,使用Infura、Alchemy等提供的免费节点服务,避免主网操作的风险和成本。
- 代码组织:将区块链RPC客户端封装成独立的服务类(如
BlockchainService),集中管理节点URL、请求构造、错误重试和日志记录,提高代码可维护性和复用性。 - 安全第一:私钥和助记词的管理必须与业务代码分离。永远不要在PHP代码中硬编码密钥,应使用环境变量(如
.env文件)或安全的秘密管理服务(如AWS Secrets Manager)。 - 性能考量:对于复杂或大量的链上数据查询(如全历史事件扫描),直接使用
eth_getLogs对节点压力大、速度慢。最佳实践是引入中间索引层,如使用The Graph子图服务,或直接调用已提供增强API的节点服务商,让PHP像查询普通数据库一样高效获取链上数据。
常见问题与解决方案汇总:
- 连接与查询:使用cURL/Guzzle发送格式正确的JSON-RPC请求即可连接节点。面对
eth_getLogs查询慢的问题,应实施分页查询(分段设置fromBlock/toBlock)或转向索引服务。 - 数据区分:清晰区分ETH转账(查询交易
value)与ERC-20转账(查询Transfer事件日志),这是正确处理区块链交易的基础。 - 数据解读:遇到极大整数的金额时,务必进行单位换算(除以
10^decimals),可通过调用代币合约的decimals()函数或查询权威列表获取精度值。

被折叠的 条评论
为什么被折叠?



