Implementación de almacenamiento de Ethereum a nivel de base de datos: cómo se almacena

He leído el papel amarillo muchas veces, así como varios artículos, supongo que si no puedo encontrar la respuesta aquí, buscaré el código.

Tengo entendido que cada cuenta de contrato contiene una raíz de almacenamiento. y puede recuperar la raíz del nivel db.

Pero, ¿cuál es el valor en el leveldb?

Estas son algunas de mis preguntas:

  1. LevelDB es clave-valor, por lo que si podemos usar la raíz de almacenamiento para obtener el almacenamiento de un contrato, ¿cuál es el valor?

  2. Voy a hacer una suposición descabellada, ¿que el valor es el árbol patrica?

  3. Si 2 es verdadero, ¿no es extremadamente ineficiente cada vez que queremos ejecutar el contrato? ¿Tenemos que tomar el almacenamiento de todo el contrato? Si un contrato tiene mucho almacenamiento, ¿entonces es potencialmente extremadamente lento hacerlo?

  4. Última pregunta: tengo entendido que toda la información del saldo del token ERC20 se almacena en el almacenamiento, entonces esto significa que si hago ICO en 5 millones de cuentas, ¿esto significa que ahora tengo un gran almacenamiento que es extremadamente lento de recuperar? - Dado que todo el almacenamiento del contrato se recupera de una sola vez.

La representación real en disco de la cadena de bloques no forma parte de la especificación del protocolo y, por lo tanto, está definida por la implementación. La respuesta variará dramáticamente dependiendo del cliente.
Cpp-ethereum es lo que busco

Respuestas (1)

Las instrucciones que almacenan y recuperan datos son:

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

Y así es como se codifican:

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 y GetState se implementan así:

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 almacena estos datos:

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

Como puede ver, el Root common.Hashmiembro de la estructura tiene el hash del almacenamiento interno del contrato. Esto significa que, donde sea que actualice el almacenamiento del contrato, el hash cambiará y, como el Accountobjeto es parte de todo el proceso, el cambio se propagará a los nodos superiores y StateRootel bloque cambiará al final.

Entonces, en resumen:

  1. El almacenamiento de contratos se modifica solo por pares clave->valor.
  2. Toda modificación de estado interno del contrato implica la actualización del conjunto del mismo.
  3. Incluso con este diseño, Ethereum ya es muy ineficiente, pero fue diseñado para ser seguro, no rápido.
La raíz common.Hash se usa para acceder a leveldb para recuperar todo el trie, ¿es correcto? Entonces, ¿significa esto que para modificar cualquier cosa dentro del almacenamiento, se debe recuperar todo el almacenamiento -> luego modificar -> luego volver a colocarlo en leveldb? ¿Es esto correcto? ¡Esto es Loco!
no, Root common.hashes solo el sub-trie del almacenamiento del contrato solo
cualquier modificación al almacenamiento del contrato actualizará todo el trie verticalmente, eso es alrededor de 200 modificaciones por transacción, y es por eso que Ethereum solo funciona con discos SSD, los discos HDD solo pueden hacer 110 IOps aleatorios por segundo
"no, Root common.hash es solo el sub-trie del almacenamiento del contrato solamente" -> ¿Puede aclarar más? Diga: Tengo un contrato inteligente compatible con ERC20 y alguien simplemente envía al contrato algo de Ether a cambio de un token. En el nivel de implementación, ¿significa que el código debe recuperar todo el intento de almacenamiento que pertenece a ese contrato inteligente a través de common.Hash? realice la actualización y vuelva a escribir el árbol en la base de datos. Esto significa que si tengo 5 millones de cuentas que tienen mi token, ¿la operación sería extremadamente lenta? gracias de antemano
solo se escriben los nodos que han cambiado + los nodos principales de esos nodos.
en realidad la implementación es muy eficiente, un cambio en un trie puede no producir ni una sola escritura en el disco. gethtiene caché y, dependiendo de la configuración, puede ser tan grande que será suficiente para contener muchos pares clave-valor sin necesidad de leer nada. Además, LevelDB acumula escrituras hasta que se acumulan 128 MB de escrituras, reduciendo IO
pero, ¿qué pasa si el almacenamiento es tan grande? Digamos que tengo un ICO enorme y 5 millones de cuentas tienen saldo de mi token. Esto significa que cada vez que actualizo el saldo de cualquier cuenta, tendré que recuperar todo el intento de almacenamiento, que incluye 5 millones de cuentas. muchas gracias por responder
no, su contrato no actualizará 5 millones de cuentas. Primero es imposible porque el limite de gas del bloque es solo de 7.000.000 y cada uno lleva 20.000 de gas (si no me equivoco), entonces solo pueden haber 350 modificaciones al almacenamiento, de todos los contratos del bloque combinados. En segundo lugar, Solidity compila el contrato de la forma en que el contrato accede al almacenamiento como un par clave-valor, solo se modifican las celdas de almacenamiento que han cambiado.
Está bien, lo siento, no lo dejé claro. A lo que estoy tratando de llegar es: si el almacenamiento del contrato es anormalmente grande, digamos que ya tiene 5 millones de cuentas en el contrato inteligente. Cuando solo quiero actualizar 1 cuenta en el almacenamiento, necesitaría recuperar todo el intento de almacenamiento de la base de datos antes de poder modificar algo. ¿es esto correcto? porque, según tengo entendido, no hay forma de recuperar solo un elemento de almacenamiento de la base de datos sin recuperar todo el almacenamiento que posee el contrato.
No. Mira el código de arriba. GetState()La función usa la clave (un tipo de hash común) para buscar el valor, es una sola operación. ¿Por qué tendría que leer todo el trie? La operación costosa aquí es la commitde todo el trie al disco, un proceso en el que todos los nodos principales (ubicados más arriba en el trie) se actualizan con un nuevo hash.
¿Estás hablando de esta línea val := evm.StateDB.GetState(contract.Address(), common.BigToHash(loc))? entonces, ¿está diciendo que solo recupera el nodo de patricia trie que contiene ese elemento de almacenamiento? Cuando estaba leyendo CPP ethereum impl, no vi tal cosa. Esto tiene mucho más sentido ahora. Entonces, la confirmación actualizará todo el hash, en esencia, actualizará la raíz con los nuevos hash.
cpp-ethereum parece abandonware, nadie lo usa para ejecutar nodos. Y (yo mismo) estoy compilando go-ethereumen bibliotecas compartidas para usarlas en mi billetera C++ Ethereum, en lugar de usar el código nativo de C++.