Retrying Transactions

몇몇 상황에서 유효해 보이는 Trasaction이 block에 포함되기 전에 거절될지도 모릅니다. RPC 노드가 leaderopen in new window에게 그 Transaction을 rebroadcast 하는 것을 실패하는 상황 같은, 네트워크가 혼잡한 동안에 종종 발생합니다. 이것은 end-user에게 그들의 Transaction이 완전히 사라진 것처럼 보일지도 모릅니다. RPC 노드들은 generic rebroadcasting 알고리즘을 갖추고 있지만, application 개발자들은 자신만의 cutom rebroadcasting logic을 만들 수도 있습니다.

Facts

Fact Sheet

  • RPC 노드들은 Transaction에 대한 generic 알고리즘을 사용해서 rebroadcast를 시도할 것입니다.
  • Application 개발자들은 자신만의 custom rebroadcasting logic을 구현할 수 있습니다.
  • 개발자들은 sendTransaction JSON-RPC 메소드에 있는 maxRetries 파라미터의 이점을 활용해야 합니다.
  • 개발자들은 Transaction이 보내지기 전에 error들을 발생시키기 위한 앞선 check들을 가능하게 해야합니다.
  • 어떤 Transaction에 다시 서명하기 전에, 첫 Transaction의 Blockhash가 만료됐는지 확인하는 것은 매우 중요합니다.

The Journey of a Transaction

How Clients Submit Transactions

Solana에는 mempool에 대한 개념이 없습니다. 프로그램이나 end-user에 의해 초기화된 모든 Transaction들은 block 안으로 처리될 수 있게 효율적으로 leader들에게 라우팅 됩니다. Transaction이 leader들에게 보내질 수 있는 두 가지 주된 방법이 있습니다:

  1. RPC 서버를 통한 Proxy 그리고 sendTransactionopen in new window JSON-RPC method에 의해
  2. TPU Clientopen in new window를 통해 leader들에게 직접

대부분의 end-user들은 RPC 서버를 통해 Transaction들을 보낼 것입니다. Client가 Transaction을 보낼 때, 수신한 RPC node는 Transaction을 현재와 다음 leader들에게 차례로 broadcast 할 것입니다. Transaction이 leader에 의해 처리될 때까지 Client와 전달 중인 RPC 노드들이 알고 있는 것 외부에는 그 Transaction에 대한 기록이 존재하지 않을 것입니다. TPC Client의 경우, rebroadcast와 leader forwarding은 온전히 Client software에 의해 다뤄집니다.

Transaction Journey

How RPC Nodes Broadcast Transactions

RPC노드는 sendTransaction을 통해 Transaction을 수신한 후에, 관련된 leader들에게 전달하기 전에 Transaction을 UDPopen in new window 패킷으로 변환할 것입니다. UDP는 validator들이 빠르게 서로 통신할 수 있게 해주지만, Transcation 전달을 보장하지는 않습니다.

Solana의 leader 스케줄은 매 epochopen in new window 보다 앞선 것으로 알려져 있기 때문에 (~2 days), RPC 노드는 Transaction을 현재와 다음 leader들에게 즉시 broadcast 할 것입니다. 이것은 Transaction들을 전체 네트워크에 랜덤하게 전파하는 Ethereum과 같은 다른 프로토콜들과 다른 것입니다. 기본적으로, RPC 노드들은 Transaction이 종결되거나 Transaction의 Blockhash가 만료(150 blocks or ~1 분 19초, 이 글 작성 시점 기준)될 때까지 매 2초 마다 Trnasaction들을 보내는 시도를 할 것입니다. 만약 아직 처리되지 않은 rebroadcast의 큐 사이즈가 10,000 transactionsopen in new window 보다 크다면, 새로 보내지는 Transaction들은 드랍될 것입니다. 이러한 재시도 로직의 기본 행위를 변경하기 위해 RPC 운영자들이 조정할 수 있는 command-line argumentsopen in new window들이 존재합니다.

RPC 노드가 Transaction을 broadcast할 때, 노드는 이 Transaction을 leader의 Transaction Processing Unit (TPU)open in new window에 보내려고 할 것입니다. TPU는 Transaction들을 다섯 단계로 처리합니다:

TPU OverviewImage Courtesy of Jito Labs

이 다섯 단계 중 Fetch Stage는 Transaction들을 수신하는 책임을 갖습니다. Fetch Stage에서 validator들은 들어오는 Transaction들을 3가지 포트에 따라 분류할 것입니다.

  • tpuopen in new window는 token 전송들, NFT mint들 그리고 Program Instruction들과 같은 일반적인 Transaction들을 다룹니다.
  • tpu_voteopen in new window는 voting Transaction들을 집중적으로 다룹니다.
  • tpu_forwardsopen in new window는 만약 현재 leader가 모든 Transaction들을 처리할 수 없다면 가공되지 않은 패킷들을 다음 leader에게 보냅니다.

TPU에 대한 더 많은 정보는 다음을 참고해주세요. this excellent writeup by Jito Labsopen in new window.

How Transactions Get Dropped

Transaction의 여정 동안에 Transaction이 의도치 않게 네트워크로부터 드랍될 수 있는 몇 가지 시나리오들이 존재합니다.

Before a transaction is processed

만약 네트워크가 Transaction을 드랍한다면, 이것은 대부분 Transaction이 leader에 의해 처리되기 전 일 것입니다. UDP packet lossopen in new window는 이것이 발생하는 가장 단순한 이유입니다. 네트워크에 심한 부하가 걸리는 동안, validator들은 처리를 요청받은 Transaction들의 수에 의해 압도될 수 있습니다. validator들은 과도한 Transaction들을 tpu_forwards를 통해 보낼 수도 있지만, forwardedopen in new window 수 있는 데이터 양에 대한 제한이 있습니다. 뿐만 아니라, 각 forward는 validator들 사이에 단일 홉으로 제한되어 있습니다. tpu_forwards 포트를 통해 수신된 Transaction들이 다른 validator들에게 보내지지 않는 이유입니다.

Trnasaction이 처리되기 전 드랍될 수 있는 덜 알려진 두 가지 이유도 있습니다. 첫 번째 시나리오는 RPC 풀을 통해 보내진 Transaction들을 호출합니다. 가끔 RPC 풀의 한 부분이 풀의 나머지 부분보다 앞서 있을 수 있습니다. 이것은 풀 안에 있는 노드들이 함께 동작하도록 요청될 때 이슈를 야기할 수 있습니다. 이 예제에서, Transaction의 recentBlockhashopen in new window는 풀의 앞선 부분으로부터 질의를 받습니다 (Backend A). Transaction이 풀의 뒤떨어진 부분에 보내질 때, 노드들은 앞선 blockhash 라는 것을 알아차릴 것이고 Transaction을 드랍할 것입니다. 만약 개발자들이 sendTransactionpreflight checksopen in new window를 가능하게 한다면 Transaction이 보내지기 전에 이것을 발견할 수 있습니다.

Dropped via RPC Pool

일시적인 네트워크 포크 또한 Transaction들을 드랍시키는 결과를 야기할 수 있습니다. 만약 validator가 Banking Stage 내에서 block들을 느리게 재생한다면, 결국 minority fork를 생성해낼지 모릅니다. Client가 Transaction을 만들 때, 그 Transaction이 오직 minority fork에만 존재하는 recentBlockhash를 참조하도록 할 수 있습니다. 그 Transaction이 보내지고 나면, 그 Transaction이 처리되기 전에 cluster가 minority fork를 바꿔버릴 수 있습니다. 이 시나리오에서, blockhash는 발견될 수 없기 때문에 그 Transaction은 드랍됩니다.

Dropped due to Minority Fork (Before Processed)

After a transaction is processed and before it is finalized

Transaction이 minority fork로부터 recentBlockhash를 호출한 경우에도 Transaction이 처리될 수도 있습니다. 그러나, 이런 경우에는 minority fork에 있는 leader에 의해 처리될 것입니다. 이 leader가 처리한 Transaction들을 나머지 네트워크와 공유하려고 할 때, minority fork를 인정하지 않는 majority of validator들과의 합의에 실패할 것입니다. 이때, 그 Transaction은 완료되기 전에 드랍될 것입니다.

Dropped due to Minority Fork (After Processed)

Handling Dropped Transactions

