Thử lại Transaction

Trong một vài tính huống, một Transaction trông có vẻ hợp lệ có thể bị hết hạn ngay trước khi được chấp nhận (thêm vào block). Điều đó thường diễn ra nhất là khi mạng lưới bị nghẽn và một nốt RPC không thể truyền Transaction đến leaderopen in new window. Dưới góc độ người dùng, bạn có thể nhận ra trường hợp này khi Transaction biến mất hoàn toàn. Trong khi các nốt RPC được trang bị một thuật toán lan truyền chung, ứng dụng của các lập trình viên vẫn có khả năng phát triển các luận lý lan truyền tuý chỉnh.

Có thể bạn chưa biết

Những điều có thể bạn chưa biết

  • Các nốt RPC sẽ thử lan truyền lại Transaction khi sử dụng một thuật toán chung
  • Ứng dụng riêng có thể hiện thực các luận lý lan truyền tuỳ chỉnh
  • Lập trình viên nên hiểu rõ tham số maxRetries của phương thức sendTransaction trong JSON-RPC.
  • Lập trình viên nên kích hoạt preflight để kiểm tra các tình huống lỗi trước khi gửi Transaction đi
  • Trước khi ký lại bất kỳ một Transaction nào, là rất quan trọng khi đảm bảo rằng blockhash của Transaction đã hết hạn

Hành trình của một Transaction

Làm thế nào để người dùng gửi Transactions

Trong Solana, không tồn tại khái niệm mempool. Tất cả các Transaction, dù là được tạo nên từ các Program hay là từ người dùng, đều được điều hướng hiệu quả đến các leader để họ có thể xử lý và ghi nhận chúng vào block. Có 2 cách mà một Transaction có thể được gửi đến các leader:

  1. Uỷ quyền cho các máy chủ RPC bằng phương thức sendTransactionopen in new window trong JSON-RPC.
  2. Gửi trực tiếp đến các leader thông qua TPU Clientopen in new window

Phần lớn người dùng sẽ gửi Transaction thông qua máy chủ RPC. Khi một người dùng gửi Transaction đi, nốt RPC sẽ tiếp nhận và cố gắng truyền lần lượt các Transaction đến leader hiện tại cũng như leader tiếp theo. Cho đến khi Transaction được xử lý bởi một leader, sẽ không tồn tại bất kỳ bản sao nào của Transaction được lưu trữ ngoại trừ người dùng và các nốt RPC trung chuyển. Trong trường hợp TPU Client, quá trình lan truyền và điều hướng đến leader sẽ được xử lý toàn bộ bởi người dùng.

Transaction Journey

Làm thế nào để các nốt RPC lan truyền Transaction

Ngay sau khi một nốt RPC tiếp nhận Transaction thông qua sendTransaction, nó sẽ chuyển Transaction đó thành một gói tin UDPopen in new window trước khi truyền đến các leader thích hợp. UDP cho phép validator có thể giao tiếp nhanh chóng với nhau, nhưng không đảm bảo gói tin có thể chắc chắn được chuyển đi đúng hướng.

Bởi vì lịch trình của các leader trong Solana là biết trước cho mỗi epochopen in new window (~2 ngày), một nốt RPC sẽ lan truyền Transaction của nó trực tiếp đến leader hiện tại cũng như tiếp sau. Điều này trái ngược với các giao thức gossip khác, ví dụ như Ethereum truyền Transaction một cách ngẫu nhiên và phủ khắp trên toàn mạng. Mặc định, các nốt RPC sẽ thử chuyển Transaction đến các leader cứ mỗi 2 giây cho để khi, hoặc Transaction thành công, hoặc blockhash của Transaction bị quá hạn (150 blocks hoặc ~1 phút 19 giây tại thời điểm viết bài). Nếu hàng đợi các Transaction cần được truyền đi lớn hơn 10,000 transactionsopen in new window, các Transaction đến sau sẽ bị từ chối. Để điều chỉnh cài đặt mặc định, tham khảo các tham số cho câu lệnh tại đâyopen in new window.

Khi một nốt RPC lan truyền một Transaction, nó sẽ cố gắng chuyển Transaction đó để Transaction Processing Unit (TPU)open in new window của một leader.

TPU xử lý các Transaction trong 5 pha riêng biệt:

TPU OverviewHình ảnh được cho phép bởi Jito Labs

Trong 5 pha này, Fetch Stage chịu trách nhiệm cho việc tiếp nhận Transaction. Trong phạm vi Fetch Stage, validator sẽ phân loại các Transaction mới đến dựa theo 3 cổng:

  • tpuopen in new window xử lý các Transaction bình thường như là chuyển token, tạo NFT, và các chỉ thị cho các Program
  • tpu_voteopen in new window tập trung hoàn toàn vào Transaction bỏ phiếu
  • tpu_forwardsopen in new window điều hướng các gói tin chưa xử lý đến các leader tiếp theo nếu leader hiện tại không đủ khả năng xử lý hết tất cả các Transaction

Chi tiết hơn về TPU, vui lòng tham khảo bài viết rất xuất sắc của Jito Labsopen in new window.

Khi nào các Transaction bị huỷ

Xuyên suốt hành trình của một Transaction, luôn có một vài tình huống Transaction đó có thể bị làm mất một cách tình cờ do mạng.

Trước khi Transaction được xử lý

Nếu mạng làm mất một Transaction, khả năng gần như nó sẽ bị huỹ trước khi được xử lý bởi một leader. Mất gói tinopen in new window trong UDP là một nguyên nhân đơn giản nhất dẫn đến tình trạng trên. Trong thời gian mạng nghẽn, các validator có thể đã bị quá tải với số lượng khổng lồ các Transaction cần xử lý. Trong khi các validator được trang bị tpu_forwards để điều hướng các Transaction đến sau, thì vẫn luôn có một giới hạn số lượng các gói tin được điều hướngopen in new window. Hơn nữa, mỗi lần điều hướng sẽ bị giới hạn trong phạm vị một đơn vị kết nối (hop) giữa các validator. Bởi vậy mà các Transaction được nhận thông qua cổng tpu_forwards sẽ không bao giờ được điều hướng thêm cho các validator khác.

Ngoài ra, cũng có 2 lý do được ghi nhận khác dẫn đến một Transaction bị đánh mất trước khi nó được xử lý. Trường hợp đầu tiên là những Transaction được gửi từ một RPC pool. Thi thoảng, một phần của RPC pool có thể đi nhanh hơn đáng kể phần còn lại của pool. Vấn đề này thường gặp khi các nốt trong pool cần làm việc kết hợp cùng nhau. Trong ví dụ này, recentBlockhashopen in new window của Transaction được truy vấn từ phần đi nhanh hơn của pool (Máy chủ A). Khi Transaction được gửi đến phần đi chậm của pool (Máy chủ B), các nốt này sẽ không nhận ra blockhash và sẽ vô hiệu hoá Transaction đó. Chúng ta có thể phát hiện lỗi này, nếu lập trình viên kích hoạt việc kiểm tra preflightopen in new window lúc gọi sendTransaction.

Dropped via RPC Pool

Một mạng bị rẽ nhánh tạm thời cũng có thể dẫn đến Transaction không hợp lệ. Nếu một validator bị chậm trong quá trình trung chuyển các block tại pha Banking Stage, rất có thể nó sẽ rẽ sang một nhánh thiểu số. Khi một Transaction được tạo, có khả năng Transaction tham chiếu đến recentBlockhash mà chỉ hợp lên trên nhánh thiểu số. Sau khi Transaction này được gửi đi, mạng lưới có thể nhảy về nhánh chính từ nhánh thiểu số trước khi Transaction được xử lý. Trong tình huống đó, Transaction sẽ không hợp lệ vì mạng không thể tìm thấy blockhash.

Dropped due to Minority Fork (Before Processed)

Sau khi Transaction được xử lý và trước khi được ghi vào block

Trong trường hợp một Transaction tham chiếu recentBlockhash từ một nhánh thiểu số, nó vẫn có thể được xử lý bính thường. Tuy nhiên trong trường hợp đó, nó chỉ được tiếp nhận bởi leader trên nhánh thiểu số. Khi leader này cố gắng chia sẻ những Transaction mà nó đã xử lý với phần còn lại của mạng, lỗi đồng thuận sẽ xảy ra với phần cồng các validator khác đang duy trì trên nhánh chính và không hề nhận ra nhánh thiểu số. Lúc đó, Transaction sẽ bị xem là không hợp lên trước khi được đóng vào block.

Dropped due to Minority Fork (After Processed)

Xử trí với Transaction bị huỷ

Trong khi các nốt RPC sẽ cố gắng lan truyền các Transaction, thuật toán được dùng thường chỉ đáp ứng các nhu cầu phổ biến và không tương thích với các nhu cầu đặc biệt. Để dự phòng trong tình huống mạng nghẽn, các lập trình viên sẽ phải tuỳ chỉnh thuật toán lan truyền trong ứng dụng của họ.

Nghiên cứu sendTransaction

Khi cần gửi Transaction, phương thức sendTransaction trong RPC là công cụ cơ bản nhất sẵn có cho lập trình viên. sendTransaction chỉ chịu trách nhiệm cho việc trung chuyển từ người dùng đến một nốt RPC. Nếu nốt đó nhận được Transaction, sendTransaction sẽ trả về id của Transaction và có thể dùng nó để theo dõi tiến độ của Transaction. Một phản hồi thành công từ RPC không đồng nghĩa với việc Transaction đó đã được tiếp nhận, xử lý và đóng vào một block trên mạng lưới Solana.

TIP

