Saltar al contenido principal

Gestión de Memoria

En el universo de la máquina virtual de KodeChain, la memoria representa el lienzo efímero donde los contratos inteligentes pintan sus operaciones temporales. Implementada en vm/core/memory.go, esta memoria expandible y segura proporciona el espacio de trabajo necesario para cálculos complejos mientras mantiene límites estrictos de seguridad.

Arquitectura de la Memoria

Estructura Fundamental

type Memory struct {
data []byte // Array de bytes que contiene los datos
size uint64 // Tamaño actual de memoria en bytes
}

La memoria se representa como un array dinámico de bytes, con un contador de tamaño que rastrea cuánto espacio está actualmente en uso.

Inicialización Eficiente

func NewMemory() *Memory {
return &Memory{
data: make([]byte, 0), // Inicia vacío
size: 0,
}
}

La memoria comienza vacía, expandiéndose solo cuando es necesario, optimizando el uso de recursos.

Operaciones de Almacenamiento

Store: Escritura Básica

func (m *Memory) Store(offset uint64, value []byte) error {
if len(value) == 0 {
return nil
}

requiredSize := offset + uint64(len(value))

// Verificar límite máximo
if requiredSize > MaxMemorySize {
return fmt.Errorf("memory overflow: required %d, max %d", requiredSize, MaxMemorySize)
}

// Expandir si es necesario
if requiredSize > m.size {
m.expand(requiredSize)
}

// Copiar datos
copy(m.data[offset:], value)
return nil
}

Store escribe datos en una posición específica, expandiendo la memoria automáticamente si es necesario.

Store32: Escritura de Valores de 256 bits

func (m *Memory) Store32(offset uint64, value *big.Int) error {
bytes := value.Bytes()

// Pad con ceros a la izquierda si es necesario
padded := make([]byte, 32)
if len(bytes) <= 32 {
copy(padded[32-len(bytes):], bytes)
} else {
// Tomar los últimos 32 bytes si es más grande
copy(padded, bytes[len(bytes)-32:])
}

return m.Store(offset, padded)
}

Store32 maneja específicamente valores de 256 bits, padding o truncando según sea necesario para mantener consistencia.

Operaciones de Lectura

Load: Lectura Básica

func (m *Memory) Load(offset uint64, size uint64) ([]byte, error) {
if size == 0 {
return []byte{}, nil
}

requiredSize := offset + size

// Verificar límite máximo
if requiredSize > MaxMemorySize {
return nil, fmt.Errorf("memory access out of bounds: required %d, max %d", requiredSize, MaxMemorySize)
}

// Expandir si es necesario
if requiredSize > m.size {
m.expand(requiredSize)
}

// Retornar una copia de los datos
result := make([]byte, size)
copy(result, m.data[offset:offset+size])
return result, nil
}

Load lee datos desde una posición específica, retornando siempre una copia para prevenir modificaciones accidentales.

Load32: Lectura de Valores de 256 bits

func (m *Memory) Load32(offset uint64) (*big.Int, error) {
data, err := m.Load(offset, 32)
if err != nil {
return nil, err
}

// Convertir bytes a big.Int (big endian)
return new(big.Int).SetBytes(data), nil
}

Load32 lee específicamente valores de 32 bytes, convirtiéndolos a big.Int para operaciones matemáticas.

Expansión Inteligente

Expansión por Chunks

func (m *Memory) expand(newSize uint64) {
if newSize <= m.size {
return
}

// Redondear al múltiplo de MemoryChunkSize más cercano
chunks := (newSize + MemoryChunkSize - 1) / MemoryChunkSize
newSize = chunks * MemoryChunkSize

// Crear nueva memoria y copiar datos existentes
newData := make([]byte, newSize)
copy(newData, m.data)

m.data = newData
m.size = newSize
}

La expansión ocurre en chunks de 32 bytes, optimizando el uso de memoria y previniendo expansiones frecuentes.

Operaciones Avanzadas

Copy: Copia Interna

func (m *Memory) Copy(dest, src, size uint64) error {
if size == 0 {
return nil
}

srcEnd := src + size
destEnd := dest + size

if srcEnd > MaxMemorySize || destEnd > MaxMemorySize {
return fmt.Errorf("memory copy out of bounds")
}

if maxSize := srcEnd; destEnd > maxSize {
maxSize = destEnd
}

if maxSize > m.size {
m.expand(maxSize)
}

copy(m.data[dest:destEnd], m.data[src:srcEnd])
return nil
}

