[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.