深度探索Hyperledger技术与应用之超级账本的策略管理

本文内容节选自《深度探索区块链:Hyperledger技术与应用》一书的第3章《超级账本的系统架构》。

本书作者:张增骏,董宁,朱轩彤,陈剑雄

感谢机械工业出版社华章分社的支持和分享。

1

消息协议结构

1、信封消息结构

信封消息是认证内容中最基本的单元。它由一个消息负载(Payload)和一个签名(Signature)组成。

// 信封包含一个带有签名的负载,以便认证该消息

message Envelope {

// 编组的负载

bytes payload = 1;

// 负载头中指定创建者签名

bytes signature = 2;

}

// 负载是消息内容(允许签名)

message Payload {

// 负载头部,提供身份验证并防止重放

Header header = 1;

// 数据,其编码由头的类型定义

bytes data = 2;

}

负载包含:

1)消息头部。头部带有类型,描述负载的性质以及如何解组数据字段。此外,头部还包含创建者的信息和随机数,以及用来标识时间逻辑窗口的时期信息。只有在两个条件都成立的情况下,Peer节点才能接受一个信封。

消息中指定的时期信息是当前窗口期;

该负载在该周期内只看到一次(即没有重放)。

2)数据字段的类型由头部指定。头部消息的组织方式如下所示:

message Header {

bytes channel_header = 1;

bytes signature_header = 2;

}

// 通道头是一个通用的预防重放和身份标识的消息,它包含在一个被签名的负载之中

message ChannelHeader {

int32 type = 1; // 头类型0-10000由HeaderType保留和定义

// 版本指示消息协议版本

int32 version = 2;

// 时间戳是发件人创建消息的本地时间

google.protobuf.Timestamp timestamp = 3;

// 该消息绑定通道的标识符

string channel_id = 4;

// 端到端使用唯一的标识符

// - 由较高层设置,如最终用户或SDK

// - 传递给背书节点(将检查唯一性)

// - 当消息正确传递时,它将被记账节点检索(也会检查唯一性)

// - 存储于账本中

string tx_id = 5;

// 时期信息基于区块高度而定义,此字段标识时间的逻辑窗口

// 只有在两个条件都成立的情况下,对方才接受提案响应

// 1. 消息中指定的时期信息是当前时期

// 2. 该消息在该时期内只看到一次(即没有重放)

uint64 epoch = 6;

// 根据消息头类型附加的扩展

bytes extension = 7;

}

enum HeaderType {

MESSAGE = 0; // 非透明消息

CONFIG = 1; // 通道配置

CONFIG_UPDATE = 2; // 通道配置更新

ENDORSER_TRANSACTION = 3; // SDK提交背书

ORDERER_TRANSACTION = 4; // 排序管理内部使用

DELIVER_SEEK_INFO = 5; // 指示Deliver API查找信息

CHAINCODE_PACKAGE = 6; // 链码打包安装

}

message SignatureHeader {

// 消息的创建者,链的证书

bytes creator = 1;

// 只能使用一次的任意数字,可用于检测重放攻击

bytes nonce = 2;

}

信封消息结构对于验证负载的签名是必要的。否则,对于大载荷消息,就必须连接所有的载荷再进行签名验证,这往往成本很高。

经过排序后,批量的信封消息交付给记账节点进行验证,通过验证后的数据被记录到账本之中。

2、配置管理结构

区块链有与之相关的配置,配置设置在创世区块之中,但可能在后续被修改。该配置信息在类型为CONFIGURATION_TRANSACTION的信封消息中编码。配置信息本身就是区块的一个单独交易。配置信息交易没有任何依赖,所以每个配置信息交易必须包含对于链的全量数据,而不是增量数据。使用全量数据更容易引导新的peer或排序节点,也便于未来进行裁剪工作。

CONFIGURATION_TRANSACTION类型的信封消息具有ConfigurationEnvelope类型的负载数据。它定义为:

message ConfigurationEnvelope {

repeated SignedConfigurationItem Items = 3;

}

配置信息的信封消息有与之关联的序列号和链ID。每次修改配置序列号必须递增,这可以作为防止重放攻击的一个简单机制。配置信息的信封中会嵌入一系列的SignedConfigurationItems,定义如下:

message SignedConfigurationItem {

bytes ConfigurationItem = 1;

repeated Envelope Signatures = 2;

}

