From 87f2033e060c5ba15b2b32e94453efc80db71fbf Mon Sep 17 00:00:00 2001 From: Aurora Gaffney Date: Thu, 16 Jan 2025 15:43:26 -0500 Subject: [PATCH] feat: nonce calculation This adds support for calculating the rolling and epoch nonce values. It also improves parsing of the extra entropy value from the Shelley genesis config --- ledger/common/nonce.go | 113 +++++++++++++++++++ ledger/common/nonce_test.go | 200 +++++++++++++++++++++++++++++++++ ledger/common/pparams.go | 38 ------- ledger/common/pparams_test.go | 57 ---------- ledger/shelley/genesis.go | 6 +- ledger/shelley/genesis_test.go | 4 +- ledger/shelley/pparams.go | 9 +- 7 files changed, 322 insertions(+), 105 deletions(-) create mode 100644 ledger/common/nonce.go create mode 100644 ledger/common/nonce_test.go delete mode 100644 ledger/common/pparams_test.go diff --git a/ledger/common/nonce.go b/ledger/common/nonce.go new file mode 100644 index 00000000..1efc721d --- /dev/null +++ b/ledger/common/nonce.go @@ -0,0 +1,113 @@ +// Copyright 2024 Blink Labs Software +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package common + +import ( + "encoding/json" + "fmt" + "slices" + + "github.com/blinklabs-io/gouroboros/cbor" +) + +const ( + NonceTypeNeutral = 0 + NonceTypeNonce = 1 +) + +type Nonce struct { + cbor.StructAsArray + Type uint + Value [32]byte +} + +func (n *Nonce) UnmarshalCBOR(data []byte) error { + nonceType, err := cbor.DecodeIdFromList(data) + if err != nil { + return err + } + + n.Type = uint(nonceType) + + switch nonceType { + case NonceTypeNeutral: + // Value uses default value + case NonceTypeNonce: + if err := cbor.DecodeGeneric(data, n); err != nil { + return err + } + default: + return fmt.Errorf("unsupported nonce type %d", nonceType) + } + return nil +} + +func (n *Nonce) UnmarshalJSON(data []byte) error { + var tmpData map[string]string + if err := json.Unmarshal(data, &tmpData); err != nil { + return err + } + tag, ok := tmpData["tag"] + if !ok { + return fmt.Errorf("did not find expected key 'tag' for nonce") + } + switch tag { + case "NeutralNonce": + n.Type = NonceTypeNeutral + default: + return fmt.Errorf("unsupported nonce tag: %s", tag) + } + return nil +} + +func (n *Nonce) MarshalCBOR() ([]byte, error) { + var tmpData []any + switch n.Type { + case NonceTypeNeutral: + tmpData = []any{NonceTypeNeutral} + case NonceTypeNonce: + tmpData = []any{NonceTypeNonce, n.Value} + } + return cbor.Encode(tmpData) +} + +// CalculateRollingNonce calculates a rolling nonce (eta_v) value from the previous block's eta_v value and the current +// block's VRF result +func CalculateRollingNonce(prevBlockNonce []byte, blockVrf []byte) (Blake2b256, error) { + if len(blockVrf) != 32 && len(blockVrf) != 64 { + return Blake2b256{}, fmt.Errorf("invalid block VRF length: %d, expected 32 or 64", len(blockVrf)) + } + blockVrfHash := Blake2b256Hash(blockVrf) + tmpData := slices.Concat(prevBlockNonce, blockVrfHash.Bytes()) + return Blake2b256Hash(tmpData), nil +} + +// CalculateEpochNonce calculates an epoch nonce from the rolling nonce (eta_v) value of the block immediately before the stability +// window and the block hash of the first block from the previous epoch. +func CalculateEpochNonce(stableBlockNonce []byte, prevEpochFirstBlockHash []byte, extraEntropy []byte) (Blake2b256, error) { + tmpData := slices.Concat( + stableBlockNonce, + prevEpochFirstBlockHash, + ) + tmpDataHash := Blake2b256Hash(tmpData) + if len(extraEntropy) > 0 { + tmpData2 := slices.Concat( + tmpDataHash.Bytes(), + extraEntropy, + ) + tmpDataHash = Blake2b256Hash(tmpData2) + } + return tmpDataHash, nil +} diff --git a/ledger/common/nonce_test.go b/ledger/common/nonce_test.go new file mode 100644 index 00000000..60e6337f --- /dev/null +++ b/ledger/common/nonce_test.go @@ -0,0 +1,200 @@ +// Copyright 2024 Blink Labs Software +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package common_test + +import ( + "encoding/hex" + "testing" + + "github.com/blinklabs-io/gouroboros/ledger/common" +) + +func TestNonceUnmarshalCBOR(t *testing.T) { + testCases := []struct { + name string + data []byte + expectedErr string + }{ + { + name: "NonceTypeNeutral", + data: []byte{0x81, 0x00}, + }, + { + name: "NonceTypeNonce", + data: []byte{0x82, 0x01, 0x42, 0x01, 0x02}, + }, + { + name: "UnsupportedNonceType", + data: []byte{0x82, 0x02}, + expectedErr: "unsupported nonce type 2", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + n := &common.Nonce{} + err := n.UnmarshalCBOR(tc.data) + if err != nil { + if tc.expectedErr == "" || err.Error() != tc.expectedErr { + t.Errorf("unexpected error: %v", err) + } + } else if tc.expectedErr != "" { + t.Errorf("expected error: %v, got nil", tc.expectedErr) + } + }) + } +} + +func TestCalculateRollingNonce(t *testing.T) { + testDefs := []struct { + prevBlockNonce string + blockVrf string + expectedNonce string + }{ + { + // Shelley genesis hash (mainnet) + prevBlockNonce: "1a3be38bcbb7911969283716ad7aa550250226b76a61fc51cc9a9a35d9276d81", + blockVrf: "36ec5378d1f5041a59eb8d96e61de96f0950fb41b49ff511f7bc7fd109d4383e1d24be7034e6749c6612700dd5ceb0c66577b88a19ae286b1321d15bce1ab736", + expectedNonce: "2af15f57076a8ff225746624882a77c8d2736fe41d3db70154a22b50af851246", + }, + { + blockVrf: "e0bf34a6b73481302f22987cde4c12807cbc2c3fea3f7fcb77261385a50e8ccdda3226db3efff73e9fb15eecf841bbc85ce37550de0435ebcdcb205e0ed08467", + expectedNonce: "a815ff978369b57df09b0072485c26920dc0ec8e924a852a42f0715981cf0042", + }, + { + blockVrf: "7107ef8c16058b09f4489715297e55d145a45fc0df75dfb419cab079cd28992854a034ad9dc4c764544fb70badd30a9611a942a03523c6f3d8967cf680c4ca6b", + expectedNonce: "f112d91435b911b6b5acaf27198762905b1cdec8c5a7b712f925ce3c5c76bb5f", + }, + { + blockVrf: "6f561aad83884ee0d7b19fd3d757c6af096bfd085465d1290b13a9dfc817dfcdfb0b59ca06300206c64d1ba75fd222a88ea03c54fbbd5d320b4fbcf1c228ba4e", + expectedNonce: "5450d95d9be4194a0ded40fbb4036b48d1f1d6da796e933fefd2c5c888794b4b", + }, + { + blockVrf: "3d3ba80724db0a028783afa56a85d684ee778ae45b9aa9af3120f5e1847be1983bd4868caf97fcfd82d5a3b0b7c1a6d53491d75440a75198014eb4e707785cad", + expectedNonce: "c5c0f406cb522ad3fead4ecc60bce9c31e80879bc17eb1bb9acaa9b998cdf8bf", + }, + { + blockVrf: "0b07976bc04321c2e7ba0f1acb3c61bd92b5fc780a855632e30e6746ab4ac4081490d816928762debd3e512d22ad512a558612adc569718df1784261f5c26aff", + expectedNonce: "5857048c728580549de645e087ba20ef20bb7c51cc84b5bc89df6b8b0ed98c41", + }, + { + blockVrf: "5e9e001fb1e2ddb0dc7ff40af917ecf4ba9892491d4bcbf2c81db2efc57627d40d7aac509c9bcf5070d4966faaeb84fd76bb285af2e51af21a8c024089f598c1", + expectedNonce: "d6f40ef403687115db061b2cb9b1ab4ddeb98222075d5a3e03c8d217d4d7c40e", + }, + { + blockVrf: "182e83f8c67ad2e6bddead128e7108499ebcbc272b50c42783ef08f035aa688fecc7d15be15a90dbfe7fe5d7cd9926987b6ec12b05f2eadfe0eb6cad5130aca4", + expectedNonce: "5489d75a9f4971c1824462b5e2338609a91f121241f21fee09811bd5772ae0a8", + }, + { + blockVrf: "275e7404b2385a9d606d67d0e29f5516fb84c1c14aaaf91afa9a9b3dcdfe09075efdadbaf158cfa1e9f250cc7c691ed2db4a29288d2426bd74a371a2a4b91b57", + expectedNonce: "04716326833ecdb595153adac9566a4b39e5c16e8d02526cb4166e4099a00b1a", + }, + { + blockVrf: "0f35c7217792f8b0cbb721ae4ae5c9ae7f2869df49a3db256aacc10d23997a09e0273261b44ebbcecd6bf916f2c1cd79cf25b0c2851645d75dd0747a8f6f92f5", + expectedNonce: "39db709f50c8a279f0a94adcefb9360dbda6cdce168aed4288329a9cd53492b6", + }, + { + blockVrf: "14c28bf9b10421e9f90ffc9ab05df0dc8c8a07ffac1c51725fba7e2b7972d0769baea248f93ed0f2067d11d719c2858c62fc1d8d59927b41d4c0fbc68d805b32", + expectedNonce: "c784b8c8678e0a04748a3ad851dd7c34ed67141cd9dc0c50ceaff4df804699a7", + }, + { + blockVrf: "e4ce96fee9deb9378a107db48587438cddf8e20a69e21e5e4fbd35ef0c56530df77eba666cb152812111ba66bbd333ed44f627c727115f8f4f15b31726049a19", + expectedNonce: "cc1a5861358c075de93a26a91c5a951d5e71190d569aa2dc786d4ca8fc80cc38", + }, + { + blockVrf: "b38f315e3ce369ea2551bf4f44e723dd15c7d67ba4b3763997909f65e46267d6540b9b00a7a65ae3d1f3a3316e57a821aeaac33e4e42ded415205073134cd185", + expectedNonce: "514979c89313c49e8f59fb8445113fa7623e99375cc4917fe79df54f8d4bdfce", + }, + { + blockVrf: "4bcbf774af9c8ff24d4d96099001ec06a24802c88fea81680ea2411392d32dbd9b9828a690a462954b894708d511124a2db34ec4179841e07a897169f0f1ac0e", + expectedNonce: "6a783e04481b9e04e8f3498a3b74c90c06a1031fb663b6793ce592a6c26f56f4", + }, + { + blockVrf: "65247ace6355f978a12235265410c44f3ded02849ec8f8e6db2ac705c3f57d322ea073c13cf698e15d7e1d7f2bc95e7b3533be0dee26f58864f1664df0c1ebba", + expectedNonce: "1190f5254599dcee4f3cf1afdf4181085c36a6db6c30f334bfe6e6f320a6ed91", + }, + } + var rollingNonce []byte + for _, testDef := range testDefs { + // Populate initial nonce + if testDef.prevBlockNonce != "" { + tmpNonce, err := hex.DecodeString(testDef.prevBlockNonce) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + rollingNonce = tmpNonce + } + blockVrfBytes, err := hex.DecodeString(testDef.blockVrf) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + nonce, err := common.CalculateRollingNonce(rollingNonce, blockVrfBytes) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + nonceHex := hex.EncodeToString(nonce.Bytes()) + if nonceHex != testDef.expectedNonce { + t.Fatalf("did not get expected nonce value: got %s, wanted %s", nonceHex, testDef.expectedNonce) + } + rollingNonce = nonce.Bytes() + } +} + +func TestCalculateEpochNonce(t *testing.T) { + testDefs := []struct { + stableBlockNonce string + prevEpochFirstBlockHash string + extraEntropy string + expectedNonce string + }{ + { + stableBlockNonce: "e86e133bd48ff5e79bec43af1ac3e348b539172f33e502d2c96735e8c51bd04d", + prevEpochFirstBlockHash: "d7a1ff2a365abed59c9ae346cba842b6d3df06d055dba79a113e0704b44cc3e9", + expectedNonce: "e536a0081ddd6d19786e9d708a85819a5c3492c0da7349f59c8ad3e17e4acd98", + }, + { + stableBlockNonce: "d1340a9c1491f0face38d41fd5c82953d0eb48320d65e952414a0c5ebaf87587", + prevEpochFirstBlockHash: "ee91d679b0a6ce3015b894c575c799e971efac35c7a8cbdc2b3f579005e69abd", + extraEntropy: "d982e06fd33e7440b43cefad529b7ecafbaa255e38178ad4189a37e4ce9bf1fa", + expectedNonce: "0022cfa563a5328c4fb5c8017121329e964c26ade5d167b1bd9b2ec967772b60", + }, + } + for _, testDef := range testDefs { + stableBlockNonce, err := hex.DecodeString(testDef.stableBlockNonce) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + prevEpochFirstBlockHash, err := hex.DecodeString(testDef.prevEpochFirstBlockHash) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + var extraEntropy []byte + if testDef.extraEntropy != "" { + tmpEntropy, err := hex.DecodeString(testDef.extraEntropy) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + extraEntropy = tmpEntropy + } + tmpNonce, err := common.CalculateEpochNonce(stableBlockNonce, prevEpochFirstBlockHash, extraEntropy) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + tmpNonceHex := hex.EncodeToString(tmpNonce.Bytes()) + if tmpNonceHex != testDef.expectedNonce { + t.Fatalf("did not get expected epoch nonce: got %s, wanted %s", tmpNonceHex, testDef.expectedNonce) + } + } +} diff --git a/ledger/common/pparams.go b/ledger/common/pparams.go index d0c47895..0596e215 100644 --- a/ledger/common/pparams.go +++ b/ledger/common/pparams.go @@ -15,7 +15,6 @@ package common import ( - "fmt" "log/slog" "github.com/blinklabs-io/gouroboros/cbor" @@ -37,43 +36,6 @@ type ProtocolParameters interface { Utxorpc() *cardano.PParams } -const ( - NonceType0 = 0 - NonceType1 = 1 -) - -var NeutralNonce = Nonce{ - Type: NonceType0, -} - -type Nonce struct { - cbor.StructAsArray - Type uint - Value [32]byte -} - -func (n *Nonce) UnmarshalCBOR(data []byte) error { - nonceType, err := cbor.DecodeIdFromList(data) - if err != nil { - return err - } - - n.Type = uint(nonceType) - - switch nonceType { - case NonceType0: - // Value uses default value - case NonceType1: - if err := cbor.DecodeGeneric(data, n); err != nil { - fmt.Printf("Nonce decode error: %+v\n", data) - return err - } - default: - return fmt.Errorf("unsupported nonce type %d", nonceType) - } - return nil -} - type ExUnit struct { cbor.StructAsArray Mem uint diff --git a/ledger/common/pparams_test.go b/ledger/common/pparams_test.go deleted file mode 100644 index 95aab404..00000000 --- a/ledger/common/pparams_test.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2024 Blink Labs Software -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package common_test - -import ( - "testing" - - "github.com/blinklabs-io/gouroboros/ledger/common" -) - -func TestNonceUnmarshalCBOR(t *testing.T) { - testCases := []struct { - name string - data []byte - expectedErr string - }{ - { - name: "NonceType0", - data: []byte{0x81, 0x00}, - }, - { - name: "NonceType1", - data: []byte{0x82, 0x01, 0x42, 0x01, 0x02}, - }, - { - name: "UnsupportedNonceType", - data: []byte{0x82, 0x02}, - expectedErr: "unsupported nonce type 2", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - n := &common.Nonce{} - err := n.UnmarshalCBOR(tc.data) - if err != nil { - if tc.expectedErr == "" || err.Error() != tc.expectedErr { - t.Errorf("unexpected error: %v", err) - } - } else if tc.expectedErr != "" { - t.Errorf("expected error: %v, got nil", tc.expectedErr) - } - }) - } -} diff --git a/ledger/shelley/genesis.go b/ledger/shelley/genesis.go index 4c2a5de1..0fb2e7a5 100644 --- a/ledger/shelley/genesis.go +++ b/ledger/shelley/genesis.go @@ -113,7 +113,7 @@ type ShelleyGenesisProtocolParams struct { Rho *common.GenesisRat Tau *common.GenesisRat Decentralization *common.GenesisRat `json:"decentralisationParam"` - ExtraEntropy map[string]string + ExtraEntropy common.Nonce ProtocolVersion struct { Major uint Minor uint @@ -145,9 +145,7 @@ func (p ShelleyGenesisProtocolParams) MarshalCBOR() ([]byte, error) { cbor.Rat{ Rat: p.Decentralization.Rat, }, - []any{ - map[string]int{"NeutralNonce": 0}[p.ExtraEntropy["Tag"]], - }, + p.ExtraEntropy, p.ProtocolVersion.Major, p.ProtocolVersion.Minor, p.MinUtxoValue, diff --git a/ledger/shelley/genesis_test.go b/ledger/shelley/genesis_test.go index 3669323c..573f26d2 100644 --- a/ledger/shelley/genesis_test.go +++ b/ledger/shelley/genesis_test.go @@ -133,8 +133,8 @@ var expectedGenesisObj = shelley.ShelleyGenesis{ Rho: &common.GenesisRat{Rat: big.NewRat(3, 1000)}, Tau: &common.GenesisRat{Rat: big.NewRat(2, 10)}, Decentralization: &common.GenesisRat{Rat: new(big.Rat).SetInt64(1)}, - ExtraEntropy: map[string]string{ - "tag": "NeutralNonce", + ExtraEntropy: common.Nonce{ + Type: common.NonceTypeNeutral, }, ProtocolVersion: struct { Major uint diff --git a/ledger/shelley/pparams.go b/ledger/shelley/pparams.go index 9453968c..73e2d9be 100644 --- a/ledger/shelley/pparams.go +++ b/ledger/shelley/pparams.go @@ -37,7 +37,7 @@ type ShelleyProtocolParameters struct { Rho *cbor.Rat Tau *cbor.Rat Decentralization *cbor.Rat - Nonce *common.Nonce + ExtraEntropy common.Nonce ProtocolMajor uint ProtocolMinor uint MinUtxoValue uint @@ -89,8 +89,8 @@ func (p *ShelleyProtocolParameters) Update( p.ProtocolMajor = paramUpdate.ProtocolVersion.Major p.ProtocolMinor = paramUpdate.ProtocolVersion.Minor } - if paramUpdate.Nonce != nil { - p.Nonce = paramUpdate.Nonce + if paramUpdate.ExtraEntropy != nil { + p.ExtraEntropy = *paramUpdate.ExtraEntropy } if paramUpdate.MinUtxoValue != nil { p.MinUtxoValue = *paramUpdate.MinUtxoValue @@ -125,6 +125,7 @@ func (p *ShelleyProtocolParameters) UpdateFromGenesis(genesis *ShelleyGenesis) { Rat: new(big.Rat).Set(genesisParams.Decentralization.Rat), } } + p.ExtraEntropy = genesisParams.ExtraEntropy p.ProtocolMajor = genesisParams.ProtocolVersion.Major p.ProtocolMinor = genesisParams.ProtocolVersion.Minor p.MinUtxoValue = genesisParams.MinUtxoValue @@ -147,7 +148,7 @@ type ShelleyProtocolParameterUpdate struct { Rho *cbor.Rat `cbor:"10,keyasint"` Tau *cbor.Rat `cbor:"11,keyasint"` Decentralization *cbor.Rat `cbor:"12,keyasint"` - Nonce *common.Nonce `cbor:"13,keyasint"` + ExtraEntropy *common.Nonce `cbor:"13,keyasint"` ProtocolVersion *common.ProtocolParametersProtocolVersion `cbor:"14,keyasint"` MinUtxoValue *uint `cbor:"15,keyasint"` }