Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Op base64 decode #3220

Merged
merged 23 commits into from
Dec 8, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,5 @@ assets

index.html

# test summary
testresults.json
36 changes: 36 additions & 0 deletions data/transactions/logic/assembler.go
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,28 @@ func assembleEcdsa(ops *OpStream, spec *OpSpec, args []string) error {
return nil
}

func assembleBase64Decode(ops *OpStream, spec *OpSpec, args []string) error {
if len(args) != 1 {
return ops.errorf("%s expects one argument", spec.Name)
}

alph, ok := base64AlphabetSpecByName[args[0]]
if !ok {
return ops.errorf("%s unknown field: %#v", spec.Name, args[0])
}
if alph.version > ops.Version {
//nolint:errcheck // we continue to maintain typestack
ops.errorf("%s %s available in version %d. Missed #pragma version?", spec.Name, args[0], alph.version)
}

val := alph.field
ops.pending.WriteByte(spec.Opcode)
ops.pending.WriteByte(uint8(val))
ops.trace("%s (%s)", alph.field.String(), alph.ftype.String())
ops.returns(alph.ftype)
return nil
}

type assembleFunc func(*OpStream, *OpSpec, []string) error

// Basic assembly. Any extra bytes of opcode are encoded as byte immediates.
Expand Down Expand Up @@ -2668,6 +2690,20 @@ func disEcdsa(dis *disassembleState, spec *OpSpec) (string, error) {
return fmt.Sprintf("%s %s", spec.Name, EcdsaCurveNames[arg]), nil
}

func disBase64Decode(dis *disassembleState, spec *OpSpec) (string, error) {
lastIdx := dis.pc + 1
if len(dis.program) <= lastIdx {
missing := lastIdx - len(dis.program) + 1
return "", fmt.Errorf("unexpected %s opcode end: missing %d bytes", spec.Name, missing)
}
dis.nextpc = dis.pc + 2
b64dArg := dis.program[dis.pc+1]
if int(b64dArg) >= len(base64AlphabetNames) {
return "", fmt.Errorf("invalid base64_decode arg index %d at pc=%d", b64dArg, dis.pc)
}
return fmt.Sprintf("%s %s", spec.Name, base64AlphabetNames[b64dArg]), nil
}

type disInfo struct {
pcOffset []PCOffset
hasStatefulOps bool
Expand Down
6 changes: 6 additions & 0 deletions data/transactions/logic/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ var opDocByName = map[string]string{
"gloads": "push Ith scratch space index of the Xth transaction in the current group",
"gaid": "push the ID of the asset or application created in the Tth transaction of the current group",
"gaids": "push the ID of the asset or application created in the Xth transaction of the current group",
"zaid": "zush the ID of the asset or application created in the Xth transaction of the current group",

"bnz": "branch to TARGET if value X is not zero",
"bz": "branch to TARGET if value X is zero",
Expand Down Expand Up @@ -538,3 +539,8 @@ func AppParamsFieldDocs() map[string]string {
var EcdsaCurveDocs = map[string]string{
"Secp256k1": "secp256k1 curve",
}

var Base64AlphabetDocs = map[string]string{
"Standard": `Standard base-64 alphabet as specified in <a href="https://rfc-editor.org/rfc/rfc4648.html#section-4">RFC 4648 section 4</a>`,
"URL": `URL and Filename Safe base-64 alphabet as specified in <a href="https://rfc-editor.org/rfc/rfc4648.html#section-5">RFC 4648 section 5</a>`,
}
26 changes: 26 additions & 0 deletions data/transactions/logic/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"bytes"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"errors"
Expand Down Expand Up @@ -4034,3 +4035,28 @@ func (cx *EvalContext) PcDetails() (pc int, dis string) {
}
return cx.pc, dis
}

func base64Decode(encoded []byte, encoding *base64.Encoding) ([]byte, error) {
decoded := make([]byte, encoding.DecodedLen(len(encoded)))
n, err := encoding.Strict().Decode(decoded, encoded)
if err != nil {
n = 0
}
return decoded[:n], err
}

func opBase64Decode(cx *EvalContext) {
last := len(cx.stack) - 1
alphabetField := Base64Alphabet(cx.program[cx.pc+1])
fs, ok := base64AlphabetSpecByField[alphabetField]
if !ok || fs.version > cx.version {
cx.err = fmt.Errorf("invalid base64_decode field %d", alphabetField)
return
}

encoding := base64.URLEncoding
if alphabetField == StdAlph {
encoding = base64.StdEncoding
}
cx.stack[last].Bytes, cx.err = base64Decode(cx.stack[last].Bytes, encoding)
}
219 changes: 219 additions & 0 deletions data/transactions/logic/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package logic

import (
"crypto/rand"
"encoding/base64"
"encoding/binary"
"encoding/hex"
Expand Down Expand Up @@ -4909,3 +4910,221 @@ func TestPcDetails(t *testing.T) {
})
}
}

type b64DecodeTestCase struct {
Encoded string
IsURL bool
Decoded string
Error error
}

