navie코인 번역 원문: lhartikk.github.io

#4: Wallet

소개

이번 챕터의 목적은 사용자를 위한 wallet 인터페이스를 만드는 거에요. 엔드유저는

  • private key를 가진 지갑(wallet)을 만들 수 있어야 하고
  • 지갑에 잔액을 볼 수 있어야 하고
  • 코인을 다른 노드로 보낼 수 있어야 해요.

txIns나 txOut같은 것들이 어떻게 작동하는지 엔드유저는 알 필요가 없어요. 비트코인처럼 어떤 노드로 코인을 보내고, 또 코인을 받을 나만의 주소를 가질 수 있으면 되는 거죠.

private key를 만들고 저장하기

이 튜토리얼에서는 wallet을 만들기 위해 아주 간단한 방법을 사용할 거에요. private key를 암호화하지는 않은 채로 node/wallet/private_key 이 경로에 만들도록 하죠.

const privateKeyLocation = 'node/wallet/private_key';

const generatePrivatekey = (): string => {
    const keyPair = EC.genKeyPair();
    const privateKey = keyPair.getPrivate();
    return privateKey.toString(16);
};

const initWallet = () => {
    //let's not override existing private keys
    if (existsSync(privateKeyLocation)) {
        return;
    }
    const newPrivateKey = generatePrivatekey();

    writeFileSync(privateKeyLocation, newPrivateKey);
    console.log('new wallet with private key created');
};

이미 살펴봤듯이 private key로부터 public key(=address, 주소의 역할)를 만들어 낼 거에요.

const getPublicFromWallet = (): string => {
    const privateKey = getPrivateFromWallet();
    const key = EC.keyFromPrivate(privateKey, 'hex');
    return key.getPublic().encode('hex');
};

private key를 암호화 하지 않은 것은 보안상 매우 위험하다는 걸 유념해주세요. 이 과정은 간단히 만드는 데 초점을 두고 있기 때문에 그렇게 하는 것일 뿐이에요. wallet은 오직 하나의 private key만을 가져야 하고 이로부터 pulic하게 접근가능한 address를 가진 지갑을 만들 거에요.

Wallet balance

이전 챕터에서 배운 걸 기억해 보죠. 당신이 블럭체인 상에서 코인을 가지고 있다는 것은 ‘쓰이지 않은 트랜잭션 아웃풋(unspent transaction outputs)’ 한 꾸러미를 가지고 있다는 것을 의미해요. 이 아웃풋 목록은 당연히 당신만의 private key와 매칭된 public address값을 가지고 있죠.

그럼 ‘잔고’계산은 참 쉽겠죠잉? 걍 ‘쓰이지 않은 트랜잭션 아웃풋(unspent transaction outputs)’을 다 더해버리면 되는 거죠.

const getBalance = (address: string, unspentTxOuts: UnspentTxOut[]): number => {
    return _(unspentTxOuts)
        .filter((uTxO: UnspentTxOut) => uTxO.address === address)
        .map((uTxO: UnspentTxOut) => uTxO.amount)
        .sum();
};

코드가 보여주듯, 잔고조회에 private key는 필요없어요. 이는 누구나 주어진 address의 잔고를 알 수 있다는 거죠.

트랜잭션 생성 Generating transactions

코인을 보낼 때 사용자는 트랜잭션 인풋이니 아웃풋이니 알 필요가 없다고 했죠. 그럼 이 사용자 A가 단 한번의 트랜잭션으로 그가 가진 50코인 중 단지 10코인만 B에게 보내고 싶다면 어떻게 해야 할까요? 방법은 10코인은 B에게 보내고 나머지 40코인은 그 자신에게 돌려보내(back)는 거죠. 전체 트랜잭션 아웃풋은 항상 소진(spent)되어야 하고, 만약 부분으로 나누고 싶다면 새로운 아웃풋을 생성함으로 가능해요. 그림을 보시죠. tx_generation

좀 더 복잡한 트랜잭션을 시도해 보죠.

  1. User C에겐 코인이 없었어요
  2. User C가 3트랜잭션으로 10, 20, 30코인을 차례로 받았어요
  3. User C는 55코인을 user D에게 보내길 원해요. 트랜잭션은 어떻게 이루어져야 할까요?

이 경우 3번에 걸친 아웃풋이 모두 사용되어야 해요. 55코인은 D에게 보내고 5는 자신에게 back. tx_generation2

코드로 확인해 보죠. 먼저 트랜잭션 인풋을 만들어야 해요. 이를 위해 ‘소진되지 않은 트랜잭션 아웃(unspent transaction outputs)’ 목록을 순회하며 우리가 원하는 금액이 될 때까지 반복문을 돌리죠.

const findTxOutsForAmount = (amount: number, myUnspentTxOuts: UnspentTxOut[]) => {
    let currentAmount = 0;
    const includedUnspentTxOuts = [];
    for (const myUnspentTxOut of myUnspentTxOuts) {
        includedUnspentTxOuts.push(myUnspentTxOut);
        currentAmount = currentAmount + myUnspentTxOut.amount;
        if (currentAmount >= amount) {
            const leftOverAmount = currentAmount - amount;
            return {includedUnspentTxOuts, leftOverAmount}
        }
    }
    throw Error('not enough coins to send transaction');
};

코드에서 보이듯이, leftOverAmount는 나중에 자신에게 다시 back할 금액이에요. 소진되지 않은 트랜잭션 아웃풋을 가진 만큼 트랜책션 txIns를 만들어낼 수 있어요.

const toUnsignedTxIn = (unspentTxOut: UnspentTxOut) => {
    const txIn: TxIn = new TxIn();
    txIn.txOutId = unspentTxOut.txOutId;
    txIn.txOutIndex = unspentTxOut.txOutIndex;
    return txIn;
};
const {includedUnspentTxOuts, leftOverAmount} = findTxOutsForAmount(amount, myUnspentTxouts);
const unsignedTxIns: TxIn[] = includedUnspentTxOuts.map(toUnsignedTxIn);

다음으로 두 개의 txOuts를 만들어야 해요. 하나는 보낼 것. 다른 하나는 다시 back할 것. 만약 txIns가 보낼 금액과 같다면 leftOverAmount값은 0이므로 back을 위한 트랜잭션은 만들지 않아도 되죠.

const createTxOuts = (receiverAddress:string, myAddress:string, amount, leftOverAmount: number) => {
    const txOut1: TxOut = new TxOut(receiverAddress, amount);
    if (leftOverAmount === 0) {
        return [txOut1]
    } else {
        const leftOverTx = new TxOut(myAddress, leftOverAmount);
        return [txOut1, leftOverTx];
    }
};

마지막으로 트랜잭션id값을 생성하고 txIns에 서명하면 끝.

const tx: Transaction = new Transaction();
    tx.txIns = unsignedTxIns;
    tx.txOuts = createTxOuts(receiverAddress, myAddress, amount, leftOverAmount);
    tx.id = getTransactionId(tx);

    tx.txIns = tx.txIns.map((txIn: TxIn, index: number) => {
        txIn.signature = signTxIn(tx, index, privateKey, unspentTxOuts);
        return txIn;
    });

지갑 사용하기 Using the wallet

지갑을 사용하기 위해 기능을 넣어 보죠.

app.post('/mineTransaction', (req, res) => {
        const address = req.body.address;
        const amount = req.body.amount;
        const resp = generatenextBlockWithTransaction(address, amount);
        res.send(resp);
    });

위에서 보듯 사용자는 단지 주소와 코인금액만 제공하면 되요. 블럭체인의 노드가 나머지는 알아서 뿅.

결론

우리가 한건 아주 간단한 수준의 트랜잭션 알고리즘을 가진 비암호화된 지갑이에요. 이 알고리즘은 2개의 outputs만을 만들어낼 뿐이지만, 실제 현실의 블럭체인은 다르죠. 당신은 이 지갑에서 50코인의 input을 가지고 5,15,20의 output을 만들 수 있어요. 하지만 이것도 /mineRawBlock 인터페이스를 사용해서 조절 가능하죠.

블럭체인에서 직접 채굴을 함으로써 원하는 트랜잭션을 발생시킬 수도 있어요. 발생시킨 트랜잭션을 블럭체인에 연결시키는 것을 다음 챕터에서 다루게 될 거에요. 이번 챕터의 코드는 여기에 있어요.

To chapter5