NULS设计文档解读——账户模块

摘要:NULS,让区块链更简单!

为什么要有账户模块

在区块链中,信息加密、数字签名和登录认证等应用场景,都需要使用非对称加密算法。非对称加密通常在加密和解密过程中使用两个非对称的密码,分别称为公钥和私钥。

非对称密钥对(公钥和私钥)具有两个特点:一是用其中一个密钥 (公钥或私钥) 加密信息后,只有另一个对应的密钥才能解开;二是公钥可向其他人公开,私钥则保密,其他人无法通过该公钥推算出相应的私钥。

在NULS2.0中,我们选择使用的非对称加密算法是椭圆曲线算法,通过椭圆曲线算法生成公钥和私钥之后,我们需要对公钥和私钥进行管理。

账户模块就是用于管理用户的公钥和私钥的。本质上,账户模块的所有功能都是基于公钥和私钥实现的,例如,基于公钥生成地址,基于密码和私钥生成Keystore,通过私钥进行交易签名等。

账户模块功能

账户模块功能体现了该模块的作用,也是开发该模块需要达到的目标。读者阅读本文档的目标是,理解账户模块具有哪些功能,以及这些功能的实现流程。

账户模块的功能主要分为四类:

账户管理转账交易设置别名交易签名

下面我们将详细讲解以上四类功能的作用及实现流程。

账户管理

账户管理主要包含以下子功能:

创建账户导入账户账户备份修改密码移除账户账户信息查询

创建账户功能

通过创建账户功能,用户可以获得一个NULS账户。

NULS账户中,最重要的属性就是NULS地址,理解NULS账户,要先从理解NULS地址开始。

NULS地址格式

一个NULS地址由多个部分组成,这有点像现实世界的地址,现实世界的地址通常会包含国家、省、市、区等多个组成部分,例如,中国北京市朝阳区朝阳公园南路XX号。

NULS地址(addressString)由前缀(prefix)、分隔符、链ID(chainId)、地址类型(addressType)、公钥摘要(pkh)、校验位(xor)组成。

addressType = prefix +分隔符+ Base58Encode(chainId+addressType+pkh+xor)

chainId,为当前链的链id,用于区分不同的区块链。chainId占2个字节,取值范围是1~65535,chainId是地址中非常重要的数据,是跨链操作的基础;

addressType,地址类型有三种取值,分别为 :1:普通地址,2:智能合约地址,3:多重签名地址。addressType占一个字节,取值范围为1~128;

prefix,前缀是为了便于识别、区分不同链的地址。目前,NULS提供了3种prefix的生成方案:

默认前缀NULS和tNULS。NULS主网chainId为1,默认NULS主网(即chainId为1的链)地址以“NULS”为前缀;NULS核心测试网chainId为2,默认NULS核心测试网(即chainId为2的链)地址以”tNULS”为前缀;

通过登记跨链设置前缀。 在登记跨链时,需要手动填写此链的前缀,系统会维护chainId和前缀的对应关系表,根据对应关系表生成相应的地址前缀;

自动计算。NULS同构体系(通过ChainBox&ChainFactory开发搭建)的区块链,将按照如下的代码规则,自动计算生成一个地址前缀:

//将chainId转换为字节数组,使用base58算法对字节数组进行计算,计算后全部转为大写字母
String prefix = Base58.encode(SerializeUtils.int16ToBytes(chainId)).toUpperCase();

pkh,公钥摘要。1)生成一个NULS地址,首先需要基于椭圆曲线算法获得一个公私钥对(ECKey)。NULS的椭圆曲线参数和比特币一样,使用secp256k1;2)ECKey与地址的关联关系就体现在这一部分,NULS的做法是先用Sha-256对公钥进行一次计算,得到的结果再通过 RIPEMD160进行一次计算得到20个字节的结果,就是pkh;

xor,校验位。NULS在生成字符串地址时,会增加一个字节的校验位,计算方式是对前面23个字节(chainId+type+pkh)进行异或运算。 校验位不参与序列化;

分隔符,在前缀和Base58Encode(chainId+addressType+pkh+xor)之间,用一个小写字母进行分隔,便于从地址中提取chainId和验证地址类型及正确性。 小写字母的选择方式为,提供一个数组,按照字母表的顺序填充小写字母,根据prefix的长度来选择分隔的字母:

//前缀长度是几个字母,就选择第几个元素为分隔字母。
//如前缀长度为2,则用b分隔,长度为3用c分隔,长度为4用d分隔,……
String[] LENGTHPREFIX = new String[]{“”, “a”, “b”, “c”, “d”……”z”};

