撰写智能合同
在前一章中,我们学习了以太坊区块链是如何工作的,以及 PoW 共识协议是如何保证它的安全的。现在是时候开始写智能合同了,因为我们已经很好地掌握了以太坊是如何工作的。有各种语言来编写以太坊智能合约,但 Solidity 是最受欢迎的一种。在这一章中,我们将学习 Solidity 编程语言。我们将最终构建一个 DApp 来证明给定时间的存在性、完整性和所有权,也就是说,一个 DApp 可以证明文件在特定时间属于特定的所有者。
在本章中,我们将讨论以下主题:
- Solidity 源文件的布局
- 了解可靠性数据类型
- 合同的特殊变量和功能
- 控制结构
- 合同的结构和特征
- 编译和部署合同
实体源文件
使用扩展名.sol
表示实体源文件。就像任何其他编程语言一样,Solidity 也有各种版本。写这本书时的最新版本是 0.4.2。
在源文件中,您可以提到使用pragma Solidity
指令编写代码的编译器版本。
例如,看看下面的内容:
pragma Solidity ^0.4.2;
现在,源文件不能用早于 0.4.2 版本的编译器编译,也不能在 0.5.0 版本的编译器上运行(第二个条件是使用^
添加的)。0.4.2 到 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 保存所有者的以太坊钱包地址,即部署契约的地址。 - 然后,我们定义了一个事件。事件用于通知客户端一些事情。每当
data
改变时,我们将触发该事件。所有的比赛都在区块链举行。 - 然后,我们定义了一个函数修饰符。修饰符用于在执行函数之前自动检查条件。这里,修饰符检查契约的所有者是否正在调用该函数。如果不是,那么它抛出一个异常。
- 然后,我们有了契约构造函数。部署协定时,调用构造函数。构造函数用于初始化状态变量。
- 然后,我们定义了两个方法。第一种方法是获取状态变量
data
的值,第二种方法是改变状态变量data
的值。
在深入了解智能合约的特性之前,让我们先了解一些与可靠性相关的其他重要事情。然后我们将回到合同上来。
数据单元
到目前为止,你所学的所有编程语言都将变量存储在内存中。但是在 Solidity 中,变量根据上下文存储在内存和文件系统中。
根据上下文,总是有一个默认位置。但是对于复杂的数据类型,比如字符串、数组和结构,可以通过向类型追加storage
或memory
来覆盖它。函数参数(包括返回参数)的默认值是内存,局部变量的默认值是存储。并且这个位置被强制存储,对于状态变量(很明显)。
数据位置很重要,因为它们会改变赋值的行为方式:
- 存储变量和内存变量之间的赋值总是创建一个独立的副本。但是从一个内存存储的复杂类型到另一个内存存储的复杂类型的赋值不会创建副本。
- 对一个状态变量的赋值(甚至来自其他状态变量)总是创建一个独立的副本。
- 不能将内存中存储的复杂类型赋给本地存储变量。
- 在将状态变量分配给本地存储变量的情况下,本地存储变量指向状态变量;也就是说,本地存储变量变成了指针。
有哪些不同的数据类型?
Solidity 是一种静态类型的语言;变量保存的数据类型需要预先定义。默认情况下,变量的所有位都被赋值为 0。在 Solidity 中,变量是函数范围的;也就是说,在函数中的任何地方声明的变量都在整个函数的范围内,不管它是在哪里声明的。
现在让我们看看 Solidity 提供的各种数据类型:
- 最简单的数据类型是
bool
。它可以容纳true
或false
。 uint8
、uint16
、uint24
...uint256
用于保存 8 位、16 位、24 位的无符号整数...256 位。同样,int8
,int16
...int256
用于保存 8 位、16 位的有符号整数...256 位。uint
和int
是uint256
和int256
的别名。与uint
和int
类似,ufixed
和fixed
代表分数。ufixed0x8
,ufixed0x16
...ufixed0x256
用于保存 8 位、16 位的无符号小数...256 位。同样,fixed0x8
,fixed0x16
...fixed0x256
用于保存 8 位、16 位的有符号小数...256 位。如果它是一个需要超过 256 位的数,那么使用 256 位数据类型,在这种情况下,存储该数的近似值。address
用于通过分配一个十六进制文字来存储最多 20 字节的值。它用于存储以太坊地址。类型公开了两个属性:balance
和send
。balance
用于检查地址的平衡,send
用于向地址传送以太网。send 方法获取需要转移的 wei 的数量,并根据转移是否成功返回 true 或 false。从调用send
方法的契约中扣除该 wei。您可以在 Solidity 中使用0x
前缀为变量分配十六进制编码的值表示。
数组
Solidity 支持泛型和字节数组。它支持固定大小和动态数组。它还支持多维数组。
bytes1
、bytes2
、bytes3
、...,bytes32
是字节数组的类型。byte
是bytes1
的别名。
下面是一个展示通用数组语法的示例:
contract sample{
//dynamic size array
//wherever an array literal is seen a new array is created. If the array literal is in state than it's stored in storage and if it's found inside function than 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 therefore 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
属性,用于查找数组的长度。还可以为 length 属性赋值以更改数组的大小。但是,不能在内存中调整数组的大小,也不能调整非动态数组的大小。 - 如果试图访问动态数组的未设置索引,则会引发异常。
请记住,数组、结构和映射不能是函数的参数,也不能由函数返回。
用线串
在 Solidity 中,创建字符串有两种方式:使用bytes
和string
。bytes
用于创建一个原始字符串,而string
用于创建一个 UTF-8 字符串。字符串的长度总是动态的。
下面是一个显示字符串语法的示例:
contract sample{
//wherever a string literal is seen a new string is created. If the string literal is in state than it's stored in storage and if it's found inside function than 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;
}
}
结构
坚固性也支持结构。下面是一个显示结构语法的示例:
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 than it's stored in storage and if it's found inside function than its stored in memory
myStruct s2 = myStruct(true, ""); //struct method syntax
function sample(bool initBool, string initString){
//create a instance of struct
s1 = myStruct(initBool, initString);
//myStruct(initBool, initString) creates a instance in memory
myStruct memory s3 = myStruct(initBool, initString);
}
}
请注意,函数参数不能是结构,函数也不能返回结构。
枚举数
Solidity 也支持枚举。下面是一个显示枚举语法的示例:
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;
}
}
绘图
映射数据类型是哈希表。映射只能存在于存储中,而不能存在于内存中。因此,它们只被声明为状态变量。映射可以被认为是由键/值对组成的。该密钥实际上没有被存储;相反,键的 keccak256 散列用于查找该值。映射没有长度。不能将映射分配给另一个映射。
以下是如何创建和使用映射的示例:
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;
}
}
请记住,如果您试图访问一个未设置的密钥,它会给我们所有的 0 位。
删除操作符
delete
操作符可应用于任何变量,以将其重置为默认值。默认值是分配给 0 的所有位。
如果我们将delete
应用于一个动态数组,那么它会删除它的所有元素,长度变成 0。如果我们将它应用于一个静态数组,那么它的所有索引都会被重置。您也可以将delete
应用于特定的索引,在这种情况下,索引将被重置。
如果您将delete
应用到一个地图类型,什么也不会发生。但是如果您将delete
应用于一个映射的键,那么与该键相关联的值将被删除。
这里有一个例子来演示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
的类型也可以转换成address
。
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
、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);
}
}
例外
有些情况下会自动抛出异常。您可以使用throw
手动抛出一个异常。异常的效果是当前正在执行的调用被停止并恢复(也就是说,对状态和余额的所有更改都被撤消)。捕捉异常是不可能的:
contract sample
{
function myFunction()
{
throw;
}
}
外部函数调用
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
可见性应用于状态变量。- 公共函数和状态变量可以通过所有可能的方式访问。编译器生成的访问函数都是公共状态变量。您不能创建自己的访问者。实际上,它只生成 getters,不生成 setters。
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 行从不执行。在第 4 行之后,从第 8 行到第 10 行开始执行。
return
内部修饰符不能有与之关联的值。它总是返回 0 位。
回退功能
一个契约只能有一个名为fallback
函数的未命名函数。该函数不能有参数,也不能返回任何内容。如果其他函数都不匹配给定的函数标识符,则在调用协定时执行该函数。
每当契约接收到以太而没有任何函数调用时,也执行该函数;也就是说,事务向契约发送以太网,并且不调用任何方法。在这样的上下文中,通常只有很少的 gas 可用于函数调用(准确地说,是 2,300 gas),因此使回退函数尽可能便宜是很重要的。
接收以太网但未定义回退函数的协定会引发异常,发回以太网。因此,如果您希望您的合同接收以太网,您必须实现一个回退功能。
以下是回退功能的一个示例:
contract sample
{
function() payable
{
//keep a note of how much Ether has been sent 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 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's in 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 doesn't 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 中,继承通过复制代码来工作。由于库被认为是基础契约的原因,库中具有内部可见性的函数被复制到使用它的契约中;否则,具有库的内部可见性的函数不能被使用该库的协定调用,因为将需要外部调用,而具有内部可见性的函数不能使用外部调用来调用。此外,库中的结构和枚举被复制到使用该库的协定中。
- 库可用于向数据类型添加成员函数。
如果一个库只包含内部函数和/或结构/枚举,那么就不需要部署这个库,因为库中的所有内容都被复制到使用它的契约中。
用于
指令using A for B;
可以用来附加库函数(从库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 there) into the current global scope. "filename" can be a absolute or relative path. It can only be a 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
合同相关
与合同相关的变量如下:
this
:当前合约,明确可转换为address
类型。selfdestruct(address recipient)
:销毁当前合约,将其资金发送到给定地址。
乙醚单位
一个文字数字可以带一个后缀wei
、finney
、szabo
或Ether
在以太的子命名之间转换,其中不带后缀的以太货币数字假定为 wei 例如,2 Ether == 2000 finney
评估为true
。
存在、完整性和所有权合同的证明
让我们写一个可以证明文件所有权而不暴露实际文件的可靠契约。它可以证明文件在特定时间存在,并最终检查文档的完整性。
我们将通过成对存储文件的散列和所有者的名字来证明所有权。我们将通过成对存储文件的散列和块时间戳来证明存在。最后,存储散列本身证明了文件的完整性;也就是说,如果文件被修改,那么它的散列将会改变,契约将无法找到任何这样的文件,因此证明文件被修改。
下面是实现这一切的智能合约的代码:
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 or not 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 to the frontend that 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 编译器;相反,我们将使用 solcjs 和 Solidity 浏览器。Solcjs 允许我们在 Node.js 中编程编译 Solidity,而 browser Solidity 是一个 IDE,适合小契约。
现在,让我们使用以太坊提供的浏览器可靠性来编译前面的契约。在 https://Ethereum.github.io/browser-Solidity/了解更多信息。也可以下载这个浏览器 Solidity 源代码,离线使用。访问 https://github.com/Ethereum/browser-Solidity/tree/gh-pages下载。
使用这种浏览器可靠性的一个主要优点是,它提供了一个编辑器,还生成了部署合同的代码。
在编辑器中,复制并粘贴前面的合同代码。您将看到它会编译并提供 web3.js 代码,以便使用 geth 交互式控制台部署它。
您将得到以下输出:
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: '60606040526......,
gas: 4700000
}, 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 定义,因为它包含所有方法的原型。
现在,在开发人员模式下运行 geth,并启用挖掘。为此,请运行以下命令:
geth --dev --mine
现在打开另一个命令行窗口,输入以下命令打开 geth 的交互式 JavaScript 控制台:
geth attach
这应该会将 JS 控制台连接到另一个窗口中运行的 geth 实例。
在浏览器 Solidity 的右侧面板上,复制 web3 deploy textarea 中的所有内容,并将其粘贴到交互式控制台中。现在按下键进入。你会先得到交易 hash,等一段时间后,在交易被挖掘出来后,你会得到合约地址。事务哈希是事务的哈希,对于每个事务都是唯一的。每个已部署的合同都有一个唯一的合同地址来标识区块链中的合同。
契约地址是根据其创建者的地址(from 地址)和创建者已发送的事务数(事务随机数)确定性地计算出来的。这两个是 RLP 编码,然后使用 keccak-256 哈希算法进行哈希处理。稍后我们将了解更多关于事务 nonce 的信息。你可以在 https://github.com/Ethereum/wiki/wiki/RLP 了解更多关于 RLP 的事情。
现在让我们存储文件细节并检索它们。
放置此代码以广播一个事务来存储文件的详细信息:
var contract_obj = proofContract.at("0x9220c8ec6489a4298b06c2183cf04fb7e8fbd6d4");
contract_obj.set.sendTransaction("Owner Name", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", {
from: web3.eth.accounts[0],
}, function(error, transactionHash){
if (!err)
console.log(transactionHash);
})
这里,用你得到的合同地址替换合同地址。proofContract.at
方法的第一个参数是契约地址。在这里,我们没有提供气体,在这种情况下,它是自动计算的。
现在让我们来查找文件的详细信息。运行以下代码以查找文件的详细信息:
contract_obj.get.call("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
您将得到以下输出:
[1477591434, "Owner Name"]
call 方法用于在当前状态下调用 EVM 上的协定方法。它不广播交易。为了读取数据,我们不需要广播,因为我们有自己的区块链副本。
我们将在接下来的章节中学习更多关于 web3.js 的知识。
摘要
在这一章中,我们学习了 Solidity 编程语言。我们了解了数据位置、数据类型和契约的高级功能。我们还了解了编译和部署智能合约的最快最简单的方法。现在,您应该可以轻松地编写智能合同了。
在下一章中,我们将为智能合约构建一个前端,这将使部署智能合约和运行事务变得容易。