0%

usdt | 解析 Omni协议上的 USDT 转账

BTC 自带的协议是不支持解析 USDT ,而 USDTBTC 的网络中,又叫染色币,这篇博文,主要是说明如何在 BTC 网络上解析 USDT 转账记录。


参考资料



染色币


ETH 因为智能合约系统而受到了广泛地关注,大家可以用 5 分钟时间就发出自己的第一个代币,而代币的发行、转账则都依赖于智能合约,底层则是由 ETH 公链承载。事实上,远在 ETH 诞生之前,大家就想用比特币主链做点啥。

其中,一个重要的概念被提出来了:染色币。具体来说,染色币是指在普通的比特币交易中附上一些信息,借助比特币底层基础设施来记录。然而,比特币官方开发组(Core)对这种方式颇有争议,将用于存储信息的 OP_RETURN 字段从 80 字节骤然缩小到了 40 字节 。

Omni Layer(原名 Mastercoin)

Omni Layer 也属于染色币,其核心思想是将 Omni Protocol 层的数据用某种方式写入比特币区块链中,目前在协议定义中有三种,Class AClass BClass C

  • Class A - 这种方法是利用了比特币每个地址本质上都是大小为 20 bytes 的二进制字符串。先将数据分割成 20 bytes 为一组的数据块,然后使用 Base58 编码作为目标地址发送即可。当然这些被发送的 UTXO 将永远丢失。使用这项技术的还有 cryptograffiti

  • Class B - 这种方法利用了比特币的 multisig ,即多签特性。发送一笔 1 of n的多重签名交易(即,n 个地址中,任何一个地址签名即可花费这笔 UTXO)。当前版本的 Omni Layer 协议中最大支持 n = 3

  • Class C - 这种方法则利用了OP_RETURN操作码储存数据,目前来看,基本上所有的 Omni layer 层的交易都采用了这种方式。使用这项技术的还有 CoinSpark

Omni Protocol 能让用户发行自己的数字资产,其中资产编号为31 的就是被广泛使用的 USDT 。而日常中,发送 USDT 使用的交易类型是 Simple Send

下面是对 Simple Send交易的定义。

举一个经典的 USDTBTC 链上的 OP_RETURN 数据。

OP_RETURN 6f6d6e69000000000000001f00000002553fa2b8
符号 位置 含义
6f6d6e69 [10:18] omni 对应的 ASCII 编码,因为交易备注与 omni 协议有关
0000 [18:22] 交易版本
0000 [22:26] 交易类型,代表 简单发送
0000001f [26:34] 31,代表 USDT
00000002553fa2b8 [34:] USDT 转账金额,1000 00000000 最小单位 = 1000 USDT,16进制

Simple Send 是一项将 Omni Layer 层中特定的数字资产从原地址转移到目标地址的操作,注意原地址和目标地址均使用比特币的地址。

可以看到上面的交易并没有任何的余额信息,这就是说,原地址的 Omni Layer 层数字资产,由 Omni Core 自己维护一个额外的账本并校验。

换句话说,所谓的染色币只不过是借住了比特币的脚本功能,Omni 协议就是利用该功能在比特币区块中嵌入 Omni 协议的数据。通过 OP_RETURN 操作码区分。比特币网络并不会解析 Omni 协议对应的 OP_RETURN 数据段,对其来说只是‘备注信息’,由 Omni 所在的二级网络解析对应的交易信息。

假如说,用户 A 想要通过 Omni 协议,转给用户 B 一定数额的 USDT用户 A 需要给用户 BBTC ,然后,在这个转账块中,在备注上写下关于 USDT 的转账信息。所以,USDT 的转账是依附于 BTC 的,矿工费之类的也是 USDT ,因为,这笔转账的本质还是 BTC 转账。所以,Omni 协议下,用户的 BTC 地址USDT 是相同的。


业务需求


解析 BTC 链上的 USDT 转账。

