머클 페트리시아 트리(Merkle Patricia Trie)는 이더리움의 핵심 데이터 구조 중 하나로, 상태(state), 거래(transaction), 영수증(receipt) 등을 저장하고 검증 가능한 방식으로 구성하기 위해 사용됩니다. 다음은 그 구조, 특징, 작동방식, 이더리움에서의 역할을 기술 블로그 수준으로 자세히 설명한 내용입니다.

  1. 개요: 왜 머클 페트리시아 트리를 사용하는가?

이더리움은 모든 상태를 블록체인 위에서 신뢰 가능하게 저장하고 검증할 수 있어야 합니다. 이를 위해 사용하는 것이 머클 트리(Merkle Tree)패트리시아 트라이(Trie)의 결합 구조인 머클 페트리시아 트리(MPT)입니다.
• 머클 트리: 데이터의 무결성을 해시로 보장.
• 패트리시아 트라이: 키-값 쌍을 압축하여 효율적인 저장을 가능하게 함.
• 머클 페트리시아 트리: 위 두 개의 장점을 결합하여 이더리움의 상태 저장 및 검증에 사용.

  1. 구조: 머클 트리 + 패트리시아 트라이

(1) 기본 구성

MPT는 키-값(key-value) 구조의 데이터를 저장하며 다음 세 가지 노드로 구성됩니다:

노드 타입 설명
Leaf Node 최종 값(계정 정보, 스토리지 값 등)을 저장
Extension Node 중간 경로(prefix)를 저장하여 트리 깊이 축소
Branch Node 16진수(0~f) 기반 16개의 포인터 + 값 필드를 가짐

(2) 키 구조

MPT는 키를 16진수 nibble로 변환한 후 사용합니다. 예:
• 키: 0xa1b3 → nibble: [a, 1, b, 3]

압축 경로(prefix compression)를 통해 중복된 경로는 Extension으로, 분기되는 지점은 Branch로, 최종 값은 Leaf로 표현됩니다.

  1. 작동 방식

(1) 삽입
1. 키를 nibble로 변환하여 경로로 사용.
2. 경로가 겹치는 경우 extension/branch를 만들어 분기.
3. 최종 경로에 값 저장 (Leaf Node).

(2) 조회
1. 루트에서 시작하여 nibble 경로를 따라 노드 순회.
2. Leaf 또는 Branch의 값 필드에서 최종 결과 도달.

(3) 삭제
1. 해당 경로의 Leaf 제거.
2. 경로가 중복되지 않게 되면 Extension/Branch 재구성.

  1. 해시와 머클 구조

각 노드는 RLP(RLP 인코딩)된 형태로 해싱되며, 이 해시들이 상위 노드에 포함되어 루트 해시까지 이어집니다. 이를 통해:
• 전체 트리의 무결성 보장
• 특정 데이터가 존재함을 루트 해시만으로 증명 가능 (Merkle Proof)

  1. 이더리움에서의 역할

이더리움은 3개의 주요 MPT를 유지합니다:

트리 이름 설명 루트 해시 위치
State Trie 주소 → 계정 상태 (Nonce, Balance, CodeHash, StorageRoot) 블록 헤더의 stateRoot
Storage Trie 계정의 스토리지 슬롯 → 값 계정의 storageRoot
Transaction Trie 트랜잭션 인덱스 → 트랜잭션 블록 헤더의 transactionsRoot
Receipt Trie 트랜잭션 인덱스 → receipt 블록 헤더의 receiptsRoot

예: 계정 정보 조회 흐름
1. 상태 루트 (stateRoot)를 통해 State Trie 접근
2. 계정 주소 → RLP → keccak256 → 경로
3. 해당 노드에서 계정 정보 추출 (Balance 등)
4. 스토리지 필요 시, storageRoot를 사용해 Storage Trie 접근

  1. 요약: MPT의 장점
특징 설명
무결성 보장 루트 해시만으로 전체 트리의 정합성 검증 가능
변경 추적 데이터 하나만 바뀌어도 루트 해시가 바뀜 (불변성)
경로 최적화 중복되는 경로는 압축하여 저장 (효율적)
이더리움 적합 블록체인의 탈중앙 상태 검증과 궁합이 뛰어남

참고: MPT 시각화 예시

        Root Hash
           |
       Branch (a)
      /     |     \
   Ext     ...    Ext
   ("1")         ("3")
     |             |
  Leaf(a1b)     Leaf(a3f)

  1. Uniswap의 핵심 개념

1.1 자동화된 시장 조성자(AMM)

Uniswap은 AMM 모델을 사용하여 거래를 자동으로 체결합니다. 이는 전통적인 중앙화 거래소의 오더북 시스템과 달리, 유동성 풀(Liquidity Pool)을 통해 거래를 수행합니다. 유동성 풀은 사용자가 예치한 두 가지 토큰으로 구성되며, 거래자는 이 풀을 통해 토큰을 교환합니다.  

1.2 유동성 공급자와 LP 토큰

사용자는 두 가지 토큰을 유동성 풀에 예치함으로써 유동성 공급자가 될 수 있습니다. 예치한 대가로 LP(Liquidity Provider) 토큰을 받게 되며, 이는 유동성 풀에서의 지분을 나타냅니다. 거래 수수료는 유동성 공급자에게 분배되어 수익을 창출할 수 있습니다. 

  1. 가격 결정 메커니즘

