diff --git a/packages/rs-sdk/src/platform/transition/broadcast.rs b/packages/rs-sdk/src/platform/transition/broadcast.rs index 21b784b3b3..e7217e4deb 100644 --- a/packages/rs-sdk/src/platform/transition/broadcast.rs +++ b/packages/rs-sdk/src/platform/transition/broadcast.rs @@ -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] @@ -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(), @@ -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) = >::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"); diff --git a/packages/rs-sdk/src/sdk.rs b/packages/rs-sdk/src/sdk.rs index c24c0379e9..6673ba4baa 100644 --- a/packages/rs-sdk/src/sdk.rs +++ b/packages/rs-sdk/src/sdk.rs @@ -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 @@ -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()