事实上正确的思路应该是根据协议的规范解析,但是,相关资料太少了,我暂时没有找到,所以,这里我用的是经验逻辑,也就是,我尽可能找了大量不同类型的块数据,找出具有普适性的逻辑,但是,这个逻辑有一个致命的缺点,就是其并没有分析全部的数据。以后,我会从协议的角度来解析数据。


转账结构


这里展示几个比较经典的数据结构,其可以在 usdt 浏览器btc 浏览器中,找到。

eb6b2ecb5056114e3da28de2f6a2e281891cff1cf5db91f33a9cbefcbb21e149

就贴着一个的数据块。

这个是非常经典的转账记录。先看一下浏览器截图。

usdt

按照我目前的经验逻辑,usdt转账,每笔只有一个 from_address 和 一个 to_address

btc

这个就是标准的 btc 转账地址,里面包含的信息也就是基本 UTXO ,你可以先看我下面的博文,了解 UTXO

另外,看上图,地址解析失败,是因为该地址是 usdt 的标识地址,我自己取的。。。总之,那一个地址,在 btc 中是无用地址,而 usdt 的转账记录,就放在那个地址的备注信息中。

想要解析 usdt 的转账记录,需要解决下面的问题。

  • 解析 usdtfrom_addressto_address
  • 解析转账金额

我们从数据块中入手。

返回的数据块

