¿Cómo incluir en la lista blanca hasta 50k direcciones en un solo contrato?

¿Cuál es la mejor práctica para incluir en la lista blanca una gran cantidad de direcciones en un contrato? Por ejemplo, si quisiera incluir 50k direcciones en un mapeo que reflejaría truesolo las 50k direcciones, ¿sería esta la forma óptima? ¿Cuánto costaría esto, por dirección?

mapping (address => bool) userAddr;

function whitelistAddress (address user) onlyOwner { userAddr[user] = true; }

Incluir 50k direcciones codificadas en el contrato alcanzaría el límite de gas del bloque, sin duda. ¿Cómo calculo exactamente cuánto costaría esto?

Respuestas (3)

Límite inferior rápido

Puede poner un límite inferior al costo, porque cada dirección incluirá una SSTORE, como mínimo. Entonces, el costo de codificar 50k direcciones, a 20 kg cada una, sería más de 1 Gigagas en total. El último límite de bloque es de 6,7 Megagas. No encajará, por asomo.

Gsset - 20000 gas - Pagado por una operación SSTORE cuando el valor de almacenamiento se establece en distinto de cero desde cero

fuente: Libro Amarillo, Apéndice G


Costo práctico

Entonces, echemos un vistazo al costo práctico de agregar gradualmente direcciones de lista blanca. Remix es una excelente manera de estimar rápidamente los costos de gas para su función.

Usando este contrato:

pragma solidity ^0.4.15;

contract Owned {
    address owner;

    function Owned() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }
}
contract Whitelist is Owned {
    mapping (address => bool) userAddr;

    function whitelistAddress (address user) onlyOwner {
        userAddr[user] = true;
    }
}

Ejecuté la función de esta manera:

  1. Elija el entorno "Javascript VM"
  2. Haga clic en [Crear] en Lista blanca en la barra derecha
  3. Ingrese "0x5B2063246F2191f18F2675ceDB8b28102e957458"junto al botón [dirección de la lista blanca]
  4. Haga clic en el botón [dirección de la lista blanca]

Ejecutar whitelistAddress(...)resultados en:Transaction cost: 43464 gas

Coste total:50k transactions * 43k gas ~= 2 Gigagas

A los costos actuales de " bajo nivel de seguridad " de 0,5 gwei, eso equivale a aproximadamente 1 éter de los costos totales de gas.


Dosificación para mejora marginal

Parte de esa llamada para agregar una dirección en la lista blanca es sobrecarga: la llamada de función original, verificar el propietario, etc. Averigüemos cuánto.

pragma solidity ^0.4.15;

contract Whitelist is Owned {
    mapping (address => bool) userAddr;

    function whitelistAddress (address[] users) onlyOwner {
        for (uint i = 0; i < users.length; i++) {
            userAddr[users[i]] = true;
        }
    }
}

Llamar a esto con una lista de cuatro direcciones cuesta 109833 de gasolina, lo que resulta en un costo por dirección de solo 27458.25 de gasolina. No lo haremos mucho mejor que eso en una lista blanca explícita, ya que el límite inferior es 20k de gasolina por dirección.

Usando este método, el costo total de éter al precio bajo seguro se ha reducido a aproximadamente:

50k addresses * 27k gas * 0.5 gigawei ~= 0.7 Ether


Alternativas más raras

Tal vez realmente no tenga una lista blanca, tal vez sea una lista negra o una lista de invitados donde los falsos positivos ocasionales no son tan malos. Entonces un filtro de floración podría ser una solución razonable. Ajustar el filtro de floración requiere saber demasiado sobre su caso de uso específico, pero podría reducir fácilmente el costo total en 1,000x o 10,000x.

Respuesta realmente maravillosa. Todo tiene sentido, así que gracias, aunque tengo una pregunta. Al copiar ese primer bloque de código en Remix, veo un Transaction cost: 20784 gas, a diferencia del Transaction cost: 43464 gas, como dijiste. Estoy mirando Estimaciones de gas -> Externo -> whitelistAddress (dirección). ¿Hay otro lugar donde se estima el gas?
No estoy seguro de cómo remix genera esa estimación. Acabo de ejecutar la transacción haciendo clic en el botón [dirección de lista blanca] después de ingresar "0x5B2063246F2191f18F2675ceDB8b28102e957458" al lado. Editaré la respuesta para agregar esos pasos.
Debe ingresar la dirección con comillas dobles. El cuadro de entrada espera contenido codificado en JSON.
Cuando obtenga el gas de eso, ¿puede simplemente ignorar el "costo de ejecución"? ¿Cuándo entra eso en juego? Veo el 43464 ahora, pero también está el costo de ejecución.
Tacha eso, esto lo respondió. Gracias.
¿Se emplearía un enfoque similar para las direcciones de la lista blanca con límites de contribución individuales?
El filtro Classic Bloom no funcionará. Si existe alguna posibilidad de un falso positivo, un atacante puede seguir generando direcciones hasta que encuentre una.

