Skip to content

Commit

Permalink
add support for Parameters in Transactions (#11)
Browse files Browse the repository at this point in the history
* add support for Parameters in Transactions

* move transaction parameters value length into serialization logic
  • Loading branch information
gsgalloway authored Oct 22, 2019
1 parent cb37ffd commit ab6db15
Show file tree
Hide file tree
Showing 4 changed files with 351 additions and 5 deletions.
202 changes: 202 additions & 0 deletions contract_scripts.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package tezosprotocol

import (
"bytes"
"encoding"
"encoding/binary"
"math"

"golang.org/x/xerrors"
)
Expand Down Expand Up @@ -78,3 +80,203 @@ func (c *ContractScript) UnmarshalBinary(data []byte) error {

return nil
}

// EntrypointTag captures the possible tag values for $entrypoint.Tag
type EntrypointTag byte

// EntrypointTag values
const (
EntrypointTagDefault EntrypointTag = 0
EntrypointTagRoot EntrypointTag = 1
EntrypointTagDo EntrypointTag = 2
EntrypointTagSetDelegate EntrypointTag = 3
EntrypointTagRemoveDelegate EntrypointTag = 4
EntrypointTagNamed EntrypointTag = 255
)

// Entrypoint models $entrypoint
type Entrypoint struct {
tag EntrypointTag
name string
}

// Preset entrypoints (those with an implicit name)
var (
EntrypointDefault = Entrypoint{tag: EntrypointTagDefault}
EntrypointRoot = Entrypoint{tag: EntrypointTagRoot}
EntrypointDo = Entrypoint{tag: EntrypointTagDo}
EntrypointSetDelegate = Entrypoint{tag: EntrypointTagDefault}
EntrypointRemoveDelegate = Entrypoint{tag: EntrypointTagRemoveDelegate}
)

// NewNamedEntrypoint creates a named entrypoint. This should be used when attempting to
// invoke a custom entrypoint that is not one of the reserved ones (%default, %root, %do, etcetera...).
func NewNamedEntrypoint(name string) (Entrypoint, error) {
if len(name) > math.MaxUint8 {
return Entrypoint{}, xerrors.Errorf("entrypoint name %s exceeds maximum length %d", math.MaxUint8)
}
return Entrypoint{tag: EntrypointTagNamed, name: name}, nil
}

// Tag returns the entrypoint tag
func (e Entrypoint) Tag() EntrypointTag {
return e.tag
}

// Name returns the entrypoint name
func (e Entrypoint) Name() (string, error) {
switch e.tag {
case EntrypointTagDefault:
return "default", nil
case EntrypointTagRoot:
return "root", nil
case EntrypointTagDo:
return "do", nil
case EntrypointTagSetDelegate:
return "set_delegate", nil
case EntrypointTagRemoveDelegate:
return "remove_delegate", nil
case EntrypointTagNamed:
if e.name == "" {
return "", xerrors.Errorf("entrypoint is not named")
}
return e.name, nil
default:
return "", xerrors.Errorf("unrecognized entrypoint tag: %d", uint8(e.tag))
}
}

// String implements fmt.Stringer
func (e Entrypoint) String() string {
name, err := e.Name()
if err != nil {
return "<invalid entrypoint>"
}
return "%" + name
}

// MarshalBinary implements encoding.BinaryMarshaler.
func (e Entrypoint) MarshalBinary() ([]byte, error) { //nolint:golint,unparam
buffer := new(bytes.Buffer)
buffer.WriteByte(byte(e.tag))
if e.tag == EntrypointTagNamed {
buffer.WriteByte(uint8(len(e.name)))
buffer.WriteString(e.name)
}
return buffer.Bytes(), nil
}

// UnmarshalBinary implements encoding.BinaryUnmarshaler.
func (e *Entrypoint) UnmarshalBinary(data []byte) error {
if len(data) < 1 {
return xerrors.Errorf("too few bytes to unmarshal Entrypoint")
}
e.tag = EntrypointTag(data[0])
if e.tag == EntrypointTagNamed {
data = data[1:]
if len(data) < 1 {
return xerrors.Errorf("too few bytes to unmarshal Entrypoint name length")
}
nameLength := data[0]
data = data[1:]
if len(data) < int(nameLength) {
return xerrors.Errorf("too few bytes to unmarshal Entrypoint name")
}
e.name = string(data[:nameLength])
}
return nil
}

// TransactionParametersValue models $X_o.value
type TransactionParametersValue interface {
encoding.BinaryMarshaler
encoding.BinaryUnmarshaler
}

// note: want to create a rich type for this modeling Michelson instructions.
// This stopgap approach allows just using raw byte arrays in the meantime without
// sacrificing forward compatibility.

// TransactionParametersValueRawBytes is an interim way to provide the value for
// transaction parameters, until support for Michelson is added.
type TransactionParametersValueRawBytes []byte

// MarshalBinary implements encoding.BinaryMarshaler.
func (t *TransactionParametersValueRawBytes) MarshalBinary() ([]byte, error) {
var parameters []byte
if t != nil {
parameters = []byte(*t)
}
outputBuf := new(bytes.Buffer)
err := binary.Write(outputBuf, binary.BigEndian, uint32(len(parameters)))
if err != nil {
return nil, xerrors.Errorf("failed to marshal parameters length: %w', err")
}
outputBuf.Write(parameters)
return outputBuf.Bytes(), nil
}

// UnmarshalBinary implements encoding.BinaryUnmarshaler.
func (t *TransactionParametersValueRawBytes) UnmarshalBinary(data []byte) error {
var length uint32
err := binary.Read(bytes.NewReader(data), binary.BigEndian, &length)
if err != nil {
return xerrors.Errorf("invalid transaction parameters value: %w", err)
}
if len(data) != int(4+length) {
return xerrors.Errorf("parameters should be %d bytes, but was %d", length, len(data)-4)
}
*t = data[4:]
return nil
}

// TransactionParameters models $X_o.
// Reference: http://tezos.gitlab.io/babylonnet/api/p2p.html#x-0
type TransactionParameters struct {
Entrypoint Entrypoint
Value TransactionParametersValue
}

// MarshalBinary implements encoding.BinaryMarshaler.
func (t TransactionParameters) MarshalBinary() ([]byte, error) {
buffer := new(bytes.Buffer)
entrypointBytes, err := t.Entrypoint.MarshalBinary()
if err != nil {
return nil, xerrors.Errorf("failed to marshal entrypoint: %w", err)
}
buffer.Write(entrypointBytes)
valueBytes, err := t.Value.MarshalBinary()
if err != nil {
return nil, xerrors.Errorf("failed to marshal value: %w", err)
}
buffer.Write(valueBytes)
return buffer.Bytes(), nil
}

// UnmarshalBinary implements encoding.BinaryUnmarshaler.
func (t *TransactionParameters) UnmarshalBinary(data []byte) (err error) {
// cleanly recover from out of bounds exceptions
defer func() {
if err == nil {
if r := recover(); r != nil {
err = catchOutOfRangeExceptions(r)
}
}
}()
dataPtr := data
err = t.Entrypoint.UnmarshalBinary(dataPtr)
if err != nil {
return xerrors.Errorf("failed to unmarshal entrypoint: %w", err)
}
entrypointBytes, err := t.Entrypoint.MarshalBinary()
if err != nil {
return err
}
dataPtr = dataPtr[len(entrypointBytes):]
t.Value = &TransactionParametersValueRawBytes{}
err = t.Value.UnmarshalBinary(dataPtr)
if err != nil {
return xerrors.Errorf("failed to unmarshal value: %w", err)
}
return nil
}
73 changes: 73 additions & 0 deletions contract_scripts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package tezosprotocol_test

import (
"encoding/hex"
"math"
"strings"
"testing"

tezosprotocol "github.com/anchorageoss/tezosprotocol/v2"
Expand Down Expand Up @@ -37,3 +39,74 @@ func TestContractScriptUnmarshalBinary(t *testing.T) {
require.Error(err)
require.Contains(err.Error(), "failed to read storage")
}

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

// "do" entrypoint
// ---------------
// tezos-client rpc post /chains/main/blocks/head/helpers/forge/operations with '{
// "branch": "BMTiv62VhjkVXZJL9Cu5s56qTAJxyciQB2fzA9vd2EiVMsaucWB",
// "contents":
// [ { "kind": "transaction",
// "source": "tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx",
// "fee": "1266", "counter": "1", "gas_limit": "10100",
// "storage_limit": "277", "amount": "0",
// "destination": "KT1GrStTuhgMMpzbNWKTt7NoXGrYiufrHDYq",
// "parameters": {"entrypoint": "do", "value": {}} } ]
// }'
// e655948a282fcfc31b98abe9b37a82038c4c0e9b8e11f60ea0c7b33e6ecc625f6c0002298c03ed7d454a101eb7022bc95f7e5f41ac78f20901f44e950200015ab81204ccd229281b9c462edaf0a43e78075f4600ff02000000050200000000
paramsValueBytes, err := hex.DecodeString("0200000000")
require.NoError(err)
paramsValue := tezosprotocol.TransactionParametersValueRawBytes(paramsValueBytes)
params := tezosprotocol.TransactionParameters{
Entrypoint: tezosprotocol.EntrypointDo,
Value: &paramsValue,
}
expectedBytes := "02000000050200000000"
observedBytes, err := params.MarshalBinary()
require.NoError(err)
require.Equal(expectedBytes, hex.EncodeToString(observedBytes))
reserialized := tezosprotocol.TransactionParameters{}
require.NoError(reserialized.UnmarshalBinary(observedBytes))
require.Equal(params, reserialized)
}

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

// misc named entrypoint
// ---------------------
// tezos-client rpc post /chains/main/blocks/head/helpers/forge/operations with '{
// "branch": "BMTiv62VhjkVXZJL9Cu5s56qTAJxyciQB2fzA9vd2EiVMsaucWB",
// "contents":
// [ { "kind": "transaction",
// "source": "tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx",
// "fee": "1266", "counter": "1", "gas_limit": "10100",
// "storage_limit": "277", "amount": "0",
// "destination": "KT1GrStTuhgMMpzbNWKTt7NoXGrYiufrHDYq",
// "parameters": {"entrypoint": "dummy", "value": {}} } ]
// }'
// e655948a282fcfc31b98abe9b37a82038c4c0e9b8e11f60ea0c7b33e6ecc625f6c0002298c03ed7d454a101eb7022bc95f7e5f41ac78f20901f44e950200015ab81204ccd229281b9c462edaf0a43e78075f4600ffff0564756d6d79000000050200000000
paramsValueBytes, err := hex.DecodeString("0200000000")
require.NoError(err)
entrypoint, err := tezosprotocol.NewNamedEntrypoint("dummy")
require.NoError(err)
paramsValue := tezosprotocol.TransactionParametersValueRawBytes(paramsValueBytes)
expectedBytes := "ff0564756d6d79000000050200000000"
params := tezosprotocol.TransactionParameters{
Entrypoint: entrypoint,
Value: &paramsValue,
}
observedBytes, err := params.MarshalBinary()
require.NoError(err)
require.Equal(expectedBytes, hex.EncodeToString(observedBytes))
reserialized := tezosprotocol.TransactionParameters{}
require.NoError(reserialized.UnmarshalBinary(observedBytes))
require.Equal(params, reserialized)
}

func TestEndpointNameTooLong(t *testing.T) {
_, err := tezosprotocol.NewNamedEntrypoint(strings.Repeat("a", math.MaxUint8+1))
require.Error(t, err)
}
22 changes: 17 additions & 5 deletions p2p_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -684,8 +684,7 @@ type Transaction struct {
StorageLimit *big.Int
Amount *big.Int
Destination ContractID
//nolint:godox
// TODO: parameters
Parameters *TransactionParameters
}

func (t *Transaction) String() string {
Expand Down Expand Up @@ -758,8 +757,16 @@ func (t *Transaction) MarshalBinary() ([]byte, error) {
}
buf.Write(destinationBytes)

// no parameters follow
buf.WriteByte(0)
// parameters
paramsFollow := t.Parameters != nil
buf.WriteByte(serializeBoolean(paramsFollow))
if paramsFollow {
paramsBytes, err := t.Parameters.MarshalBinary()
if err != nil {
return nil, xerrors.Errorf("failed to write transaction parameters: %w", err)
}
buf.Write(paramsBytes)
}

return buf.Bytes(), nil
}
Expand Down Expand Up @@ -836,11 +843,16 @@ func (t *Transaction) UnmarshalBinary(data []byte) (err error) {

// parameters
hasParameters, err := deserializeBoolean(dataPtr[0])
dataPtr = dataPtr[1:]
if err != nil {
return xerrors.Errorf("failed to deserialialize presence of field \"parameters\": %w", err)
}
if hasParameters {
return xerrors.Errorf("deserializing parameters not supported")
t.Parameters = &TransactionParameters{Value: &TransactionParametersValueRawBytes{}}
err = t.Parameters.UnmarshalBinary(dataPtr)
if err != nil {
return xerrors.Errorf("failed to deserialize transaction parameters: %w", err)
}
}

return nil
Expand Down
Loading

0 comments on commit ab6db15

Please sign in to comment.