21分钟空气币从入门到精通——基于比特币

本文会通过教程教会大家自己动手制作“山寨币”,希望能够通过本次教程使大家了解发币的原理,避免被空气币欺骗。

虚拟币开发专题(自己动手制作山寨币教程)

源代码地址:
https://github.com/imharrywu/fastcoin

可以支持任何算法(sha256,scrypt,x11,x13,nist,grostel,以及这些算法的串联)和参数定制。

目前预先内置了比特币的SHA256和莱特币的scypt,以及SHA3-256(KECCAK)。

在线编译目前提供三种安装包:
1,Linux/unix的daemon程序(带钱包功能)
2,Mac钱包安装包 
3,windows钱包安装包

1、 截图预览

(1)安装图1

(2)安装图2

(3)安装图3

(4)安装图4

(5)安装图5

 

(6)概览图已经挖到3个block 奖励了300个FTC

(7)交易记录图

(8)点击显示交易细节显示为挖矿所得 并且还没有确认

(9)版权页

(10)调试窗口

(11)显示已经连接的p2p网络节点一个在虚拟机里面运行的节点

(12)2个节点构成的p2p通信网络

2、山寨币修改步骤:


(1)迁出最新的比特币源代码
比特币官方代码: https://github.com/bitcoin/bitcoin
山寨币代码地址(紧跟bitcoin更新):

https://github.com/imharrywu/fastcoin
Git clone https://github.com/imharrywu/fastcoin 

以下修改都是一些代码片段,请用eclipse打开迁出的代码,对照查看。

(2)修改创世块

源文件 chainparams.cpp
[cpp] view plain copy
/** 
 * Build the genesis block. Note that the output of the genesis coinbasecannot 
 * be spent as it did not originally exist in the database. 
 *  
 * CBlock(hash=000000000019d6, ver=1, hashPrevBlock=00000000000000,hashMerkleRoot=4a5e1e, nTime=1231006505, nBits=1d00ffff, nNonce=2083236893,vtx=1) 
 *   CTransaction(hash=4a5e1e, ver=1, vin.size=1, vout.size=1,nLockTime=0) 
 *     CTxIn(COutPoint(000000, -1), coinbase04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73) 
 *     CTxOut(nValue=50.00000000,scriptPubKey=0x5F1DF16B2B704C8A578D0B) 
 *   vMerkleTree: 4a5e1e 
 */  
  
// HarryWu, generate genesis block by genesis.py as following:  
//  
// localhost genesis # python genesis.py \  
//                           -t $(date +%s) \  
//                           -z “shanghai stock index closed at 2343.57, on 24thSept., 2014” \  
//                           -a SHA256 \  
//                           -p 049e02fa9aa3c19a3b112a58bab503c5caf797972f5cfe1006275aa5485a01b48f9f648bc5380ee1e82dc6f474c8e0f7e2f6bbd0de9355f92496e3ea327ccb19cc\  
//                           -v 10000000000  
// Raw block data:04ffff001d01043b7368616e676861692073746f636b20696e64657820636c6f73656420617420323334332e35372c206f6e203234746820536570742e2c2032303134 
// algorithm: SHA256  
// merkle hash:1c395aad7fab156523a095a869d3fcdf3249a8a97c8d7337adb4f33d826da32b  
// pszTimestamp: shanghai stock index closed at 2343.57, on 24th Sept., 2014 
// pubkey:049e02fa9aa3c19a3b112a58bab503c5caf797972f5cfe1006275aa5485a01b48f9f648bc5380ee1e82dc6f474c8e0f7e2f6bbd0de9355f92496e3ea327ccb19cc 
// time: 1411650667  
// bits: 0x1d00ffff  
// Searching for genesis hash..  
//  
// nonce: 1456993276  
// genesis hash:000000004df0288b461e17d9a20e557fd296861c604f1944eb9e2cca866af0a5  
  
