aboutsummaryrefslogtreecommitdiff
path: root/crates/hashx/src/constraints.rs
blob: 0e2961a800d2c1dcc394b0b63aca5aae74c66356 (plain)
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
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
//! Constraints that affect program generation
//!
//! Defines specific configurations that are not allowed, causing programs
//! or program fragments to be rejected during the generation process.
//!
//! The motivation for these constraints are in producing a good quality hash
//! function by avoiding hazards that affect timing or hash mixing. However,
//! they also form an integral part of the program generation model.
//! Generating correct HashX output depends on applying exactly the right
//! constraints.

use crate::program::{Instruction, InstructionVec, Opcode};
use crate::register::{NUM_REGISTERS, RegisterId, RegisterSet};
use crate::scheduler::Scheduler;

pub(crate) use model::{Pass, RegisterWriter};

/// The `model` attempts to document what the HashX constraints are, separate
/// from the process of implementing those constraints.
mod model {
    use crate::program::{NUM_INSTRUCTIONS, Opcode};
    use crate::register::{self, RegisterId};

    /// Programs require an exact number of instructions. (The instruction
    /// buffer must have filled without any of the other stopping conditions)
    pub(super) const REQUIRED_INSTRUCTIONS: usize = NUM_INSTRUCTIONS;

    /// Programs require an exact overall data latency, represented as the
    /// simulated cycle at which the last register write completes.
    pub(super) const REQUIRED_OVERALL_RESULT_AT_CYCLE: usize = 194;

    /// Programs require an exact total number of multiply instructions, they
    /// can't be skipped for any reason.
    pub(super) const REQUIRED_MULTIPLIES: usize = 192;

    /// Determine which ops count when testing [`REQUIRED_MULTIPLIES`].
    #[inline(always)]
    pub(super) fn is_multiply(op: Opcode) -> bool {
        matches!(op, Opcode::Mul | Opcode::SMulH | Opcode::UMulH)
    }

    /// Does an instruction prohibit using the same register for src and dst?
    ///
    /// Meaningful only for ops that have both a source and destination register.
    #[inline(always)]
    pub(super) fn disallow_src_is_dst(op: Opcode) -> bool {
        matches!(
            op,
            Opcode::AddShift | Opcode::Mul | Opcode::Sub | Opcode::Xor
        )
    }

    /// Special case for register R5
    ///
    /// HashX special-cases one specific register for AddShift in order to fit
    /// in the constraints of x86_64 under the encodings chosen by HashX's
    /// original x86 compiler backend. See Table 2-5 in the Intel 64 and IA-32
    /// Software Developer Manual Volume 2A, "Special Cases of REX Encodings".
    /// Register R13 in x86_64 maps directly to R5 in the original HashX
    /// implementation. Even on backends where this constraint is not relevant,
    /// the program generation process requires us to take it into account.
    pub(super) const DISALLOW_REGISTER_FOR_ADDSHIFT: RegisterId = register::R5;

    /// Should `proposed` be rejected as the immediate successor of `previous`?
    #[inline(always)]
    pub(super) fn disallow_opcode_pair(previous: Opcode, proposed: Opcode) -> bool {
        match proposed {
            // Never rejected at this stage
            Opcode::Mul | Opcode::UMulH | Opcode::SMulH | Opcode::Target | Opcode::Branch => false,
            // Disallow exact opcode duplicates
            Opcode::AddConst | Opcode::Xor | Opcode::XorConst | Opcode::Rotate => {
                previous == proposed
            }
            // Register add/sub can't be chosen back to back
            Opcode::AddShift | Opcode::Sub => {
                previous == Opcode::AddShift || previous == Opcode::Sub
            }
        }
    }

    /// Should `this_writer` be allowed on a register which was previously
    /// written using `last_writer`?
    ///
    /// Requires the current [`Pass`].
    #[inline(always)]
    pub(super) fn writer_pair_allowed(
        pass: Pass,
        last_writer: RegisterWriter,
        this_writer: RegisterWriter,
    ) -> bool {
        match (last_writer, this_writer) {
            // HashX disallows back-to-back 64-bit multiplies on the
            // same destination register in Pass::Original but permits
            // them on the retry if the source register isn't identical.
            (RegisterWriter::Mul(_), RegisterWriter::Mul(_)) if matches!(pass, Pass::Original) => {
                false
            }

            // Other pairings are allowed if the writer info differs at all.
            (last_writer, this_writer) => last_writer != this_writer,
        }
    }

