Implementierung der Ethereum-Speicherung auf Datenbankebene – wie wird sie gespeichert?

Ich habe das gelbe Papier viele Male sowie verschiedene Artikel gelesen, ich denke, wenn ich die Antwort hier nicht finden kann, werde ich in den Code schauen.

Mein Verständnis ist, dass jedes Vertragskonto einen Speicherstamm enthält. und Sie können die Wurzel aus der Level-Datenbank abrufen.

Aber was ist der Wert in der leveldb?

Hier sind einige meiner Fragen:

  1. LevelDB ist ein Schlüsselwert. Wenn wir also den Speicherstamm verwenden können, um einen Vertrag zu speichern, was ist dann der Wert?

  2. Ich werde eine wilde Vermutung annehmen, dass der Wert der Patrica-Baum ist?

  3. Wenn 2 wahr ist, ist es dann nicht jedes Mal extrem ineffizient, wenn wir den Vertrag ausführen wollen – wir müssen die Speicherung des gesamten Vertrages übernehmen …? Wenn ein Vertrag viel Speicherplatz hat, ist dies möglicherweise extrem langsam?

  4. Letzte Frage – Mein Verständnis, dass alle ERC20-Token-Guthabeninformationen im Speicher gespeichert sind, bedeutet dies, wenn ich ICO auf 5 Millionen Konten mache – bedeutet dies, dass ich jetzt einen riesigen Speicher habe, der extrem langsam abzurufen ist? - Vorausgesetzt, dass der gesamte Speicher des Vertrags auf einmal abgerufen wird.

Die tatsächliche On-Disk-Darstellung der Blockchain ist nicht Teil der Protokollspezifikation und somit implementierungsdefiniert. Die Antwort wird je nach Client stark variieren
Cpp-Ethereum ist das, wonach ich suche

Antworten (1)

Anweisungen zum Speichern und Abrufen von Daten sind:

    SLOAD: {
        execute:       opSload,
        gasCost:       gasSLoad,
        validateStack: makeStackFunc(1, 1),
        valid:         true,
    },
    SSTORE: {
        execute:       opSstore,
        gasCost:       gasSStore,
        validateStack: makeStackFunc(2, 0),
        valid:         true,
        writes:        true,
    },

Und so werden sie codiert:

func opSload(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
    loc := stack.peek()
    val := evm.StateDB.GetState(contract.Address(), common.BigToHash(loc))
    loc.SetBytes(val.Bytes())
    return nil, nil
}

func opSstore(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
    loc := common.BigToHash(stack.pop())
    val := stack.pop()
    evm.StateDB.SetState(contract.Address(), loc, common.BigToHash(val))

    evm.interpreter.intPool.put(val)
    return nil, nil
}

SetState und GetState werden wie folgt implementiert:

func (self *StateDB) GetState(addr common.Address, bhash common.Hash) common.Hash {
    stateObject := self.getStateObject(addr)
    if stateObject != nil {
        return stateObject.GetState(self.db, bhash)
    }
    return common.Hash{}
}
func (self *StateDB) SetState(addr common.Address, key, value common.Hash) {
    stateObject := self.GetOrNewStateObject(addr)
    if stateObject != nil {
        stateObject.SetState(self.db, key, value)
    }
}

// SetState updates a value in account storage.
func (self *stateObject) SetState(db Database, key, value common.Hash) {
    self.db.journal.append(storageChange{
        account:  &self.address,
        key:      key,
        prevalue: self.GetState(db, key),
    })
    self.setState(key, value)
}

// GetState returns a value in account storage.
func (self *stateObject) GetState(db Database, key common.Hash) common.Hash {
    value, exists := self.cachedStorage[key]
    if exists {
        return value
    }
    // Load from DB in case it is missing.
    enc, err := self.getTrie(db).TryGet(key[:])
    if err != nil {
        self.setError(err)
        return common.Hash{}
    }
    if len(enc) > 0 {
        _, content, _, err := rlp.Split(enc)
        if err != nil {
            self.setError(err)
        }
        value.SetBytes(content)
    }
    self.cachedStorage[key] = value
    return value
}

Merkle Patricia Trie speichert diese Daten:

// Account is the Ethereum consensus representation of accounts.
// These objects are stored in the main account trie.
type Account struct {
    Nonce    uint64
    Balance  *big.Int
    Root     common.Hash // merkle root of the storage trie
    CodeHash []byte
}

Wie Sie sehen können, enthält das Root common.HashMitglied der Struktur den Hash des internen Speichers des Vertrags. Das bedeutet, wo immer Sie den Speicher des Vertrages aktualisieren, wird sich der Hash ändern, und da das AccountObjekt Teil des gesamten Tries ist, wird die Änderung an höhere Knoten weitergegeben und der StateRootdes Blocks wird sich am Ende ändern.

Also kurz:

  1. Der Speicher des Vertrags wird nur durch Schlüssel->Wert-Paare modifiziert.
  2. Jede Änderung des internen Vertragsstandes impliziert eine Aktualisierung des gesamten Vertrags.
  3. Selbst mit diesem Design ist Ethereum bereits sehr ineffizient, aber es wurde entwickelt, um sicher zu sein, nicht schnell.
