Technology

동적 가스비 정책 (Dynamic Gas Fee Pricing Mechanism)

Klaytn의 v1.9.0에서는 기존에 단일 가스비로 트랜젝션이 처리되던 정책이 동적 가스비로 변경됩니다. 이 포스팅에서는 동적 가스비 정책이 도입된 이유와 이로 인한 변경사항에 대해서 설명합니다.

TL;DR:

클레이튼 v.1.9.0 하드포크와 함께 동적 가스비가 도입되며, 그 내용은 아래와 같습니다.

  • 한 블록에 들어간 트랜잭션들은 동일한 블록 가스비(baseFee)로 트랜잭션 비용을 계산하며, 블록 가스비 이상의 가스비를 설정한 트랜잭션만 블록에 담길 수 있음
  • 블록 가스비는 이전 블록의 가스 사용량에 따라 자동 증가/감소하며 최대 변동폭은 5%
  • 블록 가스비는 25 ston ~ 750 ston 이며, 이 범위는 Governance 기능으로 변경 가능
  • 매 블록에서 사용된 트랜잭션 비용의 절반은 자동 소각

dApp 및 지갑 서비스에서 가스비를 설정할 경우 아래 두 가지 방식 중 하나를 사용하시면 됩니다.

  1. 가스비 추천 API 리턴값을 사용. 노드의 경우, klay_gasPrice 또는 eth_gasPrice, caver의 경우 caver.rpc.klay.getGasPrice 리턴 값 사용
  2. 최대 가스비를 사용. 최대 가스비는 klay_upperBoundGasPrice API로 조회 가능하며, 현재 최대값은 750 ston

개요

클레이튼은 4,000 TPS 및 즉시 완결성을 보장하며 지금까지 운영되고 있습니다. 또한 지금까지 저렴한 단일 가스비를 통해 많은 사용자들이 블록체인 서비스를 이용 할 수 있도록 노력하였습니다.

하지만 클레이튼의 이러한 장점의 악용 가능성이 우려되는 여러가지 문제점이 발견되었습니다. 저렴한 가스비를 이용해 클레이튼 네트워크에 과부하를 발생시키는 사례들이 보고 되었고, 이는 일반 사용자들에게 트랜잭션이 지연되는 불편함을 초래하였습니다. 또한, 스토리지에 과부하를 발생시켜 안정적인 서비스 제공에 영향을 주었습니다.

이에 따라 클레이튼 팀은 기존 네트워크의 데이터를 분석하였고, 저렴한 가스비를 유지하면서 네트워크 악용을 억제하고 무분별한 스토리지 부하를 줄여 안정적인 서비스를 제공하기 위해 클레이튼에 맞는 동적 가스비 정책(KIP-71)을 제안하였습니다.

동적 가스비 정책

트랜잭션을 실행하기 위한 비용은 각 트랜잭션별로 소모되는 가스와 기본 가스비를 곱하여 계산합니다.

(트랜잭션 실행 비용) = (소모된 가스) x (기본 가스비)

Klaytn v1.9.0 이전까지는 기본 가스비가 단일 가스비(UnitPrice)로 고정이였다면, 동적 가스비 정책이 적용된 이후로는 네트워크 상황에 따라서 변하게 됩니다.

기본 가스비가 변경되는데 기여하는 총 7가지 파라미터가 있습니다.

  1. 이전 가스 기본료 : 직전 블록에 적용된 가스 기본료
  2. 이전 가스 사용량 : 직전 블록의 모든 트랜잭션을 실행하는데 소모된 가스량
  3. 기준 가스량 : 기본 가스료 인상/인하를 결정하는 기준 가스량(현재 30 million)
  4. 최대 가스량 : 가스 기본료를 계산하는데 사용되는 최대 가스량(현재 60 million)
  5. 변경폭 조정값 : 가스 기본료의 변동폭 조절값(현재 20)
  6. 최대 가스 기본료 : 가스 기본료가 상승할 수 있는 최대값(현재 750 ston)
  7. 최소 가스 기본료 : 가스 기본료가 하락할 수 있는 최소값(현재 25 ston)

해당 알고리즘을 단순하게 설명하자면, 최신 블록의 모든 트랜잭션을 실행하는데 소모되는 가스량이 기준 가스량보다 높으면 가스 기본료를 올리고 낮다면 내리는 것입니다. 이 값은 무한정 상승 또는 하락하는 것이 아니라, 상한선과 하한선인 최대 및 최소 가스 기본료가 있습니다. 또한 가스 기본료가 급등하는 것을 방지하기 위해 계산에 사용되는 최대 가스량과 변동폭 조정값을 두었습니다.