var testCases = []b64DecodeTestCase{
{"TU9CWS1ESUNLOwoKb3IsIFRIRSBXSEFMRS4KCgpCeSBIZXJtYW4gTWVsdmlsbGU=",
false,
`MOBY-DICK;

or, THE WHALE.


By Herman Melville`,
nil,
},
{"TU9CWS1ESUNLOwoKb3IsIFRIRSBXSEFMRS4KCgpCeSBIZXJtYW4gTWVsdmlsbGU=",
true,
`MOBY-DICK;

or, THE WHALE.


By Herman Melville`,
nil,
},
{"YWJjMTIzIT8kKiYoKSctPUB+", false, "abc123!?$*&()'-=@~", nil},
{"YWJjMTIzIT8kKiYoKSctPUB-", true, "abc123!?$*&()'-=@~", nil},
{"YWJjMTIzIT8kKiYoKSctPUB+", true, "", base64.CorruptInputError(23)},
{"YWJjMTIzIT8kKiYoKSctPUB-", false, "", base64.CorruptInputError(23)},
}

func TestBase64DecodeFunc(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()

for _, testCase := range testCases {
encoding := base64.StdEncoding
if testCase.IsURL {
encoding = base64.URLEncoding
}
encoding = encoding.Strict()
decoded, err := base64Decode([]byte(testCase.Encoded), encoding)
require.Equal(t, []byte(testCase.Decoded), decoded)
require.Equal(t, testCase.Error, err)
}
}

type b64DecodeTestArgs struct {
Raw []byte
Encoded []byte
IsURL bool
Program []byte
}

func testB64DecodeAssembleWithArgs(t *testing.T) []b64DecodeTestArgs {
sourceTmpl := `#pragma version 5
arg 0
arg 1
base64_decode %s
==`
args := []b64DecodeTestArgs{}
for _, testCase := range testCases {
if testCase.Error == nil {
field := "StdAlph"
if testCase.IsURL {
field = "URLAlph"
}
source := fmt.Sprintf(sourceTmpl, field)
ops, err := AssembleStringWithVersion(source, 5)
require.NoError(t, err)

arg := b64DecodeTestArgs{
Raw: []byte(testCase.Decoded),
Encoded: []byte(testCase.Encoded),
IsURL: testCase.IsURL,
Program: ops.Program,
}
args = append(args, arg)
}
}
return args
}

func testB64DecodeEval(tb testing.TB, args []b64DecodeTestArgs) {
for _, data := range args {
var txn transactions.SignedTxn
txn.Lsig.Logic = data.Program
txn.Lsig.Args = [][]byte{data.Raw[:], data.Encoded[:]}
ep := defaultEvalParams(&strings.Builder{}, &txn)
pass, err := Eval(data.Program, ep)
if err != nil {
require.NoError(tb, err)
}
if !pass {
fmt.Printf("FAILING WITH data = %#v", data)
require.True(tb, pass)
}
}
}

func TestOpBase64Decode(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()
args := testB64DecodeAssembleWithArgs(t)
testB64DecodeEval(t, args)
}

func benchmarkB64DecodeGenData(b *testing.B, source string, isURL bool, msgLen int) (args []b64DecodeTestArgs, err error) {
args = make([]b64DecodeTestArgs, b.N)
var ops *OpStream
ops, err = AssembleStringWithVersion(source, 5)
if err != nil {
require.NoError(b, err)
return
}

encoding := base64.StdEncoding
if isURL {
encoding = base64.URLEncoding
}
encoding = encoding.Strict()

msg := make([]byte, msgLen)
for i := 0; i < b.N; i++ {
_, err = rand.Read(msg)
if err != nil {
require.NoError(b, err)
return
}
args[i].Raw = make([]byte, msgLen)
copy(args[i].Raw, msg)
eStr := encoding.EncodeToString(msg)
args[i].Encoded = make([]byte, len(eStr))
copy(args[i].Encoded, []byte(eStr))
args[i].IsURL = isURL
args[i].Program = make([]byte, len(ops.Program))
copy(args[i].Program, ops.Program[:])
}
return
}

func benchmarkB64Decode(b *testing.B, scenario string, msgLen int) {
var source string
isURL := false

switch scenario {
case "baseline":
source = `#pragma version 5
arg 0
dup
arg 1
pop
==`
case "base64url":
isURL = true
source = `#pragma version 5
arg 0
arg 1
dup
pop
base64_decode URLAlph
==`
case "base64std":
isURL = false
source = `#pragma version 5
arg 0
arg 1
dup
pop
base64_decode StdAlph
==`
default:
source = fmt.Sprintf(`#pragma version 5
arg 0
dup
arg 1
%s
pop
==`, scenario)
}
args, err := benchmarkB64DecodeGenData(b, source, isURL, msgLen)
if err != nil {
require.NoError(b, err)
return
}
b.ResetTimer()
testB64DecodeEval(b, args)
}

var b64DecodeMsgLengths = []int{50, 1050, 2050, 3050}

var b64DecodeScenarios = []string{
"baseline",
"len",
"store 0\nload 0",
"swap\nswap",
"dup\nb&",
"b~",
"keccak256",
"sha256",
"sha512_256",
"base64url",
"base64std",
}

func BenchmarkB64DecodeWithComparisons(b *testing.B) {
for _, msgLen := range b64DecodeMsgLengths {
for _, scenario := range b64DecodeScenarios {
b.Run(fmt.Sprintf("%s_%d", scenario, msgLen), func(b *testing.B) {
benchmarkB64Decode(b, scenario, msgLen)
})
}
}
}
Loading