Hay algunas alternativas más modernas que es esencial considerar ahora que el gas es mucho más alto.

Lista blanca basada en la raíz de Merkle (bajo costo, flexibilidad moderada)

Utilice todas las direcciones de su lista para generar un árbol de Merkle. Luego, pídale al usuario que envíe su prueba (puede servir en el front-end, no confidencial ya que solo el titular de la dirección puede usar su prueba), junto con la llamada de menta.

Considero esta flexibilidad media porque si desea cambiar la lista, debe actualizar la raíz en cadena.

Código de generación (js)

      const newMerkle = new MerkleTree(
        ['0x1...', '0x2...'].map((token: string) => hashToken(token)),
        keccak256,
        {
          duplicateOdd: false,
          hashLeaves: false,
          isBitcoinTree: false,
          sortLeaves: false,
          sortPairs: true,
          sort: false,
        }
      )

export function hashToken(account: string) {
  return Buffer.from(ethers.utils.solidityKeccak256(["address"], [account]).slice(2), "hex");
}

Código de verificación (solidez)

    function mintPresale(uint256 _quantity, bytes32[] calldata _proof)
        external
        payable
    {
        require(_verify(_leaf(msg.sender), _proof), "Invalid merkle proof");
        _mint(_quantity, msg.sender);
    }
    function _verify(bytes32 leaf_, bytes32[] memory _proof)
        internal
        view
        returns (bool)
    {
        return MerkleProof.verify(_proof, root, leaf_);
    }

Este fragmento usa código de la biblioteca OpenZeppelin Merkle Proof y merkletreejs

Lista blanca basada en firmas (costo mínimo, muy flexible)

Genere firmas con anticipación o en vivo en un back-end y entréguelas al usuario para que las incluya en su llamada de menta. Estos tampoco son sensibles, por lo que se pueden servir en el front-end sin ninguna autenticación.

Código de firma (mecanografiado)

export default async function signWhitelist(
    chainId: number,
    contractAddress: string,
    whitelistKey: SignerWithAddress,
    mintingAddress: string,
    nonce: number
) {
    const domain = {
        name: '[YOUR_CONTRACT_NAME}',
        version: '1',
        chainId,
        verifyingContract: contractAddress,
    }

    const types = {
        Minter: [
            { name: 'wallet', type: 'address' },
            { name: 'nonce', type: 'uint256' },
        ],
    }

    const sig = await whitelistKey._signTypedData(domain, types, {
        wallet: mintingAddress,
        nonce,
    })

    return sig
}

Verificando código (Solidez)

    modifier requiresWhitelist(
        bytes calldata signature,
        uint256 nonce
    ) {
        // Verify EIP-712 signature by recreating the data structure
        // that we signed on the client side, and then using that to recover
        // the address that signed the signature for this data.
        bytes32 structHash = keccak256(
            abi.encode(MINTER_TYPEHASH, msg.sender, nonce)
        );
        bytes32 digest = toTypedMessageHash(structHash); /*Calculate EIP712 digest*/
        require(!signatureUsed[digest], "signature used");
        signatureUsed[digest] = true;
        // Use the recover method to see what address was used to create
        // the signature on this data.
        // Note that if the digest doesn't exactly match what was signed we'll
        // get a random recovered address.
        address recoveredAddress = digest.recover(signature);
        require(
            hasRole(WHITELISTING_ROLE, recoveredAddress),
            "Invalid Signature"
        );
        _;
    }

Estos fragmentos están adaptados de este repositorio: https://github.com/msfeldstein/EIP712-whitelisting/blob/main/contracts/EIP712Whitelisting.sol

es bastante fácil lo haces así. a través de firmas. necesitas un back-end....

función matchAddressSigner (bytes32 hash, firma de memoria de bytes) vista privada devuelve (bool) { return _signerAddress == hash.recover (firma); }

con

require(matchAddressSigner(hash, sig), "no menta directa");

....

¿Puedes explicar un poco cómo hacer esto? ¿Cómo se puede usar esto para la dirección de la lista blanca?