Wie speichert man IPFS-Hash mit bytes32?

Die folgenden Fragen/Antworten ( Welchen Datentyp sollte ich für einen IPFS-Adress-Hash verwenden? ) empfehlen uns, bytesihn zum Speichern von IPFS-Hash zu verwenden.

Ich habe das folgende Beispiel ( https://github.com/AdrianClv/ethereum-ipfs/blob/master/NotSoSimpleStorage.sol ) verwendet, in dem stringIPFS-Hash gespeichert wird, der etwa 110.000 Gaspreis kostet, was ziemlich teuer erscheint.

[F] Ist die Nutzung bytesstatt von stringzum Speichern von IPFS-Hash günstiger? Ich beobachte, dass das Speichern bytesstatt der stringKosten sehr nahe bei string(110.000 Gas) liegt. Da die Speicherung beider Datentypen teuer erscheint, sollte ich Ereignisse verwenden, um sie zu speichern?

Gibt es ein Beispiel/Tutorial zum Speichern von IPFS-Hash mit bytes?

Würde das funktionieren:

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

Antworten (7)

Ihr Beispiel zeigt das Speichern einer IPFS-Identität mit ihrer alphanumerischen Codierung ( Qm...), die dieselbe Base58 - Codierung ist, die Bitcoin verwendet. Was es jedoch im Kern darstellt, ist eine Zahl (der Hash). Das Speichern der Kennung im Base58-Format muss eine Zeichenfolge sein, da sie Buchstaben enthält (und was tatsächlich gespeichert wird, ist der ASCII-Code für jedes alphanumerische Zeichen in der Kennung). QmWmyoMoctfbAaiEs2G46gpeUmhqFRDW6KWo64y5r581VzDas bedeutet, dass Sie aus Ihrem Beispiel 46 Bytes zum Speichern benötigen .

Dieser Bezeichner kann jedoch auch hexadezimal als 12207D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89, das nur 34 Byte lang ist, ausgedrückt werden (es werden 68 Zeichen benötigt, um es hexadezimal auszugeben, da alle zwei Zeichen in Hex ein Datenbyte sind).

Aber beide sind größer als 32 Bytes, was das maximale Byte-Array mit fester Größe ist, also müssen sie ein Byte-Array mit dynamischer Größe zum Speichern verwenden ( bytesoder string, die beide teuer sind, wie Sie bemerkt haben ).

ABER , dieser IPFS-Hash besteht eigentlich aus zwei verketteten Teilen. Es handelt sich um eine Multihash - Kennung, sodass die ersten beiden Bytes die verwendete Hash-Funktion und die Größe angeben. 0x12ist sha2, 0x20ist 256 Bit lang. Derzeit ist dies das einzige Format, das IPFS verwendet, sodass Sie einfach die ersten beiden Bytes abschneiden könnten, wodurch Sie einen 32-Byte-Wert erhalten, der klein genug ist, um in ein bytes32Byte-Array mit fester Größe zu passen, und Sie dort etwas Platz sparen (und beim Abrufen kann entweder Ihr Vertrag wieder 0x1220vorne angehängt werden, oder Ihre Kunden müssen schlau genug sein, dies zu tun, nachdem sie den Wert abgerufen haben).

Wenn Sie jedoch sicherstellen möchten, dass Ihr Code zukunftssicher ist, möchten Sie wahrscheinlich den Hash-Funktionscode und die Größe speichern, die Sie mit dem Hash als Struktur kombinieren könnten:

struct Multihash {
  bytes32 hash
  uint8 hash_function
  uint8 size
}

Das funktioniert mit jedem Multihash-Format, solange sizees kleiner oder gleich 32 ist (je größer und die tatsächliche Nutzlast passt nicht in die hashEigenschaft). Diese Struktur benötigt zum Speichern zwei Speicherslots (zwei 32-Byte-Blöcke), da die beiden uint8Teile in einen Slot gesteckt werden können. Sie können dieser Struktur auch bis zu 30 Byte zusätzliche Daten hinzufügen, ohne dass weitere Speicherkosten anfallen.

Entschuldigung, ich habe nicht verstanden, wie Sie erhalten haben: 12207D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89. Hier ( codebeautify.org/string-hex-converter ) wenn ich QmWmyoMoctfbAaiEs2G46gpeUmhqFRDW6KWo64y5r581Vzin hex konvertiere, bekomme ich eine viel größere Zeichenfolge als 516d576d796f4d6f63746662416169457332473436677065556d687146524457364b576f3634793572353831567a. @MidnightLightning
Ich habe auch nicht verstanden, was Sie meinten mit: uint8 function, warum brauchen wir es und wo könnten wir es verwenden? Vielen Dank. @MidnightLightning
Die von Ihnen durchgeführte Konvertierung (String-Hex-Converter) nimmt die Zeichenfolge "QmWmy..." und zeigt Ihnen, wie die Zeichenfolge gespeichert werden würde (der ASCII-Wert für "Q" ist 0x51, "m" ist 0x6dusw.). Was ich getan habe, ist ein Tool zu verwenden, das Base58-Decodierung durchführt und das verwendet, um die tatsächliche Zahl zu erhalten, die durch diese Base58-Zeichenfolge dargestellt wird.
uint8 functionspeichert einen vorzeichenlosen ganzzahligen Wert als Namen "Funktion" in diesem Strukturobjekt. "Funktion" ist wahrscheinlich nicht der beste Name dafür, da es sich um ein spezielles Wort in Solidity und anderen Programmiersprachen handelt; Ich habe es ausgewählt, weil sie im Multihash-Standard diese Variable so nennen; die Variable, die Ihnen mitteilt, welche Hash-Funktion für diesen bestimmten Datensatz verwendet wurde (z . B. 0x12für "sha2"). Ich werde meine Antwort aktualisieren, um das spezielle Wort "Funktion" nicht zu verwenden, um es klarer zu machen.
Um es klar zu sagen (ich habe mich beim Konvertierungsteil etwas verirrt), das Base58-Decodierungstool ( lenschulwitz.com/base58 ) dekodiert "Base58-codierter (Qm..) Wert" in das Base58-decodierte Format. @MidnightLightning
Vielen Dank dafür. Du hast mir hier wirklich viel Zeit und Mühe erspart. Das ist genau das, was ich brauchte.
Vielen Dank, dass Sie mir eine Menge Zeit gespart haben. Ich habe Ihren Ansatz in einem End-to-End-Beispiel zusammengefasst und hoffe, dass es anderen Leuten etwas Zeit spart: github.com/saurfang/ipfs-multihash-on-solidity
Wenn Sie insgesamt 64 Bytes speichern, warum teilen Sie die Zeichenfolge nicht einfach in zwei Hälften und speichern die ersten 32 Bytes in einer Zeichenfolge32 und die restlichen Bytes in einer zweiten Zeichenfolge32. Zum Wiederherstellen einfach verketten. Viel einfacher, gleiche Gaskosten, billiger zu rekonstituieren und technisch zukunftssicherer, wenn sich die Länge eines IPFS-Hashes ändert.
Ist es möglich, Multihash von Solidity wieder in IPFS-Hash umzuwandeln?

Hier sind einige js-Funktionen zum Entfernen und erneuten Hinzufügen der ersten beiden Bytes, die die Hash-Funktion und -Größe enthalten, die für web3 geeignet sind.

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
}