2.1 상수 곱 공식 (x * y = k)

Uniswap은 상수 곱 공식(Constant Product Formula)을 사용하여 가격을 결정합니다. 이는 풀에 있는 두 토큰의 수량을 곱한 값이 항상 일정하게 유지되도록 하는 방식입니다. 이 공식에 따라 거래가 이루어지며, 유동성 풀의 균형을 유지합니다. 

2.2 슬리피지와 가격 영향

거래 규모가 클수록 유동성 풀의 비율이 크게 변동하여 슬리피지(Slippage)가 발생할 수 있습니다. 이는 예상한 가격과 실제 체결 가격 사이의 차이를 의미하며, 거래자는 이를 고려하여 거래를 수행해야 합니다. 

  1. Uniswap의 서비스 아키텍처

3.1 스마트 계약 구조

Uniswap은 스마트 계약을 통해 탈중앙화된 거래를 구현합니다. 주요 스마트 계약 구성 요소는 다음과 같습니다: 
• Factory Contract: 새로운 유동성 풀을 생성하고 관리합니다.
• Pair Contract: 각 유동성 풀의 거래 로직과 유동성 관리를 담당합니다.
• Router Contract: 사용자 인터페이스와 스마트 계약 간의 상호작용을 중개합니다. 

3.2 버전별 특징
• Uniswap V2: 기본적인 AMM 모델을 구현하여 다양한 토큰 간의 거래를 지원합니다.
• Uniswap V3: 집중된 유동성(Concentrated Liquidity)과 다중 수수료 계층을 도입하여 유동성 효율성과 수익성을 향상시켰습니다. 

🔍 Heap vs Red-Black Tree (RBT) 비교 요약

항목 Binary Heap Red-Black Tree (RBT)
구조 완전 이진 트리 (배열 기반) 이진 탐색 트리 (포인터 기반)
정렬 기준 힙 속성 유지 (부모 ≥ 자식) 이진 탐색 트리 속성 유지 (정렬된 순서)
삽입/삭제 O(log n) (heapify) O(log n) (트리 재구성)
순위 기반 검색 ❌ 불가 ✅ 중위 순회로 가능
특정 값 탐색 느림 (배열 순회 필요) 빠름 (이진 검색 가능)
최댓값 접근 ✅ 즉시 (heap[1]) ❌ 최댓값은 오른쪽 끝 노드

✅ 언제 RBT가 유리할까?
• 정렬된 데이터에 범위 검색 (range query) 이 필요한 경우
• 중간값, 특정 키, 순위 기반 탐색 (top 10, 3번째 큰 값)이 필요한 경우
• 트리 구조 유지가 중요할 때 (예: 상태 동기화, trie 구조 등)

✨ 예: RBT 기반 스마트 컨트랙트 응용

응용 예 설명
가격 기반 주문서 orderBook[price]으로 가격 정렬 및 탐색
정렬된 스테이킹 기록 amount 기준 탐색 및 정렬
경쟁 점수 랭킹 상위 10개 유지 (sorted insert, delete)

📦 Solidity에 RBT 구현이 가능할까?

✔️ 가능합니다!
하지만 다음 제약을 고려해야 합니다:
• 포인터 대신 mapping 구조 사용
• 왼쪽/오른쪽 자식도 모두 mapping(uint => Node)
• 컬러(bit), 균형 조건 재조정 필요 → 코드 복잡도 높음
• 라이브러리로 많이 활용됨 (예: solidity-rbtree)

이미 만들어져 있는 rbt 라이브러리
solidity-treemap

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/**
 * Red-Black Tree in Solidity (확장 버전)
 * - Self-balancing binary search tree
 * - Insert, Delete, Min/Max, Exists, In-order, Range Query 지원
 */

