Skip to content

Commit bf4e59a

Browse files
committed
feat: do not count both accept and reject
If a signer sends a duplicate block acceptance or rejection, ignore the repeats. If a signer has previously rejected a block, delete the rejection if it later accepts it. If a signer has previously accepted a block, ignore a rejection and warn about it (this shouldn't happen).
1 parent d185f2d commit bf4e59a

File tree

2 files changed

+272
-28
lines changed

2 files changed

+272
-28
lines changed

stacks-signer/src/signerdb.rs

Lines changed: 255 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ use stacks_common::types::chainstate::ConsensusHash;
4040
use stacks_common::util::get_epoch_time_secs;
4141
use stacks_common::util::hash::Sha512Trunc256Sum;
4242
use stacks_common::util::secp256k1::MessageSignature;
43-
use stacks_common::{debug, define_u8_enum, error};
43+
use stacks_common::{debug, define_u8_enum, error, warn};
4444

4545
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4646
/// A vote across the signer set for a block
@@ -649,6 +649,28 @@ static ADD_SIGNER_STATE_MACHINE_UPDATES_SIGNER_ADDR_REWARD_CYCLE_INDEX: &str = r
649649
CREATE INDEX idx_signer_addr_reward_cycle ON signer_state_machine_updates(reward_cycle, signer_addr, received_time DESC);
650650
"#;
651651

652+
static DROP_BLOCK_SIGNATURES_TABLE: &str = r#"
653+
DROP TABLE IF EXISTS block_signatures;
654+
"#;
655+
656+
static CREATE_BLOCK_SIGNATURES_TABLE_V17: &str = r#"
657+
CREATE TABLE IF NOT EXISTS block_signatures (
658+
-- The block sighash commits to all of the stacks and burnchain state as of its parent,
659+
-- as well as the tenure itself so there's no need to include the reward cycle. Just
660+
-- the sighash is sufficient to uniquely identify the block across all burnchain, PoX,
661+
-- and stacks forks.
662+
signer_signature_hash TEXT NOT NULL,
663+
-- the signer address that rejected the block
664+
signer_addr TEXT NOT NULL,
665+
-- signature itself
666+
signature TEXT NOT NULL,
667+
PRIMARY KEY (signer_addr, signer_signature_hash)
668+
) STRICT;"#;
669+
670+
static CREATE_BLOCK_SIGNATURES_INDEX: &str = r#"
671+
CREATE INDEX IF NOT EXISTS idx_block_signatures_by_sighash ON block_signatures(signer_signature_hash);
672+
"#;
673+
652674
static SCHEMA_1: &[&str] = &[
653675
DROP_SCHEMA_0,
654676
CREATE_DB_CONFIG,
@@ -759,6 +781,13 @@ static SCHEMA_16: &[&str] = &[
759781
"INSERT INTO db_config (version) VALUES (16);",
760782
];
761783

784+
static SCHEMA_17: &[&str] = &[
785+
DROP_BLOCK_SIGNATURES_TABLE,
786+
CREATE_BLOCK_SIGNATURES_TABLE_V17,
787+
CREATE_BLOCK_SIGNATURES_INDEX,
788+
"INSERT INTO db_config (version) VALUES (17);",
789+
];
790+
762791
struct Migration {
763792
version: u32,
764793
statements: &'static [&'static str],
@@ -829,11 +858,15 @@ static MIGRATIONS: &[Migration] = &[
829858
version: 16,
830859
statements: SCHEMA_16,
831860
},
861+
Migration {
862+
version: 17,
863+
statements: SCHEMA_17,
864+
},
832865
];
833866

834867
impl SignerDb {
835868
/// The current schema version used in this build of the signer binary.
836-
pub const SCHEMA_VERSION: u32 = 16;
869+
pub const SCHEMA_VERSION: u32 = 17;
837870

838871
/// Create a new `SignerState` instance.
839872
/// This will create a new SQLite database at the given path
@@ -1287,20 +1320,28 @@ impl SignerDb {
12871320
pub fn add_block_signature(
12881321
&self,
12891322
block_sighash: &Sha512Trunc256Sum,
1323+
signer_addr: &StacksAddress,
12901324
signature: &MessageSignature,
1291-
) -> Result<(), DBError> {
1292-
let qry = "INSERT OR REPLACE INTO block_signatures (signer_signature_hash, signature) VALUES (?1, ?2);";
1325+
) -> Result<bool, DBError> {
1326+
let qry = "INSERT OR IGNORE INTO block_signatures (signer_signature_hash, signer_addr, signature) VALUES (?1, ?2, ?3);";
12931327
let args = params![
12941328
block_sighash,
1329+
signer_addr.to_string(),
12951330
serde_json::to_string(signature).map_err(DBError::SerializationError)?
12961331
];
12971332

12981333
debug!("Inserting block signature.";
12991334
"signer_signature_hash" => %block_sighash,
13001335
"signature" => %signature);
13011336

1302-
self.db.execute(qry, args)?;
1303-
Ok(())
1337+
let rows_added = self.db.execute(qry, args)?;
1338+
1339+
// Remove any block rejection entry for this signer and block hash
1340+
let del_qry = "DELETE FROM block_rejection_signer_addrs WHERE signer_signature_hash = ?1 AND signer_addr = ?2";
1341+
let del_args = params![block_sighash, signer_addr.to_string()];
1342+
self.db.execute(del_qry, del_args)?;
1343+
1344+
Ok(rows_added > 0)
13041345
}
13051346

13061347
/// Get all signatures for a block
@@ -1323,22 +1364,58 @@ impl SignerDb {
13231364
block_sighash: &Sha512Trunc256Sum,
13241365
addr: &StacksAddress,
13251366
reject_reason: &RejectReason,
1326-
) -> Result<(), DBError> {
1327-
let qry = "INSERT OR REPLACE INTO block_rejection_signer_addrs (signer_signature_hash, signer_addr, reject_code) VALUES (?1, ?2, ?3);";
1328-
let args = params![
1329-
block_sighash,
1330-
addr.to_string(),
1331-
RejectReasonPrefix::from(reject_reason) as i64
1332-
];
1367+
) -> Result<bool, DBError> {
1368+
// If this signer/block already has a signature, do not allow a rejection
1369+
let sig_qry = "SELECT 1 FROM block_signatures WHERE signer_signature_hash = ?1 AND signer_addr = ?2 LIMIT 1";
1370+
let sig_args = params![block_sighash, addr.to_string()];
1371+
let has_signature: Option<i64> = self
1372+
.db
1373+
.query_row(sig_qry, sig_args, |row| row.get(0))
1374+
.optional()?;
1375+
if has_signature.is_some() {
1376+
warn!("Cannot add block rejection for signer {} and block {} because a signature already exists.",
1377+
addr, block_sighash);
1378+
return Ok(false);
1379+
}
13331380

1334-
debug!("Inserting block rejection.";
1335-
"signer_signature_hash" => %block_sighash,
1336-
"signer_address" => %addr,
1337-
"reject_reason" => %reject_reason
1338-
);
1381+
// Check if a row exists for this sighash/signer combo
1382+
let qry = "SELECT reject_code FROM block_rejection_signer_addrs WHERE signer_signature_hash = ?1 AND signer_addr = ?2 LIMIT 1";
1383+
let args = params![block_sighash, addr.to_string()];
1384+
let existing_code: Option<i64> =
1385+
self.db.query_row(qry, args, |row| row.get(0)).optional()?;
13391386

1340-
self.db.execute(qry, args)?;
1341-
Ok(())
1387+
let reject_code = RejectReasonPrefix::from(reject_reason) as i64;
1388+
1389+
match existing_code {
1390+
Some(code) if code == reject_code => {
1391+
// Row exists with same reject_reason, do nothing
1392+
Ok(false)
1393+
}
1394+
Some(_) => {
1395+
// Row exists but with different reject_reason, update it
1396+
let update_qry = "UPDATE block_rejection_signer_addrs SET reject_code = ?1 WHERE signer_signature_hash = ?2 AND signer_addr = ?3";
1397+
let update_args = params![reject_code, block_sighash, addr.to_string()];
1398+
self.db.execute(update_qry, update_args)?;
1399+
debug!("Updated block rejection reason.";
1400+
"signer_signature_hash" => %block_sighash,
1401+
"signer_address" => %addr,
1402+
"reject_reason" => %reject_reason
1403+
);
1404+
Ok(true)
1405+
}
1406+
None => {
1407+
// Row does not exist, insert it
1408+
let insert_qry = "INSERT INTO block_rejection_signer_addrs (signer_signature_hash, signer_addr, reject_code) VALUES (?1, ?2, ?3)";
1409+
let insert_args = params![block_sighash, addr.to_string(), reject_code];
1410+
self.db.execute(insert_qry, insert_args)?;
1411+
debug!("Inserted block rejection.";
1412+
"signer_signature_hash" => %block_sighash,
1413+
"signer_address" => %addr,
1414+
"reject_reason" => %reject_reason
1415+
);
1416+
Ok(true)
1417+
}
1418+
}
13421419
}
13431420

13441421
/// Get all signer addresses that rejected the block (and their reject codes)
@@ -2090,21 +2167,177 @@ pub mod tests {
20902167
let db = SignerDb::new(db_path).expect("Failed to create signer db");
20912168

20922169
let block_id = Sha512Trunc256Sum::from_data("foo".as_bytes());
2170+
let address1 = StacksAddress::burn_address(false);
2171+
let address2 = StacksAddress::burn_address(true);
20932172
let sig1 = MessageSignature([0x11; 65]);
20942173
let sig2 = MessageSignature([0x22; 65]);
20952174

20962175
assert_eq!(db.get_block_signatures(&block_id).unwrap(), vec![]);
20972176

2098-
db.add_block_signature(&block_id, &sig1).unwrap();
2177+
db.add_block_signature(&block_id, &address1, &sig1).unwrap();
20992178
assert_eq!(db.get_block_signatures(&block_id).unwrap(), vec![sig1]);
21002179

2101-
db.add_block_signature(&block_id, &sig2).unwrap();
2180+
db.add_block_signature(&block_id, &address2, &sig2).unwrap();
21022181
assert_eq!(
21032182
db.get_block_signatures(&block_id).unwrap(),
21042183
vec![sig1, sig2]
21052184
);
21062185
}
21072186

2187+
#[test]
2188+
fn duplicate_block_signatures() {
2189+
let db_path = tmp_db_path();
2190+
let db = SignerDb::new(db_path).expect("Failed to create signer db");
2191+
2192+
let block_id = Sha512Trunc256Sum::from_data("foo".as_bytes());
2193+
let address = StacksAddress::burn_address(false);
2194+
let sig1 = MessageSignature([0x11; 65]);
2195+
let sig2 = MessageSignature([0x22; 65]);
2196+
2197+
assert_eq!(db.get_block_signatures(&block_id).unwrap(), vec![]);
2198+
2199+
assert!(db.add_block_signature(&block_id, &address, &sig1).unwrap());
2200+
assert_eq!(db.get_block_signatures(&block_id).unwrap(), vec![sig1]);
2201+
2202+
assert!(!db.add_block_signature(&block_id, &address, &sig2).unwrap());
2203+
assert_eq!(db.get_block_signatures(&block_id).unwrap(), vec![sig1]);
2204+
}
2205+
2206+
#[test]
2207+
fn add_and_get_block_rejections() {
2208+
let db_path = tmp_db_path();
2209+
let db = SignerDb::new(db_path).expect("Failed to create signer db");
2210+
2211+
let block_id = Sha512Trunc256Sum::from_data("foo".as_bytes());
2212+
let address1 = StacksAddress::burn_address(false);
2213+
let address2 = StacksAddress::burn_address(true);
2214+
2215+
assert_eq!(
2216+
db.get_block_rejection_signer_addrs(&block_id).unwrap(),
2217+
vec![]
2218+
);
2219+
2220+
assert!(db
2221+
.add_block_rejection_signer_addr(
2222+
&block_id,
2223+
&address1,
2224+
&RejectReason::DuplicateBlockFound,
2225+
)
2226+
.unwrap());
2227+
assert_eq!(
2228+
db.get_block_rejection_signer_addrs(&block_id).unwrap(),
2229+
vec![(address1, RejectReasonPrefix::DuplicateBlockFound)]
2230+
);
2231+
2232+
assert!(db
2233+
.add_block_rejection_signer_addr(
2234+
&block_id,
2235+
&address2,
2236+
&RejectReason::InvalidParentBlock
2237+
)
2238+
.unwrap());
2239+
assert_eq!(
2240+
db.get_block_rejection_signer_addrs(&block_id).unwrap(),
2241+
vec![
2242+
(address1, RejectReasonPrefix::DuplicateBlockFound),
2243+
(address2, RejectReasonPrefix::InvalidParentBlock)
2244+
]
2245+
);
2246+
}
2247+
2248+
#[test]
2249+
fn duplicate_block_rejections() {
2250+
let db_path = tmp_db_path();
2251+
let db = SignerDb::new(db_path).expect("Failed to create signer db");
2252+
2253+
let block_id = Sha512Trunc256Sum::from_data("foo".as_bytes());
2254+
let address = StacksAddress::burn_address(false);
2255+
2256+
assert_eq!(
2257+
db.get_block_rejection_signer_addrs(&block_id).unwrap(),
2258+
vec![]
2259+
);
2260+
2261+
assert!(db
2262+
.add_block_rejection_signer_addr(&block_id, &address, &RejectReason::InvalidParentBlock)
2263+
.unwrap());
2264+
assert_eq!(
2265+
db.get_block_rejection_signer_addrs(&block_id).unwrap(),
2266+
vec![(address, RejectReasonPrefix::InvalidParentBlock)]
2267+
);
2268+
2269+
assert!(db
2270+
.add_block_rejection_signer_addr(&block_id, &address, &RejectReason::InvalidMiner)
2271+
.unwrap());
2272+
assert_eq!(
2273+
db.get_block_rejection_signer_addrs(&block_id).unwrap(),
2274+
vec![(address, RejectReasonPrefix::InvalidMiner)]
2275+
);
2276+
2277+
assert!(!db
2278+
.add_block_rejection_signer_addr(&block_id, &address, &RejectReason::InvalidMiner)
2279+
.unwrap());
2280+
assert_eq!(
2281+
db.get_block_rejection_signer_addrs(&block_id).unwrap(),
2282+
vec![(address, RejectReasonPrefix::InvalidMiner)]
2283+
);
2284+
}
2285+
2286+
#[test]
2287+
fn reject_then_accept() {
2288+
let db_path = tmp_db_path();
2289+
let db = SignerDb::new(db_path).expect("Failed to create signer db");
2290+
2291+
let block_id = Sha512Trunc256Sum::from_data("foo".as_bytes());
2292+
let address = StacksAddress::burn_address(false);
2293+
let sig1 = MessageSignature([0x11; 65]);
2294+
2295+
assert_eq!(db.get_block_signatures(&block_id).unwrap(), vec![]);
2296+
2297+
assert!(db
2298+
.add_block_rejection_signer_addr(&block_id, &address, &RejectReason::InvalidParentBlock)
2299+
.unwrap());
2300+
assert_eq!(
2301+
db.get_block_rejection_signer_addrs(&block_id).unwrap(),
2302+
vec![(address, RejectReasonPrefix::InvalidParentBlock)]
2303+
);
2304+
2305+
assert!(db.add_block_signature(&block_id, &address, &sig1).unwrap());
2306+
assert_eq!(db.get_block_signatures(&block_id).unwrap(), vec![sig1]);
2307+
assert!(db
2308+
.get_block_rejection_signer_addrs(&block_id)
2309+
.unwrap()
2310+
.is_empty());
2311+
}
2312+
2313+
#[test]
2314+
fn accept_then_reject() {
2315+
let db_path = tmp_db_path();
2316+
let db = SignerDb::new(db_path).expect("Failed to create signer db");
2317+
2318+
let block_id = Sha512Trunc256Sum::from_data("foo".as_bytes());
2319+
let address = StacksAddress::burn_address(false);
2320+
let sig1 = MessageSignature([0x11; 65]);
2321+
2322+
assert_eq!(db.get_block_signatures(&block_id).unwrap(), vec![]);
2323+
2324+
assert!(db.add_block_signature(&block_id, &address, &sig1).unwrap());
2325+
assert_eq!(db.get_block_signatures(&block_id).unwrap(), vec![sig1]);
2326+
assert!(db
2327+
.get_block_rejection_signer_addrs(&block_id)
2328+
.unwrap()
2329+
.is_empty());
2330+
2331+
assert!(!db
2332+
.add_block_rejection_signer_addr(&block_id, &address, &RejectReason::InvalidParentBlock)
2333+
.unwrap());
2334+
assert_eq!(db.get_block_signatures(&block_id).unwrap(), vec![sig1]);
2335+
assert!(db
2336+
.get_block_rejection_signer_addrs(&block_id)
2337+
.unwrap()
2338+
.is_empty());
2339+
}
2340+
21082341
#[test]
21092342
fn test_and_set_block_broadcasted() {
21102343
let db_path = tmp_db_path();

stacks-signer/src/v0/signer.rs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1491,12 +1491,16 @@ impl Signer {
14911491
}
14921492

14931493
// signature is valid! store it
1494-
if let Err(e) = self.signer_db.add_block_rejection_signer_addr(
1494+
match self.signer_db.add_block_rejection_signer_addr(
14951495
block_hash,
14961496
&signer_address,
14971497
&rejection.response_data.reject_reason,
14981498
) {
1499-
warn!("{self}: Failed to save block rejection signature: {e:?}",);
1499+
Err(e) => {
1500+
warn!("{self}: Failed to save block rejection signature: {e:?}",);
1501+
}
1502+
Ok(false) => return, // We already have this signature, do not process it again.
1503+
Ok(true) => (),
15001504
}
15011505
block_info.reject_reason = Some(rejection.response_data.reject_reason.clone());
15021506

@@ -1624,10 +1628,17 @@ impl Signer {
16241628
return;
16251629
}
16261630

1627-
// signature is valid! store it
1628-
self.signer_db
1629-
.add_block_signature(block_hash, signature)
1630-
.unwrap_or_else(|_| panic!("{self}: Failed to save block signature"));
1631+
let signer_address = StacksAddress::p2pkh(self.mainnet, &public_key);
1632+
1633+
// signature is valid! store it.
1634+
// if this returns false, it means the signature already exists in the DB, so just return.
1635+
if !self
1636+
.signer_db
1637+
.add_block_signature(block_hash, &signer_address, signature)
1638+
.unwrap_or_else(|_| panic!("{self}: Failed to save block signature"))
1639+
{
1640+
return;
1641+
}
16311642

16321643
// do we have enough signatures to broadcast?
16331644
// i.e. is the threshold reached?

0 commit comments

Comments
 (0)