수수료 대납에 대해 알아보자

수수료 대납 (Fee Delegation, FD)는 transaction을 발생시킨 이용자가 해당 transaction을 수수료를 지불하는게 아닌 다른 이용자가 낼 수 있도록 하는 기능입니다.
TxTypeFeeDelegated* 종류의 transaction의 경우 수수료를 from이 아닌 fee payer가 지불을 하게 되어 있어있습니다. 그렇기 때문에 fee payer의 주소와 서명이 해당 transaction에 포함이 되어야합니다. 아래의 다양한 종류의 transaction을 참고 하세요.

  • TxTypeFeeDelegatedValueTransfer
  • TxTypeFeeDelegatedValueTransferWithMemo
  • TxTypeFeeDelegatedSmartContractDeploy
  • TxTypeFeeDelegatedSmartContractExecution
  • TxTypeFeeDelegatedAccountUpdate
  • TxTypeFeeDelegatedCancel

부분적인 수수료 대납은 Transaction type명에 뒤에 WithRatio를 붙이면 됩니다.

수수료 대납의 예시

시나리오

0xALICE라는 주소인 사용자 Alice는 0xBOB주소를 갖는 Bob에서 1 KLAY를 전송하고자 합니다. 이때 Alice의 잔고가 정확히 1 KLAY라서 Alice는 0xFRED라는 주소를 가진 Fred에게 수수료를 대신 지불하도록 하려고 합니다.

Transaction의 전달 과정

1. Alice

Alice는 1 KLAY를 Bob에게 전송하며 대납이 가능하도록 대납용 transaction으로 서명하여 생성합니다. 이 경우에는 KLAY를 전송하는 대납 transaction으로 TxTypeFeeDelegatedValueTrasfer 을 사용합니다. Alice의 transaction 은 아래와 같습니다.

{
   "from" : "0xALICE",
   "to": "0xBOB",
   "type": "TxTypeFeeDelegatedValueTransfer",
   "value": "0xde0b6b3a7640000", // 1 KLAY
   "signatures": [ { "R": "0x...", "S": "0x...", "V": "0x..." } ],
   "gas" : "0x2dc6c0", // 3,000,000
   "gasPrice": "0x5d21dba00" // 25 Ston
   ...
}

Alice가 서명을 끝낸 다음, 이 transaction을 EN에 전송하지 않고 그 대신 Alice는 Fred에게 전송합니다. transaction을 encode하여 raw data를 다양한 통신방법으로 전송 가능합니다.

Alice ---> Fred

2. Fred

Fred가 Alice로 부터 transaction을 받게 되면 해당 transaction에 Fred는 자신의 address를 fee payer로 추가하고 자신의 서명을 추가하게 됩니다. 이를 EN에 전송하여 transaction을 전파시킵니다.
이때 Fred는 이미 Alice가 해당 transaction에 서명을 하였기 때문에 transaction의 내용을 변경하진 못합니다. 즉, Fred는 해당 transaction에 자신의 서명을 추가하는 것만 가능합니다.

Alice ---> Fred ---> [[ Klaytn ]]

3. Klaytn

Fred가 클레이튼 네트워크에 전파한 transaction은 다음과 같습니다.

{
   "from" : "0xALICE",
   "to": "0xBOB",
   "feePayer": "0xFRED",
   "feePayerSignatures": [ { "R": "0x...", "S": "0x...", "V": "0x..." } ],
   "type": "TxTypeFeeDelegatedValueTransfer",
   "value": "0xde0b6b3a7640000", // 1 KLAY
   "signatures": [ { "R": "0x...", "S": "0x...", "V": "0x..." } ],
   "gas" : "0x2dc6c0", // 3,000,000
   "gasPrice": "0x5d21dba00" // 25 Ston
   ...
}

위의 예시에서는 nonce와 같은 나머지 데이터는 맞다고 가정을 한다면 클레이튼 노드들은 해당 transaction에 대해서 검증/실행을 아래 단계로 수행하게 됩니다.

  1. Transaction 종류를 확인합니다.
  2. 만약, TxTypeFeeDelegated 종류이면 fromsignatures 가 일치하는지 그리고 feePayerfeePayerSignatures가 일치하는지 서명을 확인 합니다.
  3. 모든 항목이 맞다면 "from"인 fred의 계정으로 부터 gas * gasPrice 만큼의 KLAY를 차감하고 transaction을 실행하게 됩니다. (만약 Fred가 충분한 KLAY가 없다면 transaction을 실행되지 않습니다.)
  4. Transaction이 실행되며 이후 남은 수수료 ( (gas - gasUsed) * gasPrice )는 다시 Fred의 계정으로 환불됩니다.

이렇게 Fred가 대납하는 Alice의 transaction은 블록에 담기게 되며 Bob은 1 KLAY를 Alice로 부터 받을 수 있게 됩니다.

FD in Javascript

