BlockChain 상세 - planetarium/libplanet GitHub Wiki

이 문서에서는 BlockChain.Append 를 통해 실제로 블록이 붙을 때 어떤 과정을 통해 붙는지에 대해 설명합니다.

BlockChain.Append

코드

기본적으로 Swarm 을 실행했다면 Swarm 안에서 블록을 붙일 때 호출되고, 블록 객체를 인자로 넣어서 직접 호출할 수도 있습니다.

위 코드를 보게 되면 크게 두 패스로 나뉘는 것을 볼 수 있는데 AppendStateRootHashPrecededSloth가 적용되기 전 레거시 블록들에 대해서만 호출되고, 새로 찍힌 블록들은 모두 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 에서 블록을 싱크할 때는 rendervalidatetrue 이므로 해당 상황을 가정하여 설명하겠습니다.

예외 처리 로직을 제외하고 살펴보면 크게 다음 과정으로 이루어져있습니다.

  1. Block & Transaction Validation
  2. State Validation
  3. Update Storage
  4. Invoke Events

Block & Transaction Validation

ValidateBlock, ValidateBlockCommit, ValidateBlockNonces, IBlockPolicy.ValidateNextBlock, IBlockPolicy.ValidateNextBlockTx 가 포함되어 있습니다.

  • ValidateBlock: 블록의 헤더에 적혀있는 정보를 검증합니다. 블록의 Index, Timestamp 등을 확인합니다.
  • ValidateBlockCommit: 블록의 LastCommit 을 검증합니다.
  • ValidateBlockNonces: 블록 내 트랜잭션들의 논스를 검증합니다. 로컬 저장소에 저장된 논스의 값들을 가져와 비교합니다. 불일치가 발생할 시 InvalidTxNonceException이 발생합니다. Libplanet에서는 같은 서명자의 트랜잭션이 여러 개 존재할 수 있기 때문에, 트랜잭션을 논스값에 따라 정렬한 후 확인을 진행합니다.
  • IBlockPoilicy.ValidateNextBlock, IBlockPolicy.ValidateNextBlockTx: IBlockPolicy 에 작성하여 블록체인 생성자의 인자로 넣어준 정책에 따라서 블록과 트랜잭션의 정합성을 검사합니다.

State Validation

ValidateBlockStateRootHash 함수를 통해 트랜잭션들을 모두 실행하고, 그 결과와 블록에 저장된 StateRootHash 를 비교해 상태를 검증합니다.

Sloth 가 적용된 이후로는 Block.StateRootHash 에 현재 블록의 실행 결과가 적힌 게 아닌 이전 블록의 실행 결과가 지연되어 담기게 됩니다. 그렇게 블록의 실행 속도를 최적화합니다.

현재 붙이는 중인 블록의 상태는 DetermineNextBlockStateRootHash 를 통해 계산합니다. 내부적으로는 ActionEvaluator.Evaluate() 함수를 호출해 각 트랜잭션을 차례대로 실행합니다. 다만 이 과정은 후술할 Update Storage 단계 이후에 진행됩니다.

ActionEvaluator 내부에서 수행되는 로직에 대한 자세한 설명은 ActionEvaluator 상세 문서를 확인해주세요.

Update Storage

상태를 검증한 이후 (Sloth 적용 이후에는 이전 블록의 상태를 StateRootHash 와 비교한 이후) 다음의 정보들을 스토어에 업데이트합니다.

  • _blocks[block.Hash] = block;: 블록의 실제 데이터를 스토어에 업데이트합니다.
  • IStore.IncreaseTxNonce: 어드레스별 논스를 스토어에 업데이트합니다.
  • IStore.PutTxIdBlockHashIndex: 블록에 포함된 트랜잭션의 TxIdBlockHash 정보를 매칭하여 스토어에 업데이트합니다.
  • IStore.PutCommittedEvidence: 블록에 포함된 Evidence 정보를 스토어에 업데이트합니다.
  • IStore.AppendIndex: 체인에 블록의 인덱스를 포함하여 스토어를 업데이트합니다.

각 스텝에서 IStore 에 어떻게 작성되는지는 Store 상세 문서에서 보다 자세히 설명합니다.

Invoke Events

블록과 관련 정보들을 스토어에 업데이트 하고 난 뒤 블록체인의 TipChanged 이벤트와 블록 및 액션 렌더러를 통해 이벤트를 전달합니다. 그리고 추후 모니터링을 위해 필요한 TxExecution 데이터를 업데이트합니다.

위 과정들을 모두 마치면 블록이 성공적으로 붙습니다. Swarm 에서는 TipChanged 이벤트를 구독하여 블록을 전파하기도 합니다. 관련된 자세한 정보는 Swarm 상세 문서를 참고해주세요.

⚠️ **GitHub.com Fallback** ⚠️