이더리움 기반 스마트 계약 개발하기 – 계약으로 송금된 코인 관리하기

앞서 어떻게 payable을 사용해 contract으로 이더리움을 전송받는지 간단하게 체크해 봤는데요, 이제 여기서 조금 더 진화해서 어떻게 계약으로 전송받은 이더리움을 관리할 수 있는지 살펴보겠습니다.

일단 제가 만든 계약의 조건은 아래와 같습니다.

1. 계약으로는 누구든지 송금할 수 있다.
2. 계약으로 송금된 금액은 누구든지 확인할 수 있다.
3. 하지만 계약의 잔액을 누군가한테 송금하는 것은 해당 계약을 만든 자만 할 수 있다.
4. 출금은 기본적으로 계약의 잔액 전액을 계약을 만든 사람에게 보낸다.

먼저, 계약으로 누구든지 송금이 가능하게 하는 것은 앞서 다뤘다시피 payable 방법을 통해서 구현이 가능합니다.

...

function () payable public {}

...

이번엔 계약으로 송금된 금액을 누구든지 확인할 수 있게 하는 함수를 구현합시다.

...

function getBalance() constant public returns (uint) {
    address contractAddress = this;
    return contractAddress.balance;
}

...

this를 address 변수로 받아오면서 지금 계약의 주소를 가져올 수 있습니다. 그리고 그 address의 balance를 리턴함으로써 계약에 송금된 잔액을 확인할 수 있습니다.

이제는 잔액을 누군가한테 송금하는 함수와 전액 출금해서 계약 생성자에게 보내는 함수 두가지만 남았는데, 기본적으로 이 함수들은 반드시 계약 생성자에 의해서만 실행되어야 합니다.(안그러면 다른 사람들이 다 자기 주소로 잔액을 털어가 버리겠죠?) 이를 위해서 생성자에서 owner라는 멤버 변수로 계약 생성자의 주소를 저장해 놓습니다.

contract ContractMoney {
    address public owner;

    function ContractMoney() public {
        owner = msg.sender;
    }

...

그리고 이 계약 생성자의 주소를 이용하여 특정 함수가 owner에 의해서만 실행될 수 있게 modifier를 정의할 수 있습니다. (modifier에 대한 설명은 이 링크를 참조하세요.)

modifier ownerOnly() {
    if (owner == msg.sender) {_;}
}

이제 이 modifier를 가지고 특정 주소로 특정 금액을 송금하는 함수를 구현해 보겠습니다.

function send(address to, uint amount) public ownerOnly {
    to.transfer(amount);
}

마지막으로 계약에 있는 잔액을 전액 출금해서 생성자 주소로 보내는 함수를 구현해 보겠습니다.

function withdraw() public ownerOnly {
    send(owner, getBalance()); // 계약 최초 생성자에게 계약에 있는 전액을 송금합니다.
}

이제 이 함수들이 잘 동작하는지 실제로 테스트해 보겠습니다.
Screen Shot 2018-03-31 at 8.39.30 AM.png

일단 계약 생성했을 때, 계약에 잔액이 0이고 생성자가 잘 세팅되었음을 확인할 수 있습니다.
그리고 이 계약으로 1000 wei를 송금해 보겠습니다.
Screen Shot 2018-03-31 at 8.40.15 AM.png잘 입금되었음을 확인할 수 있습니다. 계약 생성자가 아닌 주소에서 send함수를 호출했을 때, transfer가 일어나지 않음을 확인하실 수 있습니다.
Screen Shot 2018-03-31 at 8.40.55 AM.png
생성자 주소인 0xca35.. 가 아닌 0x147..에서 send함수를 실행했지만, 잔액은 여전히 1000입니다.

이제 좀 더 확실한 크기로 차이를 확인하기 위해서 이 계약으로 1 ether를 보내겠습니다.
Screen Shot 2018-03-31 at 8.43.07 AM.png잔액이 wei단위라서 엄청 숫자가 크네요 🙂 이제 생성자 주소에서 withdraw함수를 사용하여 전액 본인의 주소로 송금해 보겠습니다.

Screen Shot 2018-03-31 at 8.43.25 AM.png일단 계약의 잔액은 성공적으로 0이 되었음을 보실 수 있구요,
Screen Shot 2018-03-31 at 8.43.40 AM.png생성자 주소에 원래 있던 100 ether에서 계약 생성 등의 gas로 나간 것들을 빼면 거의 101 ether가 되었으므로 성공적으로 계약에 들어온 이더리움을 출금했음을 확인할 수 있습니다 🙂

이더리움 기반 스마트 계약 개발하기 – payable

이번에 알아볼 solidity 기반 스마트 계약의 기능 중 하나는 payable이란 것입니다.

예를 들면 스마트 계약을 통해 크라우드 펀딩을 하려고 한다고 합시다. 그렇게 되면 크라우드 펀딩의 기능을 갖춘 스마트 계약을 만들고, 그 주소로 이더리움을 모아야 할 것입니다. 아니면 ICO와 같이 이더리움을 받고 새로 발행한 토큰을 가격에 맞춰 교환하는, 그런 기능을 가져야 할 것입니다.

스마트 계약의 주소로 이더리움을 받고 싶을 때 사용하는 기능이 바로 payable입니다.
아주 간단한 예제를 통해 살펴보겠습니다.

pragma solidity ^0.4.21;

contract PayableTest {
    address public last_sender;
    uint public received_wei;

    function () payable public {
        last_sender = msg.sender;
        received_wei += msg.value;
    }
}

위의 코드에서 눈여겨 봐야 할 부분은 function () payable public이란 부분입니다.
일단 특이한 점은 함수 이름과 parameter가 없습니다. 물론 이름도 지정할 수 있고, parameter도 넣을 수 있습니다. 하지만 이렇게 이름이 없고 parameter가 없는 payable함수의 경우 일반적으로 그냥 계약 주소로 이더리움을 송금했을 때 호출되는 함수입니다.

msg.sender는 송금자의 주소를, msg.value는 송금액을 나타내게 됩니다.

계약을 만들고 처음은 받은 wei도 없고, 보낸이의 주소도 0으로 초기화되어 있습니다.
Screen Shot 2018-03-31 at 3.13.44 AM.png

이제 해당 계약의 주소로 100 wei를 보내보도록 하겠습니다.
Screen Shot 2018-03-31 at 3.14.12 AM.png

그리고 다시 한번 last_senderreceived_wei 값을 확인해 보겠습니다.
Screen Shot 2018-03-31 at 3.14.22 AM.png

보시는 바와 같이 방금 보낸 송금자의 주소와 금액으로 함수가 잘 호출 되었음을 확인할 수 있습니다 🙂

Algorithm 문제 : Find Duplicate Subtrees

문제 출처 : https://leetcode.com/problems/find-duplicate-subtrees/description/

이번 문제는, binary tree가 주어졌을 때, 가능한 모든 중복되는 subtree를 리턴하는 문제입니다. 단, 리턴할 때 해당 subtree의 root node만 리턴하면 됩니다.
중복된다는 의미는 두 tree가 동일한 구조를 갖고 각 노드가 동일한 값을 갖는 것을 의미합니다.

예를 들면,

        1
       / \
      2   3
     /   / \
    4   2   4
       /
      4

이런 트리가 있으면, 이 구조에선 두 개의 duplicate subtree를 발견할 수 있습니다.

      2
     /       와      4
    4

이 때 정답은 각 duplicate subtree의 root인 2와 4의 노드만 리턴하면 됩니다.

이 문제를 접근하는 첫번째 방법은 tree를 serialize해서 해당 serial이 두번 이상 등장했는지 체크하는 방법입니다. 아래의 코드를 보시겠습니다.

def findDuplicateSubtrees(root):
    """
    :type root: TreeNode
    :rtype: List[TreeNode]
    """

    # 각 serial이 몇번 등장하는지를 기록할 dictionary입니다.
    counter = {}
    
    # 최종적으로 duplicate subtree의 정보를 가지고 있을 리스트입니다.
    result = []

    def serializeTree(node):
        # 해당 노드가 None이면 '#'으로 serialize합니다.
        # 이를 통해 자식 노드가 없으면 없다는 걸 확실히 표현할 수 있습니다.
        if node is None:
            return "#"
        
        # 본인의 값과 각 좌우 자식에서 만들어진 serial을 가져와 붙여서 새로운 serial을 만듭니다.
        serial = "{},{},{}".format(node.val, serializeTree(node.left), serializeTree(node.right))
        
        # counter에 해당 serial의 등장 횟수를 증가시킨 후,
        # 2번 이상이면 바로 최종 결과에 append 시켜줍니다.
        counter[serial] = counter.get(serial, 0) + 1
        if counter[serial] == 2:
            result.append(node)
            
        return serial

    serializeTree(root)
    return result

이렇게 되면 각 노드를 한번씩 방문하게 되고, 그 한번 방문때마다 자녀들의 serial들을 붙여야 하기 때문에 O(N)이 소요되므로 결국 O(N^2)의 complexity를 갖습니다.

그렇다면 사실 문제가 되는 부분은 자녀들의 serial을 붙이는 부분입니다. 이것이 root와 가까워질수록 붙여야 하는 길이가 길어지기 때문에 이런 사태가 벌어지는 것입니다. 따라서 우리가 각 노드마다 어떻게 생겼는지에 따라 unique한 id를 부여하면 어떨까요? 그리하여 일일이 긴 serial을 가지고 다니는 대신에 (node.val, left_child's id, right_child's id) 이렇게 간단하게 쿼리할 수 있게 말입니다. 이렇게 되면 불필요한 O(N)이 사라지므로 각 노드를 방문할 때 소요되는 O(N)의 complexity만 가질 수 있게 됩니다!

def findDuplicateSubtrees(self, root):
    # 종전과 다른 것은 고유한 tree를 저장하기 위한 데이터 구조가 새로 필요합니다.
    trees = {}
    
    count = {}
    ans = []

    def lookup(node):
        if node:
            # 왼쪽 자식의 고유 id를 가져옵니다.
            left_id = lookup(node.left)
            # 오른쪽 자식의 고유 id를 가져옵니다.
            right_id = lookup(node.right)
            
            # (자신의 값, 왼쪽 자식 고유 id, 오른쪽 자식 고유 id)로 key를 만들어서
            # 그것이 고유한 tree에 있으면 해당 id를 받아오고,
            # 기존에 없던 tree이면 unique한 번호를 부여합니다.
            if (node.val, left_id, right_id) not in trees:
                trees[(node.val, left_id, right_id)] = len(trees)
            uid = trees[(node.val, left_id, right_id)]

            # 나머지 로직은 똑같습니다. uid가 두번 이상 출현하면 최종 결과에 추가해줍니다.
            count[uid] = count.get(uid, 0) + 1
            if count[uid] == 2:
                ans.append(node)

            print uid
            return uid

    lookup(root)
    return ans