以太坊(Ethereum)是一个开源的有智能合约功能的公共区块链平台。通过其专用加密货币以太币(Ether)提供去中心化的虚拟机(“以太虚拟机” Ethereum Virtual Machine)来处理点对点合约。
Solidity
Solidity 是面向智能合约的高级编程语言,它的设计受 C++、Python和JavaScript影响,旨在针对以太坊虚拟机(EVM)。Solidity 是静态类型语言,支持继承,库和复杂的自定义类型等功能。
或者这么理解:Solidity是用于编写在以太坊区块链上运行的智能合约的最流行的编程语言。 它是一种高级语言,当编译转换为EVM字节码。 这与Java非常相似,其中有诸如 Scala,Groovy,Clojure,JRuby等JVM语言。所有这些编译都生成在JVM(Java虚拟机)中运行的字节码。
Remix
Remix 基于浏览器的IDE,含编译器和运行环境,无需服务端组件。以太坊官方推荐的智能合约开发IDE。
Truffle
Truffle是用于开发以太坊DApps的最常用的框架。 抽象出在区块链上编译和部署合同的许多复杂的东西。
Parity
Parity 是对以太坊协议的另一个很好的实现,并且是用Rust编程语言编写的。 这是一个由一家名为Parity Inc的公司来维护的非官方客户端。任何人都可以实现这个客户端软件,并加入以太坊网络。
Web3.js
javascript库,可以用来与一个节点进行交互。 由于它是一个 JavaScript 库,您可以使用它来构建基于Web的dapps。
网络学习资料
以太坊学习目录
常见问题内容(来自ethereum.mochain.info精通以太坊)
基础相关概念
区块链基础知识
对于程序员来说并不难理解区块链(Blockchains)这个概念,这是因为大多数难懂的东西(挖矿(mining), 哈希函数(hashing), 椭圆曲线密码学(elliptic-curve cryptography), 点对点网络(peer-to-peer networks, 等等)是提供些功能和约定。你只需接受这些既有的特性功能,不必关心底层技术,就想你难懂必须知道亚马逊的 AWS 内部原理,你才能使用它吗?
交易/事务 Transactions
区块链是全球共享的事务性数据库,这意味着每个人都可加入网络来阅读数据库中的记录。如果你想改变数据库中的某些东西,你必须创建一个被所有其他人所接受的事务(Transaction)。事务一词意味着你想做的(假设您想要同时更改两个值)要么一点没完成,要么完完全全生效。此外,当你的事务被应用到数据库时,其他事务不能修改数据库。
举个例子,设想一张表,列出电子货币中所有账户的余额。如果请求从一个账户转移到另一个账户,数据库的事务特性确保了如果从一个账户扣除金额,它总被添加到另一个账户。如果由于某些原因,无法添加金额到目标账户时,源账户也不会发生任何变化。
区块 Blocks
要解决的主要难题,在比特币中被称为“双花攻击 (double-spend attack)”:如果网络存在两笔交易,都想花光同一个账户的钱时(即所谓的冲突)会发生什么情况?
简单的回答是你不必在乎此问题。交易会被排序,并打包到所谓的“块”中,然后它们将在所有参与节点中执行和分发。如果两笔交易相冲突,最后一笔交易将被拒绝,而不成为该块的一部分。
这些块按时间形成了一个线性序列,这正是“区块链”这个词的来源。区块以一定规律的时间间隔添加到链上 - 对于以太坊,这间隔大约是17秒。
作为“顺序选择机制”(也就是所谓的“挖矿”)的一部分,可能会发生这样的情况:块不时地被回滚,但只发送在区块链的“顶端”。在顶端涉及回滚区块越多,其发生的概率越小。所以你的交易有可能被回滚,甚至从区块链中抹除,但你等待的时间越长,这种情况发送的概率就越小。
以太坊虚拟机
总览
以太坊虚拟机 EVM 是智能合约的运行环境。它不仅是沙盒封装的,而且是完全隔离的,也就是说在 EVM 中运行代码是无法访问网络、文件系统和其他进程的。合约与其他合约也是只有有限接触。
账户
以太坊中有两类账户,共用同一个地址空间。外部账户,该类账户被公钥-私钥对控制(人类)。合约账户,该类账户被存储在账户中的代码控制。
外部账户的地址是由公钥决定的,而合约账户的地址是在创建该合约时确定的(这个地址由合约创建者的地址和该地址发出过的交易数量计算得到,地址发出过的交易数量也被称作”nonce”)。
无论帐户是否存储代码,这两类账户对 EVM 来说是一样的。
每个账户有一个key-value形式的持久化存储。其中key和value的长度都是256比特,名字叫做 storage。
此外,每个账户有一个以太币余额(单位是”Wei”),能够通过向它发送含以太币的交易来改变。
交易
交易即消息,从一个账户到另一个账户(这可能是相同的或零帐户,见下文)。它能包含一个二进制数据(称 payload)和以太币。
如果目标账户含有代码,此代码将以 payload 作为入参所执行。
如果目标账户是零账户(账户地址为 0),此交易将创建一个新合约。如前所说,合约地址不是零地址,而是由合约创建者的地址和该地址发出过的交易数量(被称为nonce)计算得到。此交易中的 payload 作为 EVM 字节码所执行。执行结果作为合约代码被永久存储。这意味着,为创建一个合约,你不需要向合约发送真正的合约代码,而是发送能够返回真正代码的代码。
Gas
一经创建,每笔交易都收取一定数量的 gas,目的是限制执行交易所需要的工作量和为交易支付手续费。EVM 执行交易时,gas 将按特定规则逐渐耗尽。
gas price 是被交易发送者设置的一个数值,发送者账户需要预付的手续费= gas price*gas 。交易执行后会将剩余的 gas 返还。
无论执行到什么位置,一旦 gas 被耗尽(比如降为负值),将会触发一个 out-of-gas 异常。当前调用帧所做的所有状态修改都将被回滚。
译者注:即使交易回滚, gas 不会再返还给发送者。哪怕是失败,也是消耗了计算资源。
存储,内存和栈
每个账户有一块持久化内存区被称为存储(storage)。其形式为key-value,key和value的长度均为256比特。在合约中,不能遍历账户的存储,且存储的读操作相对开销高,修改存储开销更高。一个合约只能对它自己的存储进行读写。
第二个内存区称为主存(memory)。合约执行每次消息调用时,都分配一块新的,被清除过的主存。主存是线性的,可按字节级寻址,但读被限制为256比特长度,而写可以是8比特或256比特长度。当访问(读或写)一个从未接触及的值时,主存将以256比特扩容。扩容也将消耗gas。内存越大,开销就越高(平方级别)。
EVM 不是基于寄存器的,而是基于栈的,因此所有的计算都在一个被称为statck的区域执行。 栈最大有1024个元素,每个元素256比特。对栈的访问只限于其顶端,限制方式为:允许拷贝最顶端的16个元素中的一个到栈顶,或者是交换栈顶元素和下面16个元素中的一个。所有其他操作都只能取最顶的两个(或一个,或更多,取决于具体的操作)元素,并把结果压入栈顶。当然可以把栈上的元素放到存储或主存中。但是无法只访问栈上指定深度的那个元素,除非先从栈顶移除其他元素。
指令集
EVM的指令集量尽量少,以尽可能避免可能导致共识问题的错误实现。所有的指令都是针对256比特这个基本的数据类型的操作。具备常用的算术、位、逻辑和比较操作。也可以做到有条件和无条件跳转。此外,合约可以访问当前区块的相关属性,比如它的编号和时间戳。
消息调用
合约可以通过消息调用的方式来调用其它合约或者发送以太币到非合约账户。消息调用和交易非常类似,它们都有一个源、目标、数据、以太币、gas和返回数据。事实上每个交易都由一个顶层消息调用组成,这个消息调用又可创建更多的消息调用。
一个合约可以决定剩余 gas的分配。比如内部消息调用时使用多少gas,或者期望保留多少 gas 。如果在内部消息调用时发生了 out-of-gas 异常(或者其他异常),将会把错误信息将被压如栈顶。这种情况只是内部消息调用的 gas 耗尽。在 Solidity 中,这种情况下发起调用的合约默认会触发一个辅助异常,此异常会打印出调用栈。
正如之前所说,被调用的合约(发起调用的合约也一样)会拥有崭新的主存并能够访问调用 payload - 存储在一个独立的被称为 calldata 的区域中。调用执行结束后,返回数据将被存放在调用方预先分配好的一块内存中。
调用深度被限制为 1024 ,因此对于更加复杂的操作,我们应使用循环而不是递归。
委托调用/代码调用和库
存在一种特殊类型的消息调用,被称为 委托调用(delegatecall)。除了目标地址的代码将在发起调用的合约上下文中执行,以及msg.sender 和 msg.value 不改变外,它跟消息调用一样。
这意味着一个合约可以在运行时从另外一个地址动态加载代码。存储、当前地址和余额都指向发起调用的合约,只有代码是从被调用地址获取的。
这使得 Solidity 可以实现”库“能力:可复用的代码库可以应用在一个合约的存储上,如用来实现复杂的数据结构。
日志
在区块层级上,可用一种特殊的可索引的数据结构来存储数据。这个特性被称为日志(logs),Solidity用它来实现事件(events)。合约创建之后就无法访问日志数据,但是这些数据可以从区块链外高效的访问。因为部分日志数据被存储在布隆过滤器(Bloom filter) 中,我们可以高效并且安全的搜索日志,所以那些没有下载整个区块链的网络节点(轻客户端)也可以找到这些日志。
创建
合约甚至可以通过一个特殊的指令来创建其他合约(不是简单的向零地址发起调用)。创建合约的调用create calls*)跟普通的消息调用的区别在于,payload 数据执行的结果被当作合约代码,调用者/创建者在栈上得到新合约的地址。
自毁
只有在某个地址上的合约执行自毁操作 selfdestruct 时,合约代码才会从区块链上移除。合约账户上剩余的以太币会发送给指定的目标,然后其存储和代码被移除。
一个简单的智能合约
如果你智能合约知之甚少,那从些简单的示例开始是必要的,更多细节后续再讲讲。
存储
pragma solidity ^0.4.0;
contract SimpleStorage {
uint storedData;
function set(uint x) public {
storedData = x;
}
function get() public constant returns (uint) {
return storedData;
}
}
首行简洁的说明该源代码为Solidity 0.4.0版本编写或兼容新版本(直到但不包含0.5.0版本)。已确保合约不会与新版编译器有突异行为。被用关键字pragma原因是,一般编译指令是告知编译器该如何对待源代码(如:pragma once(编译一次))。
一份Solidity所示合同就是一组代码(称为方法)合集和数据(称为状态),而数据位于以太坊区块链上特点地址上。行uint storedData;声明了一个类型为unit(无符号256位整数)的状态变量,名为storeData。你可将其想象为在数据库上的单个插槽,能通过调用管理数据库的代码来访问与修改。在以太坊下,它始终属于合约。这个示例中,方法set和get就能修改和获得此变量值。
为访问状态变量,无需借助这个在其他语言常见的前缀this.。
除允许任何人存储存储一个任何人都可访问的数字,而没有(可行的)方法类阻止你发布这个数字,这个合约做的还远远不够(基于以太坊建立基础设施)。当然,任何人只需再次用不同的值调用set就可覆盖这个数字。但是这个数字仍被存储在区块链的历史记录中。稍后我们来看看如何加强访问限制,以便只有你才能改变它。
提醒
所有标识符(合约名,函数名和变量名)都被限制为 ASCII 字符集。可以将 UTF-8 格式数据存储在字符串变量中。
当心 Unicode 字符
看着很像(甚至一样)的 Unicode 字符是由不同的代码表示的,因此会被编码称不同的字节数组。
子币示例
下面的合约将实现简单版加密货币。可以凭空创建币,但是只有创建这个合约的人才可这样做(实行不同的发行计划是微不足道的)。此外,任何人可以发送币到其他人而不需要使用用户名和密码注册 - 你仅需要一个以太坊密钥。
pragma solidity ^0.4.0;
contract Coin {
// The keyword "public" makes those variables
// readable from outside.
address public minter;
mapping (address => uint) public balances;
// Events allow light clients to react on
// changes efficiently.
event Sent(address from, address to, uint amount);
// This is the constructor whose code is
// run only when the contract is created.
function Coin() public {
minter = msg.sender;
}
function mint(address receiver, uint amount) public {
if (msg.sender != minter) return;
balances[receiver] += amount;
}
function send(address receiver, uint amount) public {
if (balances[msg.sender] < amount) return;
balances[msg.sender] -= amount;
balances[receiver] += amount;
Sent(msg.sender, receiver, amount);
}
}
此合约涉及些新概念,让我们一个个过下。
行 address public minter; 声明一个类型为 address 的状态变量可公开访问。address类型是一个 160 比特值,不允许任何计算操作。它适合存储合约地址或外部人员的密钥对地址。关键字 public 会自动生成一个方法,允许访问外部访问此状态变量当前值。没有此关键字,其他合约就无法访问此变量。这个方法形同如下:
function minter() returns (address) { return minter; }
当然,显示地添加类似的上面的方法是行不通的,因为方法名和状态名相同。可还是望你试试 - 编译器会为你指出错误的。
下一行 mapping (address => uint) public balances; 同样是声明了一个公共的状态变量,但是是一个更复杂的数据类型。 类型将地址映射到无符号整数。mapping 可看成是一个 哈希表 ,它被虚拟初始化后使得每个键 key 都存在被映射到字节全为零的值 value。但此比喻并不严谨,因为即不能获取 mapping 下键列表,也无法获取值列表。所以要记住(或更好,保留一个列表或使用更高级的数据类型),你在添加到 mapping 或在不需要地情况下使用它,就像这个 getter function 由 public 关键字生成的访问函数将会更加复杂,其代码大致如下:
function balances(address _account) public view returns (uint) {
return balances[_account];
}
如你所见,你可使用此方法轻松查询单帐户余额。
行 event Sent(address from, address to, uint amount); 声明一个被称之为 “event” 在方法 send 中末尾行触发。客户端(当然也包括服务器应用程序)可以在没有太多成本的情况下监听在区块链上被触发的事件。一旦事件被触发,监听方就可以接受到 from、to 和 amount这些参数,可轻松跟踪交易。为监听此事件,你可这样使用:
Coin.Sent().watch({}, '', function(error, result) {
if (!error) {
console.log("Coin transfer: " + result.args.amount +
" coins were sent from " + result.args.from +
" to " + result.args.to + ".");
console.log("Balances now:\n" +
"Sender: " + Coin.balances.call(result.args.from) +
"Receiver: " + Coin.balances.call(result.args.to));
}
})
注意,在客户端中是如何调用自动生成的方法 balances。
特殊的方法 Coin 是运行创建合约的构造函数,之后就不能被调用。它会永久存储合约创建人地址: msg (以及 tx 和 block )是神奇的全局变量,包含些可访问的区块链的属性。 msg.sender 总是存放着当前(外部)方法调用方地址。
最后,真正被用户或其他合约所调用的,以完成本合约功能的方法是 mint 和 send。 如果 mint 被合约创建者外的其他人调用则什么也不发送。另一方面,send 能被任何人(已有些币的)发送币给其他人。记住,如果你使用合约发送币给一个地址,当你区块链浏览器上查看该地址时是看不到任何信息的。因为实际上你发送币和更改余额这仅仅存储在特定合约的数据存储器中。通过使用事件,我们可容易地创建一个跟踪合约的交易和余额的“区块链浏览器”。
首先默认安装完成Ubuntu16.04环境。
sudo passwd -u root
输入密码
sudo passwd root
设置root用户密码
安装必要环境
git
sudo apt-get install git
跳出对话输入:Y(下面类同)
安装完成后查看git版本
git version
如图:
Curl
安装Curl
sudo apt-get install curl
curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash
NodeJS
安装NodeJS
sudo apt-get install -y nodejs
安装完成后查看NodeJS版本
nodejs -v
如图:
npm
安装npm
sudo apt-get install npm
安装完成后查看npm版本
npm -v
如图:
solc
安装solc和solc-cli
sudo npm install -g solc solc-cli --save-dev
truffle
安装truffle
sudo npm install -g truffle
安装完成后查看版本
truffle version
testrpc
sudo npm install -g ethereumjs-testrpc
Geth
安装Geth
sudo add-apt-repository -y ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install ethereum
安装完成后查看版本信息
geth version
go(可选)
安装golang
wget https://storage.googleapis.com/golang/go1.10.1.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.10.1.linux-amd64.tar.gz
配置环境变量
cd ~ vi.bashrc
export GOROOT=/usr/local/go
export GOPATH=$HOME/go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
安装完成查看版本
go env
go version
如图:
web3
安装web3
npm install web3@^0.20.0
Ganache
安装Ganache
wget https://github.com/trufflesuite/ganache/releases/download/v1.0.1/ganache-1.0.1-x86_64.AppImage
chmod -x ganache-1.0.1-x86_64.AppImage
./ganache-1.0.1-x86_64.AppImage
配置启动
初始化创世区块
选择目录创建genesis.json文件,编辑保存。
{
"config": {
"chainId": xxxx(修改任意数字),
"homesteadBlock": 0,
"eip155Block": 0,
"eip158Block": 0
},
"alloc" : {},
"coinbase" : "0x0000000000000000000000000000000000000000",
"difficulty" : "0x20000"(修改难度值,便于挖矿),
"extraData" : "",
"gasLimit" : "0x2fefd8",
"nonce" : "0x0000000000000042",
"mixhash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
"parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
"timestamp" : "0x00"
}
完成后运行命令初始化:
geth init 所在目录/genesis.json
完成后输出如图:
启动私有链
geth --identity "OneTestETH" --rpccorsdomain "*" --nodiscover --rpcapi "*" --cache=1024 --networkid 9999 console
说明各个参数作用:
nodiscover 使用这个命令可以确保你的节点不会被非手动添加你的人发现。否则,你的节点可能因为你与他有相同的创世文件和网络ID而被陌生人的区块链无意添加。
maxpeers 0 如果你不希望其他人连接到你的测试链,可以使用maxpeers 0。反之,如果你确切知道希望多少人连接到你的节点,你也可以通过调整数字来实现。
rpc 可激活你节点的 rpc 服务。它在 geth 中通常被默认激活。
rpcapi "db,eth,net,web3" 决定允许哪些 API 开放 rpc 服务。在默认情况下只激活了 web3 api。
rpcport "8080" 将8000改变为你网络上开放的任何端口。Geth的默认设置是8080.
rpccorsdomain "https://xxxx.com" 这个可以指示什么URL能连接到你的节点来执行RPC定制端任务。务必谨慎,输入一个特定的URL而不是wildcard ( * ),后者会使所有的URL都能连接到你的RPC实例。
datadir "/home/TestChain1" 这是你的私有链数据所储存在的数据目录(在nubits下)。选择一个与你以太坊公有链文件夹分开的位置。
identity "TestnetMainNode" 这会为你的节点设置一个身份,使之更容易在端点列表中被辨认出来。这个例子说明了这些身份如何在网络上出现。
networkid 1999 数字类型,区分与其他的网络ID,以太坊公链的网络ID=1。必须区分,以放置钱包等误认为是以太坊公链。 2=Morden (disused), 3=Ropsten, 4=Rinkeby,默认为1。
port 30303 P2P网络监听端口,默认30303。
fast 这个命令是 Geth1.6.0之前的,只会被改成--syncmode=fast,但该命令继续有效。配置此命令能够快速的同步区块。
cache=1024 程序内置的可用内存,单位MB。默认是16MB(最小值)。可以根据服务器能力配置到56, 512, 1024 (1GB), or 2048 (2GB)。
创建解锁账户
创建账户
personal.newAccount("jacky")
personal.newAccount("bill")
解锁账户
personal.unlockAccount(eth.accounts[1],"jacky");
true
personal.unlockAccount(eth.accounts[2],"bill");
true
挖矿
开始挖矿
miner.start(1) 一个线程挖矿
停止挖矿
miner.stop()
普通转账
eth.sendTransaction({from: '发送者', to: '接受者', value: web3.toWei(1, "ether")})
可能会遇到lock的情况,那么把默认账号解锁
personal.unlockAccount("账号", "密码", 300)
常用命令:
查看账户:eth.accounts
查看余额:web3.fromWei(eth.getBalance(账号), "ether")
部署合约
http://remix.ethereum.org 在这里左边编辑代码:
pragma solidity ^0.4.21;
contract hello {
string greeting;
function hello(string _greeting) public {
greeting = _greeting;
}
function say() constant public returns (string) {
return greeting; }
}
右边Details生成部署代码,对话框弹出,选择WEB3DEPLOY-拷贝,粘贴到本地文本编辑器进行修改。
var _greeting = "hello ethereum" ;
var helloContract = web3.eth.contract([{"constant":true,"inputs":[],"name":"say","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_greeting","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]);
var hello = helloContract.new(
_greeting,
{
from: web3.eth.accounts[2],
data: '0x608060405234801561001057600080fd5b506040516102a83803806102a8833981018060405281019080805182019291905050508060009080519060200190610049929190610050565b50506100f5565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061009157805160ff19168380011785556100bf565b828001600101855582156100bf579182015b828111156100be5782518255916020019190600101906100a3565b5b5090506100cc91906100d0565b5090565b6100f291905b808211156100ee5760008160009055506001016100d6565b5090565b90565b6101a4806101046000396000f300608060405260043610610041576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063954ab4b214610046575b600080fd5b34801561005257600080fd5b5061005b6100d6565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561009b578082015181840152602081019050610080565b50505050905090810190601f1680156100c85780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b606060008054600181600116156101000203166002900480601f01602080910402602001604051908101604052809291908181526020018280546001816001161561010002031660029004801561016e5780601f106101435761010080835404028352916020019161016e565b820191906000526020600020905b81548152906001019060200180831161015157829003601f168201915b50505050509050905600a165627a7a723058206b801e5b597ecc38167442c7a44a761b128cd92cf19d6a153f7c0a2caab455ba0029',
gas: '1700000'
}, function (e, contract){
console.log(e, contract);
if (typeof contract.address !== 'undefined') {
console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);
}
})
修改后部署到本地以太坊环境。
没有报错的情况下,说明合约部署完成,那么在console控制台中:
hello.say()
输出:hello ethereum