diff --git a/bin/floresta-cli/src/main.rs b/bin/floresta-cli/src/main.rs index 92ae76f97..f274f9348 100644 --- a/bin/floresta-cli/src/main.rs +++ b/bin/floresta-cli/src/main.rs @@ -85,8 +85,8 @@ fn do_request(cmd: &Cli, client: Client) -> anyhow::Result { Methods::SendRawTransaction { tx } => { serde_json::to_string_pretty(&client.send_raw_transaction(tx)?)? } - Methods::GetBlockHeader { hash } => { - serde_json::to_string_pretty(&client.get_block_header(hash)?)? + Methods::GetBlockHeader { hash, verbosity } => { + serde_json::to_string_pretty(&client.get_block_header(hash, verbosity)?)? } Methods::LoadDescriptor { desc } => { serde_json::to_string_pretty(&client.load_descriptor(desc)?)? @@ -262,8 +262,17 @@ pub enum Methods { SendRawTransaction { tx: String }, /// Returns the block header for the given block hash - #[command(name = "getblockheader")] - GetBlockHeader { hash: BlockHash }, + #[doc = include_str!("../../../doc/rpc/getblockheader.md")] + #[command( + name = "getblockheader", + about = "Returns the block header for the given block hash", + long_about = Some(include_str!("../../../doc/rpc/getblockheader.md")), + disable_help_subcommand = true + )] + GetBlockHeader { + hash: BlockHash, + verbosity: Option, + }, /// Loads a new descriptor to the watch only wallet #[doc = include_str!("../../../doc/rpc/loaddescriptor.md")] diff --git a/crates/floresta-node/src/json_rpc/blockchain.rs b/crates/floresta-node/src/json_rpc/blockchain.rs index a33e75484..8254e2f90 100644 --- a/crates/floresta-node/src/json_rpc/blockchain.rs +++ b/crates/floresta-node/src/json_rpc/blockchain.rs @@ -15,6 +15,7 @@ use bitcoin::ScriptBuf; use bitcoin::Txid; use bitcoin::VarInt; use corepc_types::v29::GetTxOut; +use corepc_types::v30::GetBlockHeaderVerbose; use corepc_types::v30::GetBlockVerboseOne; use corepc_types::ScriptPubkey; use floresta_chain::extensions::HeaderExt; @@ -24,6 +25,7 @@ use serde_json::json; use serde_json::Value; use tracing::debug; +use super::res::GetBlockHeaderRes; use super::res::GetBlockchainInfoRes; use super::res::GetTxOutProof; use super::res::JsonRpcError; @@ -106,7 +108,7 @@ impl RpcImpl { at: u32, ) -> Result { let hash = provider.get_block_hash(at)?; - let block = provider.get_block_header(hash)?; + let block = provider.get_block_header_inner(hash)?; Ok(block.time) } @@ -182,20 +184,7 @@ impl RpcImpl { return Ok(GetBlockRes::Zero(hex)); } if verbosity == 1 { - let header = &block.header; - let height = header.get_height(&self.chain)?; - let median_time = header.calculate_median_time_past(&self.chain)?; - let chain_work = header.calculate_chain_work(&self.chain)?.to_string_hex(); - let confirmations = header.get_confirmations(&self.chain)? as i64; - let version_hex = header.get_version_hex(); - - let next_block_hash = header - .get_next_block_hash(&self.chain)? - .map(|h| h.to_string()); - - let bits = header.get_bits_hex(); - let difficulty = header.get_difficulty(); - let target = header.get_target_hex(); + let header_fields = self.get_block_header_verbose_inner(&block)?; // Stripped size is the size of the block without witness data // Header + VarInt for number of transactions + sum of base sizes of each transaction @@ -203,10 +192,7 @@ impl RpcImpl { let total_tx_base_size: usize = block.txdata.iter().map(|tx| tx.base_size()).sum(); let stripped_size_bytes = Header::SIZE + tx_count_varint_size + total_tx_base_size; - let stripped_size = Some(stripped_size_bytes as i64); - - let previous_block_hash = (header.prev_blockhash != BlockHash::all_zeros()) - .then_some(header.prev_blockhash.to_string()); + let stripped_size = Some(stripped_size_bytes.try_into()?); let tx = block .txdata @@ -215,26 +201,26 @@ impl RpcImpl { .collect(); let block = GetBlockVerboseOne { - bits, - chain_work, - confirmations, - difficulty, - hash: header.block_hash().to_string(), - height: height as i64, - merkle_root: header.merkle_root.to_string(), - nonce: header.nonce as i64, - previous_block_hash, - size: block.total_size() as i64, - time: header.time as i64, + bits: header_fields.bits, + chain_work: header_fields.chain_work, + confirmations: header_fields.confirmations, + difficulty: header_fields.difficulty, + hash: header_fields.hash, + height: header_fields.height, + merkle_root: header_fields.merkle_root, + nonce: header_fields.nonce, + previous_block_hash: header_fields.previous_block_hash, + size: block.total_size().try_into()?, + time: header_fields.time, tx, - version: header.version.to_consensus(), - version_hex, + version: header_fields.version, + version_hex: header_fields.version_hex, weight: block.weight().to_wu(), - median_time: Some(median_time as i64), - n_tx: block.txdata.len() as i64, - next_block_hash, + median_time: Some(header_fields.median_time), + n_tx: header_fields.n_tx.into(), + next_block_hash: header_fields.next_block_hash, stripped_size, - target, + target: header_fields.target, }; return Ok(GetBlockRes::One(Box::new(block))); @@ -302,10 +288,23 @@ impl RpcImpl { } // getblockheader - pub(super) fn get_block_header(&self, hash: BlockHash) -> Result { - self.chain - .get_block_header(&hash) - .map_err(|_| JsonRpcError::BlockNotFound) + pub(super) async fn get_block_header( + &self, + hash: BlockHash, + verbosity: bool, + ) -> Result { + let header = self.get_block_header_inner(hash)?; + + if !verbosity { + let hex = serialize_hex(&header); + return Ok(GetBlockHeaderRes::Raw(hex)); + } + + let block = self.get_block_inner(hash).await?; + + let get_block_header = self.get_block_header_verbose_inner(&block)?; + + Ok(GetBlockHeaderRes::Verbose(Box::new(get_block_header))) } // getblockstats @@ -320,6 +319,55 @@ impl RpcImpl { // getmempoolinfo // getrawmempool + /// Same as `get_block_header_inner` but verbose. + fn get_block_header_verbose_inner( + &self, + block: &Block, + ) -> Result { + let header = &block.header; + let height = header.get_height(&self.chain)?; + let median_time = header.calculate_median_time_past(&self.chain)?; + let chain_work = header.calculate_chain_work(&self.chain)?.to_string_hex(); + let confirmations = header.get_confirmations(&self.chain)?; + let version_hex = header.get_version_hex(); + + let next_block_hash = header + .get_next_block_hash(&self.chain)? + .map(|h| h.to_string()); + + let bits = header.get_bits_hex(); + let difficulty = header.get_difficulty(); + let target = header.get_target_hex(); + let previous_block_hash = (header.prev_blockhash != BlockHash::all_zeros()) + .then_some(header.prev_blockhash.to_string()); + + Ok(GetBlockHeaderVerbose { + bits, + chain_work, + confirmations: confirmations.into(), + difficulty, + hash: header.block_hash().to_string(), + height: height.into(), + median_time: median_time.into(), + next_block_hash, + version: header.version.to_consensus(), + version_hex, + previous_block_hash, + merkle_root: header.merkle_root.to_string(), + time: header.time.into(), + target, + nonce: header.nonce.into(), + n_tx: block.txdata.len().try_into()?, + }) + } + + /// Helper method to get a block header by its hash, used by multiple rpcs. + fn get_block_header_inner(&self, hash: BlockHash) -> Result { + self.chain + .get_block_header(&hash) + .map_err(|_| JsonRpcError::BlockNotFound) + } + /// Check if the script is anchor type fn is_anchor_type(script: &Script) -> bool { script.as_bytes().starts_with(&[0x51, 0x02, 0x4e, 0x73]) diff --git a/crates/floresta-node/src/json_rpc/res.rs b/crates/floresta-node/src/json_rpc/res.rs index d62faf411..5710cc1d2 100644 --- a/crates/floresta-node/src/json_rpc/res.rs +++ b/crates/floresta-node/src/json_rpc/res.rs @@ -4,8 +4,11 @@ use core::fmt; use core::fmt::Debug; use core::fmt::Display; use core::fmt::Formatter; +use core::num::TryFromIntError; +use std::convert::Infallible; use axum::response::IntoResponse; +use corepc_types::v30::GetBlockHeaderVerbose; use corepc_types::v30::GetBlockVerboseOne; use floresta_chain::extensions::HeaderExtError; use floresta_common::impl_error_from; @@ -131,6 +134,18 @@ pub enum GetBlockRes { One(Box), } +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +/// The response for `getblockheader`, which can be either a raw hex-encoded block header or a verbose +/// one with all the fields parsed and decoded. +pub enum GetBlockHeaderRes { + /// The raw hex-encoded block header, as returned by `getblockheader` with verbosity false + Raw(String), + + /// A verbose block header, as returned by `getblockheader` with verbosity true + Verbose(Box), +} + #[derive(Debug, Deserialize, Serialize)] pub struct RpcError { pub code: i32, @@ -226,6 +241,9 @@ pub enum JsonRpcError { /// Something went wrong when attempting to publish a transaction to mempool MempoolAccept(MempoolError), + + /// A numeric conversion overflows, e.g., u64 to u32 + ConversionOverflow(String), } impl_error_from!(JsonRpcError, MempoolError, MempoolAccept); @@ -260,6 +278,7 @@ impl Display for JsonRpcError { JsonRpcError::InvalidDisconnectNodeCommand => write!(f, "Invalid disconnectnode command"), JsonRpcError::PeerNotFound => write!(f, "Peer not found in the peer list"), JsonRpcError::MempoolAccept(e) => write!(f, "Could not send transaction to mempool due to {e}"), + JsonRpcError::ConversionOverflow(e) => write!(f, "Numeric conversion overflow: {e}"), } } } @@ -289,6 +308,18 @@ impl From for JsonRpcError { } } +impl From for JsonRpcError { + fn from(e: TryFromIntError) -> Self { + JsonRpcError::ConversionOverflow(e.to_string()) + } +} + +impl From for JsonRpcError { + fn from(e: Infallible) -> Self { + JsonRpcError::ConversionOverflow(e.to_string()) + } +} + impl_error_from!(JsonRpcError, DescriptorError, InvalidDescriptor); impl From> for JsonRpcError { diff --git a/crates/floresta-node/src/json_rpc/server.rs b/crates/floresta-node/src/json_rpc/server.rs index 32c87c25b..2fd8ed4c6 100644 --- a/crates/floresta-node/src/json_rpc/server.rs +++ b/crates/floresta-node/src/json_rpc/server.rs @@ -255,8 +255,11 @@ async fn handle_json_rpc_request( "getblockheader" => { let hash = get_hash(¶ms, 0, "block_hash")?; + let verbosity = get_optional_field(¶ms, 1, "verbosity", get_bool)?.unwrap_or(true); + state - .get_block_header(hash) + .get_block_header(hash, verbosity) + .await .map(|h| serde_json::to_value(h).unwrap()) } @@ -440,6 +443,7 @@ fn get_http_error_code(err: &JsonRpcError) -> u16 { | JsonRpcError::InvalidParameterType(_) | JsonRpcError::MissingParameter(_) | JsonRpcError::ChainWorkOverflow + | JsonRpcError::ConversionOverflow(_) | JsonRpcError::MempoolAccept(_) | JsonRpcError::Wallet(_) => 400, @@ -480,6 +484,7 @@ fn get_json_rpc_error_code(err: &JsonRpcError) -> i32 { | JsonRpcError::InvalidRescanVal | JsonRpcError::NoAddressesToRescan | JsonRpcError::ChainWorkOverflow + | JsonRpcError::ConversionOverflow(_) | JsonRpcError::Wallet(_) | JsonRpcError::MempoolAccept(_) => -32600, diff --git a/crates/floresta-rpc/src/lib.rs b/crates/floresta-rpc/src/lib.rs index b42368e82..fafe3e015 100644 --- a/crates/floresta-rpc/src/lib.rs +++ b/crates/floresta-rpc/src/lib.rs @@ -43,6 +43,7 @@ mod tests { use crate::jsonrpc_client::Client; use crate::rpc::FlorestaRPC; + use crate::rpc_types::GetBlockHeaderRes; use crate::rpc_types::GetBlockRes; struct Florestad { @@ -226,9 +227,14 @@ mod tests { let (_proc, client) = start_florestad(); let blockhash = client.get_block_hash(0).expect("rpc not working"); - let block_header = client.get_block_header(blockhash).expect("rpc not working"); + let block_header = client + .get_block_header(blockhash, Some(true)) + .expect("rpc not working"); + let GetBlockHeaderRes::Verbose(block_header) = block_header else { + panic!("Expected verbose block header"); + }; - assert_eq!(block_header.block_hash(), blockhash); + assert_eq!(block_header.hash, blockhash.to_string()); } #[test] diff --git a/crates/floresta-rpc/src/rpc.rs b/crates/floresta-rpc/src/rpc.rs index 6c419af32..3bd951644 100644 --- a/crates/floresta-rpc/src/rpc.rs +++ b/crates/floresta-rpc/src/rpc.rs @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use core::fmt::Debug; +use std::vec; -use bitcoin::block::Header as BlockHeader; use bitcoin::BlockHash; use bitcoin::Txid; use corepc_types::v29::GetTxOut; @@ -42,7 +42,12 @@ pub trait FlorestaRPC { /// in the Bitcoin protocol specification. A header contains the block's version, /// the previous block hash, the merkle root, the timestamp, the difficulty target, /// and the nonce. - fn get_block_header(&self, hash: BlockHash) -> Result; + #[doc = include_str!("../../../doc/rpc/getblockheader.md")] + fn get_block_header( + &self, + hash: BlockHash, + verbosity: Option, + ) -> Result; /// Gets a transaction from the blockchain /// /// This method returns a transaction that's cached in our wallet. If the verbosity flag is @@ -323,8 +328,16 @@ impl FlorestaRPC for T { self.call("getblockfilter", &[Value::Number(Number::from(height))]) } - fn get_block_header(&self, hash: BlockHash) -> Result { - self.call("getblockheader", &[Value::String(hash.to_string())]) + fn get_block_header( + &self, + hash: BlockHash, + verbosity: Option, + ) -> Result { + let mut params = vec![Value::String(hash.to_string())]; + if let Some(verbosity) = verbosity { + params.push(Value::Bool(verbosity)); + } + self.call("getblockheader", ¶ms) } fn get_blockchain_info(&self) -> Result { diff --git a/crates/floresta-rpc/src/rpc_types.rs b/crates/floresta-rpc/src/rpc_types.rs index 1ebce51d7..02377eb1b 100644 --- a/crates/floresta-rpc/src/rpc_types.rs +++ b/crates/floresta-rpc/src/rpc_types.rs @@ -5,6 +5,7 @@ use core::fmt; use core::fmt::Display; use core::fmt::Formatter; +use corepc_types::v30::GetBlockHeaderVerbose; use corepc_types::v30::GetBlockVerboseOne; use serde::Deserialize; use serde::Serialize; @@ -196,6 +197,18 @@ pub enum GetBlockRes { One(Box), } +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +/// The response for getblockheader, which can be either a raw hex-encoded block header or a verbose +/// one with all the fields parsed and decoded. +pub enum GetBlockHeaderRes { + /// The raw hex-encoded block header, as returned by getblockheader with verbosity false + Raw(String), + + /// A verbose block header, as returned by getblockheader with verbosity true + Verbose(Box), +} + /// A confidence enum to auxiliate rescan timestamp values. /// /// Tells how much confidence you need for this rescan request. That is, the how conservative you want floresta to be when determining which block to start the rescan. diff --git a/doc/rpc/getblockheader.md b/doc/rpc/getblockheader.md new file mode 100644 index 000000000..a22933475 --- /dev/null +++ b/doc/rpc/getblockheader.md @@ -0,0 +1,67 @@ +# `getblockheader` + +Retrieve information about a specific block header by its hash. The verbosity parameter determines the format of the returned data. + +## Usage + +### Synopsis + +```bash +floresta-cli getblockheader [verbosity] +``` + +### Examples + +```bash +# Returns a JSON object with detailed block header information (default verbosity = true) +floresta-cli getblockheader "000000000000000000007ae6247b184396b8a1a292b8435508f448669ead45a6" + +# Returns a serialized, hex-encoded string of the block header data (verbosity = false) +floresta-cli getblockheader "000000000000000000007ae6247b184396b8a1a292b8435508f448669ead45a6" false + +# Returns a JSON object with detailed block header information (verbosity = true) +floresta-cli getblockheader "000000000000000000007ae6247b184396b8a1a292b8435508f448669ead45a6" true +``` + +## Arguments + +- `blockhash` - (string, required) The block hash. +- `verbosity` - (bool, optional, default=true) + - `false`: Returns a serialized, hex-encoded string of the block header data. + - `true`: Returns a JSON object with detailed block header information. + +## Returns + +### Ok Response (for verbosity = false) + +- `"hex"` - (string) A serialized, hex-encoded string of the block header data. + +### Ok Response (for verbosity = true) + +Return JSON object +- `confirmations` - (numeric) The number of confirmations. +- `height` - (numeric) The block height or index. +- `version` - (numeric) The block version. +- `versionHex` - (string) The block version formatted in hexadecimal. +- `merkleroot` - (string) The merkle root. +- `time` - (numeric) The block time expressed in UNIX epoch time. +- `mediantime` - (numeric) The median block time expressed in UNIX epoch time. +- `nonce` - (numeric) The nonce. +- `bits` - (string) Compact representation of the block difficulty target. +- `target` - (string) The difficulty target. +- `difficulty` - (numeric) The difficulty. +- `chainwork` - (string) Expected number of hashes required to produce the chain up to this block (in hex). +- `nTx` - (numeric) The number of transactions in the block. +- `previousblockhash` - (string, optional) The hash of the previous block. +- `nextblockhash` - (string, optional) The hash of the next block. + +### Error Enum + +* `JsonRpcError::ChainWorkOverflow` - Overflow occurred while calculating accumulated chain work +* `JsonRpcError::BlockNotFound` - The requested block hash was not found in the blockchain +* `JsonRpcError::Chain` - If there's an error accessing blockchain data + +## Notes + +- To retrieve block hashes, you can use the `getblockhash` RPC to obtain the hash of a specific block by its height, or the `getbestblockhash` RPC to get the hash of the latest known block. These hashes can then be used with the `getblockheader` RPC to retrieve detailed block information. +- **In regtest**, the difficulty value may not match real-world conditions due to easier mining. \ No newline at end of file diff --git a/tests/floresta-cli/getblockheader.py b/tests/floresta-cli/getblockheader.py index ec3e0df79..b770f247a 100644 --- a/tests/floresta-cli/getblockheader.py +++ b/tests/floresta-cli/getblockheader.py @@ -6,28 +6,119 @@ This functional test cli utility to interact with a Floresta node with `getblockheader` """ +import time +import random +from typing import Any import pytest +from requests.exceptions import HTTPError -from test_framework.constants import GENESIS_BLOCK_HASH - - -@pytest.mark.rpc -def test_get_block_header(florestad_node): - """ - Test `getblockheader` to get the genesis block header. - """ - - result = florestad_node.rpc.get_blockheader(GENESIS_BLOCK_HASH) - - assert result["version"] == 1 - assert ( - result["prev_blockhash"] - == "0000000000000000000000000000000000000000000000000000000000000000" - ) - assert ( - result["merkle_root"] - == "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b" - ) - assert result["time"] == 1296688602 - assert result["bits"] == 545259519 - assert result["nonce"] == 2 +TIMEOUT_SECONDS = 20 + + +class TestGetBlockheader: + """Functional tests for the getblockheader RPC, comparing Florestad vs Bitcoin Core.""" + + # define attributes at class level to avoid "defined outside __init__" warnings + florestad: Any = None + bitcoind: Any = None + log: Any = None + node_manager: Any = None + + @pytest.mark.rpc + def test_get_blockheader( + self, setup_logging, node_manager, florestad_node, bitcoind_node + ): + """ + Test the getblockheader RPC command. Verifies that Florestad's getblockheader RPC responses + are compliant with Bitcoin Core's getblockheader behavior and values. + """ + self.log = setup_logging + self.node_manager = node_manager + self.florestad = florestad_node + self.bitcoind = bitcoind_node + + self.log.info("Testing getblockheader with non-existent hash") + + invalid_hash = ( + "000000000000000000015abb3038f926d74fcdc171bf6c8aadc20a9a75310ffa" + ) + with pytest.raises(HTTPError): + self.florestad.rpc.get_blockheader(invalid_hash) + + with pytest.raises(HTTPError): + self.florestad.rpc.get_blockheader(invalid_hash, False) + + with pytest.raises(HTTPError): + self.florestad.rpc.get_blockheader(invalid_hash, True) + + self.bitcoind.rpc.generate_block(2017) + # Sleep is required to ensure blocks have different timestamps. In regtest, blocks are mined + # almost instantaneously, so without this sleep, block timestamps would be nearly identical. + # We need different timestamps to cause the median time of recent blocks to be different + # from earlier blocks, which is necessary for proper testing. + time.sleep(1) + self.bitcoind.rpc.generate_block(5) + + self.node_manager.connect_nodes(self.florestad, self.bitcoind) + + block_count = self.bitcoind.rpc.get_block_count() + start = time.time() + while time.time() - start < TIMEOUT_SECONDS: + florestad_count = self.florestad.rpc.get_block_count() + if florestad_count == block_count: + break + time.sleep(0.5) + + assert florestad_count == block_count + + self.log.info("Testing getblockheader RPC in the genesis block") + self.validate_block_header(0) + + random_block = random.randint(1, block_count) + self.log.info(f"Testing getblockheader RPC in block {random_block}") + self.validate_block_header(random_block) + + self.log.info(f"Testing getblockheader RPC in block {block_count}") + self.validate_block_header(block_count) + + def validate_block_header(self, height: int): + """ + Compare a block header at given height between Florestad and Bitcoin Core for several + verbosity levels. + """ + block_hash = self.bitcoind.rpc.get_blockhash(height) + self.log.info( + f"Comparing block header {block_hash} between florestad and bitcoind" + ) + + self.log.info("Fetching request without verbosity") + florestad_header = self.florestad.rpc.get_blockheader(block_hash) + bitcoind_header = self.bitcoind.rpc.get_blockheader(block_hash) + self.validate_headers_match(florestad_header, bitcoind_header) + + verbosity = False + self.log.info(f"Fetching request with verbosity {verbosity}") + florestad_header = self.florestad.rpc.get_blockheader(block_hash, verbosity) + bitcoind_header = self.bitcoind.rpc.get_blockheader(block_hash, verbosity) + assert florestad_header == bitcoind_header + + verbosity = True + self.log.info(f"Fetching request with verbosity {verbosity}") + florestad_header = self.florestad.rpc.get_blockheader(block_hash, verbosity) + bitcoind_header = self.bitcoind.rpc.get_blockheader(block_hash, verbosity) + + self.validate_headers_match(florestad_header, bitcoind_header) + + def validate_headers_match(self, florestad_header: dict, bitcoind_header: dict): + """ + Compare two block headers and assert that they match. + """ + for key, bval in bitcoind_header.items(): + fval = florestad_header[key] + + self.log.info(f"Comparing {key} field: florestad={fval} bitcoind={bval}") + if key == "difficulty": + # Allow small differences in floating point representation + assert round(fval, 3) == round(bval, 3) + else: + assert fval == bval diff --git a/tests/test_framework/rpc/base.py b/tests/test_framework/rpc/base.py index 34b79dc13..4be48cdab 100644 --- a/tests/test_framework/rpc/base.py +++ b/tests/test_framework/rpc/base.py @@ -259,14 +259,18 @@ def get_block_count(self) -> int: """ return self.perform_request("getblockcount") - def get_blockheader(self, blockhash: str) -> dict: + def get_blockheader(self, blockhash: str, verbosity: bool | None = None) -> dict: """ Get the header of a block """ if not bool(re.fullmatch(r"^[a-f0-9]{64}$", blockhash)): raise ValueError(f"Invalid blockhash '{blockhash}'.") - return self.perform_request("getblockheader", params=[blockhash]) + params = [blockhash] + if verbosity is not None: + params.append(verbosity) + + return self.perform_request("getblockheader", params=params) def get_block(self, blockhash: str, verbosity: int = 1): """