File size: 11,292 Bytes
2e9a79c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
"""
Inference script for Expert Classification models.

Loads a binary classifier (AutoModelForSequenceClassification + LoRA) that was
trained with train_expert_classifier.py, and predicts whether a Solidity
contract contains a specific vulnerability type.

Output: a single label (safe / vulnerable) with confidence score.

Usage:
    # From a local checkpoint folder (after training):
    python inference_classifier.py --checkpoint ./cls-expert-reentrancy/best_model --file contract.sol

    # From a Hub model (if pushed):
    python inference_classifier.py --checkpoint jhsu12/solidity-vuln-cls-reentrancy-v1 --file contract.sol

    # Inline code:
    python inference_classifier.py --checkpoint ./cls-expert-reentrancy/best_model \
        --code "pragma solidity ^0.8.0; contract Vault { ... }"

    # Interactive mode:
    python inference_classifier.py --checkpoint ./cls-expert-reentrancy/best_model

    # Run all 5 experts at once (pass multiple checkpoints):
    python inference_classifier.py --file contract.sol \
        --checkpoint ./cls-expert-reentrancy/best_model \
        --checkpoint ./cls-expert-access-control/best_model \
        --checkpoint ./cls-expert-integer-overflow-underflow/best_model \
        --checkpoint ./cls-expert-timestamp-dependence/best_model \
        --checkpoint ./cls-expert-unchecked-low-level-calls/best_model
"""

import argparse
import os
import sys
import json
import torch
import torch.nn.functional as F
from transformers import AutoTokenizer, AutoModelForSequenceClassification, BitsAndBytesConfig
from peft import PeftModel, PeftConfig

# ── Configuration ─────────────────────────────────────────────────────────────
BASE_MODEL = "Qwen/Qwen2.5-Coder-3B-Instruct"
LABEL_MAP = {0: "safe", 1: "vulnerable"}


def parse_args():
    parser = argparse.ArgumentParser(
        description="Run expert classifier inference on Solidity code."
    )
    parser.add_argument(
        "--checkpoint", type=str, action="append", required=True,
        help="Path to classifier checkpoint (local folder or Hub ID). "
             "Can specify multiple times to run several experts."
    )
    parser.add_argument(
        "--file", type=str, default=None,
        help="Path to a .sol file to analyze"
    )
    parser.add_argument(
        "--code", type=str, default=None,
        help="Inline Solidity code string to analyze"
    )
    parser.add_argument(
        "--max_seq_len", type=int, default=1536,
        help="Max sequence length for tokenization (default: 1536)"
    )
    parser.add_argument(
        "--load_in_4bit", action="store_true", default=True,
        help="Use 4-bit quantization (default: True)"
    )
    parser.add_argument(
        "--load_in_8bit", action="store_true", default=False,
        help="Use 8-bit quantization instead of 4-bit"
    )
    parser.add_argument(
        "--threshold", type=float, default=0.5,
        help="Confidence threshold for 'vulnerable' prediction (default: 0.5)"
    )
    parser.add_argument(
        "--json", action="store_true", default=False,
        help="Output results as JSON"
    )
    return parser.parse_args()


def detect_base_model(checkpoint_path):
    """Try to read the base model from the adapter config, fallback to default."""
    # Check for adapter_config.json in local path
    config_path = os.path.join(checkpoint_path, "adapter_config.json")
    if os.path.isfile(config_path):
        with open(config_path, "r") as f:
            cfg = json.load(f)
        base = cfg.get("base_model_name_or_path", BASE_MODEL)
        print(f"   Base model (from adapter_config): {base}")
        return base

    # Try loading PeftConfig from Hub
    try:
        peft_config = PeftConfig.from_pretrained(checkpoint_path)
        base = peft_config.base_model_name_or_path
        print(f"   Base model (from PeftConfig): {base}")
        return base
    except Exception:
        pass

    print(f"   Base model (default): {BASE_MODEL}")
    return BASE_MODEL


def load_classifier(checkpoint_path, load_in_4bit=True, load_in_8bit=False):
    """Load base model (SeqCls) + LoRA adapter for classification."""
    print(f"\nπŸ”Œ Loading classifier from: {checkpoint_path}")

    base_model_id = detect_base_model(checkpoint_path)

    # Device / dtype
    if torch.cuda.is_available():
        gpu_name = torch.cuda.get_device_name(0)
        gpu_mem = torch.cuda.get_device_properties(0).total_memory / 1e9
        has_bf16 = torch.cuda.is_bf16_supported()
        print(f"   πŸ–₯️  GPU: {gpu_name} ({gpu_mem:.1f} GB)")
    else:
        has_bf16 = False
        print("   ⚠️  No GPU β€” running on CPU (slow)")

    compute_dtype = torch.bfloat16 if has_bf16 else torch.float16

    # Quantization
    if load_in_8bit:
        bnb_config = BitsAndBytesConfig(load_in_8bit=True)
    elif load_in_4bit:
        bnb_config = BitsAndBytesConfig(
            load_in_4bit=True,
            bnb_4bit_quant_type="nf4",
            bnb_4bit_compute_dtype=compute_dtype,
            bnb_4bit_use_double_quant=True,
        )
    else:
        bnb_config = None

    # Attention implementation
    attn_impl = "sdpa"
    try:
        import flash_attn
        attn_impl = "flash_attention_2"
    except ImportError:
        pass

    # Load base as SequenceClassification model
    model_kwargs = dict(
        num_labels=2,
        id2label={0: "safe", 1: "vulnerable"},
        label2id={"safe": 0, "vulnerable": 1},
        device_map="auto",
        torch_dtype=compute_dtype,
        trust_remote_code=True,
        attn_implementation=attn_impl,
        ignore_mismatched_sizes=True,
    )
    if bnb_config is not None:
        model_kwargs["quantization_config"] = bnb_config

    model = AutoModelForSequenceClassification.from_pretrained(
        base_model_id, **model_kwargs
    )

    # Load LoRA adapter (includes the trained score head via modules_to_save)
    model = PeftModel.from_pretrained(model, checkpoint_path)
    model.eval()

    # Tokenizer β€” try from checkpoint first, fall back to base
    try:
        tokenizer = AutoTokenizer.from_pretrained(checkpoint_path, trust_remote_code=True)
    except Exception:
        tokenizer = AutoTokenizer.from_pretrained(base_model_id, trust_remote_code=True)

    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token
    model.config.pad_token_id = tokenizer.pad_token_id

    # Infer expert name from checkpoint path
    expert_name = os.path.basename(checkpoint_path.rstrip("/"))
    for prefix in ["cls-expert-", "solidity-vuln-cls-", "best_model"]:
        expert_name = expert_name.replace(prefix, "")
    if not expert_name or expert_name == "best_model":
        expert_name = os.path.basename(os.path.dirname(checkpoint_path.rstrip("/")))
        for prefix in ["cls-expert-", "solidity-vuln-cls-"]:
            expert_name = expert_name.replace(prefix, "")

    print(f"   βœ… Classifier loaded (expert: {expert_name})")
    return model, tokenizer, expert_name


def classify(model, tokenizer, solidity_code, max_seq_len=1536, threshold=0.5):
    """Run a single classification inference. Returns dict with prediction."""
    inputs = tokenizer(
        solidity_code,
        return_tensors="pt",
        truncation=True,
        max_length=max_seq_len,
        padding=True,
    )
    inputs = {k: v.to(model.device) for k, v in inputs.items()}

    with torch.no_grad():
        outputs = model(**inputs)

    logits = outputs.logits[0]  # shape: (2,)
    probs = F.softmax(logits, dim=-1)

    safe_prob = probs[0].item()
    vuln_prob = probs[1].item()
    predicted_label = "vulnerable" if vuln_prob >= threshold else "safe"

    return {
        "prediction": predicted_label,
        "confidence": max(safe_prob, vuln_prob),
        "prob_safe": round(safe_prob, 4),
        "prob_vulnerable": round(vuln_prob, 4),
        "logit_safe": round(logits[0].item(), 4),
        "logit_vulnerable": round(logits[1].item(), 4),
    }


def main():
    args = parse_args()

    # ── Get the Solidity code ─────────────────────────────────────────────────
    if args.file:
        print(f"πŸ“„ Reading Solidity file: {args.file}")
        with open(args.file, "r") as f:
            solidity_code = f.read()
    elif args.code:
        solidity_code = args.code
    else:
        print("πŸ“ Paste your Solidity code below, then press Ctrl-D (Linux/Mac) "
              "or Ctrl-Z+Enter (Windows) to submit:\n")
        solidity_code = sys.stdin.read()

    if not solidity_code.strip():
        print("❌ No code provided. Use --file, --code, or pipe to stdin.")
        sys.exit(1)

    print(f"πŸ“ Input code length: {len(solidity_code)} characters")

    # ── Run each expert ───────────────────────────────────────────────────────
    all_results = []

    for ckpt in args.checkpoint:
        model, tokenizer, expert_name = load_classifier(
            ckpt,
            load_in_4bit=args.load_in_4bit and not args.load_in_8bit,
            load_in_8bit=args.load_in_8bit,
        )

        result = classify(
            model, tokenizer, solidity_code,
            max_seq_len=args.max_seq_len,
            threshold=args.threshold,
        )
        result["expert"] = expert_name
        result["checkpoint"] = ckpt
        all_results.append(result)

        # Free memory before loading next expert
        del model
        if torch.cuda.is_available():
            torch.cuda.empty_cache()

    # ── Output ────────────────────────────────────────────────────────────────
    if args.json:
        print(json.dumps(all_results, indent=2))
    else:
        print("\n" + "=" * 60)
        print("  EXPERT CLASSIFICATION RESULTS")
        print("=" * 60)

        for r in all_results:
            icon = "πŸ”΄" if r["prediction"] == "vulnerable" else "🟒"
            print(f"\n  {icon} Expert: {r['expert']}")
            print(f"     Prediction:  {r['prediction'].upper()}")
            print(f"     Confidence:  {r['confidence']:.1%}")
            print(f"     P(safe):     {r['prob_safe']:.4f}")
            print(f"     P(vuln):     {r['prob_vulnerable']:.4f}")
            print(f"     Logits:      safe={r['logit_safe']:.4f}  vuln={r['logit_vulnerable']:.4f}")

        # Summary if multiple experts
        if len(all_results) > 1:
            flagged = [r for r in all_results if r["prediction"] == "vulnerable"]
            print(f"\n{'─' * 60}")
            if flagged:
                print(f"  ⚠️  Flagged by {len(flagged)}/{len(all_results)} experts:")
                for r in flagged:
                    print(f"     β€’ {r['expert']} ({r['prob_vulnerable']:.1%} confidence)")
            else:
                print(f"  βœ… Passed all {len(all_results)} expert checks")

        print("\n" + "=" * 60)


if __name__ == "__main__":
    main()