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:
IsSignatureContractdetects standard 35-byte verification scripts (0x21 <pubkey> 0xac)IsSingleSignatureInvocationScriptvalidates 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
verifymethod directly throughengine.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
trueto succeed. The VM execution consumes GAS proportional to the script's computational complexity, with the consumed amount returned via thefeeparameter 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: YieldsCalledByEntryCondition, allowing calls only from the transaction's entry scriptCustomContracts: GeneratesScriptHashConditionrules for each allowed contract hashCustomGroups: CreatesGroupConditionrules for cryptographic group membershipWitnessRules: Embeds explicit user-definedWitnessRuleobjects with custom logicGlobal: Creates aBooleanConditionwithExpression = 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.VerifyStateIndependentprovides a fast path for simple ECDSA signatures, directly callingCrypto.VerifySignaturewithout 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.GetAllRulesiterator, supporting built-in scopes (CalledByEntry,CustomContracts,CustomGroups,Global) and user-definedWitnessRuleobjects 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →