账户结构

以太坊基本数据单元(Metadata):Account

不同于Bitcoin 基于 UTXO 的 Blockchain/Ledger 系统,Ethereum是以 Account/State 模型为核心的基于交易驱动的状态机(Transaction-based State Machine),我们常说的世界状态(State),其根本是反应了某一账户(Account)在某一时刻下的属性。

其中,State 对应的基本数据结构,称为 stateObject(内部维护,不可导出)。当 stateObject 的值由Transaction 的执行而触发数据更新/删除/创建而发生了变化时,我们称为状态转移

即stateObject 的状态从当前的 State 转移到另一个 State

// core/state/state_object.go
type stateObject struct {
  address  common.Address
  addrHash common.Hash // hash(address)
  
  // account state
  data     types.StateAccount
  // 指向stateDB: 真正存储数据的地方
  // 方便调用 StateDB 相关的API对Account所对应的stateObject进行CRUD操作
  db       *StateDB

  // DB error.
  // State objects are used by the consensus core and VM which are
  // unable to deal with database-level errors. Any error that occurs
  // during a database read is memoized here and will eventually be returned
  // by StateDB.Commit.
  dbErr error

  // 内存缓存相关逻辑
  trie Trie // storage trie
  code Code // contract bytecode, 缓存代码当代码被从DB storage中加载出来 

  // 在执行 Transaction 的时候缓存合约修改的持久化数据
  // EOA账户为空
  originStorage  Storage 
  pendingStorage Storage 
  dirtyStorage   Storage 
  fakeStorage    Storage 

  dirtyCode bool // true: 当code被更新
  suicided  bool
  deleted   bool
}

1)EOA账户

的创建:包含本地创建链上注册(stateDB进行链上账户管理),入口函数NewAccount

passphrase入参仅用于加密本地保存私钥的Keystore文件(使用对称加密算法来加密私钥生成),与生成账户的私钥、地址的生成无关。

私钥泄露、助记词泄露、Keystore+密码泄露都会导致账户控制权丢失

// accounts/keystore/keystore.go
// 该API是geth暴露出来,用于方便用户本地创建于管理账户的
func (ks *KeyStore) NewAccount(passphrase string) (accounts.Account, error) {
  // 生成account的函数(ECDSA)
  _, account, err := storeNewKey(ks.storage, crand.Reader, passphrase)
  if err != nil {
    return accounts.Account{}, err
  }

  // 缓存并等待系统落盘
  ks.cache.add(account)
  ks.refreshWallets()
  return account, nil
}
  • 账号创建

    这里实际上只进行了ecdsa计算和keystore存储,实际账户在以太坊世界状态中存储,只需要等待有相关联的Transaction发生,若不存在就会自动通过newObject() //core/state/state_object.go创建

    // internal/ethapi/api.go
    func (s *PersonalAccountAPI) NewAccount(password string) (common.Address, error) {
       ks, err := fetchKeystore(s.am)
       if err != nil {
          return common.Address{}, err
       }
       acc, err := ks.NewAccount(password)
       if err == nil {
          log.Info("Your new key was generated", "address", acc.Address)
          log.Warn("Please backup your key file!", "path", acc.URL.Path)
          log.Warn("Please remember your password!")
          return acc.Address, nil
       }
       return common.Address{}, err
    }
  • 算法生成过程:

    • 第一步:32字节,私钥 (private key)

      伪随机数产生的256bit私钥示例(256bit)

      18e14a7b6a307f426a94f8114701e7c8e774e7f9a47e2c2035db29a206321725

    • 第二步:64字节,公钥 (public key)

      采用椭圆曲线数字签名算法ECDSA-secp256k1将私钥(32字节)映射成公钥(算上前缀65字节)

    ​ (前缀04+X公钥+Y公钥),公钥是椭圆曲线上的一点,故有(X, Y)

      04   50863ad64a87ae8a2fe83c1af1a8403cb53f53e486d8511dad8a04887e5b2352   2cd470243453a299fa9e77237716103abc11a1df38855ed6f2ee187e9c582ba6

    ​ (去掉04前缀)计算公钥的 Keccak-256 哈希值(32bytes):

      fc12ad814631ba689f7abe67 1016f75c54c607f082ae6b0881fac0abeda21781

    • 第三步:20字节,地址 (address)

      取上一步hash值的后20bytes,加上前缀0x,即以太坊地址:

      0x 1016f75c54c607f082ae6b0881fac0abeda21781

    比特币地址生成过程,在上述第三步之后还有其他操作:

    1. 计算RIPEMD-160哈希: 对上一步得到的SHA-256哈希值执行RIPEMD-160哈希运算。这将会得到一个20字节的值。

    2. 添加地址版本前缀: 为了区分主网和测试网的地址,会在RIPEMD-160哈希的前面添加一个字节的版本。例如,比特币主网地址的版本前缀是0x00。

    3. 计算双重SHA-256哈希校验和: 对前一步得到的结果执行两次SHA-256哈希,并取哈希的前4个字节作为校验和。

    4. 组合并进行Base58编码: 将前缀、RIPEMD-160哈希和校验和组合在一起,然后使用Base58编码得到比特币地址。

  • 签名Sign(65字节)