(가스 기본료 변동율) = (이전 가스 사용량 — 기준 가스값) (조정된 가스 기본료 변동율) = (가스 기본료 변동율) / (기준 가스량) / (변동폭 조정값)(가스 기본료 변동폭) = (이전 가스 기본료) * (조정된 가스 기본료 변동률)(기본 가스비) = (이전 가스 기본료) + (가스 기본료 변동폭)

가스 기본료는 매 블럭마다 계산되므로 매 초마다 변동이 일어날 수 있습니다. 가스 소모량이 많다면 기본료는 상승하고 가스 소모량이 적다면 하락하며 이는 네트워크에서 실행되는 트랜잭션의 개수 및 가스 소모량과 밀접하게 관련이 있습니다.

100번 블록의 가스 사용량이 200이고 가스 기본료가 100이라면 101번 블록의 가스 기본료는 아래와 같이 구할 수 있다.가스 기본료 변동율 = 200–100 = 100조정된 가스 기본료 변동율 = 100 / 100 / 20 = 0.05 = 5%가스 기본료 변동폭 = 100 * 5% = 5101번 블록의 가스 기본료 = 100 + 5 = 105위와 같이 102번 블록의 가스 기본료를 구하면 115임을 알 수 있다.

주요 변경사항 및 특징

하드포크 : 동적 가스비 정책을 적용하면서 클레이튼 네트워크에 하드포크가 발생합니다. 블럭 헤더에 기존에는 없었던 BaseFee 필드가 추가되었기 때문에 하드포크 전과 후로 블럭 헤더 데이터가 변경됩니다. 블럭 헤더를 조회하는 API를 호출할 때 기존에도 동일한 의미를 가진 BaseFeePerGas 필드가 있었으므로 차이를 느끼지 못 할 수 있지만, 하드포크 이전에는 이 값이 0이였다면 이후에는 실제 계산된 값이 적용됩니다.

거버넌스를 통한 파라미터 변경 : 기본 가스비를 계산하는데 필요한 파라메터들을 거버넌스 투표를 통해 변경 할 수 있습니다. 이를 통해 기본 가스비의 최대값, 최소값 및 변동폭을 조절 할 수 있으며, 메인넷을 운영하며 발생할 수 있는 이슈에 유연하게 대처가 가능합니다.

트랜잭션 풀 : 트랜잭션을 일시적으로 저장하는 트랜잭션 풀에 대한 정책에도 변화가 생깁니다. 가장 두드러지는 변화는 현재의 가스 기본료보다 낮은 가스비를 적어 제출한 트랜잭션은 네트워크에서 거부됩니다.

클레이튼 팀은 사용성이 저하되지 않도록 하기 위해 트랜잭션을 제출 할 때 (가스 기본료) x 2 로 가스값을 설정하도록 권장하고 있습니다. 최신 블럭에 대한 (가스 기본료)는 API호출을 통해 알 수 있습니다.

알고리즘 구현상 현재의 (가스 기본료)가 2배가 되기 위해서는 최대의 과부하 상태에서 14–15블럭 즉, 14~15초가 소요됩니다. 따라서 트랜잭션을 제출하는 시점에 권장값으로 설정하면 과부하로 인한 네트워크 지연이 있더라도 14블록까지 유효한 트랜잭션으로 남을 수 있습니다.

또한 최대 가스 기본료로 설정하는 방법도 있습니다. 트랜잭션의 가스값을 최대 가스 기본료로 설정한다면, 전송하는 매 순간마다 현재의 가스 기본료를 조회하지 않아도 됩니다. 이 때 현재 가스 기본료로 계산된 가스비를 제외한 나머지는 환불됩니다.

차별점 : 클레이튼의 타 체인 특히 이더리움의 EIP-1559과의 차별점은 팁(Tip)이 없다는 것입니다. 이더리움에 트랜잭션을 제출할때는 우선순위를 높이기 위해서는 팁을 높게 설정해야하며, 낮은 팁을 주어 저렴하게 트랜잭션을 제출하고자 할 때는 잘 처리되지 않아 여러번 시도를 해야하는 불편함이 있을 수 있습니다. 하지만 클레이튼에서는 팁이 없으며, 먼저 제출된 트랜잭션을 먼저 처리하도록 First Come First Served (FCFS) 정책이 적용되어 있습니다.

