1
1
//! Contains functions related to the migration script of Testnet78.
2
- use cnidarium:: { StateDelta , Storage } ;
3
- use std:: time:: Duration ;
2
+ use cnidarium:: { Snapshot , StateDelta , Storage } ;
3
+ use futures:: TryStreamExt as _;
4
+ use jmt:: RootHash ;
5
+ use pbjson_types:: Any ;
6
+ use penumbra_app:: app:: StateReadExt as _;
7
+ use penumbra_proto:: { DomainType as _, StateReadProto as _, StateWriteProto as _} ;
8
+ use penumbra_sct:: component:: clock:: EpochRead as _;
9
+ use penumbra_stake:: validator:: Validator ;
10
+ use std:: { path:: PathBuf , time:: Duration } ;
4
11
use tracing:: instrument;
5
12
13
+ use crate :: testnet:: generate:: TestnetConfig ;
14
+
6
15
/// Run the full migration, given an export path and a start time for genesis.
7
16
///
8
17
/// Menu:
9
18
/// - Truncate various user-supplied `String` fields to a maximum length.
10
- /// * Validator Definitions :
19
+ /// * Validators :
11
20
/// - `name` (140 characters)
12
21
/// - `website` (70 characters)
13
22
/// - `description` (280 characters)
@@ -25,19 +34,154 @@ use tracing::instrument;
25
34
/// * Signaling Proposals:
26
35
/// - `commit hash` (64 characters)
27
36
#[ instrument]
28
- pub async fn migrate ( storage : Storage ) -> anyhow:: Result < Duration > {
37
+ pub async fn migrate (
38
+ storage : Storage ,
39
+ pd_home : PathBuf ,
40
+ genesis_start : Option < tendermint:: time:: Time > ,
41
+ ) -> anyhow:: Result < ( ) > {
29
42
// Setup:
30
43
let initial_state = storage. latest_snapshot ( ) ;
44
+ let chain_id = initial_state. get_chain_id ( ) . await ?;
45
+ let root_hash = initial_state
46
+ . root_hash ( )
47
+ . await
48
+ . expect ( "chain state has a root hash" ) ;
49
+ let pre_upgrade_root_hash: RootHash = root_hash. into ( ) ;
50
+ let pre_upgrade_height = initial_state
51
+ . get_block_height ( )
52
+ . await
53
+ . expect ( "chain state has a block height" ) ;
54
+ let post_upgrade_height = pre_upgrade_height. wrapping_add ( 1 ) ;
31
55
32
56
// We initialize a `StateDelta` and start by reaching into the JMT for all entries matching the
33
57
// swap execution prefix. Then, we write each entry to the nv-storage.
34
58
let mut delta = StateDelta :: new ( initial_state) ;
35
- let start_time = std:: time:: SystemTime :: now ( ) ;
59
+ let ( migration_duration, post_upgrade_root_hash) = {
60
+ let start_time = std:: time:: SystemTime :: now ( ) ;
61
+ // Adjust the length of `Validator` fields.
62
+ truncate_validator_fields ( & mut delta) . await ?;
63
+
64
+ let post_upgrade_root_hash = storage. commit_in_place ( delta) . await ?;
65
+ tracing:: info!( ?post_upgrade_root_hash, "post-migration root hash" ) ;
66
+
67
+ (
68
+ start_time. elapsed ( ) . expect ( "start time not set" ) ,
69
+ post_upgrade_root_hash,
70
+ )
71
+ } ;
72
+ storage. release ( ) . await ;
73
+
74
+ // The migration is complete, now we need to generate a genesis file. To do this, we need
75
+ // to lookup a validator view from the chain, and specify the post-upgrade app hash and
76
+ // initial height.
77
+ let app_state = penumbra_app:: genesis:: Content {
78
+ chain_id,
79
+ ..Default :: default ( )
80
+ } ;
81
+ let mut genesis = TestnetConfig :: make_genesis ( app_state. clone ( ) ) . expect ( "can make genesis" ) ;
82
+ genesis. app_hash = post_upgrade_root_hash
83
+ . 0
84
+ . to_vec ( )
85
+ . try_into ( )
86
+ . expect ( "infaillible conversion" ) ;
87
+ genesis. initial_height = post_upgrade_height as i64 ;
88
+ genesis. genesis_time = genesis_start. unwrap_or_else ( || {
89
+ let now = tendermint:: time:: Time :: now ( ) ;
90
+ tracing:: info!( %now, "no genesis time provided, detecting a testing setup" ) ;
91
+ now
92
+ } ) ;
93
+ let checkpoint = post_upgrade_root_hash. 0 . to_vec ( ) ;
94
+ let genesis = TestnetConfig :: make_checkpoint ( genesis, Some ( checkpoint) ) ;
95
+
96
+ let genesis_json = serde_json:: to_string ( & genesis) . expect ( "can serialize genesis" ) ;
97
+ tracing:: info!( "genesis: {}" , genesis_json) ;
98
+ let genesis_path = pd_home. join ( "genesis.json" ) ;
99
+ std:: fs:: write ( genesis_path, genesis_json) . expect ( "can write genesis" ) ;
100
+
101
+ let validator_state_path = pd_home. join ( "priv_validator_state.json" ) ;
102
+
103
+ let fresh_validator_state = crate :: testnet:: generate:: TestnetValidator :: initial_state ( ) ;
104
+ std:: fs:: write ( validator_state_path, fresh_validator_state) . expect ( "can write validator state" ) ;
105
+
106
+ tracing:: info!(
107
+ pre_upgrade_height,
108
+ post_upgrade_height,
109
+ ?pre_upgrade_root_hash,
110
+ ?post_upgrade_root_hash,
111
+ duration = migration_duration. as_secs( ) ,
112
+ "successful migration!"
113
+ ) ;
114
+
115
+ Ok ( ( ) )
116
+ }
117
+
118
+ async fn truncate_validator_fields ( delta : & mut StateDelta < Snapshot > ) -> anyhow:: Result < ( ) > {
119
+ let key_prefix_validators = penumbra_stake:: state_key:: validators:: definitions:: prefix ( ) ;
120
+ let all_validators = delta
121
+ . prefix_proto :: < Any > ( & key_prefix_validators)
122
+ . map_ok ( |( k, v) | ( k, Validator :: decode ( v. value ) . expect ( "only validators" ) ) )
123
+ . try_collect :: < Vec < ( String , Validator ) > > ( )
124
+ . await ?;
125
+
126
+ for ( key, mut validator) in all_validators {
127
+ validator. name = truncate ( & validator. name , 140 ) . to_string ( ) ;
128
+
129
+ delta. put ( key, validator) ;
130
+ }
131
+
132
+ Ok ( ( ) )
133
+ }
134
+
135
+ // Since the limits are based on `String::len`, which returns
136
+ // the number of bytes, we need to truncate the UTF-8 strings at the
137
+ // correct byte boundaries.
138
+ //
139
+ // This can be simplified once https://github.com/rust-lang/rust/issues/93743
140
+ // is stabilized.
141
+ #[ inline]
142
+ pub fn floor_char_boundary ( s : & str , index : usize ) -> usize {
143
+ if index >= s. len ( ) {
144
+ s. len ( )
145
+ } else {
146
+ let lower_bound = index. saturating_sub ( 3 ) ;
147
+ let new_index = s. as_bytes ( ) [ lower_bound..=index]
148
+ . iter ( )
149
+ . rposition ( |b| is_utf8_char_boundary ( * b) ) ;
150
+
151
+ // SAFETY: we know that the character boundary will be within four bytes
152
+ unsafe { lower_bound + new_index. unwrap_unchecked ( ) }
153
+ }
154
+ }
155
+
156
+ #[ inline]
157
+ pub ( crate ) const fn is_utf8_char_boundary ( b : u8 ) -> bool {
158
+ // This is bit magic equivalent to: b < 128 || b >= 192
159
+ ( b as i8 ) >= -0x40
160
+ }
161
+
162
+ // Truncates a utf-8 string to the nearest character boundary,
163
+ // not exceeding max_bytes
164
+ fn truncate ( s : & str , max_bytes : usize ) -> & str {
165
+ let closest = floor_char_boundary ( s, max_bytes) ;
166
+
167
+ & s[ ..closest]
168
+ }
169
+
170
+ mod tests {
171
+ use super :: * ;
172
+
173
+ #[ test]
174
+ fn truncation ( ) {
175
+ let s = "Hello, world!" ;
36
176
37
- // TODO: adjust data lengths here
177
+ assert_eq ! ( truncate ( s , 5 ) , "Hello" ) ;
38
178
39
- let post_upgrade_root_hash = storage. commit_in_place ( delta) . await ?;
40
- tracing:: info!( ?post_upgrade_root_hash, "post-migration root hash" ) ;
179
+ let s = "❤️🧡💛💚💙💜" ;
180
+ assert_eq ! ( s. len( ) , 26 ) ;
181
+ assert_eq ! ( "❤" . len( ) , 3 ) ;
41
182
42
- Ok ( start_time. elapsed ( ) . expect ( "start time not set" ) )
183
+ assert_eq ! ( truncate( s, 2 ) , "" ) ;
184
+ assert_eq ! ( truncate( s, 3 ) , "❤" ) ;
185
+ assert_eq ! ( truncate( s, 4 ) , "❤" ) ;
186
+ }
43
187
}
0 commit comments