Permalink
Browse files

feat(fat0): Switch back to object format for inputs/outputs, error on…

… duplicate fields or appended content data

Previously we switched back to an array of {"address": "FA...",
"amount": 5} objects for inputs and outputs in order to avoid issues
with duplicate addresses. However this did not really eliminate the
problem entirely because any field could appear twice in a valid JSON
and this leads to ambiguity and implementation specificity. Now we
compute the expected length on the compacted JSON and compare it with
the compacted JSON in the entry content. This catches any errors due to
duplicate fields or additional data appeneded to the JSON in the entry
content. For example, previously the content "{ <valid fat0 entry> }{}"
would have been valid despite the additional "{}". Although this
particular data would make no difference, it is simpler if any entry
that is not just a single JSON object, or is a JSON object with
duplicate fields is rejected by default.
  • Loading branch information...
AdamSLevy committed Dec 9, 2018
1 parent 379a4c4 commit e971f992cb8511914e6c9b3d9b02a4bdfffa8ca1
Showing with 176 additions and 117 deletions.
  1. +11 −22 fat0/addressamountmap.go
  2. +39 −7 fat0/entry.go
  3. +18 −0 fat0/issuance.go
  4. +4 −4 fat0/issuance_test.go
  5. +34 −0 fat0/transaction.go
  6. +70 −84 fat0/transaction_test.go
@@ -11,30 +11,23 @@ import (
// Transaction.
type AddressAmountMap map[factom.Bytes32]uint64

// AddressAmount is used to marshal and unmarshal the JSON representation of a
// list of inputs or outputs in a Transaction.
type AddressAmount struct {
Address factom.Address `json:"address"`
Amount uint64 `json:"amount"`
}

// UnmarshalJSON unmarshals a list of addresses and amounts used in the inputs
// or outputs of a transaction. Duplicate addresses or addresses with a 0
// amount cause an error.
func (a *AddressAmountMap) UnmarshalJSON(data []byte) error {
aam := make(AddressAmountMap)
var aaS []AddressAmount
var aaS map[string]uint64
if err := json.Unmarshal(data, &aaS); err != nil {
return err
}
for _, aa := range aaS {
if aa.Amount == 0 {
return fmt.Errorf("invalid amount (0) for address: %v", aa.Address)
}
if _, duplicate := aam[aa.Address.RCDHash()]; duplicate {
return fmt.Errorf("duplicate address: %v", aa.Address)
for address, amount := range aaS {
data := []byte(fmt.Sprintf("%#v", address))
address := factom.Address{}
if amount == 0 {
return fmt.Errorf("invalid amount (0) for address: %v", address)
}
aam[aa.Address.RCDHash()] = aa.Amount
json.Unmarshal(data, &address)
aam[address.RCDHash()] = amount
}
*a = aam
return nil
@@ -43,18 +36,14 @@ func (a *AddressAmountMap) UnmarshalJSON(data []byte) error {
// MarshalJSON marshals a list of addresses and amounts used in the inputs or
// outputs of a transaction. Addresses with a 0 amount are omitted.
func (a AddressAmountMap) MarshalJSON() ([]byte, error) {
as := make([]AddressAmount, 0, len(a))
as := make(map[string]uint64, len(a))
for rcdHash, amount := range a {
rcdHash := rcdHash
// Omit addresses with 0 amounts.
if amount == 0 {
continue
}

as = append(as, AddressAmount{
Address: factom.NewAddress(&rcdHash),
Amount: amount,
})
address := factom.NewAddress(&rcdHash)
as[address.String()] = amount
}
return json.Marshal(as)
}
@@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"strconv"
"time"
@@ -21,13 +22,45 @@ type Entry struct {

// unmarshalEntry unmarshals the content of the factom.Entry into the provided
// variable v, disallowing all unknown fields.
func (e Entry) unmarshalEntry(v interface{}) error {
func (e Entry) unmarshalEntry(v interface {
ExpectedJSONLength() int
}) error {
contentJSONLen := compactJSONLen(e.Content)
if contentJSONLen == 0 {
return fmt.Errorf("not a single valid JSON")
}
d := json.NewDecoder(bytes.NewReader(e.Content))
d.DisallowUnknownFields()
return d.Decode(v)
if err := d.Decode(v); err != nil {
return err
}
expectedJSONLen := v.ExpectedJSONLength()
if contentJSONLen != expectedJSONLen {
return fmt.Errorf("contentJSONLen (%v) != expectedJSONLen (%v)",
contentJSONLen, expectedJSONLen)
}
return nil
}

func (e Entry) metadataLen() int {
if e.Metadata == nil {
return 0
}
l := len(`,`)
l += len(`"metadata":`) + compactJSONLen(e.Metadata)
return l
}

func compactJSONLen(data []byte) int {
buf := bytes.NewBuffer(make([]byte, 0, len(data)))
json.Compact(buf, data)
cmp, _ := ioutil.ReadAll(buf)
return len(cmp)
}

func (e *Entry) marshalEntry(v interface{ ValidData() error }) error {
func (e *Entry) marshalEntry(v interface {
ValidData() error
}) error {
if err := v.ValidData(); err != nil {
return err
}
@@ -39,10 +72,9 @@ func (e *Entry) marshalEntry(v interface{ ValidData() error }) error {
return nil
}

// validExtIDs validates the structure of the external IDs of the entry to make
// sure that it has the correct number of RCD/signature pairs. ValidExtIDs does
// not validate the content of the RCD or signature. ValidExtIDs assumes that
// the entry content has been unmarshaled and that ValidData returns nil.
// ValidExtIDs validates the structure of the ExtIDs of the factom.Entry to
// make sure that it has a valid timestamp salt and a valid set of
// RCD/signature pairs.
func (e Entry) ValidExtIDs() error {
if len(e.ExtIDs) < 3 || len(e.ExtIDs)%2 != 1 {
return fmt.Errorf("invalid number of ExtIDs")
@@ -45,6 +45,24 @@ type Issuance struct {
Entry
}

func (i Issuance) ExpectedJSONLength() int {
l := len(`{`)
l += len(`"type":`) + len(`"`) + len(i.Type) + len(`"`)
l += len(`,"supply":`) + digitLen(i.Supply)
l += jsonStringLen("symbol", i.Symbol)
l += jsonStringLen("name", i.Name)
l += i.metadataLen()
l += len(`}`)
return l
}

func jsonStringLen(name, value string) int {
if len(value) != 0 {
return len(`,"`) + len(name) + len(`":`) + len(`"`) + len(value) + len(`"`)
}
return 0
}

// NewIssuance returns an Issuance initialized with the given entry.
func NewIssuance(entry factom.Entry) Issuance {
return Issuance{Entry: Entry{Entry: entry}}
@@ -124,7 +124,7 @@ var issuanceTests = []struct {
Issuance: invalidIssuance("name"),
}, {
Name: "invalid JSON (nil)",
Error: `EOF`,
Error: `not a single valid JSON`,
IssuerKey: issuerKey,
Issuance: issuance(nil),
}, {
@@ -134,7 +134,7 @@ var issuanceTests = []struct {
Issuance: setFieldIssuance("type", "invalid"),
}, {
Name: "invalid data (type omitted)",
Error: `invalid "type": ""`,
Error: `contentJSONLen (68) != expectedJSONLen (78)`,
IssuerKey: issuerKey,
Issuance: omitFieldIssuance("type"),
}, {
@@ -149,7 +149,7 @@ var issuanceTests = []struct {
Issuance: setFieldIssuance("supply", -5),
}, {
Name: "invalid data (supply: omitted)",
Error: `invalid "supply": must be positive or -1`,
Error: `contentJSONLen (67) != expectedJSONLen (78)`,
IssuerKey: issuerKey,
Issuance: omitFieldIssuance("supply"),
}, {
@@ -259,7 +259,7 @@ var issuanceMarshalEntryTests = []struct {
}(),
}, {
Name: "invalid data",
Error: "invalid type",
Error: `invalid "type": "invalid"`,
Issuance: func() Issuance {
i := newIssuance()
i.Type = "invalid"
@@ -31,6 +31,40 @@ func (t *Transaction) UnmarshalEntry() error {
return t.unmarshalEntry(t)
}

func (t Transaction) ExpectedJSONLength() int {
l := len(`{`)
l += len(`"inputs":`) + addressAmountMapJSONLen(t.Inputs)
l += len(`,`)
l += len(`"outputs":`) + addressAmountMapJSONLen(t.Outputs)
l += t.metadataLen()
l += len(`}`)
return l
}

func addressAmountMapJSONLen(m AddressAmountMap) int {
l := len(`{}`)
if len(m) > 0 {
l += len(m) * len(`"FA3p291ptJvHAFjf22naELozdFEKfbAPt8zLKaGiSVXfM6AUDVM5":,`)
l -= len(`,`)
for _, a := range m {
l += digitLen(int64(a))
}
}
return l
}

func digitLen(d int64) int {
l := 1
if d < 0 {
l += 1
d *= -1
}
for pow := int64(10); d/pow != 0; pow *= 10 {
l++
}
return l
}

// MarshalEntry marshals the entry content as a Transaction.
func (t *Transaction) MarshalEntry() error {
return t.marshalEntry(t)
Oops, something went wrong.

0 comments on commit e971f99

Please sign in to comment.