Split hard/soft AES implementations

This commit is contained in:
DataHoarder 2024-05-01 16:25:35 +02:00
parent 25b7fc4cc0
commit 1d83de4880
Signed by: DataHoarder
SSH key fingerprint: SHA256:OLTRf6Fl87G52SiR7sWLGNzlJt4WOX+tfI2yxo0z7xk
17 changed files with 239 additions and 236 deletions

View file

@ -23,10 +23,10 @@ type Cache struct {
JitPrograms [RANDOMX_PROGRAM_COUNT]SuperScalarProgramFunc
Flags uint64
Flags Flag
}
func NewCache(flags uint64) *Cache {
func NewCache(flags Flag) *Cache {
if flags == RANDOMX_FLAG_DEFAULT {
flags = RANDOMX_FLAG_JIT
}

View file

@ -6,7 +6,7 @@ type Dataset interface {
InitDataset(startItem, itemCount uint64)
ReadDataset(address uint64, r *RegisterLine)
PrefetchDataset(address uint64)
Flags() uint64
Flags() Flag
Cache() *Cache
Memory() []RegisterLine
}

View file

@ -33,7 +33,7 @@ func (d *DatasetFull) Cache() *Cache {
return d.cache
}
func (d *DatasetFull) Flags() uint64 {
func (d *DatasetFull) Flags() Flag {
return d.cache.Flags
}

View file

@ -27,7 +27,7 @@ func (d *DatasetLight) ReadDataset(address uint64, r *RegisterLine) {
}
}
func (d *DatasetLight) Flags() uint64 {
func (d *DatasetLight) Flags() Flag {
return d.cache.Flags
}

View file

@ -1,15 +1,18 @@
package randomx
import (
"git.gammaspectra.live/P2Pool/go-randomx/v3/internal/aes"
"golang.org/x/sys/cpu"
"runtime"
)
const RANDOMX_FLAG_DEFAULT = 0
type Flag uint64
const RANDOMX_FLAG_DEFAULT Flag = 0
const (
// RANDOMX_FLAG_LARGE_PAGES not implemented
RANDOMX_FLAG_LARGE_PAGES = 1 << iota
RANDOMX_FLAG_LARGE_PAGES = Flag(1 << iota)
// RANDOMX_FLAG_HARD_AES not implemented
RANDOMX_FLAG_HARD_AES
// RANDOMX_FLAG_FULL_MEM Selects between full or light mode dataset
@ -20,17 +23,30 @@ const (
RANDOMX_FLAG_SECURE
RANDOMX_FLAG_ARGON2_SSSE3
RANDOMX_FLAG_ARGON2_AVX2
RANDOMX_FLAG_ARGON2
RANDOMX_FLAG_ARGON2 = RANDOMX_FLAG_ARGON2_AVX2 | RANDOMX_FLAG_ARGON2_SSSE3
)
func GetFlags() (flags uint64) {
func GetFlags() (flags Flag) {
flags = RANDOMX_FLAG_DEFAULT
if runtime.GOARCH == "amd64" {
flags |= RANDOMX_FLAG_JIT
if cpu.X86.HasAES {
if aes.HasHardAESImplementation && cpu.X86.HasAES {
flags |= RANDOMX_FLAG_HARD_AES
}
if cpu.X86.HasSSSE3 {
flags |= RANDOMX_FLAG_ARGON2_SSSE3
}
if cpu.X86.HasAVX2 {
flags |= RANDOMX_FLAG_ARGON2_AVX2
}
}
if runtime.GOOS == "openbsd" || runtime.GOOS == "netbsd" || ((runtime.GOOS == "darwin" || runtime.GOOS == "ios") && runtime.GOARCH == "arm64") {
flags |= RANDOMX_FLAG_SECURE
}
return flags
}

View file

@ -0,0 +1,63 @@
//go:build amd64 && !purego
package aes
import (
"git.gammaspectra.live/P2Pool/go-randomx/v3/internal/asm"
"git.gammaspectra.live/P2Pool/go-randomx/v3/internal/keys"
"golang.org/x/sys/cpu"
"runtime"
"unsafe"
)
const HasHardAESImplementation = true
type hardAES struct {
}
func NewHardAES() AES {
if cpu.X86.HasAES {
return hardAES{}
}
return nil
}
func (h hardAES) HashAes1Rx4(input []byte, output *[64]byte) {
if len(input)%len(output) != 0 {
panic("unsupported")
}
asm.HashAes1Rx4(&keys.AesHash1R_State, &keys.AesHash1R_XKeys, output, unsafe.SliceData(input), uint64(len(input)))
}
func (h hardAES) FillAes1Rx4(state *[64]byte, output []byte) {
if len(output)%len(state) != 0 {
panic("unsupported")
}
// Reference to state without copying
states := (*[4][4]uint32)(unsafe.Pointer(state))
asm.FillAes1Rx4(states, &keys.AesGenerator1R_Keys, unsafe.SliceData(output), uint64(len(output)))
runtime.KeepAlive(state)
}
func (h hardAES) FillAes4Rx4(state [64]byte, output []byte) {
if len(output)%len(state) != 0 {
panic("unsupported")
}
// state is copied on caller
// Copy state
states := (*[4][4]uint32)(unsafe.Pointer(&state))
for outptr := 0; outptr < len(output); outptr += len(state) {
asm.AESRoundTrip_DecEnc(states, &fillAes4Rx4Keys0)
asm.AESRoundTrip_DecEnc(states, &fillAes4Rx4Keys1)
asm.AESRoundTrip_DecEnc(states, &fillAes4Rx4Keys2)
asm.AESRoundTrip_DecEnc(states, &fillAes4Rx4Keys3)
copy(output[outptr:], state[:])
}
}

View file

@ -0,0 +1,9 @@
//go:build !amd64 || purego
package aes
const HasHardAESImplementation = false
func NewHardAES() AES {
return nil
}

View file

@ -31,45 +31,8 @@ package aes
import (
"git.gammaspectra.live/P2Pool/go-randomx/v3/internal/keys"
"unsafe"
)
// HashAes1Rx4
//
// Calculate a 512-bit hash of 'input' using 4 lanes of AES.
// The input is treated as a set of round keys for the encryption
// of the initial state.
//
// 'inputSize' must be a multiple of 64.
//
// For a 2 MiB input, this has the same security as 32768-round
// AES encryption.
//
// Hashing throughput: >20 GiB/s per CPU core with hardware AES
func HashAes1Rx4(input []byte, output *[64]byte) {
if len(input)%64 != 0 {
panic("unsupported")
}
hashAes1Rx4(input, output)
}
// FillAes1Rx4
//
// Fill 'output' with pseudorandom data based on 512-bit 'state'.
// The state is encrypted using a single AES round per 16 bytes of output
// in 4 lanes.
//
// 'output' size must be a multiple of 64.
//
// The modified state is written back to 'state' to allow multiple
// calls to this function.
func FillAes1Rx4(state *[64]byte, output []byte) {
if len(output)%len(state) != 0 {
panic("unsupported")
}
fillAes1Rx4(state, output)
}
var fillAes4Rx4Keys0 = [4][4]uint32{
keys.AesGenerator4R_Keys[0],
keys.AesGenerator4R_Keys[0],
@ -94,25 +57,3 @@ var fillAes4Rx4Keys3 = [4][4]uint32{
keys.AesGenerator4R_Keys[7],
keys.AesGenerator4R_Keys[7],
}
// FillAes4Rx4 used to generate final program
func FillAes4Rx4(state [64]byte, output []byte) {
if len(output)%len(state) != 0 {
panic("unsupported")
}
// state is copied on caller
// Copy state
states := (*[4][4]uint32)(unsafe.Pointer(&state))
for outptr := 0; outptr < len(output); outptr += len(state) {
aesroundtrip_decenc(states, &fillAes4Rx4Keys0)
aesroundtrip_decenc(states, &fillAes4Rx4Keys1)
aesroundtrip_decenc(states, &fillAes4Rx4Keys2)
aesroundtrip_decenc(states, &fillAes4Rx4Keys3)
copy(output[outptr:], state[:])
}
}

View file

@ -1,50 +0,0 @@
//go:build amd64 && !purego
package aes
import (
"git.gammaspectra.live/P2Pool/go-randomx/v3/internal/asm"
"git.gammaspectra.live/P2Pool/go-randomx/v3/internal/keys"
"golang.org/x/sys/cpu"
"unsafe"
)
var supportsAES = cpu.X86.HasAES
func fillAes1Rx4(state *[64]byte, output []byte) {
// Reference to state without copying
states := (*[4][4]uint32)(unsafe.Pointer(state))
if supportsAES {
asm.FillAes1Rx4(states, &keys.AesGenerator1R_Keys, unsafe.SliceData(output), uint64(len(output)))
return
}
for outptr := 0; outptr < len(output); outptr += len(state) {
aesroundtrip_decenc(states, &keys.AesGenerator1R_Keys)
copy(output[outptr:], state[:])
}
}
func hashAes1Rx4(input []byte, output *[64]byte) {
if supportsAES {
asm.HashAes1Rx4(&keys.AesHash1R_State, &keys.AesHash1R_XKeys, output, unsafe.SliceData(input), uint64(len(input)))
return
}
// states are copied
states := keys.AesHash1R_State
for input_ptr := 0; input_ptr < len(input); input_ptr += 64 {
in := (*[4][4]uint32)(unsafe.Pointer(unsafe.SliceData(input[input_ptr:])))
aesroundtrip_encdec(&states, in)
}
aesroundtrip_encdec1(&states, &keys.AesHash1R_XKeys[0])
aesroundtrip_encdec1(&states, &keys.AesHash1R_XKeys[1])
copy(output[:], (*[64]byte)(unsafe.Pointer(&states))[:])
}

View file

@ -1,36 +0,0 @@
//go:build !amd64 || purego
package aes
import (
"git.gammaspectra.live/P2Pool/go-randomx/v3/internal/keys"
"unsafe"
)
func fillAes1Rx4(state *[64]byte, output []byte) {
// Reference to state without copying
states := (*[4][4]uint32)(unsafe.Pointer(state))
for outptr := 0; outptr < len(output); outptr += len(state) {
aesroundtrip_decenc(states, &keys.AesGenerator1R_Keys)
copy(output[outptr:], state[:])
}
}
func hashAes1Rx4(input []byte, output *[64]byte) {
// states are copied
states := keys.AesHash1R_State
for input_ptr := 0; input_ptr < len(input); input_ptr += 64 {
in := (*[4][4]uint32)(unsafe.Pointer(unsafe.SliceData(input[input_ptr:])))
aesroundtrip_encdec(&states, in)
}
aesroundtrip_encdec1(&states, &keys.AesHash1R_XKeys[0])
aesroundtrip_encdec1(&states, &keys.AesHash1R_XKeys[1])
copy(output[:], (*[64]byte)(unsafe.Pointer(&states))[:])
}

35
internal/aes/impl.go Normal file
View file

@ -0,0 +1,35 @@
package aes
type AES interface {
// HashAes1Rx4
//
// Calculate a 512-bit hash of 'input' using 4 lanes of AES.
// The input is treated as a set of round keys for the encryption
// of the initial state.
//
// 'input' size must be a multiple of 64.
//
// For a 2 MiB input, this has the same security as 32768-round
// AES encryption.
//
// Hashing throughput: >20 GiB/s per CPU core with hardware AES
HashAes1Rx4(input []byte, output *[64]byte)
// FillAes1Rx4
//
// Fill 'output' with pseudorandom data based on 512-bit 'state'.
// The state is encrypted using a single AES round per 16 bytes of output
// in 4 lanes.
//
// 'output' size must be a multiple of 64.
//
// The modified state is written back to 'state' to allow multiple
// calls to this function.
FillAes1Rx4(state *[64]byte, output []byte)
// FillAes4Rx4 used to generate final program
//
// 'state' is copied when calling
FillAes4Rx4(state [64]byte, output []byte)
}

View file

@ -29,3 +29,24 @@ func soft_aesdec(state *[4]uint32, key *[4]uint32) {
state[2] = key[2] ^ td0[uint8(s2)] ^ td1[uint8(s1>>8)] ^ td2[uint8(s0>>16)] ^ td3[uint8(s3>>24)]
state[3] = key[3] ^ td0[uint8(s3)] ^ td1[uint8(s2>>8)] ^ td2[uint8(s1>>16)] ^ td3[uint8(s0>>24)]
}
func soft_aesroundtrip_decenc(states *[4][4]uint32, keys *[4][4]uint32) {
soft_aesdec(&states[0], &keys[0])
soft_aesenc(&states[1], &keys[1])
soft_aesdec(&states[2], &keys[2])
soft_aesenc(&states[3], &keys[3])
}
func soft_aesroundtrip_encdec(states *[4][4]uint32, keys *[4][4]uint32) {
soft_aesenc(&states[0], &keys[0])
soft_aesdec(&states[1], &keys[1])
soft_aesenc(&states[2], &keys[2])
soft_aesdec(&states[3], &keys[3])
}
func soft_aesroundtrip_encdec1(states *[4][4]uint32, key *[4]uint32) {
soft_aesenc(&states[0], key)
soft_aesdec(&states[1], key)
soft_aesenc(&states[2], key)
soft_aesdec(&states[3], key)
}

View file

@ -1,40 +0,0 @@
//go:build amd64 && !purego
package aes
import (
"git.gammaspectra.live/P2Pool/go-randomx/v3/internal/asm"
)
func aesroundtrip_decenc(states *[4][4]uint32, keys *[4][4]uint32) {
if supportsAES {
asm.AESRoundTrip_DecEnc(states, keys)
} else {
soft_aesdec(&states[0], &keys[0])
soft_aesenc(&states[1], &keys[1])
soft_aesdec(&states[2], &keys[2])
soft_aesenc(&states[3], &keys[3])
}
}
func aesroundtrip_encdec(states *[4][4]uint32, keys *[4][4]uint32) {
if supportsAES {
asm.AESRoundTrip_EncDec(states, keys)
} else {
soft_aesenc(&states[0], &keys[0])
soft_aesdec(&states[1], &keys[1])
soft_aesenc(&states[2], &keys[2])
soft_aesdec(&states[3], &keys[3])
}
}
func aesroundtrip_encdec1(states *[4][4]uint32, key *[4]uint32) {
if supportsAES {
asm.AESRoundTrip_EncDec1(states, key)
} else {
soft_aesenc(&states[0], key)
soft_aesdec(&states[1], key)
soft_aesenc(&states[2], key)
soft_aesdec(&states[3], key)
}
}

View file

@ -1,32 +0,0 @@
//go:build !amd64 || purego
package aes
func aesenc(state *[4]uint32, key *[4]uint32) {
soft_aesenc(state, key)
}
func aesdec(state *[4]uint32, key *[4]uint32) {
soft_aesdec(state, key)
}
func aesroundtrip_decenc(states *[4][4]uint32, keys *[4][4]uint32) {
aesdec(&states[0], &keys[0])
aesenc(&states[1], &keys[1])
aesdec(&states[2], &keys[2])
aesenc(&states[3], &keys[3])
}
func aesroundtrip_encdec(states *[4][4]uint32, keys *[4][4]uint32) {
aesenc(&states[0], &keys[0])
aesdec(&states[1], &keys[1])
aesenc(&states[2], &keys[2])
aesdec(&states[3], &keys[3])
}
func aesroundtrip_encdec1(states *[4][4]uint32, key *[4]uint32) {
aesenc(&states[0], key)
aesdec(&states[1], key)
aesenc(&states[2], key)
aesdec(&states[3], key)
}

69
internal/aes/soft.go Normal file
View file

@ -0,0 +1,69 @@
package aes
import (
"git.gammaspectra.live/P2Pool/go-randomx/v3/internal/keys"
"runtime"
"unsafe"
)
type softAES struct {
}
func NewSoftAES() AES {
return softAES{}
}
func (aes softAES) HashAes1Rx4(input []byte, output *[64]byte) {
if len(input)%len(output) != 0 {
panic("unsupported")
}
// states are copied
states := (*[4][4]uint32)(unsafe.Pointer(output))
*states = keys.AesHash1R_State
for input_ptr := 0; input_ptr < len(input); input_ptr += 64 {
in := (*[4][4]uint32)(unsafe.Pointer(unsafe.SliceData(input[input_ptr:])))
soft_aesroundtrip_encdec(states, in)
}
soft_aesroundtrip_encdec1(states, &keys.AesHash1R_XKeys[0])
soft_aesroundtrip_encdec1(states, &keys.AesHash1R_XKeys[1])
runtime.KeepAlive(output)
}
func (aes softAES) FillAes1Rx4(state *[64]byte, output []byte) {
if len(output)%len(state) != 0 {
panic("unsupported")
}
// Reference to state without copying
states := (*[4][4]uint32)(unsafe.Pointer(state))
for outptr := 0; outptr < len(output); outptr += len(state) {
soft_aesroundtrip_decenc(states, &keys.AesGenerator1R_Keys)
copy(output[outptr:], state[:])
}
}
func (aes softAES) FillAes4Rx4(state [64]byte, output []byte) {
if len(output)%len(state) != 0 {
panic("unsupported")
}
// state is copied on caller
// Copy state
states := (*[4][4]uint32)(unsafe.Pointer(&state))
for outptr := 0; outptr < len(output); outptr += len(state) {
soft_aesroundtrip_decenc(states, &fillAes4Rx4Keys0)
soft_aesroundtrip_decenc(states, &fillAes4Rx4Keys1)
soft_aesroundtrip_decenc(states, &fillAes4Rx4Keys2)
soft_aesroundtrip_decenc(states, &fillAes4Rx4Keys3)
copy(output[outptr:], state[:])
}
}

18
vm.go
View file

@ -45,6 +45,8 @@ type REG struct {
type VM struct {
ScratchPad ScratchPad
AES aes.AES
Dataset Dataset
program ByteCode
@ -55,6 +57,15 @@ func NewVM(dataset Dataset) *VM {
vm := &VM{
Dataset: dataset,
}
if dataset.Flags()&RANDOMX_FLAG_HARD_AES > 0 {
vm.AES = aes.NewHardAES()
}
// fallback
if vm.AES == nil {
vm.AES = aes.NewSoftAES()
}
if dataset.Cache().HasJIT() {
vm.jitProgram = mapProgram(nil, int(RandomXCodeSize))
if dataset.Flags()&RANDOMX_FLAG_SECURE == 0 {
@ -74,7 +85,7 @@ func (vm *VM) run(inputHash [64]byte, roundingMode uint8) (reg RegisterFile) {
// buffer first 128 bytes are entropy below rest are program bytes
var buffer [16*8 + RANDOMX_PROGRAM_SIZE*8]byte
aes.FillAes4Rx4(inputHash, buffer[:])
vm.AES.FillAes4Rx4(inputHash, buffer[:])
entropy := (*[16]uint64)(unsafe.Pointer(&buffer))
@ -202,7 +213,8 @@ func (vm *VM) run(inputHash [64]byte, roundingMode uint8) (reg RegisterFile) {
}
func (vm *VM) initScratchpad(seed *[64]byte) {
vm.ScratchPad.Init(seed)
clear(vm.ScratchPad[:])
vm.AES.FillAes1Rx4(seed, vm.ScratchPad[:])
}
func (vm *VM) runLoops(tempHash [64]byte) RegisterFile {
@ -243,7 +255,7 @@ func (vm *VM) CalculateHash(input []byte, output *[32]byte) {
reg := vm.runLoops(tempHash)
// now hash the scratch pad as it will act as register A
aes.HashAes1Rx4(vm.ScratchPad[:], &tempHash)
vm.AES.HashAes1Rx4(vm.ScratchPad[:], &tempHash)
regMem := reg.Memory()
// write hash onto register A

View file

@ -30,7 +30,6 @@ USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package randomx
import (
"git.gammaspectra.live/P2Pool/go-randomx/v3/internal/aes"
"unsafe"
)
import "encoding/binary"
@ -359,15 +358,11 @@ func CompileProgramToByteCode(prog []byte, bc *ByteCode) {
type ScratchPad [ScratchpadSize]byte
func (pad *ScratchPad) Init(seed *[64]byte) {
// calculate and fill scratchpad
clear(pad[:])
aes.FillAes1Rx4(seed, pad[:])
}
func (pad *ScratchPad) Store64(addr uint32, val uint64) {
*(*uint64)(unsafe.Pointer(&pad[addr])) = val
//binary.LittleEndian.PutUint64(pad[addr:], val)
}
func (pad *ScratchPad) Load64(addr uint32) uint64 {
return *(*uint64)(unsafe.Pointer(&pad[addr]))
}