library RedBlackTreeLib {
    uint private constant NULL = 0;

    struct Node {
        uint parent;
        uint left;
        uint right;
        bool red;
        bool exists;
    }

    struct Tree {
        mapping(uint => Node) nodes;
        uint root;
    }

    function insert(Tree storage self, uint key) internal {
        require(key != NULL, "invalid key");
        require(!self.nodes[key].exists, "already exists");

        uint cursor = self.root;
        uint parent = NULL;

        while (cursor != NULL) {
            parent = cursor;
            if (key < cursor) {
                cursor = self.nodes[cursor].left;
            } else {
                cursor = self.nodes[cursor].right;
            }
        }

        self.nodes[key] = Node({ parent: parent, left: NULL, right: NULL, red: true, exists: true });

        if (parent == NULL) {
            self.root = key;
        } else if (key < parent) {
            self.nodes[parent].left = key;
        } else {
            self.nodes[parent].right = key;
        }

        insertFixup(self, key);
    }

    function insertFixup(Tree storage self, uint key) private {
        while (key != self.root && self.nodes[self.nodes[key].parent].red) {
            uint parent = self.nodes[key].parent;
            uint grandparent = self.nodes[parent].parent;

            if (parent == self.nodes[grandparent].left) {
                uint uncle = self.nodes[grandparent].right;
                if (self.nodes[uncle].red) {
                    self.nodes[parent].red = false;
                    self.nodes[uncle].red = false;
                    self.nodes[grandparent].red = true;
                    key = grandparent;
                } else {
                    if (key == self.nodes[parent].right) {
                        key = parent;
                        rotateLeft(self, key);
                        parent = self.nodes[key].parent;
                    }
                    self.nodes[parent].red = false;
                    self.nodes[grandparent].red = true;
                    rotateRight(self, grandparent);
                }
            } else {
                uint uncle = self.nodes[grandparent].left;
                if (self.nodes[uncle].red) {
                    self.nodes[parent].red = false;
                    self.nodes[uncle].red = false;
                    self.nodes[grandparent].red = true;
                    key = grandparent;
                } else {
                    if (key == self.nodes[parent].left) {
                        key = parent;
                        rotateRight(self, key);
                        parent = self.nodes[key].parent;
                    }
                    self.nodes[parent].red = false;
                    self.nodes[grandparent].red = true;
                    rotateLeft(self, grandparent);
                }
            }
        }
        self.nodes[self.root].red = false;
    }

    function rotateLeft(Tree storage self, uint x) private {
        uint y = self.nodes[x].right;
        self.nodes[x].right = self.nodes[y].left;
        if (self.nodes[y].left != NULL) {
            self.nodes[self.nodes[y].left].parent = x;
        }
        self.nodes[y].parent = self.nodes[x].parent;
        if (self.nodes[x].parent == NULL) {
            self.root = y;
        } else if (x == self.nodes[self.nodes[x].parent].left) {
            self.nodes[self.nodes[x].parent].left = y;
        } else {
            self.nodes[self.nodes[x].parent].right = y;
        }
        self.nodes[y].left = x;
        self.nodes[x].parent = y;
    }

    function rotateRight(Tree storage self, uint x) private {
        uint y = self.nodes[x].left;
        self.nodes[x].left = self.nodes[y].right;
        if (self.nodes[y].right != NULL) {
            self.nodes[self.nodes[y].right].parent = x;
        }
        self.nodes[y].parent = self.nodes[x].parent;
        if (self.nodes[x].parent == NULL) {
            self.root = y;
        } else if (x == self.nodes[self.nodes[x].parent].right) {
            self.nodes[self.nodes[x].parent].right = y;
        } else {
            self.nodes[self.nodes[x].parent].left = y;
        }
        self.nodes[y].right = x;
        self.nodes[x].parent = y;
    }

    function getMin(Tree storage self) internal view returns (uint) {
        uint current = self.root;
        require(current != NULL, "empty tree");
        while (self.nodes[current].left != NULL) {
            current = self.nodes[current].left;
        }
        return current;
    }

    function getMax(Tree storage self) internal view returns (uint) {
        uint current = self.root;
        require(current != NULL, "empty tree");
        while (self.nodes[current].right != NULL) {
            current = self.nodes[current].right;
        }
        return current;
    }

    function exists(Tree storage self, uint key) internal view returns (bool) {
        return self.nodes[key].exists;
    }

    function inOrder(Tree storage self, uint node, uint[] storage output) internal view {
        if (node == NULL) return;
        inOrder(self, self.nodes[node].left, output);
        output.push(node);
        inOrder(self, self.nodes[node].right, output);
    }

    function inOrderTraversal(Tree storage self) internal view returns (uint[] memory) {
        uint[] memory temp = new uint[](1000); // max 1000 nodes for example
        uint[] storage output;
        inOrder(self, self.root, output);
        return output;
    }

    function rangeQuery(Tree storage self, uint min, uint max, uint[] storage results) internal view {
        _rangeQuery(self, self.root, min, max, results);
    }

    function _rangeQuery(Tree storage self, uint node, uint min, uint max, uint[] storage results) private view {
        if (node == NULL) return;
        if (min < node) _rangeQuery(self, self.nodes[node].left, min, max, results);
        if (min <= node && node <= max) results.push(node);
        if (max > node) _rangeQuery(self, self.nodes[node].right, min, max, results);
    }
}

contract RedBlackTreeTest {
    using RedBlackTreeLib for RedBlackTreeLib.Tree;
    RedBlackTreeLib.Tree private tree;

    function insert(uint key) external {
        tree.insert(key);
    }

    function getMinimum() external view returns (uint) {
        return tree.getMin();
    }

    function getMaximum() external view returns (uint) {
        return tree.getMax();
    }

    function checkExists(uint key) external view returns (bool) {
        return tree.exists(key);
    }

    function getRange(uint min, uint max) external view returns (uint[] memory result) {
        tree.rangeQuery(min, max, result);
    }

    function getInOrder() external view returns (uint[] memory result) {
        result = tree.inOrderTraversal();
    }
}
함수 설명
insert(uint) 트리에 값 삽입 (자동 균형 유지)
getMin() 최소값 조회
getMax() 최대값 조회
내부 함수 insertFixup, rotateLeft, rotateRight
exists(key) 특정 키 존재 여부 확인 (mapping 기반)
inOrderTraversal() 중위 순회 (작은 값부터 정렬된 리스트 반환)
rangeQuery(min, max) 특정 범위 내의 값들만 반환

🧱 구조 설명:
• mapping(uint => Node)을 통해 포인터 대신 키 기반 연결
• Node는 parent, left, right, red, exists 필드로 구성
• rotateLeft와 rotateRight로 균형 조정
• mapping(uint => Node)을 통해 트리 구조 유지
• insert 시 자동으로 균형 맞춤 (insertFixup)
• 중위 순회 및 범위 탐색을 통해 정렬된 데이터 활용 가능
• 최대 1000개 노드 기준으로 inOrderTraversal() 지원 (확장 가능)

