Skip to content

Commit

Permalink
Implement Algorand addresses decoding with string literal prefixes
Browse files Browse the repository at this point in the history
  • Loading branch information
algorandskiy committed Dec 5, 2019
1 parent 624e7e9 commit 93502cb
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 12 deletions.
24 changes: 22 additions & 2 deletions GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,27 @@ let x = b
function test(x) { return x; }
```

Declarations, definitions and assignments are statement
Declarations, definitions and assignments are statements.

## String literals

String literals are decoded and stored as byte arrays in underlying **TEAL** program.
Literals might be encoded and contain hex escape sequence. The following encoding prefixes are supported:
* b32 for **base32** strings
* b64 for **base64** strings
* addr for Algorand addresses

```
const zeroAddress = addr"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ"
const someval = b64"MTIz"
function logic() {
if txn.Receiver == zeroAddress {
return 1
}
return 0
}
```

## Functions

Expand All @@ -50,7 +70,7 @@ function logic() { return inc(0); }
Must exist in every program and return integer. The return value (zero/non-zero) is **TRUE** or **FALSE** return code for entire **TEAL** program (smart contract).
```
function logic() {
if txn.Sender == "ABC" {
if txn.Sender == addr"47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU" {
return 1
}
return 0
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Teal Language

High-level language for Algorand Smart Contracts at Layer-1 and it's low-level **TEAL** language.
High-level language for Algorand Smart Contracts at Layer-1 and its low-level **TEAL** language.
The goal is to abstract the stack-based **TEAL** VM and provide imperative Go/JS/Python-like syntax.

## Language Features
Expand All @@ -10,7 +10,7 @@ The goal is to abstract the stack-based **TEAL** VM and provide imperative Go/JS
* Variables and constants
```
let variable1 = 1
const myaddr = "XYZ"
const myaddr = addr"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ"
```

* All binary and unary operations from **TEAL**
Expand Down
1 change: 1 addition & 0 deletions TealangLexer.l4
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ LAND : '&&';
fragment EncodingPrefix
: 'b32'
| 'b64'
| 'addr'
;

fragment StringChar
Expand Down
24 changes: 24 additions & 0 deletions compiler/codegen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,3 +362,27 @@ function logic() {
a.Equal("bnz end_logic", lines[9])
a.Equal("end_logic:", lines[10])
}

func TestAddressStringLiteralDecoding(t *testing.T) {
a := require.New(t)

source := `
function logic() {
let a = addr"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ"
return 0
}
`
result, errors := Parse(source)
a.NotEmpty(result, errors)
a.Empty(errors)
prog := Codegen(result)
lines := strings.Split(prog, "\n")
a.Equal("intcblock 0 1", lines[0])
a.Equal("bytecblock 0x"+strings.Repeat("00", 32), lines[1])
a.Equal("bytec 0", lines[2])
a.Equal("store 0", lines[3])
a.Equal("intc 0", lines[4])
a.Equal("intc 1", lines[5])
a.Equal("bnz end_logic", lines[6])
a.Equal("end_logic:", lines[7])
}
6 changes: 6 additions & 0 deletions compiler/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ const b = "abc\x01"
let x = b
function test(x) { return x; }
let sender = if global.GroupSize > 1 { txn.Sender } else { gtxn[1].Sender }
const zeroAddress = addr"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ"
const someval = b64"MTIz"
function inc(x) { return x+1; }
const myconst = 1
Expand All @@ -73,6 +75,10 @@ function logic() {
let x = txn.Receiver
}
if txn.Receiver == zeroAddress {
return 1
}
return inc(0);
let ret = TxTypePayment
Expand Down
24 changes: 24 additions & 0 deletions compiler/syntax_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,27 @@ function logic() {
a.Empty(errors)

}

func TestStringLiteralPrefixes(t *testing.T) {
a := require.New(t)

source := `
function logic() {
let a = b32"GEZDGCQ="
return 0
}
`
result, errors := Parse(source)
a.NotEmpty(result, errors)
a.Empty(errors)

source = `
function logic() {
let a = addr"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ"
return 0
}
`
result, errors = Parse(source)
a.NotEmpty(result, errors)
a.Empty(errors)
}
96 changes: 88 additions & 8 deletions compiler/util.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,106 @@
package compiler

import (
"bytes"
"crypto/sha512"
"encoding/base32"
"encoding/base64"
"fmt"
"strconv"
"strings"
)

const prefixBase32 = "b32"
const prefixBase64 = "b64"
const prefixAddr = "addr"

var decoders = map[string]func(string, int, int) ([]byte, error){
prefixBase32: b32String,
prefixBase64: b64String,
prefixAddr: addrString,
}

// parseStringLiteral unquotes string and returns []byte
func parseStringLiteral(input string) (result []byte, err error) {
if input[0] != '"' || input[len(input)-1] != '"' {
err = fmt.Errorf("no quotes")
return
start := 0
end := len(input) - 1
if input[start] != '"' {
// check encoding prefixes
for prefix, decoder := range decoders {
if strings.HasPrefix(input, prefix) {
start = len(prefix)
return decoder(input, start+1, end)
}
}
}

if input[start] != '"' || input[end] != '"' {
return nil, fmt.Errorf("no quotes")
}

return rawString(input, start+1, end)
}

func b32String(input string, start int, end int) (result []byte, err error) {
return base32.StdEncoding.DecodeString(input[start:end])
}

func b64String(input string, start int, end int) (result []byte, err error) {
return base64.StdEncoding.DecodeString(input[start:end])
}

func addrString(input string, start int, end int) (result []byte, err error) {
const checksumLength = 4
type digest [sha512.Size256]byte

checksum := func(data digest) []byte {
shortAddressHash := sha512.Sum512_256(data[:])
checksum := shortAddressHash[len(shortAddressHash)-checksumLength:]
return checksum
}
canonical := func(data digest) string {
var addrWithChecksum []byte
addrWithChecksum = append(data[:], checksum(data)...)
return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(addrWithChecksum)
}

address := input[start:end]
decoded, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(address)
if err != nil {
return nil, fmt.Errorf("failed to decode address %s to base 32", address)
}
var short digest
if len(decoded) < len(short) {
return nil, fmt.Errorf("decoded bad addr: %s", address)
}

copy(short[:], decoded[:len(short)])
incomingchecksum := decoded[len(decoded)-checksumLength:]

calculatedchecksum := checksum(short)
isValid := bytes.Equal(incomingchecksum, calculatedchecksum)

if !isValid {
return nil, fmt.Errorf("address %s is malformed, checksum verification failed", address)
}

var char byte
// Validate that we had a canonical string representation
if canonical(short) != address {
return nil, fmt.Errorf("address %s is non-canonical", address)
}

return short[:], nil
}

func rawString(input string, start int, end int) (result []byte, err error) {
escapeSeq := false
hexSeq := false
result = make([]byte, 0, len(input)-2)
result = make([]byte, 0, end-start+1)

// skip first and last quotes
pos := 1
for pos < len(input)-1 {
char = input[pos]
pos := start
for pos < end {
char := input[pos]
if char == '\\' && !escapeSeq {
if hexSeq {
return nil, fmt.Errorf("escape seq inside hex number")
Expand Down
51 changes: 51 additions & 0 deletions compiler/util_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package compiler

import (
"bytes"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -68,3 +69,53 @@ func TestStringDecoding(t *testing.T) {
result, err = parseStringLiteral(s)
require.EqualError(t, err, "non-terminated hex seq")
}

func TestStringEncodingPrefixes(t *testing.T) {
a := require.New(t)

s := `b64"MTIz"`
e := []byte(`123`)
result, err := parseStringLiteral(s)
a.NoError(err)
a.Equal(e, result)

s = `b64"MTIzCg=="`
e = []byte("123\n")
result, err = parseStringLiteral(s)
a.NoError(err)
a.Equal(e, result)

s = `b64"123"`
result, err = parseStringLiteral(s)
a.Error(err)

s = `b32"GEZDGCQ="`
e = []byte("123\n")
result, err = parseStringLiteral(s)
a.NoError(err)
a.Equal(e, result)

s = `b32"GEZDG==="`
e = []byte("123")
result, err = parseStringLiteral(s)
a.NoError(err)
a.Equal(e, result)

s = `b32"123"`
result, err = parseStringLiteral(s)
a.Error(err)

s = `addr"J5YDZLPOHWB5O6MVRHNFGY4JXIQAYYM6NUJWPBSYBBIXH5ENQ4Z5LTJELU"`
result, err = parseStringLiteral(s)
a.NoError(err)

s = `addr"J5YDZLPOHWB5O6MVRHNFGY4JXIQAYYM6NUJWPBSYBBIXH5ENQ4Z5LTJELY"`
result, err = parseStringLiteral(s)
a.Error(err)

s = `addr"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ"`
e = bytes.Repeat([]byte("\x00"), 32)
result, err = parseStringLiteral(s)
a.NoError(err)
a.Equal(e, result)
}

0 comments on commit 93502cb

Please sign in to comment.