撰写智能合同
在前一章中,我们学习了法定人数是如何工作的,以及各种共识协议是如何保护它的。现在我们已经了解了 Quorum 是如何工作的,让我们继续编写智能合同。法定智能合同可以使用多种语言编写;最受欢迎的是坚固度。在这一章中,我们将学习 Solidity,并构建一个企业可以用来对文档进行数字签名的 DApp。
在本章中,我们将讨论以下主题:
- Solidity 源文件的布局
- 了解可靠性数据类型
- 特殊变量和合同函数
- 控制结构
- 合同的结构和特征
- 编译和部署合同
本章与作者前一本书项目区块链中的章节相同。这不是再版的书,它是用来向读者解释基本概念的。
实体源文件
您可以通过扩展名.sol
识别实体源文件。和编程语言一样,它有不同的版本。写这本书时的最新版本是0.4.17
。
在源文件中,您可以使用pragma Solidity
指令来指明编写代码的编译器版本。例如:
pragma Solidity ^0.4.17;
需要注意的是,源文件不能用早于0.4.17
和晚于0.5.0
的编译器版本编译(第二个条件是使用^
添加的)。在0.4.17
和0.5.0
之间的编译器版本最有可能包含错误修复,不太可能破坏任何东西。
我们可以为编译器版本指定更复杂的规则;该表达式沿用了npm
所使用的表达式。
智能合同的结构
类似于一个类。它可以是函数、修饰符、状态变量、事件、结构和枚举。契约也支持继承。您可以通过在编译期间复制代码来实现继承。智能合约也可以是多态的。
下面是一个智能合同的例子:
contract Sample
{
//state variables
uint256 data;
address owner;
//event definition
event logData(uint256 dataToLog);
//function modifier
modifier onlyOwner() {
if (msg.sender != owner) throw;
_;
}
//constructor
function Sample(uint256 initData, address initOwner){
data = initData;
owner = initOwner;
}
//functions
function getData() returns (uint256 returnedData){
return data;
}
function setData(uint256 newData) onlyOwner{
logData(newData);
data = newData;
}
}
让我们看看前面提到的代码是如何工作的:
- 首先,我们使用了
contract
关键字来声明一个契约。 - 接下来,我们声明了两个状态变量:
data
保存一些数据;而owner
持有他们以太坊钱包的地址,也就是合约部署的地址。状态变量形成智能合约的状态,并且存储在智能合约的存储器中。智能合约存储在数据库中。 - 然后,我们定义了事件。事件用于客户端通知。只要数据发生变化,我们的事件就会被触发。所有的比赛都在区块链举行。
- 接下来,我们定义了一个修改函数。修饰符在执行函数之前自动检查条件。我们的修饰符检查契约所有者是否是调用该函数的人。如果不是,那么它将抛出一个异常。
- 之后,我们有了契约构造函数。它在部署协定时被调用。构造函数用于初始化状态变量。
- 最后,我们定义了两个方法。第一个方法获取数据状态变量的值,第二个方法更改数据值。
在更深入地研究智能合约特性之前,我们必须了解一些与可靠性相关的重要内容。之后,我们将回到合同上来。
Solidity 中的数据位置
与其他编程语言不同,Solidity 的变量根据上下文存储在内存和数据库中。
默认位置总是存在的,但对于复杂类型的数据(如字符串、数组和结构),可以通过向该类型追加存储或内存来重写它。内存默认为函数参数(包括return
参数),存储为局部和状态变量(很明显)。
数据位置很重要,因为它们会改变赋值的行为:
- 总是为存储变量和内存变量之间的赋值创建一个独立的副本。但是,对于从一个内存存储的复杂类型到另一个复杂类型的赋值,不会创建副本。
- 对于一个状态变量的赋值,总是会创建一个独立的副本(甚至来自其他状态变量)。
- 内存存储的复杂类型不能赋给本地存储变量。
- 如果状态变量被分配给本地存储变量,则本地存储变量指向状态变量;基本上,本地存储变量充当指针。
不同类型的数据
Solidity 是一种静态类型的语言;变量保存的数据类型需要预先定义。默认情况下,变量的所有位都被赋值为零。在 Solidity 中,变量是函数范围的;也就是说,在函数中的任何地方声明的变量都在整个函数的范围内,不管它是在哪里声明的。
以下是 Solidity 提供的数据类型:
- 最简单的数据类型是
bool
。它可以容纳true
或false
。 uint8
、uint16
、uint24
直至uint256
分别用于保存 8 位、16 位、24 位、直至 256 位的无符号整数。类似地,int8
、int16
到int256
分别用于保存 8 位、16 位到 256 位的有符号整数。uint
和int
是uint256
和int256
的别名。ufixed
和fixed
代表分数。ufixed0x8
、ufixed0x16
至ufixed0x256
分别用于保存 8 位、16 位至 256 位的无符号小数。类似地,fixed0x8
、fixed0x16
至fixed0x256
分别用于保存 8 位、16 位至 256 位的有符号小数。如果我们有一个需要 256 位以上的数,那么使用 256 位数据类型,在这种情况下,存储该数的近似值。- 通过分配十六进制文字,Address 用于存储最多 20 字节的值。它用于存储以太坊地址。您可以在 Solidity 中使用
0x
前缀将值的十六进制编码表示分配给变量。
数组
Solidity 支持泛型和字节数组,固定大小和动态数组,以及多维数组。
bytes1
、bytes2
、bytes3
、高达、bytes32
都是字节数组的类型。我们将用字节来表示bytes1
。
下面是一个通用数组语法的示例:
contract sample{
//dynamic size array
//wherever an array literal is seen a new array is created. If the //array literal is in state, then it's stored in storage and if it's //found inside function, then its stored in memory
//Here myArray stores [0, 0] array. The type of [0, 0] is decided based //on its values.
//Therefore, you cannot assign an empty array literal.
int[] myArray = [0, 0];
function sample(uint index, int value){
//index of an array should be uint256 type
myArray[index] = value;
//myArray2 holds pointer to myArray
int[] myArray2 = myArray;
//a fixed size array in memory
//here we are forced to use uint24 because 99999 is the max value and //24 bits is the max size required to hold it.
//This restriction is applied to literals in memory because memory is //expensive. As [1, 2, 99999] is of type uint24, myArray3 also has to //be the same type to store pointer to it.
uint24[3] memory myArray3 = [1, 2, 99999]; //array literal
//throws exception while compiling as myArray4 cannot be assigned to //complex type stored in memory
uint8[2] myArray4 = [1, 2];
}
}
以下是您应该了解的关于阵列的一些重要信息:
- 数组还拥有一个
length
属性,可以用来计算数组的长度。value
也可以赋给length
属性来改变数组大小。但是,内存中的数组或非动态数组不能调整大小。 - 如果访问动态数组的未设置的
index
,则会引发异常。
用线串
可以通过两种方式在 Solidity 中创建字符串:使用bytes
和string
。bytes
用于创建一个原始字符串,而string
用于创建一个 UTF-8 字符串。字符串的长度总是动态的。
这里有一个展示string
语法的例子:
contract sample {
//wherever a string literal is seen, a new string is created. If the //string literal is in state, then it's stored in storage and if it's //found inside function, then its stored in memory
//Here myString stores "" string.
string myString = ""; //string literal
bytes myRawString;
function sample(string initString, bytes rawStringInit){
myString = initString;
//myString2 holds a pointer to myString
string myString2 = myString;
//myString3 is a string in memory
string memory myString3 = "ABCDE";
//here the length and content changes
myString3 = "XYZ";
myRawString = rawStringInit;
//incrementing the length of myRawString
myRawString.length++;
//throws exception while compiling
string myString4 = "Example";
//throws exception while compiling
string myString5 = initString;
}
}
结构
坚固结构。这里有一个显示struct
的语法:
contract sample{
struct myStruct {
bool myBool;
string myString;
}
myStruct s1;
//wherever a struct method is seen, a new struct is created. If
//the struct method is in state, then it's stored in storage
//and if it's found inside function, then its stored in memory
myStruct s2 = myStruct(true, ""); //struct method syntax
function sample(bool initBool, string initString){
//create an instance of struct
s1 = myStruct(initBool, initString);
//myStruct(initBool, initString) creates an instance in memory
myStruct memory s3 = myStruct(initBool, initString);
}
}
枚举数
坚实枚举。这里有一个显示enum
的语法:
contract sample {
//The integer type which can hold all enum values and is the
//smallest is chosen to hold enum values
enum OS { Windows, Linux, OSX, UNIX }
OS choice;
function sample(OS chosen){
choice = chosen;
}
function setLinuxOS(){
choice = OS.Linux;
}
function getChoice() returns (OS chosenOS){
return choice;
}
}
绘图
哈希表是一种映射数据类型。因为映射只能存在于存储中,所以它们被声明为状态变量。你可以把一个映射想象成有key
和value
对。key
并非实际储存;相反,key
的 keccak256 哈希用于查找value
。映射没有长度,不能分配给另一个映射。
下面是一个创建和使用mapping
的例子:
contract sample{
mapping (int => string) myMap;
function sample(int key, string value){
myMap[key] = value;
//myMap2 is a reference to myMap
mapping (int => string) myMap2 = myMap;
}
}
删除操作符
delete
操作符可应用于任何变量,以将其重置为默认值。默认值是所有位都被赋值为零。
如果我们将delete
应用于一个动态数组,它将删除其所有元素,长度变为零。如果我们把它应用于一个静态数组,它的所有索引都会被重置。我们还可以将delete
应用于特定的索引,以重置它们。
但是,如果将delete
应用于一个地图类型,什么也不会发生。但是,如果您将delete
应用到地图的key
,与key
相关的值将被删除。
让我们看看工作中的delete
操作符,如下所示:
contract sample {
struct Struct {
mapping (int => int) myMap;
int myNumber;
}
int[] myArray;
Struct myStruct;
function sample(int key, int value, int number, int[] array) {
//maps cannot be assigned so while constructing struct we
// ignore the maps
myStruct = Struct(number);
//here set the map key/value
myStruct.myMap[key] = value;
myArray = array;
}
function reset(){
//myArray length is now 0
delete myArray;
//myNumber is now 0 and myMap remains as it is
delete myStruct;
}
function deleteKey(int key){
//here we are deleting the key
delete myStruct.myMap[key];
}
}
基本类型之间的转换
除了数组、字符串、结构、枚举和映射之外的一切都被称为基本类型。
如果我们将一个运算符应用于不同的类型,编译器会尝试将其中一个操作数隐式转换为另一个操作数的类型。一般来说,如果在语义上有意义并且不丢失任何信息,值类型之间的隐式转换是可能的:uint8
可转换为uint16
,int128
可转换为int256
,但int8
不可转换为uint256
(因为uint256
不能保持,例如-1
)。此外,无符号整数可以转换为相同大小或更大大小的字节,但不能反过来。任何可以转换成uint160
的类型也可以转换成地址。
Solidity 也支持显式转换。如果编译器不允许两种数据类型之间的隐式转换,您可以选择显式转换。我们建议避免显式转换,因为这可能会产生意想不到的结果。
下面是显式转换的例子,如下:
uint32 a = 0x12345678;
uint16 b = uint16(a); // b will be 0x5678 now
这里我们是将uint32
类型显式转换为uint16
,也就是将一个大的类型转换为一个小的类型;因此,高位被截掉。
使用 var
为了声明变量,Solidity 提供了var
关键字。在这种情况下,变量的类型是动态决定的,取决于分配给它的第一个值。一旦赋值,类型就固定了;如果给它赋另一种类型,就会导致类型转换。
让我们看看var
是如何工作的,如下:
int256 x = 12;
//y type is int256
var y = x;
uint256 z= 9;
//exception because implicit conversion not possible
y = z;
注意在定义数组和映射时不能使用var
。它也不能用于定义函数参数和状态变量。
控制结构
坚固性支持if...else
、do...while
、for
、break
、continue
、return
和?:
控制结构。
这里有一个结构:
contract sample{
int a = 12;
int[] b;
function sample()
{
//"==" throws exception for complex types
if(a == 12)
{
}
else if(a == 34)
{
}
else
{
}
var temp = 10;
while(temp < 20)
{
if(temp == 17)
{
break;
}
else
{
continue;
}
temp++;
}
for(var iii = 0; iii < b.length; iii++)
{
}
}
}
使用 new 运算符创建合同
合同可以使用new
关键字创建新合同。必须知道正在创建的合同的完整代码。
我们来演示一下,如下:
contract sample1
{
int a;
function assign(int b)
{
a = b;
}
}
contract sample2{
function sample2()
{
sample1 s = new sample1();
s.assign(12);
}
}
例外
在某些情况下,会自动引发异常。您可以使用assert()
、revert()
和require()
来抛出手动异常。异常停止并恢复任何当前正在执行的调用(也就是说,对状态和余额的所有更改都被撤消)。在 Solidity 中,还不可能捕捉异常。
以下三行都是 Solidity 中抛出异常的不同方式:
if(x != y) { revert(); }
//In assert() and require(), the conditional statement is an inversion //to "if" block's condition, switching the comparison operator != to ==
assert(x == y);
require(x == y);
assert()
将带走所有的气体,而require()
和revert()
将退还剩余的气体。
Solidity 不支持返回异常原因,但预计很快就会支持。你可以访问 https://github.com/ethereum/solidity/issues/1686 的一期获取更新。然后你就可以写revert("Something bad happened")
和require(condition, "Something bad happened")
了。
外部函数调用
Solidity 有两种函数调用:内部的和外部的。内部函数调用是指一个函数调用同一契约中的另一个函数。外部函数调用是指一个函数调用另一个契约的函数。
下面举一个例子:
contract sample1
{
int a;
//"payable" is a built-in modifier
//This modifier is required if another contract is sending
// Ether while calling the method
function sample1(int b) payable
{
a = b;
}
function assign(int c)
{
a = c;
}
function makePayment(int d) payable
{
a = d;
}
}
contract sample2{
function hello()
{
}
function sample2(address addressOfContract)
{
//send 12 wei while creating contract instance
sample1 s = (new sample1).value(12)(23);
s.makePayment(22);
//sending Ether also
s.makePayment.value(45)(12);
//specifying the amount of gas to use
s.makePayment.gas(895)(12);
//sending Ether and also specifying gas
s.makePayment.value(4).gas(900)(12);
//hello() is internal call whereas this.hello() is
external call
this.hello();
//pointing a contract that's already deployed
sample1 s2 = sample1(addressOfContract);
s2.makePayment(112);
}
}
使用this
关键字进行的调用称为外部调用。函数中的this
关键字代表当前的合同实例。
合同的特征
是时候更深入地研究合同了。让我们从一些新功能开始,然后我们将更深入地了解我们已经看到的功能。
能见度
状态变量或函数的可见性定义了谁可以看到它。可见性有四种:external
、public
、internal
和private
。
默认情况下,函数的可见性是public
,状态变量的可见性是internal
。让我们看看这些可见性函数的含义:
external
:外部函数只能从其他合同或通过交易调用。例如,我们不能在内部调用一个f
外部函数:f()
不会工作,但是this.f()
会。我们也不能将external
可见性应用于状态变量。- 公共函数和状态变量可以用任何可能的方式访问。编译器生成的访问器函数都是
public
状态变量。不可能创建我们自己的访问器。实际上,它只产生吸气器,而不产生setter。 internal
:内部函数和状态变量只能在内部访问,即从当前契约和继承它的契约内部访问。我们不能使用this
来访问它。- 私有函数和状态变量类似于内部函数和状态变量,只是它们不能被继承契约访问。
下面是演示可见性和访问器的代码示例:
contract sample1
{
int public b = 78;
int internal c = 90;
function sample1()
{
//external access
this.a();
//compiler error
a();
//internal access
b = 21;
//external access
this.b;
//external access
this.b();
//compiler error
this.b(8);
//compiler error
this.c();
//internal access
c = 9;
}
function a() external
{
}
}
contract sample2
{
int internal d = 9;
int private e = 90;
}
//sample3 inherits sample2
contract sample3 is sample2
{
sample1 s;
function sample3()
{
s = new sample1();
//external access
s.a();
//external access
var f = s.b;
//compiler error as accessor cannot used to assign a value
s.b = 18;
//compiler error
s.c();
//internal access
d = 8;
//compiler error
e = 7;
}
}
功能修饰符
我们已经看到了什么是函数修饰符,我们写了它的一个基本版本。现在我们来详细看看。
修改量由子契约继承,也可以被子契约覆盖。通过在空格分隔的列表中指定多个修饰符,可以将它们应用于一个函数,并且它们将按顺序进行计算。您也可以将参数传递给修饰符。
在修饰符内部,下一个修饰符体或函数体,无论哪一个出现,都被插入到_;
出现的地方。
让我们来看看函数修饰符的复杂代码示例,如下所示:
contract sample
{
int a = 90;
modifier myModifier1(int b) {
int c = b;
_;
c = a;
a = 8;
}
modifier myModifier2 {
int c = a;
_;
}
modifier myModifier3 {
a = 96;
return;
_;
a = 99;
}
modifier myModifier4 {
int c = a;
_;
}
function myFunction() myModifier1(a) myModifier2 myModifier3 returns (int d)
{
a = 1;
return a;
}
}
以下是myFunction()
的执行方式:
int c = b;
int c = a;
a = 96;
return;
int c = a;
a = 1;
return a;
a = 99;
c = a;
a = 8;
在这里,当你调用myFunction
方法时,它将返回0
。但是在那之后,当你试图访问状态变量a
,你会得到8
。
在修饰符或函数体中立即离开整个函数,返回值被赋给它需要的任何变量。
在函数的情况下,return
之后的代码在调用者的代码执行完成后执行。在修饰符的情况下,在调用者的代码执行完成后,执行前面修饰符中_;
之后的代码。在前面提到的例子中,第 5、6 和 7 行永远不会被执行。在第四行之后,执行直接从第八到第十行开始。
return
内部修饰符不能有与之关联的值。它总是返回 0 位。
回退功能
回退函数是契约可以拥有的唯一未命名函数。该函数不能有参数,也不能返回任何内容。如果其他函数都不匹配给定的函数标识符,则在调用协定时执行该函数。
每当契约接收到以太而没有任何函数调用时,也执行该函数;也就是说,事务将以太坊发送给契约,并且不调用任何方法。在这种情况下,通常只有很少的气体可用于函数调用(准确地说是 2300 个气体),因此使回退函数尽可能便宜很重要。
当协定收到以太网但没有定义的回退函数时,它们会引发异常,从而发回以太网。因此,如果您希望您的合同接收以太坊,您必须实现一个回退功能。
以下是回退功能的一个示例:
contract sample
{
function() payable
{
//Note how much Ether has been sent and by whom
}
}
遗产
Solidity 通过复制代码支持多重继承,包括多态。即使一个合同从多个其他合同继承而来,也只能在区块链上创建一个合同。此外,父协定中的代码将总是被复制到最终协定中。
让我们回顾一个继承的例子:
contract sample1
{
function a(){}
function b(){}
}
//sample2 inherits sample1
contract sample2 is sample1
{
function b(){}
}
contract sample3
{
function sample3(int b)
{
}
}
//sample4 inherits from sample1 and sample2
//Note that sample1 is also a parent of sample2; yet there is only a
// single instance of sample1
contract sample4 is sample1, sample2
{
function a(){}
function c(){
//this executes the "a" method of sample3 contract
a();
//this executes the "a" method of sample1 contract
sample1.a();
//calls sample2.b() because it is last in the parent
contracts list and therefore it overrides sample1.b()
b();
}
}
//If a constructor takes an argument, it needs to be provided at
//the constructor of the child contract.
//In Solidity, child constructor does not call parent constructor,
// instead parent is initialized and copied to child
contract sample5 is sample3(122)
{
}
超级关键词
关键字super
用于引用最终继承链中的下一个契约。以下是一个示例,可以帮助您更好地理解它:
contract sample1
{
}
contract sample2
{
}
contract sample3 is sample2
{
}
contract sample4 is sample2
{
}
contract sample5 is sample4
{
function myFunc()
{
}
}
contract sample6 is sample1, sample2, sample3, sample5
{
function myFunc()
{
//sample5.myFunc()
super.myFunc();
}
}
关于sample6
契约的最终继承链是sample6
、sample5
、sample4
、sample2
、sample3
、sample1
。继承链从派生程度最高的契约开始,到派生程度最低的契约结束。
抽象合同
抽象契约是那些只包含功能原型而不包含实现的契约。它们不能被编译(即使它们包含已实现的函数和未实现的函数)。如果一个契约继承自一个抽象契约,并且没有通过重写实现所有未实现的功能,那么它本身就会变成抽象的。
提供抽象契约的原因是为了让编译器知道这个接口。这对于引用已部署的协定并调用其函数非常有用。
让我们演示一下,如下所示:
contract sample1
{
function a() returns (int b);
}
contract sample2
{
function myFunc()
{
sample1 s =
sample1(0xd5f9d8d94886e70b06e474c3fb14fd43e2f23970);
//without abstract contract this wouldn't have compiled
s.a();
}
}
图书馆
库类似于契约,但是它们只在特定的地址部署一次,并且它们的代码被不同的契约重用。这意味着如果调用库函数,它们的代码将在调用契约的上下文中执行;因此,this
指向调用协定,具体来说,允许从调用协定访问存储。由于库是一段孤立的源代码,所以只有显式地提供了状态变量,它才能访问调用契约的状态变量(否则就没有办法命名它们)。
库可以包含结构和枚举,但不能有状态变量。他们不支持继承,也不能接收以太坊。
一旦 Solidity 库被部署到区块链,任何人都可以使用它,只要知道它的地址并有源代码(只有原型或完整的实现)。Solidity 编译器需要源代码,因此它可以确保被访问的方法确实存在于库中。
这里有一个例子:
library math
{
function addInt(int a, int b) returns (int c)
{
return a + b;
}
}
contract sample
{
function data() returns (int d)
{
return math.addInt(1, 2);
}
}
不能在协定源代码中添加库的地址。我们需要在编译期间向编译器提供库地址。
库有许多用例。两个主要的用例如下:
- 如果您有几个包含一些公共代码的契约,那么您可以将这些公共代码部署为一个库。这样会省油,这也要看合同大小。因此,我们可以将库视为使用它的契约的基础契约。用一个基础契约代替一个库来拆分通用代码是不会省油的,因为 Solidity 中的继承是通过复制代码来实现的。因为库被认为是基础契约,所以库中具有内部可见性的函数被复制到使用它的契约中。否则,具有库的
internal
可见性的函数不能被使用该库的契约调用,因为需要外部调用。不能使用外部调用来调用具有internal
可见性的函数。此外,库中的结构和枚举被复制到使用该库的协定中。 - 库可用于向数据类型添加成员函数。
不需要部署只包含内部函数和/或结构/枚举的库,因为库中的所有内容都被复制到使用它的契约中。
用于
指令可以用来附加库函数(从库,A
,到任何类型,B
)。这些函数将接收调用它们的对象作为它们的第一个参数。
using A for *;
的作用是来自库A
的函数被附加到所有类型上。
这里有一个例子来演示for
:
library math
{
struct myStruct1 {
int a;
}
struct myStruct2 {
int a;
}
//Here we have to make 's' location storage so that we
//get a reference.
//Otherwise addInt will end up accessing/modifying a
//different instance of myStruct1 than the one on which its invoked
function addInt(myStruct1 storage s, int b) returns (int c)
{
return s.a + b;
}
function subInt(myStruct2 storage s, int b) returns (int c)
{
return s.a + b;
}
}
contract sample
{
//"*" attaches the functions to all the structs
using math for *;
math.myStruct1 s1;
math.myStruct2 s2;
function sample()
{
s1 = math.myStruct1(9);
s2 = math.myStruct2(9);
s1.addInt(2);
//compiler error as the first parameter of addInt is
//of type myStruct1 so addInt is not attached to myStruct2
s2.addInt(1);
}
}
返回多个值
坚固性允许函数返回多个值。让我们演示一下,如下所示:
contract sample
{
function a() returns (int a, string c)
{
return (1, "ss");
}
function b()
{
int A;
string memory B;
//A is 1 and B is "ss"
(A, B) = a();
//A is 1
(A,) = a();
//B is "ss"
(, B) = a();
}
}
导入其他实体源文件
Solidity 允许一个源文件导入其他源文件。这里有一个例子来说明这一点:
//This statement imports all global symbols from "filename" (and //symbols imported therein) to the current global scope. "filename" can //be an absolute or relative path. It can only be an HTTP URL
//import "filename";
//creates a new global symbol symbolName whose members are all the //global symbols from "filename".
import * as symbolName from "filename";
//creates new global symbols alias and symbol2 which reference symbol1 //and symbol2 from "filename", respectively.
import {symbol1 as alias, symbol2} from "filename";
//this is equivalent to import * as symbolName from "filename";.
import "filename" as symbolName;
全局可用的变量
有一些特殊的变量和函数总是全局存在的。我们将在接下来的章节中讨论它们。
块和事务属性
块和事务属性如下:
block.blockhash(uint blockNumber) returns (bytes32)
:给定块的哈希只对 256 个最近的块有效。block.coinbase (address)
:当前块的矿工地址。block.difficulty (uint)
:当前方块的难度。block.gaslimit (uint)
:当前区块的气体极限。它定义了整个块中的所有事务被允许消耗的最大气体量。它的目的是保持块传播和处理时间低,从而允许一个足够分散的网络。矿工有权将当前区块的气体限制设置在最后一个区块的气体限制的约 0.0975%(1/1024)内,因此得到的气体限制应该是矿工偏好的中间值。block.number (uint)
:当前块的编号。block.timestamp (uint)
:当前块的时间戳。msg.data (bytes)
:完整的调用数据保存了事务调用的函数及其参数。msg.gas (uint)
:剩余气体。msg.sender (address)
:消息的发送方(当前呼叫)。-
msg.sig (bytes4)
:调用数据的前四个字节(函数标识符)。 -
msg.value (uint)
:随消息发送的魏数。 now (uint)
:当前块的时间戳(block.timestamp 的别名)。tx.gasprice (uint)
:交易的气价。tx.origin (address)
:交易的发送方(全调用链)。
与地址类型相关的变量
与地址类型相关的变量如下:
<address>.balance (uint256)
:卫中地址的平衡。<address>.send(uint256 amount) returns (bool)
:发送给定的金额给address
;失败时返回false
。即使执行失败,当前契约也不会因异常而停止。<address>.transfer(uint256 amount)
:将小薇发送到一个地址。如果执行耗尽汽油或失败,乙醚转移将被逆转,当前合同将异常终止。
合同相关变量
与合同相关的变量如下:
this
:当前合同,可明确转换为地址类型selfdestruct(address recipient)
:销毁当前合同,将其资金发送到给定地址
乙醚单位
文字数字可以带后缀wei
、finney
、szabo
或ether
在以太的子面值之间转换,其中不带后缀的以太货币数字假定为 wei。例如,2 Ether == 2000 finney
评估为true
。
存在、完整性和所有权合同的证明
如今,企业正在使用电子签名解决方案来签署协议。但是,这些文件的详细信息存储在可以轻易更改的数据库中,因此不能信任它们来进行审计。区块链可以通过集成区块链作为这些电子签名系统的解决方案来解决这个问题。
让我们写一个可以证明文件所有权而不暴露实际文件的可靠契约。它可以证明文件在特定时间存在,并检查文件的完整性。
企业可以使用这个解决方案来存储他们在区块链上的协议的散列。在区块链上这样做的好处是可以证明协议日期/时间、协议的实际条款等等。
我们将通过成对存储文件的散列和所有者的名字来证明所有权。所有者可以是创建协议的企业。另一方面,我们将通过成对存储文件的散列和块时间戳来证明存在。最后,存储散列本身证明了文件的完整性。如果文件被修改,它的散列将会改变,契约将无法找到该文件,从而证明该文件被修改。
我们将使用 Quorum 的私人交易,因为实体之间签署的协议对他们来说是私人的,细节不会暴露给其他实体。虽然只有文件的散列会被公开,但是让其他实体知道一个实体签署了多少个协议仍然不是一个好主意。
下面是实现这一切的智能合约的代码:
contract Proof
{
struct FileDetails
{
uint timestamp;
string owner;
}
mapping (string => FileDetails) files;
event logFileAddedStatus(bool status, uint timestamp,
string owner, string fileHash);
//this is used to store the owner of file at the block timestamp
function set(string owner, string fileHash)
{
//There is no proper way to check if a key already exists,
//therefore we are checking for default value i.e., all bits are 0
if(files[fileHash].timestamp == 0)
{
files[fileHash] = FileDetails(block.timestamp, owner);
//we are triggering an event so that the frontend of our app
//knows that the file's existence and ownership
//details have been stored
logFileAddedStatus(true, block.timestamp, owner, fileHash);
}
else
{
//this tells the frontend that the file's existence and
//ownership details couldn't be stored because the
//file's details had already been stored earlier
logFileAddedStatus(false, block.timestamp, owner, fileHash);
}
}
//this is used to get file information
function get(string fileHash) returns (uint timestamp, string owner)
{
return (files[fileHash].timestamp, files[fileHash].owner);
}
}
编译和部署合同
以太坊提供了 solc 编译器,它提供了命令行接口来编译.sol
文件。访问http://solidity . readthe docs . io/en/develop/installing-solidity . html # binary-packages找到如何安装它的说明,访问https://solidity . readthe docs . io/en/develop/using-the-compiler . html找到如何使用它的说明。我们不会直接使用 solc 编译器;相反,我们将使用浏览器可靠性。Browser Solidity 是一个 IDE,适合小合同。
现在,让我们使用 browser Solidity 编译前面的契约。在 https://Ethereum.github.io/browser-Solidity/了解更多信息。也可以下载浏览器 Solidity 源代码离线使用:【https://github.com/Ethereum/browser-Solidity/tree/gh-pages。
使用 browser Solidity 的一个主要优点是,它提供了一个编辑器,还可以生成代码来部署契约。
在编辑器中,复制并粘贴前面的合同代码。您将看到它会编译并提供 web3.js 代码,以便使用 Geth 交互式控制台部署它。
您将获得没有privateFor
属性的以下输出:
var proofContract = web3.eth.contract([{"constant":false,"inputs":[{"name":"fileHash","type":"string"}],"name":"get","outputs":[{"name":"timestamp","type":"uint256"},{"name":"owner","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"owner","type":"string"},{"name":"fileHash","type":"string"}],"name":"set","outputs":[],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"status","type":"bool"},{"indexed":false,"name":"timestamp","type":"uint256"},{"indexed":false,"name":"owner","type":"string"},{"indexed":false,"name":"fileHash","type":"string"}],"name":"logFileAddedStatus","type":"event"}]);
var proof = proofContract.new(
{
from: web3.eth.accounts[0],
data: '0x606060.......',
gas: 4700000,
privateFor: ['CGXyBlYOGgU4fZ7n8dVLaTW24p+ZOF8kSiUJkQCUABk=',
'zumojc44Dge0juFgph4xzqOUyNVw+QNZUaY7wOL0P0o=']
}, function (e, contract){
console.log(e, contract);
if (typeof contract.address !== 'undefined') {
console.log('Contract mined! address: ' + contract.address + '
transactionHash: ' + contract.transactionHash);
}
})
代表 EVM 理解的合同的编译版本(字节码)。源代码首先被转换成操作码,操作码然后被转换成字节码。每个操作码都有与之相关联的gas
。
web3.eth.contract
的第一个论点是 ABI 定义。ABI 定义包含所有方法的原型,在创建事务时使用。
现在是时候部署智能合约了。在继续之前,确保您启动了我们在上一章中创建的具有三个节点的 raft 网络。我们将假设这三个节点属于三个不同的企业。还要确保启用了星座,并复制所有星座成员的公钥。在privateFor
数组中,用您生成的公钥替换公钥。在这里,我使私有智能契约对所有三个网络成员可见。
privateFor
is only used when sending a private transaction. It's assigned to an array of the recipients' base64-encoded public keys. In the preceding code, in the privateFor
array, I only have two public keys. That's because the sender doesn't have to add its public key to the array. If you add it, then it will throw an error.
在第一个节点的交互控制台中,使用personal.unlockAccount(web3.eth.accounts[0], "", 0)
无限期解锁以太坊账户。
在 browser Solidity 的右侧面板上,复制 web3 deploy textarea 中的所有内容,然后添加privateFor
并粘贴到第一个节点的交互控制台中。现在按下键进入。你会先得到交易 hash,等一段时间后,在交易被挖掘出来后,你会得到合约地址。事务哈希是事务的哈希,对于每个事务都是唯一的。每个部署的合同都有一个唯一的合同地址,用于标识区块链中的合同。
契约地址是根据其创建者的地址(from
地址)和创建者已发送的事务数量(事务随机数)确定性地计算出来的。这两个是 RLP 编码的,然后使用 keccak256 哈希算法进行哈希处理。稍后我们将了解更多关于事务 nonce 的信息。在https://github.com/Ethereum/wiki/wiki/RLP可以了解更多关于递归长度前缀 ( RLP )。
现在让我们存储文件细节并检索它们。假设前两个实体已经签署了一项协议,并希望在区块链上存储文件的详细信息。放置此代码以广播一个事务来存储文件的详细信息:
var contract_obj = proofContract.at
("0x006c3e992b6e3f52e81560aa3ef6d66e1706b45c");
contract_obj.set.sendTransaction("Enterprise 1",
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
{
from: web3.eth.accounts[0],
privateFor: ['CGXyBlYOGgU4fZ7n8dVLaTW24p+ZOF8kSiUJkQCUABk=']
}, function(error, transactionHash){
if (!err)
console.log(transactionHash);
})
这里,用你得到的合同地址替换合同地址。proofContract.at
方法的第一个参数是契约地址。这里,我们没有提供气体,在这种情况下,它是自动计算的。最后,由于这是前两个实体之间的协议,并且第一个实体使用第二个实体的公钥发送事务,因此我们在privateFor
属性中有第二个实体的公钥。
现在运行下面的代码来查找文件的详细信息:
contract_obj.get.call("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934c
a495991b7852b855");
您将得到以下输出:
[1477591434, "Owner Name"]
call 方法用于在当前状态下调用 EVM 上的协定方法。它不广播交易。为了读取数据,我们不需要广播,因为我们有自己的区块链副本。
如果您在节点 3 中运行前面的代码,那么您将不会获得任何详细信息,因为数据对第三个实体是不可见的。但是第一和第二节点可以读取细节。我们将在接下来的章节中学习更多关于 web3.js 的知识。
摘要
在这一章中,我们学习了 Solidity 编程语言。我们了解了数据位置、数据类型和契约的高级功能。我们还了解了编译和部署智能合约的最快最简单的方法。现在,您应该可以轻松地编写智能合同了。
在下一章中,我们将为智能合约构建一个前端,这将使部署智能合约和运行事务变得容易。