'Solidity' 카테고리의 다른 글

binary Heap vs. RBT  (0) 2025.03.26
Solidity : 최대값 검색을 위한 Heap Tree 구현  (0) 2025.03.26
연결 리스트 구현 개선  (0) 2025.03.26
Solidity 로 연결 리스트 구현하기  (0) 2025.03.26
인터페이스 + 상속 조합  (0) 2025.03.25
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/**
 * ✅ Max Heap (Binary Heap 기반)
 * - Efficient insert and extractMax (O(log n))
 * - Implemented as a 1-based array (0th index unused)
 */
contract MaxHeap {
    uint[] private heap;

    constructor() {
        heap.push(0); // index 0은 사용하지 않음
    }

    function insert(uint value) external {
        heap.push(value);
        _heapifyUp(heap.length - 1);
    }

    function extractMax() external returns (uint) {
        require(heap.length > 1, "heap is empty");

        uint max = heap[1];
        heap[1] = heap[heap.length - 1];
        heap.pop();
        _heapifyDown(1);
        return max;
    }

    function peekMax() external view returns (uint) {
        require(heap.length > 1, "heap is empty");
        return heap[1];
    }

    function size() external view returns (uint) {
        return heap.length - 1;
    }

    function getAll() external view returns (uint[] memory) {
        uint[] memory result = new uint[](heap.length - 1);
        for (uint i = 1; i < heap.length; i++) {
            result[i - 1] = heap[i];
        }
        return result;
    }

    function _heapifyUp(uint index) internal {
        while (index > 1) {
            uint parent = index / 2;
            if (heap[parent] >= heap[index]) {
                break;
            }
            (heap[parent], heap[index]) = (heap[index], heap[parent]);
            index = parent;
        }
    }

    function _heapifyDown(uint index) internal {
        uint length = heap.length;
        while (2 * index < length) {
            uint left = 2 * index;
            uint right = 2 * index + 1;
            uint largest = index;

            if (heap[left] > heap[largest]) {
                largest = left;
            }
            if (right < length && heap[right] > heap[largest]) {
                largest = right;
            }
            if (largest == index) break;

            (heap[index], heap[largest]) = (heap[largest], heap[index]);
            index = largest;
        }
    }
}

✨ 주요 기능:
• insert(uint) → 값을 힙에 추가 (O(log n))
• extractMax() → 최대값 추출 (O(log n))
• peekMax() → 최대값 확인 (읽기 전용)
• getAll() → 전체 힙 배열 반환
• 내부적으로 1-based 배열로 구현하여 부모/자식 인덱스 계산을 직관적으로 구성

'Solidity' 카테고리의 다른 글

binary Heap vs. RBT  (0) 2025.03.26
Solidity: RBT 구현 예제  (0) 2025.03.26
연결 리스트 구현 개선  (0) 2025.03.26
Solidity 로 연결 리스트 구현하기  (0) 2025.03.26
인터페이스 + 상속 조합  (0) 2025.03.25

이건 EVM(Ethereum Virtual Machine)의 스토리지 구조의 근본적인 이해와 관련된 주제로,
왜 Solidity에서 배열보다 mapping이 많이 사용되는지에 대한 배경이기도 합니다.

✅ 먼저 요약부터

접근 구조 특징 어떤 자료구조가 유리한가?
✅ 랜덤 접근 기반 (EVM) 연속된 메모리 주소가 없음, 해시로 위치 계산 ✅ mapping
✅ 연속 메모리 기반 (CPU, 일반 컴퓨터) 배열은 캐시 친화적, 빠른 순회 ✅ array (배열)

📦 1. EVM의 Storage 구조: 왜 랜덤 접근인가?

EVM에서의 storage는 “key-value 형태의 거대한 해시 테이블”로 이해할 수 있어요.

uint256 public foo; // 저장됨: slot 0
mapping(address => uint256) balances; // 저장됨: slot keccak256(encodePacked(address, slot))

🎯 중요한 사실:
• EVM은 연속된 물리적 메모리를 사용하지 않아요.
• 대신, storage는 2^256 개의 슬롯을 가진 가상 공간이며,
• 접근은 keccak256(slot) 계산을 통해 이뤄져요.

➕ 예시: mapping(address => uint)

slot = keccak256(abi.encodePacked(key, mappingSlot))

✔️ 이 말은, 각 키가 전혀 다른 위치에 저장되며, 해시 기반으로 빠르게 접근할 수 있다는 뜻입니다.

💡 왜 EVM에서는 mapping이 유리할까?

✅ 이유 1. 슬롯이 연속되지 않음
• 배열의 arr[i]처럼 slot = base + i로 계산할 수 없는 경우가 많음

✅ 이유 2. 해시 접근이 기본 설계
• mapping은 해시 기반으로 바로 슬롯을 찾아가기 때문에 속도는 빠르고, 구조가 심플

✅ 이유 3. gas 효율성
• 배열은 크기를 늘리거나 반복할 때 storage 반복이 필요
• mapping은 특정 키만 저장 → 불필요한 루프 없이 직접 접근 가능

🔁 반대로: 왜 일반 컴퓨터에서는 배열이 유리할까?

✅ CPU의 메모리 접근은 연속 메모리 기반
• 배열은 메모리에 연속으로 배치됨
• CPU 캐시, branch prediction, SIMD 등과 결합되어 폭발적인 순회 속도 제공

예:

int arr[100];
for (int i = 0; i < 100; i++) {
  arr[i] += 1; // 매우 빠름 (CPU 캐시 프렌들리)
}

🔄 반면 해시 테이블 (HashMap 등)은:
• 포인터와 해시 충돌을 고려해야 해서
• 순회 성능은 느릴 수 있음

📌 비교 요약

구조 일반 컴퓨터 (CPU/메모리) EVM (블록체인 가상 머신)
메모리 구조 연속된 물리적 메모리 2^256개의 해시 슬롯
배열 성능 ✅ 빠름 (cache-friendly) ❌ 느림 (slot 순회 필요)
mapping 성능 ❌ 해시 계산 필요 ✅ 빠름 (keccak 기반 접근)
순회 배열이 유리 mapping은 순회 불가 (keys 저장 필요)

🎯 결론

✅ EVM에서 mapping이 배열보다 더 유리한 이유는 다음과 같습니다:
1. EVM의 스토리지는 연속 메모리 접근이 불가능함
2. keccak256(slot) 기반 랜덤 슬롯 계산이 기본 설계
3. mapping은 필요한 데이터에만 직접 접근 가능
4. 배열은 순회와 크기 증가 시 gas 소모가 크고, 확장성 낮음

🔧 추가된 항목:
1. LinkedListLib
• append, remove, getAll 함수를 포함한 재사용 가능한 라이브러리
• mapping 기반으로 설계된 List 구조체 포함
2. GenericLinkedList 컨트랙트
• 위 라이브러리를 사용한 일반화된 Linked List
• 코드 재사용 가능, 깔끔한 구조
3. Gas 최적화
• 불필요한 상태 변경 제거
• 라이브러리화를 통해 반복되는 코드 최소화

✅ 확장 완료된 기능:

📌 LinkedListLib에 추가된 고급 기능
1. insertAfter(afterId, data)
특정 노드 뒤에 새로운 노드 삽입
2. findByData(target)
값을 기준으로 노드 ID 탐색
3. clear()
전체 리스트 초기화 (모든 노드 제거)

🧪 GenericLinkedList 컨트랙트에 연동된 함수
• insertAfter(...)
• find(...)
• clearAll()

이제 리스트 구조는 완전한 CRUD + 검색 + 초기화 + 재사용까지 가능한 구조입니다.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

library LinkedListLib {
    struct Node {
        uint data;
        uint next;
        bool exists;
    }

    struct List {
        mapping(uint => Node) nodes;
        uint head;
        uint length;
        uint idCounter;
    }

    function init(List storage list) internal {
        list.head = 0;
        list.nodes[0] = Node({ data: 0, next: 0, exists: true });
        list.idCounter = 1;
    }

    function append(List storage list, uint _data) internal {
        uint current = list.head;
        while (list.nodes[current].next != 0) {
            current = list.nodes[current].next;
        }

        uint newId = list.idCounter++;
        list.nodes[newId] = Node({ data: _data, next: 0, exists: true });
        list.nodes[current].next = newId;
        list.length++;
    }

    function insertAfter(List storage list, uint afterId, uint _data) internal {
        require(list.nodes[afterId].exists, "after node not found");

        uint newId = list.idCounter++;
        list.nodes[newId] = Node({ data: _data, next: list.nodes[afterId].next, exists: true });
        list.nodes[afterId].next = newId;
        list.length++;
    }

    function remove(List storage list, uint id) internal {
        require(id != list.head, "cannot remove head");
        require(list.nodes[id].exists, "not found");

        uint prev = list.head;
        while (list.nodes[prev].next != id && list.nodes[prev].next != 0) {
            prev = list.nodes[prev].next;
        }
        require(list.nodes[prev].next == id, "not found");

        list.nodes[prev].next = list.nodes[id].next;
        delete list.nodes[id];
        list.length--;
    }

    function findByData(List storage list, uint target) internal view returns (uint) {
        uint current = list.nodes[list.head].next;
        while (current != 0) {
            if (list.nodes[current].data == target) {
                return current;
            }
            current = list.nodes[current].next;
        }
        return 0; // not found
    }

    function getAll(List storage list) internal view returns (uint[] memory) {
        uint[] memory result = new uint[](list.length);
        uint current = list.nodes[list.head].next;
        uint i = 0;
        while (current != 0) {
            result[i++] = list.nodes[current].data;
            current = list.nodes[current].next;
        }
        return result;
    }

    function clear(List storage list) internal {
        uint current = list.nodes[list.head].next;
        while (current != 0) {
            uint next = list.nodes[current].next;
            delete list.nodes[current];
            current = next;
        }
        list.nodes[list.head].next = 0;
        list.length = 0;
    }
}

contract GenericLinkedList {
    using LinkedListLib for LinkedListLib.List;
    LinkedListLib.List private list;

    constructor() {
        list.init();
    }

    function add(uint value) external {
        list.append(value);
    }

    function insertAfter(uint afterId, uint value) external {
        list.insertAfter(afterId, value);
    }

    function remove(uint id) external {
        list.remove(id);
    }

    function find(uint value) external view returns (uint) {
        return list.findByData(value);
    }

    function all() external view returns (uint[] memory) {
        return list.getAll();
    }

    function clearAll() external {
        list.clear();
    }
}

'Solidity' 카테고리의 다른 글

Solidity: RBT 구현 예제  (0) 2025.03.26
Solidity : 최대값 검색을 위한 Heap Tree 구현  (0) 2025.03.26
Solidity 로 연결 리스트 구현하기  (0) 2025.03.26
인터페이스 + 상속 조합  (0) 2025.03.25
Solidity 에서의 상속  (0) 2025.03.25

Solidity는 기본적으로 배열(array)과 mapping(map)은 제공하지만,
✅ linked list 자료구조는 제공하지 않습니다.
→ 따라서 직접 구현해야 합니다.

🧩 왜 Solidity는 Linked List를 제공하지 않을까?
• EVM의 storage 구조가 랜덤 접근 기반이기 때문에
→ 연속된 메모리보다 keccak256(slot) 방식의 mapping 기반 인덱싱이 더 유리합니다.
• Solidity의 gas 비용 구조상, linked list는 효율적이지 않은 경우가 많습니다.
• 그래도 👇 순차적 삽입/삭제가 필요한 경우엔 여전히 유용합니다.

✅ 예제1: 단일 연결 리스트 (Singly Linked List)

아래는 Solidity로 구현한 간단한 Singly Linked List 예제입니다.

🔧 Solidity 코드 (0.8.x 이상)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract LinkedList {
    struct Node {
        uint data;
        uint next;
        bool exists;
    }

    mapping(uint => Node) public nodes;
    uint public head;
    uint public length;
    uint private idCounter;

    constructor() {
        head = 0; // 초기에는 0이 head (0은 dummy head 역할)
        nodes[head] = Node({ data: 0, next: 0, exists: true });
        idCounter = 1;
    }

    /// @notice 리스트 끝에 추가
    function append(uint _data) external {
        uint current = head;
        while (nodes[current].next != 0) {
            current = nodes[current].next;
        }

        uint newId = idCounter++;
        nodes[newId] = Node({ data: _data, next: 0, exists: true });
        nodes[current].next = newId;
        length++;
    }

    /// @notice 특정 노드 ID 기준으로 삭제 (head는 삭제 불가)
    function remove(uint id) external {
        require(id != head, "cannot remove dummy head");
        require(nodes[id].exists, "node not exists");

        uint prev = head;
        while (nodes[prev].next != id && nodes[prev].next != 0) {
            prev = nodes[prev].next;
        }
        require(nodes[prev].next == id, "node not found");

        nodes[prev].next = nodes[id].next;
        delete nodes[id];
        length--;
    }

    /// @notice 전체 리스트 조회
    function getAll() external view returns (uint[] memory) {
        uint[] memory result = new uint[](length);
        uint current = nodes[head].next;
        uint i = 0;
        while (current != 0) {
            result[i++] = nodes[current].data;
            current = nodes[current].next;
        }
        return result;
    }
}

📌 핵심 설명

요소 설명
mapping(uint => Node) 노드를 ID로 저장 (linked list의 key 역할)
Node 구조체 data, next, exists 필드 포함
head 항상 0번 노드 (dummy head)
idCounter 노드마다 고유한 ID 부여

✅ 사용 예시
1. append(100) → 노드 추가
2. getAll() → [100]
3. append(200) → [100, 200]
4. remove(1) → [200] (ID 1번 노드 삭제)

🧠 추가 팁
• 양방향 리스트도 구현 가능: prev 필드를 추가하면 됩니다
• LinkedListLib로 만들면 재사용 가능
• 메모리 구조와 storage 구조를 구분해서 설계하세요

✅ 예제2: 단일/양방향/Enumable(반복가능한) 연결 리스트

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/**
 * ✅ Singly Linked List
 */
contract SinglyLinkedList {
    struct Node {
        uint data;
        uint next;
        bool exists;
    }

    mapping(uint => Node) public nodes;
    uint public head;
    uint public length;
    uint private idCounter;

    constructor() {
        head = 0; // dummy head
        nodes[head] = Node({ data: 0, next: 0, exists: true });
        idCounter = 1;
    }

    function append(uint _data) external {
        uint current = head;
        while (nodes[current].next != 0) {
            current = nodes[current].next;
        }

        uint newId = idCounter++;
        nodes[newId] = Node({ data: _data, next: 0, exists: true });
        nodes[current].next = newId;
        length++;
    }

    function remove(uint id) external {
        require(id != head, "cannot remove head");
        require(nodes[id].exists, "not found");

        uint prev = head;
        while (nodes[prev].next != id && nodes[prev].next != 0) {
            prev = nodes[prev].next;
        }
        require(nodes[prev].next == id, "not found");

        nodes[prev].next = nodes[id].next;
        delete nodes[id];
        length--;
    }

    function getAll() external view returns (uint[] memory) {
        uint[] memory result = new uint[](length);
        uint current = nodes[head].next;
        uint i = 0;
        while (current != 0) {
            result[i++] = nodes[current].data;
            current = nodes[current].next;
        }
        return result;
    }
}

/**
 * ✅ Doubly Linked List
 */