const char* pszTimestamp = “shanghai stock index closed at 2343.57, on24th Sept., 2014”;  
CMutableTransaction txNew;  
txNew.vin.resize(1);  
txNew.vout.resize(1);  
txNew.vin[0].scriptSig = CScript() << 0x1d00ffff << CScriptNum(4)<< vector<unsigned char>((const unsigned char*)pszTimestamp, (constunsigned char*)pszTimestamp + strlen(pszTimestamp));  
txNew.vout[0].nValue = nGenesisSubsidy * COIN;  
txNew.vout[0].scriptPubKey = CScript() <<ParseHex(“049e02fa9aa3c19a3b112a58bab503c5caf797972f5cfe1006275aa5485a01b48f9f648bc5380ee1e82dc6f474c8e0f7e2f6bbd0de9355f92496e3ea327ccb19cc”)<< OP_CHECKSIG;  
genesis.vtx.push_back(txNew);  
genesis.hashPrevBlock = 0;  
genesis.hashMerkleRoot = genesis.BuildMerkleTree();  
genesis.nVersion = 1;  
genesis.nTime    = 1411666331;  
genesis.nBits    = 0x1d00ffff;  
genesis.nNonce   = 2056985438;  
  
hashGenesisBlock = genesis.GetHash();  
assert(hashGenesisBlock == uint256(“0x0000000061b1aca334b059920fed7bace2336ea4d23d63428c7aee04da49e942”)); 
assert(genesis.hashMerkleRoot ==uint256(“0x7bf229f629a6666596c1ce57117c28d1d29299e8a5303347929bd70847c49adb”)); 
 

(3)修改网络协议魔数