이제 Klaytn의 Caver SDK를 통해 어떻게 구현을 하는지 살펴보겠습니다. 아래 코드를 참조해주세요.

const Caver = require('caver-js');
const caver = new Caver('https://api.baobab.klaytn.net:8651/');

let sender = caver.klay.accounts.wallet.add("0xALICE_PRIV_KEY");
let payer = caver.klay.accounts.wallet.add("0xFRED_PRIV_KEY");

let { rawTransaction: senderRawTransaction } = await caver.klay.accounts.signTransaction({
        type: 'FEE_DELEGATED_VALUE_TRANSFER',
        from: sender.address,
        to: '0xBOB',
        gas: '3000000',
        value: caver.utils.toPeb('1', 'KLAY'),
    }, sender.privateKey);

// Alice ---> Fred
// : Alice somehow delivers raw tx to Fred

let { rawTransaction: finalTx } = await caver.klay.accounts.signTransaction({
        senderRawTransaction: senderRawTransaction,
        feePayer: payer.address
    }, payer.privateKey);

caver.klay.sendSignedTransaction(finalTx).then((err, receipt) => {
   console.log(receipt);
});

위의 코드는 앞서서 설명한 transaction의 전달과정과는 다른 점이 있습니다. 이 코드는 Fred가 Alice를 전적으로 신뢰한다는 가정하에 Fred의 대납을 위한 private key를 Alice가 가지고 있다는 가정으로 만들어진 코드 입니다.

Alice

위의 코드에서 처음에 transaction을 작성하고 private key로 서명을 합니다. caver-js에서는 transaction type의 경우 TxTypeFeeDelegatedValueTransfer가 아닌 영문 대문자로 구성된 FEE_DELEGATED_VALUE_TRANSFER를 사용합니다.

자세한 내용은 링크를 참조 하십시요.

Fred

두번재는 Fred의 private key를 이용해서 feePayer를 추가하고 transaction에 서명을 하게 됩니다. 최종적으로 모든 서명이 완료된 transaction은 sendSignedTransaction을 통해 클레이튼 네트워크에 전송하게 됩니다.

정리

추가적인 자세한 내용을 위해서는 이 링크를 추가로 참조 하십시요.

FD in Java

아래의 코드는 먼저 설명드린 Javascript 코드와 동일한 기능을 구현한 Caver.java 코드 입니다.
더 자세한 내용은 이 링크를 참조 하십시요.

/* Omitted package statement and import statements for brevity */

public class FeeDelegatedValueTransfer {
    private static final BigInteger GAS_LIMIT = BigInteger.valueOf(3000000L);
    private static final BigInteger GAS_PRICE = BigInteger.valueOf(25000000000L);
    private static DefaultBlockParameter BLK_PARAM = DefaultBlockParameterName.LATEST;

    private static int CHAIN_ID;
    private static Caver caver;

    private static KlayCredentials senderCredential;
    private static KlayCredentials payerCredential;

    @BeforeClass
    public static void setup() {
        // Baobab setting
        CHAIN_ID = ChainId.BAOBAB_TESTNET;
        caver = Caver.build("https://api.baobab.klaytn.net:8651/");
        senderCredential = KlayCredentials.create("0xALICE_PRIV_KEY");
        payerCredential = KlayCredentials.create("0xFRED_PRIV_KEY");
    }

    @Test
    public void feeDelegatedValueTransferTest() throws IOException {
        BigInteger nonce = caver.klay().getTransactionCount(senderCredential.getAddress(), BLK_PARAM).send().getValue();
        TxTypeFeeDelegatedValueTransfer tx = TxTypeFeeDelegatedValueTransfer
                .createTransaction(
                        nonce,
                        GAS_PRICE,
                        GAS_LIMIT,
                        "0xBOB",
                        BigInteger.ONE,
                        senderCredential.getAddress()
                );
        String rawTx = tx.sign(senderCredential, CHAIN_ID).getValueAsString();

        FeePayerManager feePayerManager = new FeePayerManager.Builder(caver, payerCredential)
                .setTransactionReceiptProcessor(new PollingTransactionReceiptProcessor(caver, 1000, 10))
                .setChainId(CHAIN_ID)
                .build();

        KlayTransactionReceipt.TransactionReceipt transactionReceipt = feePayerManager.executeTransaction(rawTx);
    }
}

2개의 좋아요

@Ethan 글 올려주셔서 잘 봤습니다 감사합니다.

글 보면서 두 개의 서명이 필요하다는 것까지 알게됐는데요.
마지막 코드 부분(아래 사진 참고)을 출력하면 receipt가 undefined라고 나오지만 transactionList로 가보면 수수료가 빠져나간게 보이네요.
image

Sender(서명) → Payer(서명) → Receiver라는건 이해했는데요.
아래와 같은 코드를 실행하려면 서명된 transaction과 어떻게 연결해서 사용해야되나요?

현재는 아래처럼 에러메시지가 나오고 끝입니다.


