From ad5fd54b3d1d8f638fa44a531bca71306fbb8c6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20C=C3=A1rdenas?= Date: Fri, 25 Oct 2024 15:09:20 -0600 Subject: [PATCH] feat: add mock signer message support (#669) --- .../chainhook-cli/src/storage/signers.rs | 287 +++++++++++++++++- .../chainhook-sdk/src/indexer/stacks/mod.rs | 72 ++++- components/chainhook-types-rs/src/signers.rs | 33 +- .../client/typescript/package-lock.json | 4 +- components/client/typescript/package.json | 2 +- .../typescript/src/schemas/stacks/signers.ts | 62 +++- 6 files changed, 433 insertions(+), 27 deletions(-) diff --git a/components/chainhook-cli/src/storage/signers.rs b/components/chainhook-cli/src/storage/signers.rs index 40a65321..c01c6abd 100644 --- a/components/chainhook-cli/src/storage/signers.rs +++ b/components/chainhook-cli/src/storage/signers.rs @@ -5,13 +5,13 @@ use chainhook_sdk::{ types::{ BlockAcceptedResponse, BlockIdentifier, BlockProposalData, BlockPushedData, BlockRejectReasonCode, BlockRejectedResponse, BlockResponseData, BlockValidationFailedCode, - NakamotoBlockData, NakamotoBlockHeaderData, SignerMessageMetadata, - StacksNonConsensusEventData, StacksNonConsensusEventPayloadData, StacksSignerMessage, - StacksStackerDbChunk, + MockBlockData, MockProposalData, MockSignatureData, NakamotoBlockData, + NakamotoBlockHeaderData, PeerInfoData, SignerMessageMetadata, StacksNonConsensusEventData, + StacksNonConsensusEventPayloadData, StacksSignerMessage, StacksStackerDbChunk, }, utils::Context, }; -use rusqlite::Connection; +use rusqlite::{Connection, Transaction}; use super::sqlite::{create_or_open_readwrite_db, open_existing_readonly_db}; @@ -49,7 +49,7 @@ pub fn initialize_signers_db(base_dir: &PathBuf, ctx: &Context) -> Result Result, + peer_info: &PeerInfoData, + message_id: Option, +) -> Result { + let mut proposal_stmt = db_tx + .prepare( + "INSERT INTO mock_proposals + (message_id, burn_block_height, stacks_tip_consensus_hash, stacks_tip, stacks_tip_height, pox_consensus, + server_version, network_id, index_block_hash) + VALUES (?,?,?,?,?,?,?,?) + RETURNING id", + ) + .map_err(|e| format!("unable to prepare statement: {e}"))?; + let mock_proposal_id: u64 = proposal_stmt + .query(rusqlite::params![ + &message_id, + &peer_info.burn_block_height, + &peer_info.stacks_tip_consensus_hash, + &peer_info.stacks_tip, + &peer_info.stacks_tip_height, + &peer_info.pox_consensus, + &peer_info.server_version, + &peer_info.network_id, + &peer_info.index_block_hash, + ]) + .map_err(|e| format!("unable to write mock proposal: {e}"))? + .next() + .map_err(|e| format!("unable to retrieve mock proposal id: {e}"))? + .ok_or("mock proposal id is empty")? + .get(0) + .map_err(|e| format!("unable to convert message id: {e}"))?; + Ok(mock_proposal_id) +} + +fn store_mock_signature( + db_tx: &Transaction<'_>, + peer_info: &PeerInfoData, + metadata: &SignerMessageMetadata, + message_id: Option, + mock_block_id: Option, +) -> Result<(), String> { + let mock_proposal_id = store_mock_proposal_peer_info(&db_tx, &peer_info, None)?; + let mut signature_stmt = db_tx + .prepare( + "INSERT INTO mock_signatures + (message_id, mock_proposal_id, mock_block_id, server_version) + VALUES (?,?,?,?)", + ) + .map_err(|e| format!("unable to prepare statement: {e}"))?; + signature_stmt + .execute(rusqlite::params![ + &message_id, + &mock_proposal_id, + &mock_block_id, + &metadata.server_version, + ]) + .map_err(|e| format!("unable to write mock signature: {e}"))?; + Ok(()) +} + pub fn store_signer_db_messages( base_dir: &PathBuf, events: &Vec, @@ -139,6 +251,9 @@ pub fn store_signer_db_messages( StacksSignerMessage::BlockProposal(_) => "block_proposal", StacksSignerMessage::BlockResponse(_) => "block_response", StacksSignerMessage::BlockPushed(_) => "block_pushed", + StacksSignerMessage::MockBlock(_) => "mock_block", + StacksSignerMessage::MockSignature(_) => "mock_signature", + StacksSignerMessage::MockProposal(_) => "mock_proposal", }; let message_id: u64 = message_stmt .query(rusqlite::params![ @@ -314,6 +429,61 @@ pub fn store_signer_db_messages( } }; } + StacksSignerMessage::MockSignature(data) => { + try_info!( + ctx, + "Storing stacks MockSignature by signer {}", + chunk.pubkey + ); + store_mock_signature( + &db_tx, + &data.mock_proposal.peer_info, + &data.metadata, + Some(message_id), + None, + )?; + } + StacksSignerMessage::MockProposal(data) => { + try_info!( + ctx, + "Storing stacks MockProposal by signer {}", + chunk.pubkey + ); + let _ = store_mock_proposal_peer_info(&db_tx, data, Some(message_id)); + } + StacksSignerMessage::MockBlock(data) => { + try_info!(ctx, "Storing stacks MockBlock by signer {}", chunk.pubkey); + let mock_proposal_id = store_mock_proposal_peer_info( + &db_tx, + &data.mock_proposal.peer_info, + None, + )?; + let mut block_stmt = db_tx + .prepare( + "INSERT INTO mock_blocks + (message_id, mock_proposal_id) + VALUES (?,?) + RETURNING id", + ) + .map_err(|e| format!("unable to prepare statement: {e}"))?; + let mock_block_id: u64 = block_stmt + .query(rusqlite::params![&message_id, &mock_proposal_id,]) + .map_err(|e| format!("unable to write mock block: {e}"))? + .next() + .map_err(|e| format!("unable to retrieve mock block id: {e}"))? + .ok_or("mock block id is empty")? + .get(0) + .map_err(|e| format!("unable to convert message id: {e}"))?; + for signature in data.mock_signatures.iter() { + store_mock_signature( + &db_tx, + &signature.mock_proposal.peer_info, + &signature.metadata, + None, + Some(mock_block_id), + )?; + } + } } } } @@ -481,6 +651,113 @@ pub fn get_signer_db_messages_received_at_block( }, ) .map_err(|e| format!("unable to query block response: {e}"))?, + "mock_signature" => db_tx + .query_row( + "SELECT p.burn_block_height, p.stacks_tip_consensus_hash, p.stacks_tip, p.stacks_tip_height, + p.pox_consensus, p.server_version AS peer_version, p.network_id, s.server_version + FROM mock_signatures AS s + INNER JOIN mock_proposals AS p ON p.id = s.mock_proposal_id + WHERE s.message_id = ?", + rusqlite::params![&message_id], + |signature_row| { + Ok(StacksSignerMessage::MockSignature(MockSignatureData { + mock_proposal: MockProposalData { + peer_info: PeerInfoData { + burn_block_height: signature_row.get(0).unwrap(), + stacks_tip_consensus_hash: signature_row.get(1).unwrap(), + stacks_tip: signature_row.get(2).unwrap(), + stacks_tip_height: signature_row.get(3).unwrap(), + pox_consensus: signature_row.get(4).unwrap(), + server_version: signature_row.get(5).unwrap(), + network_id: signature_row.get(6).unwrap(), + index_block_hash: signature_row.get(7).unwrap(), + } + }, + metadata: SignerMessageMetadata { + server_version: signature_row.get(8).unwrap() + } + })) + }, + ) + .map_err(|e| format!("unable to query mock signature: {e}"))?, + "mock_proposal" => db_tx + .query_row( + "SELECT burn_block_height, stacks_tip_consensus_hash, stacks_tip, stacks_tip_height, + pox_consensus, server_version, network_id, index_block_hash + FROM mock_proposals + WHERE message_id = ?", + rusqlite::params![&message_id], + |proposal_row| { + Ok(StacksSignerMessage::MockProposal(PeerInfoData { + burn_block_height: proposal_row.get(0).unwrap(), + stacks_tip_consensus_hash: proposal_row.get(1).unwrap(), + stacks_tip: proposal_row.get(2).unwrap(), + stacks_tip_height: proposal_row.get(3).unwrap(), + pox_consensus: proposal_row.get(4).unwrap(), + server_version: proposal_row.get(5).unwrap(), + network_id: proposal_row.get(6).unwrap(), + index_block_hash: proposal_row.get(7).unwrap(), + })) + }, + ) + .map_err(|e| format!("unable to query mock proposal: {e}"))?, + "mock_block" => db_tx + .query_row( + "SELECT b.id, p.burn_block_height, p.stacks_tip_consensus_hash, p.stacks_tip, p.stacks_tip_height, + p.pox_consensus, p.server_version, p.network_id, p.index_block_hash + FROM mock_blocks AS b + INNER JOIN mock_proposals AS p ON p.id = b.mock_proposal_id + WHERE b.message_id = ?", + rusqlite::params![&message_id], + |block_row| { + let mock_block_id: u64 = block_row.get(0).unwrap(); + let mut sig_stmt = db_tx + .prepare( + "SELECT p.burn_block_height, p.stacks_tip_consensus_hash, p.stacks_tip, + p.stacks_tip_height, p.pox_consensus, p.server_version AS peer_version, + p.network_id, p.index_block_hash, s.server_version + FROM mock_signatures AS s + INNER JOIN mock_proposals AS p ON p.id = s.mock_proposal_id + WHERE s.mock_block_id = ?")?; + let mut signatures_iter = sig_stmt.query(rusqlite::params![&mock_block_id])?; + let mut mock_signatures = vec![]; + while let Some(signature_row) = signatures_iter.next()? { + mock_signatures.push(MockSignatureData { + mock_proposal: MockProposalData { + peer_info: PeerInfoData { + burn_block_height: signature_row.get(0).unwrap(), + stacks_tip_consensus_hash: signature_row.get(1).unwrap(), + stacks_tip: signature_row.get(2).unwrap(), + stacks_tip_height: signature_row.get(3).unwrap(), + pox_consensus: signature_row.get(4).unwrap(), + server_version: signature_row.get(5).unwrap(), + network_id: signature_row.get(6).unwrap(), + index_block_hash: signature_row.get(7).unwrap(), + } + }, + metadata: SignerMessageMetadata { + server_version: signature_row.get(8).unwrap() + } + }); + } + Ok(StacksSignerMessage::MockBlock(MockBlockData { + mock_proposal: MockProposalData { + peer_info: PeerInfoData { + burn_block_height: block_row.get(1).unwrap(), + stacks_tip_consensus_hash: block_row.get(2).unwrap(), + stacks_tip: block_row.get(3).unwrap(), + stacks_tip_height: block_row.get(4).unwrap(), + pox_consensus: block_row.get(5).unwrap(), + server_version: block_row.get(6).unwrap(), + network_id: block_row.get(7).unwrap(), + index_block_hash: block_row.get(8).unwrap(), + } + }, + mock_signatures + })) + }, + ) + .map_err(|e| format!("unable to query mock block: {e}"))?, _ => return Err(format!("invalid message type: {type_str}")), }; events.push(event_data_from_message_row( diff --git a/components/chainhook-sdk/src/indexer/stacks/mod.rs b/components/chainhook-sdk/src/indexer/stacks/mod.rs index 9cc5b892..3a4a12a8 100644 --- a/components/chainhook-sdk/src/indexer/stacks/mod.rs +++ b/components/chainhook-sdk/src/indexer/stacks/mod.rs @@ -693,15 +693,13 @@ pub fn standardize_stacks_marshalled_stackerdb_chunks( #[cfg(feature = "stacks-signers")] pub fn standardize_stacks_stackerdb_chunks( stackerdb_chunks: &NewStackerDbChunks, - ctx: &Context, + _ctx: &Context, ) -> Result, String> { use stacks_codec::codec::BlockResponse; use stacks_codec::codec::RejectCode; use stacks_codec::codec::SignerMessage; use stacks_codec::codec::ValidateRejectCode; - use crate::try_debug; - let contract_id = &stackerdb_chunks.contract_id.name; let mut parsed_chunks: Vec = vec![]; for slot in stackerdb_chunks.modified_slots.iter() { @@ -790,18 +788,28 @@ pub fn standardize_stacks_stackerdb_chunks( block: standardize_stacks_nakamoto_block(&nakamoto_block)?, }) } - SignerMessage::MockSignature(_) => { - try_debug!(ctx, "Ignoring MockSignature stacks signer message"); - continue; - } - SignerMessage::MockProposal(_) => { - try_debug!(ctx, "Ignoring MockProposal stacks signer message"); - continue; - } - SignerMessage::MockBlock(_) => { - try_debug!(ctx, "Ignoring MockBlock stacks signer message"); - continue; - } + SignerMessage::MockSignature(signature) => StacksSignerMessage::MockSignature( + standardize_stacks_signer_mock_signature(&signature)?, + ), + SignerMessage::MockProposal(data) => StacksSignerMessage::MockProposal( + standardize_stacks_signer_peer_info(&data.peer_info)?, + ), + SignerMessage::MockBlock(data) => StacksSignerMessage::MockBlock(MockBlockData { + mock_proposal: MockProposalData { + peer_info: standardize_stacks_signer_peer_info(&data.mock_proposal.peer_info)?, + }, + mock_signatures: data + .mock_signatures + .iter() + .map(|signature| standardize_stacks_signer_mock_signature(signature)) + .try_fold(Vec::new(), |mut acc, item| -> Result, String> { + item.and_then(|val| { + acc.push(val); + Ok(()) + })?; + Ok(acc) + })?, + }), }; parsed_chunks.push(StacksStackerDbChunk { contract: contract_id.clone(), @@ -817,6 +825,40 @@ pub fn standardize_stacks_stackerdb_chunks( Ok(parsed_chunks) } +#[cfg(feature = "stacks-signers")] +pub fn standardize_stacks_signer_mock_signature( + signature: &stacks_codec::codec::MockSignature, +) -> Result { + Ok(MockSignatureData { + mock_proposal: MockProposalData { + peer_info: standardize_stacks_signer_peer_info(&signature.mock_proposal.peer_info)?, + }, + metadata: SignerMessageMetadata { + server_version: signature.metadata.server_version.clone(), + }, + }) +} + +#[cfg(feature = "stacks-signers")] +pub fn standardize_stacks_signer_peer_info( + peer_info: &stacks_codec::codec::PeerInfo, +) -> Result { + let block_hash = format!("0x{}", peer_info.stacks_tip.to_hex()); + Ok(PeerInfoData { + burn_block_height: peer_info.burn_block_height, + stacks_tip_consensus_hash: format!("0x{}", peer_info.stacks_tip_consensus_hash.to_hex()), + stacks_tip: block_hash.clone(), + stacks_tip_height: peer_info.stacks_tip_height, + pox_consensus: format!("0x{}", peer_info.pox_consensus.to_hex()), + server_version: peer_info.server_version.clone(), + network_id: peer_info.network_id, + index_block_hash: get_nakamoto_index_block_hash( + &block_hash, + &peer_info.stacks_tip_consensus_hash, + )?, + }) +} + #[cfg(feature = "stacks-signers")] pub fn standardize_stacks_nakamoto_block( block: &stacks_codec::codec::NakamotoBlock, diff --git a/components/chainhook-types-rs/src/signers.rs b/components/chainhook-types-rs/src/signers.rs index c0754dd2..3cb78d7f 100644 --- a/components/chainhook-types-rs/src/signers.rs +++ b/components/chainhook-types-rs/src/signers.rs @@ -91,13 +91,44 @@ pub struct BlockPushedData { pub block: NakamotoBlockData, } +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct PeerInfoData { + pub burn_block_height: u64, + pub stacks_tip_consensus_hash: String, + pub stacks_tip: String, + pub stacks_tip_height: u64, + pub pox_consensus: String, + pub server_version: String, + pub network_id: u32, + pub index_block_hash: String, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct MockProposalData { + pub peer_info: PeerInfoData, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct MockSignatureData { + pub mock_proposal: MockProposalData, + pub metadata: SignerMessageMetadata +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct MockBlockData { + pub mock_proposal: MockProposalData, + pub mock_signatures: Vec +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] #[serde(tag = "type", content = "data")] pub enum StacksSignerMessage { BlockProposal(BlockProposalData), BlockResponse(BlockResponseData), BlockPushed(BlockPushedData), - // TODO(rafaelcr): Add mock messages + MockSignature(MockSignatureData), + MockProposal(PeerInfoData), + MockBlock(MockBlockData), } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] diff --git a/components/client/typescript/package-lock.json b/components/client/typescript/package-lock.json index 55291fdc..af8a39f3 100644 --- a/components/client/typescript/package-lock.json +++ b/components/client/typescript/package-lock.json @@ -1,12 +1,12 @@ { "name": "@hirosystems/chainhook-client", - "version": "2.1.1", + "version": "2.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@hirosystems/chainhook-client", - "version": "2.1.1", + "version": "2.2.0", "license": "Apache 2.0", "dependencies": { "@fastify/type-provider-typebox": "^3.2.0", diff --git a/components/client/typescript/package.json b/components/client/typescript/package.json index 5cb41df8..f558fb8d 100644 --- a/components/client/typescript/package.json +++ b/components/client/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@hirosystems/chainhook-client", - "version": "2.1.1", + "version": "2.2.0", "description": "Chainhook TypeScript client", "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/components/client/typescript/src/schemas/stacks/signers.ts b/components/client/typescript/src/schemas/stacks/signers.ts index 7b9ceeb8..57503acc 100644 --- a/components/client/typescript/src/schemas/stacks/signers.ts +++ b/components/client/typescript/src/schemas/stacks/signers.ts @@ -50,6 +50,11 @@ export type StacksSignerMessageBlockResponseAccepted = Static< typeof StacksSignerMessageBlockResponseAcceptedSchema >; +export const StacksSignerMessageMetadataSchema = Type.Object({ + server_version: Type.String(), +}); +export type StacksSignerMessageMetadata = Static; + export const StacksSignerMessageBlockResponseRejectedSchema = Type.Object({ type: Type.Literal('Rejected'), data: Type.Object({ @@ -75,9 +80,7 @@ export const StacksSignerMessageBlockResponseRejectedSchema = Type.Object({ signer_signature_hash: Type.String(), chain_id: Type.Integer(), signature: Type.String(), - metadata: Type.Object({ - server_version: Type.String(), - }), + metadata: StacksSignerMessageMetadataSchema, }), }); export type StacksSignerMessageBlockResponseRejected = Static< @@ -103,10 +106,63 @@ export const StacksSignerMessageBlockPushedSchema = Type.Object({ }); export type StacksSignerMessageBlockPushed = Static; +export const StacksSignerMessagePeerInfoSchema = Type.Object({ + burn_block_height: Type.Integer(), + stacks_tip_consensus_hash: Type.String(), + stacks_tip: Type.String(), + stacks_tip_height: Type.Integer(), + pox_consensus: Type.String(), + server_version: Type.String(), + network_id: Type.Integer(), + index_block_hash: Type.String(), +}); +export type StacksSignerMessagePeerInfo = Static; + +export const StacksSignerMessageMockProposalDataSchema = Type.Object({ + peer_info: StacksSignerMessagePeerInfoSchema, +}); +export type StacksSignerMessageMockProposalData = Static< + typeof StacksSignerMessageMockProposalDataSchema +>; + +export const StacksSignerMessageMockSignatureDataSchema = Type.Object({ + mock_proposal: StacksSignerMessageMockProposalDataSchema, + metadata: StacksSignerMessageMetadataSchema, +}); +export type StacksSignerMessageMockSignatureData = Static< + typeof StacksSignerMessageMockSignatureDataSchema +>; + +export const StacksSignerMessageMockSignatureSchema = Type.Object({ + type: Type.Literal('MockSignature'), + data: StacksSignerMessageMockSignatureDataSchema, +}); +export type StacksSignerMessageMockSignature = Static< + typeof StacksSignerMessageMockSignatureSchema +>; + +export const StacksSignerMessageMockProposalSchema = Type.Object({ + type: Type.Literal('MockProposal'), + data: StacksSignerMessagePeerInfoSchema, +}); +export type StacksSignerMessageMockProposal = Static; + +export const StacksSignerMessageMockBlockSchema = Type.Object({ + type: Type.Literal('MockBlock'), + data: Type.Object({ + mock_proposal: StacksSignerMessageMockProposalDataSchema, + mock_signatures: Type.Array(StacksSignerMessageMockSignatureDataSchema), + }), +}); +export type StacksSignerMessageMockBlock = Static; + export const StacksSignerMessageSchema = Type.Union([ StacksSignerMessageBlockProposalSchema, StacksSignerMessageBlockResponseSchema, StacksSignerMessageBlockPushedSchema, + StacksSignerMessageMockSignatureSchema, + StacksSignerMessageMockProposalSchema, + StacksSignerMessageMockBlockSchema, ]); export type StacksSignerMessage = Static;