[C#] 比特幣冒險:掌握BRC-20標準及Witness資料的抽取技巧

2023-12-22

最近幣圈風風火火關於比特幣銘文(Ordinals),這時候要說到 BRC-20,我相信前幾年如果你有聽過 NFT 或是乙太幣,應該對這名詞覺得有點熟悉但是又陌生

BRC-20 是一個實驗性的比特幣同質化代幣標準,由推特用戶 @domodata 於 2023 年 3 月 8 日基於 Ordinal 協議創建它類似於以太坊的 ERC20 標準,

規定了比特幣上發行代幣的名稱、發行量、轉帳等功能BRC-20 代幣可以通過 Ordinal 協議在比特幣網路上鑄造和轉移

如果您對 BRC-20 代幣等虛擬貨幣感興趣可以看看下面這影片,說的是淺顯易懂

看完之後,簡單的說,就是一群想在比特幣老大哥的鏈上搞事情的人們,至於 NFT 這東西會不會在紅一波我不知道

今天主要就是既然都把銘文寫在鏈上面,我們是不是有辦法透過程式碼,把那張銘文取出

網路上我有找到,講解銘文放置的地方 ,基本上他就是基於升級後的 Bitcoin 鏈上,一個叫做  Segregated Witness 的地方變大了,可以放到 4M 左右的資料

這邊有相關的資料 比特幣、以太坊的一些問題介紹,我就不贅述 

至於規格上面要怎麼提取,這邊有不用程式的做法,而且講解的很詳細,我也慢慢的看完且跟著做了一遍去好好理解到底在幹嘛

這影片有興趣也可以看一下,會有點無聊,但是研究技術就是這樣要搞懂都是需要時間

接下來就是 如何使用 C# 來提取 鏈上面 Witness 中的資料

這邊是我示範用的 TXID : 12d980d930ae49a9aa69d81cf466116259617410bf1c0f89ec1f1ba0c2c3bfc9

1. 我這邊是用 .Net 7 做編譯,首先先安裝套件 NBitcoin , Newtonsoft.Json

2. 之後就是程碼的部份了,這邊我是取用  https://blockchain.info 這網站的資料來調用 Bitcoin 上面的鏈上資料

using NBitcoin; using NBitcoin.DataEncoders; using Newtonsoft.Json; using System.Net.Http; using System.Reflection; using System.Text; namespace DownloadOrdinalSample { internal class Program { public static readonly HttpClient _httpClient = new(); static void Main(string[] args) { Console.WriteLine("Hello, Ordinal!"); var ordinalTxId = "12d980d930ae49a9aa69d81cf466116259617410bf1c0f89ec1f1ba0c2c3bfc9"; var oData = GetOrdinalData(ordinalTxId); File.WriteAllBytes(AppDomain.CurrentDomain.BaseDirectory + "sample.png",oData.Metadata); } /// <summary> /// 取得 TXID 的銘文資料 /// </summary> /// <param name="ordinalTxId"></param> /// <returns></returns> /// <exception cref="Exception"></exception> public static OrdinalData GetOrdinalData(string ordinalTxId) { using (var request = new HttpRequestMessage()) { request.Method = HttpMethod.Get; request.RequestUri = new Uri(string.Format("https://blockchain.info/rawtx/{0}", ordinalTxId)); try { using (var response = _httpClient.Send(request)) { var content = response.Content.ReadAsStringAsync().Result; if (content != null) { BlockchainInfoTxModel? json = JsonConvert.DeserializeObject<BlockchainInfoTxModel>(content); Input inputs = json?.inputs[0]; string witness = Convert.ToString(inputs?.witness); if (witness == null) { throw new Exception(string.Format("Error parsing API response from {0}", string.Format("https://blockchain.info/rawtx/", ordinalTxId))); } OrdinalData ordinal = DecodeWitnessData(ordinalTxId, witness); return ordinal; } return null; } } catch (Exception exp) { throw new Exception("Error requesting API ", exp); } } } /// <summary> /// 解析 WitnessData /// </summary> /// <param name="bitcoinTxId"></param> /// <param name="witnessHex"></param> /// <returns></returns> /// <exception cref="Exception"></exception> private static OrdinalData DecodeWitnessData(string bitcoinTxId, string witnessHex) { BitcoinStream stream = new(Encoders.Hex.DecodeData(witnessHex)); if (!stream.ProtocolCapabilities.SupportWitness) { throw new Exception(string.Format("The transaction id {0} is not a witness transaction.", bitcoinTxId)); } WitScript witness = WitScript.Load(stream); IEnumerable<byte[]> pushes = witness.Pushes; foreach (byte[] push in pushes) { OrdinalData ordinalData = ParseScriptData(push); if (ordinalData != null) { return ordinalData; } } throw new Exception(string.Format("No ordinal data found in the witness transaction: {0}.", bitcoinTxId)); } private static OrdinalData ParseScriptData(byte[] push) { Script script = new(push); //Debug.WriteLine(script.ToString()); bool bIsOrdDataRegion = false; OrdinalData data = new(); byte[] arrayMetadata = { }; foreach (Op op in script.ToOps()) { // https://developer.bitcoin.org/reference/transactions.html // OP_1 indicates that the next push contains the content type // and OP_0 indicates that subsequent data pushes contain the content itself. // Multiple data pushes must be used for large inscriptions, as one of taproot's few restrictions is that individual data pushes may not be larger than 520 bytes. /*OP_FALSE * OP_IF * OP_PUSH "ord" * OP_1 * OP_PUSH "text/plain;charset=utf-8" * OP_0 * OP_PUSH "Hello, world!" * OP_ENDIF*/ // https://docs.ordinals.com/inscriptions.html /* * * 2b699194e41d48 OP_IF OP_UNKNOWN(0xed) 6b8c3fec1d7f100325baa82bf0ec3f22ed0aa93b97 e OP_DEPTH OP_UNKNOWN(0xfd) a737b713920eded10ba5 0 * 117f692257b2331233b5705ce9c682be8719ff1b2b64cbca290bd6faeb54423e [OP_CHECKSIG dfeb9d208701] [OP_DROP 0] [OP_IF 6f7264] [1 696d6167652f706e67] [0 89504e470d0a1a0a0000000d49484452000000360000003608020000000327fd8a0000000467414d410000b18f0bfc6105000000017352474200aece1ce9000000097048597300000ec300000ec301c76fa864000002514944415468deed99bd4a03411485a74840d1c6c242d4c22aa0b1d04a10120b094454ec6c04312882222a88f847888d565a6950b11285348a0fe00bd80afa0e163e81e07a926baec3ec3aab9bc96e90190ec3dd9b09f7db33bbb3938d705e2e1a5cc2225a448b5837c4c144826470a4791745a5e96be3531a16cd4453ed782cf61325f2f8347a448d916c61f4889e94325f64887a23cd5af87f113f9ecf7fa254f86864944bb706b1819e2e1322aea8b929aefbcadfe9458d7ceffdeb2427572c6baf84a48e2c4c44e273bb08501d65c888b8e6327ddd8a90bf6e1ff6a69c110e2a8aaaea8d78393f0a2d0d7dc371c603919850eeaed287e022509c4a634a25e3e9a2105517eb8d4834a857281488c99d01a5075fa02bb226446e72c607b1ba9a8637d17cc72813ed0921c389d06e17aaaadc2e887d09c240f45c71587a8230265ad93572f3450cf004af09b1b7a3ed4f2ed2394489c804d42bbb46f6d834229a1c48bd2fa29c0fcce78338d7d992cfe7d1cb01c77a4488c6287c261151e07165ea75739a042cee494c4c526c4386c6e034a03ab8e838a0b9cd8d411cb845e7703ada7f931d60b7284006f932fad99b78f8 805a93d960bb719d8b87a93eb7d2e934058a8bf2b4d2e117e2f6130988e67f18d0350726eee580c6a0f0c841295b28ceee4cce6d74ed9f24111393cc2716eecd23824f0e94435679fa2a100757e333ab89e9e59e4c2e357fb44558acc07c665edea1fce2f971666d1701898ce4c35af88c212a979a3b63117fb19990173c7726e2b711cabee1379b9d685c9469ccbe73b2ff1858448b68112da245b4880dab4feb0262817f6e5c6f0000000049454e44ae426082] OP_ENDIF * OP_UNKNOWN(0xc1) 7f692257b2331233b5705ce9c682be8719 OP_UNKNOWN(0xff) 0 */ //Debug.WriteLine(op.ToString()); //Debug.WriteLine(op.PushData != null ? Encoding.UTF8.GetString(op.PushData).ToString() : ""); /*if (op.PushData != null && Encoding.UTF8.GetString(op.PushData) == "text/plain;charset=utf-8") { }*/ if ((byte)op.Code == 3) { if (op.PushData == null) continue; // Convert UTF8 bytes to string string pushDataString = Encoding.UTF8.GetString(op.PushData); if (pushDataString == "ord") { // specification standard to filter out other junk https://docs.ordinals.com/inscriptions.html bIsOrdDataRegion = true; // flag } //Debug.WriteLine(op.ToString()); } else if ((byte)op.Code == 9 || (byte)op.Code == 24) { string pushDataString = Encoding.UTF8.GetString(op.PushData); data.MetadataType = pushDataString; // set } else if (op.Code == OpcodeType.OP_PUSHDATA2 && bIsOrdDataRegion) { if (op.PushData == null) continue; // combine the old arrayMetadata array with op.PushData byte[] newArray = new byte[arrayMetadata.Length + op.PushData.Length]; Buffer.BlockCopy(arrayMetadata, 0, newArray, 0, arrayMetadata.Length); Buffer.BlockCopy(op.PushData, 0, newArray, arrayMetadata.Length, op.PushData.Length); arrayMetadata = newArray; } } if (arrayMetadata.Length > 0) { data.Metadata = arrayMetadata; // set return data; } return null; } public class OrdinalData { /// <summary> /// text/plain;charset=utf-8 /// </summary> public string MetadataType { get; set; } /// <summary> /// Hello, world! /// </summary> public byte[] Metadata { get; set; } } /// <summary> /// 處理 https://blockchain.info 的模型資料 /// </summary> public class Input { public class PrevOut { public class SpendingOutpoint { public int n { get; set; } public long tx_index { get; set; } } public string addr { get; set; } public int n { get; set; } public string script { get; set; } public List<SpendingOutpoint> spending_outpoints { get; set; } public bool spent { get; set; } public long tx_index { get; set; } public int type { get; set; } public int value { get; set; } } public long sequence { get; set; } public string witness { get; set; } public string script { get; set; } public int index { get; set; } public PrevOut prev_out { get; set; } } /// <summary> /// 處理 https://blockchain.info 的模型資料 /// </summary> public class BlockchainInfoTxModel { public class Out { public int type { get; set; } public bool spent { get; set; } public int value { get; set; } public List<object> spending_outpoints { get; set; } public int n { get; set; } public object tx_index { get; set; } public string script { get; set; } public string addr { get; set; } } public string hash { get; set; } public int ver { get; set; } public int vin_sz { get; set; } public int vout_sz { get; set; } public int size { get; set; } public int weight { get; set; } public int fee { get; set; } public string relayed_by { get; set; } public int lock_time { get; set; } public long tx_index { get; set; } public bool double_spend { get; set; } public int time { get; set; } public uint block_index { get; set; } public uint block_height { get; set; } public List<Input> inputs { get; set; } public List<Out> @out { get; set; } } } }


之後就會下載到一張 這樣的圖片

大概就是這樣吧,基本上我也是改寫,一個網路大大的分享的專案,抽取這部份來用,最近有需要用到,想說就整理一下分享給大家

參考程式碼: 

https://github.com/lastbattle/ordinal_inscription.netcore


這範例我放在這有需要的自己取用

https://github.com/donma/DownloadOrdinalSample






當麻許的碎念筆記 2014 | Donma Hsu Design.