因为SignedConfigurationItem必须支持多个签名,所以它包含一组重复的信封消息。这些消息中每个都有一个类型为CONFIGURATION_ITEM的头部。负载的数据部分在ConfigurationItem中保存,定义为:

message ConfigurationItem {

enum ConfigurationType {

Policy = 0;

Chain = 1;

Orderer = 2;

Fabric = 3;

}

bytes ChainID = 1;

uint64 LastModified = 2;

ConfigurationType Type = 3;

string ModificationPolicy = 4;

string Key = 5;

bytes Value = 6;

}

Type提供了配置项的范围和编码信息。LastModified字段设置为上一次配置项被修改时配置信息信封中的序列号。ModificationPolicy指向一个已经命名的策略,用来对将来的签名进行评估,以确定修改是否被授权。Key和Value字段分别用作引用配置项及其内容。

修改配置包含以下内容:

检索现有配置;

递增配置信息信封消息中的序列号;

修改所需的配置项,将每个配置项的LastModified字段设置为递增后的序列号;

更新SignedConfigurationItem中的签名信息;

将签名后的信封信息提交给排序节点。

配置管理员将验证:

所有配置项和信封都指向正确的链;

添加或修改了哪些配置项;

有没有现有的配置项被忽略(即提交的数据是全集);

所有配置更改的LastModification都等于信封消息中的序列号;

所有配置更改均符合相应的修改策略。

任何有权更改配置项的角色都可以构建新的配置信息交易。修改配置项将更新序列号并产生新的创始区块,这将引导新加入网络的各种节点。

3、背书流程结构

当Envelope.data中携带与链码相关的消息时,使用ENDORSER_TRANSACTION类型。

获得批准的ENDORSER_TRANSACTION负载流程如下。

首先,客户端向所有相关的背书节点发送提案消息(提案基本上是要进行一些影响账本的动作)。

然后,每个背书节点向客户端发送一个提案响应消息。提案响应包含背书结果的成功/错误码、应答负载和签名。应答负载之中包含提案的哈希值信息,用此信息可以将提案和针对该提案的应答安全地连接起来。

最后,客户端将背书结果写入交易中,签名并发送到排序服务。

在接下来的章节中,我们将详细介绍消息及其流程。

1. 交易提案结构

一个提案消息包含头部(包含描述它的一些元数据,例如类型、调用者的身份、时间、链的ID、加密的随机数)和不透明的负载:

message SignedProposal {

// 提案

bytes proposal_bytes = 1;

// 对提案进行签名,该签名将和头部的创建者标识进行验证

bytes signature = 2;

}

message Proposal {

// 提案头部

bytes header = 1;

// 提案负载,具体结构由头部的类型决定

bytes payload = 2;

// 提案的可选扩展。对于CHAINCODE类型的消息,其内容可能是ChaincodeAction消息

bytes extension = 3;

}

一个提案发送给背书节点进行背书。该提案包含:

头部,可以解组为头部信息;

负载,由头部的类型决定;

扩展,由头部的类型决定。

和信封消息结构类似,这种SignedProposal消息结构也是重要的。否则,对于大载荷消息,我们必须连接所有的载荷再进行签名验证,这往往成本很高。

当背书节点收到签名后的提案消息后,它将验证消息中的签名。验证签名需要以下步骤。

预验证用户生成签名证书的有效性。一旦SignedProposal.proposal_bytes和Proposal.header都解组成功,就可以认为证书基本是可用的。虽然这种在证书验证前的解组操作可能并不太理想,但是在这个阶段可以过滤掉证书过期的情况。

验证证书是否可信(证书是否由受信任的CA签名),并允许交易(能否通过ACL检查)。

验证SignedProposal.proposal_bytes上的签名是否有效。

检测重放攻击。

以下是当ChainHeader的类型为ENDORSER_TRANSACTION时的消息:

message ChaincodeHeaderExtension {

// 控制提案的负载在最终交易和账本中的可见程度

bytes payload_visibility = 1;

// 要定位的链代码ID

ChaincodeID chaincode_id = 2;

}

ChaincodeHeaderExtension消息用于指定要调用的链码以及应在账本中呈现的内容。理想情况下,payload_visibility是可配置的,支持至少3种主要可见性模式:

负载所有字节都可见;

只有负载的哈希值可见;

任何东西都不可见。

注意,可见性功能可能也会由ESCC设置,此时本字段将会被覆盖。另外本字段也将影响ProposalResponsePayload.proposalHash的内容。

