contracts/release: move package release to contracts/
This change also deletes generator.go, moving the only interesting line in it into release.go. The binding has been regenerated with abigen from develop and solc v0.3.6.
This commit is contained in:
432
contracts/release/contract.go
Normal file
432
contracts/release/contract.go
Normal file
File diff suppressed because one or more lines are too long
249
contracts/release/contract.sol
Normal file
249
contracts/release/contract.sol
Normal file
@ -0,0 +1,249 @@
|
||||
// Copyright 2016 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// ReleaseOracle is an Ethereum contract to store the current and previous
|
||||
// versions of the go-ethereum implementation. Its goal is to allow Geth to
|
||||
// check for new releases automatically without the need to consult a central
|
||||
// repository.
|
||||
//
|
||||
// The contract takes a vote based approach on both assigning authorised signers
|
||||
// as well as signing off on new Geth releases.
|
||||
//
|
||||
// Note, when a signer is demoted, the currently pending release is auto-nuked.
|
||||
// The reason is to prevent suprises where a demotion actually tilts the votes
|
||||
// in favor of one voter party and pushing out a new release as a consequence of
|
||||
// a simple demotion.
|
||||
contract ReleaseOracle {
|
||||
// Votes is an internal data structure to count votes on a specific proposal
|
||||
struct Votes {
|
||||
address[] pass; // List of signers voting to pass a proposal
|
||||
address[] fail; // List of signers voting to fail a proposal
|
||||
}
|
||||
|
||||
// Version is the version details of a particular Geth release
|
||||
struct Version {
|
||||
uint32 major; // Major version component of the release
|
||||
uint32 minor; // Minor version component of the release
|
||||
uint32 patch; // Patch version component of the release
|
||||
bytes20 commit; // Git SHA1 commit hash of the release
|
||||
|
||||
uint64 time; // Timestamp of the release approval
|
||||
Votes votes; // Votes that passed this release
|
||||
}
|
||||
|
||||
// Oracle authorization details
|
||||
mapping(address => bool) authorised; // Set of accounts allowed to vote on updating the contract
|
||||
address[] voters; // List of addresses currently accepted as signers
|
||||
|
||||
// Various proposals being voted on
|
||||
mapping(address => Votes) authProps; // Currently running user authorization proposals
|
||||
address[] authPend; // List of addresses being voted on (map indexes)
|
||||
|
||||
Version verProp; // Currently proposed release being voted on
|
||||
Version[] releases; // All the positively voted releases
|
||||
|
||||
// isSigner is a modifier to authorize contract transactions.
|
||||
modifier isSigner() {
|
||||
if (authorised[msg.sender]) {
|
||||
_
|
||||
}
|
||||
}
|
||||
|
||||
// Constructor to assign the initial set of signers.
|
||||
function ReleaseOracle(address[] signers) {
|
||||
// If no signers were specified, assign the creator as the sole signer
|
||||
if (signers.length == 0) {
|
||||
authorised[msg.sender] = true;
|
||||
voters.push(msg.sender);
|
||||
return;
|
||||
}
|
||||
// Otherwise assign the individual signers one by one
|
||||
for (uint i = 0; i < signers.length; i++) {
|
||||
authorised[signers[i]] = true;
|
||||
voters.push(signers[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// signers is an accessor method to retrieve all te signers (public accessor
|
||||
// generates an indexed one, not a retreive-all version).
|
||||
function signers() constant returns(address[]) {
|
||||
return voters;
|
||||
}
|
||||
|
||||
// authProposals retrieves the list of addresses that authorization proposals
|
||||
// are currently being voted on.
|
||||
function authProposals() constant returns(address[]) {
|
||||
return authPend;
|
||||
}
|
||||
|
||||
// authVotes retrieves the current authorization votes for a particular user
|
||||
// to promote him into the list of signers, or demote him from there.
|
||||
function authVotes(address user) constant returns(address[] promote, address[] demote) {
|
||||
return (authProps[user].pass, authProps[user].fail);
|
||||
}
|
||||
|
||||
// currentVersion retrieves the semantic version, commit hash and release time
|
||||
// of the currently votec active release.
|
||||
function currentVersion() constant returns (uint32 major, uint32 minor, uint32 patch, bytes20 commit, uint time) {
|
||||
if (releases.length == 0) {
|
||||
return (0, 0, 0, 0, 0);
|
||||
}
|
||||
var release = releases[releases.length - 1];
|
||||
|
||||
return (release.major, release.minor, release.patch, release.commit, release.time);
|
||||
}
|
||||
|
||||
// proposedVersion retrieves the semantic version, commit hash and the current
|
||||
// votes for the next proposed release.
|
||||
function proposedVersion() constant returns (uint32 major, uint32 minor, uint32 patch, bytes20 commit, address[] pass, address[] fail) {
|
||||
return (verProp.major, verProp.minor, verProp.patch, verProp.commit, verProp.votes.pass, verProp.votes.fail);
|
||||
}
|
||||
|
||||
// promote pitches in on a voting campaign to promote a new user to a signer
|
||||
// position.
|
||||
function promote(address user) {
|
||||
updateSigner(user, true);
|
||||
}
|
||||
|
||||
// demote pitches in on a voting campaign to demote an authorised user from
|
||||
// its signer position.
|
||||
function demote(address user) {
|
||||
updateSigner(user, false);
|
||||
}
|
||||
|
||||
// release votes for a particular version to be included as the next release.
|
||||
function release(uint32 major, uint32 minor, uint32 patch, bytes20 commit) {
|
||||
updateRelease(major, minor, patch, commit, true);
|
||||
}
|
||||
|
||||
// nuke votes for the currently proposed version to not be included as the next
|
||||
// release. Nuking doesn't require a specific version number for simplicity.
|
||||
function nuke() {
|
||||
updateRelease(0, 0, 0, 0, false);
|
||||
}
|
||||
|
||||
// updateSigner marks a vote for changing the status of an Ethereum user, either
|
||||
// for or against the user being an authorised signer.
|
||||
function updateSigner(address user, bool authorize) internal isSigner {
|
||||
// Gather the current votes and ensure we don't double vote
|
||||
Votes votes = authProps[user];
|
||||
for (uint i = 0; i < votes.pass.length; i++) {
|
||||
if (votes.pass[i] == msg.sender) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (i = 0; i < votes.fail.length; i++) {
|
||||
if (votes.fail[i] == msg.sender) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If no authorization proposal is open, add the user to the index for later lookups
|
||||
if (votes.pass.length == 0 && votes.fail.length == 0) {
|
||||
authPend.push(user);
|
||||
}
|
||||
// Cast the vote and return if the proposal cannot be resolved yet
|
||||
if (authorize) {
|
||||
votes.pass.push(msg.sender);
|
||||
if (votes.pass.length <= voters.length / 2) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
votes.fail.push(msg.sender);
|
||||
if (votes.fail.length <= voters.length / 2) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Proposal resolved in our favor, execute whatever we voted on
|
||||
if (authorize && !authorised[user]) {
|
||||
authorised[user] = true;
|
||||
voters.push(user);
|
||||
} else if (!authorize && authorised[user]) {
|
||||
authorised[user] = false;
|
||||
|
||||
for (i = 0; i < voters.length; i++) {
|
||||
if (voters[i] == user) {
|
||||
voters[i] = voters[voters.length - 1];
|
||||
voters.length--;
|
||||
|
||||
delete verProp; // Nuke any version proposal (no suprise releases!)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Finally delete the resolved proposal, index and garbage collect
|
||||
delete authProps[user];
|
||||
|
||||
for (i = 0; i < authPend.length; i++) {
|
||||
if (authPend[i] == user) {
|
||||
authPend[i] = authPend[authPend.length - 1];
|
||||
authPend.length--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateRelease votes for a particular version to be included as the next release,
|
||||
// or for the currently proposed release to be nuked out.
|
||||
function updateRelease(uint32 major, uint32 minor, uint32 patch, bytes20 commit, bool release) internal isSigner {
|
||||
// Skip nuke votes if no proposal is pending
|
||||
if (!release && verProp.votes.pass.length == 0) {
|
||||
return;
|
||||
}
|
||||
// Mark a new release if no proposal is pending
|
||||
if (verProp.votes.pass.length == 0) {
|
||||
verProp.major = major;
|
||||
verProp.minor = minor;
|
||||
verProp.patch = patch;
|
||||
verProp.commit = commit;
|
||||
}
|
||||
// Make sure positive votes match the current proposal
|
||||
if (release && (verProp.major != major || verProp.minor != minor || verProp.patch != patch || verProp.commit != commit)) {
|
||||
return;
|
||||
}
|
||||
// Gather the current votes and ensure we don't double vote
|
||||
Votes votes = verProp.votes;
|
||||
for (uint i = 0; i < votes.pass.length; i++) {
|
||||
if (votes.pass[i] == msg.sender) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (i = 0; i < votes.fail.length; i++) {
|
||||
if (votes.fail[i] == msg.sender) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Cast the vote and return if the proposal cannot be resolved yet
|
||||
if (release) {
|
||||
votes.pass.push(msg.sender);
|
||||
if (votes.pass.length <= voters.length / 2) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
votes.fail.push(msg.sender);
|
||||
if (votes.fail.length <= voters.length / 2) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Proposal resolved in our favor, execute whatever we voted on
|
||||
if (release) {
|
||||
verProp.time = uint64(now);
|
||||
releases.push(verProp);
|
||||
delete verProp;
|
||||
} else {
|
||||
delete verProp;
|
||||
}
|
||||
}
|
||||
}
|
374
contracts/release/contract_test.go
Normal file
374
contracts/release/contract_test.go
Normal file
@ -0,0 +1,374 @@
|
||||
// Copyright 2016 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package release
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
)
|
||||
|
||||
// setupReleaseTest creates a blockchain simulator and deploys a version oracle
|
||||
// contract for testing.
|
||||
func setupReleaseTest(t *testing.T, prefund ...*ecdsa.PrivateKey) (*ecdsa.PrivateKey, *ReleaseOracle, *backends.SimulatedBackend) {
|
||||
// Generate a new random account and a funded simulator
|
||||
key, _ := crypto.GenerateKey()
|
||||
auth := bind.NewKeyedTransactor(key)
|
||||
|
||||
accounts := []core.GenesisAccount{{Address: auth.From, Balance: big.NewInt(10000000000)}}
|
||||
for _, key := range prefund {
|
||||
accounts = append(accounts, core.GenesisAccount{Address: crypto.PubkeyToAddress(key.PublicKey), Balance: big.NewInt(10000000000)})
|
||||
}
|
||||
sim := backends.NewSimulatedBackend(accounts...)
|
||||
|
||||
// Deploy a version oracle contract, commit and return
|
||||
_, _, oracle, err := DeployReleaseOracle(auth, sim, []common.Address{auth.From})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to deploy version contract: %v", err)
|
||||
}
|
||||
sim.Commit()
|
||||
|
||||
return key, oracle, sim
|
||||
}
|
||||
|
||||
// Tests that the version contract can be deployed and the creator is assigned
|
||||
// the sole authorized signer.
|
||||
func TestContractCreation(t *testing.T) {
|
||||
key, oracle, _ := setupReleaseTest(t)
|
||||
|
||||
owner := crypto.PubkeyToAddress(key.PublicKey)
|
||||
signers, err := oracle.Signers(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve list of signers: %v", err)
|
||||
}
|
||||
if len(signers) != 1 || signers[0] != owner {
|
||||
t.Fatalf("Initial signer mismatch: have %v, want %v", signers, owner)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that subsequent signers can be promoted, each requiring half plus one
|
||||
// votes for it to pass through.
|
||||
func TestSignerPromotion(t *testing.T) {
|
||||
// Prefund a few accounts to authorize with and create the oracle
|
||||
keys := make([]*ecdsa.PrivateKey, 5)
|
||||
for i := 0; i < len(keys); i++ {
|
||||
keys[i], _ = crypto.GenerateKey()
|
||||
}
|
||||
key, oracle, sim := setupReleaseTest(t, keys...)
|
||||
|
||||
// Gradually promote the keys, until all are authorized
|
||||
keys = append([]*ecdsa.PrivateKey{key}, keys...)
|
||||
for i := 1; i < len(keys); i++ {
|
||||
// Check that no votes are accepted from the not yet authed user
|
||||
if _, err := oracle.Promote(bind.NewKeyedTransactor(keys[i]), common.Address{}); err != nil {
|
||||
t.Fatalf("Iter #%d: failed invalid promotion attempt: %v", i, err)
|
||||
}
|
||||
sim.Commit()
|
||||
|
||||
pend, err := oracle.AuthProposals(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Iter #%d: failed to retrieve active proposals: %v", i, err)
|
||||
}
|
||||
if len(pend) != 0 {
|
||||
t.Fatalf("Iter #%d: proposal count mismatch: have %d, want 0", i, len(pend))
|
||||
}
|
||||
// Promote with half - 1 voters and check that the user's not yet authorized
|
||||
for j := 0; j < i/2; j++ {
|
||||
if _, err = oracle.Promote(bind.NewKeyedTransactor(keys[j]), crypto.PubkeyToAddress(keys[i].PublicKey)); err != nil {
|
||||
t.Fatalf("Iter #%d: failed valid promotion attempt: %v", i, err)
|
||||
}
|
||||
}
|
||||
sim.Commit()
|
||||
|
||||
signers, err := oracle.Signers(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Iter #%d: failed to retrieve list of signers: %v", i, err)
|
||||
}
|
||||
if len(signers) != i {
|
||||
t.Fatalf("Iter #%d: signer count mismatch: have %v, want %v", i, len(signers), i)
|
||||
}
|
||||
// Promote with the last one needed to pass the promotion
|
||||
if _, err = oracle.Promote(bind.NewKeyedTransactor(keys[i/2]), crypto.PubkeyToAddress(keys[i].PublicKey)); err != nil {
|
||||
t.Fatalf("Iter #%d: failed valid promotion completion attempt: %v", i, err)
|
||||
}
|
||||
sim.Commit()
|
||||
|
||||
signers, err = oracle.Signers(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Iter #%d: failed to retrieve list of signers: %v", i, err)
|
||||
}
|
||||
if len(signers) != i+1 {
|
||||
t.Fatalf("Iter #%d: signer count mismatch: have %v, want %v", i, len(signers), i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that subsequent signers can be demoted, each requiring half plus one
|
||||
// votes for it to pass through.
|
||||
func TestSignerDemotion(t *testing.T) {
|
||||
// Prefund a few accounts to authorize with and create the oracle
|
||||
keys := make([]*ecdsa.PrivateKey, 5)
|
||||
for i := 0; i < len(keys); i++ {
|
||||
keys[i], _ = crypto.GenerateKey()
|
||||
}
|
||||
key, oracle, sim := setupReleaseTest(t, keys...)
|
||||
|
||||
// Authorize all the keys as valid signers and verify cardinality
|
||||
keys = append([]*ecdsa.PrivateKey{key}, keys...)
|
||||
for i := 1; i < len(keys); i++ {
|
||||
for j := 0; j <= i/2; j++ {
|
||||
if _, err := oracle.Promote(bind.NewKeyedTransactor(keys[j]), crypto.PubkeyToAddress(keys[i].PublicKey)); err != nil {
|
||||
t.Fatalf("Iter #%d: failed valid promotion attempt: %v", i, err)
|
||||
}
|
||||
}
|
||||
sim.Commit()
|
||||
}
|
||||
signers, err := oracle.Signers(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve list of signers: %v", err)
|
||||
}
|
||||
if len(signers) != len(keys) {
|
||||
t.Fatalf("Signer count mismatch: have %v, want %v", len(signers), len(keys))
|
||||
}
|
||||
// Gradually demote users until we run out of signers
|
||||
for i := len(keys) - 1; i >= 0; i-- {
|
||||
// Demote with half - 1 voters and check that the user's not yet dropped
|
||||
for j := 0; j < (i+1)/2; j++ {
|
||||
if _, err = oracle.Demote(bind.NewKeyedTransactor(keys[j]), crypto.PubkeyToAddress(keys[i].PublicKey)); err != nil {
|
||||
t.Fatalf("Iter #%d: failed valid demotion attempt: %v", len(keys)-i, err)
|
||||
}
|
||||
}
|
||||
sim.Commit()
|
||||
|
||||
signers, err := oracle.Signers(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Iter #%d: failed to retrieve list of signers: %v", len(keys)-i, err)
|
||||
}
|
||||
if len(signers) != i+1 {
|
||||
t.Fatalf("Iter #%d: signer count mismatch: have %v, want %v", len(keys)-i, len(signers), i+1)
|
||||
}
|
||||
// Demote with the last one needed to pass the demotion
|
||||
if _, err = oracle.Demote(bind.NewKeyedTransactor(keys[(i+1)/2]), crypto.PubkeyToAddress(keys[i].PublicKey)); err != nil {
|
||||
t.Fatalf("Iter #%d: failed valid demotion completion attempt: %v", i, err)
|
||||
}
|
||||
sim.Commit()
|
||||
|
||||
signers, err = oracle.Signers(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Iter #%d: failed to retrieve list of signers: %v", len(keys)-i, err)
|
||||
}
|
||||
if len(signers) != i {
|
||||
t.Fatalf("Iter #%d: signer count mismatch: have %v, want %v", len(keys)-i, len(signers), i)
|
||||
}
|
||||
// Check that no votes are accepted from the already demoted users
|
||||
if _, err = oracle.Promote(bind.NewKeyedTransactor(keys[i]), common.Address{}); err != nil {
|
||||
t.Fatalf("Iter #%d: failed invalid promotion attempt: %v", i, err)
|
||||
}
|
||||
sim.Commit()
|
||||
|
||||
pend, err := oracle.AuthProposals(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Iter #%d: failed to retrieve active proposals: %v", i, err)
|
||||
}
|
||||
if len(pend) != 0 {
|
||||
t.Fatalf("Iter #%d: proposal count mismatch: have %d, want 0", i, len(pend))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that new versions can be released, honouring both voting rights as well
|
||||
// as the minimum required vote count.
|
||||
func TestVersionRelease(t *testing.T) {
|
||||
// Prefund a few accounts to authorize with and create the oracle
|
||||
keys := make([]*ecdsa.PrivateKey, 5)
|
||||
for i := 0; i < len(keys); i++ {
|
||||
keys[i], _ = crypto.GenerateKey()
|
||||
}
|
||||
key, oracle, sim := setupReleaseTest(t, keys...)
|
||||
|
||||
// Track the "current release"
|
||||
var (
|
||||
verMajor = uint32(0)
|
||||
verMinor = uint32(0)
|
||||
verPatch = uint32(0)
|
||||
verCommit = [20]byte{}
|
||||
)
|
||||
// Gradually push releases, always requiring more signers than previously
|
||||
keys = append([]*ecdsa.PrivateKey{key}, keys...)
|
||||
for i := 1; i < len(keys); i++ {
|
||||
// Check that no votes are accepted from the not yet authed user
|
||||
if _, err := oracle.Release(bind.NewKeyedTransactor(keys[i]), 0, 0, 0, [20]byte{0}); err != nil {
|
||||
t.Fatalf("Iter #%d: failed invalid release attempt: %v", i, err)
|
||||
}
|
||||
sim.Commit()
|
||||
|
||||
prop, err := oracle.ProposedVersion(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Iter #%d: failed to retrieve active proposal: %v", i, err)
|
||||
}
|
||||
if len(prop.Pass) != 0 {
|
||||
t.Fatalf("Iter #%d: proposal vote count mismatch: have %d, want 0", i, len(prop.Pass))
|
||||
}
|
||||
// Authorize the user to make releases
|
||||
for j := 0; j <= i/2; j++ {
|
||||
if _, err = oracle.Promote(bind.NewKeyedTransactor(keys[j]), crypto.PubkeyToAddress(keys[i].PublicKey)); err != nil {
|
||||
t.Fatalf("Iter #%d: failed valid promotion attempt: %v", i, err)
|
||||
}
|
||||
}
|
||||
sim.Commit()
|
||||
|
||||
// Propose release with half voters and check that the release does not yet go through
|
||||
for j := 0; j < (i+1)/2; j++ {
|
||||
if _, err = oracle.Release(bind.NewKeyedTransactor(keys[j]), uint32(i), uint32(i+1), uint32(i+2), [20]byte{byte(i + 3)}); err != nil {
|
||||
t.Fatalf("Iter #%d: failed valid release attempt: %v", i, err)
|
||||
}
|
||||
}
|
||||
sim.Commit()
|
||||
|
||||
ver, err := oracle.CurrentVersion(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Iter #%d: failed to retrieve current version: %v", i, err)
|
||||
}
|
||||
if ver.Major != verMajor || ver.Minor != verMinor || ver.Patch != verPatch || ver.Commit != verCommit {
|
||||
t.Fatalf("Iter #%d: version mismatch: have %d.%d.%d-%x, want %d.%d.%d-%x", i, ver.Major, ver.Minor, ver.Patch, ver.Commit, verMajor, verMinor, verPatch, verCommit)
|
||||
}
|
||||
|
||||
// Pass the release and check that it became the next version
|
||||
verMajor, verMinor, verPatch, verCommit = uint32(i), uint32(i+1), uint32(i+2), [20]byte{byte(i + 3)}
|
||||
if _, err = oracle.Release(bind.NewKeyedTransactor(keys[(i+1)/2]), uint32(i), uint32(i+1), uint32(i+2), [20]byte{byte(i + 3)}); err != nil {
|
||||
t.Fatalf("Iter #%d: failed valid release completion attempt: %v", i, err)
|
||||
}
|
||||
sim.Commit()
|
||||
|
||||
ver, err = oracle.CurrentVersion(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Iter #%d: failed to retrieve current version: %v", i, err)
|
||||
}
|
||||
if ver.Major != verMajor || ver.Minor != verMinor || ver.Patch != verPatch || ver.Commit != verCommit {
|
||||
t.Fatalf("Iter #%d: version mismatch: have %d.%d.%d-%x, want %d.%d.%d-%x", i, ver.Major, ver.Minor, ver.Patch, ver.Commit, verMajor, verMinor, verPatch, verCommit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that proposed versions can be nuked out of existence.
|
||||
func TestVersionNuking(t *testing.T) {
|
||||
// Prefund a few accounts to authorize with and create the oracle
|
||||
keys := make([]*ecdsa.PrivateKey, 9)
|
||||
for i := 0; i < len(keys); i++ {
|
||||
keys[i], _ = crypto.GenerateKey()
|
||||
}
|
||||
key, oracle, sim := setupReleaseTest(t, keys...)
|
||||
|
||||
// Authorize all the keys as valid signers
|
||||
keys = append([]*ecdsa.PrivateKey{key}, keys...)
|
||||
for i := 1; i < len(keys); i++ {
|
||||
for j := 0; j <= i/2; j++ {
|
||||
if _, err := oracle.Promote(bind.NewKeyedTransactor(keys[j]), crypto.PubkeyToAddress(keys[i].PublicKey)); err != nil {
|
||||
t.Fatalf("Iter #%d: failed valid promotion attempt: %v", i, err)
|
||||
}
|
||||
}
|
||||
sim.Commit()
|
||||
}
|
||||
// Propose releases with more and more keys, always retaining enough users to nuke the proposals
|
||||
for i := 1; i < (len(keys)+1)/2; i++ {
|
||||
// Propose release with an initial set of signers
|
||||
for j := 0; j < i; j++ {
|
||||
if _, err := oracle.Release(bind.NewKeyedTransactor(keys[j]), uint32(i), uint32(i+1), uint32(i+2), [20]byte{byte(i + 3)}); err != nil {
|
||||
t.Fatalf("Iter #%d: failed valid proposal attempt: %v", i, err)
|
||||
}
|
||||
}
|
||||
sim.Commit()
|
||||
|
||||
prop, err := oracle.ProposedVersion(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Iter #%d: failed to retrieve active proposal: %v", i, err)
|
||||
}
|
||||
if len(prop.Pass) != i {
|
||||
t.Fatalf("Iter #%d: proposal vote count mismatch: have %d, want %d", i, len(prop.Pass), i)
|
||||
}
|
||||
// Nuke the release with half+1 voters
|
||||
for j := i; j <= i+(len(keys)+1)/2; j++ {
|
||||
if _, err := oracle.Nuke(bind.NewKeyedTransactor(keys[j])); err != nil {
|
||||
t.Fatalf("Iter #%d: failed valid nuke attempt: %v", i, err)
|
||||
}
|
||||
}
|
||||
sim.Commit()
|
||||
|
||||
prop, err = oracle.ProposedVersion(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Iter #%d: failed to retrieve active proposal: %v", i, err)
|
||||
}
|
||||
if len(prop.Pass) != 0 || len(prop.Fail) != 0 {
|
||||
t.Fatalf("Iter #%d: proposal vote count mismatch: have %d/%d pass/fail, want 0/0", i, len(prop.Pass), len(prop.Fail))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that demoting a signer will auto-nuke the currently pending release.
|
||||
func TestVersionAutoNuke(t *testing.T) {
|
||||
// Prefund a few accounts to authorize with and create the oracle
|
||||
keys := make([]*ecdsa.PrivateKey, 5)
|
||||
for i := 0; i < len(keys); i++ {
|
||||
keys[i], _ = crypto.GenerateKey()
|
||||
}
|
||||
key, oracle, sim := setupReleaseTest(t, keys...)
|
||||
|
||||
// Authorize all the keys as valid signers
|
||||
keys = append([]*ecdsa.PrivateKey{key}, keys...)
|
||||
for i := 1; i < len(keys); i++ {
|
||||
for j := 0; j <= i/2; j++ {
|
||||
if _, err := oracle.Promote(bind.NewKeyedTransactor(keys[j]), crypto.PubkeyToAddress(keys[i].PublicKey)); err != nil {
|
||||
t.Fatalf("Iter #%d: failed valid promotion attempt: %v", i, err)
|
||||
}
|
||||
}
|
||||
sim.Commit()
|
||||
}
|
||||
// Make a release proposal and check it's existence
|
||||
if _, err := oracle.Release(bind.NewKeyedTransactor(keys[0]), 1, 2, 3, [20]byte{4}); err != nil {
|
||||
t.Fatalf("Failed valid proposal attempt: %v", err)
|
||||
}
|
||||
sim.Commit()
|
||||
|
||||
prop, err := oracle.ProposedVersion(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve active proposal: %v", err)
|
||||
}
|
||||
if len(prop.Pass) != 1 {
|
||||
t.Fatalf("Proposal vote count mismatch: have %d, want 1", len(prop.Pass))
|
||||
}
|
||||
// Demote a signer and check release proposal deletion
|
||||
for i := 0; i <= len(keys)/2; i++ {
|
||||
if _, err := oracle.Demote(bind.NewKeyedTransactor(keys[i]), crypto.PubkeyToAddress(keys[len(keys)-1].PublicKey)); err != nil {
|
||||
t.Fatalf("Iter #%d: failed valid demotion attempt: %v", i, err)
|
||||
}
|
||||
}
|
||||
sim.Commit()
|
||||
|
||||
prop, err = oracle.ProposedVersion(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve active proposal: %v", err)
|
||||
}
|
||||
if len(prop.Pass) != 0 {
|
||||
t.Fatalf("Proposal vote count mismatch: have %d, want 0", len(prop.Pass))
|
||||
}
|
||||
}
|
152
contracts/release/release.go
Normal file
152
contracts/release/release.go
Normal file
@ -0,0 +1,152 @@
|
||||
// Copyright 2016 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Package release contains the node service that tracks client releases.
|
||||
package release
|
||||
|
||||
//go:generate abigen --sol ./contract.sol --pkg release --out ./contract.go
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/logger"
|
||||
"github.com/ethereum/go-ethereum/logger/glog"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// Interval to check for new releases
|
||||
const releaseRecheckInterval = time.Hour
|
||||
|
||||
// Config contains the configurations of the release service.
|
||||
type Config struct {
|
||||
Oracle common.Address // Ethereum address of the release oracle
|
||||
Major uint32 // Major version component of the release
|
||||
Minor uint32 // Minor version component of the release
|
||||
Patch uint32 // Patch version component of the release
|
||||
Commit [20]byte // Git SHA1 commit hash of the release
|
||||
}
|
||||
|
||||
// ReleaseService is a node service that periodically checks the blockchain for
|
||||
// newly released versions of the client being run and issues a warning to the
|
||||
// user about it.
|
||||
type ReleaseService struct {
|
||||
config Config // Current version to check releases against
|
||||
oracle *ReleaseOracle // Native binding to the release oracle contract
|
||||
quit chan chan error // Quit channel to terminate the version checker
|
||||
}
|
||||
|
||||
// NewReleaseService creates a new service to periodically check for new client
|
||||
// releases and notify the user of such.
|
||||
func NewReleaseService(ctx *node.ServiceContext, config Config) (node.Service, error) {
|
||||
// Retrieve the Ethereum service dependency to access the blockchain
|
||||
var ethereum *eth.Ethereum
|
||||
if err := ctx.Service(ðereum); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Construct the release service
|
||||
contract, err := NewReleaseOracle(config.Oracle, eth.NewContractBackend(ethereum))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ReleaseService{
|
||||
config: config,
|
||||
oracle: contract,
|
||||
quit: make(chan chan error),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Protocols returns an empty list of P2P protocols as the release service does
|
||||
// not have a networking component.
|
||||
func (r *ReleaseService) Protocols() []p2p.Protocol { return nil }
|
||||
|
||||
// APIs returns an empty list of RPC descriptors as the release service does not
|
||||
// expose any functioanlity to the outside world.
|
||||
func (r *ReleaseService) APIs() []rpc.API { return nil }
|
||||
|
||||
// Start spawns the periodic version checker goroutine
|
||||
func (r *ReleaseService) Start(server *p2p.Server) error {
|
||||
go r.checker()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop terminates all goroutines belonging to the service, blocking until they
|
||||
// are all terminated.
|
||||
func (r *ReleaseService) Stop() error {
|
||||
errc := make(chan error)
|
||||
r.quit <- errc
|
||||
return <-errc
|
||||
}
|
||||
|
||||
// checker runs indefinitely in the background, periodically checking for new
|
||||
// client releases.
|
||||
func (r *ReleaseService) checker() {
|
||||
// Set up the timers to periodically check for releases
|
||||
timer := time.NewTimer(0) // Immediately fire a version check
|
||||
defer timer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
// If the time arrived, check for a new release
|
||||
case <-timer.C:
|
||||
// Rechedule the timer before continuing
|
||||
timer.Reset(releaseRecheckInterval)
|
||||
|
||||
// Retrieve the current version, and handle missing contracts gracefully
|
||||
ctx, _ := context.WithTimeout(context.Background(), time.Second*5)
|
||||
opts := &bind.CallOpts{Context: ctx}
|
||||
version, err := r.oracle.CurrentVersion(opts)
|
||||
if err != nil {
|
||||
if err == bind.ErrNoCode {
|
||||
glog.V(logger.Debug).Infof("Release oracle not found at %x", r.config.Oracle)
|
||||
continue
|
||||
}
|
||||
glog.V(logger.Error).Infof("Failed to retrieve current release: %v", err)
|
||||
continue
|
||||
}
|
||||
// Version was successfully retrieved, notify if newer than ours
|
||||
if version.Major > r.config.Major ||
|
||||
(version.Major == r.config.Major && version.Minor > r.config.Minor) ||
|
||||
(version.Major == r.config.Major && version.Minor == r.config.Minor && version.Patch > r.config.Patch) {
|
||||
|
||||
warning := fmt.Sprintf("Client v%d.%d.%d-%x seems older than the latest upstream release v%d.%d.%d-%x",
|
||||
r.config.Major, r.config.Minor, r.config.Patch, r.config.Commit[:4], version.Major, version.Minor, version.Patch, version.Commit[:4])
|
||||
howtofix := fmt.Sprintf("Please check https://github.com/ethereum/go-ethereum/releases for new releases")
|
||||
separator := strings.Repeat("-", len(warning))
|
||||
|
||||
glog.V(logger.Warn).Info(separator)
|
||||
glog.V(logger.Warn).Info(warning)
|
||||
glog.V(logger.Warn).Info(howtofix)
|
||||
glog.V(logger.Warn).Info(separator)
|
||||
} else {
|
||||
glog.V(logger.Debug).Infof("Client v%d.%d.%d-%x seems up to date with upstream v%d.%d.%d-%x",
|
||||
r.config.Major, r.config.Minor, r.config.Patch, r.config.Commit[:4], version.Major, version.Minor, version.Patch, version.Commit[:4])
|
||||
}
|
||||
|
||||
// If termination was requested, return
|
||||
case errc := <-r.quit:
|
||||
errc <- nil
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user