Enfoque eficiente para eliminar elementos de la matriz en Solidity

Para cada usuario, quiero mantener una matriz de activos retenidos (cada activo tiene una identificación).

Mi solución hasta ahora es:

struct User {
        uint userId;
        uint[] assets;
    }

Por cada retención assetdel usuario, empujo a la matriz del usuario la IDde asset.

Quiero darle al usuario la opción de eliminar un archivo asset.

¿Cuál sería el enfoque más eficiente para esto?

Asignar todo lo disponible assetspara cada usuario (sería un desperdicio dado que tiene muchos activos disponibles) VS. iterando sobre todos sus assetscada vez que le gustaría eliminar un asset, encontrándolo en la matriz, luego eliminándolo y cambiando toda la matriz en consecuencia; además, el código para esto es un poco horrible:

function deleteAsset(uint assetId) external returns(uint) {
        bool assetFound = false;
        // Check if the asset found is the last asset (or we go out of boundaries)
        if (allUsers[msg.sender].assets[allUsers[msg.sender].assets.length - 1] == assetId){
            assetFound = true;
        }

        else{
            // Iterate over all user assets and find its index
            for (uint i = 0; i < allUsers[msg.sender].assets.length - 1; i++) {
                if (!assetFound && allUsers[msg.sender].assets[i] == assetId)
                    assetFound = true;

                if(assetFound)
                    allUsers[msg.sender].assets[i] = allUsers[msg.sender].assets[i + 1];
            }
        }

        if (assetFound){
            delete allUsers[msg.sender].assets[allUsers[msg.sender].assets.length - 1];
            allUsers[msg.sender].assets.length--;
        }
    }

Sería mucho más fácil si pudiera guardar un mappingpara cada usuario que indique qué activo tiene, pero no puede devolver un mappingde una función y no conozco los puntos de referencia de las viewfunciones y "fuerza bruta" todos los activos disponible para cada usuario puede llevar mucho tiempo, supongo.

Aquí hay un método para eliminar un elemento de matriz en un índice dado ethereum.stackexchange.com/questions/1527/…
Soy consciente de estos hilos, también presenté su método. Aunque mi situación es un poco diferente y estoy preguntando sobre la eficiencia.
El título debería ser algo como: Enfoque eficiente para eliminar elementos en la matriz de tamaño dinámico de Solidity
@RomanFrolov Cierto, editado.

Respuestas (3)

Fuente: https://github.com/su-squares/ethereum-contract/blob/master/contracts/SuNFT.sol

Aqui tienes:

Algoritmo:

uint[] assets;
mapping(uint=>uint) indexOfAsset;

function removeAssetFromArray(uint _assetToDelete) {
  uint index = indexOfAsset[_assetToDelete];
  if (!index) return;

  if (assets.length > 1) {
    assets[index] = assets[assets.length-1];
  }
  assets.length--; // Implicitly recovers gas from last element storage
}

Advertencia: este enfoque asume un conjunto ordenado, no una matriz. En otras palabras, asume que la matriz no tiene elementos duplicados.

@random ¿Esto ayuda?
No, solo reembolsa el ajuste de gas a valores de almacenamiento distintos de cero a cero nuevamente. Es un poco más barato que decir explícitamente eliminar matriz [índice] Dicho esto, no hay respuestas sobre la eficiencia en este hilo, así que supongo que tendré que investigar yo mismo.
Esta es una solución increíblemente simple para mi caso de uso... gracias...
¿También necesita eliminar la entrada de indexOfAsset?
¿Cómo funciona para el primer elemento?
Quitar de indexOfAssetdepende de su caso de uso. Para mi caso de uso (NFT que nunca se destruyen), a removeAssetFromArraysiempre va seguido de a addAssetToArrayy, por lo tanto, no es necesario eliminarlo de indexOfAsset.
Eliminar el primer elemento es lo mismo que eliminar cualquier activo: el primer elemento se sobrescribe con el último elemento y luego se elimina el último elemento.

Generalmente, moviendo el último elemento de la lista a la fila para eliminar.

Trabajando desde el Usuario structen el código del OP.

pragma solidity 0.5.1;

contract DeleteUser {

    struct UserStruct {
        uint userId;
        uint[] assets;
    }

    mapping(address => UserStruct) public userStructs;

    function deleteUserAsset(address user, uint assetIndex, uint asset) public {
        UserStruct storage u = userStructs[user];
        require(u.assets.length > assetIndex);
        require(u.assets[assetIndex] == asset); // this is a sanity check in case the list was re-ordered
        u.assets[assetIndex] = u.assets[u.assets.length-1];
        u.assets.length--;
    }

}

La verificación de cordura no es estrictamente necesaria ya que la pregunta está redactada, pero la función depende de que la persona que llama sepa qué fila eliminar. Esto podría ser un problema real si la lista se reorganiza por la deleteUserAsset()función que se llama. Una solución simple es agregar ese cheque, para estar seguro.

Aún mejor, en mi opinión , es no confiar en que la persona que llama sepa esto. Para hacerlo, necesitaríamos una búsqueda de un solo paso para encontrar la fila en la que se encuentra una clave en particular. Se necesitará otra palabra de 32 bytes para almacenar eso.

pragma solidity 0.5.1;

contract DeleteUser {

    struct UserStruct {
        bytes32[] assets;
        mapping(bytes32 => uint) assetPointers;
    }

    mapping(address => UserStruct) userStructs;

    function isUserAsset(address user, bytes32 assetId) public view returns(bool isIndeed) {
        if(userStructs[user].assets.length == 0) return false;
        return userStructs[user].assets[userStructs[user].assetPointers[assetId]] == assetId;
    }

    function deleteUserAsset(address user, bytes32 assetId) public {
        UserStruct storage u = userStructs[user];
        require(isUserAsset(user, assetId));
        uint rowToDelete = u.assetPointers[assetId];
        u.assets[rowToDelete] = u.assets[u.assets.length-1];
        u.assets.length--;
    }

}

Espero eso ayude.

Para empezar, no pude crear una matriz de almacenamiento con más de 100 elementos en un constructor. Del mismo modo, supongo que tener que pasar por encima de uno también fallará cuando crezca demasiado.

Los resultados son claros, mover índices después de eliminar un elemento de una matriz dinámica es una idea horrible y los valores 0 deberían filtrarse en el cliente.

Recorriendo una matriz de almacenamiento con 10 entradas y eliminando el ÚLTIMO elemento

 transaction cost   20514 gas

Recorriendo una matriz de almacenamiento con 100 entradas y eliminando el ÚLTIMO elemento

 transaction cost   43349 gas

Recorriendo una matriz de almacenamiento con 10 entradas y eliminando el elemento MEDIO

transaction cost    44762 gas 

Recorriendo una matriz de almacenamiento con 100 entradas y eliminando el elemento MEDIO

transaction cost    340387 gas

Ese aumento en el costo del gas simplemente no es justificable.

Aquí se explica cómo crear una matriz de almacenamiento con muchos elementos:pragma solidity ^0.5.1; contract S {uint[] a;constructor() public {a.length = 2**20;}}
El bucle es casi siempre incorrecto. Según su otra nota, tal vez tenga un caso de uso diferente, tal vez pueda abrir una pregunta separada.