message ChaincodeProposalPayload {

// 包含调用链码的参数,

bytes input = 1;

// 用于实现某些应用程序级的加密数据

map TransientMap = 2;

}

ChaincodeProposalPayload消息包含调用链码的参数。TransientMap字段的内容应始终从信封消息中省略掉,并不记录到账本之中。

message ChaincodeAction {

// 调用链码产生的读/写集

bytes results = 1;

// 调用链码产生的事件

bytes events = 2;

// 调用链码的结果

Response response = 3;

// 含链ID、背书节点在模拟执行提案时设置

// 账本节点将验证版本号是否与链码最新版本匹配,含有链ID信息将支持单个交易打开多个链码

ChaincodeID chaincode_id = 4;

}

ChaincodeAction消息包含执行链码所产生的动作和事件。results字段包含读取集合,events字段包含由链码执行生成的事件。

2. 提案响应结构

提案响应消息从背书节点返回给提案客户端。背书节点使用该消息表达对于交易提案的处理结果。应答结果可能是成功也可能是失败,另外还会包含动作描述和背书节点的签名。如果足够数量的背书节点同意相同的动作并进行签名,则可以生成负载消息,并发送给排序节点。

message ProposalResponse {

// 消息协议版本

int32 version = 1;

// 消息创建时间,由消息发送者定义

google.protobuf.Timestamp timestamp = 2;

// 某个动作的背书是否成功

Response response = 4;

// 负载,ProposalResponsePayload字节序列

bytes payload = 5;

// 提案的具体背书内容,基本上就是背书节点的签名

Endorsement endorsement = 6;

}

message ProposalResponsePayload {

// 触发此应答交易提案的哈希值

bytes proposal_hash = 1;

// 扩展内容,应该解组为特定类型的消息

bytes extension = 2;

}

message Endorsement {

// 背书节点身份(例如,证书信息)

bytes endorser = 1;

// 签名,对提案应答负载和背书节点证书这两个内容进行签名

bytes signature = 2;

}

ProposalResponsePayload消息是提案响应的负载部分。这个消息是客户端请求和背书节点动作之间的“桥梁”。对于链码来说,它包含一个表示提议的哈希值proposal_hash,以及表示链码状态变化和事件extension字段。

proposal_hash字段将交易提案和提案响应两者对应起来,即为了实现异步系统的记账功能也为了追责和抗抵赖的安全诉求。哈希值通常会覆盖整个提案消息的所有字节中。但是,这样实现就意味着只有获得完整的提案消息才能验证哈希值的正确性。

出于保密原因,使用链码不太可能将提案的负载直接存储在账本中。例如,类型为ENDORSER_TRANSACTION的消息,需要将提案的头部和负载分开进行处理:头部总是进行完整散列的,而负载则可能进行完整散列或对哈希值再进行散列,或者根本不进行散列。

3. 背书交易结构

客户端获得足够的背书后,可以将这些背书组合成一个交易信息。这个交易信息可以设置为负载信息的数据字段。以下是在这种情况下要使用的具体消息:

message Transaction {

// 负载是一个TransactionAction数组,每个交易需要一个数组来适应多个动作

repeated TransactionAction actions = 1;

}

message TransactionAction {

// 提案头部

bytes header = 1;

// 负载由头部类型决定,它是ChaincodeActionPayload字节序列

bytes payload = 2;

}

TransactionAction消息将提案绑定到其动作。它的头部是SignatureHeader消息,它的负载是ChaincodeActionPayload消息。

message ChaincodeActionPayload {

// ChaincodeProposalPayload消息的字节序列,内容来自链码原始调用的参数

bytes chaincode_proposal_payload = 1;

// 应用于账本的动作列表

ChaincodeEndorsedAction action = 2;

}

ChaincodeActionPayload消息携带chaincodeProposalPayload和已经通过背书的动作以应用于账本。主要的可见性模式是“full”(整个ChaincodeProposalPayload消息包含在这里)、“hash”(仅包含ChaincodeProposalPayload消息的哈希值)或“nothing”。该字段将用于检查ProposalResponsePayload.proposalHash的一致性。此外,action字段包含应用于账本的动作列表。

message ChaincodeEndorsedAction {

// 由背书节点签名的ProposalResponsePayload消息字节序列

bytes proposal_response_payload = 1;

// 提案背书,基本上是背书节点的签名

repeated Endorsement endorsements = 2;

}

