트랜잭션 수명주기

트랜잭션이 어떻게 받아들여지고, 대기열에 들어가고, 실행되어 블록에 포함되는지에 대해 설명합니다.

Substrate에서 트랜잭션은 블록에 포함될 데이터를 포함합니다. 트랜잭션의 데이터는 런타임 외부에서 들어오기 때문에, 트랜잭션은 때때로 외부 데이터나 외부 요소로 더 넓은 의미로 언급됩니다. 하지만, 가장 일반적인 외부 요소는 서명된 트랜잭션입니다. 따라서, 이 트랜잭션 수명주기에 대한 논의는 서명된 트랜잭션이 어떻게 유효성을 검사하고 실행되는지에 초점을 맞춥니다.

이미 배웠듯이, 서명된 트랜잭션에는 런타임 호출을 실행하기 위해 요청을 보내는 계정의 서명이 포함됩니다. 일반적으로, 요청은 요청을 제출하는 계정의 개인 키를 사용하여 서명됩니다. 대부분의 경우, 요청을 제출하는 계정은 트랜잭션 수수료를 지불합니다. 하지만, 트랜잭션 수수료와 트랜잭션 처리의 다른 요소는 런타임 로직이 어떻게 정의되었는지에 따라 다릅니다.

트랜잭션이 정의되는 곳

런타임 개발에서 설명한 것처럼, Substrate 런타임에는 트랜잭션 속성을 정의하는 비즈니스 로직이 포함됩니다. 이 속성에는 다음과 같은 내용이 포함됩니다:

  • 유효한 트랜잭션으로 간주되는 것.

  • 트랜잭션이 서명된 것인지 아닌지.

  • 트랜잭션이 체인의 상태를 어떻게 변경하는지.

일반적으로, 원하는 체인을 지원하기 위해 런타임 함수를 구성하고 트랜잭션을 구현하는 데에는 팔렛을 사용합니다. 런타임을 컴파일한 후에 사용자는 블록체인과 상호작용하여 트랜잭션으로 처리되는 요청을 제출합니다. 예를 들어, 사용자는 한 계정에서 다른 계정으로 자금을 이체하는 요청을 제출할 수 있습니다. 이 요청은 해당 사용자 계정의 서명을 포함하는 서명된 트랜잭션으로 변환되며, 사용자 계정에 충분한 자금이 있어 트랜잭션 수수료를 지불할 수 있는 경우, 트랜잭션은 성공적으로 실행되고 이체가 이루어집니다.

블록 생성 노드에서 트랜잭션 처리 방법

네트워크의 구성에 따라, 블록을 생성할 수 있는 권한을 가진 노드와 블록 생성 권한이 없는 노드의 조합이 있을 수 있습니다. Substrate 노드가 블록을 생성할 권한을 가지면, 수신한 서명된 및 서명되지 않은 트랜잭션을 처리할 수 있습니다. 다음 다이어그램은 네트워크에 제출된 트랜잭션이 블록 생성 노드에서 처리되는 트랜잭션 수명주기를 보여줍니다.

블록 생성 노드로 전송된 서명된 또는 서명되지 않은 트랜잭션은 네트워크의 다른 노드로 전파되어 트랜잭션 풀에 들어갈 때까지 대기열에 들어갑니다.

트랜잭션 유효성 검사 및 대기열

합의에서 설명한 것처럼, 블록 내 트랜잭션의 순서에 대해 네트워크의 대다수 노드가 동의하여 블록체인의 상태를 동의하고 안전하게 블록을 추가할 수 있습니다. 합의를 이루기 위해, 두 개의 세부 트랜잭션 실행 및 결과 상태 변경에 대해 2/3의 노드가 동의해야 합니다. 합의를 위해, 트랜잭션은 먼저 로컬 노드의 트랜잭션 풀에 유효성을 검사하고 대기열에 저장됩니다.

트랜잭션 풀에서 트랜잭션 유효성 검사

런타임에서 정의된 규칙을 사용하여 트랜잭션 풀은 각 트랜잭션의 유효성을 검사합니다. 이 검사는 특정 조건을 충족하는 유효한 트랜잭션만이 블록에 포함될 수 있도록 대기열에 저장됩니다. 예를 들어, 트랜잭션 풀은 다음과 같은 검사를 수행하여 트랜잭션이 유효한지 여부를 결정할 수 있습니다:

  • 트랜잭션 인덱스(트랜잭션 nonce로도 알려짐)가 올바른지 확인합니다.

  • 트랜잭션에 서명한 계정에 충분한 자금이 있는지 확인합니다.

  • 트랜잭션에 사용된 서명이 유효한지 확인합니다.

초기 유효성 검사 이후, 트랜잭션 풀은 주기적으로 풀 내에 있는 기존 트랜잭션이 여전히 유효한지 확인합니다. 유효하지 않거나 만료된 트랜잭션이 발견되면 풀에서 삭제됩니다.

