¿Cómo encuentra el EVM la entrada de una función llamada?

Leí el código fuente de go-ethereum y me confundí acerca de cómo EVM encuentra la entrada de una función llamada. Como decía la especificación, el campo de datos en una transacción especifica la función y los argumentos. Entonces, según tengo entendido, la función específica debe ejecutarse en el EVM con datos de entrada relacionados en lugar de todo el código del contrato. Sin embargo, no puedo encontrar el código relacionado. En el ciclo de ejecución, la PC comienza desde 0:

pc = uint64(0) // program counter
for ; ; instrCount++ {



    // Get the memory location of pc
    op = contract.GetOp(pc)
    // calculate the new memory size and gas price for the current executing opcode
    newMemSize, cost, err = calculateGasAndSize(evm.env, contract, caller, op, statedb, mem, stack)
    if err != nil {
        return nil, err
    }

    // Use the calculated gas. When insufficient gas is present, use all gas and return an
    // Out Of Gas error
    if !contract.UseGas(cost) {
        return nil, OutOfGasError
    }

    // Resize the memory calculated previously
    mem.Resize(newMemSize.Uint64())
    // Add a log message
    if evm.cfg.Debug {
        evm.logger.captureState(pc, op, contract.Gas, cost, mem, stack, contract, evm.env.Depth(), nil)
    }

    if opPtr := evm.jumpTable[op]; opPtr.valid {
        if opPtr.fn != nil {
            opPtr.fn(instruction{}, &pc, evm.env, contract, mem, stack)
        } else {
            switch op {
            case PC:
                opPc(instruction{data: new(big.Int).SetUint64(pc)}, &pc, evm.env, contract, mem, stack)
            case JUMP:
                if err := jump(pc, stack.pop()); err != nil {
                    return nil, err
                }

                continue
            case JUMPI:
                pos, cond := stack.pop(), stack.pop()

                if cond.Cmp(common.BigTrue) >= 0 {
                    if err := jump(pc, pos); err != nil {
                        return nil, err
                    }

                    continue
                }
            case RETURN:
                offset, size := stack.pop(), stack.pop()
                ret := mem.GetPtr(offset.Int64(), size.Int64())

                return ret, nil
            case SUICIDE:
                opSuicide(instruction{}, nil, evm.env, contract, mem, stack)

                fallthrough
            case STOP: // Stop the contract
                return nil, nil
            }
        }
    } else {
        return nil, fmt.Errorf("Invalid opcode %x", op)
    }

    pc++

}

Entonces, ¿cómo ejecuta la EVM la función específica?

Respuestas (3)

la función específica debe ejecutarse en EVM con datos de entrada relacionados en lugar de todo el código del contrato

  1. La EVM ejecutará el código del contrato. El EVM solo ejecuta el código de bytes y no sabe nada sobre las funciones. Solidity, Serpent y web3.js implementan la misma interfaz binaria de aplicación, que es cómo se codifican las funciones y los datos: ¿Qué es una ABI y por qué se necesita para interactuar con los contratos?

Como se indica aquí , el ABI es una abstracción sobre el protocolo Ethereum y el EVM.

  1. Un compilador EVM produce código de contrato que simula funciones según la ABI. Aquí hay un ejemplo.

Ejemplo

Para las funciones que se adhieren a la ABI, es trabajo del compilador producir el código de bytes EVM correcto.

Un ejemplo muy aproximado, solo con fines conceptuales, del código de bytes de "tabla de salto" que produciría un compilador EVM:

method_id = first 4 bytes of msg.data
if method_id == 0x25d8dcf2 jump to 0x11
if method_id == 0xaabbccdd jump to 0x22
if method_id == 0xffaaccee jump to 0x33
other code <- Solidity fallback function code could be here
0x11:
code for function with method id 0x25d8dcf2
0x22:
code for function with method id 0xaabbccdd
0x33:
code for function with method id 0xffaaccee

Puede ver que los primeros 4 bytes de msg.data(Method ID es el término dado por ABI) se usan para verificar a qué función saltar y ejecutar.

A partir del ejemplo, también puede ver que puede hacer que su propio compilador genere un código de bytes con una lógica diferente (tal vez quiera usar los primeros 8 bytes de msg.data), pero las personas que llaman tendrían que seguir esas convenciones en lugar de simplemente usar un biblioteca como web3.js.

El ejemplo es de:

¿Cuándo se llama a la función de reserva?

¿Cómo se almacena una ABI en bytecode?

Así que EVM solo ejecuta los códigos de bytes. por ejemplo, llamo a una función de contrato func, y la función opCalldataLoad cargará los datos de entrada y puede saltar a la ubicación relacionada usando la firma de la función en los datos de entrada y luego finalizar la ejecución de evm cuando se cumpla el código de operación RETURN.
Sí, eso suena razonable.
@eth ¿qué hay de las funciones privadas? No puedo encontrar ningún hash para ellos en el código de bytes.

Estoy usando el código de ejemplo modificado de Syntax para llamar a los métodos de cambio de estado del contrato , guardándolo en C.sol:

contract C {
    uint[] public numbers;

    function initNumbers() {
         numbers.push(1);
         numbers.push(2);
    }

    function stateChanger(uint a) {
         numbers.push(a);
    }
}

A continuación se muestra el código binario compilado en formato hexadecimal, formateado en columnas de 80 caracteres. Puede ver las 3 firmas de función dentro del código binario marcado con '<' y '>'.

user@Kumquat:~$ solc --abi --hashes --bin C.sol

======= C =======
Binary:
6060604052610217806100126000396000f360606040526000357c01000000000000000000000000
0000000000000000000000000000000090048063<5a7dc897>1461004f578063<65060775>1461005e57
8063<d39fa233>146100765761004d565b005b61005c60048050506100a2565b005b61007460048080
35906020019091905050610181565b005b61008c60048080359060200190919050506101f2565b60
40518082815260200191505060405180910390f35b60006000508054806001018281815481835581
8115116100f4578183600052602060002091820191016100f391906100d5565b808211156100ef57
600081815060009055506001016100d5565b5090565b5b5050509190906000526020600020900160
005b6001909190915055506000600050805480600101828181548183558181151161016257818360
0052602060002091820191016101619190610143565b8082111561015d5760008181506000905550
600101610143565b5090565b5b5050509190906000526020600020900160005b6002909190915055
505b565b600060005080548060010182818154818355818115116101d35781836000526020600020
91820191016101d291906101b4565b808211156101ce57600081815060009055506001016101b456
5b5090565b5b5050509190906000526020600020900160005b83909190915055505b50565b600060
005081815481101561000257906000526020600020900160005b91509050548156

Function signatures:
5a7dc897: initNumbers()
65060775: stateChanger(uint256)
d39fa233: numbers(uint256)

Contract JSON ABI
[{"constant":false,"inputs":[],"name":"initNumbers","outputs":[],"type":"function"},
{"constant":false,"inputs":[{"name":"a","type":"uint256"}],"name":"stateChanger","outputs":[],"type":"function"},
{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"numbers","outputs":[{"name":"","type":"uint256"}],"type":"function"}]

Así es como se calculan las firmas de función en geth:

> web3.sha3('initNumbers()').substr(0,10)
"0x5a7dc897"
> web3.sha3('stateChanger(uint256)').substr(0,10)
"0x65060775"
> web3.sha3('numbers(uint256)').substr(0,10)
"0xd39fa233"
Si quiero detectar el cambio de saldo en el contrato, ¿cómo puedo hacerlo? ¿Debería comenzar una nueva pregunta para hacer eso?
Sí. Nueva pregunta es la mejor manera.

Como señala BokkyPooBah, la firma de la función se usa para encontrar el lugar en el código de bytes que contiene el código para el método llamado. El Ethereum ABI define cómo se pasan los argumentos. Los primeros cuatro bytes de la cadena de argumento proporcionada es la firma de la función, codificada con Keccak (SHA3). Se utiliza una instrucción similar a un interruptor para comparar los primeros cuatro bytes de la cadena de argumento proporcionada con los métodos disponibles para este contrato. Si no se encuentra ninguna coincidencia, los contratos generan una excepción (llamando a un código de operación que no existe) y la llamada al método falla. Si se encuentra una coincidencia , el contador del programa se mueve a ese método mediante el JUMPIcódigo de operación.

Consulte también ¿Qué es el contador del programa al comienzo de la ejecución de un método Ethereum?