Der Root common.Hash wird verwendet, um auf leveldb zuzugreifen, um den gesamten Trie abzurufen - ist das richtig? Bedeutet das also - um irgendetwas im Speicher zu ändern, muss der gesamte Speicher abgerufen -> dann geändert -> dann wieder in leveldb abgelegt werden? Ist das richtig? Das ist verrückt!
nein, Root common.hashist nur der Unterpunkt der vertragsgemäßen Speicherung
Jede Änderung am Speicher des Vertrags aktualisiert den gesamten Trie vertikal, das sind etwa 200 Änderungen pro Transaktion, und deshalb funktioniert Ethereum nur mit SSD-Festplatten, HDD-Festplatten können nur 110 zufällige IOps pro Sekunde ausführen
"nein, Root common.hash ist nur der Sub-Trie des Vertragsspeichers" -> Können Sie das näher erläutern? Sagen Sie: Ich habe einen ERC20-konformen Smart Contract und jemand schickt dem Vertrag einfach etwas Ether im Austausch gegen Token. Bedeutet dies auf Implementierungsebene, dass der Code den gesamten Speicher-Trie, der zu diesem Smart Contract gehört, über common.Hash abrufen muss. Führen Sie die Aktualisierung durch und schreiben Sie den Baum zurück in die Datenbank. Das heißt, wenn ich 5 Millionen Konten habe, die mein Token enthalten, wäre der Vorgang extrem langsam? Vielen Dank im Voraus
nur die Knoten, die sich geändert haben, werden geschrieben + die übergeordneten Knoten dieser Knoten.
Tatsächlich ist die Implementierung sehr effizient, eine Änderung in einem Trie führt möglicherweise nicht zu einem einzigen Schreibvorgang auf der Festplatte. gethhat einen Cache und kann je nach Konfiguration so groß sein, dass er ausreicht, um viele Schlüssel-Wert-Paare zu speichern, ohne dass etwas gelesen werden muss. Außerdem akkumuliert LevelDB Schreibvorgänge, bis 128 MB Schreibvorgänge akkumuliert sind, wodurch IO reduziert wird
aber was ist, wenn der Speicher so groß ist – sagen wir, ich habe ein riesiges ICO und 5 Millionen Konten halten Guthaben meines Tokens. Dies bedeutet, dass ich jedes Mal, wenn ich den Kontostand eines Kontos aktualisiere, den gesamten Speicher Trie abrufen müsste - der 5 Millionen Konten umfasst? vielen Dank für die Antwort
Nein, Ihr Vertrag wird keine 5 Millionen Konten aktualisieren. Erstens ist es unmöglich, weil das Gaslimit des Blocks nur 7.000.000 beträgt und jeder 20.000 Gas verbraucht (wenn ich mich nicht irre), so dass es nur 350 Änderungen am Speicher geben kann, aller Kontrakte des Blocks zusammen. Zweitens kompiliert Solidity den Vertrag so, wie der Vertrag als Schlüssel-Wert-Paar auf den Speicher zugreift, nur die Speicherzellen, die sich geändert haben, werden geändert
Okay, tut mir leid, dass ich es nicht deutlich gemacht habe. Was ich versuche zu erreichen ist: Wenn der Speicher des Vertrags ungewöhnlich groß ist, sagen Sie, dass er bereits 5 Millionen Konten im Smart Contract hat. Wenn ich nur 1 Konto im Speicher aktualisieren möchte, müsste ich den gesamten Speicherversuch aus der Datenbank abrufen, bevor ich etwas ändern kann. ist das richtig? denn nach meinem Verständnis gibt es keine Möglichkeit, nur ein Speicherelement aus der Datenbank abzurufen, ohne den gesamten Speicher abzurufen, den der Vertrag besitzt.
Nein. Sehen Sie sich den obigen Code an. GetState()Funktion verwendet Schlüssel (eine Art von common.Hash), um den Wert zu suchen, es ist eine einzelne Operation. Warum sollte es den gesamten Trie lesen müssen? Die kostspielige Operation hier ist der commitgesamte Trie auf die Festplatte, ein Prozess, bei dem alle übergeordneten Knoten (die sich höher im Trie befinden) mit einem neuen Hash aktualisiert werden.
redest du von dieser Zeile val := evm.StateDB.GetState(contract.Address(), common.BigToHash(loc))? Sie sagen also, es wird nur der Knoten von Patricia Trie abgerufen, der dieses Speicherelement enthält? Als ich CPP Ethereum Impl las, sah ich so etwas nicht. Das macht jetzt viel mehr Sinn. Der Commit aktualisiert also den gesamten Hash - im Wesentlichen wird der Root mit den neuen Hashes aktualisiert.
cpp-ethereum sieht aus wie Abandonware, niemand verwendet es, um Nodes auszuführen. Und ich kompiliere (selbst) go-ethereumin gemeinsam genutzte Bibliotheken, um sie in meiner C++ Ethereum-Brieftasche zu verwenden, anstatt nativen C++-Code zu verwenden.