以下合约相当复杂,但展示了很多Solidity的功能。它实施投票合约。当然,电子投票的主要问题是如何为正确的人分配投票权以及如何防止操纵。我们不会在这里解决所有问题,但至少我们将展示如何进行委派投票,以便投票计数是自动的,同时完全透明。
这个想法是每次投票创建一个合约,为每个选项提供一个简短的名称。然后,作为主席的合同的创建者将分别对每个地址进行投票。
然后,地址背后的人可以选择自己投票或将投票委托给他们信任的人。
在投票时间结束时,winningProposal()
将返回具有最多投票数的提案。
pragma solidity >=0.4.22 <0.6.0;
/// @title Voting with delegation.
contract Ballot {
// This declares a new complex type which will
// be used for variables later.
// It will represent a single voter.
struct Voter {
uint weight; // weight is accumulated by delegation
bool voted; // if true, that person already voted
address delegate; // person delegated to
uint vote; // index of the voted proposal
}
// This is a type for a single proposal.
struct Proposal {
bytes32 name; // short name (up to 32 bytes)
uint voteCount; // number of accumulated votes
}
address public chairperson;
// This declares a state variable that
// stores a `Voter` struct for each possible address.
mapping(address => Voter) public voters;
// A dynamically-sized array of `Proposal` structs.
Proposal[] public proposals;
/// Create a new ballot to choose one of `proposalNames`.
constructor(bytes32[] memory proposalNames) public {
chairperson = msg.sender;
voters[chairperson].weight = 1;
// For each of the provided proposal names,
// create a new proposal object and add it
// to the end of the array.
for (uint i = 0; i < proposalNames.length; i++) {
// `Proposal({...})` creates a temporary
// Proposal object and `proposals.push(...)`
// appends it to the end of `proposals`.
proposals.push(Proposal({
name: proposalNames[i],
voteCount: 0
}));
}
}
// Give `voter` the right to vote on this ballot.
// May only be called by `chairperson`.
function giveRightToVote(address voter) public {
// If the first argument of `require` evaluates
// to `false`, execution terminates and all
// changes to the state and to Ether balances
// are reverted.
// This used to consume all gas in old EVM versions, but
// not anymore.
// It is often a good idea to use `require` to check if
// functions are called correctly.
// As a second argument, you can also provide an
// explanation about what went wrong.
require(
msg.sender == chairperson,
"Only chairperson can give right to vote."
);
require(
!voters[voter].voted,
"The voter already voted."
);
require(voters[voter].weight == 0);
voters[voter].weight = 1;
}
/// Delegate your vote to the voter `to`.
function delegate(address to) public {
// assigns reference
Voter storage sender = voters[msg.sender];
require(!sender.voted, "You already voted.");
require(to != msg.sender, "Self-delegation is disallowed.");
// Forward the delegation as long as
// `to` also delegated.
// In general, such loops are very dangerous,
// because if they run too long, they might
// need more gas than is available in a block.
// In this case, the delegation will not be executed,
// but in other situations, such loops might
// cause a contract to get "stuck" completely.
while (voters[to].delegate != address(0)) {
to = voters[to].delegate;
// We found a loop in the delegation, not allowed.
require(to != msg.sender, "Found loop in delegation.");
}
// Since `sender` is a reference, this
// modifies `voters[msg.sender].voted`
sender.voted = true;
sender.delegate = to;
Voter storage delegate_ = voters[to];
if (delegate_.voted) {
// If the delegate already voted,
// directly add to the number of votes
proposals[delegate_.vote].voteCount += sender.weight;
} else {
// If the delegate did not vote yet,
// add to her weight.
delegate_.weight += sender.weight;
}
}
/// Give your vote (including votes delegated to you)
/// to proposal `proposals[proposal].name`.
function vote(uint proposal) public {
Voter storage sender = voters[msg.sender];
require(sender.weight != 0, "Has no right to vote");
require(!sender.voted, "Already voted.");
sender.voted = true;
sender.vote = proposal;
// If `proposal` is out of the range of the array,
// this will throw automatically and revert all
// changes.
proposals[proposal].voteCount += sender.weight;
}
/// @dev Computes the winning proposal taking all
/// previous votes into account.
function winningProposal() public view
returns (uint winningProposal_)
{
uint winningVoteCount = 0;
for (uint p = 0; p < proposals.length; p++) {
if (proposals[p].voteCount > winningVoteCount) {
winningVoteCount = proposals[p].voteCount;
winningProposal_ = p;
}
}
}
// Calls winningProposal() function to get the index
// of the winner contained in the proposals array and then
// returns the name of the winner
function winnerName() public view
returns (bytes32 winnerName_)
{
winnerName_ = proposals[winningProposal()].name;
}
}
目前,需要许多交易来将投票权分配给所有参与者。你能想到一个更好的方法吗?
在本节中,我们将展示在以太坊上创建一个完全盲目的拍卖合同是多么容易。我们将从公开竞价开始,每个人都可以看到所做的出价,然后将此合约延伸到盲目拍卖中,直到竞标期结束才能看到实际出价。
以下简单拍卖合约的一般概念是每个人都可以在竞标期间发送出价。出价已包括汇款/以太币,以便将投标人与其出价绑定。如果提高出价,那么之前出价最高的出价者会收回她的钱。在投标期结束后,必须手动调用合同以便受益人收到他们的钱 - 合同无法自行激活。
pragma solidity >=0.4.22 <0.6.0;
contract SimpleAuction {
// Parameters of the auction. Times are either
// absolute unix timestamps (seconds since 1970-01-01)
// or time periods in seconds.
address payable public beneficiary;
uint public auctionEndTime;
// Current state of the auction.
address public highestBidder;
uint public highestBid;
// Allowed withdrawals of previous bids
mapping(address => uint) pendingReturns;
// Set to true at the end, disallows any change.
// By default initialized to `false`.
bool ended;
// Events that will be emitted on changes.
event HighestBidIncreased(address bidder, uint amount);
event AuctionEnded(address winner, uint amount);
// The following is a so-called natspec comment,
// recognizable by the three slashes.
// It will be shown when the user is asked to
// confirm a transaction.
/// Create a simple auction with `_biddingTime`
/// seconds bidding time on behalf of the
/// beneficiary address `_beneficiary`.
constructor(
uint _biddingTime,
address payable _beneficiary
) public {
beneficiary = _beneficiary;
auctionEndTime = now + _biddingTime;
}
/// Bid on the auction with the value sent
/// together with this transaction.
/// The value will only be refunded if the
/// auction is not won.
function bid() public payable {
// No arguments are necessary, all
// information is already part of
// the transaction. The keyword payable
// is required for the function to
// be able to receive Ether.
// Revert the call if the bidding
// period is over.
require(
now <= auctionEndTime,
"Auction already ended."
);
// If the bid is not higher, send the
// money back.
require(
msg.value > highestBid,
"There already is a higher bid."
);
if (highestBid != 0) {
// Sending back the money by simply using
// highestBidder.send(highestBid) is a security risk
// because it could execute an untrusted contract.
// It is always safer to let the recipients
// withdraw their money themselves.
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
emit HighestBidIncreased(msg.sender, msg.value);
}
/// Withdraw a bid that was overbid.
function withdraw() public returns (bool) {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
// It is important to set this to zero because the recipient
// can call this function again as part of the receiving call
// before `send` returns.
pendingReturns[msg.sender] = 0;
if (!msg.sender.send(amount)) {
// No need to call throw here, just reset the amount owing
pendingReturns[msg.sender] = amount;
return false;
}
}
return true;
}
/// End the auction and send the highest bid
/// to the beneficiary.
function auctionEnd() public {
// It is a good guideline to structure functions that interact
// with other contracts (i.e. they call functions or send Ether)
// into three phases:
// 1. checking conditions
// 2. performing actions (potentially changing conditions)
// 3. interacting with other contracts
// If these phases are mixed up, the other contract could call
// back into the current contract and modify the state or cause
// effects (ether payout) to be performed multiple times.
// If functions called internally include interaction with external
// contracts, they also have to be considered interaction with
// external contracts.
// 1. Conditions
require(now >= auctionEndTime, "Auction not yet ended.");
require(!ended, "auctionEnd has already been called.");
// 2. Effects
ended = true;
emit AuctionEnded(highestBidder, highestBid);
// 3. Interaction
beneficiary.transfer(highestBid);
}
}
之前的公开拍卖延伸至以下盲目拍卖。盲目拍卖的优势在于,在竞标期结束时没有时间压力。在透明的计算平台上进行盲目拍卖可能听起来像是一个矛盾,但加密技术得到了拯救。
在竞标期间,投标人实际上并没有发送她的出价,而只是一个哈希版本。由于目前认为实际上不可能找到其哈希值相等的两个(足够长的)值,因此投标人承诺投标。在投标期结束后,投标人必须公开他们的投标:他们发送未加密的价值,合约检查哈希值是否与投标期间提供的值相同。
另一个挑战是如何同时使拍卖 具有约束力和盲目性:防止投标人在赢得拍卖后不发送资金的唯一方法是让她将其与投标一起发送。由于价值转移不能在以太坊中被蒙蔽,任何人都可以看到价值。
以下合约通过接受任何大于最高出价的值来解决此问题。由于这当然只能在显示阶段进行检查,因此某些出价可能无效,这是有目的的(它甚至提供了一个明确的标记,用于设置高价值转移的无效出价):投标人可以通过放置几个高价或低无效出价。
pragma solidity >0.4.23 <0.6.0;
contract BlindAuction {
struct Bid {
bytes32 blindedBid;
uint deposit;
}
address payable public beneficiary;
uint public biddingEnd;
uint public revealEnd;
bool public ended;
mapping(address => Bid[]) public bids;
address public highestBidder;
uint public highestBid;
// Allowed withdrawals of previous bids
mapping(address => uint) pendingReturns;
event AuctionEnded(address winner, uint highestBid);
/// Modifiers are a convenient way to validate inputs to
/// functions. `onlyBefore` is applied to `bid` below:
/// The new function body is the modifier's body where
/// `_` is replaced by the old function body.
modifier onlyBefore(uint _time) { require(now < _time); _; }
modifier onlyAfter(uint _time) { require(now > _time); _; }
constructor(
uint _biddingTime,
uint _revealTime,
address payable _beneficiary
) public {
beneficiary = _beneficiary;
biddingEnd = now + _biddingTime;
revealEnd = biddingEnd + _revealTime;
}
/// Place a blinded bid with `_blindedBid` =
/// keccak256(abi.encodePacked(value, fake, secret)).
/// The sent ether is only refunded if the bid is correctly
/// revealed in the revealing phase. The bid is valid if the
/// ether sent together with the bid is at least "value" and
/// "fake" is not true. Setting "fake" to true and sending
/// not the exact amount are ways to hide the real bid but
/// still make the required deposit. The same address can
/// place multiple bids.
function bid(bytes32 _blindedBid)
public
payable
onlyBefore(biddingEnd)
{
bids[msg.sender].push(Bid({
blindedBid: _blindedBid,
deposit: msg.value
}));
}
/// Reveal your blinded bids. You will get a refund for all
/// correctly blinded invalid bids and for all bids except for
/// the totally highest.
function reveal(
uint[] memory _values,
bool[] memory _fake,
bytes32[] memory _secret
)
public
onlyAfter(biddingEnd)
onlyBefore(revealEnd)
{
uint length = bids[msg.sender].length;
require(_values.length == length);
require(_fake.length == length);
require(_secret.length == length);
uint refund;
for (uint i = 0; i < length; i++) {
Bid storage bidToCheck = bids[msg.sender][i];
(uint value, bool fake, bytes32 secret) =
(_values[i], _fake[i], _secret[i]);
if (bidToCheck.blindedBid != keccak256(abi.encodePacked(value, fake, secret))) {
// Bid was not actually revealed.
// Do not refund deposit.
continue;
}
refund += bidToCheck.deposit;
if (!fake && bidToCheck.deposit >= value) {
if (placeBid(msg.sender, value))
refund -= value;
}
// Make it impossible for the sender to re-claim
// the same deposit.
bidToCheck.blindedBid = bytes32(0);
}
msg.sender.transfer(refund);
}
// This is an "internal" function which means that it
// can only be called from the contract itself (or from
// derived contracts).
function placeBid(address bidder, uint value) internal
returns (bool success)
{
if (value <= highestBid) {
return false;
}
if (highestBidder != address(0)) {
// Refund the previously highest bidder.
pendingReturns[highestBidder] += highestBid;
}
highestBid = value;
highestBidder = bidder;
return true;
}
/// Withdraw a bid that was overbid.
function withdraw() public {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
// It is important to set this to zero because the recipient
// can call this function again as part of the receiving call
// before `transfer` returns (see the remark above about
// conditions -> effects -> interaction).
pendingReturns[msg.sender] = 0;
msg.sender.transfer(amount);
}
}
/// End the auction and send the highest bid
/// to the beneficiary.
function auctionEnd()
public
onlyAfter(revealEnd)
{
require(!ended);
emit AuctionEnded(highestBidder, highestBid);
ended = true;
beneficiary.transfer(highestBid);
}
}
pragma solidity >=0.4.22 <0.6.0;
contract Purchase {
uint public value;
address payable public seller;
address payable public buyer;
enum State { Created, Locked, Inactive }
State public state;
// Ensure that `msg.value` is an even number.
// Division will truncate if it is an odd number.
// Check via multiplication that it wasn't an odd number.
constructor() public payable {
seller = msg.sender;
value = msg.value / 2;
require((2 * value) == msg.value, "Value has to be even.");
}
modifier condition(bool _condition) {
require(_condition);
_;
}
modifier onlyBuyer() {
require(
msg.sender == buyer,
"Only buyer can call this."
);
_;
}
modifier onlySeller() {
require(
msg.sender == seller,
"Only seller can call this."
);
_;
}
modifier inState(State _state) {
require(
state == _state,
"Invalid state."
);
_;
}
event Aborted();
event PurchaseConfirmed();
event ItemReceived();
/// Abort the purchase and reclaim the ether.
/// Can only be called by the seller before
/// the contract is locked.
function abort()
public
onlySeller
inState(State.Created)
{
emit Aborted();
state = State.Inactive;
seller.transfer(address(this).balance);
}
/// Confirm the purchase as buyer.
/// Transaction has to include `2 * value` ether.
/// The ether will be locked until confirmReceived
/// is called.
function confirmPurchase()
public
inState(State.Created)
condition(msg.value == (2 * value))
payable
{
emit PurchaseConfirmed();
buyer = msg.sender;
state = State.Locked;
}
/// Confirm that you (the buyer) received the item.
/// This will release the locked ether.
function confirmReceived()
public
onlyBuyer
inState(State.Locked)
{
emit ItemReceived();
// It is important to change the state first because
// otherwise, the contracts called using `send` below
// can call in again here.
state = State.Inactive;
// NOTE: This actually allows both the buyer and the seller to
// block the refund - the withdraw pattern should be used.
buyer.transfer(value);
seller.transfer(address(this).balance);
}
}
在本节中,我们将学习如何构建支付渠道的简单实现。它使用密码签名在同一方之间重复传输以太网,安全,即时,无需交易费用。为此,我们需要了解如何签名和验证签名,以及设置付款渠道。
想象一下,Alice希望向Bob发送一定数量的Ether,即Alice是发件人而Bob是收件人。Alice只需要通过离线(例如通过电子邮件)向Bob发送加密签名的消息,这与编写支票非常相似。
签名用于授权交易,它们是智能合约可用的通用工具。爱丽丝将建立一个简单的智能合约,让她传输以太网,但是以一种不寻常的方式,她会让鲍勃这样做,而不是自己调用一个功能,因此支付交易费用。合同将如下工作:
- 爱丽丝部署
ReceiverPays
合同,附上足够的以支付将要支付的款项。- Alice通过使用其私钥对邮件进行签名来授权付款。
- Alice将加密签名的消息发送给Bob。消息不需要保密,并且发送它的机制无关紧要。
- Bob通过向智能合约提交签名的消息来声明他们的付款,它验证消息的真实性然后释放资金。
创建签名
Alice不需要与以太坊网络交互来签署交易,该过程完全脱机。在本教程中,我们将使用web3.js
和在浏览器中对消息进行签名MetaMask
。特别是,我们将使用EIP-762中描述的标准方法,因为它提供了许多其他安全优势。
/// Hashing first makes a few things easier
var hash = web3.sha3("message to sign");
web3.personal.sign(hash, web3.eth.defaultAccount, function () {...});
请注意,web3.personal.sign
预先将消息的长度添加到签名数据。由于我们首先进行哈希,因此消息总是精确地为32个字节,因此这个长度前缀始终相同,使一切变得更容易。
签到什么
对于履行付款的合同,签名的邮件必须包括:
- 收件人的地址
- 要转移的金额
- 防止重播攻击
重放攻击是指重新使用已签名的邮件来声明第二个操作的授权。为了避免重放攻击,我们将使用与以太坊交易本身相同的方法,即所谓的nonce,即帐户发送的交易数量。智能合约将检查是否多次使用nonce。
还有另一种类型的重放攻击,它发生在所有者部署ReceiverPays
智能合约,执行一些付款,然后销毁合约时。后来,她决定再次部署 RecipientPays
智能合约,但新合同不知道先前部署中使用的nonce,因此攻击者可以再次使用旧消息。
Alice可以保护它,包括邮件中的合约地址,并且只接受包含合约地址的邮件。此功能可以在claimPayment()
本章末尾的完整合约中的函数的前两行中找到。
打包参数
既然我们已经确定了要在签名消息中包含哪些信息,我们就可以将消息放在一起,哈希并对其进行签名。为简单起见,我们只是连接数据。该 ethereumjs-ABI库提供了一个调用的函数soliditySHA3
,模仿密实度的行为keccak256
功能适用于使用的编码参数abi.encodePacked
。总而言之,这是一个JavaScript函数,可以为ReceiverPays
示例创建正确的签名:
// recipient is the address that should be paid.
// amount, in wei, specifies how much ether should be sent.
// nonce can be any unique number to prevent replay attacks
// contractAddress is used to prevent cross-contract replay attacks
function signPayment(recipient, amount, nonce, contractAddress, callback) {
var hash = "0x" + ethereumjs.ABI.soliditySHA3(
["address", "uint256", "uint256", "address"],
[recipient, amount, nonce, contractAddress]
).toString("hex");
web3.personal.sign(hash, web3.eth.defaultAccount, callback);
}
在Solidity中恢复消息签名者
通常,ECDSA签名由两个参数组成,r
和s
。以太坊中的签名包括一个名为的第三个参数v
,可用于恢复用于登录邮件的帐户的私钥,即交易的发件人。密实度提供了一个内置函数 ecrecover 接受的消息与沿r
,s
和v
参数,并返回用于签署的消息中的地址。
提取签名参数
通过web3.js产生签名的串接r
,s
并且v
,因此,第一个步骤是分裂的那些参数回来。它可以在客户端上完成,但在智能合约中执行它意味着只需要发送一个签名参数而不是三个。将字节数组拆分为组件部分有点混乱。我们将使用内联汇编来完成splitSignature
函数中的工作(本章末尾的完整契约中的第三个函数)。
计算消息散列
智能合约需要确切地知道签署了哪些参数,因此必须从参数重新创建消息并将其用于签名验证。可以在函数中找到函数prefixed
和 recoverSigner
执行此操作及其使用 claimPayment
。
完整合同
pragma solidity >=0.4.24 <0.6.0;
contract ReceiverPays {
address owner = msg.sender;
mapping(uint256 => bool) usedNonces;
constructor() public payable {}
function claimPayment(uint256 amount, uint256 nonce, bytes memory signature) public {
require(!usedNonces[nonce]);
usedNonces[nonce] = true;
// this recreates the message that was signed on the client
bytes32 message = prefixed(keccak256(abi.encodePacked(msg.sender, amount, nonce, this)));
require(recoverSigner(message, signature) == owner);
msg.sender.transfer(amount);
}
/// destroy the contract and reclaim the leftover funds.
function kill() public {
require(msg.sender == owner);
selfdestruct(msg.sender);
}
/// signature methods.
function splitSignature(bytes memory sig)
internal
pure
returns (uint8 v, bytes32 r, bytes32 s)
{
require(sig.length == 65);
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)))
}
return (v, r, s);
}
function recoverSigner(bytes32 message, bytes memory sig)
internal
pure
returns (address)
{
(uint8 v, bytes32 r, bytes32 s) = splitSignature(sig);
return ecrecover(message, v, r, s);
}
/// builds a prefixed hash to mimic the behavior of eth_sign.
function prefixed(bytes32 hash) internal pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
}
}
Alice现在将构建一个简单但完整的支付渠道实施。支付渠道使用加密签名安全,即时地重复传输以太网,无需交易费用。
什么是付款渠道?
支付渠道允许参与者在不使用交易的情况下重复转移以太网。这意味着可以避免与交易相关的延迟和费用。我们将探讨双方(Alice和Bob)之间的简单单向支付渠道。使用它涉及三个步骤:
- Alice与Ether合作提供智能合约。这将“打开”付款渠道。
- Alice会签署一些消息,指明对接收者的欠款量。每次付款都会重复此步骤。
- Bob“关闭”支付渠道,撤回其部分以太网并将剩余部分发送回发件人。
请注意,只有步骤1和3需要以太坊交易,步骤2意味着发件人通过离线方式(例如电子邮件)将加密签名的消息发送给收件人。这意味着只需要两个事务来支持任意数量的传输。
Bob保证收到他们的资金,因为智能合约托管以太并尊重有效的签名消息。智能合约还会执行超时,因此即使收件人拒绝关闭渠道,Alice也能保证最终收回资金。由付款渠道的参与者决定保持打开的时间。对于短期交易,例如向员工支付小时工资,支付可能持续数月或数年。
打开支付渠道
为了打开支付渠道,Alice部署了智能合约,附加了要托管的以太网,并指定了对方收件人以及该渠道存在的最长持续时间。它是SimplePaymentChannel
合同中的功能 ,即本章末尾。
付款
Alice通过向Bob发送签名消息来进行付款。该步骤完全在以太坊网络之外执行。邮件由发件人以加密方式签名,然后直接传输给收件人。
每条消息都包含以下信息:
- 智能合约的地址,用于防止交叉合同重播攻击。
- 到目前为止,接收方所欠的以太网总量。
在一系列转账结束时,付款渠道仅关闭一次。因此,只有一条发送的邮件将被兑换。这就是为什么每条消息都指定了所欠欧元的累计总量,而不是单个小额支付的金额。收件人自然会选择兑换最新消息,因为这是总数最高的消息。不再需要nonce per-message,因为智能合约只会尊重单个消息。智能合约的地址仍用于防止用于一个支付渠道的消息被用于不同的渠道。
以下是修改后的javascript代码,用于对上一章中的消息进行加密签名:
function constructPaymentMessage(contractAddress, amount) {
return ethereumjs.ABI.soliditySHA3(
["address", "uint256"],
[contractAddress, amount]
);
}
function signMessage(message, callback) {
web3.personal.sign(
"0x" + message.toString("hex"),
web3.eth.defaultAccount,
callback
);
}
// contractAddress is used to prevent cross-contract replay attacks.
// amount, in wei, specifies how much Ether should be sent.
function signPayment(contractAddress, amount, callback) {
var message = constructPaymentMessage(contractAddress, amount);
signMessage(message, callback);
}
关闭支付渠道
当Bob准备好接收他们的资金时,是时候通过调用close
智能合约上的功能来关闭支付渠道。关闭频道会向收件人支付他们所欠的以太币并销毁合同,将任何剩余的以太网发送给Alice。要关闭频道,Bob需要提供Alice签名的消息。
智能合约必须验证邮件是否包含发件人的有效签名。执行此验证的过程与收件人使用的过程相同。Solidity的功能isValidSignature
和recoverSigner
工作方式与上一节中的JavaScript对应功能相同。后者是从ReceiverPays
前一章的合约借来的 。
该close
功能只能由支付渠道收件人调用,支付渠道收件人自然会传递最新的支付消息,因为该消息的总欠款最高。如果允许发件人调用此函数,他们可以提供较低金额的邮件,并欺骗收件人的欠款。
该函数验证签名的消息是否与给定的参数匹配。如果一切都结束,收件人将被发送他们的以太部分,发送者将通过一个发送给其余的selfdestruct
。您可以close
在完整合约中查看该功能。
频道过期
Bob可以随时关闭支付渠道,但如果他们不这样做,Alice需要一种方法来收回他们托管的资金。一个过期时间设置在合约部署的时间。一旦达到该时间,爱丽丝可以打电话 claimTimeout
来收回他们的资金。您可以claimTimeout
在完整合约中查看该功能。
调用此功能后,Bob无法再接收任何以太网,因此Bob必须在到期之前关闭该通道。
完整合同
pragma solidity >=0.4.24 <0.6.0;
contract SimplePaymentChannel {
address payable public sender; // The account sending payments.
address payable public recipient; // The account receiving the payments.
uint256 public expiration; // Timeout in case the recipient never closes.
constructor (address payable _recipient, uint256 duration)
public
payable
{
sender = msg.sender;
recipient = _recipient;
expiration = now + duration;
}
function isValidSignature(uint256 amount, bytes memory signature)
internal
view
returns (bool)
{
bytes32 message = prefixed(keccak256(abi.encodePacked(this, amount)));
// check that the signature is from the payment sender
return recoverSigner(message, signature) == sender;
}
/// the recipient can close the channel at any time by presenting a
/// signed amount from the sender. the recipient will be sent that amount,
/// and the remainder will go back to the sender
function close(uint256 amount, bytes memory signature) public {
require(msg.sender == recipient);
require(isValidSignature(amount, signature));
recipient.transfer(amount);
selfdestruct(sender);
}
/// the sender can extend the expiration at any time
function extend(uint256 newExpiration) public {
require(msg.sender == sender);
require(newExpiration > expiration);
expiration = newExpiration;
}
/// if the timeout is reached without the recipient closing the channel,
/// then the Ether is released back to the sender.
function claimTimeout() public {
require(now >= expiration);
selfdestruct(sender);
}
/// All functions below this are just taken from the chapter
/// 'creating and verifying signatures' chapter.
function splitSignature(bytes memory sig)
internal
pure
returns (uint8 v, bytes32 r, bytes32 s)
{
require(sig.length == 65);
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)))
}
return (v, r, s);
}
function recoverSigner(bytes32 message, bytes memory sig)
internal
pure
returns (address)
{
(uint8 v, bytes32 r, bytes32 s) = splitSignature(sig);
return ecrecover(message, v, r, s);
}
/// builds a prefixed hash to mimic the behavior of eth_sign.
function prefixed(bytes32 hash) internal pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
}
}
注意:该功能splitSignature
非常简单,不使用所有安全检查。真正的实现应该使用更严格测试的库,例如openzepplin的此代码版本。
验证付款
与上一章不同,付款渠道中的邮件不会立即兑换。收件人会跟踪最新消息,并在关闭付款渠道时将其兑换。这意味着收件人对每条消息执行自己的验证至关重要。否则,无法保证收件人最终能够获得报酬。
收件人应使用以下过程验证每条消息:
- 验证邮件中的联系地址是否与付款渠道中匹配。
- 验证新总计是否为预期金额。
- 验证新总计不超过托管的以太网数量。
- 验证签名是否有效并来自付款渠道发件人。
我们将使用ethereumjs-util 库来编写此验证。最后一步可以通过多种方式完成,但如果是在JavaScript中完成的话。以下代码借用了 上面签名JavaScript代码中的constructMessage函数:
// this mimics the prefixing behavior of the eth_sign JSON-RPC method.
function prefixed(hash) {
return ethereumjs.ABI.soliditySHA3(
["string", "bytes32"],
["\x19Ethereum Signed Message:\n32", hash]
);
}
function recoverSigner(message, signature) {
var split = ethereumjs.Util.fromRpcSig(signature);
var publicKey = ethereumjs.Util.ecrecover(message, split.v, split.r, split.s);
var signer = ethereumjs.Util.pubToAddress(publicKey).toString("hex");
return signer;
}
function isValidSignature(contractAddress, amount, signature, expectedSigner) {
var message = prefixed(constructPaymentMessage(contractAddress, amount));
var signer = recoverSigner(message, signature);
return signer.toLowerCase() ==
ethereumjs.Util.stripHexPrefix(expectedSigner).toLowerCase();
}