BlockChain 상세 - planetarium/libplanet GitHub Wiki
이 문서에서는 BlockChain.Append
를 통해 실제로 블록이 붙을 때 어떤 과정을 통해 붙는지에 대해 설명합니다.
기본적으로 Swarm
을 실행했다면 Swarm
안에서 블록을 붙일 때 호출되고, 블록 객체를 인자로 넣어서 직접 호출할 수도 있습니다.
위 코드를 보게 되면 크게 두 패스로 나뉘는 것을 볼 수 있는데 AppendStateRootHashPreceded
는 Sloth가 적용되기 전 레거시 블록들에 대해서만 호출되고, 새로 찍힌 블록들은 모두 else
블록을 타게 됩니다. else
블록에서는 아래의 Append
함수를 호출합니다.
internal void Append(
Block block,
BlockCommit blockCommit,
bool render,
bool validate = true)
{
if (Count == 0)
{
throw new ArgumentException(
"Cannot append a block to an empty chain.");
}
else if (block.Index == 0)
{
throw new ArgumentException(
$"Cannot append genesis block #{block.Index} {block.Hash} to a chain.",
nameof(block));
}
_logger.Information(
"Trying to append block #{BlockIndex} {BlockHash}...", block.Index, block.Hash);
if (validate)
{
block.ValidateTimestamp();
}
_rwlock.EnterUpgradeableReadLock();
Block prevTip = Tip;
try
{
if (validate)
{
ValidateBlock(block);
ValidateBlockCommit(block, blockCommit);
}
var nonceDeltas = ValidateBlockNonces(
block.Transactions
.Select(tx => tx.Signer)
.Distinct()
.ToDictionary(signer => signer, signer => Store.GetTxNonce(Id, signer)),
block);
if (validate)
{
ValidateBlockLoadActions(block);
}
if (validate && Policy.ValidateNextBlock(this, block) is { } bpve)
{
throw bpve;
}
foreach (Transaction tx in block.Transactions)
{
if (validate && Policy.ValidateNextBlockTx(this, tx) is { } tpve)
{
throw new TxPolicyViolationException(
"According to BlockPolicy, this transaction is not valid.",
tx.Id,
tpve);
}
}
_rwlock.EnterWriteLock();
try
{
if (validate)
{
ValidateBlockStateRootHash(block);
}
// FIXME: Using evaluateActions as a proxy flag for preloading status.
const string TimestampFormat = "yyyy-MM-ddTHH:mm:ss.ffffffZ";
_logger
.ForContext("Tag", "Metric")
.ForContext("Subtag", "BlockAppendTimestamp")
.Information(
"Block #{BlockIndex} {BlockHash} with " +
"timestamp {BlockTimestamp} appended at {AppendTimestamp}",
block.Index,
block.Hash,
block.Timestamp.ToString(
TimestampFormat, CultureInfo.InvariantCulture),
DateTimeOffset.UtcNow.ToString(
TimestampFormat, CultureInfo.InvariantCulture));
_blocks[block.Hash] = block;
foreach (KeyValuePair<Address, long> pair in nonceDeltas)
{
Store.IncreaseTxNonce(Id, pair.Key, pair.Value);
}
foreach (var tx in block.Transactions)
{
Store.PutTxIdBlockHashIndex(tx.Id, block.Hash);
}
if (block.Index != 0 && blockCommit is { })
{
Store.PutChainBlockCommit(Id, blockCommit);
}
foreach (var ev in block.Evidence)
{
if (Store.GetPendingEvidence(ev.Id) != null)
{
Store.DeletePendingEvidence(ev.Id);
}
Store.PutCommittedEvidence(ev);
}
Store.AppendIndex(Id, block.Hash);
_nextStateRootHash = null;
foreach (var ev in GetPendingEvidence().ToArray())
{
if (IsEvidenceExpired(ev))
{
Store.DeletePendingEvidence(ev.Id);
}
}
}
finally
{
_rwlock.ExitWriteLock();
}
if (IsCanonical)
{
_logger.Information(
"Unstaging {TxCount} transactions from block #{BlockIndex} {BlockHash}...",
block.Transactions.Count(),
block.Index,
block.Hash);
foreach (Transaction tx in block.Transactions)
{
UnstageTransaction(tx);
}
_logger.Information(
"Unstaged {TxCount} transactions from block #{BlockIndex} {BlockHash}...",
block.Transactions.Count(),
block.Index,
block.Hash);
}
else
{
_logger.Information(
"Skipping unstaging transactions from block #{BlockIndex} {BlockHash} " +
"for non-canonical chain {ChainID}",
block.Index,
block.Hash,
Id);
}
TipChanged?.Invoke(this, (prevTip, block));
_logger.Information(
"Appended the block #{BlockIndex} {BlockHash}",
block.Index,
block.Hash);
HashDigest<SHA256> nextStateRootHash =
DetermineNextBlockStateRootHash(block, out var actionEvaluations);
_nextStateRootHash = nextStateRootHash;
IEnumerable<TxExecution> txExecutions =
MakeTxExecutions(block, actionEvaluations);
UpdateTxExecutions(txExecutions);
if (render)
{
_logger.Information(
"Invoking {RendererCount} renderers and " +
"{ActionRendererCount} action renderers for #{BlockIndex} {BlockHash}",
Renderers.Count,
ActionRenderers.Count,
block.Index,
block.Hash);
foreach (IRenderer renderer in Renderers)
{
renderer.RenderBlock(oldTip: prevTip ?? Genesis, newTip: block);
}
if (ActionRenderers.Any())
{
RenderActions(evaluations: actionEvaluations, block: block);
foreach (IActionRenderer renderer in ActionRenderers)
{
renderer.RenderBlockEnd(oldTip: prevTip ?? Genesis, newTip: block);
}
}
_logger.Information(
"Invoked {RendererCount} renderers and " +
"{ActionRendererCount} action renderers for #{BlockIndex} {BlockHash}",
Renderers.Count,
ActionRenderers.Count,
block.Index,
block.Hash);
}
}
finally
{
_rwlock.ExitUpgradeableReadLock();
}
}
우선 인자부터 살펴보면 block
은 붙일 블록, blockCommit
은 해당 블록의 LastCommit
, render
는 이 블록을 붙이면서 renderer
를 호출할지 여부, validate
는 이 블록을 검증할지에 대한 여부를 정합니다.
Swarm
에서 블록을 싱크할 때는 render
및 validate
가 true
이므로 해당 상황을 가정하여 설명하겠습니다.
예외 처리 로직을 제외하고 살펴보면 크게 다음 과정으로 이루어져있습니다.
- Block & Transaction Validation
- State Validation
- Update Storage
- Invoke Events
ValidateBlock
, ValidateBlockCommit
, ValidateBlockNonces
, IBlockPolicy.ValidateNextBlock
, IBlockPolicy.ValidateNextBlockTx
가 포함되어 있습니다.
-
ValidateBlock
: 블록의 헤더에 적혀있는 정보를 검증합니다. 블록의Index
,Timestamp
등을 확인합니다. -
ValidateBlockCommit
: 블록의LastCommit
을 검증합니다. -
ValidateBlockNonces
: 블록 내 트랜잭션들의 논스를 검증합니다. 로컬 저장소에 저장된 논스의 값들을 가져와 비교합니다. 불일치가 발생할 시InvalidTxNonceException
이 발생합니다. Libplanet에서는 같은 서명자의 트랜잭션이 여러 개 존재할 수 있기 때문에, 트랜잭션을 논스값에 따라 정렬한 후 확인을 진행합니다. -
IBlockPoilicy.ValidateNextBlock
,IBlockPolicy.ValidateNextBlockTx
:IBlockPolicy
에 작성하여 블록체인 생성자의 인자로 넣어준 정책에 따라서 블록과 트랜잭션의 정합성을 검사합니다.
ValidateBlockStateRootHash
함수를 통해 트랜잭션들을 모두 실행하고, 그 결과와 블록에 저장된 StateRootHash
를 비교해 상태를 검증합니다.
Sloth 가 적용된 이후로는 Block.StateRootHash
에 현재 블록의 실행 결과가 적힌 게 아닌 이전 블록의 실행 결과가 지연되어 담기게 됩니다. 그렇게 블록의 실행 속도를 최적화합니다.
현재 붙이는 중인 블록의 상태는 DetermineNextBlockStateRootHash
를 통해 계산합니다. 내부적으로는 ActionEvaluator.Evaluate()
함수를 호출해 각 트랜잭션을 차례대로 실행합니다. 다만 이 과정은 후술할 Update Storage 단계 이후에 진행됩니다.
ActionEvaluator
내부에서 수행되는 로직에 대한 자세한 설명은 ActionEvaluator 상세 문서를 확인해주세요.
상태를 검증한 이후 (Sloth 적용 이후에는 이전 블록의 상태를 StateRootHash
와 비교한 이후) 다음의 정보들을 스토어에 업데이트합니다.
-
_blocks[block.Hash] = block;
: 블록의 실제 데이터를 스토어에 업데이트합니다. -
IStore.IncreaseTxNonce
: 어드레스별 논스를 스토어에 업데이트합니다. -
IStore.PutTxIdBlockHashIndex
: 블록에 포함된 트랜잭션의TxId
에BlockHash
정보를 매칭하여 스토어에 업데이트합니다. -
IStore.PutCommittedEvidence
: 블록에 포함된Evidence
정보를 스토어에 업데이트합니다. -
IStore.AppendIndex
: 체인에 블록의 인덱스를 포함하여 스토어를 업데이트합니다.
각 스텝에서 IStore
에 어떻게 작성되는지는 Store
상세 문서에서 보다 자세히 설명합니다.
블록과 관련 정보들을 스토어에 업데이트 하고 난 뒤 블록체인의 TipChanged
이벤트와 블록 및 액션 렌더러를 통해 이벤트를 전달합니다. 그리고 추후 모니터링을 위해 필요한 TxExecution
데이터를 업데이트합니다.
위 과정들을 모두 마치면 블록이 성공적으로 붙습니다. Swarm
에서는 TipChanged
이벤트를 구독하여 블록을 전파하기도 합니다. 관련된 자세한 정보는 Swarm
상세 문서를 참고해주세요.