Skip to content

Commit 0b4f3b2

Browse files
authored
Merge pull request #5153 from jbencin/feat/signature-count-endpoint
feat: Signer signature count endpoint
2 parents 529d6c5 + 3d9e13a commit 0b4f3b2

File tree

11 files changed

+661
-13
lines changed

11 files changed

+661
-13
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ jobs:
126126
- tests::nakamoto_integrations::follower_bootup_across_multiple_cycles
127127
- tests::nakamoto_integrations::utxo_check_on_startup_panic
128128
- tests::nakamoto_integrations::utxo_check_on_startup_recover
129+
- tests::nakamoto_integrations::v3_signer_api_endpoint
129130
# TODO: enable these once v1 signer is supported by a new nakamoto epoch
130131
# - tests::signer::v1::dkg
131132
# - tests::signer::v1::sign_request_rejected

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
1616
- `get-stacks-block-info?` added
1717
- `get-tenure-info?` added
1818
- `get-block-info?` removed
19+
- Added `/v3/signer/{signer_pubkey}/{reward_cycle}` endpoint
1920

2021
## [2.5.0.0.7]
2122

docs/rpc-endpoints.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,3 +533,8 @@ highest sortition), `reward_cycle` identifies the reward cycle number of this
533533
tenure, `tip_block_id` identifies the highest-known block in this tenure, and
534534
`tip_height` identifies that block's height.
535535

536+
### GET /v3/signer/[Signer Pubkey]/[Reward Cycle]
537+
538+
Get number of blocks signed by signer during a given reward cycle
539+
540+
Returns a non-negative integer

docs/rpc/openapi.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,3 +716,32 @@ paths:
716716
required: false
717717
schema:
718718
type: string
719+
/v3/signer/{signer}/{cycle_number}:
720+
get:
721+
summary: Get number of blocks signed by signer during a given reward cycle
722+
tags:
723+
- Blocks
724+
- Signers
725+
operationId: get_signer
726+
description: Get number of blocks signed by signer during a given reward cycle
727+
parameters:
728+
- name: signer
729+
in: path
730+
required: true
731+
description: Hex-encoded compressed Secp256k1 public key of signer
732+
schema:
733+
type: string
734+
- name: cycle_number
735+
in: path
736+
required: true
737+
description: Reward cycle number
738+
schema:
739+
type: integer
740+
responses:
741+
200:
742+
description: Number of blocks signed
743+
content:
744+
text/plain:
745+
schema:
746+
type: integer
747+
example: 7

stackslib/src/chainstate/nakamoto/mod.rs

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,12 @@
1414
// You should have received a copy of the GNU General Public License
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

17-
use std::collections::{BTreeMap, HashMap, HashSet};
17+
use std::collections::HashMap;
1818
use std::fs;
1919
use std::ops::{Deref, DerefMut, Range};
2020
use std::path::PathBuf;
2121

22-
use clarity::types::PublicKey;
23-
use clarity::util::secp256k1::{secp256k1_recover, Secp256k1PublicKey};
22+
use clarity::util::secp256k1::Secp256k1PublicKey;
2423
use clarity::vm::ast::ASTRules;
2524
use clarity::vm::costs::{ExecutionCost, LimitedCostTracker};
2625
use clarity::vm::database::{BurnStateDB, ClarityDatabase};
@@ -31,6 +30,7 @@ use lazy_static::{__Deref, lazy_static};
3130
use rusqlite::blob::Blob;
3231
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput};
3332
use rusqlite::{params, Connection, OpenFlags, OptionalExtension};
33+
use sha2::digest::typenum::Integer;
3434
use sha2::{Digest as Sha2Digest, Sha512_256};
3535
use stacks_common::bitvec::BitVec;
3636
use stacks_common::codec::{
@@ -268,6 +268,26 @@ lazy_static! {
268268
ADD COLUMN height_in_tenure;
269269
"#.into(),
270270
];
271+
272+
pub static ref NAKAMOTO_CHAINSTATE_SCHEMA_4: [&'static str; 2] = [
273+
r#"
274+
UPDATE db_config SET version = "7";
275+
"#,
276+
// Add a `signer_stats` table to keep track of how many blocks have been signed by each signer
277+
r#"
278+
-- Table for signer stats
279+
CREATE TABLE signer_stats (
280+
-- Signers public key
281+
public_key TEXT NOT NULL,
282+
-- Stacking rewards cycle ID
283+
reward_cycle INTEGER NOT NULL,
284+
-- Number of blocks signed during reward cycle
285+
blocks_signed INTEGER DEFAULT 1 NOT NULL,
286+
287+
PRIMARY KEY(public_key,reward_cycle)
288+
);
289+
"#,
290+
];
271291
}
272292

273293
#[cfg(test)]
@@ -3305,6 +3325,41 @@ impl NakamotoChainState {
33053325
.map_err(ChainstateError::from)
33063326
}
33073327

3328+
/// Keep track of how many blocks each signer is signing
3329+
fn record_block_signers(
3330+
tx: &mut ChainstateTx,
3331+
block: &NakamotoBlock,
3332+
reward_cycle: u64,
3333+
) -> Result<(), ChainstateError> {
3334+
let signer_sighash = block.header.signer_signature_hash();
3335+
for signer_signature in &block.header.signer_signature {
3336+
let signer_pubkey =
3337+
StacksPublicKey::recover_to_pubkey(signer_sighash.bits(), &signer_signature)
3338+
.map_err(|e| ChainstateError::InvalidStacksBlock(e.to_string()))?;
3339+
let sql = "INSERT INTO signer_stats(public_key,reward_cycle) VALUES(?1,?2) ON CONFLICT(public_key,reward_cycle) DO UPDATE SET blocks_signed=blocks_signed+1";
3340+
let params = params![signer_pubkey.to_hex(), reward_cycle];
3341+
tx.execute(sql, params)?;
3342+
}
3343+
Ok(())
3344+
}
3345+
3346+
/// Fetch number of blocks signed for a given signer and reward cycle
3347+
/// This is the data tracked by `record_block_signers()`
3348+
pub fn get_signer_block_count(
3349+
chainstate_db: &Connection,
3350+
signer_pubkey: &Secp256k1PublicKey,
3351+
reward_cycle: u64,
3352+
) -> Result<u64, ChainstateError> {
3353+
let sql =
3354+
"SELECT blocks_signed FROM signer_stats WHERE public_key = ?1 AND reward_cycle = ?2";
3355+
let params = params![signer_pubkey.to_hex(), reward_cycle];
3356+
chainstate_db
3357+
.query_row(sql, params, |row| row.get("blocks_signed"))
3358+
.optional()
3359+
.map(Option::unwrap_or_default) // It's fine to map `NONE` to `0`, because it's impossible to have `Some(0)`
3360+
.map_err(ChainstateError::from)
3361+
}
3362+
33083363
/// Begin block-processing and return all of the pre-processed state within a
33093364
/// `SetupBlockResult`.
33103365
///
@@ -4088,6 +4143,11 @@ impl NakamotoChainState {
40884143
let new_block_id = new_tip.index_block_hash();
40894144
chainstate_tx.log_transactions_processed(&new_block_id, &tx_receipts);
40904145

4146+
let reward_cycle = pox_constants.block_height_to_reward_cycle(
4147+
first_block_height.into(),
4148+
chain_tip_burn_header_height.into(),
4149+
);
4150+
40914151
// store the reward set calculated during this block if it happened
40924152
// NOTE: miner and proposal evaluation should not invoke this because
40934153
// it depends on knowing the StacksBlockId.
@@ -4118,6 +4178,12 @@ impl NakamotoChainState {
41184178
}
41194179
}
41204180

4181+
if let Some(reward_cycle) = reward_cycle {
4182+
Self::record_block_signers(chainstate_tx, block, reward_cycle)?;
4183+
} else {
4184+
warn!("No reward cycle found, skipping record_block_signers()");
4185+
}
4186+
41214187
monitoring::set_last_block_transaction_count(u64::try_from(block.txs.len()).unwrap());
41224188
monitoring::set_last_execution_cost_observed(&block_execution_cost, &block_limit);
41234189

stackslib/src/chainstate/stacks/db/mod.rs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ use crate::chainstate::burn::{ConsensusHash, ConsensusHashExtensions};
5555
use crate::chainstate::nakamoto::{
5656
HeaderTypeNames, NakamotoBlock, NakamotoBlockHeader, NakamotoChainState,
5757
NakamotoStagingBlocksConn, NAKAMOTO_CHAINSTATE_SCHEMA_1, NAKAMOTO_CHAINSTATE_SCHEMA_2,
58-
NAKAMOTO_CHAINSTATE_SCHEMA_3,
58+
NAKAMOTO_CHAINSTATE_SCHEMA_3, NAKAMOTO_CHAINSTATE_SCHEMA_4,
5959
};
6060
use crate::chainstate::stacks::address::StacksAddressExtensions;
6161
use crate::chainstate::stacks::boot::*;
@@ -298,14 +298,14 @@ impl DBConfig {
298298
});
299299
match epoch_id {
300300
StacksEpochId::Epoch10 => true,
301-
StacksEpochId::Epoch20 => version_u32 >= 1 && version_u32 <= 6,
302-
StacksEpochId::Epoch2_05 => version_u32 >= 2 && version_u32 <= 6,
303-
StacksEpochId::Epoch21 => version_u32 >= 3 && version_u32 <= 6,
304-
StacksEpochId::Epoch22 => version_u32 >= 3 && version_u32 <= 6,
305-
StacksEpochId::Epoch23 => version_u32 >= 3 && version_u32 <= 6,
306-
StacksEpochId::Epoch24 => version_u32 >= 3 && version_u32 <= 6,
307-
StacksEpochId::Epoch25 => version_u32 >= 3 && version_u32 <= 6,
308-
StacksEpochId::Epoch30 => version_u32 >= 3 && version_u32 <= 6,
301+
StacksEpochId::Epoch20 => version_u32 >= 1 && version_u32 <= 7,
302+
StacksEpochId::Epoch2_05 => version_u32 >= 2 && version_u32 <= 7,
303+
StacksEpochId::Epoch21 => version_u32 >= 3 && version_u32 <= 7,
304+
StacksEpochId::Epoch22 => version_u32 >= 3 && version_u32 <= 7,
305+
StacksEpochId::Epoch23 => version_u32 >= 3 && version_u32 <= 7,
306+
StacksEpochId::Epoch24 => version_u32 >= 3 && version_u32 <= 7,
307+
StacksEpochId::Epoch25 => version_u32 >= 3 && version_u32 <= 7,
308+
StacksEpochId::Epoch30 => version_u32 >= 3 && version_u32 <= 7,
309309
}
310310
}
311311
}
@@ -679,7 +679,7 @@ impl<'a> DerefMut for ChainstateTx<'a> {
679679
}
680680
}
681681

682-
pub const CHAINSTATE_VERSION: &'static str = "6";
682+
pub const CHAINSTATE_VERSION: &'static str = "7";
683683

684684
const CHAINSTATE_INITIAL_SCHEMA: &'static [&'static str] = &[
685685
"PRAGMA foreign_keys = ON;",
@@ -1120,6 +1120,15 @@ impl StacksChainState {
11201120
tx.execute_batch(cmd)?;
11211121
}
11221122
}
1123+
"6" => {
1124+
// migrate to nakamoto 3
1125+
info!(
1126+
"Migrating chainstate schema from version 6 to 7: adds signer_stats table"
1127+
);
1128+
for cmd in NAKAMOTO_CHAINSTATE_SCHEMA_4.iter() {
1129+
tx.execute_batch(cmd)?;
1130+
}
1131+
}
11231132
_ => {
11241133
error!(
11251134
"Invalid chain state database: expected version = {}, got {}",

0 commit comments

Comments
 (0)