ChaincodeEndorsedAction消息承载有关具体提案的背书信息。proposalResponsePayload是由背书节点签名的,对于ENDORSER_TRANSACTION类型,ProposalResponsePayload的extenstion字段会带有一个ChaincodeAction。此外,endorsements字段包含提案已经收到的背书信息。

2

策略管理和访问控制

在Hyperledger Fabric 1.0中,较多的地方都使用策略进行管理,它是一种权限管理的方法,包括交易背书策略、链码的实例化策略、通道管理策略等。

1、策略定义及其类型

策略定义了一些规则,验证签名数据是否符合定义的条件,结果为TRUE或者FALSE。策略的定义如下:

type Policy struct {

Type int32 // 策略的类型

Value []byte // 策略的内容

}

策略的类型有两种。

1)SignaturePolicy:在基于验证签名策略的基础上,支持条件AND、OR、NOutOf的任意组合,其中的NOutOf指的是满足m个条件中的n个就表示满足策略(m≥n)。比如OR(Org1.Admin, NOutOf(2, Org2.Member))表示Org1的管理员或者两个Org2的成员签名都满足策略。

2)ImplicitMetaPolicy:隐含的元策略,是在SignaturePolicy之上的策略,支持大多数的组织管理员这种策略,只适用于通道管理策略。

SignaturePolicy实际只有两种类型,SignedBy和NOutOf,其他的,比如AND和OR都会转换成NOutOf类型。其定义如下:

type SignaturePolicy struct {

// 支持的类型有:

// *SignaturePolicy_SignedBy,验证单个签名是否正确

// *SignaturePolicy_NOutOf_,验证是否有n个签名都正确

Type isSignaturePolicy_Type `protobuf_oneof:"Type"`

}

ImplicitMetaPolicy是递归策略的定义方法,名称中的Implicit说明规则是由子策略生成的,Meta说明策略依赖其他策略的验证结果。

type ImplicitMetaPolicy struct {

SubPolicy string // 子策略的名称

Rule ImplicitMetaPolicy_Rule // 策略的规则

}

策略的规则支持3种形式:

ImplicitMetaPolicy_ANY:任意一个子规则成立就满足策略;

ImplicitMetaPolicy_ALL:全部子规则都成立才满足策略;

ImplicitMetaPolicy_MAJORITY:大多数的子规则成立就满足策略。

特别说明ImplicitMetaPolicy_MAJORITY需要满足子规则数的计算方法:

threshold = len(subPolicies)/2 + 1

比如一共有3个子策略,需要至少2个子策略成立才能满足策略。如果总共有4个子策略,需要至少3个子策略成立才能满足策略。如果没有子策略,默认是满足的。

策略的内容可以有多种,下面分别来看几种策略:交易背书策略、链码实例化策略和通道管理策略。

2、交易背书策略

交易背书策略是对交易进行背书的规则,是跟通道和链码相关的,在链码实例化的时候指定。在链码调用的时候,需要从背书节点收集足够的签名背书,只有通过背书策略的交易才是有效的。这是通过应用程序和背书节点之间的交互来完成的,这在前面的交易流程里已经介绍过了。

交易背书策略的验证

背书是由一组签名组成的,每个Peer节点接收到区块时,都能根据交易的内容本地验证背书是否符合背书策略,不需要和其他节点交互。验证交易背书的基本原则是:

所有的背书都是有效的,验证消息用有效的证书进行正确的签名;

满足背书策略的有效背书数量,转化为NOutOf格式进行比较;

背书是期望的背书节点签名的,在背书策略中指定了哪些组织和角色是有效的背书节点。

如何来实现这几个原则的呢?我们先从背书签名的命令行语法开始,背书签名的语法AND和OR都可以转为NOutOf:

AND(A, B)可以转换为NOutOf(1, A, B);

OR(A, B)可以转换为NOutOf(2, A, B)。

我们主要来看下NOutOf如何实现,背书策略的定义如下:

其中,MSP主体(Principal)是基于MSP的身份标识的,有如下几种类型。

MSPPrincipal_ROLE:基于MSP角色的验证方法,目前只有admin和member两种。

MSPPrincipal_ORGANIZATION_UNIT:基于部门的验证方法,同一个MSP中的不同部门。

MSPPrincipal_IDENTITY:基于某个具体身份证书的验证方法,验证签名是否有效。

MSPPrincipal的定义如下:

type MSPPrincipal struct {

PrincipalClassification MSPPrincipal_Classification // MSP的类型

Principal []byte // 根据MSP的类型不同,实体有不同的内容

}

根据不同的MSP类型,主体是不同的。

(1)基于MSP角色的验证

当PrincipalClassification是MSPPrincipal_ROLE时,主体存储的内容如下:

type MSPRole struct {

// MSP标识符

MspIdentifier string

// MSP角色:可选值是MSPRole_MEMBER和MSPRole_ADMIN

Role MSPRole_MSPRoleType

}

不同角色的验证方法如下:

MSPRole_MEMBER:验证是否为同一个MSP的有效签名;

MSPRole_ADMIN:验证签名者是否是MSP设置好的admin成员。

(2)基于部门的验证

当PrincipalClassification是MSPPrincipal_ORGANIZATION_UNIT时,主体存储的内容如下:

type OrganizationUnit struct {

// MSP标识符

MspIdentifier string

// 组织部门标识符

OrganizationalUnitIdentifier string

// 证书标识符:信任证书链和组织部门信息的哈希

CertifiersIdentifier []byte

}

验证过程的步骤是:

验证是否为相同的MSP;

验证是否是有效的证书;

验证组织部门信息是否匹配。

(3)基于身份证书的验证

当PrincipalClassification是MSPPrincipal_IDENTITY时,主体存储的内容如下:

type identity struct {

// 身份标识符,包含MSP标识符和身份编号

id *IdentityIdentifier

// 身份的数字证书,包含了对公钥的签名

cert *x509.Certificate

// 身份的公钥

pk bccsp.Key

// 身份的MSP信息

msp *bccspmsp

}

这样验证MSP是否是有效证书就可以了。

命令行的背书策略语法

在命令行里,可以用一种简单的语言,根据主体的布尔表达式来表示策略。主体是用MSP来表示的,用来验证签名者的标识和签名者在MSP里的角色。目前支持两种角色:member和admin。主体的表示方法是MSP.ROLE,其中MSP是MSP的标识,ROLE可以是memeber也可以是admin。这都是有效的主体:Org0.admin表示由MSP标识Org0的任何一个管理员,Org1.memeber表示由MSP标识Org1的任何一个成员。

其语法是:EXPR(E[, E...]),其中EXPR可以是AND也可以是OR,E可以为一个主体,也可以为嵌套的EXPR。比如:

AND('Org1.member', 'Org2.member', 'Org3.member')要求3个MSP标识Org1、Org2和Org3,其中每个MSP都有1个成员有1个签名;

OR('Org1.member', 'Org2.member')要求2个MSP标识Org1、Org2,其中任何1个成员有1个签名;

OR('Org1.member', AND('Org2.member', 'Org3.member'))要求MSP标识Org1的成员有1个签名,或者MSP标识Org2和Org3的成员都有1个签名。

目前在命令行的语法中,背书策略只支持AND和OR两种,并不支持更为复杂的NOutOf。这部分的设计在后续内容中也会有调整。SDK对背书策略都会转换成NOutOf语法,不过不是所有的SDK都支持。比如目前fabric-sdk-go提供的默认接口不支持NOutOf语法,但其内部是支持的,稍加改动很容易就能支持。详细可以参考cauthdsl_builder.go文件。

给链码指定背书策略

背书策略可以在部署的时候用-P参数指定,后面是具体的背书策略。比如:

peer chaincode deploy -C testchainid -n mycc -p github.com/hyperledger/fabric/

examples/chaincode/go/chaincode_example02 -c '{"Args":["init","a","100","b","200"]}’

-P "AND('Org1.member', 'Org2.member')"

这个命令在链testchainid上部署链码 mycc,背书策略是AND('Org1.member', 'Org2.member')。如果命令行里没有指定策略,那么默认的背书策略要求MSP标识DEFAULT成员的一个签名。

3、链码实例化策略

链码实例化策略是用来验证是否有权限进行链码实例化和链码升级的。链码实例化策略是在对链码打包和签名的时候指定的,如果没有指定实例化策略,默认是通道的管理员才能实例化。

type SignedChaincodeDeploymentSpec struct {

// 链码部署规范

ChaincodeDeploymentSpec []byte

// 链码的实例化策略,结构同背书策略,在实例化的时候验证

InstantiationPolicy []byte

// 链码所有者的签名背书列表

OwnerEndorsements []*Endorsement

}

