-
Notifications
You must be signed in to change notification settings - Fork 1k
Expand file tree
/
Copy pathvp_testnet_faucet.rs
More file actions
466 lines (410 loc) · 18.1 KB
/
Copy pathvp_testnet_faucet.rs
File metadata and controls
466 lines (410 loc) · 18.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
//! A "faucet" account for testnet.
//!
//! This VP allows anyone to withdraw up to
//! [`testnet_pow::read_withdrawal_limit`] tokens without the faucet's
//! signature, but with a valid PoW challenge solution that cannot be replayed.
//!
//! Any other storage key changes are allowed only with a valid signature.
use namada_vp_prelude::*;
use once_cell::unsync::Lazy;
#[validity_predicate]
fn validate_tx(
ctx: &Ctx,
tx_data: Tx,
addr: Address,
keys_changed: BTreeSet<storage::Key>,
verifiers: BTreeSet<Address>,
) -> VpResult {
debug_log!(
"vp_testnet_faucet called with user addr: {}, key_changed: {:?}, \
verifiers: {:?}",
addr,
keys_changed,
verifiers
);
let valid_sig = Lazy::new(|| {
let pk = key::get(ctx, &addr);
match pk {
Ok(Some(pk)) => tx_data
.verify_signature(
&pk,
&[*tx_data.data_sechash(), *tx_data.code_sechash()],
)
.is_ok(),
_ => false,
}
});
if !is_valid_tx(ctx, &tx_data)? {
return reject();
}
for key in keys_changed.iter() {
let is_valid = if let Some([token, owner]) =
token::is_any_token_balance_key(key)
{
if owner == &addr {
let pre: token::Amount = ctx.read_pre(key)?.unwrap_or_default();
let post: token::Amount =
ctx.read_post(key)?.unwrap_or_default();
let change = post.change() - pre.change();
let maybe_denom =
storage_api::token::read_denom(&ctx.pre(), token, None)?;
if maybe_denom.is_none() {
debug_log!(
"A denomination for token address {} does not exist \
in storage",
token,
);
return reject();
}
let denom = maybe_denom.unwrap();
if !change.non_negative() {
// Allow to withdraw without a sig if there's a valid PoW
if ctx.has_valid_pow() {
let max_free_debit =
testnet_pow::read_withdrawal_limit(
&ctx.pre(),
&addr,
)?;
token::Amount::from_uint(change.abs(), 0).unwrap()
<= token::Amount::from_uint(max_free_debit, denom)
.unwrap()
} else {
debug_log!("No PoW solution, a signature is required");
// Debit without a solution has to signed
*valid_sig
}
} else {
// credit is permissive
true
}
} else {
// balance changes of other accounts
true
}
} else if let Some(owner) = key.is_validity_predicate() {
let has_post: bool = ctx.has_key_post(key)?;
if owner == &addr {
if has_post {
let vp_hash: Vec<u8> = ctx.read_bytes_post(key)?.unwrap();
return Ok(*valid_sig && is_vp_whitelisted(ctx, &vp_hash)?);
} else {
return reject();
}
} else {
let vp_hash: Vec<u8> = ctx.read_bytes_post(key)?.unwrap();
return is_vp_whitelisted(ctx, &vp_hash);
}
} else {
// Allow any other key change if authorized by a signature
*valid_sig
};
if !is_valid {
debug_log!("key {} modification failed vp", key);
return reject();
}
}
accept()
}
#[cfg(test)]
mod tests {
use address::testing::arb_non_internal_address;
use namada::proto::{Code, Data, Signature};
use namada::types::transaction::TxType;
use namada_test_utils::TestWasms;
// Use this as `#[test]` annotation to enable logging
use namada_tests::log::test;
use namada_tests::tx::{self, tx_host_env, TestTxEnv};
use namada_tests::vp::vp_host_env::storage::Key;
use namada_tests::vp::*;
use namada_tx_prelude::{StorageWrite, TxEnv};
use namada_vp_prelude::key::RefTo;
use proptest::prelude::*;
use storage::testing::arb_account_storage_key_no_vp;
use super::*;
/// Allows anyone to withdraw up to 1_000 tokens in a single tx
pub const MAX_FREE_DEBIT: i128 = 1_000_000_000; // in micro units
/// Test that no-op transaction (i.e. no storage modifications) accepted.
#[test]
fn test_no_op_transaction() {
let mut tx_data = Tx::new(TxType::Raw);
tx_data.set_data(Data::new(vec![]));
let addr: Address = address::testing::established_address_1();
let keys_changed: BTreeSet<storage::Key> = BTreeSet::default();
let verifiers: BTreeSet<Address> = BTreeSet::default();
// The VP env must be initialized before calling `validate_tx`
vp_host_env::init();
assert!(
validate_tx(&CTX, tx_data, addr, keys_changed, verifiers).unwrap()
);
}
/// Test that a credit transfer is accepted.
#[test]
fn test_credit_transfer_accepted() {
// Initialize a tx environment
let mut tx_env = TestTxEnv::default();
let vp_owner = address::testing::established_address_1();
let source = address::testing::established_address_2();
let token = address::nam();
let amount = token::Amount::from_uint(10_098_123, 0).unwrap();
// Spawn the accounts to be able to modify their storage
tx_env.spawn_accounts([&vp_owner, &source, &token]);
// Credit the tokens to the source before running the transaction to be
// able to transfer from it
tx_env.credit_tokens(&source, &token, None, amount);
let amount = token::DenominatedAmount {
amount,
denom: token::NATIVE_MAX_DECIMAL_PLACES.into(),
};
// Initialize VP environment from a transaction
vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| {
// Apply transfer in a transaction
tx_host_env::token::transfer(
tx_host_env::ctx(),
&source,
address,
&token,
None,
amount,
&None,
&None,
&None,
)
.unwrap();
});
let vp_env = vp_host_env::take();
let mut tx_data = Tx::new(TxType::Raw);
tx_data.set_data(Data::new(vec![]));
let keys_changed: BTreeSet<storage::Key> =
vp_env.all_touched_storage_keys();
let verifiers: BTreeSet<Address> = BTreeSet::default();
vp_host_env::set(vp_env);
assert!(
validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers)
.unwrap()
);
}
/// Test that a validity predicate update without a valid signature is
/// rejected.
#[test]
fn test_unsigned_vp_update_rejected() {
// Initialize a tx environment
let mut tx_env = TestTxEnv::default();
let vp_owner = address::testing::established_address_1();
let vp_code = TestWasms::VpAlwaysTrue.read_bytes();
let vp_hash = sha256(&vp_code);
// for the update
tx_env.store_wasm_code(vp_code);
// Spawn the accounts to be able to modify their storage
tx_env.spawn_accounts([&vp_owner]);
// Initialize VP environment from a transaction
vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| {
// Update VP in a transaction
tx::ctx()
.update_validity_predicate(address, vp_hash)
.unwrap();
});
let vp_env = vp_host_env::take();
let mut tx_data = Tx::new(TxType::Raw);
tx_data.set_data(Data::new(vec![]));
let keys_changed: BTreeSet<storage::Key> =
vp_env.all_touched_storage_keys();
let verifiers: BTreeSet<Address> = BTreeSet::default();
vp_host_env::set(vp_env);
assert!(
!validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers)
.unwrap()
);
}
/// Test that a validity predicate update with a valid signature is
/// accepted.
#[test]
fn test_signed_vp_update_accepted() {
// Initialize a tx environment
let mut tx_env = TestTxEnv::default();
let vp_owner = address::testing::established_address_1();
let keypair = key::testing::keypair_1();
let public_key = &keypair.ref_to();
let vp_code = TestWasms::VpAlwaysTrue.read_bytes();
let vp_hash = sha256(&vp_code);
// for the update
tx_env.store_wasm_code(vp_code);
// Spawn the accounts to be able to modify their storage
tx_env.spawn_accounts([&vp_owner]);
tx_env.write_public_key(&vp_owner, public_key);
// Initialize VP environment from a transaction
vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| {
// Update VP in a transaction
tx::ctx()
.update_validity_predicate(address, vp_hash)
.unwrap();
});
let mut vp_env = vp_host_env::take();
let mut tx = vp_env.tx.clone();
tx.set_data(Data::new(vec![]));
tx.set_code(Code::new(vec![]));
tx.add_section(Section::Signature(Signature::new(
vec![*tx.data_sechash(), *tx.code_sechash()],
&keypair,
)));
let signed_tx = tx.clone();
vp_env.tx = signed_tx.clone();
let keys_changed: BTreeSet<storage::Key> =
vp_env.all_touched_storage_keys();
let verifiers: BTreeSet<Address> = BTreeSet::default();
vp_host_env::set(vp_env);
assert!(
validate_tx(&CTX, signed_tx, vp_owner, keys_changed, verifiers)
.unwrap()
);
}
prop_compose! {
/// Generates an account address and a storage key inside its storage.
fn arb_account_storage_subspace_key()
// Generate an address
(address in arb_non_internal_address())
// Generate a storage key other than its VP key (VP cannot be
// modified directly via `write`, it has to be modified via
// `tx::update_validity_predicate`.
(storage_key in arb_account_storage_key_no_vp(address.clone()),
// Use the generated address too
address in Just(address))
-> (Address, Key) {
(address, storage_key)
}
}
proptest! {
/// Test that a debit of more than [`MAX_FREE_DEBIT`] tokens without a valid signature is rejected.
#[test]
fn test_unsigned_debit_over_limit_rejected(amount in (MAX_FREE_DEBIT as u64 + 1..)) {
// Initialize a tx environment
let mut tx_env = TestTxEnv::default();
// Init the VP
let vp_owner = address::testing::established_address_1();
let difficulty = testnet_pow::Difficulty::try_new(0).unwrap();
let withdrawal_limit = token::Amount::from_uint(MAX_FREE_DEBIT as u64, 0).unwrap();
testnet_pow::init_faucet_storage(&mut tx_env.wl_storage, &vp_owner, difficulty, withdrawal_limit.into()).unwrap();
let target = address::testing::established_address_2();
let token = address::nam();
let amount = token::Amount::from_uint(amount, 0).unwrap();
// Spawn the accounts to be able to modify their storage
tx_env.spawn_accounts([&vp_owner, &target, &token]);
// Credit the tokens to the VP owner before running the transaction to
// be able to transfer from it
tx_env.credit_tokens(&vp_owner, &token, None, amount);
tx_env.commit_genesis();
let amount = token::DenominatedAmount {
amount,
denom: token::NATIVE_MAX_DECIMAL_PLACES.into()
};
// Initialize VP environment from a transaction
vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| {
// Apply transfer in a transaction
tx_host_env::token::transfer(tx::ctx(), address, &target, &token, None, amount, &None, &None, &None).unwrap();
});
let vp_env = vp_host_env::take();
let mut tx_data = Tx::new(TxType::Raw);
tx_data.set_data(Data::new(vec![]));
let keys_changed: BTreeSet<storage::Key> =
vp_env.all_touched_storage_keys();
let verifiers: BTreeSet<Address> = BTreeSet::default();
vp_host_env::set(vp_env);
assert!(!validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers).unwrap());
}
/// Test that a debit of less than or equal to [`MAX_FREE_DEBIT`] tokens
/// without a valid signature but with a valid PoW solution is accepted.
#[test]
fn test_unsigned_debit_under_limit_accepted(amount in (..MAX_FREE_DEBIT as u64 + 1)) {
// Initialize a tx environment
let mut tx_env = TestTxEnv::default();
// Init the VP
let vp_owner = address::testing::established_address_1();
let difficulty = testnet_pow::Difficulty::try_new(0).unwrap();
let withdrawal_limit = token::Amount::from_uint(MAX_FREE_DEBIT as u64, 0).unwrap();
testnet_pow::init_faucet_storage(&mut tx_env.wl_storage, &vp_owner, difficulty, withdrawal_limit.into()).unwrap();
let target = address::testing::established_address_2();
let target_key = key::testing::keypair_1();
let token = address::nam();
let amount = token::Amount::from_uint(amount, 0).unwrap();
// Spawn the accounts to be able to modify their storage
tx_env.spawn_accounts([&vp_owner, &target, &token]);
// Credit the tokens to the VP owner before running the transaction to
// be able to transfer from it
tx_env.credit_tokens(&vp_owner, &token, None, amount);
// write the denomination of NAM into storage
storage_api::token::write_denom(&mut tx_env.wl_storage, &token, None, token::NATIVE_MAX_DECIMAL_PLACES.into()).unwrap();
tx_env.commit_genesis();
// Construct a PoW solution like a client would
let challenge = testnet_pow::Challenge::new(&mut tx_env.wl_storage, &vp_owner, target.clone()).unwrap();
let solution = challenge.solve();
let solution_bytes = solution.try_to_vec().unwrap();
let amount = token::DenominatedAmount {
amount,
denom: token::NATIVE_MAX_DECIMAL_PLACES.into(),
};
// Initialize VP environment from a transaction
vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| {
// Don't call `Solution::invalidate_if_valid` - this is done by the
// shell's finalize_block.
let valid = solution.validate(tx::ctx(), address, target.clone()).unwrap();
assert!(valid);
// Apply transfer in a transaction
tx_host_env::token::transfer(tx::ctx(), address, &target, &token, None, amount, &None, &None, &None).unwrap();
});
let mut vp_env = vp_host_env::take();
// This is set by the protocol when the wrapper tx has a valid PoW
vp_env.has_valid_pow = true;
let mut tx_data = Tx::new(TxType::Raw);
tx_data.set_data(Data::new(solution_bytes));
tx_data.set_code(Code::new(vec![]));
tx_data.add_section(Section::Signature(Signature::new(vec![*tx_data.data_sechash(), *tx_data.code_sechash()], &target_key)));
let keys_changed: BTreeSet<storage::Key> =
vp_env.all_touched_storage_keys();
let verifiers: BTreeSet<Address> = BTreeSet::default();
vp_host_env::set(vp_env);
assert!(validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers).unwrap());
}
/// Test that a signed tx that performs arbitrary storage writes or
/// deletes to the account is accepted.
#[test]
fn test_signed_arb_storage_write(
(vp_owner, storage_key) in arb_account_storage_subspace_key(),
// Generate bytes to write. If `None`, delete from the key instead
storage_value in any::<Option<Vec<u8>>>(),
) {
// Initialize a tx environment
let mut tx_env = TestTxEnv::default();
// Init the VP
let difficulty = testnet_pow::Difficulty::try_new(0).unwrap();
let withdrawal_limit = token::Amount::from_uint(MAX_FREE_DEBIT as u64, 0).unwrap();
testnet_pow::init_faucet_storage(&mut tx_env.wl_storage, &vp_owner, difficulty, withdrawal_limit.into()).unwrap();
let keypair = key::testing::keypair_1();
let public_key = &keypair.ref_to();
// Spawn all the accounts in the storage key to be able to modify
// their storage
let storage_key_addresses = storage_key.find_addresses();
tx_env.spawn_accounts(storage_key_addresses);
tx_env.write_public_key(&vp_owner, public_key);
// Initialize VP environment from a transaction
vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| {
// Write or delete some data in the transaction
if let Some(value) = &storage_value {
tx::ctx().write(&storage_key, value).unwrap();
} else {
tx::ctx().delete(&storage_key).unwrap();
}
});
let mut vp_env = vp_host_env::take();
let mut tx = vp_env.tx.clone();
tx.set_data(Data::new(vec![]));
tx.set_code(Code::new(vec![]));
tx.add_section(Section::Signature(Signature::new(vec![*tx.data_sechash(), *tx.code_sechash()], &keypair)));
let signed_tx = tx.clone();
vp_env.tx = signed_tx.clone();
let keys_changed: BTreeSet<storage::Key> =
vp_env.all_touched_storage_keys();
let verifiers: BTreeSet<Address> = BTreeSet::default();
vp_host_env::set(vp_env);
assert!(validate_tx(&CTX, signed_tx, vp_owner, keys_changed, verifiers).unwrap());
}
}
}