1
+ use thiserror:: Error ;
2
+
1
3
use fvm_sdk:: crypto;
2
4
3
5
/// Minimal interface for a hashing function
4
6
///
5
- /// Hasher::hash() must return a digest that is at least 4 bytes long so that it can be cast to a u32
7
+ /// Hasher::hash() must return a digest that is at least 4 bytes long so that it can be cast to a
8
+ /// u32
6
9
pub trait Hasher {
7
10
/// For an input of bytes return a digest that is at least 4 bytes long
8
11
fn hash ( & self , bytes : & [ u8 ] ) -> Vec < u8 > ;
@@ -18,45 +21,84 @@ impl Hasher for Blake2bSyscall {
18
21
}
19
22
}
20
23
24
+ /// Uses an underlying hashing function (blake2b by convention) to generate method numbers from
25
+ /// method names
21
26
#[ derive( Default ) ]
22
- pub struct MethodHasher < T : Hasher > {
27
+ pub struct MethodResolver < T : Hasher > {
23
28
hasher : T ,
24
29
}
25
30
26
- impl < T : Hasher > MethodHasher < T > {
31
+ #[ derive( Error , PartialEq , Debug ) ]
32
+ pub enum MethodNameErr {
33
+ #[ error( "empty method name provided" ) ]
34
+ EmptyString ,
35
+ #[ error( "illegal symbol used in method name" ) ]
36
+ IllegalSymbol ,
37
+ #[ error( "unable to calculate method id, choose a another method name" ) ]
38
+ IndeterminableId ,
39
+ }
40
+
41
+ impl < T : Hasher > MethodResolver < T > {
27
42
const CONSTRUCTOR_METHOD_NAME : & ' static str = "Constructor" ;
28
43
const CONSTRUCTOR_METHOD_NUMBER : u64 = 1_u64 ;
44
+ const RESERVED_METHOD_NUMBER : u64 = 0_u64 ;
45
+
46
+ /// Creates a MethodResolver with an instance of a hasher (blake2b by convention)
29
47
pub fn new ( hasher : T ) -> Self {
30
48
Self { hasher }
31
49
}
32
50
33
- pub fn method_number ( & self , method_name : & str ) -> u64 {
51
+ /// Generates a standard FRC-XXX compliant method number
52
+ ///
53
+ /// The method number is calculated as the first four bytes of `hash(method-name)`.
54
+ /// The name `Constructor` is always hashed to 1 and other method names that hash to
55
+ /// 0 or 1 are avoided via rejection sampling.
56
+ ///
57
+ ///
58
+ pub fn method_number ( & self , method_name : & str ) -> Result < u64 , MethodNameErr > {
59
+ // TODO: sanitise method_name before checking (or reject invalid whitespace)
60
+ if method_name. contains ( '|' ) {
61
+ return Err ( MethodNameErr :: IllegalSymbol ) ;
62
+ }
63
+
64
+ if method_name. is_empty ( ) {
65
+ return Err ( MethodNameErr :: EmptyString ) ;
66
+ }
67
+
34
68
if method_name == Self :: CONSTRUCTOR_METHOD_NAME {
35
- Self :: CONSTRUCTOR_METHOD_NUMBER
36
- } else {
37
- let digest = self . hasher . hash ( method_name. as_bytes ( ) ) ;
38
- if digest. len ( ) < 4 {
39
- panic ! ( "Invalid hasher used: digest must be at least 4 bytes long" ) ;
69
+ return Ok ( Self :: CONSTRUCTOR_METHOD_NUMBER ) ;
70
+ }
71
+ let mut digest = self . hasher . hash ( method_name. as_bytes ( ) ) ;
72
+ while digest. len ( ) >= 4 {
73
+ let method_id = as_u32 ( digest. as_slice ( ) ) as u64 ;
74
+ if method_id != Self :: CONSTRUCTOR_METHOD_NUMBER
75
+ && method_id != Self :: RESERVED_METHOD_NUMBER
76
+ {
77
+ return Ok ( method_id) ;
78
+ } else {
79
+ digest. remove ( 0 ) ;
40
80
}
41
- as_u32 ( digest. as_slice ( ) ) as u64
42
81
}
82
+ Err ( MethodNameErr :: IndeterminableId )
43
83
}
44
84
}
45
85
46
86
/// Takes a byte array and interprets it as a u32 number
47
- /// Assumes little-endian order
48
- #[ rustfmt:: skip]
87
+ ///
88
+ /// Using big-endian order interperets the first four bytes to an int.
89
+ /// The slice passed to this must be at least length 4
49
90
fn as_u32 ( bytes : & [ u8 ] ) -> u32 {
50
- ( ( bytes[ 0 ] as u32 ) << ( 8 * 3 ) ) +
51
- ( ( bytes[ 1 ] as u32 ) << ( 8 * 2 ) ) +
52
- ( ( bytes[ 2 ] as u32 ) << ( 8 * 1 ) ) +
53
- ( bytes[ 3 ] as u32 )
91
+ u32:: from_be_bytes (
92
+ bytes[ 0 ..4 ]
93
+ . try_into ( )
94
+ . expect ( "bytes was not at least length 4" ) ,
95
+ )
54
96
}
55
97
56
98
#[ cfg( test) ]
57
99
mod tests {
58
100
59
- use super :: { Blake2bSyscall , Hasher , MethodHasher } ;
101
+ use super :: { Hasher , MethodNameErr , MethodResolver } ;
60
102
61
103
#[ derive( Clone , Copy ) ]
62
104
struct FakeHasher { }
@@ -67,24 +109,56 @@ mod tests {
67
109
}
68
110
69
111
#[ test]
70
- # [ allow ( unused ) ]
71
- fn compile ( ) {
72
- let method_hasher = MethodHasher :: new ( Blake2bSyscall { } ) ;
112
+ fn constructor_is_1 ( ) {
113
+ let method_hasher = MethodResolver :: new ( FakeHasher { } ) ;
114
+ assert_eq ! ( method_hasher. method_number ( "Constructor" ) . unwrap ( ) , 1 ) ;
73
115
}
74
116
75
117
#[ test]
76
- fn constructor_method_number ( ) {
77
- let method_hasher = MethodHasher :: new ( FakeHasher { } ) ;
78
- assert_eq ! ( method_hasher. method_number( "Constructor" ) , 1 ) ;
118
+ fn normal_method_is_hashed ( ) {
119
+ let fake_hasher = FakeHasher { } ;
120
+ let method_hasher = MethodResolver :: new ( fake_hasher) ;
121
+ assert_eq ! (
122
+ method_hasher. method_number( "NormalMethod" ) . unwrap( ) ,
123
+ super :: as_u32( & fake_hasher. hash( b"NormalMethod" ) ) as u64
124
+ ) ;
125
+
126
+ assert_eq ! (
127
+ method_hasher. method_number( "NormalMethod2" ) . unwrap( ) ,
128
+ super :: as_u32( & fake_hasher. hash( b"NormalMethod2" ) ) as u64
129
+ ) ;
79
130
}
80
131
81
132
#[ test]
82
- fn normal_method_number ( ) {
83
- let fake_hasher = FakeHasher { } ;
84
- let method_hasher = MethodHasher :: new ( fake_hasher) ;
133
+ fn disallows_invalid_method_names ( ) {
134
+ let method_hasher = MethodResolver :: new ( FakeHasher { } ) ;
85
135
assert_eq ! (
86
- method_hasher. method_number( "NormalMethod" ) ,
87
- super :: as_u32 ( & fake_hasher . hash ( b"NormalMethod" ) ) as u64
136
+ method_hasher. method_number( "Invalid|Method" ) . unwrap_err ( ) ,
137
+ MethodNameErr :: IllegalSymbol
88
138
) ;
139
+ assert_eq ! (
140
+ method_hasher. method_number( "" ) . unwrap_err( ) ,
141
+ MethodNameErr :: EmptyString
142
+ ) ;
143
+ }
144
+
145
+ #[ test]
146
+ fn avoids_disallowed_method_numbers ( ) {
147
+ let hasher = FakeHasher { } ;
148
+ let method_hasher = MethodResolver :: new ( hasher) ;
149
+
150
+ // This simulates a method name that would hash to 0
151
+ let contrived_0 = "\0 \0 \0 \0 MethodName" ;
152
+ let contrived_0_digest = hasher. hash ( contrived_0. as_bytes ( ) ) ;
153
+ assert_eq ! ( super :: as_u32( & contrived_0_digest) , 0 ) ;
154
+ // But the method number is not a collision
155
+ assert_ne ! ( method_hasher. method_number( contrived_0) . unwrap( ) , 0 ) ;
156
+
157
+ // This simulates a method name that would hash to 1
158
+ let contrived_1 = "\0 \0 \0 \x01 MethodName" ;
159
+ let contrived_1_digest = hasher. hash ( contrived_1. as_bytes ( ) ) ;
160
+ assert_eq ! ( super :: as_u32( & contrived_1_digest) , 1 ) ;
161
+ // But the method number is not a collision
162
+ assert_ne ! ( method_hasher. method_number( contrived_1) . unwrap( ) , 1 ) ;
89
163
}
90
164
}
0 commit comments