How Witness Rules and Signature Verification Work in Neo's Transaction Validation Pipeline

Neo validates transactions through a two-stage pipeline where state-independent checks verify simple ECDSA signatures in Transaction.cs, while state-dependent execution evaluates complex witness rules via Helper.VerifyWitness and ApplicationEngine.Runtime.CheckWitness to determine if a signer is authorized to spend funds.

Neo employs a sophisticated dual-phase approach to witness rules and signature verification that balances performance with flexibility. When a node receives a transaction, it first performs lightweight syntactic checks and fast-path ECDSA validation without accessing blockchain state. If these checks pass, the system executes the Neo Virtual Machine to evaluate complex smart contract logic and flexible witness policies defined by signers. Understanding these mechanisms requires examining the core implementation files in the neo-project/neo repository, particularly Transaction.cs, Helper.cs, and the witness condition system under src/Neo/Network/P2P/Payloads/Conditions/.

State-Independent Verification: Fast-Path Signature Checks

The first validation stage, implemented in Transaction.VerifyStateIndependent within src/Neo/Network/P2P/Payloads/Transaction.cs, performs syntactic validation and optimized signature verification without accessing blockchain state or executing the Virtual Machine.

Detecting Simple Signature Contracts

Neo distinguishes between simple signature contracts and complex verification scripts using pattern matching in the state-independent phase:

  • IsSignatureContract detects standard 35-byte verification scripts (0x21 <pubkey> 0xac)
  • IsSingleSignatureInvocationScript validates 66-byte push-data scripts carrying the signature

When both patterns match, the engine bypasses VM execution and verifies the ECDSA signature directly against the transaction hash. This optimization eliminates GAS costs for standard transfers and significantly improves node throughput.

ECDSA Verification with Crypto.VerifySignature

For simple contracts, the validation pipeline calls Crypto.VerifySignature from src/Neo/Cryptography/Crypto.cs:

var pubkey = witness.VerificationScript.Span[2..35];
if (!Crypto.VerifySignature(this.GetSignData(settings.Network), signature.Span, pubkey, ECCurve.Secp256r1))
    return VerifyResult.InvalidSignature;

This fast-path verification uses the secp256r1 elliptic curve and returns immediately if the signature is invalid. If a witness does not match the simple contract pattern, verification falls back to the state-dependent step.

State-Dependent Verification: VM Execution and Witness Rules

When witnesses contain complex scripts, multi-signature requirements, or empty verification scripts (indicating contract-based verification), the pipeline enters the state-dependent phase via Transaction.VerifyStateDependent. This stage executes the Neo Virtual Machine and evaluates witness rules against the current blockchain state.

Helper.VerifyWitness Implementation

The core logic resides in Helper.VerifyWitness within src/Neo/SmartContract/Helper.cs:

internal static bool VerifyWitness(this IVerifiable verifiable,
    ProtocolSettings settings, DataCache snapshot, UInt160 hash,
    Witness witness, long datoshi, out long fee)
{
    // Load verification script (contract or built-in verification method)
    if (witness.VerificationScript.Length == 0)
    {
        // Call contract's `verify` method if the witness is empty
        engine.LoadContract(cs, md, CallFlags.ReadOnly);
    }
    else
    {
        // Load custom verification script
        if (NativeContract.IsNative(hash)) return false;
        if (hash != witness.ScriptHash) return false;
        engine.LoadScript(new Script(witness.VerificationScript, true), ...);
    }

    // Load the invocation script (the signature or parameters)
    engine.LoadScript(invocationScript, configureState: p => p.CallFlags = CallFlags.None);

    // Execute VM and expect a Boolean result
    if (engine.Execute() == VMState.FAULT) return false;
    if (engine.ResultStack.Count != 1) return false;
    if (!engine.ResultStack.Peek().GetBoolean()) return false;

    fee = engine.FeeConsumed;
    return true;
}

Empty vs. Custom Verification Scripts

  • Empty verification scripts trigger contract-based verification. The system locates the contract corresponding to the witness hash and invokes its verify method directly through engine.LoadContract. This allows smart contracts to implement dynamic authorization logic, such as multi-factor authentication or time-locked spending, without requiring the verification script to be attached to the transaction.

  • Custom verification scripts execute directly in the VM. The script must return a single Boolean true to succeed. The VM execution consumes GAS proportional to the script's computational complexity, with the consumed amount returned via the fee parameter and deducted from the transaction's network fee.

Witness Rules and Condition Evaluation

Neo N3 introduces flexible witness rules that enable granular access control beyond simple signature verification. These rules allow signers to define complex authorization policies that the runtime evaluates during CheckWitness calls.

Signer.GetAllRules and Scope-Based Rules

The Signer class in src/Neo/Network/P2P/Payloads/Signer.cs generates rules based on scope flags via GetAllRules:

public IEnumerable<WitnessRule> GetAllRules()
{
    if (Scopes == WitnessScope.Global)
        yield return new WitnessRule { Action = WitnessRuleAction.Allow,
                                        Condition = new BooleanCondition { Expression = true } };
    else
    {
        if (Scopes.HasFlag(WitnessScope.CalledByEntry))
            yield return new WitnessRule { Action = WitnessRuleAction.Allow,
                                            Condition = new CalledByEntryCondition() };
        if (Scopes.HasFlag(WitnessScope.CustomContracts))
            foreach (var hash in AllowedContracts!)
                yield return new WitnessRule { Action = WitnessRuleAction.Allow,
                                                Condition = new ScriptHashCondition { Hash = hash } };
        if (Scopes.HasFlag(WitnessScope.CustomGroups))
            foreach (var group in AllowedGroups!)
                yield return new WitnessRule { Action = WitnessRuleAction.Allow,
                                                Condition = new GroupCondition { Group = group } };
        if (Scopes.HasFlag(WitnessScope.WitnessRules))
            foreach (var rule in Rules!)
                yield return rule;   // explicit user-defined rules
    }
}

Scope flags automatically generate corresponding WitnessRule objects:

  • CalledByEntry: Yields CalledByEntryCondition, allowing calls only from the transaction's entry script
  • CustomContracts: Generates ScriptHashCondition rules for each allowed contract hash
  • CustomGroups: Creates GroupCondition rules for cryptographic group membership
  • WitnessRules: Embeds explicit user-defined WitnessRule objects with custom logic
  • Global: Creates a BooleanCondition with Expression = true, allowing all calls

The WitnessCondition Hierarchy

All conditions derive from WitnessCondition in src/Neo/Network/P2P/Payloads/Conditions/ and implement bool Match(ApplicationEngine engine):

Condition Evaluation Logic
BooleanCondition Returns constant true or false
CalledByEntryCondition Validates engine.EntryScriptHash matches the transaction entry
ScriptHashCondition Compares engine.CallingScriptHash against allowed hashes
GroupCondition Checks engine.CallingScriptGroup against group public keys
AndCondition Recursively evaluates all nested conditions with logical AND
OrCondition Recursively evaluates nested conditions with logical OR
NotCondition Negates the result of a nested condition

Runtime Evaluation via CheckWitness

The evaluation trigger resides in ApplicationEngine.Runtime.cs:

Signer? signer = signers.FirstOrDefault(p => p.Account.Equals(hash));
if (signer is null) return false;
foreach (WitnessRule rule in signer.GetAllRules())
{
    if (rule.Condition.Match(this))          // evaluate condition against engine context
        return rule.Action == WitnessRuleAction.Allow;
}
return false;

When a contract calls System.Runtime.CheckWitness, the engine locates the corresponding signer, iterates through rules yielded by GetAllRules(), and returns immediately upon the first match. This short-circuit evaluation allows efficient policy enforcement while supporting complex, nested conditional logic.

Practical Implementation Examples

Single-Signature Transactions

For standard single-signature accounts, Neo provides optimized verification that bypasses VM execution:

var tx = new Transaction
{
    Version = 0,
    Signers = new[] { new Signer { Account = senderHash } },
    Script = someScript,
    NetworkFee = 0,
    SystemFee = 0,
    ValidUntilBlock = 500000
};

