Ver3.5 ๐๏ธ ๋ผ๋ผ๋ฒจ(Laravel)๊ณผ ํ์ด์ฌ(Python)์ ํตํฉํDAO ๋ฐฑ์๋ ๊ตฌ์กฐ ์ค๊ณํ๊ธฐ โ Web3 ์๋์ ํ์ด๋ธ๋ฆฌ๋ ์ํคํ ์ฒ ์์ ์ ๋ณต

๐๏ธ ๋ผ๋ผ๋ฒจ(Laravel)๊ณผ ํ์ด์ฌ(Python)์ ํตํฉํ
DAO ๋ฐฑ์๋ ๊ตฌ์กฐ ์ค๊ณํ๊ธฐ โ Web3 ์๋์ ํ์ด๋ธ๋ฆฌ๋ ์ํคํ
์ฒ ์์ ์ ๋ณต
Laravel ร Python ร DAO ร Blockchain Backend Architecture
๐ ๋ค์ด๊ฐ๋ฉฐ โ ์ Laravel + Python + DAO ์กฐํฉ์ธ๊ฐ?
์ผ, ์์งํ ๋งํ์. ์ฒ์์ ์ด ์กฐํฉ ๋ค์์ ๋ "์ด๊ฒ ๋ญ ์กฐํฉ์ด์ผ?" ์ถ์์ง? ๐
Laravel์ PHP ๊ธฐ๋ฐ ์น ํ๋ ์์ํฌ๊ณ , Python์ ๋ฐ์ดํฐ/AI/๋ธ๋ก์ฒด์ธ ์ชฝ์์ ๊ฐ์ธ์ธ ์ธ์ด๊ณ , DAO๋ ํ์ค์ํ ์์จ ์กฐ์ง(Decentralized Autonomous Organization)์ด์์.
๊ทผ๋ฐ ์ด ์ธ ๊ฐ์ง๋ฅผ ํฉ์น๋ฉด ์ง์ง ๊ฐ๋ ฅํ Web3 ๋ฐฑ์๋ ์ํคํ
์ฒ๊ฐ ํ์ํ๋ค๋ ๊ฑฐ, ์ง๊ธ๋ถํฐ ๊ฐ์ด ํํค์ณ๋ณด์!
DAO ํ๋ก์ ํธ๋ฅผ ๋ง๋ค ๋ ๊ฐ์ฅ ํํ ์ค์๊ฐ ๋ญ์ง ์์? ๋ชจ๋ ๊ฑธ ํ๋์ ์ธ์ด, ํ๋์ ํ๋ ์์ํฌ๋ก ํด๊ฒฐํ๋ ค๋ ๊ฑฐ์ผ. ๊ทผ๋ฐ ํ์ค์ ๋ฌ๋ผ.
์ฌ์ฉ์ ์ธ์ฆ, ๊ถํ ๊ด๋ฆฌ, ์คํ์ฒด์ธ ๋ฐ์ดํฐ ๊ด๋ฆฌ๋ Laravel์ด ์๋์ ์ผ๋ก ์ํ๊ณ ,
Web3.py๋ฅผ ํตํ ์ด๋๋ฆฌ์ ์ฐ๋, ์ค๋งํธ ์ปจํธ๋ํธ ์ด๋ฒคํธ ๋ฆฌ์ค๋, ์จ์ฒด์ธ ๋ฐ์ดํฐ ๋ถ์์ Python์ด ํจ์ฌ ์ํ๊ณ๊ฐ ํ๋ถํ๊ฑฐ๋ .
๊ทธ๋์ ์ด ๊ธ์์๋ ๋ ๊ธฐ์ ์ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ ๋ฐฉ์์ผ๋ก ํตํฉํด์ ์์ฑ๋ ๋์ DAO ๋ฐฑ์๋๋ฅผ ์ค๊ณํ๋ ๋ฐฉ๋ฒ์ ์ฒ์๋ถํฐ ๋๊น์ง ๋ค๋ฃฐ ๊ฑฐ์ผ. ์ฝ๋๋ ์ค์ ๋ก ์ธ ์ ์๋ ์์ค์ผ๋ก ๋ณด์ฌ์ค ํ ๋๊น ์ง์คํด! ๐ช
๐๏ธ DAO๋ ๋ฌด์์ธ๊ฐ? โ ๋ฐฑ์๋ ์ค๊ณ ์ ์ ๊ฐ๋ ๋ถํฐ ์ก์
DAO๋ฅผ ๋ชจ๋ฅด๋ฉด ๋ฐฑ์๋๋ฅผ ์ค๊ณํ ์๊ฐ ์์ด. ๊ทธ๋ฌ๋๊น ์ ๊น ๊ฐ๋ ์ ๋ฆฌํ๊ณ ๊ฐ์.
DAO์ ํต์ฌ ๊ตฌ์ฑ ์์
ERC-20 ๊ธฐ๋ฐ์ ํฌํ๊ถ ํ ํฐ. ํ ํฐ์ ๋ง์ด ๊ฐ์ง์๋ก ํฌํ ์ํฅ๋ ฅ์ด ์ปค์ง๋ค. ์: Compound์ COMP, Uniswap์ UNI.
๋ฉค๋ฒ๋ค์ด ์ ์์ ์ฌ๋ฆฌ๊ณ , ์ผ์ ๊ธฐ๊ฐ ๋์ ํฌํ๊ฐ ์งํ๋๋ค. ์จ์ฒด์ธ์ ๊ธฐ๋ก๋๋ฉฐ ๋ณ๊ฒฝ ๋ถ๊ฐ๋ฅํ๋ค.
For/Against/Abstain ํฌํ. ์ฟผ๋ผ(์ต์ ์ฐธ์ฌ์จ)์ ์ถฉ์กฑํด์ผ ํต๊ณผ๋๋ค. ํ์๋ฝ(Timelock)์ผ๋ก ์คํ ์ง์ฐ.
DAO๊ฐ ๊ด๋ฆฌํ๋ ์๊ธ. ํ๋กํฌ์ ์ด ํต๊ณผ๋๋ฉด ์๋์ผ๋ก ์คํ๋๋ ์ค๋งํธ ์ปจํธ๋ํธ๊ฐ ์๊ธ์ ์งํํ๋ค.
ํ๋กํฌ์ ํต๊ณผ ํ ์ค์ ์คํ๊น์ง ์ผ์ ์๊ฐ ์ง์ฐ. ์ ์์ ์ธ ๊ฑฐ๋ฒ๋์ค ๊ณต๊ฒฉ ๋ฐฉ์ง์ฉ.
์คํ์ฒด์ธ vs ์จ์ฒด์ธ โ ๋ฌด์์ ์ด๋์ ์ ์ฅํ ๊น?
| ํญ๋ชฉ | ์จ์ฒด์ธ (์ค๋งํธ ์ปจํธ๋ํธ) | ์คํ์ฒด์ธ (Laravel/Python) |
|---|---|---|
| ํฌํ ๊ฒฐ๊ณผ | โ ์ต์ข ๊ฒฐ๊ณผ ๊ธฐ๋ก | โ ์ค์๊ฐ ์ง๊ณ ์บ์ฑ |
| ํ๋กํฌ์ ๋ด์ฉ | โ ํด์๊ฐ๋ง ์ ์ฅ | โ ์ ์ฒด ํ ์คํธ/๋ฉํ๋ฐ์ดํฐ |
| ์ฌ์ฉ์ ํ๋กํ | โ ๋น์ฉ ๊ณผ๋ค | โ DB์ ์ ์ฅ |
| ํ ํฐ ์์ก | โ ์๋ณธ ๋ฐ์ดํฐ | โ ์ธ๋ฑ์ฑ/์บ์ฑ |
| ์๋ฆผ/์ด๋ฉ์ผ | โ ๋ถ๊ฐ | โ ์ด๋ฒคํธ ๋ฆฌ์ค๋ ํ ์ฒ๋ฆฌ |
| ๋ถ์ ๋ฐ์ดํฐ | โ ์ฟผ๋ฆฌ ๋ถํธ | โ ์ธ๋ฑ์๋ก ์์ง ํ ๋ถ์ |
๐บ๏ธ ์ ์ฒด ์์คํ ์ํคํ ์ฒ ์ค๊ณ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์ค๊ณ ๋ค์ด๊ฐ๋ณด์. ํฐ ๊ทธ๋ฆผ๋ถํฐ ๊ทธ๋ ค์ผ ์ธ๋ถ ๊ตฌํ์ด ํ๋ค๋ฆฌ์ง ์์.
๋ง์ดํฌ๋ก์๋น์ค ๊ตฌ์ฑ๋
๊ฐ ์๋น์ค์ ์ญํ ์ ๋ช ํํ๊ฒ ๋ถ๋ฆฌํ๋ ๊ฒ ํต์ฌ์ด์ผ. ์๋ก ์ง์ ํธ์ถํ์ง ์๊ณ ๋ฉ์์ง ํ๋ฅผ ํตํด ๋น๋๊ธฐ ํต์ ํ๋ ๊ตฌ์กฐ์ผ.
โข JWT/Sanctum ์ธ์ฆ ์ฒ๋ฆฌ
โข ์ฌ์ฉ์/๋ฉค๋ฒ ๊ด๋ฆฌ (MySQL)
โข ํ๋กํฌ์ ๋ฉํ๋ฐ์ดํฐ CRUD
โข ์คํ์ฒด์ธ ํฌํ ์ง๊ณ ์บ์ฑ
โข ์ด๋ฉ์ผ/์๋ฆผ ๋ฐ์ก
โข ๊ด๋ฆฌ์ ๋์๋ณด๋ API
โข ํ์ผ/IPFS ๋ฉํ๋ฐ์ดํฐ ๊ด๋ฆฌ
โข ์ค๋งํธ ์ปจํธ๋ํธ ์ด๋ฒคํธ ๋ฆฌ์ค๋
โข ์จ์ฒด์ธ ํธ๋์ญ์ ์๋ช /์ ์ก
โข ํ ํฐ ์์ก ์ธ๋ฑ์ฑ
โข ํฌํ ์ง๊ณ ์จ์ฒด์ธ ๊ฒ์ฆ
โข ๊ฑฐ๋ฒ๋์ค ๋ถ์ (AI/ML)
โข The Graph ์ฟผ๋ฆฌ ์ฒ๋ฆฌ
โข ๊ฐ์ค๋น ์ต์ ํ ๋ก์ง
Docker Compose ๊ธฐ๋ฐ ์๋น์ค ๊ตฌ์ฑ
์ค์ ํ๋ก์ ํธ์์๋ Docker Compose๋ก ๋ชจ๋ ์๋น์ค๋ฅผ ํ ๋ฒ์ ๊ด๋ฆฌํด. ์๋๊ฐ ๊ธฐ๋ณธ ๊ตฌ์กฐ์ผ:
# docker-compose.yml
version: '3.8'
services:
# Laravel API ์๋ฒ
laravel-api:
build: ./laravel
ports: ["8000:8000"]
environment:
- DB_HOST=mysql
- REDIS_HOST=redis
- PYTHON_SERVICE_URL=http://python-worker:5000
depends_on: [mysql, redis]
# Python Web3 Worker
python-worker:
build: ./python
ports: ["5000:5000"]
environment:
- WEB3_PROVIDER_URL=https://mainnet.infura.io/v3/YOUR_KEY
- REDIS_URL=redis://redis:6379
- CONTRACT_ADDRESS=0x...
depends_on: [redis]
# MySQL ๋ฐ์ดํฐ๋ฒ ์ด์ค
mysql:
image: mysql:8.0
volumes: ["mysql_data:/var/lib/mysql"]
environment:
MYSQL_DATABASE: dao_backend
# Redis (์บ์ + ๋ฉ์์ง ๋ธ๋ก์ปค)
redis:
image: redis:7-alpine
ports: ["6379:6379"]
# Nginx ๋ฆฌ๋ฒ์ค ํ๋ก์
nginx:
image: nginx:alpine
ports: ["80:80", "443:443"]
depends_on: [laravel-api, python-worker]
๐ฏ Laravel ๋ฐฑ์๋ ๊ตฌํ โ ์คํ์ฒด์ธ ๋ ์ด์ด ์ค๊ณ
Laravel ์ชฝ ๊ตฌํ๋ถํฐ ์์ํ์. ์ฌ๊ธฐ์ ์ค์ํ ๊ฑด ๋ธ๋ก์ฒด์ธ ๋ก์ง์ ์ง์ ์ฒ๋ฆฌํ์ง ์๊ณ , ์คํ์ฒด์ธ ๋ฐ์ดํฐ ๊ด๋ฆฌ์ Python ์๋น์ค๋ก์ ์์ ์์์ ์ง์คํ๋ ๊ฑฐ์ผ.
1. ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์ ์ค๊ณ
DAO ๋ฐฑ์๋์์ ์คํ์ฒด์ธ์ผ๋ก ๊ด๋ฆฌํด์ผ ํ ํ ์ด๋ธ๋ค์ด์ผ:
// database/migrations/create_proposals_table.php
Schema::create('proposals', function (Blueprint $table) {
$table->id();
$table->string('onchain_id')->nullable()->index(); // ์จ์ฒด์ธ ํ๋กํฌ์ ID
$table->string('title');
$table->text('description'); // IPFS์ ์ฌ๋ฆฌ๊ธฐ ์ ์คํ์ฒด์ธ ์ ์ฅ
$table->string('ipfs_hash')->nullable(); // IPFS CID
$table->string('proposer_address', 42)->index(); // ์ด๋๋ฆฌ์ ์ฃผ์
$table->foreignId('proposer_id')->constrained('members');
$table->enum('status', [
'draft', 'pending', 'active', 'succeeded',
'defeated', 'queued', 'executed', 'cancelled'
])->default('draft');
$table->string('tx_hash')->nullable(); // ์จ์ฒด์ธ ์ ์ถ ํธ๋์ญ์
$table->bigInteger('for_votes')->default(0);
$table->bigInteger('against_votes')->default(0);
$table->bigInteger('abstain_votes')->default(0);
$table->integer('quorum_required');
$table->timestamp('voting_start_at')->nullable();
$table->timestamp('voting_end_at')->nullable();
$table->timestamp('execution_eta')->nullable();
$table->json('actions')->nullable(); // ์คํํ ์ปจํธ๋ํธ ์ก์
๋ค
$table->timestamps();
});
// create_votes_table.php
Schema::create('votes', function (Blueprint $table) {
$table->id();
$table->foreignId('proposal_id')->constrained();
$table->string('voter_address', 42)->index();
$table->enum('support', ['for', 'against', 'abstain']);
$table->decimal('voting_power', 36, 18); // ํ ํฐ ๊ธฐ๋ฐ ํฌํ๋ ฅ
$table->string('tx_hash')->nullable();
$table->string('reason')->nullable();
$table->boolean('is_onchain_verified')->default(false);
$table->timestamps();
$table->unique(['proposal_id', 'voter_address']);
});
// create_members_table.php
Schema::create('members', function (Blueprint $table) {
$table->id();
$table->string('wallet_address', 42)->unique();
$table->string('ens_name')->nullable(); // ENS ๋๋ฉ์ธ
$table->string('display_name')->nullable();
$table->string('email')->nullable();
$table->string('avatar_url')->nullable();
$table->decimal('token_balance', 36, 18)->default(0);
$table->decimal('delegated_votes', 36, 18)->default(0);
$table->string('delegate_address', 42)->nullable();
$table->integer('proposals_created')->default(0);
$table->integer('votes_cast')->default(0);
$table->timestamp('last_activity_at')->nullable();
$table->timestamps();
});
2. ์ง๊ฐ ์๋ช ๊ธฐ๋ฐ ์ธ์ฆ ์์คํ
DAO์์๋ ์ด๋ฉ์ผ/๋น๋ฐ๋ฒํธ ๋์ ์ด๋๋ฆฌ์ ์ง๊ฐ ์๋ช (SIWE - Sign-In with Ethereum)์ผ๋ก ์ธ์ฆํด. Laravel์์ ์ด๊ฑธ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ด์ผ:
// app/Http/Controllers/AuthController.php
class AuthController extends Controller
{
public function getNonce(Request $request): JsonResponse
{
$address = strtolower($request->wallet_address);
$nonce = Str::random(32);
// Redis์ 5๋ถ๊ฐ nonce ์ ์ฅ
Cache::put("siwe_nonce:{$address}", $nonce, 300);
return response()->json([
'nonce' => $nonce,
'message' => "DAO์ ๋ก๊ทธ์ธํฉ๋๋ค.\n\n์ฃผ์: {$address}\nNonce: {$nonce}"
]);
}
public function verifySignature(Request $request): JsonResponse
{
$address = strtolower($request->wallet_address);
$signature = $request->signature;
$message = $request->message;
// Redis์์ nonce ํ์ธ
$storedNonce = Cache::get("siwe_nonce:{$address}");
if (!$storedNonce || !str_contains($message, $storedNonce)) {
return response()->json(['error' => '์ ํจํ์ง ์์ nonce'], 401);
}
// Python ์๋น์ค์ ์๋ช
๊ฒ์ฆ ์์
$verified = $this->pythonService->verifyEthSignature(
$address, $message, $signature
);
if (!$verified) {
return response()->json(['error' => '์๋ช
๊ฒ์ฆ ์คํจ'], 401);
}
// ๋ฉค๋ฒ ์กฐํ ๋๋ ์์ฑ
$member = Member::firstOrCreate(
['wallet_address' => $address],
['display_name' => substr($address, 0, 6) . '...' . substr($address, -4)]
);
// Sanctum ํ ํฐ ๋ฐ๊ธ
$token = $member->createToken('dao-session')->plainTextToken;
Cache::forget("siwe_nonce:{$address}");
return response()->json(['token' => $token, 'member' => $member]);
}
}
3. ํ๋กํฌ์ ์๋น์ค โ IPFS ์ฐ๋
ํ๋กํฌ์ ์ ์์ธ ๋ด์ฉ์ IPFS์ ์ ์ฅํ๊ณ , ์จ์ฒด์ธ์๋ IPFS ํด์๋ง ์ฌ๋ ค. Laravel์์ IPFS ์ ๋ก๋๋ฅผ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ด์ผ:
// app/Services/ProposalService.php
class ProposalService
{
public function __construct(
private PythonServiceClient $pythonClient,
private IpfsService $ipfsService
) {}
public function createProposal(array $data, Member $proposer): Proposal
{
// 1. IPFS์ ๋ฉํ๋ฐ์ดํฐ ์
๋ก๋
$metadata = [
'title' => $data['title'],
'description' => $data['description'],
'actions' => $data['actions'],
'proposer' => $proposer->wallet_address,
'created_at' => now()->toIso8601String(),
];
$ipfsHash = $this->ipfsService->uploadJson($metadata);
// 2. DB์ ์คํ์ฒด์ธ ๋ฐ์ดํฐ ์ ์ฅ
$proposal = Proposal::create([
'title' => $data['title'],
'description' => $data['description'],
'ipfs_hash' => $ipfsHash,
'proposer_address' => $proposer->wallet_address,
'proposer_id' => $proposer->id,
'status' => 'pending',
'actions' => $data['actions'],
'quorum_required' => config('dao.quorum_percentage'),
]);
// 3. Python ์๋น์ค์ ์จ์ฒด์ธ ์ ์ถ ์์
ํ์
SubmitProposalJob::dispatch($proposal->id, $ipfsHash, $data['actions']);
return $proposal;
}
public function syncOnchainStatus(int $proposalId): void
{
// Python ์๋น์ค์์ ์จ์ฒด์ธ ์ํ ์กฐํ
$onchainData = $this->pythonClient->getProposalState($proposalId);
Proposal::where('id', $proposalId)->update([
'status' => $this->mapOnchainStatus($onchainData['state']),
'for_votes' => $onchainData['for_votes'],
'against_votes' => $onchainData['against_votes'],
'abstain_votes' => $onchainData['abstain_votes'],
]);
// Redis ์บ์ ๊ฐฑ์
Cache::put("proposal_status:{$proposalId}", $onchainData, 60);
}
}
4. Python ์๋น์ค ํด๋ผ์ด์ธํธ โ HTTP + Redis ์ด์ค ํต์
Laravel์์ Python ์๋น์ค์ ํต์ ํ๋ ๋ฐฉ๋ฒ์ ๋ ๊ฐ์ง์ผ. ๋๊ธฐ HTTP ์์ฒญ๊ณผ ๋น๋๊ธฐ Redis ํ. ์ํฉ์ ๋ฐ๋ผ ๊ณจ๋ผ ์จ์ผ ํด:
// app/Services/PythonServiceClient.php
class PythonServiceClient
{
private string $baseUrl;
public function __construct()
{
$this->baseUrl = config('services.python.url', 'http://python-worker:5000');
}
// ๋๊ธฐ ๋ฐฉ์: ์ฆ์ ์๋ต์ด ํ์ํ ๊ฒฝ์ฐ
public function verifyEthSignature(string $address, string $message, string $sig): bool
{
$response = Http::timeout(10)->post("{$this->baseUrl}/verify-signature", [
'address' => $address,
'message' => $message,
'signature' => $sig,
]);
return $response->successful() && $response->json('verified') === true;
}
// ๋๊ธฐ ๋ฐฉ์: ํ ํฐ ์์ก ์กฐํ
public function getTokenBalance(string $address): string
{
$cacheKey = "token_balance:{$address}";
return Cache::remember($cacheKey, 30, function () use ($address) {
$response = Http::timeout(15)->get("{$this->baseUrl}/token-balance/{$address}");
return $response->json('balance', '0');
});
}
// ๋น๋๊ธฐ ๋ฐฉ์: Redis ํ๋ฅผ ํตํ ์์
์์
public function queueBlockchainJob(string $jobType, array $payload): string
{
$jobId = Str::uuid()->toString();
Redis::lpush('blockchain_jobs', json_encode([
'job_id' => $jobId,
'type' => $jobType,
'payload' => $payload,
'created_at' => now()->timestamp,
]));
return $jobId; // ํด๋ผ์ด์ธํธ์ job_id ๋ฐํ, ํด๋ง์ผ๋ก ์ํ ํ์ธ
}
// ์์
์ํ ํ์ธ
public function getJobStatus(string $jobId): array
{
$result = Redis::get("job_result:{$jobId}");
return $result ? json_decode($result, true) : ['status' => 'pending'];
}
}
๐ Python Web3 ์๋น์ค ๊ตฌํ โ ์จ์ฒด์ธ ๋ ์ด์ด ์ค๊ณ
์ด์ ์ง์ง ์ฌ๋ฐ๋ ํํธ์ผ! Python์ผ๋ก ๋ธ๋ก์ฒด์ธ์ด๋ ์ง์ ๋ํํ๋ ๋ถ๋ถ์ด๊ฑฐ๋ . ๐ฅ
Python ์๋น์ค ๊ธฐ์ ์คํ
FastAPI HTTP API ์๋ฒ
celery ๋น๋๊ธฐ ์์ ์ฒ๋ฆฌ
redis-py Redis ์ฐ๋
eth-account ์๋ช ๊ฒ์ฆ
eth-abi ABI ์ธ์ฝ๋ฉ/๋์ฝ๋ฉ
pydantic ๋ฐ์ดํฐ ๊ฒ์ฆ
sqlalchemy DB ์ฐ๋
python-dotenv ํ๊ฒฝ๋ณ์
gql The Graph ์ฟผ๋ฆฌ
pandas ๊ฑฐ๋ฒ๋์ค ๋ถ์
1. FastAPI ๋ฉ์ธ ์๋ฒ ๊ตฌ์กฐ
# python/main.py
from fastapi import FastAPI, HTTPException
from web3 import Web3
from eth_account.messages import encode_defunct
from eth_account import Account
import redis
import json
import os
from contextlib import asynccontextmanager
# Web3 ์ฐ๊ฒฐ ์ด๊ธฐํ
w3 = Web3(Web3.HTTPProvider(os.getenv('WEB3_PROVIDER_URL')))
redis_client = redis.from_url(os.getenv('REDIS_URL', 'redis://localhost:6379'))
@asynccontextmanager
async def lifespan(app: FastAPI):
# ์์ ์ ๋ธ๋ก์ฒด์ธ ์ฐ๊ฒฐ ํ์ธ
if not w3.is_connected():
raise Exception("๋ธ๋ก์ฒด์ธ ์ฐ๊ฒฐ ์คํจ!")
print(f"โ
๋ธ๋ก์ฒด์ธ ์ฐ๊ฒฐ ์ฑ๊ณต - ํ์ฌ ๋ธ๋ก: {w3.eth.block_number}")
yield
print("๐ด Python ์๋น์ค ์ข
๋ฃ")
app = FastAPI(title="DAO Python Service", lifespan=lifespan)
# ๋ผ์ฐํฐ ๋ฑ๋ก
from routers import signature, token, proposal, governance
app.include_router(signature.router, prefix="/api")
app.include_router(token.router, prefix="/api")
app.include_router(proposal.router, prefix="/api")
app.include_router(governance.router, prefix="/api")
2. ์๋ช ๊ฒ์ฆ ๋ผ์ฐํฐ
# python/routers/signature.py
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from eth_account.messages import encode_defunct
from eth_account import Account
from web3 import Web3
router = APIRouter()
class SignatureVerifyRequest(BaseModel):
address: str
message: str
signature: str
@router.post("/verify-signature")
async def verify_signature(req: SignatureVerifyRequest):
try:
# EIP-191 ๋ฉ์์ง ํด์ ์์ฑ
message_hash = encode_defunct(text=req.message)
# ์๋ช
์์ ์ฃผ์ ๋ณต๊ตฌ
recovered_address = Account.recover_message(
message_hash,
signature=req.signature
)
# ์ฃผ์ ๋น๊ต (๋์๋ฌธ์ ๋ฌด์)
verified = recovered_address.lower() == req.address.lower()
return {"verified": verified, "recovered_address": recovered_address}
except Exception as e:
raise HTTPException(status_code=400, detail=f"์๋ช
๊ฒ์ฆ ์ค๋ฅ: {str(e)}")
@router.post("/verify-typed-data")
async def verify_typed_data(req: dict):
"""EIP-712 ํ์
๋ ๋ฐ์ดํฐ ์๋ช
๊ฒ์ฆ (๋ฉํํธ๋์ญ์
์ฉ)"""
try:
from eth_account._utils.structured_data.hashing import hash_domain
# EIP-712 ๊ฒ์ฆ ๋ก์ง
structured_data = req.get('structured_data')
signature = req.get('signature')
recovered = Account.recover_message(
encode_defunct(hexstr=Web3.keccak(text=str(structured_data)).hex()),
signature=signature
)
return {"verified": True, "signer": recovered}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
3. ์ค๋งํธ ์ปจํธ๋ํธ ์ฐ๋ โ Governor ์ปจํธ๋ํธ
OpenZeppelin์ Governor ์ปจํธ๋ํธ์ ์ฐ๋ํ๋ ํต์ฌ ์ฝ๋์ผ. ์ด๊ฒ DAO์ ์ฌ์ฅ์ด๋ผ๊ณ ๋ณผ ์ ์์ด:
# python/services/governor_service.py
from web3 import Web3
import json
import os
# Governor ABI (ํต์ฌ ํจ์๋ค๋ง)
GOVERNOR_ABI = json.loads('''[
{"name":"propose","type":"function","inputs":[
{"name":"targets","type":"address[]"},
{"name":"values","type":"uint256[]"},
{"name":"calldatas","type":"bytes[]"},
{"name":"description","type":"string"}
],"outputs":[{"name":"","type":"uint256"}]},
{"name":"castVote","type":"function","inputs":[
{"name":"proposalId","type":"uint256"},
{"name":"support","type":"uint8"}
],"outputs":[{"name":"","type":"uint256"}]},
{"name":"state","type":"function","inputs":[
{"name":"proposalId","type":"uint256"}
],"outputs":[{"name":"","type":"uint8"}]},
{"name":"proposalVotes","type":"function","inputs":[
{"name":"proposalId","type":"uint256"}
],"outputs":[
{"name":"againstVotes","type":"uint256"},
{"name":"forVotes","type":"uint256"},
{"name":"abstainVotes","type":"uint256"}
]}
]''')
class GovernorService:
# ํ๋กํฌ์ ์ํ ๋งคํ
PROPOSAL_STATES = {
0: "pending", 1: "active", 2: "cancelled",
3: "defeated", 4: "succeeded", 5: "queued",
6: "expired", 7: "executed"
}
def __init__(self, w3: Web3):
self.w3 = w3
self.contract = w3.eth.contract(
address=Web3.to_checksum_address(os.getenv('GOVERNOR_ADDRESS')),
abi=GOVERNOR_ABI
)
def get_proposal_state(self, proposal_id: int) -> dict:
"""์จ์ฒด์ธ ํ๋กํฌ์ ์ํ ์กฐํ"""
state_code = self.contract.functions.state(proposal_id).call()
votes = self.contract.functions.proposalVotes(proposal_id).call()
return {
"state": self.PROPOSAL_STATES.get(state_code, "unknown"),
"state_code": state_code,
"against_votes": str(votes[0]),
"for_votes": str(votes[1]),
"abstain_votes": str(votes[2]),
}
def submit_proposal(
self,
targets: list,
values: list,
calldatas: list,
description: str,
private_key: str # ์ค์ ๋ก๋ HSM์ด๋ KMS ์ฌ์ฉ ๊ถ์ฅ!
) -> str:
"""์จ์ฒด์ธ ํ๋กํฌ์ ์ ์ถ"""
account = self.w3.eth.account.from_key(private_key)
# ๊ฐ์ค ์ถ์
gas_estimate = self.contract.functions.propose(
targets, values, calldatas, description
).estimate_gas({'from': account.address})
# ํธ๋์ญ์
๋น๋
tx = self.contract.functions.propose(
targets, values, calldatas, description
).build_transaction({
'from': account.address,
'gas': int(gas_estimate * 1.2), # 20% ๋ฒํผ
'gasPrice': self.w3.eth.gas_price,
'nonce': self.w3.eth.get_transaction_count(account.address),
})
# ์๋ช
๋ฐ ์ ์ก
signed_tx = self.w3.eth.account.sign_transaction(tx, private_key)
tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction)
# ์์์ฆ ๋๊ธฐ
receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
return tx_hash.hex()
4. ์ด๋ฒคํธ ๋ฆฌ์ค๋ โ ์จ์ฒด์ธ ์ด๋ฒคํธ ์ค์๊ฐ ๊ฐ์ง
์ด๊ฒ ์ง์ง ํต์ฌ์ด์ผ! ๋ธ๋ก์ฒด์ธ์์ ๋ฐ์ํ๋ ์ด๋ฒคํธ๋ฅผ ์ค์๊ฐ์ผ๋ก ๊ฐ์งํด์ ์คํ์ฒด์ธ DB๋ฅผ ์ ๋ฐ์ดํธํ๋ ๋ก์ง์ด๊ฑฐ๋ :
# python/workers/event_listener.py
import asyncio
from web3 import Web3, AsyncWeb3
from web3.middleware import geth_poa_middleware
import httpx
import redis
import json
import logging
logger = logging.getLogger(__name__)
class DAOEventListener:
def __init__(self, w3: Web3, redis_client, laravel_url: str):
self.w3 = w3
self.redis = redis_client
self.laravel_url = laravel_url
self.governor_contract = self._load_governor_contract()
self.token_contract = self._load_token_contract()
async def listen_proposal_created(self):
"""ProposalCreated ์ด๋ฒคํธ ๋ฆฌ์ค๋"""
event_filter = self.governor_contract.events.ProposalCreated.create_filter(
fromBlock='latest'
)
while True:
try:
events = event_filter.get_new_entries()
for event in events:
await self._handle_proposal_created(event)
await asyncio.sleep(12) # ์ด๋๋ฆฌ์ ๋ธ๋ก ์๊ฐ ~12์ด
except Exception as e:
logger.error(f"์ด๋ฒคํธ ๋ฆฌ์ค๋ ์ค๋ฅ: {e}")
await asyncio.sleep(30)
async def _handle_proposal_created(self, event):
"""ProposalCreated ์ด๋ฒคํธ ์ฒ๋ฆฌ"""
args = event['args']
proposal_id = args['proposalId']
proposer = args['proposer']
description = args['description']
tx_hash = event['transactionHash'].hex()
logger.info(f"์ ํ๋กํฌ์ ๊ฐ์ง: {proposal_id}")
# Laravel API์ ์ํ ์
๋ฐ์ดํธ ์์ฒญ
async with httpx.AsyncClient() as client:
await client.post(
f"{self.laravel_url}/internal/proposals/onchain-created",
json={
"onchain_id": str(proposal_id),
"proposer_address": proposer,
"tx_hash": tx_hash,
"start_block": args['startBlock'],
"end_block": args['endBlock'],
},
headers={"X-Internal-Secret": "your-secret-key"}
)
# Redis์ ์ด๋ฒคํธ ๋ฐํ (์ค์๊ฐ ์๋ฆผ์ฉ)
self.redis.publish('dao_events', json.dumps({
'type': 'PROPOSAL_CREATED',
'proposal_id': str(proposal_id),
'proposer': proposer,
}))
async def listen_vote_cast(self):
"""VoteCast ์ด๋ฒคํธ ๋ฆฌ์ค๋"""
event_filter = self.governor_contract.events.VoteCast.create_filter(
fromBlock='latest'
)
while True:
try:
events = event_filter.get_new_entries()
for event in events:
args = event['args']
# Laravel์ ํฌํ ๊ฒฐ๊ณผ ๋๊ธฐํ
async with httpx.AsyncClient() as client:
await client.post(
f"{self.laravel_url}/internal/votes/sync",
json={
"proposal_id": str(args['proposalId']),
"voter": args['voter'],
"support": args['support'],
"weight": str(args['weight']),
"reason": args.get('reason', ''),
},
headers={"X-Internal-Secret": "your-secret-key"}
)
await asyncio.sleep(12)
except Exception as e:
logger.error(f"VoteCast ๋ฆฌ์ค๋ ์ค๋ฅ: {e}")
await asyncio.sleep(30)
async def run_all_listeners(self):
"""๋ชจ๋ ์ด๋ฒคํธ ๋ฆฌ์ค๋ ๋์ ์คํ"""
await asyncio.gather(
self.listen_proposal_created(),
self.listen_vote_cast(),
self.listen_transfer_events(), # ํ ํฐ ์ด์ ๊ฐ์ง
self.listen_delegate_changed(), # ์์ ๋ณ๊ฒฝ ๊ฐ์ง
)
๐ Laravel โ Python ํต์ ํจํด ์ฌํ
๋ ์๋น์ค๊ฐ ์ด๋ป๊ฒ ๋ํํ๋์ง ๋ ๊น์ด ํ๋ณด์. ์ด ๋ถ๋ถ์ด ์ํคํ ์ฒ์ ํต์ฌ์ด๊ฑฐ๋ !
ํจํด 1: ๋๊ธฐ HTTP ํต์ (์ฆ์ ์๋ต ํ์)
์๋ช ๊ฒ์ฆ, ํ ํฐ ์์ก ์กฐํ์ฒ๋ผ ์ฆ์ ์๋ต์ด ํ์ํ ๊ฒฝ์ฐ์ ์ฌ์ฉํด:
// Laravel โ Python ๋๊ธฐ ํธ์ถ ์์
// app/Http/Controllers/MemberController.php
public function getTokenBalance(Request $request): JsonResponse
{
$address = $request->user()->wallet_address;
try {
// Python ์๋น์ค์์ ์ค์๊ฐ ์์ก ์กฐํ
$balance = $this->pythonClient->getTokenBalance($address);
// DB ์
๋ฐ์ดํธ (๋น๋๊ธฐ๋ก ์ฒ๋ฆฌ)
UpdateMemberBalanceJob::dispatch($address, $balance);
return response()->json([
'address' => $address,
'balance' => $balance,
'formatted' => number_format($balance / 1e18, 4) . ' GOV',
]);
} catch (ConnectionException $e) {
// Python ์๋น์ค ๋ค์ด ์ ์บ์ ํด๋ฐฑ
$cachedBalance = Cache::get("token_balance:{$address}", '0');
return response()->json([
'balance' => $cachedBalance,
'source' => 'cache', // ์บ์์์ ๊ฐ์ ธ์์์ ๋ช
์
]);
}
}
ํจํด 2: ๋น๋๊ธฐ Redis ํ ํต์ (์๊ฐ์ด ๊ฑธ๋ฆฌ๋ ์์ )
ํธ๋์ญ์ ์ ์ก์ฒ๋ผ ์๊ฐ์ด ๊ฑธ๋ฆฌ๋ ์์ ์ ํ๋ฅผ ํตํด ๋น๋๊ธฐ๋ก ์ฒ๋ฆฌํด:
# Python ์ชฝ Redis ํ ์์ปค
# python/workers/job_processor.py
import redis
import json
import asyncio
from services.governor_service import GovernorService
class BlockchainJobProcessor:
def __init__(self, redis_client, governor_service: GovernorService):
self.redis = redis_client
self.governor = governor_service
async def process_jobs(self):
"""Redis ํ์์ ์์
๊บผ๋ด์ ์ฒ๋ฆฌ"""
while True:
try:
# ๋ธ๋กํน ํ (์์
์ด ์ฌ ๋๊น์ง ๋๊ธฐ)
job_data = self.redis.brpop('blockchain_jobs', timeout=30)
if job_data:
_, job_json = job_data
job = json.loads(job_json)
await self._process_single_job(job)
except Exception as e:
print(f"์์
์ฒ๋ฆฌ ์ค๋ฅ: {e}")
await asyncio.sleep(5)
async def _process_single_job(self, job: dict):
job_id = job['job_id']
job_type = job['type']
payload = job['payload']
try:
result = None
if job_type == 'SUBMIT_PROPOSAL':
result = self.governor.submit_proposal(
targets=payload['targets'],
values=payload['values'],
calldatas=payload['calldatas'],
description=payload['description'],
private_key=payload['private_key']
)
elif job_type == 'CAST_VOTE':
result = await self._cast_vote(payload)
elif job_type == 'QUEUE_PROPOSAL':
result = await self._queue_proposal(payload)
elif job_type == 'EXECUTE_PROPOSAL':
result = await self._execute_proposal(payload)
# ๊ฒฐ๊ณผ๋ฅผ Redis์ ์ ์ฅ (Laravel์ด ํด๋ง์ผ๋ก ํ์ธ)
self.redis.setex(
f"job_result:{job_id}",
3600, # 1์๊ฐ ๋ณด๊ด
json.dumps({
'status': 'completed',
'result': result,
'completed_at': asyncio.get_event_loop().time()
})
)
except Exception as e:
self.redis.setex(
f"job_result:{job_id}",
3600,
json.dumps({'status': 'failed', 'error': str(e)})
)
ํจํด 3: WebSocket์ ํตํ ์ค์๊ฐ ์ ๋ฐ์ดํธ
Laravel์ Broadcasting ๊ธฐ๋ฅ๊ณผ Redis Pub/Sub์ ๊ฒฐํฉํด์ ์ค์๊ฐ ์ ๋ฐ์ดํธ๋ฅผ ๊ตฌํํด:
// Laravel Broadcasting ์ค์
// app/Events/ProposalStatusUpdated.php
class ProposalStatusUpdated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public Proposal $proposal
) {}
public function broadcastOn(): array
{
return [
new Channel('proposals'),
new PrivateChannel("proposal.{$this->proposal->id}"),
];
}
public function broadcastWith(): array
{
return [
'id' => $this->proposal->id,
'status' => $this->proposal->status,
'for_votes' => $this->proposal->for_votes,
'against_votes' => $this->proposal->against_votes,
'updated_at' => $this->proposal->updated_at,
];
}
}
// Python์์ Redis Pub/Sub์ผ๋ก Laravel์ ์ด๋ฒคํธ ์ ๋ฌ
# python/workers/event_broadcaster.py
class EventBroadcaster:
def __init__(self, redis_client):
self.redis = redis_client
self.pubsub = redis_client.pubsub()
def broadcast_proposal_update(self, proposal_id: int, status: str):
"""Laravel Broadcasting ์ฑ๋์ ์ด๋ฒคํธ ๋ฐํ"""
# Laravel Echo ์๋ฒ๊ฐ ๊ตฌ๋
ํ๋ ์ฑ๋์ ๋ฐํ
channel_data = {
'event': 'ProposalStatusUpdated',
'data': {
'proposal_id': proposal_id,
'status': status,
}
}
self.redis.publish(
f'laravel_database_proposals',
json.dumps(channel_data)
)
๐ ๋ณด์ ์ค๊ณ โ DAO ๋ฐฑ์๋์ ๋ณด์์ ์๋ช ์ด๋ค
DAO๋ ์ค์ ์๊ธ์ ๋ค๋ฃจ๊ธฐ ๋๋ฌธ์ ๋ณด์์ด ์ ๋ง ์ค์ํด. ์ ๋ ๋์ถฉ ๋์ด๊ฐ๋ฉด ์ ๋๋ ํํธ์ผ! ๐ค
ํต์ฌ ๋ณด์ ๊ณ ๋ ค์ฌํญ
Python ์๋น์ค์์ ํธ๋์ญ์ ์ ์๋ช ํ ๋ ํ๋ผ์ด๋น ํค๋ฅผ ์ฝ๋์ ํ๋์ฝ๋ฉํ๋ฉด ์ ๋ ์ ๋ผ. AWS KMS, HashiCorp Vault, ๋๋ ํ๋์จ์ด ๋ณด์ ๋ชจ๋(HSM)์ ์ฌ์ฉํด์ผ ํด.
# python/services/key_management.py
import boto3
from web3 import Web3
class AWSKMSSigner:
"""AWS KMS๋ฅผ ์ฌ์ฉํ ์์ ํ ํธ๋์ญ์
์๋ช
"""
def __init__(self, key_id: str, region: str = 'ap-northeast-2'):
self.kms_client = boto3.client('kms', region_name=region)
self.key_id = key_id
def sign_transaction(self, tx_hash: bytes) -> bytes:
"""KMS๋ก ํธ๋์ญ์
ํด์ ์๋ช
"""
response = self.kms_client.sign(
KeyId=self.key_id,
Message=tx_hash,
MessageType='DIGEST',
SigningAlgorithm='ECDSA_SHA_256'
)
return response['Signature']
def get_public_address(self) -> str:
"""KMS ํค์ ์ด๋๋ฆฌ์ ์ฃผ์ ๋ฐํ"""
response = self.kms_client.get_public_key(KeyId=self.key_id)
# DER ์ธ์ฝ๋ฉ๋ ๊ณต๊ฐํค์์ ์ด๋๋ฆฌ์ ์ฃผ์ ์ถ์ถ
public_key_der = response['PublicKey']
# ... ์ฃผ์ ์ถ์ถ ๋ก์ง
return address
Laravel๊ณผ Python ์๋น์ค ๊ฐ ํต์ ์๋ ๋ฐ๋์ ๋ด๋ถ ์ํฌ๋ฆฟ ํค๋ฅผ ์ฌ์ฉํด. ์ธ๋ถ์์ ๋ด๋ถ API๋ฅผ ์ง์ ํธ์ถํ์ง ๋ชปํ๋๋ก ๋ง์์ผ ํด.
// Laravel ๋ด๋ถ API ๋ณดํธ ๋ฏธ๋ค์จ์ด
// app/Http/Middleware/InternalServiceAuth.php
class InternalServiceAuth
{
public function handle(Request $request, Closure $next): Response
{
$secret = $request->header('X-Internal-Secret');
if (!$secret || !hash_equals(config('services.internal_secret'), $secret)) {
return response()->json(['error' => '๋ด๋ถ ์๋น์ค ์ธ์ฆ ์คํจ'], 403);
}
// IP ํ์ดํธ๋ฆฌ์คํธ ํ์ธ (Docker ๋ด๋ถ ๋คํธ์ํฌ)
$allowedIps = ['172.16.0.0/12', '10.0.0.0/8'];
$clientIp = $request->ip();
$isAllowed = collect($allowedIps)->some(fn($range) =>
$this->ipInRange($clientIp, $range)
);
if (!$isAllowed) {
return response()->json(['error' => 'ํ์ฉ๋์ง ์์ IP'], 403);
}
return $next($request);
}
}
ํนํ ํฌํ API๋ ์ค๋ณต ํฌํ ๋ฐฉ์ง๋ฅผ ์ํด ์๊ฒฉํ Rate Limiting์ด ํ์ํด.
// routes/api.php
Route::middleware(['auth:sanctum'])->group(function () {
// ํฌํ๋ 1๋ถ์ 5๋ฒ์ผ๋ก ์ ํ
Route::post('/proposals/{proposal}/vote', [VoteController::class, 'castVote'])
->middleware('throttle:5,1');
// ํ๋กํฌ์ ์์ฑ์ 1์๊ฐ์ 3๋ฒ์ผ๋ก ์ ํ
Route::post('/proposals', [ProposalController::class, 'store'])
->middleware('throttle:3,60');
// ์ผ๋ฐ ์กฐํ๋ 1๋ถ์ 60๋ฒ
Route::get('/proposals', [ProposalController::class, 'index'])
->middleware('throttle:60,1');
});
๐ ๊ฑฐ๋ฒ๋์ค ๋ถ์ โ Python ML๋ก DAO ๊ฑด๊ฐ๋ ์ธก์ ํ๊ธฐ
Python์ ์ง์ง ๊ฐ์ ์ด ์ฌ๊ธฐ์ ๋์! ๋ฐ์ดํฐ ๋ถ์๊ณผ ML์ ํ์ฉํด์ DAO์ ๊ฑฐ๋ฒ๋์ค ๊ฑด๊ฐ๋๋ฅผ ์ธก์ ํ๋ ๊ธฐ๋ฅ์ ๋ง๋ค์ด๋ณด์.
๊ฑฐ๋ฒ๋์ค ๋ถ์ ์๋น์ค
# python/services/governance_analytics.py
import pandas as pd
import numpy as np
from web3 import Web3
from gql import gql, Client
from gql.transport.requests import RequestsHTTPTransport
class GovernanceAnalytics:
"""DAO ๊ฑฐ๋ฒ๋์ค ๊ฑด๊ฐ๋ ๋ถ์ ์๋น์ค"""
def __init__(self, subgraph_url: str):
# The Graph ์๋ธ๊ทธ๋ํ ํด๋ผ์ด์ธํธ
transport = RequestsHTTPTransport(url=subgraph_url)
self.graph_client = Client(transport=transport)
def get_voter_participation_stats(self, days: int = 30) -> dict:
"""ํฌํ ์ฐธ์ฌ์จ ํต๊ณ"""
query = gql("""
query GetVotes($since: Int!) {
votes(where: {blockTimestamp_gte: $since}) {
voter { id tokenBalance }
proposal { id }
support
weight
}
}
""")
since = int((pd.Timestamp.now() - pd.Timedelta(days=days)).timestamp())
result = self.graph_client.execute(query, {'since': since})
df = pd.DataFrame(result['votes'])
if df.empty:
return {'participation_rate': 0, 'unique_voters': 0}
# ๊ณ ๋ vs ์์ก ํฌํ์ ๋ถ์
df['weight_float'] = df['weight'].astype(float) / 1e18
whale_threshold = df['weight_float'].quantile(0.9)
return {
'unique_voters': df['voter'].apply(lambda x: x['id']).nunique(),
'total_votes': len(df),
'whale_voters': len(df[df['weight_float'] >= whale_threshold]),
'avg_voting_power': df['weight_float'].mean(),
'median_voting_power': df['weight_float'].median(),
'gini_coefficient': self._calculate_gini(df['weight_float'].values),
'participation_trend': self._calculate_trend(df),
}
def _calculate_gini(self, values: np.ndarray) -> float:
"""์ง๋ ๊ณ์ ๊ณ์ฐ (ํฌํ๊ถ ๋ถ๋ฐฐ ๋ถํ๋ฑ๋)"""
sorted_vals = np.sort(values)
n = len(sorted_vals)
cumsum = np.cumsum(sorted_vals)
return (2 * np.sum((np.arange(1, n+1) * sorted_vals))) / (n * cumsum[-1]) - (n+1)/n
def calculate_dao_health_score(self) -> dict:
"""DAO ๊ฑด๊ฐ๋ ์ข
ํฉ ์ ์ (0-100)"""
participation = self.get_voter_participation_stats()
# ๊ฐ ์งํ๋ณ ์ ์ ๊ณ์ฐ
participation_score = min(participation['unique_voters'] / 100 * 30, 30)
decentralization_score = max(0, 30 - participation['gini_coefficient'] * 30)
activity_score = min(participation['total_votes'] / 50 * 20, 20)
diversity_score = self._calculate_voter_diversity_score() * 20
total_score = participation_score + decentralization_score + activity_score + diversity_score
return {
'total_score': round(total_score, 1),
'grade': self._score_to_grade(total_score),
'breakdown': {
'participation': round(participation_score, 1),
'decentralization': round(decentralization_score, 1),
'activity': round(activity_score, 1),
'diversity': round(diversity_score, 1),
}
}
๐ ๋ฐฐํฌ ์ ๋ต โ ํ๋ก๋์ ํ๊ฒฝ ๊ตฌ์ฑ
๊ฐ๋ฐ ํ๊ฒฝ์์ ์ ๋์๊ฐ๋ค๊ณ ๋์ด ์๋์ผ. ํ๋ก๋์ ๋ฐฐํฌ๊น์ง ์๊ฐํด์ผ ์ง์ง ์์ฑ์ด๊ฑฐ๋ !
Kubernetes ๊ธฐ๋ฐ ๋ฐฐํฌ ๊ตฌ์ฑ
# k8s/laravel-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-api
labels:
app: dao-laravel
spec:
replicas: 3 # ์ํ ํ์ฅ
selector:
matchLabels:
app: dao-laravel
template:
metadata:
labels:
app: dao-laravel
spec:
containers:
- name: laravel
image: your-registry/dao-laravel:latest
ports:
- containerPort: 8000
env:
- name: DB_HOST
valueFrom:
secretKeyRef:
name: dao-secrets
key: db-host
- name: REDIS_HOST
value: "redis-service"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /api/health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
---
# k8s/python-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: python-worker
spec:
replicas: 2
selector:
matchLabels:
app: dao-python
template:
spec:
containers:
- name: python
image: your-registry/dao-python:latest
env:
- name: WEB3_PROVIDER_URL
valueFrom:
secretKeyRef:
name: dao-secrets
key: web3-provider
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
๋ชจ๋ํฐ๋ง ๋ฐ ์๋ฆผ ์ค์
โข Grafana: ๋์๋ณด๋ ์๊ฐํ
โข Loki: ๋ก๊ทธ ์ง๊ณ
โข Alertmanager: ์๋ฆผ ๋ฐ์ก
์ฃผ์ ๋ชจ๋ํฐ๋ง ์งํ:
- API ์๋ต ์๊ฐ
- ๋ธ๋ก์ฒด์ธ ์ฐ๊ฒฐ ์ํ
- ํ ์ ์ฒด ์
- ์ด๋ฒคํธ ์ฒ๋ฆฌ ์ง์ฐ
โข ๋ธ๋ก์ฒด์ธ ์ฐ๊ฒฐ ๋๊น โ ์ฆ์ ์๋ฆผ
โข ํ ์ ์ฒด 100๊ฐ ์ด๊ณผ โ ๊ฒฝ๊ณ
โข ์ด๋ฒคํธ ์ฒ๋ฆฌ ์ง์ฐ 5๋ถ ์ด๊ณผ โ ๊ฒฝ๊ณ
โข ๊ฐ์ค๋น ๊ธ๋ฑ โ ์ ๋ณด ์๋ฆผ
โข ํ๋กํฌ์ ํฌํ ์์ โ ๋ฉค๋ฒ ์๋ฆผ
โข ํ๋กํฌ์ ์คํ ์๋ฃ โ ์ ์ฒด ์๋ฆผ
- โ ๏ธ ํ๋ก๋์ ์ฒดํฌ๋ฆฌ์คํธ:
- โ ํ๋ผ์ด๋น ํค๋ ์ ๋ ํ๊ฒฝ๋ณ์์ ์ง์ ์ ์ฅ ๊ธ์ง โ KMS/Vault ์ฌ์ฉ
- โ ๋ด๋ถ ์๋น์ค ํต์ ์ TLS ์ ์ฉ
- โ ๋ธ๋ก์ฒด์ธ ๋ ธ๋๋ Infura/Alchemy ์ธ์ ์์ฒด ๋ ธ๋ ๋ฐฑ์ ๊ตฌ์ฑ
- โ ์ด๋ฒคํธ ๋ฆฌ์ค๋ ์ฌ์์ ์ ๋์น ์ด๋ฒคํธ ๋ณต๊ตฌ ๋ก์ง ํ์
- โ ํธ๋์ญ์ ์คํจ ์ ์ฌ์๋ ๋ก์ง ๊ตฌํ
- โ ์ค๋งํธ ์ปจํธ๋ํธ ABI ๋ฒ์ ๊ด๋ฆฌ ์ฒ ์ ํ
๐งช ํ ์คํธ ์ ๋ต โ ๋ฏฟ์ ์ ์๋ DAO ๋ฐฑ์๋ ๋ง๋ค๊ธฐ
DAO๋ ์ค์ ์๊ธ์ด ์ค๊ฐ๋ ์์คํ ์ด์ผ. ํ ์คํธ ์์ด ๋ฐฐํฌํ๋ ๊ฑด ์์ดํ์์ผ! ๐ฑ
Laravel ํ ์คํธ
// tests/Feature/ProposalTest.php
class ProposalTest extends TestCase
{
use RefreshDatabase;
public function test_member_can_create_proposal(): void
{
// Python ์๋น์ค Mock
Http::fake([
'http://python-worker:5000/*' => Http::response([
'ipfs_hash' => 'QmTest123',
'tx_hash' => '0xabc123',
], 200),
]);
$member = Member::factory()->create([
'wallet_address' => '0x742d35Cc6634C0532925a3b8D4C9C5e3',
'token_balance' => '1000000000000000000000', // 1000 GOV
]);
$response = $this->actingAs($member, 'sanctum')
->postJson('/api/proposals', [
'title' => 'ํ
์คํธ ํ๋กํฌ์ ',
'description' => '์ด๊ฒ์ ํ
์คํธ์
๋๋ค.',
'actions' => [],
]);
$response->assertStatus(201)
->assertJsonStructure(['id', 'title', 'status', 'ipfs_hash']);
$this->assertDatabaseHas('proposals', [
'title' => 'ํ
์คํธ ํ๋กํฌ์ ',
'status' => 'pending',
]);
}
public function test_member_cannot_vote_twice(): void
{
$proposal = Proposal::factory()->create(['status' => 'active']);
$member = Member::factory()->create();
// ์ฒซ ๋ฒ์งธ ํฌํ
$this->actingAs($member, 'sanctum')
->postJson("/api/proposals/{$proposal->id}/vote", ['support' => 'for'])
->assertStatus(200);
// ๋ ๋ฒ์งธ ํฌํ ์๋ โ ์คํจํด์ผ ํจ
$this->actingAs($member, 'sanctum')
->postJson("/api/proposals/{$proposal->id}/vote", ['support' => 'against'])
->assertStatus(422)
->assertJsonFragment(['error' => '์ด๋ฏธ ํฌํํ์
จ์ต๋๋ค.']);
}
}
Python ํ ์คํธ โ ์ค๋งํธ ์ปจํธ๋ํธ Mock
# python/tests/test_governor_service.py
import pytest
from unittest.mock import MagicMock, patch
from web3 import Web3
from services.governor_service import GovernorService
@pytest.fixture
def mock_w3():
w3 = MagicMock(spec=Web3)
w3.is_connected.return_value = True
return w3
@pytest.fixture
def governor_service(mock_w3):
with patch.dict('os.environ', {'GOVERNOR_ADDRESS': '0x' + '1' * 40}):
return GovernorService(mock_w3)
def test_get_proposal_state_active(governor_service):
"""ํ์ฑ ํ๋กํฌ์ ์ํ ์กฐํ ํ
์คํธ"""
# Mock ์ปจํธ๋ํธ ์๋ต
governor_service.contract.functions.state.return_value.call.return_value = 1
governor_service.contract.functions.proposalVotes.return_value.call.return_value = (
100 * 10**18, # against
500 * 10**18, # for
50 * 10**18, # abstain
)
result = governor_service.get_proposal_state(12345)
assert result['state'] == 'active'
assert result['for_votes'] == str(500 * 10**18)
assert result['against_votes'] == str(100 * 10**18)
def test_verify_signature_valid():
"""์ ํจํ ์๋ช
๊ฒ์ฆ ํ
์คํธ"""
from eth_account import Account
from eth_account.messages import encode_defunct
# ํ
์คํธ ๊ณ์ ์์ฑ
account = Account.create()
message = "ํ
์คํธ ๋ฉ์์ง"
msg_hash = encode_defunct(text=message)
signed = account.sign_message(msg_hash)
# ์๋ช
๊ฒ์ฆ
recovered = Account.recover_message(msg_hash, signature=signed.signature)
assert recovered.lower() == account.address.lower()
๐ก ์ค์ ํ โ ๊ฐ๋ฐํ๋ฉด์ ์์ฃผ ๋ง๋๋ ๋ฌธ์ ๋ค
์ด๋ก ์ ๋ค ์๊ฒ ๋๋ฐ ์ค์ ๋ก ๊ฐ๋ฐํ๋ค ๋ณด๋ฉด ์์์น ๋ชปํ ๋ฌธ์ ๋ค์ด ํฐ์ ธ. ๋ฏธ๋ฆฌ ์๊ณ ๊ฐ์!
๋ฌธ์ 1: ์ด๋ฒคํธ ์ค๋ณต ์ฒ๋ฆฌ
์ด๋ฒคํธ ๋ฆฌ์ค๋๊ฐ ์ฌ์์๋๊ฑฐ๋ ์ฌ๋ฌ ์ธ์คํด์ค๊ฐ ๋์์ ์คํ๋ ๋ ๊ฐ์ ์ด๋ฒคํธ๋ฅผ ์ฌ๋ฌ ๋ฒ ์ฒ๋ฆฌํ ์ ์์ด. Redis์ ์ฒ๋ฆฌ๋ ํธ๋์ญ์ ํด์๋ฅผ ์ ์ฅํด์ ์ค๋ณต ์ฒ๋ฆฌ๋ฅผ ๋ฐฉ์งํด์ผ ํด.
processed_key = f"processed_event:{tx_hash}:{log_index}"if redis.exists(processed_key): return # ์ด๋ฏธ ์ฒ๋ฆฌ๋จredis.setex(processed_key, 86400, "1") # 24์๊ฐ ๋ณด๊ด
๋ฌธ์ 2: ๋ธ๋ก ์ฌ์กฐ์ง(Reorg) ์ฒ๋ฆฌ
# ์์ ํ ์ด๋ฒคํธ ์ฒ๋ฆฌ - ์ถฉ๋ถํ ๋ธ๋ก ํ์ธ ํ ์ฒ๋ฆฌ
SAFE_BLOCK_CONFIRMATIONS = 12 # ์ด๋๋ฆฌ์ ๋ฉ์ธ๋ท
# SAFE_BLOCK_CONFIRMATIONS = 64 # Polygon
async def process_event_safely(event, current_block: int):
event_block = event['blockNumber']
confirmations = current_block - event_block
if confirmations < SAFE_BLOCK_CONFIRMATIONS:
# ์์ง ํ์ธ์ด ๋ถ์กฑํจ - ๋์ค์ ๋ค์ ์ฒ๋ฆฌ
await asyncio.sleep(12 * (SAFE_BLOCK_CONFIRMATIONS - confirmations))
return False
return True # ์์ ํ๊ฒ ์ฒ๋ฆฌ ๊ฐ๋ฅ
๋ฌธ์ 3: ๊ฐ์ค๋น ๊ธ๋ฑ ์ ํธ๋์ญ์ ์ฒ๋ฆฌ
# python/services/gas_optimizer.py
class GasOptimizer:
def get_optimal_gas_params(self, urgency: str = 'normal') -> dict:
"""EIP-1559 ์ต์ ๊ฐ์ค ํ๋ผ๋ฏธํฐ ๊ณ์ฐ"""
latest_block = w3.eth.get_block('latest')
base_fee = latest_block['baseFeePerGas']
# ์ฐ์ ์์์ ๋ฐ๋ฅธ max_priority_fee ์ค์
priority_fees = {
'slow': Web3.to_wei(1, 'gwei'),
'normal': Web3.to_wei(2, 'gwei'),
'fast': Web3.to_wei(5, 'gwei'),
'urgent': Web3.to_wei(10, 'gwei'),
}
max_priority_fee = priority_fees.get(urgency, priority_fees['normal'])
max_fee = base_fee * 2 + max_priority_fee # ๋ค์ ๋ธ๋ก ๋๋น ์ฌ์
return {
'maxFeePerGas': max_fee,
'maxPriorityFeePerGas': max_priority_fee,
}
๋ฌธ์ 4: IPFS ํ๋ ์๋น์ค ์ ํ
IPFS์ ๋ฐ์ดํฐ๋ฅผ ์ฌ๋ ธ๋ค๊ณ ์๊ตฌ ๋ณด์กด๋๋ ๊ฒ ์๋์ผ! ํ๋ ์๋น์ค๋ฅผ ์ฌ์ฉํด์ผ ํด:
| ์๋น์ค | ๋ฌด๋ฃ ํ๋ | ํน์ง | ์ถ์ฒ ์ฉ๋ |
|---|---|---|---|
| Pinata | 1GB | API ์ฌ์ฉ ํธ๋ฆฌ, ์์ ์ | ํ๋ก๋์ ์ถ์ฒ |
| Web3.Storage | 5GB | Filecoin ๋ฐฑ์ | ๋์ฉ๋ ํ์ผ |
| Infura IPFS | 5GB | Infura ํตํฉ | ๊ธฐ์กด Infura ์ฌ์ฉ์ |
| ์์ฒด IPFS ๋ ธ๋ | ๋ฌด์ ํ | ์์ ํ ํต์ | ์ํฐํ๋ผ์ด์ฆ |
๐ ๋ง๋ฌด๋ฆฌ โ ์ด ์ํคํ ์ฒ์ ์ง์ง ๊ฐ์
์, ์ด์ ์ ์ฒด ๊ทธ๋ฆผ์ด ๊ทธ๋ ค์ก์ง? ๋ง์ง๋ง์ผ๋ก ์ด ํ์ด๋ธ๋ฆฌ๋ ์ํคํ ์ฒ๊ฐ ์ ์ข์์ง ์ ๋ฆฌํด๋ณด์.
1. ๊ด์ฌ์ฌ์ ๋ถ๋ฆฌ (Separation of Concerns)
๊ฐ ์๋น์ค๊ฐ ์์ ์ด ๊ฐ์ฅ ์ํ๋ ์ผ๋ง ํด. Laravel์ ์คํ์ฒด์ธ ๋น์ฆ๋์ค ๋ก์ง, Python์ ์จ์ฒด์ธ ์ธํฐ๋์ . ์๋ก ๊ฐ์ญ ์์ด ๋ ๋ฆฝ์ ์ผ๋ก ๋ฐ์ ํ ์ ์์ด.
2. ๊ธฐ์ ์ํ๊ณ ์ต์ ํ์ฉ
PHP/Laravel์ ํ๋ถํ ์น ๊ฐ๋ฐ ์ํ๊ณ + Python์ Web3/ML ์ํ๊ณ๋ฅผ ๋์์ ํ์ฉ. ์ด๋ ํ๋๋ ํฌ๊ธฐํ์ง ์์๋ ๋ผ.
3. ์ฅ์ ๊ฒฉ๋ฆฌ (Fault Isolation)
Python ์๋น์ค๊ฐ ๋ค์ด๋์ด๋ Laravel์ ์บ์๋ ๋ฐ์ดํฐ๋ก ๊ณ์ ์๋น์ค ๊ฐ๋ฅ. ๋ธ๋ก์ฒด์ธ ์ฐ๊ฒฐ์ด ๋๊ฒจ๋ ์คํ์ฒด์ธ ๊ธฐ๋ฅ์ ์ ์ ๋์.
4. ๋ ๋ฆฝ์ ํ์ฅ (Independent Scaling)
ํธ๋ํฝ์ด ๋ชฐ๋ฆด ๋ Laravel Pod๋ง ๋๋ฆฌ๊ฑฐ๋, ์ด๋ฒคํธ ์ฒ๋ฆฌ๊ฐ ๋ง์ ๋ Python Worker๋ง ๋๋ฆด ์ ์์ด. ํจ์จ์ ์ธ ๋ฆฌ์์ค ๊ด๋ฆฌ ๊ฐ๋ฅ.
์ด ์ํคํ ์ฒ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋ ๋ฐ์ ์ํฌ ์ ์๋ ๋ฐฉํฅ๋ค์ด์ผ:
โข The Graph ์๋ธ๊ทธ๋ํ ๊ตฌ์ถ์ผ๋ก ์จ์ฒด์ธ ๋ฐ์ดํฐ ์ธ๋ฑ์ฑ ๊ณ ๋ํ
โข Snapshot.org ํตํฉ์ผ๋ก ๊ฐ์ค๋น ์๋ ์คํ์ฒด์ธ ํฌํ ์ง์
โข Safe(Gnosis Safe) ๋ฉํฐ์๊ทธ ํตํฉ์ผ๋ก ํธ๋ ์ ๋ฆฌ ๋ณด์ ๊ฐํ
โข zkProof ๊ธฐ๋ฐ ํ๋ผ์ด๋ฒ์ ํฌํ ๊ตฌํ
โข ํฌ๋ก์ค์ฒด์ธ ๊ฑฐ๋ฒ๋์ค (์ฌ๋ฌ ์ฒด์ธ์ ํ ํฐ ํ๋ ํตํฉ ํฌํ)
์ด ์ํคํ
์ฒ๋ ์ค์ ํ๋ก๋์
DAO ํ๋ก์ ํธ์์ ๊ฒ์ฆ๋ ํจํด๋ค์ ๊ธฐ๋ฐ์ผ๋ก ์์ฑ๋์์ด.
๋ฌผ๋ก ํ๋ก์ ํธ๋ง๋ค ์๊ตฌ์ฌํญ์ด ๋ค๋ฅด๋๊น, ์ด๊ฑธ ๋ฒ ์ด์ค๋ก ์ผ์์ ์์ ์ DAO์ ๋ง๊ฒ ์ปค์คํฐ๋ง์ด์งํ๋ ๊ฒ ์ค์ํด! ๐ช
Happy Building! ๐๏ธโ๏ธ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.