diff --git a/pallets/creditcoin/src/lib.rs b/pallets/creditcoin/src/lib.rs index 88e608f9a3..2d62793158 100644 --- a/pallets/creditcoin/src/lib.rs +++ b/pallets/creditcoin/src/lib.rs @@ -73,8 +73,11 @@ pub mod pallet { pallet_prelude::*, }; use ocw::errors::VerificationFailureCause; + use sp_runtime::offchain::storage_lock::{BlockAndTime, StorageLock}; + use sp_runtime::offchain::Duration; use sp_runtime::traits::{ - IdentifyAccount, Saturating, UniqueSaturatedFrom, UniqueSaturatedInto, Verify, + IdentifyAccount, SaturatedConversion, Saturating, UniqueSaturatedFrom, UniqueSaturatedInto, + Verify, }; /// Configure the pallet by specifying the parameters and types on which it depends. @@ -608,10 +611,27 @@ pub mod pallet { }; for (deadline, id, task) in PendingTasks::::iter() { - let storage_key = (deadline, &id).encode(); - let status = ocw::LocalVerificationStatus::new(&storage_key); - if status.is_complete() { + let storage_key = crate::ocw::tasks::storage_key(&id); + let offset = + T::UnverifiedTaskTimeout::get().saturated_into::().saturating_sub(2u32); + + let mut lock = StorageLock::>>::with_block_and_time_deadline( + &storage_key, + offset, + Duration::from_millis(0), + ); + + let guard = match lock.try_lock() { + Ok(g) => g, + Err(_) => continue, + }; + + if match &id { + TaskId::VerifyTransfer(id) => Transfers::::contains_key(id), + TaskId::CollectCoins(id) => CollectedCoins::::contains_key(id), + } { log::debug!("Already handled Task ({:?}, {:?})", deadline, id); + guard.forget(); continue; } @@ -623,13 +643,13 @@ pub mod pallet { match Self::offchain_signed_tx(auth_id.clone(), |_| { Call::persist_task_output { deadline, task_output: output.clone() } }) { + Ok(_) => guard.forget(), Err(e) => { log::error!( "Failed to send persist dispatchable transaction: {:?}", e ) }, - Ok(_) => status.mark_complete(), } }, Err((task, ocw::OffchainError::InvalidTask(cause))) => { @@ -644,7 +664,7 @@ pub mod pallet { "Failed to send fail dispatchable transaction: {:?}", e ), - Ok(_) => status.mark_complete(), + Ok(_) => guard.forget(), } } }, diff --git a/pallets/creditcoin/src/mock.rs b/pallets/creditcoin/src/mock.rs index ed1332226a..5fdb1ba4dd 100644 --- a/pallets/creditcoin/src/mock.rs +++ b/pallets/creditcoin/src/mock.rs @@ -10,23 +10,23 @@ use frame_support::{ traits::{ConstU32, ConstU64, GenesisBuild, Get, Hooks}, }; use frame_system as system; -use parking_lot::RwLock; +pub(crate) use parking_lot::RwLock; use serde_json::Value; use sp_core::H256; use sp_keystore::{testing::KeyStore, KeystoreExt, SyncCryptoStore}; +pub(crate) use sp_runtime::offchain::testing::OffchainState; use sp_runtime::{ offchain::{ storage::StorageValueRef, - testing::{ - OffchainState, PendingRequest, PoolState, TestOffchainExt, TestTransactionPoolExt, - }, + testing::{PendingRequest, PoolState, TestOffchainExt, TestTransactionPoolExt}, OffchainDbExt, OffchainWorkerExt, TransactionPoolExt, }, testing::{Header, TestXt}, traits::{BlakeTwo256, Extrinsic as ExtrinsicT, IdentifyAccount, IdentityLookup, Verify}, MultiSignature, RuntimeAppPublic, }; -use std::{cell::Cell, collections::HashMap, sync::Arc}; +pub(crate) use std::sync::Arc; +use std::{cell::Cell, collections::HashMap}; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; diff --git a/pallets/creditcoin/src/ocw.rs b/pallets/creditcoin/src/ocw.rs index a913cf8a82..133b434600 100644 --- a/pallets/creditcoin/src/ocw.rs +++ b/pallets/creditcoin/src/ocw.rs @@ -77,35 +77,5 @@ impl Pallet { } } -pub(crate) struct LocalVerificationStatus<'a> { - storage_ref: StorageValueRef<'a>, - key: &'a [u8], -} - -impl<'a> LocalVerificationStatus<'a> { - pub(crate) fn new(storage_key: &'a [u8]) -> Self { - Self { storage_ref: StorageValueRef::persistent(storage_key), key: storage_key } - } - - pub(crate) fn is_complete(&self) -> bool { - match self.storage_ref.get::<()>() { - Ok(Some(())) => true, - Ok(None) => false, - Err(e) => { - log::warn!( - "Failed to decode offchain storage for {}: {:?}", - hex::encode(self.key), - e - ); - true - }, - } - } - - pub(crate) fn mark_complete(&self) { - self.storage_ref.set(&()); - } -} - #[cfg(test)] mod tests; diff --git a/pallets/creditcoin/src/ocw/tasks.rs b/pallets/creditcoin/src/ocw/tasks.rs index 2d9337b6ca..8f27862853 100644 --- a/pallets/creditcoin/src/ocw/tasks.rs +++ b/pallets/creditcoin/src/ocw/tasks.rs @@ -6,7 +6,16 @@ use crate::types::{ CollectedCoins, Task, TaskOutput, Transfer, UnverifiedCollectedCoins, UnverifiedTransfer, }; use crate::{CollectedCoinsId, Config, TaskData, TransferId}; +use codec::Encode; +pub use sp_runtime::offchain::storage_lock::{BlockAndTime, Lockable, StorageLock}; use sp_runtime::traits::{UniqueSaturatedFrom, UniqueSaturatedInto}; +use sp_std::vec::Vec; + +#[inline] +pub(crate) fn storage_key(id: &Id) -> Vec { + const TASK_GUARD: &[u8] = b"creditcoin/task/guard/"; + id.using_encoded(|encoded_id| TASK_GUARD.iter().chain(encoded_id).copied().collect()) +} impl UnverifiedTransfer where diff --git a/pallets/creditcoin/src/ocw/tasks/collect_coins.rs b/pallets/creditcoin/src/ocw/tasks/collect_coins.rs index 9049b2f05f..1391d0cb70 100644 --- a/pallets/creditcoin/src/ocw/tasks/collect_coins.rs +++ b/pallets/creditcoin/src/ocw/tasks/collect_coins.rs @@ -108,7 +108,7 @@ impl Pallet { } #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; use crate::TaskId; @@ -156,13 +156,13 @@ mod tests { input_bytes.into() }); - static TX_HASH: Lazy = Lazy::new(|| { + pub(crate) static TX_HASH: Lazy = Lazy::new(|| { let responses = &*RESPONSES; let val = responses["eth_getTransactionByHash"].result.clone().unwrap()["hash"].clone(); serde_json::from_value(val).unwrap() }); - static RPC_RESPONSE_AMOUNT: Lazy = Lazy::new(|| { + pub(crate) static RPC_RESPONSE_AMOUNT: Lazy = Lazy::new(|| { let transfer_fn = burn_vested_cc_abi(); let inputs = transfer_fn.decode_input(&(INPUT.0)[4..]).unwrap(); @@ -177,6 +177,7 @@ mod tests { use std::convert::TryFrom; + use alloc::sync::Arc; use assert_matches::assert_matches; use codec::Decode; use ethereum_types::{H160, U64}; @@ -186,7 +187,8 @@ mod tests { use crate::helpers::non_paying_error; use crate::mock::{ - roll_by_with_ocw, set_rpc_uri, AccountId, ExtBuilder, MockedRpcRequests, Origin, Test, + roll_by_with_ocw, set_rpc_uri, AccountId, ExtBuilder, MockedRpcRequests, OffchainState, + Origin, RwLock, Test, }; use crate::ocw::{ errors::{OffchainError, VerificationFailureCause as Cause}, @@ -198,6 +200,16 @@ mod tests { use crate::Pallet as Creditcoin; use crate::{ocw::rpc::JsonRpcResponse, ExternalAddress}; + /// call from externalities context + pub(crate) fn mock_rpc_for_collect_coins(state: &Arc>) { + let dummy_url = "dummy"; + set_rpc_uri(&CONTRACT_CHAIN, &dummy_url); + + let mut rpcs = + MockedRpcRequests::new(dummy_url, &*TX_HASH, &*BLOCK_NUMBER_STR, &*RESPONSES); + rpcs.mock_get_block_number(&mut *state.write()); + } + struct PassingCollectCoins { to: ExternalAddress, receipt: EthTransactionReceipt, @@ -460,7 +472,6 @@ mod tests { } #[test] - // #[tracing_test::traced_test] fn persist_collect_coins() { let mut ext = ExtBuilder::default(); let acct_pubkey = ext.generate_authority(); @@ -511,7 +522,6 @@ mod tests { } #[test] - #[tracing_test::traced_test] fn persist_unregistered_address() { let mut ext = ExtBuilder::default(); let acct_pubkey = ext.generate_authority(); @@ -540,7 +550,6 @@ mod tests { } #[test] - #[tracing_test::traced_test] fn persist_more_than_max_balance_should_error() { let mut ext = ExtBuilder::default(); let acct_pubkey = ext.generate_authority(); @@ -580,7 +589,6 @@ mod tests { } #[test] - #[tracing_test::traced_test] fn request_persisted_not_reentrant() { let mut ext = ExtBuilder::default(); let acct_pubkey = ext.generate_authority(); @@ -622,7 +630,6 @@ mod tests { } #[test] - #[tracing_test::traced_test] fn request_pending_not_reentrant() { let mut ext = ExtBuilder::default(); ext.generate_authority(); @@ -749,12 +756,7 @@ mod tests { let mut ext = ExtBuilder::default(); ext.generate_authority(); ext.build_offchain_and_execute_with_state(|state, pool| { - let dummy_url = "dummy"; - set_rpc_uri(&CONTRACT_CHAIN, &dummy_url); - - let mut rpcs = - MockedRpcRequests::new(dummy_url, &*TX_HASH, &*BLOCK_NUMBER_STR, &*RESPONSES); - rpcs.mock_get_block_number(&mut state.write()); + mock_rpc_for_collect_coins(&state); let (acc, addr, sign, _) = generate_address_with_proof("collector"); @@ -836,17 +838,11 @@ mod tests { } #[test] - #[tracing_test::traced_test] fn unverified_collect_coins_are_removed() { let mut ext = ExtBuilder::default(); ext.generate_authority(); ext.build_offchain_and_execute_with_state(|state, _| { - let dummy_url = "dummy"; - set_rpc_uri(&CONTRACT_CHAIN, &dummy_url); - - let mut rpcs = - MockedRpcRequests::new(dummy_url, &*TX_HASH, &*BLOCK_NUMBER_STR, &*RESPONSES); - rpcs.mock_get_block_number(&mut state.write()); + mock_rpc_for_collect_coins(&state); let (acc, addr, sign, _) = generate_address_with_proof("collector"); diff --git a/pallets/creditcoin/src/ocw/tasks/tests.rs b/pallets/creditcoin/src/ocw/tasks/tests.rs new file mode 100644 index 0000000000..397d765695 --- /dev/null +++ b/pallets/creditcoin/src/ocw/tasks/tests.rs @@ -0,0 +1,109 @@ +#![cfg(test)] + +use super::{StorageLock, Task}; +use crate::{ + mock::{roll_by_with_ocw, set_rpc_uri, AccountId, ExtBuilder, MockedRpcRequests, Origin, Test}, + ocw::{ + errors::{OffchainError, VerificationFailureCause as Cause}, + rpc::{EthTransaction, EthTransactionReceipt, JsonRpcResponse}, + EncodeLike, ETH_CONFIRMATIONS, + }, + pallet::{Config, Store}, + tests::{generate_address_with_proof, RefstrExt}, + types::CollectedCoinsId, + ExternalAddress, Pallet as Creditcoin, +}; +use assert_matches::assert_matches; +use codec::Decode; +use ethereum_types::{H160, U64}; +use frame_support::{assert_noop, assert_ok, once_cell::sync::Lazy}; +use frame_system::Pallet as System; +use pallet_timestamp::Pallet as Timestamp; +use sp_core::offchain::Duration; +use sp_io::offchain; +use sp_runtime::{ + offchain::{ + storage::StorageValueRef, + storage_lock::{BlockAndTime, Lockable, Time}, + }, + traits::{BlockNumberProvider, IdentifyAccount}, +}; +use std::convert::{TryFrom, TryInto}; +use std::sync::{ + atomic::{AtomicU8, Ordering}, + Arc, +}; +use std::thread; + +#[test] +fn lock_released_when_guard_is_dropped() { + let ext = ExtBuilder::default(); + ext.build_offchain_and_execute_with_state(|state, _| { + let key = b"id_1"; + let mut l1 = StorageLock::<'_, Time>::new(key); + let g = l1.try_lock(); + assert!(g.is_ok()); + drop(g); + assert!(state.read().persistent_storage.get(key).is_none()); + }); +} + +#[test] +fn lock_guard_is_kept_alive() { + let ext = ExtBuilder::default(); + ext.build_offchain_and_execute_with_state(|_, _| { + let mut l1 = StorageLock::<'_, Time>::new(b"id_1"); + let g = l1.try_lock(); + g.expect("ok").forget(); + let g = l1.try_lock(); + assert!(g.is_err()); + }); +} + +#[test] +fn lock_expires() { + let ext = ExtBuilder::default(); + ext.build_offchain_and_execute_with_state(|_, _| { + System::::set_block_number(1); + let mut l1 = StorageLock::<'_, BlockAndTime>>::with_block_and_time_deadline( + b"id_1", + 1, + Duration::from_millis(0), + ); + let g = l1.try_lock().expect("ok"); + g.forget(); + System::::set_block_number(3); + let sleep_until = offchain::timestamp().add(Duration::from_millis(1)); + offchain::sleep_until(sleep_until); + let g = l1.try_lock(); + assert!(g.is_ok()); + }); +} + +#[test] +fn lock_mutual_exclusion() { + let ext = ExtBuilder::default(); + ext.build_offchain_and_execute_with_state(|state, _| { + let mut l1 = StorageLock::<'_, Time>::new(b"id_1"); + let mut l2 = StorageLock::<'_, Time>::new(b"id_2"); + + let g1 = l1.try_lock().expect("ok"); + g1.forget(); + let g1 = l1.try_lock(); + assert!(g1.is_err()); + //won't release because it was not a guard. + drop(g1); + let g2 = l2.try_lock().expect("ok"); + drop(g2); + let g2 = l2.try_lock(); + assert!(g2.is_ok()); + + let g1 = l1.try_lock(); + assert!(g1.is_err()); + drop(g2); + let deadline = state.read().persistent_storage.get(b"id_2"); + assert!(deadline.is_none()); + let deadline = state.read().persistent_storage.get(b"id_1"); + assert!(deadline.is_some()); + }); +} diff --git a/pallets/creditcoin/src/ocw/tests.rs b/pallets/creditcoin/src/ocw/tests.rs index 3e6020be93..39c992bd7c 100644 --- a/pallets/creditcoin/src/ocw/tests.rs +++ b/pallets/creditcoin/src/ocw/tests.rs @@ -5,31 +5,41 @@ use super::errors::{ RpcUrlError, VerificationFailureCause::{self, *}, }; + +use super::tasks::collect_coins::tests::{mock_rpc_for_collect_coins, RPC_RESPONSE_AMOUNT}; +use super::tasks::BlockAndTime; +use super::tasks::Lockable; +use crate::ocw::tasks::collect_coins::{tests::TX_HASH, CONTRACT_CHAIN}; +use crate::tests::generate_address_with_proof; +use crate::types::{AddressId, CollectedCoins, CollectedCoinsId, TaskId}; +use crate::Pallet as Creditcoin; use crate::{ mock::{ get_mock_amount, get_mock_contract, get_mock_from_address, get_mock_input_data, - get_mock_nonce, get_mock_to_address, set_rpc_uri, AccountId, ExtBuilder, - Test as TestRuntime, ETHLESS_RESPONSES, + get_mock_nonce, get_mock_timestamp, get_mock_to_address, roll_to, roll_to_with_ocw, + set_rpc_uri, AccountId, Call, ExtBuilder, Extrinsic, MockedRpcRequests, Origin, + PendingRequestExt, RwLock, Test as TestRuntime, ETHLESS_RESPONSES, }, - mock::{MockedRpcRequests, PendingRequestExt}, ocw::rpc::{errors::RpcError, JsonRpcError, JsonRpcResponse}, tests::{RefstrExt, TestInfo}, - types::DoubleMapExt, - Blockchain, ExternalAddress, Id, LoanTerms, TransferKind, + types::{DoubleMapExt, TransferId}, + Blockchain, ExternalAddress, Id, LoanTerms, OrderId, TransferKind, }; - use alloc::sync::Arc; use assert_matches::assert_matches; use codec::Decode; use ethabi::Token; use ethereum_types::{BigEndianHash, H160, U256, U64}; use frame_support::{assert_ok, once_cell::sync::Lazy, BoundedVec}; -use parking_lot::RwLock; +use frame_system::Pallet as System; use sp_core::H256; +use sp_io::offchain; +use sp_runtime::traits::Dispatchable; use sp_runtime::{ offchain::{ storage::{StorageRetrievalError, StorageValueRef}, testing::OffchainState, + Duration, }, traits::IdentifyAccount, }; @@ -391,7 +401,7 @@ fn offchain_signed_tx_works() { crate::mock::roll_to(2); assert_matches!(pool.write().transactions.pop(), Some(tx) => { - let tx = crate::mock::Extrinsic::decode(&mut &*tx).unwrap(); + let tx = Extrinsic::decode(&mut &*tx).unwrap(); assert_eq!(tx.call, crate::mock::Call::Creditcoin(call)); }); }); @@ -713,3 +723,368 @@ fn verify_transfer_get_block_invalid_address() { ); }); } + +#[test] +fn completed_oversubscribed_tasks_are_skipped() { + let mut ext = ExtBuilder::default(); + let acct_pubkey = ext.generate_authority(); + let auth = AccountId::from(acct_pubkey.into_account().0); + ext.build_offchain_and_execute_with_state(|state, pool| { + mock_rpc_for_collect_coins(&state); + + let (acc, addr, sign, _) = generate_address_with_proof("collector"); + + assert_ok!(Creditcoin::::register_address( + Origin::signed(acc.clone()), + CONTRACT_CHAIN, + addr.clone(), + sign + )); + + roll_to(1); + let deadline = TestRuntime::unverified_transfer_deadline(); + //register twice (oversubscribe) under different expiration (aka deadline). + assert_ok!(Creditcoin::::request_collect_coins( + Origin::signed(acc.clone()), + addr.clone(), + TX_HASH.hex_to_address() + )); + roll_to(2); + let deadline_2 = TestRuntime::unverified_transfer_deadline(); + assert_ok!(Creditcoin::::request_collect_coins( + Origin::signed(acc), + addr.clone(), + TX_HASH.hex_to_address() + )); + + //We now have 2 enqueued tasks. + + roll_to_with_ocw(3); + + let collected_coins_id = + CollectedCoinsId::new::(TX_HASH.hex_to_address().as_slice()); + let collected_coins = CollectedCoins { + to: AddressId::new::(&CONTRACT_CHAIN, addr.as_ref()), + amount: RPC_RESPONSE_AMOUNT.as_u128(), + tx_id: TX_HASH.hex_to_address(), + }; + + let tx = pool.write().transactions.pop().expect("persist collect_coins"); + assert!(pool.read().transactions.is_empty()); + let tx = Extrinsic::decode(&mut &*tx).unwrap(); + assert_eq!( + tx.call, + Call::Creditcoin(crate::Call::persist_task_output { + task_output: (collected_coins_id.clone(), collected_coins).into(), + deadline + }) + ); + + assert_ok!(tx.call.dispatch(Origin::signed(auth))); + + roll_to_with_ocw(deadline_2); + + //task expires without yielding txns. + assert!(pool.read().transactions.is_empty()); + + let key = super::tasks::storage_key(&TaskId::from(collected_coins_id)); + + type Y = > as Lockable>::Deadline; + //lock set + assert!(StorageValueRef::persistent(key.as_ref()).get::().expect("decoded").is_some()); + }); +} + +//tasks can be oversubscribed with different deadlines +#[test] +fn task_deadline_oversubscription() { + let ext = ExtBuilder::default(); + ext.build_offchain_and_execute_with_state(|_, _| { + let (acc, addr, sign, _) = generate_address_with_proof("collector"); + + assert_ok!(Creditcoin::::register_address( + Origin::signed(acc.clone()), + CONTRACT_CHAIN, + addr.clone(), + sign + )); + + roll_to(1); + let deadline_1 = TestRuntime::unverified_transfer_deadline(); + //register twice under different (expiration aka deadline) + assert_ok!(Creditcoin::::request_collect_coins( + Origin::signed(acc.clone()), + addr.clone(), + TX_HASH.hex_to_address() + )); + roll_to(2); + let deadline_2 = TestRuntime::unverified_transfer_deadline(); + assert_ok!(Creditcoin::::request_collect_coins( + Origin::signed(acc), + addr, + TX_HASH.hex_to_address() + )); + + let collected_coins_id = + CollectedCoinsId::new::(TX_HASH.hex_to_address().as_slice()); + + assert!(Creditcoin::::pending_tasks( + deadline_1, + TaskId::from(collected_coins_id.clone()) + ) + .is_some()); + assert!(Creditcoin::::pending_tasks( + deadline_2, + TaskId::from(collected_coins_id) + ) + .is_some()); + }); +} + +use crate::mock::{get_mock_tx_block_num, get_mock_tx_hash, roll_by_with_ocw}; +use crate::tests::adjust_deal_order_to_nonce; + +#[test] +#[tracing_test::traced_test] +fn ocw_retries() { + let mut ext = ExtBuilder::default(); + ext.generate_authority(); + ext.build_offchain_and_execute_with_state(|state, pool| { + roll_to(1); + + let dummy_url = "dummy"; + let tx_hash = get_mock_tx_hash(); + let contract = get_mock_contract().hex_to_address(); + let tx_block_num = get_mock_tx_block_num(); + let blockchain = Blockchain::Rinkeby; + + let tx_block_num_value = + u64::from_str_radix(tx_block_num.trim_start_matches("0x"), 16).unwrap(); + + set_rpc_uri(&Blockchain::Rinkeby, &dummy_url); + + let loan_amount = get_mock_amount(); + let terms = LoanTerms { amount: loan_amount, ..Default::default() }; + + let test_info = TestInfo { blockchain, loan_terms: terms, ..Default::default() }; + + let (_, deal_order_id) = test_info.create_deal_order(); + + let deal_order_id = adjust_deal_order_to_nonce(&deal_order_id, get_mock_nonce()); + + let lender = test_info.lender.account_id; + assert_ok!(Creditcoin::::register_funding_transfer( + Origin::signed(lender), + TransferKind::Ethless(contract), + deal_order_id, + tx_hash.hex_to_address(), + )); + + let mock_unconfirmed_tx = || { + let mut requests = + MockedRpcRequests::new(dummy_url, &tx_hash, &tx_block_num, &*ETHLESS_RESPONSES); + requests.get_block_number.set_response(JsonRpcResponse { + jsonrpc: "2.0".into(), + id: 1, + error: None, + result: Some(format!("0x{:x}", tx_block_num_value + 1)), + }); + + requests.mock_get_block_number(&mut state.write()); + }; + + // mock requests so the tx is unconfirmed + mock_unconfirmed_tx(); + + roll_by_with_ocw(1); + assert!(logs_contain("TaskUnconfirmed")); + + // we failed, we should retry again here + + mock_unconfirmed_tx(); + + roll_by_with_ocw(1); + assert!(logs_contain("TaskUnconfirmed")); + + // now mock requests so the tx is confirmed + + MockedRpcRequests::new(dummy_url, &tx_hash, &tx_block_num, &*ETHLESS_RESPONSES) + .mock_all(&mut state.write()); + + roll_by_with_ocw(1); + + // we should have retried and successfully verified the transfer + let tx = pool.write().transactions.pop().expect("verify transfer"); + assert!(pool.read().transactions.is_empty()); + let verify_tx = Extrinsic::decode(&mut &*tx).unwrap(); + assert_matches!( + verify_tx.call, + crate::mock::Call::Creditcoin(crate::Call::persist_task_output { .. }) + ); + }); +} + +#[test] +fn duplicate_retry_fail_and_succeed() { + let mut ext = ExtBuilder::default(); + ext.generate_authority(); + ext.build_offchain_and_execute_with_state(|state, pool| { + let dummy_url = "dummy"; + let tx_hash = get_mock_tx_hash(); + let contract = get_mock_contract().hex_to_address(); + let tx_block_num = get_mock_tx_block_num(); + let blockchain = Blockchain::Rinkeby; + + // mocks for when we expect failure + MockedRpcRequests::new(dummy_url, &tx_hash, &tx_block_num, &*ETHLESS_RESPONSES) + .mock_get_block_number(&mut state.write()); + // mocks for when we expect success + MockedRpcRequests::new(dummy_url, &tx_hash, &tx_block_num, &*ETHLESS_RESPONSES) + .mock_all(&mut state.write()); + + set_rpc_uri(&Blockchain::Rinkeby, &dummy_url); + + let loan_amount = get_mock_amount(); + let terms = LoanTerms { amount: loan_amount, ..Default::default() }; + + let test_info = + TestInfo { blockchain: blockchain.clone(), loan_terms: terms, ..Default::default() }; + let (_, deal_order_id) = test_info.create_deal_order(); + let lender = test_info.lender.account_id.clone(); + + // test that we get a "fail_transfer" tx when verification fails + assert_ok!(Creditcoin::::register_funding_transfer( + Origin::signed(lender.clone()), + TransferKind::Ethless(contract.clone()), + deal_order_id.clone(), + tx_hash.hex_to_address(), + )); + let deadline = TestRuntime::unverified_transfer_deadline(); + + roll_to_with_ocw(1); + + let transfer_id = TransferId::new::(&blockchain, &tx_hash.hex_to_address()); + let tx = pool.write().transactions.pop().expect("fail transfer"); + assert!(pool.read().transactions.is_empty()); + let fail_tx = Extrinsic::decode(&mut &*tx).unwrap(); + assert_eq!( + fail_tx.call, + Call::Creditcoin(crate::Call::fail_task { + task_id: transfer_id.clone().into(), + deadline, + cause: VerificationFailureCause::IncorrectNonce + }) + ); + + // test for successful verification + + // this is kind of a gross hack, basically when I made the test transfer on luniverse to pull the mock responses + // I didn't pass the proper `nonce` to the smart contract, and it's a pain to redo the transaction and update all the tests, + // so here we just "change" the deal_order_id to one with a `hash` that matches the expected nonce so that the transfer + // verification logic is happy + let fake_deal_order_id = adjust_deal_order_to_nonce(&deal_order_id, get_mock_nonce()); + + assert_ok!(Creditcoin::::register_funding_transfer( + Origin::signed(lender.clone()), + TransferKind::Ethless(contract.clone()), + fake_deal_order_id.clone(), + tx_hash.hex_to_address(), + )); + + let deadline_2 = TestRuntime::unverified_transfer_deadline(); + + let expected_transfer = crate::Transfer { + blockchain: test_info.blockchain.clone(), + kind: TransferKind::Ethless(contract), + amount: loan_amount, + block: System::::block_number(), + from: test_info.lender.address_id.clone(), + to: test_info.borrower.address_id, + order_id: OrderId::Deal(fake_deal_order_id), + is_processed: false, + account_id: lender, + tx_id: tx_hash.hex_to_address(), + timestamp: Some(get_mock_timestamp()), + }; + + //We expect the guard to expire on the next roll, sleep to meet time requirements. + let lock_expires = offchain::timestamp().add(Duration::from_millis(1)); + offchain::sleep_until(lock_expires); + + //guard picked at 1 is reacquireable at... + roll_to_with_ocw(deadline); + + let tx = pool.write().transactions.pop().expect("verify transfer"); + assert!(pool.read().transactions.is_empty()); + let verify_tx = Extrinsic::decode(&mut &*tx).unwrap(); + assert_eq!( + verify_tx.call, + Call::Creditcoin(crate::Call::persist_task_output { + task_output: (transfer_id, expected_transfer).into(), + deadline: deadline_2 + }) + ); + }); +} + +#[test] +fn effective_guard_lifetime_until_task_expiration() { + let mut ext = ExtBuilder::default(); + ext.generate_authority(); + ext.build_offchain_and_execute_with_state(|state, pool| { + mock_rpc_for_collect_coins(&state); + + let (acc, addr, sign, _) = generate_address_with_proof("collector"); + assert_ok!(Creditcoin::::register_address( + Origin::signed(acc.clone()), + CONTRACT_CHAIN, + addr.clone(), + sign + )); + + roll_to(1); + let deadline = TestRuntime::unverified_transfer_deadline(); + assert_ok!(Creditcoin::::request_collect_coins( + Origin::signed(acc), + addr.clone(), + TX_HASH.hex_to_address() + )); + roll_to_with_ocw(2); + + let tx = pool.write().transactions.pop().expect("persist collect_coins"); + assert!(pool.read().transactions.is_empty()); + let tx = Extrinsic::decode(&mut &*tx).unwrap(); + + let collected_coins_id = + CollectedCoinsId::new::(TX_HASH.hex_to_address().as_slice()); + let collected_coins = CollectedCoins { + to: AddressId::new::(&CONTRACT_CHAIN, addr.as_ref()), + amount: RPC_RESPONSE_AMOUNT.as_u128(), + tx_id: TX_HASH.hex_to_address(), + }; + + assert_eq!( + tx.call, + Call::Creditcoin(crate::Call::persist_task_output { + task_output: (collected_coins_id, collected_coins).into(), + deadline + }) + ); + + let key = { + let collected_coins_id = + CollectedCoinsId::new::(TX_HASH.hex_to_address().as_slice()); + + super::tasks::storage_key(&TaskId::from(collected_coins_id)) + }; + + type Y = > as Lockable>::Deadline; + //lock set + let Y { block_number, .. } = StorageValueRef::persistent(key.as_ref()) + .get::() + .expect("decoded") + .expect("deadline"); + println!("{block_number} {deadline}"); + assert!(block_number >= deadline - 1); + }); +} diff --git a/pallets/creditcoin/src/tests.rs b/pallets/creditcoin/src/tests.rs index 4bfaa32e89..feeaa35021 100644 --- a/pallets/creditcoin/src/tests.rs +++ b/pallets/creditcoin/src/tests.rs @@ -1,7 +1,6 @@ use crate::{ helpers::{non_paying_error, EVMAddress, PublicToAddress}, mock::*, - ocw::{rpc::JsonRpcResponse, VerificationFailureCause}, types::DoubleMapExt, AddressId, AskOrder, AskOrderId, Authorities, BidOrder, BidOrderId, Blockchain, Currency, CurrencyId, DealOrder, DealOrderId, DealOrders, Duration, EvmInfo, EvmTransferKind, @@ -11,7 +10,7 @@ use crate::{ use assert_matches::assert_matches; use bstr::B; -use codec::{Decode, Encode}; +use codec::Encode; use ethereum_types::{BigEndianHash, H256, U256}; use frame_support::{assert_noop, assert_ok, traits::Get, BoundedVec}; use frame_system::RawOrigin; @@ -483,102 +482,6 @@ fn verify_ethless_transfer() { }); } -#[test] -fn register_transfer_ocw() { - let mut ext = ExtBuilder::default(); - ext.generate_authority(); - ext.build_offchain_and_execute_with_state(|state, pool| { - let dummy_url = "dummy"; - let tx_hash = get_mock_tx_hash(); - let contract = get_mock_contract().hex_to_address(); - let tx_block_num = get_mock_tx_block_num(); - let blockchain = Blockchain::Rinkeby; - - // mocks for when we expect failure - MockedRpcRequests::new(dummy_url, &tx_hash, &tx_block_num, &*ETHLESS_RESPONSES) - .mock_get_block_number(&mut state.write()); - // mocks for when we expect success - MockedRpcRequests::new(dummy_url, &tx_hash, &tx_block_num, &*ETHLESS_RESPONSES) - .mock_all(&mut state.write()); - - set_rpc_uri(&Blockchain::Rinkeby, &dummy_url); - - let loan_amount = get_mock_amount(); - let terms = LoanTerms { amount: loan_amount, ..Default::default() }; - - let test_info = - TestInfo { blockchain: blockchain.clone(), loan_terms: terms, ..Default::default() }; - let (_, deal_order_id) = test_info.create_deal_order(); - let lender = test_info.lender.account_id.clone(); - - // test that we get a "fail_transfer" tx when verification fails - assert_ok!(Creditcoin::register_funding_transfer( - Origin::signed(lender.clone()), - TransferKind::Ethless(contract.clone()), - deal_order_id.clone(), - tx_hash.hex_to_address(), - )); - let deadline = Test::unverified_transfer_deadline(); - - roll_by_with_ocw(1); - - let transfer_id = TransferId::new::(&blockchain, &tx_hash.hex_to_address()); - let tx = pool.write().transactions.pop().expect("fail transfer"); - assert!(pool.read().transactions.is_empty()); - let fail_tx = Extrinsic::decode(&mut &*tx).unwrap(); - assert_eq!( - fail_tx.call, - Call::Creditcoin(crate::Call::fail_task { - task_id: transfer_id.clone().into(), - deadline, - cause: VerificationFailureCause::IncorrectNonce - }) - ); - - // test for successful verification - - // this is kind of a gross hack, basically when I made the test transfer on luniverse to pull the mock responses - // I didn't pass the proper `nonce` to the smart contract, and it's a pain to redo the transaction and update all the tests, - // so here we just "change" the deal_order_id to one with a `hash` that matches the expected nonce so that the transfer - // verification logic is happy - let fake_deal_order_id = adjust_deal_order_to_nonce(&deal_order_id, get_mock_nonce()); - - assert_ok!(Creditcoin::register_funding_transfer( - Origin::signed(lender.clone()), - TransferKind::Ethless(contract.clone()), - fake_deal_order_id.clone(), - tx_hash.hex_to_address(), - )); - let expected_transfer = crate::Transfer { - blockchain: test_info.blockchain.clone(), - kind: TransferKind::Ethless(contract), - amount: loan_amount, - block: System::block_number(), - from: test_info.lender.address_id.clone(), - to: test_info.borrower.address_id, - order_id: OrderId::Deal(fake_deal_order_id), - is_processed: false, - account_id: lender, - tx_id: tx_hash.hex_to_address(), - timestamp: Some(get_mock_timestamp()), - }; - let deadline = Test::unverified_transfer_deadline(); - - roll_by_with_ocw(1); - - let tx = pool.write().transactions.pop().expect("verify transfer"); - assert!(pool.read().transactions.is_empty()); - let verify_tx = Extrinsic::decode(&mut &*tx).unwrap(); - assert_eq!( - verify_tx.call, - Call::Creditcoin(crate::Call::persist_task_output { - task_output: (transfer_id, expected_transfer).into(), - deadline - }) - ); - }); -} - #[test] #[tracing_test::traced_test] fn register_transfer_ocw_fail_to_send() { @@ -643,7 +546,10 @@ fn register_transfer_ocw_fail_to_send() { }); } -fn adjust_deal_order_to_nonce(deal_order_id: &TestDealOrderId, nonce: U256) -> TestDealOrderId { +pub(crate) fn adjust_deal_order_to_nonce( + deal_order_id: &TestDealOrderId, + nonce: U256, +) -> TestDealOrderId { let deal_id_hash = H256::from_uint(&nonce); let deal = crate::DealOrders::::try_get_id(&deal_order_id).unwrap(); crate::DealOrders::::remove(deal_order_id.expiration(), deal_order_id.hash()); @@ -653,91 +559,6 @@ fn adjust_deal_order_to_nonce(deal_order_id: &TestDealOrderId, nonce: U256) -> T fake_deal_order_id } -#[test] -#[tracing_test::traced_test] -fn ocw_retries() { - let mut ext = ExtBuilder::default(); - ext.generate_authority(); - ext.build_offchain_and_execute_with_state(|state, pool| { - System::set_block_number(1); - - let dummy_url = "dummy"; - let tx_hash = get_mock_tx_hash(); - let contract = get_mock_contract().hex_to_address(); - let tx_block_num = get_mock_tx_block_num(); - let blockchain = Blockchain::Rinkeby; - - let tx_block_num_value = - u64::from_str_radix(tx_block_num.trim_start_matches("0x"), 16).unwrap(); - - set_rpc_uri(&Blockchain::Rinkeby, &dummy_url); - - let loan_amount = get_mock_amount(); - let terms = LoanTerms { amount: loan_amount, ..Default::default() }; - - let test_info = TestInfo { blockchain, loan_terms: terms, ..Default::default() }; - - let (_, deal_order_id) = test_info.create_deal_order(); - - let deal_order_id = adjust_deal_order_to_nonce(&deal_order_id, get_mock_nonce()); - - let lender = test_info.lender.account_id; - assert_ok!(Creditcoin::register_funding_transfer( - Origin::signed(lender), - TransferKind::Ethless(contract), - deal_order_id, - tx_hash.hex_to_address(), - )); - - let mock_unconfirmed_tx = || { - let mut requests = - MockedRpcRequests::new(dummy_url, &tx_hash, &tx_block_num, &*ETHLESS_RESPONSES); - requests.get_block_number.set_response(JsonRpcResponse { - jsonrpc: "2.0".into(), - id: 1, - error: None, - result: Some(format!("0x{:x}", tx_block_num_value + 1)), - }); - - requests.mock_get_block_number(&mut state.write()); - }; - - // mock requests so the tx is unconfirmed - mock_unconfirmed_tx(); - - roll_by_with_ocw(1); - assert!(logs_contain("TaskUnconfirmed")); - - // we failed, we should retry again here - - mock_unconfirmed_tx(); - - roll_by_with_ocw(1); - assert!(logs_contain("TaskUnconfirmed")); - - // now mock requests so the tx is confirmed - - MockedRpcRequests::new(dummy_url, &tx_hash, &tx_block_num, &*ETHLESS_RESPONSES) - .mock_all(&mut state.write()); - - roll_by_with_ocw(1); - - // we should have retried and successfully verified the transfer - - let tx = pool.write().transactions.pop().expect("verify transfer"); - assert!(pool.read().transactions.is_empty()); - let verify_tx = Extrinsic::decode(&mut &*tx).unwrap(); - assert_matches!( - verify_tx.call, - super::mock::Call::Creditcoin(crate::Call::persist_task_output { .. }) - ); - - roll_by_with_ocw(1); - - assert!(logs_contain("Already handled Task")); - }); -} - #[test] fn add_ask_order_basic() { let (mut ext, _, _) = ExtBuilder::default().build_offchain(); diff --git a/runtime/src/version.rs b/runtime/src/version.rs index b62ca95d30..031e44f8a7 100644 --- a/runtime/src/version.rs +++ b/runtime/src/version.rs @@ -14,7 +14,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 206, + spec_version: 207, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 7,