比特幣入門之使用PRC應用開發介面
一、RPC API概述
比特幣定義了RPC API來允許第三方應用通過節點軟體訪問比特幣網路。 事實上,bitcoin-cli就是通過這個介面來實現其功能的,也就是說, 我們可以在自己的C#程式中完全實現bitcoin-cli的功能。
JSON RPC採用JSON語法表示一個遠端過程呼叫(Remote Procedure Call) 的請求與應答訊息。例如對於getbalance
呼叫,請求訊息與應答訊息的格式 示意如下:
在請求訊息中使用method
欄位宣告要呼叫的遠端方法名,使用params
欄位 宣告呼叫引數列表;訊息中的jsonrpc
欄位宣告所採用的JSON RPC版本號, 而可選的id
響應訊息中的result
欄位記錄了遠端呼叫的執行結果,而error
欄位 則記錄了呼叫執行過程中出現的錯誤,id
欄位則對應於請求訊息中的同名 欄位值。
JSON RPC是傳輸協議無關的,但基於HTTP的廣泛應用,節點通常都會提供基於 HTTP協議的實現,也就是說將JSON PRC訊息作為HTTP報文的內容載荷進行傳輸:
bitcoind在不同的執行模式下,會在不同的預設埠監聽HTTP RPC API請求:
- 主網模式:8332
- 測試網模式:18332
- Regtest開發模式:18443
可以在bitcoind的配置檔案中使用rpcbind
選項和rpcport
選項修改監聽端結點, 例如,設定為本地7878埠:
rpcbind=127.0.0.1 rpcport=7878
二、使用curl測試RPC API
curl是一個支援URL語法的多協議命令列資料傳輸工具,可以從 官網下載:
curl支援HTTP、FTP等多種協議,因此我們可以使用它來驗證節點基於HTTP旳rpc介面 是否正常工作。例如,使用如下的命令訪問節點旳getnetworkinfo
介面:
~$ curl -X POST -d '{ > "jsonrpc":"1.0", > "method":"getnetworkinfo", > "params":[], > "id":"123" > }' http://user:123456@localhost:18443
curl提供了很多選項用來定製HTTP請求。例如,可以使用-X
選項宣告HTTP請求 的方法,對於JSON RPC來說,我們總是使用POST
方法;-d
選項則用來宣告請求中包含 的資料,對於JSON RPC呼叫,這部分就是請求訊息,例如我們按照getnetworkinfo
呼叫的 要求進行組織即可;命令的最後,也就是RPC呼叫訊息的傳送目的地址,即節點RPC API的訪問URL。
預設情況下curl返回的結果是沒有格式化的JSON字串,對機器友好,但並不適合人類查閱:
如果你希望結果顯示的更友好一些,可以級聯一個命令列的json解析工具例如jq
:
~$ curl -X POST -s -d '{...}' http://user:123456@localhost:18443 | jq
jq
是一個輕量級的命令列JSON處理器,你可以從官網 下載它。
curl -X POST -s -d '{"method":"getnetworkinfo","params":[],"id":123,"jsonrpc":"1.0"}' \ http://user:123456@localhost:18443 | jq
三、在C#程式碼中訪問RPC API
自然,我們也可以在C#程式碼中來呼叫節點旳JSON RPC開發介面,可以藉助於一個 http協議封裝庫來執行這些發生在HTTP之上的遠端呼叫,例如.NET內建的HttpClient:
例如,下面的程式碼使用HttpClient呼叫比特幣節點的getnetworkinfo
介面:
首先下載bitcoin: https://bitcoin.org/zh_CN/download,如果使用主網路需要同步240G的資料,這裡在本地以私鏈模式執行。私鏈模式執行也比較容易配置,只需要在bitcoin.conf中配置regtest=1。在windows下,bitcoin.conf的預設路徑為%APPDATA%\bitcoin\bitcoin.conf。我的電腦在C:\Users\Administrator\AppData\Roaming\Bitcoin目錄下。預設情況下bitcoind並不會自動建立上述路徑下的bitcoin.conf配置檔案,因此需要 自行製作一份放入上述目錄。如果你沒有現成的配置檔案可用,可以從github拷貝一份:https://github.com/bitcoin/bitcoin/blob/master/share/examples/bitcoin.conf。關於bitcoin.conf的配置可以參考我的另一部落格。
這裡regtest=1使用私鏈模式,server=1啟動rpc,rpcuser=usertest、rpcpassword=usertest 設定使用者名稱、密碼。
#testnet=0 regtest=1 proxy=127.0.0.1:9050 #bind=<addr> #whitebind=<addr> #addnode=69.164.218.197 #addnode=10.0.0.2:8333 #connect=69.164.218.197 #listen=1 #maxconnections= server=1 #rpcbind=<addr> rpcuser=usertest rpcpassword=usertest #rpcclienttimeout=30 #rpcallowip=10.1.1.34/255.255.255.0 #rpcallowip=1.2.3.4/24 #rpcallowip=2001:db8:85a3:0:0:8a2e:370:7334/96 #rpcport=8332 #rpcconnect=127.0.0.1 #txconfirmtarget=n #paytxfee=0.000x #keypool=100 #prune=550 #min=1 #minimizetotray=1View Code
啟動之後如下圖所示:會有一個regtest標記。
using System; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; namespace RPCHttpClient { class Program { static void Main(string[] args) { Task.Run(async () => { HttpClient httpClient = new HttpClient(); byte[] authBytes = Encoding.ASCII.GetBytes("usertest:usertest"); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(authBytes)); string payload = "{\"jsonrpc\":\"1.0\",\"method\":\"getnetworkinfo\",\"params\":[],\"id\":7878}"; StringContent content = new StringContent(payload, Encoding.UTF8, "application/json"); HttpResponseMessage rsp = await httpClient.PostAsync("http://127.0.0.1:18443", content); string ret = await rsp.Content.ReadAsStringAsync(); Console.WriteLine(ret); Console.ReadLine(); }).Wait(); } } }View Code
在上面的程式碼中,我們首先例項化一個HttpClient物件並設定HTTP驗證資訊,然後呼叫該物件 的PostAsync()方法向節點旳RPC埠傳送請求訊息即可完成呼叫。
四、序列化與反序列化
在應用邏輯裡直接拼接RPC請求字串,或者直接解析RPC響應字串,都不是件令人舒心的事情, 我們需要改進這一點。
更乾淨的辦法是使用資料傳輸物件(Data Transfer Object)來 隔離這個問題,在DTO層將 C#的物件序列化為Json字串,或者從Json字串 反序列化為C#的物件,應用程式碼只需要操作C#物件即可。
我們首先定義出JSON請求與響應所對應的C#類。例如:
現在我們獲取比特幣網路資訊的程式碼可以不用直接操作字串了:
using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Text; namespace RPCHttpDTO { class RpcRequestMessage { [JsonProperty("id")] public int Id; [JsonProperty("method")] public string Method; [JsonProperty("params")] public object[] Parameters; [JsonProperty("jsonrpc")] public string JsonRPC = "1.0"; public RpcRequestMessage(string method, params object[] parameters) { Id = Environment.TickCount; Method = method; Parameters = parameters; } } class RpcResponseMessage { [JsonProperty("id")] public int Id { get; set; } [JsonProperty("result")] public object Result { get; set; } [JsonProperty("jsonrpc")] public string JsonRPC { get; set; } } }View Code
using Newtonsoft.Json; using System; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; namespace RPCHttpDTO { class Program { static void Main(string[] args) { Task.Run(async () => { HttpClient httpClient = new HttpClient(); byte[] authBytes = Encoding.ASCII.GetBytes("usertest:usertest"); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(authBytes)); RpcRequestMessage reqMsg = new RpcRequestMessage("getnetworkinfo"); Console.WriteLine("=> {0}", reqMsg.Method); string payload = JsonConvert.SerializeObject(reqMsg); StringContent content = new StringContent(payload, Encoding.UTF8, "application/json"); HttpResponseMessage rsp = await httpClient.PostAsync("http://localhost:18443", content); string ret = await rsp.Content.ReadAsStringAsync(); RpcResponseMessage rspMsg = JsonConvert.DeserializeObject<RpcResponseMessage>(ret); Console.WriteLine("<= {0}", rspMsg.Result); Console.ReadLine(); }).Wait(); } } }View Code
五、使用JSON RPC封裝庫
除了直接使用HTTP協議庫來訪問比特幣節點,在開源社群中也有一些直接針對 比特幣RPC協議的封裝,例如MetacoSA的NBitcoin:
NBitcoin是.NET平臺上最完整的比特幣開發庫,實現了很多相關的比特幣改進建議(Bitcoin Improvement Proposal)。 與RPC協議封裝相關的類主要在NBitcoin.RPC名稱空間下,入口類為RPCClient
, 它代表了對一個的比特幣RPC訪問端結點的協議封裝。
例如,下面的程式碼建立一個指向本機的私有鏈節點RPC的RPCClient例項:
//using NBitcon.RPC; string auth = "user:123456"; //rpc介面的賬號和密碼 string url = "http://localhost:18443" //本機私有鏈的預設訪問端結點 Network network = Network.RegTest; //網路引數物件 RPCClient client = new RPCClient(auth,url,network); //例項化
比特幣有三個不同的網路:主網、測試網和私有鏈,分別有一套對應的網路引數。 在NBitcoin中,使用Network類來表徵比特幣網路,它提供了三個靜態屬性分別 返回對應於三個不同網路的Network例項。在例項化RPCClient時需要傳入與節點 對應的網路引數物件,例如當連線的節點是主網節點時,需要傳入Network.Main, 而當需要本地私有鏈節點時,就需要傳入Network.RegTest
。
一旦例項化了RPCClient,就可以使用其SendCommand()
或SendCommandAsync()
方法呼叫比特幣節點的RPC介面了。容易理解,這兩個方法分別對應於同步呼叫 和非同步呼叫,除此之外,兩者是完全一致的。
例如,下面的程式碼使用同步方法呼叫getnetworkinfo
介面返回節點軟體版本號:
//using Newtonsoft.Json.Linq; RPCRequest req = new RPCRequest{ //RPC請求物件 Method = "getnetworkinfo", Params = new object[]{} }; RPCResponse rsp = client.SendCommand(req); //返回RPC響應物件 Console.WriteLine(rsp.ResultString); //ResultString返回原始的響應字串
SendCommand/SendCommandAsync的過載
如果你注意到例項化RPCRequest物件最重要的是Method和Params這兩個屬性,就容易 理解應該有更簡單的SendCommand/SendCommandAsync方法了。下面是最常用的一種, 只需要傳入方法名和動態引數列表,不需要自己再定義RPCRequest資料:
public RPCResponse SendCommand(string commandName, params object[] parameters)
例如,下面的程式碼分別展示了無參和有參呼叫的使用方法:
client.SendCommand("getnetworkworkinfo"); //無參呼叫 client.SendCommand("generate",1); //有參呼叫
容易理解,這個過載在內部幫我們構建了RPCRequest物件。
從響應結果中提取資料
RPCResponse的ResultString屬性返回原始的JSON響應字串,因此從中提取 資料的一個辦法就是將其轉換為C#的動態物件,這是最簡明直接的方法:
dynamic ret = JsonConvert.DeserializeObject(rsp.ResultString); Console.WriteLine(ret.networks[0].name);
另一種提取資料的方法是使用RPCResponse的Result屬性,它返回一個JToken
物件, 因此可以非常方便地使用JPath表示式來提取指定路徑的資料。
例如,下面的程式碼從getnetworkinfo的響應結果中提取並顯示節點啟用的所有網路 介面名稱:
IEnumerable<JToken> names = rsp.Result.SelectTokens("networks[*].name"/*JPath表示式*/); foreach(var name in names) Console.WriteLine(name);
如果你不熟悉JToken和JPath,那麼JToken的使用方法可以訪問其 官網文件, 關於JPath表示式可以訪問這裡。
首先需要引入NBitcoin。
using NBitcoin; using NBitcoin.RPC; using Newtonsoft.Json; using System; using System.Threading.Tasks; namespace RPCNbitcoin { class Program { static void Main(string[] args) { Task.Run(async () => { RPCClient client = new RPCClient("usertest:usertest", "http://localhost:18443", Network.RegTest); RPCRequest req = new RPCRequest { Method = "getnetworkinfo", Params = { } }; RPCResponse rsp = await client.SendCommandAsync(req); dynamic ret = JsonConvert.DeserializeObject(rsp.ResultString); Console.WriteLine("network#0 => {0}", ret.networks[0].name); var names = rsp.Result.SelectTokens("networks[*].name"); foreach (var name in names) Console.WriteLine(name); Console.ReadLine(); }).Wait(); } } }View Code
六、NBitcoin的RPC封裝完成度
在大多數情況下,使用RPCClient的SendCommand或SendCommandAsync方法, 就可以完成比特幣的RPC呼叫工作了。考慮到比特幣RPC介面本身的不穩定性, 這是萬能的使用方法。
不過看起來NBitcoin似乎是希望在RPCClient中逐一實現RPC介面,雖然 這一任務還沒有完成。例如,對於getbalance呼叫,其對應的方法為 GetBalance和GetBalanceAsync,因此我們也可以採用如下的方法獲取錢包餘額:
Money balance = client.GetBalance(); Console.WriteLine("balance: {0} BTC", balance.ToUnit(MoneyUnit.BTC)); //單位:btc Console.WriteLine("balance: {0} SAT", balance.Satoshi); //單位:sat
顯然,NBitcoin的預封裝方法進行了額外的資料處理以返回一個Money例項, 這比直接使用SendCommand會更方便一些:
因此如果NBitcoin已經實現了你需要的那個RPC介面的直接封裝,建議首選直接封裝方法, 可以在這裡 檢視RCPClient的官方完整參考文件。
下表列出了部分在RPCClient中已經實現的RPC介面及對應的同步方法名,考慮到空間問題, 表中省去了非同步方法名,相信這個清單會隨著NBitcoin的開發越來越長:
分類 | RPC介面 | RPCClient方法 | 備註 |
---|---|---|---|
P2P網路 | addnode | AddNode | 新增/刪除P2P節點地址 |
getaddednodeinfo | GetAddedNodeInfo | 獲取新增的P2P節點的資訊 | |
getpeerinfo | GetPeerInfo | 獲取已連線節點的資訊 | |
區塊鏈 | getblockchaininfo | GteBlockchainInfo | 獲取區塊鏈的當前資訊 |
getbestblockhash | GetBestBlockHash | 獲取最優鏈的最近區塊雜湊 | |
getblockcount | GetBlockCount | 獲取本地最優鏈中的區塊數量 | |
getblock | GetBlock | 獲取具有指定塊頭雜湊的區塊 | |
getblockhash | GetBlockHash | 獲取指定高度區塊的塊頭雜湊 | |
getrawmempool | GetRawMemPool | 獲取記憶體池中的交易ID陣列 | |
gettxout | GetTxOut | 獲取指定的未消費交易輸出的詳細資訊 | |
工具類 | estimatefee | EstimateFee | 估算千位元組交易費率 |
estimatesmartfee | EstimateSmartFee | ||
未公開 | invalidateblock | InvalidateBlock | |
錢包 | backupwallet | BackupWallet | 備份錢包檔案 |
dumpprivkey | DumpPrivateKey | 匯出指定地址的私鑰 | |
getaccountaddress | GetAccountAddress | 返回指定賬戶的當前地址 | |
importprivkey | ImportPrivKey | 匯入WIF格式的私鑰 | |
importaddress | ImportAddress | 匯入地址以監聽其相關交易 | |
listaccounts | ListAccounts | 獲取賬戶及對應餘額清單 | |
listaddressgroupings | ListAddressGroupings | 獲取地址分組清單 | |
listunspent | ListUnspent | 獲取錢包內未消費交易輸出清單 | |
lockunspent | LockUnspent | 鎖定/解鎖指定的交易輸出 | |
walletpassphrase | WalletPassphrase | 解鎖錢包 | |
getbalance | GetBalance | 獲取錢包餘額 | |
getnewaddress | GetNewAddress | 建立並返回一個新的錢包地址 |
值得指出的是,NBitcoin採用了PASCAL命名規則來生成RPC介面對應的方法名稱, 即每個單詞的首字母大寫。
using NBitcoin; using NBitcoin.RPC; using System; namespace RPCNbitcoinAdvanced { class Program { static void Main(string[] args) { RPCClient client = new RPCClient("usertest:usertest", "http://localhost:18443", Network.RegTest); Money balance = client.GetBalance(); Console.WriteLine("balance => {0} btc", balance); BitcoinAddress address = client.GetNewAddress(); Console.WriteLine("address => {0}", address); uint256 txid = client.SendToAddress(address, Money.Coins(0.1m)); Console.WriteLine("sent 0.1 btc to above address."); client.Generate(100); Console.WriteLine("mined a block."); UnspentCoin[] coins = client.ListUnspent(0, 9999, address); foreach (var coin in coins) { Console.WriteLine("unspent coin => {0} btc", coin.Amount); } Console.ReadLine(); } } }
七、利用UTXO計算錢包餘額
我們知道,比特幣都在UTXO上存著,因此容易理解,錢包的餘額 應該就是錢包內所有的地址相關的UTXO的彙總:
首先檢視錢包餘額:
Money balance = client.getBalance();
然後使用listunspent
介面列出錢包內地址相關的UTXO:
UnspentCoin[] coins = client.ListUnspent(); //listunspent介面封裝方法 long amount = 0; foreach(var coin in coins){ //累加所有utxo的金額 amount += coin.Amount.Satoshi; }
ListUnspent()方法返回的結果是一個數組,每個成員都是一個UnspentCoin 物件:
最後我們比較一下:
if(balance.Satoshi == amount){ Console.WriteLine("verified!"); }
using NBitcoin; using NBitcoin.RPC; using System; using System.Threading.Tasks; namespace CalcBalance { class Program { static void Main(string[] args) { Task.Run(async () => { RPCClient client = new RPCClient("usertest:usertest", "http://localhost:18443", Network.RegTest); Money balance = await client.GetBalanceAsync(); Console.WriteLine("getbalance => {0}", balance.Satoshi); UnspentCoin[] coins = await client.ListUnspentAsync(); long amount = 0; foreach (var coin in coins) { amount += coin.Amount.Satoshi; } Console.WriteLine("unspent acc => {0}", amount); if (balance.Equals(Money.Satoshis(amount))) Console.WriteLine("verified successfully!"); else Console.WriteLine("failed to verify balance"); Console.ReadLine(); }).Wait(); } } }View Code
八、讓網站支援比特幣支付
使用bitcoind,我們可以非常快速地為網站增加接受比特幣支付的功能:
當用戶選擇採用比特幣支付其訂單時,網站將自動提取該訂單對應的 比特幣地址(如果訂單沒有對應的比特幣地址,則可以使用getnewaddress
建立一個), 並在支付網頁中顯示訂單資訊、支付地址和比特幣支付金額。為了方便 使用手機錢包的使用者,可以將支付資訊以二維碼的形式在頁面展現出來:
使用者使用比特幣錢包向指定的地址支付指定數量的比特幣後,即可點選 [已支付]按鈕,提請網站檢查支付結果。網站則開始週期性地呼叫節點 的getreceivedbyaddress
命令來檢查訂單對應地址的收款情況,一旦 收到足量比特幣,即可結束該訂單的支付並啟動使用者產品或服務的交付。 預設情況下,getreceivedbyaddress
將至少需要六個確認才會報告 地址收到的交易。
除了使用getreceivedbyadress
命令來輪詢收款交易,另一種檢查 使用者支付的方法是使用bitcoind的walletnotify選項。當bitcoind檢測 到錢包中的地址發生交易時,將會呼叫walletnotify選項設定的指令碼, 並傳入交易id作為引數,因此可以在指令碼中進一步獲取交易詳細資訊。 例如在下面的配置檔案中,當錢包中的地址發生交易時,將觸發 tx-monitor.sh指令碼:
walletnofity=/var/myshop/tx-monitor.sh %s
這是一個相當樸素的方案,但很容易實現。此外,如果你需要實時進行 法幣和比特幣的換算,還可以使用blockchain.info 提供的相關api。
&n