Skip to content

Commit

Permalink
Add documentation to PoW structs
Browse files Browse the repository at this point in the history
Also
- refactor `genesis_number` => `genesis_block_number`
- clean up example page changes
  • Loading branch information
sisou authored and jsdanielh committed Dec 13, 2024
1 parent c1ce957 commit c160c9f
Show file tree
Hide file tree
Showing 11 changed files with 109 additions and 30 deletions.
2 changes: 1 addition & 1 deletion pow-migration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ build-data = "0.2"

[dev-dependencies]
serde_json = "1.0"
nimiq-web-client = { workspace = true, default-features = false, features = ["primitives"] }

nimiq-test-log = { workspace = true }
nimiq-web-client = { workspace = true, features = ["primitives"] }

[features]
pow-migration-tests = []
1 change: 1 addition & 0 deletions pow-migration/src/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,7 @@ mod test {
assert_eq!(txn.hash, pos_transaction.hash::<Blake2bHash>().to_hex());

let web_transaction: WebTransaction = pos_transaction.into();
// Convert with the genesis block number and timestamp of mainnet.
web_transaction.to_plain_transaction(Some(3456000), Some(1732034720000));
}
}
Expand Down
43 changes: 38 additions & 5 deletions primitives/transaction/src/account/htlc_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ impl AccountTransactionVerification for HashedTimeLockedContractVerifier {

CreationTransactionData::parse(transaction)?.verify()
} else {
// PoW HTLC creation data specified the timeout (last field) as a u32 block number instead of a timestamp.
if transaction.recipient_data.len() != (20 * 2 + 1 + 32 + 1 + 4)
&& transaction.recipient_data.len() != (20 * 2 + 1 + 64 + 1 + 4)
{
Expand Down Expand Up @@ -217,11 +218,21 @@ add_serialization_fns_typed_arr!(AnyHash64, AnyHash64::SIZE);

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct CreationTransactionData {
/// Address that is allowed to redeem the funds after the timeout.
pub sender: Address,
/// Address that is allowed to redeem the funds before the timeout.
pub recipient: Address,
/// The hash root of the contract. The recipient can redeem the funds before the timeout by providing
/// a pre-image that hashes to this root.
pub hash_root: AnyHash,
/// The number of times the pre-image must be hashed to match the `hash_root`. Must be at least 1.
/// A number higher than 1 allows the recipient to provide an already hashed pre-image, with the
/// remaining number of hashes required to match the `hash_root` corresponding to the fraction of
/// the funds that can be claimed.
pub hash_count: u8,
#[serde(with = "nimiq_serde::fixint::be")]
/// The timeout as a millisecond timestamp before which the `recipient` and after which the `sender`
/// can claim the funds.
pub timeout: u64,
}

Expand All @@ -243,14 +254,26 @@ impl CreationTransactionData {
}
}

/// This struct represents HTLC creation data in the Proof-of-Work chain. The only difference to the data in
/// the Albatross chain is that the `timeout` was a u32 block number in PoW instead of a u64 timestamp.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct PoWCreationTransactionData {
/// Address that is allowed to redeem the funds after the timeout.
pub sender: Address,
/// Address that is allowed to redeem the funds before the timeout.
pub recipient: Address,
/// The hash root of the contract. The recipient can redeem the funds before the timeout by providing
/// a pre-image that hashes to this root.
pub hash_root: AnyHash,
/// The number of times the pre-image must be hashed to match the `hash_root`. Must be at least 1.
/// A number higher than 1 allows the recipient to provide an already hashed pre-image, with the
/// remaining number of hashes required to match the `hash_root` corresponding to the fraction of
/// the funds that can be claimed.
pub hash_count: u8,
#[serde(with = "nimiq_serde::fixint::be")]
pub timeout: u32, // Block height
/// The timeout as a block height before which the `recipient` and after which the `sender`
/// can claim the funds.
pub timeout: u32,
}

