Skip to content

Commit 2a4efda

Browse files
authored
feat: nonce calculation (#832)
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
1 parent 87aaa8e commit 2a4efda

File tree

7 files changed

+322
-105
lines changed

7 files changed

+322
-105
lines changed

ledger/common/nonce.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright 2024 Blink Labs Software
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package common
16+
17+
import (
18+
"encoding/json"
19+
"fmt"
20+
"slices"
21+
22+
"github.com/blinklabs-io/gouroboros/cbor"
23+
)
24+
25+
const (
26+
NonceTypeNeutral = 0
27+
NonceTypeNonce = 1
28+
)
29+
30+
type Nonce struct {
31+
cbor.StructAsArray
32+
Type uint
33+
Value [32]byte
34+
}
35+
36+
func (n *Nonce) UnmarshalCBOR(data []byte) error {
37+
nonceType, err := cbor.DecodeIdFromList(data)
38+
if err != nil {
39+
return err
40+
}
41+
42+
n.Type = uint(nonceType)
43+
44+
switch nonceType {
45+
case NonceTypeNeutral:
46+
// Value uses default value
47+
case NonceTypeNonce:
48+
if err := cbor.DecodeGeneric(data, n); err != nil {
49+
return err
50+
}
51+
default:
52+
return fmt.Errorf("unsupported nonce type %d", nonceType)
53+
}
54+
return nil
55+
}
56+
57+
func (n *Nonce) UnmarshalJSON(data []byte) error {
58+
var tmpData map[string]string
59+
if err := json.Unmarshal(data, &tmpData); err != nil {
60+
return err
61+
}
62+
tag, ok := tmpData["tag"]
63+
if !ok {
64+
return fmt.Errorf("did not find expected key 'tag' for nonce")
65+
}
66+
switch tag {
67+
case "NeutralNonce":
68+
n.Type = NonceTypeNeutral
69+
default:
70+
return fmt.Errorf("unsupported nonce tag: %s", tag)
71+
}
72+
return nil
73+
}
74+
75+
func (n *Nonce) MarshalCBOR() ([]byte, error) {
76+
var tmpData []any
77+
switch n.Type {
78+
case NonceTypeNeutral:
79+
tmpData = []any{NonceTypeNeutral}
80+
case NonceTypeNonce:
81+
tmpData = []any{NonceTypeNonce, n.Value}
82+
}
83+
return cbor.Encode(tmpData)
84+
}
85+
86+
// CalculateRollingNonce calculates a rolling nonce (eta_v) value from the previous block's eta_v value and the current
87+
// block's VRF result
88+
func CalculateRollingNonce(prevBlockNonce []byte, blockVrf []byte) (Blake2b256, error) {
89+
if len(blockVrf) != 32 && len(blockVrf) != 64 {
90+
return Blake2b256{}, fmt.Errorf("invalid block VRF length: %d, expected 32 or 64", len(blockVrf))
91+
}
92+
blockVrfHash := Blake2b256Hash(blockVrf)
93+
tmpData := slices.Concat(prevBlockNonce, blockVrfHash.Bytes())
94+
return Blake2b256Hash(tmpData), nil
95+
}
96+
97+
// CalculateEpochNonce calculates an epoch nonce from the rolling nonce (eta_v) value of the block immediately before the stability
98+
// window and the block hash of the first block from the previous epoch.
99+
func CalculateEpochNonce(stableBlockNonce []byte, prevEpochFirstBlockHash []byte, extraEntropy []byte) (Blake2b256, error) {
100+
tmpData := slices.Concat(
101+
stableBlockNonce,
102+
prevEpochFirstBlockHash,
103+
)
104+
tmpDataHash := Blake2b256Hash(tmpData)
105+
if len(extraEntropy) > 0 {
106+
tmpData2 := slices.Concat(
107+
tmpDataHash.Bytes(),
108+
extraEntropy,
109+
)
110+
tmpDataHash = Blake2b256Hash(tmpData2)
111+
}
112+
return tmpDataHash, nil
113+
}

ledger/common/nonce_test.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// Copyright 2024 Blink Labs Software
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package common_test
16+
17+
import (
18+
"encoding/hex"
19+
"testing"
20+
21+
"github.com/blinklabs-io/gouroboros/ledger/common"
22+
)
23+
24+
func TestNonceUnmarshalCBOR(t *testing.T) {
25+
testCases := []struct {
26+
name string
27+
data []byte
28+
expectedErr string
29+
}{
30+
{
31+
name: "NonceTypeNeutral",
32+
data: []byte{0x81, 0x00},
33+
},
34+
{
35+
name: "NonceTypeNonce",
36+
data: []byte{0x82, 0x01, 0x42, 0x01, 0x02},
37+
},
38+
{
39+
name: "UnsupportedNonceType",
40+
data: []byte{0x82, 0x02},
41+
expectedErr: "unsupported nonce type 2",
42+
},
43+
}
44+
45+
for _, tc := range testCases {
46+
t.Run(tc.name, func(t *testing.T) {
47+
n := &common.Nonce{}
48+
err := n.UnmarshalCBOR(tc.data)
49+
if err != nil {
50+
if tc.expectedErr == "" || err.Error() != tc.expectedErr {
51+
t.Errorf("unexpected error: %v", err)
52+
}
53+
} else if tc.expectedErr != "" {
54+
t.Errorf("expected error: %v, got nil", tc.expectedErr)
55+
}
56+
})
57+
}
58+
}
59+
60+
func TestCalculateRollingNonce(t *testing.T) {
61+
testDefs := []struct {
62+
prevBlockNonce string
63+
blockVrf string
64+
expectedNonce string
65+
}{
66+
{
67+
// Shelley genesis hash (mainnet)
68+
prevBlockNonce: "1a3be38bcbb7911969283716ad7aa550250226b76a61fc51cc9a9a35d9276d81",
69+
blockVrf: "36ec5378d1f5041a59eb8d96e61de96f0950fb41b49ff511f7bc7fd109d4383e1d24be7034e6749c6612700dd5ceb0c66577b88a19ae286b1321d15bce1ab736",
70+
expectedNonce: "2af15f57076a8ff225746624882a77c8d2736fe41d3db70154a22b50af851246",
71+
},
72+
{
73+
blockVrf: "e0bf34a6b73481302f22987cde4c12807cbc2c3fea3f7fcb77261385a50e8ccdda3226db3efff73e9fb15eecf841bbc85ce37550de0435ebcdcb205e0ed08467",
74+
expectedNonce: "a815ff978369b57df09b0072485c26920dc0ec8e924a852a42f0715981cf0042",
75+
},
76+
{
77+
blockVrf: "7107ef8c16058b09f4489715297e55d145a45fc0df75dfb419cab079cd28992854a034ad9dc4c764544fb70badd30a9611a942a03523c6f3d8967cf680c4ca6b",
78+
expectedNonce: "f112d91435b911b6b5acaf27198762905b1cdec8c5a7b712f925ce3c5c76bb5f",
79+
},
80+
{
81+
blockVrf: "6f561aad83884ee0d7b19fd3d757c6af096bfd085465d1290b13a9dfc817dfcdfb0b59ca06300206c64d1ba75fd222a88ea03c54fbbd5d320b4fbcf1c228ba4e",
82+
expectedNonce: "5450d95d9be4194a0ded40fbb4036b48d1f1d6da796e933fefd2c5c888794b4b",
83+
},
84+
{
85+
blockVrf: "3d3ba80724db0a028783afa56a85d684ee778ae45b9aa9af3120f5e1847be1983bd4868caf97fcfd82d5a3b0b7c1a6d53491d75440a75198014eb4e707785cad",
86+
expectedNonce: "c5c0f406cb522ad3fead4ecc60bce9c31e80879bc17eb1bb9acaa9b998cdf8bf",
87+
},
88+
{
89+
blockVrf: "0b07976bc04321c2e7ba0f1acb3c61bd92b5fc780a855632e30e6746ab4ac4081490d816928762debd3e512d22ad512a558612adc569718df1784261f5c26aff",
90+
expectedNonce: "5857048c728580549de645e087ba20ef20bb7c51cc84b5bc89df6b8b0ed98c41",
91+
},
92+
{
93+
blockVrf: "5e9e001fb1e2ddb0dc7ff40af917ecf4ba9892491d4bcbf2c81db2efc57627d40d7aac509c9bcf5070d4966faaeb84fd76bb285af2e51af21a8c024089f598c1",
94+
expectedNonce: "d6f40ef403687115db061b2cb9b1ab4ddeb98222075d5a3e03c8d217d4d7c40e",
95+
},
96+
{
97+
blockVrf: "182e83f8c67ad2e6bddead128e7108499ebcbc272b50c42783ef08f035aa688fecc7d15be15a90dbfe7fe5d7cd9926987b6ec12b05f2eadfe0eb6cad5130aca4",
98+
expectedNonce: "5489d75a9f4971c1824462b5e2338609a91f121241f21fee09811bd5772ae0a8",
99+
},
100+
{
101+
blockVrf: "275e7404b2385a9d606d67d0e29f5516fb84c1c14aaaf91afa9a9b3dcdfe09075efdadbaf158cfa1e9f250cc7c691ed2db4a29288d2426bd74a371a2a4b91b57",
102+
expectedNonce: "04716326833ecdb595153adac9566a4b39e5c16e8d02526cb4166e4099a00b1a",
103+
},
104+
{
105+
blockVrf: "0f35c7217792f8b0cbb721ae4ae5c9ae7f2869df49a3db256aacc10d23997a09e0273261b44ebbcecd6bf916f2c1cd79cf25b0c2851645d75dd0747a8f6f92f5",
106+
expectedNonce: "39db709f50c8a279f0a94adcefb9360dbda6cdce168aed4288329a9cd53492b6",
107+
},
108+
{
109+
blockVrf: "14c28bf9b10421e9f90ffc9ab05df0dc8c8a07ffac1c51725fba7e2b7972d0769baea248f93ed0f2067d11d719c2858c62fc1d8d59927b41d4c0fbc68d805b32",
110+
expectedNonce: "c784b8c8678e0a04748a3ad851dd7c34ed67141cd9dc0c50ceaff4df804699a7",
111+
},
112+
{
113+
blockVrf: "e4ce96fee9deb9378a107db48587438cddf8e20a69e21e5e4fbd35ef0c56530df77eba666cb152812111ba66bbd333ed44f627c727115f8f4f15b31726049a19",
114+
expectedNonce: "cc1a5861358c075de93a26a91c5a951d5e71190d569aa2dc786d4ca8fc80cc38",
115+
},
116+
{
117+
blockVrf: "b38f315e3ce369ea2551bf4f44e723dd15c7d67ba4b3763997909f65e46267d6540b9b00a7a65ae3d1f3a3316e57a821aeaac33e4e42ded415205073134cd185",
118+
expectedNonce: "514979c89313c49e8f59fb8445113fa7623e99375cc4917fe79df54f8d4bdfce",
119+
},
120+
{
121+
blockVrf: "4bcbf774af9c8ff24d4d96099001ec06a24802c88fea81680ea2411392d32dbd9b9828a690a462954b894708d511124a2db34ec4179841e07a897169f0f1ac0e",
122+
expectedNonce: "6a783e04481b9e04e8f3498a3b74c90c06a1031fb663b6793ce592a6c26f56f4",
123+
},
124+
{
125+
blockVrf: "65247ace6355f978a12235265410c44f3ded02849ec8f8e6db2ac705c3f57d322ea073c13cf698e15d7e1d7f2bc95e7b3533be0dee26f58864f1664df0c1ebba",
126+
expectedNonce: "1190f5254599dcee4f3cf1afdf4181085c36a6db6c30f334bfe6e6f320a6ed91",
127+
},
128+
}
129+
var rollingNonce []byte
130+
for _, testDef := range testDefs {
131+
// Populate initial nonce
132+
if testDef.prevBlockNonce != "" {
133+
tmpNonce, err := hex.DecodeString(testDef.prevBlockNonce)
134+
if err != nil {
135+
t.Fatalf("unexpected error: %s", err)
136+
}
137+
rollingNonce = tmpNonce
138+
}
139+
blockVrfBytes, err := hex.DecodeString(testDef.blockVrf)
140+
if err != nil {
141+
t.Fatalf("unexpected error: %s", err)
142+
}
143+
nonce, err := common.CalculateRollingNonce(rollingNonce, blockVrfBytes)
144+
if err != nil {
145+
t.Fatalf("unexpected error: %s", err)
146+
}
147+
nonceHex := hex.EncodeToString(nonce.Bytes())
148+
if nonceHex != testDef.expectedNonce {
149+
t.Fatalf("did not get expected nonce value: got %s, wanted %s", nonceHex, testDef.expectedNonce)
150+
}
151+
rollingNonce = nonce.Bytes()
152+
}
153+
}
154+
155+
func TestCalculateEpochNonce(t *testing.T) {
156+
testDefs := []struct {
157+
stableBlockNonce string
158+
prevEpochFirstBlockHash string
159+
extraEntropy string
160+
expectedNonce string
161+
}{
162+
{
163+
stableBlockNonce: "e86e133bd48ff5e79bec43af1ac3e348b539172f33e502d2c96735e8c51bd04d",
164+
prevEpochFirstBlockHash: "d7a1ff2a365abed59c9ae346cba842b6d3df06d055dba79a113e0704b44cc3e9",
165+
expectedNonce: "e536a0081ddd6d19786e9d708a85819a5c3492c0da7349f59c8ad3e17e4acd98",
166+
},
167+
{
168+
stableBlockNonce: "d1340a9c1491f0face38d41fd5c82953d0eb48320d65e952414a0c5ebaf87587",
169+
prevEpochFirstBlockHash: "ee91d679b0a6ce3015b894c575c799e971efac35c7a8cbdc2b3f579005e69abd",
170+
extraEntropy: "d982e06fd33e7440b43cefad529b7ecafbaa255e38178ad4189a37e4ce9bf1fa",
171+
expectedNonce: "0022cfa563a5328c4fb5c8017121329e964c26ade5d167b1bd9b2ec967772b60",
172+
},
173+
}
174+
for _, testDef := range testDefs {
175+
stableBlockNonce, err := hex.DecodeString(testDef.stableBlockNonce)
176+
if err != nil {
177+
t.Fatalf("unexpected error: %s", err)
178+
}
179+
prevEpochFirstBlockHash, err := hex.DecodeString(testDef.prevEpochFirstBlockHash)
180+
if err != nil {
181+
t.Fatalf("unexpected error: %s", err)
182+
}
183+
var extraEntropy []byte
184+
if testDef.extraEntropy != "" {
185+
tmpEntropy, err := hex.DecodeString(testDef.extraEntropy)
186+
if err != nil {
187+
t.Fatalf("unexpected error: %s", err)
188+
}
189+
extraEntropy = tmpEntropy
190+
}
191+
tmpNonce, err := common.CalculateEpochNonce(stableBlockNonce, prevEpochFirstBlockHash, extraEntropy)
192+
if err != nil {
193+
t.Fatalf("unexpected error: %s", err)
194+
}
195+
tmpNonceHex := hex.EncodeToString(tmpNonce.Bytes())
196+
if tmpNonceHex != testDef.expectedNonce {
197+
t.Fatalf("did not get expected epoch nonce: got %s, wanted %s", tmpNonceHex, testDef.expectedNonce)
198+
}
199+
}
200+
}

ledger/common/pparams.go

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
package common
1616

1717
import (
18-
"fmt"
1918
"log/slog"
2019

2120
"github.com/blinklabs-io/gouroboros/cbor"
@@ -37,43 +36,6 @@ type ProtocolParameters interface {
3736
Utxorpc() *cardano.PParams
3837
}
3938

40-
const (
41-
NonceType0 = 0
42-
NonceType1 = 1
43-
)
44-
45-
var NeutralNonce = Nonce{
46-
Type: NonceType0,
47-
}
48-
49-
type Nonce struct {
50-
cbor.StructAsArray
51-
Type uint
52-
Value [32]byte
53-
}
54-
55-
func (n *Nonce) UnmarshalCBOR(data []byte) error {
56-
nonceType, err := cbor.DecodeIdFromList(data)
57-
if err != nil {
58-
return err
59-
}
60-
61-
n.Type = uint(nonceType)
62-
63-
switch nonceType {
64-
case NonceType0:
65-
// Value uses default value
66-
case NonceType1:
67-
if err := cbor.DecodeGeneric(data, n); err != nil {
68-
fmt.Printf("Nonce decode error: %+v\n", data)
69-
return err
70-
}
71-
default:
72-
return fmt.Errorf("unsupported nonce type %d", nonceType)
73-
}
74-
return nil
75-
}
76-
7739
type ExUnit struct {
7840
cbor.StructAsArray
7941
Mem uint

0 commit comments

Comments
 (0)