diff --git a/ledger/ledgercore/statedelta.go b/ledger/ledgercore/statedelta.go index 1d2562ca4f..22e17592b6 100644 --- a/ledger/ledgercore/statedelta.go +++ b/ledger/ledgercore/statedelta.go @@ -17,6 +17,7 @@ package ledgercore import ( + "encoding/json" "fmt" "github.com/algorand/go-algorand/data/basics" @@ -126,6 +127,46 @@ type StateDelta struct { Totals AccountTotals } +// StateDeltaSerializable is nearly identical to StateDelta, +// but is able to be serialized/deserialized to/from JSON without custom +// MarshalJSON() and UnmarshalJSON() methods. +type StateDeltaSerializable struct { + // modified new accounts + Accts AccountDeltas + + // modified kv pairs (nil == delete) + // not preallocated use .AddKvMod to insert instead of direct assignment + KvMods map[string]KvValueDelta + + // new Txids for the txtail and TxnCounter, mapped to txn.LastValid + Txids map[string]IncludedTransactions + + // new txleases for the txtail mapped to expiration + // not pre-allocated so use .AddTxLease to insert instead of direct assignment + Txleases map[string]basics.Round + + // new creatables creator lookup table + // not pre-allocated so use .AddCreatable to insert instead of direct assignment + Creatables map[basics.CreatableIndex]ModifiedCreatable + + // new block header; read-only + Hdr bookkeeping.BlockHeader + + // StateProofNext represents modification on StateProofNextRound field in the block header. If the block contains + // a valid state proof transaction, this field will contain the next round for state proof. + // otherwise it will be set to 0. + StateProofNext basics.Round + + // previous block timestamp + PrevTimestamp int64 + + // initial hint for allocating data structures for StateDelta + initialHint int + + // The account totals reflecting the changes in this StateDelta object. + Totals AccountTotals +} + // BalanceRecord is similar to basics.BalanceRecord but with decoupled base and voting data type BalanceRecord struct { Addr basics.Address @@ -246,6 +287,94 @@ func (sd *StateDelta) Dehydrate() { } } +// ToSerializable() converts a StateDeltaSerializable to a StateDelta +func (sd StateDelta) ToSerializable() (StateDeltaSerializable, error) { + serializableTxleases := map[string]basics.Round{} + for k, v := range sd.Txleases { + json, err := json.Marshal(k) + if err != nil { + return StateDeltaSerializable{}, err + } + serializableTxleases[string(json)] = v + } + serializableTxids := map[string]IncludedTransactions{} + for k, v := range sd.Txids { + json := k.String() + serializableTxids[string(json)] = v + } + return StateDeltaSerializable{ + Accts: sd.Accts, + KvMods: sd.KvMods, + Txids: serializableTxids, + Txleases: serializableTxleases, + Creatables: sd.Creatables, + Hdr: *sd.Hdr, + StateProofNext: sd.StateProofNext, + PrevTimestamp: sd.PrevTimestamp, + initialHint: sd.initialHint, + Totals: sd.Totals, + }, nil +} + +// MarshalJSON() encodes a StateDelta into JSON +func (sd StateDelta) MarshalJSON() ([]byte, error) { + serializable, err := sd.ToSerializable() + if err != nil { + return nil, err + } + serialized, err := json.Marshal(serializable) + return serialized, err +} + +// UnmarshalJSON() converts JSON into a StateDelta +func (sd *StateDelta) UnmarshalJSON(data []byte) error { + var serializable StateDeltaSerializable + err := json.Unmarshal(data, &serializable) + if err != nil { + return err + } + nonSerializable, err := serializable.ToNonSerializable() + if err != nil { + return err + } + *sd = nonSerializable + return nil +} + +// ToNonSerializable() converts a StateDeltaSerializable to a StateDelta +func (sd StateDeltaSerializable) ToNonSerializable() (StateDelta, error) { + nonSerializableTxleases := map[Txlease]basics.Round{} + for k, v := range sd.Txleases { + var txlease Txlease + err := json.Unmarshal([]byte(k), &txlease) + if err != nil { + return StateDelta{}, err + } + nonSerializableTxleases[txlease] = v + } + nonSerializableTxids := map[transactions.Txid]IncludedTransactions{} + for k, v := range sd.Txids { + var txid transactions.Txid + err := txid.UnmarshalText([]byte(k)) + if err != nil { + return StateDelta{}, err + } + nonSerializableTxids[txid] = v + } + return StateDelta{ + Accts: sd.Accts, + KvMods: sd.KvMods, + Txids: nonSerializableTxids, + Txleases: nonSerializableTxleases, + Creatables: sd.Creatables, + Hdr: &sd.Hdr, + StateProofNext: sd.StateProofNext, + PrevTimestamp: sd.PrevTimestamp, + initialHint: sd.initialHint, + Totals: sd.Totals, + }, nil +} + // MakeAccountDeltas creates account delta // if adding new fields make sure to add them to the .reset() and .isEmpty() methods func MakeAccountDeltas(hint int) AccountDeltas { diff --git a/ledger/ledgercore/statedelta_test.go b/ledger/ledgercore/statedelta_test.go index 1e2efabe08..f4a726f8f8 100644 --- a/ledger/ledgercore/statedelta_test.go +++ b/ledger/ledgercore/statedelta_test.go @@ -17,8 +17,10 @@ package ledgercore import ( + "encoding/json" "reflect" "testing" + "fmt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -539,6 +541,50 @@ func TestStateDeltaReflect(t *testing.T) { } } +func TestStateDeltaJSON(t *testing.T) { + partitiontest.PartitionTest(t) + sd := StateDelta{ + Accts: AccountDeltas{ + Accts: []BalanceRecord{}, + AppResources: []AppResourceRecord{}, + AssetResources: []AssetResourceRecord{}, + }, + KvMods: map[string]KvValueDelta{ + "123": KvValueDelta{ + Data: []byte("abc"), + OldData: []byte("xyz"), + }, + }, + Txids: map[transactions.Txid]IncludedTransactions{ + transactions.Txid{}: IncludedTransactions { + }, + }, + Txleases: map[Txlease]basics.Round{ + Txlease{ + Sender: basics.Address{}, + Lease: [32]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}, + }: basics.Round(123), + }, + Creatables: map[basics.CreatableIndex]ModifiedCreatable{}, + Hdr: &bookkeeping.BlockHeader{}, + StateProofNext: basics.Round(123), + PrevTimestamp: 123, + initialHint: 0, // Ignore initialHint as it's not exported + Totals: AccountTotals{}, + } + encoded, err := json.Marshal(sd) + if err != nil { + panic(err) + } + fmt.Println(string(encoded)) + var decoded StateDelta + err = json.Unmarshal(encoded, &decoded) + if err != nil { + panic(err) + } + assert.Equal(t, sd, decoded) +} + func TestAccountDeltaReflect(t *testing.T) { partitiontest.PartitionTest(t)