트랜잭션 풀은 트랜잭션의 유효성과 유효한 트랜잭션의 순서 지정만을 다룹니다. 수수료, 계정 또는 서명에 대한 처리와 같은 유효성 검사 메커니즘의 구체적인 세부사항은 validate_transaction 메서드에서 찾을 수 있습니다.

유효한 트랜잭션을 트랜잭션 대기열에 추가

트랜잭션이 유효하다고 확인되면, 트랜잭션 풀은 해당 트랜잭션을 트랜잭션 대기열로 이동시킵니다. 유효한 트랜잭션에는 두 가지 트랜잭션 대기열이 있습니다:

  • 준비 대기열은 새로운 대기 중인 블록에 포함될 수 있는 트랜잭션을 포함합니다. FRAME으로 빌드된 런타임의 경우, 트랜잭션은 준비 대기열에 배치된 순서대로 따라야 합니다.

  • 미래 대기열은 나중에 유효해질 수 있는 트랜잭션을 포함합니다. 예를 들어, 트랜잭션이 해당 계정에 대한 nonce가 너무 높은 경우, 해당 트랜잭션은 체인에 포함된 트랜잭션 수만큼의 적절한 횟수가 될 때까지 미래 대기열에 대기할 수 있습니다.

유효하지 않은 트랜잭션 처리

트랜잭션이 유효하지 않은 경우(예: 너무 큰 경우 또는 유효한 서명이 없는 경우) 블록에 추가되지 않고 거부됩니다. 트랜잭션이 다음과 같은 이유로 거부될 수 있습니다:

  • 트랜잭션이 이미 블록에 포함되었으므로 검증 대기열에서 삭제됩니다.

  • 트랜잭션의 서명이 유효하지 않으므로 즉시 거부됩니다.

  • 트랜잭션이 현재 블록에 맞지 않을 정도로 크므로 새로운 검증 라운드를 위해 다시 대기열에 넣습니다.

우선순위에 따라 정렬된 트랜잭션

노드가 다음 블록 작성자인 경우, 노드는 다음 블록을 위해 트랜잭션을 정렬하기 위해 우선순위 시스템을 사용합니다. 트랜잭션은 블록이 최대 가중치 또는 길이에 도달할 때까지 높은 우선순위에서 낮은 우선순위로 정렬됩니다.

트랜잭션 우선순위는 런타임에서 계산되어 트랜잭션의 태그로 외부 노드에 제공됩니다. FRAME 런타임에서는 트랜잭션과 관련된 가중치와 수수료를 기반으로 우선순위를 계산하기 위해 특수한 팔렛이 사용됩니다. 이 우선순위 계산은 상속을 제외한 모든 유형의 트랜잭션에 적용됩니다. 상속은 항상 EnsureInherentsAreFirst 특성을 사용하여 먼저 배치됩니다.

계정 기반 트랜잭션 정렬

FRAME으로 빌드된 런타임에서는 모든 서명된 트랜잭션에는 특정 계정에 의해 새로운 트랜잭션이 만들어질 때마다 증가하는 nonce가 포함됩니다. 예를 들어, 새 계정의 첫 번째 트랜잭션은 nonce = 0이고, 동일한 계정에 대한 두 번째 트랜잭션은 nonce = 1입니다. 블록 작성 노드는 블록에 포함될 트랜잭션을 정렬할 때 이 nonce를 사용할 수 있습니다.

의존성이 있는 트랜잭션의 경우, 트랜잭션이 지불하는 수수료 및 다른 트랜잭션에 대한 의존성을 고려하여 정렬됩니다. 예를 들어:

  • TransactionPriority::max_value()를 가진 서명되지 않은 트랜잭션과 다른 서명된 트랜잭션이 있는 경우, 서명되지 않은 트랜잭션이 대기열의 첫 번째에 배치됩니다.

  • 서로 다른 송신자로부터의 두 개의 트랜잭션이 있는 경우, priority가 어떤 트랜잭션이 더 중요하고 먼저 블록에 포함되어야 하는지를 결정합니다.

  • 동일한 nonce를 가진 동일한 송신자로부터의 두 개의 트랜잭션이 있는 경우: 블록에는 하나의 트랜잭션만 포함될 수 있으므로, 더 높은 수수료를 가진 트랜잭션이 대기열에 포함됩니다.

트랜잭션 실행 및 블록 생성

유효한 트랜잭션이 트랜잭션 대기열에 배치된 후, 별도의 executive module이 트랜잭션이 어떻게 실행되어 블록을 생성하는지 조정합니다. executive module은 런타임 모듈의 함수를 호출하고 그 함수를 특정한 순서로 실행합니다.

