No necesitas LTL para usar un model checker
No necesitas aprender lógica temporal lineal para usar un model checker. Herramientas como Kani, CBMC y Alloy te permiten verificar propiedades con assertions y constraints relacionales ordinarios. Renuncias a la capacidad de probar propiedades de liveness a cambio de una curva de aprendizaje medida en horas en lugar de semanas, y para la mayoría de los bugs de software, es un intercambio que vale la pena.
La temporal logic es el guardián que exigen la mayoría de los model checkers
Los model checkers clásicos, SPIN y NuSMV, te piden que expreses propiedades en LTL o CTL. Escribes cosas como G(request -> F(response)) para decir: “globalmente, cada solicitud es eventualmente seguida por una respuesta”. Esto es poderoso. Puede probar que tu protocol nunca entra en deadlock, que cada mensaje es eventualmente reconocido, que tu sistema es fair.
También es una habilidad especializada que la mayoría de los desarrolladores en activo no tienen. Leer una fórmula LTL no es como leer código. Los operadores son modales, la semántica se define sobre traces infinitos, y la intuición que desarrollaste escribiendo unit tests no se transfiere. Así que la pregunta es justa: si quieres el poder de bug-finding del model checking, ¿realmente tienes que escalar esa montaña primero?
No. Una clase diferente de herramientas ha existido durante décadas, y verifican código con los mismos assertions que ya escribes.
Los bounded model checkers convierten assertions en problemas SAT
Los bounded model checkers no te piden que aprendas una nueva lógica. Te piden que escribas un test harness. Declaras inputs no deterministas, los restringes con assumptions y afirmas propiedades en el lenguaje anfitrión. La herramienta luego desenrolla loops hasta un bound, codifica el programa como una fórmula SAT o SMT, y le pide al solver que encuentre un counterexample.
Si el solver devuelve UNSAT, tu propiedad se cumple para todos los paths dentro de ese bound. Si encuentra un counterexample, obtienes un trace concreto que muestra exactamente qué inputs activan el bug. Sin operadores temporales. Sin traces infinitos. Solo un assertion fallido con un vector de input reproducible.
Kani es el bounded model checker más accesible para Rust. Se instala con cargo install kani-verifier y funciona con código Rust ordinario.
Un ejemplo real: verificando una state machine en Rust
Aquí hay una state machine con un bug. Rastrea un counter simple que decrementa en cada tick hasta llegar a cero, luego transiciona de vuelta a idle.
#[derive(Clone, Copy, PartialEq, Debug)]
enum State {
Idle,
Running,
Stopped,
}
struct Machine {
state: State,
count: u32,
}
impl Machine {
fn start(&mut self, initial: u32) {
if self.state == State::Idle && initial > 0 {
self.state = State::Running;
self.count = initial;
}
}
fn tick(&mut self) {
if self.state == State::Running {
self.count -= 1;
if self.count == 0 {
self.state = State::Idle;
}
}
}
fn stop(&mut self) {
if self.state == State::Running {
self.state = State::Stopped;
}
}
}
El bug es sutil. Mira stop. Establece el estado en Stopped pero deja count sin cambios. Si algo más tarde asume que count == 0 cuando state == State::Stopped, esa assumption es incorrecta.
Aquí hay un Kani proof harness que lo detecta:
#[kani::proof]
fn check_stopped_implies_count_zero() {
let mut machine = Machine {
state: State::Idle,
count: 0,
};
let initial: u32 = kani::any();
kani::assume(initial > 0 && initial <= 10);
machine.start(initial);
machine.tick();
machine.stop();
assert!(
machine.state != State::Stopped || machine.count == 0,
"Stopped state should have count == 0"
);
}
Kani explora cada path. Encuentra que si initial == 2, después de start la máquina está Running con count == 2. Un tick decrementa count a 1 pero el estado permanece Running. Luego stop establece el estado en Stopped con count == 1. El assertion falla. Kani reporta este trace exacto.
Esta es la experiencia de model checking sin temporal logic. Escribes Rust. Escribes assertions en Rust. La herramienta te dice qué inputs los rompen.
Alloy encuentra bugs a nivel de diseño con lógica relacional
Los bounded model checkers verifican código. Alloy verifica diseños.
Alloy es un model finder, no un model checker tradicional, pero la distinción importa menos que el workflow. Describes tu sistema como un conjunto de relaciones, invariantes de estado como constraints de first-order logic, y pides a Alloy que encuentre un counterexample. Busca todas las instancias posibles hasta un scope definido por el usuario y te muestra diagramas del fallo.
Aquí hay un modelo Alloy de una propiedad de grafo dirigido simple:
sig Node {
next: set Node
}
pred reachable[n1, n2: Node] {
n2 in n1.^next
}
assert symmetric_reachability {
all n1, n2: Node |
reachable[n1, n2] implies reachable[n2, n1]
}
check symmetric_reachability for 3
El assertion afirma que la reachability es simétrica. Alloy verifica esto para todos los grafos de hasta tres nodes e inmediatamente dibuja un counterexample: un grafo donde n1 apunta a n2 pero n2 no tiene aristas salientes. No aparecen operadores G, F o U en ninguna parte.
Lo que renuncias: liveness y comportamiento infinito
Esta conveniencia tiene un costo. Los bounded model checkers solo verifican comportamiento hasta un loop bound o longitud de trace. No pueden probar que una solicitud es eventualmente respondida, solo que no suceden cosas malas dentro de los primeros N pasos. Alloy solo verifica instancias dentro de su scope. No puede probar propiedades para sistemas arbitrariamente grandes, solo que no existe un counterexample debajo del bound.
Si necesitas probar que tu protocol de consensus nunca pierde writes comprometidos, o que cada mensaje es eventualmente entregado, aún necesitas temporal logic y unbounded model checking. Herramientas como TLA+ envuelven temporal logic en una sintaxis que parece más matemática que lógica modal, pero la semántica subyacente sigue siendo temporal.
Para invariantes de estructuras de datos, enforcement de contratos de API, y encontrar la race condition que solo se activa en el 47º path de ejecución, las herramientas bounded suelen ser suficientes. Atrapan los bugs que los unit tests pasan por alto, y lo hacen con assertions que puedes leer sin un libro de texto.
Empezando con Kani en cinco minutos
Si tienes Rust instalado, el bounded model checking está a un comando de distancia.
cargo install kani-verifier
cargo kani setup
Crea una nueva crate, escribe una función con un bug sutil, y añade un harness #[kani::proof]. Ejecuta cargo kani. Si Kani encuentra un counterexample, imprime los inputs concretos que activan el fallo. Si reporta VERIFICATION SUCCESSFUL, tu propiedad se cumple para todos los paths dentro de los bounds predeterminados.
Comienza con funciones que tengan espacios de estado pequeños e invariantes claras. State machines, validación de parsers, y transiciones de estado de protocols son objetivos ideales para empezar. Evita intentar verificar un servidor HTTP completo en tu primer intento. El SAT solver tiene límites, y tu paciencia también.
FAQ: bounded vs. unbounded, liveness y por dónde empezar
¿Es el bounded model checking realmente model checking?
Técnicamente, es una variante que codifica el problema como una satisfiability query en lugar de explorar el state graph explícitamente. Para un desarrollador intentando encontrar bugs, la distinción es académica. Explora todos los paths sistemáticamente, que es lo que model checking significa en la práctica.
¿Puedo probar liveness con Kani o CBMC?
No directamente. Las propiedades de liveness requieren razonar sobre comportamiento infinito, y las herramientas bounded limitan explícitamente su búsqueda. Puedes a veces codificar una verificación de liveness bounded desenrollando suficientes pasos para alcanzar un fixpoint, pero eso es una técnica avanzada.
¿Qué hay de TLA+? ¿Requiere temporal logic?
TLA+ usa la Temporal Logic of Actions, así que sí, técnicamente. Pero Leslie Lamport diseñó la sintaxis para que se lea como matemáticas ordinarias. La mayoría de las especificaciones TLA+ dedican el 90% de su texto a invariantes de estado y constraints de estructuras de datos, no a operadores temporales. Es el camino más accesible si necesitas razonamiento temporal unbounded.
¿Debería usar Alloy o Kani?
Usa Kani si tienes código Rust y quieres verificar detalles de implementación. Usa Alloy si aún estás diseñando el sistema y quieres explorar si tus invariantes siquiera son posibles antes de escribir el código.
Elige la herramienta que coincida con tu problema, no con tu ambición
La temporal logic es hermosa y poderosa, pero no es un prerrequisito para la verificación formal. Los bounded model checkers y los relational model finders te permiten expresar propiedades en los lenguajes que ya conoces. No probarán que tu sistema eventualmente termine, pero encontrarán el error off-by-one que corrompe tu state machine de base de datos. Para la mayoría de los equipos, ese es el bug que importa.
Comienza con Kani en una sola función con estado. Escribe un assertion. Deja que el solver te diga qué te perdiste.