创建账户功能主要流程如下:

理解NULS地址的格式之后,下面介绍创建NULS账户的主要流程:

1、生成公私钥对ECKey;

2、生成公钥摘要pkh;

3、将address=(chainId(2) + type(1) + PKH(20))序列化;

4、根据序列化内容,生成校验位xor;

5、添加前缀prefix和分隔符,生成字符串地址:

固定前缀字符串地址

addressString = prefix + 分隔符 + Base58Encode(address+xor)

自动前缀字符串地址

addressString = Base58Encode(chainId) + 分隔符 + Base58Encode(address+xor)

对于像比特币、以太坊,这样非NULS体系内的区块链,想要接入NULS生态,就需要有对应的NULS地址,与这些区块链上的每个原生地址形成映射关系。

NULS设计了一个地址转换协议,在NULS生态内,生成对应的映射地址:

address = Base58Encode(chainId+原始地址长度+原始地址+xor)

例如:比特币地址,在地址之前追加两个字节的chainId,之后跟随比特币的原始地址,地址解析方式根据链配置决定,确保任何一个地址都可以在NULS获得映射的地址。

导入账户

通过导入账户功能,用户可以在钱包中将私钥或Keystore导入,在输入密码后,登录到账户中。

导入私钥的主要流程如下:

1、用户导入私钥,输入新密码;2、根据私钥,计算出公钥;3、参考创建账户功能中的流程,在节点本地生成账户,从而得到已有账户信息,完成登录;

导入Keystore的主要流程如下:

1、用户导入Keystore,输入密码;2、根据Keystore和密码,计算得到私钥,再计算出公钥;3、参考创建账户功能中的流程,在节点本地生成账户,从而得到已有账户信息,完成登录。

这里有一个困惑:为何两种方式,都需要重新生成账户,而常见的系统,用账号和密码直接登录就可以?

这是因为区块链的去中心化导致的,因为对于每一个本地的节点来说,用户无论是导入私钥,还是Keystore,本地节点都不一定存储了已有账户的信息,所以通过两种方式导入账户,我们都是重新生成账户,得到已有账户的信息。

而中心化的系统中,每次使用账号密码登录,都是与中心的服务器数据在进行匹配,匹配成功,就会认为登录成功,不需要再生成新的账户。

账户备份

账户备份分为明文私钥备份和Keystore备份两种方式。

明文私钥备份主要流程如下:

1、用户输入密码,发起明文私钥备份请求;2、密码验证通过,根据密码,将本地保存的加密后的私钥进行解密;3、生成明文私钥字符串,返回给用户。

Keystore备份主要流程如下:

1、用户输入密码,发起Keystore备份请求;2、密码验证通过,计算得到账户的公钥;3、将本地保存的加密后的私钥、账户地址字符串、公钥,组装成Json字符串;4、生成Keystore文件,返回给用户。

修改密码

通过修改密码功能,存储用户账户数据的节点,将会根据新密码,对私钥进行加密。

修改密码主要流程如下:

1、用户输入原密码和新密码,发起修改密码请求;2、原密码验证通过,使用原密码将加密的私钥解密;3、用新密码对私钥进行重新加密;4、将重新加密的私钥保存在本地。

移除账户

通过移除账户功能,用户可以将本地节点保存的账户数据删除。

移除账户功能主要流程如下:

1、用户输入密码,发起移除账户请求;2、密码验证通过,删除本地存储的账户信息;3、操作成功,返会操作成功信息给前端。

账户信息查询

账户信息查询功能,是通过提供接口的形式,对外提供服务,让其他模块在实现相关功能时使用。用户通过账户信息查询功能提供的接口,可以查询到账户的地址、余额、别名等账户信息 。具体可以查询到的账户信息,可以参考账户模块提供的查询接口,接口参考[账户模块RPC-API接口文档] 。

转账交易

通过转账交易功能,用户可以发起一笔普通转账,将数字资产从一个账户转移到另一个账户。

理解转账交易功能,需要先理解交易协议,交易协议规定了一个交易需要包含哪些数据。以下是交易协议格式:

Len Fields Data Type Remark 2 type uint16 交易类型 4 time uint32 时间,精确到秒 ? txData VarByte 业务数据 ? coinData VarByte 资产数据 ? remark VarString 备注 ? sigData VarByte 包含公钥和签名数据

CoinData的数据格式

交易协议中的CoinData,是交易的资产数据,由两个列表数据List<CoinFrom>、List<CoinTo>组成,NULS目前定义了一套通用的CoinData格式,具体如下:

froms://List<CoinFrom>格式,
tos://List<CoinTo>格式

CoinFrom[70]:

address: //byte[24] 账户地址
assetsChainId://uint16 资产发行链的
assetsId: //uint16 资产
amount: //uint128,转出数量
nonce : //byte[8] 交易顺序号,前一笔交易的hash的后8个字节
locked : //byte 是否是锁定状态(locktime:-1),1代表锁定,0代表非锁定

CoinTo[68]:

address: //byte[24],目标地址
assetsChainId://uint16 资产发行链的
assetsId: //uint16 资产
amount : //uint128,转账金额
lockTime://uint32,解锁高度或解锁时间,-1为永久锁定

在交易协议中,我们规定普通转账的交易类型(type)取值为2,因为转账交易不涉及业务数据,所以业务数据(txData)取值为空,其他数据会根据具体转账交易的情况对数据进行填充。

转账交易功能的主要流程如下:

1、用户填写转账金额,输入密码,发起交易;2、账户模块根据交易协议数据格式,填充对应转账交易数据;3、计算交易数据的Hash值,对交易Hash进行签名(签名过程见下文),广播交易。

设置别名

通过设置别名功能,用户可以为账户设置一个自己想要的别名。

因为设置别名,需要将账户的别名数据上链,所以该功能将会触发一笔交易。设置别名交易的数据格式,遵循前面提到的交易协议,只是交易类型(type)、业务数据(txData)取值的不同。

type: 3 //设置别名交易的类型为3
txData:{
address: //VarByte 设置别名的地址
alias: //VarByte 别名字符串转成的字节数组,用UTF-8解码
}

alias(别名)数据格式

Len Fields Data Type Remark 24 address byte[] 设置别名的地址 32 alias byte[] 别名字符串转成的字节数组,用UTF-8解码

设置别名功能的主要流程如下:

1、用户输入别名,输入密码,发起设置别名请求;2、进行相关数据验证,具体包括:

1、别名格式合法性验证
2、主网上是否已经存在此别名
3、该地址是否已经设置过别名

3、账户模块根据交易协议数据格式,填充对应转账交易数据;(注:设置别名交易需要烧毁一个Token单位,所以交易目标地址是一个黑洞地址,其他交易的操作本质上与转账交易没有差别。)4、计算交易数据的Hash值,对交易Hash进行签名(签名过程见下文),广播交易。5、在本地对别名交易进行业务验证,包括主网上是否已经存在此别名、该地址是否已经设置过别名,验证通过,等待其他节点对该交易的处理结果;6、如果其他节点验证该交易通过,则进行提交操作,在节点本地保存该交易,否则,执行回滚。

交易签名

通过交易签名功能,账户模块会使用私钥,对通过交易数据计算得到的交易Hash进行签名。

签名后的交易被认为获得了用户的授权,在签名完成后,交易才会被广播到NULS网络中,其他节点会验证交易的正确性。

交易签名功能的主要流程如下:

1、组装交易数据,计算交易数据的Hash值;2、通过密码和Keystore,获得私钥,对交易Hash进行签名。

其他

Java特有的设计

Account对象设计

该表存储时使用的key:

NULS同构体系:chainId+type+hash160

NULS异构体系:chainId+length+address

`字段名称` `type` `说明` chainId short 链ID address String 账户地址(Base58(address)+Base58(chainId)) alias String 账户别名 status Integer 是否默认账户(不保存) pubKey byte[] 公匙 priKey byte[] 私匙-未加密 encryptedPriKey byte[] 已加密私匙 extend byte[] 扩展数据 remark String 备注 createTime long 创建时间

Address对象设计(不持久化存储)

`字段名称` `type` `说明` chainId short 链ID addressType byte 地址类型 hash160 byte[] 公匙hash addressBytes byte[] 地址字节数组

Alias对象设计

该表存储时使用的key:

address和alias分别作为key存储,别名数据存储两份

需要按照不同的链分别创建不同的别名表

`字段名称` `type` `说明` address byte[] 账户地址 alias String 账户别名

账户模块启动时需要依赖的模块

账户模块在启动时,需要依赖以下三个模块:

1、交易模块:因为账户模块需要提供转账交易和设置别名两个功能,这两个功能是涉及到交易,需要在交易模块中,进行注册;

2、网络模块:账户模块需要通过网络模块对外发送消息,需要在网络模块中注册自己需要发送的消息类型;

3、协议升级模块:协议升级模块会自动检测是否有协议升级,账户模块在启动时,会将转账交易和设置别名交易的协议注册到协议升级模块中。