런타임 개발자로서, executive module이 시스템 팔렛과 블록체인의 비즈니스 로직을 구성하는 다른 팔렛들과 어떻게 상호작용하는지 이해하는 것이 중요합니다. 왜냐하면 다음 작업의 일부로 executive module에 대한 로직을 삽입할 수 있기 때문입니다:

  • 블록 초기화

  • 블록에 포함될 트랜잭션 실행

  • 블록 빌딩 완료

블록 초기화

블록을 초기화하기 위해, executive module은 먼저 시스템 팔렛의 on_initialize 함수를 호출한 다음 모든 다른 런타임 팔렛에서 호출합니다. on_initialize 함수를 사용하여 트랜잭션이 실행되기 전에 완료되어야 하는 비즈니스 로직을 정의할 수 있습니다. 시스템 팔렛의 on_initialize 함수는 항상 가장 먼저 실행됩니다. 나머지 팔렛은 construct_runtime! 매크로에서 정의된 순서대로 호출됩니다.

모든 on_initialize 함수가 실행된 후, executive module은 블록 헤더의 부모 해시와 트라이 루트를 확인하여 정보가 올바른지 검증합니다.

트랜잭션 실행

블록이 초기화된 후, 각 유효한 트랜잭션이 트랜잭션 우선순위의 순서대로 실행됩니다. 실행 전에 상태가 캐시되지 않는다는 것을 기억하는 것이 중요합니다. 대신, 상태 변경은 실행 중에 직접 스토리지에 기록됩니다. 트랜잭션이 실행 중에 실패하면, 실패하기 전에 발생한 상태 변경은 되돌릴 수 없으므로 블록은 복구할 수 없는 상태로 남게 됩니다. 스토리지에 상태 변경을 커밋하기 전에, 런타임 로직은 트랜잭션이 성공할 수 있도록 필요한 모든 검사를 수행해야 합니다.

이벤트도 스토리지에 기록됩니다. 따라서 런타임 로직은 보완적인 작업을 수행하기 전에 이벤트를 발생시키지 않아야 합니다. 트랜잭션이 실패한 후에 이벤트가 발생하면, 이벤트는 되돌려지지 않습니다.

블록 빌딩 완료

대기 중인 모든 트랜잭션이 실행된 후, executive module은 각 팔렛의 on_idleon_finalize 함수를 호출하여 블록의 끝에서 수행되어야 하는 최종 비즈니스 로직을 수행합니다. 팔렛은 다시 construct_runtime! 매크로에서 정의된 순서대로 실행됩니다. 그러나 이 경우에는 시스템 팔렛의 on_finalize 함수가 마지막으로 실행됩니다.

모든 on_finalize 함수가 실행된 후, executive module은 블록 헤더의 다이제스트와 스토리지 루트가 초기화될 때 계산된 값과 일치하는지 확인합니다.

on_idle 함수는 블록의 남은 가중치를 전달하여 블록 사용량에 기반한 실행을 허용합니다.

블록 작성 및 블록 가져오기

지금까지 로컬 노드에서 생성된 블록에 트랜잭션이 포함되는 방법을 살펴보았습니다. 로컬 노드가 블록을 생성할 권한을 가지면, 트랜잭션 수명주기는 다음과 같은 경로를 따릅니다:

  1. 로컬 노드는 네트워크에서 트랜잭션을 수신 대기합니다.

  2. 각 트랜잭션을 검증합니다.

  3. 유효한 트랜잭션을 트랜잭션 풀에 배치합니다.

  4. 트랜잭션 풀은 유효한 트랜잭션을 적절한 트랜잭션 대기열에 정렬하고, executive module은 런타임에 대한 호출을 시작하여 다음 블록을 생성합니다.

  5. 트랜잭션을 실행하고 상태 변경을 로컬 메모리에 저장합니다.

  6. 구성된 블록을 네트워크에 게시합니다.

블록이 네트워크에 게시된 후, 다른 노드가 가져올 수 있습니다. 블록 가져오기 대기열은 모든 Substrate 노드의 외부 노드의 일부입니다. 블록 가져오기 대기열은 들어오는 블록 및 합의 관련 메시지를 수신하고 풀에 추가합니다. 풀 내에서 들어오는 정보는 유효성을 확인하고 유효하지 않은 경우 폐기됩니다. 블록 또는 메시지가 유효한지 확인한 후, 블록 가져오기 대기열은 들어오는 정보를 로컬 노드의 상태로 가져와 노드가 알고 있는 블록 데이터베이스에 추가합니다.

대부분의 경우, 트랜잭션이 어떻게 전파되는지 또는 다른 노드에서 블록이 가져오는 방법에 대한 세부 사항을 알 필요는 없습니다. 그러나 사용자 정의 합의 로직을 작성하거나 블록 가져오기 대기열의 구현에 대해 자세히 알고 싶은 경우, Rust API 문서에서 자세한 내용을 찾을 수 있습니다.

다음 단계로 넘어가기

Last updated