diff --git a/database/SqliteDatabase.h b/database/SqliteDatabase.h index 7cd94f45e1..8ba9d796b7 100644 --- a/database/SqliteDatabase.h +++ b/database/SqliteDatabase.h @@ -40,5 +40,6 @@ public: uint64 getBigInt(int colIndex); void escape(const unsigned char* start,int size,std::string& retStr); - }; + +// vim:ts=4 diff --git a/database/database.cpp b/database/database.cpp index f0148e5949..725af6a2f6 100644 --- a/database/database.cpp +++ b/database/database.cpp @@ -5,9 +5,9 @@ Database::Database(const char* host,const char* user,const char* pass) : mNumCol(0) { - mDBPass=pass; - mHost=host; - mUser=user; + mDBPass = pass; + mHost = host; + mUser = user; } Database::~Database() @@ -17,68 +17,80 @@ Database::~Database() bool Database::getNull(const char* colName) { int index; - if(getColNumber(colName,&index)) + + if (getColNumber(colName,&index)) { - return(getNull(index)); + return getNull(index); } + return true; } char* Database::getStr(const char* colName,std::string& retStr) { int index; - if(getColNumber(colName,&index)) + + if (getColNumber(colName,&index)) { - return(getStr(index,retStr)); + return getStr(index,retStr); } - return(NULL); + + return NULL; } int32 Database::getInt(const char* colName) { int index; - if(getColNumber(colName,&index)) + + if (getColNumber(colName,&index)) { - return(getInt(index)); + return getInt(index); } - return(0); + + return 0; } float Database::getFloat(const char* colName) { int index; - if(getColNumber(colName,&index)) + + if (getColNumber(colName,&index)) { - return(getFloat(index)); + return getFloat(index); } - return(0); + + return 0; } bool Database::getBool(const char* colName) { int index; - if(getColNumber(colName,&index)) + + if (getColNumber(colName,&index)) { - return(getBool(index)); + return getBool(index); } - return(0); + + return 0; } int Database::getBinary(const char* colName,unsigned char* buf,int maxSize) { int index; - if(getColNumber(colName,&index)) + + if (getColNumber(colName,&index)) { return(getBinary(index,buf,maxSize)); } + return(0); } -std::vector Database::getBinary(const char* colName) +std::vector Database::getBinary(const std::string& strColName) { int index; - if (getColNumber(colName,&index)) + if (getColNumber(strColName.c_str(), &index)) { return getBinary(index); } @@ -86,37 +98,44 @@ std::vector Database::getBinary(const char* colName) return std::vector(); } +std::string Database::getStrBinary(const std::string& strColName) +{ + // YYY Could eliminate a copy if getStrBinary was a template. + return strCopy(getBinary(strColName.c_str())); +} + uint64 Database::getBigInt(const char* colName) { int index; - if(getColNumber(colName,&index)) + + if (getColNumber(colName,&index)) { - return(getBigInt(index)); + return getBigInt(index); } - return(0); + + return 0; } - - // returns false if can't find col bool Database::getColNumber(const char* colName,int* retIndex) { - for(unsigned int n=0; n #include #include "../src/types.h" +#include "../src/utils.h" #define SQL_FOREACH(_db, _strQuery) \ if ((_db)->executeSQL(_strQuery)) \ @@ -58,12 +59,15 @@ public: // get Data from the current row bool getNull(const char* colName); char* getStr(const char* colName,std::string& retStr); + std::string getStrBinary(const std::string& strColName); int32 getInt(const char* colName); float getFloat(const char* colName); bool getBool(const char* colName); + // returns amount stored in buf - int getBinary(const char* colName,unsigned char* buf,int maxSize); - std::vector getBinary(const char* colName); + int getBinary(const char* colName, unsigned char* buf, int maxSize); + std::vector getBinary(const std::string& strColName); + uint64 getBigInt(const char* colName); virtual bool getNull(int colIndex)=0; diff --git a/newcoind.cfg b/newcoind.cfg index e34c6b2490..916275fee6 100644 --- a/newcoind.cfg +++ b/newcoind.cfg @@ -1,46 +1,65 @@ # # Sample newcoind.cfg # -# This file is UTF-8 with Dos, UNIX, or Mac style end of lines. -# Blank lines and lines beginning with '#' are ignored. -# Undefined sections are reserved. -# No escapes are currently defined. +# This file should be named newcoind.cfg. This file is UTF-8 with Dos, UNIX, +# or Mac style end of lines. Blank lines and lines beginning with '#' are +# ignored. Undefined sections are reserved. No escapes are currently defined. # # When you launch newcoind, it will attempt to find this file. # -# You may specify the location of this file with --conf=. The base -# directory for other files will be the directory containing this file. +# --conf=: +# You may specify the location of this file with --conf=. The config +# directory is the directory containing this file. The data directory is a +# the subdirectory named "dbs". # -# Windows: -# This file is named newcoind.cfg. -# The base directory for this configuration file and other information is the -# same directory as the newcoind program. +# Windows and no --conf: +# The config directory is the same directory as the newcoind program. The +# data directory is a the subdirectory named "dbs". # -# Other OSes: -# This file may be named newcoind.cfg. The file will be looked for in the -# following order: +# Other OSes and no --conf: +# This file will be looked for in these places in the following order: # ./newcoind.cfg # $XDG_CONFIG_HOME/newcoin/newcoind.cfg # -# If newcoind.cfg, is found in the current working directory, the directory -# will be used as the base directory for other information. Otherwise, the -# base directory for data is: +# If newcoind.cfg, is found in the current working directory, the directory +# will be used as the config directory. The data directory is a the +# subdirectory named "dbs". +# +# Otherwise, the data directory data is: # $XDG_DATA_HOME/newcoin/ # # Note: $XDG_CONFIG_HOME defaults to $HOME/.config # $XDG_DATA_HOME defaults to $HOME/.local/share # -# To perform validation, one of these sections must be provided: -# [validation_key], [validation_password], or [validation_seed]. +# [unl_default]: +# Specifies how to bootstrap the UNL list. The UNL list is based on a +# validators.txt file and is maintained in the databases. When newcoind +# starts up, if the databases are missing or are obsolete due to an upgrade +# of newcoind, newcoind will reconstruct the UNL list as specified here. +# +# If this entry is not present or empty, newcoind will look for a validators.txt in the +# config directory. If not found there, it will attempt to retrieve the file +# from the newcoin foundation's web site. +# +# This entry is also used by the RPC command unl_load. +# +# Specify the file by specifying its full path. +# +# Examples: C:/home/johndoe/newcoin/validators.txt +# /home/johndoe/newcoin/validators.txt # # [peer_ip]: -# IP address or domain to bind to if allowing external connections from peers. +# IP address or domain to bind to allow external connections from peers. +# Defaults to not allow external connections from peers. +# +# Examples: 0.0.0.0 - Bind on all interfaces. # # [peer_port]: -# Port to bind to if allowing external connections from peers. +# Port to bind to allow external connections from peers. # # [rpc_ip]: -# IP address or domain to bind to if allowing insecure RPC connections. +# IP address or domain to bind to allow insecure RPC connections. +# Defaults to not allow RPC connections. # # [rpc_port]: # Port to bind to if allowing insecure RPC connections. @@ -48,14 +67,24 @@ # [rpc_allow_remote]: # 0 or 1. 0 only allows RPC connections from 127.0.0.1. [default 0] # +# [websocket_ip]: +# IP address or domain to bind to allow client connections. +# +# Examples: 0.0.0.0 - Bind on all interfaces. +# 127.0.0.1 - Bind on localhost interface. Only local programs may connect. +# +# [websocket_port]: +# Port to bind to allow client connections. +# # [validation_seed]: # To perform validation, this section should contain either a validation seed or key. # The validation seed is used to generate the validation public/private key pair. # To obtain a validation seed, use the validation_create command. +# # Examples: RASH BUSH MILK LOOK BAD BRIM AVID GAFF BAIT ROT POD LOVE # shfArahZT9Q9ckTf3s1psJ7C7qzVN - # + [peer_ip] 0.0.0.0 diff --git a/src/Application.cpp b/src/Application.cpp index cf6809392a..9e4cdd5ec8 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -58,11 +58,11 @@ void Application::run() // // Construct databases. // - mTxnDB = new DatabaseCon("transaction.db", TxnDBInit, TxnDBCount); - mLedgerDB = new DatabaseCon("ledger.db", LedgerDBInit, LedgerDBCount); - mWalletDB = new DatabaseCon("wallet.db", WalletDBInit, WalletDBCount); - mHashNodeDB = new DatabaseCon("hashnode.db", HashNodeDBInit, HashNodeDBCount); - mNetNodeDB = new DatabaseCon("netnode.db", NetNodeDBInit, NetNodeDBCount); + mTxnDB = new DatabaseCon("transaction.db", TxnDBInit, TxnDBCount); + mLedgerDB = new DatabaseCon("ledger.db", LedgerDBInit, LedgerDBCount); + mWalletDB = new DatabaseCon("wallet.db", WalletDBInit, WalletDBCount); + mHashNodeDB = new DatabaseCon("hashnode.db", HashNodeDBInit, HashNodeDBCount); + mNetNodeDB = new DatabaseCon("netnode.db", NetNodeDBInit, NetNodeDBCount); // // Begin validation and ip maintenance. @@ -70,10 +70,15 @@ void Application::run() // mWallet.start(); + // + // Set up UNL. + // + getUNL().nodeBootstrap(); + // // Allow peer connections. // - if(!theConfig.PEER_IP.empty() && theConfig.PEER_PORT) + if (!theConfig.PEER_IP.empty() && theConfig.PEER_PORT) { mPeerDoor = new PeerDoor(mIOService); } @@ -85,7 +90,7 @@ void Application::run() // // Allow RPC connections. // - if(!theConfig.RPC_IP.empty() && theConfig.RPC_PORT) + if (!theConfig.RPC_IP.empty() && theConfig.RPC_PORT) { mRPCDoor = new RPCDoor(mIOService); } @@ -122,7 +127,6 @@ void Application::run() mNetOps.setStateTimer(0); - // temporary mIOService.run(); // This blocks std::cout << "Done." << std::endl; diff --git a/src/Config.cpp b/src/Config.cpp index b02c468e95..176bb2b522 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -3,32 +3,36 @@ #include "ParseSection.h" #include "utils.h" -#include -#include #include - -// Fees are in XNS raw. -#define DEFAULT_FEE_ACCOUNT_CREATE 1000 -#define DEFAULT_FEE_NICKNAME_CREATE 1000 -#define DEFAULT_FEE_DEFAULT 100 +#include +#include #define CONFIG_FILE_NAME SYSTEM_NAME "d.cfg" // newcoind.cfg + +#define SECTION_ACCOUNT_PROBE_MAX "account_probe_max" +#define SECTION_FEE_ACCOUNT_CREATE "fee_account_create" +#define SECTION_FEE_DEFAULT "fee_default" +#define SECTION_FEE_NICKNAME_CREATE "fee_nickname_create" +#define SECTION_NETWORK_QUORUM "network_quorum" +#define SECTION_PEER_CONNECT_LOW_WATER "peer_connect_low_water" #define SECTION_PEER_IP "peer_ip" #define SECTION_PEER_PORT "peer_port" +#define SECTION_PEER_SCAN_INTERVAL_MIN "peer_scan_interval_min" +#define SECTION_PEER_SSL_CIPHER_LIST "peer_ssl_cipher_list" +#define SECTION_PEER_START_MAX "peer_start_max" +#define SECTION_RPC_ALLOW_REMOTE "rpc_allow_remote" #define SECTION_RPC_IP "rpc_ip" #define SECTION_RPC_PORT "rpc_port" -#define SECTION_RPC_ALLOW_REMOTE "rpc_allow_remote" -#define SECTION_VALIDATION_SEED "validation_seed" -#define SECTION_PEER_SSL_CIPHER_LIST "peer_ssl_cipher_list" -#define SECTION_PEER_SCAN_INTERVAL_MIN "peer_scan_interval_min" -#define SECTION_PEER_START_MAX "peer_start_max" -#define SECTION_PEER_CONNECT_LOW_WATER "peer_connect_low_water" -#define SECTION_NETWORK_QUORUM "network_quorum" +#define SECTION_UNL_DEFAULT "unl_default" #define SECTION_VALIDATION_QUORUM "validation_quorum" -#define SECTION_FEE_ACCOUNT_CREATE "fee_account_create" -#define SECTION_FEE_NICKNAME_CREATE "fee_nickname_create" -#define SECTION_FEE_DEFAULT "fee_default" -#define SECTION_ACCOUNT_PROBE_MAX "account_probe_max" +#define SECTION_VALIDATION_SEED "validation_seed" +#define SECTION_WEBSOCKET_IP "websocket_ip" +#define SECTION_WEBSOCKET_PORT "websocket_port" + +// Fees are in XNB. +#define DEFAULT_FEE_ACCOUNT_CREATE 1000 +#define DEFAULT_FEE_NICKNAME_CREATE 1000 +#define DEFAULT_FEE_DEFAULT 100 Config theConfig; @@ -169,7 +173,7 @@ void Config::load() (void) sectionSingleB(secConfig, SECTION_PEER_IP, PEER_IP); if (sectionSingleB(secConfig, SECTION_PEER_PORT, strTemp)) - PEER_PORT = boost::lexical_cast(strTemp); + PEER_PORT = boost::lexical_cast(strTemp); (void) sectionSingleB(secConfig, SECTION_RPC_IP, RPC_IP); @@ -177,7 +181,12 @@ void Config::load() RPC_PORT = boost::lexical_cast(strTemp); if (sectionSingleB(secConfig, SECTION_RPC_ALLOW_REMOTE, strTemp)) - RPC_ALLOW_REMOTE = boost::lexical_cast(strTemp); + RPC_ALLOW_REMOTE = boost::lexical_cast(strTemp); + + (void) sectionSingleB(secConfig, SECTION_WEBSOCKET_IP, WEBSOCKET_IP); + + if (sectionSingleB(secConfig, SECTION_WEBSOCKET_PORT, strTemp)) + WEBSOCKET_PORT = boost::lexical_cast(strTemp); if (sectionSingleB(secConfig, SECTION_VALIDATION_SEED, strTemp)) VALIDATION_SEED.setSeedGeneric(strTemp); @@ -187,28 +196,31 @@ void Config::load() PEER_SCAN_INTERVAL_MIN = MAX(60, boost::lexical_cast(strTemp)); if (sectionSingleB(secConfig, SECTION_PEER_START_MAX, strTemp)) - PEER_START_MAX = MAX(1, boost::lexical_cast(strTemp)); + PEER_START_MAX = MAX(1, boost::lexical_cast(strTemp)); if (sectionSingleB(secConfig, SECTION_PEER_CONNECT_LOW_WATER, strTemp)) PEER_CONNECT_LOW_WATER = MAX(1, boost::lexical_cast(strTemp)); if (sectionSingleB(secConfig, SECTION_NETWORK_QUORUM, strTemp)) - NETWORK_QUORUM = MAX(0, boost::lexical_cast(strTemp)); + NETWORK_QUORUM = MAX(0, boost::lexical_cast(strTemp)); if (sectionSingleB(secConfig, SECTION_VALIDATION_QUORUM, strTemp)) - VALIDATION_QUORUM = MAX(0, boost::lexical_cast(strTemp)); + VALIDATION_QUORUM = MAX(0, boost::lexical_cast(strTemp)); if (sectionSingleB(secConfig, SECTION_FEE_ACCOUNT_CREATE, strTemp)) - FEE_ACCOUNT_CREATE = boost::lexical_cast(strTemp); + FEE_ACCOUNT_CREATE = boost::lexical_cast(strTemp); if (sectionSingleB(secConfig, SECTION_FEE_NICKNAME_CREATE, strTemp)) - FEE_NICKNAME_CREATE = boost::lexical_cast(strTemp); + FEE_NICKNAME_CREATE = boost::lexical_cast(strTemp); if (sectionSingleB(secConfig, SECTION_FEE_DEFAULT, strTemp)) - FEE_DEFAULT = boost::lexical_cast(strTemp); + FEE_DEFAULT = boost::lexical_cast(strTemp); if (sectionSingleB(secConfig, SECTION_ACCOUNT_PROBE_MAX, strTemp)) - ACCOUNT_PROBE_MAX = boost::lexical_cast(strTemp); + ACCOUNT_PROBE_MAX = boost::lexical_cast(strTemp); + + if (sectionSingleB(secConfig, SECTION_UNL_DEFAULT, strTemp)) + UNL_DEFAULT = strTemp; } } } diff --git a/src/Config.h b/src/Config.h index 4ef20c81b9..10d1a1ce11 100644 --- a/src/Config.h +++ b/src/Config.h @@ -44,32 +44,31 @@ public: boost::filesystem::path CONFIG_FILE; boost::filesystem::path CONFIG_DIR; boost::filesystem::path DATA_DIR; + boost::filesystem::path UNL_DEFAULT; // Network parameters - int NETWORK_START_TIME; // The Unix time we start ledger 0 - int TRANSACTION_FEE_BASE; - int LEDGER_SECONDS; - int LEDGER_PROPOSAL_DELAY_SECONDS; - int LEDGER_AVALANCHE_SECONDS; + int NETWORK_START_TIME; // The Unix time we start ledger 0 + int TRANSACTION_FEE_BASE; + int LEDGER_SECONDS; + int LEDGER_PROPOSAL_DELAY_SECONDS; + int LEDGER_AVALANCHE_SECONDS; // Note: The following parameters do not relate to the UNL or trust at all - int NETWORK_QUORUM; // Minimum number of nodes to consider the network present - int VALIDATION_QUORUM; // Minimum validations to consider ledger authoritative + int NETWORK_QUORUM; // Minimum number of nodes to consider the network present + int VALIDATION_QUORUM; // Minimum validations to consider ledger authoritative // Peer networking parameters std::string PEER_IP; - int PEER_PORT; - int NUMBER_CONNECTIONS; + int PEER_PORT; + int NUMBER_CONNECTIONS; std::string PEER_SSL_CIPHER_LIST; - int PEER_SCAN_INTERVAL_MIN; - int PEER_START_MAX; - int PEER_CONNECT_LOW_WATER; + int PEER_SCAN_INTERVAL_MIN; + int PEER_START_MAX; + int PEER_CONNECT_LOW_WATER; -// bool NODE_INBOUND; // We accept inbound connections -// bool NODE_DATABASE; // We offer historical data services -// bool NODE_PUBLIC; // We do not attempt to hide our identity -// bool NODE_DUMB; // We are a 'dumb' client -// bool NODE_SMART; // We offer services to 'dumb' clients + // Client networking parameters + std::string WEBSOCKET_IP; + int WEBSOCKET_PORT; // RPC parameters std::string RPC_IP; @@ -95,4 +94,5 @@ public: extern Config theConfig; #endif + // vim:ts=4 diff --git a/src/ConnectionPool.cpp b/src/ConnectionPool.cpp index 931fe93e2e..728795c41b 100644 --- a/src/ConnectionPool.cpp +++ b/src/ConnectionPool.cpp @@ -81,26 +81,27 @@ bool ConnectionPool::savePeer(const std::string& strIp, int iPort,char code) return false; } +// <-- true, if a peer is available to connect to bool ConnectionPool::peerAvailable(std::string& strIp, int& iPort) { Database* db = theApp->getWalletDB()->getDB(); std::vector vstrIpPort; + // Convert mIpMap (list of open connections) to a vector of " ". { boost::mutex::scoped_lock sl(mPeerLock); - pipPeer ipPeer; - vstrIpPort.reserve(mIpMap.size()); - BOOST_FOREACH(ipPeer, mIpMap) + BOOST_FOREACH(pipPeer ipPeer, mIpMap) { - std::string& strIp = ipPeer.first.first; - int iPort = ipPeer.first.second; + const std::string& strIp = ipPeer.first.first; + int iPort = ipPeer.first.second; vstrIpPort.push_back(db->escape(str(boost::format("%s %d") % strIp % iPort))); } } + // Get the first IpPort entry which is not in vector and which is not scheduled for scanning. std::string strIpPort; ScopedLock sl(theApp->getWalletDB()->getDBLock()); diff --git a/src/DBInit.cpp b/src/DBInit.cpp index fc4a1d6d4a..7406ce2ce4 100644 --- a/src/DBInit.cpp +++ b/src/DBInit.cpp @@ -117,6 +117,11 @@ const char *WalletDBInit[] = { // Table of PublicKeys user has asked to trust. // Fetches are made to the CAS. This gets the newcoin.txt so even validators without a web server can publish a newcoin.txt. + // Source: + // 'M' = Manually added. : 1500 + // 'V' = validators.txt : 1000 + // 'W' = Web browsing. : 200 + // 'R' = Referral : 0 // Next: // Time of next fetch attempt. // Scan: @@ -129,6 +134,7 @@ const char *WalletDBInit[] = { // User supplied comment. "CREATE TABLE SeedNodes ( \ PublicKey CHARACTER(53) PRIMARY KEY NOT NULL, \ + Source CHARACTER(1) NOT NULL, \ Next DATETIME, \ Scan DATETIME, \ Fetch DATETIME, \ @@ -139,7 +145,7 @@ const char *WalletDBInit[] = { // Allow us to easily find the next SeedNode to fetch. "CREATE INDEX SeedNodeNext ON SeedNodes (Next);", - // Nodes we trust not grossly consipire. Derived from SeedDomains, SeedNodes, and ValidatorReferrals. + // Nodes we trust to not grossly collude against us. Derived from SeedDomains, SeedNodes, and ValidatorReferrals. // // Score: // Computed trust score. Higher is better. diff --git a/src/Ledger.cpp b/src/Ledger.cpp index 7565d2a1cf..aa57d69373 100644 --- a/src/Ledger.cpp +++ b/src/Ledger.cpp @@ -300,7 +300,6 @@ void Ledger::saveAcceptedLedger(Ledger::pointer ledger) "UPDATE Transactions SET LedgerSeq = '%d', Status = '%c' WHERE TransID = '%s';") % ledger->getLedgerSeq() % TXN_SQL_VALIDATED % txn.getTransactionID().GetHex())); } - // FIXME: If above updates no rows, modify seq/status (upsert) } db->executeSQL("COMMIT TRANSACTION;"); } diff --git a/src/LedgerConsensus.cpp b/src/LedgerConsensus.cpp index bf17cd74a9..b93875ea5a 100644 --- a/src/LedgerConsensus.cpp +++ b/src/LedgerConsensus.cpp @@ -363,7 +363,7 @@ int LedgerConsensus::timerEntry() case lcsCUTOFF: return stateCutoff(sinceClose); case lcsFINISHED: return stateFinished(sinceClose); case lcsACCEPTED: return stateAccepted(sinceClose); - case lcsABORTED: return stateAccepted(sinceClose); + case lcsABORTED: return 1; } assert(false); return 1; @@ -702,7 +702,6 @@ void LedgerConsensus::accept(SHAMap::pointer set) #endif ScopedLock sl = theApp->getMasterLedger().getLock(); - applyTransactions(theApp->getMasterLedger().getCurrentLedger()->peekTransactionMap(), newOL, failedTransactions); theApp->getMasterLedger().pushLedger(newLCL, newOL); mState = lcsACCEPTED; diff --git a/src/RPCServer.cpp b/src/RPCServer.cpp index 6ae450ae07..7bdc257c9d 100644 --- a/src/RPCServer.cpp +++ b/src/RPCServer.cpp @@ -1,5 +1,4 @@ -#include #include #include @@ -25,14 +24,6 @@ #include "NicknameState.h" #include "utils.h" -#define VALIDATORS_FETCH_SECONDS 30 -#define VALIDATORS_FILE_PATH "/" VALIDATORS_FILE_NAME -#define VALIDATORS_FILE_BYTES_MAX (50 << 10) - -/* -Just read from wire until the entire request is in. -*/ - RPCServer::RPCServer(boost::asio::io_service& io_service , NetworkOPs* nopNetwork) : mNetOps(nopNetwork), mSocket(io_service) { @@ -1462,11 +1453,11 @@ Json::Value RPCServer::doUnlAdd(Json::Value& params) std::string strNode = params[0u].asString(); std::string strComment = (params.size() == 2) ? params[1u].asString() : ""; - NewcoinAddress nodePublic; + NewcoinAddress naNodePublic; - if (nodePublic.setNodePublic(strNode)) + if (naNodePublic.setNodePublic(strNode)) { - theApp->getUNL().nodeAddPublic(nodePublic, strComment); + theApp->getUNL().nodeAddPublic(naNodePublic, UniqueNodeList::vsManual, strComment); return "adding node by public key"; } @@ -1476,8 +1467,6 @@ Json::Value RPCServer::doUnlAdd(Json::Value& params) return "adding node by domain"; } - - return "invalid params"; } // validation_create [||] @@ -1906,110 +1895,66 @@ Json::Value RPCServer::doWalletSeed(Json::Value& params) } } -void RPCServer::validatorsResponse(const boost::system::error_code& err, std::string strResponse) +// unl_delete | +Json::Value RPCServer::doUnlDelete(Json::Value& params) { - std::cerr << "Fetch '" VALIDATORS_FILE_NAME "' complete." << std::endl; - - if (!err) - { - theApp->getUNL().nodeDefault(strResponse); - } - else - { - std::cerr << "Error: " << err.message() << std::endl; - } -} - -// Populate the UNL from a validators.txt file. -Json::Value RPCServer::doUnlDefault(Json::Value& params) { - if (!params.size() || (1==params.size() && !params[0u].compare("network"))) - { - bool bNetwork = 1 == params.size(); - std::string strValidators; - - if (!bNetwork) - { - std::ifstream ifsDefault(VALIDATORS_FILE_NAME, std::ios::in); - - if (!ifsDefault) - { - std::cerr << "Failed to open '" VALIDATORS_FILE_NAME "'." << std::endl; - - bNetwork = true; - } - else - { - strValidators.assign((std::istreambuf_iterator(ifsDefault)), - std::istreambuf_iterator()); - - if (ifsDefault.bad()) - { - std::cerr << "Failed to read '" VALIDATORS_FILE_NAME "'." << std::endl; - - bNetwork = true; - } - } - } - - if (bNetwork) - { - HttpsClient::httpsGet( - theApp->getIOService(), - VALIDATORS_SITE, - 443, - VALIDATORS_FILE_PATH, - VALIDATORS_FILE_BYTES_MAX, - boost::posix_time::seconds(VALIDATORS_FETCH_SECONDS), - boost::bind(&RPCServer::validatorsResponse, this, _1, _2)); - - return "fetching " VALIDATORS_FILE_NAME; - } - else - { - theApp->getUNL().nodeDefault(strValidators); - - return "processing " VALIDATORS_FILE_NAME; - } - } - else - return RPCError(rpcINVALID_PARAMS); -} - -// unl_delete -Json::Value RPCServer::doUnlDelete(Json::Value& params) -{ - std::string strNodePublic = params[0u].asString(); + std::string strNode = params[0u].asString(); NewcoinAddress naNodePublic; - if (naNodePublic.setNodePublic(strNodePublic)) + if (naNodePublic.setNodePublic(strNode)) { - theApp->getUNL().nodeRemove(naNodePublic); + theApp->getUNL().nodeRemovePublic(naNodePublic); - return "removing node"; + return "removing node by public key"; } else { - return "invalid public key"; + theApp->getUNL().nodeRemoveDomain(strNode); + + return "removing node by domain"; } } -Json::Value RPCServer::doUnlList(Json::Value& params) +Json::Value RPCServer::doUnlList(Json::Value& params) { Json::Value obj(Json::objectValue); + obj["unl"]=theApp->getUNL().getUnlJson(); + return obj; } +// Populate the UNL from a local validators.txt file. +Json::Value RPCServer::doUnlLoad(Json::Value& params) +{ + if (!theApp->getUNL().nodeLoad()) + { + return RPCError(rpcLOAD_FAILED); + } + + return "loading"; +} + +// Populate the UNL from newcoin.org's validators.txt file. +Json::Value RPCServer::doUnlNetwork(Json::Value& params) +{ + theApp->getUNL().nodeNetwork(); + + return "fetching"; +} + // unl_reset -Json::Value RPCServer::doUnlReset(Json::Value& params) { +Json::Value RPCServer::doUnlReset(Json::Value& params) +{ theApp->getUNL().nodeReset(); return "removing nodes"; } // unl_score -Json::Value RPCServer::doUnlScore(Json::Value& params) { +Json::Value RPCServer::doUnlScore(Json::Value& params) +{ theApp->getUNL().nodeScore(); return "scoring requested"; @@ -2055,9 +2000,10 @@ Json::Value RPCServer::doCommand(const std::string& command, Json::Value& params { "tx", &RPCServer::doTx, 1, 1, }, { "unl_add", &RPCServer::doUnlAdd, 1, 2, }, - { "unl_default", &RPCServer::doUnlDefault, 0, 1, }, { "unl_delete", &RPCServer::doUnlDelete, 1, 1, }, { "unl_list", &RPCServer::doUnlList, 0, 0, }, + { "unl_load", &RPCServer::doUnlLoad, 0, 0, }, + { "unl_network", &RPCServer::doUnlNetwork, 0, 0, }, { "unl_reset", &RPCServer::doUnlReset, 0, 0, }, { "unl_score", &RPCServer::doUnlScore, 0, 0, }, diff --git a/src/RPCServer.h b/src/RPCServer.h index 4a3c6ece99..af9274e514 100644 --- a/src/RPCServer.h +++ b/src/RPCServer.h @@ -17,6 +17,9 @@ public: enum { rpcSUCCESS, + // Misc failure + rpcLOAD_FAILED, + // Networking rpcNO_CLOSED, rpcNO_CURRENT, @@ -40,7 +43,7 @@ public: rpcINVALID_PARAMS, rpcUNKNOWN_COMMAND, - // Bad paramater + // Bad parameter rpcACT_MALFORMED, rpcBAD_SEED, rpcDST_ACT_MALFORMED, @@ -131,10 +134,11 @@ private: Json::Value doTx(Json::Value& params); Json::Value doUnlAdd(Json::Value& params); - Json::Value doUnlDefault(Json::Value& params); Json::Value doUnlDelete(Json::Value& params); Json::Value doUnlFetch(Json::Value& params); Json::Value doUnlList(Json::Value& params); + Json::Value doUnlLoad(Json::Value& params); + Json::Value doUnlNetwork(Json::Value& params); Json::Value doUnlReset(Json::Value& params); Json::Value doUnlScore(Json::Value& params); @@ -151,8 +155,6 @@ private: Json::Value doWalletUnlock(Json::Value& params); Json::Value doWalletVerify(Json::Value& params); - void validatorsResponse(const boost::system::error_code& err, std::string strResponse); - public: typedef boost::shared_ptr pointer; diff --git a/src/Transaction.cpp b/src/Transaction.cpp index 56a95a5dbf..355a6a2fa5 100644 --- a/src/Transaction.cpp +++ b/src/Transaction.cpp @@ -513,7 +513,7 @@ bool Transaction::save() if ((mStatus == INVALID) || (mStatus == REMOVED)) return false; char status; - switch(mStatus) + switch (mStatus) { case NEW: status = TXN_SQL_NEW; break; case INCLUDED: status = TXN_SQL_INCLUDED; break; @@ -523,8 +523,6 @@ bool Transaction::save() default: status = TXN_SQL_UNKNOWN; } - // FIXME: This needs to check if the transaction is already there and not - // de-confirm it Database *db = theApp->getTxnDB()->getDB(); ScopedLock dbLock = theApp->getTxnDB()->getDBLock(); return db->executeSQL(mTransaction->getSQLInsertHeader() + mTransaction->getSQL(getLedger(), status) + ";"); diff --git a/src/UniqueNodeList.cpp b/src/UniqueNodeList.cpp index 50a73df507..fbee423211 100644 --- a/src/UniqueNodeList.cpp +++ b/src/UniqueNodeList.cpp @@ -9,6 +9,7 @@ #include "UniqueNodeList.h" #include "utils.h" +#include #include #include #include @@ -16,6 +17,13 @@ #include #include +#include +#include + +#define VALIDATORS_FETCH_SECONDS 30 +#define VALIDATORS_FILE_PATH "/" VALIDATORS_FILE_NAME +#define VALIDATORS_FILE_BYTES_MAX (50 << 10) + // Gather string constants. #define SECTION_CURRENCIES "currencies" #define SECTION_DOMAIN "domain" @@ -26,6 +34,7 @@ #define SECTION_VALIDATORS_URL "validators_url" // Limit pollution of database. +// YYY Move to config file. #define REFERRAL_VALIDATORS_MAX 50 #define REFERRAL_IPS_MAX 50 @@ -90,13 +99,10 @@ void UniqueNodeList::trustedLoad() mUNL.clear(); + // XXX Needs to limit by quanity and quality. SQL_FOREACH(db, "SELECT PublicKey FROM TrustedNodes WHERE Score != 0;") { - std::string strPublicKey; - - db->getStr("PublicKey", strPublicKey); - - mUNL.insert(strPublicKey); + mUNL.insert(db->getStrBinary("PublicKey")); } } @@ -169,12 +175,14 @@ void UniqueNodeList::scoreCompute() { strIndex umPulicIdx; // Map of public key to index. strIndex umDomainIdx; // Map of domain to index. - std::vector vsnNodes; + std::vector vsnNodes; // Index to scoring node. Database* db=theApp->getWalletDB()->getDB(); std::string strSql; + // For each entry in SeedDomains with a PublicKey: + // - Add an entry in umPulicIdx, umDomainIdx, and vsnNodes. { ScopedLock sl(theApp->getWalletDB()->getDBLock()); @@ -186,32 +194,91 @@ void UniqueNodeList::scoreCompute() } else { - std::string strDomain; - std::string strPublicKey; - std::string strSource; + std::string strDomain = db->getStrBinary("Domain"); + std::string strPublicKey = db->getStrBinary("PublicKey"); + std::string strSource = db->getStrBinary("Source"); + int iScore = iSourceScore(static_cast(strSource[0])); + strIndex::iterator siOld = umPulicIdx.find(strPublicKey); - db->getStr("Domain", strDomain); - db->getStr("PublicKey", strPublicKey); - db->getStr("Source", strSource); + if (siOld == umPulicIdx.end()) + { + // New node + int iNode = vsnNodes.size(); + umPulicIdx[strPublicKey] = iNode; + umDomainIdx[strDomain] = iNode; + + scoreNode snCurrent; + + snCurrent.strValidator = strPublicKey; + snCurrent.iScore = iScore; + snCurrent.iRoundSeed = snCurrent.iScore; + snCurrent.iRoundScore = 0; + snCurrent.iSeen = -1; + + vsnNodes.push_back(snCurrent); + } + else + { + scoreNode& snOld = vsnNodes[siOld->second]; + + if (snOld.iScore < iScore) + { + // Update old node + + snOld.iScore = iScore; + snOld.iRoundSeed = snOld.iScore; + } + } + } + } + } + + // For each entry in SeedNodes: + // - Add an entry in umPulicIdx, umDomainIdx, and vsnNodes. + { + ScopedLock sl(theApp->getWalletDB()->getDBLock()); + + SQL_FOREACH(db, "SELECT PublicKey,Source FROM SeedNodes;") + { + std::string strPublicKey = db->getStrBinary("PublicKey"); + std::string strSource = db->getStrBinary("Source"); + int iScore = iSourceScore(static_cast(strSource[0])); + strIndex::iterator siOld = umPulicIdx.find(strPublicKey); + + if (siOld == umPulicIdx.end()) + { + // New node int iNode = vsnNodes.size(); umPulicIdx[strPublicKey] = iNode; - umDomainIdx[strDomain] = iNode; scoreNode snCurrent; snCurrent.strValidator = strPublicKey; - snCurrent.iScore = iSourceScore(static_cast(strSource[0])); + snCurrent.iScore = iScore; snCurrent.iRoundSeed = snCurrent.iScore; snCurrent.iRoundScore = 0; snCurrent.iSeen = -1; vsnNodes.push_back(snCurrent); } + else + { + scoreNode& snOld = vsnNodes[siOld->second]; + + if (snOld.iScore < iScore) + { + // Update old node + + snOld.iScore = iScore; + snOld.iRoundSeed = snOld.iScore; + } + } } } + // For debugging, print out initial scores. BOOST_FOREACH(scoreNode& sn, vsnNodes) { std::cerr << str(boost::format("%s| %d, %d, %d") @@ -222,11 +289,10 @@ void UniqueNodeList::scoreCompute() << std::endl; } - // XXX When we have a CAS do SeedNodes here. - // std::cerr << str(boost::format("vsnNodes.size=%d") % vsnNodes.size()) << std::endl; // Step through growing list of nodes adding each validation list. + // - Each validator may have provided referals. Add those referals as validators. for (int iNode=0; iNode != vsnNodes.size(); iNode++) { scoreNode& sn = vsnNodes[iNode]; @@ -238,11 +304,9 @@ void UniqueNodeList::scoreCompute() SQL_FOREACH(db, str(boost::format("SELECT Referral FROM ValidatorReferrals WHERE Validator=%s ORDER BY Entry;") % db->escape(strValidator))) { - std::string strReferral; + std::string strReferral = db->getStrBinary("Referral"); int iReferral; - db->getStr("Referral", strReferral); - strIndex::iterator itEntry; NewcoinAddress na; @@ -335,11 +399,7 @@ void UniqueNodeList::scoreCompute() SQL_FOREACH(db, str(boost::format("SELECT PublicKey,Seen FROM TrustedNodes WHERE PublicKey IN (%s);") % strJoin(vstrPublicKeys.begin(), vstrPublicKeys.end(), ","))) { - std::string strPublicKey; - - db->getStr("PublicKey", strPublicKey); - - vsnNodes[umPulicIdx[strPublicKey]].iSeen = db->getNull("Seen") ? -1 : db->getInt("Seen"); + vsnNodes[umPulicIdx[db->getStrBinary("PublicKey")]].iSeen = db->getNull("Seen") ? -1 : db->getInt("Seen"); } } @@ -358,7 +418,7 @@ void UniqueNodeList::scoreCompute() std::string strSeen = sn.iSeen >= 0 ? str(boost::format("%d") % sn.iSeen) : "NULL"; vstrValues[iNode] = str(boost::format("(%s,%s,%s)") - % db->escape(sn.strValidator) + % sqlEscape(sn.strValidator) % sn.iScore % strSeen); @@ -372,6 +432,7 @@ void UniqueNodeList::scoreCompute() { ScopedLock sl(mUNLLock); + // XXX Should limit to scores above a certain minimum and limit to a certain number. mUNL.swap(usUNL); } @@ -387,11 +448,7 @@ void UniqueNodeList::scoreCompute() // For every IpReferral add a score for the IP and PORT. SQL_FOREACH(db, "SELECT Validator,COUNT(*) AS Count FROM IpReferrals GROUP BY Validator;") { - std::string strValidator; - - db->getStr("Validator", strValidator); - - umValidators[strValidator] = db->getInt("Count"); + umValidators[db->getStrBinary("Validator")] = db->getInt("Count"); // std::cerr << strValidator << ":" << db->getInt("Count") << std::endl; } @@ -418,13 +475,11 @@ void UniqueNodeList::scoreCompute() % db->escape(strValidator))) { score iPoints = iBase * (iEntries - iEntry) / iEntries; - std::string strIP; int iPort; - db->getStr("IP", strIP); iPort = db->getNull("Port") ? -1 : db->getInt("Port"); - std::pair< std::string, int> ep = std::make_pair(strIP, iPort); + std::pair< std::string, int> ep = std::make_pair(db->getStrBinary("IP"), iPort); epScore::iterator itEp = umScore.find(ep); @@ -491,6 +546,7 @@ void UniqueNodeList::scoreTimerHandler(const boost::system::error_code& err) } // Start a timer to update scores. +// <-- bNow: true, to force scoring for debugging. void UniqueNodeList::scoreNext(bool bNow) { // std::cerr << str(boost::format("scoreNext: mtpFetchUpdated=%s mtpScoreStart=%s mtpScoreUpdated=%s mtpScoreNext=%s") % mtpFetchUpdated % mtpScoreStart % mtpScoreUpdated % mtpScoreNext) << std::endl; @@ -534,6 +590,7 @@ void UniqueNodeList::fetchFinish() fetchNext(); } +// Called when we need to update scores. void UniqueNodeList::fetchDirty() { // Note update. @@ -631,11 +688,14 @@ void UniqueNodeList::processIps(const std::string& strSite, const NewcoinAddress } // Persist ValidatorReferrals. -void UniqueNodeList::processValidators(const std::string& strSite, const std::string& strValidatorsSrc, const NewcoinAddress& naNodePublic, section::mapped_type* pmtVecStrValidators) +// --> strSite: source site for display +// --> strValidatorsSrc: source details for display +// --> naNodePublic: remote source public key - not valid for local +// --> vsWhy: reason for adding validator to SeedDomains or SeedNodes. +void UniqueNodeList::processValidators(const std::string& strSite, const std::string& strValidatorsSrc, const NewcoinAddress& naNodePublic, validatorSource vsWhy, section::mapped_type* pmtVecStrValidators) { - Database* db=theApp->getWalletDB()->getDB(); - - std::string strEscNodePublic = db->escape(naNodePublic.humanNodePublic()); + Database* db = theApp->getWalletDB()->getDB(); + std::string strNodePublic = naNodePublic.isValid() ? naNodePublic.humanNodePublic() : "local"; std::cerr << str(boost::format("Validator: '%s' : '%s' : processing %d validators.") @@ -647,7 +707,8 @@ void UniqueNodeList::processValidators(const std::string& strSite, const std::st // Remove all current Validator's entries in ValidatorReferrals { ScopedLock sl(theApp->getWalletDB()->getDBLock()); - db->executeSQL(str(boost::format("DELETE FROM ValidatorReferrals WHERE Validator=%s;") % strEscNodePublic)); + + db->executeSQL(str(boost::format("DELETE FROM ValidatorReferrals WHERE Validator='%s';") % strNodePublic)); // XXX Check result. } @@ -655,38 +716,68 @@ void UniqueNodeList::processValidators(const std::string& strSite, const std::st if (pmtVecStrValidators && pmtVecStrValidators->size()) { std::vector vstrValues; - vstrValues.resize(MIN(pmtVecStrValidators->size(), REFERRAL_VALIDATORS_MAX)); + vstrValues.reserve(MIN(pmtVecStrValidators->size(), REFERRAL_VALIDATORS_MAX)); - int i = 0; + int iValues = 0; BOOST_FOREACH(std::string strReferral, *pmtVecStrValidators) { - if (i == REFERRAL_VALIDATORS_MAX) + if (iValues == REFERRAL_VALIDATORS_MAX) break; - vstrValues[i] = str(boost::format("(%s,%d,%s)") % strEscNodePublic % i % db->escape(strReferral)); - i++; + boost::smatch smMatch; + std::string strIP; - NewcoinAddress naValidator; + // domain comment? + // public_key comment? + static boost::regex reReferral("\\`\\s*(\\S+)(?:\\s+(.+))?\\s*\\'"); - if (naValidator.setNodePublic(strReferral)) + if (!boost::regex_match(strReferral, smMatch, reReferral)) { - // A public key. - // XXX Schedule for CAS lookup. + std::cerr + << str(boost::format("Validator: '%s' ["SECTION_VALIDATORS"]: rejecting '%s'") + % strSite % strReferral) + << std::endl; } else { - // A domain: need to look it up. - nodeAddDomain(strReferral, vsReferral); + std::string strRefered = smMatch[1]; + std::string strComment = smMatch[2]; + NewcoinAddress naValidator; + + // std::cerr << str(boost::format("Validator: '%s' : '%s'") % strRefered % strComment) << std::endl; + + if (naValidator.setNodePublic(strRefered)) + { + // A public key. + // XXX Schedule for CAS lookup. + nodeAddPublic(naValidator, vsWhy, strComment); + + if (naNodePublic.isValid()) + vstrValues.push_back(str(boost::format("('%s',%d,'%s')") % strNodePublic % iValues % naValidator.humanNodePublic())); + } + else + { + // A domain: need to look it up. + nodeAddDomain(strRefered, vsWhy, strComment); + + if (naNodePublic.isValid()) + vstrValues.push_back(str(boost::format("('%s',%d,%s)") % strNodePublic % iValues % sqlEscape(strRefered))); + } + + iValues++; } } - std::string strSql = str(boost::format("INSERT INTO ValidatorReferrals (Validator,Entry,Referral) VALUES %s;") - % strJoin(vstrValues.begin(), vstrValues.end(), ",")); + if (!vstrValues.empty()) + { + std::string strSql = str(boost::format("INSERT INTO ValidatorReferrals (Validator,Entry,Referral) VALUES %s;") + % strJoin(vstrValues.begin(), vstrValues.end(), ",")); - ScopedLock sl(theApp->getWalletDB()->getDBLock()); + ScopedLock sl(theApp->getWalletDB()->getDBLock()); - db->executeSQL(strSql); - // XXX Check result. + db->executeSQL(strSql); + // XXX Check result. + } } fetchDirty(); @@ -707,7 +798,7 @@ void UniqueNodeList::responseIps(const std::string& strSite, const NewcoinAddres // Process section [ips_url]. // If we have a section with a single entry, fetch the url and process it. -void UniqueNodeList::getIpsUrl(NewcoinAddress naNodePublic, section secSite) +void UniqueNodeList::getIpsUrl(const NewcoinAddress& naNodePublic, section secSite) { std::string strIpsUrl; std::string strDomain; @@ -733,20 +824,20 @@ void UniqueNodeList::getIpsUrl(NewcoinAddress naNodePublic, section secSite) } // Given a section with validators, parse and persist it. -void UniqueNodeList::responseValidators(const std::string& strValidatorsUrl, NewcoinAddress naNodePublic, section secSite, const std::string& strSite, const boost::system::error_code& err, const std::string strValidatorsFile) +void UniqueNodeList::responseValidators(const std::string& strValidatorsUrl, const NewcoinAddress& naNodePublic, section secSite, const std::string& strSite, const boost::system::error_code& err, const std::string strValidatorsFile) { if (!err) { section secFile = ParseSection(strValidatorsFile, true); - processValidators(strSite, strValidatorsUrl, naNodePublic, sectionEntries(secFile, SECTION_VALIDATORS)); + processValidators(strSite, strValidatorsUrl, naNodePublic, vsValidator, sectionEntries(secFile, SECTION_VALIDATORS)); } getIpsUrl(naNodePublic, secSite); } // Process section [validators_url]. -void UniqueNodeList::getValidatorsUrl(NewcoinAddress naNodePublic, section secSite) +void UniqueNodeList::getValidatorsUrl(const NewcoinAddress& naNodePublic, section secSite) { std::string strValidatorsUrl; std::string strDomain; @@ -772,12 +863,12 @@ void UniqueNodeList::getValidatorsUrl(NewcoinAddress naNodePublic, section secSi } // Process a newcoin.txt. -void UniqueNodeList::processFile(const std::string strDomain, NewcoinAddress naNodePublic, section secSite) +void UniqueNodeList::processFile(const std::string strDomain, const NewcoinAddress& naNodePublic, section secSite) { // // Process Validators // - processValidators(strDomain, NODE_FILE_NAME, naNodePublic, sectionEntries(secSite, SECTION_VALIDATORS)); + processValidators(strDomain, NODE_FILE_NAME, naNodePublic, vsReferral, sectionEntries(secSite, SECTION_VALIDATORS)); // // Process ips @@ -981,7 +1072,7 @@ void UniqueNodeList::fetchNext() tpNow = boost::posix_time::second_clock::universal_time(); std::cerr << str(boost::format("fetchNext: iNext=%s tpNext=%s tpNow=%s") % iNext % tpNext % tpNow) << std::endl; - db->getStr("Domain", strDomain); + strDomain = db->getStrBinary("Domain"); db->endIterRows(); } @@ -1057,48 +1148,7 @@ int UniqueNodeList::iSourceScore(validatorSource vsWhy) return iScore; } -// Queue a domain for a single attempt fetch a newcoin.txt. -// --> strComment: only used on vsManual -// YYY As a lot of these may happen at once, would be nice to wrap multiple calls in a transaction. -void UniqueNodeList::nodeAddDomain(const std::string& strDomain, validatorSource vsWhy, std::string strComment) -{ - // YYY Would be best to verify strDomain is a valid domain. - // std::cerr << str(boost::format("nodeAddDomain: '%s' %c '%s'") - // % strDomain - // % vsWhy - // % strComment) << std::endl; - - seedDomain sdCurrent; - - bool bFound = getSeedDomains(strDomain, sdCurrent); - bool bChanged = false; - - if (!bFound) - { - sdCurrent.strDomain = strDomain; - sdCurrent.tpNext = boost::posix_time::second_clock::universal_time(); - } - - // Promote source, if needed. - if (!bFound || iSourceScore(vsWhy) >= iSourceScore(sdCurrent.vsSource)) - { - sdCurrent.vsSource = vsWhy; - bChanged = true; - } - - if (vsManual == vsWhy) - { - // A manual add forces immediate scan. - sdCurrent.tpNext = boost::posix_time::second_clock::universal_time(); - sdCurrent.strComment = strComment; - bChanged = true; - } - - if (bChanged) - setSeedDomains(sdCurrent, true); -} - -// Retrieve a SeedDomain for DB. +// Retrieve a SeedDomain from DB. bool UniqueNodeList::getSeedDomains(const std::string& strDomain, seedDomain& dstSeedDomain) { bool bResult; @@ -1113,13 +1163,12 @@ bool UniqueNodeList::getSeedDomains(const std::string& strDomain, seedDomain& ds if (bResult) { std::string strPublicKey; - std::string strSource; int iNext; int iScan; int iFetch; std::string strSha256; - db->getStr("Domain", dstSeedDomain.strDomain); + dstSeedDomain.strDomain = db->getStrBinary("Domain"); if (!db->getNull("PublicKey") && db->getStr("PublicKey", strPublicKey)) { @@ -1130,14 +1179,16 @@ bool UniqueNodeList::getSeedDomains(const std::string& strDomain, seedDomain& ds dstSeedDomain.naPublicKey.clear(); } - db->getStr("Source", strSource); + std::string strSource = db->getStrBinary("Source"); dstSeedDomain.vsSource = static_cast(strSource[0]); + iNext = db->getInt("Next"); dstSeedDomain.tpNext = ptFromSeconds(iNext); iScan = db->getInt("Scan"); dstSeedDomain.tpScan = ptFromSeconds(iScan); iFetch = db->getInt("Fetch"); dstSeedDomain.tpFetch = ptFromSeconds(iFetch); + if (!db->getNull("Sha256") && db->getStr("Sha256", strSha256)) { dstSeedDomain.iSha256.SetHex(strSha256); @@ -1146,7 +1197,7 @@ bool UniqueNodeList::getSeedDomains(const std::string& strDomain, seedDomain& ds { dstSeedDomain.iSha256.zero(); } - db->getStr("Comment", dstSeedDomain.strComment); + dstSeedDomain.strComment = db->getStrBinary("Comment"); db->endIterRows(); } @@ -1191,68 +1242,227 @@ void UniqueNodeList::setSeedDomains(const seedDomain& sdSource, bool bNext) } } -// Add a trusted node. Called by RPC or other source. -// XXX Broken should operate on seeds. -void UniqueNodeList::nodeAddPublic(const NewcoinAddress& naNodePublic, const std::string& strComment) +// Queue a domain for a single attempt fetch a newcoin.txt. +// --> strComment: only used on vsManual +// YYY As a lot of these may happen at once, would be nice to wrap multiple calls in a transaction. +void UniqueNodeList::nodeAddDomain(std::string strDomain, validatorSource vsWhy, const std::string& strComment) { - std::string strPublicKey = naNodePublic.humanNodePublic(); + boost::trim(strDomain); + boost::to_lower(strDomain); + // YYY Would be best to verify strDomain is a valid domain. + // std::cerr << str(boost::format("nodeAddDomain: '%s' %c '%s'") + // % strDomain + // % vsWhy + // % strComment) << std::endl; + + seedDomain sdCurrent; + + bool bFound = getSeedDomains(strDomain, sdCurrent); + bool bChanged = false; + + if (!bFound) { - Database* db=theApp->getWalletDB()->getDB(); - ScopedLock sl(theApp->getWalletDB()->getDBLock()); + sdCurrent.strDomain = strDomain; + sdCurrent.tpNext = boost::posix_time::second_clock::universal_time(); + } - if( db->executeSQL(str(boost::format("SELECT count(*) from TrustedNodes where PublicKey=%s;") % db->escape(strPublicKey))) && - db->startIterRows() && db->getInt(0)==1 ) - { // exists. update the comment - db->executeSQL(str(boost::format("UPDATE TrustedNodes set Comment=%s where PublicKey=%s;") % db->escape(strComment) % db->escape(strPublicKey) )); - }else - { // new node - db->executeSQL(str(boost::format("INSERT INTO TrustedNodes (PublicKey,Comment) values (%s,%s);") - % db->escape(strPublicKey) % db->escape(strComment))); + // Promote source, if needed. + if (!bFound || iSourceScore(vsWhy) >= iSourceScore(sdCurrent.vsSource)) + { + sdCurrent.vsSource = vsWhy; + sdCurrent.strComment = strComment; + bChanged = true; + } + + if (vsManual == vsWhy) + { + // A manual add forces immediate scan. + sdCurrent.tpNext = boost::posix_time::second_clock::universal_time(); + bChanged = true; + } + + if (bChanged) + setSeedDomains(sdCurrent, true); +} + +// Retrieve a SeedNode from DB. +bool UniqueNodeList::getSeedNodes(const NewcoinAddress& naNodePublic, seedNode& dstSeedNode) +{ + bool bResult; + Database* db=theApp->getWalletDB()->getDB(); + + std::string strSql = str(boost::format("SELECT * FROM SeedNodes WHERE PublicKey='%s';") + % naNodePublic.humanNodePublic()); + + ScopedLock sl(theApp->getWalletDB()->getDBLock()); + + bResult = db->executeSQL(strSql) && db->startIterRows(); + if (bResult) + { + std::string strPublicKey; + std::string strSource; + int iNext; + int iScan; + int iFetch; + std::string strSha256; + + if (!db->getNull("PublicKey") && db->getStr("PublicKey", strPublicKey)) + { + dstSeedNode.naPublicKey.setNodePublic(strPublicKey); + } + else + { + dstSeedNode.naPublicKey.clear(); } - + strSource = db->getStrBinary("Source"); + dstSeedNode.vsSource = static_cast(strSource[0]); + + iNext = db->getInt("Next"); + dstSeedNode.tpNext = ptFromSeconds(iNext); + iScan = db->getInt("Scan"); + dstSeedNode.tpScan = ptFromSeconds(iScan); + iFetch = db->getInt("Fetch"); + dstSeedNode.tpFetch = ptFromSeconds(iFetch); + + if (!db->getNull("Sha256") && db->getStr("Sha256", strSha256)) + { + dstSeedNode.iSha256.SetHex(strSha256); + } + else + { + dstSeedNode.iSha256.zero(); + } + dstSeedNode.strComment = db->getStrBinary("Comment"); + + db->endIterRows(); } - { - ScopedLock slUNL(mUNLLock); - - mUNL.insert(strPublicKey); - } + return bResult; } -// XXX Broken should operate on seeds. -void UniqueNodeList::nodeRemove(NewcoinAddress naNodePublic) +// Persist a SeedNode. +// <-- bNext: true, to do fetching if needed. +void UniqueNodeList::setSeedNodes(const seedNode& snSource, bool bNext) { - std::string strPublicKey = naNodePublic.humanNodePublic(); + Database* db=theApp->getWalletDB()->getDB(); + + int iNext = iToSeconds(snSource.tpNext); + int iScan = iToSeconds(snSource.tpScan); + int iFetch = iToSeconds(snSource.tpFetch); + + // std::cerr << str(boost::format("setSeedNodes: iNext=%s tpNext=%s") % iNext % sdSource.tpNext) << std::endl; + + assert(snSource.naPublicKey.isValid()); + + std::string strSql = str(boost::format("REPLACE INTO SeedNodes (PublicKey,Source,Next,Scan,Fetch,Sha256,Comment) VALUES ('%s', '%c', %d, %d, %d, '%s', %s);") + % snSource.naPublicKey.humanNodePublic() + % static_cast(snSource.vsSource) + % iNext + % iScan + % iFetch + % snSource.iSha256.GetHex() + % sqlEscape(snSource.strComment) + ); + + { + ScopedLock sl(theApp->getWalletDB()->getDBLock()); + + if (!db->executeSQL(strSql)) + { + // XXX Check result. + std::cerr << "setSeedNodes: failed." << std::endl; + } + } + +#if 0 + // YYY When we have a cas schedule lookups similar to this. + if (bNext && (mtpFetchNext.is_not_a_date_time() || mtpFetchNext > snSource.tpNext)) + { + // Schedule earlier wake up. + fetchNext(); + } +#else + fetchDirty(); +#endif +} + +// Add a trusted node. Called by RPC or other source. +void UniqueNodeList::nodeAddPublic(const NewcoinAddress& naNodePublic, validatorSource vsWhy, const std::string& strComment) +{ + seedNode snCurrent; + + bool bFound = getSeedNodes(naNodePublic, snCurrent); + bool bChanged = false; + + if (!bFound) + { + snCurrent.naPublicKey = naNodePublic; + snCurrent.tpNext = boost::posix_time::second_clock::universal_time(); + } + + // Promote source, if needed. + if (!bFound || iSourceScore(vsWhy) >= iSourceScore(snCurrent.vsSource)) + { + snCurrent.vsSource = vsWhy; + snCurrent.strComment = strComment; + bChanged = true; + } + + if (vsManual == vsWhy) + { + // A manual add forces immediate scan. + snCurrent.tpNext = boost::posix_time::second_clock::universal_time(); + bChanged = true; + } + + if (bChanged) + setSeedNodes(snCurrent, true); +} + +void UniqueNodeList::nodeRemovePublic(const NewcoinAddress& naNodePublic) +{ + { + Database* db=theApp->getWalletDB()->getDB(); + ScopedLock sl(theApp->getWalletDB()->getDBLock()); + + db->executeSQL(str(boost::format("DELETE FROM SeedNodes WHERE PublicKey=%s") % naNodePublic.humanNodePublic())); + } + + // YYY Only dirty on successful delete. + fetchDirty(); +} + +void UniqueNodeList::nodeRemoveDomain(std::string strDomain) +{ + boost::trim(strDomain); + boost::to_lower(strDomain); { Database* db=theApp->getWalletDB()->getDB(); ScopedLock sl(theApp->getWalletDB()->getDBLock()); - db->executeSQL(str(boost::format("DELETE FROM TrustedNodes where PublicKey=%s") % db->escape(strPublicKey))); + db->executeSQL(str(boost::format("DELETE FROM SeedDomains WHERE Domain=%s") % sqlEscape(strDomain))); } - { - ScopedLock slUNL(mUNLLock); - mUNL.erase(strPublicKey); - } + // YYY Only dirty on successful delete. + fetchDirty(); } -// XXX Broken should operate on seeds. void UniqueNodeList::nodeReset() { { Database* db=theApp->getWalletDB()->getDB(); ScopedLock sl(theApp->getWalletDB()->getDBLock()); - db->executeSQL("DELETE FROM TrustedNodes"); - } - { - ScopedLock slUNL(mUNLLock); - mUNL.clear(); + // XXX Check results. + db->executeSQL("DELETE FROM SeedDomains"); + db->executeSQL("DELETE FROM SeedNodes"); } + + fetchDirty(); } Json::Value UniqueNodeList::getUnlJson() @@ -1264,16 +1474,10 @@ Json::Value UniqueNodeList::getUnlJson() ScopedLock sl(theApp->getWalletDB()->getDBLock()); SQL_FOREACH(db, "SELECT * FROM TrustedNodes;") { - std::string strPublicKey; - std::string strComment; - - db->getStr("PublicKey", strPublicKey); - db->getStr("Comment", strComment); - Json::Value node(Json::objectValue); - node["publicKey"] = strPublicKey; - node["comment"] = strComment; + node["publicKey"] = db->getStrBinary("PublicKey"); + node["comment"] = db->getStrBinary("Comment"); ret.append(node); } @@ -1281,18 +1485,128 @@ Json::Value UniqueNodeList::getUnlJson() return ret; } +bool UniqueNodeList::nodeLoad() +{ + if (theConfig.UNL_DEFAULT.empty()) + { + std::cerr << "UNL_DEFAULT not specified." << std::endl; + + return false; + } + + if (!boost::filesystem::exists(theConfig.UNL_DEFAULT)) + { + std::cerr << str(boost::format("UNL_DEFAULT not found: %s") % theConfig.UNL_DEFAULT) << std::endl; + + return false; + } + + if (!boost::filesystem::is_regular_file(theConfig.UNL_DEFAULT)) + { + std::cerr << str(boost::format("UNL_DEFAULT not regular file: %s") % theConfig.UNL_DEFAULT) << std::endl; + + return false; + } + + std::ifstream ifsDefault(theConfig.UNL_DEFAULT.native().c_str(), std::ios::in); + + if (!ifsDefault) + { + std::cerr << str(boost::format("Failed to open: %s") % theConfig.UNL_DEFAULT) << std::endl; + + return false; + } + + std::string strValidators; + + strValidators.assign((std::istreambuf_iterator(ifsDefault)), + std::istreambuf_iterator()); + + if (ifsDefault.bad()) + { + std::cerr << str(boost::format("Failed to read: %s") % theConfig.UNL_DEFAULT) << std::endl; + + return false; + } + + nodeDefault(strValidators, theConfig.UNL_DEFAULT.native()); + + std::cerr << str(boost::format("Processing: %s") % theConfig.UNL_DEFAULT) << std::endl; + + return true; +} + +void UniqueNodeList::validatorsResponse(const boost::system::error_code& err, std::string strResponse) +{ + std::cerr << "Fetch '" VALIDATORS_FILE_NAME "' complete." << std::endl; + + if (!err) + { + nodeDefault(strResponse, VALIDATORS_SITE); + } + else + { + std::cerr << "Error: " << err.message() << std::endl; + } +} + +void UniqueNodeList::nodeNetwork() +{ + HttpsClient::httpsGet( + theApp->getIOService(), + VALIDATORS_SITE, + 443, + VALIDATORS_FILE_PATH, + VALIDATORS_FILE_BYTES_MAX, + boost::posix_time::seconds(VALIDATORS_FETCH_SECONDS), + boost::bind(&UniqueNodeList::validatorsResponse, this, _1, _2)); +} + +void UniqueNodeList::nodeBootstrap() +{ + int iDomains = 0; + int iNodes = 0; + + { + Database* db=theApp->getWalletDB()->getDB(); + + ScopedLock sl(theApp->getWalletDB()->getDBLock()); + + if (db->executeSQL(str(boost::format("SELECT COUNT(*) AS Count FROM SeedDomains WHERE Source='%s' OR Source='%c';") % vsManual % vsValidator)) && db->startIterRows()) + iDomains = db->getInt("Count"); + + if (db->executeSQL(str(boost::format("SELECT COUNT(*) AS Count FROM SeedNodes WHERE Source='%s' OR Source='%c';") % vsManual % vsValidator)) && db->startIterRows()) + iNodes = db->getInt("Count"); + } + + bool bLoaded = iDomains || iNodes; + + if (!bLoaded && !theConfig.UNL_DEFAULT.empty()) + { + std::cerr << "Bootstrapping UNL: loading from file." << std::endl; + + bLoaded = nodeLoad(); + } + + if (!bLoaded) + { + std::cerr << "Bootstrapping UNL: loading from " VALIDATORS_SITE "." << std::endl; + nodeNetwork(); + } +} + // Process a validators.txt. -// --> strValidators: a validators.txt -void UniqueNodeList::nodeDefault(const std::string& strValidators) { +// --> strValidators: contents of a validators.txt +void UniqueNodeList::nodeDefault(const std::string& strValidators, const std::string& strSource) { section secValidators = ParseSection(strValidators, true); section::mapped_type* pmtEntries = sectionEntries(secValidators, SECTION_VALIDATORS); if (pmtEntries) { - BOOST_FOREACH(std::string strValidator, *pmtEntries) - { - nodeAddDomain(strValidator, vsValidator); - } + NewcoinAddress naInvalid; // Don't want a referrer on added entries. + + // YYY Unspecified might be bootstrap or rpc command + processValidators("unspecified", strSource, naInvalid, vsValidator, pmtEntries); } else { diff --git a/src/UniqueNodeList.h b/src/UniqueNodeList.h index 40c6d99841..8ef9b88f0e 100644 --- a/src/UniqueNodeList.h +++ b/src/UniqueNodeList.h @@ -47,6 +47,7 @@ private: boost::recursive_mutex mUNLLock; // XXX Make this faster, make this the contents vector unsigned char or raw public key. + // XXX Contents needs to based on score. boost::unordered_set mUNL; bool miscLoad(); @@ -63,6 +64,16 @@ private: std::string strComment; } seedDomain; + typedef struct { + NewcoinAddress naPublicKey; + validatorSource vsSource; + boost::posix_time::ptime tpNext; + boost::posix_time::ptime tpScan; + boost::posix_time::ptime tpFetch; + uint256 iSha256; + std::string strComment; + } seedNode; + // Used to distribute scores. typedef struct { int iScore; @@ -104,35 +115,45 @@ private: void fetchProcess(std::string strDomain); void fetchTimerHandler(const boost::system::error_code& err); - void getValidatorsUrl(NewcoinAddress naNodePublic, section secSite); - void getIpsUrl(NewcoinAddress naNodePublic, section secSite); + void getValidatorsUrl(const NewcoinAddress& naNodePublic, section secSite); + void getIpsUrl(const NewcoinAddress& naNodePublic, section secSite); void responseIps(const std::string& strSite, const NewcoinAddress& naNodePublic, const boost::system::error_code& err, const std::string strIpsFile); - void responseValidators(const std::string& strValidatorsUrl, NewcoinAddress naNodePublic, section secSite, const std::string& strSite, const boost::system::error_code& err, const std::string strValidatorsFile); + void responseValidators(const std::string& strValidatorsUrl, const NewcoinAddress& naNodePublic, section secSite, const std::string& strSite, const boost::system::error_code& err, const std::string strValidatorsFile); void processIps(const std::string& strSite, const NewcoinAddress& naNodePublic, section::mapped_type* pmtVecStrIps); - void processValidators(const std::string& strSite, const std::string& strValidatorsSrc, const NewcoinAddress& naNodePublic, section::mapped_type* pmtVecStrValidators); + void processValidators(const std::string& strSite, const std::string& strValidatorsSrc, const NewcoinAddress& naNodePublic, validatorSource vsWhy, section::mapped_type* pmtVecStrValidators); - void processFile(const std::string strDomain, NewcoinAddress naNodePublic, section secSite); + void processFile(const std::string strDomain, const NewcoinAddress& naNodePublic, section secSite); bool getSeedDomains(const std::string& strDomain, seedDomain& dstSeedDomain); void setSeedDomains(const seedDomain& dstSeedDomain, bool bNext); + bool getSeedNodes(const NewcoinAddress& naNodePublic, seedNode& dstSeedNode); + void setSeedNodes(const seedNode& snSource, bool bNext); + + void validatorsResponse(const boost::system::error_code& err, std::string strResponse); + void nodeDefault(const std::string& strValidators, const std::string& strSource); + public: UniqueNodeList(boost::asio::io_service& io_service); // Begin processing. void start(); - void nodeAddPublic(const NewcoinAddress& naNodePublic, const std::string& strComment); - void nodeAddDomain(const std::string& strDomain, validatorSource vsWhy, std::string strComment=""); - void nodeRemove(NewcoinAddress naNodePublic); - void nodeDefault(const std::string& strValidators); + void nodeAddPublic(const NewcoinAddress& naNodePublic, validatorSource vsWhy, const std::string& strComment); + void nodeAddDomain(std::string strDomain, validatorSource vsWhy, const std::string& strComment=""); + void nodeRemovePublic(const NewcoinAddress& naNodePublic); + void nodeRemoveDomain(std::string strDomain); void nodeReset(); void nodeScore(); bool nodeInUNL(const NewcoinAddress& naNodePublic); + void nodeBootstrap(); + bool nodeLoad(); + void nodeNetwork(); + Json::Value getUnlJson(); }; diff --git a/src/Wallet.cpp b/src/Wallet.cpp index eaecc2f302..9f12140f0f 100644 --- a/src/Wallet.cpp +++ b/src/Wallet.cpp @@ -53,13 +53,8 @@ bool Wallet::nodeIdentityLoad() mNodePublicKey.setNodePublic(strPublicKey); mNodePrivateKey.setNodePrivate(strPrivateKey); - std::string strDh512, strDh1024; - - db->getStr("Dh512", strDh512); - db->getStr("Dh1024", strDh1024); - - mDh512 = DH_der_load_hex(strDh512); - mDh1024 = DH_der_load_hex(strDh1024); + mDh512 = DH_der_load(db->getStrBinary("Dh512")); + mDh1024 = DH_der_load(db->getStrBinary("Dh1024")); db->endIterRows(); bSuccess = true; @@ -81,13 +76,11 @@ bool Wallet::nodeIdentityCreate() { // Make new key. - std::string strDh512, strDh1024; - - DH_der_gen_hex(strDh512, 512); // Using hex as db->escape in insufficient. + std::string strDh512 = DH_der_gen(512); #if 1 - strDh1024 = strDh512; // For testing and most cases 512 is fine. + std::string strDh1024 = strDh512; // For testing and most cases 512 is fine. #else - DH_der_gen_hex(strDh1024, 1024); + std::string strDh1024 = DH_der_gen(1024); #endif // @@ -96,11 +89,11 @@ bool Wallet::nodeIdentityCreate() { Database* db = theApp->getWalletDB()->getDB(); ScopedLock sl(theApp->getWalletDB()->getDBLock()); - db->executeSQL(str(boost::format("INSERT INTO NodeIdentity (PublicKey,PrivateKey,Dh512,Dh1024) VALUES (%s,%s,%s,%s);") - % db->escape(naNodePublic.humanNodePublic()) - % db->escape(naNodePrivate.humanNodePrivate()) - % db->escape(strDh512) - % db->escape(strDh1024))); + db->executeSQL(str(boost::format("INSERT INTO NodeIdentity (PublicKey,PrivateKey,Dh512,Dh1024) VALUES ('%s','%s',%s,%s);") + % naNodePublic.humanNodePublic() + % naNodePrivate.humanNodePrivate() + % sqlEscape(strDh512) + % sqlEscape(strDh1024))); // XXX Check error result. std::cerr << "NodeIdentity: Created." << std::endl; diff --git a/src/main.cpp b/src/main.cpp index 7c1ee53746..8ec61c211a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -59,8 +59,10 @@ void printHelp(const po::options_description& desc) cout << " transit_set " << endl; cout << " tx " << endl; cout << " unl_add | []" << endl; - cout << " unl_delete " << endl; + cout << " unl_delete |" << endl; cout << " unl_list" << endl; + cout << " unl_load" << endl; + cout << " unl_network" << endl; cout << " unl_reset" << endl; cout << " validation_create [||]" << endl; cout << " validation_seed [||]" << endl; diff --git a/src/utils.cpp b/src/utils.cpp index 92f2379c26..d70caeb72c 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -95,10 +95,11 @@ std::string strCopy(const std::vector& vucSrc) // DH support // -void DH_der_gen(std::string& strDer, int iKeyLength) +std::string DH_der_gen(int iKeyLength) { - DH* dh = 0; - int iCodes; + DH* dh = 0; + int iCodes; + std::string strDer; do { dh = DH_generate_parameters(iKeyLength, DH_GENERATOR_5, NULL, NULL); @@ -111,15 +112,8 @@ void DH_der_gen(std::string& strDer, int iKeyLength) unsigned char* next = reinterpret_cast(&strDer[0]); (void) i2d_DHparams(dh, &next); -} -void DH_der_gen_hex(std::string& strDer, int iKeyLength) -{ - std::string strBuf; - - DH_der_gen(strBuf, iKeyLength); - - strDer = strHex(strBuf); + return strDer; } DH* DH_der_load(const std::string& strDer) @@ -129,15 +123,6 @@ DH* DH_der_load(const std::string& strDer) return d2i_DHparams(NULL, &pbuf, strDer.size()); } -DH* DH_der_load_hex(const std::string& strDer) -{ - std::string strBuf; - - strUnHex(strBuf, strDer); - - return DH_der_load(strBuf); -} - /* void intIPtoStr(int ip,std::string& retStr) { diff --git a/src/utils.h b/src/utils.h index 2fa78e09d9..a069c96fc7 100644 --- a/src/utils.h +++ b/src/utils.h @@ -82,6 +82,11 @@ inline std::string strHex(const uint64 uiHost) return strHex((unsigned char*) &uBig, sizeof(uBig)); } +inline static std::string sqlEscape(const std::string& strSrc) +{ + return str(boost::format("X'%s'") % strHex(strSrc)); +} + template bool isZero(Iterator first, int iSize) { @@ -100,9 +105,7 @@ std::vector strCopy(const std::string& strSrc); std::string strCopy(const std::vector& vucSrc); DH* DH_der_load(const std::string& strDer); -DH* DH_der_load_hex(const std::string& strDer); -void DH_der_gen(std::string& strDer, int iKeyLength); -void DH_der_gen_hex(std::string& strDer, int iKeyLength); +std::string DH_der_gen(int iKeyLength); inline std::string strGetEnv(const std::string& strKey) { diff --git a/validators.txt b/validators.txt index bbd2c3f2e8..728f72bacc 100644 --- a/validators.txt +++ b/validators.txt @@ -9,8 +9,14 @@ # All other lines should be hankos or domain names. # # [validators]: -# To acquire a UNL, newcoind will probe for https web servers at the domains -# listed below in the following order: newcoin.DOMAIN, www.DOMAIN, DOMAIN +# List of nodes to accept as validators speficied by public key or domain. +# +# For domains, newcoind will probe for https web servers at the specied +# domain in the following order: newcoin.DOMAIN, www.DOMAIN, DOMAIN +# +# Examples: redstem.com +# n9KorY8QtTdRx7TVDpwnG9NvyxsDwHUKUEeDLY3AkiGncVaSXZi5 +# n9MqiExBcoG19UXwoLjBJnhsxEhAZMuWwJDRdkyDz1EkEkwzQTNt John Doe # [validators]