Klaytn SDK v1.9.0

Klaytn의 v1.9.0에서 KIP-71(Dynamic Gas Fee Pricing Mechanism)을 적용하면서 Klaytn SDK (caver-js, caver-java) v1.9.0에서도 Dynamic Gas Fee Pricing Mechanism을 사용할 수 있는 기능을 제공합니다.

Gas Price Setting Logic

Klaytn SDK는 SDK에서 제공되는 서명함수를 사용하거나, 혹은 트랜잭션 오브젝트를 노드로 전송하여 노드에 있는 키스토어를 사용할 때, gasPrice 필드가 비어있으면 적절한 값으로 채워주는 기능을 제공합니다. 이 기능이 Klaytn SDK v1.9.0부터는 Dynamic Gas Fee Pricing Mechanism에 맞게 적절한 값을 세팅하도록 변경되었습니다. 또한 트랜잭션의 메소드에 추천 gas price 를 리턴하는 tx.suggestGasPrice 함수도 추가로 제공합니다.

Klaytn SDK는 KIP-71을 적용한 Magma 하드포크 이후에는, 현재 baseFee 의 2배가 트랜잭션의 gasPrice에 세팅되어야 합니다.

TxTypeEthereumDynamicFee 트랜잭션의 경우, maxFeePerGas 필드에는 위에서 설명된 것과 동일하게 권장되는 값인 최신 블록 헤더의 baseFeePerGas 값의 2배를 사용하고 maxPriorityFeePerGas 필드는 caver.rpc.klay.getMaxPriorityFeePerGas에서 리턴되는 값을 사용합니다.

caver.rpc.klay.getGasPrice API는 Magma 하드포크에 따라 제안되는 gas price 값을 리턴합니다. Magma 하드포크 이전에는 고정된 unit price를 리턴하며, Magma 하드포크 이후에는 위에서 설명된 바와 같이 현재 base fee의 2배를 리턴하며, Klaytn SDK는 gasPrice (혹은 maxFeePerGas) 필드에 세팅할 값을 구하기 위해 caver.rpc.klay.getGasPrice API를 사용합니다.

참고로 블록 헤더의 baseFeePerGas의 값이 0보다 큰 지의 여부에 따라 Magma 하드포크 여부를 판단할 수 있습니다. caver.rpc.klay.getHeader의 리턴 오브젝트에 baseFeePerGas 필드가 없거나 0으로 리턴된다면 Magma 하드포크 이전이며, baseFeePerGas가 0보다 크다면 Magma 하드포크 이후를 의미합니다.

아래와 같이 Klaytn SDK의 서명함수를 사용할 때 트랜잭션의 gasPrice를 직접 세팅하지 않았다면 변경사항 없이 그대로 사용할 수 있습니다.

// caver-js: sign함수를 gasPrice 를 지정하지 않고 사용
const tx = caver.transaction.valueTransfer.create({ from: keyring.address, to: to.address, value: caver.utils.convertToPeb(1, ‘KLAY’), gas: 30000 })
await caver.wallet.sign(keyring.address, tx) // fillTransaction 호출// caver-java: sign함수를 gasPrice 를 지정하지 않고 사용
ValueTransfer tx = caver.transaction.valueTransfer.create(
TxPropertyBuilder.valueTransfer()
.setFrom(keyring.getAddress())
.setTo(to.getAddress())
.setValue(caver.utils.convertToPeb(1, Utils.KlayUnit.KLAY))
.setGas(BigInteger.valueOf(30000))
);
caver.wallet.sign(keyring.getAddress(), tx); // fillTransaction 호출

아래와 같이 gasPrice 필드에 caver.rpc.klay.getGasPrice의 리턴 값을 할당하고 있는 경우에는 변경사항 없이 그대로 사용할 수 있습니다.

// caver-js: caver.rpc.klay.getGasPrice 를 사용하여 트랜잭션 생성 (Magma 하드포크 이후 실행할 수 없는 트랜잭션)
const gasPrice = await caver.rpc.klay.getGasPrice(‘latest’)
const tx = caver.transaction.valueTransfer.create({ from: keyring.address, to: to.address, value: caver.utils.convertToPeb(1, ‘KLAY’), gas: 30000, gasPrice })// caver-java: caver.rpc.klay.getGasPrice 를 사용하여 트랜잭션 생성 (Magma 하드포크 이후 실행할 수 없는 트랜잭션)
BigInteger gasPrice = caver.rpc.klay.getGasPrice().send().getValue();
ValueTransfer tx = caver.transaction.valueTransfer.create(
 TxPropertyBuilder.valueTransfer()
 .setFrom(keyring.getAddress())
 .setTo(to.getAddress())
 .setValue(caver.utils.convertToPeb(1, Utils.KlayUnit.KLAY))
 .setGas(BigInteger.valueOf(30000))
 .setGasPrice(gasPrice)
 );

