Sistema híbrido off-chain/on-chain que integra tokens de recompensa (ERC-20) e insignias digitales verificables (ERC-1155) con la plataforma Moodle. Desarrollado como proyecto académico en la Universidad Tecnológica de Bolívar.
+---------------------------+
| Dapp / Moodle Plugin | ← equivale al "Dapp/wallet"
| (PHP, dashboard, market) |
+------------+--------------+
| HMAC-SHA256 / POST
v
+============================================+
| Interface Layer |
| FastAPI REST · /events · /students | ← JSON-RPC equivalente
| /tokens · /wallets · /health |
+========+===================+===============+
| |
+--------+-------+ +--------+---------+
| Execution | | Off-chain DB |
| Core | | |
| | | PostgreSQL |
| Event ingest | | audit_log |
| Rules engine | | events |
| HMAC verifier | | |
| Badge builder | | MariaDB (Moodle) |
| (OBv2 meta) | | queue / rules |
+--------+-------+ +------------------+
|
+--------+--------+
| IPFS (Kubo) | ← almacenamiento descentralizado de metadatos OBv2
| nodo local |
| modo --offline |
+--------+--------+
|
+========+==============================+
| Besu Core (red privada) |
| |
| Networking Execution-Core |
| ───────── ───────────── |
| Discovery Transaction pool |
| RLPx Synchronizer |
| ETH sub-proto Block validator |
| QBFT sub-proto └─ Tx processor |
| 4 nodos EVM |
| (bootnode+3) Pluggable consensus |
| QBFT PoA |
| |
| Storage |
| ─────── |
| World state (Trie Bonsai) |
| Blockchain |
+========+==============================+
|
+--------+--------------------+
| Smart Contracts (Solidity)|
| |
| ERC-20 MeritCoinERC20 |
| - mint / burn |
| - MINTER_ROLE |
| - BURNER_ROLE |
| |
| ERC-1155 MeritBadges1155 |
| - mintBadge |
| - ISSUER_ROLE |
| - metadata URI → IPFS |
+-----------------------------+
- Un estudiante completa una actividad o recibe una calificación en Moodle.
- El observer del plugin (
observer.php) captura el evento Moodle (core\event\*) y consultalocal_meritcoin_rulespara calcular las monedas MRT según el tipo de actividad y la nota mínima configurada por el profesor. - Se verifica que el estudiante no haya superado el límite de MRT por curso consultando
local_meritcoin_earnings— si lo supera, el evento es descartado silenciosamente. - Detección de wallet:
- Curso normal: el observer lee el campo de perfil
walletdel estudiante. Si no tiene wallet registrada, el evento es descartado. - Curso piloto: si el estudiante aún no tiene wallet custodial, el evento se encola con
status = pending_walletywallet_servicellama aPOST /wallets/provisionen el backend para crearla automáticamente. Una vez provisionada, el estado pasa apending.
- Curso normal: el observer lee el campo de perfil
- El evento se encola en
local_meritcoin_queueconstatus = pending, incluyendoevent_id(MD5 deuserid+cmid+grade) para garantizar idempotencia. - La tarea programada
send_events_task(cron cada minuto) toma los eventospending, firma el payload con HMAC-SHA256 usandoHMAC_SECRETy lo envía aPOST /events/ingesten el backend FastAPI. - El backend valida la firma HMAC, verifica que el
event_idno haya sido procesado previamente y:- a. Genera los metadatos de la insignia en formato Open Badges v2 (OBv2).
- b. Sube los metadatos al nodo IPFS local (Kubo) y obtiene el CID (
ipfs://...). - c. Llama al contrato ERC-1155 (
mintBadge) con el URI del CID como metadata de la insignia. - d. Llama al contrato ERC-20 (
mint) para acreditar los MRT en la wallet del estudiante. - e. Ambas transacciones se ejecutan en la red privada Hyperledger Besu (QBFT).
- El backend registra el resultado en PostgreSQL (
audit_log,events) para trazabilidad completa. - El plugin actualiza el evento en
local_meritcoin_queueastatus = senty registra las ganancias enlocal_meritcoin_earnings, manteniendo el ledger del límite por curso. - El estudiante consulta su saldo MRT e insignias en tiempo real desde MeritCoin → Mi Dashboard, que llama a
GET /students/{wallet}/summaryen el backend.
meritcoin/
├── contracts/ # Solidity + Hardhat (ERC-1155 y ERC-20)
│ ├── contracts/ # MeritBadges1155.sol, MeritCoinERC20.sol
│ ├── test/ # 19 tests con Hardhat + Chai
│ ├── scripts/
│ │ └── deploy.js # Despliega ambos contratos y asigna roles
│ ├── hardhat.config.js
│ └── package.json
├── backend/ # FastAPI (procesamiento off-chain)
│ ├── app/
│ │ ├── api/ # Endpoints: events, students, tokens, badges, wallets
│ │ ├── core/ # Config, DB, seguridad HMAC
│ │ ├── models/ # Pydantic + SQLAlchemy
│ │ ├── services/ # Blockchain, badges, tokens, audit, IPFS, wallets
│ │ └── main.py
│ ├── alembic/ # Migraciones de base de datos
│ ├── tests/ # 24 tests con pytest
│ ├── requirements.txt
│ ├── pytest.ini
│ └── Dockerfile
├── besu/ # Red privada Hyperledger Besu (QBFT, 4 nodos)
│ └── QBFT-Network/
│ ├── docker-compose.yml
│ ├── genesis.json # Bloque génesis de la red EVM privada
│ ├── qbftConfigFile.json # Configuración del consenso QBFT
│ └── networkFiles/ # Claves y datos de cada nodo
├── plugin/ # Plugin Moodle local_meritcoin (PHP)
│ ├── classes/ # observer, api_client, rules_service, wallet_service, tasks
│ ├── db/ # install.xml, upgrade.php, events.php, tasks.php, access.php
│ ├── lang/ # Strings en inglés y español
│ ├── styles/ # dashboard.css
│ └── *.php # Vistas: dashboard, manage, marketplace, badges, etc.
├── scripts/
│ ├── test_e2e.py # Pruebas E2E automatizadas
│ └── test_curl.py # Generador de comandos curl de prueba
├── docker-compose.yml # Moodle + MariaDB + PostgreSQL + Backend + IPFS
├── .env.example
└── README.md
Instala estas herramientas antes de comenzar:
| Herramienta | Versión mínima | Notas |
|---|---|---|
| Docker + Docker Compose | v2+ | Incluye Compose v2 integrado |
| Node.js | 20+ | Solo para compilar y desplegar contratos |
| pnpm | 9+ | Gestor de paquetes para contratos (npm install -g pnpm) |
| Python | 3.11+ | Para tests del backend y scripts E2E |
| Git | cualquiera | — |
¿Por qué pnpm? pnpm usa un almacén de paquetes compartido y enlaces duros, lo que lo hace más rápido, más eficiente en disco y con mejor aislamiento de dependencias que npm. Además evita los CVEs de supply-chain asociados al registro por defecto de npm.
Si tu entorno tiene todos los requisitos previos, puedes levantar todo el sistema con un solo comando:
chmod +x setup.sh
./setup.shEl script ejecuta los 7 pasos en orden, espera a que cada servicio esté listo y al final muestra las direcciones de los contratos y las URLs de acceso.
⚠️ Si el script falla en algún paso, continúa desde ese punto con la guía manual que encontrarás a continuación. El script no hace nada que no puedas hacer a mano — simplemente automatiza la secuencia.
ℹ️ Linux: si
host.docker.internalno resuelve, agregaextra_hostsal serviciobackendendocker-compose.ymlantes de correr el script:extra_hosts: - "host.docker.internal:host-gateway"
Sigue los pasos exactamente en este orden. Si los mezclas, el backend no encontrará los contratos o el plugin no conectará con el backend.
Levantar red Besu
Clonar y configurar .env
Levantar servicios Docker principales
Desplegar contratos en Besu y otorgar roles al signer del backend
Actualizar .env del backend con las direcciones
Recrear el backend
Aplicar migraciones de base de datos
Instalar y configurar el plugin en Moodle
La blockchain privada corre de forma independiente al docker-compose principal. Debe estar activa antes de desplegar los contratos y de arrancar el backend.
cd besu/QBFT-Network
docker compose up -dEsto levanta 4 nodos Hyperledger Besu con consenso QBFT:
| Nodo | RPC HTTP | P2P |
|---|---|---|
besu-node-1 |
http://localhost:8545 | 30303 |
besu-node-2 |
http://localhost:8546 | 30304 |
besu-node-3 |
http://localhost:8547 | 30305 |
besu-node-4 |
http://localhost:8548 | 30306 |
El nodo 1 es el bootnode y el punto de entrada principal de la red. Los nodos 2, 3 y 4 se conectan automáticamente usando el enode del nodo 1.
Verifica que la red está produciendo bloques:
curl -s http://localhost:8545 \
-X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
# Respuesta esperada: {"result":"0x5",...} — el número de bloque sube con el tiempoSi el resultado es
"0x0"espera 10 segundos y repite. La red tarda unos segundos en producir el primer bloque después del arranque.
⚠️ Si los nodos Besu se caen (se veExitedendocker ps), cualquier transacción en vuelo se pierde aunque eltx_hashhaya quedado guardado en la BD. Después de recuperarlos deberás marcar los eventos comofailedpara que el worker reintente el mint:docker exec meritcoin-postgres psql -U meritcoin -d meritcoin_db \ -c "UPDATE events SET status='failed' WHERE status='processed' \ AND event_id IN (SELECT event_id FROM audit_log WHERE tx_mrt IS NOT NULL);"
Vuelve a la raíz del proyecto:
cd ../..git clone <url-del-repo>
cd meritcoin
cp .env.example .envEdita .env y ajusta obligatoriamente estos valores:
# Genera la clave Fernet ejecutando este comando:
# python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
WALLET_ENCRYPTION_KEY=tu-clave-fernet-aqui # ⚠️ el backend no arranca sin esto
HMAC_SECRET=cambia-este-secreto # cualquier string largo y aleatorio
# Clave privada de la cuenta deployer (cuenta #0 del génesis de Besu)
# Esta cuenta también actúa como SIGNER del backend para firmar transacciones.
# Solo para desarrollo local. Nunca uses esta clave en producción.
DEPLOYER_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
BACKEND_SIGNER_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
WALLET_ENCRYPTION_KEYcifra las claves privadas de las wallets custodiales de los estudiantes con Fernet (AES-128-CBC). Sin esta variable el backend se niega a iniciar.
⚠️ Importante: la cuenta cuya clave privada uses enBACKEND_SIGNER_PRIVATE_KEYdebe tenerMINTER_ROLEyBURNER_ROLEen los contratos ERC-20 y ERC-1155. Esto se otorga en el Paso 3. Si usas cuentas distintas para deployer y signer, asegúrate de otorgar los roles explícitamente a la dirección del signer.
docker compose up -dLa primera vez tarda 3-5 minutos porque Moodle realiza su instalación inicial. Puedes monitorear el progreso:
docker compose logs -f moodle
# Espera hasta ver: "Welcome to the Bitnami moodle container"Servicios disponibles una vez levantados:
| Servicio | URL | Credenciales |
|---|---|---|
| Moodle | http://localhost:8080 | admin / Admin1234! |
| Backend FastAPI | http://localhost:8000 | — |
| Docs API (Swagger) | http://localhost:8000/docs | — |
| PostgreSQL | localhost:5432 | meritcoin / meritcoin_pass |
| IPFS (Kubo) API | http://localhost:5001 | — |
| IPFS Gateway | http://localhost:8081 | — |
cd contracts
pnpm installEjecuta los tests para verificar que todo compila correctamente:
pnpm exec hardhat test
# Resultado esperado: 19 passingDespliega los contratos en la red Besu (el nodo 1 debe estar corriendo desde el paso 0):
pnpm exec hardhat run scripts/deploy.js --network besuVerás una salida similar a:
Deploying contracts with: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
MeritCoin ERC20 deployed to: 0xABC123...
MeritBadge ERC1155 deployed to: 0xDEF456...
MINTER_ROLE granted to deployer ✓
BURNER_ROLE granted to deployer ✓
ISSUER_ROLE granted to deployer ✓
Copia las dos direcciones — las necesitas en el siguiente paso.
⚠️ Nunca corrasdeploy.jsmás de una vez sin actualizar el.envdel backend. Cada ejecución despliega contratos nuevos en direcciones distintas. Si el backend apunta a contratos viejos, todas las transacciones serán revertidas silenciosamente constatus: 0x0y el balance quedará en 0.
⚠️ MINTER_ROLE y el signer del backend: el scriptdeploy.jsotorga los roles a la cuenta deployer. Si la cuenta que firma las transacciones en el backend (BACKEND_SIGNER_PRIVATE_KEY) es diferente a la cuenta deployer, debes otorgarle los roles explícitamente. Agrega esto al final dedeploy.js:const BACKEND_SIGNER = "0xDIRECCION_DEL_SIGNER"; const MINTER_ROLE = await token.MINTER_ROLE(); await token.grantRole(MINTER_ROLE, BACKEND_SIGNER); const BURNER_ROLE = await token.BURNER_ROLE(); await token.grantRole(BURNER_ROLE, BACKEND_SIGNER); const ISSUER_ROLE = await badges.ISSUER_ROLE(); await badges.grantRole(ISSUER_ROLE, BACKEND_SIGNER); console.log(`Roles otorgados al signer del backend: ${BACKEND_SIGNER}`);Para verificar que el signer tiene el rol después del deploy:
curl -s -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_call","params":[{"to":"<MRT_ADDRESS>", "data":"0x91d148540000000000000000000000009f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6000000000000000000000000<SIGNER_ADDRESS_SIN_0x>"},"latest"],"id":1}' # Resultado esperado: 0x0000...0001 (true = tiene el rol)
Vuelve a la raíz:
cd ..Abre .env y añade/reemplaza:
MRT_CONTRACT_ADDRESS=0xABC123... # dirección ERC-20 del paso anterior
BADGE_CONTRACT_ADDRESS=0xDEF456... # dirección ERC-1155 del paso anterior
BLOCKCHAIN_RPC_URL=http://host.docker.internal:8545¿Por qué
host.docker.internal? El backend corre dentro de Docker Compose y necesita acceder a los nodos Besu que corren en su propio compose independiente.host.docker.internalresuelve al host físico desde dentro del contenedor.Linux: si
host.docker.internalno resuelve, agrega esta línea al serviciobackendendocker-compose.yml:extra_hosts: - "host.docker.internal:host-gateway"
Un simple restart no recarga el .env. Hay que recrear el contenedor:
docker compose up -d --force-recreate backendVerifica que el backend está activo y conectado a Besu:
curl http://localhost:8000/healthRespuesta esperada:
{
"status": "ok",
"blockchain_connected": true,
"ipfs_connected": true,
"database": "ok"
}Si blockchain_connected es false, revisa que los nodos Besu del paso 0
estén corriendo y que BLOCKCHAIN_RPC_URL apunte a host.docker.internal:8545.
⚠️ Paso crítico que se omite fácilmente. Si el backend se levanta concreate_all(modo desarrollo) y luego se agregan columnas al modelo sin migrar, esas columnas no existirán en la BD real aunque el ORM las tenga definidas. El síntoma es queipfs_cid,tx_hashychain_statusquedan enNULLdespués de otorgar insignias.
Verifica el estado actual de las migraciones:
docker exec -it meritcoin-backend alembic currentSi no muestra ninguna revisión (salida vacía), la BD fue creada con create_all
sin pasar por Alembic. Márcala como sincronizada sin ejecutar migraciones:
docker exec -it meritcoin-backend alembic stamp headAplica cualquier migración pendiente:
docker exec -it meritcoin-backend alembic upgrade headSi hay columnas nuevas en el modelo que no tienen migración, genera una:
docker exec -it meritcoin-backend alembic revision --autogenerate -m "descripcion de los cambios"
docker exec -it meritcoin-backend alembic upgrade headVerifica que las columnas críticas existen en badge_awards:
docker exec -it meritcoin-postgres psql -U meritcoin -d meritcoin_db -c "\d badge_awards"
# Debe incluir: tx_hash, ipfs_cid, chain_statusAbre docker-compose.yml y descomenta esta línea bajo el servicio moodle:
volumes:
- ./plugin:/bitnami/moodle/local/meritcoin # ← descomenta esta línea
⚠️ Si es la primera vez que levantaste Moodle, asegúrate de que completó su instalación inicial antes de montar este volumen. Si lo montas desde el inicio, Moodle puede fallar al detectar el plugin en un estado de BD incompleto.
Recrea el contenedor de Moodle:
docker compose up -d --force-recreate moodleVerifica que el plugin está montado:
docker exec meritcoin-moodle ls /bitnami/moodle/local/meritcoin
# Debe listar: classes db lang styles dashboard.php lib.php version.php ...- Entra a http://localhost:8080 como admin (contraseña:
Admin1234!) - Moodle detecta el plugin automáticamente. Ve a: Administración del sitio → Notificaciones
- Haz clic en Actualizar base de datos de Moodle y completa el proceso.
Ve a: Administración del sitio → Plugins → Plugins locales → MeritCoin
| Campo | Valor |
|---|---|
| Habilitado | ✓ Sí |
| URL del backend | http://meritcoin-backend:8000 |
| Secreto HMAC | el mismo valor de HMAC_SECRET en .env |
| Campo wallet | wallet |
| Límite MRT por estudiante/curso | 16 (o 0 para sin límite) |
Usa
http://meritcoin-backend:8000(nombre del servicio Docker) cuando ambos contenedores (Moodle y backend) están en el mismo Docker Compose. Usahttp://host.docker.internal:8000solo si corres el backend fuera de Docker.
- Administración del sitio → Usuarios → Campos de perfil de usuario
- Agrega un campo de tipo Entrada de texto:
- Nombre corto:
wallet - Nombre visible:
Wallet Ethereum
- Nombre corto:
- Guarda.
Una vez que todos los servicios están activos, sigue este flujo para verificar que el sistema funciona de extremo a extremo.
# Stack principal
docker compose ps
# Red Besu
cd besu/QBFT-Network && docker compose ps && cd ../..Todos deben aparecer como running. Confirma que Besu produce bloques:
curl -s http://localhost:8545 \
-X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'- En Moodle: Administración del sitio → Usuarios → Agregar usuario
- En el campo Wallet Ethereum del perfil asigna una dirección válida de la red:
# Ver cuentas preconfiguradas en el génesis de Besu
curl -s http://localhost:8545 \
-X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_accounts","params":[],"id":1}'Usa cualquiera de las direcciones retornadas. La cuenta #0
(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266) tiene ETH preacuñado en el génesis
y es la cuenta deployer — úsala solo para pruebas.
- Crea un curso en Moodle y matricula al estudiante de prueba.
- Agrega una actividad (tarea, quiz, foro, etc.).
- En el menú lateral del curso ve a MeritCoin → Gestión de reglas.
- Crea una regla:
- Tipo de regla: Por tipo de actividad (por ej.
assign) - Monedas:
5 - Nota mínima:
6.0(opcional)
- Tipo de regla: Por tipo de actividad (por ej.
- Guarda la regla.
- Entra a Moodle como el estudiante de prueba.
- Completa la actividad configurada.
- Como profesor/admin, califica la actividad con una nota ≥ a
min_grade.
Verifica que el evento fue encolado en Moodle:
docker exec meritcoin-mariadb \
mysql -u bn_moodle -pmoodle_pass bitnami_moodle \
-e "SELECT userid, event_type, coins_amount, status, attempts \
FROM mdl_local_meritcoin_queue ORDER BY id DESC LIMIT 5;"Debe aparecer un registro con status = pending.
La tarea se ejecuta automáticamente cada minuto. Para forzarla de inmediato:
docker exec meritcoin-moodle \
php /bitnami/moodle/admin/cli/scheduled_task.php \
--execute='\local_meritcoin\task\send_events_task'Git Bash en Windows: usa doble barra al inicio:
php //bitnami/moodle/admin/cli/scheduled_task.php
Verifica que el evento fue procesado por el backend:
docker exec meritcoin-postgres \
psql -U meritcoin -d meritcoin_db \
-c "SELECT event_id, student_wallet, coins_amount, processed_at \
FROM events ORDER BY processed_at DESC LIMIT 5;"Y que la cola de Moodle lo marcó como enviado:
docker exec meritcoin-mariadb \
mysql -u bn_moodle -pmoodle_pass bitnami_moodle \
-e "SELECT status, attempts FROM mdl_local_meritcoin_queue ORDER BY id DESC LIMIT 3;"
# status debe ser: sentDesde la API del backend:
# Reemplaza <WALLET> con la dirección del estudiante
curl -s http://localhost:8000/students/<WALLET>/summary | python3 -m json.toolVerifica también en la BD que el CID e tx_hash se guardaron:
docker exec -it meritcoin-postgres psql -U meritcoin -d meritcoin_db \
-c "SELECT id, chain_status, tx_hash, ipfs_cid FROM badge_awards ORDER BY issued_at DESC LIMIT 5;"Desde Moodle: entra como el estudiante y ve a MeritCoin → Mi Dashboard. Debe mostrar el saldo MRT real del contrato ERC-20 y las insignias ERC-1155 ganadas.
- Como profesor/admin, ve al curso → MeritCoin → Recompensas.
- Crea una recompensa con precio en MRT ≤ saldo actual del estudiante.
- Entra como estudiante → MeritCoin → Mercado.
- Canjea la recompensa. El plugin procesará el canje automáticamente
vía
process_redemptions_task(cada minuto) o manualmente:
docker exec meritcoin-moodle \
php /bitnami/moodle/admin/cli/scheduled_task.php \
--execute='\local_meritcoin\task\process_redemptions_task'Los cursos piloto eliminan el requisito de que el estudiante registre su wallet manualmente. El sistema la crea y gestiona de forma transparente.
- Administración del sitio → MeritCoin → Cursos Piloto (
admin_pilot_courses.php) - Selecciona el curso.
- Opcionalmente elige un grupo piloto (solo ese grupo recibirá wallet custodial)
y una fecha de cierre manual (sobreescribe
course.enddate). - Guarda.
A partir de ese momento:
- La primera calificación del semestre activa el provisionado automático de wallet
- El observer encola el evento con
status = pending_wallet wallet_servicellama aPOST /wallets/provisionen el backend- Cuando la wallet está lista, el evento pasa a
pendingy se envía normalmente
La tarea expire_courses_task (cron 2 AM diario) detecta cursos piloto vencidos
y llama a POST /wallets/expire-course en el backend para congelar el saldo MRT.
Para forzarlo manualmente en pruebas:
docker exec meritcoin-moodle \
php /bitnami/moodle/admin/cli/scheduled_task.php \
--execute='\local_meritcoin\task\expire_courses_task'Limpiar la BD del backend (PostgreSQL):
docker exec meritcoin-postgres psql -U meritcoin -d meritcoin_db \
-c "TRUNCATE TABLE audit_log, events RESTART IDENTITY CASCADE;"Limpiar cola y canjes en Moodle (MariaDB):
docker exec meritcoin-mariadb \
mysql -u bn_moodle -pmoodle_pass bitnami_moodle \
-e "DELETE FROM mdl_local_meritcoin_queue;
DELETE FROM mdl_local_meritcoin_redemptions;
DELETE FROM mdl_local_meritcoin_earnings;"Re-desplegar contratos:
cd contracts
pnpm exec hardhat run scripts/deploy.js --network besu
# Actualiza MRT_CONTRACT_ADDRESS y BADGE_CONTRACT_ADDRESS en .env
cd ..
docker compose up -d --force-recreate backendReiniciar la red Besu desde cero
cd besu/QBFT-Network
docker compose down -v
docker compose up -d
cd ../..Después de reiniciar Besu debes re-desplegar los contratos, ya que las direcciones anteriores dejarán de existir en la nueva cadena. También debes aplicar nuevamente las migraciones de Alembic si la BD de PostgreSQL fue recreada.
- Verifica que la transacción de mint no fue revertida:
curl -s -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_getTransactionReceipt","params":["<TX_HASH>"],"id":1}' # Si "status": "0x0" → la transacción fue revertida
- Si fue revertida, verifica que el signer tiene
MINTER_ROLE(ver Paso 3). - Si el receipt devuelve
null, los nodos Besu se cayeron antes de incluir el bloque. Marca el evento comofailedpara reintento:docker exec -it meritcoin-postgres psql -U meritcoin -d meritcoin_db \ -c "UPDATE events SET status='failed' WHERE event_id='<EVENT_ID>'; DELETE FROM audit_log WHERE event_id='<EVENT_ID>';"
-
Verifica que las columnas existen en la tabla:
docker exec -it meritcoin-postgres psql -U meritcoin -d meritcoin_db -c "\d badge_awards"
Si no existen, aplica las migraciones (ver Paso 6).
-
Verifica que IPFS es accesible desde el backend:
docker exec -it meritcoin-backend curl -s -X POST http://meritcoin-ipfs:5001/api/v0/id # Debe devolver el ID del nodo IPFS
-
Revisa los logs del backend mientras otorgas una insignia:
docker logs meritcoin-backend --tail=50 -f
La BD fue creada con create_all en lugar de migraciones. Sincroniza sin ejecutar:
docker exec -it meritcoin-backend alembic stamp head
docker exec -it meritcoin-backend alembic current
# Debe mostrar la revisión más reciente como (head)La BD tiene columnas que Alembic no sabe que ya aplicó. Solución: usar stamp head primero (ver punto anterior).
Agrega extra_hosts al servicio backend en docker-compose.yml:
services:
backend:
extra_hosts:
- "host.docker.internal:host-gateway"Luego recrea el contenedor:
docker compose up -d --force-recreate backendDocumentación interactiva disponible en http://localhost:8000/docs
| Método | Endpoint | Descripción |
|---|---|---|
GET |
/health |
Estado del servicio, blockchain e IPFS |
POST |
/events/ingest |
Recibir evento académico (requiere HMAC) |
GET |
/students/{wallet}/badges |
Listar insignias de un estudiante |
GET |
/students/{wallet}/balance |
Consultar saldo MRT on-chain |
GET |
/students/{wallet}/summary |
Saldo MRT + badges (usado por el dashboard) |
POST |
/tokens/spend |
Quemar MRT al confirmar un canje del marketplace |
POST |
/wallets/provision |
Provisionar wallet custodial para un estudiante |
POST |
/wallets/expire-course |
Congelar saldo al cerrar un curso piloto |
POST |
/badges/award |
Otorgar insignia manualmente a un estudiante |
POST |
/badges/awards/{id}/retry-chain |
Reintentar mint en blockchain de una insignia pendiente |
| Contrato | Estándar | Descripción |
|---|---|---|
MeritBadges1155 |
ERC-1155 | Insignias digitales con metadatos OBv2 en IPFS |
MeritCoinERC20 |
ERC-20 | Token MRT de recompensa académica |
Ambos usan exclusivamente OpenZeppelin 5.x (sin librerías de pago).
Incluyen AccessControl (ISSUER_ROLE, MINTER_ROLE, BURNER_ROLE)
y Pausable para emergencias.
Ver documentación detallada en contracts/README.md.
- HMAC-SHA256: toda comunicación Moodle → FastAPI está firmada con el secreto compartido
- Sin datos personales en blockchain: solo wallets e IDs numéricos ofuscados
- Idempotencia: eventos duplicados son rechazados por
event_idúnico (MD5 determinístico deuserid+cmid+grade) - Límite MRT por estudiante: el observer descarta eventos que exceden el tope configurado por curso
- Roles Moodle: capabilities por contexto de curso, no globales
- Contratos con AccessControl:
ISSUER_ROLE,MINTER_ROLE,BURNER_ROLEyPausable require_sesskey(): todas las acciones de escritura del plugin validan la sesión- Wallets custodiales cifradas: claves privadas cifradas con Fernet (AES-128-CBC) usando
WALLET_ENCRYPTION_KEY - pnpm: gestor de paquetes con almacén centralizado y mejor aislamiento de dependencias
| Componente | Tecnología |
|---|---|
| LMS | Moodle 4.3 (Docker, imagen Bitnami) |
| Contratos | Solidity 0.8.28 · OpenZeppelin 5.x · Hardhat 2.28 |
| Backend | FastAPI · SQLAlchemy async · web3.py · PostgreSQL 16 |
| IPFS | Kubo (nodo local, modo --offline) |
| Plugin | PHP 8.x (Moodle Plugin API v0.5.1) |
| Base de datos | MariaDB 10.11 (Moodle) + PostgreSQL 16 (Backend) |
| Blockchain | Hyperledger Besu — red privada QBFT, 4 nodos |
| Gestor de paquetes | pnpm 9+ (contratos) |
| Componente | Tests | Framework | Estado |
|---|---|---|---|
| Contratos Solidity | 19 | Hardhat + Chai | ✅ Estables |
| Backend FastAPI | 24 | pytest + httpx | ✅ Estables |
| E2E flujo completo | 18 | Python (stdlib) | ✅ Estables |
| Total | 61 |
| Fase | Descripción | Estado |
|---|---|---|
| 1 | Entorno de desarrollo (Docker) | ✅ Completa |
| 2 | Contratos inteligentes (Solidity) | ✅ Completa |
| 3 | Backend FastAPI (Python) | ✅ Completa |
| 4 | Plugin Moodle — core (observer, task, queue) | ✅ Completa |
| 5 | Prueba de flujo completo (E2E) | ✅ Completa |
| 6 | Gestión de reglas por curso (manage.php, editrule.php, rules_service) | ✅ Completa |
| 7 | Ledger de ganancias y gasto por curso (earnings, spend) | ✅ Completa |
| 8 | Dashboard del estudiante + Mercado de recompensas | ✅ Completa |
| 9 | Insignias personalizadas (imagen, nombre y descripción configurables por curso) | ✅ Completa |
| 10 | Integración Hyperledger Besu (red privada QBFT, 4 nodos) | ✅ Completa |
| 11 | Finalización del MVP | ✅ Completa |
La visión del proyecto es integrarse nativamente con SAVIO, la plataforma institucional de la Universidad Tecnológica de Bolívar. Las líneas de trabajo contempladas para esa siguiente fase son:
- Despliegue productivo en SAVIO: empaquetar el plugin como release estable y coordinar su instalación en la instancia oficial de Moodle/SAVIO
- Hardening de seguridad: rotación de claves, gestión centralizada de secretos y monitoreo de eventos anómalos
- Escalabilidad de infraestructura: separación de ambientes (dev/stage/prod) y orquestación con Kubernetes
- Observabilidad y monitoreo: paneles Prometheus/Grafana, logging estructurado y alertas sobre fallos en cola, backend o red Besu
- Mejoras de UX: reportes más detallados por curso, nuevos tipos de reglas y recompensas
- Extensión on-chain: interoperabilidad con otras redes EVM institucionales, manteniendo privacidad de datos académicos
Proyecto académico — Universidad Tecnológica de Bolívar, 2026.