相比较于UTXO模型,Account模型更加符合我们的认知。其类似传统的银行账户,无论如何转账账户地址都是保持不变的,除了注销账户重新开户,以太坊采用账户模型主要是为了支持智能合约,对于智能合约来说需要一个相对稳定的身份。当我们签订一份合同,希望双方的身份明确,权责清晰。但是在UTXO模型中,身份可以通过找零地址变更,当合同出现问题很难明确主体,给使用带了很多的不便。
针对这些问题,以太坊作为智能合约操作平台采用了账户模型,但是又和传统意义上的账户有所不同。它对去中心化的场景稍微进行了改造,在以太坊中将账户划分为两类:外部账户(EOAs)和合约账户(contract account),虽然二者功能有所区分,但是却都使用相同的地址空间。
外部账户(external owned accouts)
和传统的银行账户很像,用工具生成一个私钥作为账户的密码。谁掌握这个私钥,谁就可以控制这个账户,并且是不可找回的,因此也需要妥善保管。当拥有这个私钥以后,通过椭圆曲线算法生成一个公钥,然后通过keccak256(public key)得到结果后再取后40位得到,当然这一切都有成熟的工具,跨语言跨平台,并不需要为这个过程担忧太多。
外部账户的核心就是私钥,创建的外部账户具有以下特点;
- 拥有以太余额;
- 能发送交易,包括转账和执行合约代码;
- 被私钥控制;
- 没有相关的可执行代码;
还需要特别注意的一点是当我们在本地创建的账户并不会立刻存在于以太坊上,对于以太坊来说,本地创建的账户地址在链上没有交易提及到的时候保存下来是没有意义的,保存只会占用链上资源。一旦有交易向这个账户转移ETH,保存这个账户地址的意义就开始体现,以太坊就会把这个地址存储起来,这个时候就可以向以太坊查询到这个地址的信息。这一机制也避免了恶意用户大量创建无效地址占用链上资源的行为。
合约账户就是含有合约代码的账户,被外部账户或者合约创建,合约在创建时被自动分配到一个账户地址,用于存储合约代码以及合约部署或执行过程中产生的存储数据。 合约账户地址是通过SHA3哈希算法产生,而非私钥。 因为没有私钥,所以没有人可以拿合约账户当做外部账户使用,只能通过外部账户来驱动合约账户执行合约代码。其具有如下特点;
- 不能发送交易;
- 合约账户接收到外部账户发来的交易以后可以通过Message调用其他合约账户;
- 合约账户存储了合约代码和合约状态;
下面是合约地址生成算法:Keccak256(rlp([sender,nonce])[12:]
// crypto/crypto.go:74
func CreateAddress(b common.Address, nonce uint64) common.Address {
data, _ := rlp.EncodeToBytes([]interface{}{b, nonce})
return common.BytesToAddress(Keccak256(data)[12:])
}
其中b
是合约创建者的地址,nonce
是创建合约交易的nonce
。
因为合约由其他账户创建,因此将创建者地址和该交易的随机数进行哈希后截取部分生成。
特别需要注意的是,在EIP1014中提出的另一种生成合约地址的算法。其目的是为状态通道提供便利,通过确定内容输出稳定的合约地址,且在部署合约前就可以知道确切的合约地址。下面是算法方法:keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:]
。
// crypto/crypto.go:81
func CreateAddress2(b common.Address, salt [32]byte, inithash []byte) common.Address {
return common.BytesToAddress(Keccak256([]byte{0xff}, b.Bytes(), salt[:], inithash)[12:])
}
由外部账户发起的交易,无论这笔交易是查询,转账,或者是触发任何操作都必须有两个参数from
和to
。from
表示自己的地址,说明从哪里来,to
说明这个交易发送到哪里去。而当to
为空的时候,并不知道接收方是谁,这个时候就表示这笔交易是部署合约的交易,生成一个地址作为合约地址,而交易的内容就被当做合约的内容部署再新生成的地址,这也是以太坊区分二者的判断因素。
综上,下面表格列出两类账户差异,合约账户更优于外部账户, 但外部账户是人们和以太坊沟通的唯一媒介,和合约账户相辅相成。
上面有列出多重签名,是因为以太坊外部账户只由一个独立私钥创建,无法进行多签。但合约具有可编程性,可编写符合多重签名的逻辑,实现一个支持多签的账户。
合约账户可以设置多重签名(multisign),比如一个简单示例是:现有一个合约账户,它要求一个转账由发起转账的人(Alice)和另一个人(Charles)签名。因此,当 Alice 通过这个合约向 Bob 转账 20 个 ETH 时,合约会通知 Charles 签名,在他签名后,Bob 才可以收到这 20 个 ETH。如下图所示:
在以太坊中,合约账户之间是不能主动发送消息的,只有当外部账户A调用合约账户B,合约账户B才可以和合约账户C发送消息。而合约账户B和C之间的交互叫做跨合约调用。因为跨合约调用的存在,一个合约可以肆无忌惮地调用另一个合约的方法。当被调用者的合约方法出现漏洞时,攻击者可以很容易地利用跨合约调用进行攻击,事实证明以太坊中大量的合约安全问题都是由此引起的,所以在编写智能合约的时候要十分注意。
世界状态是以太坊中账户状态的总和,既包括外部账户中的余额,也包括合约账户中的各种合约状态。这些状态通过默克尔帕特里夏树(Merkle Patricia Tree)来进行组织,每当以太坊网络中执行一笔交易,这个状态也就随之改变。
当我们想知道账户余额或者合约状态的时候只需要对组织世界状态的默克尔帕特里夏树进行查询即可。无论分布在任何地方的以太坊节点,正常情况下其世界状态都是一致的。