From 2d53bca12f8311fa610961c8d22a4c66562f942a Mon Sep 17 00:00:00 2001 From: Akhil Repala Date: Sun, 30 Nov 2025 15:13:28 -0600 Subject: [PATCH] feat(aws): Added string method for Voter by following CIP-129 specifications to generate bech32 encodings for each voter type Signed-off-by: Akhil Repala --- ledger/common/gov.go | 74 +++++++++++++++++++++++++++++++++++++++ ledger/common/gov_test.go | 62 ++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/ledger/common/gov.go b/ledger/common/gov.go index 025eab86..c02569e0 100644 --- a/ledger/common/gov.go +++ b/ledger/common/gov.go @@ -15,10 +15,12 @@ package common import ( + "fmt" "math/big" "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/plutigo/data" + "github.com/btcsuite/btcd/btcutil/bech32" ) // VotingProcedures is a convenience type to avoid needing to duplicate the full type definition everywhere @@ -38,6 +40,78 @@ type Voter struct { Hash [28]byte } +const ( + // CIP-129 key type identifiers and CIP-129 credential types mirror address. + cip129KeyTypeConstitutionalCommitteeHot uint8 = 0 + cip129KeyTypeDRep uint8 = 2 + cip129CredentialTypeKeyHash uint8 = 0x02 + cip129CredentialTypeScriptHash uint8 = 0x03 +) + +func encodeCip129Voter( + prefix string, + keyType uint8, + credentialType uint8, + hash []byte, +) string { + header := byte((keyType << 4) | (credentialType & 0x0f)) + data := make([]byte, 1+len(hash)) + data[0] = header + copy(data[1:], hash) + return encodeVoterBech32(prefix, data) +} + +func encodeVoterBech32(prefix string, data []byte) string { + convData, err := bech32.ConvertBits(data, 8, 5, true) + if err != nil { + panic(fmt.Sprintf("unexpected error converting voter data to base32: %s", err)) + } + encoded, err := bech32.Encode(prefix, convData) + if err != nil { + panic(fmt.Sprintf("unexpected error encoding voter data as bech32: %s", err)) + } + return encoded +} + +// Generates bech32-encoded identifier for the voter credential. +func (v Voter) String() string { + switch v.Type { + case VoterTypeConstitutionalCommitteeHotKeyHash: + return encodeCip129Voter( + "cc_hot", + cip129KeyTypeConstitutionalCommitteeHot, + cip129CredentialTypeKeyHash, + v.Hash[:], + ) + case VoterTypeConstitutionalCommitteeHotScriptHash: + return encodeCip129Voter( + "cc_hot", + cip129KeyTypeConstitutionalCommitteeHot, + cip129CredentialTypeScriptHash, + v.Hash[:], + ) + case VoterTypeDRepKeyHash: + return encodeCip129Voter( + "drep", + cip129KeyTypeDRep, + cip129CredentialTypeKeyHash, + v.Hash[:], + ) + case VoterTypeDRepScriptHash: + return encodeCip129Voter( + "drep", + cip129KeyTypeDRep, + cip129CredentialTypeScriptHash, + v.Hash[:], + ) + case VoterTypeStakingPoolKeyHash: + poolId := PoolId(v.Hash) + return poolId.String() + default: + panic(fmt.Sprintf("unknown voter type: %d", v.Type)) + } +} + func (v Voter) ToPlutusData() data.PlutusData { switch v.Type { case VoterTypeConstitutionalCommitteeHotScriptHash: diff --git a/ledger/common/gov_test.go b/ledger/common/gov_test.go index 4d421609..07a2afab 100644 --- a/ledger/common/gov_test.go +++ b/ledger/common/gov_test.go @@ -168,6 +168,68 @@ func TestVoterToPlutusData(t *testing.T) { } } +func TestVoterString(t *testing.T) { + var zeroHash [28]byte + var sequentialHash [28]byte + for i := range sequentialHash { + sequentialHash[i] = byte(i) + } + testCases := []struct { + name string + voter Voter + want string + }{ + { + name: "CIP129CcHotKeyHash", + voter: Voter{ + Type: VoterTypeConstitutionalCommitteeHotKeyHash, + Hash: zeroHash, + }, + want: "cc_hot1qgqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqvcdjk7", + }, + { + name: "CIP129CcHotScriptHash", + voter: Voter{ + Type: VoterTypeConstitutionalCommitteeHotScriptHash, + Hash: zeroHash, + }, + want: "cc_hot1qvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqv2arke", + }, + { + name: "CIP129DRepKeyHash", + voter: Voter{ + Type: VoterTypeDRepKeyHash, + Hash: zeroHash, + }, + want: "drep1ygqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq7vlc9n", + }, + { + name: "CIP129DRepScriptHash", + voter: Voter{ + Type: VoterTypeDRepScriptHash, + Hash: zeroHash, + }, + want: "drep1yvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq770f95", + }, + { + name: "StakingPoolKeyHash", + voter: Voter{ + Type: VoterTypeStakingPoolKeyHash, + Hash: sequentialHash, + }, + want: "pool1qqqsyqcyq5rqwzqfpg9scrgwpugpzysnzs23v9ccrydpk35lkuk", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, tc.voter.String()) + }) + } + assert.Panics(t, func() { + _ = Voter{Type: 99}.String() + }) +} + // Tests the ToPlutusData method for Vote types func TestVoteToPlutusData(t *testing.T) { testCases := []struct {