源文件 chainparams.cpp
[cpp] view plain copy
class CMainParams : public CChainParams {  
public:  
    CMainParams() {  
        networkID = CBaseChainParams::MAIN;  
        strNetworkID = “main”;  
        /**  
         * The message start string is designed to beunlikely to occur in normal data. 
         * The characters are rarely used upper ASCII,not valid as UTF-8, and produce 
         * a large 4-byte int at any alignment. 
         */  
        pchMessageStart[0] = 0x90;  
        pchMessageStart[1] = 0x0d;  
        pchMessageStart[2] = 0xf0;  
        pchMessageStart[3] = 0x0d;  

0xgoodcafe(好咖啡的意思),大家可以自己选择,但是也有一些基本建议,看上面那个英文注释就好了,就是和某些常用的字符集可用字段不要冲突。

另外:对应的修改TESTNET和REGTESTNET的参数

(4)修改地址前缀

源文件 chainparams.cpp
[cpp] view plain copy
base58Prefixes[PUBKEY_ADDRESS] = list_of(35); // F prefix  
base58Prefixes[SCRIPT_ADDRESS] = list_of(65); // T prefix  
base58Prefixes[SECRET_KEY] =     list_of(45); // 7 prefix  
base58Prefixes[EXT_PUBLIC_KEY] = list_of(0x04)(0x88)(0xEE)(0x35);  
base58Prefixes[EXT_SECRET_KEY] = list_of(0x04)(0x88)(0xEE)(0x45);  

有一个wiki的对照表可以用来悬在公钥和脚本地址的前缀,但是这个表不使用私钥等(因为长度不一样,规律不一样)。

(5)修改网络端口

源文件 chainparams.cpp
[cpp] view plain copy
class CMainParams : public CChainParams {  
public:  
    CMainParams() {  
        networkID = CBaseChainParams::MAIN;  
        strNetworkID = “main”;  
        /**  
         * The message start string is designed to beunlikely to occur in normal data. 
         * The characters are rarely used upper ASCII,not valid as UTF-8, and produce 
         * a large 4-byte int at any alignment. 
         */  
        pchMessageStart[0] = 0x90;  
        pchMessageStart[1] = 0x0d;  
        pchMessageStart[2] = 0xf0;  
        pchMessageStart[3] = 0x0d;  
        vAlertPubKey =ParseHex(“04e01590abdc5967eb550413fcf04bbd7cead46f13579b58d52ea2f08d71a1a94196c476cd4fa60c30b51737fe3d9c8c88a04a6bec2282ebb1f22286130a153b85”); 
        nDefaultPort = 9999;  
        bnProofOfWorkLimit = ~uint256(0) >> 32; 

修改了nDefaultPort=9999,比特币默认是8333

另外同时修改一下TESTNET和REGTESTNET的端口配置,一般加10000和20000即可。

[cpp] view plain copy
/** 
 * Testnet (v3) 
 */  
class CTestNetParams : public CMainParams {  
public:  
    CTestNetParams() {  
        networkID = CBaseChainParams::TESTNET;  
        strNetworkID = “test”;  
        pchMessageStart[0] = 0xC0;  
        pchMessageStart[1] = 0x1d;  
        pchMessageStart[2] = 0xf0;  
        pchMessageStart[3] = 0x0d;  
        vAlertPubKey =ParseHex(“045d2d29beffb0a0cbea44f266286ff8b1d11c035538fbb4dadcf6b4073b08f318afea74f01d5a3782e72a22273fb01ab40e99d93adff488236585cc8031323e7c”); 
        nDefaultPort = 19999;  

[cpp] view plain copy
/** 
 * Regression test 
 */  
class CRegTestParams : public CTestNetParams {  
public:  
    CRegTestParams() {  
        networkID = CBaseChainParams::REGTEST;  
        strNetworkID = “regtest”;  
        pchMessageStart[0] = 0x0b;  
        pchMessageStart[1] = 0xad;  
        pchMessageStart[2] = 0xf0;  
        pchMessageStart[3] = 0x0d;  
        nDefaultPort = 29999;  
        bnProofOfWorkLimit = ~uint256(0) >> 1;  

(6)修改种子网络连接参数

源文件 chainparams.cpp
[cpp] view plain copy
//vSeeds.push_back(CDNSSeedData(“bitcoin.sipa.be”,”seed.bitcoin.sipa.be”));  
//vSeeds.push_back(CDNSSeedData(“bluematt.me”,”dnsseed.bluematt.me”));  
//vSeeds.push_back(CDNSSeedData(“dashjr.org”,”dnsseed.bitcoin.dashjr.org”));  
//vSeeds.push_back(CDNSSeedData(“bitcoinstats.com”,”seed.bitcoinstats.com”));  
//vSeeds.push_back(CDNSSeedData(“bitnodes.io”,”seed.bitnodes.io”));  
//vSeeds.push_back(CDNSSeedData(“xf2.org”,”bitseed.xf2.org”));  
  
base58Prefixes[PUBKEY_ADDRESS] = list_of(35); // F prefix  
base58Prefixes[SCRIPT_ADDRESS] = list_of(65); // T prefix  
base58Prefixes[SECRET_KEY] =     list_of(45); // 7 prefix  
base58Prefixes[EXT_PUBLIC_KEY] = list_of(0x04)(0x88)(0xEE)(0x35);  
base58Prefixes[EXT_SECRET_KEY] = list_of(0x04)(0x88)(0xEE)(0x45);  
  
//convertSeed6(vFixedSeeds, pnSeed6_main, ARRAYLEN(pnSeed6_main));  

如果你有7×24在线的服务器运行节点,那么请在这里加入他们的地址,可以是dns也可以是ip地址(v4,v6,–enable-ipv6)。

如果没有的话,可以考虑和我一样暂时注释掉,在运行时候指定连接

-addnoe=123.123.123.或者-dns-addnode=mynode.domain.com
对应的修改一下TESTNET和REGTESTNET的网络节点地址。

(7)修改工作量机制

源文件 miner.cpp
比特币在函数ScanHash()里面搜索随机数,
[cpp] view plain copy
//  
// ScanHash scans nonces looking for a hash with at least some zero bits. 
// The nonce is usually preserved between calls, but periodically or if the 
// nonce is 0xffff0000 or above, the block is rebuilt and nNonce starts over at 
// zero.  
//  
bool static ScanHash(const CBlockHeader *pblock, uint32_t& nNonce, uint256*phash)  
{  
    // Write the first 76 bytes of the block header to adouble-SHA256 state.  
    //CHash256 hasher;  
    //CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);  
    //ss << *pblock;  
    //assert(ss.size() == 80);  
    //hasher.Write((unsigned char*)&ss[0], 76);  
  
    while (true) {  
        nNonce++;  
  
        // Write the last 4 bytes of the block header (thenonce) to a copy of  
        // the double-SHA256 state, and compute the result. 
        //CHash256(hasher).Write((unsignedchar*)&nNonce, 4).Finalize((unsigned char*)phash);  
  
        *phash = pblock->ComputePowHash(nNonce);  
  
        // Return the nonce if the hash has at least somezero bits,  
        // caller will check if it has enough to reach thetarget  
        if (((uint16_t*)phash)[15] == 0)  
            return true;  
  
        // If nothing found after trying for a while,return -1  
        if ((nNonce & 0xffff) == 0)  
            return false;  
        if ((nNonce & 0xfff) == 0)  
           boost::this_thread::interruption_point();  
    }  
}  

在函数BitcoinMiner()里面创建新的block,设定时间戳(扰动1),获取mempool里面的最新交易(扰动2),调用ScanHash()搜索随机数(扰动3),直到找到满足条件的hash。

另外比特币的工作量证明的hash同时用作这个block的索引,litecoin里面这2个概念是区分的对待的(sha256的hash作为索引,scrypt出来的pow hash作为工作量证明)。

注意:设置独立挖矿的条件(chainparams.cpp里面设置)

[cpp] view plain copy
fMiningRequiresPeers = !true; // See BitcoinMiner() for details.  

挖矿代码:

[cpp] view plain copy
void static BitcoinMiner(CWallet *pwallet)  
{  
    LogPrintf(“FastCoinMiner started\n”);  
    SetThreadPriority(THREAD_PRIORITY_LOWEST);  
    RenameThread(“fastcoin-miner”);  
  
    // Each thread has its own key and counter  
    CReserveKey reservekey(pwallet);  
    unsigned int nExtraNonce = 0;  
  
    try {  
        while (true) {  
            if (Params().MiningRequiresPeers()) { 
                // Busy-wait for thenetwork to come online so we don’t waste time mining  
                // on an obsoletechain. In regtest mode we expect to fly solo.  
                while (vNodes.empty()) 
                   MilliSleep(1000);  
            }  
  
            //  
            // Create new block  
            //  
            unsigned int nTransactionsUpdatedLast= mempool.GetTransactionsUpdated();  
            CBlockIndex* pindexPrev =chainActive.Tip();  
  
            auto_ptr<CBlockTemplate>pblocktemplate(CreateNewBlockWithKey(reservekey));  
            if (!pblocktemplate.get())  
            {  
                LogPrintf(“Errorin FastCoinMiner: Keypool ran out, please call keypoolrefill before restartingthe mining thread\n”);  
                return;  
            }  
            CBlock *pblock =&pblocktemplate->block;  
            IncrementExtraNonce(pblock,pindexPrev, nExtraNonce);  
  
            LogPrintf(“Running FastCoinMinerwith %u transactions in block (%u bytes)\n”, pblock->vtx.size(),  
                ::GetSerializeSize(*pblock,SER_NETWORK, PROTOCOL_VERSION));  

博主稍作修改,设置了几个预置的hash算法(sha256,scrypt,keccak(即SHA3)),可以通过block的版本号区分,那么在scanhash里面调用ComputePowHash的时候,可以选择不同的pow算法,甚至你可以把这些算法串联起来,以增加算法被破解的方攻击性。

看代码吧:

[cpp] view plain copy
uint256 CBlockHeader::ComputePowHash(uint32_t nNonce) const  
{  
    if (nVersion == 1 || nVersion == 2){  
        /** 
         * Use SHA256+SHA256 to make PoW 
         */  
        // Write the first 76 bytes of the block header toa double-SHA256 state.  
        CDoubleSHA256Pow hasher; // TODO: Create a newPowHasher named CPowHash256  
        CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); 
        ss << *this;  
        assert(ss.size() == 80);  
        hasher.Write((unsigned char*)&ss[0], 76); 
        uint256 powHash;  
        CDoubleSHA256Pow(hasher).Write((unsignedchar*)&nNonce, 4).Finalize((unsigned char*)&powHash);  
        return powHash;  
    }else if (nVersion == 3){  
        /** 
         * Scrypt PoW 
         */  
        CScryptHash256Pow hasher;  
        CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); 
        ss << *this;  
        assert(ss.size() == 80);  
        hasher.Write((unsigned char*)&ss[0], 76); 
        uint256 powHash;  
        CScryptHash256Pow(hasher).Write((unsignedchar*)&nNonce, 4).Finalize((unsigned char*)&powHash);  
        return powHash;  
    }else if (nVersion == 4){  
        /** 
         *  Scrypt+SHA256 PoW 
         */  
    }else if (nVersion == 5){  
        /** 
         * Keccak(1088,512,256) or Known as SHA3-256(fips202draft)Pow 
         */  
        CKeccak_256 hasher;  
        CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); 
        ss << *this;  
        assert(ss.size() == 80);  
        hasher.Write((unsigned char*)&ss[0], 76); 
        uint256 powHash;  
        CKeccak_256(hasher).Write((unsignedchar*)&nNonce, 4).Finalize((unsigned char*)&powHash);  
        return powHash;  
    }else{  
        // Abort, unknown block version.  
        assert(false);  
        return ~(uint256)0;  
    }  
}  

(8)修改奖励机制

参考9.3

8.1) 修改挖矿奖励的成熟时间

源代码:main.h
[cpp] view plain copy
/** Coinbase transaction outputs can only be spent after this number of newblocks (network rule) */  
static const int COINBASE_MATURITY = 14;  

8.2) 修改交易确认块数推荐值

源代码 qt/transactionrecord.h
[cpp] view plain copy
/** UI model for a transaction. A core transaction can be represented bymultiple UI transactions if it has 
    multiple outputs. 
 */  
class TransactionRecord  
{  
public:  
    enum Type  
    {  
        Other,  
        Generated,  
        SendToAddress,  
        SendToOther,  
        RecvWithAddress,  
        RecvFromOther,  
        SendToSelf  
    };  
  
    /** Number of confirmation recommended for accepting atransaction */  
    static const int RecommendedNumConfirmations = 2;  

(9)修改难度配置

源文件 chainparams.cpp
[cpp] view plain copy
class CMainParams : public CChainParams {  
public:  
    CMainParams() {  
        networkID = CBaseChainParams::MAIN;  
        strNetworkID = “main”;  
        /**  
         * The message start string is designed to beunlikely to occur in normal data. 
         * The characters are rarely used upper ASCII,not valid as UTF-8, and produce 
         * a large 4-byte int at any alignment. 
         */  
        pchMessageStart[0] = 0x90;  
        pchMessageStart[1] = 0x0d;  
        pchMessageStart[2] = 0xf0;  
        pchMessageStart[3] = 0x0d;  
        vAlertPubKey =ParseHex(“04e01590abdc5967eb550413fcf04bbd7cead46f13579b58d52ea2f08d71a1a94196c476cd4fa60c30b51737fe3d9c8c88a04a6bec2282ebb1f22286130a153b85”); 
        nDefaultPort = 9999;  
        bnProofOfWorkLimit = ~uint256(0) >> 32; 
        nSubsidyHalvingInterval = 210000;  
        nEnforceBlockUpgradeMajority = 750;  
        nRejectBlockOutdatedMajority = 950;  
        nToCheckBlockUpgradeMajority = 1000;  
        nMinerThreads = 1; // 0 for all available cpus. 
        nTargetTimespan = 60 * 60; // re-targeting everyone hour  
        nTargetSpacing = 1 * 60;  // do new pow every1 minutes.  
        nGenesisSubsidy = 100;  

a) bnProofOfWorkLimit=~uint256(0) >> 32;

是一个大整数(256bit)表示,前面32个位数是0,这个参数表示全网允许的最小难度,低于这个难度的block是不会被挖掘的。

b) nGenesisSubsidy = 100;

初始津贴,比特币的第一个块的奖励是50个btc。

c) nSubsidyHavlingInterval = 210000; 

这个参数决定了多少个block以后比特币的奖励(补贴,挖矿奖励)会减半。这个参数结合初始奖励(比如比特币50)基本可以估算全网总的货币产量(比如比特币的2100万),这个初始津贴也是可以配置的,如2)所示。比如比特币,一个等比数列求和公式就可以计算货币总量 50(1/(1 – 0.5))*210000=2100万btc。

上面2个参数在查询区块奖励的时候用到,请查看main.cpp 的 GetBlockValue(height, fees)找到细节

[cpp] view plain copy
CAmount GetBlockValue(int nHeight, const CAmount& nFees)  
{  
    CAmount nSubsidy = Params().GenesisSubsidy() * COIN;  
  
    int halvings = nHeight / Params().SubsidyHalvingInterval(); 
  
    // Force block reward to zero when right shift is undefined. 
    if (halvings >= 64)  
        return nFees;  
  
    // Subsidy is cut in half every 210,000 blocks which will occurapproximately every 4 years.  
    nSubsidy >>= halvings;  
  
    return nSubsidy + nFees;  
}  


很多山寨币也是在这里修改货币分布的,比如 nHeight==1 的时候,奖励1000万个,其他块都不予奖励,这个即使预挖模式了吧。

另外,关于货币分布,这里satoshi同学可能已经考虑很多,他为什么设置到很多年以后才矿源枯竭,为什么采取递减的方式,可能都是出于保护区块链的目的(timestampserver)。

d)难度调节周期

nTargetTimespan = 60 * 60; // re-targeting every one hour
nTargetSpacing = 1 * 60;  // do new pow every 1 minutes.

这里的设置是60分钟(3600秒),重新评估难度,下一个块的挖掘可能就要使用新的难度设置了。

区块挖掘超时设置,在网络算力足够的情况下,一个节点可以挖矿的时间是有限的,比如这里这是的1分钟(60秒),超过这个时间,要么提交目前已找到的block(nNonce),要放弃自己的挖掘结果(使用网络上其他节点找到的一个最优结果)来寻找下一个block。否则其他节点也已经开始寻找next了,不过这个前提是算力足够,比如btc网络(现在应该到78个0的位置了吧),你如果10分钟内找不到,那么好了,可以放弃了,用别人的block吧,赶紧进入下一个回合的挖掘,否则很可能是浪费时间。

比特币的调节周期是 2周/每10分钟,也就是 2 * 7 * 24 * 6 = 2016

参考:http://bitcoindifficulty.com/blog/

https://en.bitcoin.it/wiki/Difficulty

(10) 修改checkpoints检查

(11) 修改界面

github地址

TODO:
1)模板化的山寨币生成,可配置修改部分通过宏或者变量提取出来,用脚本批量替换修改。
2)矿池搭建与节点测试 P2pool  &Poclbm
3)移动支付和网页支付
4)应用生态
5)交易备注
6)复杂交易应用(脚本,非标准交易)

备注:
1)记得加上-checkpoints=0 参数启动,否则,会导致daemon报错,“FastCoin is downloading blocks…”

 – END – 



往期精彩文章导读:


流传最广的区块链研究报告


麦肯锡《区块链,银行业游戏规则的颠覆者》报告导读


穆迪《区块链的现状及未来》报告导读


TED演讲:区块链将如何改变金融与贸易(文字版)


顶级投资人和经济学家是如何看待区块链?