하지만 트랜잭션을 생성할 때 아래와 같이 고정된 gasPrice를 직접 지정해 주었다면 위의 코드와 같이 gasPrice를 지정하지 않거나 Dynamic Gas Fee Pricing Mechanism에 맞게 gasPrice를 세팅하도록 코드를 변경해 주어야 합니다.

// caver-js: gasPrice 를 지정하여 트랜잭션 생성const tx = caver.transaction.valueTransfer.create({ from: keyring.address, to: to.address, value: caver.utils.convertToPeb(1, ‘KLAY’), gas: 30000, gasPrice: caver.utils.convertToPeb(250, ‘ston’) })// caver-java: gasPrice 를 지정하여 트랜잭션 생성ValueTransfer tx = caver.transaction.valueTransfer.create(
 TxPropertyBuilder.valueTransfer()
 .setFrom(keyring.getAddress())
 .setTo(to.getAddress())
 .setValue(caver.utils.convertToPeb(1, Utils.KlayUnit.KLAY))
 .setGas(BigInteger.valueOf(30000))
 .setGasPrice(caver.utils.convertToPeb(250, Utils.KlayUnit.ston))
 );

아래에는 Magma 하드포크에 따라 적절한 gasPrice를 리턴하는 tx.suggestGasPrice 함수를 사용하는 예제입니다. suggestGasPrice는 caver.rpc.klay.getGasPrice API를 호출합니다.

// caver-js: suggestGasPrice 사용
const suggestGasPrice = await tx.suggestGasPrice()
tx.gasPrice = suggestGasPrice// caver-java: suggestGasPrice 사용
String suggestGasPrice = tx.suggestGasPrice();
tx.setGasPrice(suggestGasPrice);

여기까지 gasPrice 필드를 사용하는 트랜잭션에 대한 예시였습니다. 만약 TxTypeEthereumDynamicFee 트랜잭션을 사용한다면 위의 설명에서 gasPrice 대신 maxFeePerGas에 권장되는 gasPrice(Magma 하드포크 전 unit price, Magma 하드포크 이후 baseFee * 2 )를 할당하여 사용할 수 있습니다.

Newly Added effectiveGasPrice field in transaction receipt

KIP-71이 도입되면서 사용자가 트랜잭션에 지정한 gasPrice는 실제로 트랜잭션이 처리될 때 사용된 gasPrice(트랜잭션이 처리된 블록헤더의 baseFee)와 달라지게 되었습니다. 이로 인해 caver.rpc.klay.getTransactionReceipt에서 리턴하는 오브젝트에는 실제 트랜잭션이 처리될 때 사용된 gasPrice인 effectiveGasPrice 필드가 추가되었습니다.

이로 인하여 트랜잭션 발송인(수수료 대납 트랜잭션인 경우, 수수료 대납자)이 지불하는 수수료는 반드시 기존이 gasPrice가 아닌 effectiveGasPrice를 사용하여 tx.gasUsed * tx.effectiveGasPrice 와 같이 계산되도록 수정되어야 합니다.

아래에는 Klaytn SDK에서 effectiveGasPrice 필드를 조회하는 예제입니다.

// caver-js: receipt 에서 effectiveGasPrice 조회
const receipt = await caver.rpc.klay.getTransactionReceipt(‘0x{transaction hash}’)
const effectiveGasPrice = receipt.effectiveGasPrice// caver-java: receipt 에서 effectiveGasPrice 조회
TransactionReceiptProcessor receiptProcessor = new PollingTransactionReceiptProcessor(caver, 1000, 15);
TransactionReceipt.TransactionReceiptData receipt = receiptProcessor.waitForTransactionReceipt(“0x{transaction hash}”);
String effectiveGasPrice = receipt.getEffectiveGasPrice();

여기까지 Klaytn SDK v1.9.0의 변경사항과 Magma 하드포크에 따른 변경되는 사용성에 대해 설명했습니다. 이 글이 Klaytn의 KIP-71 기능을 쉽고 편리하게 사용하는 데 많은 도움이 되었으면 좋겠습니다.