contract DoublyLinkedList {
    struct Node {
        uint data;
        uint next;
        uint prev;
        bool exists;
    }

    mapping(uint => Node) public nodes;
    uint public head;
    uint public tail;
    uint public length;
    uint private idCounter;

    constructor() {
        head = 0;
        tail = 0;
        idCounter = 1;
    }

    function append(uint _data) external {
        uint newId = idCounter++;
        nodes[newId] = Node({ data: _data, next: 0, prev: tail, exists: true });

        if (length == 0) {
            head = newId;
        } else {
            nodes[tail].next = newId;
        }

        tail = newId;
        length++;
    }

    function remove(uint id) external {
        require(nodes[id].exists, "not found");

        if (nodes[id].prev != 0) {
            nodes[nodes[id].prev].next = nodes[id].next;
        } else {
            head = nodes[id].next;
        }

        if (nodes[id].next != 0) {
            nodes[nodes[id].next].prev = nodes[id].prev;
        } else {
            tail = nodes[id].prev;
        }

        delete nodes[id];
        length--;
    }

    function getAllForward() external view returns (uint[] memory) {
        uint[] memory result = new uint[](length);
        uint current = head;
        uint i = 0;
        while (current != 0) {
            result[i++] = nodes[current].data;
            current = nodes[current].next;
        }
        return result;
    }

    function getAllBackward() external view returns (uint[] memory) {
        uint[] memory result = new uint[](length);
        uint current = tail;
        uint i = 0;
        while (current != 0) {
            result[i++] = nodes[current].data;
            current = nodes[current].prev;
        }
        return result;
    }
}

/**
 * ✅ Enumerable Iterable Linked List (Mapping + Array)
 */
contract EnumerableList {
    struct Item {
        uint data;
        bool exists;
    }

    mapping(uint => Item) public items;
    uint[] public keys;

    function add(uint key, uint value) external {
        require(!items[key].exists, "already exists");
        items[key] = Item({ data: value, exists: true });
        keys.push(key);
    }

    function remove(uint key) external {
        require(items[key].exists, "not exists");
        delete items[key];
        for (uint i = 0; i < keys.length; i++) {
            if (keys[i] == key) {
                keys[i] = keys[keys.length - 1];
                keys.pop();
                break;
            }
        }
    }

    function getAll() external view returns (uint[] memory values) {
        uint count = 0;
        for (uint i = 0; i < keys.length; i++) {
            if (items[keys[i]].exists) count++;
        }

        values = new uint[](count);
        uint j = 0;
        for (uint i = 0; i < keys.length; i++) {
            if (items[keys[i]].exists) {
                values[j++] = items[keys[i]].data;
            }
        }
    }
}

'Solidity' 카테고리의 다른 글

Solidity : 최대값 검색을 위한 Heap Tree 구현  (0) 2025.03.26
연결 리스트 구현 개선  (0) 2025.03.26
인터페이스 + 상속 조합  (0) 2025.03.25
Solidity 에서의 상속  (0) 2025.03.25
solidity 의 hash table  (0) 2025.03.25

web3.js와 ethers.js는 둘 다 Ethereum과 상호작용할 수 있게 해주는 대표적인 JavaScript/TypeScript 라이브러리입니다.
하지만 내부 설계, API 스타일, 기능 지원, 타입 안전성 등에서 꽤 큰 차이가 있어요.

🔍 web3.js vs ethers.js v6 비교분석

항목 web3.js ethers.js v6
🧠 설계 철학 API 친숙함 & 전통적인 JS 스타일 경량화 + 함수형 + 타입 안정성
🧾 최신 버전 v1.10.x (2024 기준) ✅ v6.x (대폭 리팩토링됨)
🔧 모듈화 대부분 하나의 번들로 제공 ✅ 모듈별 tree-shaking 지원
📦 번들 크기 크고 무겁다 (~800KB) ✅ 작고 가볍다 (~100KB)
🔤 타입스크립트 지원 제한적 (@types/web3) ✅ 완전 지원, 타입 안전성 강력
📡 Provider 체계 단순한 연결 모델 ✅ 정교한 Provider 구조 (JsonRpcProvider, WebSocketProvider, etc)
🧮 단위 변환 .toWei(), .fromWei() 등 util 중심 ✅ parseEther(), formatUnits() 등 직관적 함수형 API
🛠 ABI 사용 복잡하거나 manual한 처리 필요 ✅ Contract 생성이 깔끔하고 직관적
🔐 Wallet/Signer 불편하고 외부 패키지 필요 ✅ Wallet, Signer 내장
🧩 플러그인 구조 없음 ✅ 커스터마이징 가능한 Plugin 설계 지원
🧪 테스트 환경 호환 Hardhat, Ganache 등과 호환 ✅ 완벽한 Hardhat 연동 (ethers.provider)
🗃 상태관리 (call/send) .methods.foo().call() ✅ 함수처럼 contract.foo()

🧪 코드 비교 예시

  1. 잔액 조회

🔷 web3.js

const Web3 = require("web3");
const web3 = new Web3("https://mainnet.infura.io/v3/KEY");

const balanceWei = await web3.eth.getBalance("0x...");
const balanceEth = web3.utils.fromWei(balanceWei, "ether");

🔶 ethers v6

import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider("https://mainnet.infura.io/v3/KEY");

const balance = await provider.getBalance("0x...");
console.log(ethers.formatEther(balance));

  1. Contract 호출

🔷 web3.js

const contract = new web3.eth.Contract(abi, address);
const name = await contract.methods.name().call();

🔶 ethers v6

const contract = new ethers.Contract(address, abi, provider);
const name = await contract.name();

✅ ethers는 .methods.call() 패턴 없이 자연스럽게 동작합니다.

✅ 장단점 요약

🔷 web3.js

✅ 장점
• 문서 많고 사용자가 많음
• Ethereum 초창기부터 사용된 안정된 라이브러리
• 간단한 기능은 배우기 쉽다