    /// One specific pass in the multi-pass instruction choice process
    ///
    /// [`super::Instruction`] choice can take multiple attempts to complete,
    /// and we allow the [`super::Validator`] to make different decisions on
    /// each pass.
    #[derive(Debug, Copy, Clone, Eq, PartialEq)]
    pub(crate) enum Pass {
        /// First pass, nothing has failed
        Original,
        /// Single retry pass before a timing stall
        Retry,
    }

    /// Information about the instruction that writes to a register, from the
    /// perspective of our particular constraints here
    ///
    /// This is conceptually similar to storing the last [`super::Instruction`]
    /// that wrote to a register, but HashX sometimes needs information for
    /// constraints which won't end up in the final `Instruction`.
    ///
    /// We've chosen the encoding to minimize the code size in
    /// writer_pair_allowed. Most pairwise comparisons can just be a register
    /// equality test.
    ///
    /// The instructions here fall into three categories which use their own
    /// format for encoding arguments:
    ///
    ///  - Wide Multiply, extra u32
    ///
    ///    UMulH and SMulH use an additional otherwise unused 32-bit value
    ///    from the Rng when considering writer collisions.
    ///
    ///    As far as I can tell this is a bug in the original implementation
    ///    but we can't change the behavior without breaking compatibility.
    ///
    ///    The collisions are rare enough not to be a worthwhile addition
    ///    to ASIC-resistance. It seems like this was a vestigial feature
    ///    left over from immediate value matching features which were removed
    ///    during the development of HashX, but I can't be sure.
    ///
    ///  - Constant source
    ///
    ///    Only considers the opcode itself, not the specific immediate value.
    ///
    ///  - Register source
    ///
    ///    Considers the source register, collapses add/subtract into one op.
    ///
    #[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
    pub(crate) enum RegisterWriter {
        /// Register not written yet
        #[default]
        None,
        /// Register source writer for [`super::Instruction::Mul`]
        Mul(RegisterId),
        /// Wide multiply writer for [`super::Instruction::UMulH`]
        UMulH(u32),
        /// Wide multiply writer for [`super::Instruction::SMulH`]
        SMulH(u32),
        /// Register source writer for [`super::Instruction::AddShift`]
        /// and [`super::Instruction::Sub`]
        AddSub(RegisterId),
        /// Constant source writer for [`super::Instruction::AddConst`]
        AddConst,
        /// Register source writer for [`super::Instruction::Xor`]
        Xor(RegisterId),
        /// Constant source writer for [`super::Instruction::XorConst`]
        XorConst,
        /// Constant source writer for [`super::Instruction::Rotate`]
        Rotate,
    }
}

/// Stateful program constraint checker
///
/// This keeps additional state during the construction of a new program,
/// in order to check constraints that may reject registers and entire programs.
#[derive(Debug, Clone)]
pub(crate) struct Validator {
    /// For each register in the file, keep track of the instruction it was
    /// written by.
    ///
    /// This becomes part of the constraints for
    /// destination registers in future instructions.
    writer_map: RegisterWriterMap,

    /// Total multiplication operations of all types
    multiply_count: usize,
}

impl Validator {
    /// Construct a new empty Validator.
    #[inline(always)]
    pub(crate) fn new() -> Self {
        Self {
            writer_map: RegisterWriterMap::new(),
            multiply_count: 0,
        }
    }

    /// Commit a new instruction to the validator state.
    #[inline(always)]
    pub(crate) fn commit_instruction(&mut self, inst: &Instruction, regw: RegisterWriter) {
        if model::is_multiply(inst.opcode()) {
            self.multiply_count += 1;
        }
        match inst.destination() {
            None => debug_assert_eq!(regw, RegisterWriter::None),
            Some(dst) => self.writer_map.insert(dst, regw),
        }
    }

    /// Is the completed program acceptable?
    ///
    /// Once the whole program is assembled, HashX still has a chance to reject
    /// it if it fails certain criteria.
    #[inline(always)]
    pub(crate) fn check_whole_program(
        &self,
        scheduler: &Scheduler,
        instructions: &InstructionVec,
    ) -> Result<(), ()> {
        if instructions.len() == model::REQUIRED_INSTRUCTIONS
            && scheduler.overall_latency().as_usize() == model::REQUIRED_OVERALL_RESULT_AT_CYCLE
            && self.multiply_count == model::REQUIRED_MULTIPLIES
        {
            Ok(())
        } else {
            Err(())
        }
    }

