Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sandbox): fast forward timestamp and epoch height #6211

Merged
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
f9aa23f
Added fast-forwarding to sandbox
ChaoticTempest Jan 21, 2022
232e901
Changed name to delta_height for better clarity
ChaoticTempest Jan 21, 2022
c9f9492
Remove print statements
ChaoticTempest Jan 21, 2022
85eb896
Cargo format
ChaoticTempest Jan 21, 2022
b70475a
Merge branch 'master' of https://github.com/near/nearcore into phuong…
ChaoticTempest Jan 21, 2022
9b0d8ed
Added sandbox fast forward test
ChaoticTempest Jan 21, 2022
f5b1eb1
Move import into inplaced location as full path
ChaoticTempest Jan 21, 2022
691a2dc
Move other sandbox imports into inplaced locations
ChaoticTempest Jan 22, 2022
f35d1a5
Added forwarded timestmaps for sandbox after fast-forwarding
ChaoticTempest Jan 27, 2022
31a9397
Change visibility of fastforward delta
ChaoticTempest Jan 27, 2022
0a4631b
Update epoch_height based on block info for sandbox
ChaoticTempest Jan 27, 2022
d7370d0
Correct timestamp after each block produced after forwarding
ChaoticTempest Jan 28, 2022
671350d
Added integration test for timestamp
ChaoticTempest Jan 28, 2022
a001fd2
Cargo fmt
ChaoticTempest Jan 28, 2022
6cb23f0
Added overflow panic for sandbox_delta_time
ChaoticTempest Jan 28, 2022
f6b18e6
Merge branch 'master' of github.com:near/nearcore into phuong/sandbox…
ChaoticTempest Feb 3, 2022
df6fa4e
Fix merge conflict typo
ChaoticTempest Feb 4, 2022
1eec9d2
Use accrued delta height to update timestamps
ChaoticTempest Feb 4, 2022
f8cc241
Update fast_forward pytest w/ time travel assertions
ChaoticTempest Feb 4, 2022
cc3872c
Merge branch 'master' of github.com:near/nearcore into phuong/sandbox…
ChaoticTempest Feb 4, 2022
61e168a
Fix formatting and another merge that slipped in
ChaoticTempest Feb 4, 2022
15836ad
Address comments
ChaoticTempest Feb 5, 2022
864a20d
Correct typo & use wait_for_blocks
ChaoticTempest Feb 7, 2022
f9a507c
Merge branch 'master' of github.com:near/nearcore into phuong/sandbox…
ChaoticTempest Feb 7, 2022
17a4518
Removed redundant test
ChaoticTempest Feb 8, 2022
b7d6fb4
Merge branch 'master' of github.com:near/nearcore into phuong/sandbox…
ChaoticTempest Feb 8, 2022
5d2de98
Update epoch height calculation & add unit test
ChaoticTempest Feb 11, 2022
b66850d
Merge branch 'master' of https://github.com/near/nearcore into phuong…
ChaoticTempest Feb 11, 2022
8adb77f
Added block hash fix and added in more pytest checks
ChaoticTempest Mar 16, 2022
0158ad1
Merge branch 'master' of https://github.com/near/nearcore into phuong…
ChaoticTempest Mar 16, 2022
3e4f833
Fix typo
ChaoticTempest Mar 16, 2022
79e1a25
Added new way to fast forward wrt epoch boundaries
ChaoticTempest Mar 24, 2022
427652f
Added timeout for fast-forward to complete
ChaoticTempest Mar 24, 2022
eab87db
Added epoch boundary test
ChaoticTempest Mar 24, 2022
07775d7
Merge branch 'master' of https://github.com/near/nearcore into phuong…
ChaoticTempest Mar 24, 2022
6a1903d
Remove sandbox feature from epoch_manager
ChaoticTempest Mar 24, 2022
39f429b
Remove outdated integration test
ChaoticTempest Mar 24, 2022
7bc2983
Merge branch 'master' of https://github.com/near/nearcore into phuong…
ChaoticTempest Mar 24, 2022
bf98f33
Cleanup sandbox related code in chain/client.rs
ChaoticTempest Mar 25, 2022
832a604
Added large constant for sandbox acceptable time diff
ChaoticTempest Mar 28, 2022
7c53fdc
Cleanup and added pre/post_block_production methods
ChaoticTempest Mar 28, 2022
d6f3261
Cleaned up block produce logic
ChaoticTempest Mar 28, 2022
e3634c6
Merge branch 'master' of https://github.com/near/nearcore into phuong…
ChaoticTempest Mar 28, 2022
625e036
Added extra produce block argument
ChaoticTempest Mar 28, 2022
d9e674c
Some more cleanup
ChaoticTempest Mar 29, 2022
3e6aa18
Merge branch 'master' of https://github.com/near/nearcore into phuong…
ChaoticTempest Mar 29, 2022
7e0761d
Merge branch 'master' of https://github.com/near/nearcore into phuong…
ChaoticTempest Mar 30, 2022
98d1d50
Merge branch 'phuong/sandbox-fast-forward-timestamp-and-epoch-height'…
ChaoticTempest Mar 30, 2022
2a1ca7f
Adjusted raw timestamp type for Duration type
ChaoticTempest Mar 31, 2022
8170d09
Changed fastforward_delta to not be an Option type
ChaoticTempest Mar 31, 2022
66adcad
Updated to Duration type
ChaoticTempest Mar 31, 2022
83203a0
Merge branch 'master' of https://github.com/near/nearcore into phuong…
ChaoticTempest Mar 31, 2022
0d90208
Merge branch 'master' into phuong/sandbox-fast-forward-timestamp-and-…
ChaoticTempest Apr 1, 2022
c1a31d6
Merge master into phuong/sandbox-fast-forward-timestamp-and-epoch-height
near-bulldozer[bot] Apr 1, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 5 additions & 2 deletions chain/chain/src/chain.rs
Expand Up @@ -4334,8 +4334,11 @@ impl<'a> ChainUpdate<'a> {
provenance: &Provenance,
on_challenge: &mut dyn FnMut(ChallengeBody),
) -> Result<(), Error> {
// Refuse blocks from the too distant future.
if header.timestamp() > Clock::utc() + Duration::seconds(ACCEPTABLE_TIME_DIFFERENCE) {
// Refuse blocks from the too distant future. Only exception to this is while we're within
// sandbox, we're allowed to jump to the future via fast forwarding.
if !cfg!(feature = "sandbox")
&& header.timestamp() > Clock::utc() + Duration::seconds(ACCEPTABLE_TIME_DIFFERENCE)
ChaoticTempest marked this conversation as resolved.
Show resolved Hide resolved
{
return Err(ErrorKind::InvalidBlockFutureTime(header.timestamp()).into());
}

Expand Down
39 changes: 38 additions & 1 deletion chain/client/src/client.rs
Expand Up @@ -67,6 +67,10 @@ pub struct Client {
#[cfg(feature = "test_features")]
pub adv_produce_blocks_only_valid: bool,

/// Fast Forward accrued delta height used to calculate fast forwarded timestamps for each block.
#[cfg(feature = "sandbox")]
pub(crate) accrued_fastforward_delta: near_primitives::types::BlockHeightDelta,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh, if only we did the work for virtualizing time, than we'd be able to express this in a much cleaner way, by just making the time go forward faster.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, this was definitely a hack to circumvent that. Maybe we can revisit this in the future, but for now, this was pretty simple to do just for contract testing side of things


pub config: ClientConfig,
pub sync_status: SyncStatus,
pub chain: Chain,
Expand Down Expand Up @@ -172,6 +176,8 @@ impl Client {
adv_produce_blocks: false,
#[cfg(feature = "test_features")]
adv_produce_blocks_only_valid: false,
#[cfg(feature = "sandbox")]
accrued_fastforward_delta: 0,
config,
sync_status,
chain,
Expand Down Expand Up @@ -494,7 +500,8 @@ impl Client {
let next_epoch_protocol_version =
self.runtime_adapter.get_epoch_protocol_version(&next_epoch_id)?;

let block = Block::produce(
#[allow(unused_mut)] // unused if not in sandbox
let mut block = Block::produce(
this_epoch_protocol_version,
next_epoch_protocol_version,
prev_header,
Expand All @@ -516,6 +523,24 @@ impl Client {
block_merkle_root,
);

// If sandbox is enabled, set the fast-forwarded block timestamp to the produced block.
// This is current time + total amount of time forwarded from fast-forwarding.
#[cfg(feature = "sandbox")]
{
let header = match block {
Block::BlockV1(ref mut block) => &mut block.header,
Block::BlockV2(ref mut block) => &mut block.header,
};
let mut inner_lite = match header {
BlockHeader::BlockHeaderV1(ref mut header) => &mut header.inner_lite,
BlockHeader::BlockHeaderV2(ref mut header) => &mut header.inner_lite,
BlockHeader::BlockHeaderV3(ref mut header) => &mut header.inner_lite,
};

// Set block timestamp to the actual fast forwarded timestamp:
inner_lite.timestamp = to_timestamp(Clock::utc()) + self.sandbox_delta_time();
mina86 marked this conversation as resolved.
Show resolved Hide resolved
}

// Update latest known even before returning block out, to prevent race conditions.
self.chain.mut_store().save_latest_known(LatestKnown {
height: next_height,
Expand Down Expand Up @@ -960,6 +985,18 @@ impl Client {
Ok(())
}

/// Gets the advanced timestamp delta in nanoseconds for sandbox once it has been fast-forwarded
#[cfg(feature = "sandbox")]
pub fn sandbox_delta_time(&self) -> u64 {
ChaoticTempest marked this conversation as resolved.
Show resolved Hide resolved
let avg_block_prod_time = (self.config.min_block_production_delay.as_nanos()
+ self.config.max_block_production_delay.as_nanos())
/ 2;
ChaoticTempest marked this conversation as resolved.
Show resolved Hide resolved
(self.accrued_fastforward_delta as u128 * avg_block_prod_time).try_into().expect(&format!(
"Too high of a delta_height {} to convert into u64",
self.accrued_fastforward_delta
))
}

pub fn send_approval(
&mut self,
parent_hash: &CryptoHash,
Expand Down
4 changes: 3 additions & 1 deletion chain/client/src/client_actor.rs
Expand Up @@ -775,9 +775,11 @@ impl ClientActor {

#[cfg(feature = "sandbox")]
let latest_known = if let Some(delta_height) = self.fastforward_delta.take() {
self.client.accrued_fastforward_delta += delta_height;
let timestamp_delta_ns = self.client.sandbox_delta_time();
let new_latest_known = near_chain::types::LatestKnown {
height: latest_known.height + delta_height,
seen: near_primitives::utils::to_timestamp(Clock::utc()),
seen: near_primitives::utils::to_timestamp(Clock::utc()) + timestamp_delta_ns,
};

self.client.chain.mut_store().save_latest_known(new_latest_known.clone())?;
Expand Down
9 changes: 7 additions & 2 deletions chain/client/src/test_utils.rs
Expand Up @@ -66,6 +66,11 @@ use near_primitives::utils::MaybeValidated;

pub type PeerManagerMock = Mocker<PeerManagerActor>;

/// min block production time in milliseconds
pub const MIN_BLOCK_PROD_TIME: u64 = 100;
ChaoticTempest marked this conversation as resolved.
Show resolved Hide resolved
/// max block production time in milliseconds
pub const MAX_BLOCK_PROD_TIME: u64 = 200;

const TEST_SEED: RngSeed = [3; 32];
/// Sets up ClientActor and ViewClientActor viewing the same store/runtime.
pub fn setup(
Expand Down Expand Up @@ -284,8 +289,8 @@ pub fn setup_mock_with_validity_period_and_no_epoch_sync(
5,
account_id,
skip_sync_wait,
100,
200,
MIN_BLOCK_PROD_TIME,
MAX_BLOCK_PROD_TIME,
enable_doomslug,
false,
false,
Expand Down
1 change: 1 addition & 0 deletions chain/epoch_manager/Cargo.toml
Expand Up @@ -28,6 +28,7 @@ near-store = { path = "../../core/store" }
near-chain-configs = { path = "../../core/chain-configs" }

[features]
sandbox = []
expensive_tests = []
protocol_feature_chunk_only_producers = [
"near-primitives/protocol_feature_chunk_only_producers",
Expand Down
74 changes: 73 additions & 1 deletion chain/epoch_manager/src/lib.rs
Expand Up @@ -382,7 +382,8 @@ impl EpochManager {
let validator_stake =
epoch_info.validators_iter().map(|r| r.account_and_stake()).collect::<HashMap<_, _>>();
let next_epoch_id = self.get_next_epoch_id_from_info(block_info)?;
let next_epoch_info = self.get_epoch_info(&next_epoch_id)?.clone();
#[allow(unused_mut)] // unused mut when not in sandbox
let mut next_epoch_info = self.get_epoch_info(&next_epoch_id)?.clone();
self.save_epoch_validator_info(store_update, block_info.epoch_id(), &epoch_summary)?;

let EpochSummary {
Expand Down Expand Up @@ -410,6 +411,19 @@ impl EpochManager {
)
};
let next_next_epoch_config = self.config.for_protocol_version(next_version);
#[cfg(feature = "sandbox")]
{
// explaination on calculation:
// (block_height / length) gives us epoch_height, but we index from 1, so +1 at the end.
// Note, this is a retroactive update for sandbox since we can receive fast-forward
// requests in the middle of an epoch. block_height is different based on when it gets
// called. When fast-forwarded, we need to retroactively adjust the epoch height, but
// with a normal `finalize_epoch`, it's the start of the epoch's next block height, so
// (height - 1) is needed after adding +1 at the end.
ChaoticTempest marked this conversation as resolved.
Show resolved Hide resolved
*next_epoch_info.epoch_height_mut() =
((block_info.height() - 1) / next_next_epoch_config.epoch_length) + 1;
}

let next_next_epoch_info = match proposals_to_epoch_info(
next_next_epoch_config,
rng_seed,
Expand Down Expand Up @@ -445,6 +459,13 @@ impl EpochManager {
// This epoch info is computed for the epoch after next (T+2),
// where epoch_id of it is the hash of last block in this epoch (T).
self.save_epoch_info(store_update, &next_next_epoch_id, next_next_epoch_info)?;

// Store next_epoch info again since sandbox changes it. Need to retroactively update
// since a fast-forward request can happen in the middle of an epoch.
if cfg!(feature = "sandbox") {
self.save_epoch_info(store_update, &next_epoch_id, next_epoch_info)?;
ChaoticTempest marked this conversation as resolved.
Show resolved Hide resolved
}

Ok(())
}

Expand Down Expand Up @@ -4006,4 +4027,55 @@ mod tests2 {
epoch_manager.epoch_validators_ordered_unique.get(&epoch_id).unwrap().clone();
assert_eq!(epoch_validators_unique, epoch_validators_unique_in_cache);
}

#[test]
#[cfg(feature = "sandbox")]
fn test_sandbox_epoch_height_change() {
use near_primitives::types::BlockHeightDelta;
const EPOCH_LENGTH: BlockHeightDelta = 5;
const BLOCKS_TO_FAST_FORWARD: BlockHeightDelta = 1000;

// setup epoch manager
let amount_staked = 1_000_000;
let validators = vec![
("test1".parse().unwrap(), amount_staked),
("test2".parse().unwrap(), amount_staked),
];
let mut epoch_manager =
setup_default_epoch_manager(validators, EPOCH_LENGTH, 1, 10, 0, 90, 60);
let h = hash_range(11);
record_block(&mut epoch_manager, CryptoHash::default(), h[0], 0, vec![]);

// Initially record 7 blocks without any fast-forwarding:
for i in 1..8 {
record_block(&mut epoch_manager, h[i - 1], h[i], i as u64, vec![]);
}

// First two epoch heights should be 1 and 2
let epoch_info = epoch_manager.get_epoch_info(&EpochId(h[0])).unwrap().clone();
assert_eq!(epoch_info.epoch_height(), 1);
let epoch_info = epoch_manager.get_epoch_info(&EpochId(h[5])).unwrap().clone();
assert_eq!(epoch_info.epoch_height(), 2);

// Then record some fast-forwarded blocks, starting in the middle of an epoch:
for i in 8..11 {
record_block(
&mut epoch_manager,
h[i - 1],
h[i],
i as u64 + BLOCKS_TO_FAST_FORWARD,
vec![],
);
}

// Fast-forward in the middle of an epoch should reflect in that the epoch height got updated:
let epoch_info = epoch_manager.get_epoch_info(&EpochId(h[5])).unwrap().clone();
let expected_epoch_height = (5 + BLOCKS_TO_FAST_FORWARD) / EPOCH_LENGTH + 1;
assert_eq!(epoch_info.epoch_height(), expected_epoch_height);
// Subsequent one should just be additonal prev epoch height + 1
// NOTE: that the requests happens on the 8th recorded block because that's when the sandbox
// request to fast-forward would have been sent.
let epoch_info = epoch_manager.get_epoch_info(&EpochId(h[8])).unwrap().clone();
assert_eq!(epoch_info.epoch_height(), expected_epoch_height + 1);
}
}
52 changes: 42 additions & 10 deletions integration-tests/src/tests/client/sandbox.rs
@@ -1,13 +1,13 @@
use std::path::Path;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;

use actix::System;

use near_actix_test_utils::run_actix;
use near_chain::{ChainGenesis, Provenance, RuntimeAdapter};
use near_chain_configs::Genesis;
use near_client::test_utils::{setup_mock, TestEnv};
use near_client::test_utils::{setup_mock, TestEnv, MAX_BLOCK_PROD_TIME, MIN_BLOCK_PROD_TIME};
use near_crypto::{InMemorySigner, KeyType};
use near_logger_utils::init_test_logger;
use near_network::types::{
Expand Down Expand Up @@ -119,36 +119,68 @@ fn test_patch_account() {

#[test]
fn test_fast_forward() {
const BLOCKS_TO_FASTFORWARD: BlockHeight = 10_000;
const BLOCKS_TO_PRODUCE: u64 = 20;

// the 1_000_000 is to convert milliseconds to nanoseconds, where MIN/MAX_BLOCK_PROD_TIME are in milliseconds:
const FAST_FORWARD_DELTA: u64 = (BLOCKS_TO_FASTFORWARD + BLOCKS_TO_PRODUCE) * 1_000_000;

init_test_logger();
run_actix(async {
let count = Arc::new(AtomicUsize::new(0));
let first_block_timestamp = Arc::new(AtomicU64::new(0));

// Produce 20 blocks
let (client, _view_client) = setup_mock(
let (_client, _view_client) = setup_mock(
vec!["test".parse().unwrap()],
"test".parse().unwrap(),
true,
false,
Box::new(move |msg, _ctx, _| {
Box::new(move |msg, _ctx, client| {
if let NetworkRequests::Block { block } = msg.as_network_requests_ref() {
let height = block.header().height();
let timestamp = block.header().raw_timestamp();

count.fetch_add(1, Ordering::Relaxed);
if count.load(Ordering::Relaxed) >= 20 {
let at = count.load(Ordering::Relaxed);

// Fast forward by 10,000 blocks after the first block produced:
if at == 1 {
first_block_timestamp.store(timestamp, Ordering::Relaxed);
client.do_send(NetworkClientMessages::Sandbox(
NetworkSandboxMessage::SandboxFastForward(BLOCKS_TO_FASTFORWARD),
));
}

// Check at final block if timestamp and block height matches up:
if at >= BLOCKS_TO_PRODUCE as usize {
let before_forward = first_block_timestamp.load(Ordering::Relaxed);
let min_forwarded_time =
before_forward + FAST_FORWARD_DELTA * MIN_BLOCK_PROD_TIME;
let max_forwarded_time =
before_forward + FAST_FORWARD_DELTA * MAX_BLOCK_PROD_TIME;
assert!(
height >= 10000,
height >= BLOCKS_TO_FASTFORWARD,
"Was not able to fast forward. Current height: {}",
height
);

// Check if final block timestamp is within fast forwarded time min and max:
assert!(
(min_forwarded_time..max_forwarded_time).contains(&timestamp),
"Forwarded timestamp {} is not within expected range ({},{})",
timestamp,
min_forwarded_time,
max_forwarded_time
);

ChaoticTempest marked this conversation as resolved.
Show resolved Hide resolved
System::current().stop();
}
}
PeerManagerMessageResponse::NetworkResponses(NetworkResponses::NoResponse)
}),
);

// Fast forward by 10,000 blocks:
client.do_send(NetworkClientMessages::Sandbox(NetworkSandboxMessage::SandboxFastForward(
10000,
)));
near_network::test_utils::wait_or_panic(5000);
});
}
1 change: 1 addition & 0 deletions nearcore/Cargo.toml
Expand Up @@ -147,4 +147,5 @@ sandbox = [
"near-client/sandbox",
"node-runtime/sandbox",
"near-jsonrpc/sandbox",
"near-epoch-manager/sandbox",
]