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:
- Cargan datos del storage a memoria
- Realizan cálculos en memoria
- 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.