added governance rules and fractional NFT updates

This commit is contained in:
viraladmin 2026-07-24 16:12:25 -06:00
parent b1a3829654
commit 9381914a2c
64 changed files with 4967 additions and 123 deletions

View File

@ -0,0 +1,124 @@
use contractless::blocks::activation_vote::{ActivationVote, UnsignedActivationVote};
use contractless::common::cli_prompts::{prompt_path, prompt_visible};
use contractless::common::governance::{hash_governance_document, normalize_development_location};
use contractless::common::types::{ACTIVATION_VOTE_FEE, ACTIVATION_VOTE_TYPE};
use contractless::standalone_tools::governance::{
load_wallet, parse_fee, parse_vote, prompt_fee, validate_hash, write_transaction,
};
use contractless::{env, json, Utc};
#[tokio::main]
async fn main() {
let args: Vec<String> = env::args().collect();
if args.len() > 6 {
println!("Usage: ./create_activation_vote_tx <proposal_key> <implementation_file> <development_location> <yes|no> [txfee]");
return;
}
let proposal_input = match args.get(1) {
Some(value) => value.clone(),
None => prompt_visible("Please enter the approved proposal key: ").await,
};
let proposal_key = match validate_hash(&proposal_input, "Proposal key") {
Ok(hash) => hash,
Err(err) => {
eprintln!("{err}");
return;
}
};
let implementation_path = if let Some(path) = args.get(2) {
path.clone()
} else {
match prompt_path("Please enter the path to the implementation document: ") {
Ok(path) => path,
Err(err) => {
eprintln!("Could not read implementation path: {err}");
return;
}
}
};
let document = match tokio::fs::read(&implementation_path).await {
Ok(document) => document,
Err(err) => {
eprintln!("Could not read implementation document: {err}");
return;
}
};
let location_input = match args.get(3) {
Some(value) => value.clone(),
None => {
prompt_visible("Please enter the repository location of the implementation document: ")
.await
}
};
let development_location = match normalize_development_location(location_input.trim()) {
Ok(location) => location,
Err(err) => {
eprintln!("{err}");
return;
}
};
let vote_input = match args.get(4) {
Some(value) => value.clone(),
None => prompt_visible("Vote to activate this implementation (yes/no): ").await,
};
let vote = match parse_vote(&vote_input) {
Ok(vote) => vote,
Err(err) => {
eprintln!("{err}");
return;
}
};
let txfee = match args.get(5) {
Some(value) => match parse_fee(value, ACTIVATION_VOTE_FEE) {
Ok(fee) => fee,
Err(err) => {
eprintln!("{err}");
return;
}
},
None => prompt_fee("activation-vote transaction", ACTIVATION_VOTE_FEE).await,
};
let wallet = match load_wallet().await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("{err}");
return;
}
};
let timestamp = Utc::now().timestamp() as u32;
let development_hash = hash_governance_document(&document);
let unsigned = UnsignedActivationVote::new(
ACTIVATION_VOTE_TYPE,
timestamp,
&proposal_key,
&development_hash,
&development_location,
&wallet.saved.short_address,
vote,
txfee,
)
.await;
let transaction = match ActivationVote::new(unsigned, &wallet.saved.private_key).await {
Ok(transaction) => transaction,
Err(err) => {
eprintln!("Could not sign activation vote: {err}");
return;
}
};
let hash = transaction.unsigned_activationvote.hash().await;
let output = json!({
"txtype": ACTIVATION_VOTE_TYPE,
"timestamp": timestamp,
"proposal_key": proposal_key,
"development_hash": development_hash,
"development_location": development_location,
"address": wallet.saved.short_address,
"vote": vote,
"txfee": txfee,
"hash": hash,
"signature": transaction.signature,
});
if let Err(err) = write_transaction(&hash, &output).await {
eprintln!("{err}");
}
}

View File

@ -32,6 +32,20 @@ async fn main() {
0 0
}; };
// Collections are always indivisible. Standalone NFTs/RWAs can opt
// into fractional ownership at creation and cannot change it later.
let ownership_type = if series == 1 {
0
} else {
let ownership_data =
prompt_visible("Ownership type (indivisible/fractional, default indivisible): ").await;
if ownership_data.trim().eq_ignore_ascii_case("fractional") {
1
} else {
0
}
};
// get user input ticker // get user input ticker
let nft_ticker_name = prompt_visible("Please enter the name for your NFT. This can be 3 to 15 characters and must be 100% unique. First come first serve: ").await; let nft_ticker_name = prompt_visible("Please enter the name for your NFT. This can be 3 to 15 characters and must be 100% unique. First come first serve: ").await;
let Some(nft_name) = padded_normalized_asset_input(nft_ticker_name.trim()) else { let Some(nft_name) = padded_normalized_asset_input(nft_ticker_name.trim()) else {
@ -100,6 +114,7 @@ async fn main() {
timestamp, timestamp,
address.trim_matches('"'), address.trim_matches('"'),
series, series,
ownership_type,
&nft_name, &nft_name,
&item_ipfs, &item_ipfs,
count, count,
@ -125,6 +140,7 @@ async fn main() {
"timestamp": timestamp, "timestamp": timestamp,
"creator": address.trim_matches('"'), "creator": address.trim_matches('"'),
"series": series, "series": series,
"ownership_type": ownership_type,
"nft_name": nft_name, "nft_name": nft_name,
"item_ipfs": item_ipfs, "item_ipfs": item_ipfs,
"count": count, "count": count,

View File

@ -0,0 +1,82 @@
use contractless::blocks::proposal_key::{ProposalKey, UnsignedProposalKey};
use contractless::common::cli_prompts::prompt_path;
use contractless::common::governance::hash_governance_document;
use contractless::common::types::{PROPOSAL_KEY_FEE, PROPOSAL_KEY_TYPE};
use contractless::standalone_tools::governance::{
load_wallet, parse_fee, prompt_fee, write_transaction,
};
use contractless::{env, json, Utc};
#[tokio::main]
async fn main() {
let args: Vec<String> = env::args().collect();
if args.len() > 3 {
println!("Usage: ./create_proposal_key_tx <proposal_file> [txfee]");
return;
}
let proposal_path = if let Some(path) = args.get(1) {
path.clone()
} else {
match prompt_path("Please enter the path to the finalized CLP document: ") {
Ok(path) => path,
Err(err) => {
eprintln!("Could not read proposal path: {err}");
return;
}
}
};
let document = match tokio::fs::read(&proposal_path).await {
Ok(document) => document,
Err(err) => {
eprintln!("Could not read proposal document: {err}");
return;
}
};
let txfee = match args.get(2) {
Some(value) => match parse_fee(value, PROPOSAL_KEY_FEE) {
Ok(fee) => fee,
Err(err) => {
eprintln!("{err}");
return;
}
},
None => prompt_fee("proposal-key transaction", PROPOSAL_KEY_FEE).await,
};
let wallet = match load_wallet().await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("{err}");
return;
}
};
let timestamp = Utc::now().timestamp() as u32;
let proposal_hash = hash_governance_document(&document);
let unsigned = UnsignedProposalKey::new(
PROPOSAL_KEY_TYPE,
timestamp,
&proposal_hash,
&wallet.saved.short_address,
txfee,
)
.await;
let transaction = match ProposalKey::new(unsigned, &wallet.saved.private_key).await {
Ok(transaction) => transaction,
Err(err) => {
eprintln!("Could not sign proposal-key transaction: {err}");
return;
}
};
let hash = transaction.unsigned_proposalkey.hash().await;
let output = json!({
"txtype": PROPOSAL_KEY_TYPE,
"timestamp": timestamp,
"proposal_hash": proposal_hash,
"address": wallet.saved.short_address,
"txfee": txfee,
"hash": hash,
"signature": transaction.signature,
});
if let Err(err) = write_transaction(&hash, &output).await {
eprintln!("{err}");
}
}

View File

@ -0,0 +1,86 @@
use contractless::blocks::proposal_vote::{ProposalVote, UnsignedProposalVote};
use contractless::common::cli_prompts::prompt_visible;
use contractless::common::types::{PROPOSAL_VOTE_FEE, PROPOSAL_VOTE_TYPE};
use contractless::standalone_tools::governance::{
load_wallet, parse_fee, parse_vote, prompt_fee, validate_hash, write_transaction,
};
use contractless::{env, json, Utc};
#[tokio::main]
async fn main() {
let args: Vec<String> = env::args().collect();
if args.len() > 4 {
println!("Usage: ./create_proposal_vote_tx <proposal_key> <yes|no> [txfee]");
return;
}
let proposal_input = match args.get(1) {
Some(value) => value.clone(),
None => prompt_visible("Please enter the proposal key (64 hex characters): ").await,
};
let proposal_key = match validate_hash(&proposal_input, "Proposal key") {
Ok(hash) => hash,
Err(err) => {
eprintln!("{err}");
return;
}
};
let vote_input = match args.get(2) {
Some(value) => value.clone(),
None => prompt_visible("Vote on this proposal (yes/no): ").await,
};
let vote = match parse_vote(&vote_input) {
Ok(vote) => vote,
Err(err) => {
eprintln!("{err}");
return;
}
};
let txfee = match args.get(3) {
Some(value) => match parse_fee(value, PROPOSAL_VOTE_FEE) {
Ok(fee) => fee,
Err(err) => {
eprintln!("{err}");
return;
}
},
None => prompt_fee("proposal-vote transaction", PROPOSAL_VOTE_FEE).await,
};
let wallet = match load_wallet().await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("{err}");
return;
}
};
let timestamp = Utc::now().timestamp() as u32;
let unsigned = UnsignedProposalVote::new(
PROPOSAL_VOTE_TYPE,
timestamp,
&proposal_key,
&wallet.saved.short_address,
vote,
txfee,
)
.await;
let transaction = match ProposalVote::new(unsigned, &wallet.saved.private_key).await {
Ok(transaction) => transaction,
Err(err) => {
eprintln!("Could not sign proposal vote: {err}");
return;
}
};
let hash = transaction.unsigned_proposalvote.hash().await;
let output = json!({
"txtype": PROPOSAL_VOTE_TYPE,
"timestamp": timestamp,
"proposal_key": proposal_key,
"address": wallet.saved.short_address,
"vote": vote,
"txfee": txfee,
"hash": hash,
"signature": transaction.signature,
});
if let Err(err) = write_transaction(&hash, &output).await {
eprintln!("{err}");
}
}

View File

@ -0,0 +1,52 @@
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
use contractless::standalone_tools::governance::validate_hash;
use contractless::{env, Value};
#[tokio::main]
async fn main() {
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("Usage: ./lookup_governance_proposal <proposal_key>");
return;
}
let proposal_key = match validate_hash(&args[1], "Proposal key") {
Ok(hash) => hash,
Err(err) => {
eprintln!("{err}");
return;
}
};
let wallet_path = prompt_wallet_path().await;
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
for connection in get_connections().await {
let Ok(address) = connection.parse() else {
continue;
};
let result = handshake::connect_and_handshake(
address,
proposal_key.clone(),
57,
handshake::HandshakeWallet::WalletKey {
encryption_key: encryption_key.clone(),
wallet_path: wallet_path.clone(),
},
generate_uid(),
)
.await;
if let Ok(response) = result {
match serde_json::from_slice::<Value>(&response) {
Ok(json) => println!("{}", serde_json::to_string_pretty(&json).unwrap()),
Err(_) => println!("{}", String::from_utf8_lossy(&response)),
}
return;
}
}
eprintln!("failed to connect");
}

View File