Hier ist die im Kontext verwendete Funktion, die einen Listingmit Truffle bereitgestellten Vertrag aufruft.

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)
      })
    })
  })

Entnommen aus meiner Arbeit an der Origin Demo Dapp hier: https://github.com/OriginProtocol/origin-js/blob/1cfc84d4693974bbf18e345e6c0def843321130c/src/services/contract-service.js#L102-L128

Funktioniert super!!
Funktioniert nicht für Basis-URI in ERC 721. Wenn Sie ein Array von Bytes32 als URIS übergeben (z. B. für Basis-URI), werden Sie viele Probleme mit ASCII-Zeichen haben, nicht mit alphanumerischen Zeichen. Versuchen Sie denselben Hash: "0x017dfd85d4f6cb4dcd715a88101f7b1f06cd1e009b2327a0809d01eb9c91f231"

Ich habe eine ähnliche Situation mit dieser util-Funktion in web3.py behandelt:

import base58

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

Sie benötigen das base58-Modul. Das Konzept ist dasselbe wie die akzeptierte Antwort.

Diese Antwort ist nur die Implementierung der oben akzeptierten AntwortPython von @ MidnightLightning . Ich habe Web3.py verwendet .

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

Hier ist ein vollständigeres Beispiel mit der js-multihash- Bibliothek:

MeinVertrag.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)
    })
  })

Auslesen der Daten

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()
    })

Alle Antworten hier sind unnötig kompliziert. Wenn Sie am Ende sowieso 64 Bytes speichern wollen, warum speichern Sie nicht einfach die ersten 32 Bytes des IPFS-Hashs in einem string32und den Rest des Hashs in einem anderen string32? Kein Umrüsten beim Lagern nötig, daher weniger Gas zu verarbeiten. Einfach verketten, um den Hash wiederherzustellen, daher billiger zu verarbeiten. Gleiche Gaskosten zum Speichern (64 Byte). Und zukunftssicherer . Anders als bei der ausgewählten Lösung wächst (oder schrumpft) die Länge der IPFS-Hash-Änderung, ohne dass eine Änderung am Code erforderlich ist.