{'type': 'transaction', 'hash': 'eb6b2ecb5056114e3da28de2f6a2e281891cff1cf5db91f33a9cbefcbb21e149', 'size': 694, 'virtual_size': 694, 'version': 1, 'lock_time': 0, 'block_number': 497113, 'block_hash': '000000000000000000694c4a23986f09d70cccf4f316e25254f4d1732030b17c', 'block_timestamp': 1512172332, 'is_coinbase': False, 'index': 521, 'inputs': [{'index': 0, 'spent_transaction_hash': 'f6d6a4f82190fe9d0e6606417bb7f28e9455779b9f91d8714ef7f1f568eb67b4', 'spent_output_index': 2, 'script_asm': '0 3045022100b5470538da4374b16356d05b277bb21c8ef031f469667bdc1dd322d685bf6f870220253100900c5a5e2ca3ed8c90b5c4de7402746c7b6bb8c4578c6f8dc52c9a7145[ALL] 3044022058156d06a6f6cbffc299c22b425e3b7695d331bc9c7e660b842d09ea45530d44022036fcd61dfbdfc5f844989c417f75b1f2eb4493800208896d6788589aa71cdff9[ALL] 5221030050e567a4d4e36fea2c3e7f229f181283be92653c02af8acdb305d7bf20197b2102f4aef4731a9caf7a287ddaea6968c7a03ab3b276edc66f1a26e3bf764641a0eb2102e0dd9380c00d58af4ca2e3fe5f1fcefc8de234ec711a9f4e66a9bb9448a8c33953ae', 'script_hex': '00483045022100b5470538da4374b16356d05b277bb21c8ef031f469667bdc1dd322d685bf6f870220253100900c5a5e2ca3ed8c90b5c4de7402746c7b6bb8c4578c6f8dc52c9a714501473044022058156d06a6f6cbffc299c22b425e3b7695d331bc9c7e660b842d09ea45530d44022036fcd61dfbdfc5f844989c417f75b1f2eb4493800208896d6788589aa71cdff9014c695221030050e567a4d4e36fea2c3e7f229f181283be92653c02af8acdb305d7bf20197b2102f4aef4731a9caf7a287ddaea6968c7a03ab3b276edc66f1a26e3bf764641a0eb2102e0dd9380c00d58af4ca2e3fe5f1fcefc8de234ec711a9f4e66a9bb9448a8c33953ae', 'sequence': 4294967295, 'required_signatures': 1, 'type': 'scripthash', 'addresses': ['3Mqo31DSxsBpZj7MRNJGyaWc4jArjhMeR6'], 'value': 540}, {'index': 1, 'spent_transaction_hash': 'af81bda6612ff9bf24bbce60b067916ff5e478d9721e089870371d9602e29cac', 'spent_output_index': 0, 'script_asm': '0 30440220427092195d8dcca30eb985af4a4c2241a6d2170f8b2c9defd96de8f609c6e75002203c9354118451bdf8588aa1626d846323878a07be5d4612fce215a2407453910c[ALL] 3044022030e60d45f49af0282f5810544b6575be9953b74c4f7c2610a632eb9c0b8b723e02203da4726f72881f0347d90046433958ed6d33abd42ea7017032f96488a3362bb7[ALL] 5221035c037a36e4ec9aa1aefcef02ff742bec63c098c0a7f1c9a1b822b6294afbb7102102a1ccf15bc58870240578a2922b3f0e922260f37098adbd988e15be36943816912102af3be4803e51fef309a51f0332f399026e99dae117dc2d22a71f4eacc1f3f4f153ae', 'script_hex': '004730440220427092195d8dcca30eb985af4a4c2241a6d2170f8b2c9defd96de8f609c6e75002203c9354118451bdf8588aa1626d846323878a07be5d4612fce215a2407453910c01473044022030e60d45f49af0282f5810544b6575be9953b74c4f7c2610a632eb9c0b8b723e02203da4726f72881f0347d90046433958ed6d33abd42ea7017032f96488a3362bb7014c695221035c037a36e4ec9aa1aefcef02ff742bec63c098c0a7f1c9a1b822b6294afbb7102102a1ccf15bc58870240578a2922b3f0e922260f37098adbd988e15be36943816912102af3be4803e51fef309a51f0332f399026e99dae117dc2d22a71f4eacc1f3f4f153ae', 'sequence': 4294967295, 'required_signatures': 1, 'type': 'scripthash', 'addresses': ['3GyeFJmQynJWd8DeACm4cdEnZcckAtrfcN'], 'value': 9158882}], 'outputs': [{'index': 0, 'script_asm': 'OP_HASH160 a7aedd0d1e77300fb06aa154c9a517b74c08d245 OP_EQUAL', 'script_hex': 'a914a7aedd0d1e77300fb06aa154c9a517b74c08d24587', 'required_signatures': 1, 'type': 'scripthash', 'addresses': ['3GyeFJmQynJWd8DeACm4cdEnZcckAtrfcN'], 'value': 9068519}, {'index': 1, 'script_asm': 'OP_HASH160 a7aedd0d1e77300fb06aa154c9a517b74c08d245 OP_EQUAL', 'script_hex': 'a914a7aedd0d1e77300fb06aa154c9a517b74c08d24587', 'required_signatures': 1, 'type': 'scripthash', 'addresses': ['3GyeFJmQynJWd8DeACm4cdEnZcckAtrfcN'], 'value': 2730}, {'index': 2, 'script_asm': 'OP_RETURN 6f6d6e69000000000000001f00000032d518a03b', 'script_hex': '6a146f6d6e69000000000000001f00000032d518a03b', 'required_signatures': None, 'type': 'nonstandard', 'addresses': ['nonstandard1344ece84dae066bba791ecc885453455932e1fe'], 'value': 0}], 'input_count': 2, 'output_count': 3, 'input_value': 9159422, 'output_value': 9071249, 'fee': 88173, 'item_id': 'transaction_eb6b2ecb5056114e3da28de2f6a2e281891cff1cf5db91f33a9cbefcbb21e149'}

在这里我们看一下 inputs ,去掉多余的信息后。

'inputs': [
{'index': 0, 'addresses': ['3Mqo31DSxsBpZj7MRNJGyaWc4jArjhMeR6'], 'value': 540}, 
{'index': 1, 'addresses': ['3GyeFJmQynJWd8DeACm4cdEnZcckAtrfcN'], 'value': 9158882}]

