Как хранить хэш IPFS с помощью bytes32?

После вопросов и ответов ( какой тип данных следует использовать для хэша IPFS-адреса? ) рекомендуется использовать bytesего для хранения хэша IPFS.

Я использовал следующий пример ( https://github.com/AdrianClv/ethereum-ipfs/blob/master/NotSoSimpleStorage.sol ), который используется stringдля хранения хэша IPFS, который стоит около 110 000 газа, что кажется довольно дорогим.

[В] Обходится ли использование bytesвместо stringтого, чтобы хранить хеш IPFS дешевле? Я наблюдаю, что хранение bytesвместо stringстоит очень близко к string(110 000 газа). Поскольку хранение обоих типов данных кажется дорогим, должен ли я использовать события для их хранения?

Есть ли какой-нибудь пример/учебник, связанный с хранением хеша IPFS с использованием bytes?

Будет ли это работать:

myContract.insertHash("QmWmyoMoctfbAaiEs2G46gpeUmhqFRDW6KWo64y5r581Vz");

contract Example_bytes {
    bytes[] list;
    function insertHash(bytes ipfsHash) {
       list.push(ipfsHash); //costs around 110,000 gas. 
    }
}

contract Example_string {
    struct hashes{
         string hash;
    }

    hashes[] list;
    function insertHash(string ipfsHash) {
       list.push(hashes{hash: ipfsHash); //costs around 110,000 gas. 
    }
}

Ответы (7)

В вашем примере показано сохранение идентификатора IPFS с использованием его буквенно-цифровой кодировки ( Qm...), которая является той же кодировкой Base58 , которую использует биткойн. Однако по своей сути он представляет собой число (хэш). Сохранение идентификатора в формате Base58 должно быть строкой, поскольку она включает буквы (и фактически сохраняется код ASCII для каждого буквенно-цифрового символа в идентификаторе). Это означает, что вам нужно 46 байт для хранения QmWmyoMoctfbAaiEs2G46gpeUmhqFRDW6KWo64y5r581Vzиз вашего примера.

Однако этот идентификатор также может быть выражен в шестнадцатеричном виде как 12207D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89, длина которого составляет всего 34 байта (для записи в шестнадцатеричном формате требуется 68 символов, поскольку каждые два символа в шестнадцатеричном формате представляют собой байт данных).

Но оба они больше 32 байтов, что является максимальным массивом байтов фиксированного размера, поэтому им потребуется использовать массив байтов динамического размера для хранения ( bytesили stringоба из которых дороги, как вы заметили ).

НО , этот хэш IPFS на самом деле представляет собой две соединенные части. Это мультихэш- идентификатор, поэтому первые два байта указывают используемую хеш-функцию и размер. 0x12это sha2, 0x20имеет длину 256 бит. В настоящее время это единственный формат, который использует IPFS, поэтому вы можете просто отрезать первые два байта, что оставит вам 32-байтовое значение, достаточно маленькое, чтобы поместиться в bytes32массив байтов фиксированного размера, и вы сэкономите там немного места . (и при извлечении либо ваш контракт может быть повторно присоединен 0x1220к нему, либо ваши клиенты должны быть достаточно умными, чтобы сделать это после извлечения значения).

Однако, если вы хотите убедиться, что ваш код рассчитан на будущее, вы, вероятно, захотите сохранить код и размер хэш-функции, которые вы можете объединить с хэшем в виде структуры:

struct Multihash {
  bytes32 hash
  uint8 hash_function
  uint8 size
}

Это будет работать с любым мультихеш-форматом, если sizeон меньше или равен 32 (больше и фактическая полезная нагрузка не будет соответствовать hashсвойству). Эта структура займет два слота хранения (два фрагмента по 32 байта) для хранения, так как две uint8части могут быть помещены в один слот. Вы также можете добавить до 30 байтов дополнительных данных в эту структуру без дополнительных затрат на хранение.

Извините, я не понял, как вы получили: 12207D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89. Здесь ( codebeautify.org/string-hex-converter ), когда я конвертирую QmWmyoMoctfbAaiEs2G46gpeUmhqFRDW6KWo64y5r581Vzв шестнадцатеричный формат, я получаю гораздо большую строку, чем 516d576d796f4d6f63746662416169457332473436677065556d687146524457364b576f3634793572353831567a. @MidnightLightning
Также я не понял, что вы имели в виду: uint8 functionзачем нам это нужно и где мы можем это использовать? Спасибо. @MidnightLightning
Преобразование, которое вы сделали (string-hex-converter), берет строку "QmWmy..." и показывает, как будет храниться строка (значение ASCII для "Q" равно 0x51, "m" равно и т. д 0x6d.). Что я сделал, так это использовал инструмент, который выполняет декодирование Base58, и использовал его, чтобы получить фактическое число, представленное этой строкой Base58.
uint8 functionхранит целочисленное значение без знака в качестве имени «функция» в этом объекте структуры. «Функция», вероятно, не лучшее название для этого, поскольку это специальное слово в Solidity и других языках программирования; Я выбрал его, потому что в стандарте мультихеширования именно так называется эта переменная; переменная, которая сообщает вам, какая функция хеширования использовалась для этой конкретной записи (например 0x12, для "sha2"). Я обновлю свой ответ, чтобы не использовать это специальное слово «функция», чтобы быть более понятным.
Чтобы было ясно (я немного запутался в части преобразования), инструмент декодирования Base58 ( Lenschulwitz.com/base58 ) декодирует «значение в кодировке Base58 (Qm..)» в декодированный формат Base58. @MidnightLightning
Спасибо тебе большое за это. Вы действительно сэкономили мне много времени и усилий здесь. Это именно то, что мне нужно.
Спасибо, что сэкономили мне кучу времени. Я привожу ваш подход в сквозном примере и надеюсь, что он сэкономит время другим людям: github.com/saurfang/ipfs-multihash-on-solidity .
Если вы храните всего 64 байта, почему бы просто не разделить строку пополам и сохранить первые 32 байта в одной строке32, а оставшиеся байты во второй строке32. Чтобы воссоздать, просто конкатенировать. Гораздо проще, те же затраты на газ, дешевле восстановить и технически более перспективно, если длина хэша IPFS изменится.
Можно ли преобразовать мультихэш обратно в хеш IPFS с помощью Solidity?

Вот несколько js-функций для удаления и повторного добавления первых двух байтов, содержащих хеш-функцию и размер, подходящих для web3.

import bs58 from 'bs58'

// Return bytes32 hex string from base58 encoded ipfs hash,
// stripping leading 2 bytes from 34 byte IPFS hash
// Assume IPFS defaults: function:0x12=sha2, size:0x20=256 bits
// E.g. "QmNSUYVKDSvPUnRLKmuxk9diJ6yS96r1TrAXzjTiBcCLAL" -->
// "0x017dfd85d4f6cb4dcd715a88101f7b1f06cd1e009b2327a0809d01eb9c91f231"

getBytes32FromIpfsHash(ipfsListing) {
  return "0x"+bs58.decode(ipfsListing).slice(2).toString('hex')
}

// Return base58 encoded ipfs hash from bytes32 hex string,
// E.g. "0x017dfd85d4f6cb4dcd715a88101f7b1f06cd1e009b2327a0809d01eb9c91f231"
// --> "QmNSUYVKDSvPUnRLKmuxk9diJ6yS96r1TrAXzjTiBcCLAL"

getIpfsHashFromBytes32(bytes32Hex) {
  // Add our default ipfs values for first 2 bytes:
  // function:0x12=sha2, size:0x20=256 bits
  // and cut off leading "0x"
  const hashHex = "1220" + bytes32Hex.slice(2)
  const hashBytes = Buffer.from(hashHex, 'hex');
  const hashStr = bs58.encode(hashBytes)
  return hashStr
}

Вот функция, используемая в контексте, вызывающая Listingконтракт, развернутый с помощью Truffle.

submitListing(ipfsListing, ethPrice, units) {
  return new Promise((resolve, reject) => {
    this.listingContract.setProvider(window.web3.currentProvider)
    window.web3.eth.getAccounts((error, accounts) => {
      this.listingContract.deployed().then((instance) => {
        let weiToGive = window.web3.toWei(ethPrice, 'ether')
        return instance.create(
          this.getBytes32FromIpfsHash(ipfsListing), /*** IPFS here ***/
          weiToGive,
          units,
          {from: accounts[0]})
      }).then((result) => {
        resolve(result)
      }).catch((error) => {
        console.error("Error submitting to the Ethereum blockchain: " + error)
        reject(error)
      })
    })
  })

Взято из моей работы над демо-приложением Origin здесь: https://github.com/OriginProtocol/origin-js/blob/1cfc84d4693974bbf18e345e6c0def843321130c/src/services/contract-service.js#L102-L128

Работает удовольствие!
Не работает для базового URI в ERC 721. Если вы передаете массив байтов32 как uris (например, для baseURI), у вас будет много проблем с ascii, а не с буквенно-цифровыми символами. Попробуйте этот же хеш: "0x017dfd85d4f6cb4dcd715a88101f7b1f06cd1e009b2327a0809d01eb9c91f231"

Я обработал аналогичную ситуацию с этой функцией util в web3.py:

import base58

def convertIpfsBytes32(hash_string):           
  bytes_array = base58.b58decode(hash_string) 
  return bytes_array[2:]

Вам нужен модуль base58. Концепция такая же, как и принятый ответ.

Этот ответ является просто Pythonреализацией принятого ответа @MidnightLightning выше . Я использовал Web3.py.

from web3.auto import w3

def _ipfs_to_bytes32(hash_str: str):
    """Ipfs hash is converted into bytes32 format."""
    bytes_array = base58.b58decode(hash_str)
    b = bytes_array[2:]
    return binascii.hexlify(b).decode("utf-8")

def ipfs_to_bytes32(ipfs_hash: str) -> str:
    """bytes32 is converted back into Ipfs hash format."""
    ipfs_hash_bytes32 = _ipfs_to_bytes32(ipfs_hash)
    return w3.toBytes(hexstr=ipfs_hash_bytes32)

def bytes32_to_ipfs(bytes_array):
    """Convert bytes_array into IPFS hash format."""
    merge = Qm + bytes_array
    return base58.b58encode(merge).decode("utf-8")

if __name__ == "__main__":
    ipfs_hash = "QmWmyoMoctfbAaiEs2G46gpeUmhqFRDW6KWo64y5r581Vd"
    ipfs_bytes32 = ipfs_to_bytes32(ipfs_hash)
    _ipfs_hash = bytes32_to_ipfs(ipfs_bytes32)
    assert ipfs_hash == _ipfs_hash  # They should be equal to each other

Вот более полный пример с использованием библиотеки js-multihash :

MyContract.sol

pragma solidity ^0.4.24;

contract MyContract {

  event AddFile(address indexed owner, bytes32 digest, bytes2 hashFunction, uint8 size, bytes4 storageEngine);

  function addFile(bytes32 _digest, bytes2 _hashFunction, uint8 _size, bytes4 _storageEnginge) public {
    emit AddFile(msg.sender, _digest, _hashFunction, _size, _storageEngine);
  }
}

Javascript

import Web3 from 'web3'
import multihashes from 'multihashes'
import ipfsAPI from 'ipfs-api'

var web3 = window.web3
web3 = new Web3(web3.currentProvider)

// Utility functions:
const utils = {
  ipfs2multihash (hash) {
    let mh = multihashes.fromB58String(Buffer.from(hash))
    return {
      hashFunction: '0x' + mh.slice(0, 2).toString('hex'),
      digest: '0x' + mh.slice(2).toString('hex'),
      size: mh.length - 2
    }
  },

  multihash2hash (hashFunction, digest, size, storageEngine) {
    storageEngine = web3.toAscii(storageEngine)

    if (storageEngine === 'ipfs') {
      hashFunction = hashFunction.substr(2)
      digest = digest.substr(2)
      return {
        hash: multihashes.toB58String(multihashes.fromHexString(hashFunction + digest)),
        engine: storageEngine
      }
    }

    throw new Error('Unknown storage engine:', storageEngine)
  }
}

// ... code to instantiate contract
// ... code to get the file buffer

ipfs.add(buffer)
  .then((response) => {
    console.log('ipfs hash:', response[0].hash)

    // Prepare data
    let mh = utils.ipfs2multihash(response[0].hash)
    let storageEnginge = web3.fromAscii('ipfs')

    // Call contract
    myContractInstance.addFile.sendTransaction(mh.digest, mh.hashFunction, mh.size, storageEnginge, {from: myAccount, gas: 1000000}, (error, result) => {
      if (error) throw error
      console.log(result)
    })
  })

Чтение данных

let fileFilter = myContract.AddFile({
    owner: myAccount
  }, {
       fromBlock: 0,
       toBlock: 'latest'
     }).watch((error, log) => {
      if (error) reject(error)

      console.log('file log:', log)

      let hash = utils.multihash2hash(log.args.hashFunction, log.args.digest, log.args.size, log.args.storageEngine)
      console.log('Hash:', hash)

      fileFilter.stopWatching()
    })

Все ответы здесь излишне сложны. Если, в конце концов, вы все равно собираетесь хранить 64 байта, почему бы просто не сохранить первые 32 байта хэша IPFS в одном string32, а оставшуюся часть хэша в другом string32? Нет необходимости конвертировать при хранении, поэтому требуется перерабатывать меньше газа. Просто объедините, чтобы восстановить хэш, поэтому его обработка дешевле. Та же стоимость газа для хранения (64 байта). И, больше будущего доказательства. В отличие от выбранного решения, длина изменения хэша IPFS увеличивается (или уменьшается) без необходимости изменения кода.

Стоимость памяти в Эфириуме очень высока, так как если вы можете хранить ее в одном bytes32, зачем хранить ее в двух bytes32? В вашем контракте вы должны преобразовать все для 2 слотов bytes32, если вы вставите эти два в , listбудет дополнительная lengthпамять, поэтому будет потребляться ненужная память. «Та же стоимость газа для хранения (64 байта)» => это неправильно, хранение 32 bytesдешевле, чем хранение 64 байтов в случаях, tight variable packingпожалуйста, см . fravoll.github.io/solidity-patterns/tight_variable_packing.html . также, когда вы храните его в одном слоте, вы можете использовать его как keyвходнойmap
«просто объединить» на самом деле не так просто в Solidity (для этого нет встроенной поддержки, что приводит к необходимости выполнять некоторые сложные манипуляции, на выполнение которых уходит куча газа). Ваш контракт может экспортировать две половины как отдельные строки, но тогда это менее интуитивно понятно для конечного пользователя (им необходимо выполнить постобработку).
Я не предполагаю, что восстановление происходит в сети, поэтому затраты на газ для восстановления будут равны нулю. Нет смысла использовать хэш IPFS «в цепочке» — смарт-контракты не могут прочитать документ, на который указывает хэш. Я предлагаю конкатенировать вне сети и чтобы контракт раскрывал хеш в двух частях. Слегка сбивает с толку пользователя, но не требует затрат на поиск. На входе все вышеперечисленные решения требуют разделения строки (плюс другие решения требуют преобразования из Base58), поэтому стоимость также ниже. Разделите хеш перед вставкой, и вы даже можете исключить эту стоимость.
Мое структурное решение также предполагает, что разделение/объединение происходит вне цепочки: ввод трех частей (не в виде строк) и сохранение стоят столько же, сколько ввод двух строк и сохранение. Таким образом, действительно зависит от потребностей приложения. Большинство автономных библиотек IPFS будут иметь функции для распаковки идентификатора строки в его три числовые/байтовые части, но разделение строк будет настраиваемым действием, не являющимся частью стандарта IPFS.
Я не буду продолжать разговор после этого комментария, но есть две вещи. Разделение строки является частью каждого языка программирования вне сети, который я когда-либо видел, поэтому не уверен, почему это должно быть частью стандарта IPFS, плюс я хочу отметить, что ваше решение менее перспективно, поскольку лежащее в основе 32-байтовый хэш не может выйти за пределы 32-байтовой границы, не требуя при этом его разбиения, в то время как чисто строковое решение не имеет этой проблемы.
Решение @MidnightLightning Томаса является правильным для 2021 года. Если вы вставите этот хэш «0x017dfd85d4f6cb4dcd715a88101f7b1f06cd1e009b2327a0809d01eb9c91f231», например, в ваш базовый URI, у вас будет много проблем с символами ascii, а не с буквенно-цифровыми. Я не знаю, изменился ли baseURI с прошлого года, но помните: baseURI возвращает STRING, а не bytes32! Попробуйте решить эту проблему, декодируя bytes32 в строку. Я потратил 3 дня, пытаясь это сделать. Так что, если вы получите его, дайте мне знать.

Если у вас нет времени изучать алгоритм, я предлагаю использовать content-hashмодуль узла. Вы можете легко преобразовать многие форматы строк, такие как: CID v0, CID v1, base58, base32.

пример:

 const ipfs = 'QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG'

 const cidV1 = contentHash.helpers.cidV0ToV1Base32(ipfs)
 // 'bafybeibj6lixxzqtsb45ysdjnupvqkufgdvzqbnvmhw2kf7cfkesy7r7d4'

обратитесь к: https://github.com/ensdomains/content-hash