Copy permite copiar datos dentro de la memoria, esencial para operaciones de manipulación de datos complejas.

Gestión del Estado

Tamaño Actual

func (m *Memory) Size() uint64 {
return m.size
}

Size retorna el tamaño actual de memoria asignada.

Reset Completo

func (m *Memory) Reset() {
m.data = make([]byte, 0)
m.size = 0
}

Reset limpia completamente la memoria, preparándola para una nueva ejecución.

Inspección para Debugging

func (m *Memory) Data() []byte {
result := make([]byte, m.size)
copy(result, m.data)
return result
}

Data proporciona acceso completo a la memoria para herramientas de debugging.

Seguridad por Diseño

Límites Máximos

const MaxMemorySize = 16 * 1024 * 1024 // 16 MB

Un límite máximo de 16 MB previene ataques de agotamiento de memoria.

Validación de Accesos

if requiredSize > MaxMemorySize {
return fmt.Errorf("memory access out of bounds")
}

Cada operación valida que no exceda los límites permitidos.

Copias Defensivas

result := make([]byte, size)
copy(result, m.data[offset:offset+size])

Todas las lecturas retornan copias, previniendo modificaciones accidentales del estado interno.

Optimizaciones de Rendimiento

Lazy Expansion

La memoria solo se expande cuando es estrictamente necesario, minimizando el uso de recursos.

Chunk-Based Growth

El crecimiento en chunks de 32 bytes optimiza las asignaciones de memoria del sistema operativo.

Efficient Copy Operations

Las operaciones de copia utilizan las funciones optimizadas de Go para máxima eficiencia.

Representación y Debugging

String Representation

func (m *Memory) String() string {
if m.size == 0 {
return "Memory: []"
}

displaySize := m.size
if displaySize > 256 {
displaySize = 256
}

return fmt.Sprintf("Memory [size=%d]: %x...", m.size, m.data[:displaySize])
}

Esta representación muestra los primeros 256 bytes de memoria, útil para debugging sin sobrecargar la salida.

El Rol de la Memoria en los Contratos

Espacio de Trabajo Temporal

La memoria sirve como pizarra temporal donde los contratos pueden:

  • Preparar datos para operaciones
  • Almacenar resultados intermedios
  • Construir estructuras de datos complejas
  • Preparar datos de retorno

Interfaz con Storage

Mientras que el storage es persistente y costoso, la memoria es efímera y eficiente. Los contratos típicamente:

  1. Cargan datos del storage a memoria
  2. Realizan cálculos en memoria
  3. Escriben resultados de vuelta al storage

Gestión de Datos de Retorno

Los contratos construyen sus datos de retorno en memoria antes de retornarlos al caller.

Integración con la VM

Contexto de Ejecución

type ExecutionContext struct {
Stack *Stack
Memory *Memory
Storage *Storage
// ... otros campos
}

La memoria es parte integral del contexto de ejecución, coordinando con stack y storage.

Operaciones de Bytecode

Opcodes como MLOAD, MSTORE, y MCOPY interactúan directamente con la memoria:

func (vm *VM) opMload(ctx *ExecutionContext) error {
offset, err := ctx.Stack.Pop()
if err != nil {
return err
}

value, err := ctx.Memory.Load32(offset.Uint64())
if err != nil {
return err
}

return ctx.Stack.Push(value)
}

Filosofía de Diseño

Eficiencia vs Seguridad

La memoria balancea eficiencia con seguridad:

  • Eficiencia: Expansión lazy y operaciones optimizadas
  • Seguridad: Límites estrictos y validaciones completas
  • Simplicidad: API clara y operaciones intuitivas

Determinismo

Todas las operaciones de memoria son completamente deterministas: mismas entradas producen mismos resultados, esencial para la consistencia blockchain.

Aislamiento

Cada ejecución de contrato tiene su propia instancia de memoria, previniendo interferencias entre contratos.

El Arte de la Memoria

La memoria representa el equilibrio perfecto entre expresividad y control. Proporciona a los contratos inteligentes el espacio necesario para realizar cálculos complejos mientras mantiene la seguridad y previsibilidad que exige el entorno blockchain.

Cada byte almacenado, cada expansión de memoria, cada operación de copia es una decisión deliberada en el diseño de un sistema que debe ser simultáneamente poderoso y confiable.

En KodeChain, la memoria no es solo un componente técnico; es el lienzo donde la lógica de negocio cobra vida, transformando código en valor económico.