outputs 去掉多余的信息后

'outputs': [
{'index': 0, 'addresses': ['3GyeFJmQynJWd8DeACm4cdEnZcckAtrfcN'], 'value': 9068519}, 
{'index': 1, 'addresses': ['3GyeFJmQynJWd8DeACm4cdEnZcckAtrfcN'], 'value': 2730}, 
{'index': 2, 'script_asm': 'OP_RETURN 6f6d6e69000000000000001f00000032d518a03b', 'addresses': ['nonstandard1344ece84dae066bba791ecc885453455932e1fe'], 'value': 0}],

上面的规律自己找一下,我们继续看其它的转账数据。

fa1bcd8ea077414608290d9e3c39b4f33a83deaaba35409c1c54710b75d85bf4

usdt

btc

e8cb9f108f6374c9686a828c6f0dc5ab74ffb6dbd9b2460c124563be17ce9e01

usdt

btc

870ccc55f0a4106cb7993acca535e9150ba7be19bbe24c7e8a0cb9264412ff2c

这个地址自己去浏览器查看吧,第一次看的时候,差点把我惊呆了。

c248c88817318078a224d8f6b5b3af807f465f92c267b37f079af24fc194bef5

这个的转账记录也是特殊表现。

从上面那些例子,还有他们返回的数据块,我找到了下面的特点。

  • 无法从数据块中,直接获取 usdtfromto
  • 包含 usdt 的转账的那个地址,在 outputs 中的位置并不固定

解析结构


直接说我找到的结论。

  • inputs 的第一个 item 的地址,是 usdtfrom
  • outputs 中的那些地址,减去包含信息的那个地址,还有减去找零地址,剩下的 outputs-1 位置是 usdtto

代码编写


  • 先区分该笔交易中有没有 usdt 转账
  • 按照上面的逻辑进行解析

这里只是贴一下大概的思路。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def get_usdt_from_and_to(self, item):
try:
input_address = []
output_address = []
inputs = item.get("inputs")
outputs = item.get("outputs")
for input_item in inputs:
input_address_item = input_item.get("addresses")[0]
if input_address_item not in inputs:
input_address.append(input_address_item)

for output_item in outputs:
output_address_item = output_item.get("addresses")[0]
if output_address_item in input_address:
input_address.remove(output_address_item)
else:
if output_address_item.startswith("nonstandard"):
continue
output_address.append(output_address_item)
return inputs[0].get("addresses")[0], output_address[-1]
except Exception as e:
self.logger.error(f"insert db exception {e}, traceback is {traceback.format_exc()}, the item is {item}")
return "", ""

def parse_usdt(self, usdt_script_asm, item):

value = int(usdt_script_asm[34:], 16)
from_addresses, to_addresses = self.get_usdt_from_and_to(item)
token_transfer = {
...
}

transaction = {
...
}

return token_transfer, transaction

def parse_items(self, items):
data_list = defaultdict(list)
for item in items:
# block 数据不要
if item.get("type") == "block":
continue

block_number = item.get("block_number")
is_coinbase = item.get("is_coinbase")
if is_coinbase:
# 出块数据
transaction = self.parse_btc(item)
data_list[block_number].append(transaction)
else:
is_usdt = False
outputs = item.get("outputs")
usdt_script_asm = ""
for output in outputs:
script_asm = output.get("script_asm", None)
if script_asm and len(script_asm) > 34:
if script_asm[:18] == "OP_RETURN 6f6d6e69" and script_asm[26:34] == "0000001f":
usdt_script_asm = script_asm
is_usdt = True
if is_usdt:
token_transfer, transaction = self.parse_usdt(usdt_script_asm, item)
...
else:
...
return data_list

经过长达几个星期的校验,上述代码解析的数据可以和浏览器对的上。

祝好运!

请我喝杯咖啡吧~