    /// Begin checking which destination registers are allowed for an op after
    /// its source is known, using the current state of the validator.
    ///
    /// Returns a DstRegisterChecker which can be used to test each specific
    /// destination RegisterId quickly.
    #[inline(always)]
    pub(crate) fn dst_registers_allowed(
        &self,
        op: Opcode,
        pass: Pass,
        writer_info: RegisterWriter,
        src: Option<RegisterId>,
    ) -> DstRegisterChecker<'_> {
        DstRegisterChecker {
            pass,
            writer_info,
            writer_map: &self.writer_map,
            op_is_add_shift: op == Opcode::AddShift,
            disallow_equal: if model::disallow_src_is_dst(op) {
                src
            } else {
                None
            },
        }
    }
}

/// State information returned by [`Validator::dst_registers_allowed`]
#[derive(Debug, Clone)]
pub(crate) struct DstRegisterChecker<'v> {
    /// Is this the original or retry pass?
    pass: Pass,
    /// Reference to a table of [`RegisterWriter`] information for each register
    writer_map: &'v RegisterWriterMap,
    /// The new [`RegisterWriter`] under consideration
    writer_info: RegisterWriter,
    /// Was this [`Opcode::AddShift`]?
    op_is_add_shift: bool,
    /// Optionally disallow one matching register, used to implement [`model::disallow_src_is_dst`]
    disallow_equal: Option<RegisterId>,
}

impl<'v> DstRegisterChecker<'v> {
    /// Check a single destination register for usability, using context from
    /// [`Validator::dst_registers_allowed`]
    #[inline(always)]
    pub(crate) fn check(&self, dst: RegisterId) -> bool {
        // One register specified by DISALLOW_REGISTER_FOR_ADDSHIFT can't
        // be used as destination for AddShift.
        if self.op_is_add_shift && dst == model::DISALLOW_REGISTER_FOR_ADDSHIFT {
            return false;
        }

        // A few instructions disallow choosing src and dst as the same
        if Some(dst) == self.disallow_equal {
            return false;
        }

        // Additional constraints are written on the pair of previous and
        // current instructions with the same destination.
        model::writer_pair_allowed(self.pass, self.writer_map.get(dst), self.writer_info)
    }
}

/// Figure out the allowed register set for an operation, given what's available
/// in the schedule.
#[inline(always)]
pub(crate) fn src_registers_allowed(available: RegisterSet, op: Opcode) -> RegisterSet {
    // HashX defines a special case DISALLOW_REGISTER_FOR_ADDSHIFT for
    // destination registers, and it also includes a look-ahead
    // condition here in source register allocation to prevent the dst
    // allocation from getting stuck as often. If we have only two
    // remaining registers for AddShift and one is the disallowed reg,
    // HashX defines that the random choice is short-circuited early
    // here and we always choose the one combination which is actually
    // allowed.
    if op == Opcode::AddShift
        && available.contains(model::DISALLOW_REGISTER_FOR_ADDSHIFT)
        && available.len() == 2
    {
        RegisterSet::from_filter(
            #[inline(always)]
            |reg| reg == model::DISALLOW_REGISTER_FOR_ADDSHIFT,
        )
    } else {
        available
    }
}

/// Should `proposed` be allowed as an opcode selector output immediately
/// following an output of `previous`?
///
/// Some pairs of adjacent [`Opcode`]s are rejected at the opcode selector level
/// without causing an entire instruction generation pass to fail.
#[inline(always)]
pub(crate) fn opcode_pair_allowed(previous: Option<Opcode>, proposed: Opcode) -> Result<(), ()> {
    match previous {
        None => Ok(()),
        Some(previous) => {
            if model::disallow_opcode_pair(previous, proposed) {
                Err(())
            } else {
                Ok(())
            }
        }
    }
}

/// Map each [`RegisterId`] to an [`RegisterWriter`]
#[derive(Default, Debug, Clone)]
struct RegisterWriterMap([RegisterWriter; NUM_REGISTERS]);

impl RegisterWriterMap {
    /// Make a new empty register writer map.
    ///
    /// All registers are set to None.
    #[inline(always)]
    fn new() -> Self {
        Default::default()
    }

    /// Write or overwrite the last [`RegisterWriter`] associated with `reg`.
    #[inline(always)]
    fn insert(&mut self, reg: RegisterId, writer: RegisterWriter) {
        self.0[reg.as_usize()] = writer;
    }

    /// Return the most recent mapping for 'reg', if any.
    #[inline(always)]
    fn get(&self, reg: RegisterId) -> RegisterWriter {
        self.0[reg.as_usize()]
    }
}