❌ 단점
• 타입스크립트 지원 부족
• 모듈 크고 무거움
• .methods().call() 등 불편한 호출 방식
• 비동기 체계와 예외 처리 어려움

🔶 ethers.js v6

✅ 장점
• 완전한 TypeScript 지원 (타입 추론 정확)
• 더 깔끔하고 모던한 API
• 가볍고 빠른 빌드 타임
• Hardhat과 궁합 좋음 (기본 내장)
• Wallet, Signer, Provider, Interface 등이 모두 잘 구조화됨

❌ 단점
• v6은 v5와 호환 안 되므로 일부 사용법 변경 필요
• 최신 기능 학습이 조금 필요

🧭 선택 가이드

상황 추천
TypeScript 프로젝트 ✅ ethers.js v6
Hardhat 기반 테스트 ✅ ethers.js
오래된 코드/문서 기반 유지 web3.js (v1.x)
속도/용량 최적화 필요 ✅ ethers.js
단순한 학습용/시작용 web3.js 가능하지만 오래된 구조

✅ 결론

실전 프로젝트, 특히 TypeScript 기반이라면
🔥 ethers.js v6가 사실상 표준이며 최선의 선택입니다.

JavaScript(JS)와 TypeScript(TS)는 모두 스마트 컨트랙트 개발에서 널리 사용되지만,
📦 툴링, 🔧 디버깅, 🧪 테스트 작성, 💡 스마트 컨트랙트와 상호작용 같은 실제 개발 업무에서는 TypeScript가 훨씬 더 유리합니다.

아래에서 스마트 컨트랙트 개발자 입장에서 JS와 TS를 기능/생산성/안정성/도구 호환성 측면으로 비교 분석해드릴게요.

🧠 개요: JavaScript vs TypeScript

항목 JavaScript (JS) TypeScript (TS)
기본 언어 동적(dynamic), 느슨한 타입 정적(static), 엄격한 타입
실행 대상 바로 실행 가능 컴파일 후 실행 (.ts → .js)
타입 검사 없음 (런타임 오류) ✅ 컴파일 시점 오류 확인
학습 난이도 낮음 중간 (타입 선언 등 추가 개념 필요)

🔍 스마트 컨트랙트 개발 관점 비교

1️⃣ ✅ 타입 안정성 & 자동완성

항목 JavaScript TypeScript
컨트랙트 ABI 자동 완성 ❌ 없음 ✅ 지원 (TypeChain, Hardhat, ethers.js)
balanceOf(address) 호출 시 주소 누락해도 실행됨 → 런타임 오류 ✅ 타입 미스 오류 컴파일 시 감지
오탈자 방지 ❌ 없음 ✅ 강력한 IDE 지원 (VSCode 등)

📌 정확한 타입은 테스트 코드, 배포 스크립트, 유저 인터랙션에서 필수 요소입니다.

2️⃣ 🔧 Hardhat, ethers.js, web3.js와의 호환성

라이브러리 JS 지원 TS 지원
Hardhat ✅ (TS가 기본 권장)
ethers.js ✅ 완벽 (v6는 TS 기반으로 작성됨)
web3.js ✅ (@types/web3 필요)
TypeChain ✅ 스마트 컨트랙트 → 정적 타입 자동 생성 도구

✅ TypeScript는 최신 Ethereum 개발 툴체인과 가장 잘 맞는 환경입니다.

3️⃣ 🧪 테스트 및 배포 스크립트

🔷 JavaScript

const balance = await token.methods.balanceOf(account).call();
•    오타 → 실행 중 실패
•    인자 부족 → 런타임 오류

🔶 TypeScript

const balance: bigint = await token.balanceOf(account);
•    account 누락 시 컴파일 에러
•    반환값 타입 자동 추론 (bigint, string, BigNumber 등)

4️⃣ 📦 확장성과 유지보수

|항목| JavaScript| TypeScript|
|팀 협업| ❌ 타입 추론 어려움| ✅ 강력한 타입 계약으로 협업 수월|
|코드 규모 확장| ❌ 오류 잦음| ✅ 구조적 리팩토링 용이|
|테스트 커버리지| ❌ IDE 도움 부족| ✅ VSCode, IntelliJ 등에서 완벽한 추적 지원|

5️⃣ 🌐 프론트엔드 연계 (React, Next.js 등)
• dApp 개발 시 React 연동이 많음 → ethers.js + TS 조합이 사실상 표준
• wagmi, viem, useContractRead() 등 모두 TS 기반으로 설계

✅ 요약: 스마트 컨트랙트 개발자 기준 비교표

항목 JavaScript TypeScript
타입 안정성 ❌ 없음 ✅ 있음 (컴파일 타임 확인)
도구 통합 보통 ✅ 최적화됨 (ethers, hardhat, TypeChain 등)
ABI 활용 수동 ✅ 자동 타입 생성 가능
생산성 초반 빠름 ✅ 중장기 더 안정적
유지보수 어렵고 위험 ✅ 큰 규모에 강함
권장 여부 ❌ 단기 실습용 ✅ 실전 개발자용 (사실상 표준)

🎯 결론

✅ 스마트 컨트랙트 개발자의 실전 선택지는 TypeScript입니다.
특히 Hardhat, Ethers.js, TypeChain, React와의 궁합이 탁월해요.

+ Recent posts