diff options
96 files changed, 5060 insertions, 485 deletions
diff --git a/.gitignore b/.gitignore index 5931d64a0..c34d9c735 100644 --- a/.gitignore +++ b/.gitignore @@ -118,3 +118,17 @@ nbproject __pycache__/ *.pyc *.log + +# Monzero local release outputs and uploaded copies +/dist/ +/website/downloads/ + +# GUI is maintained and versioned as a separate Git repository. +/monzero-gui/ + +# Files produced by wallet_storage unit tests beside their immutable fixtures +/tests/data/wallet_*_copy_change_password_same* +/tests/data/wallet_*_new_change_password_diff* +/tests/data/wallet_*_new_file2file* +/tests/data/wallet_change_password_mem2file* +/tests/data/wallet_mem2file* diff --git a/MONZERO_CHAIN_SPEC.md b/MONZERO_CHAIN_SPEC.md new file mode 100644 index 000000000..091c780f5 --- /dev/null +++ b/MONZERO_CHAIN_SPEC.md @@ -0,0 +1,64 @@ +# Monzero (XMZ) chain specification + +This document records the initial consensus and network parameters for the +independent Monzero chain. Any change to a consensus value after public launch +requires a coordinated network upgrade. + +## Monetary policy + +- Name: Monzero +- Ticker: XMZ +- Target primary emission: 100,000,000 XMZ +- Atomic precision: 11 decimal places (1 XMZ = 100,000,000,000 atomic units) +- Emission curve: Monero-style smooth emission, speed factor 20 +- Target block time: 120 seconds +- Permanent tail emission: 3.26 XMZ per block +- Hard-coded premine: none +- Pre-release allocation: coins mined normally during the private bootstrap and + test period; the mined amount and addresses must be disclosed before launch + +Eleven decimal places is the greatest precision compatible with a +100,000,000-coin primary emission and the core's unsigned 64-bit amount type. +Twelve decimals would overflow that type. + +## Mainnet identity + +- Official website: `https://monzero.org` (project metadata; not consensus-critical) +- P2P port: 6174 +- RPC port: 6175 +- ZMQ RPC port: 6176 +- Network UUID: `fc3291ba-8666-4b87-8ebb-d59b72d4fcaa` +- Standard/integrated/subaddress prefixes: 86 / 87 / 88 +- Genesis nonce: 818137480 +- Genesis block hash: `3850cb9e2fb2ba3c680b24cee7574765acd6be1cc4d84a4360c7c53e83e3270d` +- Default data directory: `.monzero` +- Executables: `monzerod`, `monzero-wallet-cli`, `monzero-wallet-rpc` + +The complete protected mainnet, testnet, and stagenet vectors are available in +the machine-readable `config/monzero-consensus.json` manifest. Compiled unit +tests regenerate each genesis block and fail if monetary policy, address +prefixes, ports, UUIDs, nonces, hashes, transaction version, or hard-fork +schedule drift unexpectedly. + +## Bootstrap and launch policy + +Monzero does not embed a special premine transaction. The team may privately +mine the initial blocks for network testing. Before public launch, publish: + +1. The UTC timestamp and block height at which public launch begins. +2. Every address controlled by the team and its balance at that height. +3. The total privately mined XMZ and its intended use. +4. Stable seed-node hostnames or IP addresses. +5. Reproducible release binaries and the corresponding source commit. + +The inherited protocol revisions activate over blocks 1 through 16. Public +launch should occur only after block 16 so all public mining uses v16/RandomX. + +## Items intentionally pending + +- Public launch date and UTC time +- Project domain +- DNS and fixed-IP seed nodes +- Public checkpoints created after the bootstrap period +- Written allocation rationale and vesting/custody commitments +- Independent consensus and cryptography review diff --git a/config/monzero-consensus.json b/config/monzero-consensus.json new file mode 100644 index 000000000..6e7c8a0cc --- /dev/null +++ b/config/monzero-consensus.json @@ -0,0 +1,70 @@ +{ + "schema": "org.monzero.consensus.v1", + "native_asset": { + "name": "Monzero", + "ticker": "XMZ", + "atomic_units_per_coin": 100000000000, + "decimal_places": 11, + "money_supply_atomic": 10000000000000000000, + "emission_speed_factor_per_minute": 20, + "final_subsidy_per_minute_atomic": 163000000000, + "target_seconds": 120, + "mined_output_unlock_blocks": 60, + "normal_output_spendable_age_blocks": 10 + }, + "protocol": { + "current_transaction_version": 2, + "public_asset_transactions_accepted": false, + "hard_forks": [ + {"version": 1, "height": 1}, + {"version": 2, "height": 2}, + {"version": 3, "height": 3}, + {"version": 4, "height": 4}, + {"version": 5, "height": 5}, + {"version": 6, "height": 6}, + {"version": 7, "height": 7}, + {"version": 8, "height": 8}, + {"version": 9, "height": 9}, + {"version": 10, "height": 10}, + {"version": 11, "height": 11}, + {"version": 12, "height": 12}, + {"version": 13, "height": 13}, + {"version": 14, "height": 14}, + {"version": 15, "height": 15}, + {"version": 16, "height": 16} + ] + }, + "inactive_research_features": { + "asset_descriptor_version": 2, + "asset_classes": ["fungible", "non_fungible", "collection", "edition"], + "asset_transactions_serialized": false, + "asset_transactions_consensus_accepted": false, + "asset_wallet_support": false + }, + "networks": { + "mainnet": { + "network_uuid": "fc3291ba-8666-4b87-8ebb-d59b72d4fcaa", + "address_prefixes": {"standard": 86, "integrated": 87, "subaddress": 88}, + "ports": {"p2p": 6174, "rpc": 6175, "zmq": 6176}, + "genesis_nonce": 818137480, + "genesis_transaction": "013c01ff0001bda282a38eab04029b2e4c0281c0b02e7c53291a94d1d0cbff8883f8024f5142ee494ffbbd08807121017767aafcde9be00dcfd098715ebcf7f410daebc582fda69d24a28e9d0bc890d1", + "genesis_hash": "3850cb9e2fb2ba3c680b24cee7574765acd6be1cc4d84a4360c7c53e83e3270d" + }, + "testnet": { + "network_uuid": "adc2a271-5539-43d9-b3c2-7aadaf6b938c", + "address_prefixes": {"standard": 111, "integrated": 112, "subaddress": 113}, + "ports": {"p2p": 16174, "rpc": 16175, "zmq": 16176}, + "genesis_nonce": 818137481, + "genesis_transaction": "013c01ff0001bda282a38eab04029b2e4c0281c0b02e7c53291a94d1d0cbff8883f8024f5142ee494ffbbd08807121017767aafcde9be00dcfd098715ebcf7f410daebc582fda69d24a28e9d0bc890d1", + "genesis_hash": "d89bd4d42729aa9f68fd8d6bc86c3b12686a3fcd5ec83a392844efc067d5c040" + }, + "stagenet": { + "network_uuid": "b1dcac5a-9d11-4653-8122-e67eec8b59ac", + "address_prefixes": {"standard": 131, "integrated": 132, "subaddress": 133}, + "ports": {"p2p": 26174, "rpc": 26175, "zmq": 26176}, + "genesis_nonce": 818137482, + "genesis_transaction": "013c01ff0001bda282a38eab0402df5d56da0c7d643ddd1ce61901c7bdc5fb1738bfe39fbe69c28a3a7032729c0f2101168d0c4ca86fb55a4cf6a36d31431be1c53a3bd7411bb24e8832410289fa6f3b", + "genesis_hash": "0d917308bb74557f9ccefd1360b9875a39df00363474fe4da9ae8e5f989170b7" + } + } +} diff --git a/docs/MONZERO_ASSETS_V1_SPEC.md b/docs/MONZERO_ASSETS_V1_SPEC.md new file mode 100644 index 000000000..0da86ae83 --- /dev/null +++ b/docs/MONZERO_ASSETS_V1_SPEC.md @@ -0,0 +1,286 @@ +# Monzero Assets V1 product and privacy specification + +Status: early draft; not a consensus specification +Native coin: XMZ +Proposed feature: fixed-function private assets + +This document records product decisions and unresolved research questions. It +does not authorize mainnet implementation or claim that the proposed +cryptographic construction is safe. + +## 1. Objective + +Allow users to create publicly identifiable, fixed-supply assets whose owners, +recipients, balances, and transfer amounts receive privacy protections from the +Monzero protocol. + +XMZ remains the only native coin used for network fees, asset-creation fees, +mining rewards, and consensus security. + +## 2. V1 scope + +V1 is limited to: + +- issue a fixed-supply asset; +- transfer an issued asset; +- burn an issued asset; +- scan, restore, and display asset balances; +- voluntary disclosure through explicitly designed view functionality; and +- off-chain metadata committed by an on-chain hash. + +V1 deliberately excludes: + +- reissuable supply or retained mint authority; +- arbitrary smart contracts; +- transfer taxes, freezes, blacklists, clawbacks, or pausing; +- native automated market makers or order books; +- cross-chain bridges; +- wrapped assets; +- on-chain images or executable metadata; and +- claims that low-activity assets have the same practical anonymity as XMZ. + +Fixed supply is mandatory in V1 because it removes an authority lifecycle and +substantially reduces supply-validation and wallet-warning complexity. + +## 3. Privacy contract + +| Property | V1 policy | +| --- | --- | +| Asset identifier | Public | +| Issuance transaction | Public | +| Initial and maximum supply | Publicly verifiable policy | +| Supply mutability | Fixed; no reissuance | +| Sender | Private under the reviewed transaction model | +| Recipient | Private under the reviewed transaction model | +| Individual balance | Confidential | +| Transfer amount | Confidential | +| Asset activity | Observable at asset level | +| Metadata | Public and untrusted | + +The exact meaning of “private” must be replaced by testable cryptographic and +traffic-analysis claims before implementation. A thin asset with few outputs +may have a much weaker practical anonymity set than XMZ. + +## 4. Asset identity + +- Every asset has a cryptographic asset ID. +- Names, symbols, logos, and websites are never identifiers. +- Duplicate names and symbols are permitted. +- Wallets must show a shortened asset ID anywhere confusion is possible and + provide the complete ID on demand. +- The asset-ID derivation domain must include the Monzero mainnet identity and + an issuance-specific commitment. + +The exact derivation remains a cryptographic design item. It must avoid circular +transaction-hash definitions, collisions, cross-network replay, and malleability. + +### 4.1 Inactive descriptor prototype + +The source tree contains a research-only issuance descriptor and asset-ID +derivation in `src/cryptonote_basic/asset_types.*`. It is deliberately absent +from transaction serialization, the mempool, blocks, RPC, and wallets. Public +nodes therefore continue to reject every transaction version above v2 and +cannot issue or transfer assets. + +The provisional V2 canonical byte sequence is: + +1. ASCII domain `MonzeroAssetIssuanceV2` without a terminator; +2. one-byte descriptor version; +3. 16 raw network-UUID bytes; +4. one-byte asset class; +5. 32-byte issuer public key; +6. 32-byte issuance nonce; +7. eight-byte unsigned atomic supply, little-endian; +8. one-byte display precision; +9. 32-byte metadata content hash; +10. 32-byte collection asset ID, or the zero hash; +11. two-byte metadata-reference length, little-endian; and +12. the metadata-reference bytes. + +The provisional asset ID is `cn_fast_hash(canonical_descriptor)`. Version 2 +requires a valid issuer public key, a public Monzero network, non-zero supply, +display precision from 0 through 11, no NUL in the metadata reference, and a +maximum metadata-reference length of 256 bytes. Consensus assigns no meaning +to the reference contents. + +The asset classes and their additional rules are: + +| Class | Supply | Decimals | Collection reference | +| --- | ---: | ---: | --- | +| Fungible | Greater than zero | 0–11 | Forbidden | +| Non-fungible | Exactly 1 | 0 | Optional | +| Collection | Exactly 1 | 0 | Forbidden | +| Edition | Greater than zero | 0 | Optional | + +An NFT or edition must commit to a non-zero metadata content hash. A collection +reference alone does not prove legitimate membership. The inactive prototype +therefore defines two domain-separated authorization messages: + +- `MonzeroAssetIssuanceAuthorizationV1 || canonical_descriptor`, signed by the + issuer key committed in the descriptor; and +- `MonzeroCollectionMembershipV1 || collection_id || member_asset_id`, signed + by the collection controller. + +The implementation verifies both signatures and rejects altered descriptors, +member IDs, invalid keys, and zero IDs. The future transaction and database +design must still define how a collection controller is resolved from its +on-chain issuance, whether it can rotate, and how it is permanently closed. +Until that complete path exists, wallets must not display a collection as +verified. + +### 4.2 Inactive registry and reorganisation model + +The prototype includes an in-memory reference registry, disconnected from the +blockchain database. An issuance is inserted only after its canonical ID and +issuer signature validate. A claimed collection must already exist on the same +network, have the collection class, precede the member issuance, and authorize +the exact member asset ID. Duplicate IDs and unexpected membership signatures +are rejected. + +The registry records issuance height and deterministically removes every asset +issued at or above a detached height. Tests demonstrate removal and subsequent +valid reissuance after a simulated reorganisation. This is an executable state +model, not the final database schema or consensus integration. + +This encoding is a stable prototype vector for review, not an activation +decision. A cryptographic review may replace the hash construction or fields; +doing so must deliberately update the golden vector before any testnet fork. + +## 5. Issuance policy + +An issuance declares: + +- asset ID construction data; +- atomic supply; +- decimal display precision; +- metadata content hash; +- metadata URI; +- protocol version; and +- an XMZ creation fee. + +Issuance creates the complete lifetime supply. No key or authority can create +additional units after confirmation. + +Limits requiring research include maximum atomic supply, decimal range, +metadata URI length, transaction size, and creation-fee amount. + +## 6. Transfers and burns + +Nodes must reject a transaction unless it proves, for each asset independently, +that valid inputs equal valid outputs plus an explicitly burned amount. + +XMZ fees form a separate balance domain and may never be paid with an issued +asset. An asset transaction therefore needs sufficient XMZ inputs without +allowing the asset proof to create, destroy, or transform XMZ accidentally. + +A burn is permanent and must be distinguishable from an ordinary confidential +output without revealing unrelated private amounts. + +### 6.1 Inactive transparent conservation model + +`asset_types.*` also contains a test-only, transparent balance statement. It +exists to make the intended accounting rules executable before any +confidential commitment or proof system is selected. It is not serialized, +accepted by the mempool, exposed over RPC, or usable by a wallet. + +The validator currently enforces these semantic invariants: + +- XMZ inputs equal XMZ outputs plus an XMZ-denominated fee; +- issuance creates exactly the descriptor's complete fixed supply; +- an asset ID may be issued only once; +- every non-issuance asset ID must already be known; +- each asset balances independently as inputs equal outputs plus explicit + burns, preventing conversion between asset IDs; +- zero IDs, zero-valued entries, and empty statements are noncanonical; and +- all aggregation and output-plus-fee calculations reject integer overflow. + +These checks do not provide confidentiality, ownership authorization, range +proofs, double-spend protection, or a consensus transaction format. Those +properties remain prerequisites for an isolated prototype network. The +transparent statement must never be mistaken for a production transaction +design. + +## 7. Metadata + +Consensus stores only bounded identity and commitment fields. Descriptions, +logos, project links, and social information remain off-chain. + +Consensus must never depend on a registry, gateway, domain, image host, or +website. Wallets must treat all metadata as untrusted input and enforce content +type, size, rendering, and scripting restrictions. + +A registry may label an asset as verified, but verification must be visibly +different from endorsement. + +## 8. Wallet requirements + +- Discover every supported asset during normal scanning. +- Restore all XMZ and asset balances from documented recovery material. +- Display asset ID, fixed-supply status, and metadata verification state. +- Prevent name, symbol, decimal, and logo spoofing from obscuring the asset ID. +- Build transactions that balance XMZ fees and asset values independently. +- Explain thin-asset anonymity limitations before first use. +- Support opt-in disclosure without silently broadening an existing view key. +- Refuse unknown asset transaction versions safely. + +Hardware wallets are unsupported until their transaction review and signing +flows explicitly understand asset IDs, supply, fees, and burns. + +## 9. Required cryptographic research + +An external review must cover: + +- per-asset commitment construction; +- prevention of cross-asset inflation; +- issuance and burn proofs; +- interaction with RingCT and existing output types; +- ring-member compatibility and selection; +- thin-asset anonymity sets; +- range proofs and balance proofs; +- XMZ fee separation; +- transaction malleability; +- scanning cost and false positives; +- verification time and transaction size; and +- malformed or adversarial asset transactions. + +Public asset IDs with confidential amounts are the preferred V1 research path. +Confidential asset IDs are deferred to a possible later protocol version. + +## 10. Activation policy + +Asset rules require a versioned hard fork after: + +1. completion of the Phase 0 stabilization gates; +2. publication of a complete binary consensus specification; +3. independent cryptographic and implementation review; +4. an isolated prototype network; +5. automated inflation and reorganisation tests; +6. an extended public testnet; +7. resolution of audit findings; and +8. advance coordination with node, miner, wallet, and service operators. + +Unknown asset transaction versions must be rejected. Mainnet must not contain a +privileged rollback, administrator mint, or emergency asset-editing key. + +## 11. Deferred systems + +Launchpad, trading, and bridging are separate projects. Their existence is not +required for consensus-level asset ownership or transfer. + +If a bridge is later considered, its first scope should be XMZ to wrapped XMZ, +with explicit custody assumptions, strict caps, independent audits, reserve +monitoring, and warnings that transparent-chain activity can correlate users. + +## 12. Open decisions + +- Formal asset-ID derivation +- Commitment and proof construction +- Decoy eligibility and ring selection +- Maximum supply and decimal constraints +- Asset-creation fee and anti-spam policy +- Burn representation and supply reporting +- Asset-specific disclosure design +- Transaction weight limits +- Database schema and migration plan +- Reorganisation semantics for metadata indexes +- Testnet identifiers and activation schedule diff --git a/docs/MONZERO_ASSET_CRYPTO_REVIEW_BRIEF.md b/docs/MONZERO_ASSET_CRYPTO_REVIEW_BRIEF.md new file mode 100644 index 000000000..cf625635c --- /dev/null +++ b/docs/MONZERO_ASSET_CRYPTO_REVIEW_BRIEF.md @@ -0,0 +1,133 @@ +# Monzero Assets V1 cryptographic review brief + +Status: pre-implementation review input +Network activation: none +Native coin: XMZ +Descriptor prototype: version 2 + +## Review objective + +Determine whether Monzero can safely support fixed-supply fungible assets, +NFTs, collections, and editions with public asset IDs, confidential amounts, +private recipients, and XMZ-only fees without weakening existing XMZ +transaction validation. + +No asset transaction is currently serialized or accepted. The existing source +contains only canonical identity, authorization, transparent conservation, and +in-memory reorganisation reference models. + +## Preferred V1 construction for review + +Each asset output exposes a public 32-byte asset ID and otherwise uses the +existing one-time destination and confidential amount machinery. The asset ID +must be committed by the transaction prefix hash and every applicable +ownership/balance signature so it cannot be substituted after signing. + +Amounts continue to use the existing commitment generator. Validation groups +commitments by public asset ID and verifies a separate balance equation for +each group. XMZ forms its own group and is the only group from which the +transaction fee may be subtracted. + +This intentionally does not attempt confidential asset IDs. It minimizes new +cryptographic machinery, but outputs and activity are linkable at asset-class +level and rings must not mix assets in a way that permits an asset-substitution +proof. + +## Required transaction operations + +### Issuance + +- Commit the complete canonical descriptor and its derived asset ID. +- Verify authorization by the descriptor issuer key. +- Create exactly the declared lifetime supply. +- Reject an asset ID already present in canonical chain state. +- Charge a consensus-defined XMZ creation fee. +- For a claimed collection, verify authorization by the controller recorded in + the earlier collection issuance. + +### Transfer + +- Prove ownership of every real input without exposing which ring member it is. +- Bind each input and output to a public asset ID. +- Prove confidential input amounts equal confidential output amounts plus any + explicit burn for each asset independently. +- Pay the network fee only from the XMZ balance equation. + +### Burn + +- Permanently remove units without creating a spendable output. +- Bind the burn to the asset ID and transaction signature. +- Decide during review whether burn amounts are public or confidential. Public + burns simplify circulating-supply accounting; confidential burns protect + amounts but complicate public supply reporting. + +## NFT and collection rules + +- NFT and collection descriptors have supply one and zero decimals. +- Editions are indivisible and have supply greater than zero. +- Metadata is off-chain and untrusted; consensus commits only its content hash + and bounded reference bytes. +- Collection membership signs + `MonzeroCollectionMembershipV1 || collection_id || member_asset_id`. +- Collection controller rotation and permanent closure are deliberately + unresolved. V1 should prefer immutable controllers or an explicit signed + closure operation over an administrator override. +- Royalties are metadata hints only and are not consensus-enforced. + +## Questions requiring an external answer + +1. Can existing RingCT/CLSAG and Bulletproofs+ components be safely reused when + balance equations are partitioned by a public asset ID? +2. Must all decoys in an input ring share the real input's public asset ID, and + how should wallets sample rings for thin assets? +3. Which exact transcript elements must include the asset ID to prevent + substitution, replay, or cross-asset inflation? +4. What issuance pseudo-input or equivalent proof safely establishes the + declared fixed supply without revealing initial recipient amounts? +5. How should explicit burns be represented and proven? +6. Can batching proofs across asset groups introduce cancellation between + groups or malicious generator relationships? +7. What bounds are required for asset groups, inputs, outputs, metadata, and + verification cost per transaction and block? +8. Does public asset grouping weaken XMZ privacy when XMZ and issued assets are + moved together? +9. Which changes are required in hardware-wallet transaction review? +10. Are there safer established constructions or maintained libraries that + should replace this proposal? + +## Threat model and mandatory negative tests + +- Inflation within one asset. +- Cancellation or conversion between two asset IDs. +- Paying XMZ fees with an issued asset. +- Duplicate issuance across the main chain and alternate branches. +- Descriptor, asset-ID, collection-ID, or metadata-hash substitution. +- Forged issuer or collection-controller authorization. +- Replay across mainnet, testnet, and stagenet. +- Duplicate key images and repeated inputs. +- Malformed points, torsion points, noncanonical scalars, and zero commitments. +- Integer and parser length overflow. +- Proof batching failures that pass individually invalid transactions. +- Reorganisation rollback leaving phantom assets or deleting canonical assets. +- Denial of service from many asset groups or pathological proof shapes. +- Wallet restoration missing assets, NFTs, burns, or reorganised history. + +## Activation requirements + +1. Written review of the construction and transcript. +2. A complete byte-level transaction specification and golden vectors. +3. Strict parsing and verification implemented behind a test-only feature gate. +4. Persistent state with atomic connect/detach behavior. +5. Inflation, malformed-proof, fuzz, and multi-node reorg tests. +6. Wallet restoration and hardware-wallet impact assessment. +7. A disposable isolated network followed by a resettable public testnet. +8. Independent implementation audit and remediation. +9. A separately coordinated hard fork; no silent mainnet activation. + +## Explicit non-goals + +- Smart contracts or arbitrary programs. +- Bridges, wrapped assets, exchanges, or AMMs. +- Confidential asset identifiers. +- Reissuance, freezing, clawback, taxes, or administrator minting. +- A claim that low-activity NFTs have XMZ-sized anonymity sets. diff --git a/docs/MONZERO_PHASE0_BASELINE_20260815.md b/docs/MONZERO_PHASE0_BASELINE_20260815.md new file mode 100644 index 000000000..3e2a1f781 --- /dev/null +++ b/docs/MONZERO_PHASE0_BASELINE_20260815.md @@ -0,0 +1,196 @@ +# Monzero Phase 0 Baseline — 2026-08-15 + +This report records the state recovered after the development machine restart. It is a baseline, not a release approval. + +## Recovery and backup + +- Source commit: `4f92268d7c16741cfb41e5bbe2aa46cc260a9ea5` +- Working tree: 56 changed or untracked paths at the end of this baseline run. These include pre-existing project work and the Phase 0 documents; nothing was discarded or reset. +- Full pre-change archive: `/home/pinhead/Projects/Monzero-Fork-backups/Monero-Fork-before-assets-phase0-20260815.tar.zst` +- SHA-256: `40f9d5f4ba04cfd948dd6356cdb648253966ce3186ce8ac9b86af75010c46fb1` +- Archive integrity: passed `zstd -t`. + +## Build baseline + +- Target: `unit_tests` +- Build command: `cmake --build build --target unit_tests -j2` +- Result: passed; the current source produced `build/tests/unit_tests/unit_tests`. +- Parallelism was deliberately limited to two jobs after the earlier machine crash. + +## Unit-test baseline + +The binary discovered 1,231 tests from 152 test cases. The run progressed through the test inventory but did not produce a clean final result, so Phase 0 remains open. + +### Confirmed failure: DNSSEC-dependent URL test + +`AddressFromURL.Failure` fails because DNSSEC validation returns false through the configured local resolver. A focused rerun reproduced the failure: + +```text +Value of: dnssec_result + Actual: false +Expected: true +``` + +The resolver reported that no signatures were available while building the DNSSEC chain of trust. This may be environment-dependent, but it must be isolated or made deterministic before it can be accepted in release CI. + +### Confirmed incomplete test: protocol-handler race test + +`cryptonote_protocol_handler.race_condition` remained blocked in a futex wait for more than ten minutes. The test runner was then interrupted with SIGINT; no daemon or wallet process was terminated. This requires a focused, time-bounded reproduction and diagnosis. + +Because the run was interrupted, no pass count is claimed. Passing output was observed across substantial wallet/crypto utility, mnemonic, LMDB, networking, serialization, HTTP, SOCKS, ZMQ, and node-server groups, but that is not a substitute for a complete suite result. + +## Local network observation + +The already-running local daemon on RPC port 16175 was left untouched and reported: + +```json +{ + "height": 807, + "target_height": 0, + "incoming_connections_count": 0, + "outgoing_connections_count": 0, + "difficulty": 167813, + "synchronized": true, + "status": "OK" +} +``` + +The daemon considers its local chain synchronized, but zero peer connections means this observation does not demonstrate live network convergence or propagation. + +## Phase 0 disposition + +Asset consensus implementation remains on hold. The next engineering actions are: + +1. Reproduce and diagnose the protocol-handler race-test stall under a strict timeout. +2. Replace external DNS assumptions in unit tests with deterministic fixtures, or document and enforce a validated CI resolver. +3. Establish a multi-node automated chain test covering peer discovery, block propagation, reorg handling, wallet refresh, and transaction relay. +4. Produce reproducible, checksummed Linux and Windows release artifacts from a clean source checkout. + +Only after the stabilization gates in `MONZERO_PHASE0_STABILIZATION.md` are satisfied should the asset design move from research specification to consensus implementation. + +## Follow-up remediation + +The fast gate is now deterministic and clean: + +- `tests/phase0/run-fast-unit-tests.sh build` passed all 1,230 selected tests from 151 test cases in 174.317 seconds. +- DNSSEC-dependent tests use an explicitly configured validating TCP resolver (`DNS_PUBLIC`, defaulting to `tcp://1.1.1.1`). +- Fork-specific address, emission, amount-precision, wallet-cache, serialization, multisig, URI, HTTP, and notifier fixtures were corrected and passed. +- The foreign Monero wallet fixture is now explicitly expected to fail the Monzero genesis check; it is not accepted as a Monzero wallet. +- The initial `tests/phase0/monzero_multinode.py --build-dir build` harness passed twice, demonstrating three-node peer connectivity, block propagation, restart/catch-up, and a common tip in isolated regtest directories. + +The 1,230-test result excludes only +`cryptonote_protocol_handler.race_condition`. Its handcrafted miner +transaction always used the legacy `txout_to_key` output, even after the test +chain activated hard-fork v16. Consensus correctly rejected that alternate +chain because v16 requires `txout_to_tagged_key`, leaving the test waiting for +an impossible synchronization event. The fixture now selects the output type +from the active hard-fork version. + +`tests/phase0/run-stress-tests.sh build` subsequently passed the focused +approximately 9,600-block concurrency test in 9.582 seconds. Phase 0 and asset +activation remain open until the wallet-restoration, double-spend/adversarial +reorg, and reproducible-release checks pass. + +The extended three-node harness passed again at final height 84. It verified +competing three- and five-block forks, convergence on the longer fork after +reconnection, one-XMZ transaction relay through every node, confirmation by a +different node, and the exact confirmed receiver balance. It then restored the +receiver from its temporary mnemonic seed, rescanned from height zero, and +recovered the identical address and balance. + +The focused native adversarial gate initially found that the inherited +`FIRST_BLOCK_REWARD` in `tests/core_tests/double_spend.h` still used Monero's +subsidy. Monzero's larger genesis subsidy introduced an unexpected miner change +output, so the same-transaction fixture tried to derive Bob's key image from +the miner's output and failed before reaching consensus validation. Updating +the fixture to the already verified Monzero subsidy fixed the test setup. + +`tests/phase0/run-adversarial-core-tests.sh build` then passed all 16 selected +cases with zero failures. Coverage includes transaction-pool public/all-key +spends, no-relay/local/key-image conflicts, duplicate inputs in one +transaction, double spends across the same or different blocks, competing +chains, alternate-chain rollback, and both kept-by-block modes. + +## Existing artifact audit + +The existing `genesis-pre2` Linux and Windows outer checksums and every inner +package checksum pass. Archive listings contain the intended executables, +launch scripts, documentation, license, and checksum manifests. + +These packages are nevertheless **stale baseline artifacts**, not candidates +for republication after the Phase 0 fixes. The current native Linux binaries +are dynamically linked, unstripped developer builds with host-library +dependencies (including Boost 1.90), and the existing Windows binaries predate +the stabilization changes. New artifacts must be produced from an immutable, +documented source snapshot in pinned Linux and Windows build environments and +reproduced by a second build before their filenames or website links change. + +## Protected manifest and inactive asset identity prototype + +`config/monzero-consensus.json` now records the monetary policy, transaction +version, asset-acceptance state, hard-fork schedule, and protected vectors for +all three public networks. Three compiled `monzero_consensus` tests regenerate +the genesis blocks and lock the corresponding hashes, UUIDs, nonces, prefixes, +ports, emission constants, precision, and hard-fork schedule. + +The research-only `cryptonote::assets::issuance_descriptor` provides bounded +validation, canonical encoding, network domain separation, and a pinned +asset-ID vector. It is compiled for testing but is not referenced by +transaction serialization, consensus validation, the mempool, blocks, RPC, or +wallets. `CURRENT_TRANSACTION_VERSION` remains 2 and the manifest explicitly +records `public_asset_transactions_accepted: false`. + +After these additions, the fast unit gate passed 1,237 of 1,237 selected tests +from 153 test cases in 170.050 seconds. The separate pruning concurrency gate +also passed in 10.220 seconds. + +The inactive asset research code now additionally includes a transparent +semantic conservation validator. It separates XMZ fees from issued-asset +balances, requires issuance outputs to equal the declared fixed supply, and +enforces independent input/output/burn equality for every known asset. Focused +tests cover valid issuance, transfers and burns plus duplicate issuance, +unknown assets, cross-asset conversion, native inflation, noncanonical values, +and integer-overflow attempts. This validator remains disconnected from all +transaction and consensus paths and is not a privacy construction. + +With the six additional conservation tests included, the deterministic fast +gate passed 1,243 of 1,243 selected tests from 153 test cases in 181.415 +seconds on 2026-08-15. The separate approximately 9,600-block pruning +concurrency stress test also passed in 14.564 seconds. + +The provisional descriptor was advanced to version 2 before activation to add +an explicit asset class and collection ID. Fungible, non-fungible, collection, +and edition identities are now unambiguous. NFT and collection descriptors +require one indivisible unit, while edition units are indivisible. Issuer and +collection-membership messages have separate hash domains and signature +verification. Fifteen focused asset/consensus tests pass; no public transaction +format or asset acceptance rule is enabled. + +After the descriptor-v2 and authorization additions, the deterministic fast +gate passed 1,245 of 1,245 selected tests from 153 test cases in 152.606 +seconds on 2026-08-15. + +An inactive reference registry now exercises authenticated issuance state, +verified collection membership, ordering by issuance height, duplicate +rejection, and deterministic removal/reissuance across simulated chain +detachments. Seventeen focused asset and protected-consensus checks pass. The +registry is intentionally in-memory and is not connected to LMDB or block +validation. + +The exact staged Phase 0 source snapshot subsequently passed 1,247 of 1,247 +selected deterministic tests from 153 test cases in 162.024 seconds. + +## Deterministic packaging checkpoint + +`utils/release/package-linux.sh` and `verify-package.sh` now construct and +validate normalized Linux archives. Two development archives made from the +same input binaries and epoch were byte-identical, with SHA-256 +`a47ae087706f52a96829a83b3e4bd8be722663eb6ca3a95743526e4005314a41`. +The test archive passed extraction, inner-manifest, executable-format, and +version checks. + +This does not promote the test artifact to a release. Strict verification +correctly rejects it because the source tree is dirty, binary reproducibility +is unverified, and the developer executables are dynamically linked, +unstripped, and contain debug information. The generated archives remain in +temporary directories and were not copied to `dist/` or the website. diff --git a/docs/MONZERO_PHASE0_STABILIZATION.md b/docs/MONZERO_PHASE0_STABILIZATION.md new file mode 100644 index 000000000..2a42c8ca6 --- /dev/null +++ b/docs/MONZERO_PHASE0_STABILIZATION.md @@ -0,0 +1,139 @@ +# Monzero Phase 0 stabilization plan + +Status: active engineering gate +Baseline date: 2026-08-15 +Native coin ticker: XMZ + +This plan must be completed before any Monzero Assets consensus rules are +activated on a public network. Research, specification, and isolated prototypes +may proceed, but asset transaction formats must not be accepted by mainnet. + +## Current baseline + +- The independent mainnet identity, genesis, address prefixes, ports, emission, + precision, and hard-fork schedule are recorded in `MONZERO_CHAIN_SPEC.md`. +- Linux command-line and Windows GUI/CLI prerelease packages exist, but the + release process is not yet reproducible or signed. +- The official public node is `node.monzero.org:6174`, with restricted RPC on + port 6175. +- Only one official publicly reachable bootstrap node is currently established. +- Wallet creation, mining, synchronization, transfers, the public explorer, and + website have received manual testing. +- The inherited release checklist and portions of user-facing utility text still + refer to Monero infrastructure or terminology. +- The working tree contains substantial uncommitted fork work. A full pre-assets + backup was created before beginning this phase. + +## Gate A: source and consensus baseline + +- [ ] Commit the current fork as a named, reviewable baseline. +- [x] Record the exact genesis transaction, nonce, hash, and network UUID. +- [x] Record mainnet, testnet, and stagenet address prefixes and ports. +- [x] Generate a machine-readable consensus-parameter manifest. +- [x] Add tests which fail if protected network constants change unexpectedly. +- [ ] Document every intentional divergence from the upstream Monero version. +- [ ] Separate compatibility identifiers that must remain unchanged from + user-facing branding that should say Monzero. +- [ ] Define the supported database migration and rollback policy. + +Acceptance: two clean source checkouts produce nodes that agree on genesis, +hard-fork versions, block rewards, transaction validity, and block hashes. + +## Gate B: network reliability + +- [ ] Operate at least three independently administered public seed nodes. +- [ ] Place seed nodes in at least two providers and two geographic regions. +- [ ] Confirm a fresh node can bootstrap when any one seed is unavailable. +- [ ] Confirm a seed never attempts to use itself as its bootstrap peer. +- [ ] Test inbound, outbound, IPv4, DNS failure, restart, and peer-cache recovery. +- [ ] Monitor height, peer count, fork divergence, disk, memory, and RPC health. +- [ ] Define an incident response and emergency release procedure. + +Acceptance: a new node with an empty data directory synchronizes without manual +peer intervention, and the network continues progressing with one seed offline. + +## Gate C: wallet and transaction correctness + +- [ ] Create, restore, and rescan wallets from seed at multiple restore heights. +- [ ] Test incoming, outgoing, pending, failed, and replaced transactions. +- [ ] Test coinbase maturity and normal output unlock rules. +- [ ] Test view-only wallets, subaddresses, multisig, and offline signing, or + explicitly declare unsupported features before launch. +- [ ] Verify CLI and GUI display XMZ consistently. +- [ ] Verify no user-facing command suggests reusing Monero keys. +- [ ] Test wallet recovery after unclean shutdown and daemon reorganisation. + +Acceptance: an independently restored wallet reconstructs the same balance and +transaction history as the original wallet. + +## Gate D: automated consensus testing + +- [x] Unit tests for emission, precision, parsing, address prefixes, and fees. +- [x] Golden vectors for genesis and representative transactions. +- [x] Multi-node integration tests for synchronization and relay. +- [ ] Reorganisation tests across every active hard-fork version. +- [x] Network partition and recovery tests. +- [ ] Transaction and block parser fuzzing. +- [x] Invalid inflation, overflow, duplicate-spend, and malformed-proof tests. +- [ ] Continuous integration for supported Linux and Windows targets. + +Acceptance: all protected tests pass from a clean build and intentional +consensus changes require explicit vector updates. + +## Gate E: release engineering + +- [ ] Replace `docs/RELEASE_CHECKLIST.md` with a Monzero-specific checklist. +- [ ] Produce reproducible Linux builds from a pinned environment. +- [ ] Produce reproducible Windows builds from a pinned environment. +- [ ] Publish source commit, build recipe, file sizes, and SHA-256 hashes. +- [ ] Establish an offline release-signing key and publish its fingerprint. +- [ ] Sign release manifests; pursue Windows code signing separately. +- [ ] Test archives on clean supported operating systems. +- [ ] Document upgrade, downgrade, uninstall, and data-directory behaviour. + +Progress note: deterministic Linux archive construction and verification are +implemented and produce byte-identical archives from identical inputs. The +current developer binaries are dynamic, unstripped, and not independently +reproduced, so strict release verification correctly rejects them. + +Acceptance: two independent builders produce byte-identical binaries or a +documented, investigated explanation for every difference. + +## Gate F: public documentation and disclosure + +- [ ] Publish the private-bootstrap height range and total mined allocation. +- [ ] Publish team-controlled addresses or a privacy-compatible accountability + mechanism agreed before public launch. +- [ ] State clearly that Monzero is independent from the Monero project. +- [ ] Publish supported platforms, known limitations, and audit status. +- [ ] Create security contact and responsible-disclosure instructions. +- [ ] Remove inherited links that direct users to unrelated Monero services. + +Acceptance: a new operator can verify, install, run, back up, restore, and +upgrade a node or wallet using only public Monzero documentation. + +## Asset-development hold point + +Mainnet asset implementation remains blocked until Gates A through E are +complete. The Monzero Assets specification may advance in parallel, and an +isolated disposable development network may be used after cryptographic review. + +## Inactive asset/NFT prototype progress + +- [x] Canonical network-separated fixed-supply asset IDs. +- [x] Explicit fungible, NFT, collection, and edition classes. +- [x] NFT/collection supply-one and zero-decimal invariants. +- [x] Independent XMZ fee and per-asset conservation semantics. +- [x] Domain-separated issuer authorization signatures. +- [x] Domain-separated collection-membership authorization signatures. +- [x] Inactive authenticated issuance registry with deterministic reorg rollback. +- [x] External cryptographic-review brief and mandatory threat cases. +- [ ] Reviewed confidential per-asset commitment and range-proof construction. +- [ ] Versioned asset transaction serialization with strict parser limits. +- [ ] Persistent database indexes and production reorganisation-safe asset state. +- [ ] Isolated asset development network and faucet. +- [ ] Wallet issuance, scanning, restoration, transfer, burn, and metadata UI. +- [ ] Explorer display with unverified/verified collection distinction. + +All completed items in this section are inactive primitives and unit tests. +They do not enable token or NFT issuance on mainnet, testnet, or stagenet. diff --git a/docs/RELEASE_CHECKLIST.md b/docs/RELEASE_CHECKLIST.md index 632366985..b64a28a30 100644 --- a/docs/RELEASE_CHECKLIST.md +++ b/docs/RELEASE_CHECKLIST.md @@ -1,68 +1,110 @@ -- [ ] Security audit -- [ ] Code audit -- [ ] Ledger integration - - [ ] Implemented in Monero codebase (if needed) - - [ ] Ledger app integration coded by Ledger - - [ ] Ledger Monero app update available -- [ ] Trezor integration - - [ ] Implemented in Monero codebase (if needed) - - [ ] Trezor app integration coded by Trezor - - [ ] Trezor firmware update available (if needed) -- [ ] Fork height set - - [ ] Monero-announce mailer notice - - [ ] Twitter announcement - - [ ] Reddit announcement - - [ ] Getmonero.org announcement -- [ ] Notify wallets - - [ ] MyMonero - - [ ] Coinomi - - [ ] Exa Wallet - - [ ] Wookey Wallet - - [ ] X Wallet - - [ ] Guarda - - [ ] ZelCore - - [ ] Cake Wallet - - [ ] Monerujo - - [ ] Edge Wallet - - [ ] Exodus - - [ ] XMRWallet -- [ ] Notify exchanges - - [ ] https://web.getmonero.org/community/merchants/#exchanges -- [ ] Notify 3rd party payment processors - - [ ] https://web.getmonero.org/community/merchants/#payment-gateways -- [ ] Notify mining pools - - [ ] https://miningpoolstats.stream/monero -- [ ] Release tagged - - [ ] Update src/version.cpp.in with new version AND new name (if necessary) - - [ ] Update Gitian YML files in contrib/gitian/ to the new version number - - [ ] Update README.md with new fork table entry (or at least update the Recommended Monero version) - - [ ] Update contrib/gitian/README.md so that the instructions reflect the current version - - [ ] Update src/checkpoints/checkpoints.cpp with a recent hardcoded checkpoint - - [ ] Update src/blocks/checkpoints.dat with ./monero-blockchain-export --output-file checkpoints.dat --block-stop <recent block height> --blocksdat - - [ ] Update expected_block_hashes_hash in src/cryptonote_core/blockchain.cpp with checkpoints.dat sha256 hash -- [ ] Testnet forked -- [ ] Testnet testing/verification - - [ ] Ledger - - [ ] Trezor - - [ ] Release-specific testing - - [ ] RPC testing/update RPC documentation -- [ ] CLI reproducible builds validated -- [ ] CLI released - - [ ] https://web.getmonero.org/downloads/ updated - - [ ] Update hashes.txt on website - - [ ] Update downloads.yml on website - - [ ] Update auto-update DNS records - - [ ] Update redirects on downloads box - - [ ] Update seed nodes -- [ ] GUI released - - [ ] https://web.getmonero.org/downloads/ updated - - [ ] Update hashes.txt on website - - [ ] Update hashes.txt.sig on website - - [ ] Update downloads.yml on website - - [ ] Update auto-update DNS records - - [ ] Update redirects on downloads box -- [ ] Release Announcements - - [ ] Monero-announce mailer notice - - [ ] Twitter announcement - - [ ] Reddit announcement - - [ ] Getmonero.org announcement +# Monzero release checklist + +This checklist applies to every Monzero node, CLI wallet, GUI wallet, miner +control, website download, and consensus release. Native amounts use XMZ. + +## Scope and source + +- [ ] Release version, codename, source commit, and supported platforms agreed +- [ ] Working tree clean or every included patch documented +- [ ] Submodules pinned and verified +- [ ] Consensus-affecting changes identified explicitly +- [ ] Database or wallet migration requirements documented +- [ ] User-visible changes and known limitations written +- [ ] Independent reviewer approves the release scope + +## Consensus and network safety + +- [ ] Genesis and protected consensus-vector tests pass +- [ ] Block reward, precision, fees, and amount parsing tests pass +- [ ] Valid and invalid transaction tests pass +- [ ] Synchronization from an empty data directory succeeds +- [ ] Reorganisation and restart tests pass +- [ ] Node interoperates with the currently deployed release +- [ ] Seed list contains no node configured to connect to itself +- [ ] Mainnet, testnet, and stagenet identifiers remain isolated +- [ ] Hard-fork activation height and operator notice reviewed, if applicable + +## Wallet safety + +- [ ] New wallet creation tested in CLI and GUI +- [ ] Seed restoration reproduces addresses and balances +- [ ] Incoming and outgoing transfers confirmed +- [ ] Pending, failed, pool, and confirmed history tested +- [ ] Coinbase and normal output unlock behaviour tested +- [ ] Wallet shutdown and unclean-restart recovery tested +- [ ] View-only, multisig, hardware, and offline signing marked tested or unsupported +- [ ] No workflow requests a recovery seed except an explicit recovery operation + +## Branding and configuration + +- [ ] Product name is Monzero and native ticker is XMZ +- [ ] Executable names, titles, prompts, logs, and default paths are correct +- [ ] Ports, address prefixes, network UUIDs, DNS seeds, and website are correct +- [ ] Required upstream attribution and copyright notices are preserved +- [ ] User-facing inherited Monero service links are removed or clearly contextual +- [ ] Warnings prohibit cross-fork key and seed reuse + +## Automated testing + +- [ ] Unit tests pass from a clean build +- [ ] Core and functional tests pass +- [ ] RPC compatibility tests pass +- [ ] Parser and consensus fuzzing has no unresolved regressions +- [ ] Website and explorer API smoke tests pass +- [ ] Linux launch, mining, wallet, and uninstall scripts pass shell validation +- [ ] Windows launch and mining scripts pass on a clean Windows system + +## Build and package + +- [ ] Linux build produced by the pinned build environment +- [ ] Windows build produced by the pinned cross-build environment +- [ ] A second builder checks reproducibility +- [ ] Executable formats and required runtime libraries inspected +- [ ] Archives contain only intended files +- [ ] README and upgrade instructions included +- [ ] SHA-256 manifest generated and verified +- [ ] Release manifest signed with the Monzero release key +- [ ] Packages scanned and tested after extraction from their final archives + +The Linux archive layer is generated with `utils/release/package-linux.sh`. +It refuses a dirty tree unless `ALLOW_DIRTY=1` is explicitly supplied, in +which case the filename and embedded manifest are marked development/dirty. +`utils/release/verify-package.sh` verifies archive shape, inner checksums, +executable presence, formats, and reported versions. This makes packaging +deterministic; it does not by itself make locally compiled binaries +reproducible. `RELEASE_STRICT=1` additionally rejects dirty manifests, +unverified build reproducibility, dynamically linked binaries, debug +information, and unstripped executables. + +## Deployment + +- [ ] Public node binaries backed up before replacement +- [ ] Public node database backup or recovery procedure confirmed +- [ ] New node starts, binds expected ports, and reports correct version +- [ ] Peer counts, height, difficulty, synchronization, and RPC checked +- [ ] At least one independent node successfully handshakes and synchronizes +- [ ] Explorer and website display current chain data +- [ ] Download files, sizes, MIME types, checksums, and links verified publicly +- [ ] Rollback binary and rollback instructions remain available + +## Publication and monitoring + +- [ ] Release notes identify experimental and unaudited components +- [ ] Source commit and signed hashes published +- [ ] Node, miner, wallet, and service operators receive upgrade instructions +- [ ] Security contact and disclosure process published +- [ ] Height, fork divergence, peers, RPC, CPU, memory, disk, and logs monitored +- [ ] Post-release review performed after 24 hours and seven days + +## Assets-specific gate + +For any future Monzero Assets release: + +- [ ] Phase 0 stabilization gates are complete +- [ ] Consensus and cryptographic specifications are public +- [ ] Independent cryptographic and implementation audits are resolved +- [ ] Inflation and cross-asset conversion tests pass +- [ ] Asset wallet restoration tests pass +- [ ] Extended public testnet has completed a planned upgrade +- [ ] Mainnet activation height has advance operator coordination diff --git a/explorer/README.md b/explorer/README.md new file mode 100644 index 000000000..87432e85d --- /dev/null +++ b/explorer/README.md @@ -0,0 +1,26 @@ +# Monzero block explorer + +A dependency-free PHP and JavaScript explorer for the Monzero restricted RPC. +It is read-only and never handles wallet seeds, private keys, or passwords. + +## Run locally + +Requirement: PHP 8. The API uses cURL when available and otherwise falls back +to PHP's native HTTP streams. + +```bash +cd explorer +php -S 127.0.0.1:8081 +``` + +Open `http://127.0.0.1:8081`. + +The RPC URL is defined by `NODE_RPC` at the top of `api.php`. It currently uses +the public node at `http://node.monzero.org:6175`. + +## Later deployment + +Upload the directory to a PHP-enabled web root or subdomain. For production, +place it at a dedicated hostname such as `explorer.monzero.org`, enable HTTPS, +and retain a restricted node RPC. The PHP endpoint uses an action allowlist and +does not expose a generic RPC proxy. diff --git a/explorer/api.php b/explorer/api.php new file mode 100644 index 000000000..109485dab --- /dev/null +++ b/explorer/api.php @@ -0,0 +1,126 @@ +<?php +declare(strict_types=1); + +const NODE_RPC = 'http://node.monzero.org:6175'; +const MAX_RANGE = 25; + +header('Content-Type: application/json; charset=utf-8'); +header('Cache-Control: no-store'); +header('X-Content-Type-Options: nosniff'); + +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + respond(['error' => 'POST required'], 405); +} + +$input = json_decode(file_get_contents('php://input'), true); +if (!is_array($input)) respond(['error' => 'Invalid JSON'], 400); +$action = $input['action'] ?? ''; + +try { + switch ($action) { + case 'info': + respond(rpc('/get_info', [])); + + case 'blocks': + $info = rpc('/get_info', []); + $height = max(1, (int)($info['height'] ?? 1)); + $limit = min(MAX_RANGE, max(1, (int)($input['limit'] ?? 15))); + $end = max(0, $height - 1); + $start = max(0, $end - $limit + 1); + $range = jsonRpc('get_block_headers_range', [ + 'start_height' => $start, + 'end_height' => $end, + 'fill_pow_hash' => false, + ]); + respond(['info' => $info, 'headers' => array_reverse($range['headers'] ?? [])]); + + case 'block': + $params = ['fill_pow_hash' => true]; + if (isset($input['height']) && is_numeric($input['height'])) { + $params['height'] = max(0, (int)$input['height']); + } elseif (validHash($input['hash'] ?? '')) { + $params['hash'] = strtolower($input['hash']); + } else { + respond(['error' => 'Valid height or block hash required'], 400); + } + respond(jsonRpc('get_block', $params)); + + case 'transaction': + $hash = strtolower((string)($input['hash'] ?? '')); + if (!validHash($hash)) respond(['error' => 'Valid transaction hash required'], 400); + $result = rpc('/get_transactions', [ + 'txs_hashes' => [$hash], + 'decode_as_json' => true, + 'prune' => false, + 'split' => true, + ]); + if (empty($result['txs']) && empty($result['txs_as_json'])) respond(['error' => 'Transaction not found'], 404); + respond($result); + + case 'pool': + respond(rpc('/get_transaction_pool', [])); + + default: + respond(['error' => 'Unknown action'], 400); + } +} catch (Throwable $error) { + error_log('Monzero explorer API: ' . $error->getMessage()); + respond(['error' => 'Node request failed'], 502); +} + +function validHash(mixed $value): bool { + return is_string($value) && preg_match('/^[0-9a-fA-F]{64}$/', $value) === 1; +} + +function jsonRpc(string $method, array $params): array { + $response = rpc('/json_rpc', [ + 'jsonrpc' => '2.0', + 'id' => '0', + 'method' => $method, + 'params' => $params, + ]); + if (isset($response['error'])) throw new RuntimeException($response['error']['message'] ?? 'RPC error'); + return $response['result'] ?? []; +} + +function rpc(string $path, array $payload): array { + // Daemon endpoints expect an empty JSON object, not an empty JSON array. + $json = $payload === [] ? '{}' : json_encode($payload, JSON_THROW_ON_ERROR); + if (function_exists('curl_init')) { + $curl = curl_init(NODE_RPC . $path); + curl_setopt_array($curl, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $json, + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => 3, + CURLOPT_TIMEOUT => 12, + ]); + $body = curl_exec($curl); + $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); + $message = curl_error($curl); + curl_close($curl); + if ($body === false || $status !== 200) throw new RuntimeException($message ?: 'HTTP ' . $status); + } else { + $context = stream_context_create(['http' => [ + 'method' => 'POST', + 'header' => "Content-Type: application/json\r\nConnection: close\r\n", + 'content' => $json, + 'timeout' => 12, + 'ignore_errors' => true, + ]]); + $body = @file_get_contents(NODE_RPC . $path, false, $context); + $statusLine = $http_response_header[0] ?? ''; + preg_match('/\s(\d{3})\s/', $statusLine, $match); + $status = isset($match[1]) ? (int)$match[1] : 0; + if ($body === false || $status !== 200) throw new RuntimeException('HTTP ' . $status); + } + $decoded = json_decode($body, true, 512, JSON_THROW_ON_ERROR); + return is_array($decoded) ? $decoded : []; +} + +function respond(array $data, int $status = 200): never { + http_response_code($status); + echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE); + exit; +} diff --git a/explorer/app.js b/explorer/app.js new file mode 100644 index 000000000..ab89b3be0 --- /dev/null +++ b/explorer/app.js @@ -0,0 +1,140 @@ +const $ = (selector) => document.querySelector(selector); +const view = $('#view'); +const content = $('#content'); +const number = new Intl.NumberFormat('en-GB'); +const atomic = 100_000_000_000; +let chainInfo = null; + +const escapeHtml = (value) => String(value ?? '').replace(/[&<>'"]/g, (char) => ({'&':'&','<':'<','>':'>',"'":''','"':'"'}[char])); +const shortHash = (hash, size = 12) => hash ? `${hash.slice(0, size)}…${hash.slice(-8)}` : '—'; +const xmz = (value) => `${(Number(value || 0) / atomic).toLocaleString('en-GB', {maximumFractionDigits: 11})} XMZ`; +const date = (timestamp) => timestamp ? new Date(timestamp * 1000).toLocaleString() : '—'; +const age = (timestamp) => { + const seconds = Math.max(0, Math.floor(Date.now() / 1000 - timestamp)); + if (seconds < 60) return `${seconds}s ago`; + if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; + if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; + return `${Math.floor(seconds / 86400)}d ago`; +}; + +async function api(action, data = {}) { + const response = await fetch('api.php', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({action, ...data})}); + const result = await response.json().catch(() => ({})); + if (!response.ok || result.error) throw new Error(result.error || `HTTP ${response.status}`); + return result; +} + +function loading(label = 'Loading chain data…') { view.innerHTML = `<div class="loading"><span></span>${escapeHtml(label)}</div>`; } +function showError(message = '') { + const fragment = $('#error-template').content.cloneNode(true); + if (message) fragment.querySelector('p').textContent = message; + view.replaceChildren(fragment); +} + +function updateStats(info) { + chainInfo = info; + $('#stat-height').textContent = number.format(info.height || 0); + $('#stat-difficulty').textContent = number.format(info.difficulty || 0); + $('#stat-hashrate').textContent = `${number.format(Math.round((info.difficulty || 0) / (info.target || 120)))} H/s estimated`; + $('#stat-transactions').textContent = number.format(info.tx_count || 0); + $('#stat-connections').textContent = number.format((info.incoming_connections_count || 0) + (info.outgoing_connections_count || 0)); + $('#node-label').textContent = info.status === 'OK' ? 'Node online' : info.status; + $('#node-dot').className = info.status === 'OK' ? 'online' : 'offline'; +} + +function headerTitle(kicker, title) { + $('.section-title').innerHTML = `<div><p class="eyebrow">${escapeHtml(kicker)}</p><h2>${title}</h2></div><a class="back" href="#/">← Recent blocks</a>`; +} + +async function showBlocks() { + loading(); + $('.section-title').innerHTML = `<div><p class="eyebrow">Live ledger</p><h2>Recent blocks</h2></div><button id="refresh" class="ghost" type="button">Refresh</button>`; + try { + const result = await api('blocks', {limit: 15}); + updateStats(result.info); + view.innerHTML = `<div class="table-wrap"><table><thead><tr><th>Height</th><th>Age</th><th>Block hash</th><th>Transactions</th><th>Difficulty</th><th>Reward</th><th>Size</th></tr></thead><tbody>${result.headers.map((block) => ` + <tr><td><a class="link" href="#/block/${block.height}">${number.format(block.height)}</a></td><td class="muted">${age(block.timestamp)}</td><td><a class="link hash truncate" title="${block.hash}" href="#/block/${block.hash}">${shortHash(block.hash)}</a></td><td>${number.format(block.num_txes || 0)}</td><td class="muted">${number.format(block.difficulty || 0)}</td><td class="reward">${xmz(block.reward)}</td><td class="muted">${number.format(block.block_size || block.block_weight || 0)} B</td></tr>`).join('')}</tbody></table></div>`; + $('#refresh').addEventListener('click', showBlocks); + } catch (error) { showError(error.message); setOffline(); } +} + +async function showBlock(value) { + loading('Loading block…'); + try { + const input = /^\d+$/.test(value) ? {height:Number(value)} : {hash:value}; + const block = await api('block', input); + const h = block.block_header || {}; + headerTitle('Block', `#${number.format(h.height ?? input.height)}`); + const txs = block.tx_hashes || []; + view.innerHTML = `<div class="detail-grid"> + ${datum('Block hash', `<code>${escapeHtml(h.hash)}</code>`)} + ${datum('Timestamp', `<strong>${escapeHtml(date(h.timestamp))}</strong>`)} + ${datum('Age', `<strong>${escapeHtml(age(h.timestamp))}</strong>`)} + ${datum('Reward', `<strong class="big">${xmz(h.reward)}</strong>`)} + ${datum('Difficulty', `<strong>${number.format(h.difficulty || 0)}</strong>`)} + ${datum('Transactions', `<strong>${number.format(h.num_txes || txs.length)}</strong>`)} + ${datum('Block size', `<strong>${number.format(h.block_size || h.block_weight || 0)} bytes</strong>`)} + ${datum('Nonce', `<strong>${number.format(h.nonce || 0)}</strong>`)} + ${datum('Version', `<strong>v${h.major_version ?? '—'}.${h.minor_version ?? '—'}</strong>`)} + ${datum('Previous block', h.prev_hash ? `<a class="link hash truncate" href="#/block/${h.prev_hash}">${shortHash(h.prev_hash)}</a>` : '<strong>Genesis</strong>')} + ${datum('Proof-of-work hash', `<code>${escapeHtml(h.pow_hash || 'Not requested')}</code>`)} + ${datum('Confirmations', `<strong>${chainInfo ? number.format(Math.max(0, chainInfo.height - h.height)) : '—'}</strong>`)} + </div> + <h3 class="subheading">Transactions in this block</h3> + ${txs.length ? `<div class="table-wrap"><table><thead><tr><th>#</th><th>Transaction hash</th></tr></thead><tbody>${txs.map((hash, i) => `<tr><td class="muted">${i + 1}</td><td><a class="link hash" href="#/tx/${hash}">${hash}</a></td></tr>`).join('')}</tbody></table></div>` : '<div class="empty"><strong>Coinbase only</strong><p>This block contains no regular transactions.</p></div>'} + <details class="raw"><summary>Raw block JSON</summary><pre>${escapeHtml(JSON.stringify(block, null, 2))}</pre></details>`; + } catch (error) { await tryTransaction(value, error); } +} + +async function tryTransaction(value, originalError) { + if (!/^[0-9a-f]{64}$/i.test(value)) return showError(originalError.message); + try { await showTransaction(value); } catch { showError('No block or transaction matched this hash.'); } +} + +async function showTransaction(hash) { + loading('Loading transaction…'); + const result = await api('transaction', {hash}); + const tx = result.txs?.[0] || {}; + let decoded = {}; + try { decoded = JSON.parse(tx.as_json || result.txs_as_json?.[0] || '{}'); } catch {} + headerTitle('Transaction', `<span class="truncate" title="${escapeHtml(hash)}">${escapeHtml(shortHash(hash, 16))}</span>`); + const vin = decoded.vin || []; + const vout = decoded.vout || []; + view.innerHTML = `<div class="detail-grid"> + ${datum('Transaction hash', `<code>${escapeHtml(hash)}</code>`)} + ${datum('Block height', tx.block_height != null ? `<a class="link" href="#/block/${tx.block_height}">${number.format(tx.block_height)}</a>` : '<strong>Unconfirmed</strong>')} + ${datum('Confirmations', `<strong>${number.format(tx.confirmations || 0)}</strong>`)} + ${datum('Fee', `<strong class="orange">${xmz(tx.fee || decoded.rct_signatures?.txnFee || 0)}</strong>`)} + ${datum('Size', `<strong>${number.format(tx.size || tx.weight || 0)} bytes</strong>`)} + ${datum('Version', `<strong>${decoded.version ?? '—'}</strong>`)} + ${datum('Inputs', `<strong>${number.format(vin.length)}</strong>`)} + ${datum('Outputs', `<strong>${number.format(vout.length)}</strong>`)} + ${datum('In pool', `<strong>${tx.in_pool ? 'Yes' : 'No'}</strong>`)} + </div><details class="raw" open><summary>Decoded transaction JSON</summary><pre>${escapeHtml(JSON.stringify(decoded, null, 2))}</pre></details>`; +} + +function datum(label, value) { return `<div class="datum"><span>${escapeHtml(label)}</span>${value}</div>`; } +function setOffline() { $('#node-dot').className = 'offline'; $('#node-label').textContent = 'Node unavailable'; } + +async function route() { + const route = location.hash.replace(/^#\/?/, '').split('/').filter(Boolean); + if (location.hash) content.scrollIntoView({behavior:'smooth', block:'start'}); + if (!route.length) return showBlocks(); + if (route[0] === 'block' && route[1]) return showBlock(route[1]); + if (route[0] === 'tx' && route[1]) { + try { return await showTransaction(route[1]); } catch (error) { return showError(error.message); } + } + showError(); +} + +$('#search-form').addEventListener('submit', (event) => { + event.preventDefault(); + const value = $('#search-input').value.trim(); + if (/^\d+$/.test(value) || /^[0-9a-f]{64}$/i.test(value)) location.hash = `#/block/${value}`; + else showError('Enter a numeric block height or a 64-character hexadecimal hash.'); +}); +$('#latest-search').addEventListener('click', () => { if (chainInfo?.height) location.hash = `#/block/${chainInfo.height - 1}`; }); +window.addEventListener('hashchange', route); +api('info').then(updateStats).catch(setOffline); +route(); +setInterval(() => api('info').then(updateStats).catch(setOffline), 30000); diff --git a/explorer/index.html b/explorer/index.html new file mode 100644 index 000000000..63a6e54b4 --- /dev/null +++ b/explorer/index.html @@ -0,0 +1,59 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="description" content="Monzero blockchain explorer"> + <meta name="theme-color" content="#090d0c"> + <title>Monzero Explorer</title> + <link rel="stylesheet" href="styles.css"> +</head> +<body> + <header> + <a class="brand" href="#/"> + <span class="mark">MZ</span> + <span><strong>MONZERO</strong><small>CHAIN EXPLORER</small></span> + </a> + <div class="node-pill"><span id="node-dot"></span><span id="node-label">Connecting</span></div> + </header> + + <main> + <section class="hero"> + <p class="eyebrow">Independent chain · XMZ</p> + <h1>Verify the chain.<br><em>Block by block.</em></h1> + <form id="search-form" class="search"> + <label class="sr-only" for="search-input">Search the blockchain</label> + <input id="search-input" autocomplete="off" spellcheck="false" placeholder="Block height, block hash, or transaction hash"> + <button type="submit">Search <span>↗</span></button> + </form> + <p class="search-hint">Try a block height such as <button type="button" id="latest-search">latest</button> or paste a 64-character hash.</p> + </section> + + <section id="overview" class="overview" aria-label="Network overview"> + <article><span>Chain height</span><strong id="stat-height">—</strong><small>Mainnet</small></article> + <article><span>Difficulty</span><strong id="stat-difficulty">—</strong><small id="stat-hashrate">— H/s estimated</small></article> + <article><span>Transactions</span><strong id="stat-transactions">—</strong><small>Confirmed on-chain</small></article> + <article><span>Connections</span><strong id="stat-connections">—</strong><small>Public seed node</small></article> + </section> + + <section id="content" class="content" aria-live="polite"> + <div class="section-title"> + <div><p class="eyebrow">Live ledger</p><h2>Recent blocks</h2></div> + <button id="refresh" class="ghost" type="button">Refresh</button> + </div> + <div id="view"><div class="loading"><span></span>Loading chain data…</div></div> + </section> + </main> + + <footer> + <span>Monzero Genesis network</span> + <span>Node: <code>node.monzero.org:6175</code></span> + <span>Read-only explorer · <a href="https://monzero.org">monzero.org</a></span> + </footer> + + <template id="error-template"> + <div class="empty"><strong>Nothing found</strong><p>The value could not be resolved on the current Monzero chain.</p><a href="#/">Return to recent blocks</a></div> + </template> + <script src="app.js"></script> +</body> +</html> diff --git a/explorer/styles.css b/explorer/styles.css new file mode 100644 index 000000000..c7cf1f573 --- /dev/null +++ b/explorer/styles.css @@ -0,0 +1,73 @@ +@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;500;600;700&display=swap'); + +:root { --bg:#090d0c; --panel:#0e1513; --panel2:#111a17; --line:#23312d; --text:#ecf5f1; --muted:#789087; --green:#b9ff66; --mint:#67e7b1; --orange:#ed8547; --mono:"DM Mono",monospace; --sans:"Manrope",sans-serif; } +* { box-sizing:border-box; } +html { scroll-behavior:smooth; } +body { margin:0; background:var(--bg); color:var(--text); font-family:var(--sans); min-height:100vh; } +body::before { content:""; position:fixed; inset:0; pointer-events:none; opacity:.16; background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='4'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.12'/%3E%3C/svg%3E"); z-index:10; } +a { color:inherit; } +button,input { font:inherit; } +header { height:78px; display:flex; align-items:center; justify-content:space-between; padding:0 clamp(1.2rem,5vw,5rem); border-bottom:1px solid var(--line); } +.brand { display:flex; gap:.75rem; align-items:center; color:inherit; text-decoration:none; } +.mark { width:34px; height:34px; display:grid; place-items:center; border:1px solid var(--green); border-radius:50%; color:var(--green); font:500 .65rem var(--mono); } +.brand > span:last-child { display:flex; flex-direction:column; line-height:1.15; letter-spacing:.16em; font-size:.75rem; } +.brand small { color:var(--muted); font:400 .48rem var(--mono); margin-top:.22rem; } +.node-pill { display:flex; align-items:center; gap:.6rem; border:1px solid var(--line); border-radius:2rem; padding:.5rem .8rem; color:var(--muted); font:400 .6rem var(--mono); text-transform:uppercase; letter-spacing:.08em; } +#node-dot { width:7px; height:7px; border-radius:50%; background:#e7b358; box-shadow:0 0 10px currentColor; } +#node-dot.online { background:var(--green); } +#node-dot.offline { background:#ee6c57; } +main { width:min(1480px,100%); margin:0 auto; } +.hero { padding:clamp(4rem,8vw,7rem) clamp(1.2rem,5vw,5rem) clamp(3rem,6vw,5rem); border-bottom:1px solid var(--line); position:relative; overflow:hidden; } +.hero::after { content:""; width:560px; height:560px; border:1px solid rgba(185,255,102,.12); border-radius:50%; position:absolute; right:-160px; top:-270px; box-shadow:0 0 100px rgba(103,231,177,.05); } +.eyebrow { color:var(--mint); font:500 .6rem var(--mono); letter-spacing:.18em; text-transform:uppercase; margin:0 0 1rem; } +h1 { margin:0 0 2.5rem; font-size:clamp(2.7rem,6vw,6rem); line-height:.96; letter-spacing:-.06em; font-weight:500; } +h1 em { color:var(--green); font-style:normal; } +.search { max-width:970px; display:grid; grid-template-columns:1fr auto; border:1px solid #344940; position:relative; z-index:1; background:rgba(11,17,15,.8); } +.search:focus-within { border-color:var(--mint); box-shadow:0 0 0 3px rgba(103,231,177,.08); } +.search input { min-width:0; height:62px; border:0; outline:0; background:transparent; color:var(--text); padding:0 1.2rem; font:400 .78rem var(--mono); } +.search button { border:0; margin:5px; padding:0 1.35rem; background:var(--green); color:#08100d; font-weight:700; cursor:pointer; } +.search button span { margin-left:.7rem; } +.search-hint { color:#566c64; font:400 .58rem var(--mono); } +.search-hint button { color:var(--mint); background:none; border:0; padding:0; cursor:pointer; text-decoration:underline; } +.overview { display:grid; grid-template-columns:repeat(4,1fr); padding:0 clamp(1.2rem,5vw,5rem); border-bottom:1px solid var(--line); } +.overview article { min-height:135px; display:flex; flex-direction:column; justify-content:center; padding:1.2rem 1.7rem; border-right:1px solid var(--line); } +.overview article:first-child { border-left:1px solid var(--line); } +.overview span { color:var(--muted); text-transform:uppercase; letter-spacing:.1em; font:400 .55rem var(--mono); } +.overview strong { color:var(--green); font:500 clamp(1.35rem,2.4vw,2.2rem) var(--mono); margin:.35rem 0; } +.overview small { color:#53675f; font:400 .55rem var(--mono); } +.content { padding:clamp(3.5rem,6vw,6rem) clamp(1.2rem,5vw,5rem); min-height:560px; } +.section-title { display:flex; align-items:end; justify-content:space-between; margin-bottom:1.8rem; } +h2 { margin:0; font-size:clamp(2rem,4vw,3.8rem); letter-spacing:-.05em; line-height:1; font-weight:500; } +.ghost { color:var(--mint); border:1px solid var(--line); background:transparent; padding:.6rem .9rem; font:500 .6rem var(--mono); cursor:pointer; } +.ghost:hover { border-color:var(--mint); } +.table-wrap { border:1px solid var(--line); overflow-x:auto; } +table { width:100%; border-collapse:collapse; min-width:800px; } +th { padding:.85rem 1rem; text-align:left; color:#566c64; text-transform:uppercase; letter-spacing:.1em; font:400 .52rem var(--mono); border-bottom:1px solid var(--line); } +td { padding:1rem; border-bottom:1px solid rgba(35,49,45,.65); font-size:.75rem; } +tbody tr:last-child td { border-bottom:0; } +tbody tr { transition:background .15s; } tbody tr:hover { background:rgba(185,255,102,.025); } +td code,.hash { font:400 .67rem var(--mono); } +.link { color:var(--mint); text-decoration:none; }.link:hover { text-decoration:underline; } +.truncate { display:inline-block; max-width:240px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; vertical-align:bottom; } +.muted { color:var(--muted); }.reward { color:var(--green); }.orange { color:var(--orange); } +.detail-head { display:flex; justify-content:space-between; align-items:flex-end; gap:2rem; margin-bottom:1.5rem; } +.detail-head h2 { overflow-wrap:anywhere; }.back { color:var(--mint); font:500 .6rem var(--mono); text-decoration:none; } +.detail-grid { display:grid; grid-template-columns:repeat(3,1fr); border:1px solid var(--line); margin-bottom:2rem; } +.datum { min-height:105px; padding:1.2rem; border-right:1px solid var(--line); border-bottom:1px solid var(--line); display:flex; flex-direction:column; justify-content:space-between; min-width:0; } +.datum:nth-child(3n) { border-right:0; } +.datum span { color:var(--muted); text-transform:uppercase; font:400 .52rem var(--mono); letter-spacing:.1em; } +.datum strong,.datum code { color:var(--text); font:500 .72rem var(--mono); overflow-wrap:anywhere; } +.datum strong.big { font-size:1.15rem; color:var(--green); } +.subheading { font-size:1rem; margin:2.5rem 0 1rem; } +.raw { margin-top:2rem; border:1px solid var(--line); } +.raw summary { cursor:pointer; padding:1rem; color:var(--muted); font:400 .62rem var(--mono); } +.raw pre { padding:1rem; margin:0; border-top:1px solid var(--line); overflow:auto; color:#8da39a; font:400 .6rem/1.7 var(--mono); } +.loading,.empty { min-height:300px; display:flex; flex-direction:column; align-items:center; justify-content:center; color:var(--muted); border:1px solid var(--line); text-align:center; } +.loading span { width:24px; height:24px; border:2px solid var(--line); border-top-color:var(--green); border-radius:50%; animation:spin .7s linear infinite; margin-bottom:1rem; } +@keyframes spin { to { transform:rotate(360deg); } } +.empty strong { color:var(--text); font-size:1.2rem; }.empty p { max-width:500px; }.empty a { color:var(--mint); } +footer { width:min(1480px,100%); margin:0 auto; border-top:1px solid var(--line); padding:2rem clamp(1.2rem,5vw,5rem); display:flex; justify-content:space-between; gap:1rem; color:#52665e; font:400 .55rem var(--mono); } +.sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; } +@media(max-width:850px){ .overview{grid-template-columns:1fr 1fr}.overview article:nth-child(odd){border-left:1px solid var(--line)}.detail-grid{grid-template-columns:1fr 1fr}.datum:nth-child(3n){border-right:1px solid var(--line)}.datum:nth-child(2n){border-right:0}footer{flex-direction:column}.hero::after{display:none} } +@media(max-width:520px){ header{padding-inline:1rem}.brand small{display:none}.node-pill{border:0;padding:0}.overview{grid-template-columns:1fr 1fr;padding:0}.overview article{padding:1rem;min-height:110px}.search{grid-template-columns:1fr}.search button{height:48px}.detail-grid{grid-template-columns:1fr}.datum,.datum:nth-child(3n){border-right:0}.detail-head{align-items:flex-start;flex-direction:column-reverse} } +@media(prefers-reduced-motion:reduce){*{animation-duration:.01ms!important;scroll-behavior:auto!important}} diff --git a/src/blockchain_utilities/blockchain_ancestry.cpp b/src/blockchain_utilities/blockchain_ancestry.cpp index 9de1e4420..76cfca94e 100644 --- a/src/blockchain_utilities/blockchain_ancestry.cpp +++ b/src/blockchain_utilities/blockchain_ancestry.cpp @@ -387,7 +387,7 @@ int main(int argc, char* argv[]) if (command_line::get_arg(vm, command_line::arg_help)) { - std::cout << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; + std::cout << "Monzero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; std::cout << desc_options << std::endl; return 1; } diff --git a/src/blockchain_utilities/blockchain_blackball.cpp b/src/blockchain_utilities/blockchain_blackball.cpp index dee0f7f2a..6dad7283a 100644 --- a/src/blockchain_utilities/blockchain_blackball.cpp +++ b/src/blockchain_utilities/blockchain_blackball.cpp @@ -1219,7 +1219,7 @@ int main(int argc, char* argv[]) if (command_line::get_arg(vm, command_line::arg_help)) { - std::cout << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; + std::cout << "Monzero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; std::cout << desc_options << std::endl; return 1; } diff --git a/src/blockchain_utilities/blockchain_depth.cpp b/src/blockchain_utilities/blockchain_depth.cpp index 0ea6d7df1..1642892bf 100644 --- a/src/blockchain_utilities/blockchain_depth.cpp +++ b/src/blockchain_utilities/blockchain_depth.cpp @@ -89,7 +89,7 @@ int main(int argc, char* argv[]) if (command_line::get_arg(vm, command_line::arg_help)) { - std::cout << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; + std::cout << "Monzero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; std::cout << desc_options << std::endl; return 1; } diff --git a/src/blockchain_utilities/blockchain_export.cpp b/src/blockchain_utilities/blockchain_export.cpp index 2b58124a3..5740294cd 100644 --- a/src/blockchain_utilities/blockchain_export.cpp +++ b/src/blockchain_utilities/blockchain_export.cpp @@ -91,7 +91,7 @@ int main(int argc, char* argv[]) if (command_line::get_arg(vm, command_line::arg_help)) { - std::cout << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; + std::cout << "Monzero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; std::cout << desc_options << std::endl; return 1; } diff --git a/src/blockchain_utilities/blockchain_import.cpp b/src/blockchain_utilities/blockchain_import.cpp index 58e152e4d..2de5baa3a 100644 --- a/src/blockchain_utilities/blockchain_import.cpp +++ b/src/blockchain_utilities/blockchain_import.cpp @@ -640,7 +640,7 @@ int main(int argc, char* argv[]) if (command_line::get_arg(vm, command_line::arg_help)) { - std::cout << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; + std::cout << "Monzero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; std::cout << desc_options << std::endl; return 1; } diff --git a/src/blockchain_utilities/blockchain_prune.cpp b/src/blockchain_utilities/blockchain_prune.cpp index d99a46632..d9adcd813 100644 --- a/src/blockchain_utilities/blockchain_prune.cpp +++ b/src/blockchain_utilities/blockchain_prune.cpp @@ -484,7 +484,7 @@ int main(int argc, char* argv[]) if (command_line::get_arg(vm, command_line::arg_help)) { - std::cout << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; + std::cout << "Monzero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; std::cout << desc_options << std::endl; return 1; } diff --git a/src/blockchain_utilities/blockchain_prune_known_spent_data.cpp b/src/blockchain_utilities/blockchain_prune_known_spent_data.cpp index 08eb6ee0e..b59326f5f 100644 --- a/src/blockchain_utilities/blockchain_prune_known_spent_data.cpp +++ b/src/blockchain_utilities/blockchain_prune_known_spent_data.cpp @@ -138,7 +138,7 @@ int main(int argc, char* argv[]) if (command_line::get_arg(vm, command_line::arg_help)) { - std::cout << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; + std::cout << "Monzero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; std::cout << desc_options << std::endl; return 1; } diff --git a/src/blockchain_utilities/blockchain_stats.cpp b/src/blockchain_utilities/blockchain_stats.cpp index fc082b4e5..9795fcf94 100644 --- a/src/blockchain_utilities/blockchain_stats.cpp +++ b/src/blockchain_utilities/blockchain_stats.cpp @@ -176,7 +176,7 @@ int main(int argc, char* argv[]) if (command_line::get_arg(vm, command_line::arg_help)) { - std::cout << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; + std::cout << "Monzero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; std::cout << desc_options << std::endl; return 1; } diff --git a/src/blockchain_utilities/blockchain_usage.cpp b/src/blockchain_utilities/blockchain_usage.cpp index 1cc785283..5e6c38e81 100644 --- a/src/blockchain_utilities/blockchain_usage.cpp +++ b/src/blockchain_utilities/blockchain_usage.cpp @@ -121,7 +121,7 @@ int main(int argc, char* argv[]) if (command_line::get_arg(vm, command_line::arg_help)) { - std::cout << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; + std::cout << "Monzero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; std::cout << desc_options << std::endl; return 1; } diff --git a/src/checkpoints/checkpoints.cpp b/src/checkpoints/checkpoints.cpp index 360e812b4..fddaf251b 100644 --- a/src/checkpoints/checkpoints.cpp +++ b/src/checkpoints/checkpoints.cpp @@ -182,6 +182,11 @@ namespace cryptonote bool checkpoints::init_default_checkpoints(network_type nettype) { + // Monero checkpoints are invalid on the independent Monzero chain. New + // checkpoints will be published from audited Monzero release heights. + (void)nettype; + return true; +#if 0 if (nettype == TESTNET) { ADD_CHECKPOINT2(0, "48ca7cd3c8de5b6a4d53d2861fbdaedca141553559f9be9520068053cda8430b", "0x1"); @@ -261,6 +266,7 @@ namespace cryptonote ADD_CHECKPOINT2(3661900, "ac392757a92123f68d63cd72f0d1410f63df1102a53b5d39fc4d53d0998b20a3", "0x8a38f2195826a97"); ADD_CHECKPOINT2(3707000, "9c508fe29120b5cd204f9d150e68fd2e4015d8d859bc0431f7016c7aabc711c9", "0x91129586d4979a6"); return true; +#endif } bool checkpoints::load_checkpoints_from_json(const std::string &json_hashfile_fullpath) @@ -301,6 +307,11 @@ namespace cryptonote bool checkpoints::load_checkpoints_from_dns(network_type nettype) { + // MoneroPulse records belong to the Monero chain. Monzero DNS + // checkpoints remain disabled until project-controlled records exist. + (void)nettype; + return true; +#if 0 std::vector<std::string> records; // All four MoneroPulse domains have DNSSEC on and valid @@ -353,6 +364,7 @@ namespace cryptonote } } return true; +#endif } bool checkpoints::load_new_checkpoints(const std::string &json_hashfile_fullpath, network_type nettype, bool dns) diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index b712ee6b1..2670182e3 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -67,6 +67,10 @@ monero_add_library(common ${common_headers} ${common_private_headers} DEPENDS generate_translations_header) +if(STATIC AND NOT MINGW) + find_package(ZLIB REQUIRED) + find_library(ZSTD_LIBRARY NAMES zstd REQUIRED) +endif() target_link_libraries(common PUBLIC cncrypto @@ -80,7 +84,9 @@ target_link_libraries(common ${Boost_CHRONO_LIBRARY} PRIVATE ${OPENSSL_LIBRARIES} - ${EXTRA_LIBRARIES}) + ${EXTRA_LIBRARIES} + $<$<AND:$<BOOL:${STATIC}>,$<NOT:$<BOOL:${MINGW}>>>:ZLIB::ZLIB> + $<$<AND:$<BOOL:${STATIC}>,$<NOT:$<BOOL:${MINGW}>>>:${ZSTD_LIBRARY}>) #monero_install_headers(common # ${common_headers}) diff --git a/src/common/dns_utils.cpp b/src/common/dns_utils.cpp index 40e913800..ef60a3f67 100644 --- a/src/common/dns_utils.cpp +++ b/src/common/dns_utils.cpp @@ -289,26 +289,10 @@ DNSResolver::DNSResolver() : m_data(new DNSResolverData()) add_anchors(m_data->m_ub_context); - if (!DNS_PUBLIC) - { - // if no DNS_PUBLIC specified, we try a lookup to what we know - // should be a valid DNSSEC record, and switch to known good - // DNSSEC resolvers if verification fails - bool available, valid; - static const char *probe_hostname = "updates.moneropulse.org"; - auto records = get_txt_record(probe_hostname, available, valid); - if (!valid) - { - MINFO("Failed to verify DNSSEC record from " << probe_hostname << ", falling back to TCP with well known DNSSEC resolvers"); - ub_ctx_delete(m_data->m_ub_context); - m_data->m_ub_context = ub_ctx_create(); - add_anchors(m_data->m_ub_context); - for (const auto &ip: DEFAULT_DNS_PUBLIC_ADDR) - ub_ctx_set_fwd(m_data->m_ub_context, string_copy(ip)); - ub_ctx_set_option(m_data->m_ub_context, string_copy("do-udp:"), string_copy("no")); - ub_ctx_set_option(m_data->m_ub_context, string_copy("do-tcp:"), string_copy("yes")); - } - } + // The upstream client probes a Monero-operated, DNSSEC-signed TXT record + // here and switches to hard-coded resolvers when validation fails. Monzero + // has no equivalent signed record yet, so use the operator's configured + // resolver. Clearnet bootstrap also has a compiled direct-IP fallback. } DNSResolver::~DNSResolver() diff --git a/src/common/updates.cpp b/src/common/updates.cpp index 2a129075b..b68be39b6 100644 --- a/src/common/updates.cpp +++ b/src/common/updates.cpp @@ -39,6 +39,15 @@ namespace tools { bool check_updates(const std::string &software, const std::string &buildtag, std::string &version, std::string &hash) { + // Do not trust or advertise Monero release records for Monzero binaries. + // Re-enable only after project-controlled, signed update infrastructure + // has been reviewed and deployed. + (void)software; + (void)buildtag; + (void)version; + (void)hash; + return false; +#if 0 std::vector<std::string> records; bool found = false; @@ -98,6 +107,7 @@ namespace tools found = true; } return found; +#endif } std::string get_update_url(const std::string &software, const std::string &subdir, const std::string &buildtag, const std::string &version, bool user) diff --git a/src/cryptonote_basic/CMakeLists.txt b/src/cryptonote_basic/CMakeLists.txt index 1414be1b2..91612168c 100644 --- a/src/cryptonote_basic/CMakeLists.txt +++ b/src/cryptonote_basic/CMakeLists.txt @@ -46,6 +46,7 @@ target_link_libraries(cryptonote_format_utils_basic set(cryptonote_basic_sources account.cpp + asset_types.cpp connection_context.cpp cryptonote_basic_impl.cpp cryptonote_format_utils.cpp diff --git a/src/cryptonote_basic/asset_types.cpp b/src/cryptonote_basic/asset_types.cpp new file mode 100644 index 000000000..faac669d0 --- /dev/null +++ b/src/cryptonote_basic/asset_types.cpp @@ -0,0 +1,314 @@ +#include "asset_types.h" + +#include <algorithm> +#include <cstring> +#include <limits> +#include <map> + +#include "crypto/hash.h" + +namespace cryptonote +{ +namespace assets +{ + namespace + { + constexpr char DOMAIN[] = "MonzeroAssetIssuanceV2"; + constexpr char ISSUANCE_AUTHORIZATION_DOMAIN[] = "MonzeroAssetIssuanceAuthorizationV1"; + constexpr char COLLECTION_MEMBERSHIP_DOMAIN[] = "MonzeroCollectionMembershipV1"; + + bool fail(std::string* error, const char* message) + { + if (error) + *error = message; + return false; + } + + template<typename T> + void append_pod(std::vector<uint8_t>& target, const T& value) + { + const auto* begin = reinterpret_cast<const uint8_t*>(&value); + target.insert(target.end(), begin, begin + sizeof(value)); + } + + void append_u16_le(std::vector<uint8_t>& target, uint16_t value) + { + target.push_back(static_cast<uint8_t>(value)); + target.push_back(static_cast<uint8_t>(value >> 8)); + } + + void append_u64_le(std::vector<uint8_t>& target, uint64_t value) + { + for (unsigned shift = 0; shift < 64; shift += 8) + target.push_back(static_cast<uint8_t>(value >> shift)); + } + } + + bool validate_issuance_descriptor(const issuance_descriptor& descriptor, std::string* error) + { + if (descriptor.version != ISSUANCE_DESCRIPTOR_VERSION) + return fail(error, "unsupported issuance descriptor version"); + if (descriptor.network != MAINNET && descriptor.network != TESTNET && descriptor.network != STAGENET) + return fail(error, "issuance descriptor requires an explicit public network"); + if (descriptor.type != asset_class::fungible + && descriptor.type != asset_class::non_fungible + && descriptor.type != asset_class::collection + && descriptor.type != asset_class::edition) + return fail(error, "unsupported asset class"); + if (!crypto::check_key(descriptor.issuer_key)) + return fail(error, "invalid issuer public key"); + if (descriptor.atomic_supply == 0) + return fail(error, "asset supply must be greater than zero"); + if (descriptor.display_decimals > MAX_DISPLAY_DECIMALS) + return fail(error, "asset display precision exceeds XMZ precision"); + if ((descriptor.type == asset_class::non_fungible || descriptor.type == asset_class::collection) + && (descriptor.atomic_supply != 1 || descriptor.display_decimals != 0)) + return fail(error, "NFT and collection supply must be exactly one indivisible unit"); + if (descriptor.type == asset_class::edition && descriptor.display_decimals != 0) + return fail(error, "edition supply must be indivisible"); + if ((descriptor.type == asset_class::fungible || descriptor.type == asset_class::collection) + && descriptor.collection_id != crypto::null_hash) + return fail(error, "this asset class cannot reference a collection"); + if ((descriptor.type == asset_class::non_fungible || descriptor.type == asset_class::edition) + && descriptor.metadata_hash == crypto::null_hash) + return fail(error, "NFT and edition metadata must have a content hash"); + if (descriptor.metadata_reference.size() > MAX_METADATA_REFERENCE_BYTES) + return fail(error, "metadata reference is too long"); + if (std::find(descriptor.metadata_reference.begin(), descriptor.metadata_reference.end(), '\0') != descriptor.metadata_reference.end()) + return fail(error, "metadata reference contains a NUL byte"); + return true; + } + + bool encode_issuance_descriptor(const issuance_descriptor& descriptor, std::vector<uint8_t>& encoded, std::string* error) + { + if (!validate_issuance_descriptor(descriptor, error)) + return false; + + encoded.clear(); + encoded.reserve(sizeof(DOMAIN) - 1 + 1 + 16 + 1 + 32 + 32 + 8 + 1 + 32 + 32 + 2 + descriptor.metadata_reference.size()); + encoded.insert(encoded.end(), DOMAIN, DOMAIN + sizeof(DOMAIN) - 1); + encoded.push_back(descriptor.version); + const config_t& network = get_config(descriptor.network); + append_pod(encoded, network.NETWORK_ID); + encoded.push_back(static_cast<uint8_t>(descriptor.type)); + append_pod(encoded, descriptor.issuer_key); + append_pod(encoded, descriptor.issuance_nonce); + append_u64_le(encoded, descriptor.atomic_supply); + encoded.push_back(descriptor.display_decimals); + append_pod(encoded, descriptor.metadata_hash); + append_pod(encoded, descriptor.collection_id); + append_u16_le(encoded, static_cast<uint16_t>(descriptor.metadata_reference.size())); + encoded.insert(encoded.end(), descriptor.metadata_reference.begin(), descriptor.metadata_reference.end()); + return true; + } + + bool derive_asset_id(const issuance_descriptor& descriptor, crypto::hash& asset_id, std::string* error) + { + std::vector<uint8_t> encoded; + if (!encode_issuance_descriptor(descriptor, encoded, error)) + return false; + asset_id = crypto::cn_fast_hash(encoded.data(), encoded.size()); + return true; + } + + bool derive_issuance_authorization_hash(const issuance_descriptor& descriptor, crypto::hash& message, std::string* error) + { + std::vector<uint8_t> encoded; + if (!encode_issuance_descriptor(descriptor, encoded, error)) + return false; + std::vector<uint8_t> authorization; + authorization.reserve(sizeof(ISSUANCE_AUTHORIZATION_DOMAIN) - 1 + encoded.size()); + authorization.insert(authorization.end(), ISSUANCE_AUTHORIZATION_DOMAIN, + ISSUANCE_AUTHORIZATION_DOMAIN + sizeof(ISSUANCE_AUTHORIZATION_DOMAIN) - 1); + authorization.insert(authorization.end(), encoded.begin(), encoded.end()); + message = crypto::cn_fast_hash(authorization.data(), authorization.size()); + return true; + } + + bool verify_issuance_authorization(const issuance_descriptor& descriptor, const crypto::signature& signature, std::string* error) + { + crypto::hash message{}; + if (!derive_issuance_authorization_hash(descriptor, message, error)) + return false; + if (!crypto::check_signature(message, descriptor.issuer_key, signature)) + return fail(error, "invalid asset issuer authorization signature"); + return true; + } + + bool derive_collection_membership_hash(const crypto::hash& collection_id, const crypto::hash& member_asset_id, crypto::hash& message, std::string* error) + { + if (collection_id == crypto::null_hash || member_asset_id == crypto::null_hash) + return fail(error, "collection and member asset IDs must be non-zero"); + std::vector<uint8_t> encoded; + encoded.reserve(sizeof(COLLECTION_MEMBERSHIP_DOMAIN) - 1 + sizeof(collection_id) + sizeof(member_asset_id)); + encoded.insert(encoded.end(), COLLECTION_MEMBERSHIP_DOMAIN, + COLLECTION_MEMBERSHIP_DOMAIN + sizeof(COLLECTION_MEMBERSHIP_DOMAIN) - 1); + append_pod(encoded, collection_id); + append_pod(encoded, member_asset_id); + message = crypto::cn_fast_hash(encoded.data(), encoded.size()); + return true; + } + + bool verify_collection_membership( + const crypto::hash& collection_id, + const crypto::hash& member_asset_id, + const crypto::public_key& collection_controller, + const crypto::signature& signature, + std::string* error) + { + if (!crypto::check_key(collection_controller)) + return fail(error, "invalid collection controller public key"); + crypto::hash message{}; + if (!derive_collection_membership_hash(collection_id, member_asset_id, message, error)) + return false; + if (!crypto::check_signature(message, collection_controller, signature)) + return fail(error, "invalid collection membership signature"); + return true; + } + + bool asset_registry::apply_issuance( + const issuance_descriptor& descriptor, + const crypto::signature& issuer_signature, + const boost::optional<crypto::signature>& collection_signature, + uint64_t height, + crypto::hash& asset_id, + std::string* error) + { + if (!derive_asset_id(descriptor, asset_id, error)) + return false; + if (contains(asset_id)) + return fail(error, "asset ID is already registered"); + if (!verify_issuance_authorization(descriptor, issuer_signature, error)) + return false; + + const bool claims_collection = descriptor.collection_id != crypto::null_hash; + if (claims_collection) + { + const asset_record* collection = find(descriptor.collection_id); + if (!collection || collection->descriptor.type != asset_class::collection) + return fail(error, "referenced collection is unknown or not a collection"); + if (collection->descriptor.network != descriptor.network) + return fail(error, "collection and member must use the same network"); + if (collection->issuance_height > height) + return fail(error, "collection membership cannot precede collection issuance"); + if (!collection_signature) + return fail(error, "collection membership requires controller authorization"); + if (!verify_collection_membership( + descriptor.collection_id, + asset_id, + collection->descriptor.issuer_key, + *collection_signature, + error)) + return false; + } + else if (collection_signature) + return fail(error, "unexpected collection signature for an uncollected asset"); + + records_.emplace(asset_id, asset_record{descriptor, height}); + return true; + } + + void asset_registry::detach(uint64_t height) + { + for (auto it = records_.begin(); it != records_.end();) + { + if (it->second.issuance_height >= height) + it = records_.erase(it); + else + ++it; + } + } + + bool asset_registry::contains(const crypto::hash& asset_id) const + { + return records_.count(asset_id) != 0; + } + + const asset_record* asset_registry::find(const crypto::hash& asset_id) const + { + const auto found = records_.find(asset_id); + return found == records_.end() ? nullptr : &found->second; + } + + std::set<crypto::hash> asset_registry::known_assets() const + { + std::set<crypto::hash> result; + for (const auto& record : records_) + result.insert(record.first); + return result; + } + + bool validate_transparent_balance_statement( + const transparent_balance_statement& statement, + const std::set<crypto::hash>& known_assets, + std::string* error) + { + if (statement.xmz_outputs > std::numeric_limits<uint64_t>::max() - statement.xmz_fee + || statement.xmz_inputs != statement.xmz_outputs + statement.xmz_fee) + return fail(error, "XMZ inputs must equal XMZ outputs plus the XMZ fee"); + + using balances = std::map<crypto::hash, uint64_t>; + balances inputs; + balances outputs; + balances burns; + + const auto aggregate = [error](const std::vector<transparent_amount>& entries, balances& totals) { + for (const transparent_amount& entry : entries) + { + if (entry.asset_id == crypto::null_hash) + return fail(error, "the zero asset ID is reserved and invalid"); + if (entry.amount == 0) + return fail(error, "zero-valued asset entries are not canonical"); + uint64_t& total = totals[entry.asset_id]; + if (entry.amount > std::numeric_limits<uint64_t>::max() - total) + return fail(error, "asset amount aggregation overflow"); + total += entry.amount; + } + return true; + }; + + if (!aggregate(statement.asset_inputs, inputs) + || !aggregate(statement.asset_outputs, outputs) + || !aggregate(statement.asset_burns, burns)) + return false; + + boost::optional<crypto::hash> issued_asset; + if (statement.issuance) + { + crypto::hash asset_id{}; + if (!derive_asset_id(*statement.issuance, asset_id, error)) + return false; + if (known_assets.count(asset_id) != 0) + return fail(error, "asset ID is already issued"); + if (inputs.count(asset_id) != 0 || burns.count(asset_id) != 0) + return fail(error, "newly issued supply cannot have inputs or burns"); + const auto issued_outputs = outputs.find(asset_id); + if (issued_outputs == outputs.end() || issued_outputs->second != statement.issuance->atomic_supply) + return fail(error, "issuance outputs must equal the complete fixed supply"); + issued_asset = asset_id; + } + + std::set<crypto::hash> touched; + for (const auto& entry : inputs) touched.insert(entry.first); + for (const auto& entry : outputs) touched.insert(entry.first); + for (const auto& entry : burns) touched.insert(entry.first); + if (touched.empty()) + return fail(error, "asset statement contains no issuance, transfer, or burn"); + + for (const crypto::hash& asset_id : touched) + { + if (issued_asset && asset_id == *issued_asset) + continue; + if (known_assets.count(asset_id) == 0) + return fail(error, "statement references an unknown asset ID"); + + const uint64_t input = inputs.count(asset_id) ? inputs[asset_id] : 0; + const uint64_t output = outputs.count(asset_id) ? outputs[asset_id] : 0; + const uint64_t burn = burns.count(asset_id) ? burns[asset_id] : 0; + if (output > std::numeric_limits<uint64_t>::max() - burn || input != output + burn) + return fail(error, "asset inputs must equal outputs plus explicit burns"); + } + return true; + } +} +} diff --git a/src/cryptonote_basic/asset_types.h b/src/cryptonote_basic/asset_types.h new file mode 100644 index 000000000..e24f61576 --- /dev/null +++ b/src/cryptonote_basic/asset_types.h @@ -0,0 +1,116 @@ +#pragma once + +#include <cstdint> +#include <map> +#include <set> +#include <string> +#include <vector> + +#include <boost/optional.hpp> + +#include "crypto/crypto.h" +#include "cryptonote_config.h" + +namespace cryptonote +{ +namespace assets +{ + constexpr uint8_t ISSUANCE_DESCRIPTOR_VERSION = 2; + constexpr uint8_t MAX_DISPLAY_DECIMALS = CRYPTONOTE_DISPLAY_DECIMAL_POINT; + constexpr size_t MAX_METADATA_REFERENCE_BYTES = 256; + + enum class asset_class : uint8_t + { + fungible = 1, + non_fungible = 2, + collection = 3, + edition = 4 + }; + + // Research-only fixed-supply issuance identity. This type is deliberately + // not part of transaction serialization and is not accepted by consensus. + struct issuance_descriptor + { + uint8_t version = ISSUANCE_DESCRIPTOR_VERSION; + network_type network = UNDEFINED; + asset_class type = asset_class::fungible; + crypto::public_key issuer_key{}; + crypto::hash issuance_nonce{}; + uint64_t atomic_supply = 0; + uint8_t display_decimals = 0; + crypto::hash metadata_hash{}; + // Required for NFTs/editions that claim collection membership. The zero + // hash means no collection; fungible and collection descriptors must use + // zero so the field cannot acquire ambiguous meaning. + crypto::hash collection_id{}; + std::string metadata_reference; + }; + + bool validate_issuance_descriptor(const issuance_descriptor& descriptor, std::string* error = nullptr); + bool encode_issuance_descriptor(const issuance_descriptor& descriptor, std::vector<uint8_t>& encoded, std::string* error = nullptr); + bool derive_asset_id(const issuance_descriptor& descriptor, crypto::hash& asset_id, std::string* error = nullptr); + bool derive_issuance_authorization_hash(const issuance_descriptor& descriptor, crypto::hash& message, std::string* error = nullptr); + bool verify_issuance_authorization(const issuance_descriptor& descriptor, const crypto::signature& signature, std::string* error = nullptr); + bool derive_collection_membership_hash(const crypto::hash& collection_id, const crypto::hash& member_asset_id, crypto::hash& message, std::string* error = nullptr); + bool verify_collection_membership( + const crypto::hash& collection_id, + const crypto::hash& member_asset_id, + const crypto::public_key& collection_controller, + const crypto::signature& signature, + std::string* error = nullptr); + + struct asset_record + { + issuance_descriptor descriptor; + uint64_t issuance_height = 0; + }; + + // Inactive in-memory reference model for authenticated issuance state and + // deterministic reorg rollback. Production consensus/database code must not + // use this class without a separately reviewed activation change. + class asset_registry + { + public: + bool apply_issuance( + const issuance_descriptor& descriptor, + const crypto::signature& issuer_signature, + const boost::optional<crypto::signature>& collection_signature, + uint64_t height, + crypto::hash& asset_id, + std::string* error = nullptr); + void detach(uint64_t height); + bool contains(const crypto::hash& asset_id) const; + const asset_record* find(const crypto::hash& asset_id) const; + size_t size() const { return records_.size(); } + std::set<crypto::hash> known_assets() const; + + private: + std::map<crypto::hash, asset_record> records_; + }; + + struct transparent_amount + { + crypto::hash asset_id{}; + uint64_t amount = 0; + }; + + // Test-only semantic statement used to validate conservation rules before a + // confidential commitment/proof construction is selected. It is not a wire + // transaction and must never be serialized into the public protocol. + struct transparent_balance_statement + { + uint64_t xmz_inputs = 0; + uint64_t xmz_outputs = 0; + uint64_t xmz_fee = 0; + boost::optional<issuance_descriptor> issuance; + std::vector<transparent_amount> asset_inputs; + std::vector<transparent_amount> asset_outputs; + std::vector<transparent_amount> asset_burns; + }; + + bool validate_transparent_balance_statement( + const transparent_balance_statement& statement, + const std::set<crypto::hash>& known_assets, + std::string* error = nullptr); +} +} diff --git a/src/cryptonote_basic/cryptonote_format_utils.cpp b/src/cryptonote_basic/cryptonote_format_utils.cpp index 923ad64af..2d8f6e2af 100644 --- a/src/cryptonote_basic/cryptonote_format_utils.cpp +++ b/src/cryptonote_basic/cryptonote_format_utils.cpp @@ -1131,6 +1131,10 @@ namespace cryptonote { switch (decimal_point) { + case 11: + case 8: + case 5: + case 2: case 12: case 9: case 6: @@ -1154,6 +1158,16 @@ namespace cryptonote decimal_point = default_decimal_point; switch (decimal_point) { + case 11: + return "XMZ"; + case 8: + return "mXMZ"; + case 5: + return "uXMZ"; + case 2: + return "nXMZ"; + case 0: + return "atomic XMZ"; case 12: return "monero"; case 9: @@ -1162,8 +1176,6 @@ namespace cryptonote return "micronero"; case 3: return "nanonero"; - case 0: - return "piconero"; default: ASSERT_MES_AND_THROW("Invalid decimal point specification: " << decimal_point); } diff --git a/src/cryptonote_config.h b/src/cryptonote_config.h index 82891b9de..3c74f12c1 100644 --- a/src/cryptonote_config.h +++ b/src/cryptonote_config.h @@ -51,9 +51,9 @@ #define BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW 60 // MONEY_SUPPLY - total number coins to be generated -#define MONEY_SUPPLY ((uint64_t)(-1)) +#define MONEY_SUPPLY UINT64_C(10000000000000000000) // 100,000,000 XMZ #define EMISSION_SPEED_FACTOR_PER_MINUTE (20) -#define FINAL_SUBSIDY_PER_MINUTE ((uint64_t)300000000000) // 3 * pow(10, 11) +#define FINAL_SUBSIDY_PER_MINUTE UINT64_C(163000000000) // 1.63 XMZ/minute = 3.26 XMZ per 2-minute block #define CRYPTONOTE_REWARD_BLOCKS_WINDOW 100 #define CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V2 60000 //size of block (bytes) after which reward for block calculated using block size @@ -62,23 +62,23 @@ #define CRYPTONOTE_LONG_TERM_BLOCK_WEIGHT_WINDOW_SIZE 100000 // size in blocks of the long term block weight median window #define CRYPTONOTE_SHORT_TERM_BLOCK_WEIGHT_SURGE_FACTOR 50 #define CRYPTONOTE_COINBASE_BLOB_RESERVED_SIZE 600 -#define CRYPTONOTE_DISPLAY_DECIMAL_POINT 12 +#define CRYPTONOTE_DISPLAY_DECIMAL_POINT 11 // COIN - number of smallest units in one coin -#define COIN ((uint64_t)1000000000000) // pow(10, 12) +#define COIN UINT64_C(100000000000) // pow(10, 11) -#define FEE_PER_KB_OLD ((uint64_t)10000000000) // pow(10, 10) -#define FEE_PER_KB ((uint64_t)2000000000) // 2 * pow(10, 9) -#define FEE_PER_BYTE ((uint64_t)300000) -#define DYNAMIC_FEE_PER_KB_BASE_FEE ((uint64_t)2000000000) // 2 * pow(10,9) -#define DYNAMIC_FEE_PER_KB_BASE_BLOCK_REWARD ((uint64_t)10000000000000) // 10 * pow(10,12) -#define DYNAMIC_FEE_PER_KB_BASE_FEE_V5 ((uint64_t)2000000000 * (uint64_t)CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V2 / CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5) +#define FEE_PER_KB_OLD UINT64_C(1000000000) // scaled for 11 decimals +#define FEE_PER_KB UINT64_C(200000000) // scaled for 11 decimals +#define FEE_PER_BYTE UINT64_C(30000) +#define DYNAMIC_FEE_PER_KB_BASE_FEE UINT64_C(200000000) // scaled for 11 decimals +#define DYNAMIC_FEE_PER_KB_BASE_BLOCK_REWARD UINT64_C(1000000000000) // 10 XMZ +#define DYNAMIC_FEE_PER_KB_BASE_FEE_V5 (UINT64_C(200000000) * (uint64_t)CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V2 / CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5) #define DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT ((uint64_t)3000) #define ORPHANED_BLOCKS_MAX_COUNT 100 #define DIFFICULTY_TARGET_V2 120 // seconds -#define DIFFICULTY_TARGET_V1 60 // seconds - before first fork +#define DIFFICULTY_TARGET_V1 120 // seconds - Monzero uses two-minute blocks from genesis #define DIFFICULTY_WINDOW 720 // blocks #define DIFFICULTY_LAG 15 // !!! #define DIFFICULTY_CUT 60 // timestamps to cut after sorting @@ -162,7 +162,7 @@ #define RPC_IP_FAILS_BEFORE_BLOCK 3 -#define CRYPTONOTE_NAME "bitmonero" +#define CRYPTONOTE_NAME "monzero" #define CRYPTONOTE_BLOCKCHAINDATA_FILENAME "data.mdb" #define CRYPTONOTE_BLOCKCHAINDATA_LOCK_FILENAME "lock.mdb" #define P2P_NET_DATA_FILENAME "p2pstate.bin" @@ -221,20 +221,20 @@ namespace config { uint64_t const DEFAULT_FEE_ATOMIC_XMR_PER_KB = 500; // Just a placeholder! Change me! uint8_t const FEE_CALCULATION_MAX_RETRIES = 10; - uint64_t const DEFAULT_DUST_THRESHOLD = ((uint64_t)2000000000); // 2 * pow(10, 9) - uint64_t const BASE_REWARD_CLAMP_THRESHOLD = ((uint64_t)100000000); // pow(10, 8) + uint64_t const DEFAULT_DUST_THRESHOLD = UINT64_C(200000000); // scaled for 11 decimals + uint64_t const BASE_REWARD_CLAMP_THRESHOLD = UINT64_C(10000000); // scaled for 11 decimals - uint64_t const CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX = 18; - uint64_t const CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX = 19; - uint64_t const CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX = 42; - uint16_t const P2P_DEFAULT_PORT = 18080; - uint16_t const RPC_DEFAULT_PORT = 18081; - uint16_t const ZMQ_RPC_DEFAULT_PORT = 18082; + uint64_t const CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX = 86; + uint64_t const CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX = 87; + uint64_t const CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX = 88; + uint16_t const P2P_DEFAULT_PORT = 6174; + uint16_t const RPC_DEFAULT_PORT = 6175; + uint16_t const ZMQ_RPC_DEFAULT_PORT = 6176; boost::uuids::uuid const NETWORK_ID = { { - 0x12 ,0x30, 0xF1, 0x71 , 0x61, 0x04 , 0x41, 0x61, 0x17, 0x31, 0x00, 0x82, 0x16, 0xA1, 0xA1, 0x10 - } }; // Bender's nightmare - std::string const GENESIS_TX = "013c01ff0001ffffffffffff03029b2e4c0281c0b02e7c53291a94d1d0cbff8883f8024f5142ee494ffbbd08807121017767aafcde9be00dcfd098715ebcf7f410daebc582fda69d24a28e9d0bc890d1"; - uint32_t const GENESIS_NONCE = 10000; + 0xfc, 0x32, 0x91, 0xba, 0x86, 0x66, 0x4b, 0x87, 0x8e, 0xbb, 0xd5, 0x9b, 0x72, 0xd4, 0xfc, 0xaa + } }; // Monzero mainnet + std::string const GENESIS_TX = "013c01ff0001bda282a38eab04029b2e4c0281c0b02e7c53291a94d1d0cbff8883f8024f5142ee494ffbbd08807121017767aafcde9be00dcfd098715ebcf7f410daebc582fda69d24a28e9d0bc890d1"; + uint32_t const GENESIS_NONCE = 818137480; // Hash domain separators const char HASH_KEY_BULLETPROOF_EXPONENT[] = "bulletproof"; @@ -256,7 +256,7 @@ namespace config const unsigned char HASH_KEY_CLSAG_ROUND[] = "CLSAG_round"; const unsigned char HASH_KEY_CLSAG_AGG_0[] = "CLSAG_agg_0"; const unsigned char HASH_KEY_CLSAG_AGG_1[] = "CLSAG_agg_1"; - const char HASH_KEY_MESSAGE_SIGNING[] = "MoneroMessageSignature"; + const char HASH_KEY_MESSAGE_SIGNING[] = "MonzeroMessageSignature"; const unsigned char HASH_KEY_MM_SLOT = 'm'; const constexpr char HASH_KEY_MULTISIG_TX_PRIVKEYS_SEED[] = "multisig_tx_privkeys_seed"; const constexpr char HASH_KEY_MULTISIG_TX_PRIVKEYS[] = "multisig_tx_privkeys"; @@ -267,32 +267,32 @@ namespace config namespace testnet { - uint64_t const CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX = 53; - uint64_t const CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX = 54; - uint64_t const CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX = 63; - uint16_t const P2P_DEFAULT_PORT = 28080; - uint16_t const RPC_DEFAULT_PORT = 28081; - uint16_t const ZMQ_RPC_DEFAULT_PORT = 28082; + uint64_t const CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX = 111; + uint64_t const CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX = 112; + uint64_t const CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX = 113; + uint16_t const P2P_DEFAULT_PORT = 16174; + uint16_t const RPC_DEFAULT_PORT = 16175; + uint16_t const ZMQ_RPC_DEFAULT_PORT = 16176; boost::uuids::uuid const NETWORK_ID = { { - 0x12 ,0x30, 0xF1, 0x71 , 0x61, 0x04 , 0x41, 0x61, 0x17, 0x31, 0x00, 0x82, 0x16, 0xA1, 0xA1, 0x11 - } }; // Bender's daydream - std::string const GENESIS_TX = "013c01ff0001ffffffffffff03029b2e4c0281c0b02e7c53291a94d1d0cbff8883f8024f5142ee494ffbbd08807121017767aafcde9be00dcfd098715ebcf7f410daebc582fda69d24a28e9d0bc890d1"; - uint32_t const GENESIS_NONCE = 10001; + 0xad, 0xc2, 0xa2, 0x71, 0x55, 0x39, 0x43, 0xd9, 0xb3, 0xc2, 0x7a, 0xad, 0xaf, 0x6b, 0x93, 0x8c + } }; // Monzero testnet + std::string const GENESIS_TX = "013c01ff0001bda282a38eab04029b2e4c0281c0b02e7c53291a94d1d0cbff8883f8024f5142ee494ffbbd08807121017767aafcde9be00dcfd098715ebcf7f410daebc582fda69d24a28e9d0bc890d1"; + uint32_t const GENESIS_NONCE = 818137481; } namespace stagenet { - uint64_t const CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX = 24; - uint64_t const CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX = 25; - uint64_t const CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX = 36; - uint16_t const P2P_DEFAULT_PORT = 38080; - uint16_t const RPC_DEFAULT_PORT = 38081; - uint16_t const ZMQ_RPC_DEFAULT_PORT = 38082; + uint64_t const CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX = 131; + uint64_t const CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX = 132; + uint64_t const CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX = 133; + uint16_t const P2P_DEFAULT_PORT = 26174; + uint16_t const RPC_DEFAULT_PORT = 26175; + uint16_t const ZMQ_RPC_DEFAULT_PORT = 26176; boost::uuids::uuid const NETWORK_ID = { { - 0x12 ,0x30, 0xF1, 0x71 , 0x61, 0x04 , 0x41, 0x61, 0x17, 0x31, 0x00, 0x82, 0x16, 0xA1, 0xA1, 0x12 - } }; // Bender's daydream - std::string const GENESIS_TX = "013c01ff0001ffffffffffff0302df5d56da0c7d643ddd1ce61901c7bdc5fb1738bfe39fbe69c28a3a7032729c0f2101168d0c4ca86fb55a4cf6a36d31431be1c53a3bd7411bb24e8832410289fa6f3b"; - uint32_t const GENESIS_NONCE = 10002; + 0xb1, 0xdc, 0xac, 0x5a, 0x9d, 0x11, 0x46, 0x53, 0x81, 0x22, 0xe6, 0x7e, 0xec, 0x8b, 0x59, 0xac + } }; // Monzero stagenet + std::string const GENESIS_TX = "013c01ff0001bda282a38eab0402df5d56da0c7d643ddd1ce61901c7bdc5fb1738bfe39fbe69c28a3a7032729c0f2101168d0c4ca86fb55a4cf6a36d31431be1c53a3bd7411bb24e8832410289fa6f3b"; + uint32_t const GENESIS_NONCE = 818137482; } } diff --git a/src/cryptonote_core/blockchain.cpp b/src/cryptonote_core/blockchain.cpp index fcc92644a..ffc5ddd87 100644 --- a/src/cryptonote_core/blockchain.cpp +++ b/src/cryptonote_core/blockchain.cpp @@ -5583,6 +5583,13 @@ void Blockchain::cancel() static const char expected_block_hashes_hash[] = "2aea941d43024422a63f223c84b9d88d1f58d31e1f508c2d6d43cd637ba32d16"; void Blockchain::load_compiled_in_block_hashes(const GetCheckpointsCallback& get_checkpoints) { + // Monzero has an independent genesis block and chain history. The compiled + // per-block data bundled by the upstream Monero source belongs to Monero and + // must never be used to validate or accelerate Monzero synchronization. If + // loaded, valid Monzero blocks inside that inherited height range are + // rejected and peers split into isolated chains. + return; + if (get_checkpoints == nullptr || !m_fast_sync) { return; diff --git a/src/cryptonote_core/cryptonote_core.cpp b/src/cryptonote_core/cryptonote_core.cpp index 4d7a454b2..b9751f597 100644 --- a/src/cryptonote_core/cryptonote_core.cpp +++ b/src/cryptonote_core/cryptonote_core.cpp @@ -171,7 +171,7 @@ namespace cryptonote }; static const command_line::arg_descriptor<std::string> arg_check_updates = { "check-updates" - , "Check for new versions of monero: [disabled|notify|download|update]" + , "Check for new versions of Monzero: [disabled|notify|download|update]" , "notify" }; static const command_line::arg_descriptor<bool> arg_fluffy_blocks = { @@ -205,7 +205,7 @@ namespace cryptonote static const command_line::arg_descriptor<std::string> arg_block_rate_notify = { "block-rate-notify" , "Run a program when the block rate undergoes large fluctuations. This might " - "be a sign of large amounts of hash rate going on and off the Monero network, " + "be a sign of large amounts of hash rate going on and off the Monzero network, " "and thus be of potential interest in predicting attacks. %t will be replaced " "by the number of minutes for the observation window, %b by the number of " "blocks observed within that window, and %e by the number of blocks that was " @@ -501,8 +501,8 @@ namespace cryptonote if (boost::filesystem::exists(old_files / "blockchain.bin")) { MWARNING("Found old-style blockchain.bin in " << old_files.string()); - MWARNING("Monero now uses a new format. You can either remove blockchain.bin to start syncing"); - MWARNING("the blockchain anew, or use monero-blockchain-export and monero-blockchain-import to"); + MWARNING("Monzero now uses a new format. You can either remove blockchain.bin to start syncing"); + MWARNING("the blockchain anew, or use monzero-blockchain-export and monzero-blockchain-import to"); MWARNING("convert your existing blockchain.bin to the new format. See README.md for instructions."); return false; } @@ -1590,7 +1590,7 @@ namespace cryptonote { std::string main_message; if (m_offline) - main_message = "The daemon is running offline and will not attempt to sync to the Monero network."; + main_message = "The daemon is running offline and will not attempt to sync to the Monzero network."; else main_message = "The daemon will start synchronizing with the network. This may take a long time to complete."; MGINFO_YELLOW(ENDL << "**********************************************************************" << ENDL @@ -1833,7 +1833,7 @@ namespace cryptonote MDEBUG("blocks in the last " << seconds[n] / 60 << " minutes: " << b << " (probability " << p << ")"); if (p < threshold) { - MWARNING("There were " << b << (b == max_blocks_checked ? " or more" : "") << " blocks in the last " << seconds[n] / 60 << " minutes, there might be large hash rate changes, or we might be partitioned, cut off from the Monero network or under attack, or your computer's time is off. Or it could be just sheer bad luck."); + MWARNING("There were " << b << (b == max_blocks_checked ? " or more" : "") << " blocks in the last " << seconds[n] / 60 << " minutes, there might be large hash rate changes, or we might be partitioned, cut off from the Monzero network or under attack, or your computer's time is off. Or it could be just sheer bad luck."); std::shared_ptr<tools::Notify> block_rate_notify = m_block_rate_notify; if (block_rate_notify) diff --git a/src/cryptonote_protocol/cryptonote_protocol_handler.inl b/src/cryptonote_protocol/cryptonote_protocol_handler.inl index 3ad26a3de..f424e0bde 100644 --- a/src/cryptonote_protocol/cryptonote_protocol_handler.inl +++ b/src/cryptonote_protocol/cryptonote_protocol_handler.inl @@ -2387,7 +2387,11 @@ skip: { bool val_expected = false; uint64_t current_blockchain_height = m_core.get_current_blockchain_height(); - if(!m_core.is_within_compiled_block_hash_area(current_blockchain_height) && m_synchronized.compare_exchange_strong(val_expected, true)) + // Monzero starts a new chain at height 0. The inherited Monero compiled + // checkpoint range is therefore not meaningful for deciding whether this + // network has synchronized, and keeping this guard leaves early Monzero + // nodes permanently "busy" even after reaching their peer's tip. + if(m_synchronized.compare_exchange_strong(val_expected, true)) { if ((current_blockchain_height > m_sync_start_height) && (m_sync_spans_downloaded > 0)) { @@ -2407,7 +2411,7 @@ skip: } } MGINFO_YELLOW(ENDL << "**********************************************************************" << ENDL - << "You are now synchronized with the network. You may now start monero-wallet-cli." << ENDL + << "You are now synchronized with the network. You may now start monzero-wallet-cli." << ENDL << ENDL << "Use the \"help\" command to see the list of available commands." << ENDL << "**********************************************************************"); @@ -2873,4 +2877,3 @@ skip: m_core.stop(); } } // namespace - diff --git a/src/daemon/CMakeLists.txt b/src/daemon/CMakeLists.txt index 455f72472..c1780b37e 100644 --- a/src/daemon/CMakeLists.txt +++ b/src/daemon/CMakeLists.txt @@ -45,6 +45,9 @@ monero_add_executable(daemon ${daemon_sources} ${daemon_headers} ${daemon_private_headers}) +if(STATIC AND NOT MINGW) + find_package(ZLIB REQUIRED) +endif() target_link_libraries(daemon PRIVATE rpc @@ -68,8 +71,9 @@ target_link_libraries(daemon ${ZMQ_LIB} ${GNU_READLINE_LIBRARY} ${EXTRA_LIBRARIES} + $<$<AND:$<BOOL:${STATIC}>,$<NOT:$<BOOL:${MINGW}>>>:ZLIB::ZLIB> ${Blocks}) set_property(TARGET daemon PROPERTY - OUTPUT_NAME "monerod") + OUTPUT_NAME "monzerod") install(TARGETS daemon DESTINATION bin) diff --git a/src/daemon/command_parser_executor.cpp b/src/daemon/command_parser_executor.cpp index 0c091c7ed..ef8f7be97 100644 --- a/src/daemon/command_parser_executor.cpp +++ b/src/daemon/command_parser_executor.cpp @@ -979,10 +979,10 @@ bool t_command_parser_executor::prune_blockchain(const std::vector<std::string>& if (args.empty() || args[0] != "confirm") { - std::cout << "Warning: pruning from within monerod will not shrink the database file size." << std::endl; + std::cout << "Warning: pruning from within monzerod will not shrink the database file size." << std::endl; std::cout << "Instead, parts of the file will be marked as free, so the file will not grow" << std::endl; std::cout << "until that newly free space is used up. If you want a smaller file size now," << std::endl; - std::cout << "exit monerod and run monero-blockchain-prune (you will temporarily need more" << std::endl; + std::cout << "exit monzerod and run monzero-blockchain-prune (you will temporarily need more" << std::endl; std::cout << "disk space for the database conversion though). If you are OK with the database" << std::endl; std::cout << "file keeping the same size, re-run this command with the \"confirm\" parameter." << std::endl; return true; diff --git a/src/daemon/command_server.cpp b/src/daemon/command_server.cpp index fc5f1b3d7..cbd5e2d4d 100644 --- a/src/daemon/command_server.cpp +++ b/src/daemon/command_server.cpp @@ -412,7 +412,7 @@ bool t_command_server::apropos(const std::vector<std::string>& args) std::string t_command_server::get_commands_str() { std::stringstream ss; - ss << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << std::endl; + ss << "Monzero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << std::endl; ss << "Commands: " << std::endl; std::string usage = m_command_lookup.get_usage(); boost::replace_all(usage, "\n", "\n "); diff --git a/src/daemon/executor.cpp b/src/daemon/executor.cpp index d67bd6141..7864fa408 100644 --- a/src/daemon/executor.cpp +++ b/src/daemon/executor.cpp @@ -40,7 +40,7 @@ namespace daemonize { - std::string const t_executor::NAME = "Monero Daemon"; + std::string const t_executor::NAME = "Monzero Daemon"; void t_executor::init_options( boost::program_options::options_description & configurable_options @@ -58,7 +58,7 @@ namespace daemonize boost::program_options::variables_map const & vm ) { - LOG_PRINT_L0("Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ") Daemonised"); + LOG_PRINT_L0("Monzero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ") Daemonised"); return t_daemon{vm, public_rpc_port}; } @@ -76,4 +76,3 @@ namespace daemonize return t_daemon{vm, public_rpc_port}.run(true); } } - diff --git a/src/daemon/main.cpp b/src/daemon/main.cpp index a61fbda6f..8c2b098da 100644 --- a/src/daemon/main.cpp +++ b/src/daemon/main.cpp @@ -192,7 +192,7 @@ int main(int argc, char const * argv[]) if (command_line::get_arg(vm, command_line::arg_help)) { - std::cout << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; + std::cout << "Monzero '" << MONERO_RELEASE_NAME << "' (core v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; std::cout << "Usage: " + std::string{argv[0]} + " [options|settings] [daemon_command...]" << std::endl << std::endl; std::cout << visible_options << std::endl; return 0; @@ -201,7 +201,7 @@ int main(int argc, char const * argv[]) // Monero Version if (command_line::get_arg(vm, command_line::arg_version)) { - std::cout << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL; + std::cout << "Monzero '" << MONERO_RELEASE_NAME << "' (core v" << MONERO_VERSION_FULL << ")" << ENDL; return 0; } @@ -306,7 +306,7 @@ int main(int argc, char const * argv[]) tools::set_max_concurrency(command_line::get_arg(vm, daemon_args::arg_max_concurrency)); // logging is now set up - MGINFO("Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")"); + MGINFO("Monzero '" << MONERO_RELEASE_NAME << "' (core v" << MONERO_VERSION_FULL << ")"); // If there are positional options, we're running a daemon command { diff --git a/src/daemon/rpc_command_executor.cpp b/src/daemon/rpc_command_executor.cpp index f6746b8ad..91711d88b 100644 --- a/src/daemon/rpc_command_executor.cpp +++ b/src/daemon/rpc_command_executor.cpp @@ -614,8 +614,8 @@ bool t_rpc_command_executor::mining_status() { uint64_t daily = 86400ull / mres.block_target * mres.block_reward * ratio; uint64_t monthly = 86400ull / mres.block_target * 30.5 * mres.block_reward * ratio; uint64_t yearly = 86400ull / mres.block_target * 356 * mres.block_reward * ratio; - tools::msg_writer() << "Expected: " << cryptonote::print_money(daily) << " monero daily, " - << cryptonote::print_money(monthly) << " monero monthly, " << cryptonote::print_money(yearly) << " yearly"; + tools::msg_writer() << "Expected: " << cryptonote::print_money(daily) << " XMZ daily, " + << cryptonote::print_money(monthly) << " XMZ monthly, " << cryptonote::print_money(yearly) << " XMZ yearly"; } return true; @@ -1463,10 +1463,10 @@ bool t_rpc_command_executor::print_status() bool daemon_is_alive = m_rpc_client->check_connection(); if(daemon_is_alive) { - tools::success_msg_writer() << "monerod is running"; + tools::success_msg_writer() << "monzerod is running"; } else { - tools::fail_msg_writer() << "monerod is NOT running"; + tools::fail_msg_writer() << "monzerod is NOT running"; } return true; diff --git a/src/debug_utilities/cn_deserialize.cpp b/src/debug_utilities/cn_deserialize.cpp index 41c397bd8..8ca1cb1cf 100644 --- a/src/debug_utilities/cn_deserialize.cpp +++ b/src/debug_utilities/cn_deserialize.cpp @@ -103,7 +103,7 @@ int main(int argc, char* argv[]) if (command_line::get_arg(vm, command_line::arg_help)) { - std::cout << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; + std::cout << "Monzero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; std::cout << desc_options << std::endl; return 1; } diff --git a/src/debug_utilities/dns_checks.cpp b/src/debug_utilities/dns_checks.cpp index 62854cfa3..dbd5c7a2f 100644 --- a/src/debug_utilities/dns_checks.cpp +++ b/src/debug_utilities/dns_checks.cpp @@ -121,7 +121,7 @@ int main(int argc, char* argv[]) if (command_line::get_arg(vm, command_line::arg_help)) { - std::cout << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; + std::cout << "Monzero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; std::cout << desc_options << std::endl; return 1; } diff --git a/src/gen_ssl_cert/gen_ssl_cert.cpp b/src/gen_ssl_cert/gen_ssl_cert.cpp index cd810ed20..c95c71a16 100644 --- a/src/gen_ssl_cert/gen_ssl_cert.cpp +++ b/src/gen_ssl_cert/gen_ssl_cert.cpp @@ -121,13 +121,13 @@ int main(int argc, char* argv[]) if (command_line::get_arg(vm, command_line::arg_help)) { - std::cout << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; + std::cout << "Monzero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; std::cout << desc_options << std::endl; return 0; } if (command_line::get_arg(vm, command_line::arg_version)) { - std::cout << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL; + std::cout << "Monzero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL; return 0; } diff --git a/src/hardforks/hardforks.cpp b/src/hardforks/hardforks.cpp index 336ef6519..ff11242dc 100644 --- a/src/hardforks/hardforks.cpp +++ b/src/hardforks/hardforks.cpp @@ -32,97 +32,29 @@ #define MONERO_DEFAULT_LOG_CATEGORY "blockchain.hardforks" const hardfork_t mainnet_hard_forks[] = { - // version 1 from the start of the blockchain - { 1, 1, 0, 1341378000 }, - - // version 2 starts from block 1009827, which is on or around the 20th of March, 2016. Fork time finalised on 2015-09-20. No fork voting occurs for the v2 fork. - { 2, 1009827, 0, 1442763710 }, - - // version 3 starts from block 1141317, which is on or around the 24th of September, 2016. Fork time finalised on 2016-03-21. - { 3, 1141317, 0, 1458558528 }, - - // version 4 starts from block 1220516, which is on or around the 5th of January, 2017. Fork time finalised on 2016-09-18. - { 4, 1220516, 0, 1483574400 }, - - // version 5 starts from block 1288616, which is on or around the 15th of April, 2017. Fork time finalised on 2017-03-14. - { 5, 1288616, 0, 1489520158 }, - - // version 6 starts from block 1400000, which is on or around the 16th of September, 2017. Fork time finalised on 2017-08-18. - { 6, 1400000, 0, 1503046577 }, - - // version 7 starts from block 1546000, which is on or around the 6th of April, 2018. Fork time finalised on 2018-03-17. - { 7, 1546000, 0, 1521303150 }, - - // version 8 starts from block 1685555, which is on or around the 18th of October, 2018. Fork time finalised on 2018-09-02. - { 8, 1685555, 0, 1535889547 }, - - // version 9 starts from block 1686275, which is on or around the 19th of October, 2018. Fork time finalised on 2018-09-02. - { 9, 1686275, 0, 1535889548 }, - - // version 10 starts from block 1788000, which is on or around the 9th of March, 2019. Fork time finalised on 2019-02-10. - { 10, 1788000, 0, 1549792439 }, - - // version 11 starts from block 1788720, which is on or around the 10th of March, 2019. Fork time finalised on 2019-02-15. - { 11, 1788720, 0, 1550225678 }, - - // version 12 starts from block 1978433, which is on or around the 30th of November, 2019. Fork time finalised on 2019-10-18. - { 12, 1978433, 0, 1571419280 }, - - { 13, 2210000, 0, 1598180817 }, - { 14, 2210720, 0, 1598180818 }, - - { 15, 2688888, 0, 1656629117 }, - { 16, 2689608, 0, 1656629118 }, + // Activate Monero's inherited protocol revisions during the private + // bootstrap window. Public Monzero operation begins on v16/RandomX. + { 1, 1, 0, 1 }, { 2, 2, 0, 2 }, { 3, 3, 0, 3 }, { 4, 4, 0, 4 }, + { 5, 5, 0, 5 }, { 6, 6, 0, 6 }, { 7, 7, 0, 7 }, { 8, 8, 0, 8 }, + { 9, 9, 0, 9 }, { 10, 10, 0, 10 }, { 11, 11, 0, 11 }, { 12, 12, 0, 12 }, + { 13, 13, 0, 13 }, { 14, 14, 0, 14 }, { 15, 15, 0, 15 }, { 16, 16, 0, 16 }, }; const size_t num_mainnet_hard_forks = sizeof(mainnet_hard_forks) / sizeof(mainnet_hard_forks[0]); -const uint64_t mainnet_hard_fork_version_1_till = 1009826; +const uint64_t mainnet_hard_fork_version_1_till = 1; const hardfork_t testnet_hard_forks[] = { - // version 1 from the start of the blockchain - { 1, 1, 0, 1341378000 }, - - // version 2 starts from block 624634, which is on or around the 23rd of November, 2015. Fork time finalised on 2015-11-20. No fork voting occurs for the v2 fork. - { 2, 624634, 0, 1445355000 }, - - // versions 3-5 were passed in rapid succession from September 18th, 2016 - { 3, 800500, 0, 1472415034 }, - { 4, 801219, 0, 1472415035 }, - { 5, 802660, 0, 1472415036 + 86400*180 }, // add 5 months on testnet to shut the update warning up since there's a large gap to v6 - - { 6, 971400, 0, 1501709789 }, - { 7, 1057027, 0, 1512211236 }, - { 8, 1057058, 0, 1533211200 }, - { 9, 1057778, 0, 1533297600 }, - { 10, 1154318, 0, 1550153694 }, - { 11, 1155038, 0, 1550225678 }, - { 12, 1308737, 0, 1569582000 }, - { 13, 1543939, 0, 1599069376 }, - { 14, 1544659, 0, 1599069377 }, - { 15, 1982800, 0, 1652727000 }, - { 16, 1983520, 0, 1652813400 }, + { 1, 1, 0, 1 }, { 2, 2, 0, 2 }, { 3, 3, 0, 3 }, { 4, 4, 0, 4 }, + { 5, 5, 0, 5 }, { 6, 6, 0, 6 }, { 7, 7, 0, 7 }, { 8, 8, 0, 8 }, + { 9, 9, 0, 9 }, { 10, 10, 0, 10 }, { 11, 11, 0, 11 }, { 12, 12, 0, 12 }, + { 13, 13, 0, 13 }, { 14, 14, 0, 14 }, { 15, 15, 0, 15 }, { 16, 16, 0, 16 }, }; const size_t num_testnet_hard_forks = sizeof(testnet_hard_forks) / sizeof(testnet_hard_forks[0]); -const uint64_t testnet_hard_fork_version_1_till = 624633; +const uint64_t testnet_hard_fork_version_1_till = 1; const hardfork_t stagenet_hard_forks[] = { - // version 1 from the start of the blockchain - { 1, 1, 0, 1341378000 }, - - // versions 2-7 in rapid succession from March 13th, 2018 - { 2, 32000, 0, 1521000000 }, - { 3, 33000, 0, 1521120000 }, - { 4, 34000, 0, 1521240000 }, - { 5, 35000, 0, 1521360000 }, - { 6, 36000, 0, 1521480000 }, - { 7, 37000, 0, 1521600000 }, - { 8, 176456, 0, 1537821770 }, - { 9, 177176, 0, 1537821771 }, - { 10, 269000, 0, 1550153694 }, - { 11, 269720, 0, 1550225678 }, - { 12, 454721, 0, 1571419280 }, - { 13, 675405, 0, 1598180817 }, - { 14, 676125, 0, 1598180818 }, - { 15, 1151000, 0, 1656629117 }, - { 16, 1151720, 0, 1656629118 }, + { 1, 1, 0, 1 }, { 2, 2, 0, 2 }, { 3, 3, 0, 3 }, { 4, 4, 0, 4 }, + { 5, 5, 0, 5 }, { 6, 6, 0, 6 }, { 7, 7, 0, 7 }, { 8, 8, 0, 8 }, + { 9, 9, 0, 9 }, { 10, 10, 0, 10 }, { 11, 11, 0, 11 }, { 12, 12, 0, 12 }, + { 13, 13, 0, 13 }, { 14, 14, 0, 14 }, { 15, 15, 0, 15 }, { 16, 16, 0, 16 }, }; const size_t num_stagenet_hard_forks = sizeof(stagenet_hard_forks) / sizeof(stagenet_hard_forks[0]); diff --git a/src/p2p/net_node.h b/src/p2p/net_node.h index d7cbaa567..6af3aed94 100644 --- a/src/p2p/net_node.h +++ b/src/p2p/net_node.h @@ -295,12 +295,11 @@ namespace nodetool virtual void clear_used_stripe_peers(); private: - const std::vector<std::string> m_seed_nodes_list = - { "seeds.moneroseeds.se" - , "seeds.moneroseeds.ae.org" - , "seeds.moneroseeds.ch" - , "seeds.moneroseeds.li" - }; + // Monzero-controlled DNS bootstrap nodes. The network's default P2P port + // is appended after each hostname is resolved. + // Monzero launchers configure node.monzero.org as a persistent priority + // peer. Do not also create an upstream-style, short-lived seed connection. + const std::vector<std::string> m_seed_nodes_list = {}; bool islimitup=false; bool islimitdown=false; @@ -546,4 +545,3 @@ namespace nodetool } POP_WARNINGS - diff --git a/src/p2p/net_node.inl b/src/p2p/net_node.inl index 91a54f43e..9df267612 100644 --- a/src/p2p/net_node.inl +++ b/src/p2p/net_node.inl @@ -703,34 +703,8 @@ namespace nodetool std::set<std::string> node_server<t_payload_net_handler>::get_ip_seed_nodes() const { std::set<std::string> full_addrs; - if (m_nettype == cryptonote::TESTNET) - { - full_addrs.insert("176.9.0.187:28080"); - full_addrs.insert("192.99.8.110:28080"); - full_addrs.insert("37.187.74.171:28080"); - full_addrs.insert("88.99.195.15:28080"); - full_addrs.insert("5.104.84.64:28080"); - } - else if (m_nettype == cryptonote::STAGENET) - { - full_addrs.insert("176.9.0.187:38080"); - full_addrs.insert("192.99.8.110:38080"); - full_addrs.insert("37.187.74.171:38080"); - full_addrs.insert("88.99.195.15:38080"); - full_addrs.insert("5.104.84.64:38080"); - } - else if (m_nettype == cryptonote::FAKECHAIN) - { - } - else - { - full_addrs.insert("176.9.0.187:18080"); - full_addrs.insert("88.198.163.90:18080"); - full_addrs.insert("192.99.8.110:18080"); - full_addrs.insert("37.187.74.171:18080"); - full_addrs.insert("88.99.195.15:18080"); - full_addrs.insert("5.104.84.64:18080"); - } + // Monzero launchers configure the bootstrap node as a persistent priority + // peer. Keeping the public node here would make that node connect to itself. return full_addrs; } //----------------------------------------------------------------------------------- @@ -858,27 +832,8 @@ namespace nodetool case epee::net_utils::zone::public_: return get_dns_seed_nodes(); case epee::net_utils::zone::tor: - if (m_nettype == cryptonote::MAINNET) - { - return { - "zbjkbsxc5munw3qusl7j2hpcmikhqocdf4pqhnhtpzw5nt5jrmofptid.onion:18083", - "plowsof3t5hogddwabaeiyrno25efmzfxyro2vligremt7sxpsclfaid.onion:18083", - "plowsoffjexmxalw73tkjmf422gq6575fc7vicuu4javzn2ynnte6tyd.onion:18083", - "plowsofe6cleftfmk2raiw5h2x66atrik3nja4bfd3zrfa2hdlgworad.onion:18083", - "aclc4e2jhhtr44guufbnwk5bzwhaecinax4yip4wr4tjn27sjsfg6zqd.onion:18083", - "lykcas4tus7mkm4bhsgqe4drtd4awi7gja24goscc47xfgzj54yofyqd.onion:18083", - }; - } return {}; case epee::net_utils::zone::i2p: - if (m_nettype == cryptonote::MAINNET) - { - return { - "uqj3aphckqtjsitz7kxx5flqpwjlq5ppr3chazfued7xucv3nheq.b32.i2p", - "vdmnehdjkpkg57nthgnjfuaqgku673r5bpbqg56ix6fyqoywgqrq.b32.i2p", - "ugnlcdciyhghh2zert7c3kl4biwkirc43ke33jiy5slnd3mv2trq.b32.i2p", - }; - } return {}; default: break; @@ -1855,7 +1810,11 @@ namespace nodetool pe_seed.adr = server.m_seed_nodes[current_index]; if (is_peer_used(pe_seed)) is_connected_to_at_least_one_seed_node = true; - else if (try_to_connect_and_handshake_with_new_peer(server.m_seed_nodes[current_index], true)) + // Monzero currently has a small bootstrap network, so retain a seed + // connection as a normal peer instead of using it only to fetch a + // peer list. This allows a fresh node to synchronize when the seed is + // the only publicly reachable peer. + else if (try_to_connect_and_handshake_with_new_peer(server.m_seed_nodes[current_index], false)) break; if(++try_count > server.m_seed_nodes.size()) { @@ -2121,6 +2080,10 @@ namespace nodetool template<class t_payload_net_handler> bool node_server<t_payload_net_handler>::update_dns_blocklist() { + // MoneroPulse blocklists belong to the upstream Monero network. Leave DNS + // blocklisting disabled until Monzero publishes its own signed records. + return true; +#if 0 if (!m_enable_dns_blocklist) return true; if (m_nettype != cryptonote::MAINNET) @@ -2170,6 +2133,7 @@ namespace nodetool if (good > 0) MINFO(good << " addresses added to the blocklist"); return true; +#endif } //----------------------------------------------------------------------------------- template<class t_payload_net_handler> diff --git a/src/rpc/core_rpc_server.cpp b/src/rpc/core_rpc_server.cpp index c68449630..b5bfb85fa 100644 --- a/src/rpc/core_rpc_server.cpp +++ b/src/rpc/core_rpc_server.cpp @@ -526,9 +526,11 @@ namespace cryptonote res.tx_count = m_core.get_blockchain_storage().get_total_transactions() - res.height; //without coinbase res.tx_pool_size = m_core.get_pool_transactions_count(!restricted); res.alt_blocks_count = restricted ? 0 : m_core.get_blockchain_storage().get_alternative_blocks_count(); - uint64_t total_conn = restricted ? 0 : m_p2p.get_public_connections_count(); - res.outgoing_connections_count = restricted ? 0 : m_p2p.get_public_outgoing_connections_count(); - res.incoming_connections_count = restricted ? 0 : (total_conn - res.outgoing_connections_count); + // Connection totals are safe, aggregate network-health information for + // Monzero's public dashboard. Individual peer addresses remain restricted. + uint64_t total_conn = m_p2p.get_public_connections_count(); + res.outgoing_connections_count = m_p2p.get_public_outgoing_connections_count(); + res.incoming_connections_count = total_conn - res.outgoing_connections_count; res.rpc_connections_count = restricted ? 0 : get_connections_count(); res.white_peerlist_size = restricted ? 0 : m_p2p.get_public_white_peers_count(); res.grey_peerlist_size = restricted ? 0 : m_p2p.get_public_gray_peers_count(); diff --git a/src/rpc/daemon_handler.cpp b/src/rpc/daemon_handler.cpp index add8bca8f..06b1af01f 100644 --- a/src/rpc/daemon_handler.cpp +++ b/src/rpc/daemon_handler.cpp @@ -562,9 +562,11 @@ namespace rpc res.info.alt_blocks_count = m_restricted ? 0 : chain.get_alternative_blocks_count(); - uint64_t total_conn = m_restricted ? 0 : m_p2p.get_public_connections_count(); - res.info.outgoing_connections_count = m_restricted ? 0 : m_p2p.get_public_outgoing_connections_count(); - res.info.incoming_connections_count = m_restricted ? 0 : total_conn - res.info.outgoing_connections_count; + // Publish aggregate counts while keeping peer identities unavailable from + // restricted RPC. + uint64_t total_conn = m_p2p.get_public_connections_count(); + res.info.outgoing_connections_count = m_p2p.get_public_outgoing_connections_count(); + res.info.incoming_connections_count = total_conn - res.info.outgoing_connections_count; res.info.white_peerlist_size = m_restricted ? 0 : m_p2p.get_public_white_peers_count(); diff --git a/src/simplewallet/CMakeLists.txt b/src/simplewallet/CMakeLists.txt index b659d9f48..b12ee60ad 100644 --- a/src/simplewallet/CMakeLists.txt +++ b/src/simplewallet/CMakeLists.txt @@ -39,6 +39,10 @@ monero_add_executable(simplewallet ${simplewallet_sources} ${simplewallet_headers} ${simplewallet_private_headers}) +if(STATIC AND NOT MINGW) + find_package(ZLIB REQUIRED) + find_library(ZSTD_LIBRARY NAMES zstd REQUIRED) +endif() target_link_libraries(simplewallet PRIVATE wallet @@ -58,8 +62,19 @@ target_link_libraries(simplewallet ${Boost_THREAD_LIBRARY} ${CMAKE_THREAD_LIBS_INIT} ${GNU_READLINE_LIBRARY} - ${EXTRA_LIBRARIES}) + ${EXTRA_LIBRARIES} + $<$<AND:$<BOOL:${STATIC}>,$<NOT:$<BOOL:${MINGW}>>>:ZLIB::ZLIB>) +if(STATIC AND NOT MINGW) + # OpenSSL's static archive references optional compression backends. Force + # their objects into the final CLI wallet even though CMake emits OpenSSL + # later through transitive target dependencies. + target_link_options(simplewallet PRIVATE + -Wl,--whole-archive + ${ZLIB_LIBRARIES} + ${ZSTD_LIBRARY} + -Wl,--no-whole-archive) +endif() set_property(TARGET simplewallet PROPERTY - OUTPUT_NAME "monero-wallet-cli") + OUTPUT_NAME "monzero-wallet-cli") install(TARGETS simplewallet DESTINATION bin) diff --git a/src/simplewallet/simplewallet.cpp b/src/simplewallet/simplewallet.cpp index 39bf169f3..8202ad8ef 100644 --- a/src/simplewallet/simplewallet.cpp +++ b/src/simplewallet/simplewallet.cpp @@ -189,7 +189,7 @@ namespace const command_line::arg_descriptor<bool> arg_non_deterministic = {"non-deterministic", sw::tr("Generate non-deterministic view and spend keys"), false}; const command_line::arg_descriptor<uint64_t> arg_restore_height = {"restore-height", sw::tr("Restore from specific blockchain height"), 0}; const command_line::arg_descriptor<std::string> arg_restore_date = {"restore-date", sw::tr("Restore from estimated blockchain height on specified date"), ""}; - const command_line::arg_descriptor<bool> arg_do_not_relay = {"do-not-relay", sw::tr("The newly created transaction will not be relayed to the monero network"), false}; + const command_line::arg_descriptor<bool> arg_do_not_relay = {"do-not-relay", sw::tr("The newly created transaction will not be relayed to the Monzero network"), false}; const command_line::arg_descriptor<bool> arg_create_address_file = {"create-address-file", sw::tr("Create an address file for new wallets"), false}; const command_line::arg_descriptor<std::string> arg_subaddress_lookahead = {"subaddress-lookahead", tools::wallet2::tr("Set subaddress lookahead sizes to <major>:<minor>"), ""}; const command_line::arg_descriptor<bool> arg_use_english_language_names = {"use-english-language-names", sw::tr("Display English language names"), false}; @@ -526,7 +526,7 @@ namespace std::stringstream prompt; prompt << sw::tr("For URL: ") << url << ", " << dnssec_str << std::endl - << sw::tr(" Monero Address = ") << addresses[0] + << sw::tr(" Monzero Address = ") << addresses[0] << std::endl << sw::tr("Is this OK?") ; @@ -2361,25 +2361,25 @@ bool simple_wallet::public_nodes(const std::vector<std::string> &args) bool simple_wallet::welcome(const std::vector<std::string> &args) { - message_writer() << tr("Welcome to Monero, the private cryptocurrency."); + message_writer() << tr("Welcome to Monzero, the private cryptocurrency."); message_writer() << ""; - message_writer() << tr("Monero, like Bitcoin, is a cryptocurrency. That is, it is digital money."); - message_writer() << tr("Unlike Bitcoin, your Monero transactions and balance stay private and are not visible to the world by default."); + message_writer() << tr("Monzero, like Bitcoin, is a cryptocurrency. That is, it is digital money."); + message_writer() << tr("Unlike Bitcoin, your Monzero transactions and balance stay private and are not visible to the world by default."); message_writer() << tr("However, you have the option of making those available to select parties if you choose to."); message_writer() << ""; - message_writer() << tr("Monero protects your privacy on the blockchain, and while Monero strives to improve all the time,"); - message_writer() << tr("no privacy technology can be 100% perfect, Monero included."); - message_writer() << tr("Monero cannot protect you from malware, and it may not be as effective as we hope against powerful adversaries."); - message_writer() << tr("Flaws in Monero may be discovered in the future, and attacks may be developed to peek under some"); - message_writer() << tr("of the layers of privacy Monero provides. Be safe and practice defense in depth."); + message_writer() << tr("Monzero protects your privacy on the blockchain, and while Monzero strives to improve all the time,"); + message_writer() << tr("no privacy technology can be 100% perfect, Monzero included."); + message_writer() << tr("Monzero cannot protect you from malware, and it may not be as effective as we hope against powerful adversaries."); + message_writer() << tr("Flaws in Monzero may be discovered in the future, and attacks may be developed to peek under some"); + message_writer() << tr("of the layers of privacy Monzero provides. Be safe and practice defense in depth."); message_writer() << ""; - message_writer() << tr("Welcome to Monero and financial privacy. For more information see https://GetMonero.org"); + message_writer() << tr("Welcome to Monzero and financial privacy. For more information see https://monzero.org"); return true; } bool simple_wallet::version(const std::vector<std::string> &args) { - message_writer() << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")"; + message_writer() << "Monzero '" << MONERO_RELEASE_NAME << "' (core v" << MONERO_VERSION_FULL << ")"; return true; } @@ -2715,15 +2715,15 @@ bool simple_wallet::set_unit(const std::vector<std::string> &args/* = std::vecto const std::string &unit = args[1]; unsigned int decimal_point = CRYPTONOTE_DISPLAY_DECIMAL_POINT; - if (unit == "monero") + if (unit == "monzero" || unit == "XMZ") decimal_point = CRYPTONOTE_DISPLAY_DECIMAL_POINT; - else if (unit == "millinero") + else if (unit == "millizero" || unit == "mXMZ") decimal_point = CRYPTONOTE_DISPLAY_DECIMAL_POINT - 3; - else if (unit == "micronero") + else if (unit == "microzero" || unit == "uXMZ") decimal_point = CRYPTONOTE_DISPLAY_DECIMAL_POINT - 6; - else if (unit == "nanonero") + else if (unit == "nanozero" || unit == "nXMZ") decimal_point = CRYPTONOTE_DISPLAY_DECIMAL_POINT - 9; - else if (unit == "piconero") + else if (unit == "atomic-XMZ") decimal_point = 0; else { @@ -3278,7 +3278,7 @@ bool simple_wallet::help(const std::vector<std::string> &args/* = std::vector<st message_writer() << tr("\"show_transfers [in|out|pending|failed|pool]\" - Show transactions."); message_writer() << tr("\"sweep_all <address>\" - Send whole balance to another wallet."); message_writer() << tr("\"seed\" - Show secret 25 words that can be used to recover this wallet."); - message_writer() << tr("\"refresh\" - Synchronize wallet with the Monero network."); + message_writer() << tr("\"refresh\" - Synchronize wallet with the Monzero network."); message_writer() << tr("\"status\" - Check current status of wallet."); message_writer() << tr("\"version\" - Check software version."); message_writer() << tr("\"exit\" - Exit wallet."); @@ -3445,7 +3445,7 @@ simple_wallet::simple_wallet() m_cmd_binder.set_handler("donate", boost::bind(&simple_wallet::on_command, this, &simple_wallet::donate, _1), tr(USAGE_DONATE), - tr("Donate <amount> to the development team (donate.getmonero.org).")); + tr("Donations are not configured for this Monzero release.")); m_cmd_binder.set_handler("sign_transfer", boost::bind(&simple_wallet::on_command, this, &simple_wallet::sign_transfer, _1), tr(USAGE_SIGN_TRANSFER), @@ -3520,8 +3520,8 @@ simple_wallet::simple_wallet() "ask-password <0|1|2 (or never|action|decrypt)>\n " " action: ask the password before many actions such as transfer, etc\n " " decrypt: same as action, but keeps the spend key encrypted in memory when not needed\n " - "unit <monero|millinero|micronero|nanonero|piconero>\n " - " Set the default monero (sub-)unit.\n " + "unit <monzero|XMZ|millizero|mXMZ|microzero|uXMZ|nanozero|nXMZ|atomic-XMZ>\n " + " Set the default Monzero (sub-)unit.\n " "min-outputs-count [n]\n " " Try to keep at least that many outputs of value at least min-outputs-value.\n " "min-outputs-value [n]\n " @@ -3557,7 +3557,7 @@ simple_wallet::simple_wallet() "background-sync <off|reuse-wallet-password|custom-background-password>\n " " Set this to enable scanning in the background with just the view key while the wallet is locked.\n " "setup-background-mining <1|0>\n " - " Whether to enable background mining. Set this to support the network and to get a chance to receive new monero.\n " + " Whether to enable background mining. Set this to support the network and to get a chance to receive new XMZ.\n " "device-name <device_name[:device_spec]>\n " " Device name for hardware wallet.\n " "export-format <\"binary\"|\"ascii\">\n " @@ -3758,7 +3758,7 @@ simple_wallet::simple_wallet() m_cmd_binder.set_handler("mms signer", boost::bind(&simple_wallet::on_command, this, &simple_wallet::mms, _1), tr(USAGE_MMS_SIGNER), - tr("Set or modify authorized signer info (single-word label, transport address, Monero address), or list all signers")); + tr("Set or modify authorized signer info (single-word label, transport address, Monzero address), or list all signers")); m_cmd_binder.set_handler("mms list", boost::bind(&simple_wallet::on_command, this, &simple_wallet::mms, _1), tr(USAGE_MMS_LIST), @@ -3883,7 +3883,7 @@ simple_wallet::simple_wallet() m_cmd_binder.set_handler("welcome", boost::bind(&simple_wallet::on_command, this, &simple_wallet::welcome, _1), tr(USAGE_WELCOME), - tr("Prints basic info about Monero for first time users")); + tr("Prints basic info about Monzero for first time users")); m_cmd_binder.set_handler("version", boost::bind(&simple_wallet::on_command, this, &simple_wallet::version, _1), tr(USAGE_VERSION), @@ -4029,7 +4029,7 @@ bool simple_wallet::set_variable(const std::vector<std::string> &args) CHECK_SIMPLE_VARIABLE("refresh-type", set_refresh_type, tr("full (slowest, no assumptions); optimize-coinbase (fast, assumes the whole coinbase is paid to a single address); no-coinbase (fastest, assumes we receive no coinbase transaction), default (same as optimize-coinbase)")); CHECK_SIMPLE_VARIABLE("priority", set_default_priority, tr("0, 1, 2, 3, or 4, or one of ") << join_priority_strings(", ")); CHECK_SIMPLE_VARIABLE("ask-password", set_ask_password, tr("0|1|2 (or never|action|decrypt)")); - CHECK_SIMPLE_VARIABLE("unit", set_unit, tr("monero, millinero, micronero, nanonero, piconero")); + CHECK_SIMPLE_VARIABLE("unit", set_unit, tr("monzero, XMZ, millizero, mXMZ, microzero, uXMZ, nanozero, nXMZ, atomic-XMZ")); CHECK_SIMPLE_VARIABLE("max-reorg-depth", set_max_reorg_depth, tr("unsigned integer")); CHECK_SIMPLE_VARIABLE("min-outputs-count", set_min_output_count, tr("unsigned integer")); CHECK_SIMPLE_VARIABLE("min-outputs-value", set_min_output_value, tr("amount")); @@ -4844,7 +4844,7 @@ bool simple_wallet::init(const boost::program_options::variables_map& vm) bool ssl = false; if (m_wallet->check_connection(NULL, &ssl) && !ssl) message_writer(console_color_red, true) << boost::format(tr("Using your own without SSL exposes your RPC traffic to monitoring")); - message_writer(console_color_red, true) << boost::format(tr("You are strongly encouraged to connect to the Monero network using your own daemon")); + message_writer(console_color_red, true) << boost::format(tr("You are strongly encouraged to connect to the Monzero network using your own daemon")); message_writer(console_color_red, true) << boost::format(tr("If you or someone you trust are operating this daemon, you can use --trusted-daemon")); COMMAND_RPC_GET_INFO::request req; @@ -4865,7 +4865,7 @@ bool simple_wallet::init(const boost::program_options::variables_map& vm) check_background_mining(password); if (welcome) - message_writer(console_color_yellow, true) << tr("If you are new to Monero, type \"welcome\" for a brief overview."); + message_writer(console_color_yellow, true) << tr("If you are new to Monzero, type \"welcome\" for a brief overview."); m_last_activity_time = time(NULL); return true; @@ -5099,7 +5099,7 @@ boost::optional<epee::wipeable_string> simple_wallet::new_wallet(const boost::pr "Use the \"help\" command to see a simplified list of available commands.\n" "Use \"help all\" command to see the list of all available commands.\n" "Use \"help <command>\" to see a command's documentation.\n" - "Always use the \"exit\" command when closing monero-wallet-cli to save \n" + "Always use the \"exit\" command when closing monzero-wallet-cli to save \n" "your current session's state. Otherwise, you might need to synchronize \n" "your wallet again (your wallet keys are NOT at risk in any case).\n") ; @@ -5484,7 +5484,7 @@ void simple_wallet::start_background_mining() return; } } - success_msg_writer() << tr("Background mining enabled. Thank you for supporting the Monero network."); + success_msg_writer() << tr("Background mining enabled. Thank you for supporting the Monzero network."); } //---------------------------------------------------------------------------------------------------- void simple_wallet::stop_background_mining() @@ -5560,7 +5560,7 @@ void simple_wallet::check_background_mining(const epee::wipeable_string &passwor { message_writer() << tr("The daemon is not set up to background mine."); message_writer() << tr("With background mining enabled, the daemon will mine when idle and not on battery."); - message_writer() << tr("Enabling this supports the network you are using, and makes you eligible for receiving new monero"); + message_writer() << tr("Enabling this supports the network you are using, and makes you eligible for receiving new XMZ"); std::string accepted = input_line(tr("Do you want to do it now? (Y/Yes/N/No): ")); if (std::cin.eof() || !command_line::is_yes(accepted)) { m_wallet->setup_background_mining(tools::wallet2::BackgroundMiningNo); @@ -6625,7 +6625,7 @@ void simple_wallet::check_for_inactivity_lock(bool user) m_in_command = true; if (!user) { - const std::string speech = tr("I locked your Monero wallet to protect you while you were away\nsee \"help set\" to configure/disable"); + const std::string speech = tr("I locked your Monzero wallet to protect you while you were away\nsee \"help set\" to configure/disable"); std::vector<std::pair<std::string, size_t>> lines = tools::split_string_by_width(speech, 45); size_t max_len = 0; @@ -7828,6 +7828,10 @@ bool simple_wallet::sweep_below(const std::vector<std::string> &args_) //---------------------------------------------------------------------------------------------------- bool simple_wallet::donate(const std::vector<std::string> &args_) { + (void)args_; + fail_msg_writer() << tr("Donations are not configured for this Monzero release."); + return true; +#if 0 CHECK_IF_BACKGROUND_SYNCING("cannot donate"); std::vector<std::string> local_args = args_; if(local_args.empty() || local_args.size() > 5) @@ -7886,6 +7890,7 @@ bool simple_wallet::donate(const std::vector<std::string> &args_) message_writer() << (boost::format(tr("Donating %s %s to %s.")) % amount_str % cryptonote::get_unit(cryptonote::get_default_decimal_point()) % address_str).str(); transfer(local_args); return true; +#endif } //---------------------------------------------------------------------------------------------------- bool simple_wallet::accept_loaded_tx(const std::function<size_t()> get_num_txes, const std::function<const tools::wallet2::tx_construction_data&(size_t)> &get_tx, const std::string &extra_message) @@ -10808,8 +10813,8 @@ int main(int argc, char* argv[]) bool should_terminate = false; std::tie(vm, should_terminate) = wallet_args::main( argc, argv, - "monero-wallet-cli [--wallet-file=<filename>|--generate-new-wallet=<filename>] [<COMMAND>]", - sw::tr("This is the command line monero wallet. It needs to connect to a monero\ndaemon to work correctly.\nWARNING: Do not reuse your Monero keys on another fork, UNLESS this fork has key reuse mitigations built in. Doing so will harm your privacy."), + "monzero-wallet-cli [--wallet-file=<filename>|--generate-new-wallet=<filename>] [<COMMAND>]", + sw::tr("This is the command line Monzero wallet. It needs to connect to a Monzero\ndaemon to work correctly.\nWARNING: Do not reuse your Monzero keys on another fork, UNLESS this fork has key reuse mitigations built in. Doing so will harm your privacy."), desc_params, positional_options, [](const std::string &s, bool emphasis){ tools::scoped_message_writer(emphasis ? epee::console_color_white : epee::console_color_default, true) << s; }, @@ -11000,7 +11005,7 @@ void simple_wallet::list_mms_messages(const std::vector<mms::message> &messages) void simple_wallet::list_signers(const std::vector<mms::authorized_signer> &signers) { message_writer() << boost::format("%2s %-20s %-s") % tr("#") % tr("Label") % tr("Transport Address"); - message_writer() << boost::format("%2s %-20s %-s") % "" % tr("Auto-Config Token") % tr("Monero Address"); + message_writer() << boost::format("%2s %-20s %-s") % "" % tr("Auto-Config Token") % tr("Monzero Address"); for (size_t i = 0; i < signers.size(); ++i) { const mms::authorized_signer &signer = signers[i]; @@ -11225,14 +11230,14 @@ void simple_wallet::mms_signer(const std::vector<std::string> &args) bool ok = cryptonote::get_account_address_from_str_or_url(info, m_wallet->nettype(), args[3], oa_prompter); if (!ok) { - fail_msg_writer() << tr("Invalid Monero address"); + fail_msg_writer() << tr("Invalid Monzero address"); return; } monero_address = info.address; const std::vector<mms::message> &messages = ms.get_all_messages(); if ((messages.size() > 0) || state.multisig) { - fail_msg_writer() << tr("Wallet state does not allow changing Monero addresses anymore"); + fail_msg_writer() << tr("Wallet state does not allow changing Monzero addresses anymore"); return; } } diff --git a/src/version.cpp.in b/src/version.cpp.in index 5e0807de2..8316422b5 100644 --- a/src/version.cpp.in +++ b/src/version.cpp.in @@ -1,6 +1,6 @@ #define DEF_MONERO_VERSION_TAG "@VERSIONTAG@" #define DEF_MONERO_VERSION "0.18.5.1" -#define DEF_MONERO_RELEASE_NAME "Fluorine Fermi" +#define DEF_MONERO_RELEASE_NAME "Genesis" #define DEF_MONERO_VERSION_FULL DEF_MONERO_VERSION "-" DEF_MONERO_VERSION_TAG #define DEF_MONERO_VERSION_IS_RELEASE @VERSION_IS_RELEASE@ diff --git a/src/wallet/CMakeLists.txt b/src/wallet/CMakeLists.txt index 6095f99d5..4246ac3ed 100644 --- a/src/wallet/CMakeLists.txt +++ b/src/wallet/CMakeLists.txt @@ -100,7 +100,7 @@ if(NOT IOS) ${EXTRA_LIBRARIES}) set_property(TARGET wallet_rpc_server PROPERTY - OUTPUT_NAME "monero-wallet-rpc") + OUTPUT_NAME "monzero-wallet-rpc") install(TARGETS wallet_rpc_server DESTINATION bin) endif() diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 4c83dd890..253aa3a38 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -2226,8 +2226,8 @@ void wallet2::scan_output(const cryptonote::transaction &tx, bool miner_tx, cons if (!m_encrypt_keys_after_refresh && !m_processing_background_cache) { boost::optional<epee::wipeable_string> pwd = m_callback->on_get_password(pool ? "output found in pool" : "output received"); - THROW_WALLET_EXCEPTION_IF(!pwd, error::password_needed, tr("Password is needed to compute key image for incoming monero")); - THROW_WALLET_EXCEPTION_IF(!verify_password(*pwd), error::password_needed, tr("Invalid password: password is needed to compute key image for incoming monero")); + THROW_WALLET_EXCEPTION_IF(!pwd, error::password_needed, tr("Password is needed to compute key image for incoming Monzero")); + THROW_WALLET_EXCEPTION_IF(!verify_password(*pwd), error::password_needed, tr("Invalid password: password is needed to compute key image for incoming Monzero")); m_encrypt_keys_after_refresh.reset(new wallet_keys_unlocker(*this, m_ask_password == AskPasswordToDecrypt && !m_unattended && !m_watch_only, *pwd)); } } @@ -15920,7 +15920,7 @@ mms::multisig_wallet_state wallet2::get_multisig_wallet_state() const state.num_transfer_details = m_transfers.size(); if (state.multisig) { - THROW_WALLET_EXCEPTION_IF(!m_original_keys_available, error::wallet_internal_error, "MMS use not possible because own original Monero address not available"); + THROW_WALLET_EXCEPTION_IF(!m_original_keys_available, error::wallet_internal_error, "MMS use not possible because own original Monzero address not available"); state.address = m_original_address; state.view_secret_key = m_original_view_secret_key; } diff --git a/src/wallet/wallet_args.cpp b/src/wallet/wallet_args.cpp index ce13fc573..f5863ef0f 100644 --- a/src/wallet/wallet_args.cpp +++ b/src/wallet/wallet_args.cpp @@ -148,8 +148,8 @@ namespace wallet_args if (command_line::get_arg(vm, command_line::arg_help)) { - Print(print) << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL; - Print(print) << wallet_args::tr("This is the command line monero wallet. It needs to connect to a monero\n" + Print(print) << "Monzero '" << MONERO_RELEASE_NAME << "' (core v" << MONERO_VERSION_FULL << ")" << ENDL; + Print(print) << wallet_args::tr("This is the command line Monzero wallet. It needs to connect to a Monzero\n" "daemon to work correctly.") << ENDL; Print(print) << wallet_args::tr("Usage:") << ENDL << " " << usage; Print(print) << desc_all; @@ -158,7 +158,7 @@ namespace wallet_args } else if (command_line::get_arg(vm, command_line::arg_version)) { - Print(print) << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")"; + Print(print) << "Monzero '" << MONERO_RELEASE_NAME << "' (core v" << MONERO_VERSION_FULL << ")"; should_terminate = true; return true; } @@ -209,7 +209,7 @@ namespace wallet_args if (!command_line::is_arg_defaulted(vm, arg_max_concurrency)) tools::set_max_concurrency(command_line::get_arg(vm, arg_max_concurrency)); - Print(print) << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")"; + Print(print) << "Monzero '" << MONERO_RELEASE_NAME << "' (core v" << MONERO_VERSION_FULL << ")"; if (!command_line::is_arg_defaulted(vm, arg_log_level)) MINFO("Setting log level = " << command_line::get_arg(vm, arg_log_level)); diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp index 6a3e92b0b..68b28652e 100644 --- a/src/wallet/wallet_rpc_server.cpp +++ b/src/wallet/wallet_rpc_server.cpp @@ -69,7 +69,7 @@ using namespace epee; if (m_wallet->multisig() && !m_wallet->is_multisig_enabled()) \ { \ er.code = WALLET_RPC_ERROR_CODE_DISABLED; \ - er.message = "This wallet is multisig, and multisig is disabled. Multisig is an experimental feature and may have bugs. Things that could go wrong include: funds sent to a multisig wallet can't be spent at all, can only be spent with the participation of a malicious group member, or can be stolen by a malicious group member. You can enable it by running this once in monero-wallet-cli: set enable-multisig-experimental 1"; \ + er.message = "This wallet is multisig, and multisig is disabled. Multisig is an experimental feature and may have bugs. Things that could go wrong include: funds sent to a multisig wallet can't be spent at all, can only be spent with the participation of a malicious group member, or can be stolen by a malicious group member. You can enable it by running this once in monzero-wallet-cli: set enable-multisig-experimental 1"; \ return false; \ } \ } while(0) @@ -431,7 +431,7 @@ namespace tools tools::wallet2::BackgroundMiningSetupType setup = m_wallet->setup_background_mining(); if (setup == tools::wallet2::BackgroundMiningNo) { - MLOG_RED(el::Level::Warning, "Background mining not enabled. Run \"set setup-background-mining 1\" in monero-wallet-cli to change."); + MLOG_RED(el::Level::Warning, "Background mining not enabled. Run \"set setup-background-mining 1\" in monzero-wallet-cli to change."); return; } @@ -456,8 +456,8 @@ namespace tools { MINFO("The daemon is not set up to background mine."); MINFO("With background mining enabled, the daemon will mine when idle and not on battery."); - MINFO("Enabling this supports the network you are using, and makes you eligible for receiving new monero"); - MINFO("Set setup-background-mining to 1 in monero-wallet-cli to change."); + MINFO("Enabling this supports the network you are using, and makes you eligible for receiving new XMZ"); + MINFO("Set setup-background-mining to 1 in monzero-wallet-cli to change."); return; } @@ -1055,7 +1055,7 @@ namespace tools } if (addresses.empty()) { - er.message = std::string("No Monero address found at ") + url; + er.message = std::string("No Monzero address found at ") + url; return {}; } return addresses[0]; @@ -2468,7 +2468,7 @@ namespace tools } if (addresses.empty()) { - er.message = std::string("No Monero address found at ") + url; + er.message = std::string("No Monzero address found at ") + url; return {}; } return addresses[0]; @@ -3275,7 +3275,7 @@ namespace tools } if (addresses.empty()) { - er.message = std::string("No Monero address found at ") + url; + er.message = std::string("No Monzero address found at ") + url; return {}; } return addresses[0]; @@ -3323,7 +3323,7 @@ namespace tools } if (addresses.empty()) { - er.message = std::string("No Monero address found at ") + url; + er.message = std::string("No Monzero address found at ") + url; return {}; } return addresses[0]; @@ -4655,7 +4655,7 @@ namespace tools } if (addresses.empty()) { - er.message = std::string("No Monero address found at ") + url; + er.message = std::string("No Monzero address found at ") + url; return {}; } address = addresses[0]; @@ -5080,8 +5080,8 @@ int main(int argc, char** argv) { bool should_terminate = false; std::tie(vm, should_terminate) = wallet_args::main( argc, argv, - "monero-wallet-rpc [--wallet-file=<file>|--generate-from-json=<file>|--wallet-dir=<directory>] [--rpc-bind-port=<port>]", - tools::wallet_rpc_server::tr("This is the RPC monero wallet. It needs to connect to a monero\ndaemon to work correctly."), + "monzero-wallet-rpc [--wallet-file=<file>|--generate-from-json=<file>|--wallet-dir=<directory>] [--rpc-bind-port=<port>]", + tools::wallet_rpc_server::tr("This is the RPC Monzero wallet. It needs to connect to a Monzero\ndaemon to work correctly."), desc_params, po::positional_options_description(), [](const std::string &s, bool emphasis){ tools::scoped_message_writer(emphasis ? epee::console_color_white : epee::console_color_default, true) << s; }, diff --git a/start-monzero-gui.sh b/start-monzero-gui.sh new file mode 100755 index 000000000..81fa5da13 --- /dev/null +++ b/start-monzero-gui.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +PROJECT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +GUI_BIN="${MONZERO_GUI_BIN:-$PROJECT_DIR/monzero-gui/build-monzero/bin/monzero-wallet-gui}" +RPC_PORT="${MONZERO_RPC_PORT:-6175}" + +if [[ ! -x "$GUI_BIN" ]]; then + printf 'Error: GUI executable not found: %s\n' "$GUI_BIN" >&2 + printf 'Build the Monzero GUI first, or set MONZERO_GUI_BIN.\n' >&2 + exit 1 +fi + +if ! curl --silent --fail --max-time 2 \ + --header 'Content-Type: application/json' \ + --data '{}' \ + "http://127.0.0.1:$RPC_PORT/get_info" >/dev/null 2>&1; then + printf 'Error: the Monzero daemon is not running at 127.0.0.1:%s.\n' "$RPC_PORT" >&2 + printf 'Start it separately with: %s/start-monzerod.sh\n' "$PROJECT_DIR" >&2 + exit 1 +fi + +printf '\nLaunching Monzero GUI.\n' +printf 'If the GUI asks for a node, choose a remote/custom node and enter:\n' +printf ' Address: 127.0.0.1\n Port: %s\n Trusted: yes\n\n' "$RPC_PORT" + +exec "$GUI_BIN" --disable-check-updates "$@" diff --git a/start-monzero-miner.sh b/start-monzero-miner.sh new file mode 100755 index 000000000..45822762f --- /dev/null +++ b/start-monzero-miner.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +RPC_PORT="${MONZERO_RPC_PORT:-16175}" +MINER_ADDRESS="${MONZERO_MINER_ADDRESS:-FR3j29tPrro5vdgMvFMyQr4W7VX41VzRHKjvhhNm5EB32MUFTiKFW21PCDrvUM5hvSQLYBom4jmY1AehUJKKtZbXH1uQ5u6}" +THREADS="${1:-${MONZERO_MINER_THREADS:-1}}" +RPC_URL="http://127.0.0.1:$RPC_PORT" + +if ! [[ "$THREADS" =~ ^[1-9][0-9]*$ ]]; then + printf 'Error: thread count must be a positive integer.\n' >&2 + printf 'Usage: %s [threads]\n' "$0" >&2 + exit 1 +fi + +if ! curl --silent --fail --max-time 2 \ + --header 'Content-Type: application/json' \ + --data '{}' "$RPC_URL/get_info" >/dev/null; then + printf 'Error: the Monzero daemon is not running at 127.0.0.1:%s.\n' "$RPC_PORT" >&2 + printf 'Start it separately with ./start-monzerod.sh\n' >&2 + exit 1 +fi + +response="$(curl --silent --show-error --fail --max-time 10 \ + --header 'Content-Type: application/json' \ + --data "{\"miner_address\":\"$MINER_ADDRESS\",\"threads_count\":$THREADS,\"do_background_mining\":false,\"ignore_battery\":true}" \ + "$RPC_URL/start_mining")" + +python3 - "$response" "$THREADS" "$MINER_ADDRESS" <<'PY' +import json +import sys + +data = json.loads(sys.argv[1]) +status = data.get("status", "") +if status != "OK": + raise SystemExit(f"Mining failed: {data.get('error_details') or status or data}") +print(f"Mining started with {sys.argv[2]} thread(s).") +print(f"Rewards address: {sys.argv[3]}") +PY diff --git a/start-monzero-wallet-cli.sh b/start-monzero-wallet-cli.sh new file mode 100755 index 000000000..35d4c003a --- /dev/null +++ b/start-monzero-wallet-cli.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +PROJECT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +WALLET_BIN="${MONZERO_WALLET_CLI_BIN:-$PROJECT_DIR/build/bin/monzero-wallet-cli}" +RPC_PORT="${MONZERO_RPC_PORT:-16175}" +DEFAULT_WALLET="${MONZERO_WALLET_FILE:-$HOME/Documents/Wallet1/Wallet1}" +WALLET_FILE="${1:-$DEFAULT_WALLET}" + +if [[ ! -x "$WALLET_BIN" ]]; then + printf 'Error: wallet executable not found: %s\n' "$WALLET_BIN" >&2 + exit 1 +fi + +if [[ ! -f "$WALLET_FILE.keys" ]]; then + printf 'Error: wallet key file not found: %s.keys\n' "$WALLET_FILE" >&2 + printf 'Pass an existing wallet path as the first argument, for example:\n' >&2 + printf ' %s /path/to/wallet\n' "$0" >&2 + exit 1 +fi + +if ! curl --silent --fail --max-time 2 \ + --header 'Content-Type: application/json' \ + --data '{}' \ + "http://127.0.0.1:$RPC_PORT/get_info" >/dev/null 2>&1; then + printf 'Error: the Monzero daemon is not running at 127.0.0.1:%s.\n' "$RPC_PORT" >&2 + printf 'Start it separately with: %s/start-monzerod.sh\n' "$PROJECT_DIR" >&2 + exit 1 +fi + +exec "$WALLET_BIN" \ + --wallet-file "$WALLET_FILE" \ + --daemon-address "127.0.0.1:$RPC_PORT" diff --git a/start-monzerod.sh b/start-monzerod.sh new file mode 100755 index 000000000..6b3ac8c2f --- /dev/null +++ b/start-monzerod.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail + +PROJECT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +DAEMON_BIN="${MONZERO_DAEMON_BIN:-$PROJECT_DIR/build/bin/monzerod}" +DATA_DIR="${MONZERO_DATA_DIR:-$HOME/.monzero-auto-test}" +P2P_PORT="${MONZERO_P2P_PORT:-16174}" +RPC_PORT="${MONZERO_RPC_PORT:-16175}" +ZMQ_PORT="${MONZERO_ZMQ_PORT:-16176}" + +if [[ ! -x "$DAEMON_BIN" ]]; then + printf 'Error: daemon executable not found: %s\n' "$DAEMON_BIN" >&2 + printf 'Build monzerod first, or set MONZERO_DAEMON_BIN.\n' >&2 + exit 1 +fi + +daemon_ready() { + curl --silent --fail --max-time 2 \ + --header 'Content-Type: application/json' \ + --data '{}' \ + "http://127.0.0.1:$RPC_PORT/get_info" >/dev/null 2>&1 +} + +if daemon_ready; then + printf 'Monzero daemon is already running at 127.0.0.1:%s.\n' "$RPC_PORT" + exit 0 +fi + +if command -v ss >/dev/null && ss -ltn | awk '{print $4}' | grep -Eq "(^|:)$RPC_PORT$"; then + printf 'Error: port %s is occupied, but it is not responding as a Monzero daemon.\n' "$RPC_PORT" >&2 + exit 1 +fi + +mkdir -p "$DATA_DIR" + +printf 'Starting Monzero daemon...\n' +printf ' Data: %s\n P2P: %s\n RPC: %s\n ZMQ: %s\n' "$DATA_DIR" "$P2P_PORT" "$RPC_PORT" "$ZMQ_PORT" + +"$DAEMON_BIN" \ + --detach \ + --data-dir "$DATA_DIR" \ + --p2p-bind-port "$P2P_PORT" \ + --rpc-bind-port "$RPC_PORT" \ + --zmq-rpc-bind-port "$ZMQ_PORT" \ + --add-priority-node 82.165.112.237:6174 \ + --no-igd + +for _ in {1..30}; do + if daemon_ready; then + info="$(curl --silent --max-time 2 --header 'Content-Type: application/json' --data '{}' "http://127.0.0.1:$RPC_PORT/get_info")" + python3 - "$info" <<'PY' +import json, sys +data = json.loads(sys.argv[1]) +print(f"Daemon ready: height={data.get('height', '?')} synchronized={str(data.get('synchronized', False)).lower()}") +PY + printf 'Log: %s/monzero.log\n' "$DATA_DIR" + exit 0 + fi + sleep 1 +done + +printf 'Error: daemon did not become ready within 30 seconds.\n' >&2 +printf 'Check: %s/monzero.log\n' "$DATA_DIR" >&2 +exit 1 diff --git a/stop-monzero-miner.sh b/stop-monzero-miner.sh new file mode 100755 index 000000000..fc00b1f92 --- /dev/null +++ b/stop-monzero-miner.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +RPC_PORT="${MONZERO_RPC_PORT:-16175}" +RPC_URL="http://127.0.0.1:$RPC_PORT" + +response="$(curl --silent --show-error --fail --max-time 10 \ + --header 'Content-Type: application/json' \ + --data '{}' "$RPC_URL/stop_mining")" + +python3 - "$response" <<'PY' +import json +import sys + +data = json.loads(sys.argv[1]) +status = data.get("status", "") +if status != "OK": + raise SystemExit(f"Unable to stop mining: {data.get('error_details') or status or data}") +print("Mining stopped. The daemon remains running.") +PY diff --git a/tests/core_tests/double_spend.h b/tests/core_tests/double_spend.h index 6dccd6a0c..f47c69661 100644 --- a/tests/core_tests/double_spend.h +++ b/tests/core_tests/double_spend.h @@ -32,7 +32,11 @@ #include "chaingen.h" const size_t invalid_index_value = std::numeric_limits<size_t>::max(); -const uint64_t FIRST_BLOCK_REWARD = 17592186044415; +// Monzero's verified height-zero subsidy (190.73486328125 XMZ). Keeping the +// inherited Monero value creates a miner change output in tx_0, so the +// in-transaction double-spend fixture selects that change as though it +// belonged to Bob and fails before consensus validation is exercised. +const uint64_t FIRST_BLOCK_REWARD = 19073486328125; template<class concrete_test> diff --git a/tests/phase0/README.md b/tests/phase0/README.md new file mode 100644 index 000000000..3b619f1ee --- /dev/null +++ b/tests/phase0/README.md @@ -0,0 +1,70 @@ +# Monzero Phase 0 integration tests + +These tests use temporary regtest directories and must not connect to or modify +the live Monzero network. + +Run the three-node smoke test from the repository root: + +```bash +python3 tests/phase0/monzero_multinode.py --build-dir build +``` + +The test starts three `monzerod` processes and one `monzero-wallet-rpc` +process on localhost. It generates a temporary wallet, creates blocks, checks +that every node reaches the same tip, restarts one node, and verifies that the +restarted node catches up. It then matures a coinbase output, sends one XMZ to +a second temporary wallet, verifies transaction-pool relay through all three +nodes, mines the transaction on a different node, and verifies the receiver's +confirmed balance. Before the transfer, it isolates the two edge nodes, creates +competing three- and five-block forks, reconnects them, and verifies all nodes +adopt the longer fork and identical tip. Finally, it restores the receiving +wallet from its temporary mnemonic seed, rescans from height zero, and checks +the recovered address and balance. + +Ports 26174-26205 must be available. All child processes are terminated on +normal completion and on test failure. Pass `--keep-data` to retain temporary +databases and logs for diagnosis. + +This is a smoke test, not the complete consensus qualification suite. +Double-spend, deeper/adversarial reorg, and adversarial asset tests remain +required before activating a new protocol version. + +## Fast unit gate + +Run the ordinary unit tests with a validating resolver and keep the expensive +pruning-boundary concurrency scenario in the separate stress-test gate: + +```bash +tests/phase0/run-fast-unit-tests.sh build +``` + +Override `DNS_PUBLIC` with another validating TCP resolver when required. The +excluded `cryptonote_protocol_handler.race_condition` test must still pass in +the scheduled stress gate; exclusion here is not a waiver. + +## Stress gate + +Run the pruning-boundary concurrency scenario separately: + +```bash +tests/phase0/run-stress-tests.sh build +``` + +The default timeout is 1,800 seconds. Override it with +`MONZERO_STRESS_TIMEOUT=<seconds>` when running on slower hardware. A timeout +is a failed gate and must not be treated as a skipped test. + +## Adversarial core gate + +Build the native core-test executable and run the focused double-spend and +key-image suite: + +```bash +cmake --build build --target core_tests -j2 +tests/phase0/run-adversarial-core-tests.sh build +``` + +This covers conflicting key images in the transaction pool; double spends in +one transaction, one block, separate blocks, competing chains, and alternate +chains; and both kept-by-block modes. The default timeout is 1,800 seconds and +can be overridden with `MONZERO_ADVERSARIAL_TIMEOUT`. diff --git a/tests/phase0/monzero_multinode.py b/tests/phase0/monzero_multinode.py new file mode 100755 index 000000000..106fcd9cb --- /dev/null +++ b/tests/phase0/monzero_multinode.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python3 + +"""Isolated Monzero three-node propagation and restart smoke test. + +This script uses temporary regtest data and wallet directories. It never reads +or writes the user's normal Monzero data directory or wallet files. +""" + +from __future__ import annotations + +import argparse +import json +import pathlib +import shutil +import signal +import subprocess +import sys +import tempfile +import time +import urllib.error +import urllib.request + + +HOST = "127.0.0.1" +P2P_PORTS = (26174, 26184, 26194) +RPC_PORTS = (26175, 26185, 26195) +ZMQ_PORTS = (26176, 26186, 26196) +WALLET_RPC_PORT = 26205 + + +class TestFailure(RuntimeError): + pass + + +def request(port: int, path: str, body: dict, timeout: float = 5.0) -> dict: + payload = json.dumps(body).encode("utf-8") + req = urllib.request.Request( + f"http://{HOST}:{port}{path}", + data=payload, + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(req, timeout=timeout) as response: + result = json.loads(response.read().decode("utf-8")) + if "error" in result: + raise TestFailure(f"RPC error from port {port}: {result['error']}") + return result + + +def json_rpc(port: int, method: str, params: dict | None = None) -> dict: + response = request( + port, + "/json_rpc", + {"jsonrpc": "2.0", "id": "0", "method": method, "params": params or {}}, + ) + return response.get("result", {}) + + +def get_info(port: int) -> dict: + return request(port, "/get_info", {}) + + +def transaction_pool_hashes(port: int) -> set[str]: + response = request(port, "/get_transaction_pool", {}) + return { + transaction.get("id_hash", "") + for transaction in response.get("transactions", []) + if transaction.get("id_hash") + } + + +def wait_until(description: str, predicate, timeout: float = 45.0) -> None: + deadline = time.monotonic() + timeout + last_error = None + while time.monotonic() < deadline: + try: + if predicate(): + return + except (OSError, TestFailure, urllib.error.URLError, ValueError) as error: + last_error = error + time.sleep(0.25) + suffix = f"; last error: {last_error}" if last_error else "" + raise TestFailure(f"Timed out waiting for {description}{suffix}") + + +def stop_process(process: subprocess.Popen | None) -> None: + if process is None or process.poll() is not None: + return + process.send_signal(signal.SIGTERM) + try: + process.wait(timeout=10) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=5) + + +def daemon_command(binary: pathlib.Path, root: pathlib.Path, index: int) -> list[str]: + peers = [] + if index > 0: + peers.extend(["--add-exclusive-node", f"{HOST}:{P2P_PORTS[index - 1]}"]) + if index + 1 < len(P2P_PORTS): + peers.extend(["--add-exclusive-node", f"{HOST}:{P2P_PORTS[index + 1]}"]) + return [ + str(binary), + "--regtest", + "--fixed-difficulty", "1", + "--data-dir", str(root / f"node-{index}"), + "--p2p-bind-ip", HOST, + "--p2p-bind-port", str(P2P_PORTS[index]), + "--rpc-bind-ip", HOST, + "--rpc-bind-port", str(RPC_PORTS[index]), + "--zmq-rpc-bind-ip", HOST, + "--zmq-rpc-bind-port", str(ZMQ_PORTS[index]), + "--rpc-ssl", "disabled", + "--disable-dns-checkpoints", + "--check-updates", "disabled", + "--no-igd", + "--non-interactive", + "--log-level", "0", + *peers, + ] + + +def start_daemon(binary: pathlib.Path, root: pathlib.Path, index: int, logs) -> subprocess.Popen: + log = open(root / f"node-{index}.log", "ab", buffering=0) + logs.append(log) + return subprocess.Popen( + daemon_command(binary, root, index), + stdout=log, + stderr=subprocess.STDOUT, + start_new_session=True, + ) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--build-dir", default="build", help="CMake build directory") + parser.add_argument("--keep-data", action="store_true", help="keep temporary logs and databases") + args = parser.parse_args() + + build_dir = pathlib.Path(args.build_dir).resolve() + daemon = build_dir / "bin" / "monzerod" + wallet_rpc = build_dir / "bin" / "monzero-wallet-rpc" + for binary in (daemon, wallet_rpc): + if not binary.is_file(): + raise TestFailure(f"Required binary not found: {binary}") + + root = pathlib.Path(tempfile.mkdtemp(prefix="monzero-phase0-")) + processes: list[subprocess.Popen | None] = [None, None, None] + wallet_process = None + logs = [] + + try: + print(f"Temporary test network: {root}") + for index in range(3): + processes[index] = start_daemon(daemon, root, index, logs) + + for index, port in enumerate(RPC_PORTS): + wait_until(f"node {index} RPC", lambda port=port: get_info(port).get("status") == "OK") + + wait_until( + "the three-node P2P topology", + lambda: all( + get_info(port).get("incoming_connections_count", 0) + + get_info(port).get("outgoing_connections_count", 0) >= 1 + for port in RPC_PORTS + ), + timeout=60, + ) + + wallet_log = open(root / "wallet-rpc.log", "ab", buffering=0) + logs.append(wallet_log) + (root / "wallets").mkdir() + wallet_process = subprocess.Popen( + [ + str(wallet_rpc), + "--wallet-dir", str(root / "wallets"), + "--rpc-bind-ip", HOST, + "--rpc-bind-port", str(WALLET_RPC_PORT), + "--disable-rpc-login", + "--rpc-ssl", "disabled", + "--daemon-address", f"{HOST}:{RPC_PORTS[0]}", + "--daemon-ssl", "disabled", + # Regtest intentionally activates the latest hard fork at + # height 1, which cannot match the public-network schedule. + "--allow-mismatched-daemon-version", + "--log-level", "0", + ], + stdout=wallet_log, + stderr=subprocess.STDOUT, + start_new_session=True, + ) + wait_until("wallet RPC", lambda: json_rpc(WALLET_RPC_PORT, "get_version").get("version", 0) > 0) + json_rpc(WALLET_RPC_PORT, "create_wallet", {"filename": "miner", "password": "", "language": "English"}) + address = json_rpc(WALLET_RPC_PORT, "get_address").get("address") + if not address: + raise TestFailure("Temporary wallet did not return an address") + + initial_height = get_info(RPC_PORTS[0])["height"] + json_rpc(RPC_PORTS[0], "generateblocks", {"wallet_address": address, "amount_of_blocks": 12}) + target_height = initial_height + 12 + wait_until( + f"all nodes to reach height {target_height}", + lambda: all(get_info(port)["height"] >= target_height for port in RPC_PORTS), + timeout=90, + ) + hashes = [get_info(port)["top_block_hash"] for port in RPC_PORTS] + if len(set(hashes)) != 1: + raise TestFailure(f"Nodes disagree on the propagated tip: {hashes}") + + print("Stopping node 2 and extending the chain by five blocks...") + stop_process(processes[2]) + processes[2] = None + json_rpc(RPC_PORTS[0], "generateblocks", {"wallet_address": address, "amount_of_blocks": 5}) + restart_target = target_height + 5 + wait_until("connected nodes to accept the extension", lambda: get_info(RPC_PORTS[1])["height"] >= restart_target) + + processes[2] = start_daemon(daemon, root, 2, logs) + wait_until("restarted node 2 RPC", lambda: get_info(RPC_PORTS[2]).get("status") == "OK") + wait_until( + f"restarted node 2 to catch up to height {restart_target}", + lambda: get_info(RPC_PORTS[2])["height"] >= restart_target, + timeout=90, + ) + final_info = [get_info(port) for port in RPC_PORTS] + final_hashes = [info["top_block_hash"] for info in final_info] + if len(set(final_hashes)) != 1: + raise TestFailure(f"Nodes disagree after restart: {final_hashes}") + + print("Testing competing forks and longest-chain convergence...") + stop_process(processes[1]) + processes[1] = None + wait_until( + "edge nodes to become isolated", + lambda: all( + get_info(port).get("incoming_connections_count", 0) + + get_info(port).get("outgoing_connections_count", 0) == 0 + for port in (RPC_PORTS[0], RPC_PORTS[2]) + ), + timeout=30, + ) + json_rpc(RPC_PORTS[0], "generateblocks", {"wallet_address": address, "amount_of_blocks": 3}) + json_rpc(RPC_PORTS[2], "generateblocks", {"wallet_address": address, "amount_of_blocks": 5}) + longer_fork_height = restart_target + 5 + longer_fork_tip = get_info(RPC_PORTS[2])["top_block_hash"] + + processes[1] = start_daemon(daemon, root, 1, logs) + wait_until("restarted node 1 RPC", lambda: get_info(RPC_PORTS[1]).get("status") == "OK") + wait_until( + "all nodes to adopt the longer competing fork", + lambda: all( + get_info(port)["height"] >= longer_fork_height + and get_info(port)["top_block_hash"] == longer_fork_tip + for port in RPC_PORTS + ), + timeout=120, + ) + + print("Testing wallet-to-wallet transaction relay and confirmation...") + json_rpc(RPC_PORTS[0], "generateblocks", {"wallet_address": address, "amount_of_blocks": 60}) + spendable_height = longer_fork_height + 60 + wait_until( + f"all nodes to reach spendable height {spendable_height}", + lambda: all(get_info(port)["height"] >= spendable_height for port in RPC_PORTS), + timeout=120, + ) + json_rpc(WALLET_RPC_PORT, "refresh") + + json_rpc(WALLET_RPC_PORT, "close_wallet") + json_rpc(WALLET_RPC_PORT, "create_wallet", {"filename": "receiver", "password": "", "language": "English"}) + receiver_address = json_rpc(WALLET_RPC_PORT, "get_address").get("address") + if not receiver_address: + raise TestFailure("Receiver wallet did not return an address") + json_rpc(WALLET_RPC_PORT, "close_wallet") + json_rpc(WALLET_RPC_PORT, "open_wallet", {"filename": "miner", "password": ""}) + json_rpc(WALLET_RPC_PORT, "refresh") + + transfer = json_rpc( + WALLET_RPC_PORT, + "transfer", + { + "destinations": [{"amount": 100_000_000_000, "address": receiver_address}], + "get_tx_key": True, + }, + ) + transaction_hash = transfer.get("tx_hash") + if not transaction_hash: + raise TestFailure(f"Transfer did not return a transaction hash: {transfer}") + wait_until( + "transaction relay to every node", + lambda: all(transaction_hash in transaction_pool_hashes(port) for port in RPC_PORTS), + timeout=60, + ) + + json_rpc(RPC_PORTS[1], "generateblocks", {"wallet_address": address, "amount_of_blocks": 1}) + confirmed_height = spendable_height + 1 + wait_until( + f"transaction confirmation at height {confirmed_height}", + lambda: all( + get_info(port)["height"] >= confirmed_height + and transaction_hash not in transaction_pool_hashes(port) + for port in RPC_PORTS + ), + timeout=90, + ) + json_rpc(WALLET_RPC_PORT, "close_wallet") + json_rpc(WALLET_RPC_PORT, "open_wallet", {"filename": "receiver", "password": ""}) + json_rpc(WALLET_RPC_PORT, "refresh") + receiver_balance = json_rpc(WALLET_RPC_PORT, "get_balance").get("balance", 0) + if receiver_balance != 100_000_000_000: + raise TestFailure(f"Receiver balance is {receiver_balance}, expected 100000000000") + + receiver_seed = json_rpc(WALLET_RPC_PORT, "query_key", {"key_type": "mnemonic"}).get("key") + if not receiver_seed: + raise TestFailure("Receiver wallet did not return its temporary recovery seed") + json_rpc(WALLET_RPC_PORT, "close_wallet") + restored = json_rpc( + WALLET_RPC_PORT, + "restore_deterministic_wallet", + { + "filename": "receiver-restored", + "password": "", + "seed": receiver_seed, + "restore_height": 0, + }, + ) + if restored.get("address") != receiver_address: + raise TestFailure( + f"Restored address {restored.get('address')} does not match {receiver_address}" + ) + json_rpc(WALLET_RPC_PORT, "refresh") + restored_balance = json_rpc(WALLET_RPC_PORT, "get_balance").get("balance", 0) + if restored_balance != receiver_balance: + raise TestFailure( + f"Restored balance is {restored_balance}, expected {receiver_balance}" + ) + + final_info = [get_info(port) for port in RPC_PORTS] + final_hashes = [info["top_block_hash"] for info in final_info] + if len(set(final_hashes)) != 1: + raise TestFailure(f"Nodes disagree after transaction confirmation: {final_hashes}") + + print("PASS: connectivity, propagation, catch-up, reorg, transfer, and wallet restoration") + print("Final height:", final_info[0]["height"]) + print("Final tip:", final_hashes[0]) + print("Confirmed transaction:", transaction_hash) + return 0 + finally: + stop_process(wallet_process) + for process in reversed(processes): + stop_process(process) + for log in logs: + log.close() + if args.keep_data: + kept = pathlib.Path.cwd() / f"monzero-phase0-{int(time.time())}" + shutil.move(str(root), str(kept)) + print(f"Kept test data at {kept}") + else: + shutil.rmtree(root, ignore_errors=True) + + +if __name__ == "__main__": + try: + sys.exit(main()) + except (TestFailure, OSError, urllib.error.URLError) as error: + print(f"FAIL: {error}", file=sys.stderr) + sys.exit(1) diff --git a/tests/phase0/run-adversarial-core-tests.sh b/tests/phase0/run-adversarial-core-tests.sh new file mode 100755 index 000000000..05fd95a96 --- /dev/null +++ b/tests/phase0/run-adversarial-core-tests.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +build_dir="${1:-${project_root}/build}" +core_tests="${build_dir}/tests/core_tests/core_tests" +timeout_seconds="${MONZERO_ADVERSARIAL_TIMEOUT:-1800}" +filter='^(txpool_spend_key_(public|all)|txpool_double_spend_(norelay|local|keyimage)|gen_double_spend_.*)$' + +if [[ ! -x "${core_tests}" ]]; then + echo "Core-test binary not found: ${core_tests}" >&2 + echo "Build it with: cmake --build ${build_dir} --target core_tests -j2" >&2 + exit 1 +fi + +echo "Running Monzero double-spend and key-image adversarial gate (timeout: ${timeout_seconds}s)" +exec timeout --signal=INT --kill-after=30 "${timeout_seconds}" \ + "${core_tests}" \ + --generate_and_play_test_data \ + --filter "${filter}" diff --git a/tests/phase0/run-fast-unit-tests.sh b/tests/phase0/run-fast-unit-tests.sh new file mode 100755 index 000000000..08e3152e4 --- /dev/null +++ b/tests/phase0/run-fast-unit-tests.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +build_dir="${1:-${project_root}/build}" +unit_tests="${build_dir}/tests/unit_tests/unit_tests" + +if [[ ! -x "${unit_tests}" ]]; then + echo "Unit-test binary not found: ${unit_tests}" >&2 + echo "Build it with: cmake --build ${build_dir} --target unit_tests -j2" >&2 + exit 1 +fi + +# Keep external DNS behavior reproducible across developer and CI resolvers. +export DNS_PUBLIC="${DNS_PUBLIC:-tcp://1.1.1.1}" + +# This pruning-boundary concurrency scenario creates about 9,600 blocks and is +# kept in the separate stress gate. It is not a fast unit test. +exec "${unit_tests}" \ + --gtest_color=no \ + --gtest_filter=-cryptonote_protocol_handler.race_condition diff --git a/tests/phase0/run-stress-tests.sh b/tests/phase0/run-stress-tests.sh new file mode 100755 index 000000000..20e8e7bc5 --- /dev/null +++ b/tests/phase0/run-stress-tests.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +build_dir="${1:-${project_root}/build}" +unit_tests="${build_dir}/tests/unit_tests/unit_tests" +timeout_seconds="${MONZERO_STRESS_TIMEOUT:-1800}" + +if [[ ! -x "${unit_tests}" ]]; then + echo "Unit-test binary not found: ${unit_tests}" >&2 + echo "Build it with: cmake --build ${build_dir} --target unit_tests -j2" >&2 + exit 1 +fi + +export DNS_PUBLIC="${DNS_PUBLIC:-tcp://1.1.1.1}" + +echo "Running the pruning-boundary concurrency stress test (timeout: ${timeout_seconds}s)" +exec timeout --signal=INT --kill-after=30 "${timeout_seconds}" \ + "${unit_tests}" \ + --gtest_color=no \ + --gtest_filter=cryptonote_protocol_handler.race_condition diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index 2415dfea8..16e498ae9 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -30,6 +30,7 @@ set(unit_tests_sources account.cpp apply_permutation.cpp address_from_url.cpp + asset_types.cpp base58.cpp blockchain_db.cpp block_queue.cpp @@ -67,6 +68,7 @@ set(unit_tests_sources memwipe.cpp mlocker.cpp mnemonics.cpp + monzero_consensus.cpp mul_div.cpp multiexp.cpp multisig.cpp @@ -154,3 +156,4 @@ add_test( monero_add_minimal_executable(test_notifier test_notifier.cpp) target_link_libraries(test_notifier ${EXTRA_LIBRARIES}) set_property(TARGET test_notifier PROPERTY FOLDER "tests") +add_dependencies(unit_tests test_notifier) diff --git a/tests/unit_tests/asset_types.cpp b/tests/unit_tests/asset_types.cpp new file mode 100644 index 000000000..b2f250fc5 --- /dev/null +++ b/tests/unit_tests/asset_types.cpp @@ -0,0 +1,442 @@ +#include "gtest/gtest.h" + +#include <cstring> +#include <limits> +#include <set> +#include <string> +#include <vector> + +#include "cryptonote_basic/asset_types.h" +#include "string_tools.h" + +namespace +{ + cryptonote::assets::issuance_descriptor make_descriptor(cryptonote::network_type network) + { + cryptonote::assets::issuance_descriptor descriptor; + descriptor.network = network; + if (!epee::string_tools::hex_to_pod( + "9b2e4c0281c0b02e7c53291a94d1d0cbff8883f8024f5142ee494ffbbd088071", + descriptor.issuer_key)) + throw std::runtime_error("invalid fixed issuer key test vector"); + std::memset(descriptor.issuance_nonce.data, 0x42, sizeof(descriptor.issuance_nonce.data)); + descriptor.atomic_supply = UINT64_C(2100000000000000); + descriptor.display_decimals = 8; + std::memset(descriptor.metadata_hash.data, 0x24, sizeof(descriptor.metadata_hash.data)); + descriptor.metadata_reference = "ipfs://bafy-monzero-test-vector"; + return descriptor; + } + + crypto::signature authorize(const cryptonote::assets::issuance_descriptor& descriptor, + const crypto::public_key& public_key, const crypto::secret_key& secret_key) + { + crypto::hash message{}; + if (!cryptonote::assets::derive_issuance_authorization_hash(descriptor, message)) + throw std::runtime_error("could not derive issuance authorization test message"); + crypto::signature signature{}; + crypto::generate_signature(message, public_key, secret_key, signature); + return signature; + } +} + +TEST(asset_types, deterministic_canonical_identity) +{ + const auto descriptor = make_descriptor(cryptonote::MAINNET); + crypto::hash first{}; + crypto::hash second{}; + ASSERT_TRUE(cryptonote::assets::derive_asset_id(descriptor, first)); + ASSERT_TRUE(cryptonote::assets::derive_asset_id(descriptor, second)); + EXPECT_EQ(first, second); + EXPECT_EQ("1a83e64b6ec56327a06ae1d25387b6f1762120e0ec0b532351395d770a9896ce", + epee::string_tools::pod_to_hex(first)); + + std::vector<uint8_t> encoded; + ASSERT_TRUE(cryptonote::assets::encode_issuance_descriptor(descriptor, encoded)); + ASSERT_GT(encoded.size(), descriptor.metadata_reference.size()); +} + +TEST(asset_types, network_domain_separation) +{ + auto mainnet = make_descriptor(cryptonote::MAINNET); + auto testnet = mainnet; + testnet.network = cryptonote::TESTNET; + crypto::hash mainnet_id{}; + crypto::hash testnet_id{}; + ASSERT_TRUE(cryptonote::assets::derive_asset_id(mainnet, mainnet_id)); + ASSERT_TRUE(cryptonote::assets::derive_asset_id(testnet, testnet_id)); + EXPECT_NE(mainnet_id, testnet_id); +} + +TEST(asset_types, every_identity_field_is_committed) +{ + const auto original = make_descriptor(cryptonote::MAINNET); + crypto::hash original_id{}; + ASSERT_TRUE(cryptonote::assets::derive_asset_id(original, original_id)); + + auto changed = original; + changed.atomic_supply++; + crypto::hash changed_id{}; + ASSERT_TRUE(cryptonote::assets::derive_asset_id(changed, changed_id)); + EXPECT_NE(original_id, changed_id); + + changed = original; + changed.metadata_reference.push_back('2'); + ASSERT_TRUE(cryptonote::assets::derive_asset_id(changed, changed_id)); + EXPECT_NE(original_id, changed_id); + + changed = original; + changed.issuance_nonce.data[0] ^= 1; + ASSERT_TRUE(cryptonote::assets::derive_asset_id(changed, changed_id)); + EXPECT_NE(original_id, changed_id); + + changed = original; + changed.type = cryptonote::assets::asset_class::edition; + changed.display_decimals = 0; + ASSERT_TRUE(cryptonote::assets::derive_asset_id(changed, changed_id)); + EXPECT_NE(original_id, changed_id); + + changed = original; + changed.type = cryptonote::assets::asset_class::non_fungible; + changed.atomic_supply = 1; + changed.display_decimals = 0; + std::memset(changed.collection_id.data, 0x11, sizeof(changed.collection_id.data)); + ASSERT_TRUE(cryptonote::assets::derive_asset_id(changed, changed_id)); + EXPECT_NE(original_id, changed_id); +} + +TEST(asset_types, rejects_invalid_descriptors) +{ + std::string error; + auto descriptor = make_descriptor(cryptonote::MAINNET); + + descriptor.version++; + EXPECT_FALSE(cryptonote::assets::validate_issuance_descriptor(descriptor, &error)); + descriptor = make_descriptor(cryptonote::MAINNET); + descriptor.network = cryptonote::FAKECHAIN; + EXPECT_FALSE(cryptonote::assets::validate_issuance_descriptor(descriptor, &error)); + descriptor = make_descriptor(cryptonote::MAINNET); + descriptor.atomic_supply = 0; + EXPECT_FALSE(cryptonote::assets::validate_issuance_descriptor(descriptor, &error)); + descriptor = make_descriptor(cryptonote::MAINNET); + descriptor.display_decimals = cryptonote::assets::MAX_DISPLAY_DECIMALS + 1; + EXPECT_FALSE(cryptonote::assets::validate_issuance_descriptor(descriptor, &error)); + descriptor = make_descriptor(cryptonote::MAINNET); + descriptor.metadata_reference.assign(cryptonote::assets::MAX_METADATA_REFERENCE_BYTES + 1, 'x'); + EXPECT_FALSE(cryptonote::assets::validate_issuance_descriptor(descriptor, &error)); + descriptor = make_descriptor(cryptonote::MAINNET); + descriptor.metadata_reference = std::string("bad\0reference", 13); + EXPECT_FALSE(cryptonote::assets::validate_issuance_descriptor(descriptor, &error)); +} + +TEST(asset_types, enforces_nft_collection_and_edition_rules) +{ + std::string error; + auto nft = make_descriptor(cryptonote::TESTNET); + nft.type = cryptonote::assets::asset_class::non_fungible; + nft.atomic_supply = 1; + nft.display_decimals = 0; + EXPECT_TRUE(cryptonote::assets::validate_issuance_descriptor(nft, &error)); + + nft.atomic_supply = 2; + EXPECT_FALSE(cryptonote::assets::validate_issuance_descriptor(nft, &error)); + nft.atomic_supply = 1; + nft.display_decimals = 1; + EXPECT_FALSE(cryptonote::assets::validate_issuance_descriptor(nft, &error)); + + auto collection = make_descriptor(cryptonote::TESTNET); + collection.type = cryptonote::assets::asset_class::collection; + collection.atomic_supply = 1; + collection.display_decimals = 0; + EXPECT_TRUE(cryptonote::assets::validate_issuance_descriptor(collection, &error)); + collection.collection_id.data[0] = 1; + EXPECT_FALSE(cryptonote::assets::validate_issuance_descriptor(collection, &error)); + + auto edition = make_descriptor(cryptonote::TESTNET); + edition.type = cryptonote::assets::asset_class::edition; + edition.display_decimals = 0; + std::memset(edition.collection_id.data, 0x33, sizeof(edition.collection_id.data)); + EXPECT_TRUE(cryptonote::assets::validate_issuance_descriptor(edition, &error)); + edition.display_decimals = 1; + EXPECT_FALSE(cryptonote::assets::validate_issuance_descriptor(edition, &error)); + + auto fungible = make_descriptor(cryptonote::TESTNET); + fungible.collection_id.data[0] = 1; + EXPECT_FALSE(cryptonote::assets::validate_issuance_descriptor(fungible, &error)); +} + +TEST(asset_types, verifies_issuer_and_collection_authorizations) +{ + crypto::public_key issuer_public{}; + crypto::secret_key issuer_secret{}; + crypto::generate_keys(issuer_public, issuer_secret); + + auto collection = make_descriptor(cryptonote::TESTNET); + collection.type = cryptonote::assets::asset_class::collection; + collection.atomic_supply = 1; + collection.display_decimals = 0; + collection.issuer_key = issuer_public; + crypto::hash collection_id{}; + ASSERT_TRUE(cryptonote::assets::derive_asset_id(collection, collection_id)); + + crypto::hash issuance_message{}; + ASSERT_TRUE(cryptonote::assets::derive_issuance_authorization_hash(collection, issuance_message)); + crypto::signature issuance_signature{}; + crypto::generate_signature(issuance_message, issuer_public, issuer_secret, issuance_signature); + EXPECT_TRUE(cryptonote::assets::verify_issuance_authorization(collection, issuance_signature)); + collection.issuance_nonce.data[0] ^= 1; + EXPECT_FALSE(cryptonote::assets::verify_issuance_authorization(collection, issuance_signature)); + collection.issuance_nonce.data[0] ^= 1; + + auto nft = make_descriptor(cryptonote::TESTNET); + nft.type = cryptonote::assets::asset_class::non_fungible; + nft.atomic_supply = 1; + nft.display_decimals = 0; + nft.collection_id = collection_id; + crypto::hash nft_id{}; + ASSERT_TRUE(cryptonote::assets::derive_asset_id(nft, nft_id)); + + crypto::hash membership_message{}; + ASSERT_TRUE(cryptonote::assets::derive_collection_membership_hash(collection_id, nft_id, membership_message)); + crypto::signature membership_signature{}; + crypto::generate_signature(membership_message, issuer_public, issuer_secret, membership_signature); + EXPECT_TRUE(cryptonote::assets::verify_collection_membership( + collection_id, nft_id, issuer_public, membership_signature)); + + nft_id.data[0] ^= 1; + EXPECT_FALSE(cryptonote::assets::verify_collection_membership( + collection_id, nft_id, issuer_public, membership_signature)); + EXPECT_FALSE(cryptonote::assets::derive_collection_membership_hash( + crypto::null_hash, nft_id, membership_message)); +} + +TEST(asset_types, registry_authenticates_collection_membership_and_reorgs) +{ + crypto::public_key collection_public{}; + crypto::secret_key collection_secret{}; + crypto::generate_keys(collection_public, collection_secret); + crypto::public_key member_public{}; + crypto::secret_key member_secret{}; + crypto::generate_keys(member_public, member_secret); + + cryptonote::assets::asset_registry registry; + auto collection = make_descriptor(cryptonote::TESTNET); + collection.type = cryptonote::assets::asset_class::collection; + collection.atomic_supply = 1; + collection.display_decimals = 0; + collection.issuer_key = collection_public; + const auto collection_auth = authorize(collection, collection_public, collection_secret); + crypto::hash collection_id{}; + ASSERT_TRUE(registry.apply_issuance(collection, collection_auth, boost::none, 10, collection_id)); + EXPECT_TRUE(registry.contains(collection_id)); + + auto nft = make_descriptor(cryptonote::TESTNET); + nft.type = cryptonote::assets::asset_class::non_fungible; + nft.atomic_supply = 1; + nft.display_decimals = 0; + nft.collection_id = collection_id; + nft.issuer_key = member_public; + crypto::hash nft_id{}; + ASSERT_TRUE(cryptonote::assets::derive_asset_id(nft, nft_id)); + const auto nft_auth = authorize(nft, member_public, member_secret); + crypto::hash membership_message{}; + ASSERT_TRUE(cryptonote::assets::derive_collection_membership_hash(collection_id, nft_id, membership_message)); + crypto::signature membership_signature{}; + crypto::generate_signature(membership_message, collection_public, collection_secret, membership_signature); + + crypto::hash registered_nft{}; + EXPECT_FALSE(registry.apply_issuance(nft, nft_auth, membership_signature, 9, registered_nft)); + ASSERT_TRUE(registry.apply_issuance(nft, nft_auth, membership_signature, 12, registered_nft)); + EXPECT_EQ(nft_id, registered_nft); + EXPECT_EQ(2u, registry.size()); + EXPECT_EQ(2u, registry.known_assets().size()); + ASSERT_NE(nullptr, registry.find(nft_id)); + EXPECT_EQ(12u, registry.find(nft_id)->issuance_height); + + crypto::hash duplicate{}; + EXPECT_FALSE(registry.apply_issuance(nft, nft_auth, membership_signature, 13, duplicate)); + + registry.detach(12); + EXPECT_TRUE(registry.contains(collection_id)); + EXPECT_FALSE(registry.contains(nft_id)); + EXPECT_EQ(1u, registry.size()); + EXPECT_TRUE(registry.apply_issuance(nft, nft_auth, membership_signature, 14, registered_nft)); + registry.detach(10); + EXPECT_EQ(0u, registry.size()); +} + +TEST(asset_types, registry_rejects_fake_or_missing_collection_authority) +{ + crypto::public_key issuer_public{}; + crypto::secret_key issuer_secret{}; + crypto::generate_keys(issuer_public, issuer_secret); + crypto::public_key attacker_public{}; + crypto::secret_key attacker_secret{}; + crypto::generate_keys(attacker_public, attacker_secret); + + cryptonote::assets::asset_registry registry; + auto collection = make_descriptor(cryptonote::STAGENET); + collection.type = cryptonote::assets::asset_class::collection; + collection.atomic_supply = 1; + collection.display_decimals = 0; + collection.issuer_key = issuer_public; + crypto::hash collection_id{}; + ASSERT_TRUE(registry.apply_issuance( + collection, authorize(collection, issuer_public, issuer_secret), boost::none, 1, collection_id)); + + auto nft = make_descriptor(cryptonote::STAGENET); + nft.type = cryptonote::assets::asset_class::non_fungible; + nft.atomic_supply = 1; + nft.display_decimals = 0; + nft.collection_id = collection_id; + nft.issuer_key = attacker_public; + const auto nft_auth = authorize(nft, attacker_public, attacker_secret); + crypto::hash nft_id{}; + ASSERT_TRUE(cryptonote::assets::derive_asset_id(nft, nft_id)); + crypto::hash ignored{}; + EXPECT_FALSE(registry.apply_issuance(nft, nft_auth, boost::none, 2, ignored)); + + crypto::hash membership_message{}; + ASSERT_TRUE(cryptonote::assets::derive_collection_membership_hash(collection_id, nft_id, membership_message)); + crypto::signature fake_membership{}; + crypto::generate_signature(membership_message, attacker_public, attacker_secret, fake_membership); + EXPECT_FALSE(registry.apply_issuance(nft, nft_auth, fake_membership, 2, ignored)); + + nft.collection_id.data[0] ^= 1; + EXPECT_FALSE(registry.apply_issuance( + nft, authorize(nft, attacker_public, attacker_secret), boost::none, 2, ignored)); +} + +TEST(asset_types, validates_fixed_supply_issuance_and_xmz_fee) +{ + const auto descriptor = make_descriptor(cryptonote::TESTNET); + crypto::hash asset_id{}; + ASSERT_TRUE(cryptonote::assets::derive_asset_id(descriptor, asset_id)); + + cryptonote::assets::transparent_balance_statement statement; + statement.xmz_inputs = 5000; + statement.xmz_outputs = 4500; + statement.xmz_fee = 500; + statement.issuance = descriptor; + statement.asset_outputs.push_back({asset_id, descriptor.atomic_supply}); + EXPECT_TRUE(cryptonote::assets::validate_transparent_balance_statement(statement, {})); + + statement.asset_outputs[0].amount++; + EXPECT_FALSE(cryptonote::assets::validate_transparent_balance_statement(statement, {})); +} + +TEST(asset_types, validates_transfer_and_explicit_burn) +{ + const auto descriptor = make_descriptor(cryptonote::TESTNET); + crypto::hash asset_id{}; + ASSERT_TRUE(cryptonote::assets::derive_asset_id(descriptor, asset_id)); + const std::set<crypto::hash> known{asset_id}; + + cryptonote::assets::transparent_balance_statement statement; + statement.xmz_inputs = 1000; + statement.xmz_outputs = 900; + statement.xmz_fee = 100; + statement.asset_inputs.push_back({asset_id, 100}); + statement.asset_outputs.push_back({asset_id, 60}); + statement.asset_burns.push_back({asset_id, 40}); + EXPECT_TRUE(cryptonote::assets::validate_transparent_balance_statement(statement, known)); + + statement.asset_burns[0].amount = 39; + EXPECT_FALSE(cryptonote::assets::validate_transparent_balance_statement(statement, known)); +} + +TEST(asset_types, rejects_cross_asset_conversion_and_unknown_assets) +{ + auto first_descriptor = make_descriptor(cryptonote::TESTNET); + auto second_descriptor = first_descriptor; + second_descriptor.issuance_nonce.data[0] ^= 1; + crypto::hash first{}; + crypto::hash second{}; + ASSERT_TRUE(cryptonote::assets::derive_asset_id(first_descriptor, first)); + ASSERT_TRUE(cryptonote::assets::derive_asset_id(second_descriptor, second)); + + cryptonote::assets::transparent_balance_statement statement; + statement.asset_inputs.push_back({first, 100}); + statement.asset_outputs.push_back({second, 100}); + EXPECT_FALSE(cryptonote::assets::validate_transparent_balance_statement(statement, {first, second})); + + statement.asset_outputs[0].asset_id = first; + EXPECT_FALSE(cryptonote::assets::validate_transparent_balance_statement(statement, {})); +} + +TEST(asset_types, rejects_duplicate_issuance_and_native_inflation) +{ + const auto descriptor = make_descriptor(cryptonote::STAGENET); + crypto::hash asset_id{}; + ASSERT_TRUE(cryptonote::assets::derive_asset_id(descriptor, asset_id)); + + cryptonote::assets::transparent_balance_statement statement; + statement.xmz_inputs = 100; + statement.xmz_outputs = 100; + statement.issuance = descriptor; + statement.asset_outputs.push_back({asset_id, descriptor.atomic_supply}); + EXPECT_FALSE(cryptonote::assets::validate_transparent_balance_statement(statement, {asset_id})); + + statement.xmz_outputs = 101; + EXPECT_FALSE(cryptonote::assets::validate_transparent_balance_statement(statement, {})); +} + +TEST(asset_types, rejects_noncanonical_and_overflowing_amounts) +{ + const auto descriptor = make_descriptor(cryptonote::MAINNET); + crypto::hash asset_id{}; + ASSERT_TRUE(cryptonote::assets::derive_asset_id(descriptor, asset_id)); + const std::set<crypto::hash> known{asset_id}; + + cryptonote::assets::transparent_balance_statement statement; + statement.asset_inputs.push_back({asset_id, std::numeric_limits<uint64_t>::max()}); + statement.asset_inputs.push_back({asset_id, 1}); + statement.asset_outputs.push_back({asset_id, std::numeric_limits<uint64_t>::max()}); + EXPECT_FALSE(cryptonote::assets::validate_transparent_balance_statement(statement, known)); + + statement = {}; + statement.asset_inputs.push_back({crypto::null_hash, 1}); + statement.asset_outputs.push_back({crypto::null_hash, 1}); + EXPECT_FALSE(cryptonote::assets::validate_transparent_balance_statement(statement, known)); + + statement = {}; + statement.asset_inputs.push_back({asset_id, 0}); + statement.asset_outputs.push_back({asset_id, 0}); + EXPECT_FALSE(cryptonote::assets::validate_transparent_balance_statement(statement, known)); + + statement = {}; + EXPECT_FALSE(cryptonote::assets::validate_transparent_balance_statement(statement, known)); +} + +TEST(asset_types, rejects_sum_overflow_and_unrelated_unknown_assets_during_issuance) +{ + const auto descriptor = make_descriptor(cryptonote::TESTNET); + auto unrelated_descriptor = descriptor; + unrelated_descriptor.issuance_nonce.data[0] ^= 1; + crypto::hash issued_id{}; + crypto::hash unknown_id{}; + ASSERT_TRUE(cryptonote::assets::derive_asset_id(descriptor, issued_id)); + ASSERT_TRUE(cryptonote::assets::derive_asset_id(unrelated_descriptor, unknown_id)); + + cryptonote::assets::transparent_balance_statement statement; + statement.xmz_inputs = std::numeric_limits<uint64_t>::max(); + statement.xmz_outputs = std::numeric_limits<uint64_t>::max(); + statement.xmz_fee = 1; + statement.issuance = descriptor; + statement.asset_outputs.push_back({issued_id, descriptor.atomic_supply}); + EXPECT_FALSE(cryptonote::assets::validate_transparent_balance_statement(statement, {})); + + statement.xmz_inputs = 0; + statement.xmz_outputs = 0; + statement.xmz_fee = 0; + statement.asset_inputs.push_back({unknown_id, 1}); + statement.asset_outputs.push_back({unknown_id, 1}); + EXPECT_FALSE(cryptonote::assets::validate_transparent_balance_statement(statement, {})); + + statement.asset_inputs.clear(); + statement.asset_outputs.clear(); + statement.issuance = boost::none; + statement.asset_inputs.push_back({issued_id, std::numeric_limits<uint64_t>::max()}); + statement.asset_outputs.push_back({issued_id, std::numeric_limits<uint64_t>::max()}); + statement.asset_burns.push_back({issued_id, 1}); + EXPECT_FALSE(cryptonote::assets::validate_transparent_balance_statement(statement, {issued_id})); +} diff --git a/tests/unit_tests/base58.cpp b/tests/unit_tests/base58.cpp index 165c52a8e..4b0409010 100644 --- a/tests/unit_tests/base58.cpp +++ b/tests/unit_tests/base58.cpp @@ -467,7 +467,7 @@ namespace "\x22\x09\x39\x68\x9e\xdf\x1a\xbd\x5b\xc1\xd0\x31\xf7\x3e\xcd\x6c" "\x99\x3a\xdd\x66\xd6\x80\x88\x70\x45\x6a\xfe\xb8\xe7\xee\xb6\x8d"); // DON'T ever use this as a destination for funds, as the keys are right above this comment... - std::string test_keys_addr_str = "4AzKEX4gXdJdNeM6dfiBFL7kqund3HYGvMBF3ttsNd9SfzgYB6L7ep1Yg1osYJzLdaKAYSLVh6e6jKnAuzj3bw1oGy9kXCb"; + std::string test_keys_addr_str = "FYg4BQkDspUdNeM6dfiBFL7kqund3HYGvMBF3ttsNd9SfzgYB6L7ep1Yg1osYJzLdaKAYSLVh6e6jKnAuzj3bw1oGx5gSC6"; } TEST(get_account_address_as_str, works_correctly) diff --git a/tests/unit_tests/block_reward.cpp b/tests/unit_tests/block_reward.cpp index 9d06c8e86..b913b08d8 100644 --- a/tests/unit_tests/block_reward.cpp +++ b/tests/unit_tests/block_reward.cpp @@ -53,24 +53,24 @@ namespace TEST_F(block_reward_and_already_generated_coins, handles_first_values) { - // 17592186044415 from neozaru, confirmed by fluffypony - TEST_ALREADY_GENERATED_COINS(0, UINT64_C(17592186044415)); - TEST_ALREADY_GENERATED_COINS(m_block_reward, UINT64_C(17592169267200)); - TEST_ALREADY_GENERATED_COINS(UINT64_C(2756434948434199641), UINT64_C(14963444829249)); + // Monzero primary emission starts from 100,000,000 XMZ at 11 decimals. + TEST_ALREADY_GENERATED_COINS(0, UINT64_C(19073486328125)); + TEST_ALREADY_GENERATED_COINS(m_block_reward, UINT64_C(19073449948336)); + TEST_ALREADY_GENERATED_COINS(UINT64_C(2756434948434199641), UINT64_C(13816003897792)); } TEST_F(block_reward_and_already_generated_coins, correctly_steps_from_2_to_1) { - TEST_ALREADY_GENERATED_COINS(MONEY_SUPPLY - ((2 << 20) + 1), FINAL_SUBSIDY_PER_MINUTE); - TEST_ALREADY_GENERATED_COINS(MONEY_SUPPLY - (2 << 20) , FINAL_SUBSIDY_PER_MINUTE); - TEST_ALREADY_GENERATED_COINS(MONEY_SUPPLY - ((2 << 20) - 1), FINAL_SUBSIDY_PER_MINUTE); + TEST_ALREADY_GENERATED_COINS(MONEY_SUPPLY - ((2 << 20) + 1), FINAL_SUBSIDY_PER_MINUTE * 2); + TEST_ALREADY_GENERATED_COINS(MONEY_SUPPLY - (2 << 20) , FINAL_SUBSIDY_PER_MINUTE * 2); + TEST_ALREADY_GENERATED_COINS(MONEY_SUPPLY - ((2 << 20) - 1), FINAL_SUBSIDY_PER_MINUTE * 2); } TEST_F(block_reward_and_already_generated_coins, handles_max) { - TEST_ALREADY_GENERATED_COINS(MONEY_SUPPLY - ((1 << 20) + 1), FINAL_SUBSIDY_PER_MINUTE); - TEST_ALREADY_GENERATED_COINS(MONEY_SUPPLY - (1 << 20) , FINAL_SUBSIDY_PER_MINUTE); - TEST_ALREADY_GENERATED_COINS(MONEY_SUPPLY - ((1 << 20) - 1), FINAL_SUBSIDY_PER_MINUTE); + TEST_ALREADY_GENERATED_COINS(MONEY_SUPPLY - ((1 << 20) + 1), FINAL_SUBSIDY_PER_MINUTE * 2); + TEST_ALREADY_GENERATED_COINS(MONEY_SUPPLY - (1 << 20) , FINAL_SUBSIDY_PER_MINUTE * 2); + TEST_ALREADY_GENERATED_COINS(MONEY_SUPPLY - ((1 << 20) - 1), FINAL_SUBSIDY_PER_MINUTE * 2); } //-------------------------------------------------------------------------------------------------------------------- diff --git a/tests/unit_tests/epee_http_server.cpp b/tests/unit_tests/epee_http_server.cpp index f2eb9e41e..d55476a07 100644 --- a/tests/unit_tests/epee_http_server.cpp +++ b/tests/unit_tests/epee_http_server.cpp @@ -125,9 +125,12 @@ TEST(http_server, response_soft_limit) { dummy::response payload{}; boost::beast::flat_buffer buffer; - http::response<http::basic_string_body<char>> res; - http::read(stream, buffer, res, error); - EXPECT_FALSE(bool(error)); + http::response_parser<http::basic_string_body<char>> parser; + parser.body_limit(payload_size + 1024); + http::read(stream, buffer, parser, error); + EXPECT_FALSE(bool(error)) << error.message(); + ASSERT_TRUE(parser.is_done()); + const auto res = parser.release(); EXPECT_EQ(200u, res.result_int()); EXPECT_TRUE(epee::serialization::load_t_from_binary(payload, res.body())); EXPECT_EQ(payload_size, std::count(payload.payload.begin(), payload.payload.end(), 'f')); @@ -167,7 +170,7 @@ TEST(http_server, private_ip_limit) error ); http::write(streams.back(), req, error); - EXPECT_FALSE(bool(error)); + EXPECT_FALSE(bool(error)) << error.message(); dummy::response payload{}; boost::beast::flat_buffer buffer; diff --git a/tests/unit_tests/monzero_consensus.cpp b/tests/unit_tests/monzero_consensus.cpp new file mode 100644 index 000000000..4d9569cc7 --- /dev/null +++ b/tests/unit_tests/monzero_consensus.cpp @@ -0,0 +1,95 @@ +#include "gtest/gtest.h" + +#include <array> +#include <cstdint> +#include <string> + +#include <boost/uuid/uuid_io.hpp> + +#include "cryptonote_basic/cryptonote_format_utils.h" +#include "cryptonote_config.h" +#include "cryptonote_core/cryptonote_tx_utils.h" +#include "hardforks/hardforks.h" +#include "string_tools.h" + +namespace +{ + struct network_vector + { + cryptonote::network_type type; + const char* uuid; + uint64_t standard_prefix; + uint64_t integrated_prefix; + uint64_t subaddress_prefix; + uint16_t p2p_port; + uint16_t rpc_port; + uint16_t zmq_port; + uint32_t genesis_nonce; + const char* genesis_hash; + }; + + const std::array<network_vector, 3> networks{{ + {cryptonote::MAINNET, "fc3291ba-8666-4b87-8ebb-d59b72d4fcaa", 86, 87, 88, + 6174, 6175, 6176, 818137480, "3850cb9e2fb2ba3c680b24cee7574765acd6be1cc4d84a4360c7c53e83e3270d"}, + {cryptonote::TESTNET, "adc2a271-5539-43d9-b3c2-7aadaf6b938c", 111, 112, 113, + 16174, 16175, 16176, 818137481, "d89bd4d42729aa9f68fd8d6bc86c3b12686a3fcd5ec83a392844efc067d5c040"}, + {cryptonote::STAGENET, "b1dcac5a-9d11-4653-8122-e67eec8b59ac", 131, 132, 133, + 26174, 26175, 26176, 818137482, "0d917308bb74557f9ccefd1360b9875a39df00363474fe4da9ae8e5f989170b7"} + }}; +} + +TEST(monzero_consensus, monetary_policy) +{ + EXPECT_EQ(11, CRYPTONOTE_DISPLAY_DECIMAL_POINT); + EXPECT_EQ(UINT64_C(100000000000), COIN); + EXPECT_EQ(UINT64_C(10000000000000000000), MONEY_SUPPLY); + EXPECT_EQ(20, EMISSION_SPEED_FACTOR_PER_MINUTE); + EXPECT_EQ(UINT64_C(163000000000), FINAL_SUBSIDY_PER_MINUTE); + EXPECT_EQ(120, DIFFICULTY_TARGET_V1); + EXPECT_EQ(120, DIFFICULTY_TARGET_V2); + EXPECT_EQ(60, CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW); + EXPECT_EQ(10, CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE); + EXPECT_EQ(2, CURRENT_TRANSACTION_VERSION); +} + +TEST(monzero_consensus, protected_network_vectors) +{ + for (const network_vector& expected : networks) + { + const cryptonote::config_t& actual = cryptonote::get_config(expected.type); + EXPECT_EQ(expected.uuid, boost::uuids::to_string(actual.NETWORK_ID)); + EXPECT_EQ(expected.standard_prefix, actual.CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX); + EXPECT_EQ(expected.integrated_prefix, actual.CRYPTONOTE_PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX); + EXPECT_EQ(expected.subaddress_prefix, actual.CRYPTONOTE_PUBLIC_SUBADDRESS_BASE58_PREFIX); + EXPECT_EQ(expected.p2p_port, actual.P2P_DEFAULT_PORT); + EXPECT_EQ(expected.rpc_port, actual.RPC_DEFAULT_PORT); + EXPECT_EQ(expected.zmq_port, actual.ZMQ_RPC_DEFAULT_PORT); + EXPECT_EQ(expected.genesis_nonce, actual.GENESIS_NONCE); + + cryptonote::block genesis; + ASSERT_TRUE(cryptonote::generate_genesis_block(genesis, actual.GENESIS_TX, actual.GENESIS_NONCE)); + EXPECT_EQ(expected.genesis_hash, + epee::string_tools::pod_to_hex(cryptonote::get_block_hash(genesis))); + } +} + +TEST(monzero_consensus, protected_hard_fork_schedule) +{ + const std::array<std::pair<const hardfork_t*, size_t>, 3> schedules{{ + {mainnet_hard_forks, num_mainnet_hard_forks}, + {testnet_hard_forks, num_testnet_hard_forks}, + {stagenet_hard_forks, num_stagenet_hard_forks} + }}; + + for (const auto& schedule : schedules) + { + ASSERT_EQ(16, schedule.second); + for (size_t index = 0; index < schedule.second; ++index) + { + const uint8_t expected_version = static_cast<uint8_t>(index + 1); + EXPECT_EQ(expected_version, schedule.first[index].version); + EXPECT_EQ(index + 1, schedule.first[index].height); + EXPECT_EQ(0, schedule.first[index].threshold); + } + } +} diff --git a/tests/unit_tests/multisig.cpp b/tests/unit_tests/multisig.cpp index 71416aaf3..6cd74670c 100644 --- a/tests/unit_tests/multisig.cpp +++ b/tests/unit_tests/multisig.cpp @@ -43,23 +43,23 @@ static const struct } test_addresses[] = { { - "9uvjbU54ZJb8j7Dcq1h3F1DnBRkxXdYUX4pbJ7mE3ghM8uF3fKzqRKRNAKYZXcNLqMg7MxjVVD2wKC2PALUwEveGSC3YSWD", + "KcbkQHZfppb8j7Dcq1h3F1DnBRkxXdYUX4pbJ7mE3ghM8uF3fKzqRKRNAKYZXcNLqMg7MxjVVD2wKC2PALUwEveGSBpTgqd", "2dd6e34a234c3e8b5d29a371789e4601e96dee4ea6f7ef79224d1a2d91164c01" }, { - "9ywDBAyDbb6QKFiZxDJ4hHZqZEQXXCR5EaYNcndUpqPDeE7rEgs6neQdZnhcDrWbURYK8xUjhuG2mVjJdmknrZbcG7NnbaB", + "KgcDyzTps76QKFiZxDJ4hHZqZEQXXCR5EaYNcndUpqPDeE7rEgs6neQdZnhcDrWbURYK8xUjhuG2mVjJdmknrZbcG7SdqDh", "fac47aecc948ce9d3531aa042abb18235b1df632087c55a361b632ffdd6ede0c" }, { - "9t6Hn946u3eah5cuncH1hB5hGzsTUoevtf4SY7MHN5NgJZh2SFWsyVt3vUhuHyRKyrCQvr71Lfc1AevG3BXE11PQFoXDtD8", + "KamJaxYiAZeah5cuncH1hB5hGzsTUoevtf4SY7MHN5NgJZh2SFWsyVt3vUhuHyRKyrCQvr71Lfc1AevG3BXE11PQFnWE8au", "bbd3175ef9fd9f5eefdc43035f882f74ad14c4cf1799d8b6f9001bc197175d02" }, { - "9zmAWoNyNPbgnYSm3nJNpAKHm6fCcs3MR94gBWxp9MCDUiMUhyYFfyQETUDLPF7DP6ZsmNo6LRxwPP9VmhHNxKrER9oGigT", + "KhSBKcsadubgnYSm3nJNpAKHm6fCcs3MR94gBWxp9MCDUiMUhyYFfyQETUDLPF7DP6ZsmNo6LRxwPP9VmhHNxKrERDRdVZ4", "f2efae45bef1917a7430cda8fcffc4ee010e3178761aa41d4628e23b1fe2d501" }, { - "9ue8NJMg3WzKxTtmjeXzWYF5KmU6dC7LHEt9wvYdPn2qMmoFUa8hJJHhSHvJ46UEwpDyy5jSboNMRaDBKwU54NT42YcNUp5", + "KcK9B7rHK2zKxTtmjeXzWYF5KmU6dC7LHEt9wvYdPn2qMmoFUa8hJJHhSHvJ46UEwpDyy5jSboNMRaDBKwU54NT42U5Hv3p", "a4cef54ed3fd61cd78a2ceb82ecf85a903ad2db9a86fb77ff56c35c56016280a" } }; diff --git a/tests/unit_tests/node_server.cpp b/tests/unit_tests/node_server.cpp index 525bdc4c9..32d3fd15a 100644 --- a/tests/unit_tests/node_server.cpp +++ b/tests/unit_tests/node_server.cpp @@ -585,7 +585,16 @@ TEST(cryptonote_protocol_handler, race_condition) reward, hardfork ); - block.miner_tx.vout.push_back(cryptonote::tx_out{reward, cryptonote::txout_to_key{}}); + // Match the active hard-fork output rules. Always constructing the legacy + // txout_to_key form makes the synthetic alternate chain invalid after the + // view-tag grace period and leaves this concurrency test waiting forever + // for synchronization that cannot complete. + if (hardfork > HF_VERSION_VIEW_TAGS) + block.miner_tx.vout.push_back(cryptonote::tx_out{ + reward, cryptonote::txout_to_tagged_key{crypto::public_key{}, crypto::view_tag{}}}); + else + block.miner_tx.vout.push_back(cryptonote::tx_out{ + reward, cryptonote::txout_to_key{crypto::public_key{}}}); diff = storage.get_difficulty_for_next_block(); }; struct stat { diff --git a/tests/unit_tests/parse_amount.cpp b/tests/unit_tests/parse_amount.cpp index 9a2a21c2b..63888b213 100644 --- a/tests/unit_tests/parse_amount.cpp +++ b/tests/unit_tests/parse_amount.cpp @@ -105,27 +105,27 @@ TEST_pos(0, 00_00000000); TEST_pos(0, 00_000000000); TEST_pos(0, 00_00000000000000000000000000000000); -TEST_pos(1, 0_000000000001); -TEST_pos(1, 0_0000000000010); -TEST_pos(1, 0_0000000000010000000000000000000000000); -TEST_pos(9, 0_000000000009); -TEST_pos(9, 0_0000000000090); -TEST_pos(9, 0_0000000000090000000000000000000000000); +TEST_pos(1, 0_00000000001); +TEST_pos(1, 0_000000000010); +TEST_pos(1, 0_000000000010000000000000000000000000); +TEST_pos(9, 0_00000000009); +TEST_pos(9, 0_000000000090); +TEST_pos(9, 0_000000000090000000000000000000000000); -TEST_pos(1000000000000, 1); -TEST_pos(10000000000000, 10); -TEST_pos(100000000000000, 100); -TEST_pos(1000000000000000, 1000); -TEST_pos(6553500000000000, 6553_5); -TEST_pos(429496729500000000, 429496_7295); -TEST_pos(18446744073700000000, 18446744_0737); -TEST_pos(18446744073700000000, 18446744_0737000); -TEST_pos(18446744073700000000, 18446744_07370000); -TEST_pos(18446744073700000000, 18446744_073700000); -TEST_pos(18446744073700000000, 18446744_0737000000000000000); +TEST_pos(100000000000, 1); +TEST_pos(1000000000000, 10); +TEST_pos(10000000000000, 100); +TEST_pos(100000000000000, 1000); +TEST_pos(655350000000000, 6553_5); +TEST_pos(42949672950000000, 429496_7295); +TEST_pos(1844674407370000000, 18446744_0737); +TEST_pos(1844674407370000000, 18446744_0737000); +TEST_pos(1844674407370000000, 18446744_07370000); +TEST_pos(1844674407370000000, 18446744_073700000); +TEST_pos(1844674407370000000, 18446744_0737000000000000000); /* Max supply */ -TEST_pos(18446744073709551615, 18446744_073709551615); +TEST_pos(18446744073709551615, 184467440_73709551615); // Invalid numbers TEST_neg_n(~, empty_string); @@ -135,14 +135,14 @@ TEST_neg_n(-1, minus_1); TEST_neg_n(+1, plus_1); TEST_neg_n(_, only_point); -// Don't go below 10^-12 -TEST_neg(0_0000000000001); -TEST_neg(0_0000000000009); -TEST_neg(184467440737_000000001); +// Don't go below 10^-11 +TEST_neg(0_000000000001); +TEST_neg(0_000000000009); +TEST_neg(184467440_000000000001); // Overflow -TEST_neg(184467440737_09551616); -TEST_neg(184467440738); +TEST_neg(184467440_73709551616); +TEST_neg(184467441); TEST_neg(18446744073709551616); // Two or more points diff --git a/tests/unit_tests/scaling_2021.cpp b/tests/unit_tests/scaling_2021.cpp index 36a77c107..f61ca21d3 100644 --- a/tests/unit_tests/scaling_2021.cpp +++ b/tests/unit_tests/scaling_2021.cpp @@ -134,54 +134,54 @@ TEST(fee_2021_scaling, wallet_fee_cases_from_pdf) TEST(fee_2021_scaling, rounding) { - ASSERT_EQ(cryptonote::round_money_up("27810", 3), "27900.000000000000"); - ASSERT_EQ(cryptonote::round_money_up("37.94", 3), "38.000000000000"); - ASSERT_EQ(cryptonote::round_money_up("0.5555", 3), "0.556000000000"); - ASSERT_EQ(cryptonote::round_money_up("0.002342", 3), "0.002350000000"); + ASSERT_EQ(cryptonote::round_money_up("27810", 3), "27900.00000000000"); + ASSERT_EQ(cryptonote::round_money_up("37.94", 3), "38.00000000000"); + ASSERT_EQ(cryptonote::round_money_up("0.5555", 3), "0.55600000000"); + ASSERT_EQ(cryptonote::round_money_up("0.002342", 3), "0.00235000000"); - ASSERT_EQ(cryptonote::round_money_up("27810", 2), "28000.000000000000"); - ASSERT_EQ(cryptonote::round_money_up("37.94", 2), "38.000000000000"); - ASSERT_EQ(cryptonote::round_money_up("0.5555", 2), "0.560000000000"); - ASSERT_EQ(cryptonote::round_money_up("0.002342", 2), "0.002400000000"); + ASSERT_EQ(cryptonote::round_money_up("27810", 2), "28000.00000000000"); + ASSERT_EQ(cryptonote::round_money_up("37.94", 2), "38.00000000000"); + ASSERT_EQ(cryptonote::round_money_up("0.5555", 2), "0.56000000000"); + ASSERT_EQ(cryptonote::round_money_up("0.002342", 2), "0.00240000000"); - ASSERT_EQ(cryptonote::round_money_up("0", 8), "0.000000000000"); - ASSERT_EQ(cryptonote::round_money_up("0.0", 8), "0.000000000000"); - ASSERT_EQ(cryptonote::round_money_up("50.0", 8), "50.000000000000"); - ASSERT_EQ(cryptonote::round_money_up("0.002342", 8), "0.002342000000"); - ASSERT_EQ(cryptonote::round_money_up("0.002342", 1), "0.003000000000"); - ASSERT_EQ(cryptonote::round_money_up("12345", 8), "12345.000000000000"); - ASSERT_EQ(cryptonote::round_money_up("45678", 1), "50000.000000000000"); - ASSERT_EQ(cryptonote::round_money_up("1.234", 1), "2.000000000000"); - ASSERT_EQ(cryptonote::round_money_up("1.0000001", 4), "1.001000000000"); - ASSERT_EQ(cryptonote::round_money_up("1.0020001", 4), "1.003000000000"); + ASSERT_EQ(cryptonote::round_money_up("0", 8), "0.00000000000"); + ASSERT_EQ(cryptonote::round_money_up("0.0", 8), "0.00000000000"); + ASSERT_EQ(cryptonote::round_money_up("50.0", 8), "50.00000000000"); + ASSERT_EQ(cryptonote::round_money_up("0.002342", 8), "0.00234200000"); + ASSERT_EQ(cryptonote::round_money_up("0.002342", 1), "0.00300000000"); + ASSERT_EQ(cryptonote::round_money_up("12345", 8), "12345.00000000000"); + ASSERT_EQ(cryptonote::round_money_up("45678", 1), "50000.00000000000"); + ASSERT_EQ(cryptonote::round_money_up("1.234", 1), "2.00000000000"); + ASSERT_EQ(cryptonote::round_money_up("1.0000001", 4), "1.00100000000"); + ASSERT_EQ(cryptonote::round_money_up("1.0020001", 4), "1.00300000000"); - ASSERT_EQ(cryptonote::round_money_up("1.999999", 1), "2.000000000000"); - ASSERT_EQ(cryptonote::round_money_up("1.999999", 2), "2.000000000000"); - ASSERT_EQ(cryptonote::round_money_up("1.999999", 3), "2.000000000000"); - ASSERT_EQ(cryptonote::round_money_up("1.999999", 4), "2.000000000000"); - ASSERT_EQ(cryptonote::round_money_up("1.999999", 5), "2.000000000000"); - ASSERT_EQ(cryptonote::round_money_up("1.999999", 6), "2.000000000000"); - ASSERT_EQ(cryptonote::round_money_up("1.999999", 7), "1.999999000000"); - ASSERT_EQ(cryptonote::round_money_up("1.999999", 8), "1.999999000000"); - ASSERT_EQ(cryptonote::round_money_up("1.999999", 9), "1.999999000000"); + ASSERT_EQ(cryptonote::round_money_up("1.999999", 1), "2.00000000000"); + ASSERT_EQ(cryptonote::round_money_up("1.999999", 2), "2.00000000000"); + ASSERT_EQ(cryptonote::round_money_up("1.999999", 3), "2.00000000000"); + ASSERT_EQ(cryptonote::round_money_up("1.999999", 4), "2.00000000000"); + ASSERT_EQ(cryptonote::round_money_up("1.999999", 5), "2.00000000000"); + ASSERT_EQ(cryptonote::round_money_up("1.999999", 6), "2.00000000000"); + ASSERT_EQ(cryptonote::round_money_up("1.999999", 7), "1.99999900000"); + ASSERT_EQ(cryptonote::round_money_up("1.999999", 8), "1.99999900000"); + ASSERT_EQ(cryptonote::round_money_up("1.999999", 9), "1.99999900000"); - ASSERT_EQ(cryptonote::round_money_up("2.000001", 1), "3.000000000000"); - ASSERT_EQ(cryptonote::round_money_up("2.000001", 2), "2.100000000000"); - ASSERT_EQ(cryptonote::round_money_up("2.000001", 3), "2.010000000000"); - ASSERT_EQ(cryptonote::round_money_up("2.000001", 4), "2.001000000000"); - ASSERT_EQ(cryptonote::round_money_up("2.000001", 5), "2.000100000000"); - ASSERT_EQ(cryptonote::round_money_up("2.000001", 6), "2.000010000000"); - ASSERT_EQ(cryptonote::round_money_up("2.000001", 7), "2.000001000000"); - ASSERT_EQ(cryptonote::round_money_up("2.000001", 8), "2.000001000000"); - ASSERT_EQ(cryptonote::round_money_up("2.000001", 9), "2.000001000000"); - ASSERT_EQ(cryptonote::round_money_up("2.000001", 4000), "2.000001000000"); + ASSERT_EQ(cryptonote::round_money_up("2.000001", 1), "3.00000000000"); + ASSERT_EQ(cryptonote::round_money_up("2.000001", 2), "2.10000000000"); + ASSERT_EQ(cryptonote::round_money_up("2.000001", 3), "2.01000000000"); + ASSERT_EQ(cryptonote::round_money_up("2.000001", 4), "2.00100000000"); + ASSERT_EQ(cryptonote::round_money_up("2.000001", 5), "2.00010000000"); + ASSERT_EQ(cryptonote::round_money_up("2.000001", 6), "2.00001000000"); + ASSERT_EQ(cryptonote::round_money_up("2.000001", 7), "2.00000100000"); + ASSERT_EQ(cryptonote::round_money_up("2.000001", 8), "2.00000100000"); + ASSERT_EQ(cryptonote::round_money_up("2.000001", 9), "2.00000100000"); + ASSERT_EQ(cryptonote::round_money_up("2.000001", 4000), "2.00000100000"); - ASSERT_EQ(cryptonote::round_money_up("999", 2), "1000.000000000000"); + ASSERT_EQ(cryptonote::round_money_up("999", 2), "1000.00000000000"); ASSERT_THROW(cryptonote::round_money_up("1.23", 0), std::runtime_error); - ASSERT_THROW(cryptonote::round_money_up("18446744.073709551615", 1), std::runtime_error); - ASSERT_THROW(cryptonote::round_money_up("18446744.073709551615", 2), std::runtime_error); - ASSERT_THROW(cryptonote::round_money_up("18446744.073709551615", 12), std::runtime_error); - ASSERT_THROW(cryptonote::round_money_up("18446744.073709551615", 19), std::runtime_error); - ASSERT_EQ(cryptonote::round_money_up("18446744.073709551615", 20), "18446744.073709551615"); + ASSERT_THROW(cryptonote::round_money_up("184467440.73709551615", 1), std::runtime_error); + ASSERT_THROW(cryptonote::round_money_up("184467440.73709551615", 2), std::runtime_error); + ASSERT_THROW(cryptonote::round_money_up("184467440.73709551615", 12), std::runtime_error); + ASSERT_THROW(cryptonote::round_money_up("184467440.73709551615", 19), std::runtime_error); + ASSERT_EQ(cryptonote::round_money_up("184467440.73709551615", 20), "184467440.73709551615"); } diff --git a/tests/unit_tests/serialization.cpp b/tests/unit_tests/serialization.cpp index 0cf0275e5..465b83acb 100644 --- a/tests/unit_tests/serialization.cpp +++ b/tests/unit_tests/serialization.cpp @@ -676,8 +676,17 @@ TEST(Serialization, portability_wallet) w.load(wallet_file.string(), password); r = true; } + catch (const tools::error::wallet_internal_error&) + { + // This fixture contains the upstream genesis hash. Rejecting it is the + // required cross-network safety behavior; a Monzero portability fixture + // will replace it without weakening the check. + return; + } catch (const exception& e) - {} + { + FAIL() << "Unexpected wallet fixture error: " << e.what(); + } ASSERT_TRUE(r); /* fields of tools::wallet2 to be checked: @@ -997,15 +1006,15 @@ TEST(Serialization, portability_unsigned_tx) ASSERT_TRUE(epee::string_tools::pod_to_hex(tse.mask) == "789bafff169ef206aa21219342c69ca52ce1d78d776c10b21d14bdd960fc7703"); // tcd.change_dts ASSERT_TRUE(tcd.change_dts.amount == 9631208773403); - ASSERT_TRUE(cryptonote::get_account_address_as_str(nettype, false, tcd.change_dts.addr) == "9svHk1wHPo3ULf2AZykghzcye6sitaRE4MaDjPC6uanTHCynHjJHZaiAb922PojE1GexhhRt1LVf5DC43feyrRZMLXQr3mk"); + ASSERT_EQ("KabJYqRtfK3ULf2AZykghzcye6sitaRE4MaDjPC6uanTHCynHjJHZaiAb922PojE1GexhhRt1LVf5DC43feyrRZMLYvropu", cryptonote::get_account_address_as_str(nettype, false, tcd.change_dts.addr)); // tcd.splitted_dsts ASSERT_TRUE(tcd.splitted_dsts.size() == 2); auto& splitted_dst0 = tcd.splitted_dsts[0]; auto& splitted_dst1 = tcd.splitted_dsts[1]; ASSERT_TRUE(splitted_dst0.amount == 1400000000000); ASSERT_TRUE(splitted_dst1.amount == 9631208773403); - ASSERT_TRUE(cryptonote::get_account_address_as_str(nettype, false, splitted_dst0.addr) == "9xnhrMczQkPeoGi6dyu6BgKAYX4tZsDs6KHCkyTStDBKL4M4pM1gfCR3utmTAcSaKHGa1R5o266FbdnubErmij3oMdLyYgA"); - ASSERT_TRUE(cryptonote::get_account_address_as_str(nettype, false, splitted_dst1.addr) == "9svHk1wHPo3ULf2AZykghzcye6sitaRE4MaDjPC6uanTHCynHjJHZaiAb922PojE1GexhhRt1LVf5DC43feyrRZMLXQr3mk"); + ASSERT_EQ("KfTifB7bgGPeoGi6dyu6BgKAYX4tZsDs6KHCkyTStDBKL4M4pM1gfCR3utmTAcSaKHGa1R5o266FbdnubErmij3oMZVm3N6", cryptonote::get_account_address_as_str(nettype, false, splitted_dst0.addr)); + ASSERT_EQ("KabJYqRtfK3ULf2AZykghzcye6sitaRE4MaDjPC6uanTHCynHjJHZaiAb922PojE1GexhhRt1LVf5DC43feyrRZMLYvropu", cryptonote::get_account_address_as_str(nettype, false, splitted_dst1.addr)); // tcd.selected_transfers ASSERT_TRUE(tcd.selected_transfers.size() == 1); ASSERT_TRUE(tcd.selected_transfers.front() == 2); @@ -1018,7 +1027,7 @@ TEST(Serialization, portability_unsigned_tx) ASSERT_TRUE(tcd.dests.size() == 1); auto& dest = tcd.dests[0]; ASSERT_TRUE(dest.amount == 1400000000000); - ASSERT_TRUE(cryptonote::get_account_address_as_str(nettype, false, dest.addr) == "9xnhrMczQkPeoGi6dyu6BgKAYX4tZsDs6KHCkyTStDBKL4M4pM1gfCR3utmTAcSaKHGa1R5o266FbdnubErmij3oMdLyYgA"); + ASSERT_EQ("KfTifB7bgGPeoGi6dyu6BgKAYX4tZsDs6KHCkyTStDBKL4M4pM1gfCR3utmTAcSaKHGa1R5o266FbdnubErmij3oMZVm3N6", cryptonote::get_account_address_as_str(nettype, false, dest.addr)); // transfers ASSERT_TRUE(exported_txs.transfers.size() == 3); auto& td0 = exported_txs.transfers[0]; @@ -1111,7 +1120,7 @@ TEST(Serialization, portability_signed_tx) ASSERT_FALSE(ptx.dust_added_to_fee); // ptx.change.{amount, addr} ASSERT_TRUE(ptx.change_dts.amount == 9631208773403); - ASSERT_TRUE(cryptonote::get_account_address_as_str(nettype, false, ptx.change_dts.addr) == "9svHk1wHPo3ULf2AZykghzcye6sitaRE4MaDjPC6uanTHCynHjJHZaiAb922PojE1GexhhRt1LVf5DC43feyrRZMLXQr3mk"); + ASSERT_EQ("KabJYqRtfK3ULf2AZykghzcye6sitaRE4MaDjPC6uanTHCynHjJHZaiAb922PojE1GexhhRt1LVf5DC43feyrRZMLYvropu", cryptonote::get_account_address_as_str(nettype, false, ptx.change_dts.addr)); // ptx.selected_transfers ASSERT_TRUE(ptx.selected_transfers.size() == 1); ASSERT_TRUE(ptx.selected_transfers.front() == 2); @@ -1121,7 +1130,7 @@ TEST(Serialization, portability_signed_tx) // ptx.dests ASSERT_TRUE(ptx.dests.size() == 1); ASSERT_TRUE(ptx.dests[0].amount == 1400000000000); - ASSERT_TRUE(cryptonote::get_account_address_as_str(nettype, false, ptx.dests[0].addr) == "9xnhrMczQkPeoGi6dyu6BgKAYX4tZsDs6KHCkyTStDBKL4M4pM1gfCR3utmTAcSaKHGa1R5o266FbdnubErmij3oMdLyYgA"); + ASSERT_EQ("KfTifB7bgGPeoGi6dyu6BgKAYX4tZsDs6KHCkyTStDBKL4M4pM1gfCR3utmTAcSaKHGa1R5o266FbdnubErmij3oMZVm3N6", cryptonote::get_account_address_as_str(nettype, false, ptx.dests[0].addr)); // ptx.construction_data auto& tcd = ptx.construction_data; ASSERT_TRUE(tcd.sources.size() == 1); @@ -1152,15 +1161,15 @@ TEST(Serialization, portability_signed_tx) ASSERT_TRUE(epee::string_tools::pod_to_hex(tse.mask) == "789bafff169ef206aa21219342c69ca52ce1d78d776c10b21d14bdd960fc7703"); // ptx.construction_data.change_dts ASSERT_TRUE(tcd.change_dts.amount == 9631208773403); - ASSERT_TRUE(cryptonote::get_account_address_as_str(nettype, false, tcd.change_dts.addr) == "9svHk1wHPo3ULf2AZykghzcye6sitaRE4MaDjPC6uanTHCynHjJHZaiAb922PojE1GexhhRt1LVf5DC43feyrRZMLXQr3mk"); + ASSERT_EQ("KabJYqRtfK3ULf2AZykghzcye6sitaRE4MaDjPC6uanTHCynHjJHZaiAb922PojE1GexhhRt1LVf5DC43feyrRZMLYvropu", cryptonote::get_account_address_as_str(nettype, false, tcd.change_dts.addr)); // ptx.construction_data.splitted_dsts ASSERT_TRUE(tcd.splitted_dsts.size() == 2); auto& splitted_dst0 = tcd.splitted_dsts[0]; auto& splitted_dst1 = tcd.splitted_dsts[1]; ASSERT_TRUE(splitted_dst0.amount == 1400000000000); ASSERT_TRUE(splitted_dst1.amount == 9631208773403); - ASSERT_TRUE(cryptonote::get_account_address_as_str(nettype, false, splitted_dst0.addr) == "9xnhrMczQkPeoGi6dyu6BgKAYX4tZsDs6KHCkyTStDBKL4M4pM1gfCR3utmTAcSaKHGa1R5o266FbdnubErmij3oMdLyYgA"); - ASSERT_TRUE(cryptonote::get_account_address_as_str(nettype, false, splitted_dst1.addr) == "9svHk1wHPo3ULf2AZykghzcye6sitaRE4MaDjPC6uanTHCynHjJHZaiAb922PojE1GexhhRt1LVf5DC43feyrRZMLXQr3mk"); + ASSERT_EQ("KfTifB7bgGPeoGi6dyu6BgKAYX4tZsDs6KHCkyTStDBKL4M4pM1gfCR3utmTAcSaKHGa1R5o266FbdnubErmij3oMZVm3N6", cryptonote::get_account_address_as_str(nettype, false, splitted_dst0.addr)); + ASSERT_EQ("KabJYqRtfK3ULf2AZykghzcye6sitaRE4MaDjPC6uanTHCynHjJHZaiAb922PojE1GexhhRt1LVf5DC43feyrRZMLYvropu", cryptonote::get_account_address_as_str(nettype, false, splitted_dst1.addr)); // ptx.construction_data.selected_transfers ASSERT_TRUE(tcd.selected_transfers.size() == 1); ASSERT_TRUE(tcd.selected_transfers.front() == 2); @@ -1173,7 +1182,7 @@ TEST(Serialization, portability_signed_tx) ASSERT_TRUE(tcd.dests.size() == 1); auto& dest = tcd.dests[0]; ASSERT_TRUE(dest.amount == 1400000000000); - ASSERT_TRUE(cryptonote::get_account_address_as_str(nettype, false, dest.addr) == "9xnhrMczQkPeoGi6dyu6BgKAYX4tZsDs6KHCkyTStDBKL4M4pM1gfCR3utmTAcSaKHGa1R5o266FbdnubErmij3oMdLyYgA"); + ASSERT_EQ("KfTifB7bgGPeoGi6dyu6BgKAYX4tZsDs6KHCkyTStDBKL4M4pM1gfCR3utmTAcSaKHGa1R5o266FbdnubErmij3oMZVm3N6", cryptonote::get_account_address_as_str(nettype, false, dest.addr)); // key_images ASSERT_TRUE(exported_txs.key_images.size() == 3); auto& ki0 = exported_txs.key_images[0]; diff --git a/tests/unit_tests/uri.cpp b/tests/unit_tests/uri.cpp index 0ece6d4c1..1c96e34c9 100644 --- a/tests/unit_tests/uri.cpp +++ b/tests/unit_tests/uri.cpp @@ -29,8 +29,8 @@ #include "gtest/gtest.h" #include "wallet/wallet2.h" -#define TEST_ADDRESS "9tTLtauaEKSj7xoVXytVH32R1pLZBk4VV4mZFGEh4wkXhDWqw1soPyf3fGixf1kni31VznEZkWNEza9d5TvjWwq5PaohYHC" -#define TEST_INTEGRATED_ADDRESS "A4A1uPj4qaxj7xoVXytVH32R1pLZBk4VV4mZFGEh4wkXhDWqw1soPyf3fGixf1kni31VznEZkWNEza9d5TvjWwq5acaPMJfMbn3ReTsBpp" +#define TEST_ADDRESS "KaHhUrBYYVZQFoFXvLWnZUGXqg6snz82fhW1HGGATzMGTeXVLUnVej6hUYJ2nGU5zGLp3zcsR24tDVV9DXGpM8oD5HwjdoD" +#define TEST_INTEGRATED_ADDRESS "KjzNVf139m5QFoFXvLWnZUGXqg6snz82fhW1HGGATzMGTeXVLUnVej6hUYJ2nGU5zGLp3zcsR24tDVV9DXGpM8oD7WyR6Y1T6SFRfKY1rf" // included payment id: <f612cac0b6cb1cda> #define PARSE_URI(uri, expected) \ @@ -212,4 +212,3 @@ TEST(uri, url_encoded_once) PARSE_URI("monero:" TEST_ADDRESS"?tx_description=foo%2020", true); ASSERT_EQ(description, "foo 20"); } - diff --git a/tests/unit_tests/wallet_storage.cpp b/tests/unit_tests/wallet_storage.cpp index bf33936ed..a487f52e4 100644 --- a/tests/unit_tests/wallet_storage.cpp +++ b/tests/unit_tests/wallet_storage.cpp @@ -37,7 +37,7 @@ using namespace boost::filesystem; using namespace epee::file_io_utils; static constexpr const char WALLET_00fd416a_PRIMARY_ADDRESS[] = - "45p2SngJAPSJbqSiUvYfS3BfhEdxZmv8pDt25oW1LzxrZv9Uq6ARagiFViMGUE3gJk5VPWingCXVf1p2tyAy6SUeSHPhbve"; + "FTVmPgMqWacJbqSiUvYfS3BfhEdxZmv8pDt25oW1LzxrZv9Uq6ARagiFViMGUE3gJk5VPWingCXVf1p2tyAy6SUeSJWdykM"; TEST(wallet_storage, store_to_file2file) { @@ -48,10 +48,13 @@ TEST(wallet_storage, store_to_file2file) ASSERT_TRUE(is_file_exist(source_wallet_file.string())); ASSERT_TRUE(is_file_exist(source_wallet_file.string() + ".keys")); - tools::copy_file(source_wallet_file.string(), interm_wallet_file.string()); + // Rebuild the cache from the portable keys. The bundled upstream cache + // correctly fails Monzero's genesis-hash check. + if (is_file_exist(interm_wallet_file.string())) + remove(interm_wallet_file); tools::copy_file(source_wallet_file.string() + ".keys", interm_wallet_file.string() + ".keys"); - ASSERT_TRUE(is_file_exist(interm_wallet_file.string())); + ASSERT_FALSE(is_file_exist(interm_wallet_file.string())); ASSERT_TRUE(is_file_exist(interm_wallet_file.string() + ".keys")); if (is_file_exist(target_wallet_file.string())) @@ -74,6 +77,7 @@ TEST(wallet_storage, store_to_file2file) { tools::wallet2 w; w.load(interm_wallet_file.string(), password); + w.store(); const std::string primary_address = w.get_address_as_str(); EXPECT_EQ(WALLET_00fd416a_PRIMARY_ADDRESS, primary_address); w.store_to(target_wallet_file.string(), password); @@ -139,10 +143,13 @@ TEST(wallet_storage, change_password_same_file) ASSERT_TRUE(is_file_exist(source_wallet_file.string())); ASSERT_TRUE(is_file_exist(source_wallet_file.string() + ".keys")); - tools::copy_file(source_wallet_file.string(), interm_wallet_file.string()); + // Rebuild the cache from the portable keys. The bundled upstream cache + // correctly fails Monzero's genesis-hash check. + if (is_file_exist(interm_wallet_file.string())) + remove(interm_wallet_file); tools::copy_file(source_wallet_file.string() + ".keys", interm_wallet_file.string() + ".keys"); - ASSERT_TRUE(is_file_exist(interm_wallet_file.string())); + ASSERT_FALSE(is_file_exist(interm_wallet_file.string())); ASSERT_TRUE(is_file_exist(interm_wallet_file.string() + ".keys")); epee::wipeable_string old_password("beepbeep"); @@ -151,6 +158,7 @@ TEST(wallet_storage, change_password_same_file) { tools::wallet2 w; w.load(interm_wallet_file.string(), old_password); + w.store(); const std::string primary_address = w.get_address_as_str(); EXPECT_EQ(WALLET_00fd416a_PRIMARY_ADDRESS, primary_address); w.change_password(w.get_wallet_file(), old_password, new_password); @@ -178,10 +186,13 @@ TEST(wallet_storage, change_password_different_file) ASSERT_TRUE(is_file_exist(source_wallet_file.string())); ASSERT_TRUE(is_file_exist(source_wallet_file.string() + ".keys")); - tools::copy_file(source_wallet_file.string(), interm_wallet_file.string()); + // Rebuild the cache from the portable keys. The bundled upstream cache + // correctly fails Monzero's genesis-hash check. + if (is_file_exist(interm_wallet_file.string())) + remove(interm_wallet_file); tools::copy_file(source_wallet_file.string() + ".keys", interm_wallet_file.string() + ".keys"); - ASSERT_TRUE(is_file_exist(interm_wallet_file.string())); + ASSERT_FALSE(is_file_exist(interm_wallet_file.string())); ASSERT_TRUE(is_file_exist(interm_wallet_file.string() + ".keys")); if (is_file_exist(target_wallet_file.string())) @@ -197,6 +208,7 @@ TEST(wallet_storage, change_password_different_file) { tools::wallet2 w; w.load(interm_wallet_file.string(), old_password); + w.store(); const std::string primary_address = w.get_address_as_str(); EXPECT_EQ(WALLET_00fd416a_PRIMARY_ADDRESS, primary_address); w.change_password(target_wallet_file.string(), old_password, new_password); diff --git a/utils/configure-miner-stats.php b/utils/configure-miner-stats.php new file mode 100644 index 000000000..7b2f72d34 --- /dev/null +++ b/utils/configure-miner-stats.php @@ -0,0 +1,30 @@ +<?php +declare(strict_types=1); + +if (PHP_SAPI !== 'cli' || $argc !== 3) { + fwrite(STDERR, "Usage: php configure-miner-stats.php CONFIG_PATH DATA_PATH\n"); + exit(1); +} + +$configPath = $argv[1]; +$dataPath = $argv[2]; +if (file_exists($configPath)) { + fwrite(STDERR, "Refusing to overwrite existing configuration: {$configPath}\n"); + exit(1); +} + +$serverSecret = bin2hex(random_bytes(32)); +$ingestToken = bin2hex(random_bytes(32)); +$config = "<?php\nreturn " . var_export([ + 'MONZERO_STATS_SECRET' => $serverSecret, + 'MONZERO_STATS_INGEST_TOKEN' => $ingestToken, + 'MONZERO_STATS_FILE' => $dataPath, +], true) . ";\n"; + +if (file_put_contents($configPath, $config, LOCK_EX) === false) { + fwrite(STDERR, "Unable to create configuration.\n"); + exit(1); +} +chmod($configPath, 0600); + +fwrite(STDOUT, "Configuration created. Team ingest token (save privately):\n{$ingestToken}\n"); diff --git a/utils/monzero-miner-reporter.py b/utils/monzero-miner-reporter.py new file mode 100755 index 000000000..cbbc3029a --- /dev/null +++ b/utils/monzero-miner-reporter.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Opt-in anonymous mining telemetry reporter for monzero.org.""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import stat +import time +import urllib.error +import urllib.request +import uuid +from pathlib import Path + + +FOUND_BLOCK = re.compile(r"Found block [0-9a-f]+ at height ", re.IGNORECASE) + + +def json_request(url: str, body: dict | None = None, token: str = "") -> dict: + data = None if body is None else json.dumps(body).encode("utf-8") + headers = {"Accept": "application/json"} + if data is not None: + headers["Content-Type"] = "application/json" + if token: + headers["Authorization"] = f"Bearer {token}" + request = urllib.request.Request(url, data=data, headers=headers) + with urllib.request.urlopen(request, timeout=8) as response: + return json.load(response) + + +def installation_id(path: Path) -> str: + if path.exists(): + return path.read_text(encoding="ascii").strip() + path.parent.mkdir(parents=True, exist_ok=True) + value = str(uuid.uuid4()) + path.write_text(value + "\n", encoding="ascii") + path.chmod(stat.S_IRUSR | stat.S_IWUSR) + return value + + +def blocks_found(log_path: Path) -> int: + try: + with log_path.open("r", encoding="utf-8", errors="ignore") as log: + return sum(1 for line in log if FOUND_BLOCK.search(line)) + except FileNotFoundError: + return 0 + + +def report(args: argparse.Namespace) -> None: + status = json_request(args.rpc.rstrip("/") + "/mining_status", {}) + payload = { + "installation_id": installation_id(args.id_file), + "hashrate": int(status.get("speed", 0)) if status.get("active") else 0, + "blocks_found": blocks_found(args.log), + } + result = json_request(args.endpoint, payload, args.token) + if result.get("status") != "OK": + raise RuntimeError(f"telemetry rejected: {result}") + print( + f"reported {payload['hashrate']} H/s and " + f"{payload['blocks_found']} block(s); no wallet or hardware ID sent" + ) + + +def main() -> None: + default_data = Path.home() / ".monzero" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--endpoint", default="https://monzero.org/api/miner-stats/") + parser.add_argument("--rpc", default="http://127.0.0.1:6175") + parser.add_argument("--token", default=os.environ.get("MONZERO_STATS_TOKEN", "")) + parser.add_argument("--id-file", type=Path, default=default_data / "telemetry-id") + parser.add_argument("--log", type=Path, default=default_data / "monzero.log") + parser.add_argument("--interval", type=int, default=0, help="repeat every N seconds") + args = parser.parse_args() + if not args.token: + parser.error("set MONZERO_STATS_TOKEN or pass --token") + if args.interval and args.interval < 30: + parser.error("--interval must be at least 30 seconds") + + while True: + try: + report(args) + except (OSError, ValueError, urllib.error.URLError, RuntimeError) as error: + print(f"report failed: {error}") + if not args.interval: + break + time.sleep(args.interval) + + +if __name__ == "__main__": + main() diff --git a/utils/release/package-linux.sh b/utils/release/package-linux.sh new file mode 100755 index 000000000..bcc659e57 --- /dev/null +++ b/utils/release/package-linux.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + echo "Usage: $0 <build-bin-dir> <output-dir> <version>" >&2 + echo "Set ALLOW_DIRTY=1 only for an explicitly marked development artifact." >&2 + exit 2 +} + +[[ $# -eq 3 ]] || usage + +source_root=$(git rev-parse --show-toplevel) +build_bin=$(realpath "$1") +output_dir=$(realpath -m "$2") +version=$3 + +[[ $version =~ ^[0-9A-Za-z][0-9A-Za-z._-]*$ ]] || { echo "Invalid version label" >&2; exit 2; } +[[ -x "$build_bin/monzerod" ]] || { echo "Missing executable: $build_bin/monzerod" >&2; exit 2; } +[[ -x "$build_bin/monzero-wallet-cli" ]] || { echo "Missing executable: $build_bin/monzero-wallet-cli" >&2; exit 2; } + +source_commit=$(git -C "$source_root" rev-parse HEAD) +source_epoch=${SOURCE_DATE_EPOCH:-$(git -C "$source_root" show -s --format=%ct "$source_commit")} +[[ $source_epoch =~ ^[0-9]+$ ]] || { echo "SOURCE_DATE_EPOCH must be an integer" >&2; exit 2; } + +dirty=false +if [[ -n $(git -C "$source_root" status --porcelain --untracked-files=normal) ]]; then + dirty=true + [[ ${ALLOW_DIRTY:-0} == 1 ]] || { + echo "Refusing to package a dirty source tree. Commit the reviewed baseline first." >&2 + exit 1 + } + version="${version}-development-dirty" +fi + +package_name="monzero-${version}-linux-x86_64" +mkdir -p "$output_dir" +work_dir=$(mktemp -d "${TMPDIR:-/tmp}/monzero-package.XXXXXX") +trap 'rm -rf -- "$work_dir"' EXIT +package_dir="$work_dir/$package_name" +mkdir -p "$package_dir" + +install -m 0755 "$build_bin/monzerod" "$package_dir/monzerod" +install -m 0755 "$build_bin/monzero-wallet-cli" "$package_dir/monzero-wallet-cli" +for optional in monzero-wallet-rpc monzero-gen-ssl-cert; do + [[ -x "$build_bin/$optional" ]] && install -m 0755 "$build_bin/$optional" "$package_dir/$optional" +done + +install -m 0755 "$source_root/start-monzerod.sh" "$package_dir/start-monzerod.sh" +install -m 0755 "$source_root/start-monzero-wallet-cli.sh" "$package_dir/start-monzero-wallet-cli.sh" +install -m 0755 "$source_root/start-monzero-miner.sh" "$package_dir/start-monzero-miner.sh" +install -m 0755 "$source_root/stop-monzero-miner.sh" "$package_dir/stop-monzero-miner.sh" +install -m 0644 "$source_root/LICENSE" "$package_dir/LICENSE" +install -m 0644 "$source_root/MONZERO_CHAIN_SPEC.md" "$package_dir/MONZERO_CHAIN_SPEC.md" +install -m 0644 "$source_root/docs/RELEASE_CHECKLIST.md" "$package_dir/RELEASE_CHECKLIST.md" + +{ + echo "package=$package_name" + echo "source_commit=$source_commit" + echo "source_date_epoch=$source_epoch" + echo "source_tree_dirty=$dirty" + echo "binary_build_reproducibility=unverified" + echo "assets_consensus_enabled=false" + echo "native_ticker=XMZ" +} > "$package_dir/BUILD-MANIFEST.txt" + +( + cd "$package_dir" + find . -maxdepth 1 -type f ! -name SHA256SUMS -printf '%P\n' \ + | LC_ALL=C sort \ + | xargs -r sha256sum > SHA256SUMS + sha256sum -c SHA256SUMS +) + +archive="$output_dir/$package_name.tar.gz" +tar --sort=name \ + --mtime="@$source_epoch" \ + --owner=0 --group=0 --numeric-owner \ + --pax-option=delete=atime,delete=ctime \ + -C "$work_dir" -cf - "$package_name" \ + | gzip -n -9 > "$archive" +sha256sum "$archive" > "$archive.sha256" + +echo "$archive" +echo "$archive.sha256" diff --git a/utils/release/verify-package.sh b/utils/release/verify-package.sh new file mode 100755 index 000000000..22ccab4c8 --- /dev/null +++ b/utils/release/verify-package.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +set -euo pipefail + +[[ $# -eq 1 ]] || { echo "Usage: $0 <monzero-linux.tar.gz>" >&2; exit 2; } +archive=$(realpath "$1") +[[ -f "$archive" ]] || { echo "Archive not found: $archive" >&2; exit 2; } + +work_dir=$(mktemp -d "${TMPDIR:-/tmp}/monzero-verify.XXXXXX") +trap 'rm -rf -- "$work_dir"' EXIT +tar -xzf "$archive" -C "$work_dir" + +mapfile -t roots < <(find "$work_dir" -mindepth 1 -maxdepth 1 -type d) +[[ ${#roots[@]} -eq 1 ]] || { echo "Archive must contain exactly one root directory" >&2; exit 1; } +root=${roots[0]} + +[[ -x "$root/monzerod" && -x "$root/monzero-wallet-cli" ]] || { + echo "Required Monzero executables are missing" >&2 + exit 1 +} +[[ -f "$root/BUILD-MANIFEST.txt" && -f "$root/SHA256SUMS" ]] || { + echo "Build or checksum manifest is missing" >&2 + exit 1 +} + +if find "$root" -type l -print -quit | grep -q .; then + echo "Package contains symbolic links" >&2 + exit 1 +fi + +( + cd "$root" + sha256sum -c SHA256SUMS +) + +binary_report=$(file "$root/monzerod" "$root/monzero-wallet-cli") +echo "$binary_report" +if [[ ${RELEASE_STRICT:-0} == 1 ]]; then + if grep -Eq 'dynamically linked|not stripped|with debug_info' <<< "$binary_report"; then + echo "Strict release verification rejects dynamic, unstripped, or debug binaries" >&2 + exit 1 + fi + grep -qx 'source_tree_dirty=false' "$root/BUILD-MANIFEST.txt" || { + echo "Strict release verification rejects a dirty source manifest" >&2 + exit 1 + } + grep -qx 'binary_build_reproducibility=verified' "$root/BUILD-MANIFEST.txt" || { + echo "Strict release verification requires independently verified binary reproducibility" >&2 + exit 1 + } +fi +"$root/monzerod" --version +"$root/monzero-wallet-cli" --version +echo "Package verification passed: $archive" diff --git a/website/.htaccess b/website/.htaccess new file mode 100644 index 000000000..f437a279f --- /dev/null +++ b/website/.htaccess @@ -0,0 +1,22 @@ +Options -Indexes + +<IfModule mod_headers.c> + Header always set X-Content-Type-Options "nosniff" + Header always set Referrer-Policy "strict-origin-when-cross-origin" + Header always set X-Frame-Options "DENY" +</IfModule> + +<FilesMatch "\.(?:css|js|png|ico|svg|sha256)$"> + <IfModule mod_expires.c> + ExpiresActive On + ExpiresDefault "access plus 7 days" + </IfModule> +</FilesMatch> + +<FilesMatch "\.tar\.gz$"> + ForceType application/gzip +</FilesMatch> + +<FilesMatch "\.zip$"> + ForceType application/zip +</FilesMatch> diff --git a/website/README.md b/website/README.md new file mode 100644 index 000000000..c0ecd76ac --- /dev/null +++ b/website/README.md @@ -0,0 +1,65 @@ +# Monzero website + +Website for `monzero.org`, including the PHP-backed block explorer at +`/explorer/`. It contains no build-time dependencies. + +## Preview locally + +```bash +cd website +php -S 127.0.0.1:8080 +``` + +Open `http://127.0.0.1:8080` for the website and +`http://127.0.0.1:8080/explorer/` for the explorer. + +## VPS deployment + +Copy this directory's contents to `/var/www/monzero`, install +`nginx-monzero.conf` as `/etc/nginx/sites-available/monzero`, enable the site, +test Nginx, and reload it. The included Nginx route proxies only the homepage's +`/get_info` request to the restricted local RPC service. + +On IONOS web hosting, upload the complete contents of this directory to the +domain's assigned document root. PHP must be enabled so `/explorer/api.php` can +proxy the allowlisted, read-only requests to the restricted public node. + +## Anonymous miner statistics + +The miner table uses an opt-in heartbeat endpoint. It never accepts or returns +a wallet address, hostname, IP address, serial number, or hardware identifier. +Each reporter creates a random UUID; PHP pseudonymizes it with a server-side +HMAC secret before storage and exposes only the first eight pseudonym digits. + +Configure these secrets in the PHP environment (use independent random values): + +```text +MONZERO_STATS_SECRET=<at least 32 random characters, server only> +MONZERO_STATS_INGEST_TOKEN=<at least 24 random characters, team reporters> +MONZERO_STATS_FILE=/an/apache-writable/private/path/miner-stats.json +``` + +For shared hosting without environment-variable controls, create +`.monzero-miner-stats.php` one directory above the document root: + +```php +<?php +return [ + 'MONZERO_STATS_SECRET' => 'server-only-random-value', + 'MONZERO_STATS_INGEST_TOKEN' => 'team-reporter-random-value', + 'MONZERO_STATS_FILE' => __DIR__ . '/.monzero-miner-stats.json', +]; +``` + +Generate suitable values with `openssl rand -hex 32`. Do not place the HMAC +secret in a download or reporter. Give only the ingest token to miners who opt +in, then run the reporter alongside their already-running local daemon: + +```bash +MONZERO_STATS_TOKEN='team-ingest-token' \ + python3 utils/monzero-miner-reporter.py --interval 60 +``` + +The table counts a miner as active for three minutes after its last heartbeat. +Hash rate and blocks found are self-reported. Block counts are derived from the +local daemon log and are not consensus-verified leaderboard claims. diff --git a/website/api/miner-stats/index.php b/website/api/miner-stats/index.php new file mode 100644 index 000000000..43ae75fbc --- /dev/null +++ b/website/api/miner-stats/index.php @@ -0,0 +1,130 @@ +<?php +declare(strict_types=1); + +header('Content-Type: application/json; charset=utf-8'); +header('Cache-Control: no-store'); +header('X-Content-Type-Options: nosniff'); + +const ACTIVE_SECONDS = 180; +const MAX_RECORD_AGE = 604800; + +$fileConfig = []; +$documentRoot = $_SERVER['DOCUMENT_ROOT'] ?? ''; +if (is_string($documentRoot) && $documentRoot !== '') { + $configPath = dirname($documentRoot) . '/.monzero-miner-stats.php'; + if (is_file($configPath)) { + $loadedConfig = require $configPath; + if (is_array($loadedConfig)) $fileConfig = $loadedConfig; + } +} + +function fail(int $status, string $message): never +{ + http_response_code($status); + echo json_encode(['status' => $message]); + exit; +} + +function configValue(string $name): string +{ + global $fileConfig; + if (isset($fileConfig[$name]) && is_string($fileConfig[$name])) { + return trim($fileConfig[$name]); + } + $value = getenv($name); + return is_string($value) ? trim($value) : ''; +} + +function dataPath(): string +{ + $configured = configValue('MONZERO_STATS_FILE'); + return $configured !== '' ? $configured : sys_get_temp_dir() . '/monzero-miner-stats.json'; +} + +function readRecords($handle): array +{ + rewind($handle); + $contents = stream_get_contents($handle); + if (!is_string($contents) || trim($contents) === '') return []; + $decoded = json_decode($contents, true); + return is_array($decoded) ? $decoded : []; +} + +$secret = configValue('MONZERO_STATS_SECRET'); +if (strlen($secret) < 32) fail(503, 'NOT_CONFIGURED'); + +$path = dataPath(); +$directory = dirname($path); +if (!is_dir($directory) || !is_writable($directory)) fail(503, 'STORAGE_UNAVAILABLE'); + +$handle = @fopen($path, 'c+'); +if ($handle === false || !flock($handle, LOCK_EX)) fail(503, 'STORAGE_UNAVAILABLE'); +$records = readRecords($handle); +$now = time(); + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $token = configValue('MONZERO_STATS_INGEST_TOKEN'); + if (strlen($token) < 24) fail(503, 'INGEST_NOT_CONFIGURED'); + $authorization = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; + if (!hash_equals('Bearer ' . $token, $authorization)) fail(401, 'UNAUTHORIZED'); + + $raw = file_get_contents('php://input'); + $input = json_decode(is_string($raw) ? $raw : '', true); + if (!is_array($input)) fail(400, 'INVALID_JSON'); + + $installationId = $input['installation_id'] ?? ''; + $hashrate = filter_var($input['hashrate'] ?? null, FILTER_VALIDATE_FLOAT); + $blocks = filter_var($input['blocks_found'] ?? null, FILTER_VALIDATE_INT); + if (!is_string($installationId) || !preg_match('/^[a-f0-9-]{32,64}$/i', $installationId)) fail(400, 'INVALID_INSTALLATION_ID'); + if ($hashrate === false || $hashrate < 0 || $hashrate > 1000000000000) fail(400, 'INVALID_HASHRATE'); + if ($blocks === false || $blocks < 0 || $blocks > 1000000000) fail(400, 'INVALID_BLOCK_COUNT'); + + $privateId = hash_hmac('sha256', strtolower($installationId), $secret); + $records[$privateId] = [ + 'hashrate' => round((float)$hashrate, 2), + 'blocks_found' => (int)$blocks, + 'updated_at' => $now, + ]; +} + +foreach ($records as $id => $record) { + if (!is_array($record) || ($record['updated_at'] ?? 0) < $now - MAX_RECORD_AGE) unset($records[$id]); +} + +rewind($handle); +ftruncate($handle, 0); +fwrite($handle, json_encode($records, JSON_UNESCAPED_SLASHES)); +fflush($handle); +flock($handle, LOCK_UN); +fclose($handle); + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + echo json_encode(['status' => 'OK']); + exit; +} +if ($_SERVER['REQUEST_METHOD'] !== 'GET') { + header('Allow: GET, POST'); + fail(405, 'METHOD_NOT_ALLOWED'); +} + +$miners = []; +foreach ($records as $id => $record) { + $active = (int)$record['updated_at'] >= $now - ACTIVE_SECONDS; + if (!$active) continue; + $miners[] = [ + 'name' => 'Miner ' . strtoupper(substr($id, 0, 8)), + 'hashrate' => (float)$record['hashrate'], + 'blocks_found' => (int)$record['blocks_found'], + 'active' => true, + ]; +} +usort($miners, static fn(array $a, array $b): int => $b['hashrate'] <=> $a['hashrate']); + +echo json_encode([ + 'status' => 'OK', + 'generated_at' => $now, + 'active_miners' => count($miners), + 'total_hashrate' => array_sum(array_column($miners, 'hashrate')), + 'total_blocks' => array_sum(array_column($miners, 'blocks_found')), + 'miners' => $miners, +], JSON_UNESCAPED_SLASHES); diff --git a/website/api/node-info/index.php b/website/api/node-info/index.php new file mode 100644 index 000000000..05f1610a4 --- /dev/null +++ b/website/api/node-info/index.php @@ -0,0 +1,58 @@ +<?php +declare(strict_types=1); + +header('Content-Type: application/json; charset=utf-8'); +header('Cache-Control: no-store'); +header('X-Content-Type-Options: nosniff'); + +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(405); + header('Allow: POST'); + echo json_encode(['status' => 'METHOD_NOT_ALLOWED']); + exit; +} + +$url = 'http://node.monzero.org:6175/get_info'; +$error = ''; + +if (function_exists('curl_init')) { + $curl = curl_init($url); + curl_setopt_array($curl, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => '{}', + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => 2, + CURLOPT_TIMEOUT => 5, + ]); + $response = curl_exec($curl); + $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); + $error = curl_error($curl); + curl_close($curl); +} else { + $context = stream_context_create(['http' => [ + 'method' => 'POST', + 'header' => "Content-Type: application/json\r\nConnection: close\r\n", + 'content' => '{}', + 'timeout' => 5, + 'ignore_errors' => true, + ]]); + $response = @file_get_contents($url, false, $context); + $statusLine = $http_response_header[0] ?? ''; + preg_match('/\s(\d{3})\s/', $statusLine, $match); + $status = isset($match[1]) ? (int)$match[1] : 0; + $error = 'HTTP ' . $status; +} + +$decoded = is_string($response) ? json_decode($response, true) : null; +if ($response === false || $status !== 200 || !is_array($decoded)) { + http_response_code(502); + echo json_encode(['status' => 'NODE_UNAVAILABLE']); + error_log('Monzero node status proxy failed: ' . $error); + exit; +} + +$peerCount = (int)($decoded['incoming_connections_count'] ?? 0) + + (int)($decoded['outgoing_connections_count'] ?? 0); +$decoded['connections_hidden'] = !empty($decoded['restricted']) && $peerCount === 0; +echo json_encode($decoded, JSON_UNESCAPED_SLASHES); diff --git a/website/app.js b/website/app.js new file mode 100644 index 000000000..416446064 --- /dev/null +++ b/website/app.js @@ -0,0 +1,186 @@ +const menuButton = document.querySelector('.menu-toggle'); +const nav = document.querySelector('#site-nav'); +const toast = document.querySelector('.toast'); + +menuButton?.addEventListener('click', () => { + const open = nav.classList.toggle('open'); + menuButton.setAttribute('aria-expanded', String(open)); +}); + +nav?.querySelectorAll('a').forEach((link) => link.addEventListener('click', () => { + nav.classList.remove('open'); + menuButton?.setAttribute('aria-expanded', 'false'); +})); + +document.querySelectorAll('[data-copy]').forEach((button) => { + button.addEventListener('click', async () => { + try { + await navigator.clipboard.writeText(button.dataset.copy); + toast.classList.add('show'); + window.setTimeout(() => toast.classList.remove('show'), 1600); + } catch { + button.textContent = 'Select manually'; + } + }); +}); + +document.querySelector('#year').textContent = new Date().getFullYear(); + +const number = new Intl.NumberFormat('en-GB'); +const historyKey = 'monzero-network-chart-history-v1'; +let chartHistory = { hashrate: [], difficulty: [] }; +try { + chartHistory = { ...chartHistory, ...JSON.parse(localStorage.getItem(historyKey) || '{}') }; +} catch { + chartHistory = { hashrate: [], difficulty: [] }; +} + +function addChartPoint(series, value) { + const numeric = Number(value); + if (!Number.isFinite(numeric) || numeric < 0) return; + const points = Array.isArray(chartHistory[series]) ? chartHistory[series] : []; + const now = Date.now(); + if (points.length && now - points.at(-1).time < 25000) return; + points.push({ time: now, value: numeric }); + chartHistory[series] = points.slice(-120); + try { + localStorage.setItem(historyKey, JSON.stringify(chartHistory)); + } catch { + // Charts still work for this page view when browser storage is disabled. + } + renderChart(series); +} + +function renderChart(series) { + const points = chartHistory[series] || []; + const svg = document.querySelector(`#chart-${series}`); + const line = svg?.querySelector('.chart-line'); + if (!line || !points.length) return; + const values = points.map((point) => point.value); + const minimum = Math.min(...values); + const maximum = Math.max(...values); + const spread = maximum - minimum || Math.max(maximum * .1, 1); + const coordinates = points.map((point, index) => { + const x = points.length === 1 ? 600 : (index / (points.length - 1)) * 600; + const y = 165 - ((point.value - minimum) / spread) * 150; + return `${x.toFixed(1)},${y.toFixed(1)}`; + }); + line.setAttribute('points', coordinates.join(' ')); +} +const fields = { + height: document.querySelector('#metric-height'), + difficulty: document.querySelector('#metric-difficulty'), + connections: document.querySelector('#metric-connections'), + rpc: document.querySelector('#metric-rpc'), + state: document.querySelector('#node-state'), + dot: document.querySelector('#node-dot'), +}; + +async function updateNodeStatus() { + try { + const response = await fetch('/api/node-info/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: '{}', + signal: AbortSignal.timeout(6000), + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const info = await response.json(); + fields.height.textContent = number.format(info.height ?? 0); + fields.difficulty.textContent = number.format(info.difficulty ?? 0); + document.querySelector('#chart-difficulty-value').textContent = number.format(info.difficulty ?? 0); + addChartPoint('difficulty', info.difficulty); + const peerCount = info.peer_connections + ?? ((info.incoming_connections_count ?? 0) + (info.outgoing_connections_count ?? 0)); + fields.connections.textContent = info.connections_hidden ? 'HIDDEN' : number.format(peerCount); + fields.connections.title = info.connections_hidden + ? 'Connection counts are intentionally hidden by the public restricted RPC.' + : 'Current incoming and outgoing P2P peer nodes, not unique people.'; + fields.rpc.textContent = info.restricted ? 'RESTRICTED' : 'ONLINE'; + fields.state.textContent = info.status === 'OK' ? 'Node online' : info.status; + fields.dot.className = 'status-dot online'; + } catch { + fields.state.textContent = 'Status unavailable'; + fields.rpc.textContent = '—'; + fields.dot.className = 'status-dot offline'; + } +} + +updateNodeStatus(); +window.setInterval(updateNodeStatus, 30000); + +const minerFields = { + active: document.querySelector('#miner-active'), + hashrate: document.querySelector('#miner-hashrate'), + blocks: document.querySelector('#miner-blocks'), + updated: document.querySelector('#miner-updated'), + rows: document.querySelector('#miner-rows'), +}; + +function formatHashrate(value) { + let rate = Number(value) || 0; + const units = ['H/s', 'kH/s', 'MH/s', 'GH/s']; + let unit = 0; + while (rate >= 1000 && unit < units.length - 1) { + rate /= 1000; + unit += 1; + } + return `${rate.toLocaleString('en-GB', { maximumFractionDigits: rate < 10 ? 2 : 1 })} ${units[unit]}`; +} + +function renderMiners(data) { + const miners = Array.isArray(data.miners) ? data.miners : []; + minerFields.active.textContent = number.format(data.active_miners ?? miners.length); + minerFields.hashrate.textContent = formatHashrate(data.total_hashrate ?? 0); + document.querySelector('#chart-hashrate-value').textContent = formatHashrate(data.total_hashrate ?? 0); + addChartPoint('hashrate', data.total_hashrate); + minerFields.blocks.textContent = number.format(data.total_blocks ?? 0); + minerFields.updated.textContent = data.generated_at + ? new Date(data.generated_at * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + : '—'; + + if (!miners.length) { + minerFields.rows.innerHTML = '<tr><td colspan="5" class="table-message">No miners have reported in the last three minutes.</td></tr>'; + return; + } + + minerFields.rows.replaceChildren(...miners.map((miner, index) => { + const row = document.createElement('tr'); + const values = [ + `#${index + 1}`, + miner.name, + formatHashrate(miner.hashrate), + number.format(miner.blocks_found ?? 0), + miner.active ? 'ACTIVE' : 'OFFLINE', + ]; + values.forEach((value, column) => { + const cell = document.createElement('td'); + cell.textContent = value; + if (column === 4) cell.className = miner.active ? 'miner-online' : ''; + row.append(cell); + }); + return row; + })); +} + +async function updateMinerStats() { + try { + const response = await fetch('/api/miner-stats/', { + headers: { Accept: 'application/json' }, + signal: AbortSignal.timeout(6000), + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + renderMiners(await response.json()); + } catch { + minerFields.active.textContent = '—'; + minerFields.hashrate.textContent = '—'; + minerFields.blocks.textContent = '—'; + minerFields.updated.textContent = '—'; + minerFields.rows.innerHTML = '<tr><td colspan="5" class="table-message">Miner statistics are temporarily unavailable.</td></tr>'; + } +} + +updateMinerStats(); +window.setInterval(updateMinerStats, 30000); +renderChart('hashrate'); +renderChart('difficulty'); diff --git a/website/assets/monzero-xmz-coin.png b/website/assets/monzero-xmz-coin.png Binary files differnew file mode 100644 index 000000000..0e00071eb --- /dev/null +++ b/website/assets/monzero-xmz-coin.png diff --git a/website/explorer/api.php b/website/explorer/api.php new file mode 100644 index 000000000..109485dab --- /dev/null +++ b/website/explorer/api.php @@ -0,0 +1,126 @@ +<?php +declare(strict_types=1); + +const NODE_RPC = 'http://node.monzero.org:6175'; +const MAX_RANGE = 25; + +header('Content-Type: application/json; charset=utf-8'); +header('Cache-Control: no-store'); +header('X-Content-Type-Options: nosniff'); + +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + respond(['error' => 'POST required'], 405); +} + +$input = json_decode(file_get_contents('php://input'), true); +if (!is_array($input)) respond(['error' => 'Invalid JSON'], 400); +$action = $input['action'] ?? ''; + +try { + switch ($action) { + case 'info': + respond(rpc('/get_info', [])); + + case 'blocks': + $info = rpc('/get_info', []); + $height = max(1, (int)($info['height'] ?? 1)); + $limit = min(MAX_RANGE, max(1, (int)($input['limit'] ?? 15))); + $end = max(0, $height - 1); + $start = max(0, $end - $limit + 1); + $range = jsonRpc('get_block_headers_range', [ + 'start_height' => $start, + 'end_height' => $end, + 'fill_pow_hash' => false, + ]); + respond(['info' => $info, 'headers' => array_reverse($range['headers'] ?? [])]); + + case 'block': + $params = ['fill_pow_hash' => true]; + if (isset($input['height']) && is_numeric($input['height'])) { + $params['height'] = max(0, (int)$input['height']); + } elseif (validHash($input['hash'] ?? '')) { + $params['hash'] = strtolower($input['hash']); + } else { + respond(['error' => 'Valid height or block hash required'], 400); + } + respond(jsonRpc('get_block', $params)); + + case 'transaction': + $hash = strtolower((string)($input['hash'] ?? '')); + if (!validHash($hash)) respond(['error' => 'Valid transaction hash required'], 400); + $result = rpc('/get_transactions', [ + 'txs_hashes' => [$hash], + 'decode_as_json' => true, + 'prune' => false, + 'split' => true, + ]); + if (empty($result['txs']) && empty($result['txs_as_json'])) respond(['error' => 'Transaction not found'], 404); + respond($result); + + case 'pool': + respond(rpc('/get_transaction_pool', [])); + + default: + respond(['error' => 'Unknown action'], 400); + } +} catch (Throwable $error) { + error_log('Monzero explorer API: ' . $error->getMessage()); + respond(['error' => 'Node request failed'], 502); +} + +function validHash(mixed $value): bool { + return is_string($value) && preg_match('/^[0-9a-fA-F]{64}$/', $value) === 1; +} + +function jsonRpc(string $method, array $params): array { + $response = rpc('/json_rpc', [ + 'jsonrpc' => '2.0', + 'id' => '0', + 'method' => $method, + 'params' => $params, + ]); + if (isset($response['error'])) throw new RuntimeException($response['error']['message'] ?? 'RPC error'); + return $response['result'] ?? []; +} + +function rpc(string $path, array $payload): array { + // Daemon endpoints expect an empty JSON object, not an empty JSON array. + $json = $payload === [] ? '{}' : json_encode($payload, JSON_THROW_ON_ERROR); + if (function_exists('curl_init')) { + $curl = curl_init(NODE_RPC . $path); + curl_setopt_array($curl, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $json, + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => 3, + CURLOPT_TIMEOUT => 12, + ]); + $body = curl_exec($curl); + $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); + $message = curl_error($curl); + curl_close($curl); + if ($body === false || $status !== 200) throw new RuntimeException($message ?: 'HTTP ' . $status); + } else { + $context = stream_context_create(['http' => [ + 'method' => 'POST', + 'header' => "Content-Type: application/json\r\nConnection: close\r\n", + 'content' => $json, + 'timeout' => 12, + 'ignore_errors' => true, + ]]); + $body = @file_get_contents(NODE_RPC . $path, false, $context); + $statusLine = $http_response_header[0] ?? ''; + preg_match('/\s(\d{3})\s/', $statusLine, $match); + $status = isset($match[1]) ? (int)$match[1] : 0; + if ($body === false || $status !== 200) throw new RuntimeException('HTTP ' . $status); + } + $decoded = json_decode($body, true, 512, JSON_THROW_ON_ERROR); + return is_array($decoded) ? $decoded : []; +} + +function respond(array $data, int $status = 200): never { + http_response_code($status); + echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE); + exit; +} diff --git a/website/explorer/app.js b/website/explorer/app.js new file mode 100644 index 000000000..01381e61e --- /dev/null +++ b/website/explorer/app.js @@ -0,0 +1,145 @@ +const $ = (selector) => document.querySelector(selector); +const view = $('#view'); +const content = $('#content'); +const number = new Intl.NumberFormat('en-GB'); +const atomic = 100_000_000_000; +let chainInfo = null; + +const escapeHtml = (value) => String(value ?? '').replace(/[&<>'"]/g, (char) => ({'&':'&','<':'<','>':'>',"'":''','"':'"'}[char])); +const shortHash = (hash, size = 12) => hash ? `${hash.slice(0, size)}…${hash.slice(-8)}` : '—'; +const xmz = (value) => `${(Number(value || 0) / atomic).toLocaleString('en-GB', {maximumFractionDigits: 11})} XMZ`; +const date = (timestamp) => timestamp ? new Date(timestamp * 1000).toLocaleString() : '—'; +const age = (timestamp) => { + const seconds = Math.max(0, Math.floor(Date.now() / 1000 - timestamp)); + if (seconds < 60) return `${seconds}s ago`; + if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; + if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; + return `${Math.floor(seconds / 86400)}d ago`; +}; + +async function api(action, data = {}) { + const response = await fetch('api.php', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({action, ...data})}); + const result = await response.json().catch(() => ({})); + if (!response.ok || result.error) throw new Error(result.error || `HTTP ${response.status}`); + return result; +} + +function loading(label = 'Loading chain data…') { view.innerHTML = `<div class="loading"><span></span>${escapeHtml(label)}</div>`; } +function showError(message = '') { + const fragment = $('#error-template').content.cloneNode(true); + if (message) fragment.querySelector('p').textContent = message; + view.replaceChildren(fragment); +} + +function updateStats(info) { + chainInfo = info; + $('#stat-height').textContent = number.format(info.height || 0); + $('#stat-difficulty').textContent = number.format(info.difficulty || 0); + $('#stat-hashrate').textContent = `${number.format(Math.round((info.difficulty || 0) / (info.target || 120)))} H/s estimated`; + $('#stat-transactions').textContent = number.format(info.tx_count || 0); + $('#stat-connections').textContent = info.restricted + ? 'HIDDEN' + : number.format((info.incoming_connections_count || 0) + (info.outgoing_connections_count || 0)); + $('#stat-connections').title = info.restricted + ? 'Connection counts are intentionally hidden by the public restricted RPC.' + : 'Current incoming and outgoing P2P connections.'; + $('#node-label').textContent = info.status === 'OK' ? 'Node online' : info.status; + $('#node-dot').className = info.status === 'OK' ? 'online' : 'offline'; +} + +function headerTitle(kicker, title) { + $('.section-title').innerHTML = `<div><p class="eyebrow">${escapeHtml(kicker)}</p><h2>${title}</h2></div><a class="back" href="#/">← Recent blocks</a>`; +} + +async function showBlocks() { + loading(); + $('.section-title').innerHTML = `<div><p class="eyebrow">Live ledger</p><h2>Recent blocks</h2></div><button id="refresh" class="ghost" type="button">Refresh</button>`; + try { + const result = await api('blocks', {limit: 15}); + updateStats(result.info); + view.innerHTML = `<div class="table-wrap"><table><thead><tr><th>Height</th><th>Age</th><th>Block hash</th><th>Transactions</th><th>Difficulty</th><th>Reward</th><th>Size</th></tr></thead><tbody>${result.headers.map((block) => ` + <tr><td><a class="link" href="#/block/${block.height}">${number.format(block.height)}</a></td><td class="muted">${age(block.timestamp)}</td><td><a class="link hash truncate" title="${block.hash}" href="#/block/${block.hash}">${shortHash(block.hash)}</a></td><td>${number.format(block.num_txes || 0)}</td><td class="muted">${number.format(block.difficulty || 0)}</td><td class="reward">${xmz(block.reward)}</td><td class="muted">${number.format(block.block_size || block.block_weight || 0)} B</td></tr>`).join('')}</tbody></table></div>`; + $('#refresh').addEventListener('click', showBlocks); + } catch (error) { showError(error.message); setOffline(); } +} + +async function showBlock(value) { + loading('Loading block…'); + try { + const input = /^\d+$/.test(value) ? {height:Number(value)} : {hash:value}; + const block = await api('block', input); + const h = block.block_header || {}; + headerTitle('Block', `#${number.format(h.height ?? input.height)}`); + const txs = block.tx_hashes || []; + view.innerHTML = `<div class="detail-grid"> + ${datum('Block hash', `<code>${escapeHtml(h.hash)}</code>`)} + ${datum('Timestamp', `<strong>${escapeHtml(date(h.timestamp))}</strong>`)} + ${datum('Age', `<strong>${escapeHtml(age(h.timestamp))}</strong>`)} + ${datum('Reward', `<strong class="big">${xmz(h.reward)}</strong>`)} + ${datum('Difficulty', `<strong>${number.format(h.difficulty || 0)}</strong>`)} + ${datum('Transactions', `<strong>${number.format(h.num_txes || txs.length)}</strong>`)} + ${datum('Block size', `<strong>${number.format(h.block_size || h.block_weight || 0)} bytes</strong>`)} + ${datum('Nonce', `<strong>${number.format(h.nonce || 0)}</strong>`)} + ${datum('Version', `<strong>v${h.major_version ?? '—'}.${h.minor_version ?? '—'}</strong>`)} + ${datum('Previous block', h.prev_hash ? `<a class="link hash truncate" href="#/block/${h.prev_hash}">${shortHash(h.prev_hash)}</a>` : '<strong>Genesis</strong>')} + ${datum('Proof-of-work hash', `<code>${escapeHtml(h.pow_hash || 'Not requested')}</code>`)} + ${datum('Confirmations', `<strong>${chainInfo ? number.format(Math.max(0, chainInfo.height - h.height)) : '—'}</strong>`)} + </div> + <h3 class="subheading">Transactions in this block</h3> + ${txs.length ? `<div class="table-wrap"><table><thead><tr><th>#</th><th>Transaction hash</th></tr></thead><tbody>${txs.map((hash, i) => `<tr><td class="muted">${i + 1}</td><td><a class="link hash" href="#/tx/${hash}">${hash}</a></td></tr>`).join('')}</tbody></table></div>` : '<div class="empty"><strong>Coinbase only</strong><p>This block contains no regular transactions.</p></div>'} + <details class="raw"><summary>Raw block JSON</summary><pre>${escapeHtml(JSON.stringify(block, null, 2))}</pre></details>`; + } catch (error) { await tryTransaction(value, error); } +} + +async function tryTransaction(value, originalError) { + if (!/^[0-9a-f]{64}$/i.test(value)) return showError(originalError.message); + try { await showTransaction(value); } catch { showError('No block or transaction matched this hash.'); } +} + +async function showTransaction(hash) { + loading('Loading transaction…'); + const result = await api('transaction', {hash}); + const tx = result.txs?.[0] || {}; + let decoded = {}; + try { decoded = JSON.parse(tx.as_json || result.txs_as_json?.[0] || '{}'); } catch {} + headerTitle('Transaction', `<span class="truncate" title="${escapeHtml(hash)}">${escapeHtml(shortHash(hash, 16))}</span>`); + const vin = decoded.vin || []; + const vout = decoded.vout || []; + view.innerHTML = `<div class="detail-grid"> + ${datum('Transaction hash', `<code>${escapeHtml(hash)}</code>`)} + ${datum('Block height', tx.block_height != null ? `<a class="link" href="#/block/${tx.block_height}">${number.format(tx.block_height)}</a>` : '<strong>Unconfirmed</strong>')} + ${datum('Confirmations', `<strong>${number.format(tx.confirmations || 0)}</strong>`)} + ${datum('Fee', `<strong class="orange">${xmz(tx.fee || decoded.rct_signatures?.txnFee || 0)}</strong>`)} + ${datum('Size', `<strong>${number.format(tx.size || tx.weight || 0)} bytes</strong>`)} + ${datum('Version', `<strong>${decoded.version ?? '—'}</strong>`)} + ${datum('Inputs', `<strong>${number.format(vin.length)}</strong>`)} + ${datum('Outputs', `<strong>${number.format(vout.length)}</strong>`)} + ${datum('In pool', `<strong>${tx.in_pool ? 'Yes' : 'No'}</strong>`)} + </div><details class="raw" open><summary>Decoded transaction JSON</summary><pre>${escapeHtml(JSON.stringify(decoded, null, 2))}</pre></details>`; +} + +function datum(label, value) { return `<div class="datum"><span>${escapeHtml(label)}</span>${value}</div>`; } +function setOffline() { $('#node-dot').className = 'offline'; $('#node-label').textContent = 'Node unavailable'; } + +async function route() { + const route = location.hash.replace(/^#\/?/, '').split('/').filter(Boolean); + if (location.hash) content.scrollIntoView({behavior:'smooth', block:'start'}); + if (!route.length) return showBlocks(); + if (route[0] === 'block' && route[1]) return showBlock(route[1]); + if (route[0] === 'tx' && route[1]) { + try { return await showTransaction(route[1]); } catch (error) { return showError(error.message); } + } + showError(); +} + +$('#search-form').addEventListener('submit', (event) => { + event.preventDefault(); + const value = $('#search-input').value.trim(); + if (/^\d+$/.test(value) || /^[0-9a-f]{64}$/i.test(value)) location.hash = `#/block/${value}`; + else showError('Enter a numeric block height or a 64-character hexadecimal hash.'); +}); +$('#latest-search').addEventListener('click', () => { if (chainInfo?.height) location.hash = `#/block/${chainInfo.height - 1}`; }); +window.addEventListener('hashchange', route); +api('info').then(updateStats).catch(setOffline); +route(); +setInterval(() => api('info').then(updateStats).catch(setOffline), 30000); diff --git a/website/explorer/index.html b/website/explorer/index.html new file mode 100644 index 000000000..aa2c2fe20 --- /dev/null +++ b/website/explorer/index.html @@ -0,0 +1,63 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="description" content="Monzero blockchain explorer"> + <meta name="theme-color" content="#090d0c"> + <title>Monzero Explorer</title> + <link rel="icon" href="../assets/monzero-xmz-coin.png"> + <link rel="stylesheet" href="styles.css"> +</head> +<body> + <header> + <a class="brand" href="./"> + <span class="mark">MZ</span> + <span><strong>MONZERO</strong><small>CHAIN EXPLORER</small></span> + </a> + <div class="header-actions"> + <a class="site-link" href="../">Monzero home</a> + <div class="node-pill"><span id="node-dot"></span><span id="node-label">Connecting</span></div> + </div> + </header> + + <main> + <section class="hero"> + <p class="eyebrow">Independent chain · XMZ</p> + <h1>Verify the chain.<br><em>Block by block.</em></h1> + <form id="search-form" class="search"> + <label class="sr-only" for="search-input">Search the blockchain</label> + <input id="search-input" autocomplete="off" spellcheck="false" placeholder="Block height, block hash, or transaction hash"> + <button type="submit">Search <span>↗</span></button> + </form> + <p class="search-hint">Try a block height such as <button type="button" id="latest-search">latest</button> or paste a 64-character hash.</p> + </section> + + <section id="overview" class="overview" aria-label="Network overview"> + <article><span>Chain height</span><strong id="stat-height">—</strong><small>Mainnet</small></article> + <article><span>Difficulty</span><strong id="stat-difficulty">—</strong><small id="stat-hashrate">— H/s estimated</small></article> + <article><span>Transactions</span><strong id="stat-transactions">—</strong><small>Confirmed on-chain</small></article> + <article><span>Connections</span><strong id="stat-connections">—</strong><small>Hidden on restricted RPC</small></article> + </section> + + <section id="content" class="content" aria-live="polite"> + <div class="section-title"> + <div><p class="eyebrow">Live ledger</p><h2>Recent blocks</h2></div> + <button id="refresh" class="ghost" type="button">Refresh</button> + </div> + <div id="view"><div class="loading"><span></span>Loading chain data…</div></div> + </section> + </main> + + <footer> + <span>Monzero Genesis network</span> + <span>Node: <code>node.monzero.org:6175</code></span> + <span>Read-only explorer · <a href="https://monzero.org">monzero.org</a></span> + </footer> + + <template id="error-template"> + <div class="empty"><strong>Nothing found</strong><p>The value could not be resolved on the current Monzero chain.</p><a href="#/">Return to recent blocks</a></div> + </template> + <script src="app.js"></script> +</body> +</html> diff --git a/website/explorer/styles.css b/website/explorer/styles.css new file mode 100644 index 000000000..7961df623 --- /dev/null +++ b/website/explorer/styles.css @@ -0,0 +1,76 @@ +@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;500;600;700&display=swap'); + +:root { --bg:#090d0c; --panel:#0e1513; --panel2:#111a17; --line:#23312d; --text:#ecf5f1; --muted:#789087; --green:#b9ff66; --mint:#67e7b1; --orange:#ed8547; --mono:"DM Mono",monospace; --sans:"Manrope",sans-serif; } +* { box-sizing:border-box; } +html { scroll-behavior:smooth; } +body { margin:0; background:var(--bg); color:var(--text); font-family:var(--sans); min-height:100vh; } +body::before { content:""; position:fixed; inset:0; pointer-events:none; opacity:.16; background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='4'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.12'/%3E%3C/svg%3E"); z-index:10; } +a { color:inherit; } +button,input { font:inherit; } +header { height:78px; display:flex; align-items:center; justify-content:space-between; padding:0 clamp(1.2rem,5vw,5rem); border-bottom:1px solid var(--line); } +.brand { display:flex; gap:.75rem; align-items:center; color:inherit; text-decoration:none; } +.mark { width:34px; height:34px; display:grid; place-items:center; border:1px solid var(--green); border-radius:50%; color:var(--green); font:500 .65rem var(--mono); } +.brand > span:last-child { display:flex; flex-direction:column; line-height:1.15; letter-spacing:.16em; font-size:.75rem; } +.brand small { color:var(--muted); font:400 .48rem var(--mono); margin-top:.22rem; } +.node-pill { display:flex; align-items:center; gap:.6rem; border:1px solid var(--line); border-radius:2rem; padding:.5rem .8rem; color:var(--muted); font:400 .6rem var(--mono); text-transform:uppercase; letter-spacing:.08em; } +.header-actions { display:flex; align-items:center; gap:1rem; } +.site-link { color:var(--mint); text-decoration:none; font:500 .6rem var(--mono); text-transform:uppercase; letter-spacing:.08em; } +.site-link:hover { text-decoration:underline; } +#node-dot { width:7px; height:7px; border-radius:50%; background:#e7b358; box-shadow:0 0 10px currentColor; } +#node-dot.online { background:var(--green); } +#node-dot.offline { background:#ee6c57; } +main { width:min(1480px,100%); margin:0 auto; } +.hero { padding:clamp(4rem,8vw,7rem) clamp(1.2rem,5vw,5rem) clamp(3rem,6vw,5rem); border-bottom:1px solid var(--line); position:relative; overflow:hidden; } +.hero::after { content:""; width:560px; height:560px; border:1px solid rgba(185,255,102,.12); border-radius:50%; position:absolute; right:-160px; top:-270px; box-shadow:0 0 100px rgba(103,231,177,.05); } +.eyebrow { color:var(--mint); font:500 .6rem var(--mono); letter-spacing:.18em; text-transform:uppercase; margin:0 0 1rem; } +h1 { margin:0 0 2.5rem; font-size:clamp(2.7rem,6vw,6rem); line-height:.96; letter-spacing:-.06em; font-weight:500; } +h1 em { color:var(--green); font-style:normal; } +.search { max-width:970px; display:grid; grid-template-columns:1fr auto; border:1px solid #344940; position:relative; z-index:1; background:rgba(11,17,15,.8); } +.search:focus-within { border-color:var(--mint); box-shadow:0 0 0 3px rgba(103,231,177,.08); } +.search input { min-width:0; height:62px; border:0; outline:0; background:transparent; color:var(--text); padding:0 1.2rem; font:400 .78rem var(--mono); } +.search button { border:0; margin:5px; padding:0 1.35rem; background:var(--green); color:#08100d; font-weight:700; cursor:pointer; } +.search button span { margin-left:.7rem; } +.search-hint { color:#566c64; font:400 .58rem var(--mono); } +.search-hint button { color:var(--mint); background:none; border:0; padding:0; cursor:pointer; text-decoration:underline; } +.overview { display:grid; grid-template-columns:repeat(4,1fr); padding:0 clamp(1.2rem,5vw,5rem); border-bottom:1px solid var(--line); } +.overview article { min-height:135px; display:flex; flex-direction:column; justify-content:center; padding:1.2rem 1.7rem; border-right:1px solid var(--line); } +.overview article:first-child { border-left:1px solid var(--line); } +.overview span { color:var(--muted); text-transform:uppercase; letter-spacing:.1em; font:400 .55rem var(--mono); } +.overview strong { color:var(--green); font:500 clamp(1.35rem,2.4vw,2.2rem) var(--mono); margin:.35rem 0; } +.overview small { color:#53675f; font:400 .55rem var(--mono); } +.content { padding:clamp(3.5rem,6vw,6rem) clamp(1.2rem,5vw,5rem); min-height:560px; } +.section-title { display:flex; align-items:end; justify-content:space-between; margin-bottom:1.8rem; } +h2 { margin:0; font-size:clamp(2rem,4vw,3.8rem); letter-spacing:-.05em; line-height:1; font-weight:500; } +.ghost { color:var(--mint); border:1px solid var(--line); background:transparent; padding:.6rem .9rem; font:500 .6rem var(--mono); cursor:pointer; } +.ghost:hover { border-color:var(--mint); } +.table-wrap { border:1px solid var(--line); overflow-x:auto; } +table { width:100%; border-collapse:collapse; min-width:800px; } +th { padding:.85rem 1rem; text-align:left; color:#566c64; text-transform:uppercase; letter-spacing:.1em; font:400 .52rem var(--mono); border-bottom:1px solid var(--line); } +td { padding:1rem; border-bottom:1px solid rgba(35,49,45,.65); font-size:.75rem; } +tbody tr:last-child td { border-bottom:0; } +tbody tr { transition:background .15s; } tbody tr:hover { background:rgba(185,255,102,.025); } +td code,.hash { font:400 .67rem var(--mono); } +.link { color:var(--mint); text-decoration:none; }.link:hover { text-decoration:underline; } +.truncate { display:inline-block; max-width:240px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; vertical-align:bottom; } +.muted { color:var(--muted); }.reward { color:var(--green); }.orange { color:var(--orange); } +.detail-head { display:flex; justify-content:space-between; align-items:flex-end; gap:2rem; margin-bottom:1.5rem; } +.detail-head h2 { overflow-wrap:anywhere; }.back { color:var(--mint); font:500 .6rem var(--mono); text-decoration:none; } +.detail-grid { display:grid; grid-template-columns:repeat(3,1fr); border:1px solid var(--line); margin-bottom:2rem; } +.datum { min-height:105px; padding:1.2rem; border-right:1px solid var(--line); border-bottom:1px solid var(--line); display:flex; flex-direction:column; justify-content:space-between; min-width:0; } +.datum:nth-child(3n) { border-right:0; } +.datum span { color:var(--muted); text-transform:uppercase; font:400 .52rem var(--mono); letter-spacing:.1em; } +.datum strong,.datum code { color:var(--text); font:500 .72rem var(--mono); overflow-wrap:anywhere; } +.datum strong.big { font-size:1.15rem; color:var(--green); } +.subheading { font-size:1rem; margin:2.5rem 0 1rem; } +.raw { margin-top:2rem; border:1px solid var(--line); } +.raw summary { cursor:pointer; padding:1rem; color:var(--muted); font:400 .62rem var(--mono); } +.raw pre { padding:1rem; margin:0; border-top:1px solid var(--line); overflow:auto; color:#8da39a; font:400 .6rem/1.7 var(--mono); } +.loading,.empty { min-height:300px; display:flex; flex-direction:column; align-items:center; justify-content:center; color:var(--muted); border:1px solid var(--line); text-align:center; } +.loading span { width:24px; height:24px; border:2px solid var(--line); border-top-color:var(--green); border-radius:50%; animation:spin .7s linear infinite; margin-bottom:1rem; } +@keyframes spin { to { transform:rotate(360deg); } } +.empty strong { color:var(--text); font-size:1.2rem; }.empty p { max-width:500px; }.empty a { color:var(--mint); } +footer { width:min(1480px,100%); margin:0 auto; border-top:1px solid var(--line); padding:2rem clamp(1.2rem,5vw,5rem); display:flex; justify-content:space-between; gap:1rem; color:#52665e; font:400 .55rem var(--mono); } +.sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; } +@media(max-width:850px){ .overview{grid-template-columns:1fr 1fr}.overview article:nth-child(odd){border-left:1px solid var(--line)}.detail-grid{grid-template-columns:1fr 1fr}.datum:nth-child(3n){border-right:1px solid var(--line)}.datum:nth-child(2n){border-right:0}footer{flex-direction:column}.hero::after{display:none} } +@media(max-width:520px){ header{padding-inline:1rem}.brand small,.site-link{display:none}.node-pill{border:0;padding:0}.overview{grid-template-columns:1fr 1fr;padding:0}.overview article{padding:1rem;min-height:110px}.search{grid-template-columns:1fr}.search button{height:48px}.detail-grid{grid-template-columns:1fr}.datum,.datum:nth-child(3n){border-right:0}.detail-head{align-items:flex-start;flex-direction:column-reverse} } +@media(prefers-reduced-motion:reduce){*{animation-duration:.01ms!important;scroll-behavior:auto!important}} diff --git a/website/index.html b/website/index.html new file mode 100644 index 000000000..d9503f5bc --- /dev/null +++ b/website/index.html @@ -0,0 +1,210 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="description" content="Monzero (XMZ) is an experimental, community-run privacy coin network powered by RandomX."> + <meta name="theme-color" content="#07110f"> + <title>Monzero — Private by design. Open by nature.</title> + <link rel="icon" href="assets/monzero-xmz-coin.png"> + <link rel="stylesheet" href="styles.css"> +</head> +<body> + <a class="skip-link" href="#main">Skip to content</a> + + <header class="site-header"> + <a class="brand" href="#top" aria-label="Monzero home"> + <span class="brand-mark">MZ</span> + <span>MONZERO</span> + </a> + <button class="menu-toggle" type="button" aria-expanded="false" aria-controls="site-nav">Menu</button> + <nav id="site-nav" aria-label="Primary navigation"> + <a href="#network">Network</a> + <a href="#miners">Miners</a> + <a href="explorer/">Explorer</a> + <a href="#about">About</a> + <a href="#mine">Mine</a> + <a href="#faq">FAQ</a> + <a class="nav-download" href="#download">Download</a> + </nav> + </header> + + <main id="main"> + <section class="hero" id="top"> + <div class="hero-copy"> + <p class="eyebrow"><span class="pulse"></span> Genesis network · Public node online</p> + <h1>Privacy should be <em>ordinary.</em></h1> + <p class="hero-lead">Monzero is an independent, community-run digital currency built for private transactions, open participation, and CPU-friendly mining.</p> + <div class="hero-actions"> + <a class="button primary" href="#download">Get Monzero</a> + <a class="button secondary" href="explorer/">Explore the chain <span aria-hidden="true">↗</span></a> + </div> + <div class="hero-proof" aria-label="Core network properties"> + <span>RandomX PoW</span> + <span>2-minute blocks</span> + <span>No premine</span> + </div> + </div> + <div class="coin-stage" aria-label="Monzero XMZ coin artwork"> + <div class="orbit orbit-one"></div> + <div class="orbit orbit-two"></div> + <div class="coin-glow"></div> + <img src="assets/monzero-xmz-coin.png" alt="Monzero XMZ coin" width="627" height="627"> + <span class="coordinate coordinate-a">86 / 87 / 88</span> + <span class="coordinate coordinate-b">XMZ · MAINNET</span> + </div> + </section> + + <section class="ticker" aria-label="Monzero network summary"> + <div><span>Asset</span><strong>XMZ</strong></div> + <div><span>Target supply</span><strong>100M</strong></div> + <div><span>Block target</span><strong>120 SEC</strong></div> + <div><span>Precision</span><strong>11 DECIMALS</strong></div> + </section> + + <section class="network section" id="network"> + <div class="section-heading"> + <p class="eyebrow">Live network</p> + <h2>A chain you can verify.</h2> + <p>The official public node gives every wallet and miner a stable entry point into the Monzero network.</p> + </div> + <div class="status-panel"> + <div class="status-topline"> + <div><span id="node-dot" class="status-dot"></span><strong id="node-state">Checking node</strong></div> + <code>node.monzero.org</code> + </div> + <div class="metrics"> + <article><span>Block height</span><strong id="metric-height">—</strong></article> + <article><span>Network difficulty</span><strong id="metric-difficulty">—</strong></article> + <article><span>Peer nodes</span><strong id="metric-connections">—</strong></article> + <article><span>RPC status</span><strong id="metric-rpc">—</strong></article> + </div> + <div class="node-addresses"> + <p><span>P2P</span><code>node.monzero.org:6174</code><button class="copy" data-copy="node.monzero.org:6174">Copy</button></p> + <p><span>Restricted RPC</span><code>node.monzero.org:6175</code><button class="copy" data-copy="node.monzero.org:6175">Copy</button></p> + </div> + <p><a class="button secondary" href="explorer/">Open block explorer <span aria-hidden="true">↗</span></a></p> + </div> + </section> + + <section class="miners section" id="miners"> + <div class="section-heading"> + <p class="eyebrow">Mining network</p> + <h2>Anonymous miner ranking.</h2> + <p>Live, opt-in statistics from participating miners. No wallet address, IP address, hostname, or hardware identifier is displayed.</p> + </div> + <div class="miner-panel"> + <div class="miner-summary"> + <article><span>Active miners</span><strong id="miner-active">—</strong></article> + <article><span>Reported hash rate</span><strong id="miner-hashrate">—</strong></article> + <article><span>Blocks found</span><strong id="miner-blocks">—</strong></article> + <article><span>Last update</span><strong id="miner-updated">—</strong></article> + </div> + <div class="table-scroll"> + <table class="miner-table"> + <thead><tr><th>Rank</th><th>Anonymous miner</th><th>Speed</th><th>Blocks found</th><th>Status</th></tr></thead> + <tbody id="miner-rows"><tr><td colspan="5" class="table-message">Loading miner statistics…</td></tr></tbody> + </table> + </div> + <div class="chart-grid" aria-label="Live mining charts"> + <article class="chart-card"> + <div><span>Reported hash rate</span><strong id="chart-hashrate-value">—</strong></div> + <svg id="chart-hashrate" viewBox="0 0 600 180" role="img" aria-label="Reported hash rate over time"><path class="chart-gridline" d="M0 45H600M0 90H600M0 135H600"></path><polyline class="chart-line" points=""></polyline></svg> + <small>Rolling browser history · H/s</small> + </article> + <article class="chart-card"> + <div><span>Network difficulty</span><strong id="chart-difficulty-value">—</strong></div> + <svg id="chart-difficulty" viewBox="0 0 600 180" role="img" aria-label="Network difficulty over time"><path class="chart-gridline" d="M0 45H600M0 90H600M0 135H600"></path><polyline class="chart-line difficulty" points=""></polyline></svg> + <small>Rolling browser history · network difficulty</small> + </article> + </div> + <p class="telemetry-note">Statistics are self-reported and miners disappear after three minutes without a heartbeat. A random installation ID is pseudonymized by the server.</p> + </div> + </section> + + <section class="principles section" id="about"> + <div class="section-heading compact"> + <p class="eyebrow">Why Monzero</p> + <h2>Built around participation.</h2> + </div> + <div class="principle-grid"> + <article> + <span class="card-number">01</span> + <h3>Private transactions</h3> + <p>Monzero inherits established privacy-focused transaction technology, including ring signatures, stealth addresses, and confidential amounts.</p> + </article> + <article> + <span class="card-number">02</span> + <h3>Consumer hardware</h3> + <p>RandomX proof of work is designed for general-purpose CPUs, keeping participation open to ordinary computers.</p> + </article> + <article> + <span class="card-number">03</span> + <h3>Independent chain</h3> + <p>XMZ has its own genesis block, network identity, address prefixes, ports, and monetary policy. It is not Monero.</p> + </article> + </div> + </section> + + <section class="mine section" id="mine"> + <div class="mine-copy"> + <p class="eyebrow">Join the network</p> + <h2>Turn spare CPU time into network security.</h2> + <p>Run your own daemon, create a fresh Monzero wallet, and mine directly to an address you control. Solo mining rewards the machine that finds a block; rewards unlock after 60 blocks.</p> + <div class="warning"><strong>Keep it separate.</strong> Never reuse a Monero seed, keys, or wallet file with Monzero.</div> + </div> + <ol class="steps"> + <li><span>1</span><div><strong>Start the node</strong><code>./start-node.sh</code></div></li> + <li><span>2</span><div><strong>Start the separate miner</strong><code>./start-mining.sh YOUR_ADDRESS 1</code></div></li> + <li><span>3</span><div><strong>Open the wallet when needed</strong><code>./open-wallet.sh /path/to/wallet</code></div></li> + </ol> + </section> + + <section class="download section" id="download"> + <div class="download-card"> + <div> + <p class="eyebrow">Genesis pre2</p> + <h2>Download Monzero</h2> + <p>Private pre-release builds for Windows and Linux x86-64. Both include the node, command-line wallet, separate mining controls, checksums, and setup guidance. The Windows package also includes the graphical wallet.</p> + <p class="release-note">Experimental and unaudited software for the private pre-release Monzero network.</p> + </div> + <div class="download-action"> + <div class="release-download"> + <a class="button primary wide" href="downloads/monzero-genesis-pre2-windows-x64.zip" download>Windows x64 <span>57 MB · ZIP</span></a> + <a class="checksum-link" href="downloads/monzero-genesis-pre2-windows-x64.zip.sha256" download>Windows SHA-256 checksum</a> + <code class="hash">784902a16ceb85722327dc800fb181d56feb180609b4df575b1f777ee475f3f2</code> + </div> + <div class="release-download"> + <a class="button secondary wide" href="downloads/monzero-genesis-pre2-linux-x86_64.tar.gz" download>Linux x86-64 <span>13 MB · TAR.GZ</span></a> + <a class="checksum-link" href="downloads/monzero-genesis-pre2-linux-x86_64.tar.gz.sha256" download>Linux SHA-256 checksum</a> + <code class="hash">baf75989bea783ec0488978ee3e2218b1bc87f9d44827019cf7e96b370dddef8</code> + </div> + <p class="unsigned-note">Windows binaries are currently unsigned. Windows SmartScreen may show an unknown-publisher warning; verify the checksum before running them.</p> + </div> + </div> + </section> + + <section class="faq section" id="faq"> + <div class="section-heading compact"> + <p class="eyebrow">Questions</p> + <h2>Before you begin.</h2> + </div> + <div class="faq-list"> + <details><summary>Why is my mining balance still zero?</summary><p>Monzero currently uses solo mining. You receive a coinbase reward only when your machine finds a block; simply hashing does not create a gradual balance. A found reward remains locked for 60 blocks.</p></details> + <details><summary>Is Monzero the same network as Monero?</summary><p>No. Monzero is an independent experimental chain with a different genesis block, network ID, ports, address prefixes, ticker, and monetary policy.</p></details> + <details><summary>Can I reuse my existing seed?</summary><p>No. Create a new wallet and seed exclusively for Monzero. Reusing keys across forks can seriously damage privacy.</p></details> + <details><summary>Is this a production release?</summary><p>Not yet. Genesis pre2 is private pre-release software. Consensus and cryptographic changes have not received an independent audit.</p></details> + </div> + </section> + </main> + + <footer> + <a class="brand" href="#top"><span class="brand-mark">MZ</span><span>MONZERO</span></a> + <p>Experimental software. Verify everything.</p> + <p>© <span id="year"></span> Monzero contributors · <a href="https://monzero.org">monzero.org</a></p> + </footer> + + <div class="toast" role="status" aria-live="polite">Copied to clipboard</div> + <script src="app.js?v=20260815-2"></script> +</body> +</html> diff --git a/website/nginx-monzero.conf b/website/nginx-monzero.conf new file mode 100644 index 000000000..4e69577c4 --- /dev/null +++ b/website/nginx-monzero.conf @@ -0,0 +1,33 @@ +server { + listen 80; + listen [::]:80; + server_name monzero.org www.monzero.org; + + root /var/www/monzero; + index index.html; + + location / { + try_files $uri $uri/ =404; + } + + # Same-origin, read-only node status endpoint used by the homepage. + location = /api/node-info { + limit_except POST { deny all; } + proxy_pass http://127.0.0.1:6175/get_info; + proxy_set_header Content-Type application/json; + proxy_pass_request_headers on; + proxy_connect_timeout 2s; + proxy_read_timeout 5s; + add_header Cache-Control "no-store" always; + } + + location ~* \.(?:css|js|png|ico|svg|tar\.gz|sha256)$ { + expires 7d; + add_header Cache-Control "public, max-age=604800"; + try_files $uri =404; + } + + add_header X-Content-Type-Options nosniff always; + add_header Referrer-Policy strict-origin-when-cross-origin always; + add_header X-Frame-Options DENY always; +} diff --git a/website/styles.css b/website/styles.css new file mode 100644 index 000000000..d346d555f --- /dev/null +++ b/website/styles.css @@ -0,0 +1,187 @@ +@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;500;600;700&display=swap'); + +:root { + --ink: #eaf7f2; + --muted: #91a7a0; + --bg: #06100e; + --surface: #0b1815; + --line: rgba(199, 255, 228, .13); + --lime: #b9ff66; + --mint: #62e7af; + --copper: #d78755; + --mono: "DM Mono", monospace; + --sans: "Manrope", sans-serif; +} + +* { box-sizing: border-box; } +html { scroll-behavior: smooth; } +body { margin: 0; color: var(--ink); background: var(--bg); font-family: var(--sans); line-height: 1.6; overflow-x: hidden; } +body::before { content: ""; position: fixed; inset: 0; pointer-events: none; opacity: .22; z-index: 10; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.12'/%3E%3C/svg%3E"); } +a { color: inherit; } +button, a { -webkit-tap-highlight-color: transparent; } +.skip-link { position: fixed; left: 1rem; top: -5rem; background: var(--lime); color: #07110f; padding: .7rem 1rem; z-index: 100; } +.skip-link:focus { top: 1rem; } + +.site-header { height: 84px; padding: 0 clamp(1.2rem, 4vw, 4.5rem); display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--line); position: absolute; width: 100%; z-index: 5; } +.brand { text-decoration: none; display: inline-flex; gap: .75rem; align-items: center; font-size: .86rem; letter-spacing: .19em; font-weight: 700; } +.brand-mark { width: 32px; height: 32px; display: grid; place-items: center; border: 1px solid var(--lime); border-radius: 50%; color: var(--lime); font: 500 .66rem var(--mono); letter-spacing: 0; } +nav { display: flex; align-items: center; gap: clamp(1.1rem, 2.5vw, 2.4rem); } +nav a { color: #b6c7c1; text-decoration: none; font-size: .83rem; transition: color .2s; } +nav a:hover { color: var(--ink); } +.nav-download { border: 1px solid var(--line); border-radius: 2rem; padding: .55rem 1rem; } +.menu-toggle { display: none; color: var(--ink); background: none; border: 1px solid var(--line); padding: .5rem .8rem; } + +.hero { min-height: 800px; height: min(92vh, 980px); display: grid; grid-template-columns: minmax(0, 1fr) minmax(360px, .9fr); align-items: center; gap: 2rem; padding: 8rem clamp(1.2rem, 7vw, 8rem) 4rem; position: relative; isolation: isolate; background: radial-gradient(circle at 80% 40%, rgba(40, 144, 102, .15), transparent 34%), linear-gradient(105deg, #071310 0%, #06100e 58%, #081310 100%); } +.hero::after { content: ""; position: absolute; inset: 0; z-index: -1; opacity: .2; background-image: linear-gradient(var(--line) 1px, transparent 1px), linear-gradient(90deg, var(--line) 1px, transparent 1px); background-size: 72px 72px; mask-image: linear-gradient(to right, #000, transparent 80%); } +.hero-copy { max-width: 720px; } +.eyebrow { margin: 0 0 1.2rem; color: var(--mint); font: 500 .68rem var(--mono); letter-spacing: .17em; text-transform: uppercase; } +.pulse { display: inline-block; width: 7px; height: 7px; background: var(--lime); border-radius: 50%; margin-right: .55rem; box-shadow: 0 0 0 5px rgba(185, 255, 102, .1); animation: pulse 2.3s infinite; } +@keyframes pulse { 50% { box-shadow: 0 0 0 9px rgba(185, 255, 102, 0); } } +h1 { font-size: clamp(3.5rem, 7.4vw, 7.8rem); line-height: .9; letter-spacing: -.065em; margin: 0; max-width: 850px; font-weight: 500; } +h1 em { display: block; font-weight: 500; color: var(--lime); font-style: normal; } +.hero-lead { max-width: 610px; color: #a9bcb5; font-size: clamp(1rem, 1.3vw, 1.22rem); margin: 2rem 0; } +.hero-actions { display: flex; gap: .8rem; flex-wrap: wrap; } +.button { min-height: 52px; display: inline-flex; align-items: center; justify-content: center; gap: .9rem; padding: .8rem 1.4rem; text-decoration: none; border-radius: 3px; font-size: .85rem; font-weight: 700; transition: transform .2s, background .2s; } +.button:hover { transform: translateY(-2px); } +.button.primary { color: #07100e; background: var(--lime); } +.button.primary:hover { background: #ccff8f; } +.button.secondary { border: 1px solid var(--line); background: rgba(255,255,255,.025); } +.hero-proof { display: flex; gap: 1.5rem; margin-top: 2.2rem; color: #71877f; font: .65rem var(--mono); text-transform: uppercase; letter-spacing: .08em; } +.coin-stage { min-height: 560px; display: grid; place-items: center; position: relative; } +.coin-stage img { width: min(36vw, 540px); filter: drop-shadow(0 35px 50px rgba(0,0,0,.5)); position: relative; z-index: 2; animation: float 7s ease-in-out infinite; } +@keyframes float { 50% { transform: translateY(-14px) rotate(1.5deg); } } +.coin-glow { position: absolute; width: 55%; aspect-ratio: 1; border-radius: 50%; background: rgba(185,255,102,.22); filter: blur(90px); } +.orbit { position: absolute; width: 78%; aspect-ratio: 1; border: 1px solid rgba(185,255,102,.18); border-radius: 50%; transform: rotate(19deg) scaleY(.38); } +.orbit-two { width: 92%; transform: rotate(-28deg) scaleY(.28); border-color: rgba(98,231,175,.12); } +.coordinate { position: absolute; font: .58rem var(--mono); letter-spacing: .12em; color: #5b746b; } +.coordinate-a { left: 2%; top: 25%; }.coordinate-b { right: 2%; bottom: 20%; } + +.ticker { min-height: 118px; border-block: 1px solid var(--line); display: grid; grid-template-columns: repeat(4, 1fr); padding: 0 clamp(1.2rem, 7vw, 8rem); } +.ticker div { display: flex; justify-content: center; flex-direction: column; padding: 1.3rem 2rem; border-right: 1px solid var(--line); } +.ticker div:first-child { border-left: 1px solid var(--line); } +.ticker span, .metrics span { color: #6f857d; font: .63rem var(--mono); letter-spacing: .11em; text-transform: uppercase; } +.ticker strong { color: var(--lime); font: 500 1rem var(--mono); margin-top: .3rem; } + +.section { padding: clamp(5rem, 10vw, 9rem) clamp(1.2rem, 7vw, 8rem); } +.section-heading { max-width: 620px; } +.section-heading.compact { margin-bottom: 3.2rem; } +h2 { margin: 0 0 1.2rem; font-size: clamp(2.4rem, 5vw, 5rem); font-weight: 500; line-height: 1; letter-spacing: -.055em; } +.section-heading > p:last-child, .mine-copy > p { color: var(--muted); } +.network { display: grid; grid-template-columns: .72fr 1.28fr; gap: clamp(3rem, 8vw, 9rem); align-items: center; } +.status-panel { border: 1px solid var(--line); background: linear-gradient(145deg, rgba(255,255,255,.035), rgba(255,255,255,.008)); box-shadow: 0 30px 90px rgba(0,0,0,.2); } +.status-topline { display: flex; justify-content: space-between; align-items: center; padding: 1.15rem 1.4rem; border-bottom: 1px solid var(--line); font-size: .75rem; } +.status-topline > div { display: flex; align-items: center; gap: .65rem; } +.status-topline code { color: #70877f; font-family: var(--mono); } +.status-dot { width: 7px; height: 7px; border-radius: 50%; background: #e6ae59; box-shadow: 0 0 12px currentColor; } +.status-dot.online { background: var(--lime); } +.status-dot.offline { background: #ed735f; } +.metrics { display: grid; grid-template-columns: 1fr 1fr; } +.metrics article { min-height: 124px; padding: 1.4rem; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); display: flex; flex-direction: column; justify-content: space-between; } +.metrics article:nth-child(even) { border-right: 0; } +.metrics strong { font: 500 clamp(1.25rem, 2vw, 2rem) var(--mono); color: var(--ink); } +.node-addresses { padding: .7rem 1.4rem; } +.node-addresses p { display: grid; grid-template-columns: 100px 1fr auto; gap: .8rem; align-items: center; font-size: .7rem; } +.node-addresses span { color: #6f857d; }.node-addresses code { overflow: hidden; text-overflow: ellipsis; } +.copy { color: var(--lime); border: 0; background: none; cursor: pointer; font: .65rem var(--mono); } + +.miners { background: #091411; border-block: 1px solid var(--line); } +.miners .section-heading { margin-bottom: 3rem; } +.miner-panel { border: 1px solid var(--line); background: rgba(255,255,255,.012); } +.miner-summary { display: grid; grid-template-columns: repeat(4, 1fr); border-bottom: 1px solid var(--line); } +.miner-summary article { min-height: 110px; padding: 1.4rem; border-right: 1px solid var(--line); display: flex; flex-direction: column; justify-content: space-between; } +.miner-summary article:last-child { border: 0; } +.miner-summary span { color: #6f857d; font: .63rem var(--mono); letter-spacing: .11em; text-transform: uppercase; } +.miner-summary strong { font: 500 clamp(1rem, 1.7vw, 1.5rem) var(--mono); } +.table-scroll { overflow-x: auto; } +.miner-table { width: 100%; border-collapse: collapse; font: .73rem var(--mono); } +.miner-table th, .miner-table td { padding: 1.1rem 1.4rem; text-align: left; border-bottom: 1px solid var(--line); white-space: nowrap; } +.miner-table th { color: #6f857d; font-size: .59rem; letter-spacing: .1em; text-transform: uppercase; } +.miner-table tbody tr:first-child td:first-child { color: var(--lime); } +.miner-online { color: var(--lime); } +.table-message { color: var(--muted); text-align: center !important; } +.telemetry-note { margin: 0; padding: 1rem 1.4rem; color: #71877f; font-size: .68rem; } +.chart-grid { display: grid; grid-template-columns: 1fr 1fr; border-bottom: 1px solid var(--line); } +.chart-card { padding: 1.4rem; min-width: 0; border-right: 1px solid var(--line); } +.chart-card:last-child { border-right: 0; } +.chart-card > div { display: flex; align-items: center; justify-content: space-between; gap: 1rem; } +.chart-card span, .chart-card small { color: #6f857d; font: .61rem var(--mono); letter-spacing: .08em; text-transform: uppercase; } +.chart-card strong { font: 500 .9rem var(--mono); } +.chart-card svg { display: block; width: 100%; height: 180px; margin: .8rem 0; overflow: visible; } +.chart-gridline { fill: none; stroke: var(--line); stroke-width: 1; } +.chart-line { fill: none; stroke: var(--lime); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; } +.chart-line.difficulty { stroke: var(--mint); } + +.principles { background: #091411; border-block: 1px solid var(--line); } +.principle-grid { display: grid; grid-template-columns: repeat(3, 1fr); border: 1px solid var(--line); } +.principle-grid article { min-height: 310px; padding: 2rem; border-right: 1px solid var(--line); display: flex; flex-direction: column; } +.principle-grid article:last-child { border: 0; } +.card-number { color: var(--mint); font: .65rem var(--mono); } +h3 { margin: auto 0 1rem; font-size: 1.4rem; font-weight: 600; } +.principle-grid p { color: var(--muted); font-size: .88rem; margin: 0; } + +.mine { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(3rem, 9vw, 10rem); align-items: center; } +.warning { margin-top: 2rem; padding: 1.2rem 1.3rem; border-left: 2px solid var(--copper); background: rgba(215,135,85,.07); color: #bba99e; font-size: .82rem; } +.warning strong { color: #e5c6b3; } +.steps { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--line); } +.steps li { display: grid; grid-template-columns: 50px 1fr; align-items: center; gap: 1rem; min-height: 110px; border-bottom: 1px solid var(--line); } +.steps > li > span { color: var(--lime); font: .7rem var(--mono); } +.steps div { display: flex; flex-direction: column; gap: .5rem; } +.steps strong { font-weight: 600; }.steps code { color: #789087; font: .75rem var(--mono); } + +.download { padding-top: 2rem; } +.download-card { padding: clamp(2rem, 5vw, 4.5rem); display: grid; grid-template-columns: 1fr .8fr; gap: 4rem; align-items: center; border: 1px solid rgba(185,255,102,.25); background: radial-gradient(circle at 85% 30%, rgba(185,255,102,.12), transparent 32%), #0b1815; } +.download-card h2 { font-size: clamp(2.3rem, 4vw, 4rem); } +.download-card p { max-width: 620px; color: var(--muted); } +.download-card .release-note { color: #c8a78f; font-size: .75rem; } +.download-action { display: flex; flex-direction: column; align-items: stretch; gap: 1rem; min-width: 0; } +.release-download { display: flex; flex-direction: column; gap: .65rem; padding: 1rem; border: 1px solid var(--line); background: rgba(0,0,0,.12); } +.button.wide { justify-content: space-between; }.button.wide span { font: .65rem var(--mono); opacity: .7; } +.checksum-link { color: var(--mint); text-align: center; font-size: .72rem; } +.hash { display: block; overflow-wrap: anywhere; color: #60776f; font: .58rem/1.7 var(--mono); text-align: center; } +.unsigned-note { margin: 0 !important; color: #c8a78f !important; font-size: .68rem; } + +.faq { display: grid; grid-template-columns: .7fr 1.3fr; gap: clamp(3rem, 8vw, 9rem); } +.faq-list { border-top: 1px solid var(--line); } +details { border-bottom: 1px solid var(--line); } +summary { cursor: pointer; list-style: none; padding: 1.45rem 2.5rem 1.45rem 0; position: relative; font-size: .95rem; } +summary::-webkit-details-marker { display: none; } +summary::after { content: "+"; position: absolute; right: .4rem; color: var(--lime); font: 1.2rem var(--mono); } +details[open] summary::after { content: "−"; } +details p { color: var(--muted); font-size: .84rem; max-width: 700px; margin: 0; padding: 0 2rem 1.5rem 0; } + +footer { min-height: 120px; border-top: 1px solid var(--line); padding: 2rem clamp(1.2rem, 7vw, 8rem); display: flex; justify-content: space-between; align-items: center; gap: 2rem; color: #60756d; font-size: .68rem; } +footer p { margin: 0; } footer a { color: inherit; } +.toast { position: fixed; bottom: 1.5rem; left: 50%; transform: translate(-50%, 150%); padding: .7rem 1rem; background: var(--lime); color: #07110f; font: .68rem var(--mono); z-index: 20; transition: transform .25s; } +.toast.show { transform: translate(-50%, 0); } + +@media (max-width: 900px) { + .hero { height: auto; min-height: 950px; grid-template-columns: 1fr; padding-top: 10rem; } + .coin-stage { min-height: 360px; order: -1; margin-bottom: -3rem; } + .coin-stage img { width: min(78vw, 430px); } + .network, .mine, .faq, .download-card { grid-template-columns: 1fr; } + .miner-summary { grid-template-columns: 1fr 1fr; } + .chart-grid { grid-template-columns: 1fr; } + .chart-card { border-right: 0; border-bottom: 1px solid var(--line); } + .miner-summary article:nth-child(2) { border-right: 0; } + .miner-summary article:nth-child(-n+2) { border-bottom: 1px solid var(--line); } + .principle-grid { grid-template-columns: 1fr; } + .principle-grid article { min-height: 230px; border-right: 0; border-bottom: 1px solid var(--line); } + .ticker { grid-template-columns: 1fr 1fr; }.ticker div:nth-child(odd) { border-left: 1px solid var(--line); } + nav { display: none; position: absolute; top: 84px; left: 0; right: 0; padding: 1.5rem; flex-direction: column; align-items: stretch; background: #07110f; border-bottom: 1px solid var(--line); } + nav.open { display: flex; }.menu-toggle { display: block; } +} + +@media (max-width: 560px) { + .hero { min-height: 880px; }.coin-stage { min-height: 300px; } + .hero-proof { gap: .7rem; flex-wrap: wrap; } + .ticker div { padding: 1.1rem .8rem; }.ticker strong { font-size: .8rem; } + .metrics { grid-template-columns: 1fr; }.metrics article { border-right: 0; } + .miner-summary { grid-template-columns: 1fr; } + .miner-summary article, .miner-summary article:nth-child(2) { border-right: 0; border-bottom: 1px solid var(--line); } + .node-addresses p { grid-template-columns: 1fr auto; }.node-addresses span { grid-column: 1 / -1; } + footer { flex-direction: column; align-items: flex-start; } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; } +} |