@ -15,14 +15,16 @@ fn decode_nft_list(response: &[u8]) -> Option<String> {
return Some("{\n \"nfts\": {}\n}".to_string()); return Some("{\n \"nfts\": {}\n}".to_string());
} }
// Each NFT-list row is 32 bytes hash, 15 bytes name, and 4 bytes series. // Each row contains the origin hash, name, series, ownership rule,
if response.len() % 51 != 0 { // and current circulating supply.
const NFT_LIST_ROW_BYTES: usize = 32 + 15 + 4 + 1 + 8;
if response.len() % NFT_LIST_ROW_BYTES != 0 {
return None; return None;
} }
let mut grouped = Map::new(); let mut grouped = Map::new();
let mut offset = 0; let mut offset = 0;
while offset + 51 <= response.len() { while offset + NFT_LIST_ROW_BYTES <= response.len() {
// Group entries under their genesis hash so a collection can contain multiple series items. // Group entries under their genesis hash so a collection can contain multiple series items.
let hash = binary_to_string(response[offset..offset + 32].to_vec()) let hash = binary_to_string(response[offset..offset + 32].to_vec())
.trim() .trim()
@ -31,6 +33,9 @@ fn decode_nft_list(response: &[u8]) -> Option<String> {
.trim() .trim()
.to_string(); .to_string();
let series = u32::from_le_bytes(response[offset + 47..offset + 51].try_into().ok()?); let series = u32::from_le_bytes(response[offset + 47..offset + 51].try_into().ok()?);
let ownership_type = response[offset + 51];
let remaining_supply =
u64::from_le_bytes(response[offset + 52..offset + 60].try_into().ok()?);
if !hash.is_empty() && !nft_name.is_empty() { if !hash.is_empty() && !nft_name.is_empty() {
let entry = grouped let entry = grouped
@ -41,11 +46,13 @@ fn decode_nft_list(response: &[u8]) -> Option<String> {
items.push(json!({ items.push(json!({
"nft_name": nft_name, "nft_name": nft_name,
"series": series, "series": series,
"ownership_type": ownership_type,
"remaining_supply": remaining_supply,
})); }));
} }
} }
offset += 51; offset += NFT_LIST_ROW_BYTES;
} }
let output = json!({ let output = json!({

View File

@ -351,6 +351,18 @@ impl Block {
let tx_bytes = storage_key_tx.to_bytes().await?; let tx_bytes = storage_key_tx.to_bytes().await?;
buffer.extend_from_slice(&tx_bytes); buffer.extend_from_slice(&tx_bytes);
} }
Transaction::ProposalKey(proposal_key_tx) => {
let tx_bytes = proposal_key_tx.to_bytes().await?;
buffer.extend_from_slice(&tx_bytes);
}
Transaction::ProposalVote(proposal_vote_tx) => {
let tx_bytes = proposal_vote_tx.to_bytes().await?;
buffer.extend_from_slice(&tx_bytes);
}
Transaction::ActivationVote(activation_vote_tx) => {
let tx_bytes = activation_vote_tx.to_bytes().await?;
buffer.extend_from_slice(&tx_bytes);
}
Transaction::StorageBool(storage_tx) => { Transaction::StorageBool(storage_tx) => {
let tx_bytes = storage_tx.to_bytes().await?; let tx_bytes = storage_tx.to_bytes().await?;
buffer.extend_from_slice(&tx_bytes); buffer.extend_from_slice(&tx_bytes);

View File

@ -1,3 +1,4 @@
pub mod activation_vote;
pub mod block; pub mod block;
pub mod burn; pub mod burn;
pub mod collateral; pub mod collateral;
@ -8,6 +9,8 @@ pub mod loan_payment;
pub mod loans; pub mod loans;
pub mod marketing; pub mod marketing;
pub mod nft; pub mod nft;
pub mod proposal_key;
pub mod proposal_vote;
pub mod rewards; pub mod rewards;
pub mod storage_key; pub mod storage_key;
pub mod storage_values; pub mod storage_values;

View File

@ -8,12 +8,13 @@ use crate::Serialize;
use crate::{decode, encode}; use crate::{decode, encode};
use crate::{AsyncReadExt, AsyncWriteExt}; use crate::{AsyncReadExt, AsyncWriteExt};
#[derive(Debug, Serialize, Clone)] // 255 bytes #[derive(Debug, Serialize, Clone)] // 256 bytes
pub struct UnsignedCreateNftTransaction { pub struct UnsignedCreateNftTransaction {
pub txtype: u8, // 1 byte transaction type, should be 4 pub txtype: u8, // 1 byte transaction type, should be 4
pub time: u32, // 4 bytes transaction timestamp pub time: u32, // 4 bytes transaction timestamp
pub creator: String, // 22 bytes creator short address pub creator: String, // 22 bytes creator short address
pub series: u8, // 1 byte 0 for single NFT, 1 for series pub series: u8, // 1 byte 0 for single NFT, 1 for series
pub ownership_type: u8, // 1 byte 0 for indivisible, 1 for fractional ownership
pub nft_name: String, // 15 bytes NFT or collection name padded with spaces pub nft_name: String, // 15 bytes NFT or collection name padded with spaces
pub item_ipfs: String, // 100 bytes padded CID string pub item_ipfs: String, // 100 bytes padded CID string
pub count: u32, // 4 bytes 1 for single NFT, otherwise series item count pub count: u32, // 4 bytes 1 for single NFT, otherwise series item count
@ -21,9 +22,9 @@ pub struct UnsignedCreateNftTransaction {
pub txfee: u64, // 8 bytes transaction fee pub txfee: u64, // 8 bytes transaction fee
} }
#[derive(Debug, Serialize, Clone)] // 921 bytes #[derive(Debug, Serialize, Clone)] // 922 bytes
pub struct CreateNftTransaction { pub struct CreateNftTransaction {
pub unsigned_create_nft: UnsignedCreateNftTransaction, // 255 bytes pub unsigned_create_nft: UnsignedCreateNftTransaction, // 256 bytes
pub signature: String, // 666 bytes signature of hash pub signature: String, // 666 bytes signature of hash
} }
@ -32,6 +33,7 @@ impl CreateNftTransaction {
+ 4 + 4
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH + Wallet::SHORT_ADDRESS_BYTES_LENGTH
+ 1 + 1
+ 1
+ 15 + 15
+ 100 + 100
+ 4 + 4
@ -48,6 +50,7 @@ impl UnsignedCreateNftTransaction {
time: u32, time: u32,
creator: &str, creator: &str,
series: u8, series: u8,
ownership_type: u8,
nft_name: &str, nft_name: &str,
item_ipfs: &str, item_ipfs: &str,
count: u32, count: u32,
@ -59,6 +62,7 @@ impl UnsignedCreateNftTransaction {
time, time,
creator: creator.to_string(), creator: creator.to_string(),
series, series,
ownership_type,
nft_name: nft_name.to_string(), nft_name: nft_name.to_string(),
item_ipfs: item_ipfs.to_string(), item_ipfs: item_ipfs.to_string(),
count, count,
@ -132,6 +136,9 @@ impl CreateNftTransaction {
cursor cursor
.write_all(&self.unsigned_create_nft.series.to_le_bytes()) .write_all(&self.unsigned_create_nft.series.to_le_bytes())
.await?; .await?;
cursor
.write_all(&self.unsigned_create_nft.ownership_type.to_le_bytes())
.await?;
cursor cursor
.write_all(self.unsigned_create_nft.nft_name.as_bytes()) .write_all(self.unsigned_create_nft.nft_name.as_bytes())
.await?; .await?;
@ -171,6 +178,7 @@ impl CreateNftTransaction {
// Decode series flag, name, CID, count, description, fee, and signature. // Decode series flag, name, CID, count, description, fee, and signature.
let series = cursor.read_u8().await?; let series = cursor.read_u8().await?;
let ownership_type = cursor.read_u8().await?;
let mut nft_name_bytes = vec![0; 15]; let mut nft_name_bytes = vec![0; 15];
cursor.read_exact(&mut nft_name_bytes).await?; cursor.read_exact(&mut nft_name_bytes).await?;
@ -198,6 +206,7 @@ impl CreateNftTransaction {
time, time,
creator, creator,
series, series,
ownership_type,
nft_name, nft_name,
item_ipfs, item_ipfs,
count, count,

535
src/common/governance.rs Normal file
View File

@ -0,0 +1,535 @@
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use crate::common::skein::skein_256_hash_bytes;
use crate::decode;
use crate::wallets::structures::Wallet;
pub const GOVERNANCE_APPROVAL_PERCENT: u64 = 85;
pub const GOVERNANCE_PERCENT_SCALE: u64 = 100;
pub const GOVERNANCE_MIN_NODE_AGE_MILLIS: u64 = 60 * 60 * 1_000;
pub const GOVERNANCE_MIN_BLOCKS_MINED: u8 = 100;
pub const GOVERNANCE_OFFLINE_GRACE_BLOCKS: u32 = 5_760;
pub const GOVERNANCE_ACTIVATION_DELAY_BLOCKS: u32 = 5_760;
pub const GOVERNANCE_DOCUMENT_HASH_BYTES: usize = 32;
pub const GOVERNANCE_DEVELOPMENT_LOCATION_BYTES: usize = 100;
pub const PROPOSAL_KEYS_TREE: &str = "proposal_keys";
pub const PROPOSAL_VOTES_TREE: &str = "proposal_votes";
pub const FINISHED_PROPOSAL_VOTES_TREE: &str = "finished_proposal_votes";
pub const ACTIVATION_VOTES_TREE: &str = "activation_votes";
pub const FINISHED_ACTIVATION_VOTES_TREE: &str = "finished_activation_votes";
pub const CONSENSUS_ACTIVATIONS_TREE: &str = "consensus_activations";
pub const GOVERNANCE_EXPIRATIONS_TREE: &str = "governance_expirations";
pub const SUPERSEDED_GOVERNANCE_VOTES_TREE: &str = "superseded_governance_votes";
pub const PROPOSAL_VOTE_RECORD_KEY_BYTES: usize =
GOVERNANCE_DOCUMENT_HASH_BYTES + Wallet::SHORT_ADDRESS_BYTES_LENGTH;
pub const ACTIVATION_CANDIDATE_KEY_BYTES: usize = GOVERNANCE_DOCUMENT_HASH_BYTES * 2;
pub const ACTIVATION_VOTE_RECORD_KEY_BYTES: usize =
ACTIVATION_CANDIDATE_KEY_BYTES + Wallet::SHORT_ADDRESS_BYTES_LENGTH;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum GovernanceVote {
No = 0,
Yes = 1,
}
impl TryFrom<u8> for GovernanceVote {
type Error = String;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::No),
1 => Ok(Self::Yes),
_ => Err("Governance vote must be 0 (no) or 1 (yes).".to_string()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProposalState {
Open,
Approved,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ActivationState {
Open,
Scheduled,
Activated,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GovernanceNodeSnapshot {
pub address: String,
pub ip: String,
pub blocks_mined: u8,
pub added_timestamp: u64,
pub deleted_block: u32,
}
pub fn countable_governance_addresses(
snapshots: &[GovernanceNodeSnapshot],
existing_voters: &HashSet<String>,
reference_timestamp_millis: u64,
current_height: u32,
) -> HashSet<String> {
// An active wallet supersedes older wallet records on the same IP for
// unfinished votes. Historical records remain available for validating
// old blocks, but one physical node cannot count twice in governance.
let active_wallet_by_ip: HashMap<&str, &str> = snapshots
.iter()
.filter(|node| node.is_active())
.map(|node| (node.ip.as_str(), node.address.as_str()))
.collect();
snapshots
.iter()
.filter(|node| {
active_wallet_by_ip
.get(node.ip.as_str())
.map(|active_address| *active_address == node.address)
.unwrap_or(true)
})
.filter(|node| {
if existing_voters.contains(&node.address) {
node.existing_vote_is_countable(current_height)
} else {
node.is_eligible_for_governance_count(reference_timestamp_millis, current_height)
}
})
.map(|node| node.address.clone())
.collect()
}
impl GovernanceNodeSnapshot {
pub fn is_active(&self) -> bool {
self.deleted_block == 0
}
pub fn meets_voting_history_requirements(&self, reference_timestamp_millis: u64) -> bool {
self.blocks_mined >= GOVERNANCE_MIN_BLOCKS_MINED
&& reference_timestamp_millis.saturating_sub(self.added_timestamp)
>= GOVERNANCE_MIN_NODE_AGE_MILLIS
}
pub fn can_cast_vote(&self, reference_timestamp_millis: u64) -> bool {
self.is_active() && self.meets_voting_history_requirements(reference_timestamp_millis)
}
pub fn can_cast_vote_at(&self, reference_timestamp_millis: u64, block_height: u32) -> bool {
let active_at_height = self.deleted_block == 0 || block_height < self.deleted_block;
active_at_height && self.meets_voting_history_requirements(reference_timestamp_millis)
}
pub fn is_within_offline_grace(&self, current_height: u32) -> bool {
self.is_active()
|| current_height.saturating_sub(self.deleted_block) < GOVERNANCE_OFFLINE_GRACE_BLOCKS
}
pub fn is_eligible_for_governance_count(
&self,
reference_timestamp_millis: u64,
current_height: u32,
) -> bool {
self.meets_voting_history_requirements(reference_timestamp_millis)
&& self.is_within_offline_grace(current_height)
}
pub fn existing_vote_is_countable(&self, current_height: u32) -> bool {
self.is_within_offline_grace(current_height)
}
}
pub fn hash_governance_document(document_bytes: &[u8]) -> String {
skein_256_hash_bytes(document_bytes)
}
pub fn is_valid_governance_hash(hash: &str) -> bool {
hex::decode(hash)
.map(|decoded| decoded.len() == GOVERNANCE_DOCUMENT_HASH_BYTES)
.unwrap_or(false)
}
pub fn normalize_development_location(location: &str) -> Result<String, String> {
if location.is_empty() {
return Err("Development document location cannot be empty.".to_string());
}
if !location.is_ascii() {
return Err(
"Development document location must contain only ASCII characters.".to_string(),
);
}
if location.len() > GOVERNANCE_DEVELOPMENT_LOCATION_BYTES {
return Err(format!(
"Development document location cannot exceed {} bytes.",
GOVERNANCE_DEVELOPMENT_LOCATION_BYTES
));
}
Ok(format!(
"{location:<width$}",
width = GOVERNANCE_DEVELOPMENT_LOCATION_BYTES
))
}
pub fn is_valid_padded_development_location(location: &str) -> bool {
location.len() == GOVERNANCE_DEVELOPMENT_LOCATION_BYTES
&& location.is_ascii()
&& !location.trim().is_empty()
}
pub fn governance_approval_reached(yes_votes: u64, eligible_nodes: u64) -> bool {
eligible_nodes > 0
&& (yes_votes as u128) * (GOVERNANCE_PERCENT_SCALE as u128)
>= (eligible_nodes as u128) * (GOVERNANCE_APPROVAL_PERCENT as u128)
}
pub fn governance_activation_height(finalized_height: u32) -> Option<u32> {
finalized_height.checked_add(GOVERNANCE_ACTIVATION_DELAY_BLOCKS)
}
/// Returns whether a specific implementation was approved and the block at
/// which it activates. A proposal without a schedule returns `(false, 0)`.
///
/// Callers compare the returned activation height with the height of the
/// specific block being validated, decoded, replayed, or undone.
pub fn implementation_activation_status(
db: &crate::sled::Db,
proposal_key: &str,
expected_development_hash: &str,
) -> Result<(bool, u32), String> {
let proposal_key =
decode(proposal_key).map_err(|err| format!("Could not decode proposal key: {err}"))?;
if proposal_key.len() != GOVERNANCE_DOCUMENT_HASH_BYTES {
return Err("Proposal key must decode to exactly 32 bytes.".to_string());
}
let expected_development_hash = decode(expected_development_hash)
.map_err(|err| format!("Could not decode expected development hash: {err}"))?;
if expected_development_hash.len() != GOVERNANCE_DOCUMENT_HASH_BYTES {
return Err("Expected development hash must decode to exactly 32 bytes.".to_string());
}
let activations = db
.open_tree(CONSENSUS_ACTIVATIONS_TREE)
.map_err(|err| format!("Could not open consensus activations: {err}"))?;
let Some(schedule) = activations
.get(proposal_key)
.map_err(|err| format!("Could not read consensus activation: {err}"))?
else {
return Ok((false, 0));
};
let expected_length = GOVERNANCE_DOCUMENT_HASH_BYTES
+ std::mem::size_of::<u32>()
+ GOVERNANCE_DEVELOPMENT_LOCATION_BYTES;
if schedule.len() != expected_length {
return Err(format!(
"Stored consensus activation has an invalid byte count: expected {expected_length}, found {}.",
schedule.len()
));
}
if schedule[..GOVERNANCE_DOCUMENT_HASH_BYTES] != expected_development_hash {
return Err(
"This proposal scheduled a different implementation than this software expects."
.to_string(),
);
}
let height_start = GOVERNANCE_DOCUMENT_HASH_BYTES;
let height_end = height_start + std::mem::size_of::<u32>();
let activation_height = u32::from_le_bytes(
schedule[height_start..height_end]
.try_into()
.map_err(|_| "Could not decode the consensus activation height.".to_string())?,
);
Ok((true, activation_height))
}
pub fn proposal_vote_record_key(
proposal_key: &str,
voter_address: &str,
) -> Result<Vec<u8>, String> {
let mut key =
decode(proposal_key).map_err(|err| format!("Could not decode proposal key: {err}"))?;
if key.len() != GOVERNANCE_DOCUMENT_HASH_BYTES {
return Err("Proposal key must decode to exactly 32 bytes.".to_string());
}
let address = Wallet::short_address_to_bytes(voter_address)
.ok_or_else(|| "Proposal voter address is invalid.".to_string())?;
key.extend(address);
Ok(key)
}
pub fn activation_candidate_key(
proposal_key: &str,
development_hash: &str,
) -> Result<Vec<u8>, String> {
let mut key =
decode(proposal_key).map_err(|err| format!("Could not decode proposal key: {err}"))?;
if key.len() != GOVERNANCE_DOCUMENT_HASH_BYTES {
return Err("Proposal key must decode to exactly 32 bytes.".to_string());
}
let development = decode(development_hash)
.map_err(|err| format!("Could not decode development document hash: {err}"))?;
if development.len() != GOVERNANCE_DOCUMENT_HASH_BYTES {
return Err("Development document hash must decode to exactly 32 bytes.".to_string());
}
key.extend(development);
Ok(key)
}
pub fn activation_vote_record_key(
proposal_key: &str,
development_hash: &str,
voter_address: &str,
) -> Result<Vec<u8>, String> {
let mut key = activation_candidate_key(proposal_key, development_hash)?;
let address = Wallet::short_address_to_bytes(voter_address)
.ok_or_else(|| "Activation voter address is invalid.".to_string())?;
key.extend(address);
Ok(key)
}
pub fn encode_finished_activation_vote(
yes_votes: u64,
no_votes: u64,
eligible_nodes: u64,
finalized_height: u32,
activation_height: u32,
development_location: &str,
) -> Result<Vec<u8>, String> {
if !is_valid_padded_development_location(development_location) {
return Err("Development location is not a canonical 100-byte value.".to_string());
}
let mut value = Vec::with_capacity(132);
value.extend_from_slice(&yes_votes.to_le_bytes());
value.extend_from_slice(&no_votes.to_le_bytes());
value.extend_from_slice(&eligible_nodes.to_le_bytes());
value.extend_from_slice(&finalized_height.to_le_bytes());
value.extend_from_slice(&activation_height.to_le_bytes());
value.extend_from_slice(development_location.as_bytes());
Ok(value)
}
pub fn encode_consensus_activation(
development_hash: &str,
activation_height: u32,
development_location: &str,
) -> Result<Vec<u8>, String> {
let development = decode(development_hash)
.map_err(|err| format!("Could not decode development document hash: {err}"))?;
if development.len() != GOVERNANCE_DOCUMENT_HASH_BYTES {
return Err("Development document hash must decode to exactly 32 bytes.".to_string());
}
if !is_valid_padded_development_location(development_location) {
return Err("Development location is not a canonical 100-byte value.".to_string());
}
let mut value = Vec::with_capacity(136);
value.extend(development);
value.extend_from_slice(&activation_height.to_le_bytes());
value.extend_from_slice(development_location.as_bytes());
Ok(value)
}
pub fn encode_finished_vote(
yes_votes: u64,
no_votes: u64,
eligible_nodes: u64,
block_height: u32,
) -> Vec<u8> {
let mut value = Vec::with_capacity(28);
value.extend_from_slice(&yes_votes.to_le_bytes());
value.extend_from_slice(&no_votes.to_le_bytes());
value.extend_from_slice(&eligible_nodes.to_le_bytes());
value.extend_from_slice(&block_height.to_le_bytes());
value
}
#[cfg(test)]
mod tests {
use super::*;
fn node() -> GovernanceNodeSnapshot {
GovernanceNodeSnapshot {
address: "address".to_string(),
ip: "127.0.0.1".to_string(),
blocks_mined: GOVERNANCE_MIN_BLOCKS_MINED,
added_timestamp: 1_000,
deleted_block: 0,
}
}
#[test]
fn approval_uses_an_exact_85_percent_threshold() {
assert!(governance_approval_reached(17, 20));
assert!(!governance_approval_reached(16, 20));
assert!(governance_approval_reached(8, 9));
assert!(!governance_approval_reached(0, 0));
}
#[test]
fn governance_hash_commits_to_exact_document_bytes() {
let unix = hash_governance_document(b"proposal\n");
let windows = hash_governance_document(b"proposal\r\n");
assert_ne!(unix, windows);
assert!(is_valid_governance_hash(&unix));
assert!(!is_valid_governance_hash("abcd"));
}
#[test]
fn development_location_is_fixed_width_ascii() {
let location = normalize_development_location("CLP-1-implementation.md").unwrap();
assert_eq!(location.len(), GOVERNANCE_DEVELOPMENT_LOCATION_BYTES);
assert!(location.starts_with("CLP-1-implementation.md"));
assert!(normalize_development_location("").is_err());
assert!(normalize_development_location("proposal-🚫").is_err());
assert!(normalize_development_location(&"a".repeat(101)).is_err());
}
#[test]
fn vote_values_are_strict() {
assert_eq!(GovernanceVote::try_from(0).unwrap(), GovernanceVote::No);
assert_eq!(GovernanceVote::try_from(1).unwrap(), GovernanceVote::Yes);
assert!(GovernanceVote::try_from(2).is_err());
}
#[test]
fn voting_requires_active_mature_node_with_mined_blocks() {
let reference_time = 1_000 + GOVERNANCE_MIN_NODE_AGE_MILLIS;
let mut candidate = node();
assert!(candidate.can_cast_vote(reference_time));
candidate.blocks_mined = GOVERNANCE_MIN_BLOCKS_MINED - 1;
assert!(!candidate.can_cast_vote(reference_time));
candidate.blocks_mined = GOVERNANCE_MIN_BLOCKS_MINED;
candidate.deleted_block = 1;
assert!(!candidate.can_cast_vote(reference_time));
}
#[test]
fn offline_vote_grace_expires_at_5760_blocks() {
let mut candidate = node();
candidate.deleted_block = 100;
assert!(candidate.existing_vote_is_countable(5_859));
assert!(!candidate.existing_vote_is_countable(5_860));
}
#[test]
fn active_replacement_wallet_supersedes_old_vote_on_same_ip() {
let reference_time = 1_000 + GOVERNANCE_MIN_NODE_AGE_MILLIS;
let old = GovernanceNodeSnapshot {
address: "old".to_string(),
ip: "1.2.3.4".to_string(),
blocks_mined: 100,
added_timestamp: 1_000,
deleted_block: 100,
};
let replacement = GovernanceNodeSnapshot {
address: "new".to_string(),
ip: "1.2.3.4".to_string(),
blocks_mined: 0,
added_timestamp: reference_time,
deleted_block: 0,
};
let voters = HashSet::from(["old".to_string()]);
let countable =
countable_governance_addresses(&[old, replacement], &voters, reference_time, 101);
assert!(!countable.contains("old"));
assert!(!countable.contains("new"));
}
#[test]
fn existing_vote_reactivates_immediately_with_same_wallet() {
let reference_time = 1_000 + GOVERNANCE_MIN_NODE_AGE_MILLIS;
let returning = GovernanceNodeSnapshot {
address: "returning".to_string(),
ip: "1.2.3.4".to_string(),
blocks_mined: 100,
added_timestamp: reference_time,
deleted_block: 0,
};
let voters = HashSet::from(["returning".to_string()]);
let countable =
countable_governance_addresses(&[returning], &voters, reference_time, 10_000);
assert!(countable.contains("returning"));
}
#[test]
fn missing_implementation_schedule_is_not_active() {
let db = crate::sled::Config::new().temporary(true).open().unwrap();
let proposal_key = "11".repeat(GOVERNANCE_DOCUMENT_HASH_BYTES);
let development_hash = "22".repeat(GOVERNANCE_DOCUMENT_HASH_BYTES);
assert_eq!(
implementation_activation_status(&db, &proposal_key, &development_hash).unwrap(),
(false, 0)
);
}
#[test]
fn approved_implementation_returns_its_scheduled_height() {
let db = crate::sled::Config::new().temporary(true).open().unwrap();
let proposal_key = "11".repeat(GOVERNANCE_DOCUMENT_HASH_BYTES);
let development_hash = "22".repeat(GOVERNANCE_DOCUMENT_HASH_BYTES);
let location = normalize_development_location("CLPs/CLP-0001-IMPLEMENTATION.md").unwrap();
let schedule = encode_consensus_activation(&development_hash, 500, &location).unwrap();
db.open_tree(CONSENSUS_ACTIVATIONS_TREE)
.unwrap()
.insert(decode(&proposal_key).unwrap(), schedule)
.unwrap();
assert_eq!(
implementation_activation_status(&db, &proposal_key, &development_hash).unwrap(),
(true, 500)
);
}
#[test]
fn unexpected_scheduled_implementation_fails_closed() {
let db = crate::sled::Config::new().temporary(true).open().unwrap();
let proposal_key = "11".repeat(GOVERNANCE_DOCUMENT_HASH_BYTES);
let scheduled_hash = "22".repeat(GOVERNANCE_DOCUMENT_HASH_BYTES);
let expected_hash = "33".repeat(GOVERNANCE_DOCUMENT_HASH_BYTES);
let location = normalize_development_location("CLPs/CLP-0001-IMPLEMENTATION.md").unwrap();
let schedule = encode_consensus_activation(&scheduled_hash, 500, &location).unwrap();
db.open_tree(CONSENSUS_ACTIVATIONS_TREE)
.unwrap()
.insert(decode(&proposal_key).unwrap(), schedule)
.unwrap();
let error =
implementation_activation_status(&db, &proposal_key, &expected_hash).unwrap_err();
assert!(error.contains("different implementation"));
}
#[test]
fn malformed_implementation_schedule_is_rejected() {
let db = crate::sled::Config::new().temporary(true).open().unwrap();
let proposal_key = "11".repeat(GOVERNANCE_DOCUMENT_HASH_BYTES);
let development_hash = "22".repeat(GOVERNANCE_DOCUMENT_HASH_BYTES);
db.open_tree(CONSENSUS_ACTIVATIONS_TREE)
.unwrap()
.insert(decode(&proposal_key).unwrap(), vec![0; 10])
.unwrap();
let error =
implementation_activation_status(&db, &proposal_key, &development_hash).unwrap_err();
assert!(error.contains("invalid byte count"));
}
}

View File

@ -2,6 +2,7 @@ pub mod asset_names;
pub mod binary_conversions; pub mod binary_conversions;
pub mod check_genesis; pub mod check_genesis;
pub mod cli_prompts; pub mod cli_prompts;
pub mod governance;
pub mod loan_schedule; pub mod loan_schedule;
pub mod network_paths_and_settings; pub mod network_paths_and_settings;
pub mod network_startup; pub mod network_startup;

View File

@ -1,3 +1,9 @@
use crate::sled::Db;
pub const NFT_UNIT: u64 = 100_000_000;
pub const NFT_OWNERSHIP_INDIVISIBLE: u8 = 0;
pub const NFT_OWNERSHIP_FRACTIONAL: u8 = 1;
// Build the visible NFT asset name, adding the series suffix only // Build the visible NFT asset name, adding the series suffix only
// for numbered series entries. // for numbered series entries.
pub fn nft_asset_name(name: &str, nft_series: u32) -> String { pub fn nft_asset_name(name: &str, nft_series: u32) -> String {
@ -24,3 +30,56 @@ pub fn nft_asset_parts(asset_name: &str) -> (String, u32) {
// Non-series assets keep their original name and use series zero. // Non-series assets keep their original name and use series zero.
(asset_name.to_string(), 0) (asset_name.to_string(), 0)
} }
// Return the immutable ownership rule stored for a concrete NFT/RWA asset.
// A missing value means the requested asset is not registered as an NFT/RWA.
pub fn nft_ownership_type(db: &Db, asset_name: &str) -> Result<Option<u8>, String> {
let tree = db
.open_tree("nfts")
.map_err(|err| format!("Failed to open NFT registry: {err}"))?;
let Some(value) = tree
.get(asset_name.as_bytes())
.map_err(|err| format!("Failed to read NFT registry: {err}"))?
else {
return Ok(None);
};
match value.as_ref() {
[NFT_OWNERSHIP_INDIVISIBLE] => Ok(Some(NFT_OWNERSHIP_INDIVISIBLE)),
[NFT_OWNERSHIP_FRACTIONAL] => Ok(Some(NFT_OWNERSHIP_FRACTIONAL)),
_ => Err("NFT registry contains an invalid ownership type.".to_string()),
}
}
// Enforce the amount rule selected when the NFT/RWA was created.
pub fn validate_nft_amount(ownership_type: u8, value: u64, operation: &str) -> Result<(), String> {
match ownership_type {
NFT_OWNERSHIP_INDIVISIBLE if value == NFT_UNIT => Ok(()),
NFT_OWNERSHIP_INDIVISIBLE => Err(format!(
"Indivisible NFT/RWA {operation} amount must be exactly {NFT_UNIT} units."
)),
NFT_OWNERSHIP_FRACTIONAL if value > 0 && value <= NFT_UNIT => Ok(()),
NFT_OWNERSHIP_FRACTIONAL => Err(format!(
"Fractional NFT/RWA {operation} amount must be between 1 and {NFT_UNIT} units."
)),
_ => Err("NFT/RWA has an invalid ownership type.".to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn indivisible_assets_require_one_complete_unit() {
assert!(validate_nft_amount(NFT_OWNERSHIP_INDIVISIBLE, NFT_UNIT, "transfer").is_ok());
assert!(validate_nft_amount(NFT_OWNERSHIP_INDIVISIBLE, NFT_UNIT - 1, "transfer").is_err());
}
#[test]
fn fractional_assets_accept_partial_ownership() {
assert!(validate_nft_amount(NFT_OWNERSHIP_FRACTIONAL, 56_000_000, "transfer").is_ok());
assert!(validate_nft_amount(NFT_OWNERSHIP_FRACTIONAL, 0, "transfer").is_err());
assert!(validate_nft_amount(NFT_OWNERSHIP_FRACTIONAL, NFT_UNIT + 1, "transfer").is_err());
}
}

View File

@ -1,3 +1,4 @@
use crate::blocks::activation_vote::ActivationVote;
use crate::blocks::burn::BurnTransaction; use crate::blocks::burn::BurnTransaction;
use crate::blocks::collateral::CollateralClaimTransaction; use crate::blocks::collateral::CollateralClaimTransaction;
use crate::blocks::delete_key::DeleteKey; use crate::blocks::delete_key::DeleteKey;
@ -7,6 +8,8 @@ use crate::blocks::loan_payment::ContractPaymentTransaction;
use crate::blocks::loans::LoanContractTransaction; use crate::blocks::loans::LoanContractTransaction;
use crate::blocks::marketing::MarketingTransaction; use crate::blocks::marketing::MarketingTransaction;
use crate::blocks::nft::CreateNftTransaction; use crate::blocks::nft::CreateNftTransaction;
use crate::blocks::proposal_key::ProposalKey;
use crate::blocks::proposal_vote::ProposalVote;
use crate::blocks::rewards::RewardsTransaction; use crate::blocks::rewards::RewardsTransaction;
use crate::blocks::storage_key::StorageKey; use crate::blocks::storage_key::StorageKey;
use crate::blocks::storage_values::{ use crate::blocks::storage_values::{
@ -49,6 +52,11 @@ pub const STORAGE_I64_TYPE: u8 = 111;
pub const STORAGE_I128_TYPE: u8 = 112; pub const STORAGE_I128_TYPE: u8 = 112;
pub const DELETE_KEY_TYPE: u8 = 113; pub const DELETE_KEY_TYPE: u8 = 113;
// On-chain governance transaction types.
pub const PROPOSAL_KEY_TYPE: u8 = 200;
pub const PROPOSAL_VOTE_TYPE: u8 = 201;
pub const ACTIVATION_VOTE_TYPE: u8 = 202;
// Coin and asset names are stored in fixed 15-byte fields. // Coin and asset names are stored in fixed 15-byte fields.
pub const COIN_LENGTH: usize = 15; pub const COIN_LENGTH: usize = 15;
@ -84,6 +92,9 @@ pub const ISSUE_TOKEN_FEE: u64 = 10000000000; // cost to issue more of an existi
pub const VANITY_ADDRESS_FEE: u64 = 500000000; // cost to register or update a vanity address, 5 CLC pub const VANITY_ADDRESS_FEE: u64 = 500000000; // cost to register or update a vanity address, 5 CLC
pub const STORAGE_KEY_FEE: u64 = 50000000000; // cost to create an app storage key, 500 CLC pub const STORAGE_KEY_FEE: u64 = 50000000000; // cost to create an app storage key, 500 CLC
pub const STORAGE_VALUE_FEE: u64 = 100000000; // cost to write app storage data, 1 CLC pub const STORAGE_VALUE_FEE: u64 = 100000000; // cost to write app storage data, 1 CLC
pub const PROPOSAL_KEY_FEE: u64 = 100000000; // cost to create a governance proposal key, 1 CLC
pub const PROPOSAL_VOTE_FEE: u64 = 100000000; // cost to vote on a governance proposal, 1 CLC
pub const ACTIVATION_VOTE_FEE: u64 = 100000000; // cost to vote on proposal activation, 1 CLC
pub const STORAGE_KEY_INDEX_TREE: &str = "storage_key_index"; pub const STORAGE_KEY_INDEX_TREE: &str = "storage_key_index";
pub const STORAGE_VALUE_ROLLBACK_TREE: &str = "storage_value_rollback"; pub const STORAGE_VALUE_ROLLBACK_TREE: &str = "storage_value_rollback";
@ -130,6 +141,9 @@ pub enum Transaction {
StorageI64(StorageI64), StorageI64(StorageI64),
StorageI128(StorageI128), StorageI128(StorageI128),
DeleteKey(DeleteKey), DeleteKey(DeleteKey),
ProposalKey(ProposalKey),
ProposalVote(ProposalVote),
ActivationVote(ActivationVote),
} }
impl Transaction { impl Transaction {
@ -164,6 +178,9 @@ impl Transaction {
Transaction::StorageI64(tx) => Some(&tx.signature), Transaction::StorageI64(tx) => Some(&tx.signature),
Transaction::StorageI128(tx) => Some(&tx.signature), Transaction::StorageI128(tx) => Some(&tx.signature),
Transaction::DeleteKey(tx) => Some(&tx.signature), Transaction::DeleteKey(tx) => Some(&tx.signature),
Transaction::ProposalKey(tx) => Some(&tx.signature),
Transaction::ProposalVote(tx) => Some(&tx.signature),
Transaction::ActivationVote(tx) => Some(&tx.signature),
} }
} }
} }

View File

@ -4,6 +4,7 @@ use crate::orphans::save_blocks::save_new_blocks;
use crate::orphans::structs::UndoTransactions; use crate::orphans::structs::UndoTransactions;
use crate::orphans::undo_block::undo_block; use crate::orphans::undo_block::undo_block;
use crate::orphans::undo_transactions::restore_mempool::restore_rolled_back_transaction; use crate::orphans::undo_transactions::restore_mempool::restore_rolled_back_transaction;
use crate::orphans::undo_transactions::undo_activation_vote::undo_activation_vote_transaction;
use crate::orphans::undo_transactions::undo_borrower::undo_borrower_transaction; use crate::orphans::undo_transactions::undo_borrower::undo_borrower_transaction;
use crate::orphans::undo_transactions::undo_burn::undo_burn_transaction; use crate::orphans::undo_transactions::undo_burn::undo_burn_transaction;
use crate::orphans::undo_transactions::undo_collateral::undo_collateral_transaction; use crate::orphans::undo_transactions::undo_collateral::undo_collateral_transaction;
@ -13,6 +14,8 @@ use crate::orphans::undo_transactions::undo_delete_key::undo_delete_key_transact
use crate::orphans::undo_transactions::undo_issue_token::undo_issue_token_transaction; use crate::orphans::undo_transactions::undo_issue_token::undo_issue_token_transaction;
use crate::orphans::undo_transactions::undo_loan_creation::undo_loan_creation_transaction; use crate::orphans::undo_transactions::undo_loan_creation::undo_loan_creation_transaction;
use crate::orphans::undo_transactions::undo_marketing::undo_marketing_transaction; use crate::orphans::undo_transactions::undo_marketing::undo_marketing_transaction;
use crate::orphans::undo_transactions::undo_proposal_key::undo_proposal_key_transaction;
use crate::orphans::undo_transactions::undo_proposal_vote::undo_proposal_vote_transaction;
use crate::orphans::undo_transactions::undo_rewards::undo_rewards_transaction; use crate::orphans::undo_transactions::undo_rewards::undo_rewards_transaction;
use crate::orphans::undo_transactions::undo_storage_key::undo_storage_key_transaction; use crate::orphans::undo_transactions::undo_storage_key::undo_storage_key_transaction;
use crate::orphans::undo_transactions::undo_storage_value::undo_storage_value_transaction; use crate::orphans::undo_transactions::undo_storage_value::undo_storage_value_transaction;
@ -142,6 +145,35 @@ pub async fn undo_transactions(
.await?; .await?;
rolled_back_transactions.push(Transaction::StorageKey(storage_key_tx)); rolled_back_transactions.push(Transaction::StorageKey(storage_key_tx));
} }
Transaction::ProposalKey(proposal_key_tx) => {
undo_proposal_key_transaction(
proposal_key_tx.clone(),
&mining_receiver,
&params.db,
)
.await?;
rolled_back_transactions.push(Transaction::ProposalKey(proposal_key_tx));
}
Transaction::ProposalVote(proposal_vote_tx) => {
undo_proposal_vote_transaction(
proposal_vote_tx.clone(),
&mining_receiver,
current_height,
&params.db,
)
.await?;
rolled_back_transactions.push(Transaction::ProposalVote(proposal_vote_tx));
}
Transaction::ActivationVote(activation_vote_tx) => {
undo_activation_vote_transaction(
activation_vote_tx.clone(),
&mining_receiver,
current_height,
&params.db,
)
.await?;
rolled_back_transactions.push(Transaction::ActivationVote(activation_vote_tx));
}
Transaction::StorageBool(_) Transaction::StorageBool(_)
| Transaction::StorageU8(_) | Transaction::StorageU8(_)
| Transaction::StorageU16(_) | Transaction::StorageU16(_)

View File

@ -1,4 +1,5 @@
pub mod restore_mempool; pub mod restore_mempool;
pub mod undo_activation_vote;
pub mod undo_borrower; pub mod undo_borrower;
pub mod undo_burn; pub mod undo_burn;
pub mod undo_collateral; pub mod undo_collateral;
@ -8,6 +9,8 @@ pub mod undo_delete_key;
pub mod undo_issue_token; pub mod undo_issue_token;
pub mod undo_loan_creation; pub mod undo_loan_creation;
pub mod undo_marketing; pub mod undo_marketing;
pub mod undo_proposal_key;
pub mod undo_proposal_vote;
pub mod undo_rewards; pub mod undo_rewards;
pub mod undo_storage_key; pub mod undo_storage_key;
pub mod undo_storage_value; pub mod undo_storage_value;

View File

@ -6,6 +6,8 @@ use crate::blocks::loan_payment::ContractPaymentTransaction;
use crate::blocks::loans::LoanContractTransaction; use crate::blocks::loans::LoanContractTransaction;
use crate::blocks::marketing::MarketingTransaction; use crate::blocks::marketing::MarketingTransaction;
use crate::blocks::nft::CreateNftTransaction; use crate::blocks::nft::CreateNftTransaction;
use crate::blocks::proposal_key::ProposalKey;
use crate::blocks::proposal_vote::ProposalVote;
use crate::blocks::storage_key::StorageKey; use crate::blocks::storage_key::StorageKey;
use crate::blocks::storage_values::StorageValueTransaction; use crate::blocks::storage_values::StorageValueTransaction;
use crate::blocks::swap::SwapTransaction; use crate::blocks::swap::SwapTransaction;
@ -334,6 +336,58 @@ pub async fn restore_storage_key(transaction: &StorageKey, db: &Db) {
.await; .await;
} }
pub async fn restore_proposal_key(transaction: &ProposalKey, db: &Db) {
let proposal = &transaction.unsigned_proposalkey;
let spendable =
balance_checkup(db, 0, proposal.txfee, BASECOIN.clone(), &proposal.address).await;
let signature = transaction.signature.clone();
restore_if_spendable_void(
"proposal_key",
"unknown",
&[signature],
spendable,
|| async {
let _ = transaction.add_to_memory().await;
},
)
.await;
}
pub async fn restore_proposal_vote(transaction: &ProposalVote, db: &Db) {
let vote = &transaction.unsigned_proposalvote;
let spendable = balance_checkup(db, 0, vote.txfee, BASECOIN.clone(), &vote.address).await;
let signature = transaction.signature.clone();
restore_if_spendable_void(
"proposal_vote",
"unknown",
&[signature],
spendable,
|| async {
let _ = transaction.add_to_memory().await;
},
)
.await;
}
pub async fn restore_activation_vote(transaction: &ActivationVote, db: &Db) {
let vote = &transaction.unsigned_activationvote;
let spendable = balance_checkup(db, 0, vote.txfee, BASECOIN.clone(), &vote.address).await;
let signature = transaction.signature.clone();
restore_if_spendable_void(
"activation_vote",
"unknown",
&[signature],
spendable,
|| async {
let _ = transaction.add_to_memory().await;
},
)
.await;
}
async fn restore_storage_value<T, F, Fut>(tx_label: &str, transaction: &T, db: &Db, insert: F) async fn restore_storage_value<T, F, Fut>(tx_label: &str, transaction: &T, db: &Db, insert: F)
where where
T: StorageValueTransaction, T: StorageValueTransaction,
@ -374,6 +428,9 @@ pub async fn restore_rolled_back_transaction(transaction: &Transaction, db: &Db)
Transaction::Collateral(tx) => restore_collateral(tx, db).await, Transaction::Collateral(tx) => restore_collateral(tx, db).await,
Transaction::Vanity(tx) => restore_vanity(tx, db).await, Transaction::Vanity(tx) => restore_vanity(tx, db).await,
Transaction::StorageKey(tx) => restore_storage_key(tx, db).await, Transaction::StorageKey(tx) => restore_storage_key(tx, db).await,
Transaction::ProposalKey(tx) => restore_proposal_key(tx, db).await,
Transaction::ProposalVote(tx) => restore_proposal_vote(tx, db).await,
Transaction::ActivationVote(tx) => restore_activation_vote(tx, db).await,
Transaction::StorageBool(tx) => { Transaction::StorageBool(tx) => {
restore_storage_value("storage_bool", tx, db, || async { restore_storage_value("storage_bool", tx, db, || async {
tx.add_to_memory().await tx.add_to_memory().await
@ -432,3 +489,4 @@ pub async fn restore_rolled_back_transaction(transaction: &Transaction, db: &Db)
Transaction::Genesis(_) | Transaction::Rewards(_) => {} Transaction::Genesis(_) | Transaction::Rewards(_) => {}
} }
} }
use crate::blocks::activation_vote::ActivationVote;

View File

@ -0,0 +1,71 @@
use crate::blocks::activation_vote::ActivationVote;
use crate::common::governance::{
activation_candidate_key, activation_vote_record_key, ACTIVATION_VOTES_TREE,
CONSENSUS_ACTIVATIONS_TREE, FINISHED_ACTIVATION_VOTES_TREE,
};
use crate::decode;
use crate::records::balance_sheet::operations::balance_sheet_operation_with_db;
use crate::records::memory::mempool::BASECOIN;
use crate::records::record_chain::wallet_tx_index::remove_wallet_transaction_index;
use crate::sled::Db;
pub async fn undo_activation_vote_transaction(
transaction: ActivationVote,
mining_receiver: &str,
block_height: u32,
db: &Db,
) -> Result<(), String> {
let vote = &transaction.unsigned_activationvote;
let txhash = vote.hash().await;
let txhash_bytes = decode(&txhash)
.map_err(|err| format!("Could not decode activation vote txhash during undo: {err}"))?;
let proposal_key = decode(&vote.proposal_key)
.map_err(|err| format!("Could not decode proposal key during undo: {err}"))?;
let candidate_key = activation_candidate_key(&vote.proposal_key, &vote.development_hash)?;
let vote_key =
activation_vote_record_key(&vote.proposal_key, &vote.development_hash, &vote.address)?;
let _ =
balance_sheet_operation_with_db(db, mining_receiver, vote.txfee, &BASECOIN, "subtraction");
let _ = balance_sheet_operation_with_db(db, &vote.address, vote.txfee, &BASECOIN, "addition");
db.open_tree(ACTIVATION_VOTES_TREE)
.map_err(|err| format!("Could not open activation votes during undo: {err}"))?
.remove(vote_key)
.map_err(|err| format!("Could not remove activation vote during undo: {err}"))?;
let finished = db
.open_tree(FINISHED_ACTIVATION_VOTES_TREE)
.map_err(|err| format!("Could not open finished activation votes during undo: {err}"))?;
if let Some(value) = finished
.get(&candidate_key)
.map_err(|err| format!("Could not read activation finalization during undo: {err}"))?
{
if value.len() == 132 {
let finalized_height = u32::from_le_bytes(value[24..28].try_into().map_err(|_| {
"Finished activation vote contains an invalid block height.".to_string()
})?);
if finalized_height == block_height {
finished.remove(&candidate_key).map_err(|err| {
format!("Could not remove activation finalization during undo: {err}")
})?;
db.open_tree(CONSENSUS_ACTIVATIONS_TREE)
.map_err(|err| {
format!("Could not open consensus activations during undo: {err}")
})?
.remove(&proposal_key)
.map_err(|err| {
format!("Could not remove scheduled activation during undo: {err}")
})?;
}
}
}
db.open_tree("txid")
.map_err(|err| format!("Could not open txid tree during activation vote undo: {err}"))?
.remove(&txhash_bytes)
.map_err(|err| format!("Could not remove activation vote txid during undo: {err}"))?;
let _ = remove_wallet_transaction_index(db, &[&vote.address, mining_receiver], &txhash_bytes);
Ok(())
}

View File

@ -1,6 +1,7 @@
use crate::blocks::burn::BurnTransaction; use crate::blocks::burn::BurnTransaction;
use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::common::network_paths_and_settings::block_extension_and_paths;
use crate::common::nft_assets::nft_asset_name; use crate::common::nft_assets::nft_asset_name;
use crate::common::nft_assets::NFT_UNIT;
use crate::decode; use crate::decode;
use crate::records::balance_sheet::operations::balance_sheet_operation_with_db; use crate::records::balance_sheet::operations::balance_sheet_operation_with_db;
use crate::records::record_chain::nft_provenance::remove_nft_history_entry; use crate::records::record_chain::nft_provenance::remove_nft_history_entry;
@ -67,15 +68,34 @@ pub async fn undo_burn_transaction(transaction: BurnTransaction, mining_receiver
// Restore NFT rows directly, or add the burned amount back into the // Restore NFT rows directly, or add the burned amount back into the
// fungible token supply if this burn targeted a token asset. // fungible token supply if this burn targeted a token asset.
let nft_tree = db.open_tree("nfts").unwrap(); let nft_tree = db.open_tree("nfts").unwrap();
let nft_origin_tree = db.open_tree("nft_origins").unwrap(); let nft_ownership_tree = db.open_tree("nft_ownership").unwrap();
if nft_origin_tree if nft_ownership_tree
.contains_key(burned_asset.as_bytes()) .contains_key(burned_asset.as_bytes())
.unwrap_or(false) .unwrap_or(false)
{ {
// NFT burns remove the live NFT row; rollback restores the row and // Restore the exact burned ownership amount. The immutable ownership
// removes the burn from NFT history. // tree restores the live registry correctly after a final-unit burn.
let _ = remove_nft_history_entry(db, &burned_asset, &hash_binary); let _ = remove_nft_history_entry(db, &burned_asset, &hash_binary);
let _ = nft_tree.insert(burned_asset.as_bytes(), b"1"); let ownership_type = nft_ownership_tree
.get(burned_asset.as_bytes())
.unwrap()
.expect("NFT/RWA ownership record disappeared during burn rollback");
let nft_supply_tree = db.open_tree("nft_supply").unwrap();
let current_supply = nft_supply_tree
.get(burned_asset.as_bytes())
.unwrap()
.map(|bytes| {
let mut supply_bytes = [0u8; 8];
supply_bytes.copy_from_slice(bytes.as_ref());
u64::from_le_bytes(supply_bytes)
})
.unwrap_or(0);
let restored_supply = current_supply
.checked_add(transaction.unsigned_burn.value)
.filter(|supply| *supply <= NFT_UNIT)
.expect("NFT/RWA supply overflow during burn rollback");
let _ = nft_supply_tree.insert(burned_asset.as_bytes(), &restored_supply.to_le_bytes());
let _ = nft_tree.insert(burned_asset.as_bytes(), ownership_type);
} else { } else {
// Token burns reduce supply; rollback adds the burned amount back. // Token burns reduce supply; rollback adds the burned amount back.
let _ = remove_token_history_entry(db, &transaction.unsigned_burn.coin, &hash_binary); let _ = remove_token_history_entry(db, &transaction.unsigned_burn.coin, &hash_binary);

View File

@ -1,14 +1,12 @@
use crate::blocks::nft::CreateNftTransaction; use crate::blocks::nft::CreateNftTransaction;
use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::common::network_paths_and_settings::block_extension_and_paths;
use crate::common::nft_assets::nft_asset_name; use crate::common::nft_assets::{nft_asset_name, NFT_UNIT};
use crate::decode; use crate::decode;
use crate::records::balance_sheet::operations::balance_sheet_operation_with_db; use crate::records::balance_sheet::operations::balance_sheet_operation_with_db;
use crate::records::record_chain::nft_provenance::{remove_nft_history_entry, remove_nft_origin}; use crate::records::record_chain::nft_provenance::{remove_nft_history_entry, remove_nft_origin};
use crate::records::record_chain::wallet_tx_index::remove_wallet_transaction_index; use crate::records::record_chain::wallet_tx_index::remove_wallet_transaction_index;
use crate::sled::Db; use crate::sled::Db;
const NFT_UNIT: u64 = 100_000_000;
pub async fn undo_create_nft_transaction( pub async fn undo_create_nft_transaction(
transaction: CreateNftTransaction, transaction: CreateNftTransaction,
mining_receiver: &str, mining_receiver: &str,
@ -69,6 +67,12 @@ pub async fn undo_create_nft_transaction(
let _ = remove_nft_history_entry(db, &nft_name, &hash_binary); let _ = remove_nft_history_entry(db, &nft_name, &hash_binary);
let _ = remove_nft_origin(db, &nft_name); let _ = remove_nft_origin(db, &nft_name);
tree.remove(nft_name.as_bytes()).unwrap(); tree.remove(nft_name.as_bytes()).unwrap();
let _ = db
.open_tree("nft_ownership")
.and_then(|tree| tree.remove(nft_name.as_bytes()));
let _ = db
.open_tree("nft_supply")
.and_then(|tree| tree.remove(nft_name.as_bytes()));
} }
} else { } else {
// Single NFT creation only writes the base NFT name. // Single NFT creation only writes the base NFT name.
@ -78,5 +82,11 @@ pub async fn undo_create_nft_transaction(
let _ = remove_nft_history_entry(db, nft_name, &hash_binary); let _ = remove_nft_history_entry(db, nft_name, &hash_binary);
let _ = remove_nft_origin(db, nft_name); let _ = remove_nft_origin(db, nft_name);
tree.remove(nft_name.as_bytes()).unwrap(); tree.remove(nft_name.as_bytes()).unwrap();
let _ = db
.open_tree("nft_ownership")
.and_then(|tree| tree.remove(nft_name.as_bytes()));
let _ = db
.open_tree("nft_supply")
.and_then(|tree| tree.remove(nft_name.as_bytes()));
} }
} }

View File

@ -0,0 +1,46 @@
use crate::blocks::proposal_key::ProposalKey;
use crate::common::governance::PROPOSAL_KEYS_TREE;
use crate::decode;
use crate::records::balance_sheet::operations::balance_sheet_operation_with_db;
use crate::records::memory::mempool::BASECOIN;
use crate::records::record_chain::wallet_tx_index::remove_wallet_transaction_index;
use crate::sled::Db;
pub async fn undo_proposal_key_transaction(
transaction: ProposalKey,
mining_receiver: &str,
db: &Db,
) -> Result<(), String> {
let proposal = &transaction.unsigned_proposalkey;
let txhash = proposal.hash().await;
let txhash_bytes = decode(&txhash)
.map_err(|err| format!("Could not decode proposal key txhash during undo: {err}"))?;
let _ = balance_sheet_operation_with_db(
db,
mining_receiver,
proposal.txfee,
&BASECOIN,
"subtraction",
);
let _ = balance_sheet_operation_with_db(
db,
&proposal.address,
proposal.txfee,
&BASECOIN,
"addition",
);
db.open_tree(PROPOSAL_KEYS_TREE)
.map_err(|err| format!("Could not open proposal keys tree during undo: {err}"))?
.remove(&txhash_bytes)
.map_err(|err| format!("Could not remove proposal key during undo: {err}"))?;
db.open_tree("txid")
.map_err(|err| format!("Could not open txid tree during proposal key undo: {err}"))?
.remove(&txhash_bytes)
.map_err(|err| format!("Could not remove proposal key txid during undo: {err}"))?;
let _ =
remove_wallet_transaction_index(db, &[&proposal.address, mining_receiver], &txhash_bytes);
Ok(())
}

View File

@ -0,0 +1,60 @@
use crate::blocks::proposal_vote::ProposalVote;
use crate::common::governance::{
proposal_vote_record_key, FINISHED_PROPOSAL_VOTES_TREE, PROPOSAL_VOTES_TREE,
};
use crate::decode;
use crate::records::balance_sheet::operations::balance_sheet_operation_with_db;
use crate::records::memory::mempool::BASECOIN;
use crate::records::record_chain::wallet_tx_index::remove_wallet_transaction_index;
use crate::sled::Db;
pub async fn undo_proposal_vote_transaction(
transaction: ProposalVote,
mining_receiver: &str,
block_height: u32,
db: &Db,
) -> Result<(), String> {
let vote = &transaction.unsigned_proposalvote;
let txhash = vote.hash().await;
let txhash_bytes = decode(&txhash)
.map_err(|err| format!("Could not decode proposal vote txhash during undo: {err}"))?;
let proposal_key = decode(&vote.proposal_key)
.map_err(|err| format!("Could not decode proposal key during undo: {err}"))?;
let vote_key = proposal_vote_record_key(&vote.proposal_key, &vote.address)?;
let _ =
balance_sheet_operation_with_db(db, mining_receiver, vote.txfee, &BASECOIN, "subtraction");
let _ = balance_sheet_operation_with_db(db, &vote.address, vote.txfee, &BASECOIN, "addition");
db.open_tree(PROPOSAL_VOTES_TREE)
.map_err(|err| format!("Could not open proposal votes during undo: {err}"))?
.remove(vote_key)
.map_err(|err| format!("Could not remove proposal vote during undo: {err}"))?;
let finished = db
.open_tree(FINISHED_PROPOSAL_VOTES_TREE)
.map_err(|err| format!("Could not open finished proposal votes during undo: {err}"))?;
if let Some(value) = finished
.get(&proposal_key)
.map_err(|err| format!("Could not read proposal finalization during undo: {err}"))?
{
if value.len() == 28 {
let finalized_height = u32::from_le_bytes(value[24..28].try_into().map_err(|_| {
"Finished proposal vote contains an invalid block height.".to_string()
})?);
if finalized_height == block_height {
finished.remove(&proposal_key).map_err(|err| {
format!("Could not remove proposal finalization during undo: {err}")
})?;
}
}
}
db.open_tree("txid")
.map_err(|err| format!("Could not open txid tree during proposal vote undo: {err}"))?
.remove(&txhash_bytes)
.map_err(|err| format!("Could not remove proposal vote txid during undo: {err}"))?;
let _ = remove_wallet_transaction_index(db, &[&vote.address, mining_receiver], &txhash_bytes);
Ok(())
}

View File

@ -1,5 +1,38 @@
use super::*; use super::*;
pub async fn proposal_vote_exists(proposal_key: &str, address: &str) -> Result<bool> {
let client_handle = db_client().await?;
let row = client_handle
.query_one(
"SELECT EXISTS(
SELECT 1 FROM proposal_vote
WHERE proposal_key = $1 AND address = $2 AND processed = false
)",
&[&proposal_key, &address],
)
.await?;
Ok(row.get(0))
}
pub async fn activation_vote_exists(
proposal_key: &str,
development_hash: &str,
address: &str,
) -> Result<bool> {
let client_handle = db_client().await?;
let row = client_handle
.query_one(
"SELECT EXISTS(
SELECT 1 FROM activation_vote
WHERE proposal_key = $1 AND development_hash = $2
AND address = $3 AND processed = false
)",
&[&proposal_key, &development_hash, &address],
)
.await?;
Ok(row.get(0))
}
pub async fn exact_transaction_exists(signature: &str, original: &[u8]) -> Result<bool> { pub async fn exact_transaction_exists(signature: &str, original: &[u8]) -> Result<bool> {
let client_handle = db_client().await?; let client_handle = db_client().await?;
let client = client_handle.as_ref(); let client = client_handle.as_ref();
@ -26,6 +59,9 @@ pub async fn exact_transaction_exists(signature: &str, original: &[u8]) -> Resul
OR EXISTS (SELECT 1 FROM loan_payment k WHERE k.signature = $1 AND k.original = $2 AND k.processed = false) OR EXISTS (SELECT 1 FROM loan_payment k WHERE k.signature = $1 AND k.original = $2 AND k.processed = false)
OR EXISTS (SELECT 1 FROM collateral_claim l WHERE l.signature = $1 AND l.original = $2 AND l.processed = false) OR EXISTS (SELECT 1 FROM collateral_claim l WHERE l.signature = $1 AND l.original = $2 AND l.processed = false)
OR EXISTS (SELECT 1 FROM storage_key sk WHERE sk.signature = $1 AND sk.original = $2 AND sk.processed = false) OR EXISTS (SELECT 1 FROM storage_key sk WHERE sk.signature = $1 AND sk.original = $2 AND sk.processed = false)
OR EXISTS (SELECT 1 FROM proposal_key pk WHERE pk.signature = $1 AND pk.original = $2 AND pk.processed = false)
OR EXISTS (SELECT 1 FROM proposal_vote pv WHERE pv.signature = $1 AND pv.original = $2 AND pv.processed = false)
OR EXISTS (SELECT 1 FROM activation_vote av WHERE av.signature = $1 AND av.original = $2 AND av.processed = false)
OR EXISTS (SELECT 1 FROM storage_value sv WHERE sv.signature = $1 AND sv.original = $2 AND sv.processed = false) OR EXISTS (SELECT 1 FROM storage_value sv WHERE sv.signature = $1 AND sv.original = $2 AND sv.processed = false)
THEN 1 THEN 1
ELSE 0 ELSE 0
@ -80,6 +116,12 @@ pub async fn transaction_by_signature(signature: &str) -> RpcResponse {
UNION ALL UNION ALL
SELECT original FROM storage_key WHERE signature = $1 AND processed = false LIMIT 1 SELECT original FROM storage_key WHERE signature = $1 AND processed = false LIMIT 1
UNION ALL UNION ALL
SELECT original FROM proposal_key WHERE signature = $1 AND processed = false LIMIT 1
UNION ALL
SELECT original FROM proposal_vote WHERE signature = $1 AND processed = false LIMIT 1
UNION ALL
SELECT original FROM activation_vote WHERE signature = $1 AND processed = false LIMIT 1
UNION ALL
SELECT original FROM storage_value WHERE signature = $1 AND processed = false LIMIT 1 SELECT original FROM storage_value WHERE signature = $1 AND processed = false LIMIT 1
) AS subquery LIMIT 1 ) AS subquery LIMIT 1
"#, "#,
@ -139,6 +181,12 @@ pub async fn transactions_by_address(db: &Db, address: &str) -> RpcResponse {
UNION ALL UNION ALL
SELECT original FROM storage_key WHERE address = ANY($1) AND processed = false SELECT original FROM storage_key WHERE address = ANY($1) AND processed = false
UNION ALL UNION ALL
SELECT original FROM proposal_key WHERE address = ANY($1) AND processed = false
UNION ALL
SELECT original FROM proposal_vote WHERE address = ANY($1) AND processed = false
UNION ALL
SELECT original FROM activation_vote WHERE address = ANY($1) AND processed = false
UNION ALL
SELECT original FROM storage_value WHERE address = ANY($1) AND processed = false SELECT original FROM storage_value WHERE address = ANY($1) AND processed = false
) AS subquery; ) AS subquery;
"#, "#,
@ -213,6 +261,15 @@ pub async fn latest_pending_txids_by_address(db: &Db, address: &str, limit: usiz
SELECT hash, time, id AS source_id FROM storage_key SELECT hash, time, id AS source_id FROM storage_key
WHERE address = ANY($1) AND processed = false WHERE address = ANY($1) AND processed = false
UNION ALL UNION ALL
SELECT hash, time, id AS source_id FROM proposal_key
WHERE address = ANY($1) AND processed = false
UNION ALL
SELECT hash, time, id AS source_id FROM proposal_vote
WHERE address = ANY($1) AND processed = false
UNION ALL
SELECT hash, time, id AS source_id FROM activation_vote
WHERE address = ANY($1) AND processed = false
UNION ALL
SELECT hash, time, id AS source_id FROM storage_value SELECT hash, time, id AS source_id FROM storage_value
WHERE address = ANY($1) AND processed = false WHERE address = ANY($1) AND processed = false
) AS pending ) AS pending
@ -276,6 +333,12 @@ pub async fn pending_transaction_by_txid(txid: &[u8]) -> Option<Vec<u8>> {
UNION ALL UNION ALL
SELECT original FROM storage_key WHERE hash = $1 AND processed = false SELECT original FROM storage_key WHERE hash = $1 AND processed = false
UNION ALL UNION ALL
SELECT original FROM proposal_key WHERE hash = $1 AND processed = false
UNION ALL
SELECT original FROM proposal_vote WHERE hash = $1 AND processed = false
UNION ALL
SELECT original FROM activation_vote WHERE hash = $1 AND processed = false
UNION ALL
SELECT original FROM storage_value WHERE hash = $1 AND processed = false SELECT original FROM storage_value WHERE hash = $1 AND processed = false
) AS subquery LIMIT 1 ) AS subquery LIMIT 1
"#, "#,
@ -325,6 +388,12 @@ pub async fn largest_fee() -> RpcResponse {
UNION ALL UNION ALL
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM storage_key SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM storage_key
UNION ALL UNION ALL
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM proposal_key
UNION ALL
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM proposal_vote
UNION ALL
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM activation_vote
UNION ALL
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM storage_value SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM storage_value
) AS combined_max_txids; ) AS combined_max_txids;
"#, "#,
@ -507,6 +576,9 @@ pub async fn get_basecoin_balance(
+ COALESCE((SELECT SUM(v.fee) FROM vanity_address v WHERE v.address = ANY($1) AND v.processed = false), 0) + COALESCE((SELECT SUM(v.fee) FROM vanity_address v WHERE v.address = ANY($1) AND v.processed = false), 0)
+ COALESCE((SELECT SUM(cc.fee) FROM collateral_claim cc WHERE cc.address = ANY($1) AND cc.processed = false), 0) + COALESCE((SELECT SUM(cc.fee) FROM collateral_claim cc WHERE cc.address = ANY($1) AND cc.processed = false), 0)
+ COALESCE((SELECT SUM(sk.fee) FROM storage_key sk WHERE sk.address = ANY($1) AND sk.processed = false), 0) + COALESCE((SELECT SUM(sk.fee) FROM storage_key sk WHERE sk.address = ANY($1) AND sk.processed = false), 0)
+ COALESCE((SELECT SUM(pk.fee) FROM proposal_key pk WHERE pk.address = ANY($1) AND pk.processed = false), 0)
+ COALESCE((SELECT SUM(pv.fee) FROM proposal_vote pv WHERE pv.address = ANY($1) AND pv.processed = false), 0)
+ COALESCE((SELECT SUM(av.fee) FROM activation_vote av WHERE av.address = ANY($1) AND av.processed = false), 0)
+ COALESCE((SELECT SUM(sv.fee) FROM storage_value sv WHERE sv.address = ANY($1) AND sv.processed = false), 0) + COALESCE((SELECT SUM(sv.fee) FROM storage_value sv WHERE sv.address = ANY($1) AND sv.processed = false), 0)
+ COALESCE((SELECT SUM(lc.fee) FROM loan_contract lc WHERE lc.lender = ANY($1) AND lc.processed = false), 0) + COALESCE((SELECT SUM(lc.fee) FROM loan_contract lc WHERE lc.lender = ANY($1) AND lc.processed = false), 0)
+ COALESCE((SELECT SUM(lp.fee) FROM loan_payment lp WHERE lp.address = ANY($1) AND lp.processed = false), 0) + COALESCE((SELECT SUM(lp.fee) FROM loan_payment lp WHERE lp.address = ANY($1) AND lp.processed = false), 0)
@ -605,6 +677,12 @@ pub async fn total_transactions() -> RpcResponse {
UNION ALL UNION ALL
SELECT COUNT(*) AS row_count FROM storage_key SELECT COUNT(*) AS row_count FROM storage_key
UNION ALL UNION ALL
SELECT COUNT(*) AS row_count FROM proposal_key
UNION ALL
SELECT COUNT(*) AS row_count FROM proposal_vote
UNION ALL
SELECT COUNT(*) AS row_count FROM activation_vote
UNION ALL
SELECT COUNT(*) AS row_count FROM storage_value SELECT COUNT(*) AS row_count FROM storage_value
) AS combined; ) AS combined;
"#, "#,

View File

@ -158,6 +158,27 @@ enum SelectedMempoolTransaction {
address: String, address: String,
hash: String, hash: String,
}, },
ProposalKey {
id: i64,
fee: i64,
address: String,
hash: String,
original: Vec<u8>,
},
ProposalVote {
id: i64,
fee: i64,
address: String,
hash: String,
original: Vec<u8>,
},
ActivationVote {
id: i64,
fee: i64,
address: String,
hash: String,
original: Vec<u8>,
},
StorageValue { StorageValue {
id: i64, id: i64,
fee: i64, fee: i64,
@ -190,6 +211,9 @@ impl SelectedMempoolTransaction {
SelectedMempoolTransaction::Borrower { .. } => "loan_payment", SelectedMempoolTransaction::Borrower { .. } => "loan_payment",
SelectedMempoolTransaction::Collateral { .. } => "collateral_claim", SelectedMempoolTransaction::Collateral { .. } => "collateral_claim",
SelectedMempoolTransaction::StorageKey { .. } => "storage_key", SelectedMempoolTransaction::StorageKey { .. } => "storage_key",
SelectedMempoolTransaction::ProposalKey { .. } => "proposal_key",
SelectedMempoolTransaction::ProposalVote { .. } => "proposal_vote",
SelectedMempoolTransaction::ActivationVote { .. } => "activation_vote",
SelectedMempoolTransaction::StorageValue { .. } => "storage_value", SelectedMempoolTransaction::StorageValue { .. } => "storage_value",
} }
} }
@ -208,6 +232,9 @@ impl SelectedMempoolTransaction {
| SelectedMempoolTransaction::Borrower { id, .. } | SelectedMempoolTransaction::Borrower { id, .. }
| SelectedMempoolTransaction::Collateral { id, .. } | SelectedMempoolTransaction::Collateral { id, .. }
| SelectedMempoolTransaction::StorageKey { id, .. } | SelectedMempoolTransaction::StorageKey { id, .. }
| SelectedMempoolTransaction::ProposalKey { id, .. }
| SelectedMempoolTransaction::ProposalVote { id, .. }
| SelectedMempoolTransaction::ActivationVote { id, .. }
| SelectedMempoolTransaction::StorageValue { id, .. } => *id, | SelectedMempoolTransaction::StorageValue { id, .. } => *id,
} }
} }
@ -225,10 +252,10 @@ mod schema;
mod selection; mod selection;
pub use lookups::{ pub use lookups::{
exact_transaction_exists, get_basecoin_balance, get_coin_balance, activation_vote_exists, exact_transaction_exists, get_basecoin_balance, get_coin_balance,
get_pending_payments_for_contract, largest_fee, latest_pending_txids_by_address, get_pending_payments_for_contract, largest_fee, latest_pending_txids_by_address,
pending_transaction_by_txid, total_transactions, transaction_by_signature, pending_transaction_by_txid, proposal_vote_exists, total_transactions,
transactions_by_address, transaction_by_signature, transactions_by_address,
}; };
pub use processing::{ pub use processing::{
delete_by_signatures, delete_unprocessed_by_address, mark_processed_by_signatures, delete_by_signatures, delete_unprocessed_by_address, mark_processed_by_signatures,
@ -402,6 +429,9 @@ async fn delete_processed_before_or_at(block_number: u32, limit: i64) -> Result<
delete_processed_rows_limited("loan_payment", bn, limit).await?; delete_processed_rows_limited("loan_payment", bn, limit).await?;
delete_processed_rows_limited("collateral_claim", bn, limit).await?; delete_processed_rows_limited("collateral_claim", bn, limit).await?;
delete_processed_rows_limited("storage_key", bn, limit).await?; delete_processed_rows_limited("storage_key", bn, limit).await?;
delete_processed_rows_limited("proposal_key", bn, limit).await?;
delete_processed_rows_limited("proposal_vote", bn, limit).await?;
delete_processed_rows_limited("activation_vote", bn, limit).await?;
delete_processed_rows_limited("storage_value", bn, limit).await?; delete_processed_rows_limited("storage_value", bn, limit).await?;
Ok(()) Ok(())

View File

@ -47,6 +47,14 @@ pub async fn mark_selected_transactions_processed(
) )
.await?; .await?;
mark_rows_by_ids("storage_key", &ids_for_table(batch, "storage_key"), bn).await?; mark_rows_by_ids("storage_key", &ids_for_table(batch, "storage_key"), bn).await?;
mark_rows_by_ids("proposal_key", &ids_for_table(batch, "proposal_key"), bn).await?;
mark_rows_by_ids("proposal_vote", &ids_for_table(batch, "proposal_vote"), bn).await?;
mark_rows_by_ids(
"activation_vote",
&ids_for_table(batch, "activation_vote"),
bn,
)
.await?;
mark_rows_by_ids("storage_value", &ids_for_table(batch, "storage_value"), bn).await?; mark_rows_by_ids("storage_value", &ids_for_table(batch, "storage_value"), bn).await?;
Ok(()) Ok(())
@ -71,6 +79,9 @@ pub async fn restore_selected_transactions_processed(batch: &SelectedMempoolBatc
) )
.await?; .await?;
unmark_rows_by_ids("storage_key", &ids_for_table(batch, "storage_key")).await?; unmark_rows_by_ids("storage_key", &ids_for_table(batch, "storage_key")).await?;
unmark_rows_by_ids("proposal_key", &ids_for_table(batch, "proposal_key")).await?;
unmark_rows_by_ids("proposal_vote", &ids_for_table(batch, "proposal_vote")).await?;
unmark_rows_by_ids("activation_vote", &ids_for_table(batch, "activation_vote")).await?;
unmark_rows_by_ids("storage_value", &ids_for_table(batch, "storage_value")).await?; unmark_rows_by_ids("storage_value", &ids_for_table(batch, "storage_value")).await?;
Ok(()) Ok(())
@ -101,6 +112,9 @@ pub async fn restore_processed_by_signatures(signatures: &[String]) -> Result<bo
restored += unmark_by_signatures("loan_payment", "signature", signatures).await?; restored += unmark_by_signatures("loan_payment", "signature", signatures).await?;
restored += unmark_by_signatures("collateral_claim", "signature", signatures).await?; restored += unmark_by_signatures("collateral_claim", "signature", signatures).await?;
restored += unmark_by_signatures("storage_key", "signature", signatures).await?; restored += unmark_by_signatures("storage_key", "signature", signatures).await?;
restored += unmark_by_signatures("proposal_key", "signature", signatures).await?;
restored += unmark_by_signatures("proposal_vote", "signature", signatures).await?;
restored += unmark_by_signatures("activation_vote", "signature", signatures).await?;
restored += unmark_by_signatures("storage_value", "signature", signatures).await?; restored += unmark_by_signatures("storage_value", "signature", signatures).await?;
Ok(restored > 0) Ok(restored > 0)
@ -233,6 +247,24 @@ pub async fn mark_processed_by_signatures(signatures: &[String], block_number: u
signatures, signatures,
) )
.await?; .await?;
pg_execute_block_signatures(
"UPDATE proposal_key SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
bn,
signatures,
)
.await?;
pg_execute_block_signatures(
"UPDATE proposal_vote SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
bn,
signatures,
)
.await?;
pg_execute_block_signatures(
"UPDATE activation_vote SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
bn,
signatures,
)
.await?;
pg_execute_block_signatures( pg_execute_block_signatures(
"UPDATE storage_value SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", "UPDATE storage_value SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
bn, bn,
@ -296,6 +328,21 @@ pub async fn delete_by_signatures(signatures: &[String]) -> Result<()> {
signatures, signatures,
) )
.await?; .await?;
pg_execute_signatures(
"DELETE FROM proposal_key WHERE signature = ANY($1)",
signatures,
)
.await?;
pg_execute_signatures(
"DELETE FROM proposal_vote WHERE signature = ANY($1)",
signatures,
)
.await?;
pg_execute_signatures(
"DELETE FROM activation_vote WHERE signature = ANY($1)",
signatures,
)
.await?;
pg_execute_signatures( pg_execute_signatures(
"DELETE FROM storage_value WHERE signature = ANY($1)", "DELETE FROM storage_value WHERE signature = ANY($1)",
signatures, signatures,
@ -374,6 +421,21 @@ pub async fn delete_unprocessed_by_address(db: &Db, address: &str) -> Result<u64
&params, &params,
) )
.await?; .await?;
deleted += pg_execute(
"DELETE FROM proposal_key WHERE processed = false AND address = ANY($1)",
&params,
)
.await?;
deleted += pg_execute(
"DELETE FROM proposal_vote WHERE processed = false AND address = ANY($1)",
&params,
)
.await?;
deleted += pg_execute(
"DELETE FROM activation_vote WHERE processed = false AND address = ANY($1)",
&params,
)
.await?;
deleted += pg_execute( deleted += pg_execute(
"DELETE FROM storage_value WHERE processed = false AND address = ANY($1)", "DELETE FROM storage_value WHERE processed = false AND address = ANY($1)",
&params, &params,

View File

@ -291,6 +291,49 @@ pub async fn setup_mempool() -> Result<()> {
original BYTEA NOT NULL original BYTEA NOT NULL
); );
CREATE TABLE IF NOT EXISTS proposal_key (
id BIGSERIAL PRIMARY KEY,
time INTEGER NOT NULL,
fee BIGINT NOT NULL,
proposal_hash VARCHAR(64) NOT NULL,
address TEXT NOT NULL,
hash VARCHAR(64) NOT NULL,
signature TEXT NOT NULL,
processed bool DEFAULT false,
processed_block_number INTEGER DEFAULT NULL,
original BYTEA NOT NULL
);
CREATE TABLE IF NOT EXISTS proposal_vote (
id BIGSERIAL PRIMARY KEY,
time INTEGER NOT NULL,
fee BIGINT NOT NULL,
proposal_key VARCHAR(64) NOT NULL,
address TEXT NOT NULL,
vote SMALLINT NOT NULL,
hash VARCHAR(64) NOT NULL,
signature TEXT NOT NULL,
processed bool DEFAULT false,
processed_block_number INTEGER DEFAULT NULL,
original BYTEA NOT NULL
);
CREATE TABLE IF NOT EXISTS activation_vote (
id BIGSERIAL PRIMARY KEY,
time INTEGER NOT NULL,
fee BIGINT NOT NULL,
proposal_key VARCHAR(64) NOT NULL,
development_hash VARCHAR(64) NOT NULL,
development_location VARCHAR(100) NOT NULL,
address TEXT NOT NULL,
vote SMALLINT NOT NULL,
hash VARCHAR(64) NOT NULL,
signature TEXT NOT NULL,
processed bool DEFAULT false,
processed_block_number INTEGER DEFAULT NULL,
original BYTEA NOT NULL
);
CREATE TABLE IF NOT EXISTS storage_value ( CREATE TABLE IF NOT EXISTS storage_value (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
txtype SMALLINT NOT NULL, txtype SMALLINT NOT NULL,
@ -341,6 +384,12 @@ pub async fn setup_mempool() -> Result<()> {
ALTER TABLE collateral_claim ALTER COLUMN signature TYPE TEXT; ALTER TABLE collateral_claim ALTER COLUMN signature TYPE TEXT;
ALTER TABLE storage_key ALTER COLUMN address TYPE TEXT; ALTER TABLE storage_key ALTER COLUMN address TYPE TEXT;
ALTER TABLE storage_key ALTER COLUMN signature TYPE TEXT; ALTER TABLE storage_key ALTER COLUMN signature TYPE TEXT;
ALTER TABLE proposal_key ALTER COLUMN address TYPE TEXT;
ALTER TABLE proposal_key ALTER COLUMN signature TYPE TEXT;
ALTER TABLE proposal_vote ALTER COLUMN address TYPE TEXT;
ALTER TABLE proposal_vote ALTER COLUMN signature TYPE TEXT;
ALTER TABLE activation_vote ALTER COLUMN address TYPE TEXT;
ALTER TABLE activation_vote ALTER COLUMN signature TYPE TEXT;
ALTER TABLE storage_value ALTER COLUMN address TYPE TEXT; ALTER TABLE storage_value ALTER COLUMN address TYPE TEXT;
ALTER TABLE storage_value ALTER COLUMN signature TYPE TEXT; ALTER TABLE storage_value ALTER COLUMN signature TYPE TEXT;
"#; "#;
@ -406,6 +455,18 @@ pub async fn setup_mempool() -> Result<()> {
USING storage_key b USING storage_key b
WHERE a.id > b.id AND a.signature = b.signature; WHERE a.id > b.id AND a.signature = b.signature;
DELETE FROM proposal_key a
USING proposal_key b
WHERE a.id > b.id AND a.signature = b.signature;
DELETE FROM proposal_vote a
USING proposal_vote b
WHERE a.id > b.id AND a.signature = b.signature;
DELETE FROM activation_vote a
USING activation_vote b
WHERE a.id > b.id AND a.signature = b.signature;
DELETE FROM storage_value a DELETE FROM storage_value a
USING storage_value b USING storage_value b
WHERE a.id > b.id AND a.signature = b.signature; WHERE a.id > b.id AND a.signature = b.signature;
@ -468,6 +529,20 @@ pub async fn setup_mempool() -> Result<()> {
CREATE INDEX IF NOT EXISTS storage_key_sig_idx ON storage_key (signature); CREATE INDEX IF NOT EXISTS storage_key_sig_idx ON storage_key (signature);
CREATE UNIQUE INDEX IF NOT EXISTS storage_key_sig_unique_idx ON storage_key (signature); CREATE UNIQUE INDEX IF NOT EXISTS storage_key_sig_unique_idx ON storage_key (signature);
CREATE INDEX IF NOT EXISTS proposal_key_pick_idx ON proposal_key (processed, fee DESC, time ASC, id ASC);
CREATE INDEX IF NOT EXISTS proposal_key_sig_idx ON proposal_key (signature);
CREATE UNIQUE INDEX IF NOT EXISTS proposal_key_sig_unique_idx ON proposal_key (signature);
CREATE INDEX IF NOT EXISTS proposal_vote_pick_idx ON proposal_vote (processed, fee DESC, time ASC, id ASC);
CREATE INDEX IF NOT EXISTS proposal_vote_sig_idx ON proposal_vote (signature);
CREATE UNIQUE INDEX IF NOT EXISTS proposal_vote_sig_unique_idx ON proposal_vote (signature);
CREATE UNIQUE INDEX IF NOT EXISTS proposal_vote_node_unique_idx ON proposal_vote (proposal_key, address);
CREATE INDEX IF NOT EXISTS activation_vote_pick_idx ON activation_vote (processed, fee DESC, time ASC, id ASC);
CREATE INDEX IF NOT EXISTS activation_vote_sig_idx ON activation_vote (signature);
CREATE UNIQUE INDEX IF NOT EXISTS activation_vote_sig_unique_idx ON activation_vote (signature);
CREATE UNIQUE INDEX IF NOT EXISTS activation_vote_node_unique_idx ON activation_vote (proposal_key, development_hash, address);
CREATE INDEX IF NOT EXISTS storage_value_pick_idx ON storage_value (processed, fee DESC, time ASC, id ASC); CREATE INDEX IF NOT EXISTS storage_value_pick_idx ON storage_value (processed, fee DESC, time ASC, id ASC);
CREATE INDEX IF NOT EXISTS storage_value_sig_idx ON storage_value (signature); CREATE INDEX IF NOT EXISTS storage_value_sig_idx ON storage_value (signature);
CREATE UNIQUE INDEX IF NOT EXISTS storage_value_sig_unique_idx ON storage_value (signature); CREATE UNIQUE INDEX IF NOT EXISTS storage_value_sig_unique_idx ON storage_value (signature);
@ -504,6 +579,9 @@ pub async fn clear_mempool() -> Result<()> {
loan_payment, loan_payment,
collateral_claim, collateral_claim,
storage_key, storage_key,
proposal_key,
proposal_vote,
activation_vote,
storage_value storage_value
RESTART IDENTITY; RESTART IDENTITY;
"#, "#,

View File

@ -1,17 +1,24 @@
use super::*; use super::*;
use crate::blocks::activation_vote::ActivationVote;
use crate::blocks::delete_key::{delete_record_keys, delete_rollback_key, DeleteKey}; use crate::blocks::delete_key::{delete_record_keys, delete_rollback_key, DeleteKey};
use crate::blocks::proposal_vote::ProposalVote;
use crate::blocks::storage_values::{ use crate::blocks::storage_values::{
storage_record_key, storage_string_record_key, StorageBool, StorageI128, StorageI16, storage_record_key, storage_string_record_key, StorageBool, StorageI128, StorageI16,
StorageI32, StorageI64, StorageI8, StorageString, StorageU128, StorageU16, StorageU32, StorageI32, StorageI64, StorageI8, StorageString, StorageU128, StorageU16, StorageU32,
StorageU64, StorageU8, StorageValueTransaction, StorageU64, StorageU8, StorageValueTransaction,
}; };
use crate::common::governance::{
activation_candidate_key, activation_vote_record_key, proposal_vote_record_key,
};
use crate::common::types::{ use crate::common::types::{
Transaction, DELETE_KEY_TYPE, STORAGE_BOOL_TYPE, STORAGE_I128_TYPE, STORAGE_I16_TYPE, Transaction, DELETE_KEY_TYPE, STORAGE_BOOL_TYPE, STORAGE_I128_TYPE, STORAGE_I16_TYPE,
STORAGE_I32_TYPE, STORAGE_I64_TYPE, STORAGE_I8_TYPE, STORAGE_STRING_TYPE, STORAGE_U128_TYPE, STORAGE_I32_TYPE, STORAGE_I64_TYPE, STORAGE_I8_TYPE, STORAGE_STRING_TYPE, STORAGE_U128_TYPE,
STORAGE_U16_TYPE, STORAGE_U32_TYPE, STORAGE_U64_TYPE, STORAGE_U8_TYPE, STORAGE_U16_TYPE, STORAGE_U32_TYPE, STORAGE_U64_TYPE, STORAGE_U8_TYPE,
}; };
use crate::records::memory::network_mapping::NodeInfo;
use crate::records::record_chain::pending_effects::{BalanceOperand, PendingEffects}; use crate::records::record_chain::pending_effects::{BalanceOperand, PendingEffects};
use crate::records::record_chain::wallet_tx_index::index_wallet_transaction; use crate::records::record_chain::wallet_tx_index::index_wallet_transaction;
use crate::wallets::structures::Wallet;
struct SelectedStorageValueParts { struct SelectedStorageValueParts {
storagekey: String, storagekey: String,
@ -342,6 +349,63 @@ pub async fn select_transactions_for_block(limit: i64) -> Result<SelectedMempool
UNION ALL UNION ALL
SELECT
'proposal_key'::TEXT AS kind, id, fee AS priority_fee, time,
NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series,
NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker,
NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count,
NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2,
NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2,
NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1, NULL::BIGINT AS value2,
NULL::TEXT AS sender1, NULL::BIGINT AS tip1, NULL::BIGINT AS tip2,
NULL::TEXT AS sender2, NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount,
NULL::TEXT AS lender, NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount,
NULL::TEXT AS borrower, NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash,
address, hash, signature AS signature1, NULL::TEXT AS signature2,
NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original
FROM proposal_key
WHERE processed = false
UNION ALL
SELECT
'proposal_vote'::TEXT AS kind, id, fee AS priority_fee, time,
NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series,
NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker,
NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count,
NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2,
NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2,
NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1, NULL::BIGINT AS value2,
NULL::TEXT AS sender1, NULL::BIGINT AS tip1, NULL::BIGINT AS tip2,
NULL::TEXT AS sender2, NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount,
NULL::TEXT AS lender, NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount,
NULL::TEXT AS borrower, NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash,
address, hash, signature AS signature1, NULL::TEXT AS signature2,
NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original
FROM proposal_vote
WHERE processed = false
UNION ALL
SELECT
'activation_vote'::TEXT AS kind, id, fee AS priority_fee, time,
NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series,
NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker,
NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count,
NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2,
NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2,
NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1, NULL::BIGINT AS value2,
NULL::TEXT AS sender1, NULL::BIGINT AS tip1, NULL::BIGINT AS tip2,
NULL::TEXT AS sender2, NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount,
NULL::TEXT AS lender, NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount,
NULL::TEXT AS borrower, NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash,
address, hash, signature AS signature1, NULL::TEXT AS signature2,
NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original
FROM activation_vote
WHERE processed = false
UNION ALL
SELECT SELECT
'storage_value'::TEXT AS kind, id, fee AS priority_fee, time, 'storage_value'::TEXT AS kind, id, fee AS priority_fee, time,
NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series, NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series,
@ -484,6 +548,27 @@ pub async fn select_transactions_for_block(limit: i64) -> Result<SelectedMempool
address: required_string(&row, "address")?, address: required_string(&row, "address")?,
hash: row.get("hash"), hash: row.get("hash"),
}, },
"proposal_key" => SelectedMempoolTransaction::ProposalKey {
id: row.get("id"),
fee: row.get("priority_fee"),
address: required_string(&row, "address")?,
hash: row.get("hash"),
original: row.get("original"),
},
"proposal_vote" => SelectedMempoolTransaction::ProposalVote {
id: row.get("id"),
fee: row.get("priority_fee"),
address: required_string(&row, "address")?,
hash: row.get("hash"),
original: row.get("original"),
},
"activation_vote" => SelectedMempoolTransaction::ActivationVote {
id: row.get("id"),
fee: row.get("priority_fee"),
address: required_string(&row, "address")?,
hash: row.get("hash"),
original: row.get("original"),
},
"storage_value" => SelectedMempoolTransaction::StorageValue { "storage_value" => SelectedMempoolTransaction::StorageValue {
id: row.get("id"), id: row.get("id"),
fee: row.get("priority_fee"), fee: row.get("priority_fee"),
@ -507,6 +592,7 @@ pub async fn apply_selected_transaction_math(
db: &Db, db: &Db,
miner: String, miner: String,
block_number: u32, block_number: u32,
block_timestamp: u32,
start_index: usize, start_index: usize,
pending_effects: &mut PendingEffects, pending_effects: &mut PendingEffects,
) -> Result<()> { ) -> Result<()> {
@ -516,7 +602,7 @@ pub async fn apply_selected_transaction_math(
let miner_address = address_key_bytes(db, &miner); let miner_address = address_key_bytes(db, &miner);
let base_coin = BASECOIN.as_bytes().to_vec(); let base_coin = BASECOIN.as_bytes().to_vec();
let mut tx_index = start_index; let mut tx_index = start_index;
for tx in &batch.transactions { for (batch_index, tx) in batch.transactions.iter().enumerate() {
// Transaction index is stored with the block number for later txid // Transaction index is stored with the block number for later txid
// lookups inside the saved block payload. // lookups inside the saved block payload.
tx_index = tx_index.wrapping_add(1); tx_index = tx_index.wrapping_add(1);
@ -713,6 +799,23 @@ pub async fn apply_selected_transaction_math(
hash, hash,
.. ..
} => { } => {
// Ownership type is part of the signed original bytes. Read
// it here so candidate-block effects match committed effects.
let original = batch
.originals
.get(batch_index)
.ok_or_else(|| anyhow!("Missing original NFT transaction bytes"))?;
if original.first().copied() != Some(4) {
return Err(anyhow!("Invalid original NFT transaction type"));
}
let nft_transaction =
crate::blocks::nft::CreateNftTransaction::from_bytes(4, &original[1..])
.await
.map_err(|err| {
anyhow!("Failed to parse original NFT transaction: {err}")
})?;
let ownership_type = nft_transaction.unsigned_create_nft.ownership_type;
// Series-one NFT creation expands into numbered assets; later // Series-one NFT creation expands into numbered assets; later
// series use the given NFT name as the asset key. // series use the given NFT name as the asset key.
if *series == 1 { if *series == 1 {
@ -729,7 +832,17 @@ pub async fn apply_selected_transaction_math(
pending_effects.set_tree( pending_effects.set_tree(
"nfts", "nfts",
nft_save_name.as_bytes().to_vec(), nft_save_name.as_bytes().to_vec(),
b"1".to_vec(), vec![ownership_type],
);
pending_effects.set_tree(
"nft_ownership",
nft_save_name.as_bytes().to_vec(),
vec![ownership_type],
);
pending_effects.set_tree(
"nft_supply",
nft_save_name.as_bytes().to_vec(),
NFT_UNIT.to_le_bytes().to_vec(),
); );
pending_effects.set_tree( pending_effects.set_tree(
"nft_origins", "nft_origins",
@ -745,7 +858,21 @@ pub async fn apply_selected_transaction_math(
} else { } else {
add_balance_change(db, &mut balance_changes, creator, nft_name, NFT_UNIT); add_balance_change(db, &mut balance_changes, creator, nft_name, NFT_UNIT);
pending_effects.set_tree("nfts", nft_name.as_bytes().to_vec(), b"1".to_vec()); pending_effects.set_tree(
"nfts",
nft_name.as_bytes().to_vec(),
vec![ownership_type],
);
pending_effects.set_tree(
"nft_ownership",
nft_name.as_bytes().to_vec(),
vec![ownership_type],
);
pending_effects.set_tree(
"nft_supply",
nft_name.as_bytes().to_vec(),
NFT_UNIT.to_le_bytes().to_vec(),
);
pending_effects.set_tree( pending_effects.set_tree(
"nft_origins", "nft_origins",
nft_name.as_bytes().to_vec(), nft_name.as_bytes().to_vec(),
@ -1133,6 +1260,166 @@ pub async fn apply_selected_transaction_math(
&txhash_bytes, &txhash_bytes,
)?; )?;
} }
SelectedMempoolTransaction::ProposalKey {
fee,
address,
hash,
original,
..
} => {
add_balance_change(db, &mut balance_changes, address, &BASECOIN, -*fee);
add_balance_change_bytes(
&mut balance_changes,
miner_address.clone(),
base_coin.clone(),
*fee,
);
let txhash_bytes = decode(hash)?;
pending_effects.set_tree(
crate::common::governance::PROPOSAL_KEYS_TREE,
txhash_bytes.clone(),
original.clone(),
);
pending_effects.set_tree(
"txid",
txhash_bytes.clone(),
format!("{block_number}:{tx_index}").into_bytes(),
);
index_selected_wallet_transaction(
pending_effects,
&[address],
block_number,
tx_index,
&txhash_bytes,
)?;
}
SelectedMempoolTransaction::ProposalVote {
fee,
address,
hash,
original,
..
} => {
let (&txtype, body) = original
.split_first()
.ok_or_else(|| anyhow!("Empty proposal vote transaction bytes"))?;
let transaction = ProposalVote::from_bytes(txtype, body).await?;
let vote = &transaction.unsigned_proposalvote;
let proposal_key = decode(&vote.proposal_key)?;
let vote_key = proposal_vote_record_key(&vote.proposal_key, &vote.address)
.map_err(|err| anyhow!(err))?;
let eligible_addresses = NodeInfo::governance_countable_nodes_for_proposal(
db,
&vote.proposal_key,
(block_timestamp as u64) * 1_000,
block_number,
)
.await
.map_err(|err| anyhow!(err))?
.into_iter()
.map(|node| {
Wallet::short_address_to_bytes(&node.address)
.ok_or_else(|| anyhow!("Eligible governance node has an invalid address"))
})
.collect::<Result<Vec<_>>>()?;
add_balance_change(db, &mut balance_changes, address, &BASECOIN, -*fee);
add_balance_change_bytes(
&mut balance_changes,
miner_address.clone(),
base_coin.clone(),
*fee,
);
pending_effects.proposal_vote(
proposal_key,
vote_key,
original.clone(),
eligible_addresses,
block_number,
);
let txhash_bytes = decode(hash)?;
pending_effects.set_tree(
"txid",
txhash_bytes.clone(),
format!("{block_number}:{tx_index}").into_bytes(),
);
index_selected_wallet_transaction(
pending_effects,
&[address],
block_number,
tx_index,
&txhash_bytes,
)?;
}
SelectedMempoolTransaction::ActivationVote {
fee,
address,
hash,
original,
..
} => {
let (&txtype, body) = original
.split_first()
.ok_or_else(|| anyhow!("Empty activation vote transaction bytes"))?;
let transaction = ActivationVote::from_bytes(txtype, body).await?;
let vote = &transaction.unsigned_activationvote;
let proposal_key = decode(&vote.proposal_key)?;
let candidate_key =
activation_candidate_key(&vote.proposal_key, &vote.development_hash)
.map_err(|err| anyhow!(err))?;
let vote_key = activation_vote_record_key(
&vote.proposal_key,
&vote.development_hash,
&vote.address,
)
.map_err(|err| anyhow!(err))?;
let eligible_addresses = NodeInfo::governance_countable_nodes_for_activation(
db,
&vote.proposal_key,
&vote.development_hash,
(block_timestamp as u64) * 1_000,
block_number,
)
.await
.map_err(|err| anyhow!(err))?
.into_iter()
.map(|node| {
Wallet::short_address_to_bytes(&node.address)
.ok_or_else(|| anyhow!("Eligible governance node has an invalid address"))
})
.collect::<Result<Vec<_>>>()?;
add_balance_change(db, &mut balance_changes, address, &BASECOIN, -*fee);
add_balance_change_bytes(
&mut balance_changes,
miner_address.clone(),
base_coin.clone(),
*fee,
);
pending_effects.activation_vote(
proposal_key,
candidate_key,
vote_key,
original.clone(),
vote.development_hash.clone(),
vote.development_location.clone(),
eligible_addresses,
block_number,
);
let txhash_bytes = decode(hash)?;
pending_effects.set_tree(
"txid",
txhash_bytes.clone(),
format!("{block_number}:{tx_index}").into_bytes(),
);
index_selected_wallet_transaction(
pending_effects,
&[address],
block_number,
tx_index,
&txhash_bytes,
)?;
}
SelectedMempoolTransaction::StorageValue { SelectedMempoolTransaction::StorageValue {
fee, fee,
address, address,
@ -1234,6 +1521,7 @@ pub async fn clear_selected_transaction_sql(
db, db,
miner, miner,
block_number, block_number,
crate::Utc::now().timestamp() as u32,
start_index, start_index,
&mut pending_effects, &mut pending_effects,
) )
@ -1278,6 +1566,9 @@ pub async fn delete_selected_transactions(batch: &SelectedMempoolBatch) -> Resul
let borrower_ids = ids_for_table(batch, "loan_payment"); let borrower_ids = ids_for_table(batch, "loan_payment");
let collateral_ids = ids_for_table(batch, "collateral_claim"); let collateral_ids = ids_for_table(batch, "collateral_claim");
let storage_key_ids = ids_for_table(batch, "storage_key"); let storage_key_ids = ids_for_table(batch, "storage_key");
let proposal_key_ids = ids_for_table(batch, "proposal_key");
let proposal_vote_ids = ids_for_table(batch, "proposal_vote");
let activation_vote_ids = ids_for_table(batch, "activation_vote");
let storage_value_ids = ids_for_table(batch, "storage_value"); let storage_value_ids = ids_for_table(batch, "storage_value");
delete_rows("transfer", &transfer_ids).await?; delete_rows("transfer", &transfer_ids).await?;
@ -1292,6 +1583,9 @@ pub async fn delete_selected_transactions(batch: &SelectedMempoolBatch) -> Resul
delete_rows("loan_payment", &borrower_ids).await?; delete_rows("loan_payment", &borrower_ids).await?;
delete_rows("collateral_claim", &collateral_ids).await?; delete_rows("collateral_claim", &collateral_ids).await?;
delete_rows("storage_key", &storage_key_ids).await?; delete_rows("storage_key", &storage_key_ids).await?;
delete_rows("proposal_key", &proposal_key_ids).await?;
delete_rows("proposal_vote", &proposal_vote_ids).await?;
delete_rows("activation_vote", &activation_vote_ids).await?;
delete_rows("storage_value", &storage_value_ids).await?; delete_rows("storage_value", &storage_value_ids).await?;
Ok(()) Ok(())

View File

@ -1,6 +1,119 @@
use super::*; use super::*;
use crate::common::governance::{
activation_vote_record_key, countable_governance_addresses, proposal_vote_record_key,
GovernanceNodeSnapshot, ACTIVATION_VOTES_TREE, PROPOSAL_VOTES_TREE,
};
use crate::sled::Db;
use std::collections::HashSet;
impl NodeInfo { impl NodeInfo {
pub async fn governance_node_snapshot(address: &str) -> Option<GovernanceNodeSnapshot> {
let map = ADDRESS_MAP.lock().await;
map.get(address).map(|node| GovernanceNodeSnapshot {
address: address.to_string(),
ip: node.ip.clone(),
blocks_mined: node.blocks_mined,
added_timestamp: node.added_timestamp,
deleted_block: node.deleted_block,
})
}
pub async fn governance_node_snapshots() -> Vec<GovernanceNodeSnapshot> {
let map = ADDRESS_MAP.lock().await;
let mut snapshots: Vec<GovernanceNodeSnapshot> = map
.iter()
.map(|(address, node)| GovernanceNodeSnapshot {
address: address.clone(),
ip: node.ip.clone(),
blocks_mined: node.blocks_mined,
added_timestamp: node.added_timestamp,
deleted_block: node.deleted_block,
})
.collect();
snapshots.sort_by(|left, right| left.address.cmp(&right.address));
snapshots
}
pub async fn governance_eligible_nodes(
reference_timestamp_millis: u64,
current_height: u32,
) -> Vec<GovernanceNodeSnapshot> {
Self::governance_node_snapshots()
.await
.into_iter()
.filter(|node| {
node.is_eligible_for_governance_count(reference_timestamp_millis, current_height)
})
.collect()
}
pub async fn governance_countable_nodes_for_proposal(
db: &Db,
proposal_key: &str,
reference_timestamp_millis: u64,
current_height: u32,
) -> Result<Vec<GovernanceNodeSnapshot>, String> {
let votes = db
.open_tree(PROPOSAL_VOTES_TREE)
.map_err(|err| format!("Could not open proposal votes: {err}"))?;
let snapshots = Self::governance_node_snapshots().await;
let mut existing_voters = HashSet::new();
for node in &snapshots {
let vote_key = proposal_vote_record_key(proposal_key, &node.address)?;
if votes
.contains_key(vote_key)
.map_err(|err| format!("Could not check existing proposal vote: {err}"))?
{
existing_voters.insert(node.address.clone());
}
}
let countable = countable_governance_addresses(
&snapshots,
&existing_voters,
reference_timestamp_millis,
current_height,
);
Ok(snapshots
.into_iter()
.filter(|node| countable.contains(&node.address))
.collect())
}
pub async fn governance_countable_nodes_for_activation(
db: &Db,
proposal_key: &str,
development_hash: &str,
reference_timestamp_millis: u64,
current_height: u32,
) -> Result<Vec<GovernanceNodeSnapshot>, String> {
let votes = db
.open_tree(ACTIVATION_VOTES_TREE)
.map_err(|err| format!("Could not open activation votes: {err}"))?;
let snapshots = Self::governance_node_snapshots().await;
let mut existing_voters = HashSet::new();
for node in &snapshots {
let vote_key =
activation_vote_record_key(proposal_key, development_hash, &node.address)?;
if votes
.contains_key(vote_key)
.map_err(|err| format!("Could not check existing activation vote: {err}"))?
{
existing_voters.insert(node.address.clone());
}
}
let countable = countable_governance_addresses(
&snapshots,
&existing_voters,
reference_timestamp_millis,
current_height,
);
Ok(snapshots
.into_iter()
.filter(|node| countable.contains(&node.address))
.collect())
}
pub async fn address_checkup(address: &str, block_number: u32) -> bool { pub async fn address_checkup(address: &str, block_number: u32) -> bool {
let map = ADDRESS_MAP.lock().await; let map = ADDRESS_MAP.lock().await;
if let Some(node_info) = map.get(address) { if let Some(node_info) = map.get(address) {

View File

@ -0,0 +1,88 @@
use crate::blocks::activation_vote::ActivationVote;
use crate::common::governance::{activation_candidate_key, activation_vote_record_key};
use crate::decode;
use crate::records::memory::mempool::BASECOIN;
use crate::records::memory::network_mapping::NodeInfo;
use crate::records::record_chain::pending_effects::{BalanceOperand, PendingEffects};
use crate::records::record_chain::wallet_tx_index::index_wallet_transaction;
use crate::sled::Db;
use crate::wallets::structures::Wallet;
use crate::Arc;
use crate::Mutex;
#[allow(clippy::too_many_arguments)]
pub async fn process_activation_vote(
transaction: &ActivationVote,
mut binary_data: Vec<u8>,
db: &Db,
index_counter: Arc<Mutex<&mut usize>>,
miner: String,
block_header_number: u32,
block_timestamp: u32,
pending_effects: &mut PendingEffects,
) -> Result<Vec<u8>, String> {
let mut index = index_counter.lock().await;
**index += 1;
let vote = &transaction.unsigned_activationvote;
let txhash = vote.hash().await;
let txhash_bytes =
decode(&txhash).map_err(|err| format!("Could not decode activation vote txhash: {err}"))?;
let proposal_key = decode(&vote.proposal_key)
.map_err(|err| format!("Could not decode proposal key: {err}"))?;
let candidate_key = activation_candidate_key(&vote.proposal_key, &vote.development_hash)?;
let vote_key =
activation_vote_record_key(&vote.proposal_key, &vote.development_hash, &vote.address)?;
let transaction_bytes = transaction
.to_bytes()
.await
.map_err(|err| format!("Could not serialize activation vote transaction: {err}"))?;
binary_data.extend(&transaction_bytes);
let eligible_addresses = NodeInfo::governance_countable_nodes_for_activation(
db,
&vote.proposal_key,
&vote.development_hash,
(block_timestamp as u64) * 1_000,
block_header_number,
)
.await?
.into_iter()
.map(|node| {
Wallet::short_address_to_bytes(&node.address)
.ok_or_else(|| "Eligible governance node has an invalid address.".to_string())
})
.collect::<Result<Vec<_>, _>>()?;
pending_effects.activation_vote(
proposal_key,
candidate_key,
vote_key,
transaction_bytes,
vote.development_hash.clone(),
vote.development_location.clone(),
eligible_addresses,
block_header_number,
);
pending_effects.add_balance(&miner, vote.txfee, &BASECOIN, BalanceOperand::Addition);
pending_effects.add_balance(
&vote.address,
vote.txfee,
&BASECOIN,
BalanceOperand::Subtraction,
);
pending_effects.set_tree(
"txid",
txhash_bytes.clone(),
format!("{block_header_number}:{}", **index).into_bytes(),
);
index_wallet_transaction(
pending_effects,
&[&vote.address],
block_header_number,
**index as u32,
&txhash_bytes,
)?;
Ok(binary_data)
}

View File

@ -1,4 +1,5 @@
// The record_chain module contains the block-save processors for each transaction family. // The record_chain module contains the block-save processors for each transaction family.
pub mod activation_vote_tx;
pub mod add_payments_db; pub mod add_payments_db;
pub mod borrower_tx; pub mod borrower_tx;
pub mod burn_tx; pub mod burn_tx;
@ -16,6 +17,8 @@ pub mod nft_tx;
pub mod parse_transactions; pub mod parse_transactions;
pub mod pending_effects; pub mod pending_effects;
pub mod previous_difficulty; pub mod previous_difficulty;
pub mod proposal_key_tx;
pub mod proposal_vote_tx;
pub mod rewards_tx; pub mod rewards_tx;
pub mod save; pub mod save;
pub mod save_flags; pub mod save_flags;

View File

@ -1,5 +1,5 @@
use crate::blocks::nft::CreateNftTransaction; use crate::blocks::nft::CreateNftTransaction;
use crate::common::nft_assets::nft_asset_name; use crate::common::nft_assets::{nft_asset_name, NFT_UNIT};
use crate::decode; use crate::decode;
use crate::records::memory::mempool::BASECOIN; use crate::records::memory::mempool::BASECOIN;
use crate::records::record_chain::pending_effects::{BalanceOperand, PendingEffects}; use crate::records::record_chain::pending_effects::{BalanceOperand, PendingEffects};
@ -8,8 +8,6 @@ use crate::sled::Db;
use crate::Arc; use crate::Arc;
use crate::Mutex; use crate::Mutex;
const NFT_UNIT: u64 = 100_000_000;
pub async fn process_nft( pub async fn process_nft(
transaction: &CreateNftTransaction, transaction: &CreateNftTransaction,
mut binary_data: Vec<u8>, mut binary_data: Vec<u8>,
@ -60,7 +58,21 @@ pub async fn process_nft(
&nft_save_name, &nft_save_name,
BalanceOperand::Addition, BalanceOperand::Addition,
); );
pending_effects.set_tree("nfts", nft_save_name.as_bytes().to_vec(), b"1".to_vec()); pending_effects.set_tree(
"nfts",
nft_save_name.as_bytes().to_vec(),
vec![transaction.unsigned_create_nft.ownership_type],
);
pending_effects.set_tree(
"nft_ownership",
nft_save_name.as_bytes().to_vec(),
vec![transaction.unsigned_create_nft.ownership_type],
);
pending_effects.set_tree(
"nft_supply",
nft_save_name.as_bytes().to_vec(),
NFT_UNIT.to_le_bytes().to_vec(),
);
pending_effects.set_tree( pending_effects.set_tree(
"nft_origins", "nft_origins",
nft_save_name.as_bytes().to_vec(), nft_save_name.as_bytes().to_vec(),
@ -80,7 +92,21 @@ pub async fn process_nft(
&nft_save_name, &nft_save_name,
BalanceOperand::Addition, BalanceOperand::Addition,
); );
pending_effects.set_tree("nfts", nft_save_name.as_bytes().to_vec(), b"1".to_vec()); pending_effects.set_tree(
"nfts",
nft_save_name.as_bytes().to_vec(),
vec![transaction.unsigned_create_nft.ownership_type],
);
pending_effects.set_tree(
"nft_ownership",
nft_save_name.as_bytes().to_vec(),
vec![transaction.unsigned_create_nft.ownership_type],
);
pending_effects.set_tree(
"nft_supply",
nft_save_name.as_bytes().to_vec(),
NFT_UNIT.to_le_bytes().to_vec(),
);
pending_effects.set_tree( pending_effects.set_tree(
"nft_origins", "nft_origins",
nft_save_name.as_bytes().to_vec(), nft_save_name.as_bytes().to_vec(),

View File

@ -10,6 +10,8 @@ use crate::records::record_chain::lender_tx::process_lender;
use crate::records::record_chain::marketing_tx::process_marketing; use crate::records::record_chain::marketing_tx::process_marketing;
use crate::records::record_chain::nft_tx::process_nft; use crate::records::record_chain::nft_tx::process_nft;
use crate::records::record_chain::pending_effects::PendingEffects; use crate::records::record_chain::pending_effects::PendingEffects;
use crate::records::record_chain::proposal_key_tx::process_proposal_key;
use crate::records::record_chain::proposal_vote_tx::process_proposal_vote;
use crate::records::record_chain::rewards_tx::process_rewards; use crate::records::record_chain::rewards_tx::process_rewards;
use crate::records::record_chain::storage_key_tx::process_storage_key; use crate::records::record_chain::storage_key_tx::process_storage_key;
use crate::records::record_chain::storage_value_tx::process_storage_value; use crate::records::record_chain::storage_value_tx::process_storage_value;
@ -203,6 +205,44 @@ pub async fn handle_transactions(
) )
.await .await
} }
Transaction::ProposalKey(proposal_key_transaction) => {
process_proposal_key(
proposal_key_transaction,
binary_data.clone(),
db,
index_mutex.clone(),
miner.clone(),
block_header_number,
pending_effects,
)
.await
}
Transaction::ProposalVote(proposal_vote_transaction) => {
process_proposal_vote(
proposal_vote_transaction,
binary_data.clone(),
db,
index_mutex.clone(),
miner.clone(),
block_header_number,
block.vrf_block.unmined_block.timestamp,
pending_effects,
)
.await
}
Transaction::ActivationVote(activation_vote_transaction) => {
process_activation_vote(
activation_vote_transaction,
binary_data.clone(),
db,
index_mutex.clone(),
miner.clone(),
block_header_number,
block.vrf_block.unmined_block.timestamp,
pending_effects,
)
.await
}
Transaction::StorageBool(_) Transaction::StorageBool(_)
| Transaction::StorageU8(_) | Transaction::StorageU8(_)
| Transaction::StorageU16(_) | Transaction::StorageU16(_)
@ -251,3 +291,4 @@ pub async fn handle_transactions(
} }
Ok(binary_data) Ok(binary_data)
} }
use crate::records::record_chain::activation_vote_tx::process_activation_vote;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,57 @@
use crate::blocks::proposal_key::ProposalKey;
use crate::common::governance::PROPOSAL_KEYS_TREE;
use crate::decode;
use crate::records::memory::mempool::BASECOIN;
use crate::records::record_chain::pending_effects::{BalanceOperand, PendingEffects};
use crate::records::record_chain::wallet_tx_index::index_wallet_transaction;
use crate::sled::Db;
use crate::Arc;
use crate::Mutex;
pub async fn process_proposal_key(
transaction: &ProposalKey,
mut binary_data: Vec<u8>,
_db: &Db,
index_counter: Arc<Mutex<&mut usize>>,
miner: String,
block_header_number: u32,
pending_effects: &mut PendingEffects,
) -> Result<Vec<u8>, String> {
let mut index = index_counter.lock().await;
**index += 1;
let proposal = &transaction.unsigned_proposalkey;
let txhash = proposal.hash().await;
let txhash_bytes =
decode(&txhash).map_err(|err| format!("Could not decode proposal key txhash: {err}"))?;
let transaction_bytes = transaction
.to_bytes()
.await
.map_err(|err| format!("Could not serialize proposal key transaction: {err}"))?;
binary_data.extend(&transaction_bytes);
// The signed transaction hash is the immutable proposal key. The value
// preserves the transaction and its exact proposal-document hash.
pending_effects.set_tree(PROPOSAL_KEYS_TREE, txhash_bytes.clone(), transaction_bytes);
pending_effects.add_balance(&miner, proposal.txfee, &BASECOIN, BalanceOperand::Addition);
pending_effects.add_balance(
&proposal.address,
proposal.txfee,
&BASECOIN,
BalanceOperand::Subtraction,
);
pending_effects.set_tree(
"txid",
txhash_bytes.clone(),
format!("{block_header_number}:{}", **index).into_bytes(),
);
index_wallet_transaction(
pending_effects,
&[&proposal.address],
block_header_number,
**index as u32,
&txhash_bytes,
)?;
Ok(binary_data)
}

View File

@ -0,0 +1,81 @@
use crate::blocks::proposal_vote::ProposalVote;
use crate::common::governance::proposal_vote_record_key;
use crate::decode;
use crate::records::memory::mempool::BASECOIN;
use crate::records::memory::network_mapping::NodeInfo;
use crate::records::record_chain::pending_effects::{BalanceOperand, PendingEffects};
use crate::records::record_chain::wallet_tx_index::index_wallet_transaction;
use crate::sled::Db;
use crate::wallets::structures::Wallet;
use crate::Arc;
use crate::Mutex;
pub async fn process_proposal_vote(
transaction: &ProposalVote,
mut binary_data: Vec<u8>,
_db: &Db,
index_counter: Arc<Mutex<&mut usize>>,
miner: String,
block_header_number: u32,
block_timestamp: u32,
pending_effects: &mut PendingEffects,
) -> Result<Vec<u8>, String> {
let mut index = index_counter.lock().await;
**index += 1;
let vote = &transaction.unsigned_proposalvote;
let txhash = vote.hash().await;
let txhash_bytes =
decode(&txhash).map_err(|err| format!("Could not decode proposal vote txhash: {err}"))?;
let proposal_key = decode(&vote.proposal_key)
.map_err(|err| format!("Could not decode proposal key: {err}"))?;
let vote_key = proposal_vote_record_key(&vote.proposal_key, &vote.address)?;
let transaction_bytes = transaction
.to_bytes()
.await
.map_err(|err| format!("Could not serialize proposal vote transaction: {err}"))?;
binary_data.extend(&transaction_bytes);
let eligible_addresses = NodeInfo::governance_countable_nodes_for_proposal(
_db,
&vote.proposal_key,
(block_timestamp as u64) * 1_000,
block_header_number,
)
.await?
.into_iter()
.map(|node| {
Wallet::short_address_to_bytes(&node.address)
.ok_or_else(|| "Eligible governance node has an invalid address.".to_string())
})
.collect::<Result<Vec<_>, _>>()?;
pending_effects.proposal_vote(
proposal_key,
vote_key,
transaction_bytes,
eligible_addresses,
block_header_number,
);
pending_effects.add_balance(&miner, vote.txfee, &BASECOIN, BalanceOperand::Addition);
pending_effects.add_balance(
&vote.address,
vote.txfee,
&BASECOIN,
BalanceOperand::Subtraction,
);
pending_effects.set_tree(
"txid",
txhash_bytes.clone(),
format!("{block_header_number}:{}", **index).into_bytes(),
);
index_wallet_transaction(
pending_effects,
&[&vote.address],
block_header_number,
**index as u32,
&txhash_bytes,
)?;
Ok(binary_data)
}

View File

@ -145,6 +145,15 @@ pub async fn save_block(params: SaveBlockParams) -> Result<(), String> {
) )
.await?; .await?;
// Recalculate every unfinished governance decision once per accepted
// block. This catches offline-vote expiry, same-wallet reactivation, and
// same-IP wallet replacement without making socket events authoritative.
pending_effects.governance_lifecycle(
NodeInfo::governance_node_snapshots().await,
(timestamp as u64) * 1_000,
block_header_number,
);
let mut saved_reward = None; let mut saved_reward = None;
for (position, transaction) in block.transactions.iter().enumerate() { for (position, transaction) in block.transactions.iter().enumerate() {
if let Transaction::Rewards(rewards) = transaction { if let Transaction::Rewards(rewards) = transaction {
@ -175,6 +184,7 @@ pub async fn save_block(params: SaveBlockParams) -> Result<(), String> {
&db, &db,
miner.clone(), miner.clone(),
block_header_number, block_header_number,
timestamp,
start_index, start_index,
&mut pending_effects, &mut pending_effects,
) )

View File

@ -8,6 +8,8 @@ use crate::blocks::loan_payment::ContractPaymentTransaction;
use crate::blocks::loans::LoanContractTransaction; use crate::blocks::loans::LoanContractTransaction;
use crate::blocks::marketing::MarketingTransaction; use crate::blocks::marketing::MarketingTransaction;
use crate::blocks::nft::CreateNftTransaction; use crate::blocks::nft::CreateNftTransaction;
use crate::blocks::proposal_key::ProposalKey;
use crate::blocks::proposal_vote::ProposalVote;
use crate::blocks::rewards::RewardsTransaction; use crate::blocks::rewards::RewardsTransaction;
use crate::blocks::storage_key::StorageKey; use crate::blocks::storage_key::StorageKey;
use crate::blocks::storage_values::{ use crate::blocks::storage_values::{
@ -19,12 +21,12 @@ use crate::blocks::token::CreateTokenTransaction;
use crate::blocks::transfer::TransferTransaction; use crate::blocks::transfer::TransferTransaction;
use crate::blocks::vanity::VanityAddressTransaction; use crate::blocks::vanity::VanityAddressTransaction;
use crate::common::types::{ use crate::common::types::{
Transaction, BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE, Transaction, ACTIVATION_VOTE_TYPE, BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE,
DELETE_KEY_TYPE, GENESIS_TYPE, ISSUE_TOKEN_TYPE, LENDER_TYPE, MARKETING_TYPE, REWARDS_TYPE, CREATE_TOKEN_TYPE, DELETE_KEY_TYPE, GENESIS_TYPE, ISSUE_TOKEN_TYPE, LENDER_TYPE,
STORAGE_BOOL_TYPE, STORAGE_I128_TYPE, STORAGE_I16_TYPE, STORAGE_I32_TYPE, STORAGE_I64_TYPE, MARKETING_TYPE, PROPOSAL_KEY_TYPE, PROPOSAL_VOTE_TYPE, REWARDS_TYPE, STORAGE_BOOL_TYPE,
STORAGE_I8_TYPE, STORAGE_KEY_TYPE, STORAGE_STRING_TYPE, STORAGE_U128_TYPE, STORAGE_U16_TYPE, STORAGE_I128_TYPE, STORAGE_I16_TYPE, STORAGE_I32_TYPE, STORAGE_I64_TYPE, STORAGE_I8_TYPE,
STORAGE_U32_TYPE, STORAGE_U64_TYPE, STORAGE_U8_TYPE, SWAP_TYPE, TRANSFER_TYPE, STORAGE_KEY_TYPE, STORAGE_STRING_TYPE, STORAGE_U128_TYPE, STORAGE_U16_TYPE, STORAGE_U32_TYPE,
VANITY_ADDRESS_TYPE, STORAGE_U64_TYPE, STORAGE_U8_TYPE, SWAP_TYPE, TRANSFER_TYPE, VANITY_ADDRESS_TYPE,
}; };
use crate::rpc::command_maps::get_bytes; use crate::rpc::command_maps::get_bytes;
@ -212,6 +214,33 @@ pub async fn load_block_from_binary(binary_data: &[u8]) -> Result<Block, String>
i += body_len; i += body_len;
storage_key storage_key
} }
PROPOSAL_KEY_TYPE => {
let proposal_key = Transaction::ProposalKey(
ProposalKey::from_bytes(txtype, body)
.await
.map_err(|e| e.to_string())?,
);
i += body_len;
proposal_key
}
PROPOSAL_VOTE_TYPE => {
let proposal_vote = Transaction::ProposalVote(
ProposalVote::from_bytes(txtype, body)
.await
.map_err(|e| e.to_string())?,
);
i += body_len;
proposal_vote
}
ACTIVATION_VOTE_TYPE => {
let activation_vote = Transaction::ActivationVote(
ActivationVote::from_bytes(txtype, body)
.await
.map_err(|e| e.to_string())?,
);
i += body_len;
activation_vote
}
STORAGE_BOOL_TYPE => { STORAGE_BOOL_TYPE => {
let storage = Transaction::StorageBool( let storage = Transaction::StorageBool(
StorageBool::from_bytes(txtype, body) StorageBool::from_bytes(txtype, body)
@ -345,3 +374,4 @@ pub async fn load_block_from_binary(binary_data: &[u8]) -> Result<Block, String>
Ok(block) Ok(block)
} }
use crate::blocks::activation_vote::ActivationVote;

View File

@ -8,6 +8,8 @@ use crate::blocks::loan_payment::ContractPaymentTransaction;
use crate::blocks::loans::LoanContractTransaction; use crate::blocks::loans::LoanContractTransaction;
use crate::blocks::marketing::MarketingTransaction; use crate::blocks::marketing::MarketingTransaction;
use crate::blocks::nft::CreateNftTransaction; use crate::blocks::nft::CreateNftTransaction;
use crate::blocks::proposal_key::ProposalKey;
use crate::blocks::proposal_vote::ProposalVote;
use crate::blocks::rewards::RewardsTransaction; use crate::blocks::rewards::RewardsTransaction;
use crate::blocks::storage_key::StorageKey; use crate::blocks::storage_key::StorageKey;
use crate::blocks::storage_values::{ use crate::blocks::storage_values::{
@ -20,12 +22,12 @@ use crate::blocks::transfer::TransferTransaction;
use crate::blocks::vanity::VanityAddressTransaction; use crate::blocks::vanity::VanityAddressTransaction;
use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::common::network_paths_and_settings::block_extension_and_paths;
use crate::common::types::{ use crate::common::types::{
Transaction, BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE, Transaction, ACTIVATION_VOTE_TYPE, BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE,
DELETE_KEY_TYPE, GENESIS_TYPE, ISSUE_TOKEN_TYPE, LENDER_TYPE, MARKETING_TYPE, REWARDS_TYPE, CREATE_TOKEN_TYPE, DELETE_KEY_TYPE, GENESIS_TYPE, ISSUE_TOKEN_TYPE, LENDER_TYPE,
STORAGE_BOOL_TYPE, STORAGE_I128_TYPE, STORAGE_I16_TYPE, STORAGE_I32_TYPE, STORAGE_I64_TYPE, MARKETING_TYPE, PROPOSAL_KEY_TYPE, PROPOSAL_VOTE_TYPE, REWARDS_TYPE, STORAGE_BOOL_TYPE,
STORAGE_I8_TYPE, STORAGE_KEY_TYPE, STORAGE_STRING_TYPE, STORAGE_U128_TYPE, STORAGE_U16_TYPE, STORAGE_I128_TYPE, STORAGE_I16_TYPE, STORAGE_I32_TYPE, STORAGE_I64_TYPE, STORAGE_I8_TYPE,
STORAGE_U32_TYPE, STORAGE_U64_TYPE, STORAGE_U8_TYPE, SWAP_TYPE, TRANSFER_TYPE, STORAGE_KEY_TYPE, STORAGE_STRING_TYPE, STORAGE_U128_TYPE, STORAGE_U16_TYPE, STORAGE_U32_TYPE,
VANITY_ADDRESS_TYPE, STORAGE_U64_TYPE, STORAGE_U8_TYPE, SWAP_TYPE, TRANSFER_TYPE, VANITY_ADDRESS_TYPE,
}; };
use crate::fs; use crate::fs;
use crate::rpc::command_maps::get_bytes; use crate::rpc::command_maps::get_bytes;
@ -229,6 +231,33 @@ pub async fn load_block(block_number: u32) -> Result<Block, String> {
i += body_len; i += body_len;
storage_key storage_key
} }
PROPOSAL_KEY_TYPE => {
let proposal_key = Transaction::ProposalKey(
ProposalKey::from_bytes(txtype, body)
.await
.map_err(|e| e.to_string())?,
);
i += body_len;
proposal_key
}
PROPOSAL_VOTE_TYPE => {
let proposal_vote = Transaction::ProposalVote(
ProposalVote::from_bytes(txtype, body)
.await
.map_err(|e| e.to_string())?,
);
i += body_len;
proposal_vote
}
ACTIVATION_VOTE_TYPE => {
let activation_vote = Transaction::ActivationVote(
ActivationVote::from_bytes(txtype, body)
.await
.map_err(|e| e.to_string())?,
);
i += body_len;
activation_vote
}
STORAGE_BOOL_TYPE => { STORAGE_BOOL_TYPE => {
let storage = Transaction::StorageBool( let storage = Transaction::StorageBool(
StorageBool::from_bytes(txtype, body) StorageBool::from_bytes(txtype, body)
@ -362,3 +391,4 @@ pub async fn load_block(block_number: u32) -> Result<Block, String> {
Ok(block) Ok(block)
} }
use crate::blocks::activation_vote::ActivationVote;

View File

@ -0,0 +1,345 @@
use crate::common::governance::{
governance_activation_height, governance_approval_reached, CONSENSUS_ACTIVATIONS_TREE,
FINISHED_ACTIVATION_VOTES_TREE, FINISHED_PROPOSAL_VOTES_TREE, PROPOSAL_KEYS_TREE,
};
use crate::records::block_height::get_block_height::get_height;
use crate::records::memory::response_channels::{reserve_entry, Command};
use crate::rpc::command_maps::RPC_GOVERNANCE_STATE_SYNC;
use crate::rpc::commands::governance_state_sync::{
CONSENSUS_ACTIVATION_TAG, FINISHED_ACTIVATION_TAG, FINISHED_PROPOSAL_TAG,
};
use crate::rpc::responses::RpcResponse;
use crate::sled::{Batch, Db};
use crate::timeout;
use crate::Arc;
use crate::Duration;
use crate::Mutex;
use crate::TcpStream;
use std::collections::{HashMap, HashSet};
const FINISHED_PROPOSAL_KEY_BYTES: usize = 32;
const FINISHED_PROPOSAL_VALUE_BYTES: usize = 28;
const FINISHED_ACTIVATION_KEY_BYTES: usize = 64;
const FINISHED_ACTIVATION_VALUE_BYTES: usize = 132;
const CONSENSUS_ACTIVATION_KEY_BYTES: usize = 32;
const CONSENSUS_ACTIVATION_VALUE_BYTES: usize = 136;
#[derive(Default)]
struct GovernanceSnapshot {
proposals: Vec<(Vec<u8>, Vec<u8>)>,
activations: Vec<(Vec<u8>, Vec<u8>)>,
schedules: Vec<(Vec<u8>, Vec<u8>)>,
}
pub async fn sync_governance_state(
stream: Arc<Mutex<TcpStream>>,
db: &Db,
map: Arc<Mutex<Command>>,
connections_key: String,
) -> Result<(), String> {
let (uid, _tx, rx) = reserve_entry(map).await;
let mut request = Vec::with_capacity(4);
request.push(RPC_GOVERNANCE_STATE_SYNC);
request.extend_from_slice(&uid);
RpcResponse::send_raw(&stream, Some(&connections_key), &request).await;
let payload = {
let mut rx = rx.lock().await;
timeout(Duration::from_secs(30), rx.recv())
.await
.map_err(|_| "Timed out waiting for governance state sync response".to_string())?
.ok_or_else(|| "Governance state sync response channel closed".to_string())?
};
let snapshot = parse_snapshot(&payload)?;
validate_snapshot(db, &snapshot)?;
import_snapshot(db, snapshot)
}
fn parse_snapshot(payload: &[u8]) -> Result<GovernanceSnapshot, String> {
let mut snapshot = GovernanceSnapshot::default();
let mut offset = 0;
while offset < payload.len() {
let tag = payload[offset];
offset += 1;
let (key_bytes, value_bytes) = match tag {
FINISHED_PROPOSAL_TAG => (FINISHED_PROPOSAL_KEY_BYTES, FINISHED_PROPOSAL_VALUE_BYTES),
FINISHED_ACTIVATION_TAG => (
FINISHED_ACTIVATION_KEY_BYTES,
FINISHED_ACTIVATION_VALUE_BYTES,
),
CONSENSUS_ACTIVATION_TAG => (
CONSENSUS_ACTIVATION_KEY_BYTES,
CONSENSUS_ACTIVATION_VALUE_BYTES,
),
_ => return Err("Governance state sync contained an unknown record type.".to_string()),
};
let end = offset
.checked_add(key_bytes + value_bytes)
.ok_or_else(|| "Governance state sync record length overflowed.".to_string())?;
if end > payload.len() {
return Err("Governance state sync ended inside a record.".to_string());
}
let key = payload[offset..offset + key_bytes].to_vec();
let value = payload[offset + key_bytes..end].to_vec();
offset = end;
match tag {
FINISHED_PROPOSAL_TAG => snapshot.proposals.push((key, value)),
FINISHED_ACTIVATION_TAG => snapshot.activations.push((key, value)),
CONSENSUS_ACTIVATION_TAG => snapshot.schedules.push((key, value)),
_ => unreachable!(),
}
}
Ok(snapshot)
}
fn validate_snapshot(db: &Db, snapshot: &GovernanceSnapshot) -> Result<(), String> {
let local_height = get_height(db);
let proposal_keys = db
.open_tree(PROPOSAL_KEYS_TREE)
.map_err(|err| format!("Could not open proposal keys during governance sync: {err}"))?;
let mut finished_proposals = HashMap::new();
for (key, value) in &snapshot.proposals {
if finished_proposals.contains_key(key) {
return Err("Governance state sync contained a duplicate proposal.".to_string());
}
if !proposal_keys
.contains_key(key)
.map_err(|err| format!("Could not verify synced proposal key: {err}"))?
{
return Err("Synced final proposal does not exist in the validated chain.".to_string());
}
validate_finished_counts(value, local_height, 24)?;
finished_proposals.insert(key.clone(), value.clone());
}
let mut finished_activations = HashMap::new();
for (key, value) in &snapshot.activations {
if finished_activations.contains_key(key) {
return Err("Governance state sync contained a duplicate implementation.".to_string());
}
let proposal_key = &key[..32];
if !finished_proposals.contains_key(proposal_key)
&& !db
.open_tree(FINISHED_PROPOSAL_VOTES_TREE)
.map_err(|err| format!("Could not open finished proposals: {err}"))?
.contains_key(proposal_key)
.map_err(|err| format!("Could not verify approved proposal: {err}"))?
{
return Err("Synced implementation belongs to an unapproved proposal.".to_string());
}
validate_finished_counts(value, local_height, 24)?;
let finalized_height = read_u32(value, 24)?;
let activation_height = read_u32(value, 28)?;
if governance_activation_height(finalized_height) != Some(activation_height) {
return Err("Synced implementation has an invalid activation height.".to_string());
}
finished_activations.insert(key.clone(), value.clone());
}
let mut scheduled_candidates = HashSet::new();
let mut scheduled_proposals = HashSet::new();
for (proposal_key, schedule) in &snapshot.schedules {
if !scheduled_proposals.insert(proposal_key.clone()) {
return Err(
"Governance state sync contained duplicate schedules for a proposal.".to_string(),
);
}
let development_hash = &schedule[..32];
let mut candidate_key = proposal_key.clone();
candidate_key.extend_from_slice(development_hash);
let finished = match finished_activations.get(&candidate_key) {
Some(value) => value.clone(),
None => db
.open_tree(FINISHED_ACTIVATION_VOTES_TREE)
.map_err(|err| format!("Could not open finished activations: {err}"))?
.get(&candidate_key)
.map_err(|err| format!("Could not verify finalized implementation: {err}"))?
.map(|value| value.to_vec())
.ok_or_else(|| {
"Synced activation schedule has no finalized implementation.".to_string()
})?,
};
if schedule[32..36] != finished[28..32] || schedule[36..136] != finished[32..132] {
return Err("Synced activation schedule conflicts with its final vote.".to_string());
}
scheduled_candidates.insert(candidate_key);
}
for candidate_key in finished_activations.keys() {
if !scheduled_candidates.contains(candidate_key) {
return Err(
"Synced finalized implementation is missing its activation schedule.".to_string(),
);
}
}
validate_no_conflicts(db, FINISHED_PROPOSAL_VOTES_TREE, &snapshot.proposals)?;
validate_no_conflicts(db, FINISHED_ACTIVATION_VOTES_TREE, &snapshot.activations)?;
validate_no_conflicts(db, CONSENSUS_ACTIVATIONS_TREE, &snapshot.schedules)?;
Ok(())
}
fn validate_finished_counts(
value: &[u8],
local_height: u32,
height_offset: usize,
) -> Result<(), String> {
let yes = read_u64(value, 0)?;
let no = read_u64(value, 8)?;
let eligible = read_u64(value, 16)?;
if yes.checked_add(no).is_none() || yes + no > eligible {
return Err("Synced governance vote counts are invalid.".to_string());
}
if !governance_approval_reached(yes, eligible) {
return Err("Synced governance result did not reach approval.".to_string());
}
if read_u32(value, height_offset)? > local_height {
return Err("Synced governance result finalizes above the local chain.".to_string());
}
Ok(())
}
fn read_u64(value: &[u8], offset: usize) -> Result<u64, String> {
let bytes = value
.get(offset..offset + 8)
.ok_or_else(|| "Synced governance value is too short.".to_string())?;
Ok(u64::from_le_bytes(bytes.try_into().unwrap()))
}
fn read_u32(value: &[u8], offset: usize) -> Result<u32, String> {
let bytes = value
.get(offset..offset + 4)
.ok_or_else(|| "Synced governance value is too short.".to_string())?;
Ok(u32::from_le_bytes(bytes.try_into().unwrap()))
}
fn import_snapshot(db: &Db, snapshot: GovernanceSnapshot) -> Result<(), String> {
apply_batch(db, FINISHED_PROPOSAL_VOTES_TREE, snapshot.proposals)?;
apply_batch(db, FINISHED_ACTIVATION_VOTES_TREE, snapshot.activations)?;
apply_batch(db, CONSENSUS_ACTIVATIONS_TREE, snapshot.schedules)
}
fn validate_no_conflicts(
db: &Db,
tree_name: &'static str,
records: &[(Vec<u8>, Vec<u8>)],
) -> Result<(), String> {
let tree = db
.open_tree(tree_name)
.map_err(|err| format!("Could not open {tree_name} during governance sync: {err}"))?;
for (key, value) in records {
if let Some(existing) = tree
.get(key)
.map_err(|err| format!("Could not read existing {tree_name} record: {err}"))?
{
if existing.as_ref() != value {
return Err(format!(
"Synced governance state conflicts with local {tree_name} state."
));
}
}
}
Ok(())
}
fn apply_batch(
db: &Db,
tree_name: &'static str,
records: Vec<(Vec<u8>, Vec<u8>)>,
) -> Result<(), String> {
let tree = db
.open_tree(tree_name)
.map_err(|err| format!("Could not open {tree_name} during governance sync: {err}"))?;
let mut batch = Batch::default();
for (key, value) in records {
batch.insert(key, value);
}
tree.apply_batch(batch)
.map_err(|err| format!("Could not import {tree_name}: {err}"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::governance::{
encode_consensus_activation, encode_finished_activation_vote, encode_finished_vote,
normalize_development_location,
};
#[test]
fn parser_rejects_partial_governance_record() {
assert!(parse_snapshot(&[FINISHED_PROPOSAL_TAG, 1, 2, 3]).is_err());
}
#[test]
fn finalized_proposal_snapshot_validates_and_imports() {
let db = crate::sled::Config::new().temporary(true).open().unwrap();
let proposal = vec![7; 32];
db.open_tree(PROPOSAL_KEYS_TREE)
.unwrap()
.insert(&proposal, vec![1])
.unwrap();
let snapshot = GovernanceSnapshot {
proposals: vec![(proposal.clone(), encode_finished_vote(9, 1, 10, 0))],
..GovernanceSnapshot::default()
};
validate_snapshot(&db, &snapshot).unwrap();
import_snapshot(&db, snapshot).unwrap();
assert!(db
.open_tree(FINISHED_PROPOSAL_VOTES_TREE)
.unwrap()
.contains_key(proposal)
.unwrap());
}
#[test]
fn conflicting_local_finality_is_rejected_before_import() {
let db = crate::sled::Config::new().temporary(true).open().unwrap();
let proposal = vec![8; 32];
db.open_tree(PROPOSAL_KEYS_TREE)
.unwrap()
.insert(&proposal, vec![1])
.unwrap();
db.open_tree(FINISHED_PROPOSAL_VOTES_TREE)
.unwrap()
.insert(&proposal, encode_finished_vote(17, 3, 20, 0))
.unwrap();
let snapshot = GovernanceSnapshot {
proposals: vec![(proposal, encode_finished_vote(18, 2, 20, 0))],
..GovernanceSnapshot::default()
};
assert!(validate_snapshot(&db, &snapshot)
.unwrap_err()
.contains("conflicts"));
}
#[test]
fn finalized_activation_requires_matching_schedule() {
let db = crate::sled::Config::new().temporary(true).open().unwrap();
let proposal = vec![9; 32];
let development = vec![10; 32];
let development_hash = crate::encode(&development);
let location = normalize_development_location("CLP-1-code.md").unwrap();
db.open_tree(PROPOSAL_KEYS_TREE)
.unwrap()
.insert(&proposal, vec![1])
.unwrap();
let mut candidate = proposal.clone();
candidate.extend_from_slice(&development);
let snapshot = GovernanceSnapshot {
proposals: vec![(proposal.clone(), encode_finished_vote(9, 1, 10, 0))],
activations: vec![(
candidate,
encode_finished_activation_vote(9, 1, 10, 0, 5_760, &location).unwrap(),
)],
schedules: vec![(
proposal,
encode_consensus_activation(&development_hash, 5_760, &location).unwrap(),
)],
};
validate_snapshot(&db, &snapshot).unwrap();
}
}

View File

@ -18,6 +18,7 @@ use crate::records::memory::network_mapping::NodeInfo;
use crate::records::memory::response_channels::Command; use crate::records::memory::response_channels::Command;
use crate::records::memory::structs::Connection; use crate::records::memory::structs::Connection;
use crate::rpc::client::genesis_compat::ensure_compatible_genesis; use crate::rpc::client::genesis_compat::ensure_compatible_genesis;
use crate::rpc::client::governance_state_sync::sync_governance_state;
use crate::rpc::client::handshake::connect_and_handshake; use crate::rpc::client::handshake::connect_and_handshake;
use crate::rpc::client::register_wallet::register_connected_wallet; use crate::rpc::client::register_wallet::register_connected_wallet;
use crate::rpc::client::structs::{Connect, Handshake}; use crate::rpc::client::structs::{Connect, Handshake};
@ -74,6 +75,7 @@ pub async fn bootstrap_peer_discovery(mut params: BootstrapParams) -> Result<(),
let mut current_key = params.connections_key.clone(); let mut current_key = params.connections_key.clone();
let mut stream = params.stream; let mut stream = params.stream;
params.first = false; params.first = false;
let mut imported_chain = false;
let mut candidate_endpoints = NodeInfo::active_node_endpoints().await; let mut candidate_endpoints = NodeInfo::active_node_endpoints().await;
candidate_endpoints.shuffle(&mut thread_rng()); candidate_endpoints.shuffle(&mut thread_rng());
@ -150,6 +152,7 @@ pub async fn bootstrap_peer_discovery(mut params: BootstrapParams) -> Result<(),
let local_genesis_exists = genesis_checkup().await; let local_genesis_exists = genesis_checkup().await;
if !local_genesis_exists || remote_height > local_height + 10 { if !local_genesis_exists || remote_height > local_height + 10 {
imported_chain = true;
info!("[sync] Starting sync from {local_height} to {remote_height}"); info!("[sync] Starting sync from {local_height} to {remote_height}");
node_syncing( node_syncing(
stream.clone(), stream.clone(),
@ -212,6 +215,18 @@ pub async fn bootstrap_peer_discovery(mut params: BootstrapParams) -> Result<(),
} }
} }
if imported_chain {
sync_governance_state(
stream.clone(),
&params.db,
params.map.clone(),
current_key.clone(),
)
.await
.map_err(|err| format!("Governance state sync error: {err}"))?;
info!("[governance] finalized state sync completed: peer={current_key}");
}
info!("[sync] post-sync checks complete, mining grace period started"); info!("[sync] post-sync checks complete, mining grace period started");
for (peer_key, _) in startup_synced_peer_streams().await { for (peer_key, _) in startup_synced_peer_streams().await {
if !mark_peer_operational(&peer_key, params.map.clone()).await { if !mark_peer_operational(&peer_key, params.map.clone()).await {

View File

@ -1,6 +1,7 @@
// The rpc client module contains the standalone client-side handshake and sync helpers. // The rpc client module contains the standalone client-side handshake and sync helpers.
pub mod block_hash_vote; pub mod block_hash_vote;
pub mod genesis_compat; pub mod genesis_compat;
pub mod governance_state_sync;
pub mod handshake; pub mod handshake;
pub mod handshake_message; pub mod handshake_message;
pub mod handshake_processing; pub mod handshake_processing;

View File

@ -7,6 +7,8 @@ use crate::blocks::loan_payment::ContractPaymentTransaction;
use crate::blocks::loans::LoanContractTransaction; use crate::blocks::loans::LoanContractTransaction;
use crate::blocks::marketing::MarketingTransaction; use crate::blocks::marketing::MarketingTransaction;
use crate::blocks::nft::CreateNftTransaction; use crate::blocks::nft::CreateNftTransaction;
use crate::blocks::proposal_key::ProposalKey;
use crate::blocks::proposal_vote::ProposalVote;
use crate::blocks::rewards::RewardsTransaction; use crate::blocks::rewards::RewardsTransaction;
use crate::blocks::storage_key::StorageKey; use crate::blocks::storage_key::StorageKey;
use crate::blocks::storage_values::{ use crate::blocks::storage_values::{
@ -71,6 +73,8 @@ pub const RPC_STORAGE_LOOKUP_COST: u8 = 52;
pub const RPC_STORAGE_LOOKUP: u8 = 53; pub const RPC_STORAGE_LOOKUP: u8 = 53;
pub const RPC_NETWORK_MONITOR_STATE: u8 = 54; pub const RPC_NETWORK_MONITOR_STATE: u8 = 54;
pub const RPC_SETUP_COMPLETE: u8 = 55; pub const RPC_SETUP_COMPLETE: u8 = 55;
pub const RPC_GOVERNANCE_STATE_SYNC: u8 = 56;
pub const RPC_GOVERNANCE_PROPOSAL: u8 = 57;
pub const RPC_REPLY: u8 = 255; pub const RPC_REPLY: u8 = 255;
pub const MAX_RPC_REPLY_BYTES: usize = 64 * 1024 * 1024; pub const MAX_RPC_REPLY_BYTES: usize = 64 * 1024 * 1024;
@ -108,6 +112,10 @@ pub fn get_bytes(tx_type: u8) -> usize {
111 => StorageI64::BYTE_LENGTH, 111 => StorageI64::BYTE_LENGTH,
112 => StorageI128::BYTE_LENGTH, 112 => StorageI128::BYTE_LENGTH,
113 => DeleteKey::BYTE_LENGTH, 113 => DeleteKey::BYTE_LENGTH,
200 => ProposalKey::BYTE_LENGTH,
201 => ProposalVote::BYTE_LENGTH,
202 => ActivationVote::BYTE_LENGTH,
_ => 0, _ => 0,
} }
} }
use crate::blocks::activation_vote::ActivationVote;

View File

@ -129,6 +129,11 @@ async fn miner_earnings_from_transactions(
Transaction::Collateral(tx) => earnings.add_fee(tx.unsigned_collateral_claim.txfee)?, Transaction::Collateral(tx) => earnings.add_fee(tx.unsigned_collateral_claim.txfee)?,
Transaction::Vanity(tx) => earnings.add_fee(tx.unsigned_vanity_address.txfee)?, Transaction::Vanity(tx) => earnings.add_fee(tx.unsigned_vanity_address.txfee)?,
Transaction::StorageKey(tx) => earnings.add_fee(tx.unsigned_storagekey.txfee)?, Transaction::StorageKey(tx) => earnings.add_fee(tx.unsigned_storagekey.txfee)?,
Transaction::ProposalKey(tx) => earnings.add_fee(tx.unsigned_proposalkey.txfee)?,
Transaction::ProposalVote(tx) => earnings.add_fee(tx.unsigned_proposalvote.txfee)?,
Transaction::ActivationVote(tx) => {
earnings.add_fee(tx.unsigned_activationvote.txfee)?
}
Transaction::StorageBool(tx) => earnings.add_fee(tx.unsigned_storagebool.txfee)?, Transaction::StorageBool(tx) => earnings.add_fee(tx.unsigned_storagebool.txfee)?,
Transaction::StorageU8(tx) => earnings.add_fee(tx.unsigned_storageu8.txfee)?, Transaction::StorageU8(tx) => earnings.add_fee(tx.unsigned_storageu8.txfee)?,
Transaction::StorageU16(tx) => earnings.add_fee(tx.unsigned_storageu16.txfee)?, Transaction::StorageU16(tx) => earnings.add_fee(tx.unsigned_storageu16.txfee)?,

View File

@ -0,0 +1,381 @@
use crate::blocks::activation_vote::ActivationVote;
use crate::blocks::proposal_key::ProposalKey;
use crate::blocks::proposal_vote::ProposalVote;
use crate::common::governance::{
countable_governance_addresses, ACTIVATION_CANDIDATE_KEY_BYTES, ACTIVATION_VOTES_TREE,
ACTIVATION_VOTE_RECORD_KEY_BYTES, CONSENSUS_ACTIVATIONS_TREE, FINISHED_ACTIVATION_VOTES_TREE,
FINISHED_PROPOSAL_VOTES_TREE, GOVERNANCE_DOCUMENT_HASH_BYTES, PROPOSAL_KEYS_TREE,
PROPOSAL_VOTES_TREE, PROPOSAL_VOTE_RECORD_KEY_BYTES,
};
use crate::encode;
use crate::records::block_height::get_block_height::get_height;
use crate::records::memory::network_mapping::NodeInfo;
use crate::rpc::responses::RpcResponse;
use crate::sled::Db;
use crate::wallets::structures::Wallet;
use crate::{to_string, Utc};
use serde::Serialize;
use std::collections::{BTreeMap, HashMap, HashSet};
#[derive(Serialize)]
struct GovernanceVoteView {
address: String,
vote: &'static str,
counted: bool,
}
#[derive(Serialize)]
struct CurrentVoteState {
yes: u64,
no: u64,
eligible_nodes: u64,
approval_percent: f64,
votes: Vec<GovernanceVoteView>,
}
#[derive(Serialize)]
struct FinalVoteState {
yes: u64,
no: u64,
eligible_nodes: u64,
approval_percent: f64,
finalized_block: u32,
}
#[derive(Serialize)]
struct ImplementationView {
development_hash: String,
development_location: String,
state: &'static str,
current_vote: CurrentVoteState,
final_vote: Option<FinalVoteState>,
activation_block: Option<u32>,
}
#[derive(Serialize)]
struct ProposalView {
proposal_key: String,
proposal_document_hash: String,
creator: String,
created_timestamp: u32,
state: &'static str,
current_vote: CurrentVoteState,
final_vote: Option<FinalVoteState>,
implementations: Vec<ImplementationView>,
}
struct StoredVote {
address: String,
vote: u8,
}
pub async fn lookup_proposal(db: &Db, proposal_key: Vec<u8>) -> RpcResponse {
match build_proposal_view(db, proposal_key).await {
Ok(view) => match to_string(&view) {
Ok(json) => RpcResponse::Binary(json.into_bytes()),
Err(err) => RpcResponse::Binary(format!(r#"{{"error":"{err}"}}"#).into_bytes()),
},
Err(err) => RpcResponse::Binary(format!(r#"{{"error":"{err}"}}"#).into_bytes()),
}
}
async fn build_proposal_view(db: &Db, proposal_key: Vec<u8>) -> Result<ProposalView, String> {
if proposal_key.len() != GOVERNANCE_DOCUMENT_HASH_BYTES {
return Err("Proposal key must be exactly 32 bytes.".to_string());
}
let proposal_bytes = db
.open_tree(PROPOSAL_KEYS_TREE)
.map_err(|err| format!("Could not open proposal keys: {err}"))?
.get(&proposal_key)
.map_err(|err| format!("Could not read proposal key: {err}"))?
.ok_or_else(|| "Proposal key was not found.".to_string())?;
if proposal_bytes.len() != ProposalKey::BYTE_LENGTH {
return Err("Stored proposal key has an invalid byte count.".to_string());
}
let proposal = ProposalKey::from_bytes(proposal_bytes[0], &proposal_bytes[1..])
.await
.map_err(|err| format!("Could not parse stored proposal key: {err}"))?;
let height = get_height(db);
let reference_time = Utc::now().timestamp_millis().max(0) as u64;
let snapshots = NodeInfo::governance_node_snapshots().await;
let proposal_votes = load_proposal_votes(db, &proposal_key).await?;
let proposal_current = current_vote_state(&snapshots, &proposal_votes, reference_time, height);
let proposal_final = load_finished_vote(db, FINISHED_PROPOSAL_VOTES_TREE, &proposal_key)?;
let proposal_state = if proposal_final.is_some() {
"approved"
} else {
"open"
};
let implementations =
load_implementations(db, &proposal_key, &snapshots, reference_time, height).await?;
Ok(ProposalView {
proposal_key: encode(&proposal_key),
proposal_document_hash: proposal.unsigned_proposalkey.proposal_hash,
creator: proposal.unsigned_proposalkey.address,
created_timestamp: proposal.unsigned_proposalkey.time,
state: proposal_state,
current_vote: proposal_current,
final_vote: proposal_final,
implementations,
})
}
async fn load_proposal_votes(db: &Db, proposal_key: &[u8]) -> Result<Vec<StoredVote>, String> {
let tree = db
.open_tree(PROPOSAL_VOTES_TREE)
.map_err(|err| format!("Could not open proposal votes: {err}"))?;
let mut votes = Vec::new();
for entry in tree.scan_prefix(proposal_key) {
let (key, value) = entry.map_err(|err| format!("Could not read proposal vote: {err}"))?;
if key.len() != PROPOSAL_VOTE_RECORD_KEY_BYTES || value.len() != ProposalVote::BYTE_LENGTH {
return Err("Stored proposal vote has an invalid byte count.".to_string());
}
let address = Wallet::bytes_to_short_address(&key[GOVERNANCE_DOCUMENT_HASH_BYTES..])
.ok_or_else(|| "Stored proposal voter address is invalid.".to_string())?;
let vote = value[ProposalVote::VOTE_BYTE_OFFSET];
if vote > 1 {
return Err("Stored proposal vote value is invalid.".to_string());
}
votes.push(StoredVote { address, vote });
}
votes.sort_by(|left, right| left.address.cmp(&right.address));
Ok(votes)
}
async fn load_implementations(
db: &Db,
proposal_key: &[u8],
snapshots: &[crate::common::governance::GovernanceNodeSnapshot],
reference_time: u64,
height: u32,
) -> Result<Vec<ImplementationView>, String> {
let tree = db
.open_tree(ACTIVATION_VOTES_TREE)
.map_err(|err| format!("Could not open activation votes: {err}"))?;
let mut candidates: BTreeMap<Vec<u8>, Vec<(Vec<u8>, Vec<u8>)>> = BTreeMap::new();
for entry in tree.scan_prefix(proposal_key) {
let (key, value) = entry.map_err(|err| format!("Could not read activation vote: {err}"))?;
if key.len() != ACTIVATION_VOTE_RECORD_KEY_BYTES
|| value.len() != ActivationVote::BYTE_LENGTH
{
return Err("Stored activation vote has an invalid byte count.".to_string());
}
candidates
.entry(key[..ACTIVATION_CANDIDATE_KEY_BYTES].to_vec())
.or_default()
.push((key.to_vec(), value.to_vec()));
}
let finished_tree = db
.open_tree(FINISHED_ACTIVATION_VOTES_TREE)
.map_err(|err| format!("Could not open finished activation votes: {err}"))?;
for entry in finished_tree.scan_prefix(proposal_key) {
let (candidate, _) =
entry.map_err(|err| format!("Could not read finished activation vote: {err}"))?;
candidates.entry(candidate.to_vec()).or_default();
}
let schedule = db
.open_tree(CONSENSUS_ACTIVATIONS_TREE)
.map_err(|err| format!("Could not open activation schedules: {err}"))?
.get(proposal_key)
.map_err(|err| format!("Could not read activation schedule: {err}"))?
.map(|value| value.to_vec());
let mut views = Vec::new();
for (candidate_key, records) in candidates {
let development_hash = encode(&candidate_key[GOVERNANCE_DOCUMENT_HASH_BYTES..]);
let mut stored_votes = Vec::new();
let mut development_location = String::new();
for (key, value) in records {
let transaction = ActivationVote::from_bytes(value[0], &value[1..])
.await
.map_err(|err| format!("Could not parse stored activation vote: {err}"))?;
if development_location.is_empty() {
development_location = transaction
.unsigned_activationvote
.development_location
.trim_end()
.to_string();
}
let address = Wallet::bytes_to_short_address(
&key[ACTIVATION_CANDIDATE_KEY_BYTES..ACTIVATION_VOTE_RECORD_KEY_BYTES],
)
.ok_or_else(|| "Stored activation voter address is invalid.".to_string())?;
stored_votes.push(StoredVote {
address,
vote: transaction.unsigned_activationvote.vote,
});
}
stored_votes.sort_by(|left, right| left.address.cmp(&right.address));
let current = current_vote_state(snapshots, &stored_votes, reference_time, height);
let final_vote = load_finished_vote(db, FINISHED_ACTIVATION_VOTES_TREE, &candidate_key)?;
let activation_block = final_vote.as_ref().and_then(|_| {
schedule.as_ref().and_then(|value| {
if value.len() == 136 && value[..32] == candidate_key[32..64] {
Some(u32::from_le_bytes(value[32..36].try_into().ok()?))
} else {
None
}
})
});
let state = match activation_block {
Some(block) if height >= block => "active",
Some(_) => "scheduled",
None => "open",
};
views.push(ImplementationView {
development_hash,
development_location,
state,
current_vote: current,
final_vote,
activation_block,
});
}
Ok(views)
}
fn current_vote_state(
snapshots: &[crate::common::governance::GovernanceNodeSnapshot],
votes: &[StoredVote],
reference_time: u64,
height: u32,
) -> CurrentVoteState {
let existing: HashSet<String> = votes.iter().map(|vote| vote.address.clone()).collect();
let countable = countable_governance_addresses(snapshots, &existing, reference_time, height);
let votes_by_address: HashMap<&str, u8> = votes
.iter()
.map(|vote| (vote.address.as_str(), vote.vote))
.collect();
let yes = countable
.iter()
.filter(|address| votes_by_address.get(address.as_str()) == Some(&1))
.count() as u64;
let no = countable
.iter()
.filter(|address| votes_by_address.get(address.as_str()) == Some(&0))
.count() as u64;
let eligible_nodes = countable.len() as u64;
CurrentVoteState {
yes,
no,
eligible_nodes,
approval_percent: percentage(yes, eligible_nodes),
votes: votes
.iter()
.map(|vote| GovernanceVoteView {
address: vote.address.clone(),
vote: if vote.vote == 1 { "yes" } else { "no" },
counted: countable.contains(&vote.address),
})
.collect(),
}
}
fn load_finished_vote(
db: &Db,
tree_name: &'static str,
key: &[u8],
) -> Result<Option<FinalVoteState>, String> {
let Some(value) = db
.open_tree(tree_name)
.map_err(|err| format!("Could not open {tree_name}: {err}"))?
.get(key)
.map_err(|err| format!("Could not read {tree_name}: {err}"))?
else {
return Ok(None);
};
if value.len() < 28 {
return Err(format!("Stored {tree_name} value is malformed."));
}
let yes = read_u64(&value, 0)?;
let no = read_u64(&value, 8)?;
let eligible_nodes = read_u64(&value, 16)?;
Ok(Some(FinalVoteState {
yes,
no,
eligible_nodes,
approval_percent: percentage(yes, eligible_nodes),
finalized_block: read_u32(&value, 24)?,
}))
}
fn percentage(yes: u64, eligible: u64) -> f64 {
if eligible == 0 {
0.0
} else {
yes as f64 * 100.0 / eligible as f64
}
}
fn read_u64(value: &[u8], offset: usize) -> Result<u64, String> {
Ok(u64::from_le_bytes(
value
.get(offset..offset + 8)
.ok_or_else(|| "Stored governance value is too short.".to_string())?
.try_into()
.unwrap(),
))
}
fn read_u32(value: &[u8], offset: usize) -> Result<u32, String> {
Ok(u32::from_le_bytes(
value
.get(offset..offset + 4)
.ok_or_else(|| "Stored governance value is too short.".to_string())?
.try_into()
.unwrap(),
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::blocks::proposal_key::UnsignedProposalKey;
async fn store_proposal(db: &Db, key: &[u8]) {
let address = format!("{:040x}.cltc", 1);
let unsigned =
UnsignedProposalKey::new(200, 123, &crate::encode(vec![4; 32]), &address, 100_000_000)
.await;
let proposal = ProposalKey::load(unsigned, &"00".repeat(Wallet::SIGNATURE_LENGTH)).await;
db.open_tree(PROPOSAL_KEYS_TREE)
.unwrap()
.insert(key, proposal.to_bytes().await.unwrap())
.unwrap();
}
#[tokio::test]
async fn lookup_returns_proposal_metadata_and_final_result() {
let db = crate::sled::Config::new().temporary(true).open().unwrap();
let proposal_key = vec![3; 32];
store_proposal(&db, &proposal_key).await;
db.open_tree(FINISHED_PROPOSAL_VOTES_TREE)
.unwrap()
.insert(
&proposal_key,
crate::common::governance::encode_finished_vote(9, 1, 10, 50),
)
.unwrap();
let view = build_proposal_view(&db, proposal_key).await.unwrap();
assert_eq!(view.state, "approved");
assert_eq!(view.created_timestamp, 123);
let final_vote = view.final_vote.unwrap();
assert_eq!(final_vote.yes, 9);
assert_eq!(final_vote.eligible_nodes, 10);
assert_eq!(final_vote.finalized_block, 50);
}
#[tokio::test]
async fn missing_proposal_returns_json_error() {
let db = crate::sled::Config::new().temporary(true).open().unwrap();
let RpcResponse::Binary(response) = lookup_proposal(&db, vec![7; 32]).await;
let json: serde_json::Value = serde_json::from_slice(&response).unwrap();
assert_eq!(json["error"], "Proposal key was not found.");
}
}

View File

@ -0,0 +1,31 @@
use crate::common::governance::{
CONSENSUS_ACTIVATIONS_TREE, FINISHED_ACTIVATION_VOTES_TREE, FINISHED_PROPOSAL_VOTES_TREE,
};
use crate::rpc::responses::RpcResponse;
use crate::sled::Db;
pub const FINISHED_PROPOSAL_TAG: u8 = 0;
pub const FINISHED_ACTIVATION_TAG: u8 = 1;
pub const CONSENSUS_ACTIVATION_TAG: u8 = 2;
pub async fn request_governance_state(db: &Db) -> RpcResponse {
let mut response = Vec::new();
for (tree_name, tag) in [
(FINISHED_PROPOSAL_VOTES_TREE, FINISHED_PROPOSAL_TAG),
(FINISHED_ACTIVATION_VOTES_TREE, FINISHED_ACTIVATION_TAG),
(CONSENSUS_ACTIVATIONS_TREE, CONSENSUS_ACTIVATION_TAG),
] {
let Ok(tree) = db.open_tree(tree_name) else {
return RpcResponse::Binary(Vec::new());
};
for entry in tree.iter() {
let Ok((key, value)) = entry else {
return RpcResponse::Binary(Vec::new());
};
response.push(tag);
response.extend_from_slice(&key);
response.extend_from_slice(&value);
}
}
RpcResponse::Binary(response)
}

View File

@ -14,6 +14,8 @@ pub mod block_height;
pub mod block_peer_ip; pub mod block_peer_ip;
pub mod contract; pub mod contract;
pub mod difficulty; pub mod difficulty;
pub mod governance_proposal;
pub mod governance_state_sync;
pub mod largest_tx_fee; pub mod largest_tx_fee;
pub mod latest_block; pub mod latest_block;
pub mod marketing_campaign_history; pub mod marketing_campaign_history;

View File

@ -8,12 +8,13 @@ pub async fn get_nfts(db: &Db) -> RpcResponse {
// series number for the compact NFT-list RPC response format. // series number for the compact NFT-list RPC response format.
let tree = db.open_tree("nfts").unwrap(); let tree = db.open_tree("nfts").unwrap();
let origin_tree = db.open_tree("nft_origins").unwrap(); let origin_tree = db.open_tree("nft_origins").unwrap();
let supply_tree = db.open_tree("nft_supply").unwrap();
let mut serialized_nfts = Vec::new(); let mut serialized_nfts = Vec::new();
for result in tree.iter() { for result in tree.iter() {
match result { match result {
Ok((key, _value)) => { Ok((key, value)) => {
let raw_key = String::from_utf8_lossy(&key).to_string(); let raw_key = String::from_utf8_lossy(&key).to_string();
let (nft_name, nft_series) = nft_asset_parts(&raw_key); let (nft_name, nft_series) = nft_asset_parts(&raw_key);
// NFT origins are kept separately so list responses can // NFT origins are kept separately so list responses can
@ -36,6 +37,15 @@ pub async fn get_nfts(db: &Db) -> RpcResponse {
if hash_bytes.len() != 32 { if hash_bytes.len() != 32 {
continue; continue;
} }
let Some(ownership_type) = value.first().copied() else {
continue;
};
let Some(supply_bytes) = supply_tree.get(&key).ok().flatten() else {
continue;
};
if supply_bytes.len() != 8 {
continue;
}
let mut padded_key = [32_u8; 15]; let mut padded_key = [32_u8; 15];
let nft_name_bytes = nft_name.as_bytes(); let nft_name_bytes = nft_name.as_bytes();
@ -47,6 +57,8 @@ pub async fn get_nfts(db: &Db) -> RpcResponse {
serialized_nfts.extend_from_slice(&hash_bytes); serialized_nfts.extend_from_slice(&hash_bytes);
serialized_nfts.extend_from_slice(&padded_key); serialized_nfts.extend_from_slice(&padded_key);
serialized_nfts.extend_from_slice(&nft_series.to_le_bytes()); serialized_nfts.extend_from_slice(&nft_series.to_le_bytes());
serialized_nfts.push(ownership_type);
serialized_nfts.extend_from_slice(&supply_bytes);
} }
Err(_) => continue, Err(_) => continue,
} }

View File

@ -6,6 +6,8 @@ use crate::blocks::loan_payment::ContractPaymentTransaction;
use crate::blocks::loans::LoanContractTransaction; use crate::blocks::loans::LoanContractTransaction;
use crate::blocks::marketing::MarketingTransaction; use crate::blocks::marketing::MarketingTransaction;
use crate::blocks::nft::CreateNftTransaction; use crate::blocks::nft::CreateNftTransaction;
use crate::blocks::proposal_key::ProposalKey;
use crate::blocks::proposal_vote::ProposalVote;
use crate::blocks::storage_key::StorageKey; use crate::blocks::storage_key::StorageKey;
use crate::blocks::storage_values::{ use crate::blocks::storage_values::{
StorageBool, StorageI128, StorageI16, StorageI32, StorageI64, StorageI8, StorageString, StorageBool, StorageI128, StorageI16, StorageI32, StorageI64, StorageI8, StorageString,
@ -16,11 +18,12 @@ use crate::blocks::token::CreateTokenTransaction;
use crate::blocks::transfer::TransferTransaction; use crate::blocks::transfer::TransferTransaction;
use crate::blocks::vanity::VanityAddressTransaction; use crate::blocks::vanity::VanityAddressTransaction;
use crate::common::types::{ use crate::common::types::{
BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE, DELETE_KEY_TYPE, ACTIVATION_VOTE_TYPE, BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE,
ISSUE_TOKEN_TYPE, LENDER_TYPE, MARKETING_TYPE, STORAGE_BOOL_TYPE, STORAGE_I128_TYPE, CREATE_TOKEN_TYPE, DELETE_KEY_TYPE, ISSUE_TOKEN_TYPE, LENDER_TYPE, MARKETING_TYPE,
STORAGE_I16_TYPE, STORAGE_I32_TYPE, STORAGE_I64_TYPE, STORAGE_I8_TYPE, STORAGE_KEY_TYPE, PROPOSAL_KEY_TYPE, PROPOSAL_VOTE_TYPE, STORAGE_BOOL_TYPE, STORAGE_I128_TYPE, STORAGE_I16_TYPE,
STORAGE_STRING_TYPE, STORAGE_U128_TYPE, STORAGE_U16_TYPE, STORAGE_U32_TYPE, STORAGE_U64_TYPE, STORAGE_I32_TYPE, STORAGE_I64_TYPE, STORAGE_I8_TYPE, STORAGE_KEY_TYPE, STORAGE_STRING_TYPE,
STORAGE_U8_TYPE, SWAP_TYPE, TRANSFER_TYPE, VANITY_ADDRESS_TYPE, STORAGE_U128_TYPE, STORAGE_U16_TYPE, STORAGE_U32_TYPE, STORAGE_U64_TYPE, STORAGE_U8_TYPE,
SWAP_TYPE, TRANSFER_TYPE, VANITY_ADDRESS_TYPE,
}; };
use crate::encode; use crate::encode;
use crate::records::memory::connections::get_client_type_from_memory; use crate::records::memory::connections::get_client_type_from_memory;
@ -638,6 +641,9 @@ pub async fn save_and_submit(
STORAGE_I64_TYPE => submit_storage_value!(StorageI64, txtype, tx, db, map), STORAGE_I64_TYPE => submit_storage_value!(StorageI64, txtype, tx, db, map),
STORAGE_I128_TYPE => submit_storage_value!(StorageI128, txtype, tx, db, map), STORAGE_I128_TYPE => submit_storage_value!(StorageI128, txtype, tx, db, map),
DELETE_KEY_TYPE => submit_storage_value!(DeleteKey, txtype, tx, db, map), DELETE_KEY_TYPE => submit_storage_value!(DeleteKey, txtype, tx, db, map),
PROPOSAL_KEY_TYPE => submit_storage_value!(ProposalKey, txtype, tx, db, map),
PROPOSAL_VOTE_TYPE => submit_storage_value!(ProposalVote, txtype, tx, db, map),
ACTIVATION_VOTE_TYPE => submit_storage_value!(ActivationVote, txtype, tx, db, map),
_ => { _ => {
let msg = "error: No such transaction type" let msg = "error: No such transaction type"
.to_string() .to_string()
@ -647,3 +653,4 @@ pub async fn save_and_submit(
} }
} }
} }
use crate::blocks::activation_vote::ActivationVote;

View File

@ -13,6 +13,7 @@ use crate::records::memory::response_channels::generate_uid;
use crate::records::memory::response_channels::Command; use crate::records::memory::response_channels::Command;
use crate::records::memory::structs::Connection; use crate::records::memory::structs::Connection;
use crate::rpc::client::genesis_compat::ensure_compatible_genesis; use crate::rpc::client::genesis_compat::ensure_compatible_genesis;
use crate::rpc::client::governance_state_sync::sync_governance_state;
use crate::rpc::client::register_wallet::register_connected_wallet; use crate::rpc::client::register_wallet::register_connected_wallet;
use crate::rpc::client::syncing::node_syncing; use crate::rpc::client::syncing::node_syncing;
use crate::rpc::client::wallet_registry_sync::sync_wallet_registry_with_retries; use crate::rpc::client::wallet_registry_sync::sync_wallet_registry_with_retries;
@ -86,6 +87,8 @@ async fn sync_incoming_peer_before_operational(
) )
.await?; .await?;
let initial_local_genesis_exists = genesis_checkup().await; let initial_local_genesis_exists = genesis_checkup().await;
let should_import_governance =
!initial_local_genesis_exists || initial_remote_height > initial_local_height + 10;
if initial_local_genesis_exists && initial_remote_height < initial_local_height { if initial_local_genesis_exists && initial_remote_height < initial_local_height {
warn!( warn!(
@ -175,6 +178,12 @@ async fn sync_incoming_peer_before_operational(
.map_err(|err| format!("Incoming post-sync orphan check error: {err}"))?; .map_err(|err| format!("Incoming post-sync orphan check error: {err}"))?;
} }
if should_import_governance {
sync_governance_state(stream, db, map, connections_key.to_string())
.await
.map_err(|err| format!("Incoming governance state sync error: {err}"))?;
}
Ok((true, Some(chain_sync_guard))) Ok((true, Some(chain_sync_guard)))
} }

View File

@ -802,6 +802,39 @@ pub async fn start_loop(
.send(&stream_locked, Some(&connections_key), uid) .send(&stream_locked, Some(&connections_key), uid)
.await; .await;
} }
56 => {
// Return finalized governance decisions and activation schedules.
let (uid, _) = read_bytes_from_stream::read_uid_from_stream(
&connections_key,
stream_locked.clone(),
)
.await?;
let result = commands::governance_state_sync::request_governance_state(&db).await;
result
.send(&stream_locked, Some(&connections_key), uid)
.await;
}
57 => {
// Look up the complete voting and activation state for a proposal.
let (uid, _) = read_bytes_from_stream::read_uid_from_stream(
&connections_key,
stream_locked.clone(),
)
.await?;
let proposal_key = read_bytes_from_stream::read_usize_from_stream(
&connections_key,
32,
stream_locked.clone(),
)
.await?;
let result =
commands::governance_proposal::lookup_proposal(&db, proposal_key).await;
result
.send(&stream_locked, Some(&connections_key), uid)
.await;
}
40 => { 40 => {
// request the vanity address registered to a canonical wallet // request the vanity address registered to a canonical wallet
let (uid, _) = read_bytes_from_stream::read_uid_from_stream( let (uid, _) = read_bytes_from_stream::read_uid_from_stream(

View File

@ -6,14 +6,14 @@ use crate::records::memory::response_channels::Byte3;
use crate::rpc::command_maps::{ use crate::rpc::command_maps::{
MAX_RPC_REPLY_BYTES, RPC_ADDRESS_HISTORY, RPC_ADDRESS_TOTAL_BALANCE, RPC_BLOCK_BY_HASH, MAX_RPC_REPLY_BYTES, RPC_ADDRESS_HISTORY, RPC_ADDRESS_TOTAL_BALANCE, RPC_BLOCK_BY_HASH,
RPC_BLOCK_BY_HEIGHT, RPC_BLOCK_HEIGHT, RPC_BLOCK_IP, RPC_COLLATERAL_STATUS, RPC_BLOCK_BY_HEIGHT, RPC_BLOCK_HEIGHT, RPC_BLOCK_IP, RPC_COLLATERAL_STATUS,
RPC_CONTRACT_BY_ADDRESS, RPC_DIFFICULTY, RPC_LARGEST_TX_FEE, RPC_LATEST_ADDRESS_TRANSACTIONS, RPC_CONTRACT_BY_ADDRESS, RPC_DIFFICULTY, RPC_GOVERNANCE_PROPOSAL, RPC_LARGEST_TX_FEE,
RPC_LOAN_CONTRACT, RPC_MARKETING_CAMPAIGN_HISTORY, RPC_MEMPOOL_TX_BY_ADDRESS, RPC_LATEST_ADDRESS_TRANSACTIONS, RPC_LOAN_CONTRACT, RPC_MARKETING_CAMPAIGN_HISTORY,
RPC_MEMPOOL_TX_BY_SIGNATURE, RPC_MEMPOOL_TX_COUNT, RPC_NETWORK_INFO, RPC_NFT_DETAILS, RPC_MEMPOOL_TX_BY_ADDRESS, RPC_MEMPOOL_TX_BY_SIGNATURE, RPC_MEMPOOL_TX_COUNT, RPC_NETWORK_INFO,
RPC_NFT_LIST, RPC_REGISTER_WALLET, RPC_STORAGE_LOOKUP, RPC_STORAGE_LOOKUP_COST, RPC_TIME, RPC_NFT_DETAILS, RPC_NFT_LIST, RPC_REGISTER_WALLET, RPC_STORAGE_LOOKUP,
RPC_TOKEN_CATALOG, RPC_TOKEN_DETAILS, RPC_TOKEN_LIST, RPC_TORRENT_BY_HEIGHT, RPC_STORAGE_LOOKUP_COST, RPC_TIME, RPC_TOKEN_CATALOG, RPC_TOKEN_DETAILS, RPC_TOKEN_LIST,
RPC_TOTAL_CONFIRMED_TX, RPC_TRANSACTION_BY_TXID, RPC_UNBLOCK_IP, RPC_VALIDATE_MESSAGE, RPC_TORRENT_BY_HEIGHT, RPC_TOTAL_CONFIRMED_TX, RPC_TRANSACTION_BY_TXID, RPC_UNBLOCK_IP,
RPC_VANITY_LOOKUP, RPC_VANITY_OWNER_LOOKUP, RPC_WALLET_REGISTRATION_STATUS, RPC_VALIDATE_MESSAGE, RPC_VANITY_LOOKUP, RPC_VANITY_OWNER_LOOKUP,
RPC_WALLET_REGISTRY_SYNC, RPC_WALLET_REGISTRATION_STATUS, RPC_WALLET_REGISTRY_SYNC,
}; };
use crate::standalone_tools::transaction_creator::create_transaction_request; use crate::standalone_tools::transaction_creator::create_transaction_request;
use crate::standalone_tools::vanity_resolver::canonical_vanity_address_input; use crate::standalone_tools::vanity_resolver::canonical_vanity_address_input;
@ -771,6 +771,25 @@ async fn build_request_bytes(
bin_msg.extend_from_slice(&address_bytes); bin_msg.extend_from_slice(&address_bytes);
bin_msg.extend_from_slice(&transfer_tx); bin_msg.extend_from_slice(&transfer_tx);
} }
// Look up complete governance state by its 32-byte proposal key.
57 => {
let command_number: u8 = RPC_GOVERNANCE_PROPOSAL;
let proposal_key = decode(&command_input).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("Invalid proposal key: {err}"),
)
})?;
if proposal_key.len() != 32 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Proposal key must decode to 32 bytes",
));
}
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
bin_msg.extend_from_slice(&proposal_key);
}
_ => { _ => {
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::InvalidInput, io::ErrorKind::InvalidInput,
@ -785,7 +804,7 @@ async fn build_request_bytes(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::build_request_bytes; use super::build_request_bytes;
use crate::rpc::command_maps::RPC_VALIDATE_MESSAGE; use crate::rpc::command_maps::{RPC_GOVERNANCE_PROPOSAL, RPC_VALIDATE_MESSAGE};
use crate::wallets::structures::Wallet; use crate::wallets::structures::Wallet;
#[tokio::test] #[tokio::test]
@ -807,4 +826,21 @@ mod tests {
assert_eq!(&bytes[6..13], b"message"); assert_eq!(&bytes[6..13], b"message");
assert_eq!(bytes.len(), 1 + 3 + 2 + 7 + 22 + Wallet::SIGNATURE_LENGTH); assert_eq!(bytes.len(), 1 + 3 + 2 + 7 + 22 + Wallet::SIGNATURE_LENGTH);
} }
#[tokio::test]
async fn governance_lookup_request_uses_proposal_key_bytes() {
let uid = [4, 5, 6];
let proposal_key = vec![9; 32];
let bytes = build_request_bytes(
crate::encode(&proposal_key),
RPC_GOVERNANCE_PROPOSAL as usize,
uid,
)
.await
.unwrap();
assert_eq!(bytes[0], RPC_GOVERNANCE_PROPOSAL);
assert_eq!(&bytes[1..4], &uid);
assert_eq!(&bytes[4..], proposal_key);
}
} }

View File

@ -0,0 +1,86 @@
use crate::common::cli_prompts::{
prompt_hidden_nonempty, prompt_visible_with_default, prompt_wallet_path,
};
use crate::common::governance::is_valid_governance_hash;
use crate::wallets::structures::Wallet;
use crate::{create_dir_all, AsyncWriteExt, File};
use serde_json::Value;
pub fn display_fee(value: u64) -> f64 {
value as f64 / 100_000_000.0
}
pub fn parse_fee(value: &str, minimum: u64) -> Result<u64, String> {
let decimal = value
.trim()
.parse::<f64>()
.map_err(|_| "Please enter a valid transaction fee.".to_string())?;
if !decimal.is_finite() || decimal.is_sign_negative() {
return Err("Please enter a valid transaction fee.".to_string());
}
let atomic = (decimal * 100_000_000.0).round() as u64;
if atomic < minimum {
return Err(format!(
"Transaction fee must be at least {:.8}.",
display_fee(minimum)
));
}
Ok(atomic)
}
pub async fn prompt_fee(label: &str, minimum: u64) -> u64 {
let default = format!("{:.8}", display_fee(minimum));
loop {
let value =
prompt_visible_with_default(&format!("Please enter the {label} fee"), &default).await;
match parse_fee(&value, minimum) {
Ok(fee) => return fee,
Err(err) => println!("{err}"),
}
}
}
pub fn validate_hash(value: &str, label: &str) -> Result<String, String> {
let value = value.trim();
if !is_valid_governance_hash(value) {
return Err(format!("{label} must be 64 hexadecimal characters."));
}
Ok(value.to_ascii_lowercase())
}
pub fn parse_vote(value: &str) -> Result<u8, String> {
match value.trim().to_ascii_lowercase().as_str() {
"yes" | "y" | "1" => Ok(1),
"no" | "n" | "0" => Ok(0),
_ => Err("Vote must be yes or no.".to_string()),
}
}
pub async fn load_wallet() -> Result<Wallet, String> {
let wallet_path = prompt_wallet_path().await;
let key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
Wallet::try_obtain_wallet(key, Some(&wallet_path))
.await
.map_err(|err| format!("Wallet decryption failed: {err}"))
}
pub async fn write_transaction(hash: &str, output: &Value) -> Result<(), String> {
let output = serde_json::to_string_pretty(output)
.map_err(|err| format!("Could not serialize transaction JSON: {err}"))?;
create_dir_all("./transactions")
.await
.map_err(|err| format!("Could not create transactions directory: {err}"))?;
let path = format!("./transactions/{hash}.json");
let mut file = File::create(&path)
.await
.map_err(|err| format!("Could not create {path}: {err}"))?;
file.write_all(output.as_bytes())
.await
.map_err(|err| format!("Could not write {path}: {err}"))?;
println!("{output}");
Ok(())
}

View File

@ -1,5 +1,6 @@
// The standalone_tools module holds utility helpers that mirror node serialization and connection behavior. // The standalone_tools module holds utility helpers that mirror node serialization and connection behavior.
pub mod connections; pub mod connections;
pub mod governance;
pub mod loan_lookup_json; pub mod loan_lookup_json;
pub mod transaction_creator; pub mod transaction_creator;
pub mod vanity_resolver; pub mod vanity_resolver;

View File

@ -1,3 +1,4 @@
use crate::blocks::activation_vote::{ActivationVote, UnsignedActivationVote};
use crate::blocks::burn::{BurnTransaction, UnsignedBurnTransaction}; use crate::blocks::burn::{BurnTransaction, UnsignedBurnTransaction};
use crate::blocks::collateral::{CollateralClaimTransaction, UnsignedCollateralClaimTransaction}; use crate::blocks::collateral::{CollateralClaimTransaction, UnsignedCollateralClaimTransaction};
use crate::blocks::delete_key::{DeleteKey, UnsignedDeleteKey}; use crate::blocks::delete_key::{DeleteKey, UnsignedDeleteKey};
@ -6,6 +7,8 @@ use crate::blocks::loan_payment::{ContractPaymentTransaction, UnsignedContractPa
use crate::blocks::loans::{LoanContractTransaction, UnsignedLoanContractTransaction}; use crate::blocks::loans::{LoanContractTransaction, UnsignedLoanContractTransaction};
use crate::blocks::marketing::{MarketingTransaction, UnsignedMarketingTransaction}; use crate::blocks::marketing::{MarketingTransaction, UnsignedMarketingTransaction};
use crate::blocks::nft::{CreateNftTransaction, UnsignedCreateNftTransaction}; use crate::blocks::nft::{CreateNftTransaction, UnsignedCreateNftTransaction};
use crate::blocks::proposal_key::{ProposalKey, UnsignedProposalKey};
use crate::blocks::proposal_vote::{ProposalVote, UnsignedProposalVote};
use crate::blocks::storage_key::{StorageKey, UnsignedStorageKey}; use crate::blocks::storage_key::{StorageKey, UnsignedStorageKey};
use crate::blocks::storage_values::{ use crate::blocks::storage_values::{
StorageBool, StorageI128, StorageI16, StorageI32, StorageI64, StorageI8, StorageString, StorageBool, StorageI128, StorageI16, StorageI32, StorageI64, StorageI8, StorageString,
@ -19,9 +22,10 @@ use crate::blocks::token::{CreateTokenTransaction, UnsignedCreateTokenTransactio
use crate::blocks::transfer::{TransferTransaction, UnsignedTransferTransaction}; use crate::blocks::transfer::{TransferTransaction, UnsignedTransferTransaction};
use crate::blocks::vanity::{UnsignedVanityAddressTransaction, VanityAddressTransaction}; use crate::blocks::vanity::{UnsignedVanityAddressTransaction, VanityAddressTransaction};
use crate::common::types::{ use crate::common::types::{
DELETE_KEY_TYPE, STORAGE_BOOL_TYPE, STORAGE_I128_TYPE, STORAGE_I16_TYPE, STORAGE_I32_TYPE, ACTIVATION_VOTE_TYPE, DELETE_KEY_TYPE, PROPOSAL_KEY_TYPE, PROPOSAL_VOTE_TYPE,
STORAGE_I64_TYPE, STORAGE_I8_TYPE, STORAGE_KEY_TYPE, STORAGE_STRING_TYPE, STORAGE_U128_TYPE, STORAGE_BOOL_TYPE, STORAGE_I128_TYPE, STORAGE_I16_TYPE, STORAGE_I32_TYPE, STORAGE_I64_TYPE,
STORAGE_U16_TYPE, STORAGE_U32_TYPE, STORAGE_U64_TYPE, STORAGE_U8_TYPE, STORAGE_I8_TYPE, STORAGE_KEY_TYPE, STORAGE_STRING_TYPE, STORAGE_U128_TYPE, STORAGE_U16_TYPE,
STORAGE_U32_TYPE, STORAGE_U64_TYPE, STORAGE_U8_TYPE,
}; };
use crate::records::memory::response_channels::Byte3; use crate::records::memory::response_channels::Byte3;
use crate::rpc::command_maps::RPC_SUBMIT_TRANSACTION; use crate::rpc::command_maps::RPC_SUBMIT_TRANSACTION;
@ -238,6 +242,9 @@ async fn create_transaction_bytes(transaction: &Value) -> Result<Vec<u8>, String
.as_str() .as_str()
.ok_or("Missing creator wallet address.")?, .ok_or("Missing creator wallet address.")?,
transaction["series"].as_u64().ok_or("Missing series.")? as u8, transaction["series"].as_u64().ok_or("Missing series.")? as u8,
transaction["ownership_type"]
.as_u64()
.ok_or("Missing ownership_type.")? as u8,
transaction["nft_name"] transaction["nft_name"]
.as_str() .as_str()
.ok_or("Missing or invalid NFT name.")?, .ok_or("Missing or invalid NFT name.")?,
@ -1042,8 +1049,164 @@ async fn create_transaction_bytes(transaction: &Value) -> Result<Vec<u8>, String
.await .await
.map_err(|e| format!("Failed to serialize delete-key transaction: {e}"))? .map_err(|e| format!("Failed to serialize delete-key transaction: {e}"))?
} }
PROPOSAL_KEY_TYPE => {
let unsigned = UnsignedProposalKey::new(
txtype,
read_u32_field(transaction, "timestamp", "time").ok_or("Missing timestamp.")?,
transaction["proposal_hash"]
.as_str()
.ok_or("Missing proposal document hash.")?,
transaction["address"]
.as_str()
.ok_or("Missing proposal creator wallet address.")?,
transaction["txfee"]
.as_u64()
.ok_or("Missing or invalid txfee.")?,
)
.await;
ProposalKey::load(
unsigned,
transaction["signature"]
.as_str()
.ok_or("Missing transaction signature.")?,
)
.await
.to_bytes()
.await
.map_err(|e| format!("Failed to serialize proposal-key transaction: {e}"))?
}
PROPOSAL_VOTE_TYPE => {
let unsigned = UnsignedProposalVote::new(
txtype,
read_u32_field(transaction, "timestamp", "time").ok_or("Missing timestamp.")?,
transaction["proposal_key"]
.as_str()
.ok_or("Missing proposal key.")?,
transaction["address"]
.as_str()
.ok_or("Missing proposal voter wallet address.")?,
transaction["vote"]
.as_u64()
.ok_or("Missing or invalid proposal vote.")? as u8,
transaction["txfee"]
.as_u64()
.ok_or("Missing or invalid txfee.")?,
)
.await;
ProposalVote::load(
unsigned,
transaction["signature"]
.as_str()
.ok_or("Missing transaction signature.")?,
)
.await
.to_bytes()
.await
.map_err(|e| format!("Failed to serialize proposal-vote transaction: {e}"))?
}
ACTIVATION_VOTE_TYPE => {
let unsigned = UnsignedActivationVote::new(
txtype,
read_u32_field(transaction, "timestamp", "time").ok_or("Missing timestamp.")?,
transaction["proposal_key"]
.as_str()
.ok_or("Missing proposal key.")?,
transaction["development_hash"]
.as_str()
.ok_or("Missing implementation document hash.")?,
transaction["development_location"]
.as_str()
.ok_or("Missing implementation document location.")?,
transaction["address"]
.as_str()
.ok_or("Missing activation voter wallet address.")?,
transaction["vote"]
.as_u64()
.ok_or("Missing or invalid activation vote.")? as u8,
transaction["txfee"]
.as_u64()
.ok_or("Missing or invalid txfee.")?,
)
.await;
ActivationVote::load(
unsigned,
transaction["signature"]
.as_str()
.ok_or("Missing transaction signature.")?,
)
.await
.to_bytes()
.await
.map_err(|e| format!("Failed to serialize activation-vote transaction: {e}"))?
}
_ => return Err("No such transaction type".to_string()), _ => return Err("No such transaction type".to_string()),
}; };
Ok(transaction_bytes) Ok(transaction_bytes)
} }
#[cfg(test)]
mod tests {
use super::create_transaction_bytes;
use crate::blocks::activation_vote::ActivationVote;
use crate::blocks::proposal_key::ProposalKey;
use crate::blocks::proposal_vote::ProposalVote;
use crate::common::governance::normalize_development_location;
use crate::json;
use crate::wallets::structures::Wallet;
fn address() -> String {
format!("{:040x}.cltc", 1)
}
#[tokio::test]
async fn governance_transaction_json_rebuilds_fixed_wire_bytes() {
let signature = "00".repeat(Wallet::SIGNATURE_LENGTH);
let proposal_hash = crate::encode(vec![2; 32]);
let proposal_key = crate::encode(vec![3; 32]);
let development_hash = crate::encode(vec![4; 32]);
let location = normalize_development_location("CLP-1-code.md").unwrap();
let proposal = create_transaction_bytes(&json!({
"txtype": 200,
"timestamp": 10,
"proposal_hash": proposal_hash,
"address": address(),
"txfee": 100_000_000_u64,
"signature": signature,
}))
.await
.unwrap();
let proposal_vote = create_transaction_bytes(&json!({
"txtype": 201,
"timestamp": 11,
"proposal_key": proposal_key,
"address": address(),
"vote": 1,
"txfee": 100_000_000_u64,
"signature": "00".repeat(Wallet::SIGNATURE_LENGTH),
}))
.await
.unwrap();
let activation_vote = create_transaction_bytes(&json!({
"txtype": 202,
"timestamp": 12,
"proposal_key": crate::encode(vec![3; 32]),
"development_hash": development_hash,
"development_location": location,
"address": address(),
"vote": 0,
"txfee": 100_000_000_u64,
"signature": "00".repeat(Wallet::SIGNATURE_LENGTH),
}))
.await
.unwrap();
assert_eq!(proposal.len(), ProposalKey::BYTE_LENGTH);
assert_eq!(proposal_vote.len(), ProposalVote::BYTE_LENGTH);
assert_eq!(activation_vote.len(), ActivationVote::BYTE_LENGTH);
assert_eq!(proposal[0], 200);
assert_eq!(proposal_vote[0], 201);
assert_eq!(activation_vote[0], 202);
}
}

View File

@ -438,6 +438,48 @@ async fn transaction_balance_view(
check_saved_txid: true, check_saved_txid: true,
}) })
} }
Transaction::ProposalKey(tx) => {
let proposal = &tx.unsigned_proposalkey;
let mut debits = Vec::new();
add_debit(
&mut debits,
&proposal.address,
BASECOIN.clone(),
proposal.txfee,
);
Ok(TransactionBalanceView {
hash: proposal.hash().await,
signatures: vec![tx.signature.clone()],
debits,
check_saved_txid: true,
})
}
Transaction::ProposalVote(tx) => {
let vote = &tx.unsigned_proposalvote;
let mut debits = Vec::new();
add_debit(&mut debits, &vote.address, BASECOIN.clone(), vote.txfee);
Ok(TransactionBalanceView {
hash: vote.hash().await,
signatures: vec![tx.signature.clone()],
debits,
check_saved_txid: true,
})
}
Transaction::ActivationVote(tx) => {
let vote = &tx.unsigned_activationvote;
let mut debits = Vec::new();
add_debit(&mut debits, &vote.address, BASECOIN.clone(), vote.txfee);
Ok(TransactionBalanceView {
hash: vote.hash().await,
signatures: vec![tx.signature.clone()],
debits,
check_saved_txid: true,
})
}
Transaction::StorageBool(tx) => { Transaction::StorageBool(tx) => {
let storage = &tx.unsigned_storagebool; let storage = &tx.unsigned_storagebool;
let mut debits = Vec::new(); let mut debits = Vec::new();

View File

@ -5,6 +5,7 @@ pub mod checks;
pub mod total_payments; pub mod total_payments;
pub mod transactions; pub mod transactions;
pub mod validate_torrent_data; pub mod validate_torrent_data;
pub mod verify_activation_vote;
pub mod verify_block; pub mod verify_block;
pub mod verify_borrower; pub mod verify_borrower;
pub mod verify_burn; pub mod verify_burn;
@ -16,6 +17,8 @@ pub mod verify_genesis;
pub mod verify_issue_token; pub mod verify_issue_token;
pub mod verify_lender; pub mod verify_lender;
pub mod verify_marketing; pub mod verify_marketing;
pub mod verify_proposal_key;
pub mod verify_proposal_vote;
pub mod verify_rewards; pub mod verify_rewards;
pub mod verify_storage_key; pub mod verify_storage_key;
pub mod verify_storage_values; pub mod verify_storage_values;

View File

@ -313,6 +313,62 @@ async fn verify_transaction(
} }
} }
} }
if let Transaction::ProposalKey(proposal_key_tx) = &transaction {
match proposal_key_tx.verify(db).await {
Ok(value) => {
reserve_verified_transaction(db, &transaction, balance_tracker, already_in_mempool)
.await?;
return Ok(value);
}
Err(err) => {
return Err(format!(
"Validation failed for proposal key transaction: {err}"
));
}
}
}
if let Transaction::ProposalVote(proposal_vote_tx) = &transaction {
match proposal_vote_tx
.verify_at(
db,
block_timestamp,
crate::records::block_height::get_block_height::get_height(db).saturating_add(1),
)
.await
{
Ok(value) => {
reserve_verified_transaction(db, &transaction, balance_tracker, already_in_mempool)
.await?;
return Ok(value);
}
Err(err) => {
return Err(format!(
"Validation failed for proposal vote transaction: {err}"
));
}
}
}
if let Transaction::ActivationVote(activation_vote_tx) = &transaction {
match activation_vote_tx
.verify_at(
db,
block_timestamp,
crate::records::block_height::get_block_height::get_height(db).saturating_add(1),
)
.await
{
Ok(value) => {
reserve_verified_transaction(db, &transaction, balance_tracker, already_in_mempool)
.await?;
return Ok(value);
}
Err(err) => {
return Err(format!(
"Validation failed for activation vote transaction: {err}"
));
}
}
}
if let Transaction::StorageBool(storage_tx) = &transaction { if let Transaction::StorageBool(storage_tx) = &transaction {
match storage_tx.verify(db).await { match storage_tx.verify(db).await {
Ok(value) => { Ok(value) => {

View File

@ -0,0 +1,142 @@
use crate::blocks::activation_vote::ActivationVote;
use crate::common::governance::{
activation_candidate_key, activation_vote_record_key, is_valid_governance_hash,
is_valid_padded_development_location, GovernanceVote, ACTIVATION_VOTES_TREE,
CONSENSUS_ACTIVATIONS_TREE, FINISHED_ACTIVATION_VOTES_TREE, FINISHED_PROPOSAL_VOTES_TREE,
};
use crate::common::types::{ACTIVATION_VOTE_FEE, ACTIVATION_VOTE_TYPE};
use crate::encode;
use crate::records::block_height::get_block_height::get_height;
use crate::records::memory::mempool::{activation_vote_exists, BASECOIN};
use crate::records::memory::network_mapping::NodeInfo;
use crate::records::wallet_registry::{
require_canonical_registered_short_address, resolve_pubkey_from_short_address,
};
use crate::sled::Db;
use crate::verifications::async_funcs::checks::balance_check::balance_checkup;
use crate::verifications::async_funcs::checks::verify_db::db_hex_verification;
use crate::wallets::structures::Wallet;
impl ActivationVote {
pub async fn verify(&self, db: &Db) -> Result<String, String> {
self.verify_at(
db,
crate::Utc::now().timestamp() as u32,
get_height(db).saturating_add(1),
)
.await
}
pub async fn verify_at(
&self,
db: &Db,
reference_timestamp: u32,
vote_height: u32,
) -> Result<String, String> {
let vote = &self.unsigned_activationvote;
if vote.txtype != ACTIVATION_VOTE_TYPE {
return Err("Activation vote transaction type is invalid.".to_string());
}
GovernanceVote::try_from(vote.vote)?;
if !is_valid_governance_hash(&vote.proposal_key) {
return Err("Proposal key must decode to exactly 32 bytes.".to_string());
}
if !is_valid_governance_hash(&vote.development_hash) {
return Err("Development document hash must decode to exactly 32 bytes.".to_string());
}
if !is_valid_padded_development_location(&vote.development_location) {
return Err(
"Development location must be a padded, non-empty 100-byte ASCII value."
.to_string(),
);
}
if !Wallet::short_address_validation(&vote.address) {
return Err("Activation voter wallet address is invalid.".to_string());
}
require_canonical_registered_short_address(
db,
&vote.address,
"Activation voter wallet address",
)?;
let public_key = resolve_pubkey_from_short_address(db, &vote.address)
.map_err(|_| "Activation voter wallet address is not registered.".to_string())?
.ok_or_else(|| "Activation voter wallet address is not registered.".to_string())?;
let transaction_hash = vote.hash().await;
if !Wallet::verify_transaction_with_public_key(
&transaction_hash,
&self.signature,
&encode(&public_key),
)
.await
{
return Err("Invalid signature for the Activation Vote Transaction.".to_string());
}
let proposal_key = crate::decode(&vote.proposal_key)
.map_err(|err| format!("Could not decode proposal key: {err}"))?;
if !db
.open_tree(FINISHED_PROPOSAL_VOTES_TREE)
.map_err(|err| format!("Could not open approved proposals: {err}"))?
.contains_key(&proposal_key)
.map_err(|err| format!("Could not check proposal approval: {err}"))?
{
return Err("The proposal has not been approved.".to_string());
}
if db
.open_tree(CONSENSUS_ACTIVATIONS_TREE)
.map_err(|err| format!("Could not open consensus activations: {err}"))?
.contains_key(&proposal_key)
.map_err(|err| format!("Could not check existing activation: {err}"))?
{
return Err("This proposal already has a scheduled implementation.".to_string());
}
let candidate = activation_candidate_key(&vote.proposal_key, &vote.development_hash)?;
if db
.open_tree(FINISHED_ACTIVATION_VOTES_TREE)
.map_err(|err| format!("Could not open finished activation votes: {err}"))?
.contains_key(&candidate)
.map_err(|err| format!("Could not check activation finalization: {err}"))?
{
return Err("This implementation vote is already finalized.".to_string());
}
let vote_key =
activation_vote_record_key(&vote.proposal_key, &vote.development_hash, &vote.address)?;
if db
.open_tree(ACTIVATION_VOTES_TREE)
.map_err(|err| format!("Could not open activation votes: {err}"))?
.contains_key(vote_key)
.map_err(|err| format!("Could not check existing activation vote: {err}"))?
|| activation_vote_exists(&vote.proposal_key, &vote.development_hash, &vote.address)
.await
.map_err(|err| err.to_string())?
{
return Err("This node wallet has already voted on this implementation.".to_string());
}
let voter = NodeInfo::governance_node_snapshot(&vote.address)
.await
.ok_or_else(|| "Activation votes may only be cast by network nodes.".to_string())?;
if !voter.can_cast_vote_at((reference_timestamp as u64) * 1_000, vote_height) {
return Err(
"Activation voter must be active for 60 minutes and have mined 100 blocks."
.to_string(),
);
}
if vote.txfee < ACTIVATION_VOTE_FEE {
return Err(format!(
"Activation vote transaction fee is below the minimum required fee of {ACTIVATION_VOTE_FEE}."
));
}
if !balance_checkup(db, 0, vote.txfee, BASECOIN.clone(), &vote.address).await {
return Err("Insufficient funds for this Activation Vote Transaction!".to_string());
}
if !db_hex_verification(db, "txid", &transaction_hash).await {
return Err("This transaction already exists.".to_string());
}
Ok(String::new())
}
}

View File

@ -1,6 +1,6 @@
use crate::blocks::burn::BurnTransaction; use crate::blocks::burn::BurnTransaction;
use crate::common::asset_names::is_canonical_padded_asset_identifier; use crate::common::asset_names::is_canonical_padded_asset_identifier;
use crate::common::nft_assets::nft_asset_name; use crate::common::nft_assets::{nft_asset_name, nft_ownership_type, validate_nft_amount};
use crate::common::types::{BURN_FEE, COIN_LENGTH}; use crate::common::types::{BURN_FEE, COIN_LENGTH};
use crate::encode; use crate::encode;
use crate::records::memory::mempool::BASECOIN; use crate::records::memory::mempool::BASECOIN;
@ -14,8 +14,6 @@ use crate::verifications::async_funcs::checks::verify_db::{
}; };
use crate::wallets::structures::Wallet; use crate::wallets::structures::Wallet;
const NFT_UNIT: u64 = 100_000_000;
impl BurnTransaction { impl BurnTransaction {
pub async fn verify(&self, db: &Db) -> Result<String, String> { pub async fn verify(&self, db: &Db) -> Result<String, String> {
let hash = self.unsigned_burn.hash().await; let hash = self.unsigned_burn.hash().await;
@ -55,7 +53,8 @@ impl BurnTransaction {
} }
let burn_asset = nft_asset_name(&self.unsigned_burn.coin, self.unsigned_burn.nft_series); let burn_asset = nft_asset_name(&self.unsigned_burn.coin, self.unsigned_burn.nft_series);
let nft_exists = db_bytes_verification(db, "nfts", &burn_asset).await; let nft_ownership = nft_ownership_type(db, &burn_asset)?;
let nft_exists = nft_ownership.is_some();
let token_exists = self.unsigned_burn.nft_series == 0 let token_exists = self.unsigned_burn.nft_series == 0
&& db_bytes_verification(db, "tokens", &self.unsigned_burn.coin).await; && db_bytes_verification(db, "tokens", &self.unsigned_burn.coin).await;
@ -64,12 +63,10 @@ impl BurnTransaction {
return Err("This asset does not exist.".to_string()); return Err("This asset does not exist.".to_string());
} }
// Live NFTs use a full NFT unit balance, while fungible tokens can // Indivisible assets burn as one complete unit. Fractional assets
// burn any positive integer amount. // may burn any positive ownership amount up to one complete unit.
if nft_exists { if let Some(ownership_type) = nft_ownership {
if self.unsigned_burn.value != NFT_UNIT { validate_nft_amount(ownership_type, self.unsigned_burn.value, "burn")?;
return Err(format!("NFT burns must destroy exactly {NFT_UNIT} units."));
}
} else if self.unsigned_burn.value == 0 { } else if self.unsigned_burn.value == 0 {
return Err("Burn value must be greater than 0.".to_string()); return Err("Burn value must be greater than 0.".to_string());
} }

View File

@ -1,6 +1,8 @@
use crate::blocks::nft::CreateNftTransaction; use crate::blocks::nft::CreateNftTransaction;
use crate::common::asset_names::is_canonical_padded_asset_identifier; use crate::common::asset_names::is_canonical_padded_asset_identifier;
use crate::common::nft_assets::nft_asset_name; use crate::common::nft_assets::{
nft_asset_name, NFT_OWNERSHIP_FRACTIONAL, NFT_OWNERSHIP_INDIVISIBLE,
};
use crate::common::types::{COIN_LENGTH, CREATE_NFT_FEE}; use crate::common::types::{COIN_LENGTH, CREATE_NFT_FEE};
use crate::encode; use crate::encode;
use crate::records::memory::mempool::BASECOIN; use crate::records::memory::mempool::BASECOIN;
@ -75,9 +77,24 @@ impl CreateNftTransaction {
return Err("A series must contain at least 2 items").map_err(|s| s.to_string())?; return Err("A series must contain at least 2 items").map_err(|s| s.to_string())?;
} }
// Standalone NFTs and RWAs may be indivisible or fractional.
// Numbered collection items are always indivisible.
let ownership_type = self.unsigned_create_nft.ownership_type;
if ownership_type != NFT_OWNERSHIP_INDIVISIBLE && ownership_type != NFT_OWNERSHIP_FRACTIONAL
{
return Err("Ownership type must be 0 for indivisible or 1 for fractional.")
.map_err(|s| s.to_string())?;
}
if series == 1 && ownership_type != NFT_OWNERSHIP_INDIVISIBLE {
return Err("NFT/RWA collections must use indivisible ownership.".to_string());
}
// Series NFTs reserve each numbered item name separately, while // Series NFTs reserve each numbered item name separately, while
// standalone NFTs reserve the base name directly. // standalone NFTs reserve the base name directly.
let tree = db.open_tree("nfts").unwrap(); // NFT identities remain reserved after their circulating supply is
// completely burned. Use the permanent ownership registry here
// rather than the live NFT registry, which removes fully burned assets.
let tree = db.open_tree("nft_ownership").unwrap();
if series == 1 { if series == 1 {
for item_number in 1..=count { for item_number in 1..=count {
let nft_save_name = nft_asset_name(&self.unsigned_create_nft.nft_name, item_number); let nft_save_name = nft_asset_name(&self.unsigned_create_nft.nft_name, item_number);

View File

@ -3,6 +3,7 @@ use crate::common::asset_names::{
is_canonical_loan_collateral_or_base, is_canonical_padded_asset_or_base, is_canonical_loan_collateral_or_base, is_canonical_padded_asset_or_base,
loan_collateral_asset_name_or_base, LOAN_COLLATERAL_LENGTH, loan_collateral_asset_name_or_base, LOAN_COLLATERAL_LENGTH,
}; };
use crate::common::nft_assets::{nft_ownership_type, validate_nft_amount};
use crate::common::types::{COIN_LENGTH, LENDER_FEE}; use crate::common::types::{COIN_LENGTH, LENDER_FEE};
use crate::encode; use crate::encode;
use crate::records::memory::mempool::BASECOIN; use crate::records::memory::mempool::BASECOIN;
@ -117,6 +118,13 @@ impl LoanContractTransaction {
return Err("Coin/Token/NFT does not exist.".to_string()); return Err("Coin/Token/NFT does not exist.".to_string());
} }
// NFT/RWA collateral follows the immutable ownership rule selected
// at creation. Fractional standalone assets may pledge a fraction;
// indivisible assets and collection items must pledge one full unit.
if let Some(ownership_type) = nft_ownership_type(db, &collateral)? {
validate_nft_amount(ownership_type, collateral_amount, "collateral")?;
}
// The lender must be able to fund the loan and fee, while the // The lender must be able to fund the loan and fee, while the
// borrower must already own the pledged collateral. // borrower must already own the pledged collateral.
if !balance_checkup(db, loan_amount, txfee, loan_coin, lender).await { if !balance_checkup(db, loan_amount, txfee, loan_coin, lender).await {

View File

@ -0,0 +1,70 @@
use crate::blocks::proposal_key::ProposalKey;
use crate::common::governance::is_valid_governance_hash;
use crate::common::types::{PROPOSAL_KEY_FEE, PROPOSAL_KEY_TYPE};
use crate::encode;
use crate::records::memory::mempool::BASECOIN;
use crate::records::wallet_registry::{
require_canonical_registered_short_address, resolve_pubkey_from_short_address,
};
use crate::sled::Db;
use crate::verifications::async_funcs::checks::balance_check::balance_checkup;
use crate::verifications::async_funcs::checks::verify_db::db_hex_verification;
use crate::wallets::structures::Wallet;
impl ProposalKey {
pub async fn verify(&self, db: &Db) -> Result<String, String> {
let proposal = &self.unsigned_proposalkey;
// Governance proposal keys use their reserved transaction type.
if proposal.txtype != PROPOSAL_KEY_TYPE {
return Err("Proposal key transaction type is invalid.".to_string());
}
// This hash commits the key to the exact bytes of the proposal document.
if !is_valid_governance_hash(&proposal.proposal_hash) {
return Err("Proposal document hash must decode to exactly 32 bytes.".to_string());
}
// Proposal creators must use a canonical registered wallet address.
if !Wallet::short_address_validation(&proposal.address) {
return Err("Proposal creator wallet address is invalid.".to_string());
}
require_canonical_registered_short_address(
db,
&proposal.address,
"Proposal creator wallet address",
)?;
// The registered public key proves who created this proposal key.
let public_key = resolve_pubkey_from_short_address(db, &proposal.address)
.map_err(|_| "Proposal creator wallet address is not registered.".to_string())?
.ok_or_else(|| "Proposal creator wallet address is not registered.".to_string())?;
let transaction_hash = proposal.hash().await;
if !Wallet::verify_transaction_with_public_key(
&transaction_hash,
&self.signature,
&encode(&public_key),
)
.await
{
return Err("Invalid signature for the Proposal Key Transaction.".to_string());
}
if proposal.txfee < PROPOSAL_KEY_FEE {
return Err(format!(
"Proposal key transaction fee is below the minimum required fee of {PROPOSAL_KEY_FEE}."
));
}
if !balance_checkup(db, 0, proposal.txfee, BASECOIN.clone(), &proposal.address).await {
return Err("Insufficient funds for this Proposal Key Transaction!".to_string());
}
// A confirmed transaction ID cannot be accepted into the chain twice.
if !db_hex_verification(db, "txid", &transaction_hash).await {
return Err("This transaction already exists.".to_string());
}
Ok(String::new())
}
}

View File

@ -0,0 +1,125 @@
use crate::blocks::proposal_vote::ProposalVote;
use crate::common::governance::{
is_valid_governance_hash, proposal_vote_record_key, GovernanceVote,
FINISHED_PROPOSAL_VOTES_TREE, PROPOSAL_KEYS_TREE, PROPOSAL_VOTES_TREE,
};
use crate::common::types::{PROPOSAL_VOTE_FEE, PROPOSAL_VOTE_TYPE};
use crate::encode;
use crate::records::block_height::get_block_height::get_height;
use crate::records::memory::mempool::{proposal_vote_exists, BASECOIN};
use crate::records::memory::network_mapping::NodeInfo;
use crate::records::wallet_registry::{
require_canonical_registered_short_address, resolve_pubkey_from_short_address,
};
use crate::sled::Db;
use crate::verifications::async_funcs::checks::balance_check::balance_checkup;
use crate::verifications::async_funcs::checks::verify_db::db_hex_verification;
use crate::wallets::structures::Wallet;
impl ProposalVote {
pub async fn verify(&self, db: &Db) -> Result<String, String> {
self.verify_at(
db,
crate::Utc::now().timestamp() as u32,
get_height(db).saturating_add(1),
)
.await
}
pub async fn verify_at(
&self,
db: &Db,
reference_timestamp: u32,
vote_height: u32,
) -> Result<String, String> {
let vote = &self.unsigned_proposalvote;
if vote.txtype != PROPOSAL_VOTE_TYPE {
return Err("Proposal vote transaction type is invalid.".to_string());
}
GovernanceVote::try_from(vote.vote)?;
if !is_valid_governance_hash(&vote.proposal_key) {
return Err("Proposal key must decode to exactly 32 bytes.".to_string());
}
if !Wallet::short_address_validation(&vote.address) {
return Err("Proposal voter wallet address is invalid.".to_string());
}
require_canonical_registered_short_address(
db,
&vote.address,
"Proposal voter wallet address",
)?;
let public_key = resolve_pubkey_from_short_address(db, &vote.address)
.map_err(|_| "Proposal voter wallet address is not registered.".to_string())?
.ok_or_else(|| "Proposal voter wallet address is not registered.".to_string())?;
let transaction_hash = vote.hash().await;
if !Wallet::verify_transaction_with_public_key(
&transaction_hash,
&self.signature,
&encode(&public_key),
)
.await
{
return Err("Invalid signature for the Proposal Vote Transaction.".to_string());
}
let proposal_key = crate::decode(&vote.proposal_key)
.map_err(|err| format!("Could not decode proposal key: {err}"))?;
let proposals = db
.open_tree(PROPOSAL_KEYS_TREE)
.map_err(|err| format!("Could not open proposal keys: {err}"))?;
if !proposals
.contains_key(&proposal_key)
.map_err(|err| format!("Could not check proposal key: {err}"))?
{
return Err("Proposal key does not exist.".to_string());
}
if db
.open_tree(FINISHED_PROPOSAL_VOTES_TREE)
.map_err(|err| format!("Could not open finished proposal votes: {err}"))?
.contains_key(&proposal_key)
.map_err(|err| format!("Could not check proposal finalization: {err}"))?
{
return Err("This proposal vote is already finalized.".to_string());
}
let vote_key = proposal_vote_record_key(&vote.proposal_key, &vote.address)?;
if db
.open_tree(PROPOSAL_VOTES_TREE)
.map_err(|err| format!("Could not open proposal votes: {err}"))?
.contains_key(vote_key)
.map_err(|err| format!("Could not check existing proposal vote: {err}"))?
|| proposal_vote_exists(&vote.proposal_key, &vote.address)
.await
.map_err(|err| err.to_string())?
{
return Err("This node wallet has already voted on this proposal.".to_string());
}
let voter = NodeInfo::governance_node_snapshot(&vote.address)
.await
.ok_or_else(|| "Proposal votes may only be cast by network nodes.".to_string())?;
if !voter.can_cast_vote_at((reference_timestamp as u64) * 1_000, vote_height) {
return Err(
"Proposal voter must be active for 60 minutes and have mined 100 blocks."
.to_string(),
);
}
if vote.txfee < PROPOSAL_VOTE_FEE {
return Err(format!(
"Proposal vote transaction fee is below the minimum required fee of {PROPOSAL_VOTE_FEE}."
));
}
if !balance_checkup(db, 0, vote.txfee, BASECOIN.clone(), &vote.address).await {
return Err("Insufficient funds for this Proposal Vote Transaction!".to_string());
}
if !db_hex_verification(db, "txid", &transaction_hash).await {
return Err("This transaction already exists.".to_string());
}
Ok(String::new())
}
}

View File

@ -1,7 +1,7 @@
use crate::blocks::swap::SwapTransaction; use crate::blocks::swap::SwapTransaction;
use crate::common::asset_names::is_canonical_padded_asset_or_base; use crate::common::asset_names::is_canonical_padded_asset_or_base;
use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::common::network_paths_and_settings::block_extension_and_paths;
use crate::common::nft_assets::nft_asset_name; use crate::common::nft_assets::{nft_asset_name, nft_ownership_type, validate_nft_amount};
use crate::common::types::{COIN_LENGTH, SWAP_FEE}; use crate::common::types::{COIN_LENGTH, SWAP_FEE};
use crate::encode; use crate::encode;
use crate::records::wallet_registry::{ use crate::records::wallet_registry::{
@ -109,44 +109,39 @@ impl SwapTransaction {
// mainnet and testnet validate against their own ticker. // mainnet and testnet validate against their own ticker.
let network_base_coin = network_base_coin_padded.trim().to_lowercase(); let network_base_coin = network_base_coin_padded.trim().to_lowercase();
// Numbered NFT swaps are single-item transfers, while non-series // Resolve NFT/RWA ownership rules from the concrete asset record.
// assets must already exist as a token, NFT, or base coin. let ticker1_ownership = nft_ownership_type(db, &asset1)?;
let ticker1_is_nft = if self.unsigned_swap.nft_series1 > 0 { let ticker1_is_nft = if let Some(ownership_type) = ticker1_ownership {
if self.unsigned_swap.value1 != 1 { validate_nft_amount(ownership_type, self.unsigned_swap.value1, "swap")?;
return Err("Series NFTs must swap exactly 1 item.".to_string());
}
if !db_bytes_verification(db, "nfts", &asset1).await {
return Err("Ticker1 NFT item does not exist.".to_string());
}
true true
} else { } else {
if self.unsigned_swap.nft_series1 > 0 {
return Err("Ticker1 NFT/RWA collection item does not exist.".to_string());
}
let is_base_coin = let is_base_coin =
self.unsigned_swap.ticker1.trim().to_lowercase() == network_base_coin; self.unsigned_swap.ticker1.trim().to_lowercase() == network_base_coin;
let is_token = db_bytes_verification(db, "tokens", &self.unsigned_swap.ticker1).await; let is_token = db_bytes_verification(db, "tokens", &self.unsigned_swap.ticker1).await;
let is_nft = db_bytes_verification(db, "nfts", &self.unsigned_swap.ticker1).await; if !(is_base_coin || is_token) {
if !(is_base_coin || is_token || is_nft) {
return Err("Ticker1 does not exist.".to_string()); return Err("Ticker1 does not exist.".to_string());
} }
is_nft false
}; };
let ticker2_is_nft = if self.unsigned_swap.nft_series2 > 0 { let ticker2_ownership = nft_ownership_type(db, &asset2)?;
if self.unsigned_swap.value2 != 1 { let ticker2_is_nft = if let Some(ownership_type) = ticker2_ownership {
return Err("Series NFTs must swap exactly 1 item.".to_string()); validate_nft_amount(ownership_type, self.unsigned_swap.value2, "swap")?;
}
if !db_bytes_verification(db, "nfts", &asset2).await {
return Err("Ticker2 NFT item does not exist.".to_string());
}
true true
} else { } else {
if self.unsigned_swap.nft_series2 > 0 {
return Err("Ticker2 NFT/RWA collection item does not exist.".to_string());
}
let is_base_coin = let is_base_coin =
self.unsigned_swap.ticker2.trim().to_lowercase() == network_base_coin; self.unsigned_swap.ticker2.trim().to_lowercase() == network_base_coin;
let is_token = db_bytes_verification(db, "tokens", &self.unsigned_swap.ticker2).await; let is_token = db_bytes_verification(db, "tokens", &self.unsigned_swap.ticker2).await;
let is_nft = db_bytes_verification(db, "nfts", &self.unsigned_swap.ticker2).await; if !(is_base_coin || is_token) {
if !(is_base_coin || is_token || is_nft) {
return Err("Ticker2 does not exist.".to_string()); return Err("Ticker2 does not exist.".to_string());
} }
is_nft false
}; };
// Each signer pays the fixed swap fee for their side of the trade. // Each signer pays the fixed swap fee for their side of the trade.

View File

@ -1,6 +1,6 @@
use crate::blocks::transfer::TransferTransaction; use crate::blocks::transfer::TransferTransaction;
use crate::common::asset_names::is_canonical_padded_asset_or_base; use crate::common::asset_names::is_canonical_padded_asset_or_base;
use crate::common::nft_assets::nft_asset_name; use crate::common::nft_assets::{nft_asset_name, nft_ownership_type, validate_nft_amount};
use crate::common::types::{minimum_transfer_fee, COIN_LENGTH}; use crate::common::types::{minimum_transfer_fee, COIN_LENGTH};
use crate::encode; use crate::encode;
use crate::records::memory::mempool::BASECOIN; use crate::records::memory::mempool::BASECOIN;
@ -10,9 +10,7 @@ use crate::records::wallet_registry::{
}; };
use crate::sled::Db; use crate::sled::Db;
use crate::verifications::async_funcs::checks::balance_check::balance_checkup; use crate::verifications::async_funcs::checks::balance_check::balance_checkup;
use crate::verifications::async_funcs::checks::verify_db::{ use crate::verifications::async_funcs::checks::verify_db::db_hex_verification;
db_bytes_verification, db_hex_verification,
};
use crate::wallets::structures::Wallet; use crate::wallets::structures::Wallet;
fn is_zero_burn_address(address: &str) -> bool { fn is_zero_burn_address(address: &str) -> bool {
@ -102,16 +100,12 @@ impl TransferTransaction {
)); ));
} }
// Series NFTs are handled as numbered single-item assets and must // NFT/RWA divisibility comes from the immutable asset registry,
// already exist in the NFT registry before they can be transferred. // including standalone assets whose series number is zero.
if self.unsigned_transfer.nft_series > 0 { if let Some(ownership_type) = nft_ownership_type(db, &transfer_asset)? {
if self.unsigned_transfer.value != 1 { validate_nft_amount(ownership_type, self.unsigned_transfer.value, "transfer")?;
return Err("Series NFTs must transfer exactly 1 item.".to_string()); } else if self.unsigned_transfer.nft_series > 0 {
} return Err("This NFT/RWA collection item does not exist.".to_string());
if !db_bytes_verification(db, "nfts", &transfer_asset).await {
return Err("This NFT item does not exist.".to_string());
}
} }
// Balance checks combine confirmed wallet state with pending // Balance checks combine confirmed wallet state with pending