Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e651cbd
feat: address state module (WIP)
whankinsiv Sep 22, 2025
78748fd
fix: prepare for asset deltas subscription
whankinsiv Sep 22, 2025
559336c
merge upstream/main
whankinsiv Sep 26, 2025
e6c36a7
Merge remote-tracking branch 'upstream/whankinsiv/index-normalization…
whankinsiv Sep 26, 2025
e90a68c
feat: add utxo and transaction indexing to address state
whankinsiv Sep 26, 2025
0e1314d
store utxos and txs outside rollback window on disk
whankinsiv Oct 1, 2025
8e9fe08
refactor: batch persistence of address state
whankinsiv Oct 1, 2025
e10c710
fix: cleanup address state
whankinsiv Oct 6, 2025
4377701
Merge remote-tracking branch 'upstream/whankinsiv/index-normalization…
whankinsiv Oct 7, 2025
1de8842
feat: add address info REST handler
whankinsiv Oct 7, 2025
e24b220
test: add coverage for address_state UTxOs (creation, persistence, re…
whankinsiv Oct 7, 2025
e9f840a
fix: properly filter spends vs creations in address deltas message
whankinsiv Oct 8, 2025
8cee539
Merge remote-tracking branch 'upstream/main' into whankinsiv/setup-ad…
whankinsiv Oct 8, 2025
33b17c6
refactor: remove AddressStore trait and use Fjall as the only store
whankinsiv Oct 8, 2025
16dbe2d
fix: cleanup address_state run loop and store logic
whankinsiv Oct 8, 2025
a765475
refactor: prevent blocking run loop when persisting to disk (WIP)
whankinsiv Oct 9, 2025
cb39ce5
merge upstream/main
whankinsiv Oct 9, 2025
0effd41
fix: use correct immutable store in address tests
whankinsiv Oct 13, 2025
fcf1ca3
fix: match address info REST handler with BF schema
whankinsiv Oct 13, 2025
0ac0b67
fix: enforce sequential epoch persistence and merge pending state in …
whankinsiv Oct 13, 2025
b016518
fix address info response format for addresses with no UTxOs
whankinsiv Oct 13, 2025
a1767e6
test: add coverage forShelleyAddress stake_address_string method
whankinsiv Oct 14, 2025
3dfa776
fix: add comments to explain which endpoints a REST handler covers
whankinsiv Oct 14, 2025
2a334bd
merge upstream/main
whankinsiv Oct 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ bigdecimal = "0.4.8"
bitmask-enum = "2.2"
bs58 = "0.5"
chrono = { workspace = true }
crc = "3"
gcd = "2.3"
fraction = "0.15"
hex = { workspace = true }
Expand Down
236 changes: 226 additions & 10 deletions common/src/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,81 @@
use crate::cip19::{VarIntDecoder, VarIntEncoder};
use crate::types::{KeyHash, ScriptHash};
use anyhow::{anyhow, bail, Result};
use crc::{Crc, CRC_32_ISO_HDLC};
use minicbor::data::IanaTag;
use serde_with::{hex::Hex, serde_as};

/// a Byron-era address
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct ByronAddress {
/// Raw payload
pub payload: Vec<u8>,
}

impl ByronAddress {
fn compute_crc32(&self) -> u32 {
const CRC32: Crc<u32> = Crc::<u32>::new(&CRC_32_ISO_HDLC);
CRC32.checksum(&self.payload)
}

pub fn to_string(&self) -> Result<String> {
let crc = self.compute_crc32();

let mut buf = Vec::new();
{
let mut enc = minicbor::Encoder::new(&mut buf);
enc.array(2)?;
enc.tag(IanaTag::Cbor)?;
enc.bytes(&self.payload)?;
enc.u32(crc)?;
}

Ok(bs58::encode(buf).into_string())
}

pub fn from_string(s: &str) -> Result<Self> {
let bytes = bs58::decode(s).into_vec()?;
let mut dec = minicbor::Decoder::new(&bytes);

let len = dec.array()?.unwrap_or(0);
if len != 2 {
anyhow::bail!("Invalid Byron address CBOR array length");
}

let tag = dec.tag()?;
if tag != IanaTag::Cbor.into() {
anyhow::bail!("Invalid Byron address CBOR tag, expected 24");
}

let payload = dec.bytes()?.to_vec();
let crc = dec.u32()?;

let address = ByronAddress { payload };
let computed = address.compute_crc32();

if crc != computed {
anyhow::bail!("Byron address CRC mismatch");
}

Ok(address)
}

pub fn to_bytes_key(&self) -> Result<Vec<u8>> {
let crc = self.compute_crc32();

let mut buf = Vec::new();
{
let mut enc = minicbor::Encoder::new(&mut buf);
enc.array(2)?;
enc.tag(minicbor::data::IanaTag::Cbor)?;
enc.bytes(&self.payload)?;
enc.u32(crc)?;
}

Ok(buf)
}
}

/// Address network identifier
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum AddressNetwork {
Expand Down Expand Up @@ -170,11 +236,85 @@ impl ShelleyAddress {
data.extend(delegation_hash);
Ok(bech32::encode::<bech32::Bech32>(hrp, &data)?)
}

pub fn to_bytes_key(&self) -> Result<Vec<u8>> {
let network_bits = match self.network {
AddressNetwork::Main => 1u8,
AddressNetwork::Test => 0u8,
};

let (payment_hash, payment_bits): (&Vec<u8>, u8) = match &self.payment {
ShelleyAddressPaymentPart::PaymentKeyHash(data) => (data, 0),
ShelleyAddressPaymentPart::ScriptHash(data) => (data, 1),
};

let mut data = Vec::new();

match &self.delegation {
ShelleyAddressDelegationPart::None => {
let header = network_bits | (payment_bits << 4) | (3 << 5);
data.push(header);
data.extend(payment_hash);
}
ShelleyAddressDelegationPart::StakeKeyHash(hash) => {
let header = network_bits | (payment_bits << 4) | (0 << 5);
data.push(header);
data.extend(payment_hash);
data.extend(hash);
}
ShelleyAddressDelegationPart::ScriptHash(hash) => {
let header = network_bits | (payment_bits << 4) | (1 << 5);
data.push(header);
data.extend(payment_hash);
data.extend(hash);
}
ShelleyAddressDelegationPart::Pointer(pointer) => {
let header = network_bits | (payment_bits << 4) | (2 << 5);
data.push(header);
data.extend(payment_hash);

let mut encoder = VarIntEncoder::new();
encoder.push(pointer.slot);
encoder.push(pointer.tx_index);
encoder.push(pointer.cert_index);
data.extend(encoder.to_vec());
}
}

Ok(data)
}

pub fn stake_address_string(&self) -> Result<Option<String>> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know these are fairly straightforward methods, but would it be worth adding to/creating tests to cover them?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests for both script and normal addresses to stake_address_string added in a1767e6.

let network_bit = match self.network {
AddressNetwork::Main => 1,
AddressNetwork::Test => 0,
};

match &self.delegation {
ShelleyAddressDelegationPart::StakeKeyHash(key_hash) => {
let mut data = Vec::with_capacity(29);
data.push(network_bit | (0b1110 << 4));
data.extend_from_slice(key_hash);
let stake = StakeAddress::from_binary(&data)?.to_string()?;
Ok(Some(stake))
}
ShelleyAddressDelegationPart::ScriptHash(script_hash) => {
let mut data = Vec::with_capacity(29);
data.push(network_bit | (0b1111 << 4));
data.extend_from_slice(script_hash);
let stake = StakeAddress::from_binary(&data)?.to_string()?;
Ok(Some(stake))
}
// TODO: Use chain store to resolve pointer delegation addresses
ShelleyAddressDelegationPart::Pointer(_pointer) => Ok(None),
ShelleyAddressDelegationPart::None => Ok(None),
}
}
}