虽然以太坊签名算法也采样了 secp256k1(与比特币相同) ,但是在签名的格式上有所差异,比特币在 BIP66中对签名数据格式采用严格的DER(Distinguished Encoding Rules,可辨别编码规则)编码格式。

ECDSA.spec256k1椭圆曲线签名算法

使用公钥叫加密数据,使用私钥叫签名,签名通过公钥验签

以太坊的签名格式是r+s+vrs是ECDSA签名的原始输出,而末尾的一个字节为恢复id(recovery id简称recid) ,在以太坊中用v表示,是签名的最后一个字节。 65 字节的序列:r 有 32 个字节,s 有 32 个字节,v 有一个字节。

recid称为恢复标识符。因为我们使用的是椭圆曲线算法,仅凭 r 和 s 可计算出曲线上的多个点,因此会恢复出两个不同的公钥(及其对应地址)。v 会告诉我们应该使用这些点中的哪一个(也可以理解为查找次数)。在大多数实现中,v =recid,在内部只是 0 或 1。

// crypto/signature_nocgo.go
func Sign(hash []byte, prv *ecdsa.PrivateKey) ([]byte, error) {
   // 签名是针对32字节的byte,实际上是对应待签名内容的哈希值
   if len(hash) != 32 {
      return nil, fmt.Errorf("hash is required to be exactly 32 bytes (%d)", len(hash))
   }
   ......
   defer priv.Zero()
  // 调用比特币的签名函数,传入secp256k1 、私钥和签名内容,并说明并非压缩的私钥。
  // 此时 SignCompact 函数返还的签名格式为:[27 + recid] [R] [S]
   sig, err := btc_ecdsa.SignCompact(&priv, hash, false) // ref uncompressed pubkey
   if err != nil {
      return nil, err
   }
  
   // 以太坊签名格式是[R] [S] [V],和比特币不同。因此需要进行调换位置
   // 减去27的原因是,比特币中第一个字节的值等于27+recid,因此 recid= sig[0]-27
   v := sig[0] - 27
   copy(sig, sig[1:])
   sig[RecoveryIDOffset] = v
   return sig, nil
}

在以太坊中区块中的数据需要签名的仅有交易Transaction

注意:EIP-155后,在交易签名时,v值不再是recid, 而是 v = recid+ chainID*2+ 35(旧为v = recid+27)

// core/transaction_signing.go
func SignTx(tx *Transaction, s Signer, prv *ecdsa.PrivateKey) (*Transaction, error) {
  // 交易签名时,需要提供一个签名器(Signer)和私钥(PrivateKey)。
  // 需要Singer是因为在EIP155修复重放攻击漏洞后,需要保持旧区块链的签名方式不变,
  // 但又需要提供新版本的签名方式。因此通过接口实现新旧签名方式,根据区块高度创建不同的签名器。
  h := s.Hash(tx)
  sig, err := crypto.Sign(h[:], prv)
  if err != nil {
  	return nil, err
  }
  // 将签名结果解析成三段R、S、V,拷贝交易对象并赋值签名结果。最终返回一笔新的已签名交易。
  // 对应前文transaction的结构V、R、S
  return tx.WithSignature(s, sig)
}

2)CA账户

Storage是一个 key 和 value 都是common.Hash类型的 map 结构,account code 实际存储位置,与EOA账户相比合约账户额外保存了一个存储层(Storage)用于存储合约代码中持久化的变量的数据

对应solidity智能合约的状态变量

Storage 层的基本组成单元称为槽 (Slot)。若干个 Slot 按照Stack的方式顺序集合在一起就构造成了 Storage 层。每个 Slot 的大小是 256 bits(32 bytes)的数据。

Slot 作为基本的存储单元,通过地址索引的方式被上层函数访问。Slot的地址索引的长度同样是32 bytes(256 bits),寻址空间从 0x00000000000000000000000000000000000000000000000000000000000000000xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF。因此在理论状态下,一个 合约 可以最多保存 2^256 bytes (0 - 2^256-1) 的数据,这是个相当大的数字。

# 插槽式的数组存储
----------------------------------
|               0                |     # slot 0
----------------------------------
|               1                |     # slot 1
----------------------------------
|               2                |     # slot 2
----------------------------------
|              ...               |     # ...
----------------------------------
|              ...               |     # 每个插槽大小为 32 字节
----------------------------------
|              ...               |     # ...
----------------------------------
|            2^256-1             |     # slot 2^256-1
----------------------------------

可观测宇宙中约有2^272个原子

// core/state/state_object.go
type Storage map[common.Hash]common.Hash

为了更好的管理数据,Contract 同样使用 MPT(会对slot[key] = value中key值(即slot position)进行hash存储) 作为索引树(Account stroage trie)来管理 Storage 层的Slot。

值得注意的是,合约 Storage 层的数据并不会跟随交易一起,被打包进入 Block 中。只有Account storage Trie root 被保存在 account state 结构体中,而account state构成世界状态state。

因此,当某个 Contract 的 Storage 层的数据发生变化时,任一叶子结点account state的改变会使account storage tire root改变,进而使世界状态state其中的一个叶子结点发生改变,进而改变state root,从而记录到Chain链上(包含在block header中)。

Storage 的数据读取和修改,具体是在执行相关 Transaction 的时候,通过 EVM opcodes中sloadsstore来实际执行的

Last updated