Около месяца назад я разговорился с dadev о парсинге идентификаторов токенов из общей коллекции OpenSea. Помимо того, что он научил меня чему-то новому, он превратился в действительно ценный разговор о битах, байтах, базовых разговорах и кодировании!

Замечательная команда CyberKongz (twitter) спрятала этот драгоценный камень в своем смарт-контракте, и возник вопрос: Как нам это разобрать?!

#Solidity
function isValidKong(uint256 _id) pure internal returns(bool) {
  // making sure the ID fits the opensea format:
  // first 20 bytes are the maker address
  // next 7 bytes are the nft ID
  // last 5 bytes the value associated to the ID, here will always be equal to 1
  // There will only be 1000 kongz, we can fix boundaries and remove 5 ids that dont match kongz
  if (_id >> 96 != 0x000000000000000000000000a2548e7ad6cee01eeb19d49bedb359aea3d8ad1d)
   return false;
  if (_id & 0x000000000000000000000000000000000000000000000000000000ffffffffff != 1)
   return false;
  uint256 id = (_id & 0x0000000000000000000000000000000000000000ffffffffffffff0000000000) >> 40;
  if (id > 1005 || id == 262 || id == 197 || id == 75 || id == 34 || id == 18 || id == 0)
   return false;
  return true;
 }

Web3.js FTW (для Интернета)

Лично я большой поклонник BitShields от Corso (коллекция по общему контракту OpenSea), так что эта коллекция была естественной для тестирования. Я выбрал 3 случайных элемента, поэтому посмотрите, смогу ли я разделить их с помощью утилит Web3.js:

https://opensea.io/assets/0x495f947276749ce646f68ac8c248420045cb7b5e/17773364155682651268275142113690502555604463656341524695465728811681819131905
https://opensea.io/assets/0x495f947276749ce646f68ac8c248420045cb7b5e/17773364155682651268275142113690502555604463656341524695465728854562772615169
https://opensea.io/assets/0x495f947276749ce646f68ac8c248420045cb7b5e/17773364155682651268275142113690502555604463656341524695465728795189144715265

И если свернуть это, мы можем увидеть некоторые общие элементы в «передней части» идентификатора токена, но, черт возьми, эта штука длинная! 😉

17773364155682651268275142113690502555604463656341524695465728811681819131905 17773364155682651268275142113690502555604463656341524695465728854562772615169 17773364155682651268275142113690502555604463656341524695465728795189144715265

Поскольку мы работаем в сфере Web3, давайте посмотрим, как это выглядит в шестнадцатеричном формате… и вау! Сразу бросается в глаза разделение переменных:

#JavaScript
>Web3.utils.toHex('17773364155682651268275142113690502555604463656341524695465728811681819131905');
<'0x274b5e1c7257f1092402c2d2ffb987010a48496d000000000002a30000000001'
//  |------------- MAKER ADDRESS ----------|--- NFT ID --|-- AID --|
>Web3.utils.toHex( '17773364155682651268275142113690502555604463656341524695465728854562772615169' );
<'0x274b5e1c7257f1092402c2d2ffb987010a48496d000000000002ca0000000001'
//  |------------- MAKER ADDRESS ----------|--- NFT ID --|-- AID --|

>Web3.utils.toHex('17773364155682651268275142113690502555604463656341524695465728795189144715265');
<'0x274b5e1c7257f1092402c2d2ffb987010a48496d000000000002940000000001'
//  |------------- MAKER ADDRESS ----------|--- NFT ID --|-- AID --|

Реконструкция

Теперь мы знаем, как их разъединить… Можем ли мы собрать их вместе? В частности, если в нашей коллекции 620 элементов, как мы можем проверить элемент 211? Может быть, нам нужен напольный бот? Благодаря некоторому парному программированию с Mumbu, у нас есть эта функция JS:

#JavaScript
function getOpenSeaTokenID( collectionAddress, tokenId, assocId ){
  //convert back to hex, and strip '0x' prefix
  tokenId = Web3.utils.toHex( tokenId ).substring( 2 );
  // need 14 hex digits for 7 bytes
  if( tokenId.length < 14 ){
    tokenId = tokenId.padStart( 14, '0' );
  }

  //convert back to hex, and strip '0x' prefix
  assocId = Web3.utils.toHex( assocId ).substring( 2 );
  // need 10 hex digits for 5 bytes
  if( assocId.length < 14 ){
    assocId = assocId.padStart( 10, '0' );
  }
  const fullTokenId = collectionAddress + tokenId + assocId;
  return Web3.utils.hexToNumberString( fullTokenId );
};

А результаты… да, d3 = 211 в гексе!

#JavaScript
>getOpenSeaTokenID( collectionAddress, 211, 1 );
<'17773364155682651268275142113690502555604463656341524695465728301508423843841'
>Web3.utils.toHex('17773364155682651268275142113690502555604463656341524695465728301508423843841');
<'0x274b5e1c7257f1092402c2d2ffb987010a48496d000000000000d30000000001'

Другие реализации

В своем смарт-контракте CyberKongz использует маскирование и сдвиг битов для извлечения нужных им байтов. Это немного сбивает с толку, потому что они передают uint (12345), а затем используют hex literal (0x00ff) для маскировки; у нас есть некоторые умственное жонглирование, чтобы сделать.

uint является десятичным числом (основание 10), и мы видели это с идентификаторами токенов ОС выше. Но десятичное число не выравнивается по границам байтов. От 0 до 255 — это 1 байт, от 256 до 65 535 — это 2-й байт… это не способ работы с данными. 😞 К счастью, каждые 2 шестнадцатеричных цифры составляют ровно 1 байт, и именно поэтому он выявил закономерность. Поэтому примите на веру, что целые числа имеют одинаковые двоичные данные, просто это плохой формат для людей, которым нужно видеть данные.

Учитывая, что они равны, мы можем увидеть, как работает маскирование:

#Solidity
uint _id =  17773364155682651268275142113690502555604463656341524695465728301508423843841;
//0x274b5e1c7257f1092402c2d2ffb987010a48496d000000000000d30000000001
uint isolatedTokenId = _id & 
0x0000000000000000000000000000000000000000ffffffffffffff0000000000;
//see how the bytes line up?
// 00 means "ignore this" and ff means "keep this"
uint isolatedTokenId = 0x274b5e1c7257f1092402c2d2ffb987010a48496d000000000000d30000000001 &
0x0000000000000000000000000000000000000000ffffffffffffff0000000000;

Затем мы используем битовый сдвиг, чтобы удалить 10 справа! (10 шестнадцатеричных = 5 байтов = 40 бит):

#Solidity
// bit shift 40 bits "off" of the end of the number
isolatedTokenId = isolatedTokenId >> 40;

Для solidity, python и других языков программирования нарезка может быть еще проще!

#Solidity
bytes32 data = 0x274b5e1c7257f1092402c2d2ffb987010a48496d000000000000d30000000001;
|------------- MAKER ADDRESS ----------|--- NFT ID --|-- AID --|
//1. first 20 bytes are the maker address
bytes owner = data[0:20];
//2. next 7 bytes are the nft ID
bytes nftID = data[20:27];
//3. last 5 bytes the value associated to the ID
bytes assocID = data[27:32];
#Python
# load the integer:
data = '{:x}'.format( 17773364155682651268275142113690502555604463656341524695465728301508423843841 )
# load a(r)aw (b)ytes literal:
data = rb'0x274b5e1c7257f1092402c2d2ffb987010a48496d000000000000d30000000001'
|------------- MAKER ADDRESS ----------|--- NFT ID --|-- AID --|
//1. first 40 digits are the maker address
owner = data[0:40];
//2. next 14 digits are the nft ID
nftID = data[40:54]
//3. last 10 digits the value associated to the ID
assocID = data[54:64];

Удачного взлома!
- Squeebo