RPC 노드들이 Transaction들을 rebroadcast 시도할 동안, 그들이 쓰는 알고리즘은 포괄적이고 특정 앱의 요구와는 종종 어울리지 않습니다. 네트워크 혼잡에 대비하기 위해, application 개발자들은 그들 자신의 rebroadcasting logic을 customize 해야 합니다.

An In-Depth Look at sendTransaction

Transaction들을 보내는 것에 대해서는 sendTransaction RPC method가 개발자들에게 가장 중요한 도구입니다. sendTransaction은 단지 Transaction을 client에서 RPC node로 보내는 데 책임이 있습니다. 만약 node가 Transaction을 수신하면, sendTransaction은 그 Transaction을 추적하기 위해 사용될 수 있는 Transaction id를 응답할 것입니다. 성공적인 응답이 그 Transaction이 cluster에 의해 처리되거나 완료될 것인지를 나타내지는 않습니다.

TIP

Request Parameters

  • transaction: string - 인코딩 된 문자열로 완전하게 서명된 Transaction
  • (optional) configuration object: object
    • skipPreflight: boolean - true 라면, 보내기 전 Transaction check를 건너뜁니다. (default: false)
    • (optional) preflightCommitment: string - bank slot과 반대되는 보내기 전 시뮬레이션을 위해 사용할 Commitmentopen in new window 레벨 (default: "finalized").
    • (optional) encoding: string - Transaction Data를 위해 사용되는 인코딩. "base58" (slow), 또는 "base64". (default: "base58").
    • (optional) maxRetries: usize - RPC node가 leader에게 Transaction 보내는 것을 재시도할 최대 횟수. 만약 이 파라미터가 값이 세팅되지 않는다면, RPC node는 Transaction이 완료되거나 blockhash가 만료될 때까지 재시도할 것입니다.

Response

  • transaction id: string - base-58로 인코딩된 문자열로 Transaction에 담긴 첫 번째 Transaction 시그니처. 이 Transaction id는 상태 updates들을 조사하기 위해 getSignatureStatusesopen in new window와 함께 사용될 수 있습니다.

Customizing Rebroadcast Logic

자신만의 rebroadcasting logic을 개발하기 위해서, 개발자들은 sendTransactionmaxRetries 파라미터의 이점을 활용해야 합니다. 만약 적용된다면, 개발자들이 within reasonable boundsopen in new window 재시도 처리를 수동으로 다룰 수 있게 해 주며, maxRetries가 RPC 노드의 기본 retry logic을 덮어쓸 것입니다.

Transaction들을 수동으로 재시도하는 것을 위한 흔한 패턴은 일시적으로 getLatestBlockhashopen in new window으로부터 얻을 수 있는 lastValidBlockHeight를 저장하도록 호출하는 것입니다. 일단 저장되고 나면, 이제 Application은 poll the cluster’s blockheightopen in new window할 수 있고 적절한 간격으로 수동으로 Transaction을 재시도할 수 있습니다. 네트워크가 혼잡할 때는 maxRetries를 0으로 세팅하고 custom algorithm을 통해 수동으로 rebroadcast하는 것이 유리합니다. 어떤 Application들은 exponential backoffopen in new window algorithm을 사용할수도 있고, 다른 Application들은 타임아웃이 일어날 때까지 일정한 간격으로 Transaction들을 continuously resubmitopen in new window하기 위해 Mangoopen in new window opt를 사용할 수도 있습니다.

Press </> button to view full source
import {
  Keypair,
  Connection,
  LAMPORTS_PER_SOL,
  SystemProgram,
  Transaction,
} from "@solana/web3.js";
import * as nacl from "tweetnacl";

const sleep = async (ms: number) => {
  return new Promise((r) => setTimeout(r, ms));
};