impl PoWCreationTransactionData {
Expand All @@ -269,11 +292,15 @@ impl PoWCreationTransactionData {
Ok(())
}

pub fn into_pos(self, genesis_number: u32, genesis_timestamp: u64) -> CreationTransactionData {
let timeout = if self.timeout <= genesis_number {
genesis_timestamp - (genesis_number - self.timeout) as u64 * 60_000
pub fn into_pos(
self,
genesis_block_number: u32,
genesis_timestamp: u64,
) -> CreationTransactionData {
let timeout = if self.timeout <= genesis_block_number {
genesis_timestamp - (genesis_block_number - self.timeout) as u64 * 60_000
} else {
genesis_timestamp + (self.timeout - genesis_number) as u64 * 60_000
genesis_timestamp + (self.timeout - genesis_block_number) as u64 * 60_000
};

CreationTransactionData {
Expand Down Expand Up @@ -401,6 +428,9 @@ impl OutgoingHTLCTransactionProof {
}
}

/// This struct represents a HTLC redeem proof for the regular transfer case in the Proof-of-Work chain.
/// Differences to the Proof-of-Stake is the serialization (PoW had a different position for the algorithm type
/// and no PreImage type prefix) and that the signature proof is a PoWSignatureProof and thus shorter than in PoS.
#[derive(Clone, Debug)]
pub struct PoWRegularTransfer {
// PoW regular transfers encode the hash algorithm as the first u8 byte,
Expand All @@ -411,6 +441,9 @@ pub struct PoWRegularTransfer {
signature_proof: PoWSignatureProof,
}

/// Enum over the different types of outgoing HTLC transaction proofs in the Proof-of-Work chain.
/// The differences to Proof-of-Stake are the variant IDs (they start at 1 in PoW, while they start at 0 in PoS)
/// and that all signature proofs are PoWSignatureProofs.
#[derive(Clone, Debug, Deserialize)]
#[repr(u8)]
pub enum PoWOutgoingHTLCTransactionProof {
Expand Down
18 changes: 13 additions & 5 deletions primitives/transaction/src/account/vesting_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ impl CreationTransactionData {
_ => return Err(TransactionError::InvalidData),
})
}

pub fn parse(transaction: &Transaction) -> Result<Self, TransactionError> {
CreationTransactionData::parse_data(&transaction.recipient_data, transaction.value)
}
Expand Down Expand Up @@ -207,7 +208,9 @@ impl CreationTransactionData {
}
}

/// Data used to create vesting contracts in PoW.
/// This struct represents vesting contract creation data in the Proof-of-Work chain. The differences
/// to the data in the Albatross chain are that the vesting period start and step length were u32 block numbers
/// in PoW instead of a u64 timestamps, making this serialization shorter.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct PoWCreationTransactionData {
/// The owner of the contract, the only address that can interact with it.
Expand Down Expand Up @@ -302,15 +305,20 @@ impl PoWCreationTransactionData {
_ => return Err(TransactionError::InvalidData),
})
}

pub fn parse(transaction: &Transaction) -> Result<Self, TransactionError> {
PoWCreationTransactionData::parse_data(&transaction.recipient_data, transaction.value)
}

pub fn into_pos(self, genesis_number: u32, genesis_timestamp: u64) -> CreationTransactionData {
let start_time = if self.start_block <= genesis_number {
genesis_timestamp - (genesis_number - self.start_block) as u64 * 60_000
pub fn into_pos(
self,
genesis_block_number: u32,
genesis_timestamp: u64,
) -> CreationTransactionData {
let start_time = if self.start_block <= genesis_block_number {
genesis_timestamp - (genesis_block_number - self.start_block) as u64 * 60_000
} else {
genesis_timestamp + (self.start_block - genesis_number) as u64 * 60_000
genesis_timestamp + (self.start_block - genesis_block_number) as u64 * 60_000
};
let time_step = self.step_blocks as u64 * 60_000;

Expand Down
3 changes: 3 additions & 0 deletions primitives/transaction/src/signature_proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,9 @@ impl WebauthnExtraFields {
}
}

/// This struct represents signature proofs in the Proof-of-Work chain. The difference to proofs on the
/// Albatross chain are that PoW signature could only be of type Ed25519 (they had no type-and-flags byte)
/// and that the merkle path had a different serialization.
#[derive(Clone, Debug, Deserialize)]
pub struct PoWSignatureProof {
pub public_key: Ed25519PublicKey,
Expand Down
26 changes: 26 additions & 0 deletions utils/src/merkle/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,11 +248,16 @@ pub type Blake2bMerklePath = MerklePath<Blake2bHash>;

#[test]
fn it_can_correctly_deserialize_merkle_path() {
// PoS merkle paths have the u8 count of nodes as the first byte, then the left-bits prefixed by
// their own varint length (0b01 here), then the node hashes themselves, prefixed by another varint length
// which is the same as the count in the first byte.
let bin = hex::decode("02018002de8d7ee7e54f301095294d494024430c8b251b4ebf9b1384922dc7f9dd24422f830e231d26cdc3bbd1f55f1918757568522acae62c21e8046190ea84d6e8ff16")
.unwrap();
let _ = MerklePath::<Blake2bHash>::deserialize_all(&bin).unwrap();
}

/// This struct represents the serialization of merkle paths in the Proof-of-Work chain, which was
/// different from the serialization in the Proof-of-Stake chain (it was more efficient but harder to parse).
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct PoWMerklePath<H: HashOutput> {
nodes: Vec<MerklePathNode<H>>,
Expand Down Expand Up @@ -321,6 +326,25 @@ impl<'de, H: HashOutput> Deserialize<'de> for PoWMerklePath<H> {
where
D: Deserializer<'de>,
{
// I have tried many ways to deserialize the PoW merkle path format with serde/postcard, but none
// allowed me to read the raw bytes and then parse vecs for which I already knew the length of (so
// that serde/postcard does not try to read a length prefix).
// I cannot use `deserializer.deserialize_bytes()` or anything similar that deserializes vecs, because
// they internally read the first byte as the length byte and then only let you work with that number
// of following bytes.
// Additionally, the number of fields passed to `deserializer.deserialize_stuct()` limits the number
// of times one can call `seq.next_element()` in the visitor before it throws a `SerdeDeCustom` error.
// Since we don't know how many nodes we need to parse from the input without looking at the input first
// (need to read that first byte), we cannot use it.
// `deserializer.deserialize_tuple()` has the same limitation, but we can work around it by giving it
// the maximum number of fields we will ever need to parse (length of a merkle path is serialized as a u8,
// so can be at most 255. That means the highest possible number of elements is 1 length field +
// 255.div_ceil(8) bitset bytes + 255 nodes = 288 elements). The deserializer doesn't complain when we
// parse less elements than indicated, so we can return from the visitor at any time when we have parsed
// all that we need to.
// (Technically one can do the same with `deserializer.deserialize_struct()`, but one would have to pass in
// a list of 288 strings for the `fields`. So using `deserialize_tuple()` that takes just a number is much
// easier.)
deserializer.deserialize_tuple(
288,
PoWMerklePathVisitor {
Expand All @@ -334,6 +358,8 @@ pub type PoWBlake2bMerklePath = PoWMerklePath<Blake2bHash>;

#[test]
fn it_can_correctly_deserialize_pow_merkle_path() {
// PoW merkle paths have the u8 count of nodes as the first byte, then the left-bits, then immediately the
// node hashes themselves - no length bytes inbetween.
let bin = hex::decode("0280de8d7ee7e54f301095294d494024430c8b251b4ebf9b1384922dc7f9dd24422f830e231d26cdc3bbd1f55f1918757568522acae62c21e8046190ea84d6e8ff16")
.unwrap();
let _ = PoWMerklePath::<Blake2bHash>::deserialize_all(&bin).unwrap();
Expand Down
7 changes: 5 additions & 2 deletions web-client/example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@
<div id="dead-mans-switch"></div>
</div>
<div>
<button id="address-book">Query address book</button>
<button id="query-staking-contract">Query Staking Contract balance</button>
<br />
<button id="address-book">Query address book</button><br />
<button id="query-staking-contract">Query staking contract</button><br />
<button id="query-transaction-history">Query transaction history</button><br />
<small><i>See console for output.</i></small>
</div>
<script>
const deadMansSwitch = document.querySelector('#dead-mans-switch');
Expand Down
5 changes: 4 additions & 1 deletion web-client/example/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,11 @@ init().then(async () => {
});

document.querySelector('#query-staking-contract').addEventListener("click", async () => {
let account = await client.getAccount('NQ07 0000 0000 0000 0000 0000 0000 0000 0000');
let account = await client.getAccount('NQ77 0000 0000 0000 0000 0000 0000 0000 0001');
console.log(account);
});

document.querySelector('#query-transaction-history').addEventListener("click", async () => {
let history = await client.getTransactionsByAddress('NQ07 0000 0000 0000 0000 0000 0000 0000 0000');
console.log(history);
});
Expand Down
8 changes: 4 additions & 4 deletions web-client/src/common/hashed_time_locked_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,16 @@ impl HashedTimeLockedContract {
pub fn parse_data(
bytes: &[u8],
as_pow: bool,
genesis_number: Option<u32>,
genesis_block_number: Option<u32>,
genesis_timestamp: Option<u64>,
) -> Result<PlainTransactionRecipientData, JsError> {
let data = if as_pow {
let genesis_number =
genesis_number.ok_or_else(|| JsError::new("Genesis number is required"))?;
let genesis_block_number = genesis_block_number
.ok_or_else(|| JsError::new("Genesis block number is required"))?;
let genesis_timestamp =
genesis_timestamp.ok_or_else(|| JsError::new("Genesis timestamp is required"))?;
PoWCreationTransactionData::parse_data(bytes)?
.into_pos(genesis_number, genesis_timestamp)
.into_pos(genesis_block_number, genesis_timestamp)
} else {
CreationTransactionData::parse_data(bytes)?
};
Expand Down
18 changes: 10 additions & 8 deletions web-client/src/common/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,10 +363,10 @@ impl Transaction {
#[wasm_bindgen(js_name = toPlain)]
pub fn to_plain(
&self,
genesis_number: Option<u32>,
genesis_block_number: Option<u32>,
genesis_timestamp: Option<u64>,
) -> Result<PlainTransactionType, JsError> {
let plain = self.to_plain_transaction(genesis_number, genesis_timestamp);
let plain = self.to_plain_transaction(genesis_block_number, genesis_timestamp);
Ok(serde_wasm_bindgen::to_value(&plain)?.into())
}

Expand Down Expand Up @@ -440,7 +440,7 @@ impl Transaction {

pub fn to_plain_transaction(
&self,
genesis_number: Option<u32>,
genesis_block_number: Option<u32>,
genesis_timestamp: Option<u64>,
) -> PlainTransaction {
PlainTransaction {
Expand Down Expand Up @@ -500,7 +500,7 @@ impl Transaction {
&self.inner.recipient_data,
self.inner.value,
true,
genesis_number,
genesis_block_number,
genesis_timestamp,
)
.unwrap()
Expand All @@ -520,7 +520,7 @@ impl Transaction {
HashedTimeLockedContract::parse_data(
&self.inner.recipient_data,
true,
genesis_number,
genesis_block_number,
genesis_timestamp,
)
.unwrap()
Expand Down Expand Up @@ -1065,7 +1065,7 @@ impl PlainTransactionDetails {
pub fn try_from_historic_transaction(
hist_tx: HistoricTransaction,
current_block: u32,
genesis_number: Option<u32>,
genesis_block_number: Option<u32>,
genesis_timestamp: Option<u64>,
) -> Option<PlainTransactionDetails> {
let block_number = hist_tx.block_number;
Expand All @@ -1080,11 +1080,13 @@ impl PlainTransactionDetails {
let (succeeded, transaction) = match hist_tx.data {
HistoricTransactionData::Basic(ExecutedTransaction::Ok(inner)) => (
true,
Transaction::from(inner).to_plain_transaction(genesis_number, genesis_timestamp),
Transaction::from(inner)
.to_plain_transaction(genesis_block_number, genesis_timestamp),
),
HistoricTransactionData::Basic(ExecutedTransaction::Err(inner)) => (
false,
Transaction::from(inner).to_plain_transaction(genesis_number, genesis_timestamp),
Transaction::from(inner)
.to_plain_transaction(genesis_block_number, genesis_timestamp),
),
HistoricTransactionData::Reward(ref ev) => (
true,
Expand Down
8 changes: 4 additions & 4 deletions web-client/src/common/vesting_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,16 @@ impl VestingContract {
bytes: &[u8],
tx_value: Coin,
as_pow: bool,
genesis_number: Option<u32>,
genesis_block_number: Option<u32>,
genesis_timestamp: Option<u64>,
) -> Result<PlainTransactionRecipientData, JsError> {
let data = if as_pow {
let genesis_number =
genesis_number.ok_or_else(|| JsError::new("Genesis number is required"))?;
let genesis_block_number = genesis_block_number
.ok_or_else(|| JsError::new("Genesis block number is required"))?;
let genesis_timestamp =
genesis_timestamp.ok_or_else(|| JsError::new("Genesis timestamp is required"))?;
PoWCreationTransactionData::parse_data(bytes, tx_value)?
.into_pos(genesis_number, genesis_timestamp)
.into_pos(genesis_block_number, genesis_timestamp)
} else {
CreationTransactionData::parse_data(bytes, tx_value)?
};
Expand Down

0 comments on commit c160c9f

Please sign in to comment.