블록체인 -3 트랜잭션 (23/05/02)

nazzzo·2023년 5월 15일
0

1. 개인키와 공개키

https://brunch.co.kr/@nujabes403/13

서명 : 본인임을 증명하는 행위

온라인상에서 전자서명은 본인만이 알고있는 패스워드를 사용하는 것이 일반적입니다

하지만 블록체인 시스템은 중앙 데이터베이스에 블록의 흐름(트랜잭션 데이터)만 저장할 뿐,
사용자에 관한 정보를 저장하지 않습니다
그러면 사용자에 대한 증명은 어떤 방식으로 처리하고 있을까요?


공개키(자물쇠) & 개인키(열쇠)


블록체인에서는 사용자의 증명을 위해 공개키개인키 암호화 방식을 사용합니다

먼저 사용자는 공개키와 개인키 한 쌍을 생성합니다
공개키는 누구나 볼 수 있지만 개인키는 해당 사용자만이 알고 있습니다
그래서 이를 자물쇠(공개키)와 열쇠(개인키)의 관계로 비유하기도 합니다

사용자는 자신만이 알고 있는 개인키를 써서 트랜잭션에 대한 디지털 서명을 생성합니다
그리고 이 서명은 개인키와 페어를 이루는 공개키를 통해 검증할 수 있습니다
검증 과정에서는 공개키를 사용하여 서명이 올바른지 확인하고, 이를 통해 해당 트랜잭션의 유효성도 검증합니다
이러한 방식으로 블록체인의 검증은 사용자에 대한 증명과 트랜잭션의 유효성 검증을 한꺼번에 처리하면서도 높은 신뢰도를 보장합니다

블록체인의 검증 방식은 여러 장점을 지니고 있지만 특히 중요한 점은
분산된 네트워크 상에서 자체적으로 사용자의 증명을 처리할 수 있기 대문에 중앙기관에 의존하지 않아도 된다는 점
그리고 단점은 체인의 유효성을 검증하는 과정에서 적지 않은 딜레이가 발생한다는 점입니다


공개키 생성을 위한 라이브러리

npm install elliptic

영수증 인터페이스

export class Sender {
  	//
    publicKey?: string
    account!: string
}

export class Receipt {
    sender!: Sender
    received!: string
    amount!: string
    signature?: unknown
}



2. 트랜잭션

트랜잭션, 트랜잭션 풀, 코인베이스

  • 트랜잭션 :

  • 트랜잭션 풀 : 아직 블록체인에 포함되지 않은 트랜잭션 객체(거래내역)들이 저장되는 공간입니다
    여기에 담긴 트랜잭션들은 다음 코인이 생성되는 시점에서 반영됩니다

  • 코인베이스 : 블록 생성에 대한 보상금을 지급하는 트랜잭션(새 블록의 첫번째 거래)으로
    블록체인 상에 새로운 블록이 생성될 때마다 생성됩니다


블록 마이닝에 걸리는 시간은 대략적으로 10분,
블록 마이닝 도중에 일어나는 트랜잭션은 다음번 블록 생성시에 반영됩니다

사용자가 영수증을 바탕으로 블록체인 네트워크에 요청 -> 트랜잭션 풀에 담김 ->
다음 마이닝이 성공되면 트랜잭션 처리 완료
(비트코인이 송금이 느리다고 말하는 이유이기도 합니다)

  • 트랜잭션의 구조
txInput {
  txOutId?: string
  txOutIndex!: number
  signature? : signatureInput
}


txOut {
	account! : string // 받는 사람
    amount!: number
}

class TransactionRow {
  // 인풋
  txIns? : txInput[],
  // 아웃풋
  txOuts!: txOut[],
  // Transaction에 대한 고유한 식별자
  hash?: string 
}
  • TxOut : 트랜잭션의 아웃풋(출력 결과)입니다
    아웃풋은 트랜잭션의 송신자가 수신자에게 전송할 금액과 수신자의 공개키 정보를 담고 있습니다
    하나의 트랜잭션에서 여러 개의 아웃풋이 생성될 수 있기에 배열 형태로 표현합니다

  • TxIn : 이전 거래의 TxOut을 참조하는데, 이전 거래에서 송금받은 TxOut의 인덱스(txOutIndex)와 해시(txOutId)를 저장합니다
    타입을 배열로 받는 이유는 하나의 트랜잭션에서 여러 개의 입력을 참조할 수 있기 때문

인풋과 아웃풋이 모여서 하나의 TransactionRow를 생성합니다

  • 트랜잭션 클래스
class Transaction {
    private readonly REWARD = 50
    constructor(private readonly crypto: CryptoModule) { }

    create(receipt: Receipt) {
        const totalAmount = 50
        // txin => 영수증에 있는 sender의 잔액을 확인해야한다.
        const txin1 = this.createTxIn(1, '', receipt.signature)
        const txout_sender = this.createTxOut(receipt.sender.account, totalAmount - receipt.amount)
        const txout_received = this.createTxOut(receipt.received, receipt.amount)
        return this.createRow([txin1], [txout_sender, txout_received])
    }

    createTxOut(account: string, amount: number): TxOut {
        if (account.length !== 40) throw new Error("Account 형식이 올바르지 않습니다.")
        const txout = new TxOut()
        txout.account = account
        txout.amount = amount
        return txout
    }

    serializeTxOut(txOut: TxOut): Hash {
        const { account, amount } = txOut
        const text = [account, amount].join('')
        return this.crypto.SHA256(text)
    }

  	// 코인베이스 생성시에는 index 인자만  필요합니다
    createTxIn(txOutIndex: number, txOutId?: string, signature?: SignatureInput): TxIn {
        const txIn = new TxIn()
        txIn.txOutIndex = txOutIndex
        txIn.txOutId = txOutId
        txIn.signature = signature
        return txIn
    }

