Skip to content

Commit

Permalink
2.4.0 cherry pick (#12453)
Browse files Browse the repository at this point in the history
Co-authored-by: Jan Malinowski <jan.ciolek@nearone.org>
Co-authored-by: Anton Puhach <anton@nearone.org>
Co-authored-by: Ivan Frolov <59515280+frolvanya@users.noreply.github.com>
  • Loading branch information
4 people authored Nov 13, 2024
1 parent 82707e8 commit 1b2565b
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 27 deletions.
23 changes: 19 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,28 @@
## [unreleased]

### Protocol Changes

* Fixing invalid cost used for wasm_yield_resume_byte. #12192
* Relaxing Congestion Control to allow accepting and buffering more transactions. #12241
**No Changes**

### Non-protocol Changes
**No Changes**

## [2.4.0]

### Protocol Changes

* Fixing invalid cost used for `wasm_yield_resume_byte`. [#12192](https://github.com/near/nearcore/pull/12192)
* Relaxing Congestion Control to allow accepting and buffering more transactions. [#12241](https://github.com/near/nearcore/pull/12241) [#12430](https://github.com/near/nearcore/pull/12430)
* Exclude contract code out of state witness and distribute it separately. [#11099](https://github.com/near/nearcore/issues/11099)

### Non-protocol Changes
* **Epoch Sync V4**: A capability to bootstrap a node from another active node. [#73](https://github.com/near/near-one-project-tracking/issues/73)
* **Decentralized state sync**: Before, nodes that needed to download state
(either because they're several epochs behind the chain or because they're going to start producing chunks for a shard they don't currently track)
would download them from a centralized GCS bucket. Now, nodes will attempt to download pieces of the state from peers in the network,
and only fallback to downloading from GCS if that fails. Please note that in order to participate in providing state parts to peers,
your node may generate snapshots of the state. These snapshots should not take too much space,
since they're hard links to database files that get cleaned up on every epoch. [#12004](https://github.com/near/nearcore/issues/12004)

## [2.3.0]

### Protocol Changes
Expand All @@ -18,7 +33,7 @@
### Non-protocol Changes
* Added [documentation](./docs/misc/archival_data_recovery.md) and a [reference](./scripts/recover_missing_archival_data.sh) script to recover the data lost in archival nodes at the beginning of 2024.
* **Archival nodes only:** Stop saving partial chunks to `PartialChunks` column in the Cold DB. Instead, archival nodes will reconstruct partial chunks from the `Chunks` column.
* Decentralized state sync: Before, nodes that needed to download state (either because they're several epochs behind the chain or because they're going to start producing chunks for a shard they don't currently track) would download them from a centralized GCS bucket. Now, nodes will attempt to download pieces of the state from peers in the network, and only fallback to downloading from GCS if that fails. Please note that in order to participate in providing state parts to peers, your node may generate snapshots of the state. These snapshots should not take too much space, since they're hard links to database files that get cleaned up on every epoch.
* Enabled state snapshots on every epoch to allow the nodes to take part in decentralized state sync in future releases.

### 2.2.1

Expand Down
9 changes: 9 additions & 0 deletions chain/chain/src/stateless_validation/chunk_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,15 @@ pub fn pre_validate_chunk_state_witness(
)));
}

let (new_tx_root_from_state_witness, _) = merklize(&state_witness.new_transactions);
let chunk_tx_root = state_witness.chunk_header.tx_root();
if new_tx_root_from_state_witness != chunk_tx_root {
return Err(Error::InvalidChunkStateWitness(format!(
"Witness new transactions root {:?} does not match chunk {:?}",
new_tx_root_from_state_witness, chunk_tx_root
)));
}

// Verify that all proposed transactions are valid.
let new_transactions = &state_witness.new_transactions;
if !new_transactions.is_empty() {
Expand Down
4 changes: 4 additions & 0 deletions chain/jsonrpc/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 2.4.0

* Introduced a new status code for a missing block - 422 Unprocessable Content
> Block is considered as missing if rpc returned `UNKNOWN_BLOCK` error while requested block height is less than the latest block height
## 2.3.0

Expand Down
44 changes: 38 additions & 6 deletions chain/jsonrpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub use near_jsonrpc_client as client;
pub use near_jsonrpc_primitives as primitives;
use near_jsonrpc_primitives::errors::{RpcError, RpcErrorKind};
use near_jsonrpc_primitives::message::{Message, Request};
use near_jsonrpc_primitives::types::blocks::RpcBlockRequest;
use near_jsonrpc_primitives::types::config::{RpcProtocolConfigError, RpcProtocolConfigResponse};
use near_jsonrpc_primitives::types::entity_debug::{EntityDebugHandler, EntityQueryWithParams};
use near_jsonrpc_primitives::types::query::RpcQueryRequest;
Expand Down Expand Up @@ -1354,21 +1355,51 @@ impl JsonRpcHandler {
}
}

async fn handle_unknown_block(
request: Message,
handler: web::Data<JsonRpcHandler>,
) -> actix_web::HttpResponseBuilder {
let Message::Request(request) = request else {
return HttpResponse::Ok();
};

let Some(block_id) = request.params.get("block_id") else {
return HttpResponse::Ok();
};

let Some(block_height) = block_id.as_u64() else {
return HttpResponse::Ok();
};

let Ok(latest_block) =
handler.block(RpcBlockRequest { block_reference: BlockReference::latest() }).await
else {
return HttpResponse::Ok();
};

if block_height < latest_block.block_view.header.height {
return HttpResponse::UnprocessableEntity();
}

HttpResponse::Ok()
}

async fn rpc_handler(
message: web::Json<Message>,
request: web::Json<Message>,
handler: web::Data<JsonRpcHandler>,
) -> HttpResponse {
let message = handler.process(message.0).await;
let message = handler.process(request.0.clone()).await;

let mut response = if let Message::Response(response) = &message {
match &response.result {
Ok(_) => HttpResponse::Ok(),
Err(err) => match &err.error_struct {
Some(RpcErrorKind::RequestValidationError(_)) => HttpResponse::BadRequest(),
Some(RpcErrorKind::HandlerError(error_struct)) => {
if error_struct["name"] == "TIMEOUT_ERROR" {
HttpResponse::RequestTimeout()
} else {
HttpResponse::Ok()
match error_struct.get("name").and_then(|name| name.as_str()) {
Some("UNKNOWN_BLOCK") => handle_unknown_block(request.0, handler).await,
Some("TIMEOUT_ERROR") => HttpResponse::RequestTimeout(),
_ => HttpResponse::Ok(),
}
}
Some(RpcErrorKind::InternalError(_)) => HttpResponse::InternalServerError(),
Expand All @@ -1378,6 +1409,7 @@ async fn rpc_handler(
} else {
HttpResponse::InternalServerError()
};

response.json(message)
}

Expand Down
2 changes: 2 additions & 0 deletions nightly/pytest-sanity.txt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ pytest sanity/sync_chunks_from_archival.py
pytest sanity/sync_chunks_from_archival.py --features nightly
pytest sanity/rpc_tx_forwarding.py
pytest sanity/rpc_tx_forwarding.py --features nightly
pytest sanity/rpc_missing_block.py
pytest sanity/rpc_missing_block.py --features nightly
pytest --timeout=240 sanity/one_val.py
pytest --timeout=240 sanity/one_val.py nightly --features nightly
pytest --timeout=240 sanity/lightclnt.py
Expand Down
22 changes: 16 additions & 6 deletions pytest/tests/sanity/replay_chain_from_archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import cluster
import simple_test
import state_sync_lib
from geventhttpclient import useragent

NUM_VALIDATORS = 2
EPOCH_LENGTH = 10
Expand Down Expand Up @@ -109,12 +110,21 @@ def test(self):
# Sanity check: Validators cannot return old blocks after GC (eg. genesis block) but archival node can.
logger.info("Running sanity check for archival node")
for node in self.nodes:
result = node.get_block_by_height(GC_BLOCKS_LIMIT - 1)
if node == self.archival_node:
assert 'error' not in result, result
assert result['result']['header']['hash'] is not None, result
else:
assert 'error' in result, result
try:
result = node.get_block_by_height(GC_BLOCKS_LIMIT - 1)
if node == self.archival_node:
assert "error" not in result, result
assert result["result"]["header"][
"hash"] is not None, result
else:
assert "error" in result, result
except useragent.BadStatusCode as e:
assert "code=422" in str(
e), f"Expected status code 422 in exception, got: {e}"
except Exception as e:
assert (
False
), f"Unexpected exception type raised: {type(e)}. Exception: {e}"

# Capture the last height to replay before killing the nodes.
end_height = self.testcase.wait_for_blocks(1)
Expand Down
103 changes: 103 additions & 0 deletions pytest/tests/sanity/rpc_missing_block.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import sys, time
import pathlib

sys.path.append(str(pathlib.Path(__file__).resolve().parents[2] / "lib"))

from cluster import start_cluster
from configured_logger import logger
import utils
from geventhttpclient import useragent

nodes = start_cluster(
num_nodes=4,
num_observers=1,
num_shards=4,
config=None,
extra_state_dumper=True,
genesis_config_changes=[
["min_gas_price", 0],
["max_inflation_rate", [0, 1]],
["epoch_length", 10],
["block_producer_kickout_threshold", 70],
],
client_config_changes={
0: {
"consensus": {
"state_sync_timeout": {
"secs": 2,
"nanos": 0
}
}
},
1: {
"consensus": {
"state_sync_timeout": {
"secs": 2,
"nanos": 0
}
}
},
2: {
"consensus": {
"state_sync_timeout": {
"secs": 2,
"nanos": 0
}
}
},
3: {
"consensus": {
"state_sync_timeout": {
"secs": 2,
"nanos": 0
}
}
},
4: {
"consensus": {
"state_sync_timeout": {
"secs": 2,
"nanos": 0
}
},
"tracked_shards": [0, 1, 2, 3],
},
},
)

nodes[1].kill()


def check_bad_block(node, height):
try:
node.get_block_by_height(height)
assert False, f"Expected an exception for block height {height} but none was raised"
except useragent.BadStatusCode as e:
assert "code=422" in str(
e), f"Expected status code 422 in exception, got: {e}"
except Exception as e:
assert False, f"Unexpected exception type raised: {type(e)}. Exception: {e}"


last_height = -1

for height, hash in utils.poll_blocks(nodes[0]):
if height >= 20:
break

response = nodes[0].get_block_by_height(height)
assert not "error" in response
logger.info(f"good RPC response for: {height}")

if last_height != -1:
for bad_height in range(last_height + 1, height):
response = check_bad_block(nodes[0], bad_height)
logger.info(f"422 response for: {bad_height}")

last_height = height

response = nodes[0].get_block_by_height(last_height + 9999)
assert "error" in response, f"Expected an error for block height 9999, got: {response}"
assert (
response["error"]["cause"]["name"] == "UNKNOWN_BLOCK"
), f"Expected UNKNOWN_BLOCK error, got: {response['error']['cause']['name']}"
7 changes: 6 additions & 1 deletion runtime/runtime/src/congestion_control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,12 +282,17 @@ impl ReceiptSinkV2<'_> {
fn try_forward(
receipt: Receipt,
gas: u64,
size: u64,
mut size: u64,
shard: ShardId,
outgoing_limit: &mut HashMap<ShardId, OutgoingLimit>,
outgoing_receipts: &mut Vec<Receipt>,
apply_state: &ApplyState,
) -> Result<ReceiptForwarding, RuntimeError> {
let max_receipt_size = apply_state.config.wasm_config.limit_config.max_receipt_size;
if size > max_receipt_size {
size = max_receipt_size;
}

// Default case set to `Gas::MAX`: If no outgoing limit was defined for the receiving
// shard, this usually just means the feature is not enabled. Or, it
// could be a special case during resharding events. Or even a bug. In
Expand Down
3 changes: 2 additions & 1 deletion runtime/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ use std::cmp::max;
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::Arc;
use tracing::{debug, instrument};
use verifier::validate_new_receipt;

mod actions;
pub mod adapter;
Expand Down Expand Up @@ -660,7 +661,7 @@ impl Runtime {
)?;
if new_result.result.is_ok() {
if let Err(e) = new_result.new_receipts.iter().try_for_each(|receipt| {
validate_receipt(
validate_new_receipt(
&apply_state.config.wasm_config.limit_config,
receipt,
apply_state.current_protocol_version,
Expand Down
26 changes: 17 additions & 9 deletions runtime/runtime/src/verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,15 +294,6 @@ pub(crate) fn validate_receipt(
receipt: &Receipt,
current_protocol_version: ProtocolVersion,
) -> Result<(), ReceiptValidationError> {
let receipt_size: u64 =
borsh::to_vec(receipt).unwrap().len().try_into().expect("Can't convert usize to u64");
if receipt_size > limit_config.max_receipt_size {
return Err(ReceiptValidationError::ReceiptSizeExceeded {
size: receipt_size,
limit: limit_config.max_receipt_size,
});
}

// We retain these checks here as to maintain backwards compatibility
// with AccountId validation since we illegally parse an AccountId
// in near-vm-logic/logic.rs#fn(VMLogic::read_and_parse_account_id)
Expand All @@ -325,6 +316,23 @@ pub(crate) fn validate_receipt(
}
}

pub(crate) fn validate_new_receipt(
limit_config: &LimitConfig,
receipt: &Receipt,
current_protocol_version: ProtocolVersion,
) -> Result<(), ReceiptValidationError> {
let receipt_size: u64 =
borsh::to_vec(receipt).unwrap().len().try_into().expect("Can't convert usize to u64");
if receipt_size > limit_config.max_receipt_size {
return Err(ReceiptValidationError::ReceiptSizeExceeded {
size: receipt_size,
limit: limit_config.max_receipt_size,
});
}

validate_receipt(limit_config, receipt, current_protocol_version)
}

/// Validates given ActionReceipt. Checks validity of the number of input data dependencies and all actions.
fn validate_action_receipt(
limit_config: &LimitConfig,
Expand Down

0 comments on commit 1b2565b

Please sign in to comment.