diff --git a/crypto/onetimesig_test.go b/crypto/onetimesig_test.go index d9eec123d3..96a412f7ea 100644 --- a/crypto/onetimesig_test.go +++ b/crypto/onetimesig_test.go @@ -20,6 +20,7 @@ import ( "fmt" "testing" + "github.com/algorand/go-algorand/data/basics/testing/roundtrip" "github.com/algorand/go-algorand/test/partitiontest" ) @@ -140,6 +141,17 @@ func testOneTimeSignVerifyNewStyle(t *testing.T, c *OneTimeSignatureSecrets, c2 } } +func TestHeartbeatProofRoundTrip(t *testing.T) { + partitiontest.PartitionTest(t) + + toOTS := func(h HeartbeatProof) OneTimeSignature { return h.ToOneTimeSignature() } + toProof := func(ots OneTimeSignature) HeartbeatProof { return ots.ToHeartbeatProof() } + + // Test with an empty proof as example, NearZeros will test each field + var emptyProof HeartbeatProof + roundtrip.Check(t, emptyProof, toOTS, toProof) +} + func BenchmarkOneTimeSigBatchVerification(b *testing.B) { for _, enabled := range []bool{false, true} { b.Run(fmt.Sprintf("batch=%v", enabled), func(b *testing.B) { diff --git a/daemon/algod/api/server/v2/account_test.go b/daemon/algod/api/server/v2/account_test.go index e58990ada0..7ca873b5b0 100644 --- a/daemon/algod/api/server/v2/account_test.go +++ b/daemon/algod/api/server/v2/account_test.go @@ -25,11 +25,31 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated/model" "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/basics/testing/roundtrip" ledgertesting "github.com/algorand/go-algorand/ledger/testing" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" ) +// makeAccountConverters creates conversion functions for round-trip testing between +// basics.AccountData and model.Account. +func makeAccountConverters(t *testing.T, addrStr string, round basics.Round, proto *config.ConsensusParams, withoutRewards basics.MicroAlgos) ( + toModel func(basics.AccountData) model.Account, + toBasics func(model.Account) basics.AccountData, +) { + toModel = func(ad basics.AccountData) model.Account { + converted, err := AccountDataToAccount(addrStr, &ad, round, proto, withoutRewards) + require.NoError(t, err) + return converted + } + toBasics = func(acc model.Account) basics.AccountData { + converted, err := AccountToAccountData(&acc) + require.NoError(t, err) + return converted + } + return toModel, toBasics +} + func TestAccount(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() @@ -106,8 +126,9 @@ func TestAccount(t *testing.T) { b := a.WithUpdatedRewards(proto.RewardUnit, 100) addr := basics.Address{}.String() - conv, err := AccountDataToAccount(addr, &b, round, &proto, a.MicroAlgos) - require.NoError(t, err) + toModel, toBasics := makeAccountConverters(t, addr, round, &proto, a.MicroAlgos) + + conv := toModel(b) require.Equal(t, addr, conv.Address) require.Equal(t, b.MicroAlgos.Raw, conv.Amount) require.Equal(t, a.MicroAlgos.Raw, conv.AmountWithoutPendingRewards) @@ -145,6 +166,21 @@ func TestAccount(t *testing.T) { verifyCreatedApp(0, appIdx1, appParams1) verifyCreatedApp(1, appIdx2, appParams2) + appRoundTrip := func(idx basics.AppIndex, params basics.AppParams) { + roundtrip.Check(t, params, + func(ap basics.AppParams) model.Application { + return AppParamsToApplication(addr, idx, &ap) + }, + func(app model.Application) basics.AppParams { + converted, err := ApplicationParamsToAppParams(&app.Params) + require.NoError(t, err) + return converted + }) + } + + appRoundTrip(appIdx1, appParams1) + appRoundTrip(appIdx2, appParams2) + makeTKV := func(k string, v interface{}) model.TealKeyValue { value := model.TealValue{} switch v.(type) { @@ -198,8 +234,8 @@ func TestAccount(t *testing.T) { verifyCreatedAsset(0, assetIdx1, assetParams1) verifyCreatedAsset(1, assetIdx2, assetParams2) - c, err := AccountToAccountData(&conv) - require.NoError(t, err) + // Verify round-trip conversion works for the manually constructed account + c := toBasics(toModel(b)) require.Equal(t, b, c) t.Run("IsDeterministic", func(t *testing.T) { @@ -223,11 +259,92 @@ func TestAccountRandomRoundTrip(t *testing.T) { for addr, acct := range accts { round := basics.Round(2) proto := config.Consensus[protocol.ConsensusFuture] - conv, err := AccountDataToAccount(addr.String(), &acct, round, &proto, acct.MicroAlgos) - require.NoError(t, err) - c, err := AccountToAccountData(&conv) - require.NoError(t, err) + toModel, toBasics := makeAccountConverters(t, addr.String(), round, &proto, acct.MicroAlgos) + // Test the randomly-generated account round-trips correctly + c := toBasics(toModel(acct)) require.Equal(t, acct, c) } } } + +func TestConvertTealKeyValueRoundTrip(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + t.Run("nil input", func(t *testing.T) { + require.Nil(t, convertTKVToGenerated(nil)) + result, err := convertGeneratedTKV(nil) + require.NoError(t, err) + require.Nil(t, result) + }) + + t.Run("empty map treated as nil", func(t *testing.T) { + empty := basics.TealKeyValue{} + require.Nil(t, convertTKVToGenerated(&empty)) + result, err := convertGeneratedTKV(convertTKVToGenerated(&empty)) + require.NoError(t, err) + require.Nil(t, result) + }) + + t.Run("round-trip non-empty map", func(t *testing.T) { + kv := basics.TealKeyValue{ + "alpha": {Type: basics.TealUintType, Uint: 17}, + "beta": {Type: basics.TealBytesType, Bytes: "\x00\x01binary"}, + } + + toGenerated := func(val basics.TealKeyValue) *model.TealKeyValueStore { + return convertTKVToGenerated(&val) + } + toBasics := func(store *model.TealKeyValueStore) basics.TealKeyValue { + converted, err := convertGeneratedTKV(store) + require.NoError(t, err) + return converted + } + + // Test the manually constructed map round-trips correctly + result := toBasics(toGenerated(kv)) + require.Equal(t, kv, result) + }) +} + +func TestAppLocalStateRoundTrip(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + appIdx := basics.AppIndex(42) + cases := map[string]basics.AppLocalState{ + "empty kv": { + Schema: basics.StateSchema{NumUint: 1, NumByteSlice: 0}, + KeyValue: nil, + }, + "mixed kv": { + Schema: basics.StateSchema{NumUint: 2, NumByteSlice: 3}, + KeyValue: basics.TealKeyValue{ + "counter": {Type: basics.TealUintType, Uint: 99}, + "note": {Type: basics.TealBytesType, Bytes: "hello world"}, + }, + }, + } + + for name, state := range cases { + t.Run(name, func(t *testing.T) { + modelState := AppLocalState(state, appIdx) + modelStates := []model.ApplicationLocalState{modelState} + + acc := model.Account{ + Status: basics.Offline.String(), + Amount: 0, + AppsLocalState: &modelStates, + } + + ad, err := AccountToAccountData(&acc) + require.NoError(t, err) + + require.NotNil(t, ad.AppLocalStates) + got, ok := ad.AppLocalStates[appIdx] + require.True(t, ok) + require.Equal(t, state.Schema, got.Schema) + require.Equal(t, state.KeyValue, got.KeyValue) + }) + } +} diff --git a/daemon/algod/api/server/v2/dryrun_test.go b/daemon/algod/api/server/v2/dryrun_test.go index 8bd7addc92..de5d15c826 100644 --- a/daemon/algod/api/server/v2/dryrun_test.go +++ b/daemon/algod/api/server/v2/dryrun_test.go @@ -1087,6 +1087,32 @@ func TestStateDeltaToStateDelta(t *testing.T) { gsd := globalDeltaToStateDelta(sd) require.Equal(t, 3, len(gsd)) + decode := func(msd model.StateDelta) basics.StateDelta { + if len(msd) == 0 { + return nil + } + result := make(basics.StateDelta, len(msd)) + for _, kv := range msd { + keyBytes, err := base64.StdEncoding.DecodeString(kv.Key) + require.NoError(t, err) + vd := basics.ValueDelta{Action: basics.DeltaAction(kv.Value.Action)} + if kv.Value.Bytes != nil { + decoded, err := base64.StdEncoding.DecodeString(*kv.Value.Bytes) + require.NoError(t, err) + vd.Bytes = string(decoded) + } + if kv.Value.Uint != nil { + vd.Uint = *kv.Value.Uint + } + result[string(keyBytes)] = vd + } + return result + } + + // Test the manually constructed StateDelta round-trips correctly + result := decode(globalDeltaToStateDelta(sd)) + require.Equal(t, sd, result) + var keys []string // test with a loop because sd is a map and iteration order is random for _, item := range gsd { diff --git a/data/basics/teal_test.go b/data/basics/teal_test.go index 50b0501e49..792913d231 100644 --- a/data/basics/teal_test.go +++ b/data/basics/teal_test.go @@ -20,10 +20,36 @@ import ( "testing" "github.com/stretchr/testify/require" + "pgregory.net/rapid" "github.com/algorand/go-algorand/test/partitiontest" ) +// genTealValue generates a valid TealValue with proper Type/field correspondence. +func genTealValue() *rapid.Generator[TealValue] { + return rapid.Custom(func(t *rapid.T) TealValue { + tealType := rapid.OneOf(rapid.Just(TealUintType), rapid.Just(TealBytesType)).Draw(t, "type") + + if tealType == TealUintType { + return TealValue{Type: TealUintType, Uint: rapid.Uint64().Draw(t, "uint")} + } + return TealValue{Type: TealBytesType, Bytes: rapid.String().Draw(t, "bytes")} + }) +} + +// genValueDelta generates a valid ValueDelta with proper Action/field correspondence. +// Note: DeleteAction is excluded as it doesn't round-trip to TealValue. +func genValueDelta() *rapid.Generator[ValueDelta] { + return rapid.Custom(func(t *rapid.T) ValueDelta { + action := rapid.OneOf(rapid.Just(SetUintAction), rapid.Just(SetBytesAction)).Draw(t, "action") + + if action == SetUintAction { + return ValueDelta{Action: SetUintAction, Uint: rapid.Uint64().Draw(t, "uint")} + } + return ValueDelta{Action: SetBytesAction, Bytes: rapid.String().Draw(t, "bytes")} + }) +} + func TestStateDeltaEqual(t *testing.T) { partitiontest.PartitionTest(t) @@ -60,3 +86,45 @@ func TestStateDeltaEqual(t *testing.T) { d2 = StateDelta{"test": {Action: SetBytesAction, Bytes: "val1"}} a.False(d1.Equal(d2)) } + +func TestTealValueRoundTrip(t *testing.T) { + partitiontest.PartitionTest(t) + + // Test with a simple example value + example := TealValue{Type: TealUintType, Uint: 17} + + // TealValue has constraints (Type determines valid fields), so test the specific example + toVD := func(tv TealValue) ValueDelta { return tv.ToValueDelta() } + toTV := func(vd ValueDelta) TealValue { + tv, ok := vd.ToTealValue() + require.True(t, ok) + return tv + } + result := toTV(toVD(example)) + require.Equal(t, example, result) +} + +func TestValueDeltaRoundTrip(t *testing.T) { + partitiontest.PartitionTest(t) + + // Test with a simple example value + example := ValueDelta{Action: SetUintAction, Uint: 42} + + // ValueDelta has constraints (Action determines valid fields), so test the specific example + toTV := func(vd ValueDelta) TealValue { + tv, ok := vd.ToTealValue() + require.True(t, ok) + return tv + } + toVD := func(tv TealValue) ValueDelta { return tv.ToValueDelta() } + result := toVD(toTV(example)) + require.Equal(t, example, result) +} + +func TestValueDeltaDeleteDoesNotRoundTrip(t *testing.T) { + partitiontest.PartitionTest(t) + + vd := ValueDelta{Action: DeleteAction} + _, ok := vd.ToTealValue() + require.False(t, ok) +} diff --git a/data/basics/testing/copiers.go b/data/basics/testing/copiers.go deleted file mode 100644 index e2df453e6e..0000000000 --- a/data/basics/testing/copiers.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (C) 2019-2025 Algorand, Inc. -// This file is part of go-algorand -// -// go-algorand is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// go-algorand is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with go-algorand. If not, see . - -package testing - -import ( - "reflect" - "testing" -) - -// RoundTrip checks that converting an A -> B -> A gives the original value. -// Returns true if equal, false otherwise. -func RoundTrip[A any, B any](t *testing.T, a A, toB func(A) B, toA func(B) A) bool { - b := toB(a) - a2 := toA(b) - return reflect.DeepEqual(a, a2) -} diff --git a/data/basics/testing/nearzero.go b/data/basics/testing/roundtrip/nearzero.go similarity index 99% rename from data/basics/testing/nearzero.go rename to data/basics/testing/roundtrip/nearzero.go index 0482797e94..0607172c70 100644 --- a/data/basics/testing/nearzero.go +++ b/data/basics/testing/roundtrip/nearzero.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with go-algorand. If not, see . -package testing +package roundtrip import ( "reflect" diff --git a/data/basics/testing/nearzero_test.go b/data/basics/testing/roundtrip/nearzero_test.go similarity index 99% rename from data/basics/testing/nearzero_test.go rename to data/basics/testing/roundtrip/nearzero_test.go index f1aaf32058..b8c71f1ad0 100644 --- a/data/basics/testing/nearzero_test.go +++ b/data/basics/testing/roundtrip/nearzero_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with go-algorand. If not, see . -package testing +package roundtrip import ( "testing" diff --git a/data/basics/testing/roundtrip/roundtrip.go b/data/basics/testing/roundtrip/roundtrip.go new file mode 100644 index 0000000000..c1ccd7e15e --- /dev/null +++ b/data/basics/testing/roundtrip/roundtrip.go @@ -0,0 +1,54 @@ +// Copyright (C) 2019-2025 Algorand, Inc. +// This file is part of go-algorand +// +// go-algorand is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// go-algorand is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with go-algorand. If not, see . + +package roundtrip + +import ( + "reflect" + "testing" +) + +// Check verifies that converting from A -> B -> A yields the original value. +// It tests the provided example value, then tests all NearZeros variants (setting one field at a time). +// NearZeros is tested first because failures clearly identify which field is problematic. +func Check[A any, B any](t *testing.T, example A, toB func(A) B, toA func(B) A) { + t.Helper() + + // Test the provided example + if !checkOne(t, example, toB, toA) { + t.Fatalf("Round-trip failed for provided example: %+v", example) + } + + // Test NearZeros (one test per field) - comprehensive and deterministic + // This comes first because failures clearly show which field is the problem + nearZeroValues := NearZeros(t, example) + for i, nzA := range nearZeroValues { + if !checkOne(t, nzA, toB, toA) { + t.Fatalf("Round-trip failed for NearZero variant %d: %+v", i, nzA) + } + } +} + +func checkOne[A any, B any](t *testing.T, a A, toB func(A) B, toA func(B) A) bool { + t.Helper() + b := toB(a) + a2 := toA(b) + if !reflect.DeepEqual(a, a2) { + t.Logf("Round-trip mismatch:\n Original: %+v\n After: %+v", a, a2) + return false + } + return true +} diff --git a/data/transactions/application_test.go b/data/transactions/application_test.go index 223c7a4ae8..d48105ce97 100644 --- a/data/transactions/application_test.go +++ b/data/transactions/application_test.go @@ -27,7 +27,7 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/data/basics" - basics_testing "github.com/algorand/go-algorand/data/basics/testing" + "github.com/algorand/go-algorand/data/basics/testing/roundtrip" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" ) @@ -37,7 +37,7 @@ func TestResourceRefEmpty(t *testing.T) { t.Parallel() assert.True(t, ResourceRef{}.Empty()) - for _, rr := range basics_testing.NearZeros(t, ResourceRef{}) { + for _, rr := range roundtrip.NearZeros(t, ResourceRef{}) { assert.False(t, rr.Empty(), "Empty is disregarding a non-zero field in %+v", rr) } } @@ -51,7 +51,7 @@ func TestApplicationCallFieldsEmpty(t *testing.T) { ac := ApplicationCallTxnFields{} a.True(ac.Empty()) - for _, fields := range basics_testing.NearZeros(t, ac) { + for _, fields := range roundtrip.NearZeros(t, ac) { a.False(fields.Empty(), "Empty is disregarding a non-zero field in %+v", fields) } } diff --git a/data/transactions/transaction_test.go b/data/transactions/transaction_test.go index 1d9f04d5f0..f214ca16df 100644 --- a/data/transactions/transaction_test.go +++ b/data/transactions/transaction_test.go @@ -25,7 +25,7 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" - basics_testing "github.com/algorand/go-algorand/data/basics/testing" + "github.com/algorand/go-algorand/data/basics/testing/roundtrip" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" ) @@ -124,7 +124,7 @@ func TestApplyDataEquality(t *testing.T) { t.Parallel() var empty ApplyData - for _, ad := range basics_testing.NearZeros(t, ApplyData{}) { + for _, ad := range roundtrip.NearZeros(t, ApplyData{}) { assert.False(t, ad.Equal(empty), "Equal() seems to be disregarding something %+v", ad) } @@ -135,7 +135,7 @@ func TestEvalDataEquality(t *testing.T) { t.Parallel() var empty EvalDelta - for _, ed := range basics_testing.NearZeros(t, EvalDelta{}) { + for _, ed := range roundtrip.NearZeros(t, EvalDelta{}) { assert.False(t, ed.Equal(empty), "Equal() seems to be disregarding something %+v", ed) } @@ -146,7 +146,7 @@ func TestLogicSigEquality(t *testing.T) { t.Parallel() var empty LogicSig - for _, ls := range basics_testing.NearZeros(t, LogicSig{}) { + for _, ls := range roundtrip.NearZeros(t, LogicSig{}) { assert.False(t, ls.Equal(&empty), "Equal() seems to be disregarding something %+v", ls) } diff --git a/ledger/store/trackerdb/data_test.go b/ledger/store/trackerdb/data_test.go index 4524169b89..471be82ecf 100644 --- a/ledger/store/trackerdb/data_test.go +++ b/ledger/store/trackerdb/data_test.go @@ -26,7 +26,7 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" - basics_testing "github.com/algorand/go-algorand/data/basics/testing" + "github.com/algorand/go-algorand/data/basics/testing/roundtrip" "github.com/algorand/go-algorand/ledger/ledgercore" ledgertesting "github.com/algorand/go-algorand/ledger/testing" "github.com/algorand/go-algorand/protocol" @@ -1291,9 +1291,8 @@ func TestCopyFunctions(t *testing.T) { rdToAsset := func(rd ResourcesData) basics.AssetParams { return rd.GetAssetParams() } - for _, nz := range basics_testing.NearZeros(t, basics.AssetParams{}) { - assert.True(t, basics_testing.RoundTrip(t, nz, assetToRD, rdToAsset), nz) - } + // roundtrip.Check automatically tests the example plus NearZeros variants + roundtrip.Check(t, basics.AssetParams{}, assetToRD, rdToAsset) // Asset holdings are copied into and out of ResourceData losslessly holdingToRD := func(ap basics.AssetHolding) ResourcesData { @@ -1304,9 +1303,7 @@ func TestCopyFunctions(t *testing.T) { rdToHolding := func(rd ResourcesData) basics.AssetHolding { return rd.GetAssetHolding() } - for _, nz := range basics_testing.NearZeros(t, basics.AssetHolding{}) { - assert.True(t, basics_testing.RoundTrip(t, nz, holdingToRD, rdToHolding), nz) - } + roundtrip.Check(t, basics.AssetHolding{}, holdingToRD, rdToHolding) // AppParams are copied into and out of ResourceData losslessly apToRD := func(ap basics.AppParams) ResourcesData { @@ -1317,9 +1314,7 @@ func TestCopyFunctions(t *testing.T) { rdToAP := func(rd ResourcesData) basics.AppParams { return rd.GetAppParams() } - for _, nz := range basics_testing.NearZeros(t, basics.AppParams{}) { - assert.True(t, basics_testing.RoundTrip(t, nz, apToRD, rdToAP), nz) - } + roundtrip.Check(t, basics.AppParams{}, apToRD, rdToAP) // AppLocalStates are copied into and out of ResourceData losslessly localsToRD := func(ap basics.AppLocalState) ResourcesData { @@ -1330,9 +1325,7 @@ func TestCopyFunctions(t *testing.T) { rdToLocals := func(rd ResourcesData) basics.AppLocalState { return rd.GetAppLocalState() } - for _, nz := range basics_testing.NearZeros(t, basics.AppLocalState{}) { - assert.True(t, basics_testing.RoundTrip(t, nz, localsToRD, rdToLocals), nz) - } + roundtrip.Check(t, basics.AppLocalState{}, localsToRD, rdToLocals) } @@ -1340,7 +1333,7 @@ func TestIsEmptyAppFields(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() - for _, nz := range basics_testing.NearZeros(t, basics.AppParams{}) { + for _, nz := range roundtrip.NearZeros(t, basics.AppParams{}) { var rd ResourcesData rd.SetAppParams(nz, false) assert.False(t, rd.IsEmptyAppFields(), nz) diff --git a/network/msgOfInterest_test.go b/network/msgOfInterest_test.go index c596781d64..94cbcb904f 100644 --- a/network/msgOfInterest_test.go +++ b/network/msgOfInterest_test.go @@ -17,6 +17,7 @@ package network import ( + "maps" "testing" "github.com/stretchr/testify/require" @@ -70,3 +71,22 @@ func TestMarshallMessageOfInterest(t *testing.T) { require.Equal(t, tags[protocol.AgreementVoteTag], true) require.Equal(t, 1, len(tags)) } + +func TestDefaultSendMessageTagsMarshalRoundTrip(t *testing.T) { + partitiontest.PartitionTest(t) + + cloned := maps.Clone(defaultSendMessageTags) + + toBytes := func(tags map[protocol.Tag]bool) []byte { + return marshallMessageOfInterestMap(tags) + } + toTags := func(data []byte) map[protocol.Tag]bool { + tags, err := unmarshallMessageOfInterest(data) + require.NoError(t, err) + return tags + } + + // Test that default messages of interest round-trip correctly + result := toTags(toBytes(cloned)) + require.Equal(t, cloned, result, "default messages of interest should round-trip") +}