Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 42 additions & 53 deletions packages/rs-sdk/src/platform/transition/broadcast.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
use super::broadcast_request::BroadcastRequestForStateTransition;
use super::put_settings::PutSettings;
use crate::error::StateTransitionBroadcastError;
use crate::platform::block_info_from_metadata::block_info_from_metadata;
use crate::sync::retry;
use crate::{Error, Sdk};
use dapi_grpc::platform::v0::wait_for_state_transition_result_response::wait_for_state_transition_result_response_v0;
use dapi_grpc::platform::v0::{
wait_for_state_transition_result_response, Proof, WaitForStateTransitionResultResponse,
wait_for_state_transition_result_response, BroadcastStateTransitionRequest,
WaitForStateTransitionResultResponse,
};
use dapi_grpc::platform::VersionedGrpcResponse;
use dash_context_provider::ContextProviderError;
use dpp::state_transition::proof_result::StateTransitionProofResult;
use dpp::state_transition::StateTransition;
use drive::drive::Drive;
use drive_proof_verifier::DataContractProvider;
use drive_proof_verifier::FromProof;
use rs_dapi_client::WrapToExecutionResult;
use rs_dapi_client::{DapiRequest, ExecutionError, InnerInto, IntoInner, RequestSettings};
use rs_dapi_client::{ExecutionResponse, WrapToExecutionResult};
use tracing::{trace, warn};

#[async_trait::async_trait]
Expand Down Expand Up @@ -150,26 +148,6 @@ impl BroadcastStateTransition for StateTransition {
.wrap_to_execution_result(&response);
}

trace!("wait: extracting metadata");
let metadata = grpc_response
.metadata()
.wrap_to_execution_result(&response)?
.inner;
let block_info = block_info_from_metadata(metadata)
.wrap_to_execution_result(&response)?
.inner;
trace!(block_info = ?block_info, "wait: block info extracted");

trace!("wait: extracting proof");
let proof: &Proof = (*grpc_response)
.proof()
.wrap_to_execution_result(&response)?
.inner;
trace!(
proof_size = proof.grovedb_proof.len(),
"wait: proof extracted"
);

let context_provider = sdk.context_provider().ok_or(ExecutionError {
inner: Error::from(ContextProviderError::Config(
"Context provider not initialized".to_string(),
Expand All @@ -178,36 +156,47 @@ impl BroadcastStateTransition for StateTransition {
retries: response.retries,
})?;

trace!("wait: verifying proof");
let (_, result) = match Drive::verify_state_transition_was_executed_with_proof(
self,
&block_info,
proof.grovedb_proof.as_slice(),
&context_provider.as_contract_lookup_fn(sdk.version()),
// Verify through the `FromProof` impl: it runs the GroveDB structural check AND
// `verify_tenderdash_proof` (the quorum BLS signature gate) that authenticates
// `metadata`. The request must be reconstructed to feed that verifier.
let request: BroadcastStateTransitionRequest = self
.broadcast_request_for_state_transition()
.wrap_to_execution_result(&response)?
.inner;

trace!("wait: verifying proof and quorum signature");
let (maybe_result, metadata, _proof) = <StateTransitionProofResult as FromProof<
BroadcastStateTransitionRequest,
>>::maybe_from_proof_with_metadata(
request,
grpc_response.clone(),
sdk.network,
sdk.version(),
) {
Ok(r) => Ok(ExecutionResponse {
inner: r,
retries: response.retries,
address: response.address.clone(),
}),
Err(drive::error::Error::Proof(proof_error)) => Err(ExecutionError {
inner: Error::DriveProofError(
proof_error,
proof.grovedb_proof.clone(),
block_info,
),
retries: response.retries,
address: Some(response.address.clone()),
}),
Err(e) => Err(ExecutionError {
inner: e.into(),
retries: response.retries,
address: Some(response.address.clone()),
}),
}?
&context_provider,
)
.map_err(Error::from)
.wrap_to_execution_result(&response)?
.inner;

// The current `FromProof` impl always yields `Some`; this guards only a future
// impl change, so it stays a typed error rather than an unwrap.
let result: StateTransitionProofResult = maybe_result
.ok_or_else(|| {
Error::InvalidProvedResponse(
"state transition result missing from verified proof".to_string(),
)
})
.wrap_to_execution_result(&response)?
.inner;

// `metadata` is quorum-authenticated only after the verification above, so the
// protocol-version ratchet must run here, never before. A `StaleNode` error is
// retryable and prompts another server.
let _: () = sdk
.verify_response_metadata("wait_for_state_transition_result", &metadata)
.wrap_to_execution_result(&response)?
.inner;

trace!("wait: proof verification successful");
trace!(result_variant = %result.to_string(), "wait: result variant");

Expand Down
22 changes: 12 additions & 10 deletions packages/rs-sdk/src/sdk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,10 +421,12 @@ impl Sdk {
/// no network response has been received yet to teach the SDK the real network version.
///
/// The actual network version is learned only *after* proof parsing succeeds, when
/// [`Self::verify_response_metadata()`] processes `metadata.protocol_version`. If the
/// connected network runs an older protocol version **and** proof interpretation differs
/// between that version and `latest()`, the very first request may fail before the SDK can
/// correct itself. Subsequent requests will use the correct version.
/// [`Self::verify_response_metadata()`] processes `metadata.protocol_version`. Because the
/// SDK seeds at the floor ([`DEFAULT_INITIAL_PROTOCOL_VERSION`]), the bootstrap risk is the
/// **newer**-network direction: if the connected network runs a version newer than the floor
/// **and** proof interpretation differs between the floor and that newer version, the very
/// first request may fail before the ratchet lifts the SDK to the network version.
/// Subsequent requests use the ratcheted version.
///
/// This is a known bootstrap limitation. Callers that must guarantee correct version
/// behaviour on the first request should pin the version explicitly via
Expand Down Expand Up @@ -1852,12 +1854,12 @@ mod test {
///
/// The full tampered-*signed*-proof path isn't unit-testable here: it needs a
/// quorum BLS signature, a context provider, and a `FromProof` verifier round-trip.
/// That path's safety rests on `parse_proof_with_metadata_and_proof` running proof
/// verification (the `?`) BEFORE `verify_response_metadata` → `maybe_update_protocol_version`
/// (see the guard comment at that call site). Here we lock in the ratchet's own gates:
/// it must NOT raise the stored version off untrustworthy inputs (unknown / zero / lower),
/// so even a metadata value that slipped past verification can't move the SDK to a bogus
/// protocol version.
/// Both ratchet sites run the `FromProof` verifier (structural + `verify_tenderdash_proof`)
/// BEFORE `verify_response_metadata` → `maybe_update_protocol_version`: the query path via
/// `parse_proof_with_metadata_and_proof`, the broadcast wait-path in `broadcast.rs` (see the
/// guard comments at both call sites). Here we lock in the ratchet's own gates: it must NOT
/// raise the stored version off untrustworthy inputs (unknown / zero / lower), so even a
/// metadata value that slipped past verification can't move the SDK to a bogus version.
#[test]
fn test_ratchet_rejects_unknown_and_non_upward_versions() {
let sdk = SdkBuilder::new_mock()
Expand Down
Loading