diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml index 291ace6..5cfe0cd 100644 --- a/.github/workflows/build-all.yml +++ b/.github/workflows/build-all.yml @@ -182,9 +182,9 @@ jobs: if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV else - MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | awk '{print $3}') - MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | awk '{print $3}') - REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | awk '{print $3}') + MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | tr -d '\r' | awk '{print $3}') + MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | tr -d '\r' | awk '{print $3}') + REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | tr -d '\r' | awk '{print $3}') echo "VERSION=${MAJOR}.${MINOR}.${REV}" >> $GITHUB_ENV fi @@ -413,9 +413,9 @@ jobs: if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV else - MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | awk '{print $3}') - MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | awk '{print $3}') - REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | awk '{print $3}') + MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | tr -d '\r' | awk '{print $3}') + MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | tr -d '\r' | awk '{print $3}') + REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | tr -d '\r' | awk '{print $3}') echo "VERSION=${MAJOR}.${MINOR}.${REV}" >> $GITHUB_ENV fi @@ -549,9 +549,9 @@ jobs: if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV else - MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | awk '{print $3}') - MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | awk '{print $3}') - REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | awk '{print $3}') + MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | tr -d '\r' | awk '{print $3}') + MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | tr -d '\r' | awk '{print $3}') + REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | tr -d '\r' | awk '{print $3}') echo "VERSION=${MAJOR}.${MINOR}.${REV}" >> $GITHUB_ENV fi @@ -620,9 +620,9 @@ jobs: if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV else - MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | awk '{print $3}') - MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | awk '{print $3}') - REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | awk '{print $3}') + MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | tr -d '\r' | awk '{print $3}') + MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | tr -d '\r' | awk '{print $3}') + REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | tr -d '\r' | awk '{print $3}') echo "VERSION=${MAJOR}.${MINOR}.${REV}" >> $GITHUB_ENV fi diff --git a/doc/i2p.md b/doc/i2p.md new file mode 100644 index 0000000..755b803 --- /dev/null +++ b/doc/i2p.md @@ -0,0 +1,68 @@ +# I2P support (SAM v3) + +Triangles runs over I2P in addition to Tor, giving the wallet a second +anonymous network and a `.b32.i2p` address shown directly above the `.onion` +address in the status bar. + +I2P is **on by default** and works the same way as the embedded Tor: the wallet +auto-launches a bundled **i2pd** router as a managed child process, enables its +SAM bridge, and connects to it. The user does not have to install or configure +anything — provided the i2pd binary ships with the wallet. + +## Shipping the i2pd binary + +Like `tor.exe`, the wallet looks for an `i2pd` executable in several places and +launches the first one it finds: + +1. Next to the wallet executable (recommended): `i2pd.exe` (Windows) / `i2pd` + (Linux/macOS), or in an `i2pd/` subfolder beside it. +2. In the data directory (or its `i2pd/` subfolder). +3. Common system locations (`/usr/bin/i2pd`, Homebrew, `C:\i2pd\…`, etc.). + +Get i2pd from https://i2pd.website/ (or your package manager) and place the +binary next to the wallet in your build/packaging step. That's the only manual +part, and it's a packaging concern, not something the end user does. + +If no i2pd binary is found, the wallet logs a notice and continues with **Tor +only** — I2P is strictly additive and never blocks start-up. + +## What happens at start-up + +1. If a SAM bridge is already listening on `127.0.0.1:7656` (e.g. you run your + own router), the wallet uses it and does **not** launch its own. +2. Otherwise it writes `i2pd.conf` into `/i2pd/` (SAM enabled, other + services off), launches i2pd, and waits for the SAM bridge to come up. +3. The SAM client then loads/creates a persistent destination + (`/i2p_private_key`), opens a STREAM session, derives the + `.b32.i2p` address (`base32(SHA-256(destination))`), accepts inbound I2P + streams, and dials outbound `.b32.i2p` peers. +4. On wallet exit, the SAM session is closed and the i2pd child process is + terminated (an external router you started yourself is left running). + +The first session takes a little longer while i2pd builds tunnels; the address +appears once the bridge is ready. + +## Options + +``` +-i2p Enable I2P; auto-launches bundled i2pd (default: 1; -i2p=0 to disable) +-i2psam= SAM bridge address (default: 127.0.0.1:7656). + A non-loopback address disables the bundled router and + connects to that external bridge instead. +``` + +## Checking it + +* GUI: the `.b32.i2p` address sits on top of the `.onion` in the status bar; + click either to copy. +* RPC: `getinfo` shows `toraddress` and `i2paddress`; `getnetworkinfo` shows + `toraddress` and an `i2p` object (`enabled`, `active`, `address`, `peers`). + +## Notes / limitations + +* The address serialization format carries a flag for I2P addresses, so **all + nodes must run this build** to exchange I2P peers; an old `peers.dat` is + discarded. +* `i2p_private_key` is your stable I2P identity — back it up, don't delete it. +* This was implemented without a build/CI environment here; build and test + against a real i2pd before relying on it. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0dea8ef..26661b9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -54,6 +54,8 @@ set(CORE_SOURCES net.cpp net_bootstrap.cpp netbase.cpp + i2p.cpp + i2p_process.cpp protocol.cpp script.cpp sync.cpp diff --git a/src/checkpoints.cpp b/src/checkpoints.cpp index 48dfada..5358ef4 100644 --- a/src/checkpoints.cpp +++ b/src/checkpoints.cpp @@ -1,453 +1,453 @@ -// Copyright (c) 2009-2012 The Bitcoin developers -// Distributed under the MIT/X11 software license, see the accompanying -// file COPYING or http://www.opensource.org/licenses/mit-license.php. - -#include "checkpoints.h" - -#include "txdb.h" -#include "main.h" -#include "uint256.h" - -namespace Checkpoints -{ - typedef std::map MapCheckpoints; - - // - // What makes a good checkpoint block? - // + Is surrounded by blocks with reasonable timestamps - // (no blocks before with a timestamp after, none after with - // timestamp before) - // + Contains no strange transactions - // - static MapCheckpoints mapCheckpoints = { - { 0, hashGenesisBlockOfficial }, - { 2000, uint256("0x0000000000b5f20078bf46ebdf1500813bb6b2cb482065aa93b89e073b2c6467")}, - { 2101, uint256("0xd4ea1ac45b63c8162a7fc8033cec441db8d532ba988202849d7831e32fe2d059")}, - { 2847, uint256("0xb5015e2835f13fd3bb6135cff9b31ac33310c9b77d694bdb592b8680d98d018e")}, - { 3589, uint256("0xb12a2ca3db4e288cada98aa2139768532bd4474c49dd7b8158031032dac08d51")}, - { 3935, uint256("0xe16290c9757d1368b8d7c35de07d4f8f70c2c9f9c785b667df0c3bff85086ca6")}, - { 5703, uint256("0x587db07bb2172ad7db72c5fabc2518262a1b27f503f99417510b2c6fafa6557b")}, - { 9000, uint256("0x00000000019ef6b2f5e7c324c7d083ee94502305aabc7e9cd73a7fb2a57bb8db")}, - { 9001, uint256("0x6d5c6c5f201cc9e59659ee0da30d1430dc6bf3b12a8ff4c3864ab8d6286b0007")}, - { 9002, uint256("0xa1e20fb1d44688b763690cf74d6aefe859e4cc32981f9e3f2b2ae9702bbcf249")}, - { 10881, uint256("0x4b6554c45e1e6764a6f3c309c47baf53c9edd81f624e52b072518cd15da237e6")}, - { 17650, uint256("0x224940e1f986a202209b8e762728d1452ab45870c308abf84905674acf326a47")}, - // Recent finality pin (PoS era). Closes the long unchecked span from - // 17650 to the live tip so stale-bootstrap / low-trust forks below - // this height are rejected outright. Hash from the canonical chain. - { 2205000, uint256("0x6bdd3c5e5a32e1dd9a70e705f1a28d1dd84929f89579bd2696d41bc87f39446f")}, - { 2206004, uint256("0xb34e8e6a7bb7f52167d81aaad4d26f87a876898fdd0fce860916fc1aaf9a2a46")}, - }; - - // Published UTXO snapshot file SHA256, keyed by snapshot height. - // Each entry binds height -> SHA256 of the canonical snapshot file produced by - // UtxoSnapshot::DumpSnapshot at that height. Used by SnapshotNet to verify - // P2P-delivered snapshots without trusting any peer. - // - // Maintainers: after producing a snapshot, sha256 the file and add an entry - // here. The corresponding (height, blockhash) must already exist in - // mapCheckpoints / mapCheckpointsTestnet. - static std::map mapSnapshotHashes = { - { 2206004, uint256("0x1419282dae817315ee1b955543f6248233fe5800f5e8488734a0ece5bd6781ea")}, - }; - - static std::map mapSnapshotHashesTestnet = { - }; - - static MapCheckpoints mapCheckpointsTestnet = { - { 0, hashGenesisBlockTestNet }, - { 2000, uint256("0x0000000000b5f20078bf46ebdf1500813bb6b2cb482065aa93b89e073b2c6467")}, - { 2101, uint256("0xd4ea1ac45b63c8162a7fc8033cec441db8d532ba988202849d7831e32fe2d059")}, - { 2847, uint256("0xb5015e2835f13fd3bb6135cff9b31ac33310c9b77d694bdb592b8680d98d018e")}, - { 3589, uint256("0xb12a2ca3db4e288cada98aa2139768532bd4474c49dd7b8158031032dac08d51")}, - { 3935, uint256("0xe16290c9757d1368b8d7c35de07d4f8f70c2c9f9c785b667df0c3bff85086ca6")}, - { 5703, uint256("0x587db07bb2172ad7db72c5fabc2518262a1b27f503f99417510b2c6fafa6557b")}, - { 9000, uint256("0x00000000019ef6b2f5e7c324c7d083ee94502305aabc7e9cd73a7fb2a57bb8db")}, - { 9001, uint256("0x6d5c6c5f201cc9e59659ee0da30d1430dc6bf3b12a8ff4c3864ab8d6286b0007")}, - { 9002, uint256("0xa1e20fb1d44688b763690cf74d6aefe859e4cc32981f9e3f2b2ae9702bbcf249")}, - { 10881, uint256("0x4b6554c45e1e6764a6f3c309c47baf53c9edd81f624e52b072518cd15da237e6")}, - { 17650, uint256("0x224940e1f986a202209b8e762728d1452ab45870c308abf84905674acf326a47")}, - }; - - bool CheckHardened(int nHeight, const uint256& hash) - { - MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints); - - MapCheckpoints::const_iterator i = checkpoints.find(nHeight); - if (i == checkpoints.end()) return true; - return hash == i->second; - } - - bool IsKnownCheckpoint(int nHeight, const uint256& hash) - { - MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints); - MapCheckpoints::const_iterator i = checkpoints.find(nHeight); - if (i == checkpoints.end()) return false; - return hash == i->second; - } - - int GetTotalBlocksEstimate() - { - MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints); - - return checkpoints.rbegin()->first; - } - - int GetBestSnapshotHeight() - { - std::map& snaps = (fTestNet ? mapSnapshotHashesTestnet : mapSnapshotHashes); - if (snaps.empty()) return 0; - return snaps.rbegin()->first; - } - - bool GetSnapshotHash(int nHeight, uint256& fileHashOut) - { - std::map& snaps = (fTestNet ? mapSnapshotHashesTestnet : mapSnapshotHashes); - auto it = snaps.find(nHeight); - if (it == snaps.end()) return false; - fileHashOut = it->second; - return true; - } - - CBlockIndex* GetLastCheckpoint(const std::map& mapBlockIndex) - { - MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints); - - for (auto it = checkpoints.rbegin(); it != checkpoints.rend(); ++it) - { - const uint256& hash = it->second; - std::map::const_iterator t = mapBlockIndex.find(hash); - if (t != mapBlockIndex.end()) - return t->second; - } - return nullptr; - } - - // triangles: synchronized checkpoint (centrally broadcasted) - uint256 hashSyncCheckpoint = uint256("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021"); - uint256 hashPendingCheckpoint = uint256("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021"); - CSyncCheckpoint checkpointMessage; - CSyncCheckpoint checkpointMessagePending; - uint256 hashInvalidCheckpoint = 0; - CCriticalSection cs_hashSyncCheckpoint; - - // triangles: get last synchronized checkpoint - CBlockIndex* GetLastSyncCheckpoint() - { - LOCK(cs_hashSyncCheckpoint); - if (!mapBlockIndex.count(hashSyncCheckpoint)) - error("GetSyncCheckpoint: block index missing for current sync-checkpoint %s", hashSyncCheckpoint.ToString().c_str()); - else - return mapBlockIndex[hashSyncCheckpoint]; - return nullptr; - } - - // triangles: only descendant of current sync-checkpoint is allowed - bool ValidateSyncCheckpoint(uint256 hashCheckpoint) - { - if (!mapBlockIndex.count(hashSyncCheckpoint)) - return error("ValidateSyncCheckpoint: block index missing for current sync-checkpoint %s", hashSyncCheckpoint.ToString().c_str()); - if (!mapBlockIndex.count(hashCheckpoint)) - return error("ValidateSyncCheckpoint: block index missing for received sync-checkpoint %s", hashCheckpoint.ToString().c_str()); - - CBlockIndex* pindexSyncCheckpoint = mapBlockIndex[hashSyncCheckpoint]; - CBlockIndex* pindexCheckpointRecv = mapBlockIndex[hashCheckpoint]; - - if (pindexCheckpointRecv->nHeight <= pindexSyncCheckpoint->nHeight) - { - // Received an older checkpoint, trace back from current checkpoint - // to the same height of the received checkpoint to verify - // that current checkpoint should be a descendant block - CBlockIndex* pindex = pindexSyncCheckpoint; - while (pindex->nHeight > pindexCheckpointRecv->nHeight) - if (!(pindex = pindex->pprev)) - return error("ValidateSyncCheckpoint: pprev null - block index structure failure"); - if (pindex->GetBlockHash() != hashCheckpoint) - { - hashInvalidCheckpoint = hashCheckpoint; - return error("ValidateSyncCheckpoint: new sync-checkpoint %s is conflicting with current sync-checkpoint %s", hashCheckpoint.ToString().c_str(), hashSyncCheckpoint.ToString().c_str()); - } - return false; // ignore older checkpoint - } - - // Received checkpoint should be a descendant block of the current - // checkpoint. Trace back to the same height of current checkpoint - // to verify. - CBlockIndex* pindex = pindexCheckpointRecv; - while (pindex->nHeight > pindexSyncCheckpoint->nHeight) - if (!(pindex = pindex->pprev)) - return error("ValidateSyncCheckpoint: pprev2 null - block index structure failure"); - if (pindex->GetBlockHash() != hashSyncCheckpoint) - { - hashInvalidCheckpoint = hashCheckpoint; - return error("ValidateSyncCheckpoint: new sync-checkpoint %s is not a descendant of current sync-checkpoint %s", hashCheckpoint.ToString().c_str(), hashSyncCheckpoint.ToString().c_str()); - } - return true; - } - - bool WriteSyncCheckpoint(const uint256& hashCheckpoint) - { - auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder; - txdb.TxnBegin(); - if (!txdb.WriteSyncCheckpoint(hashCheckpoint)) - { - txdb.TxnAbort(); - return error("WriteSyncCheckpoint(): failed to write to db sync checkpoint %s", hashCheckpoint.ToString().c_str()); - } - if (!txdb.TxnCommit()) - return error("WriteSyncCheckpoint(): failed to commit to db sync checkpoint %s", hashCheckpoint.ToString().c_str()); - - Checkpoints::hashSyncCheckpoint = hashCheckpoint; - return true; - } - - bool AcceptPendingSyncCheckpoint() - { - LOCK(cs_hashSyncCheckpoint); - if (hashPendingCheckpoint != 0 && mapBlockIndex.count(hashPendingCheckpoint)) - { - if (!ValidateSyncCheckpoint(hashPendingCheckpoint)) - { - hashPendingCheckpoint = 0; - checkpointMessagePending.SetNull(); - return false; - } - - auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder; - CBlockIndex* pindexCheckpoint = mapBlockIndex[hashPendingCheckpoint]; - if (!pindexCheckpoint->IsInMainChain()) - { - CBlock block; - if (!block.ReadFromDisk(pindexCheckpoint)) - return error("AcceptPendingSyncCheckpoint: ReadFromDisk failed for sync checkpoint %s", hashPendingCheckpoint.ToString().c_str()); - if (!block.SetBestChain(txdb, pindexCheckpoint)) - { - hashInvalidCheckpoint = hashPendingCheckpoint; - return error("AcceptPendingSyncCheckpoint: SetBestChain failed for sync checkpoint %s", hashPendingCheckpoint.ToString().c_str()); - } - } - - if (!WriteSyncCheckpoint(hashPendingCheckpoint)) - return error("AcceptPendingSyncCheckpoint(): failed to write sync checkpoint %s", hashPendingCheckpoint.ToString().c_str()); - hashPendingCheckpoint = 0; - checkpointMessage = checkpointMessagePending; - checkpointMessagePending.SetNull(); - printf("AcceptPendingSyncCheckpoint : sync-checkpoint at %s\n", hashSyncCheckpoint.ToString().c_str()); - // relay the checkpoint - if (!checkpointMessage.IsNull()) - { - for (CNode* pnode : vNodes) - checkpointMessage.RelayTo(pnode); - } - return true; - } - return false; - } - - // Automatically select a suitable sync-checkpoint - uint256 AutoSelectSyncCheckpoint() - { - const CBlockIndex *pindex = pindexBest; - // Search backward for a block within max span and maturity window - while (pindex->pprev && (pindex->GetBlockTime() + CHECKPOINT_MAX_SPAN > pindexBest->GetBlockTime() || pindex->nHeight + 8 > pindexBest->nHeight)) - pindex = pindex->pprev; - return pindex->GetBlockHash(); - } - - // Check against synchronized checkpoint - // Disabled: master key removed in V5, no new sync checkpoints possible. - // Always returns true to prevent stale DB-persisted checkpoints from blocking IBD. - bool CheckSync(const uint256& hashBlock, const CBlockIndex* pindexPrev) - { - return true; - } - - bool WantedByPendingSyncCheckpoint(uint256 hashBlock) - { - LOCK(cs_hashSyncCheckpoint); - if (hashPendingCheckpoint == 0) - return false; - if (hashBlock == hashPendingCheckpoint) - return true; - if (mapOrphanBlocks.count(hashPendingCheckpoint) - && hashBlock == WantedByOrphan(mapOrphanBlocks[hashPendingCheckpoint].get())) - return true; - return false; - } - - // triangles: reset synchronized checkpoint to last hardened checkpoint - bool ResetSyncCheckpoint() - { - LOCK(cs_hashSyncCheckpoint); - const uint256& hash = mapCheckpoints.rbegin()->second; - if (mapBlockIndex.count(hash) && !mapBlockIndex[hash]->IsInMainChain()) - { - // checkpoint block accepted but not yet in main chain - printf("ResetSyncCheckpoint: SetBestChain to hardened checkpoint %s\n", hash.ToString().c_str()); - auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder; - CBlock block; - if (!block.ReadFromDisk(mapBlockIndex[hash])) - return error("ResetSyncCheckpoint: ReadFromDisk failed for hardened checkpoint %s", hash.ToString().c_str()); - if (!block.SetBestChain(txdb, mapBlockIndex[hash])) - { - return error("ResetSyncCheckpoint: SetBestChain failed for hardened checkpoint %s", hash.ToString().c_str()); - } - } - else if(!mapBlockIndex.count(hash)) - { - // checkpoint block not yet accepted - hashPendingCheckpoint = hash; - checkpointMessagePending.SetNull(); - printf("ResetSyncCheckpoint: pending for sync-checkpoint %s\n", hashPendingCheckpoint.ToString().c_str()); - } - - for (auto it = mapCheckpoints.rbegin(); it != mapCheckpoints.rend(); ++it) - { - const uint256& hash = it->second; - if (mapBlockIndex.count(hash) && mapBlockIndex[hash]->IsInMainChain()) - { - if (!WriteSyncCheckpoint(hash)) - return error("ResetSyncCheckpoint: failed to write sync checkpoint %s", hash.ToString().c_str()); - printf("ResetSyncCheckpoint: sync-checkpoint reset to %s\n", hashSyncCheckpoint.ToString().c_str()); - return true; - } - } - - return false; - } - - void AskForPendingSyncCheckpoint(CNode* pfrom) - { - LOCK(cs_hashSyncCheckpoint); - if (pfrom && hashPendingCheckpoint != 0 && (!mapBlockIndex.count(hashPendingCheckpoint)) && (!mapOrphanBlocks.count(hashPendingCheckpoint))) - pfrom->AskFor(CInv(MSG_BLOCK, hashPendingCheckpoint)); - } - - bool SetCheckpointPrivKey(std::string strPrivKey) - { - // Test signing a sync-checkpoint with genesis block - CSyncCheckpoint checkpoint; - checkpoint.hashCheckpoint = !fTestNet ? hashGenesisBlockOfficial : hashGenesisBlockTestNet; - CDataStream sMsg(SER_NETWORK, PROTOCOL_VERSION); - sMsg << (CUnsignedSyncCheckpoint)checkpoint; - checkpoint.vchMsg = std::vector(sMsg.begin(), sMsg.end()); - - std::vector vchPrivKey = ParseHex(strPrivKey); - CKey key; - key.SetPrivKey(CPrivKey(vchPrivKey.begin(), vchPrivKey.end())); // if key is not correct openssl may crash - if (!key.Sign(Hash(checkpoint.vchMsg.begin(), checkpoint.vchMsg.end()), checkpoint.vchSig)) - return false; - - // Test signing successful, proceed - CSyncCheckpoint::strMasterPrivKey = strPrivKey; - return true; - } - - bool SendSyncCheckpoint(uint256 hashCheckpoint) - { - CSyncCheckpoint checkpoint; - checkpoint.hashCheckpoint = hashCheckpoint; - CDataStream sMsg(SER_NETWORK, PROTOCOL_VERSION); - sMsg << (CUnsignedSyncCheckpoint)checkpoint; - checkpoint.vchMsg = std::vector(sMsg.begin(), sMsg.end()); - - if (CSyncCheckpoint::strMasterPrivKey.empty()) - return error("SendSyncCheckpoint: Checkpoint master key unavailable."); - std::vector vchPrivKey = ParseHex(CSyncCheckpoint::strMasterPrivKey); - CKey key; - key.SetPrivKey(CPrivKey(vchPrivKey.begin(), vchPrivKey.end())); // if key is not correct openssl may crash - if (!key.Sign(Hash(checkpoint.vchMsg.begin(), checkpoint.vchMsg.end()), checkpoint.vchSig)) - return error("SendSyncCheckpoint: Unable to sign checkpoint, check private key?"); - - if(!checkpoint.ProcessSyncCheckpoint(nullptr)) - { - printf("WARNING: SendSyncCheckpoint: Failed to process checkpoint.\n"); - return false; - } - - // Relay checkpoint - { - LOCK(cs_vNodes); - for (CNode* pnode : vNodes) - checkpoint.RelayTo(pnode); - } - return true; - } - - // Is the sync-checkpoint outside maturity window? - bool IsMatureSyncCheckpoint() - { - LOCK(cs_hashSyncCheckpoint); - if (!mapBlockIndex.count(hashSyncCheckpoint)) - return true; // no valid sync checkpoint, treat as mature - const CBlockIndex* pindexSync = mapBlockIndex[hashSyncCheckpoint]; - return (nBestHeight >= pindexSync->nHeight + nCoinbaseMaturity || - pindexSync->GetBlockTime() + nStakeMinAge < GetAdjustedTime()); - } -} - -// triangles: sync-checkpoint master key (DISABLED for decentralization - v5 hard fork) -const std::string CSyncCheckpoint::strMasterPubKey = ""; - -std::string CSyncCheckpoint::strMasterPrivKey = ""; - -// triangles: verify signature of sync-checkpoint message -// Master key system disabled - checkpoint signatures are no longer required -bool CSyncCheckpoint::CheckSignature() -{ - // Deserialize the checkpoint data without signature verification - CDataStream sMsg(vchMsg, SER_NETWORK, PROTOCOL_VERSION); - sMsg >> *(CUnsignedSyncCheckpoint*)this; - return true; -} - -// triangles: process synchronized checkpoint -bool CSyncCheckpoint::ProcessSyncCheckpoint(CNode* pfrom) -{ - if (!CheckSignature()) - return false; - - LOCK(Checkpoints::cs_hashSyncCheckpoint); - if (!mapBlockIndex.count(hashCheckpoint)) - { - // We haven't received the checkpoint chain, keep the checkpoint as pending - Checkpoints::hashPendingCheckpoint = hashCheckpoint; - Checkpoints::checkpointMessagePending = *this; - printf("ProcessSyncCheckpoint: pending for sync-checkpoint %s\n", hashCheckpoint.ToString().c_str()); - // Ask this guy to fill in what we're missing - if (pfrom) - { - pfrom->PushGetBlocks(pindexBest, hashCheckpoint); - // ask directly as well in case rejected earlier by duplicate - // proof-of-stake because getblocks may not get it this time - pfrom->AskFor(CInv(MSG_BLOCK, mapOrphanBlocks.count(hashCheckpoint)? WantedByOrphan(mapOrphanBlocks[hashCheckpoint].get()) : hashCheckpoint)); - } - return false; - } - - if (!Checkpoints::ValidateSyncCheckpoint(hashCheckpoint)) - return false; - - auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder; - CBlockIndex* pindexCheckpoint = mapBlockIndex[hashCheckpoint]; - if (!pindexCheckpoint->IsInMainChain()) - { - // checkpoint chain received but not yet main chain - CBlock block; - if (!block.ReadFromDisk(pindexCheckpoint)) - return error("ProcessSyncCheckpoint: ReadFromDisk failed for sync checkpoint %s", hashCheckpoint.ToString().c_str()); - if (!block.SetBestChain(txdb, pindexCheckpoint)) - { - Checkpoints::hashInvalidCheckpoint = hashCheckpoint; - return error("ProcessSyncCheckpoint: SetBestChain failed for sync checkpoint %s", hashCheckpoint.ToString().c_str()); - } - } - - if (!Checkpoints::WriteSyncCheckpoint(hashCheckpoint)) - return error("ProcessSyncCheckpoint(): failed to write sync checkpoint %s", hashCheckpoint.ToString().c_str()); - Checkpoints::checkpointMessage = *this; - Checkpoints::hashPendingCheckpoint = 0; - Checkpoints::checkpointMessagePending.SetNull(); - printf("ProcessSyncCheckpoint: sync-checkpoint at %s\n", hashCheckpoint.ToString().c_str()); - return true; -} +// Copyright (c) 2009-2012 The Bitcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "checkpoints.h" + +#include "txdb.h" +#include "main.h" +#include "uint256.h" + +namespace Checkpoints +{ + typedef std::map MapCheckpoints; + + // + // What makes a good checkpoint block? + // + Is surrounded by blocks with reasonable timestamps + // (no blocks before with a timestamp after, none after with + // timestamp before) + // + Contains no strange transactions + // + static MapCheckpoints mapCheckpoints = { + { 0, hashGenesisBlockOfficial }, + { 2000, uint256("0x0000000000b5f20078bf46ebdf1500813bb6b2cb482065aa93b89e073b2c6467")}, + { 2101, uint256("0xd4ea1ac45b63c8162a7fc8033cec441db8d532ba988202849d7831e32fe2d059")}, + { 2847, uint256("0xb5015e2835f13fd3bb6135cff9b31ac33310c9b77d694bdb592b8680d98d018e")}, + { 3589, uint256("0xb12a2ca3db4e288cada98aa2139768532bd4474c49dd7b8158031032dac08d51")}, + { 3935, uint256("0xe16290c9757d1368b8d7c35de07d4f8f70c2c9f9c785b667df0c3bff85086ca6")}, + { 5703, uint256("0x587db07bb2172ad7db72c5fabc2518262a1b27f503f99417510b2c6fafa6557b")}, + { 9000, uint256("0x00000000019ef6b2f5e7c324c7d083ee94502305aabc7e9cd73a7fb2a57bb8db")}, + { 9001, uint256("0x6d5c6c5f201cc9e59659ee0da30d1430dc6bf3b12a8ff4c3864ab8d6286b0007")}, + { 9002, uint256("0xa1e20fb1d44688b763690cf74d6aefe859e4cc32981f9e3f2b2ae9702bbcf249")}, + { 10881, uint256("0x4b6554c45e1e6764a6f3c309c47baf53c9edd81f624e52b072518cd15da237e6")}, + { 17650, uint256("0x224940e1f986a202209b8e762728d1452ab45870c308abf84905674acf326a47")}, + // Recent finality pin (PoS era). Closes the long unchecked span from + // 17650 to the live tip so stale-bootstrap / low-trust forks below + // this height are rejected outright. Hash from the canonical chain. + { 2205000, uint256("0x6bdd3c5e5a32e1dd9a70e705f1a28d1dd84929f89579bd2696d41bc87f39446f")}, + { 2206004, uint256("0xb34e8e6a7bb7f52167d81aaad4d26f87a876898fdd0fce860916fc1aaf9a2a46")}, + }; + + // Published UTXO snapshot file SHA256, keyed by snapshot height. + // Each entry binds height -> SHA256 of the canonical snapshot file produced by + // UtxoSnapshot::DumpSnapshot at that height. Used by SnapshotNet to verify + // P2P-delivered snapshots without trusting any peer. + // + // Maintainers: after producing a snapshot, sha256 the file and add an entry + // here. The corresponding (height, blockhash) must already exist in + // mapCheckpoints / mapCheckpointsTestnet. + static std::map mapSnapshotHashes = { + { 2206004, uint256("0x1419282dae817315ee1b955543f6248233fe5800f5e8488734a0ece5bd6781ea")}, + }; + + static std::map mapSnapshotHashesTestnet = { + }; + + static MapCheckpoints mapCheckpointsTestnet = { + { 0, hashGenesisBlockTestNet }, + { 2000, uint256("0x0000000000b5f20078bf46ebdf1500813bb6b2cb482065aa93b89e073b2c6467")}, + { 2101, uint256("0xd4ea1ac45b63c8162a7fc8033cec441db8d532ba988202849d7831e32fe2d059")}, + { 2847, uint256("0xb5015e2835f13fd3bb6135cff9b31ac33310c9b77d694bdb592b8680d98d018e")}, + { 3589, uint256("0xb12a2ca3db4e288cada98aa2139768532bd4474c49dd7b8158031032dac08d51")}, + { 3935, uint256("0xe16290c9757d1368b8d7c35de07d4f8f70c2c9f9c785b667df0c3bff85086ca6")}, + { 5703, uint256("0x587db07bb2172ad7db72c5fabc2518262a1b27f503f99417510b2c6fafa6557b")}, + { 9000, uint256("0x00000000019ef6b2f5e7c324c7d083ee94502305aabc7e9cd73a7fb2a57bb8db")}, + { 9001, uint256("0x6d5c6c5f201cc9e59659ee0da30d1430dc6bf3b12a8ff4c3864ab8d6286b0007")}, + { 9002, uint256("0xa1e20fb1d44688b763690cf74d6aefe859e4cc32981f9e3f2b2ae9702bbcf249")}, + { 10881, uint256("0x4b6554c45e1e6764a6f3c309c47baf53c9edd81f624e52b072518cd15da237e6")}, + { 17650, uint256("0x224940e1f986a202209b8e762728d1452ab45870c308abf84905674acf326a47")}, + }; + + bool CheckHardened(int nHeight, const uint256& hash) + { + MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints); + + MapCheckpoints::const_iterator i = checkpoints.find(nHeight); + if (i == checkpoints.end()) return true; + return hash == i->second; + } + + bool IsKnownCheckpoint(int nHeight, const uint256& hash) + { + MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints); + MapCheckpoints::const_iterator i = checkpoints.find(nHeight); + if (i == checkpoints.end()) return false; + return hash == i->second; + } + + int GetTotalBlocksEstimate() + { + MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints); + + return checkpoints.rbegin()->first; + } + + int GetBestSnapshotHeight() + { + std::map& snaps = (fTestNet ? mapSnapshotHashesTestnet : mapSnapshotHashes); + if (snaps.empty()) return 0; + return snaps.rbegin()->first; + } + + bool GetSnapshotHash(int nHeight, uint256& fileHashOut) + { + std::map& snaps = (fTestNet ? mapSnapshotHashesTestnet : mapSnapshotHashes); + auto it = snaps.find(nHeight); + if (it == snaps.end()) return false; + fileHashOut = it->second; + return true; + } + + CBlockIndex* GetLastCheckpoint(const std::map& mapBlockIndex) + { + MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints); + + for (auto it = checkpoints.rbegin(); it != checkpoints.rend(); ++it) + { + const uint256& hash = it->second; + std::map::const_iterator t = mapBlockIndex.find(hash); + if (t != mapBlockIndex.end()) + return t->second; + } + return nullptr; + } + + // triangles: synchronized checkpoint (centrally broadcasted) + uint256 hashSyncCheckpoint = uint256("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021"); + uint256 hashPendingCheckpoint = uint256("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021"); + CSyncCheckpoint checkpointMessage; + CSyncCheckpoint checkpointMessagePending; + uint256 hashInvalidCheckpoint = 0; + CCriticalSection cs_hashSyncCheckpoint; + + // triangles: get last synchronized checkpoint + CBlockIndex* GetLastSyncCheckpoint() + { + LOCK(cs_hashSyncCheckpoint); + if (!mapBlockIndex.count(hashSyncCheckpoint)) + error("GetSyncCheckpoint: block index missing for current sync-checkpoint %s", hashSyncCheckpoint.ToString().c_str()); + else + return mapBlockIndex[hashSyncCheckpoint]; + return nullptr; + } + + // triangles: only descendant of current sync-checkpoint is allowed + bool ValidateSyncCheckpoint(uint256 hashCheckpoint) + { + if (!mapBlockIndex.count(hashSyncCheckpoint)) + return error("ValidateSyncCheckpoint: block index missing for current sync-checkpoint %s", hashSyncCheckpoint.ToString().c_str()); + if (!mapBlockIndex.count(hashCheckpoint)) + return error("ValidateSyncCheckpoint: block index missing for received sync-checkpoint %s", hashCheckpoint.ToString().c_str()); + + CBlockIndex* pindexSyncCheckpoint = mapBlockIndex[hashSyncCheckpoint]; + CBlockIndex* pindexCheckpointRecv = mapBlockIndex[hashCheckpoint]; + + if (pindexCheckpointRecv->nHeight <= pindexSyncCheckpoint->nHeight) + { + // Received an older checkpoint, trace back from current checkpoint + // to the same height of the received checkpoint to verify + // that current checkpoint should be a descendant block + CBlockIndex* pindex = pindexSyncCheckpoint; + while (pindex->nHeight > pindexCheckpointRecv->nHeight) + if (!(pindex = pindex->pprev)) + return error("ValidateSyncCheckpoint: pprev null - block index structure failure"); + if (pindex->GetBlockHash() != hashCheckpoint) + { + hashInvalidCheckpoint = hashCheckpoint; + return error("ValidateSyncCheckpoint: new sync-checkpoint %s is conflicting with current sync-checkpoint %s", hashCheckpoint.ToString().c_str(), hashSyncCheckpoint.ToString().c_str()); + } + return false; // ignore older checkpoint + } + + // Received checkpoint should be a descendant block of the current + // checkpoint. Trace back to the same height of current checkpoint + // to verify. + CBlockIndex* pindex = pindexCheckpointRecv; + while (pindex->nHeight > pindexSyncCheckpoint->nHeight) + if (!(pindex = pindex->pprev)) + return error("ValidateSyncCheckpoint: pprev2 null - block index structure failure"); + if (pindex->GetBlockHash() != hashSyncCheckpoint) + { + hashInvalidCheckpoint = hashCheckpoint; + return error("ValidateSyncCheckpoint: new sync-checkpoint %s is not a descendant of current sync-checkpoint %s", hashCheckpoint.ToString().c_str(), hashSyncCheckpoint.ToString().c_str()); + } + return true; + } + + bool WriteSyncCheckpoint(const uint256& hashCheckpoint) + { + auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder; + txdb.TxnBegin(); + if (!txdb.WriteSyncCheckpoint(hashCheckpoint)) + { + txdb.TxnAbort(); + return error("WriteSyncCheckpoint(): failed to write to db sync checkpoint %s", hashCheckpoint.ToString().c_str()); + } + if (!txdb.TxnCommit()) + return error("WriteSyncCheckpoint(): failed to commit to db sync checkpoint %s", hashCheckpoint.ToString().c_str()); + + Checkpoints::hashSyncCheckpoint = hashCheckpoint; + return true; + } + + bool AcceptPendingSyncCheckpoint() + { + LOCK(cs_hashSyncCheckpoint); + if (hashPendingCheckpoint != 0 && mapBlockIndex.count(hashPendingCheckpoint)) + { + if (!ValidateSyncCheckpoint(hashPendingCheckpoint)) + { + hashPendingCheckpoint = 0; + checkpointMessagePending.SetNull(); + return false; + } + + auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder; + CBlockIndex* pindexCheckpoint = mapBlockIndex[hashPendingCheckpoint]; + if (!pindexCheckpoint->IsInMainChain()) + { + CBlock block; + if (!block.ReadFromDisk(pindexCheckpoint)) + return error("AcceptPendingSyncCheckpoint: ReadFromDisk failed for sync checkpoint %s", hashPendingCheckpoint.ToString().c_str()); + if (!block.SetBestChain(txdb, pindexCheckpoint)) + { + hashInvalidCheckpoint = hashPendingCheckpoint; + return error("AcceptPendingSyncCheckpoint: SetBestChain failed for sync checkpoint %s", hashPendingCheckpoint.ToString().c_str()); + } + } + + if (!WriteSyncCheckpoint(hashPendingCheckpoint)) + return error("AcceptPendingSyncCheckpoint(): failed to write sync checkpoint %s", hashPendingCheckpoint.ToString().c_str()); + hashPendingCheckpoint = 0; + checkpointMessage = checkpointMessagePending; + checkpointMessagePending.SetNull(); + printf("AcceptPendingSyncCheckpoint : sync-checkpoint at %s\n", hashSyncCheckpoint.ToString().c_str()); + // relay the checkpoint + if (!checkpointMessage.IsNull()) + { + for (CNode* pnode : vNodes) + checkpointMessage.RelayTo(pnode); + } + return true; + } + return false; + } + + // Automatically select a suitable sync-checkpoint + uint256 AutoSelectSyncCheckpoint() + { + const CBlockIndex *pindex = pindexBest; + // Search backward for a block within max span and maturity window + while (pindex->pprev && (pindex->GetBlockTime() + CHECKPOINT_MAX_SPAN > pindexBest->GetBlockTime() || pindex->nHeight + 8 > pindexBest->nHeight)) + pindex = pindex->pprev; + return pindex->GetBlockHash(); + } + + // Check against synchronized checkpoint + // Disabled: master key removed in V5, no new sync checkpoints possible. + // Always returns true to prevent stale DB-persisted checkpoints from blocking IBD. + bool CheckSync(const uint256& hashBlock, const CBlockIndex* pindexPrev) + { + return true; + } + + bool WantedByPendingSyncCheckpoint(uint256 hashBlock) + { + LOCK(cs_hashSyncCheckpoint); + if (hashPendingCheckpoint == 0) + return false; + if (hashBlock == hashPendingCheckpoint) + return true; + if (mapOrphanBlocks.count(hashPendingCheckpoint) + && hashBlock == WantedByOrphan(mapOrphanBlocks[hashPendingCheckpoint].get())) + return true; + return false; + } + + // triangles: reset synchronized checkpoint to last hardened checkpoint + bool ResetSyncCheckpoint() + { + LOCK(cs_hashSyncCheckpoint); + const uint256& hash = mapCheckpoints.rbegin()->second; + if (mapBlockIndex.count(hash) && !mapBlockIndex[hash]->IsInMainChain()) + { + // checkpoint block accepted but not yet in main chain + printf("ResetSyncCheckpoint: SetBestChain to hardened checkpoint %s\n", hash.ToString().c_str()); + auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder; + CBlock block; + if (!block.ReadFromDisk(mapBlockIndex[hash])) + return error("ResetSyncCheckpoint: ReadFromDisk failed for hardened checkpoint %s", hash.ToString().c_str()); + if (!block.SetBestChain(txdb, mapBlockIndex[hash])) + { + return error("ResetSyncCheckpoint: SetBestChain failed for hardened checkpoint %s", hash.ToString().c_str()); + } + } + else if(!mapBlockIndex.count(hash)) + { + // checkpoint block not yet accepted + hashPendingCheckpoint = hash; + checkpointMessagePending.SetNull(); + printf("ResetSyncCheckpoint: pending for sync-checkpoint %s\n", hashPendingCheckpoint.ToString().c_str()); + } + + for (auto it = mapCheckpoints.rbegin(); it != mapCheckpoints.rend(); ++it) + { + const uint256& hash = it->second; + if (mapBlockIndex.count(hash) && mapBlockIndex[hash]->IsInMainChain()) + { + if (!WriteSyncCheckpoint(hash)) + return error("ResetSyncCheckpoint: failed to write sync checkpoint %s", hash.ToString().c_str()); + printf("ResetSyncCheckpoint: sync-checkpoint reset to %s\n", hashSyncCheckpoint.ToString().c_str()); + return true; + } + } + + return false; + } + + void AskForPendingSyncCheckpoint(CNode* pfrom) + { + LOCK(cs_hashSyncCheckpoint); + if (pfrom && hashPendingCheckpoint != 0 && (!mapBlockIndex.count(hashPendingCheckpoint)) && (!mapOrphanBlocks.count(hashPendingCheckpoint))) + pfrom->AskFor(CInv(MSG_BLOCK, hashPendingCheckpoint)); + } + + bool SetCheckpointPrivKey(std::string strPrivKey) + { + // Test signing a sync-checkpoint with genesis block + CSyncCheckpoint checkpoint; + checkpoint.hashCheckpoint = !fTestNet ? hashGenesisBlockOfficial : hashGenesisBlockTestNet; + CDataStream sMsg(SER_NETWORK, PROTOCOL_VERSION); + sMsg << (CUnsignedSyncCheckpoint)checkpoint; + checkpoint.vchMsg = std::vector(sMsg.begin(), sMsg.end()); + + std::vector vchPrivKey = ParseHex(strPrivKey); + CKey key; + key.SetPrivKey(CPrivKey(vchPrivKey.begin(), vchPrivKey.end())); // if key is not correct openssl may crash + if (!key.Sign(Hash(checkpoint.vchMsg.begin(), checkpoint.vchMsg.end()), checkpoint.vchSig)) + return false; + + // Test signing successful, proceed + CSyncCheckpoint::strMasterPrivKey = strPrivKey; + return true; + } + + bool SendSyncCheckpoint(uint256 hashCheckpoint) + { + CSyncCheckpoint checkpoint; + checkpoint.hashCheckpoint = hashCheckpoint; + CDataStream sMsg(SER_NETWORK, PROTOCOL_VERSION); + sMsg << (CUnsignedSyncCheckpoint)checkpoint; + checkpoint.vchMsg = std::vector(sMsg.begin(), sMsg.end()); + + if (CSyncCheckpoint::strMasterPrivKey.empty()) + return error("SendSyncCheckpoint: Checkpoint master key unavailable."); + std::vector vchPrivKey = ParseHex(CSyncCheckpoint::strMasterPrivKey); + CKey key; + key.SetPrivKey(CPrivKey(vchPrivKey.begin(), vchPrivKey.end())); // if key is not correct openssl may crash + if (!key.Sign(Hash(checkpoint.vchMsg.begin(), checkpoint.vchMsg.end()), checkpoint.vchSig)) + return error("SendSyncCheckpoint: Unable to sign checkpoint, check private key?"); + + if(!checkpoint.ProcessSyncCheckpoint(nullptr)) + { + printf("WARNING: SendSyncCheckpoint: Failed to process checkpoint.\n"); + return false; + } + + // Relay checkpoint + { + LOCK(cs_vNodes); + for (CNode* pnode : vNodes) + checkpoint.RelayTo(pnode); + } + return true; + } + + // Is the sync-checkpoint outside maturity window? + bool IsMatureSyncCheckpoint() + { + LOCK(cs_hashSyncCheckpoint); + if (!mapBlockIndex.count(hashSyncCheckpoint)) + return true; // no valid sync checkpoint, treat as mature + const CBlockIndex* pindexSync = mapBlockIndex[hashSyncCheckpoint]; + return (nBestHeight >= pindexSync->nHeight + nCoinbaseMaturity || + pindexSync->GetBlockTime() + nStakeMinAge < GetAdjustedTime()); + } +} + +// triangles: sync-checkpoint master key (DISABLED for decentralization - v5 hard fork) +const std::string CSyncCheckpoint::strMasterPubKey = ""; + +std::string CSyncCheckpoint::strMasterPrivKey = ""; + +// triangles: verify signature of sync-checkpoint message +// Master key system disabled - checkpoint signatures are no longer required +bool CSyncCheckpoint::CheckSignature() +{ + // Deserialize the checkpoint data without signature verification + CDataStream sMsg(vchMsg, SER_NETWORK, PROTOCOL_VERSION); + sMsg >> *(CUnsignedSyncCheckpoint*)this; + return true; +} + +// triangles: process synchronized checkpoint +bool CSyncCheckpoint::ProcessSyncCheckpoint(CNode* pfrom) +{ + if (!CheckSignature()) + return false; + + LOCK(Checkpoints::cs_hashSyncCheckpoint); + if (!mapBlockIndex.count(hashCheckpoint)) + { + // We haven't received the checkpoint chain, keep the checkpoint as pending + Checkpoints::hashPendingCheckpoint = hashCheckpoint; + Checkpoints::checkpointMessagePending = *this; + printf("ProcessSyncCheckpoint: pending for sync-checkpoint %s\n", hashCheckpoint.ToString().c_str()); + // Ask this guy to fill in what we're missing + if (pfrom) + { + pfrom->PushGetBlocks(pindexBest, hashCheckpoint); + // ask directly as well in case rejected earlier by duplicate + // proof-of-stake because getblocks may not get it this time + pfrom->AskFor(CInv(MSG_BLOCK, mapOrphanBlocks.count(hashCheckpoint)? WantedByOrphan(mapOrphanBlocks[hashCheckpoint].get()) : hashCheckpoint)); + } + return false; + } + + if (!Checkpoints::ValidateSyncCheckpoint(hashCheckpoint)) + return false; + + auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder; + CBlockIndex* pindexCheckpoint = mapBlockIndex[hashCheckpoint]; + if (!pindexCheckpoint->IsInMainChain()) + { + // checkpoint chain received but not yet main chain + CBlock block; + if (!block.ReadFromDisk(pindexCheckpoint)) + return error("ProcessSyncCheckpoint: ReadFromDisk failed for sync checkpoint %s", hashCheckpoint.ToString().c_str()); + if (!block.SetBestChain(txdb, pindexCheckpoint)) + { + Checkpoints::hashInvalidCheckpoint = hashCheckpoint; + return error("ProcessSyncCheckpoint: SetBestChain failed for sync checkpoint %s", hashCheckpoint.ToString().c_str()); + } + } + + if (!Checkpoints::WriteSyncCheckpoint(hashCheckpoint)) + return error("ProcessSyncCheckpoint(): failed to write sync checkpoint %s", hashCheckpoint.ToString().c_str()); + Checkpoints::checkpointMessage = *this; + Checkpoints::hashPendingCheckpoint = 0; + Checkpoints::checkpointMessagePending.SetNull(); + printf("ProcessSyncCheckpoint: sync-checkpoint at %s\n", hashCheckpoint.ToString().c_str()); + return true; +} diff --git a/src/clientversion.h b/src/clientversion.h index 2a4f3c0..b8ad4c8 100644 --- a/src/clientversion.h +++ b/src/clientversion.h @@ -1,19 +1,19 @@ -#ifndef CLIENTVERSION_H -#define CLIENTVERSION_H - -// -// client versioning -// - -// These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it -#define CLIENT_VERSION_MAJOR 6 -#define CLIENT_VERSION_MINOR 0 -#define CLIENT_VERSION_REVISION 0 -#define CLIENT_VERSION_BUILD 0 - -// Converts the parameter X to a string after macro replacement on X has been performed. -// Don't merge these into one macro! -#define STRINGIZE(X) DO_STRINGIZE(X) -#define DO_STRINGIZE(X) #X - -#endif // CLIENTVERSION_H +#ifndef CLIENTVERSION_H +#define CLIENTVERSION_H + +// +// client versioning +// + +// These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it +#define CLIENT_VERSION_MAJOR 6 +#define CLIENT_VERSION_MINOR 0 +#define CLIENT_VERSION_REVISION 0 +#define CLIENT_VERSION_BUILD 0 + +// Converts the parameter X to a string after macro replacement on X has been performed. +// Don't merge these into one macro! +#define STRINGIZE(X) DO_STRINGIZE(X) +#define DO_STRINGIZE(X) #X + +#endif // CLIENTVERSION_H diff --git a/src/i2p.cpp b/src/i2p.cpp new file mode 100644 index 0000000..fb365e6 --- /dev/null +++ b/src/i2p.cpp @@ -0,0 +1,466 @@ +// Copyright (c) 2024 Triangles developers +// I2P (SAM v3) transport support +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "i2p.h" + +#include "util.h" +#include "netbase.h" +#include "protocol.h" // CAddress +#include "net.h" // AddI2PInboundNode(), GetListenPort() + +#include + +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +#ifdef WIN32 +#include +#include +#else +#include +#include +#include +#include +#include +#ifndef closesocket +#define closesocket close +#endif +#endif + +// I2P uses a base64 variant where '+' -> '-' and '/' -> '~'. +static const char* pI2PBase64 = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~"; + +static std::vector DecodeI2PBase64(const std::string& str) +{ + int table[256]; + for (int i = 0; i < 256; i++) table[i] = -1; + for (int i = 0; i < 64; i++) table[(unsigned char)pI2PBase64[i]] = i; + + std::vector out; + int bits = 0; uint32_t buf = 0; + for (char c : str) { + if (c == '=' || c == '\r' || c == '\n') continue; + int v = table[(unsigned char)c]; + if (v < 0) continue; // skip anything unexpected + buf = (buf << 6) | v; + bits += 6; + if (bits >= 8) { + bits -= 8; + out.push_back((unsigned char)((buf >> bits) & 0xFF)); + } + } + return out; +} + +CI2PSession* CI2PSession::GetInstance() +{ + static CI2PSession instance; + return &instance; +} + +CI2PSession::CI2PSession() + : samHost(I2P_DEFAULT_SAM_HOST), samPort(I2P_DEFAULT_SAM_PORT), + hSession(INVALID_SOCKET), fEnabled(false), fActive(false), fShutdown(false) +{ +} + +CI2PSession::~CI2PSession() +{ + Stop(); +} + +std::string CI2PSession::GetB32Address() +{ + std::lock_guard lock(cs); + return b32Address; +} + +// --- low level SAM helpers ------------------------------------------------- + +bool CI2PSession::SamConnect(SOCKET& hSocketRet) +{ + SOCKET hSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (hSocket == INVALID_SOCKET) + return false; + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons((unsigned short)samPort); + addr.sin_addr.s_addr = inet_addr(samHost.c_str()); + + if (connect(hSocket, (struct sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) { + closesocket(hSocket); + return false; + } + + hSocketRet = hSocket; + return true; +} + +bool CI2PSession::SamSendLine(SOCKET hSocket, const std::string& strLine) +{ + std::string out = strLine + "\n"; + const char* p = out.c_str(); + size_t left = out.size(); + while (left > 0) { + int n = send(hSocket, p, (int)left, MSG_NOSIGNAL); + if (n <= 0) + return false; + p += n; + left -= n; + } + return true; +} + +bool CI2PSession::SamRecvLine(SOCKET hSocket, std::string& strLineRet) +{ + strLineRet.clear(); + char c; + // SAM replies are newline terminated; read one byte at a time so we stop + // exactly at the boundary and leave any following stream data untouched. + for (int i = 0; i < 16384; i++) { + int n = recv(hSocket, &c, 1, 0); + if (n <= 0) + return false; + if (c == '\n') + return true; + if (c != '\r') + strLineRet += c; + } + return false; +} + +std::string CI2PSession::SamGetValue(const std::string& strReply, const std::string& strKey) +{ + // Tokens are space separated KEY=VALUE pairs. VALUE runs to the next space. + std::string needle = strKey + "="; + size_t pos = strReply.find(needle); + if (pos == std::string::npos) + return ""; + pos += needle.size(); + size_t end = strReply.find(' ', pos); + if (end == std::string::npos) + end = strReply.size(); + return strReply.substr(pos, end - pos); +} + +bool CI2PSession::SamHandshake(SOCKET hSocket) +{ + if (!SamSendLine(hSocket, "HELLO VERSION MIN=3.1 MAX=3.3")) + return false; + std::string reply; + if (!SamRecvLine(hSocket, reply)) + return false; + if (SamGetValue(reply, "RESULT") != "OK") { + printf("I2P: SAM handshake failed: %s\n", reply.c_str()); + return false; + } + return true; +} + +std::string CI2PSession::DestToB32(const std::string& strB64Dest) +{ + std::vector dest = DecodeI2PBase64(strB64Dest); + if (dest.empty()) + return ""; + unsigned char hash[SHA256_DIGEST_LENGTH]; + SHA256(dest.data(), dest.size(), hash); + std::string b32 = EncodeBase32(hash, SHA256_DIGEST_LENGTH); + // I2P b32 addresses are unpadded. + while (!b32.empty() && b32[b32.size() - 1] == '=') + b32.erase(b32.size() - 1); + return b32 + ".b32.i2p"; +} + +// --- session bring-up ------------------------------------------------------ + +bool CI2PSession::LoadOrCreateDestination(std::string& strPrivKeyRet) +{ + fs::path keyPath = GetDataDir() / "i2p_private_key"; + + // Reuse an existing persistent destination if we have one. + { + std::ifstream f(keyPath.string().c_str()); + if (f.is_open()) { + std::string line; + std::getline(f, line); + while (!line.empty() && + (line[line.size() - 1] == '\r' || line[line.size() - 1] == '\n')) + line.erase(line.size() - 1); + if (!line.empty()) { + strPrivKeyRet = line; + printf("I2P: loaded persistent destination from %s\n", + keyPath.string().c_str()); + return true; + } + } + } + + // Generate a fresh destination via the bridge (Ed25519, SIGNATURE_TYPE=7). + SOCKET hSocket = INVALID_SOCKET; + if (!SamConnect(hSocket) || !SamHandshake(hSocket)) { + if (hSocket != INVALID_SOCKET) closesocket(hSocket); + return false; + } + + bool ok = false; + if (SamSendLine(hSocket, "DEST GENERATE SIGNATURE_TYPE=7")) { + std::string reply; + if (SamRecvLine(hSocket, reply)) { + std::string priv = SamGetValue(reply, "PRIV"); + if (!priv.empty()) { + strPrivKeyRet = priv; + std::ofstream out(keyPath.string().c_str(), std::ios::trunc); + if (out.is_open()) { + out << priv << std::endl; + out.close(); + printf("I2P: generated and saved new persistent destination\n"); + ok = true; + } else { + printf("I2P: WARNING could not write %s\n", keyPath.string().c_str()); + ok = true; // still usable for this run + } + } + } + } + closesocket(hSocket); + return ok; +} + +bool CI2PSession::CreateSession() +{ + if (!SamConnect(hSession)) + return false; + if (!SamHandshake(hSession)) + return false; + + std::ostringstream id; + id << "triangles-" << (uint64_t)GetTime() << "-" << (uint64_t)(GetRand(1000000)); + sessionId = id.str(); + + std::string cmd = "SESSION CREATE STYLE=STREAM ID=" + sessionId + + " DESTINATION=" + privateKey + " SIGNATURE_TYPE=7"; + if (!SamSendLine(hSession, cmd)) + return false; + + std::string reply; + if (!SamRecvLine(hSession, reply)) + return false; + + if (SamGetValue(reply, "RESULT") != "OK") { + printf("I2P: SESSION CREATE failed: %s\n", reply.c_str()); + return false; + } + + // The bridge echoes the (possibly newly assigned) private key back. + std::string echoed = SamGetValue(reply, "DESTINATION"); + if (!echoed.empty()) + privateKey = echoed; + + return true; +} + +bool CI2PSession::ResolveMyB32() +{ + SOCKET hSocket = INVALID_SOCKET; + if (!SamConnect(hSocket) || !SamHandshake(hSocket)) { + if (hSocket != INVALID_SOCKET) closesocket(hSocket); + return false; + } + + bool ok = false; + if (SamSendLine(hSocket, "NAMING LOOKUP NAME=ME")) { + std::string reply; + if (SamRecvLine(hSocket, reply) && SamGetValue(reply, "RESULT") == "OK") { + std::string dest = SamGetValue(reply, "VALUE"); + std::string b32 = DestToB32(dest); + if (!b32.empty()) { + std::lock_guard lock(cs); + b32Address = b32; + ok = true; + } + } + } + closesocket(hSocket); + return ok; +} + +bool CI2PSession::Start() +{ + if (!GetBoolArg("-i2p", true)) { + printf("I2P: disabled (-i2p=0)\n"); + return false; + } + fEnabled.store(true); + + // -i2psam=host:port overrides the default SAM bridge endpoint. + std::string sam = GetArg("-i2psam", ""); + if (!sam.empty()) { + int port = I2P_DEFAULT_SAM_PORT; + std::string host; + SplitHostPort(sam, port, host); + if (!host.empty()) samHost = host; + if (port > 0) samPort = port; + } + + printf("I2P: connecting to SAM bridge at %s:%d\n", samHost.c_str(), samPort); + + if (!LoadOrCreateDestination(privateKey)) { + printf("I2P: ERROR could not obtain a destination. Is an I2P router with " + "the SAM bridge enabled running at %s:%d?\n", samHost.c_str(), samPort); + return false; + } + + if (!CreateSession()) { + printf("I2P: ERROR failed to create SAM STREAM session\n"); + if (hSession != INVALID_SOCKET) { closesocket(hSession); hSession = INVALID_SOCKET; } + return false; + } + + if (!ResolveMyB32()) + printf("I2P: WARNING could not resolve our own .b32.i2p address yet\n"); + + fActive.store(true); + fShutdown.store(false); + + printf("I2P: session active. Our address: %s\n", GetB32Address().c_str()); + + // Register our I2P address as a local address so peers can learn it. + CService meI2P; + if (!b32Address.empty() && meI2P.SetSpecial(b32Address)) { + meI2P.SetPort((unsigned short)GetListenPort()); + AddLocal(meI2P, LOCAL_MANUAL); + } + + acceptThread = std::thread(&CI2PSession::AcceptLoop, this); + return true; +} + +void CI2PSession::Stop() +{ + if (!fEnabled.load()) + return; + fShutdown.store(true); + fActive.store(false); + + if (hSession != INVALID_SOCKET) { + closesocket(hSession); + hSession = INVALID_SOCKET; + } + if (acceptThread.joinable()) + acceptThread.join(); + fEnabled.store(false); + printf("I2P: session stopped\n"); +} + +// --- inbound --------------------------------------------------------------- + +void CI2PSession::AcceptLoop() +{ + while (!fShutdown.load()) { + SOCKET hSocket = INVALID_SOCKET; + if (!SamConnect(hSocket) || !SamHandshake(hSocket)) { + if (hSocket != INVALID_SOCKET) closesocket(hSocket); + if (fShutdown.load()) break; + MilliSleep(2000); + continue; + } + + // Block here until a peer dials us; the router then streams the remote + // destination on its own line, after which the socket carries data. + if (!SamSendLine(hSocket, "STREAM ACCEPT ID=" + sessionId + " SILENT=false")) { + closesocket(hSocket); + MilliSleep(1000); + continue; + } + + std::string status; + if (!SamRecvLine(hSocket, status) || SamGetValue(status, "RESULT") != "OK") { + if (!fShutdown.load()) + printf("I2P: STREAM ACCEPT rejected: %s\n", status.c_str()); + closesocket(hSocket); + MilliSleep(1000); + continue; + } + + std::string remoteDest; + if (!SamRecvLine(hSocket, remoteDest)) { + closesocket(hSocket); + continue; + } + if (fShutdown.load()) { + closesocket(hSocket); + break; + } + + // The first token is the remote full destination (base64). + std::string destTok = remoteDest; + size_t sp = destTok.find(' '); + if (sp != std::string::npos) + destTok = destTok.substr(0, sp); + + std::string b32 = DestToB32(destTok); + CAddress addr; + if (b32.empty() || !addr.SetSpecial(b32)) { + printf("I2P: could not parse inbound remote destination\n"); + closesocket(hSocket); + continue; + } + addr.nServices = 0; + addr.nTime = GetTime(); + + // Hand the live data socket to the net layer as an inbound peer. + printf("I2P: inbound connection from %s\n", b32.c_str()); + AddI2PInboundNode(hSocket, addr); + } +} + +// --- outbound -------------------------------------------------------------- + +bool CI2PSession::Connect(const std::string& strDest, SOCKET& hSocketRet) +{ + if (!fActive.load()) + return false; + + SOCKET hSocket = INVALID_SOCKET; + if (!SamConnect(hSocket) || !SamHandshake(hSocket)) { + if (hSocket != INVALID_SOCKET) closesocket(hSocket); + return false; + } + + if (!SamSendLine(hSocket, "STREAM CONNECT ID=" + sessionId + + " DESTINATION=" + strDest + " SILENT=false")) { + closesocket(hSocket); + return false; + } + + std::string status; + if (!SamRecvLine(hSocket, status) || SamGetValue(status, "RESULT") != "OK") { + printf("I2P: STREAM CONNECT to %s failed: %s\n", strDest.c_str(), status.c_str()); + closesocket(hSocket); + return false; + } + + // Socket is now a bidirectional stream to the peer. + hSocketRet = hSocket; + return true; +} + +bool StartI2P() +{ + return CI2PSession::GetInstance()->Start(); +} + +void StopI2P() +{ + CI2PSession::GetInstance()->Stop(); +} diff --git a/src/i2p.h b/src/i2p.h new file mode 100644 index 0000000..ae35516 --- /dev/null +++ b/src/i2p.h @@ -0,0 +1,95 @@ +// Copyright (c) 2024 Triangles developers +// I2P (SAM v3) transport support +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. +// +// This module gives Triangles real I2P connectivity that mirrors the existing +// embedded-Tor design: instead of a SOCKS proxy it talks the SAM v3 protocol +// to a locally running I2P router (i2pd or Java I2P) and obtains a persistent +// I2P destination whose ".b32.i2p" address is shown alongside the .onion +// address. The wallet: +// * creates / loads a persistent destination (i2p_private_key in datadir), +// * runs a STREAM session so peers can dial us, +// * accepts inbound I2P streams and feeds them to the net layer, +// * dials outbound ".b32.i2p" peers through the same session. +// +// A running I2P router with its SAM bridge enabled (default 127.0.0.1:7656) is +// required; nothing is bundled. Enable with -i2p and optionally -i2psam=host:port. + +#ifndef TRIANGLES_I2P_H +#define TRIANGLES_I2P_H + +#include +#include +#include +#include + +#include "compat.h" // SOCKET / INVALID_SOCKET + +// Default SAM bridge endpoint exposed by i2pd / Java I2P. +#define I2P_DEFAULT_SAM_HOST "127.0.0.1" +#define I2P_DEFAULT_SAM_PORT 7656 + +// Manages a single persistent I2P STREAM session over SAM v3. +class CI2PSession +{ +public: + static CI2PSession* GetInstance(); + + // Bring the session up: connect to the SAM bridge, load/generate the + // persistent destination and start accepting inbound streams. + // Returns false (and logs) if no router/SAM bridge is reachable. + bool Start(); + + // Tear the session down and stop the accept loop. + void Stop(); + + bool IsEnabled() const { return fEnabled.load(); } + bool IsActive() const { return fActive.load(); } + + // Our own ".b32.i2p" address (empty until the session is up). + std::string GetB32Address(); + + // Dial a remote ".b32.i2p" (or full base64 destination) through the + // session. On success hSocketRet is a connected, blocking data socket the + // caller can hand to a CNode. The caller takes ownership of the socket. + bool Connect(const std::string& strDest, SOCKET& hSocketRet); + +private: + CI2PSession(); + ~CI2PSession(); + + // --- low level SAM helpers --- + bool SamConnect(SOCKET& hSocketRet); // raw TCP to the bridge + bool SamHandshake(SOCKET hSocket); // HELLO VERSION + bool SamSendLine(SOCKET hSocket, const std::string& strLine); + bool SamRecvLine(SOCKET hSocket, std::string& strLineRet); + static std::string SamGetValue(const std::string& strReply, const std::string& strKey); + + bool LoadOrCreateDestination(std::string& strPrivKeyRet); + bool CreateSession(); // SESSION CREATE + bool ResolveMyB32(); // NAMING LOOKUP ME + void AcceptLoop(); // inbound STREAM ACCEPT + + // Compute the ".b32.i2p" address from a base64 (I2P alphabet) destination. + static std::string DestToB32(const std::string& strB64Dest); + + std::string samHost; + int samPort; + std::string sessionId; + std::string privateKey; // persistent destination private key (base64) + std::string b32Address; // our own .b32.i2p + SOCKET hSession; // long-lived control socket owning the session + + std::atomic fEnabled; + std::atomic fActive; + std::atomic fShutdown; + std::thread acceptThread; + std::mutex cs; +}; + +// Convenience: start/stop from init.cpp. +bool StartI2P(); +void StopI2P(); + +#endif // TRIANGLES_I2P_H diff --git a/src/i2p_process.cpp b/src/i2p_process.cpp new file mode 100644 index 0000000..bc037ab --- /dev/null +++ b/src/i2p_process.cpp @@ -0,0 +1,368 @@ +// Copyright (c) 2024 Triangles developers +// I2P Router Process Manager - launches and manages a bundled i2pd binary +// Distributed under the MIT/X11 software license + +#ifdef WIN32 +#define NOMINMAX +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0600 +#endif +#endif + +#include "i2p_process.h" +#include "util.h" + +#include +#include +#include +#include + +#ifdef WIN32 +#include +#include +#include +#include +#else +#include +#include +#include +#include +#include +#include +#include +#endif + +namespace fs = std::filesystem; + +static CI2PProcess* i2pProcessInstance = nullptr; + +CI2PProcess* CI2PProcess::GetInstance() +{ + if (!i2pProcessInstance) + i2pProcessInstance = new CI2PProcess(); + return i2pProcessInstance; +} + +CI2PProcess::CI2PProcess() + : samPort(7656) + , running(false) + , fExternal(false) +#ifdef WIN32 + , hProcess(nullptr) + , hJob(nullptr) + , processId(0) +#else + , processId(0) +#endif +{ +} + +CI2PProcess::~CI2PProcess() +{ + Stop(); +} + +// Try a quick TCP connect; success means something is already listening +// (e.g. the SAM bridge is up, or an external router is running). +bool CI2PProcess::CanConnect(const std::string& host, int port) +{ +#ifdef WIN32 + SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (s == INVALID_SOCKET) return false; +#else + int s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (s < 0) return false; +#endif + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons((unsigned short)port); + addr.sin_addr.s_addr = inet_addr(host.c_str()); + bool ok = (connect(s, (struct sockaddr*)&addr, sizeof(addr)) == 0); +#ifdef WIN32 + closesocket(s); +#else + close(s); +#endif + return ok; +} + +std::string CI2PProcess::FindI2pdBinary() +{ + std::vector candidates; + +#ifdef WIN32 + const char* exeName = "i2pd.exe"; +#else + const char* exeName = "i2pd"; +#endif + + // 1. Next to the wallet executable (this is how tor.exe is shipped). + try { + fs::path exeDir; +#ifdef WIN32 + char buf[MAX_PATH]; + if (GetModuleFileNameA(nullptr, buf, MAX_PATH) > 0) + exeDir = fs::path(buf).parent_path(); +#else + exeDir = fs::current_path(); +#endif + if (!exeDir.empty()) { + candidates.push_back((exeDir / exeName).string()); + candidates.push_back((exeDir / "i2pd" / exeName).string()); + candidates.push_back((exeDir / "I2P" / exeName).string()); + } + } catch (...) {} + + // 2. In / next to the data directory. + candidates.push_back((GetDataDir() / exeName).string()); + candidates.push_back((GetDataDir() / "i2pd" / exeName).string()); + + // 3. Common system locations. +#ifdef WIN32 + if (const char* pf = getenv("ProgramFiles")) + candidates.push_back(std::string(pf) + "\\i2pd\\" + exeName); + if (const char* pfx = getenv("ProgramFiles(x86)")) + candidates.push_back(std::string(pfx) + "\\i2pd\\" + exeName); + candidates.push_back(std::string("C:\\i2pd\\") + exeName); +#else + candidates.push_back("/usr/bin/i2pd"); + candidates.push_back("/usr/local/bin/i2pd"); + candidates.push_back("/opt/i2pd/bin/i2pd"); + candidates.push_back("/opt/homebrew/bin/i2pd"); + candidates.push_back("/usr/local/opt/i2pd/bin/i2pd"); +#endif + + for (const std::string& c : candidates) { + try { + if (fs::exists(c) && fs::is_regular_file(c)) { + printf("I2P: found i2pd binary at %s\n", c.c_str()); + return c; + } + } catch (...) {} + } + + return ""; +} + +bool CI2PProcess::WriteConfig() +{ + fs::path dir(dataDir); + try { + fs::create_directories(dir); + } catch (const std::exception& e) { + lastError = std::string("Cannot create i2pd data directory: ") + e.what(); + return false; + } + + confPath = (dir / "i2pd.conf").string(); + fs::path logPath = dir / "i2pd.log"; + + std::ofstream conf(confPath.c_str(), std::ios::trunc); + if (!conf.is_open()) { + lastError = "Cannot write i2pd.conf to " + confPath; + return false; + } + + conf << "# Triangles Wallet I2P configuration (auto-generated)\n"; + conf << "# Do not edit - this file is overwritten on startup\n\n"; + conf << "daemon = false\n"; + conf << "log = file\n"; + conf << "logfile = " << logPath.string() << "\n"; + conf << "datadir = " << dir.string() << "\n\n"; + + // The bridge our SAM client talks to. + conf << "[sam]\n"; + conf << "enabled = true\n"; + conf << "address = 127.0.0.1\n"; + conf << "port = " << samPort << "\n\n"; + + // We only need SAM; keep everything else off to minimise footprint. + conf << "[httpproxy]\nenabled = false\n\n"; + conf << "[socksproxy]\nenabled = false\n\n"; + conf << "[http]\nenabled = false\n\n"; + conf << "[i2pcontrol]\nenabled = false\n"; + + conf.close(); + printf("I2P: wrote i2pd config to %s (SAM port %d)\n", confPath.c_str(), samPort); + return true; +} + +bool CI2PProcess::Start(const std::string& dataDirIn, int samPortIn) +{ + dataDir = dataDirIn; + samPort = samPortIn; + fExternal = false; + lastError.clear(); + + // If a SAM bridge is already up, use it instead of launching our own. + if (CanConnect("127.0.0.1", samPort)) { + printf("I2P: detected an I2P router already listening on SAM port %d; using it\n", samPort); + fExternal = true; + return true; + } + + binaryPath = FindI2pdBinary(); + if (binaryPath.empty()) { + lastError = "No i2pd binary found (ship i2pd alongside the wallet, like tor)"; + printf("I2P: %s\n", lastError.c_str()); + return false; + } + + if (!WriteConfig()) + return false; + + printf("I2P: starting i2pd: %s --conf %s\n", binaryPath.c_str(), confPath.c_str()); + +#ifdef WIN32 + STARTUPINFOA si; + PROCESS_INFORMATION pi; + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + si.dwFlags = STARTF_USESHOWWINDOW; + si.wShowWindow = SW_HIDE; + ZeroMemory(&pi, sizeof(pi)); + + std::string cmdLine = "\"" + binaryPath + "\" --conf \"" + confPath + "\""; + + if (!CreateProcessA(nullptr, (LPSTR)cmdLine.c_str(), nullptr, nullptr, + FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) { + DWORD err = ::GetLastError(); + lastError = strprintf("CreateProcess failed for i2pd '%s' (Windows error %lu)", binaryPath.c_str(), err); + printf("I2P: ERROR %s\n", lastError.c_str()); + return false; + } + + hProcess = pi.hProcess; + processId = pi.dwProcessId; + CloseHandle(pi.hThread); + + // Kill i2pd if the wallet dies (matches the embedded Tor behaviour). + hJob = CreateJobObject(nullptr, nullptr); + if (hJob) { + JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = {}; + jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &jobInfo, sizeof(jobInfo)); + if (!AssignProcessToJobObject(hJob, hProcess)) + printf("I2P: WARNING could not assign i2pd to Job Object (error %lu)\n", GetLastError()); + } + + printf("I2P: i2pd started (PID %lu)\n", processId); +#else + pid_t pid = fork(); + if (pid < 0) { + lastError = "Failed to fork for i2pd process"; + printf("I2P: ERROR %s\n", lastError.c_str()); + return false; + } + if (pid == 0) { + freopen("/dev/null", "w", stdout); + freopen("/dev/null", "w", stderr); + execl(binaryPath.c_str(), binaryPath.c_str(), + "--conf", confPath.c_str(), (char*)nullptr); + _exit(1); + } + processId = pid; + printf("I2P: i2pd started (PID %d)\n", processId); +#endif + + running = true; + + // Wait for the SAM bridge to come up. The bridge opens quickly; tunnel + // build (needed for actual connectivity) continues in the background. + printf("I2P: waiting for SAM bridge on port %d...\n", samPort); + for (int i = 0; i < 45; i++) { + MilliSleep(1000); + if (fShutdown) { + Stop(); + return false; + } + if (CanConnect("127.0.0.1", samPort)) { + printf("I2P: SAM bridge ready on port %d (took %ds)\n", samPort, i + 1); + return true; + } + if (!IsRunning()) { + lastError = "i2pd exited during start-up before the SAM bridge became ready"; + printf("I2P: ERROR %s\n", lastError.c_str()); + running = false; + return false; + } + } + + lastError = strprintf("i2pd started but SAM port %d not ready after 45s", samPort); + printf("I2P: WARNING %s (it may still be building tunnels)\n", lastError.c_str()); + return true; +} + +void CI2PProcess::Stop() +{ + if (fExternal) { + // We never launched it; leave the user's router running. + running = false; + return; + } + if (!running) return; + +#ifdef WIN32 + if (hProcess != nullptr) { + printf("I2P: stopping i2pd (PID %lu)...\n", processId); + TerminateProcess(hProcess, 0); + WaitForSingleObject(hProcess, 5000); + CloseHandle(hProcess); + hProcess = nullptr; + } + if (hJob != nullptr) { + CloseHandle(hJob); + hJob = nullptr; + } +#else + if (processId > 0) { + printf("I2P: stopping i2pd (PID %d)...\n", processId); + kill(processId, SIGTERM); + for (int i = 0; i < 50; i++) { + int status; + pid_t result = waitpid(processId, &status, WNOHANG); + if (result != 0) break; + MilliSleep(100); + } + kill(processId, SIGKILL); + waitpid(processId, nullptr, 0); + } +#endif + + processId = 0; + running = false; + printf("I2P: i2pd stopped\n"); +} + +bool CI2PProcess::IsRunning() +{ + if (fExternal) return true; + if (!running) return false; + +#ifdef WIN32 + if (hProcess == nullptr) return false; + DWORD exitCode; + if (GetExitCodeProcess(hProcess, &exitCode)) + return (exitCode == STILL_ACTIVE); + return false; +#else + if (processId <= 0) return false; + int status; + pid_t result = waitpid(processId, &status, WNOHANG); + return (result == 0); // 0 => still running +#endif +} + +bool StartEmbeddedI2P(const std::string& dataDir, int samPort) +{ + return CI2PProcess::GetInstance()->Start(dataDir, samPort); +} + +void StopEmbeddedI2P() +{ + CI2PProcess::GetInstance()->Stop(); +} diff --git a/src/i2p_process.h b/src/i2p_process.h new file mode 100644 index 0000000..05e4a5f --- /dev/null +++ b/src/i2p_process.h @@ -0,0 +1,70 @@ +// Copyright (c) 2024 Triangles developers +// I2P Router Process Manager - launches and manages a bundled i2pd binary +// Distributed under the MIT/X11 software license +// +// Mirrors tor_process.cpp: locate an i2pd executable shipped alongside the +// wallet (or installed on the system), write an auto-generated config that +// enables the SAM bridge, launch it as a managed child process, and shut it +// down when the wallet exits. The SAM session in i2p.cpp then connects to it, +// so the user does not have to install or run a separate I2P router. + +#ifndef TRIANGLES_I2P_PROCESS_H +#define TRIANGLES_I2P_PROCESS_H + +#include + +#ifdef WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + +class CI2PProcess +{ +public: + static CI2PProcess* GetInstance(); + + CI2PProcess(); + ~CI2PProcess(); + + // Bring up the router. If something is already listening on the SAM port we + // assume an external router and do not launch our own (fExternal=true). + // Returns true if a SAM bridge is (or will shortly be) reachable. + bool Start(const std::string& dataDir, int samPort = 7656); + + // Terminate the launched router (no-op for an external one). + void Stop(); + + bool IsRunning(); + bool IsExternal() const { return fExternal; } + std::string GetLastError() const { return lastError; } + std::string GetBinaryPath() const { return binaryPath; } + +private: + std::string FindI2pdBinary(); + bool WriteConfig(); + static bool CanConnect(const std::string& host, int port); + + int samPort; + bool running; + bool fExternal; + std::string dataDir; + std::string binaryPath; + std::string confPath; + std::string lastError; + +#ifdef WIN32 + HANDLE hProcess; + HANDLE hJob; + DWORD processId; +#else + int processId; +#endif +}; + +// Convenience wrappers for init.cpp. +bool StartEmbeddedI2P(const std::string& dataDir, int samPort); +void StopEmbeddedI2P(); + +#endif // TRIANGLES_I2P_PROCESS_H diff --git a/src/init.cpp b/src/init.cpp index 6f0cb32..6b04ea8 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -340,6 +340,10 @@ void Shutdown(void* parg) pScriptCheckQueue.reset(); } + // Stop the I2P SAM session and its accept loop, then the i2pd router. + StopI2P(); + StopEmbeddedI2P(); + // NOW safe to destroy Tor state - all threads have stopped ShutdownTorV3(); StopEmbeddedTor(); @@ -1670,6 +1674,45 @@ bool AppInit2() if (!NewThread(ThreadTorMaintenance, nullptr)) printf("Warning: ThreadTorMaintenance could not be started\n"); } + + // Bring up I2P (SAM) transport alongside Tor so the wallet has both a + // .onion and a .b32.i2p address. On by default; disable with -i2p=0. + // A bundled i2pd router is launched automatically (mirroring embedded + // Tor); if -i2psam points at a non-loopback bridge, or a router is + // already running, we use that instead. + if (GetBoolArg("-i2p", true)) { + int64_t nI2PStart = GetTimeMillis(); + + // Resolve the SAM endpoint (default 127.0.0.1:7656). + std::string sam = GetArg("-i2psam", "127.0.0.1:7656"); + int samPort = I2P_DEFAULT_SAM_PORT; + std::string samHost = "127.0.0.1"; + SplitHostPort(sam, samPort, samHost); + if (samPort <= 0) samPort = I2P_DEFAULT_SAM_PORT; + bool loopback = samHost.empty() || samHost == "127.0.0.1" || samHost == "localhost"; + + // Auto-launch our own i2pd only when the bridge is local. + if (loopback) { + uiInterface.InitMessage(_("Starting the I2P router...")); + if (!StartEmbeddedI2P((GetDataDir() / "i2pd").string(), samPort)) { + printf("NOTICE: bundled I2P router unavailable (%s).\n", + CI2PProcess::GetInstance()->GetLastError().c_str()); + printf(" I2P will use an external router if one is running on %s.\n", sam.c_str()); + } + } + + uiInterface.InitMessage(_("Connecting to the I2P network...")); + bool i2pStarted = StartI2P(); + StartupPerfLog("i2p_start", GetTimeMillis() - nI2PStart, strprintf("started=%d", i2pStarted)); + if (i2pStarted) { + SetReachable(NET_I2P, true); + std::string i2pAddr = CI2PSession::GetInstance()->GetB32Address(); + printf("I2P network enabled. Our address: %s\n", i2pAddr.c_str()); + } else { + printf("NOTICE: I2P not available this session; continuing with Tor only\n"); + StopEmbeddedI2P(); + } + } } // ********************************************************* Step 9: import blocks diff --git a/src/main.cpp b/src/main.cpp index 489b72a..2f10b40 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4010,6 +4010,290 @@ bool LoadExternalBlockFile(FILE* fileIn) return nLoaded > 0; } + +bool FastImportBlockFile() +{ + // Fast block import: reads blk0001.dat and builds the block index + // directly without re-writing block data. LevelDB writes are batched + // every 200K blocks for speed. Only used for trusted bootstrap data + // (blocks below the hardcoded checkpoint). + + fs::path blkPath = GetDataDir() / "blk0001.dat"; + if (!fs::exists(blkPath)) + return false; + + printf("FastImportBlockFile: starting from %s\n", blkPath.string().c_str()); + int64_t nStart = GetTimeMillis(); + + FILE* fileIn = fopen(blkPath.string().c_str(), "rb"); + if (!fileIn) + return false; + + // Get file size for progress + fseek(fileIn, 0, SEEK_END); + int64_t nFileSize = ftell(fileIn); + fseek(fileIn, 0, SEEK_SET); + + int nLoaded = 0; + int64_t nLastProgressReport = 0; + + { + LOCK(cs_main); + CAutoFile blkdat(fileIn, SER_DISK, CLIENT_VERSION); + + auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder; + txdb.TxnBegin(); + + unsigned int nPos = 0; + while (nPos != (unsigned int)-1 && blkdat.good() && !fRequestShutdown) + { + // Find message start bytes (same scan as LoadExternalBlockFile) + unsigned char pchData[65536]; + do { + fseek(blkdat, nPos, SEEK_SET); + int nRead = fread(pchData, 1, sizeof(pchData), blkdat); + if (nRead <= 8) + { + nPos = (unsigned int)-1; + break; + } + void* nFind = memchr(pchData, pchMessageStart[0], nRead+1-sizeof(pchMessageStart)); + if (nFind) + { + if (memcmp(nFind, pchMessageStart, sizeof(pchMessageStart))==0) + { + nPos += ((unsigned char*)nFind - pchData) + sizeof(pchMessageStart); + break; + } + nPos += ((unsigned char*)nFind - pchData) + 1; + } + else + nPos += sizeof(pchData) - sizeof(pchMessageStart) + 1; + } while(!fRequestShutdown); + + if (nPos == (unsigned int)-1) + break; + + fseek(blkdat, nPos, SEEK_SET); + unsigned int nSize; + blkdat >> nSize; + + if (nSize == 0 || nSize > MAX_BLOCK_SIZE) + { + nPos += 4 + nSize; + continue; + } + + // nBlockPos = file position where the block data starts + // (after 4-byte message start + 4-byte size) + unsigned int nBlockPos = nPos + 4; + + CBlock block; + blkdat >> block; + + uint256 hash = block.GetHash(); + if (mapBlockIndex.count(hash)) + { + nPos += 4 + nSize; + continue; // already indexed + } + + // Create CBlockIndex + CBlockIndex* pindexNew = new CBlockIndex(1, nBlockPos, block); + if (!pindexNew) + break; + + // Link to previous block + auto miPrev = mapBlockIndex.find(block.hashPrevBlock); + if (miPrev != mapBlockIndex.end()) + { + pindexNew->pprev = miPrev->second; + pindexNew->nHeight = pindexNew->pprev->nHeight + 1; + } + + // Chain trust + pindexNew->nChainTrust = (pindexNew->pprev ? pindexNew->pprev->nChainTrust : 0) + pindexNew->GetBlockTrust(); + + // Stake entropy bit + pindexNew->SetStakeEntropyBit(block.GetStakeEntropyBit()); + + // Stake modifier (minimal for blocks far below checkpoint) + int nCheckpointHeight = Checkpoints::GetTotalBlocksEstimate(); + if (pindexNew->nHeight >= nCheckpointHeight - 1000) + { + uint64_t nStakeModifier = 0; + bool fGeneratedStakeModifier = false; + ComputeNextStakeModifier(pindexNew->pprev, nStakeModifier, fGeneratedStakeModifier); + pindexNew->SetStakeModifier(nStakeModifier, fGeneratedStakeModifier); + } + else + { + pindexNew->SetStakeModifier(0, pindexNew->nHeight == 0); + } + pindexNew->nStakeModifierChecksum = GetStakeModifierChecksum(pindexNew); + + // PoS stake seen set + if (pindexNew->IsProofOfStake()) + setStakeSeen.insert(make_pair(pindexNew->prevoutStake, pindexNew->nStakeTime)); + + // Insert into mapBlockIndex + auto mi = mapBlockIndex.insert(make_pair(hash, pindexNew)).first; + pindexNew->phashBlock = &mi->first; + + // Link pnext for previous block + if (pindexNew->pprev) + pindexNew->pprev->pnext = pindexNew; + + // NOTE: tx-index, UTXO-set and money-supply application are + // DEFERRED to a second pass over the active (best-trust) chain + // only — see the pass after this loop. Applying them here, for + // every block read from the file (which permanently retains + // ORPHANED side-chain blocks), wrote those orphans' outputs into + // the UTXO set as phantom coins and over-counted nMoneySupply. + // That was the root cause of UTXO-set / supply inflation on every + // reindex. Here we only build the block index for all blocks so + // best-chain selection by trust still works. + txdb.WriteBlockIndex(CDiskBlockIndex(pindexNew)); + + // Update best chain + if (pindexNew->nChainTrust > nBestChainTrust) + { + hashBestChain = hash; + pindexBest = pindexNew; + pblockindexFBBHLast = nullptr; + nBestHeight = pindexNew->nHeight; + nBestChainTrust = pindexNew->nChainTrust; + nTimeBestReceived = GetTime(); + } + + // Set genesis block + if (pindexGenesisBlock == nullptr && pindexNew->nHeight == 0) + pindexGenesisBlock = pindexNew; + + nLoaded++; + nPos += 4 + nSize; + + // Batch commit every 200K blocks for LevelDB efficiency + if (nLoaded % 200000 == 0) + { + txdb.WriteHashBestChain(hashBestChain); + txdb.TxnCommit(); + txdb.TxnBegin(); + } + + // Report progress every 5000 blocks to keep GUI responsive. + // AppInit2 runs on the GUI thread, so uiInterface.InitMessage + // triggers processEvents() which prevents the window from freezing. + if (nLoaded % 5000 == 0) + { + int pct = (nFileSize > 0) ? (int)((int64_t)nPos * 100 / nFileSize) : 0; + printf("FastImport: %d blocks indexed (%d%%)\n", nLoaded, pct); + uiInterface.InitMessage(strprintf(_("Importing blocks... %d indexed (%d%%)"), nLoaded, pct)); + } + } + + // ---- Pass 2: apply tx-index, UTXO set and money supply along the + // ACTIVE (best-trust) chain ONLY. The file-order pass above indexed + // every block including orphaned side-chain blocks; replaying only + // the main chain here keeps the UTXO set and money supply exactly in + // consensus and prevents orphan outputs becoming phantom coins. ---- + if (pindexBest) + { + std::vector vMain; + for (CBlockIndex* p = pindexBest; p; p = p->pprev) + vMain.push_back(p); + std::reverse(vMain.begin(), vMain.end()); + printf("FastImportBlockFile: applying UTXO/supply along %d main-chain blocks...\n", (int)vMain.size()); + uiInterface.InitMessage(_("Building UTXO set (main chain)...")); + + int64_t nRunningSupply = 0; + int nApplied = 0; + for (CBlockIndex* pindex : vMain) + { + // Genesis (height 0) is a hardcoded special block that is not + // re-read from disk this way; it contributes nothing to supply + // and the genesis-walk audit skips it identically. Carry the + // running supply (0) forward and move on. + if (pindex->nHeight == 0) + { + pindex->nMint = 0; + pindex->nMoneySupply = nRunningSupply; // still 0 here + txdb.WriteBlockIndex(CDiskBlockIndex(pindex)); + continue; + } + + CBlock blockMain; + if (!blockMain.ReadFromDisk(pindex)) + return error("FastImportBlockFile: ReadFromDisk failed at height %d", pindex->nHeight); + + int64_t nBlockValueIn = 0; + int64_t nBlockValueOut = 0; + unsigned int nTxPos2 = pindex->nBlockPos + ::GetSerializeSize(CBlock(), SER_DISK, CLIENT_VERSION) + - (2 * GetSizeOfCompactSize(0)) + GetSizeOfCompactSize(blockMain.vtx.size()); + for (const CTransaction& tx : blockMain.vtx) + { + uint256 hashTx = tx.GetHash(); + CDiskTxPos posThisTx(1, pindex->nBlockPos, nTxPos2); + txdb.UpdateTxIndex(hashTx, CTxIndex(posThisTx, tx.vout.size())); + nTxPos2 += ::GetSerializeSize(tx, SER_DISK, CLIENT_VERSION); + + nBlockValueOut += tx.GetValueOut(); + if (!tx.IsCoinBase()) + { + for (const CTxIn& txin : tx.vin) + { + CUtxoEntry uprev; + if (txdb.ReadUtxo(txin.prevout.hash, txin.prevout.n, uprev)) + nBlockValueIn += uprev.nValue; + txdb.EraseUtxo(txin.prevout.hash, txin.prevout.n); + } + } + for (unsigned int k = 0; k < tx.vout.size(); k++) + { + if (tx.vout[k].IsEmpty()) + continue; + CUtxoEntry utxo; + utxo.nValue = tx.vout[k].nValue; + utxo.nHeight = pindex->nHeight; + utxo.scriptPubKey = tx.vout[k].scriptPubKey; + utxo.fCoinBase = tx.IsCoinBase(); + utxo.fCoinStake = tx.IsCoinStake(); + utxo.nTxTime = tx.nTime; + txdb.WriteUtxo(hashTx, k, utxo); + } + } + + pindex->nMint = nBlockValueOut - nBlockValueIn; + nRunningSupply += (nBlockValueOut - nBlockValueIn); + pindex->nMoneySupply = nRunningSupply; + txdb.WriteBlockIndex(CDiskBlockIndex(pindex)); + + if (++nApplied % 200000 == 0) { txdb.TxnCommit(); txdb.TxnBegin(); } + if (nApplied % 5000 == 0) + { + int pct2 = (int)((int64_t)nApplied * 100 / (vMain.empty() ? 1 : vMain.size())); + printf("FastImport UTXO apply: %d/%d main-chain blocks (%d%%)\n", nApplied, (int)vMain.size(), pct2); + uiInterface.InitMessage(strprintf(_("Building UTXO set... %d%%"), pct2)); + } + } + } + + // Final commit + if (pindexBest) + { + txdb.WriteHashBestChain(hashBestChain); + + // Write sync checkpoint + Checkpoints::WriteSyncCheckpoint(hashBestChain); + } + txdb.TxnCommit(); + } + + nTransactionsUpdated++; + printf("FastImportBlockFile: indexed %d blocks in %" PRId64 "ms\n", nLoaded, GetTimeMillis() - nStart); + return nLoaded > 0; +} + string GetWarnings(string strFor) { string strStatusBar; diff --git a/src/net.cpp b/src/net.cpp index 6eaf862..6377d5b 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -591,6 +591,16 @@ CNode* ConnectNode(CAddress addrConnect, const char *pszDest) return nullptr; } + // For I2P make sure addrConnect carries the destination so the resulting + // CNode is labelled correctly even when we were given a bare pszDest. + if (fI2P && !addrConnect.IsI2P()) { + std::string i2pHost = addrStr; + size_t i2pEnd = i2pHost.find(".i2p"); + if (i2pEnd != std::string::npos) + i2pHost = i2pHost.substr(0, i2pEnd + 4); + addrConnect.SetSpecial(i2pHost); + } + if (pszDest == nullptr) { if (IsLocal(addrConnect)) return nullptr; @@ -614,8 +624,20 @@ CNode* ConnectNode(CAddress addrConnect, const char *pszDest) pszDest ? 0 : (double)(GetAdjustedTime() - addrConnect.nTime)/3600.0); // Connect - SOCKET hSocket; - if (pszDest ? ConnectSocketByName(addrConnect, hSocket, pszDest, GetDefaultPort()) : ConnectSocket(addrConnect, hSocket)) + SOCKET hSocket = INVALID_SOCKET; + bool fConnected; + if (fI2P) { + // Route through the I2P SAM session. Strip any :port suffix; I2P peers + // are reached purely by destination. + std::string i2pDest = addrStr; + size_t i2pEnd = i2pDest.find(".i2p"); + if (i2pEnd != std::string::npos) + i2pDest = i2pDest.substr(0, i2pEnd + 4); + fConnected = CI2PSession::GetInstance()->Connect(i2pDest, hSocket); + } else { + fConnected = pszDest ? ConnectSocketByName(addrConnect, hSocket, pszDest, GetDefaultPort()) : ConnectSocket(addrConnect, hSocket); + } + if (fConnected) { addrman.Attempt(addrConnect); @@ -650,6 +672,54 @@ CNode* ConnectNode(CAddress addrConnect, const char *pszDest) } } +// Adopt a connected I2P SAM data socket (from the accept loop in i2p.cpp) as an +// inbound peer. The socket arrives in blocking mode; switch it to non-blocking +// to match the rest of the socket handler, then register the node. +void AddI2PInboundNode(SOCKET hSocket, const CAddress& addr) +{ + if (hSocket == INVALID_SOCKET) + return; + + if (CNode::IsBanned(addr)) { + printf("I2P inbound from %s dropped (banned)\n", addr.ToString().c_str()); + closesocket(hSocket); + return; + } + + // Honour the inbound connection limit. + int nInbound = 0; + { + LOCK(cs_vNodes); + for (CNode* pnode : vNodes) + if (pnode->fInbound) + nInbound++; + } + int nMaxInbound = GetArg("-maxconnections", 125) - MAX_OUTBOUND_CONNECTIONS; + if (nInbound >= nMaxInbound) { + printf("I2P inbound from %s dropped (too many inbound)\n", addr.ToString().c_str()); + closesocket(hSocket); + return; + } + +#ifdef WIN32 + u_long nOne = 1; + if (ioctlsocket(hSocket, FIONBIO, &nOne) == SOCKET_ERROR) + printf("AddI2PInboundNode() : ioctlsocket non-blocking setting failed, error %d\n", WSAGetLastError()); +#else + if (fcntl(hSocket, F_SETFL, O_NONBLOCK) == SOCKET_ERROR) + printf("AddI2PInboundNode() : fcntl non-blocking setting failed, error %d\n", errno); +#endif + + printf("accepted I2P connection %s\n", addr.ToString().c_str()); + CNode* pnode = new CNode(hSocket, addr, "", true); + pnode->AddRef(); + pnode->nTimeConnected = GetTime(); + { + LOCK(cs_vNodes); + vNodes.push_back(pnode); + } +} + void CNode::CloseSocketDisconnect() { fDisconnect = true; diff --git a/src/net.h b/src/net.h index 8c135df..391ac5f 100644 --- a/src/net.h +++ b/src/net.h @@ -37,6 +37,8 @@ void AddressCurrentlyConnected(const CService& addr); CNode* FindNode(const CNetAddr& ip); CNode* FindNode(const CService& ip); CNode* ConnectNode(CAddress addrConnect, const char *strDest = nullptr); +// Adopt a connected I2P SAM data socket as an inbound peer (called from i2p.cpp). +void AddI2PInboundNode(SOCKET hSocket, const CAddress& addr); void MapPort(); unsigned short GetListenPort(); bool BindListenPort(const CService &bindAddr, std::string& strError=REF(std::string())); diff --git a/src/netbase.cpp b/src/netbase.cpp index a80e9d0..54310e3 100644 --- a/src/netbase.cpp +++ b/src/netbase.cpp @@ -672,6 +672,7 @@ void CNetAddr::Init() memset(ip, 0, sizeof(ip)); memset(tor_v3_pubkey, 0, sizeof(tor_v3_pubkey)); m_is_tor_v3 = false; + m_is_i2p = false; } void CNetAddr::SetIP(const CNetAddr& ipIn) @@ -679,6 +680,7 @@ void CNetAddr::SetIP(const CNetAddr& ipIn) memcpy(ip, ipIn.ip, sizeof(ip)); memcpy(tor_v3_pubkey, ipIn.tor_v3_pubkey, sizeof(tor_v3_pubkey)); m_is_tor_v3 = ipIn.m_is_tor_v3; + m_is_i2p = ipIn.m_is_i2p; } static const unsigned char pchOnionCat[] = {0xFD,0x87,0xD8,0x7E,0xEB,0x43}; @@ -734,6 +736,22 @@ bool CNetAddr::SetSpecial(const std::string &strName) return true; } } + // Modern I2P base32 address: 52 base32 chars = SHA-256(destination) (32 bytes) + // rendered as ".b32.i2p". Store the hash and flag this as an I2P address. + if (strName.size()>8 && strName.substr(strName.size() - 8, 8) == ".b32.i2p") { + std::string addrPart = strName.substr(0, strName.size() - 8); + std::vector vchAddr = DecodeBase32(addrPart.c_str()); + if (vchAddr.size() != 32) + return false; + // Keep the GarliCat prefix in ip[] so legacy reachability checks that + // look for unique-local space still treat this as a routable overlay. + memcpy(ip, pchGarliCat, sizeof(pchGarliCat)); + memset(ip + sizeof(pchGarliCat), 0, 16 - sizeof(pchGarliCat)); + memcpy(tor_v3_pubkey, vchAddr.data(), 32); + m_is_i2p = true; + m_is_tor_v3 = false; + return true; + } return false; } @@ -856,7 +874,7 @@ bool CNetAddr::IsTorV3() const bool CNetAddr::IsI2P() const { - return (memcmp(ip, pchGarliCat, sizeof(pchGarliCat)) == 0); + return m_is_i2p || (memcmp(ip, pchGarliCat, sizeof(pchGarliCat)) == 0); } bool CNetAddr::IsLocal() const @@ -962,6 +980,13 @@ std::string CNetAddr::ToStringIP() const } if (IsTor()) return EncodeBase32(&ip[6], 10) + ".onion"; + if (m_is_i2p) { + // Modern I2P: base32 of the 32-byte destination hash, unpadded. + std::string b32 = EncodeBase32(tor_v3_pubkey, 32); + while (!b32.empty() && b32[b32.size() - 1] == '=') + b32.erase(b32.size() - 1); + return b32 + ".b32.i2p"; + } if (IsI2P()) return EncodeBase32(&ip[6], 10) + ".b32.i2p"; CService serv(*this, 0); @@ -995,12 +1020,14 @@ bool operator==(const CNetAddr& a, const CNetAddr& b) { if (a.m_is_tor_v3 || b.m_is_tor_v3) return a.m_is_tor_v3 == b.m_is_tor_v3 && memcmp(a.tor_v3_pubkey, b.tor_v3_pubkey, 32) == 0; + if (a.m_is_i2p || b.m_is_i2p) + return a.m_is_i2p == b.m_is_i2p && memcmp(a.tor_v3_pubkey, b.tor_v3_pubkey, 32) == 0; return (memcmp(a.ip, b.ip, 16) == 0); } bool operator!=(const CNetAddr& a, const CNetAddr& b) { - return (memcmp(a.ip, b.ip, 16) != 0); + return !(a == b); } bool operator<(const CNetAddr& a, const CNetAddr& b) @@ -1009,6 +1036,10 @@ bool operator<(const CNetAddr& a, const CNetAddr& b) return !a.m_is_tor_v3; // non-v3 sorts before v3 if (a.m_is_tor_v3) return memcmp(a.tor_v3_pubkey, b.tor_v3_pubkey, 32) < 0; + if (a.m_is_i2p != b.m_is_i2p) + return !a.m_is_i2p; // non-i2p sorts before i2p + if (a.m_is_i2p) + return memcmp(a.tor_v3_pubkey, b.tor_v3_pubkey, 32) < 0; return (memcmp(a.ip, b.ip, 16) < 0); } @@ -1032,6 +1063,17 @@ bool CNetAddr::GetIn6Addr(struct in6_addr* pipv6Addr) const // no two connections will be attempted to addresses with the same group std::vector CNetAddr::GetGroup() const { + // Modern I2P addresses keep their identifying bytes in the 32-byte + // destination-hash field (ip[] only holds the overlay prefix), so derive + // the group from the hash to keep peers in distinct groups. + if (m_is_i2p) { + std::vector vch; + vch.push_back(NET_I2P); + vch.push_back(tor_v3_pubkey[0]); + vch.push_back(tor_v3_pubkey[1]); + return vch; + } + std::vector vchRet; int nClass = NET_IPV6; int nStartByte = 0; @@ -1106,7 +1148,7 @@ std::vector CNetAddr::GetGroup() const uint64_t CNetAddr::GetHash() const { uint256 hash; - if (m_is_tor_v3) + if (m_is_tor_v3 || m_is_i2p) hash = Hash(&tor_v3_pubkey[0], &tor_v3_pubkey[32]); else hash = Hash(&ip[0], &ip[16]); diff --git a/src/netbase.h b/src/netbase.h index aea234c..175f70d 100644 --- a/src/netbase.h +++ b/src/netbase.h @@ -106,8 +106,12 @@ class CNetAddr { protected: unsigned char ip[16]; // in network byte order - unsigned char tor_v3_pubkey[32]; // Ed25519 public key for Tor v3 onion addresses + // For Tor v3 this holds the 32-byte Ed25519 public key. When m_is_i2p is + // set it instead holds the 32-byte SHA-256 of the I2P destination (the + // value rendered as the ".b32.i2p" address). A CNetAddr is never both. + unsigned char tor_v3_pubkey[32]; bool m_is_tor_v3; + bool m_is_i2p; public: CNetAddr(); @@ -160,6 +164,7 @@ class CNetAddr READWRITE(FLATDATA(ip)); READWRITE(FLATDATA(tor_v3_pubkey)); READWRITE(m_is_tor_v3); + READWRITE(m_is_i2p); ) }; @@ -203,6 +208,7 @@ class CService : public CNetAddr READWRITE(FLATDATA(ip)); READWRITE(FLATDATA(tor_v3_pubkey)); READWRITE(m_is_tor_v3); + READWRITE(m_is_i2p); unsigned short portN = htons(port); READWRITE(portN); if (fRead) diff --git a/src/qt/trianglesgui.cpp b/src/qt/trianglesgui.cpp index 94699f4..a0b62eb 100644 --- a/src/qt/trianglesgui.cpp +++ b/src/qt/trianglesgui.cpp @@ -349,6 +349,12 @@ TrianglesGUI::TrianglesGUI(bool fIsTestnet, QWidget *parent): labelOnionAddress->setCursor(Qt::PointingHandCursor); labelOnionAddress->installEventFilter(this); + // I2P address, stacked directly above the .onion address (click to copy) + labelI2PAddress = ui->label_i2p; + labelI2PAddress->setVisible(false); + labelI2PAddress->setCursor(Qt::PointingHandCursor); + labelI2PAddress->installEventFilter(this); + // V3 indicator next to staking icon (hidden until onion is active) labelV3Icon = ui->label_v3; labelV3Icon->setVisible(false); @@ -370,7 +376,6 @@ TrianglesGUI::TrianglesGUI(bool fIsTestnet, QWidget *parent): labelI2PIcon = ui->label_i2p_icon; labelI2PIcon->setVisible(false); - QTimer *timerI2P = new QTimer(this); connect(timerI2P, SIGNAL(timeout()), this, SLOT(updateI2PAddress())); timerI2P->start(5000); diff --git a/src/qt/trianglesgui.h b/src/qt/trianglesgui.h index 186d36d..756737c 100644 --- a/src/qt/trianglesgui.h +++ b/src/qt/trianglesgui.h @@ -110,6 +110,7 @@ private: QLabel *labelConnectionsIcon; QLabel *labelBlocksIcon; QLabel *labelOnionAddress; + QLabel *labelI2PAddress; QLabel *labelV3Icon; QLabel *labelI2PAddress; QLabel *labelI2PIcon; diff --git a/src/rpcnet.cpp b/src/rpcnet.cpp index 3d70e85..7610242 100644 --- a/src/rpcnet.cpp +++ b/src/rpcnet.cpp @@ -10,6 +10,9 @@ #include "db.h" #include "walletdb.h" #include "net_bootstrap.h" +#include "i2p.h" +#include "tor/onion_v3.h" +#include "tor/tor_embedded.h" using namespace json_spirit; using namespace std; @@ -34,12 +37,34 @@ Value getnetworkinfo(const Array& params, bool fHelp) healthObj.push_back(Pair("lastblocktime", static_cast(health.lastBlockTime))); healthObj.push_back(Pair("networkmode", "tor_native")); + // Tor .onion address (wallet hidden service). + std::string onionAddress = CTorV3Manager::GetInstance()->GetWalletOnionAddress(); + if (onionAddress.empty()) + onionAddress = CTorEmbedded::GetInstance()->GetOnionAddress(); + + // I2P session state and .b32.i2p address. + CI2PSession* i2p = CI2PSession::GetInstance(); + int nI2PPeers = 0; + { + LOCK(cs_vNodes); + for (CNode* pnode : vNodes) + if (pnode->addr.IsI2P()) + nI2PPeers++; + } + Object i2pObj; + i2pObj.push_back(Pair("enabled", i2p->IsEnabled())); + i2pObj.push_back(Pair("active", i2p->IsActive())); + i2pObj.push_back(Pair("address", i2p->GetB32Address())); + i2pObj.push_back(Pair("peers", nI2PPeers)); + Object obj; obj.push_back(Pair("version", FormatFullVersion())); obj.push_back(Pair("protocolversion", (int)PROTOCOL_VERSION)); obj.push_back(Pair("connections", (int)vNodes.size())); obj.push_back(Pair("proxy", (proxy.first.IsValid() ? proxy.first.ToStringIPPort() : string()))); obj.push_back(Pair("ip", addrSeenByPeer.ToStringIP())); + obj.push_back(Pair("toraddress", onionAddress)); + obj.push_back(Pair("i2p", i2pObj)); obj.push_back(Pair("localservices", strprintf("%016"PRIx64, nLocalServices))); obj.push_back(Pair("testnet", fTestNet)); obj.push_back(Pair("networkhealth", healthObj)); diff --git a/src/rpcwallet.cpp b/src/rpcwallet.cpp index a10e7f2..70d4e27 100644 --- a/src/rpcwallet.cpp +++ b/src/rpcwallet.cpp @@ -9,6 +9,9 @@ #include "init.h" #include "base58.h" #include "smessage.h" +#include "i2p.h" +#include "tor/onion_v3.h" +#include "tor/tor_embedded.h" using namespace json_spirit; using namespace std; @@ -100,6 +103,13 @@ Value getinfo(const Array& params, bool fHelp) obj.push_back(Pair("proxy", (proxy.first.IsValid() ? proxy.first.ToStringIPPort() : string()))); obj.push_back(Pair("ip", addrSeenByPeer.ToStringIP())); + // Anonymous network identities. + std::string onionAddress = CTorV3Manager::GetInstance()->GetWalletOnionAddress(); + if (onionAddress.empty()) + onionAddress = CTorEmbedded::GetInstance()->GetOnionAddress(); + obj.push_back(Pair("toraddress", onionAddress)); + obj.push_back(Pair("i2paddress", CI2PSession::GetInstance()->GetB32Address())); + diff.push_back(Pair("proof-of-work", GetDifficulty())); diff.push_back(Pair("proof-of-stake", GetDifficulty(GetLastBlockIndex(pindexBest, true)))); obj.push_back(Pair("difficulty", diff)); diff --git a/src/tor/tor_process.cpp b/src/tor/tor_process.cpp index 6707b85..a644246 100644 --- a/src/tor/tor_process.cpp +++ b/src/tor/tor_process.cpp @@ -311,6 +311,24 @@ bool CTorProcess::WriteTorrc() torrc << "AvoidDiskWrites 1\n"; torrc << "Log notice stderr\n"; + // Append user-supplied extra Tor configuration if present. This lets + // operators on censored / DPI-filtered networks add Bridge lines, + // ClientTransportPlugin (obfs4), or a Socks5Proxy/HTTPSProxy upstream so + // Tor can reach the network when direct connections are blocked. The file + // is never overwritten by the wallet; only the auto-generated torrc is. + { + fs::path extraPath = dataPath / "torrc.extra"; + if (fs::exists(extraPath)) { + std::ifstream extra(extraPath.string().c_str()); + if (extra.is_open()) { + torrc << "\n# ---- appended from torrc.extra (user-managed) ----\n"; + torrc << extra.rdbuf(); + torrc << "\n"; + printf("Tor: appended user configuration from %s\n", extraPath.string().c_str()); + } + } + } + torrc.close(); if (hiddenServiceEnabled) {