(node:64620) UnhandledPromiseRejectionWarning: Error: Returned error: gas required exceeds allowance or always failing transaction

문제가 되는 코드는 아래 사진과 같습니다.

안녕하세요. 먼저 희소식이 있다면 caver-js v1.6.1부터 caver.kct.kip7 에서는 대납을 지원합니다.

그런데 올려주신 코드를 보니 common architecture 이전 기능과 이후 기능을 섞어서 사용하시고 계시네요.
문의한 내용을 토대로 아래에 common architecture 이후 기능을 사용한 예제를 작성했습니다.
basic tx사용해서 kip7.transfer 하는 방법과 수수료 대납을 통해 kip7.transfer하는 방법 모두 예제에 포함시켰습니다.

참고 부탁드립니다. 해보고 안되시면 말씀해 주세요.

// Add keryings to `caver.wallet` in-memory wallet
const sender = caver.wallet.add(caver.wallet.keyring.createFromPrivateKey('0{private key}'))
const feePayer = caver.wallet.add(caver.wallet.keyring.createFromPrivateKey('0{private key}'))
const receiver = caver.wallet.add(caver.wallet.keyring.createFromPrivateKey('0{private key}'))

// To send FeeDelegatedValueTransfer transaction, create a instance.
const feeDelegatedValueTransfer = new caver.transaction.feeDelegatedValueTransfer({
    from: sender.address,
    to: receiver.address,
    gas: '300000',
    value: caver.utils.toPeb('0.5', 'KLAY')
})

// Sender sign the feeDelegatedValueTransfer tx
await caver.wallet.sign(sender.address, feeDelegatedValueTransfer)

// Fee payer sign the feeDelegatedValueTransfer tx
await caver.wallet.signAsFeePayer(feePayer.address, feeDelegatedValueTransfer)

// `signatures` and `feePayerSignatures` fields are filled.
console.log(`Signed tx feeDelegatedValueTransfer`)
console.log(feeDelegatedValueTransfer)

// Send to the Klaytn network.
const receipt = await caver.rpc.klay.sendRawTransaction(feeDelegatedValueTransfer)
console.log(receipt)

// Execute contract (with basic transaction)
const smartContractAddr = '0x{smart contract address}'
const kip7 = new caver.kct.kip7(smartContractAddr)
// The keyring of the from address below should be added in `caver.wallet`
const transferReceipt = await kip7.transfer(receiver.address, 1, { from: '0x{address in hex}' })
console.log(transferReceipt)

// Execute contract (with FD transaction)
// The keyring of the from and feePayer address below should be added in `caver.wallet`
const transferWithFD = await kip7.transfer(receiver.address, 1, {
    from: sender.address,
    feePayer: feePayer.address,
    feeDelegation: true,
})
console.log(transferWithFD)
1개의 좋아요

대납 서비스에 대해서 질문이 있습니다.
현재는 대납 서버가 있어서 대납 서버에서 tx에 서명을 해 주어야 대납이 되는 것으로 보이는데,
대납 서버를 두지 않고 대납 서비스가 가능한지 궁금합니다.

예를 들어, (1) 대납자인 Fred의 서명키(sk)를 Alice와 Bob에게 주는 경우 이 서명키가 대납 서명 생성에만 쓸수 있는지, Klay sending용으로도 전용이 가능한지 궁금합니다. 서명 생성용으로만 사용될 수 있으면 좋겠습니다. (2) 특정 스마트 계약에 대해서 자동으로 대납할 수 있는 방법이 혹시 존재하는지도 궁금합니다.

안녕하세요, 클레이튼 포럼에 질문을 올려주셔서 감사드립니다.

  1. 클레이튼 네트워크 입장에서 보면 Tx.from과 tx.feePayer의 주소와 서명이 올바른 것을 확인하면 됩니다. from과 fee payer 주체가 다를 경우 서버를 구성하는 여부는 구현하는 관점에서 문제를 풀어 볼 수 있을 것 같습니다.

  2. 질문 주신것처럼 Klaytn에서는 tx.feePayer로만 동작하는 별도의 키를 설정할 수 있습니다. Account의 키를 Role-based key 로 업데이트를 하고 RoleTransaction은 tx.sender로만 서명이 가능하고, RoleFeePayer는 tx.feePayer로만 서명이 가능합니다.

  3. 특정 스마트 계약에 대해서 자동으로 대납할 수 있는 방법은 현재로는 별도의 서비스가 필요할 것 같습니다. 대납 트랜잭션의 fee payer 부분만 채워지지 않은 트랜잭션을 확인하고 tx.to가 원하는 주소일 때만 대납 서명을 하는 방식으로 서비스를 만들 수 있을 것 같습니다.

다음 번에는 별도의 질문 글을 작성해 주시면 해당 주제에 대해 더 많은 논의를 할 수 있을 것 같습니다.

감사합니다.