Line data Source code
1 : #include "fd_eqvoc.h"
2 : #include "../../ballet/shred/fd_shred.h"
3 :
4 : void *
5 0 : fd_eqvoc_new( void * shmem, ulong fec_max, ulong proof_max, ulong seed ) {
6 :
7 0 : if( FD_UNLIKELY( !shmem ) ) {
8 0 : FD_LOG_WARNING(( "NULL mem" ));
9 0 : return NULL;
10 0 : }
11 :
12 0 : if( FD_UNLIKELY( !fd_ulong_is_aligned( (ulong)shmem, fd_eqvoc_align() ) ) ) {
13 0 : FD_LOG_WARNING(( "misaligned mem" ));
14 0 : return NULL;
15 0 : }
16 :
17 0 : FD_SCRATCH_ALLOC_INIT( l, shmem );
18 0 : fd_eqvoc_t * eqvoc = FD_SCRATCH_ALLOC_APPEND( l, alignof(fd_eqvoc_t), sizeof(fd_eqvoc_t) );
19 0 : void * fec_pool = FD_SCRATCH_ALLOC_APPEND( l, fd_eqvoc_fec_pool_align(), fd_eqvoc_fec_pool_footprint( fec_max ) );
20 0 : void * fec_map = FD_SCRATCH_ALLOC_APPEND( l, fd_eqvoc_fec_map_align(), fd_eqvoc_fec_map_footprint( fec_max ) );
21 0 : void * proof_pool = FD_SCRATCH_ALLOC_APPEND( l, fd_eqvoc_proof_pool_align(), fd_eqvoc_proof_pool_footprint( proof_max ) );
22 0 : void * proof_map = FD_SCRATCH_ALLOC_APPEND( l, fd_eqvoc_proof_map_align(), fd_eqvoc_proof_map_footprint( proof_max ) );
23 0 : void * sha512 = FD_SCRATCH_ALLOC_APPEND( l, fd_sha512_align(), fd_sha512_footprint() );
24 0 : void * bmtree_mem = FD_SCRATCH_ALLOC_APPEND( l, fd_bmtree_commit_align(), fd_bmtree_commit_footprint( FD_SHRED_MERKLE_LAYER_CNT ) );
25 0 : FD_SCRATCH_ALLOC_FINI( l, fd_eqvoc_align() );
26 :
27 0 : eqvoc->fec_max = fec_max;
28 0 : eqvoc->proof_max = proof_max;
29 0 : eqvoc->shred_version = 0;
30 0 : fd_eqvoc_fec_pool_new( fec_pool, fec_max );
31 0 : fd_eqvoc_fec_map_new( fec_map, fec_max, seed );
32 0 : fd_eqvoc_proof_pool_new( proof_pool, proof_max );
33 0 : fd_eqvoc_proof_map_new( proof_map, proof_max, seed );
34 0 : fd_sha512_new( sha512 );
35 0 : (void)bmtree_mem; /* does not require new */
36 :
37 0 : return shmem;
38 0 : }
39 :
40 : fd_eqvoc_t *
41 0 : fd_eqvoc_join( void * sheqvoc ) {
42 :
43 0 : if( FD_UNLIKELY( !sheqvoc ) ) {
44 0 : FD_LOG_WARNING(( "NULL eqvoc" ));
45 0 : return NULL;
46 0 : }
47 :
48 0 : if( FD_UNLIKELY( !fd_ulong_is_aligned( (ulong)sheqvoc, fd_eqvoc_align() ) ) ) {
49 0 : FD_LOG_WARNING(( "misaligned eqvoc" ));
50 0 : return NULL;
51 0 : }
52 :
53 0 : FD_SCRATCH_ALLOC_INIT( l, sheqvoc );
54 0 : fd_eqvoc_t * eqvoc = FD_SCRATCH_ALLOC_APPEND( l, alignof(fd_eqvoc_t), sizeof(fd_eqvoc_t) );
55 0 : void * fec_pool = FD_SCRATCH_ALLOC_APPEND( l, fd_eqvoc_fec_pool_align(), fd_eqvoc_fec_pool_footprint( eqvoc->fec_max ) );
56 0 : void * fec_map = FD_SCRATCH_ALLOC_APPEND( l, fd_eqvoc_fec_map_align(), fd_eqvoc_fec_map_footprint( eqvoc->fec_max ) );
57 0 : void * proof_pool = FD_SCRATCH_ALLOC_APPEND( l, fd_eqvoc_proof_pool_align(), fd_eqvoc_proof_pool_footprint( eqvoc->proof_max ) );
58 0 : void * proof_map = FD_SCRATCH_ALLOC_APPEND( l, fd_eqvoc_proof_map_align(), fd_eqvoc_proof_map_footprint( eqvoc->proof_max ) );
59 0 : void * sha512 = FD_SCRATCH_ALLOC_APPEND( l, fd_sha512_align(), fd_sha512_footprint() );
60 0 : void * bmtree_mem = FD_SCRATCH_ALLOC_APPEND( l, fd_bmtree_commit_align(), fd_bmtree_commit_footprint( FD_SHRED_MERKLE_LAYER_CNT ) );
61 0 : FD_SCRATCH_ALLOC_FINI( l, fd_eqvoc_align() );
62 :
63 0 : eqvoc->fec_pool = fd_eqvoc_fec_pool_join( fec_pool );
64 0 : eqvoc->fec_map = fd_eqvoc_fec_map_join( fec_map );
65 0 : eqvoc->proof_pool = fd_eqvoc_proof_pool_join( proof_pool );
66 0 : eqvoc->proof_map = fd_eqvoc_proof_map_join( proof_map );
67 0 : eqvoc->sha512 = fd_sha512_join( sha512 );
68 0 : eqvoc->bmtree_mem = bmtree_mem; /* does not require join */
69 :
70 0 : return (fd_eqvoc_t *)sheqvoc;
71 0 : }
72 :
73 : void *
74 0 : fd_eqvoc_leave( fd_eqvoc_t const * eqvoc ) {
75 :
76 0 : if( FD_UNLIKELY( !eqvoc ) ) {
77 0 : FD_LOG_WARNING(( "NULL eqvoc" ));
78 0 : return NULL;
79 0 : }
80 :
81 0 : return (void *)eqvoc;
82 0 : }
83 :
84 : void *
85 0 : fd_eqvoc_delete( void * eqvoc ) {
86 :
87 0 : if( FD_UNLIKELY( !eqvoc ) ) {
88 0 : FD_LOG_WARNING(( "NULL eqvoc" ));
89 0 : return NULL;
90 0 : }
91 :
92 0 : if( FD_UNLIKELY( !fd_ulong_is_aligned( (ulong)eqvoc, fd_eqvoc_align() ) ) ) {
93 0 : FD_LOG_WARNING(( "misaligned eqvoc" ));
94 0 : return NULL;
95 0 : }
96 :
97 0 : return eqvoc;
98 0 : }
99 :
100 : void
101 0 : fd_eqvoc_init( fd_eqvoc_t * eqvoc, ulong shred_version ) {
102 0 : eqvoc->shred_version = shred_version;
103 0 : }
104 :
105 : fd_eqvoc_fec_t *
106 0 : fd_eqvoc_fec_insert( fd_eqvoc_t * eqvoc, ulong slot, uint fec_set_idx ) {
107 0 : fd_slot_fec_t key = { slot, fec_set_idx };
108 :
109 0 : #if FD_EQVOC_USE_HANDHOLDING
110 0 : if( FD_UNLIKELY( fd_eqvoc_fec_map_ele_query( eqvoc->fec_map, &key, NULL, eqvoc->fec_pool ) ) ) FD_LOG_ERR(( "[%s] key (%lu, %u) already in map.", __func__, slot, fec_set_idx ));
111 0 : #endif
112 :
113 : /* FIXME eviction */
114 :
115 0 : if( FD_UNLIKELY( !fd_eqvoc_fec_pool_free( eqvoc->fec_pool ) ) ) FD_LOG_ERR(( "[%s] map full.", __func__ ));
116 :
117 0 : fd_eqvoc_fec_t * fec = fd_eqvoc_fec_pool_ele_acquire( eqvoc->fec_pool );
118 0 : fec->key.slot = slot;
119 0 : fec->key.fec_set_idx = fec_set_idx;
120 0 : fec->code_cnt = 0;
121 0 : fec->data_cnt = 0;
122 0 : fec->last_idx = FD_SHRED_IDX_NULL;
123 0 : fd_eqvoc_fec_map_ele_insert( eqvoc->fec_map, fec, eqvoc->fec_pool);
124 0 : return fec;
125 0 : }
126 :
127 : fd_eqvoc_fec_t const *
128 0 : fd_eqvoc_fec_search( fd_eqvoc_t const * eqvoc, fd_shred_t const * shred ) {
129 0 : fd_eqvoc_fec_t const * entry = fd_eqvoc_fec_query( eqvoc, shred->slot, shred->fec_set_idx );
130 :
131 : /* If we've already seen a shred in this FEC set */
132 :
133 0 : if( FD_LIKELY( entry ) ) {
134 :
135 : /* Make sure the signature matches. Every merkle shred in the FEC
136 : set must have the same signature. */
137 :
138 0 : if( FD_UNLIKELY( 0 != memcmp( entry->sig, shred->signature, FD_ED25519_SIG_SZ ) ) ) {
139 0 : return entry;
140 0 : }
141 :
142 : /* Check if this shred's idx is higher than another shred that claimed
143 : to be the last_idx. This indicates equivocation. */
144 :
145 0 : if( FD_UNLIKELY( shred->idx > entry->last_idx ) ) {
146 0 : return entry;
147 0 : }
148 0 : }
149 :
150 : /* Look backward FEC_MAX idxs for overlap. */
151 :
152 0 : for( uint i = 1; shred->fec_set_idx >= i && i < FD_EQVOC_FEC_MAX; i++ ) {
153 0 : fd_eqvoc_fec_t const * conflict = fd_eqvoc_fec_query( eqvoc, shred->slot, shred->fec_set_idx - i );
154 0 : if( FD_UNLIKELY( conflict &&
155 0 : conflict->data_cnt > 0 &&
156 0 : conflict->key.fec_set_idx + conflict->data_cnt > shred->fec_set_idx ) ) {
157 0 : return conflict;
158 0 : }
159 0 : }
160 :
161 : /* Look forward data_cnt idxs for overlap. */
162 :
163 0 : for( uint i = 1; entry && i < entry->data_cnt; i++ ) {
164 0 : fd_eqvoc_fec_t const * conflict = fd_eqvoc_fec_query( eqvoc, shred->slot, shred->fec_set_idx + i );
165 0 : if( FD_UNLIKELY( conflict ) ) return conflict;
166 0 : }
167 :
168 0 : return NULL; /* No conflicts */
169 0 : }
170 :
171 : fd_eqvoc_proof_t *
172 0 : fd_eqvoc_proof_insert( fd_eqvoc_t * eqvoc, ulong slot, fd_pubkey_t const * from ) {
173 0 : fd_slot_pubkey_t key = { slot, *from };
174 :
175 0 : #if FD_EQVOC_USE_HANDHOLDING
176 0 : if( FD_UNLIKELY( fd_eqvoc_proof_map_ele_query( eqvoc->proof_map, &key, NULL, eqvoc->proof_pool ) ) ) {
177 0 : FD_BASE58_ENCODE_32_BYTES( from->key, from_b58 );
178 0 : FD_LOG_ERR(( "[%s] key (%lu, %s) already in map.", __func__, slot, from_b58 ));
179 0 : }
180 0 : #endif
181 :
182 : /* FIXME eviction */
183 :
184 0 : fd_eqvoc_proof_t * proof = fd_eqvoc_proof_pool_ele_acquire( eqvoc->proof_pool );
185 0 : memset( proof, 0, sizeof(fd_eqvoc_proof_t) );
186 0 : proof->key.slot = slot;
187 0 : proof->key.hash = *from;
188 0 : fd_eqvoc_proof_map_ele_insert( eqvoc->proof_map, proof, eqvoc->proof_pool );
189 0 : return proof;
190 0 : }
191 :
192 : void
193 0 : fd_eqvoc_proof_chunk_insert( fd_eqvoc_proof_t * proof, fd_gossip_duplicate_shred_t const * chunk ) {
194 0 : if( FD_UNLIKELY( chunk->wallclock > proof->wallclock ) ) {
195 0 : FD_BASE58_ENCODE_32_BYTES( proof->key.hash.key, hash_b58 );
196 0 : FD_LOG_WARNING(( "[%s] received newer chunk (slot: %lu from: %s). overwriting.", __func__, proof->key.slot, hash_b58 ));
197 0 : proof->wallclock = chunk->wallclock;
198 0 : proof->chunk_cnt = chunk->num_chunks;
199 0 : memset( proof->set, 0, 4 * sizeof(ulong) );
200 : // fd_eqvoc_proof_set_null( proof->set );
201 0 : }
202 :
203 0 : if ( FD_UNLIKELY( chunk->wallclock < proof->wallclock ) ) {
204 0 : FD_BASE58_ENCODE_32_BYTES( proof->key.hash.key, hash_b58 );
205 0 : FD_LOG_WARNING(( "[%s] received older chunk (slot: %lu from: %s). ignoring.", __func__, proof->key.slot, hash_b58 ));
206 0 : return;
207 0 : }
208 :
209 0 : if( FD_UNLIKELY( proof->chunk_cnt != chunk->num_chunks ) ) {
210 0 : FD_BASE58_ENCODE_32_BYTES( proof->key.hash.key, hash_b58 );
211 0 : FD_LOG_WARNING(( "[%s] received incompatible chunk (slot: %lu from: %s). ignoring.", __func__, proof->key.slot, hash_b58 ));
212 0 : return;
213 0 : }
214 :
215 :
216 0 : if( FD_UNLIKELY( fd_eqvoc_proof_set_test( proof->set, chunk->chunk_index ) ) ) {
217 0 : FD_BASE58_ENCODE_32_BYTES( proof->key.hash.key, hash_b58 );
218 0 : FD_LOG_WARNING(( "[%s] already received chunk %u. slot: %lu from: %s. ignoring.", __func__, chunk->chunk_index, proof->key.slot, hash_b58 ));
219 0 : return;
220 0 : }
221 :
222 0 : fd_memcpy( &proof->shreds[proof->chunk_sz * chunk->chunk_index], chunk->chunk, chunk->chunk_len );
223 0 : fd_eqvoc_proof_set_insert( proof->set, chunk->chunk_index );
224 0 : }
225 :
226 : /* fd_eqvoc_proof_init initializes a new proof entry. */
227 :
228 : void
229 0 : fd_eqvoc_proof_init( fd_eqvoc_proof_t * proof, fd_pubkey_t const * producer, long wallclock, ulong chunk_cnt, ulong chunk_sz, void * bmtree_mem ) {
230 0 : proof->producer = *producer;
231 0 : proof->bmtree_mem = bmtree_mem;
232 0 : proof->wallclock = wallclock;
233 0 : proof->chunk_cnt = chunk_cnt;
234 0 : proof->chunk_sz = chunk_sz;
235 0 : memset( proof->set, 0, 4 * sizeof(ulong) );
236 0 : memset( proof->shreds, 0, 2472 );
237 0 : }
238 :
239 :
240 : void
241 0 : fd_eqvoc_proof_remove( fd_eqvoc_t * eqvoc, fd_slot_pubkey_t const * key ) {
242 0 : fd_eqvoc_proof_t * proof = fd_eqvoc_proof_map_ele_remove( eqvoc->proof_map, key, NULL, eqvoc->proof_pool );
243 0 : if( FD_UNLIKELY( !proof ) ) {
244 0 : FD_BASE58_ENCODE_32_BYTES( key->hash.key, hash_b58 );
245 0 : FD_LOG_WARNING(( "[%s] key (%lu, %s) not in map.", __func__, key->slot, hash_b58 ));
246 0 : return;
247 0 : }
248 0 : fd_eqvoc_proof_pool_ele_release( eqvoc->proof_pool, proof );
249 0 : }
250 :
251 : int
252 0 : fd_eqvoc_proof_verify( fd_eqvoc_proof_t const * proof ) {
253 0 : return fd_eqvoc_shreds_verify( fd_eqvoc_proof_shred1_const( proof ), fd_eqvoc_proof_shred2_const( proof ), &proof->producer, proof->bmtree_mem );
254 0 : }
255 :
256 : int
257 0 : fd_eqvoc_shreds_verify( fd_shred_t const * shred1, fd_shred_t const * shred2, fd_pubkey_t const * producer, void * bmtree_mem ) {
258 0 : if( FD_UNLIKELY( shred1->slot != shred2->slot ) ) {
259 0 : return FD_EQVOC_PROOF_VERIFY_ERR_SLOT;
260 0 : }
261 :
262 0 : if( FD_UNLIKELY( shred1->version != shred2->version ) ) {
263 0 : return FD_EQVOC_PROOF_VERIFY_ERR_VERSION;
264 0 : }
265 :
266 0 : if( FD_UNLIKELY( !fd_shred_is_chained ( fd_shred_type( shred1->variant) ) &&
267 0 : !fd_shred_is_resigned( fd_shred_type( shred2->variant ) ) ) ) {
268 0 : return FD_EQVOC_PROOF_VERIFY_ERR_TYPE;
269 0 : }
270 :
271 : /* Check both shreds contain valid signatures from the assigned leader
272 : to that slot. This requires deriving the merkle root and
273 : sig-verifying it, because the leader signs the merkle root for
274 : merkle shreds.
275 :
276 : TODO remove? */
277 :
278 0 : fd_bmtree_node_t root1 = { 0 };
279 0 : if( FD_UNLIKELY( !fd_shred_merkle_root( shred1, bmtree_mem, &root1 ) ) ) {
280 0 : return FD_EQVOC_PROOF_VERIFY_ERR_MERKLE;
281 0 : }
282 0 : fd_bmtree_node_t root2;
283 0 : if( FD_UNLIKELY( !fd_shred_merkle_root( shred2, bmtree_mem, &root2 ) ) ) {
284 0 : return FD_EQVOC_PROOF_VERIFY_ERR_MERKLE;
285 0 : }
286 0 : fd_sha512_t _sha512[1];
287 0 : fd_sha512_t * sha512 = fd_sha512_join( fd_sha512_new( _sha512 ) );
288 0 : if( FD_UNLIKELY( FD_ED25519_SUCCESS != fd_ed25519_verify( root1.hash,
289 0 : 32UL,
290 0 : shred1->signature,
291 0 : producer->uc,
292 0 : sha512 ) ||
293 0 : FD_ED25519_SUCCESS != fd_ed25519_verify( root2.hash,
294 0 : 32UL,
295 0 : shred2->signature,
296 0 : producer->uc,
297 0 : sha512 ) ) ) {
298 0 : return FD_EQVOC_PROOF_VERIFY_ERR_SIGNATURE;
299 0 : }
300 :
301 : /* Same FEC set index checks */
302 :
303 0 : if( FD_LIKELY( shred1->fec_set_idx == shred2->fec_set_idx ) ) {
304 :
305 : /* Test if two shreds have different signatures when they are in the
306 : same FEC set. */
307 :
308 0 : if( FD_LIKELY( 0 != memcmp( shred1->signature, shred2->signature, FD_ED25519_SIG_SZ ) ) ) {
309 0 : return FD_EQVOC_PROOF_VERIFY_SUCCESS_SIGNATURE;
310 0 : }
311 :
312 : /* Test if the shreds have different coding metadata when they're
313 : both coding shreds in the same FEC set. */
314 :
315 0 : if( FD_UNLIKELY( fd_shred_is_code( fd_shred_type( shred1->variant ) ) &&
316 0 : fd_shred_is_code( fd_shred_type( shred2->variant ) ) &&
317 0 : ( shred1->code.code_cnt != shred2->code.code_cnt ||
318 0 : shred1->code.data_cnt != shred2->code.data_cnt ||
319 0 : shred1->idx - shred1->code.idx == shred2->idx - shred2->code.idx ) ) ) {
320 0 : return FD_EQVOC_PROOF_VERIFY_SUCCESS_META;
321 0 : }
322 :
323 : /* Test if one shred is marked the last shred in the slot, but the
324 : other shred has a higher index when both shreds are data
325 : shreds. */
326 :
327 0 : if( FD_UNLIKELY( fd_shred_is_data( fd_shred_type( shred1->variant ) ) &&
328 0 : fd_shred_is_data( fd_shred_type( shred2->variant ) ) &&
329 0 : ( ( shred1->data.flags & FD_SHRED_DATA_FLAG_SLOT_COMPLETE && shred2->idx > shred1->idx ) ||
330 0 : ( shred2->data.flags & FD_SHRED_DATA_FLAG_SLOT_COMPLETE && shred1->idx > shred2->idx ) ) ) ) {
331 0 : return FD_EQVOC_PROOF_VERIFY_SUCCESS_LAST;
332 0 : }
333 0 : }
334 :
335 : /* Different FEC set index checks. Lower FEC set index shred must be a
336 : coding shred. */
337 :
338 0 : fd_shred_t const * lo = fd_ptr_if( shred1->fec_set_idx < shred2->fec_set_idx, shred1, shred2 );
339 0 : fd_shred_t const * hi = fd_ptr_if( shred1->fec_set_idx > shred2->fec_set_idx, shred1, shred2 );
340 :
341 0 : if ( FD_UNLIKELY( fd_shred_is_code( fd_shred_type( lo->variant ) ) ) ) {
342 :
343 : /* Test for overlap. The FEC sets overlap if the lower fec_set_idx +
344 : data_cnt > higher fec_set_idx. We must have received at least one
345 : coding shred in the FEC set with the lower fec_set_idx to perform
346 : this check. */
347 :
348 0 : if( FD_UNLIKELY( lo->fec_set_idx + lo->code.data_cnt > hi->fec_set_idx ) ) {
349 0 : return FD_EQVOC_PROOF_VERIFY_SUCCESS_OVERLAP;
350 0 : }
351 :
352 : /* Test for conflicting chained merkle roots when shred1 and shred2
353 : are in adjacent FEC sets. We know the FEC sets are adjacent if the
354 : last data shred index in the lower FEC set is one less than the
355 : first data shred index in the higher FEC set. */
356 :
357 0 : if( FD_UNLIKELY( lo->fec_set_idx + lo->code.data_cnt == hi->fec_set_idx ) ) {
358 0 : uchar * merkle_hash = fd_ptr_if( shred1->fec_set_idx < shred2->fec_set_idx,
359 0 : (uchar *)shred1 + fd_shred_merkle_off( shred1 ),
360 0 : (uchar *)shred2 + fd_shred_merkle_off( shred2 ) );
361 0 : uchar * chained_hash = fd_ptr_if( shred1->fec_set_idx > shred2->fec_set_idx,
362 0 : (uchar *)shred1 + fd_shred_chain_off( shred1->variant ),
363 0 : (uchar *)shred2 + fd_shred_chain_off( shred2->variant ) );
364 0 : if ( FD_LIKELY( 0 != memcmp( merkle_hash, chained_hash, FD_SHRED_MERKLE_ROOT_SZ ) ) ) {
365 0 : return FD_EQVOC_PROOF_VERIFY_SUCCESS_CHAINED;
366 0 : };
367 0 : }
368 0 : }
369 :
370 : /* None of the equivocation tests passed, so this equivocation proof
371 : failed to verify. */
372 :
373 0 : return FD_EQVOC_PROOF_VERIFY_FAILURE;
374 0 : }
375 :
376 : void
377 : fd_eqvoc_proof_from_chunks( fd_gossip_duplicate_shred_t const * chunks,
378 0 : fd_eqvoc_proof_t * proof_out ) {
379 0 : ulong chunk_cnt = chunks[0].num_chunks;
380 0 : for ( ulong i = 0; i < chunk_cnt; i++ ) {
381 0 : fd_eqvoc_proof_chunk_insert( proof_out, chunks + i );
382 0 : }
383 0 : }
384 :
385 : void
386 0 : fd_eqvoc_proof_to_chunks( fd_eqvoc_proof_t * proof, fd_gossip_duplicate_shred_t * chunks_out ) {
387 0 : for (uchar i = 0; i < FD_EQVOC_PROOF_CHUNK_CNT; i++ ) {
388 0 : fd_gossip_duplicate_shred_t * chunk = &chunks_out[i];
389 0 : chunk->index = i;
390 0 : chunk->wallclock = fd_log_wallclock();
391 0 : chunk->slot = proof->key.slot;
392 0 : chunk->num_chunks = FD_EQVOC_PROOF_CHUNK_CNT;
393 0 : chunk->chunk_len = FD_EQVOC_PROOF_CHUNK_SZ;
394 0 : ulong off = i * FD_EQVOC_PROOF_CHUNK_SZ;
395 0 : ulong sz = fd_ulong_min( FD_EQVOC_PROOF_CHUNK_SZ, FD_EQVOC_PROOF_SZ - off );
396 0 : fd_memcpy( chunks_out[i].chunk, proof->shreds + off, sz );
397 0 : }
398 0 : }
|