Ethereum λΈλ‘μ²΄μΈ λ°μ΄ν°λ₯Ό ETL νμ΄νλΌμΈμΌλ‘ μμ§νμ¬ Neo4j κ·Έλν λ°μ΄ν°λ² μ΄μ€μ μ§μκ·Έλνλ‘ κ΅¬μΆνκ³ , μ΄λ₯Ό νμ©ν λΆμ APIλ₯Ό μ 곡νλ Kotlin/Spring Boot κΈ°λ° λ°±μλ μμ€ν μ λλ€.
blocketl/
βββ blockinfra/ # μΈνλΌ μ€μ
β βββ docker-compose.yml # Docker Compose μ€μ
β βββ neo4j/ # Neo4j μ΄κΈ°ν μ€ν¬λ¦½νΈ
β βββ postgres/ # PostgreSQL μ΄κΈ°ν μ€ν¬λ¦½νΈ
β βββ start.sh # μΈνλΌ μμ μ€ν¬λ¦½νΈ
β βββ stop.sh # μΈνλΌ μ€μ§ μ€ν¬λ¦½νΈ
β βββ README.md # μμΈ λ¬Έμ
β
βββ src/main/kotlin/com/jun/blocketl/
β βββ domain/ # λλ©μΈ λͺ¨λΈ
β β βββ blockchain/ # λΈλ‘μ²΄μΈ λ°μ΄ν° λͺ¨λΈ
β β βββ graph/ # Neo4j κ·Έλν λ
Έλ λͺ¨λΈ
β β βββ meta/ # PostgreSQL λ©νλ°μ΄ν° λͺ¨λΈ
β β
β βββ repository/ # λ°μ΄ν° μ κ·Ό κ³μΈ΅
β β βββ graph/ # Neo4j Repository
β β βββ meta/ # JPA Repository
β β
β βββ etl/ # ETL νμ΄νλΌμΈ
β β βββ EthereumEtlService.kt # λΈλ‘μ²΄μΈ λ°μ΄ν° μμ§
β β βββ GraphBuilderService.kt # κ·Έλν λΉλ
β β
β βββ service/ # λΉμ¦λμ€ λ‘μ§
β β βββ GraphQueryService.kt # κ·Έλν 쿼리
β β βββ AmlAnalysisService.kt # AML λΆμ
β β
β βββ api/ # REST API
β β βββ BlockchainGraphController.kt
β β βββ AmlController.kt
β β βββ EtlController.kt
β β
β βββ config/ # μ€μ
β βββ Neo4jConfig.kt
β βββ KafkaConfig.kt
β βββ CacheConfig.kt
β βββ OpenApiConfig.kt
β
βββ src/main/resources/
βββ application.yml # μ ν리μΌμ΄μ
μ€μ
cd blockinfra
./start.shλλ Windowsμμ:
cd blockinfra
docker-compose up -dsrc/main/resources/application.yml νμΌμ μ΄κ³ Alchemy API Keyλ₯Ό μ€μ ν©λλ€:
blockchain:
etl:
ethereum:
rpc-url: https://eth-mainnet.g.alchemy.com/v2/YOUR_ALCHEMY_KEYλ¬΄λ£ API Key λ°κΈ: https://www.alchemy.com/
./gradlew bootRun- Swagger UI: http://localhost:8080/swagger-ui.html
- Neo4j Browser: http://localhost:7474 (neo4j/blockchain123)
Ethereum λΈλ‘체μΈμμ λΈλ‘κ³Ό νΈλμμ λ°μ΄ν°λ₯Ό μ€μκ°μΌλ‘ μμ§ν©λλ€.
# λΈλ‘ λκΈ°ν μμ
POST http://localhost:8080/api/v1/etl/sync/start?startBlock=0&endBlock=1000
# λκΈ°ν μ¬κ°
POST http://localhost:8080/api/v1/etl/sync/resumeμλ λ°©μ:
- Web3jλ₯Ό ν΅ν΄ Ethereum RPCμμ λΈλ‘ λ°μ΄ν° μ‘°ν
- Kafkaλ‘ λΈλ‘/νΈλμμ μ΄λ²€νΈ λ°ν
- GraphBuilderServiceκ° μ΄λ²€νΈλ₯Ό μλΉνμ¬ Neo4jμ κ·Έλν ꡬμΆ
- PostgreSQLμ 체ν¬ν¬μΈνΈ λ° λ©νλ°μ΄ν° μ μ₯
Neo4j κ·Έλν λ°μ΄ν°λ² μ΄μ€λ₯Ό νμ©ν κ³ κΈ λΆμ κΈ°λ₯μ μ 곡ν©λλ€.
# μ£Όμ μ 보 μ‘°ν
GET /api/v1/graph/address/0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb
# μ°κ΄ μ£Όμ μ°ΎκΈ° (2-hop)
GET /api/v1/graph/address/{address}/connected?maxHops=2# λ μ£Όμ κ° μκΈ μ΄λ λΆμ
GET /api/v1/graph/flow?fromAddress=0x...&toAddress=0x...# μν°ν° μ 보 μ‘°ν (κ±°λμ, DeFi νλ‘ν μ½ λ±)
GET /api/v1/graph/entity/{entityId}
# μν°ν°κ° μμ ν λͺ¨λ μ£Όμ
GET /api/v1/graph/entity/{entityId}/addressesλΈλ‘μ²΄μΈ μ£Όμμ μκΈμΈν μνλλ₯Ό μλμΌλ‘ νκ°ν©λλ€.
# μ£Όμ μνλ νκ°
POST /api/v1/aml/evaluate/0x...
# κ³ μν μ£Όμ λͺ©λ‘
GET /api/v1/aml/high-risk?minScore=70.0μνλ νκ° μμ:
- λ―Ήμ/ν λΈλ¬ μ¬μ© μ¬λΆ
- μ μ¬ λμ μν°ν° μ°κ΄μ±
- λΉμ μμ μΈ κ±°λ ν¨ν΄
- κ³ μν μ£Όμμμ μ°κ²°μ±
- κ³μ νλ κΈ°κ° λ° κ±°λλ
| μΉ΄ν κ³ λ¦¬ | κΈ°μ |
|---|---|
| μΈμ΄ | Kotlin 2.2.21 |
| νλ μμν¬ | Spring Boot 4.0.1 |
| κ·Έλν DB | Neo4j 5.15.0 |
| κ΄κ³ν DB | PostgreSQL 16 |
| λ©μμ§ λΈλ‘컀 | Apache Kafka 7.5.3 |
| μΊμ | Redis 7.2 |
| λΈλ‘μ²΄μΈ | Web3j 4.10.3 (Ethereum) |
| API λ¬Έμ | SpringDoc OpenAPI 3 |
(Block)-[:CONTAINS]->(Transaction)
(Transaction)-[:FROM]->(Address)
(Transaction)-[:TO]->(Address)
(Address)-[:BELONGS_TO]->(Entity)
(Entity)-[:OWNS]->(Address)
λ Έλ νμ :
Block: λΈλ‘ μ 보Transaction: νΈλμμ Address: μ£Όμ (EOA, Contract, Exchange, DeFi λ±)Entity: μ€μ μμ μ/μ‘°μ§Contract: μ€λ§νΈ 컨νΈλνΈToken: ERC20/ERC721 ν ν°
// Neo4j: νΉμ κ±°λμλ‘μ λκ·λͺ¨ μ
κΈ μ°ΎκΈ°
MATCH (from:Address)-[:SENT]->(tx:Transaction)-[:TO]->(exchange:Address)
WHERE exchange.label = 'Binance Hot Wallet'
AND toFloat(tx.value) > 100000000000000000000 // > 100 ETH
AND tx.timestamp >= datetime('2024-01-01')
RETURN from, tx, exchange
ORDER BY tx.value DESC// Neo4j: λ μ΄μ΄λ§ ν¨ν΄ νμ§ (λ€λ¨κ³ μκΈ λΆμ°)
MATCH path = (start:Address)-[:SENT*3..5]->(end:Address)
WHERE start.address = '0x...'
AND ALL(r IN relationships(path) WHERE toFloat(r.value) > 1000000000000000000)
RETURN path// Neo4j: Uniswap μ¬μ©μ μ€ κ³ μ‘ κ±°λμ μ°ΎκΈ°
MATCH (user:Address)-[:SENT]->(tx:Transaction)-[:TO]->(uniswap:Address)
WHERE uniswap.label CONTAINS 'Uniswap'
WITH user, SUM(toFloat(tx.value)) as totalVolume
WHERE totalVolume > 10000000000000000000 // > 10 ETH
RETURN user, totalVolume
ORDER BY totalVolume DESC- Redis μΊμ±: μμ£Ό μ‘°νλλ μ£Όμ μ 보 λ° κ·Έλν 쿼리 κ²°κ³Ό μΊμ±
- λ°°μΉ μ²λ¦¬: 100λΈλ‘ λ¨μ λ°°μΉλ‘ ETL μ²λ¦¬
- Circuit Breaker: Resilience4jλ₯Ό νμ©ν RPC μ₯μ λμ
- λΉλκΈ° μ²λ¦¬: Coroutine κΈ°λ° λΉλκΈ° ETL νμ΄νλΌμΈ
# Health Check
GET /actuator/health
# Metrics (Prometheus)
GET /actuator/prometheus
# ETL μν νμΈ
GET /api/v1/etl/status./gradlew test./gradlew buildjava -jar build/libs/blocketl-0.0.1-SNAPSHOT.jarApache License 2.0
β Kotlin/Spring Boot κΈ°λ° λ°±μλ κ°λ° β Neo4j κ·Έλν λ°μ΄ν°λ² μ΄μ€ νμ© β λΈλ‘μ²΄μΈ ETL νμ΄νλΌμΈ κ΅¬μΆ β μ¨ν¨λ‘μ§ λͺ¨λΈλ§ (μν°ν°, κ΄κ³ μ μ) β AML λΆμ κΈ°λ₯ ꡬν β Kafka μ΄λ²€νΈ μ€νΈλ¦¬λ°
- ETL νμ΄νλΌμΈ: "λ Έλ/ETLμ΄ λ§λ κ·Έλν/μ§μ λ μ΄μ΄"λ₯Ό μ΄λ»κ² ꡬμΆνλμ§ μ€λͺ
- κ·Έλν νμ©: "κ·ΈλνDBλ₯Ό μ΄λ»κ² Spring κΈ°λ° μλΉμ€λ‘ κ°μ μ μλμ§" - Cypher 쿼리, Repository ν¨ν΄
- μ€λ¬΄ μ μ©μ±: AML, μ£Όμ λΌλ²¨λ§, μν°ν° ν΄λ¬μ€ν°λ§ λ± μ€μ μ μ€μΌμ΄μ€