链码实例化策略的定义和背书策略完全一样,验证方法也相同,只是用途和用法不一样。链码实例化策略是直接从链码打包中获取的,实例化完成后会将策略存放在链上。在链码实例化和升级的时候会先验证是否符合当前的实例化策略,验证通过才可以更新链码实例化策略。存储在链上的链码信息结构如下所示:

type ChaincodeData struct {

// 链码名称

Name string

// 链码版本

Version string

// 链码的ESCC

Escc string

// 链码的VSCC

Vscc string

// 链码的背书策略

Policy []byte

// 链码的内容:包含链码的名称、版本、链码源码哈希、链码名称和版本的元数据哈希等内容

// 不包含链码源码

Data []byte

// 链码指纹标识,目前没有使用

Id []byte

// 链码实例化策略

InstantiationPolicy []byte

}

链码信息结构ChaincodeData在链上是按链码的名称索引的。

4、通道管理策略

通道配置是递归定义的:

type ConfigGroup struct {

Version uint64 // 配置版本

Groups map[string]*ConfigGroup // 子配置

Values map[string]*ConfigValue // 配置值

Policies map[string]*ConfigPolicy // 配置策略定义

ModPolicy string // 配置修改策略的名称

}

其中,配置值ConfigValue定义的是一些配置数据,定义如下:

type ConfigValue struct {

Version uint64 // 配置版本

Value []byte // 配置数据,可以是JSON结构的

ModPolicy string // 配置修改策略名称

}

比如在通道配置中区块生成间隔BatchTimeout设置的值是“2s”,局部的格式如下:

"BatchTimeout": {

"mod_policy": "Admins",

"value": {

"timeout": "2s"

}

}

我们再来看最重要的配置策略的定义:

type ConfigPolicy struct {

Version uint64 // 配置策略版本

Policy *Policy // 配置策略的内容,这在前面已经介绍过

ModPolicy string // 配置策略中修改策略的名称

}

从上面的定义中我们可以看到,配置策略是基于SignaturePolicy和ImplicitMetaPolicy的,ModPolicy代表的是修改同级策略用到的策略名称。通道定义了3种配置策略,如表3-1所示。

通道配置的递归定义

我们来看一个简化的通道配置是如何递归定义的。

Channel:

Policies:

Readers

Writers

Admins

Groups:

Orderer:

Policies:

Readers

Writers

Admins

Groups:

OrdereringOrganization1:

Policies:

Readers

Writers

Admins

Application:

Policies:

Readers

-----------> Writers

Admins

Groups:

ApplicationOrganization1:

Policies:

Readers

Writers

Admins

ApplicationOrganization2:

Policies:

Readers

Writers

Admins

在上面的配置中,最外层是Channel,它定义了通道的子配置和策略定义。Channel的子配置里面定义了Orderer和Application配置,它们分别是相同的递归定义结构。其中"------->"显示的一行按照层级展开,代表的是/Channel/Application/Writers。

怎么来使用这些配置策略呢?比如在排序服务节点调用Deliver接口的时候会检查这个节点是否满足/Channel/Readers策略。Peer节点同步区块的时候也会检查是否满足/Channel/Application/Readers策略。

通道配置的默认策略

在使用configtxgen工具生成创世区块或者通道配置时,使用的默认策略如表3-2所示。

深度探索区块链

Hyperledger技术与应用

区块链

张增骏,董宁,朱轩彤,陈剑雄  著

本书由超级账本执行董事Brian Behlendorf领衔推荐,区块链一线落地实践团队、Hyperleger会员智链骨干团对撰写。深入讲解Hyperledger Fabric 1.0的架构、执行逻辑、核心功能实现、从零部署,并以票据案例为例,讲解具体开发实践,穿插开发所需的最佳实践和遇到的问题解决。

机械工业

出版社

华章科技是机械出版社的旗下品牌,出版了“计算机科学丛书”等近30个经典套系,在各个细分领域均处于领导地位,其中《Java编程思想》、《算法导论》、《编译原理》、《数据挖掘:概念与技术》、《深入理解计算机系统》、《深入理解Java虚拟机》等著作犹如计算机图书领域的璀璨明珠,长销不衰!

HiBlock秉承开放、协作、透明、链接、分享的价值观,致力打造区块链开发者社区。专注于在开发者中推广区块链,帮助开发者真正掌握区块链技术和应用。

活动推荐

主题:5月25-27日,Blockathon2018北京站,招募100名开发者一起挑战区块链开发。

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180430G0M9U800?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券