/// Payload of a stake address
#[serde_as]
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum StakeAddressPayload {
/// Stake key
StakeKeyHash(#[serde_as(as = "Hex")] Vec<u8>),
Expand All @@ -196,7 +336,7 @@ impl StakeAddressPayload {
}

/// A stake address
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct StakeAddress {
/// Network id
pub network: AddressNetwork,
Expand Down Expand Up @@ -271,10 +411,28 @@ impl StakeAddress {
data.extend(stake_hash);
Ok(bech32::encode::<bech32::Bech32>(hrp, &data)?)
}

pub fn to_bytes_key(&self) -> Result<Vec<u8>> {
let mut out = Vec::new();
let (bits, hash): (u8, &[u8]) = match &self.payload {
StakeAddressPayload::StakeKeyHash(h) => (0b1110, h),
StakeAddressPayload::ScriptHash(h) => (0b1111, h),
};

let net_bit = match self.network {
AddressNetwork::Main => 1,
AddressNetwork::Test => 0,
};

let header = net_bit | (bits << 4);
out.push(header);
out.extend_from_slice(hash);
Ok(out)
}
}

/// A Cardano address
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum Address {
None,
Byron(ByronAddress),
Expand Down Expand Up @@ -306,10 +464,9 @@ impl Address {
} else if text.starts_with("stake1") || text.starts_with("stake_test1") {
Ok(Self::Stake(StakeAddress::from_string(text)?))
} else {
if let Ok(bytes) = bs58::decode(text).into_vec() {
Ok(Self::Byron(ByronAddress { payload: bytes }))
} else {
Ok(Self::None)
match ByronAddress::from_string(text) {
Ok(byron) => Ok(Self::Byron(byron)),
Err(_) => Ok(Self::None),
}
}
}
Expand All @@ -318,11 +475,46 @@ impl Address {
pub fn to_string(&self) -> Result<String> {
match self {
Self::None => Err(anyhow!("No address")),
Self::Byron(byron) => Ok(bs58::encode(&byron.payload).into_string()),
Self::Byron(byron) => byron.to_string(),
Self::Shelley(shelley) => shelley.to_string(),
Self::Stake(stake) => stake.to_string(),
}
}

pub fn to_bytes_key(&self) -> Result<Vec<u8>> {
match self {
Address::Byron(b) => b.to_bytes_key(),

Address::Shelley(s) => s.to_bytes_key(),

Address::Stake(stake) => stake.to_bytes_key(),

Address::None => Err(anyhow!("No address to convert")),
}
}

pub fn kind(&self) -> &'static str {
match self {
Address::Byron(_) => "byron",
Address::Shelley(_) => "shelley",
Address::Stake(_) => "stake",
Address::None => "none",
}
}

pub fn is_script(&self) -> bool {
match self {
Address::Shelley(shelley) => match shelley.payment {
ShelleyAddressPaymentPart::PaymentKeyHash(_) => false,
ShelleyAddressPaymentPart::ScriptHash(_) => true,
},
Address::Stake(stake) => match stake.payload {
StakeAddressPayload::StakeKeyHash(_) => false,
StakeAddressPayload::ScriptHash(_) => true,
},
Address::Byron(_) | Address::None => false,
}
}
}

// -- Tests --
Expand All @@ -336,7 +528,7 @@ mod tests {
let payload = vec![42];
let address = Address::Byron(ByronAddress { payload });
let text = address.to_string().unwrap();
assert_eq!(text, "j");
assert_eq!(text, "8MMy4x9jE734Gz");

let unpacked = Address::from_string(&text).unwrap();
assert_eq!(address, unpacked);
Expand Down Expand Up @@ -546,6 +738,30 @@ mod tests {
assert_eq!(address, unpacked);
}

#[test]
fn shelley_to_stake_address_string_mainnet() {
let normal_address = ShelleyAddress::from_string("addr1q82peck5fynytkgjsp9vnpul59zswsd4jqnzafd0mfzykma625r684xsx574ltpznecr9cnc7n9e2hfq9lyart3h5hpszffds5").expect("valid normal address");
let script_address = ShelleyAddress::from_string("addr1zx0whlxaw4ksygvuljw8jxqlw906tlql06ern0gtvvzhh0c6409492020k6xml8uvwn34wrexagjh5fsk5xk96jyxk2qhlj6gf").expect("valid script address");

let normal_stake_address = normal_address
.stake_address_string()
.expect("stake_address_string should not fail")
.expect("normal address should have stake credential");
let script_stake_address = script_address
.stake_address_string()
.expect("stake_address_string should not fail")
.expect("script address should have stake credential");

assert_eq!(
normal_stake_address,
"stake1uxa92par6ngr202l4s3fuupjufu0fju4t5szljw34cm6tscq40449"
);
assert_eq!(
script_stake_address,
"stake1uyd2hj6j4848mdrdln7x8fc6hpunw5ft6yct2rtzafzrt9qh0m28h"
);
}

#[test]
fn stake_address_from_binary_mainnet_stake() {
// First withdrawal on Mainnet
Expand Down
7 changes: 5 additions & 2 deletions common/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::ledger_state::SPOState;
use crate::protocol_params::{NonceHash, ProtocolParams};
use crate::queries::parameters::{ParametersStateQuery, ParametersStateQueryResponse};
use crate::queries::spdd::{SPDDStateQuery, SPDDStateQueryResponse};
use crate::queries::utxos::{UTxOStateQuery, UTxOStateQueryResponse};
use crate::queries::{
accounts::{AccountsStateQuery, AccountsStateQueryResponse},
addresses::{AddressStateQuery, AddressStateQueryResponse},
Expand Down Expand Up @@ -394,10 +395,11 @@ pub enum StateQuery {
Mempool(MempoolStateQuery),
Metadata(MetadataStateQuery),
Network(NetworkStateQuery),
Parameters(ParametersStateQuery),
Pools(PoolsStateQuery),
Scripts(ScriptsStateQuery),
Transactions(TransactionsStateQuery),
Parameters(ParametersStateQuery),
UTxOs(UTxOStateQuery),
SPDD(SPDDStateQuery),
}

Expand All @@ -413,9 +415,10 @@ pub enum StateQueryResponse {
Mempool(MempoolStateQueryResponse),
Metadata(MetadataStateQueryResponse),
Network(NetworkStateQueryResponse),
Parameters(ParametersStateQueryResponse),
Pools(PoolsStateQueryResponse),
Scripts(ScriptsStateQueryResponse),
Transactions(TransactionsStateQueryResponse),
Parameters(ParametersStateQueryResponse),
UTxOs(UTxOStateQueryResponse),
SPDD(SPDDStateQueryResponse),
}
Loading