    serializeTxIn(txIn: TxIn): Hash {
        const { txOutIndex } = txIn
        const text = [txOutIndex].join('')
        return this.crypto.SHA256(text)
    }

  	// 트랜잭션 생성
    createRow(txIns: TxIn[], txOuts: TxOut[]) {
        const transactionRow = new TransactionRow()
        transactionRow.txIns = txIns
        transactionRow.txOuts = txOuts
        transactionRow.hash = this.serializeRow(transactionRow)
        return transactionRow
    }

  	// txOut[] or txIn[]을 받는 리듀서 함수
    serializeTx<T>(data: T[], callback: (item: T) => string) {
        return data.reduce((acc: string, item: T) => acc + callback(item), '')
    }

    // TransactionRow 객체를 해시화하는 메서드 (배열(txIn[], txOut[])에서 추출한 데이터로 해시 생성) 
    // this바인딩 처리를 위해 애로우 함수로 감쌉니다
    serializeRow(row: TransactionRow) {
        const { txIns, txOuts } = row
        const txoutText = this.serializeTx<TxOut>(txOuts, (item) => this.serializeTxOut(item))
        const txinText = this.serializeTx<TxIn>(txIns, (item) => this.serializeTxIn(item))

        return this.crypto.SHA256(txoutText + txinText)
    }

  	// 채굴자에 대한 보상 ~ 보상을 받을 계정 + 직전 블록의 높이 정보가 필요합니다
    createCoinbase(account: string, latestBlockHeight: number) {
        const txin = this.createTxIn(latestBlockHeight + 1)
        const txout = this.createTxOut(account, this.REWARD)
        return this.createRow([txin], [txout])
    }
}



  • 트랜잭션 생성
// 코인베이스
// 영수증 --> transaction --> block 생성
const privateKey = '6fb6a1482159a4b05a96636d0a390f7be0f29552c1a2edef79e83998221bc261'
const publicKey = digitalSignature.createPublicKey(privateKey)
const account = digitalSignature.createAccount(publicKey)

// 코인베이스(새 트랜잭션) 생성 ~ 계정 + 직전 블록의 높이
const coinbase2 = transaction.createCoinbase(account, GENESIS.height)


// ↓ 생성된 블록 예제
{
  nonce: 1,
  difficulty: 0,
  version: '1.0.0',
  height: 2,
  timestamp: 1683016061012,
  previousHash: '84ffab55c48e36cc480e2fd4c4bb0dc5ee1bb2d41a4f2a78a1533a8bb7df8370',
  merkleRoot: '739FC2180DC050B0E4AE51CA155F4F648C78E49728D72732A60224347E2BA829',
  data: [
    TransactionRow {
      txIns: [Array],
      txOuts: [Array],
      hash: '05b7576fae0b0afbbfed02f82a32e661d7aaeeb39180c54d678118bff6c6b393'
    }
  ],
  hash: '0b998544753a8d7b7792883d5ce72a568b55c2cd51aca8d82fa11554f23f8479'
}

  • 세번째 블록에 트랜잭션 반영시키기



2-2. 미사용 트랜잭션 (UnspentTxOuts)


미사용 객체는 이더리움에서 사용하는 방법론은 아닙니다
비트코인에서만 사용되는 방식인데, UTXO라고 줄여서 부르기도 합니다

코인의 총량은 항상 총 발행량과 같아야 합니다
즉, 트랜잭션이 발생할 때의 거래량과 미사용 객체(거래에 사용되지 않은 코인)의 합은 코인의 총량과 같아야 합니다


예를 들어 코인의 총 발행량이 100이고, 현재 미사용객체에 총 50의 코인이 남아있다고 가정해보겠습니다
이때 새로운 트랜잭션이 발생하여 A 계정에 20 코인을 보내고, B 계정에 30 코인을 보낸다고 가정해보겠습니다
그러면 트랜잭션 객체는 다음과 같겠죠

txoutIndex: 0
account: 'A'
amount: 20

txoutIndex: 1
account: 'B'
amount: 30

이후에는 미사용객체에 남아있는 코인은 50에서 20과 30을 합산한 50이 되며
이를 통해 전체 코인의 총량인 100을 유지할 수 있습니다


// 미사용 객체 인터페이스
// {[{}, {}, {} ... ]}

class UnspentTxOut {
    txOutId!: string // TransactionRow의 hash
    txOutIndex!: number // txouts의 index
    account!: string // Transaction.txOuts
    amount!: number // Transaction.txOuts
}

export type UnspentTxOutPool = UnspentTxOut[]


// 미사용 트랜잭션
class Unspent {
    private readonly UnspentTxOuts: UnspentTxOutPool = []
    constructor() { }

    createUTXO(transaction: TransactionRow) {
        const { hash, txOuts } = transaction
        if (!hash) throw new Error("hash값이 존재하지 않습니다")

        // TxOuts로 미사용 트랜잭션 객체를 만드는데 txOuts 갯수가 가변적

        const newUnspentTxOut: UnspentTxOut[] = txOuts.map((txout: TxOut, index: number) => {
            const unspentTxOut = new UnspentTxOut()
            unspentTxOut.txOutId = hash
            unspentTxOut.txOutIndex = index
            unspentTxOut.account = txout.account
            unspentTxOut.amount = txout.amount

            return unspentTxOut
        })
        return newUnspentTxOut
    }
}

export default Unspent;


//
[
  UnspentTxOut {
    txOutId: '05b7576fae0b0afbbfed02f82a32e661d7aaeeb39180c54d678118bff6c6b393',
    txOutIndex: 0,
    account: 'a7f413bcd5bf5d7272eb3bf3570c79caac848aac',
    amount: 50
  }
]

0개의 댓글