var witness = new Witness
{
    VerificationScript = Contract.CreateSignatureRedeemScript(senderPubKey),
    InvocationScript   = Contract.CreateSignatureInvocationScript(signature)
};

tx.Witnesses = new[] { witness };

During VerifyStateIndependent, the engine detects the standard 35-byte verification script pattern and calls Crypto.VerifySignature directly, consuming no GAS for VM execution.

Multi-Signature Contracts

Multi-signature contracts require multiple signatures but follow similar verification patterns:

var multiSigScript = Contract.CreateMultiSignatureRedeemScript(2, publicKeys);
var witness = new Witness
{
    VerificationScript = multiSigScript,
    InvocationScript   = Contract.CreateMultiSignatureInvocationScript(
                            new[] { sig1, sig2 })   // two signatures
};

tx.Witnesses = new[] { witness };

The state-independent verification detects multi-sig contracts via IsMultiSigContract, validates the signature count against the threshold, and deducts the appropriate MultiSignatureContractCost before falling back to generic verification if needed.

Custom Witness Rules with CalledByEntry

For scenarios requiring contextual authorization, developers can define explicit witness rules:

var signer = new Signer
{
    Account = senderHash,
    Scopes  = WitnessScope.CalledByEntry | WitnessScope.WitnessRules,
    Rules   = new[]
    {
        new WitnessRule
        {
            Action    = WitnessRuleAction.Allow,
            Condition = new CalledByEntryCondition()
        }
    }
};

tx.Signers = new[] { signer };

When the transaction's verification script invokes CheckWitness(senderHash), the runtime iterates over signer.GetAllRules(), matches the CalledByEntryCondition against engine.EntryScriptHash, and returns Allow only if the call originates from the transaction's entry point.

Summary

  • State-independent verification in Transaction.VerifyStateIndependent provides a fast path for simple ECDSA signatures, directly calling Crypto.VerifySignature without VM overhead.
  • State-dependent verification handles complex logic through Helper.VerifyWitness, executing verification scripts in the Neo Virtual Machine and deducting consumed GAS from network fees.
  • Witness rules enable granular access control through the Signer.GetAllRules iterator, supporting built-in scopes (CalledByEntry, CustomContracts, CustomGroups, Global) and user-defined WitnessRule objects with logical composition.
  • Runtime evaluation occurs via ApplicationEngine.Runtime.CheckWitness, which matches conditions against the current execution context and returns boolean authorization results based on the first matching rule.

Frequently Asked Questions

How does Neo optimize signature verification for simple single-sig transactions?

Neo bypasses the Virtual Machine for standard single-signature contracts by detecting specific script patterns in Transaction.VerifyStateIndependent. When the verification script matches the 35-byte standard format (0x21 <pubkey> 0xac) and the invocation script contains a 66-byte signature push, the system calls Crypto.VerifySignature directly using the secp256r1 curve. This fast-path verification consumes no GAS and executes immediately, optimizing throughput for common transfer operations.

What is the difference between witness scopes and explicit witness rules?

Witness scopes are convenience flags that automatically generate standard WitnessRule objects through Signer.GetAllRules. For example, WitnessScope.CalledByEntry yields a rule with CalledByEntryCondition, while WitnessScope.CustomContracts generates ScriptHashCondition rules for each allowed hash.

Explicit witness rules provide granular control through the WitnessScope.WitnessRules flag, allowing developers to define arbitrary WitnessRule objects with complex boolean logic using AndCondition, OrCondition, and NotCondition. While scopes cover common authorization patterns, explicit rules enable sophisticated policies like "allow if called by contract A AND (contract B OR group C)".

How does the CheckWitness system handle rule evaluation during contract execution?

When a contract calls System.Runtime.CheckWitness, the runtime locates the signer matching the requested hash and iterates through rules yielded by GetAllRules(). Each rule's Condition.Match(ApplicationEngine) method evaluates against the current execution context, checking properties like EntryScriptHash, CallingScriptHash, or CallingScriptGroup. The system returns immediately upon the first matching condition with its associated Allow or Deny action, implementing short-circuit evaluation. If no rules match, the witness check returns false, preventing the transaction from proceeding.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →