canoser 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +24 -0
- data/LICENSE.txt +21 -0
- data/README-CN.md +147 -0
- data/README.md +170 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/canoser.gemspec +44 -0
- data/lib/canoser.rb +46 -0
- data/lib/canoser/cursor.rb +24 -0
- data/lib/canoser/field.rb +167 -0
- data/lib/canoser/struct.rb +90 -0
- data/lib/canoser/version.rb +3 -0
- metadata +120 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 605cb23e469f4b87cd405b7c68232bbf4370efb17a3f9ee08f5835ede36b2e2d
|
4
|
+
data.tar.gz: dc1d93b906f7d3d56346038c671e756d0e10e7887b6ca8c6f688659d11fa0e84
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c9d5d29d35b7682ac3932f3320c176b2230a427ec154a1107826820daf994d77c241bdac1e82786540b9f8e470e6c52276e727b273e2bffcafdc147afd4dac61
|
7
|
+
data.tar.gz: fd1d4aa6c2bc971dc2502b1bf264cc7065f1c703e637696c0865e494aa91f84edfee21d12daf72c9aad4c0d281024a259ce087b899df56ec3fcf658dd17940dc
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
canoser (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
byebug (11.0.0)
|
10
|
+
minitest (5.11.3)
|
11
|
+
rake (10.5.0)
|
12
|
+
|
13
|
+
PLATFORMS
|
14
|
+
ruby
|
15
|
+
|
16
|
+
DEPENDENCIES
|
17
|
+
bundler (~> 2.0)
|
18
|
+
byebug (~> 11.0)
|
19
|
+
canoser!
|
20
|
+
minitest (~> 5.0)
|
21
|
+
rake (~> 10.0)
|
22
|
+
|
23
|
+
BUNDLED WITH
|
24
|
+
2.0.1
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2019 yuan xinyu
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README-CN.md
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
# Canoser
|
2
|
+
|
3
|
+
|
4
|
+
Canoser是facebook推出的Libra网络中使用的规范序列化(canonical serialization)协议的第三方ruby实现框架。
|
5
|
+
|
6
|
+
规范序列化可确保内存里的数据结构在序列化的时候保证字节一致性。它适用于双方想要有效比较他们独立维护的数据结构。在共识协议中,独立验证者需要就他们独立计算的状态达成一致。共识双方比较的是序列化数据的加密散列。要实现这一点,在计算时,相同数据结构的序列化必须相同。而独立验证器可能由不同的语言编写,有不同的实现代码,但是都遵循同一个规范。
|
7
|
+
|
8
|
+
|
9
|
+
## 安装
|
10
|
+
|
11
|
+
添加下列行到你的项目的Gemfile文件:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'canoser'
|
15
|
+
```
|
16
|
+
|
17
|
+
然后执行:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
或者直接通过命令行安装:
|
22
|
+
|
23
|
+
$ gem install canoser
|
24
|
+
|
25
|
+
## 使用
|
26
|
+
|
27
|
+
首先用Canoser定义一个数据结构,也就是写一个类继承自"Canoser::Struct",然后通过"define_field"方法来定义该结构所拥有的字段。该结构自然就拥有了序列化和反序列化的能力。例如下面的AccountResource定义了一个Libra代码中的同名数据结构:
|
28
|
+
```ruby
|
29
|
+
#ruby代码,利用canoser定义数据结构
|
30
|
+
class AccountResource < Canoser::Struct
|
31
|
+
define_field :authentication_key, [Canoser::Uint8]
|
32
|
+
define_field :balance, Canoser::Uint64
|
33
|
+
define_field :delegated_withdrawal_capability, Canoser::Bool
|
34
|
+
define_field :received_events_count, Canoser::Uint64
|
35
|
+
define_field :sent_events_count, Canoser::Uint64
|
36
|
+
define_field :sequence_number, Canoser::Uint64
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
下面是Libra中定义该数据结构以及序列化的代码:
|
41
|
+
```rust
|
42
|
+
// Libra中的rust语言代码
|
43
|
+
// 定义数据结构
|
44
|
+
pub struct AccountResource {
|
45
|
+
balance: u64,
|
46
|
+
sequence_number: u64,
|
47
|
+
authentication_key: ByteArray,
|
48
|
+
sent_events_count: u64,
|
49
|
+
received_events_count: u64,
|
50
|
+
delegated_withdrawal_capability: bool,
|
51
|
+
}
|
52
|
+
// 实现序列化
|
53
|
+
impl CanonicalSerialize for AccountResource {
|
54
|
+
fn serialize(&self, serializer: &mut impl CanonicalSerializer) -> Result<()> {
|
55
|
+
serializer
|
56
|
+
.encode_struct(&self.authentication_key)?
|
57
|
+
.encode_u64(self.balance)?
|
58
|
+
.encode_bool(self.delegated_withdrawal_capability)?
|
59
|
+
.encode_u64(self.received_events_count)?
|
60
|
+
.encode_u64(self.sent_events_count)?
|
61
|
+
.encode_u64(self.sequence_number)?;
|
62
|
+
Ok(())
|
63
|
+
}
|
64
|
+
}
|
65
|
+
```
|
66
|
+
在Libra使用的rust语言中,需要手动写代码实现数据结构的序列化/反序列化,而且数据结构中的字段顺序和序列化时的顺序不一定一致。
|
67
|
+
在Canoser中,定义好数据结构后,不需要写序列化和反序列化的代码。注意,Canoser中的数据结构顺序要按照Libra中序列化的顺序来定义。
|
68
|
+
|
69
|
+
### 支持的数据类型
|
70
|
+
|
71
|
+
字段支持的类型有:
|
72
|
+
|
73
|
+
| 字段类型 | 可选子类型 | 说明 |
|
74
|
+
| ------ | ------ | ------ |
|
75
|
+
| Canoser::Uint8 | | 无符号8位整数 |
|
76
|
+
| Canoser::Uint16 | | 无符号16位整数 |
|
77
|
+
| Canoser::Uint32 | | 无符号32位整数 |
|
78
|
+
| Canoser::Uint64 | | 无符号64位整数 |
|
79
|
+
| Canoser::Bool | | 布尔类型 |
|
80
|
+
| Canoser::Str | | 字符串 |
|
81
|
+
| [] | 支持 | 数组类型 |
|
82
|
+
| {} | 支持 | Map类型 |
|
83
|
+
| A Canoser::Struct Name | | 嵌套的另外一个结构(不能循环引用) |
|
84
|
+
|
85
|
+
### 关于数组类型
|
86
|
+
数组里的数据,如果没有定义类型,那么缺省是Uint8。下面的两个定义等价:
|
87
|
+
```ruby
|
88
|
+
class Arr1 < Canoser::Struct
|
89
|
+
define_field :addr, []
|
90
|
+
end
|
91
|
+
class Arr2 < Canoser::Struct
|
92
|
+
define_field :addr, [Canoser::Uint8]
|
93
|
+
end
|
94
|
+
```
|
95
|
+
数组还可以定义长度,表示定长数据。比如Libra中的地址是256位,也就是32个字节,所以可以如下定义:
|
96
|
+
```ruby
|
97
|
+
class Address < Canoser::Struct
|
98
|
+
define_field :addr, [Canoser::Uint8], 32
|
99
|
+
end
|
100
|
+
```
|
101
|
+
定长数据在序列化的时候,不写入长度信息。
|
102
|
+
|
103
|
+
### 关于Map类型
|
104
|
+
Map里的数据,如果没有定义类型,那么缺省是字节数组。下面的两个定义等价:
|
105
|
+
```ruby
|
106
|
+
class Map1 < Canoser::Struct
|
107
|
+
define_field :addr, {}
|
108
|
+
end
|
109
|
+
class Map2 < Canoser::Struct
|
110
|
+
define_field :addr, {[Canoser::Uint8], [Canoser::Uint8]}
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
### 结构嵌套
|
115
|
+
下面是一个复杂的例子,包含三个数据结构:
|
116
|
+
```ruby
|
117
|
+
class Addr < Canoser::Struct
|
118
|
+
define_field :addr, [Canoser::Uint8], 32
|
119
|
+
end
|
120
|
+
|
121
|
+
class Bar < Canoser::Struct
|
122
|
+
define_field :a, Canoser::Uint64
|
123
|
+
define_field :b, [Canoser::Uint8]
|
124
|
+
define_field :c, Addr
|
125
|
+
define_field :d, Canoser::Uint32
|
126
|
+
end
|
127
|
+
|
128
|
+
class Foo < Canoser::Struct
|
129
|
+
define_field :a, Canoser::Uint64
|
130
|
+
define_field :b, [Canoser::Uint8]
|
131
|
+
define_field :c, Bar
|
132
|
+
define_field :d, Canoser::Bool
|
133
|
+
define_field :e, {}
|
134
|
+
end
|
135
|
+
```
|
136
|
+
这个例子参考自libra中canonical serialization的测试代码。
|
137
|
+
|
138
|
+
### 序列化和反序列化
|
139
|
+
在定义好Canoser::Struct后,不需要自己实现序列化和反序列化代码,直接调用基类的默认实现即可。以AccountResource结构为例:
|
140
|
+
```ruby
|
141
|
+
#序列化
|
142
|
+
obj = AccountResource.new(authentication_key:[...],...)
|
143
|
+
bytes = obj.serialize
|
144
|
+
#反序列化
|
145
|
+
obj = AccountResource.deserialize(bytes)
|
146
|
+
```
|
147
|
+
|
data/README.md
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
# Canoser
|
2
|
+
|
3
|
+
[中文文档 Chinese document](/README-CN.md)
|
4
|
+
|
5
|
+
A ruby implementation of the canonical serialization for the Libra network.
|
6
|
+
|
7
|
+
Canonical serialization guarantees byte consistency when serializing an in-memory
|
8
|
+
data structure. It is useful for situations where two parties want to efficiently compare
|
9
|
+
data structures they independently maintain. It happens in consensus where
|
10
|
+
independent validators need to agree on the state they independently compute. A cryptographic
|
11
|
+
hash of the serialized data structure is what ultimately gets compared. In order for
|
12
|
+
this to work, the serialization of the same data structures must be identical when computed
|
13
|
+
by independent validators potentially running different implementations
|
14
|
+
of the same spec in different languages.
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Add this line to your application's Gemfile:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
gem 'canoser'
|
22
|
+
```
|
23
|
+
|
24
|
+
And then execute:
|
25
|
+
|
26
|
+
$ bundle
|
27
|
+
|
28
|
+
Or install it yourself as:
|
29
|
+
|
30
|
+
$ gem install canoser
|
31
|
+
|
32
|
+
## Usage
|
33
|
+
|
34
|
+
First define a data structure with Canoser, that is, write a class that inherits from "Canoser::Struct", and then define the fields owned by the structure through the "define_field" method. This structure naturally has the ability to canonical serialize and deserialize types. For example, the following AccountResource defines a data structure of the same name in the Libra code:
|
35
|
+
```ruby
|
36
|
+
#ruby code,define canoser data structure
|
37
|
+
class AccountResource < Canoser::Struct
|
38
|
+
define_field :authentication_key, [Canoser::Uint8]
|
39
|
+
define_field :balance, Canoser::Uint64
|
40
|
+
define_field :delegated_withdrawal_capability, Canoser::Bool
|
41
|
+
define_field :received_events_count, Canoser::Uint64
|
42
|
+
define_field :sent_events_count, Canoser::Uint64
|
43
|
+
define_field :sequence_number, Canoser::Uint64
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
Here is the code that defines this data structure and serialization in Libra code:
|
48
|
+
```rust
|
49
|
+
// rust code in Libra
|
50
|
+
// define the data structure
|
51
|
+
pub struct AccountResource {
|
52
|
+
balance: u64,
|
53
|
+
sequence_number: u64,
|
54
|
+
authentication_key: ByteArray,
|
55
|
+
sent_events_count: u64,
|
56
|
+
received_events_count: u64,
|
57
|
+
delegated_withdrawal_capability: bool,
|
58
|
+
}
|
59
|
+
// serialization
|
60
|
+
impl CanonicalSerialize for AccountResource {
|
61
|
+
fn serialize(&self, serializer: &mut impl CanonicalSerializer) -> Result<()> {
|
62
|
+
serializer
|
63
|
+
.encode_struct(&self.authentication_key)?
|
64
|
+
.encode_u64(self.balance)?
|
65
|
+
.encode_bool(self.delegated_withdrawal_capability)?
|
66
|
+
.encode_u64(self.received_events_count)?
|
67
|
+
.encode_u64(self.sent_events_count)?
|
68
|
+
.encode_u64(self.sequence_number)?;
|
69
|
+
Ok(())
|
70
|
+
}
|
71
|
+
}
|
72
|
+
```
|
73
|
+
In the rust language used by Libra, it is necessary to manually write code to serialize/deserialize the data structure, and the order of the fields in the data structure and the order of serialization are not necessarily the same.
|
74
|
+
|
75
|
+
In Canoser, after defining the data structure, you don't need to write code to implement serialization and deserialization. Note that the order of the data structures in Canoser is defined in the order in which they are serialized in Libra.
|
76
|
+
|
77
|
+
### Supported field types
|
78
|
+
|
79
|
+
| field type | optionl sub type | description |
|
80
|
+
| ------ | ------ | ------ |
|
81
|
+
| Canoser::Uint8 | | Unsigned 8-bit integer |
|
82
|
+
| Canoser::Uint16 | | Unsigned 16-bit integer|
|
83
|
+
| Canoser::Uint32 | | Unsigned 32-bit integer |
|
84
|
+
| Canoser::Uint64 | | Unsigned 64-bit integer |
|
85
|
+
| Canoser::Bool | | Boolean |
|
86
|
+
| Canoser::Str | | String |
|
87
|
+
| [] | supported | Array Type |
|
88
|
+
| {} | supported | Map Type |
|
89
|
+
| A Canoser::Struct Name| | Another structure nested (cannot be recycled) |
|
90
|
+
|
91
|
+
### About Array Type
|
92
|
+
The default data type (if not defined) in the array is Uint8. The following two definitions are equivalent:
|
93
|
+
```ruby
|
94
|
+
class Arr1 < Canoser::Struct
|
95
|
+
define_field :addr, []
|
96
|
+
end
|
97
|
+
class Arr2 < Canoser::Struct
|
98
|
+
define_field :addr, [Canoser::Uint8]
|
99
|
+
end
|
100
|
+
```
|
101
|
+
Arrays can also define lengths to represent fixed length data. For example, the address in Libra is 256 bits, which is 32 bytes, so it can be defined as follows:
|
102
|
+
```ruby
|
103
|
+
class Address < Canoser::Struct
|
104
|
+
define_field :addr, [Canoser::Uint8], 32
|
105
|
+
end
|
106
|
+
```
|
107
|
+
When the fixed length data is serialized, the length information is not written to the output.
|
108
|
+
|
109
|
+
|
110
|
+
### About map type
|
111
|
+
The default data type (if not defined) in the map is an array of Uint8. The following two definitions are equivalent:
|
112
|
+
```ruby
|
113
|
+
class Map1 < Canoser::Struct
|
114
|
+
define_field :addr, {}
|
115
|
+
end
|
116
|
+
class Map2 < Canoser::Struct
|
117
|
+
define_field :addr, {[Canoser::Uint8], [Canoser::Uint8]}
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
### Nested data structure
|
122
|
+
The following is a complex example with three data structures:
|
123
|
+
```ruby
|
124
|
+
class Addr < Canoser::Struct
|
125
|
+
define_field :addr, [Canoser::Uint8], 32
|
126
|
+
end
|
127
|
+
|
128
|
+
class Bar < Canoser::Struct
|
129
|
+
define_field :a, Canoser::Uint64
|
130
|
+
define_field :b, [Canoser::Uint8]
|
131
|
+
define_field :c, Addr
|
132
|
+
define_field :d, Canoser::Uint32
|
133
|
+
end
|
134
|
+
|
135
|
+
class Foo < Canoser::Struct
|
136
|
+
define_field :a, Canoser::Uint64
|
137
|
+
define_field :b, [Canoser::Uint8]
|
138
|
+
define_field :c, Bar
|
139
|
+
define_field :d, Canoser::Bool
|
140
|
+
define_field :e, {}
|
141
|
+
end
|
142
|
+
```
|
143
|
+
This example refers to the test code from canonical serialization in libra.
|
144
|
+
|
145
|
+
### Serialization and deserialization
|
146
|
+
After defining Canoser::Struct, you don't need to implement serialization and deserialization code yourself, you can directly call the default implementation of the base class. Take the AccountResource structure as an example:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
#序列化
|
150
|
+
obj = AccountResource.new(authentication_key:[...],...)
|
151
|
+
bytes = obj.serialize
|
152
|
+
#反序列化
|
153
|
+
obj = AccountResource.deserialize(bytes)
|
154
|
+
```
|
155
|
+
|
156
|
+
|
157
|
+
## Development
|
158
|
+
|
159
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
160
|
+
|
161
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
162
|
+
|
163
|
+
## Contributing
|
164
|
+
|
165
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/yuanxinyu/canoser. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
166
|
+
|
167
|
+
## License
|
168
|
+
|
169
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
170
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "canoser"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/canoser.gemspec
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "canoser/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "canoser"
|
8
|
+
spec.version = Canoser::VERSION
|
9
|
+
spec.authors = ["yuan xinyu"]
|
10
|
+
spec.email = ["yuanxinyu.hangzhou@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{A ruby implementation of the canonical serialization for the Libra network.}
|
13
|
+
spec.description = %q{A ruby implementation of the canonical serialization for the Libra network.}
|
14
|
+
spec.homepage = "https://github.com/yuanxinyu/canoser.git"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
18
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
21
|
+
|
22
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
23
|
+
spec.metadata["source_code_uri"] = "https://github.com/yuanxinyu/canoser.git"
|
24
|
+
spec.metadata["changelog_uri"] = "https://github.com/yuanxinyu/canoser.git"
|
25
|
+
else
|
26
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
27
|
+
"public gem pushes."
|
28
|
+
end
|
29
|
+
|
30
|
+
# Specify which files should be added to the gem when it is released.
|
31
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
32
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
33
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
34
|
+
end
|
35
|
+
spec.bindir = "exe"
|
36
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
37
|
+
spec.require_paths = ["lib"]
|
38
|
+
|
39
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
40
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
41
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
42
|
+
spec.add_development_dependency "byebug", "~> 11.0"
|
43
|
+
|
44
|
+
end
|
data/lib/canoser.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# What is canonical serialization?
|
2
|
+
#
|
3
|
+
# Canonical serialization guarantees byte consistency when serializing an in-memory
|
4
|
+
# data structure. It is useful for situations where two parties want to efficiently compare
|
5
|
+
# data structures they independently maintain. It happens in consensus where
|
6
|
+
# independent validators need to agree on the state they independently compute. A cryptographic
|
7
|
+
# hash of the serialized data structure is what ultimately gets compared. In order for
|
8
|
+
# this to work, the serialization of the same data structures must be identical when computed
|
9
|
+
# by independent validators potentially running different implementations
|
10
|
+
# of the same spec in different languages.
|
11
|
+
#
|
12
|
+
# One design
|
13
|
+
# goal of this serialization format is to optimize for simplicity. It is not designed to be
|
14
|
+
# another full-fledged network serialization as Protobuf or Thrift. It is designed
|
15
|
+
# for doing only one thing right, which is to deterministically generate consistent bytes
|
16
|
+
# from a data structure.
|
17
|
+
#
|
18
|
+
# An extremely simple implementation of CanonicalSerializer is also provided, the encoding
|
19
|
+
# rules are:
|
20
|
+
# (All unsigned integers are encoded in little-endian representation unless specified otherwise)
|
21
|
+
#
|
22
|
+
# 1. The encoding of an unsigned 64-bit integer is defined as its little-endian representation
|
23
|
+
# in 8 bytes
|
24
|
+
#
|
25
|
+
# 2. The encoding of an item (byte array) is defined as:
|
26
|
+
# [length in bytes, represented as 4-byte integer] || [item in bytes]
|
27
|
+
#
|
28
|
+
#
|
29
|
+
# 3. The encoding of a list of items is defined as: (This is not implemented yet because
|
30
|
+
# there is no known struct that needs it yet, but can be added later easily)
|
31
|
+
# [No. of items in the list, represented as 4-byte integer] || encoding(item_0) || ....
|
32
|
+
#
|
33
|
+
# 4. The encoding of an ordered map where the keys are ordered by lexicographic order.
|
34
|
+
# Currently, we only support key and value of type Vec<u8>. The encoding is defined as:
|
35
|
+
# [No. of key value pairs in the map, represented as 4-byte integer] || encode(key1) ||
|
36
|
+
# encode(value1) || encode(key2) || encode(value2)...
|
37
|
+
# where the pairs are appended following the lexicographic order of the key
|
38
|
+
#
|
39
|
+
require "canoser/version"
|
40
|
+
require "canoser/cursor"
|
41
|
+
require "canoser/field"
|
42
|
+
require "canoser/struct"
|
43
|
+
|
44
|
+
module Canoser
|
45
|
+
class ParseError < StandardError; end
|
46
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Canoser
|
2
|
+
class Cursor
|
3
|
+
def initialize(bytes, offset=0)
|
4
|
+
@bytes = bytes
|
5
|
+
@offset = offset
|
6
|
+
end
|
7
|
+
|
8
|
+
def read_bytes(size)
|
9
|
+
raise ParseError.new("#{@offset+size} exceed bytes size:#{@bytes.size}") if @offset+size > @bytes.size
|
10
|
+
ret = @bytes[@offset, size]
|
11
|
+
@offset += size
|
12
|
+
ret
|
13
|
+
end
|
14
|
+
|
15
|
+
def peek_bytes(size)
|
16
|
+
raise ParseError.new("#{@offset+size} exceed bytes size:#{@bytes.size}") if @offset+size > @bytes.size
|
17
|
+
@bytes[@offset, size]
|
18
|
+
end
|
19
|
+
|
20
|
+
def finished?
|
21
|
+
@offset == @bytes.size
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
|
2
|
+
module Canoser
|
3
|
+
class IntField
|
4
|
+
@@pack_map = {8 => "C", 16 => "S", 32 => "L", 64 => "Q"}
|
5
|
+
|
6
|
+
def initialize(int_bits)
|
7
|
+
@int_bits = int_bits
|
8
|
+
end
|
9
|
+
|
10
|
+
def inspect
|
11
|
+
"Uint#{@int_bits}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
inspect
|
16
|
+
end
|
17
|
+
|
18
|
+
def pack_str
|
19
|
+
@@pack_map[@int_bits]
|
20
|
+
end
|
21
|
+
|
22
|
+
def encode(value)
|
23
|
+
[value].pack(pack_str)
|
24
|
+
end
|
25
|
+
|
26
|
+
def decode_bytes(bytes)
|
27
|
+
bytes.unpack(pack_str)[0]
|
28
|
+
end
|
29
|
+
|
30
|
+
def decode(cursor)
|
31
|
+
bytes = cursor.read_bytes(@int_bits/8)
|
32
|
+
decode_bytes(bytes)
|
33
|
+
end
|
34
|
+
|
35
|
+
def max_value
|
36
|
+
2**@int_bits - 1
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
Uint8 = IntField.new(8)
|
41
|
+
Uint16 = IntField.new(16)
|
42
|
+
Uint32 = IntField.new(32)
|
43
|
+
Uint64 = IntField.new(64)
|
44
|
+
|
45
|
+
class Bool
|
46
|
+
def self.encode(value)
|
47
|
+
if value
|
48
|
+
"\1"
|
49
|
+
else
|
50
|
+
"\0"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.decode_bytes(bytes)
|
55
|
+
return true if bytes == "\1"
|
56
|
+
return false if bytes == "\0"
|
57
|
+
raise ParseError.new("bool should be 0 or 1.")
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.decode(cursor)
|
61
|
+
bytes = cursor.read_bytes(1)
|
62
|
+
decode_bytes(bytes)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
class Str
|
68
|
+
def self.encode(value)
|
69
|
+
output = ""
|
70
|
+
bytes = value.bytes
|
71
|
+
output << Uint32.encode(bytes.size)
|
72
|
+
bytes.each{|x| output << Canoser::Uint8.encode(x)}
|
73
|
+
output
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.decode(cursor)
|
77
|
+
str = ""
|
78
|
+
len = Uint32.decode(cursor)
|
79
|
+
len.times do
|
80
|
+
str << Uint8.decode(cursor)
|
81
|
+
end
|
82
|
+
str
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
class ArrayT
|
88
|
+
attr_accessor :type, :fixed_len
|
89
|
+
|
90
|
+
def initialize(type=Uint8, fixed_len=nil)
|
91
|
+
@type = type
|
92
|
+
@fixed_len = fixed_len
|
93
|
+
end
|
94
|
+
|
95
|
+
def encode(arr)
|
96
|
+
output = ""
|
97
|
+
output << Uint32.encode(arr.size) unless @fixed_len
|
98
|
+
arr.each{|x| output << @type.encode(x)}
|
99
|
+
output
|
100
|
+
end
|
101
|
+
|
102
|
+
def decode(cursor)
|
103
|
+
arr = []
|
104
|
+
len = @fixed_len
|
105
|
+
len = Uint32.decode(cursor) unless @fixed_len
|
106
|
+
len.times do
|
107
|
+
arr << @type.decode(cursor)
|
108
|
+
end
|
109
|
+
arr
|
110
|
+
end
|
111
|
+
|
112
|
+
def ==(other)
|
113
|
+
return self.type == other.type && self.fixed_len == other.fixed_len
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
DEFAULT_KV = ArrayT.new(Uint8)
|
119
|
+
|
120
|
+
class HashT
|
121
|
+
attr_accessor :ktype, :vtype
|
122
|
+
|
123
|
+
def initialize(ktype=DEFAULT_KV, vtype=DEFAULT_KV)
|
124
|
+
@ktype = ktype || DEFAULT_KV
|
125
|
+
@vtype = vtype || DEFAULT_KV
|
126
|
+
end
|
127
|
+
|
128
|
+
def encode(hash)
|
129
|
+
output = ""
|
130
|
+
output << Uint32.encode(hash.size)
|
131
|
+
sorted_map = {}
|
132
|
+
hash.each do |k, v|
|
133
|
+
sorted_map[ktype.encode(k)] = vtype.encode(v)
|
134
|
+
end
|
135
|
+
sorted_map.keys.sort.each do |k|
|
136
|
+
output << k
|
137
|
+
output << sorted_map[k]
|
138
|
+
end
|
139
|
+
output
|
140
|
+
end
|
141
|
+
|
142
|
+
def decode(cursor)
|
143
|
+
hash = {}
|
144
|
+
len = Uint32.decode(cursor)
|
145
|
+
len.times do
|
146
|
+
k = ktype.decode(cursor)
|
147
|
+
v = vtype.decode(cursor)
|
148
|
+
hash[k] = v
|
149
|
+
end
|
150
|
+
hash
|
151
|
+
end
|
152
|
+
|
153
|
+
def ==(other)
|
154
|
+
return self.ktype == other.ktype && self.vtype == other.vtype
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
class Optional
|
161
|
+
def initialize(bool, value)
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
|
2
|
+
module Canoser
|
3
|
+
|
4
|
+
class Struct
|
5
|
+
def self.define_field(name, type, arr_len=nil)
|
6
|
+
name = ":"+name.to_sym.to_s
|
7
|
+
if type.class == Array
|
8
|
+
type = ArrayT.new(type[0], arr_len)
|
9
|
+
elsif type.class == Hash
|
10
|
+
type = HashT.new(type.keys[0], type.values[0])
|
11
|
+
else
|
12
|
+
raise "type #{type} doen't support arr_len param" if arr_len
|
13
|
+
end
|
14
|
+
str = %Q{
|
15
|
+
@@names ||= []
|
16
|
+
@@names << #{name}
|
17
|
+
@@types ||= []
|
18
|
+
@@types << type
|
19
|
+
}
|
20
|
+
class_eval str
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(hash={})
|
24
|
+
@values = {}
|
25
|
+
hash.each do |k,v|
|
26
|
+
idx = self.class.class_variable_get("@@names").find_index{|x| x==k}
|
27
|
+
raise "#{k} is not a field of #{self}" unless idx
|
28
|
+
type = self.class.class_variable_get("@@types")[idx]
|
29
|
+
if type.class == ArrayT
|
30
|
+
len = type.fixed_len
|
31
|
+
raise "fix-length array #{k}: #{len} != #{v.size}" if len && v.size != len
|
32
|
+
end
|
33
|
+
@values[k] = v
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def [](name)
|
38
|
+
@values[name.to_sym]
|
39
|
+
end
|
40
|
+
|
41
|
+
def []=(name, value)
|
42
|
+
@values[name.to_sym] = value
|
43
|
+
end
|
44
|
+
|
45
|
+
def ==(other)
|
46
|
+
return true if self.equal?(other)
|
47
|
+
self.class.class_variable_get("@@names").each_with_index do |name, idx|
|
48
|
+
unless @values[name] == other[name]
|
49
|
+
return false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
true
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.encode(value)
|
56
|
+
value.serialize
|
57
|
+
end
|
58
|
+
|
59
|
+
def serialize
|
60
|
+
output = ""
|
61
|
+
self.class.class_variable_get("@@names").each_with_index do |name, idx|
|
62
|
+
type = self.class.class_variable_get("@@types")[idx]
|
63
|
+
value = @values[name]
|
64
|
+
output << type.encode(value)
|
65
|
+
end
|
66
|
+
output
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.deserialize(bytes)
|
70
|
+
cursor = Canoser::Cursor.new(bytes)
|
71
|
+
ret = decode(cursor)
|
72
|
+
raise ParseError.new("bytes not all consumed.") unless cursor.finished?
|
73
|
+
ret
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.decode(cursor)
|
77
|
+
self.new({}).decode(cursor)
|
78
|
+
end
|
79
|
+
|
80
|
+
def decode(cursor)
|
81
|
+
self.class.class_variable_get("@@names").each_with_index do |name, idx|
|
82
|
+
type = self.class.class_variable_get("@@types")[idx]
|
83
|
+
@values[name] = type.decode(cursor)
|
84
|
+
end
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: canoser
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- yuan xinyu
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-08-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: byebug
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '11.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '11.0'
|
69
|
+
description: A ruby implementation of the canonical serialization for the Libra network.
|
70
|
+
email:
|
71
|
+
- yuanxinyu.hangzhou@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- ".travis.yml"
|
78
|
+
- Gemfile
|
79
|
+
- Gemfile.lock
|
80
|
+
- LICENSE.txt
|
81
|
+
- README-CN.md
|
82
|
+
- README.md
|
83
|
+
- Rakefile
|
84
|
+
- bin/console
|
85
|
+
- bin/setup
|
86
|
+
- canoser.gemspec
|
87
|
+
- lib/canoser.rb
|
88
|
+
- lib/canoser/cursor.rb
|
89
|
+
- lib/canoser/field.rb
|
90
|
+
- lib/canoser/struct.rb
|
91
|
+
- lib/canoser/version.rb
|
92
|
+
homepage: https://github.com/yuanxinyu/canoser.git
|
93
|
+
licenses:
|
94
|
+
- MIT
|
95
|
+
metadata:
|
96
|
+
allowed_push_host: https://rubygems.org
|
97
|
+
homepage_uri: https://github.com/yuanxinyu/canoser.git
|
98
|
+
source_code_uri: https://github.com/yuanxinyu/canoser.git
|
99
|
+
changelog_uri: https://github.com/yuanxinyu/canoser.git
|
100
|
+
post_install_message:
|
101
|
+
rdoc_options: []
|
102
|
+
require_paths:
|
103
|
+
- lib
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
requirements: []
|
115
|
+
rubyforge_project:
|
116
|
+
rubygems_version: 2.7.7
|
117
|
+
signing_key:
|
118
|
+
specification_version: 4
|
119
|
+
summary: A ruby implementation of the canonical serialization for the Libra network.
|
120
|
+
test_files: []
|