diff --git a/docs/rpc-endpoints.md b/docs/rpc-endpoints.md index 581004612f..9f722273f8 100644 --- a/docs/rpc-endpoints.md +++ b/docs/rpc-endpoints.md @@ -45,7 +45,7 @@ Returns JSON data in the form: ``` { "data": "01ce...", - "marfProof": "01ab...", + "proof": "01ab...", } ``` @@ -54,7 +54,7 @@ for non-existent values, this is a serialized `none`, and for all other response object. This endpoint also accepts a querystring parameter `?proof=` which when supplied `0`, will return the -JSON object _without_ the `marfProof` field. +JSON object _without_ the `proof` field. ### GET /v2/fees/transfer @@ -213,7 +213,20 @@ This returns a JSON object of the form: ### GET /v2/contracts/source/[Stacks Address]/[Contract Name] -Fetch the source for a smart contract. Returned as a JSON string. +Fetch the source for a smart contract, along with the block height it was +published in, and the MARF proof for the data. + +``` +{ + "source": "(define-private ...", + "publishHeight": 1, + "proof": "00213..." +} +``` + +This endpoint also accepts a querystring parameter `?proof=` which +when supplied `0`, will return the JSON object _without_ the `proof` +field. ### POST /v2/contracts/call-read/[Stacks Address]/[Contract Name]/[Function Name] diff --git a/src/net/http.rs b/src/net/http.rs index f4c9b627eb..6c50f465e3 100644 --- a/src/net/http.rs +++ b/src/net/http.rs @@ -1213,14 +1213,10 @@ impl HttpRequestType { Ok(HttpRequestType::GetTransferCost(HttpRequestMetadata::from_preamble(preamble))) } - fn parse_get_account(_protocol: &mut StacksHttp, preamble: &HttpRequestPreamble, captures: &Captures, query: Option<&str>, _fd: &mut R) -> Result { - if preamble.get_content_length() != 0 { - return Err(net_error::DeserializeError("Invalid Http request: expected 0-length body for GetAccount".to_string())); - } - - let principal = PrincipalData::parse(&captures["principal"]) - .map_err(|_e| net_error::DeserializeError("Failed to parse account principal".into()))?; - + /// check whether the given option query string + /// sets proof=0 (setting proof to false). + /// Defaults to _true_ + fn get_proof_query(query: Option<&str>) -> bool { let no_proof = if let Some(query_string) = query { form_urlencoded::parse(query_string.as_bytes()) .find(|(key, _v)| key == "proof") @@ -1229,7 +1225,19 @@ impl HttpRequestType { } else { false }; - let with_proof = !no_proof; + + !no_proof + } + + fn parse_get_account(_protocol: &mut StacksHttp, preamble: &HttpRequestPreamble, captures: &Captures, query: Option<&str>, _fd: &mut R) -> Result { + if preamble.get_content_length() != 0 { + return Err(net_error::DeserializeError("Invalid Http request: expected 0-length body for GetAccount".to_string())); + } + + let principal = PrincipalData::parse(&captures["principal"]) + .map_err(|_e| net_error::DeserializeError("Failed to parse account principal".into()))?; + + let with_proof = HttpRequestType::get_proof_query(query); Ok(HttpRequestType::GetAccount(HttpRequestMetadata::from_preamble(preamble), principal, with_proof)) } @@ -1258,15 +1266,7 @@ impl HttpRequestType { let value = Value::try_deserialize_hex_untyped(&value_hex) .map_err(|_e| net_error::DeserializeError("Failed to deserialize key value".into()))?; - let no_proof = if let Some(query_string) = query { - form_urlencoded::parse(query_string.as_bytes()) - .find(|(key, _v)| key == "proof") - .map(|(_k, value)| value == "0") - .unwrap_or(false) - } else { - false - }; - let with_proof = !no_proof; + let with_proof = HttpRequestType::get_proof_query(query); Ok(HttpRequestType::GetMapEntry(HttpRequestMetadata::from_preamble(preamble), contract_addr, contract_name, map_name, value, with_proof)) } @@ -1322,9 +1322,10 @@ impl HttpRequestType { .map(|(preamble, addr, name)| HttpRequestType::GetContractABI(preamble, addr, name)) } - fn parse_get_contract_source(_protocol: &mut StacksHttp, preamble: &HttpRequestPreamble, captures: &Captures, _query: Option<&str>, _fd: &mut R) -> Result { + fn parse_get_contract_source(_protocol: &mut StacksHttp, preamble: &HttpRequestPreamble, captures: &Captures, query: Option<&str>, _fd: &mut R) -> Result { + let with_proof = HttpRequestType::get_proof_query(query); HttpRequestType::parse_get_contract_arguments(preamble, captures) - .map(|(preamble, addr, name)| HttpRequestType::GetContractSrc(preamble, addr, name)) + .map(|(preamble, addr, name)| HttpRequestType::GetContractSrc(preamble, addr, name, with_proof)) } fn parse_getblock(_protocol: &mut StacksHttp, preamble: &HttpRequestPreamble, captures: &Captures, _query: Option<&str>, _fd: &mut R) -> Result { @@ -1472,7 +1473,7 @@ impl HttpRequestType { HttpRequestType::GetTransferCost(_md) => "/v2/fees/transfer".into(), HttpRequestType::GetContractABI(_, contract_addr, contract_name) => format!("/v2/contracts/interface/{}/{}", contract_addr, contract_name.as_str()), - HttpRequestType::GetContractSrc(_, contract_addr, contract_name) => + HttpRequestType::GetContractSrc(_, contract_addr, contract_name, _with_proof) => format!("/v2/contracts/source/{}/{}", contract_addr, contract_name.as_str()), HttpRequestType::CallReadOnlyFunction(_, contract_addr, contract_name, _, func_name, ..) => { format!("/v2/contracts/call-read/{}/{}/{}", contract_addr, contract_name.as_str(), func_name.as_str()) diff --git a/src/net/mod.rs b/src/net/mod.rs index c9522b4164..c8f58d047b 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -828,12 +828,22 @@ pub struct HttpRequestMetadata { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct MapEntryResponse { pub data: String, - #[serde(rename = "marfProof")] + #[serde(rename = "proof")] #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub marf_proof: Option } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ContractSrcResponse { + pub source: String, + #[serde(rename = "publishHeight")] + pub publish_height: u32, + #[serde(rename = "proof")] + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub marf_proof: Option +} #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct CallReadOnlyResponse { @@ -913,7 +923,7 @@ pub enum HttpRequestType { CallReadOnlyFunction(HttpRequestMetadata, StacksAddress, ContractName, PrincipalData, ClarityName, Vec), GetTransferCost(HttpRequestMetadata), - GetContractSrc(HttpRequestMetadata, StacksAddress, ContractName), + GetContractSrc(HttpRequestMetadata, StacksAddress, ContractName, bool), GetContractABI(HttpRequestMetadata, StacksAddress, ContractName), } @@ -977,7 +987,7 @@ pub enum HttpResponseType { CallReadOnlyFunction(HttpResponseMetadata, CallReadOnlyResponse), GetAccount(HttpResponseMetadata, AccountEntryResponse), GetContractABI(HttpResponseMetadata, ContractInterface), - GetContractSrc(HttpResponseMetadata, String), + GetContractSrc(HttpResponseMetadata, ContractSrcResponse), // peer-given error responses BadRequest(HttpResponseMetadata, String), BadRequestJSON(HttpResponseMetadata, HashMap), diff --git a/src/net/rpc.rs b/src/net/rpc.rs index 05bfbb3b0c..69aebafe9a 100644 --- a/src/net/rpc.rs +++ b/src/net/rpc.rs @@ -35,7 +35,6 @@ use net::HttpRequestType; use net::HttpResponseType; use net::HttpRequestMetadata; use net::HttpResponseMetadata; -use net::{ MapEntryResponse, AccountEntryResponse, CallReadOnlyResponse }; use net::PeerAddress; use net::PeerInfoData; use net::NeighborAddress; @@ -48,6 +47,7 @@ use net::connection::ConnectionHttp; use net::connection::ReplyHandleHttp; use net::connection::ConnectionOptions; use net::db::PeerDB; +use net::{ MapEntryResponse, AccountEntryResponse, CallReadOnlyResponse, ContractSrcResponse }; use burnchains::Burnchain; use burnchains::BurnchainView; @@ -81,7 +81,9 @@ use vm::{ types::{ PrincipalData, QualifiedContractIdentifier }, database::{ ClarityDatabase, - ClaritySerializable }, + MarfedKV, + ClaritySerializable, + marf::ContractCommitment }, }; use rand::prelude::*; @@ -461,13 +463,23 @@ impl ConversationHttp { fn handle_get_contract_src(http: &mut StacksHttp, fd: &mut W, req: &HttpRequestType, chainstate: &mut StacksChainState, cur_burn: &BurnchainHeaderHash, cur_block: &BlockHeaderHash, - contract_addr: &StacksAddress, contract_name: &ContractName) -> Result<(), net_error> { + contract_addr: &StacksAddress, contract_name: &ContractName, with_proof: bool) -> Result<(), net_error> { let response_metadata = HttpResponseMetadata::from(req); let contract_identifier = QualifiedContractIdentifier::new(contract_addr.clone().into(), contract_name.clone()); let data = chainstate.with_read_only_clarity_tx(cur_burn, cur_block, |clarity_tx| { clarity_tx.with_clarity_db_readonly(|db| { - db.get_contract_src(&contract_identifier) + let source = db.get_contract_src(&contract_identifier)?; + let contract_commit_key = MarfedKV::make_contract_hash_key(&contract_identifier); + let (contract_commit, proof) = db.get_with_proof::(&contract_commit_key) + .expect("BUG: obtained source, but couldn't get MARF proof."); + let marf_proof = if with_proof { + Some(proof.to_hex()) + } else { + None + }; + let publish_height = contract_commit.block_height; + Some(ContractSrcResponse { source, publish_height, marf_proof }) }) }); @@ -606,10 +618,10 @@ impl ConversationHttp { } None }, - HttpRequestType::GetContractSrc(ref _md, ref contract_addr, ref contract_name) => { + HttpRequestType::GetContractSrc(ref _md, ref contract_addr, ref contract_name, ref with_proof) => { if let Some((burn_block, block)) = ConversationHttp::handle_load_stacks_chain_tip(&mut self.connection.protocol, &mut reply, &req, burndb)? { ConversationHttp::handle_get_contract_src(&mut self.connection.protocol, &mut reply, &req, chainstate, &burn_block, &block, - contract_addr, contract_name)?; + contract_addr, contract_name, *with_proof)?; } None }, diff --git a/src/vm/database/marf.rs b/src/vm/database/marf.rs index d6f2a91762..55d7215cae 100644 --- a/src/vm/database/marf.rs +++ b/src/vm/database/marf.rs @@ -2,7 +2,8 @@ use std::path::PathBuf; use vm::types::{QualifiedContractIdentifier}; use vm::errors::{InterpreterError, CheckErrors, InterpreterResult as Result, IncomparableError, RuntimeErrorType}; -use vm::database::{SqliteConnection, ClarityDatabase, HeadersDB, NULL_HEADER_DB}; +use vm::database::{SqliteConnection, ClarityDatabase, HeadersDB, NULL_HEADER_DB, + ClaritySerializable, ClarityDeserializable}; use vm::analysis::{AnalysisDatabase}; use chainstate::stacks::index::marf::MARF; use chainstate::stacks::index::{MARFValue, Error as MarfError, TrieHash}; @@ -99,16 +100,19 @@ pub trait ClarityBackingStore { } } -struct ContractCommitment { +pub struct ContractCommitment { pub hash: Sha512Trunc256Sum, pub block_height: u32 } -impl ContractCommitment { - pub fn serialize(&self) -> String { +impl ClaritySerializable for ContractCommitment { + fn serialize(&self) -> String { format!("{}{}", self.hash, to_hex(&self.block_height.to_be_bytes())) } - pub fn deserialize(input: &str) -> ContractCommitment { +} + +impl ClarityDeserializable for ContractCommitment { + fn deserialize(input: &str) -> ContractCommitment { assert_eq!(input.len(), 72); let hash = Sha512Trunc256Sum::from_hex(&input[0..64]).expect("Hex decode fail."); let height_bytes = hex_bytes(&input[64..72]).expect("Hex decode fail."); diff --git a/src/vm/tests/integrations.rs b/src/vm/tests/integrations.rs index fc750d6cbd..05cc367618 100644 --- a/src/vm/tests/integrations.rs +++ b/src/vm/tests/integrations.rs @@ -12,7 +12,7 @@ use chainstate::stacks::{ use chainstate::burn::VRFSeed; use burnchains::Address; use address::AddressHashMode; -use net::{Error as NetError, StacksMessageCodec, AccountEntryResponse, CallReadOnlyRequestBody}; +use net::{Error as NetError, StacksMessageCodec, AccountEntryResponse, ContractSrcResponse, CallReadOnlyRequestBody}; use util::{log, strings::StacksString, hash::hex_bytes, hash::to_hex}; use std::collections::HashMap; use util::db::{DBConn, FromRow}; @@ -359,7 +359,7 @@ fn integration_test_get_info() { let result_data = Value::try_deserialize_hex_untyped(&res["data"]).unwrap(); let expected_data = chain_state.clarity_eval_read_only(bhh, &contract_identifier, "(some (get-exotic-data-info u1))"); - assert!(res.get("marfProof").is_some()); + assert!(res.get("proof").is_some()); assert_eq!(result_data, expected_data); @@ -389,7 +389,7 @@ fn integration_test_get_info() { .send() .unwrap().json::>().unwrap(); - assert!(res.get("marfProof").is_none()); + assert!(res.get("proof").is_none()); let result_data = Value::try_deserialize_hex_untyped(&res["data"]).unwrap(); let expected_data = chain_state.clarity_eval_read_only(bhh, &contract_identifier, "(some (get-exotic-data-info u1))"); @@ -410,7 +410,7 @@ fn integration_test_get_info() { .send() .unwrap().json::>().unwrap(); - assert!(res.get("marfProof").is_some()); + assert!(res.get("proof").is_some()); let result_data = Value::try_deserialize_hex_untyped(&res["data"]).unwrap(); let expected_data = chain_state.clarity_eval_read_only(bhh, &contract_identifier, "(some (get-exotic-data-info u1))"); @@ -505,9 +505,20 @@ fn integration_test_get_info() { let path = format!("{}/v2/contracts/source/{}/{}", &http_origin, &contract_addr, "get-info"); eprintln!("Test: GET {}", path); - let res = client.get(&path).send().unwrap().json::().unwrap(); + let res = client.get(&path).send().unwrap().json::().unwrap(); - assert_eq!(res, GET_INFO_CONTRACT); + assert_eq!(res.source, GET_INFO_CONTRACT); + assert_eq!(res.publish_height, 2); + assert!(res.marf_proof.is_some()); + + + let path = format!("{}/v2/contracts/source/{}/{}?proof=0", &http_origin, &contract_addr, "get-info"); + eprintln!("Test: GET {}", path); + let res = client.get(&path).send().unwrap().json::().unwrap(); + + assert_eq!(res.source, GET_INFO_CONTRACT); + assert_eq!(res.publish_height, 2); + assert!(res.marf_proof.is_none()); // a missing one?