Die Speicherkosten sind in Ethereum sehr hoch, denn wenn Sie in einem einzigen speichern können bytes32, warum sollten Sie es dann in 2 speichern bytes32? In Ihrem Vertrag müssen Sie alles für 2 Slots von konvertieren bytes32, wenn Sie diese beiden in einen schieben, listwird es zusätzliche geben, lengthso dass unnötiger Speicher verbraucht wird. "Same gas cost to store (64 bytes)" => das ist falsch, das Speichern 32 bytesist billiger als das Speichern von 64 Bytes in Fällen von tight variable packingsiehe fravoll.github.io/solidity-patterns/tight_variable_packing.html . Auch wenn Sie es in einem Steckplatz aufbewahren, können Sie es als Eingang keyverwendenmap
"einfach verketten" ist in Solidity eigentlich nicht so einfach (es gibt keine native Unterstützung dafür, was dazu führt, dass einige komplexe Manipulationen vorgenommen werden müssen, die eine Menge Gas verbrauchen). Ihr Vertrag könnte die beiden Hälften als separate Zeichenfolgen exportieren, aber das ist für den Endbenutzer weniger intuitiv (er muss nachbearbeitet werden).
Ich schlage nicht vor, dass die Rekonstitution in der Kette stattfindet, sodass die Gaskosten für die Rekonstitution null wären. Es gibt keinen Nutzen für ein Hash-IPFS „on-chain“ – Smart Contracts können das Dokument, auf das der Hash verweist, nicht lesen. Ich schlage vor, Off-Chain zu verketten und dass der Vertrag den Hash in zwei Teilen offenlegen sollte. Leicht verwirrend für den Benutzer, aber keine Kosten für den Abruf. Bei der Eingabe erfordern alle oben genannten Lösungen das Aufteilen der Zeichenfolge (zusätzlich erfordern die anderen Lösungen eine Konvertierung von Base58), sodass die Kosten ebenfalls geringer sind. Teilen Sie den Hash vor dem Einfügen, und Sie können diese Kosten sogar eliminieren.
Meine Strukturlösung geht auch davon aus, dass das Teilen/Zusammenfügen außerhalb der Kette erfolgt: Die Eingabe der drei Teile (nicht als Zeichenfolgen) und das Speichern sind die gleichen Kosten wie die Eingabe von zwei Zeichenfolgen und das Speichern. Es hängt also wirklich von den Anforderungen der App ab. Die meisten Off-Chain-IPFS-Bibliotheken hätten Funktionen zum Entpacken einer String-ID in ihre drei numerischen/Byte-Teile, aber das Teilen von Strings wäre eine benutzerdefinierte Aktion, die nicht Teil des IPFS-Standards ist
Ich werde das Gespräch nach diesem Kommentar nicht fortsetzen, aber zwei Dinge. Das Teilen einer Zeichenfolge ist Teil jeder Off-Chain-Programmiersprache, die ich je gesehen habe. Ich bin mir also nicht sicher, warum sie Teil des IPFS-Standards sein sollte, und ich möchte darauf hinweisen, dass Ihre Lösung weniger zukunftssicher ist als die zugrunde liegende Ein 32-Byte-Hash kann nicht über seine 32-Byte-Grenze hinauswachsen, ohne dass er ohnehin aufgeteilt werden muss, während eine rein stringbasierte Lösung dieses Problem nicht hat.
Die Lösung von @MidnightLightning Thomas ist die richtige für 2021. Wenn Sie diesen Hash "0x017dfd85d4f6cb4dcd715a88101f7b1f06cd1e009b2327a0809d01eb9c91f231" beispielsweise in Ihren Basis-URI einfügen, werden Sie viele Probleme mit ASCII-Zeichen und nicht mit alphanumerischen Zeichen haben. Ich weiß nicht, ob sich baseURI seit letztem Jahr geändert hat, aber denken Sie daran: baseURI gibt STRING zurück, nicht bytes32! Versuchen Sie, dieses Problem zu lösen, indem Sie bytes32 in eine Zeichenfolge decodieren müssen. Ich habe 3 Tage damit verbracht, es zu versuchen. Also, wenn du es bekommst, lass es mich wissen.

Wenn Sie keine Zeit haben, den Algorithmus zu recherchieren, empfehle ich die Verwendung des content-hashKnotenmoduls. Sie können viele Formate von Zeichenfolgen einfach konvertieren, z. B.: CID v0, CID v1, base58, base32.

Beispiel:

 const ipfs = 'QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG'

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

siehe: https://github.com/ensdomains/content-hash