Skip to content
3 changes: 3 additions & 0 deletions crates/tron-actuator/src/freeze_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ pub fn execute_freeze_balance_v2(
let mut account = accounts
.get(&owner)?
.ok_or(ActuatorError::OwnerAccountMissing)?;
if dyn_props.get_long(b"ALLOW_NEW_RESOURCE_MODEL").unwrap_or(0) == 1 {
tron_tvm::votes::initialize_old_tron_power(&mut account);
}
account.balance = check_sub(account.balance, contract.frozen_balance)?;

// Capture the old weight basis BEFORE mutating frozen_v2 — java-tron's
Expand Down
66 changes: 66 additions & 0 deletions crates/tron-actuator/tests/legacy_voting_power.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use std::sync::Arc;
use tron_actuator::freeze_v2::{execute_freeze_balance_v2, validate_freeze_balance_v2};
use tron_chainbase::{AccountStore, DynamicPropertiesStore, MemBackend};
use tron_crypto::address::Address;
use tron_proto::account::FreezeV2;
use tron_proto::{Account, FreezeBalanceV2Contract};

#[test]
fn native_first_v2_stake_preserves_only_existing_legacy_power() {
for (new_model, legacy, expected_old) in [(1, 0, -1), (1, 7_000_000, 7_000_000), (0, 0, 0)] {
let accounts = AccountStore::new(Arc::new(MemBackend::new()));
let dp = DynamicPropertiesStore::new(Arc::new(MemBackend::new()));
dp.put_long(b"ALLOW_NEW_RESOURCE_MODEL", new_model);
dp.put_long(b"UNFREEZE_DELAY_DAYS", 1);
let mut bytes = [0x44; 21];
bytes[0] = 0x41;
let owner = Address::from_raw(bytes);
accounts
.put(
&owner,
&Account {
address: bytes.to_vec(),
balance: 1_000_000_000,
frozen_v2: if legacy == 0 {
vec![]
} else {
vec![FreezeV2 {
r#type: 1,
amount: legacy,
}]
},
..Default::default()
},
)
.unwrap();
for _ in 0..2 {
let stake = FreezeBalanceV2Contract {
owner_address: bytes.to_vec(),
frozen_balance: 2_000_000,
resource: 1,
};
validate_freeze_balance_v2(&accounts, &dp, &stake).unwrap();
execute_freeze_balance_v2(&accounts, &dp, &stake).unwrap();
let account = accounts.get(&owner).unwrap().unwrap();
assert_eq!(account.old_tron_power, expected_old);
if new_model == 1 {
assert_eq!(tron_tvm::votes::all_tron_power(&account), legacy);
}
}
if new_model == 1 {
let stake = FreezeBalanceV2Contract {
owner_address: bytes.to_vec(),
frozen_balance: 500_000_000,
resource: 2,
};
validate_freeze_balance_v2(&accounts, &dp, &stake).unwrap();
execute_freeze_balance_v2(&accounts, &dp, &stake).unwrap();
let account = accounts.get(&owner).unwrap().unwrap();
assert_eq!(account.old_tron_power, expected_old);
assert_eq!(
tron_tvm::votes::all_tron_power(&account),
legacy + 500_000_000
);
}
}
}
57 changes: 49 additions & 8 deletions crates/tron-index/src/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,15 +244,15 @@ pub fn count_block_txs_raw(mut buf: &[u8]) -> usize {
count
}

/// Created-contract address: `0x41 ‖ keccak256(owner ‖ tx_id)[12..]` —
/// java-tron's `Hash.sha3omit12(owner || tx_id)`, the same derivation
/// Created-contract address: `0x41 ‖ keccak256(tx_id ‖ owner)[12..]` —
/// java-tron's `Hash.sha3omit12(tx_id || owner)`, the same derivation
/// `tron-tvm`'s CREATE path uses. Public so every consumer of the rule
/// (the index extractor, the apply hook's TransactionInfo builder)
/// shares ONE copy of this consensus-critical formula.
pub fn created_contract_address(owner: &[u8], tx_id: &[u8; 32]) -> Addr {
let mut input = Vec::with_capacity(owner.len() + 32);
input.extend_from_slice(owner);
input.extend_from_slice(tx_id);
input.extend_from_slice(owner);
let h = tron_crypto::hash::keccak256(&input);
let mut a = [0u8; 21];
a[0] = 0x41;
Expand Down Expand Up @@ -522,6 +522,23 @@ impl<'a> TxInfoMatcher<'a> {
}
}

/// The block's contractRet is the advertised execution result. Locally
/// produced blocks can omit it; then use the committed execution receipt,
/// rather than labelling every such transaction as failed.
pub fn transaction_succeeded(tx: &Transaction, info: Option<&tron_proto::TransactionInfo>) -> bool {
use tron_proto::transaction::result::ContractResult;
if let Some(result) = tx.ret.first() {
return result.contract_ret == ContractResult::Success as i32;
}
info.map_or(false, |i| {
i.result == tron_proto::transaction_info::Code::Sucess as i32
&& i.receipt.as_ref().map_or(true, |r| {
r.result == ContractResult::Default as i32
|| r.result == ContractResult::Success as i32
})
})
}

/// Extract every index entry one block contributes.
///
/// `txinfo` is the block's stored `TransactionRet` (absent ⇒ the
Expand Down Expand Up @@ -562,11 +579,7 @@ pub fn extract_block(
let tx_id = matcher
.positional_id(txidx as usize)
.unwrap_or_else(|| tron_crypto::hash::sha256(&raw.encode_to_vec()));
let success = tx
.ret
.first()
.map(|r| r.contract_ret == tron_proto::transaction::result::ContractResult::Success as i32)
.unwrap_or(false);
let success = transaction_succeeded(tx, matcher.for_tx(&tx_id, txidx as usize));

// ---- idx_native: from the tx params alone --------------------
if caps.native {
Expand Down Expand Up @@ -758,6 +771,34 @@ pub fn extract_block(
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_block_ret_uses_committed_execution_outcome() {
use tron_proto::transaction::result::ContractResult;
let mut tx = Transaction::default();
let mut info = tron_proto::TransactionInfo::default();
assert!(!transaction_succeeded(&tx, None));
assert!(transaction_succeeded(&tx, Some(&info)));
info.receipt = Some(tron_proto::ResourceReceipt {
result: ContractResult::Success as i32,
..Default::default()
});
assert!(transaction_succeeded(&tx, Some(&info)));
info.receipt.as_mut().unwrap().result = ContractResult::Revert as i32;
assert!(!transaction_succeeded(&tx, Some(&info)));
info.receipt = None;
info.result = tron_proto::transaction_info::Code::Failed as i32;
assert!(!transaction_succeeded(&tx, Some(&info)));
info.result = tron_proto::transaction_info::Code::Sucess as i32;
tx.ret.push(tron_proto::transaction::Result {
contract_ret: ContractResult::Revert as i32,
..Default::default()
});
assert!(
!transaction_succeeded(&tx, Some(&info)),
"explicit block result takes precedence"
);
}


fn caps_internal() -> CaptureSet {
CaptureSet {
Expand Down
2 changes: 1 addition & 1 deletion crates/tron-index/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ pub use commitment::{
pub use db::{IndexDb, IndexError, InitOutcome, FORMAT_VERSION};
pub use engine::{EngineOptions, IndexCounters, IndexEngine, IndexStatus, Tick};
pub use extract::{
created_contract_address, extract_block, tx_facts, CaptureSet, TxFacts, TxInfoMatcher,
created_contract_address, extract_block, transaction_succeeded, tx_facts, CaptureSet, TxFacts, TxInfoMatcher,
TRANSFER_TOPIC,
};
pub use firehose_log::{
Expand Down
6 changes: 3 additions & 3 deletions crates/tron-index/tests/extract_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,10 @@ fn create_contract_indexes_deployer_and_derived_address() {
..Default::default()
};
let tx = tx_with(ContractType::CreateSmartContract, c.encode_to_vec());
// The expected created address: 0x41 ‖ keccak(owner ‖ tx_id)[12..].
// The expected created address: 0x41 ‖ keccak(tx_id ‖ owner)[12..].
let tx_id = tron_crypto::hash::sha256(&tx.raw_data.as_ref().unwrap().encode_to_vec());
let mut input = addr(1).to_vec();
input.extend_from_slice(&tx_id);
let mut input = tx_id.to_vec();
input.extend_from_slice(&addr(1));
let h = tron_crypto::hash::keccak256(&input);
let mut created = [0u8; 21];
created[0] = 0x41;
Expand Down
38 changes: 29 additions & 9 deletions crates/tron-node/src/firehose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,8 @@ pub fn build_apply(
// identical blocks.
let matcher = tron_index::TxInfoMatcher::new(ret);

let mut txinfo_missing = ret.is_none() && !block.transactions.is_empty();

let txs = block
.transactions
.iter()
Expand All @@ -414,14 +416,8 @@ pub fn build_apply(
.unwrap_or_else(|| tron_crypto::hash::sha256(&raw_tx.encode_to_vec()));
let facts = tron_index::tx_facts(tx, &tx_id);
let info = matcher.for_tx(&tx_id, i);
let success = tx
.ret
.first()
.map(|r| {
r.contract_ret
== tron_proto::transaction::result::ContractResult::Success as i32
})
.unwrap_or(false);
txinfo_missing |= info.is_none();
let success = tron_index::transaction_succeeded(tx, info);
Some(fh::Tx {
txid: tx_id.to_vec(),
contract_type: facts.as_ref().map(|f| f.contract_type).unwrap_or(0),
Expand Down Expand Up @@ -484,7 +480,7 @@ pub fn build_apply(
timestamp_ms: raw.map(|r| r.timestamp).unwrap_or(0),
witness: raw.map(|r| r.witness_address.clone()).unwrap_or_default(),
solidified_height: solidified,
txinfo_missing: ret.is_none() && !block.transactions.is_empty(),
txinfo_missing,
txs,
}
}
Expand All @@ -494,6 +490,30 @@ mod tests {
use super::*;
use tron_chainbase::MemBackend;

#[test]
fn receipt_status_without_block_ret_and_partial_receipt_gap() {
let rig = Rig::new("receipt-status");
let (mut block, _, mut ret) = rig.make_block(1, 0);
block.transactions[0].ret.clear();
ret.transactioninfo[0].receipt = Some(tron_proto::ResourceReceipt {
result: tron_proto::transaction::result::ContractResult::Success as i32,
..Default::default()
});
let event = build_apply(1, &block, Some(&ret), 1);
assert!(event.txs[0].success);
assert!(!event.txinfo_missing);
ret.transactioninfo[0].receipt.as_mut().unwrap().result =
tron_proto::transaction::result::ContractResult::Revert as i32;
assert!(!build_apply(1, &block, Some(&ret), 1).txs[0].success);
// A present TransactionRet container is insufficient: every tx needs
// its own info or the consumer must stop rather than miss events.
let mut second = block.transactions[0].clone();
second.raw_data.as_mut().unwrap().data.push(99);
block.transactions.push(second);
assert!(build_apply(1, &block, Some(&ret), 1).txinfo_missing);
std::fs::remove_dir_all(rig.dir).unwrap();
}

fn mem() -> Arc<dyn KvBackend> {
Arc::new(MemBackend::new())
}
Expand Down
Loading