在这一篇里系统得讲讲客户端/钱包如何生成比特币地址,并创建一个交易
我们知道比特币的所有权是通过数字密钥、比特币地址和数字签名来确定的。数字密钥并不存储在网络中,而是由客户端生成后保存在名为钱包的文件(wallet.dat)或者简单的数据库中。存储在用户钱包中的数字密钥完全独立于比特币协议,可由用户的钱包软件生成并管理,而无需参照区块链或访问网络。
从钱包的rpc指令getnewaddress开始看,先来看下调用结果
可以看到是返回一个比特币地址(测试网),如果是主网的话是以1开头的地址
然后在日志信息中会增加相应的记录
下面来看这个指定对应调用的函数
位于/src/rpc/wallet/rpcwallet.cpp
UniValue getnewaddress(const UniValue& params, bool fHelp)
{
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp || params.size() > 1)
throw runtime_error(
"getnewaddress ( \"account\" )\n"
"\nReturns a new Bitcoin address for receiving payments.\n"
"If 'account' is specified (DEPRECATED), it is added to the address book \n"
"so payments received with the address will be credited to 'account'.\n"
"\nArguments:\n"
"1. \"account\" (string, optional) DEPRECATED. The account name for the address to be linked to. If not provided, the default account \"\" is used. It can also be set to the empty string \"\" to represent the default account. The account does not need to exist, it will be created if there is no account by the given name.\n"
"\nResult:\n"
"\"bitcoinaddress\" (string) The new bitcoin address\n"
"\nExamples:\n"
+ HelpExampleCli("getnewaddress", "")
+ HelpExampleRpc("getnewaddress", "")
);
LOCK2(cs_main, pwalletMain->cs_wallet);
// Parse the account first so we don't generate a key if there's an error
string strAccount;
if (params.size() > 0)
strAccount = AccountFromValue(params[0]);
if (!pwalletMain->IsLocked())
pwalletMain->TopUpKeyPool();
// Generate a new key that is added to wallet
CPubKey newKey;
if (!pwalletMain->GetKeyFromPool(newKey))
throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Error: Keypool ran out, please call keypoolrefill first");
CKeyID keyID = newKey.GetID();//获取的是hash160的值
//pwalletMain是CWllat类的指针
pwalletMain->SetAddressBook(keyID, strAccount, "receive");
//CBitcoinAddress函数调用Base58编码转换
return CBitcoinAddress(keyID).ToString();
}
这里的pwalletMain是指向CWallet类对象的指针。生成一个新的密钥在这里是通过调用函数GetKeyFromPool。可以看到这段代码最后是调用CBitcoinAddress函数返回比特币地址。
在这个函数中首先调用ReserveKeyFromKeyPool查看密钥储备池中的密钥,如果没有储备的密钥,就通过GenerateNewKey生成一个新的密钥,否则根据索引获取储备池中的下一个密钥
//src/wallet/wallet.cpp
bool CWallet::GetKeyFromPool(CPubKey& result)
{
int64_t nIndex = 0;
CKeyPool keypool;
{
LOCK(cs_wallet);
ReserveKeyFromKeyPool(nIndex, keypool);
if (nIndex == -1)
{
if (IsLocked()) return false;
result = GenerateNewKey();
return true;
}
KeepKey(nIndex);
result = keypool.vchPubKey;
}
return true;
}
CKey可以参考https://blog.csdn.net/m0_37847176/article/details/81450432#2-initsanitycheck里面有写到这个类,以及makenewkey()的实现
-usehd:Use hierarchical deterministic key generation (HD) after BIP32. Only has effect during wallet creation/first start
CPubKey CWallet::GenerateNewKey()
{
AssertLockHeld(cs_wallet); // mapKeyMetadata
bool fCompressed = CanSupportFeature(FEATURE_COMPRPUBKEY); // default to compressed public keys if we want 0.6.0 wallets
// CKey是私钥的类
CKey secret;
// Create new metadata
int64_t nCreationTime = GetTime();
CKeyMetadata metadata(nCreationTime);
// use HD key derivation if HD was enabled during wallet creation
//1.如果在钱包创建时使用确定分层钱包,则使用HD密钥
if (!hdChain.masterKeyID.IsNull()) {
// for now we use a fixed keypath scheme of m/0'/0'/k
CKey key; //master key seed (256bit)
CExtKey masterKey; //hd master key
CExtKey accountKey; //key at m/0'
CExtKey externalChainChildKey; //key at m/0'/0'
CExtKey childKey; //key at m/0'/0'/<n>'
// try to get the master key
if (!GetKey(hdChain.masterKeyID, key))
throw std::runtime_error(std::string(__func__) + ": Master key not found");
masterKey.SetMaster(key.begin(), key.size());
// derive m/0' 派生
// use hardened derivation (child keys >= 0x80000000 are hardened after bip32)
//使用硬化的派生,const uint32_t BIP32_HARDENED_KEY_LIMIT = 0x80000000;
masterKey.Derive(accountKey, BIP32_HARDENED_KEY_LIMIT);
// derive m/0'/0'
accountKey.Derive(externalChainChildKey, BIP32_HARDENED_KEY_LIMIT);
// derive child key at next index, skip keys already known to the wallet
do
{
// always derive hardened keys
// childIndex | BIP32_HARDENED_KEY_LIMIT = derive childIndex in hardened child-index-range
// example: 1 | BIP32_HARDENED_KEY_LIMIT == 0x80000001 == 2147483649
externalChainChildKey.Derive(childKey, hdChain.nExternalChainCounter | BIP32_HARDENED_KEY_LIMIT);
metadata.hdKeypath = "m/0'/0'/"+std::to_string(hdChain.nExternalChainCounter)+"'";
metadata.hdMasterKeyID = hdChain.masterKeyID;
// increment childkey index
hdChain.nExternalChainCounter++;
} while(HaveKey(childKey.key.GetPubKey().GetID()));
secret = childKey.key;
// update the chain model in the database
if (!CWalletDB(strWalletFile).WriteHDChain(hdChain))
throw std::runtime_error(std::string(__func__) + ": Writing HD chain model failed");
} else {
//2.如果在创建钱包的时候是使用随机钱包
secret.MakeNewKey(fCompressed);
}
// Compressed public keys were introduced in version 0.6.0
if (fCompressed)
SetMinVersion(FEATURE_COMPRPUBKEY);
//验证公钥
CPubKey pubkey = secret.GetPubKey();
assert(secret.VerifyPubKey(pubkey));
//判断第一把密钥的创建时间
mapKeyMetadata[pubkey.GetID()] = metadata;
if (!nTimeFirstKey || nCreationTime < nTimeFirstKey)
nTimeFirstKey = nCreationTime;
if (!AddKeyPubKey(secret, pubkey))
throw std::runtime_error(std::string(__func__) + ": AddKey failed");
return pubkey;
}
其中的HD钱包部分是依据标准BIP32实现的,写在另一篇里https://blog.csdn.net/m0_37847176/article/details/82011876
如果是随机钱包,则调用CKey类的MakeNewKey
//key.cpp
void CKey::MakeNewKey(bool fCompressedIn) {
do {
GetStrongRandBytes(vch, sizeof(vch));////! The actual byte data unsigned char vch[32];
} while (!Check(vch));
fValid = true;
fCompressed = fCompressedIn;
}
具体实现就是新建一个CKey类型的对象,获取强随机数,私钥是32位的,知道获取的随机数通过验证,此时的私钥是有效的。
然后获取对应私钥的公钥,通过椭圆曲线算法调用库,验证公钥
最后添加公钥,调用CWallet类的AddKeyPubKey
//wallet.cpp
bool CWallet::AddKeyPubKey(const CKey& secret, const CPubKey &pubkey)
{
AssertLockHeld(cs_wallet); // mapKeyMetadata
if (!CCryptoKeyStore::AddKeyPubKey(secret, pubkey))
return false;
// check if we need to remove from watch-only
CScript script;
script = GetScriptForDestination(pubkey.GetID());
if (HaveWatchOnly(script))
RemoveWatchOnly(script);
script = GetScriptForRawPubKey(pubkey);
if (HaveWatchOnly(script))
RemoveWatchOnly(script);
if (!fFileBacked)
return true;
if (!IsCrypted()) {
return CWalletDB(strWalletFile).WriteKey(pubkey,
secret.GetPrivKey(),
mapKeyMetadata[pubkey.GetID()]);
}
return true;
}
首先调用CCryptoKeyStore的AddKeyPubKey
,CCryptoKeyStore是保存加密过的私钥的密钥库,继承自CBasicKeyStore,如果没有激活加密,则使用CBasicKeyStore。
bool CCryptoKeyStore::AddKeyPubKey(const CKey& key, const CPubKey &pubkey)
{
{
LOCK(cs_KeyStore);
if (!IsCrypted())//是否激活加密
return CBasicKeyStore::AddKeyPubKey(key, pubkey);
if (IsLocked())
return false;
std::vector<unsigned char> vchCryptedSecret;
CKeyingMaterial vchSecret(key.begin(), key.end());
if (!EncryptSecret(vMasterKey, vchSecret, pubkey.GetHash(), vchCryptedSecret))
return false;
if (!AddCryptedKey(pubkey, vchCryptedSecret))
return false;
}
return true;
}
未激活加密私钥的话
bool CBasicKeyStore::AddKeyPubKey(const CKey& key, const CPubKey &pubkey)
{
LOCK(cs_KeyStore);
mapKeys[pubkey.GetID()] = key;//私钥和公钥(hash值)保存在键值对mapKeys中
return true;
}
激活加密私钥
首先调用EncryptSecret
加密秘密,接着添加加密的私钥
bool CCryptoKeyStore::AddCryptedKey(const CPubKey &vchPubKey, const std::vector<unsigned char> &vchCryptedSecret)
{
{
LOCK(cs_KeyStore);
if (!SetCrypted())
return false;
mapCryptedKeys[vchPubKey.GetID()] = make_pair(vchPubKey, vchCryptedSecret);
}
return true;
}
把公钥(hash)、公钥、加密的秘密保存在键值对mapCryptedKeys中。
返回CWallet::AddKeyPubKey,最后调用 CWalletDB类的WriteKey
bool CWalletDB::WriteKey(const CPubKey& vchPubKey, const CPrivKey& vchPrivKey, const CKeyMetadata& keyMeta)
{
nWalletDBUpdated++;
if (!Write(std::make_pair(std::string("keymeta"), vchPubKey),
keyMeta, false))
return false;
// hash pubkey/privkey to accelerate wallet load
std::vector<unsigned char> vchKey;
vchKey.reserve(vchPubKey.size() + vchPrivKey.size());
vchKey.insert(vchKey.end(), vchPubKey.begin(), vchPubKey.end());
vchKey.insert(vchKey.end(), vchPrivKey.begin(), vchPrivKey.end());
return Write(std::make_pair(std::string("key"), vchPubKey), std::make_pair(vchPrivKey, Hash(vchKey.begin(), vchKey.end())), false);
}
这里是调用了CWalletDB的父类CDB的成员函数Write,CDB封装了Berkeley数据库的一系列操作接口,上述代码就是把密钥写入Berkeley数据库。
加载钱包在https://blog.csdn.net/m0_37847176/article/details/81739999这一篇中
Berkeley DB (DB)是一个高性能的,嵌入数据库编程库,和C语言,C++,Java,Perl,Python,PHP,Tcl以及其他很多语言都有绑定。Berkeley DB可以保存任意类型的键/值对,而且可以为一个键保存多个数据。Berkeley DB可以支持数千的并发线程同时操作数据库,支持最大256TB的数据,广泛用于各种操作系统包括大多数Unix类操作系统和Windows操作系统以及实时操作系统。
代码中使用的代码如下
CBitcoinAddress(keyID).ToString();
base58-encoded Bitcoin addresses.
Public-key-hash-addresses have version 0 (or 111 testnet).
The data vector contains RIPEMD160(SHA256(pubkey)), where pubkey is the serialized public key.
Script-hash-addresses have version 5 (or 196 testnet).
The data vector contains RIPEMD160(SHA256(cscript)), where cscript is the serialized redemption script.
公钥哈希地址具有版本0(或111 测试网)。
数据向量包含RIPEMD160(SHA256(pubkey)),其中pubkey是序列化公钥。
脚本哈希地址具有版本5(或196 测试网)。
数据向量包含RIPEMD160(SHA256(cscript)),其中cscript是序列化的脚本。
Type | Version Prefix(hex) | base58 result prefix |
---|---|---|
bitcoin address | 0x00 | 1 |
bitcoin testnet address | 0x6f | m、n |
Script-hash-addresses | 0x05 | 3 |
Script-hash-addresses testnet | 0xc4 |
下面来看代码,类CBitcoinAddress继承自类CBase58Data,CBase58Data中有vchVersion成员变量表示版本信息。
class CBitcoinAddress : public CBase58Data {
public:
bool Set(const CKeyID &id);
bool Set(const CScriptID &id);
bool Set(const CTxDestination &dest);
bool IsValid() const;
bool IsValid(const CChainParams ¶ms) const;
CBitcoinAddress() {}
CBitcoinAddress(const CTxDestination &dest) { Set(dest); }
CBitcoinAddress(const std::string& strAddress) { SetString(strAddress); }
CBitcoinAddress(const char* pszAddress) { SetString(pszAddress); }
CTxDestination Get() const;
bool GetKeyID(CKeyID &keyID) const;
bool IsScript() const;
};
根据代码推测是调用了第三个或第四个构造函数,都是调用SetString()
函数,SetString()函数内部是调用父类的SetString(),也就是CBase58Data::SetString,在CBase58Data中声明的bool SetString(const char* psz, unsigned int nVersionBytes = 1);
bool CBase58Data::SetString(const char* psz, unsigned int nVersionBytes)
{
std::vector<unsigned char> vchTemp;
bool rc58 = DecodeBase58Check(psz, vchTemp);//psz是要编码的内容,编码后临时存放
if ((!rc58) || (vchTemp.size() < nVersionBytes)) {
vchData.clear();
vchVersion.clear();
return false;
}
vchVersion.assign(vchTemp.begin(), vchTemp.begin() + nVersionBytes);
vchData.resize(vchTemp.size() - nVersionBytes);
if (!vchData.empty())
memcpy(&vchData[0], &vchTemp[nVersionBytes], vchData.size());
memory_cleanse(&vchTemp[0], vchTemp.size());
return true;
}
这部分代码的逻辑不是很清晰
std::string CBase58Data::ToString() const
{
std::vector<unsigned char> vch = vchVersion;
//在vch的末尾插入vchData
vch.insert(vch.end(), vchData.begin(), vchData.end());
return EncodeBase58Check(vch);
}
借用精通比特币的图,编码过程
这个函数位于rpcwallet.cpp中,需要传入交易目的地,交易金额,fSubtractFeeFromAmount(从交易金额中抽取费用),包含附加的交易信息CWalletTx(比如这笔交易是发送给谁,为什么发起)
/**
* A txout script template with a specific destination. It is either:
* * CNoDestination: no destination set
* * CKeyID: TX_PUBKEYHASH destination
* * CScriptID: TX_SCRIPTHASH destination
* A CTxDestination is the internal data type encoded in a CBitcoinAddress
*/
typedef boost::variant<CNoDestination, CKeyID, CScriptID> CTxDestination;
CWalletTx
:A transaction with a bunch of additional info that only the owner cares about.
It includes any unrecorded transactions needed to link it back to the block chain.
static void SendMoney(const CTxDestination &address, CAmount nValue, bool fSubtractFeeFromAmount, CWalletTx& wtxNew)
{
CAmount curBalance = pwalletMain->GetBalance();
// 1.Check amount 检查余额
if (nValue <= 0)
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid amount");
if (nValue > curBalance)
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Insufficient funds");
// 2.Parse Bitcoin address 解析比目的地,获得脚本
CScript scriptPubKey = GetScriptForDestination(address);
// 3.Create and send the transaction 创建并发送交易
CReserveKey reservekey(pwalletMain);
CAmount nFeeRequired;
std::string strError;
vector<CRecipient> vecSend;
int nChangePosRet = -1;
CRecipient recipient = {scriptPubKey, nValue, fSubtractFeeFromAmount};//接收者(锁定脚本,金额,是否抽取费用)
vecSend.push_back(recipient);
if (!pwalletMain->CreateTransaction(vecSend, wtxNew, reservekey, nFeeRequired, nChangePosRet, strError)) {
if (!fSubtractFeeFromAmount && nValue + nFeeRequired > pwalletMain->GetBalance())
strError = strprintf("Error: This transaction requires a transaction fee of at least %s because of its amount, complexity, or use of recently received funds!", FormatMoney(nFeeRequired));
throw JSONRPCError(RPC_WALLET_ERROR, strError);
}
if (!pwalletMain->CommitTransaction(wtxNew, reservekey))//提交交易
throw JSONRPCError(RPC_WALLET_ERROR, "Error: The transaction was rejected! This might happen if some of the coins in your wallet were already spent, such as if you used a copy of the wallet and coins were spent in the copy but not marked as spent here.");
}
首先调用GetBalance()
获取钱包的余额
CAmount CWallet::GetBalance() const
{
CAmount nTotal = 0;
{
LOCK2(cs_main, cs_wallet);
for (map<uint256, CWalletTx>::const_iterator it = mapWallet.begin(); it != mapWallet.end(); ++it)
{
const CWalletTx* pcoin = &(*it).second;//CWalletTx
if (pcoin->IsTrusted())
nTotal += pcoin->GetAvailableCredit();
}
}
return nTotal;
}
这里判断交易是否被信任
//wallet.cpp
bool CWalletTx::IsTrusted() const
{
// Quick answer in most cases
if (!CheckFinalTx(*this))//判断交易是否是最终的,函数内调用了IsFinalTx
return false;
int nDepth = GetDepthInMainChain();
if (nDepth >= 1)//交易所在区块后面的区块大于等于1即视为信任
return true;
if (nDepth < 0)
return false;
if (!bSpendZeroConfChange || !IsFromMe(ISMINE_ALL)) // using wtx's cached debit
return false;
// Don't trust unconfirmed transactions from us unless they are in the mempool.
if (!InMempool())//不信任不在内存池中的交易
return false;
// Trusted if all inputs are from us and are in the mempool:
BOOST_FOREACH(const CTxIn& txin, vin)
{
// Transactions not sent by us: not trusted
const CWalletTx* parent = pwallet->GetWalletTx(txin.prevout.hash);
if (parent == NULL)
return false;
const CTxOut& parentOut = parent->vout[txin.prevout.n];
if (pwallet->IsMine(parentOut) != ISMINE_SPENDABLE)
return false;
}
return true;
}
//main.h
/** * Check if transaction is final and can be included in a block with the * specified height and time. Consensus critical. */
bool IsFinalTx(const CTransaction &tx, int nBlockHeight, int64_t nBlockTime);
这里有几个逻辑
CAmount CWalletTx::GetAvailableCredit(bool fUseCache) const
{
if (pwallet == 0)//没有钱包信息则返回0
return 0;
// Must wait until coinbase is safely deep enough in the chain before valuing it
//如果是coinbase交易,必须等这笔交易足够安全,否则返回0
if (IsCoinBase() && GetBlocksToMaturity() > 0)
return 0;
if (fUseCache && fAvailableCreditCached)//如果使用缓存数据
return nAvailableCreditCached;
CAmount nCredit = 0;
uint256 hashTx = GetHash();
for (unsigned int i = 0; i < vout.size(); i++)
{
if (!pwallet->IsSpent(hashTx, i))//如果这笔交易的输出未支出
{
const CTxOut &txout = vout[i];
nCredit += pwallet->GetCredit(txout, ISMINE_SPENDABLE);//我可以花费的部分
if (!MoneyRange(nCredit))
throw std::runtime_error("CWalletTx::GetAvailableCredit() : value out of range");
}
}
nAvailableCreditCached = nCredit;
fAvailableCreditCached = true;
return nCredit;
}
/**
* Create a new transaction paying the recipients with a set of coins
* selected by SelectCoins(); Also create the change output, when needed
* @note passing nChangePosInOut as -1 will result in setting a random position
*/
这里的 SelectCoins()是选择一组币使得nValueRet >= nTargetValue,大于或等于目标金额
鉴于这段代码很长,分段来讲
参数
类型 | 名称 | 说明 |
---|---|---|
vector<CRecipient> |
vecSend | struct CRecipient{CScript scriptPubKey;CAmount nAmount;bool fSubtractFeeFromAmount;} |
CWalletTx& | wtxNew | 包含附加信息的交易信息,这个类只关注本钱包发起(可能包含接收)的交易 |
CReserveKey& | reservekey | 密钥池分配的密钥 |
CAmount& | nFeeRet | CAmount nFeeRequired;未赋值 |
int& | nChangePosRet | 改变标志位? |
std::string& | strFailReason | 失败的原因 |
CCoinControl* | coinControl | 币控制功能 |
bool | sign | 默认为true |
bool CWallet::CreateTransaction(const vector<CRecipient>& vecSend, CWalletTx& wtxNew, CReserveKey& reservekey, CAmount& nFeeRet,
int& nChangePosInOut, std::string& strFailReason, const CCoinControl* coinControl, bool sign)
{
CAmount nValue = 0;//初始化为0
int nChangePosRequest = nChangePosInOut;//赋值
unsigned int nSubtractFeeFromAmount = 0;
BOOST_FOREACH (const CRecipient& recipient, vecSend)//解析接收者的信息
{
if (nValue < 0 || recipient.nAmount < 0)
{//交易金额不能为负
strFailReason = _("Transaction amounts must be positive");
return false;
}
nValue += recipient.nAmount;//计算总共要支付的
if (recipient.fSubtractFeeFromAmount)
nSubtractFeeFromAmount++;
}
if (vecSend.empty() || nValue < 0)
{
strFailReason = _("Transaction amounts must be positive");
return false;
}
wtxNew.fTimeReceivedIsTxTime = true;
wtxNew.BindWallet(this);//绑定钱包
CMutableTransaction txNew;//A mutable version of CTransaction.
nLockTime是交易类的成员变量,参考这篇
https://blog.csdn.net/m0_37847176/article/details/81624052#ctransaction
锁定时间也称为 nLocktime,是来自于 Bitcoin Core 代码库中使用的变量名称。在 大多数交易中将其设置为零,以指示即时传播和执行。如果 nLocktime 不为零, 低于 5 亿,则将其解释为块高度,这意味着交易无效,并且在指定的块高度之前 未被中继或包含在块链中。
如果超过 5 亿,它被解释为 Unix 纪元时间戳(自 Jan-1-1970 之后的秒数),并且 交易在指定时间之前无效。指定未来块或时间的 nLocktime 的交易必须由始发系 统持有,并且只有在有效后才被发送到比特币网络。如果交易在指定的 nLocktime之前传输到网络,那么第一个节点就会拒绝该交易,并且不会被中继到其他节点。使用 nLocktime 等同于一张延期支票。
Discourage fee sniping.针对费用狙击
For a large miner the value of the transactions in the best block and the mempool can exceed the cost of deliberately attempting to mine two blocks to orphan the current best block. By setting nLockTime such that only the next block can include the transaction, we discourage this practice as the height restricted and limited blocksize gives miners considering fee sniping fewer options for pulling off this attack.
A simple way to think about this is from the wallet’s point of view we always want the blockchain to move forward. By setting nLockTime this way we’re basically making the statement that we only want this transaction to appear in the next block; we don’t want to potentially encourage reorgs by allowing transactions to appear at lower heights than the next block in forks of the best chain.
Of course, the subsidy is high enough, and transaction volume low enough, that fee sniping isn’t a problem yet, but by implementing a fix now we ensure code won’t be written that makes assumptions about nLockTime that preclude a fix later.
对于大型矿工而言,最佳区块和mempool中的交易价值可能超过尝试故意挖掘两个区块以孤立当前最佳区块的成本。通过设置nLockTime使得只有下一个区块可以包括交易,我们不鼓励这种做法,因为高度限制和有限的区块大小给矿工考虑费用狙击更少的选项来解除这种攻击。
考虑这个问题的一个简单方法是从钱包的角度来看,我们总是希望区块链能够向前发展。通过以这种方式设置nLockTime,我们基本上是在声明我们只希望此交易出现在下一个块中;我们不希望通过允许交易出现在比最佳链的下一个分叉块中更低的高度来促进重新排序。
当然,补贴足够高,交易量足够低,费用狙击就不是问题,但是现在通过实现一个修复,我们确保代码不会被编写对nLockTime进行假设,以防止以后修复。
以下解说引用自《精通比特币()》
费用狙击是一种理论攻击情形,矿工试图从将来的块(挑选手续费较高的交易)重写过去的块,实现“狙击”更高费用的交易,以最大限度地提高盈利能力。
例如,假设存在的最高块是块#100,000。如果不是试图把#100,001 号的矿区扩 大到区块链,那么一些矿工们会试图重新挖矿#100,000。这些矿工可以选择在候 选块#100,000 中包括任何有效的交易(尚未开采)。他们不必使用相同的交易 来恢复块。事实上,他们有动力选择最有利可图(最高每 kBB)的交易来包含在 其中。它们可以包括处于“旧”#100,000 中的任何交易,以及来自当前内存池的 任何交易。当他们重新创建块#100,000 时,他们本质上可以将交易从“现在”提取 到重写的“过去”中。
今天,这种袭击并不是非常有利可图,因为回报奖励(因为包括 一定数量的比特币奖励)远远高于每个区块的总费用。但在未来的某个时候,交 易费将是奖励的大部分(甚至是奖励的整体)。那时候这种情况变得不可避免了。
为了防止“费用狙击”,当 Bitcoin Core /钱包 创建交易时,默认情况下,它使用 nLocktime 将它们限制为“下一个块”。在我们的环境中,Bitcoin Core /钱包将在任 何创建的交易上将 nLocktime 设置为 100,001。在正常情况下,这个 nLocktime 没 有任何效果 - 交易只能包含在#100,001 块中,这是下一个区块。 但是在区块链 分叉攻击的情况下,由于所有这些交易都将被时间锁阻止在#100,001,所以矿工 们无法从筹码中提取高额交易。他们只能在当时有效的任何交易中重新挖矿 #100,000,这导致实质上不会获得新的费用。 为了实现这一点,Bitcoin Core/钱 包将所有新交易的 nLocktime 设置为,并将所有输入上的 nSequence 设置为 0xFFFFFFFE 以启用 nLocktime。
txNew.nLockTime = chainActive.Height();//当前有效区块的高度
Secondly occasionally randomly pick a nLockTime even further back, so that transactions that are delayed after signing for whatever reason, e.g. high-latency mix networks and some CoinJoin implementations, have better privacy.
接着偶尔(0.1的概率)随机获取一个甚至可能更早的nLockTime,以便签名后的交易因任意原因延迟,比如高延迟混合网络和一些CoinJoin实现,有更好的隐私性。
if (GetRandInt(10) == 0)
txNew.nLockTime = std::max(0, (int)txNew.nLockTime - GetRandInt(100));
assert(txNew.nLockTime <= (unsigned int)chainActive.Height());
assert(txNew.nLockTime < LOCKTIME_THRESHOLD);
{
LOCK2(cs_main, cs_wallet);
{
std::vector<COutput> vAvailableCoins;
//用可用的交易输出填充vAvailableCoins,vAvailableCoins就是可用的交易输出
AvailableCoins(vAvailableCoins, true, coinControl);
nFeeRet = 0;
// Start with no fee and loop until there is enough fee
//循环直到有足够的交易金额
while (true)
{
//初始化工作,清零
nChangePosInOut = nChangePosRequest;
txNew.vin.clear();
txNew.vout.clear();
txNew.wit.SetNull();
wtxNew.fFromMe = true;
bool fFirst = true;
CAmount nValueToSelect = nValue;//初始为0
if (nSubtractFeeFromAmount == 0)
nValueToSelect += nFeeRet;
double dPriority = 0;
对每个接收者的处理,对每个接收者创建一个CTxOut
https://blog.csdn.net/m0_37847176/article/details/81624052#ctxout
// vouts to the payees
BOOST_FOREACH (const CRecipient& recipient, vecSend)
{
CTxOut txout(recipient.nAmount, recipient.scriptPubKey);
if (recipient.fSubtractFeeFromAmount)
{
txout.nValue -= nFeeRet / nSubtractFeeFromAmount; // Subtract fee equally from each selected recipient
//第一个接收者支出不能被总输出整除的剩余
if (fFirst) // first receiver pays the remainder not divisible by output count
{
fFirst = false;
txout.nValue -= nFeeRet % nSubtractFeeFromAmount;
}
}
//如果这个输出是Dust输出,也就是交易输出太小,称为灰尘交易
if (txout.IsDust(::minRelayTxFee))
{
if (recipient.fSubtractFeeFromAmount && nFeeRet > 0)
{
if (txout.nValue < 0)
strFailReason = _("The transaction amount is too small to pay the fee");
else
strFailReason = _("The transaction amount is too small to send after the fee has been deducted");
}
else
strFailReason = _("Transaction amount too small");
return false;
}
txNew.vout.push_back(txout);//写入交易的输出部分
}
// Choose coins to use
set<pair<const CWalletTx*,unsigned int> > setCoins;
CAmount nValueIn = 0;
//Shuffle and select coins until nTargetValue is reached while avoiding small change;
//打乱重排并选择可用的coins直到达到nTargetValue同时避免小的找零,这里是指到达nValueToSelect
//setCoins包含支付给你本人地址的交易,即你所拥有的币
if (!SelectCoins(vAvailableCoins, nValueToSelect, setCoins, nValueIn, coinControl))
{
strFailReason = _("Insufficient funds");
return false;
}
//对选择好的这一组coins的每个来源计算优先级
BOOST_FOREACH(PAIRTYPE(const CWalletTx*, unsigned int) pcoin, setCoins)
{
CAmount nCredit = pcoin.first->vout[pcoin.second].nValue;
//The coin age after the next block (depth+1) is used instead of the current,
//reflecting an assumption the user would accept a bit more delay for
//a chance at a free transaction.
//But mempool inputs might still be in the mempool, so their age stays 0
/*用下一个块(深度+ 1)之后的硬币年龄而不是当前,这反映了一个假设,即用户可以接受更多延迟以获得免费交易的机会。但是mempool输入可能仍然在mempool中, 所以他们的年龄保持在0*/
int age = pcoin.first->GetDepthInMainChain();
assert(age >= 0);
if (age != 0)
age += 1;
dPriority += (double)nCredit * age;//增加优先级
}
const CAmount nChange = nValueIn - nValueToSelect;//超出所需支出,那么需要找零
if (nChange > 0)
{
// Fill a vout to ourself
// TODO: pass in scriptChange instead of reservekey so
// change transaction isn't always pay-to-bitcoin-address
/*为自己填写一个vout *TODO:传入scriptChange而不是reservekey,因此找零交易并不总是付费到比特币地址*/
CScript scriptChange;
// coin control: send change to custom address找零支付回习惯地址,如果设置的话
if (coinControl && !boost::get<CNoDestination>(&coinControl->destChange))
scriptChange = GetScriptForDestination(coinControl->destChange);
// no coin control: send change to newly generated address
//没有coin control,默认没有设置,那么找零到一个新创建的地址
else
{
// Note: We use a new key here to keep it from being obvious which side is the change.
// The drawback is that by not reusing a previous key, the change may be lost if a
// backup is restored, if the backup doesn't have the new private key for the change.
// If we reused the old key, it would be possible to add code to look for and
// rediscover unknown transactions that were written with keys of ours to recover
// post-backup change.
/*我们使用一个新的密钥避免找零地址变得明显。缺点是不重复使用之前的密钥,如果没有备份用于找零的新私钥, *则在恢复备份时可能会丢失找零。如果我们重复使用老的密钥,那么有可能通过添加代码去查找和重新发现 *用我们的密钥编写的未明确的交易以恢复备份后的更改*/
// Reserve a new key pair from key pool 从密钥池中预约一个新的密钥
CPubKey vchPubKey;
bool ret;
ret = reservekey.GetReservedKey(vchPubKey);
if (!ret)
{
strFailReason = _("Keypool ran out, please call keypoolrefill first");
return false;
}
scriptChange = GetScriptForDestination(vchPubKey.GetID());
}
CTxOut newTxOut(nChange, scriptChange);//新建一笔找零交易
这里的意思是使用我们之前生成过的保留在钱包中的密钥
上述代码调用reservekey
的GetReservedKey
,reservekey
是传入的参数
bool CReserveKey::GetReservedKey(CPubKey& pubkey)
{
if (nIndex == -1)
{
CKeyPool keypool;
pwallet->ReserveKeyFromKeyPool(nIndex, keypool);
if (nIndex != -1)
vchPubKey = keypool.vchPubKey;
else {
return false;
}
}
assert(vchPubKey.IsValid());
pubkey = vchPubKey;
return true;
}
GetReservedKey
调用CWallet
类的ReserveKeyFromKeyPool
void CWallet::ReserveKeyFromKeyPool(int64_t& nIndex, CKeyPool& keypool)
{
nIndex = -1;
keypool.vchPubKey = CPubKey();//构造一个无效的PubKey
{
LOCK(cs_wallet);
if (!IsLocked())
TopUpKeyPool();//充值密钥池,这个函数里一个循环,默认创建(最少)100个新的密钥添加到池中
// Get the oldest key
if(setKeyPool.empty())
return;
CWalletDB walletdb(strWalletFile);
nIndex = *(setKeyPool.begin());//返回容器指向的第一个元素
setKeyPool.erase(setKeyPool.begin());//擦除这个指针指向的元素
if (!walletdb.ReadPool(nIndex, keypool))
throw runtime_error(std::string(__func__) + ": read failed");
if (!HaveKey(keypool.vchPubKey.GetID()))
throw runtime_error(std::string(__func__) + ": unknown key in key pool");
assert(keypool.vchPubKey.IsValid());
LogPrintf("keypool reserve %d\n", nIndex);
}
}
需要注意这里的setKeyPool是signd long long的set容器类型
在日志信息中可以看到,创建了101把密钥,从1到101,也就是原本这个池中一把都没有,这个池应该是专门用于找零的池子,虽然密钥都是保存在键值对中,但是找零专用的密钥对写在池中“pool“,调用WritePool
函数,另一种是写在‘keymate‘中
发送测试币回2N8hwP1WmJrFF5QWABn38y63uYLhnJYJYTF
使用rpc指令
创建两笔交易
parallels@parallels-vm:~$ bitcoin-cli walletpassphrase aser6789dfgb 300
parallels@parallels-vm:~$ bitcoin-cli sendtoaddress "2N8hwP1WmJrFF5QWABn38y63uYLhnJYJYTF" 0.2
441bb6516409b37f0b2da928cf4691ff0508f99f5481add15d608ee39ee59b04
parallels@parallels-vm:~$ bitcoin-cli getbalance
1.09896799
parallels@parallels-vm:~$ bitcoin-cli sendtoaddress "2N8hwP1WmJrFF5QWABn38y63uYLhnJYJYTF" 0.2
aceeee8102eb3a922521edb784bf890e29c26e3336ab8e99b5430fc71345b641
parallels@parallels-vm:~$
第一笔交易输出找零到mpCjnRXL2mVbFBU77ixWsCJ88JyqfU2g1c,作为第二笔交易的输入
在创建三笔交易,创建一个地址,创建一个交易
// Never create dust outputs; if we would, just
// add the dust to the fee.
if (newTxOut.IsDust(::minRelayTxFee))
{
nChangePosInOut = -1;
nFeeRet += nChange;
reservekey.ReturnKey();
}
else
{
if (nChangePosInOut == -1)
{
// Insert change txn at random position:
nChangePosInOut = GetRandInt(txNew.vout.size()+1);
}
else if ((unsigned int)nChangePosInOut > txNew.vout.size())
{
strFailReason = _("Change index out of range");
return false;
}
vector<CTxOut>::iterator position = txNew.vout.begin()+nChangePosInOut;
txNew.vout.insert(position, newTxOut);
}
}
else
reservekey.ReturnKey();
涉及vin的序列号,这个类写在这里https://blog.csdn.net/m0_37847176/article/details/81624052#ctxin
// Fill vin
//
// Note how the sequence number is set to max()-1 so that the
// nLockTime set above actually works.
BOOST_FOREACH(const PAIRTYPE(const CWalletTx*,unsigned int)& coin, setCoins)
txNew.vin.push_back(CTxIn(coin.first->GetHash(),coin.second,CScript(),std::numeric_limits<unsigned int>::max()-1));
这里设置输入交易容器vin,对于每一笔setCoins中的交易coin,构造CTxIn对象,使用的构造函数CTxIn(uint256 hashPrevTx, uint32_t nOut, CScript scriptSigIn=CScript(), uint32_t nSequenceIn=SEQUENCE_FINAL);
第1、2个参数用于构造COutPoint,第3、4个参数是CTxIn的成员变量,这里设置序列号为max()-1,不是SEQUENCE_FINAL(=max()),因此nLockTime实际是有效的。最后将CTxIn逐个加入容器的底部。
接下来看签名
// Sign
int nIn = 0;
CTransaction txNewConst(txNew);
BOOST_FOREACH(const PAIRTYPE(const CWalletTx*,unsigned int)& coin, setCoins)
{
bool signSuccess;
const CScript& scriptPubKey = coin.first->vout[coin.second].scriptPubKey;
SignatureData sigdata;
if (sign)
//非隔离见证的交易
signSuccess = ProduceSignature(TransactionSignatureCreator(this, &txNewConst, nIn, coin.first->vout[coin.second].nValue, SIGHASH_ALL), scriptPubKey, sigdata);
else
//使用隔离见证的交易,这里使用空的签名,在交易本身之外包含一个隔离见证
signSuccess = ProduceSignature(DummySignatureCreator(this), scriptPubKey, sigdata);
if (!signSuccess)
{
strFailReason = _("Signing transaction failed");
return false;
} else {
//从交易中抽取签名数据然后插入
UpdateTransaction(txNew, nIn, sigdata);
}
nIn++;
}
对于setCoins的每一笔交易coin,coin.first是CWalletTx的指针,CWalletTx没有成员变量,不过他的父类CMerkleTx
的父类CTransaction
有vout成员变量,通过序号获取对应的那一笔交易的锁定脚本scriptPubKey。
调用函数ProduceSignature
使用通用签名创建者生成脚本签名
//sign.h
/** Produce a script signature using a generic signature creator. */
bool ProduceSignature(const BaseSignatureCreator& creator, const CScript& scriptPubKey, SignatureData& sigdata);
8)判断
unsigned int nBytes = GetVirtualTransactionSize(txNew);
// Remove scriptSigs if we used dummy signatures for fee calculation
if (!sign) {
BOOST_FOREACH (CTxIn& vin, txNew.vin)
vin.scriptSig = CScript();
txNew.wit.SetNull();
}
// Embed the constructed transaction data in wtxNew.
*static_cast<CTransaction*>(&wtxNew) = CTransaction(txNew);
// Limit size限制大小
if (GetTransactionWeight(txNew) >= MAX_STANDARD_TX_WEIGHT)
{
strFailReason = _("Transaction too large");
return false;
}
dPriority = wtxNew.ComputePriority(dPriority, nBytes);
// Can we complete this as a free transaction?
//构造一个免费的交易,费用不够的话用优先级来凑
if (fSendFreeTransactions && nBytes <= MAX_FREE_TRANSACTION_CREATE_SIZE)//默认值分别为false、1000
{
// Not enough fee: enough priority?
double dPriorityNeeded = mempool.estimateSmartPriority(nTxConfirmTarget);
// Require at least hard-coded AllowFree.
if (dPriority >= dPriorityNeeded && AllowFree(dPriority))
break;
}
//获取最小交易费
CAmount nFeeNeeded = GetMinimumFee(nBytes, nTxConfirmTarget, mempool);
if (coinControl && nFeeNeeded > 0 && coinControl->nMinimumTotalFee > nFeeNeeded) {
nFeeNeeded = coinControl->nMinimumTotalFee;
}
if (coinControl && coinControl->fOverrideFeeRate)
nFeeNeeded = coinControl->nFeeRate.GetFee(nBytes);
// If we made it here and we aren't even able to meet the relay fee on the next pass, give up
// because we must be at the maximum allowed fee.
if (nFeeNeeded < ::minRelayTxFee.GetFee(nBytes))
{
strFailReason = _("Transaction too large for fee policy");
return false;
}
if (nFeeRet >= nFeeNeeded)
break; // Done, enough fee included.
// Include more fee and try again.
nFeeRet = nFeeNeeded;
continue;
}
}
}
到这里循环结束
if (GetBoolArg("-walletrejectlongchains", DEFAULT_WALLET_REJECT_LONG_CHAINS)) {
// Lastly, ensure this tx will pass the mempool's chain limits
LockPoints lp;
CTxMemPoolEntry entry(txNew, 0, 0, 0, 0, false, 0, false, 0, lp);
CTxMemPool::setEntries setAncestors;
size_t nLimitAncestors = GetArg("-limitancestorcount", DEFAULT_ANCESTOR_LIMIT);
size_t nLimitAncestorSize = GetArg("-limitancestorsize", DEFAULT_ANCESTOR_SIZE_LIMIT)*1000;
size_t nLimitDescendants = GetArg("-limitdescendantcount", DEFAULT_DESCENDANT_LIMIT);
size_t nLimitDescendantSize = GetArg("-limitdescendantsize", DEFAULT_DESCENDANT_SIZE_LIMIT)*1000;
std::string errString;
if (!mempool.CalculateMemPoolAncestors(entry, setAncestors, nLimitAncestors, nLimitAncestorSize, nLimitDescendants, nLimitDescendantSize, errString)) {
strFailReason = _("Transaction has too long of a mempool chain");
return false;
}
}
return true;
}
那么到这里就创建完交易了,接下来提交交易。