(async () => {
  const payer = Keypair.generate();
  const toAccount = Keypair.generate().publicKey;

  const connection = new Connection("http://127.0.0.1:8899", "confirmed");

  const airdropSignature = await connection.requestAirdrop(
    payer.publicKey,
    LAMPORTS_PER_SOL
  );

  await connection.confirmTransaction(airdropSignature);

  const blockhashResponse = await connection.getLatestBlockhashAndContext();
  const lastValidBlockHeight = blockhashResponse.context.slot + 150;

  const transaction = new Transaction({
    feePayer: payer.publicKey,
    blockhash: blockhashResponse.value.blockhash,
    lastValidBlockHeight: lastValidBlockHeight,
  }).add(
    SystemProgram.transfer({
      fromPubkey: payer.publicKey,
      toPubkey: toAccount,
      lamports: 1000000,
    })
  );
  const message = transaction.serializeMessage();
  const signature = nacl.sign.detached(message, payer.secretKey);
  transaction.addSignature(payer.publicKey, Buffer.from(signature));
  const rawTransaction = transaction.serialize();
  let blockheight = await connection.getBlockHeight();

  while (blockheight < lastValidBlockHeight) {
    connection.sendRawTransaction(rawTransaction, {
      skipPreflight: true,
    });
    await sleep(500);
    blockheight = await connection.getBlockHeight();
  }
})();

getLastestBlockhash를 통해 가져올 때, Application들은 그들의 의도된 commitmentopen in new window level을 명시해야 합니다. commitment를 confirmed (voted on) 또는 finalized (~30 blocks after confirmed)으로 세팅함으로써, Application은 minority fork로 부터 blockhash를 가져오는 것을 피할 수 있습니다.

Application이 만약 load balancer 뒤의 RPC node들에 접근할 수 있다면, 특정 노드들 사이에서 작업량을 나누도록 선택할 수 있습니다. getProgramAccounts와 같은 data 집중적인 요청들을 제공하는 RPC 노드들 뒤떨어지기 쉽고 Transaction들을 보내기 또한 적절하지 않을 수 있습니다. 시간에 민감한 Transaction들을 다루는 Application들에게, sendTransaction만을 다루는 node들을 선택하는 것은 prudent할지 모릅니다.

The Cost of Skipping Preflight

기본적으로 sendTransaction은 Transaction을 보내기에 앞서 세 가지 preflight checks를 수행합니다. 구체적으로:

  • 모든 서명들이 유효한지 검증합니다.
  • 참조된 blockhash가 최근 150 blocks 안에 포함되는지 체크합니다.
  • preflightcommitment에 의해 명시된 bank slot에 대하여 Transaction을 시뮬레이션합니다.

만약 이 세 가지 preflight check가 실패하는 경우, sendTransaction은 Transaction을 보내기 전에 에러를 일으킬 것입니다. Preflight checks는 Transaction을 잃어버리는 것과 client가 우아하게 error를 다루도록 하는 것 사이의 차이를 만들어 낼 수 있습니다. 이런 흔한 에러들이 설명되도록 하고 싶다면, 개발자들이 skipPreflightfalse 값으로 유지하는 것을 추천합니다.

When to Re-Sign Transactions

모든 rebroadcast 시도에도 불구하고, Client가 Transaction에 다시 서명하도록 요구되는 시점들이 있을 수 있습니다. Transaction에 재서명하기 전에, 첫 번째 Transaction의 blockhash가 만료되었다는 것을 확인하는 것은 매우 중요합니다. 만약 첫 번째 blockhash가 여전히 유효하다면, 이 두 Transaction들이 network에 받아들여질 수도 있습니다. 이것은 end-user에게 의도치 않게 동일한 Transaction을 두 번 보내는 결과를 보여줄 것입니다.

Solana에서 드랍된 Transaction은 이 Transaction이 참조하는 blockhash가 getLatestBlockhash로부터 수신된 lastValidBlockHeight보다 오래된 상태가 됬을 때 안전하게 버려질 수 있습니다. 개발자들은 getEpochInfoopen in new window를 질의하는 것과 응답 값에 있는 blockHeight와 비교하는 것으로 lastValidBlockHeight를 추적해야 합니다. blockhash가 유효하지 않게 되면 Client들은 새롭게 질의한 blockhash를 가지고 다시 서명해야 할 것입니다.

Acknowledgements

Trent Nelson, Jacob Creechopen in new window, White Tiger, Le Yafo, Buffaluopen in new window, and Jito Labsopen in new window. 이 모든 분들의 리뷰와 피드백에 감사드립니다.

Last Updated:
Contributors: TaeGit