可拔插交易背书和验证
动机
当交易在提交被验证时,peer节点在交易本身的状态改变之前执行各种检查:
- 验证签名交易的标识
- 验证交易中背书人的签名
- 确保交易满足相应链码的命名空间的背书策略
有些用例要求与fabric验证规则不同的自定义交易验证规则,例如:
- State-based endorsemet(基于状态的背书):当背书策略取决于密钥,并不仅仅取决于命名空间。
- UTXO(Unspent Transaction Output未花费交易输出):当验证考虑到,不论交易是否不会对输入双花。
- Anonymous transactions(匿名交易):当背书不包含peer节点的身份,但是无法链接到peer节点身份的签名和公钥被共享。
可拔插背书与验证逻辑
fabric运行将定制的背书和验证逻辑实现和部署在peer节点中,以可插拔的方式与链码处理相关联。这个逻辑不仅可以编译到peer节点中,内置于可选逻辑中,也可以作为Golang插件与peer节点一起编译和部署。
回想一下,在链码实例化时,每个链码都与其自己的背书和验证逻辑相关联。如果用户未选择一个,则隐式选择默认的内置逻辑。peer节点管理员通过在peer节点启动时加载并且应用定制的背书/验证逻辑来改变通过扩展peer节点本地配置而选择的背书/验证逻辑。
配置
每一个peer节点都有一个本地配置文件(core.yaml),它声明了背书/验证逻辑名称和要运行的实现之间的映射关系。
默认的背书逻辑叫做ESCC,默认的验证逻辑叫做VSCC,他们的定义可以在本地配置文件的"handlers"部分找到:
handlers:
endorsers:
escc:
name: DefaultEndorsement
validators:
vscc:
name: DefaultValidation
当背书或者验证实现被编译到peer节点中时,"name"属性标识要运行的初始化函数,以便获得创建背书/验证逻辑实例的工程。
该函数是在"core/handlers/library/library.go"路径下的HandlerLibrary构造的实例方法,为了添加自定义背书/验证逻辑,需要使用任何额外的方法扩展此构造。
由于这很复杂并且较难于部署,因此可以以Golang插件的形式通过在名为"library"属性名称下添加另一个属性来部署自定义的背书/验证模块。
举个例子,如果我们以插件的形式实现了自定义的背书和验证逻辑模块作为基于状态的背书,我们可以在core.yaml配置文件中以如下形式定义:
handlers:
endorsers:
escc:
name: DefaultEndorsement
statebased:
name: state_based
library: /etc/hyperledger/fabric/plugins/state_based_endorsement.so
validators:
vscc:
name: DefaultValidation
statebased:
name: state_based
library: /etc/hyperledger/fabric/plugins/state_based_validation.so
我们必须将.so插件文件放在peer节点本地文件系统中。
此后,自定义背书或者验证逻辑实现将被称为"插件",即使它们被编译到peer节点中。
背书插件实现
为了实现背书插件,必须实现在"core/handlers/endorsement/api/endorsement.go"文件中相应的插件接口:
// 背书插件提案回复
type Plugin interface {
//为给定的有效载荷(ProposalResponsePayload字节)背书签名,并且可以选择改变它
// 返回:
// 背书:有效载荷上签名,以及用于验证签名的标识。
// 作为输入提供的有效载荷(可在此功能中修改)
// 或者失败时出错
Endorse(payload []byte, sp *peer.SignedProposal) (*peer.Endorsement, []byte, error)
// 初始化将依赖注入插件的实例
Init(dependencies ...Dependency) error
}
通过让peer节点调用PluginFactory接口中的New方法为每个通道创建给定插件类型(通过方法名称识别为HandlerLibrary的实例方法或者.so插件文件路径)的背书插件实例,该方法期望由插件开发人员实现:
// PluginFactory 创建一个新的插件实例
type PluginFactory interface {
New() Plugin
}
初始化方法被希望接收在"core/handlers/endorsement/api/"路径下声明的,识别为嵌入Dependency接口的所有依赖项作为输入。
在创建插件实例之后,peer节点将依赖关系作为传递参数调用初始化方法(Init)。
目前,fabric为背书插件提供了一下依赖项:
- SigningIdentityFetcher:返回一个局域给定签名提案的SigningIdentity实例:
// SigningIdentity对消息进行签名并将其公共标识序列化为字节数据
type SigningIdentity interface {
// Serialize 返回此标识的字节表示形式,用于验证此SigningIdentity签名的消息
Serialize() ([]byte, error)
// Sign 为给定有效载荷签名并返回签名
Sign([]byte) ([]byte, error)
}
- StateFetcher:获取与状态数据库(world state)交互的状态对象(State)。
// State 定义与状态数据的交互方式
type State interface {
// GetPrivateDataMultipleKeys 在一次调用中获取多个私有数据项的值
GetPrivateDataMultipleKeys(namespace, collection string, keys []string) ([][]byte, error)
// GetStateMultipleKeys 在一次调用中获取多个键的值
GetStateMultipleKeys(namespace string, keys []string) ([][]byte, error)
// GetTransientByTXID 获取与给定txID关联的私有数据值
GetTransientByTXID(txID string) ([]*rwset.TxPvtReadWriteSet, error)
// Done 释放状态数据占用的资源
Done()
}
验证插件的实现
为了实现验证插件,必须实现在路径"core/handlers/validation/api/validation.go"下的插件接口:
// 验证交易插件
type Plugin interface {
// 如果在给定块中给定位置的交易内给定位置的动作是有效的Validate函数返回nil,否则返回一个error错误
Validate(block *common.Block, namespace string, txPosition int, actionPosition int, contextData ...ContextDatum) error
// 初始化将依赖注入插件的实例
Init(dependencies ...Dependency) error
}
每个ContextDatum都是由peer节点传递给验证插件的额外的运行时派生的元数据。目前,唯一传递的ContextDatum是代表链码的背书策略:
// SerializedPolicy 定义一个序列化策略
type SerializedPolicy interface {
validation.ContextDatum
// Bytes 返回SerializedPolicy字节形式数据
Bytes() []byte
}
通过让peer节点调用PluginFactory接口中的New方法为每个通道创建给定插件类型(通过方法名称识别为HandlerLibrary的实例方法或者.so插件文件路径)的验证插件实例,该方法期望由插件开发人员实现:
// PluginFactory 创建一个新的插件实例
type PluginFactory interface {
New() Plugin
}
初始化方法被希望接收在"core/handlers/validation/api/"路径下声明的,识别为嵌入Dependency接口的所有依赖项作为输入。
在创建插件实例之后,peer节点将依赖关系作为传递参数调用初始化方法(Init)。
目前,fabric为背书插件提供了一下依赖项:
IdentityDeserializer:将标识身份的byte数据转换为可被用于验证由其签名的身份对象,并根据其对应的MSP进行验证,并查看他们是否满足给定的MSP Principal(见MSP服务相关源码)。完整的规范被定义在"core/handlers/validation/api/identities/identities.go"。
PolicyEvaluator:评估是否满足给定的策略:
// PolicyEvaluator 评估策略
type PolicyEvaluator interface {
validation.Dependency
// Evaluate 接收一组签名数据并评估这组签名是否满足给定的字节数据的策略
Evaluate(policyBytes []byte, signatureSet []*common.SignedData) error
}
- StateFetcher:获取与状态数据库(world state)交互的状态对象(State)。
// State 定义与状态数据的交互方式
type State interface {
// GetStateMultipleKeys 在一次调用中获取多个键的值
GetStateMultipleKeys(namespace string, keys []string) ([][]byte, error)
// GetStateRangeScanIterator 返回一个包含给定键范围的所有键值集合的迭代器。startKey被包含在结果中,并且排除了endKey。空的startKey引用第一个可用键,空的endKey引用最后一个可用键。为了扫描所有键,startKey和endKey都可以作为空字符串提供。但是,出于性能原因,应谨慎使用完整扫描。返回的ResultsIterator包含类型为*KV的结果,该结果定义在"protos/ledger/queryresult"
GetStateRangeScanIterator(namespace string, startKey string, endKey string) (ResultsIterator, error)
// Done 释放状态数据占用的资源
Done()
}
重要注意事项
- 所有节点验证插件的一致性:在将来的版本中,fabric通道基础设施将保证在任何给定的区块链高度,通道中的所有peer节点对给定的链码使用相同的验证逻辑,以消除可能由于在peer节点之间意外运行不同实现导致状态差异的错误配置的可能性。但是,目前系统才做元和管理员有责任确保不会发生这种情况。
- 验证插件的错误处理:每当验证插件无法确定由于某些瞬态执行问题(例如,无法访问数据库)而无法确定给定交易是否被验证有效时,它应该返回在"core/handlers/validation/api/validation.go"中定义的ExecutionFailureError类型的错误。但是,如果返回ExecutionFailureError错误,则链处理将暂停,而不是将交易标记为无效。只是为了防止不同peer节点之间的状态分歧。
- 将fabric代码导入插件:非常不鼓励导入除了协议之外的fabric代码作为插件的一部分,这可能在fabric发行版之间发生代码更改时导致问题,或者在运行不同版本peer节点时导致不可操作性问题。