Tham số của Request

  • transaction: string - Transaction đã được ký đầy đủ và được mã hoá lại thành chuỗi ký tự
  • (optional) configuration object: object
    • skipPreflight: boolean - Nếu true, bỏ qua quá trình kiểm tra Transaction bằng preflight (Mặc định: false)
    • (optional) preflightCommitment: string - Cấp độ Commitmentopen in new window được dùng cho mô phỏng preflight trong ngân hàng chỗ trống (Mặc định: "finalized").
    • (optional) encoding: string - Mã hoá được dùng cho dữ liệu trong Transaction. Hoặc "base58" (chậm), hoặc "base64". (Mặc định: "base58").
    • (optional) maxRetries: usize - Số lượng tối đa lần thử lại cho nốt RPC gửi Transaction đến các leader. Nếu tham số này không được đề cập, nốt RPC sẽ thử lại cho đến khi Transaction thành công hoặc blockhash bị hết hạn.

Response

  • transaction id: string - Chữ ký đầu tiên được nhúng vào trong Transaction. Id của transaction có thể được dùng với getSignatureStatusesopen in new window để cập nhật trạng thái mới nhất của Transaction.

Tuỳ chỉnh thuật toán lan truyền

Để phát triển thuật toán lan truyền của riêng mình, lập trình viên cần hiểu rõ tham số maxRetries trong sendTransaction. Nếu được khai báo, maxRetries sẽ ghi đè lên giá trị mặc định của nốt RPC và cho phép lập trình viên điều khiển thủ công quá trình thử lại trong phạm vi giới hạn hợp lýopen in new window.

Một cài đặt phổ biến cho việc thử lại thủ công là tạm lưu lastValidBlockHeight được truy vấn từ getLatestBlockhashopen in new window. Sau khi lưu lại, một ứng dụng có thể theo dõi blockheight của mạng lướiopen in new window và lan truyền Transaction thủ công thông qua thuật toán tuỳ chỉnh. Có một vài ứng dụng sử dụng giải thuật exponential backoffopen in new window, thì một vài ứng dụng khác ví như Mangoopen in new window chọn liên tục tái gửiopen in new window Transaction với một khoảng thời gian lặp định trước cho đến khi quá hạn.

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();
  }
})();

Khi gọi getLatestBlockhash, ứng dụng nên chỉ rõ mức commitmentopen in new window mong muốn. Bằng cách đặt commitment là confirmed (đã được bỏ phiếu chọn) hoặc finalized (~30 block sau khi confirmed), ứng dụng có thể tránh được trường hợp đọc blockhash từ một nhánh rẽ thiểu số.

Nếu một ứng dụng truy cập vào những nốt RPC thông qua một máy chủ cân bằng tải, nó có lựa chọn các nốt cụ thể để chia nhỏ tải lượng. Các nốt RPC đáp ứng các yêu cầu thiên về dữ liệu như là getProgramAccounts có thể dễ bị quá tải và bị đồng bộ chậm, cũng như là không thích hợp cho việc điều hướng Transaction. Với những ứng dụng đòi hỏi thời gian đáp ứng Transaction nhanh, nên có một máy chủ chuyên để xử lý duy nhất cho sendTransaction.

Cân nhắc khi bỏ qua Preflight

Mặc định, sendTransaction sẽ thực hiện preflight kiểm tra 3 bước trước khi gửi Transaction đó đi. Cụ thể, sendTransaction sẽ:

  • Xác nhận tất cả các chữ ký là hợp lệ
  • Kiểm tra blockhash được tham chiếu có nằm trong phạm vi 150 block không
  • Chạy giải lập transaction trong ngân hàng chỗ trống được định nghĩa bởi preflightCommitment.

Nếu một trong 3 bước trên bị lỗi, sendTransaction sẽ đẩy ra lỗi trước khi gửi transaction đi. Kiểm tra preflight sẽ không đảm bảo các trường hợp mất transaction hoặc là cho phép người dùng xử lý lỗi. Thay vào đó nó đảm bảo các lỗi cơ bản sẽ được kiểm tra trước và khuyến khích các lập trình viên nên giữ nó lại bằng cách gán false cho skipPreflight.

Khi nào nên tái ký transaction

Dù cho tất cả nỗ lực gửi lại, thì vẫn có một xác suất mà người dùng bị yêu cầu ký lại transaction đó. Trước khi tái ký bất kỳ một transaction nào, bạn cần đảm bảo rằng transaction trước đó đã hết hạn đối với blockhash. Nếu transaction vẫn còn hiệu lực, cả hai transaction có thể sẽ được xử lý bởi mạng lưới. Điều tương tự cũng có thể xảy ra trong trường hợp người dùng không may gửi 2 lần với 2 transaction giống nhau.

Trong Solana, một transaction được xem là đã vô hiệu hoá và an toàn khi giá trị blockhash được tham chiếu đã quá hạn so với lastValidBlock trả về từ hàm getRecentBlockhash. Lập trình viên có thể kiểm tra nhanh chóng giá trị blockhash thông qua hàm isBlockhashValidopen in new window. Một khi blockhash đã quá hạn, người dùng có thể tái ký trên giá trị blockhash mới và hợp lệ.

Lời cảm ơn

Rất cảm ơn Trent Nelson, Jacob Creechopen in new window, White Tiger, Le Yafo, Buffaluopen in new window, và Jito Labsopen in new window vì đã đọc và góp ý cho bài viết.

Last Updated:
Contributors: Trần Minh Quang, tuphan-dn